Skip to content

Commit 6310edd

Browse files
committed
Refactor helper functions (derivative, secant) to become graph-types
1 parent 63ec204 commit 6310edd

File tree

10 files changed

+224
-205
lines changed

10 files changed

+224
-205
lines changed

src/chart.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { Mark } from './graph-types/mark.js'
1414
import { annotation, interval, polyline, scatter, text } from './graph-types/index.js'
1515

1616
import mousetip from './tip.js'
17-
import { helpers } from './helpers/index.js'
1817
import datumDefaults from './datum-defaults.js'
1918
import datumValidation from './datum-validation.js'
2019
import globals from './globals.mjs'
@@ -553,7 +552,7 @@ export class Chart extends EventEmitter.EventEmitter {
553552
// enter
554553
const annotationsEnter = annotations.enter().append('g').attr('class', 'annotations')
555554
// enter + update
556-
annotations.merge(annotationsEnter).each(function (d: Mark | FunctionPlotDatum, index: number) {
555+
annotations.merge(annotationsEnter).each(function (d: Mark | FunctionPlotDatum) {
557556
const selection = d3Select(this)
558557
const ann = annotation(d)
559558
ann.chart = self
@@ -617,8 +616,6 @@ export class Chart extends EventEmitter.EventEmitter {
617616

618617
mark.chart = self
619618
mark.render(selection)
620-
621-
selection.call(helpers(self))
622619
})
623620
this.generation += 1
624621
}

src/graph-types/derivative.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { select as d3Select } from 'd3-selection'
2+
import type { Selection } from 'd3-selection'
3+
4+
import { Polyline } from '../graph-types/index.js'
5+
import { builtIn as builtInEvaluator } from '../samplers/eval.mjs'
6+
import datumDefaults from '../datum-defaults.js'
7+
import { infinity } from '../utils.mjs'
8+
import { Mark } from '../graph-types/mark.js'
9+
10+
import type { FunctionPlotDatum, LinearFunction } from '../types.js'
11+
12+
export class Derivative extends Mark {
13+
private derivativeDatum: LinearFunction
14+
15+
constructor(options: any) {
16+
super(options)
17+
this.derivativeDatum = datumDefaults({
18+
isHelper: true,
19+
skipTip: true,
20+
skipBoundsCheck: true,
21+
nSamples: 2,
22+
graphType: 'polyline'
23+
}) as LinearFunction
24+
}
25+
26+
private computeLine(d: FunctionPlotDatum) {
27+
if (!d.derivative) {
28+
return []
29+
}
30+
const x0 = typeof d.derivative.x0 === 'number' ? d.derivative.x0 : infinity()
31+
this.derivativeDatum.index = d.index
32+
this.derivativeDatum.scope = {
33+
m: builtInEvaluator(d.derivative, 'fn', { x: x0 }),
34+
x0,
35+
y0: builtInEvaluator(d, 'fn', { x: x0 })
36+
}
37+
this.derivativeDatum.fn = 'm * (x - x0) + y0'
38+
return [this.derivativeDatum]
39+
}
40+
41+
private checkAutoUpdate(d: FunctionPlotDatum, selection: Selection<any, FunctionPlotDatum, any, any>) {
42+
if (!d.derivative) {
43+
return
44+
}
45+
if (d.derivative.updateOnMouseMove && !d.derivative.$$mouseListener) {
46+
d.derivative.$$mouseListener = ({ x }: any) => {
47+
if (d.derivative) {
48+
d.derivative.x0 = x
49+
}
50+
this.render(selection)
51+
}
52+
this.chart.on('tip:update', d.derivative.$$mouseListener)
53+
}
54+
}
55+
56+
render(selection: Selection<any, FunctionPlotDatum, any, any>) {
57+
selection.each((d, i, nodes) => {
58+
const el = d3Select(nodes[i])
59+
const data = this.computeLine(d)
60+
this.checkAutoUpdate(d, selection)
61+
const innerSelection = el.selectAll('g.derivative').data(data)
62+
63+
const innerSelectionEnter = innerSelection.enter().append('g').attr('class', 'derivative')
64+
65+
innerSelection.merge(innerSelectionEnter).each((innerData: any, i, innerNodes) => {
66+
const polyline = new Polyline(innerData)
67+
polyline.chart = this.chart
68+
polyline.render(d3Select(innerNodes[i]))
69+
})
70+
71+
innerSelection.merge(innerSelectionEnter).selectAll('path').attr('opacity', 0.5)
72+
73+
innerSelection.exit().remove()
74+
})
75+
}
76+
}
77+
78+
export function derivative(options: any) {
79+
return new Derivative(options)
80+
}

src/graph-types/helpers.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { select as d3Select } from 'd3-selection'
2+
import type { Selection } from 'd3-selection'
3+
4+
import { derivative, type Derivative } from './derivative.js'
5+
import { secant, type Secant } from './secant.js'
6+
import { Chart } from '../chart.js'
7+
8+
export function helpers(chart: Chart) {
9+
function helper(selection: Selection<any, any, any, any>) {
10+
selection.each(function (d: any) {
11+
const el = d3Select(this)
12+
13+
let mark: Derivative | Secant
14+
mark = derivative(d)
15+
mark.chart = chart
16+
mark.render(el)
17+
18+
mark = secant(d)
19+
mark.chart = chart
20+
mark.render(el)
21+
})
22+
}
23+
24+
return helper
25+
}

src/graph-types/interval.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Selection } from 'd3-selection'
22

33
import { asyncIntervalEvaluate, intervalEvaluate } from '../evaluate-datum.js'
44
import { infinity, color } from '../utils.mjs'
5+
import { helpers } from './helpers.js'
56

67
import { Mark } from './mark.js'
78
import type { Interval as TInterval, FunctionPlotDatum, FunctionPlotScale } from '../types.js'
@@ -133,6 +134,8 @@ export class Interval extends Mark {
133134
}
134135

135136
innerSelection.exit().remove()
137+
138+
selection.call(helpers(this.chart))
136139
}
137140
}
138141

