Skip to content

Commit edb1ea7

Browse files
committed
Fix issue in computed data split, remove clamp dependency
The data wasn't split into groups as expected, sometimes there would be more groups than what's expected.
1 parent b0f1d83 commit edb1ea7

File tree

11 files changed

+263
-91
lines changed

11 files changed

+263
-91
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
},
4545
"dependencies": {
4646
"built-in-math-eval": "^0.3.0",
47-
"clamp": "^1.0.1",
4847
"d3-axis": "^3.0.0",
4948
"d3-color": "^3.1.0",
5049
"d3-format": "^3.1.0",

src/evaluate.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,14 @@ import interval from './samplers/interval'
33
import builtIn from './samplers/builtIn'
44

55
import { Chart } from './index'
6-
import { FunctionPlotDatum } from './types'
6+
import { FunctionPlotDatum, FunctionPlotScale } from './types'
77

88
type SamplerTypeFn = typeof interval | typeof builtIn
99

1010
/**
11-
* Computes the endpoints x_lo, x_hi of the range
12-
* from which the sampler will take samples
13-
*
14-
* @param {Object} scale
15-
* @param {Object} d An item from `data`
16-
* @returns {Array}
11+
* Computes the endpoints x_lo, x_hi of the range in d.range from which the sampler will take samples.
1712
*/
18-
function computeEndpoints(scale: any, d: any): [number, number] {
13+
function computeEndpoints(scale: FunctionPlotScale, d: FunctionPlotDatum): [number, number] {
1914
const range = d.range || [-Infinity, Infinity]
2015
const start = Math.max(scale.domain()[0], range[0])
2116
const end = Math.min(scale.domain()[1], range[1])

src/external.d.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
declare module 'clamp' {
2-
export default function clamp(lo: number, hi: number, n: number): number
3-
}
4-
51
declare module 'built-in-math-eval' {
62
export default function eval(expr: string): any
73
}

src/graph-types/polyline.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { select as d3Select, Selection } from 'd3-selection'
22
import { line as d3Line, area as d3Area, curveLinear as d3CurveLinear } from 'd3-shape'
3-
import clamp from 'clamp'
43

54
import utils from '../utils'
65
import evaluate from '../evaluate'
@@ -27,12 +26,12 @@ export default function polyline(chart: Chart) {
2726
yMax += diff * 1e6
2827
yMin -= diff * 1e6
2928
if (d.skipBoundsCheck) {
30-
yMax = Infinity
31-
yMin = -Infinity
29+
yMax = utils.infinity()
30+
yMin = -utils.infinity()
3231
}
3332

3433
function y(d: number[]) {
35-
return clamp(chart.meta.yScale(d[1]), yMin, yMax)
34+
return utils.clamp(chart.meta.yScale(d[1]), yMin, yMax)
3635
}
3736

3837
const line = d3Line()

src/graph-types/scatter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import evaluate from '../evaluate'
77
import { Chart } from '../index'
88
import { FunctionPlotDatum } from '../types'
99

10-
export default function scatter(chart: Chart) {
10+
export default function Scatter(chart: Chart) {
1111
const xScale = chart.meta.xScale
1212
const yScale = chart.meta.yScale
1313

@@ -27,7 +27,7 @@ export default function scatter(chart: Chart) {
2727
}
2828
}
2929

30-
const innerSelection = d3Select(this).selectAll(':scope > circle').data(joined)
30+
const innerSelection = d3Select(this).selectAll(':scope > circle.scatter').data(joined)
3131

3232
const cls = `scatter scatter-${index}`
3333
const innerSelectionEnter = innerSelection.enter().append('circle').attr('class', cls)

src/helpers/derivative.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { select as d3Select, Selection } from 'd3-selection'
33
import { polyline } from '../graph-types/'
44
import { builtIn as builtInEvaluator } from './eval'
55
import datumDefaults from '../datum-defaults'
6+
import utils from '../utils'
67

7-
import { Chart } from "../index";
8+
import { Chart } from '../index'
89
import { FunctionPlotDatum } from '../types'
910

1011
export default function derivative(chart: Chart) {
@@ -16,22 +17,22 @@ export default function derivative(chart: Chart) {
1617
graphType: 'polyline'
1718
})
1819

19-
function computeLine (d: FunctionPlotDatum) {
20+
function computeLine(d: FunctionPlotDatum) {
2021
if (!d.derivative) {
2122
return []
2223
}
23-
const x0 = typeof d.derivative.x0 === 'number' ? d.derivative.x0 : Infinity
24+
const x0 = typeof d.derivative.x0 === 'number' ? d.derivative.x0 : utils.infinity()
2425
derivativeDatum.index = d.index
2526
derivativeDatum.scope = {
2627
m: builtInEvaluator(d.derivative, 'fn', { x: x0 }),
27-
x0: x0,
28+
x0,
2829
y0: builtInEvaluator(d, 'fn', { x: x0 })
2930
}
3031
derivativeDatum.fn = 'm * (x - x0) + y0'
3132
return [derivativeDatum]
3233
}
3334

34-
function checkAutoUpdate (d: FunctionPlotDatum) {
35+
function checkAutoUpdate(d: FunctionPlotDatum) {
3536
const self = this
3637
if (!d.derivative) {
3738
return
@@ -57,22 +58,16 @@ export default function derivative(chart: Chart) {
5758
const el = d3Select(this)
5859
const data = computeLine.call(selection, d)
5960
checkAutoUpdate.call(selection, d)
60-
const innerSelection = el.selectAll('g.derivative')
61-
.data(data)
61+
const innerSelection = el.selectAll('g.derivative').data(data)
6262

63-
const innerSelectionEnter = innerSelection.enter()
64-
.append('g')
65-
.attr('class', 'derivative')
63+
const innerSelectionEnter = innerSelection.enter().append('g').attr('class', 'derivative')
6664

6765
// enter + update
68-
innerSelection.merge(innerSelectionEnter)
69-
.call(polyline(chart))
66+
innerSelection.merge(innerSelectionEnter).call(polyline(chart))
7067

7168
// update
7269
// change the opacity of the line
73-
innerSelection.merge(innerSelectionEnter)
74-
.selectAll('path')
75-
.attr('opacity', 0.5)
70+
innerSelection.merge(innerSelectionEnter).selectAll('path').attr('opacity', 0.5)
7671

7772
innerSelection.exit().remove()
7873
})

src/helpers/secant.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { select as d3Select, Selection } from 'd3-selection'
33
import { builtIn as builtInEvaluator } from './eval'
44
import datumDefaults from '../datum-defaults'
55
import { polyline } from '../graph-types/'
6+
import utils from '../utils'
67

78
import { Chart } from '../index'
89
import { FunctionPlotDatumScope, FunctionPlotDatum, FunctionPlotDatumSecant } from '../types'
@@ -27,7 +28,7 @@ export default function secant(chart: Chart) {
2728
secant.scope = secant.scope || {}
2829

2930
const x0 = secant.x0
30-
const x1 = typeof secant.x1 === 'number' ? secant.x1 : Infinity
31+
const x1 = typeof secant.x1 === 'number' ? secant.x1 : utils.infinity()
3132
Object.assign(secant.scope, {
3233
x0,
3334
x1,

src/samplers/builtIn.test.js

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { scaleLinear as d3ScaleLinear } from 'd3-scale'
2+
import { expect } from '@jest/globals'
3+
4+
import builtIn from './builtIn'
5+
6+
const width = 200
7+
const height = 100
8+
const xDomain = [-5, 5]
9+
const yDomain = [-5, 5]
10+
const xScale = d3ScaleLinear().domain(xDomain).range([0, width])
11+
const yScale = d3ScaleLinear().domain(yDomain).range([height, 0])
12+
13+
function toBeCloseTo(got, want, eps = 1e-3) {
14+
if (!Array.isArray(got) || !Array.isArray(want)) {
15+
throw new Error('Got and Want must be arrays')
16+
}
17+
if (Math.abs(got[0] - want[0]) > eps || Math.abs(got[1] - want[1]) > eps) {
18+
return {
19+
message: () =>
20+
`expected ${this.utils.printReceived(got)} to be within range of ${this.utils.printReceived(want)}`,
21+
pass: false
22+
}
23+
}
24+
return { pass: true }
25+
}
26+
27+
expect.extend({ toBeCloseTo })
28+
29+
describe('builtIn sampler', () => {
30+
describe('with linear sampler', () => {
31+
it('should render 2 points for x', () => {
32+
const nSamples = 2
33+
// map the screen coordinates [0, width] to the domain [-5, 5]
34+
const samplerParams = {
35+
d: { fn: '1000000*x', fnType: 'linear' },
36+
range: xDomain,
37+
xScale,
38+
yScale,
39+
xAxis: { type: 'linear' },
40+
nSamples
41+
}
42+
const data = builtIn(samplerParams)
43+
expect(data instanceof Array).toEqual(true)
44+
expect(data.length).toEqual(1) /* we expect 1 group of points (all connected) */
45+
expect(data[0].length).toEqual(nSamples) /* the group should have 101 points */
46+
expect(data[0][0]).toEqual([-5, -5000000])
47+
expect(data[0][1]).toEqual([5, 5000000])
48+
})
49+
50+
it('should render 101 points for x^2', () => {
51+
const nSamples = 101
52+
// map the screen coordinates [0, width] to the domain [-5, 5]
53+
const samplerParams = {
54+
d: { fn: 'x^2', fnType: 'linear' },
55+
range: xDomain,
56+
xScale,
57+
yScale,
58+
xAxis: { type: 'linear' },
59+
nSamples
60+
}
61+
const data = builtIn(samplerParams)
62+
expect(data instanceof Array).toEqual(true)
63+
expect(data.length).toEqual(1) /* we expect 1 group of points (all connected) */
64+
expect(data[0].length).toEqual(nSamples) /* the group should have 101 points */
65+
expect(data[0][0]).toEqual([-5, 25]) /* f(-5) = [-5, 25] */
66+
expect(data[0][50]).toEqual([0, 0]) /* f(0) = [0, 0] */
67+
expect(data[0][100]).toEqual([5, 25]) /* f(5) = [5, 25] */
68+
})
69+
70+
it('should render 2 groups for 1/x', () => {
71+
const nSamples = 1000
72+
// map the screen coordinates [0, width] to the domain [-5, 5]
73+
const samplerParams = {
74+
d: { fn: '1/x', fnType: 'linear' },
75+
range: xDomain,
76+
xScale,
77+
yScale,
78+
xAxis: { type: 'linear' },
79+
nSamples
80+
}
81+
const data = builtIn(samplerParams)
82+
expect(data instanceof Array).toEqual(true)
83+
expect(data.length).toEqual(2) /* we expect 2 group of points (left of 0, right of 0) */
84+
expect(data[0].length).toEqual(nSamples / 2) /* the 1st group should have 50 points */
85+
expect(data[1].length).toEqual(nSamples / 2) /* the 2nd group should have 50 points */
86+
})
87+
})
88+
89+
describe('with parametric sampler', () => {
90+
it('should render a circle of radius 1', () => {
91+
const nSamples = 1001
92+
const samplerParams = {
93+
d: { x: 'cos(t)', y: 'sin(t)', fnType: 'parametric' },
94+
range: xDomain,
95+
xScale,
96+
yScale,
97+
xAxis: { type: 'linear' },
98+
nSamples
99+
}
100+
const data = builtIn(samplerParams)
101+
expect(data instanceof Array).toEqual(true)
102+
expect(data.length).toEqual(1) /* we expect 2 group of points (left of 0, right of 0) */
103+
expect(data[0].length).toEqual(nSamples) /* the 1st group should have 50 points */
104+
expect(data[0][0]).toBeCloseTo([1, 0])
105+
expect(data[0][250]).toBeCloseTo([0, 1])
106+
expect(data[0][500]).toBeCloseTo([-1, 0])
107+
expect(data[0][750]).toBeCloseTo([0, -1])
108+
expect(data[0][1000]).toBeCloseTo([1, 0])
109+
})
110+
})
111+
112+
describe('with points sampler', () => {
113+
it('should render a cube', () => {
114+
const nSamples = 4
115+
const samplerParams = {
116+
d: {
117+
points: [
118+
[1, 1],
119+
[2, 1],
120+
[2, 2],
121+
[1, 2]
122+
],
123+
fnType: 'points'
124+
},
125+
range: xDomain,
126+
xScale,
127+
yScale,
128+
xAxis: { type: 'linear' },
129+
nSamples
130+
}
131+
const data = builtIn(samplerParams)
132+
expect(data instanceof Array).toEqual(true)
133+
expect(data.length).toEqual(1) /* we expect 1 group */
134+
expect(data[0].length).toEqual(nSamples) /* to have 4 points */
135+
expect(data[0][0]).toBeCloseTo([1, 1])
136+
expect(data[0][1]).toBeCloseTo([2, 1])
137+
expect(data[0][2]).toBeCloseTo([2, 2])
138+
expect(data[0][3]).toBeCloseTo([1, 2])
139+
})
140+
})
141+
142+
describe('with vector sampler', () => {
143+
it('should render a vector', () => {
144+
const nSamples = 2
145+
const samplerParams = {
146+
d: {
147+
vector: [2, 1],
148+
offset: [1, 2],
149+
fnType: 'vector'
150+
},
151+
range: xDomain,
152+
xScale,
153+
yScale,
154+
xAxis: { type: 'linear' },
155+
nSamples
156+
}
157+
const data = builtIn(samplerParams)
158+
expect(data instanceof Array).toEqual(true)
159+
expect(data.length).toEqual(1) /* we expect 1 group */
160+
expect(data[0].length).toEqual(nSamples) /* to have 2 points */
161+
expect(data[0][0]).toBeCloseTo([1, 2])
162+
expect(data[0][1]).toBeCloseTo([3, 3])
163+
})
164+
})
165+
})

0 commit comments

Comments
 (0)