src/graph-types/polyline.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { line as d3Line, area as d3Area, curveLinear as d3CurveLinear } from 'd3
44

55
import { color, infinity, clamp } from '../utils.mjs'
66
import { builtInEvaluate } from '../evaluate-datum.js'
7+
import { helpers } from './helpers.js'
78

89
import { Mark } from './mark.js'
910
import type { FunctionPlotDatum } from '../types.js'
@@ -158,6 +159,8 @@ export class Polyline extends Mark {
158159

159160
// exit
160161
innerSelection.exit().remove()
162+
163+
selection.call(helpers(this.chart))
161164
}
162165
}
163166

src/graph-types/scatter.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { hsl as d3Hsl } from 'd3-color'
33

44
import { color } from '../utils.mjs'
55
import { builtInEvaluate } from '../evaluate-datum.js'
6-
6+
import { helpers } from './helpers.js'
77
import { Mark } from './mark.js'
88
import type { FunctionPlotDatum } from '../types.js'
99

@@ -66,6 +66,8 @@ export class Scatter extends Mark {
6666
}
6767

6868
innerSelection.exit().remove()
69+
70+
selection.call(helpers(this.chart))
6971
}
7072
}
7173

src/graph-types/secant.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { select as d3Select } from 'd3-selection'
2+
import type { Selection } from 'd3-selection'
3+
4+
import { builtIn as builtInEvaluator } from '../samplers/eval.mjs'
5+
import datumDefaults from '../datum-defaults.js'
6+
import { Polyline } from '../graph-types/index.js'
7+
import { infinity } from '../utils.mjs'
8+
import { Mark } from '../graph-types/mark.js'
9+
10+
import type { FunctionPlotDatum, FunctionPlotDatumScope, LinearFunction, SecantDatum } from '../types.js'
11+
12+
export class Secant extends Mark {
13+
private secantDefaults: LinearFunction
14+
15+
constructor(options: any) {
16+
super(options)
17+
this.secantDefaults = datumDefaults({
18+
isHelper: true,
19+
skipTip: true,
20+
skipBoundsCheck: true,
21+
nSamples: 2,
22+
graphType: 'polyline'
23+
}) as LinearFunction
24+
}
25+
26+
private computeSlope(scope: FunctionPlotDatumScope) {
27+
scope.m = (scope.y1 - scope.y0) / (scope.x1 - scope.x0)
28+
}
29+
30+
private updateLine(d: FunctionPlotDatum, secant: SecantDatum) {
31+
if (!('x0' in secant)) {
32+
throw Error('secant must have the property `x0` defined')
33+
}
34+
secant.scope = secant.scope || {}
35+
36+
const x0 = secant.x0
37+
const x1 = typeof secant.x1 === 'number' ? secant.x1 : infinity()
38+
Object.assign(secant.scope, {
39+
x0,
40+
x1,
41+
y0: builtInEvaluator(d, 'fn', { x: x0 }),
42+
y1: builtInEvaluator(d, 'fn', { x: x1 })
43+
})
44+
this.computeSlope(secant.scope)
45+
}
46+
47+
private setFn(d: FunctionPlotDatum, secant: SecantDatum) {
48+
this.updateLine(d, secant)
49+
secant.fn = 'm * (x - x0) + y0'
50+
}
51+
52+
private setMouseListener(
53+
d: FunctionPlotDatum,
54+
secantObject: SecantDatum,
55+
selection: Selection<any, FunctionPlotDatum, any, any>
56+
) {
57+
if (secantObject.updateOnMouseMove && !secantObject.$$mouseListener) {
58+
secantObject.$$mouseListener = ({ x }: any) => {
59+
secantObject.x1 = x
60+
this.updateLine(d, secantObject)
61+
this.render(selection)
62+
}
63+
this.chart.on('tip:update', secantObject.$$mouseListener)
64+
}
65+
}
66+
67+
private computeLines(d: FunctionPlotDatum, selection: Selection<any, FunctionPlotDatum, any, any>) {
68+
const data = []
69+
d.secants = d.secants || []
70+
for (let i = 0; i < d.secants.length; i += 1) {
71+
const secant = (d.secants[i] = Object.assign({}, this.secantDefaults, d.secants[i]))
72+
// necessary to make the secant have the same color as d
73+
secant.index = d.index
74+
if (!secant.fn) {
75+
this.setFn(d, secant)
76+
this.setMouseListener(d, secant, selection)
77+
}
78+
data.push(secant)
79+
}
80+
return data
81+
}
82+
83+
render(selection: Selection<any, FunctionPlotDatum, any, any>) {
84+
selection.each((d, i, nodes) => {
85+
const el = d3Select(nodes[i])
86+
const data = this.computeLines(d, selection)
87+
const innerSelection = el.selectAll('g.secant').data(data)
88+
89+
const innerSelectionEnter = innerSelection.enter().append('g').attr('class', 'secant')
90+
91+
// enter + update
92+
innerSelection.merge(innerSelectionEnter).each((d: any, i, nodes) => {
93+
const polyline = new Polyline(d)
94+
polyline.chart = this.chart
95+
polyline.render(d3Select(nodes[i]))
96+
})
97+
98+
// change the opacity of the secants
99+
innerSelection.merge(innerSelectionEnter).selectAll('path').attr('opacity', 0.5)
100+
101+
// exit
102+
innerSelection.exit().remove()
103+
})
104+
}
105+
}
106+
107+
export function secant(options: any) {
108+
return new Secant(options)
109+
}

src/helpers/derivative.ts

Lines changed: 0 additions & 82 deletions
This file was deleted.

src/helpers/index.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

0 commit comments

Comments
 (0)