Skip to content

Commit 48c3086

Browse files
committed
Add new methods to do datum defaults and validation
1 parent ea65dcf commit 48c3086

File tree

7 files changed

+106
-38
lines changed

7 files changed

+106
-38
lines changed

src/chart.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import annotations from './helpers/annotations.js'
1313
import mousetip from './tip.js'
1414
import helpers from './helpers/index.js'
1515
import datumDefaults from './datum-defaults.js'
16+
import datumValidation from './datum-validation.js'
1617
import globals from './globals.mjs'
1718

1819
export interface ChartMetaMargin {
@@ -156,6 +157,8 @@ export class Chart extends EventEmitter.EventEmitter {
156157
*/
157158
plot() {
158159
this.emit('before:plot')
160+
this.setDefaultOptions()
161+
this.validateOptions()
159162
this.buildInternalVars()
160163
this.render()
161164
this.emit('after:plot')
@@ -182,6 +185,31 @@ export class Chart extends EventEmitter.EventEmitter {
182185
return cachedInstance
183186
}
184187

188+
private setDefaultOptions() {
189+
this.options.x = this.options.x || {}
190+
this.options.x.type = this.options.x.type || 'linear'
191+
192+
this.options.y = this.options.y || {}
193+
this.options.y.type = this.options.y.type || 'linear'
194+
195+
for (let d of this.options.data) {
196+
datumDefaults(d)
197+
}
198+
}
199+
200+
/**
201+
* Validate options provides best effort runtime validation of the options.
202+
*/
203+
private validateOptions() {
204+
try {
205+
for (let datum of this.options.data) {
206+
datumValidation(datum)
207+
}
208+
} catch (e) {
209+
throw new Error(`detected invalid options: ${e}`, e)
210+
}
211+
}
212+
185213
private buildInternalVars() {
186214
const margin = (this.meta.margin = { left: 40, right: 20, top: 20, bottom: 20 })
187215
// if there's a title make the top margin bigger
@@ -215,13 +243,7 @@ export class Chart extends EventEmitter.EventEmitter {
215243
return (self.meta.height * xDiff) / self.meta.width
216244
}
217245

218-
this.options.x = this.options.x || {}
219-
this.options.x.type = this.options.x.type || 'linear'
220-
221-
this.options.y = this.options.y || {}
222-
this.options.y.type = this.options.y.type || 'linear'
223-
224-
const xDomain = (this.meta.xDomain = (function (axis: FunctionPlotOptionsAxis) {
246+
const xDomain = (function (axis: FunctionPlotOptionsAxis): [number, number] {
225247
if (axis.domain) {
226248
return axis.domain
227249
}
@@ -232,9 +254,10 @@ export class Chart extends EventEmitter.EventEmitter {
232254
return [1, 10]
233255
}
234256
throw Error('axis type ' + axis.type + ' unsupported')
235-
})(this.options.x))
257+
})(this.options.x)
258+
this.meta.xDomain = xDomain
236259

237-
const yDomain = (this.meta.yDomain = (function (axis: FunctionPlotOptionsAxis) {
260+
const yDomain = (function (axis: FunctionPlotOptionsAxis): [number, number] {
238261
if (axis.domain) {
239262
return axis.domain
240263
}
@@ -245,7 +268,8 @@ export class Chart extends EventEmitter.EventEmitter {
245268
return [1, 10]
246269
}
247270
throw Error('axis type ' + axis.type + ' unsupported')
248-
})(this.options.y))
271+
})(this.options.y)
272+
this.meta.yDomain = yDomain
249273

250274
if (!this.meta.xScale) {
251275
this.meta.xScale = getD3Scale(this.options.x.type)()
@@ -544,7 +568,7 @@ export class Chart extends EventEmitter.EventEmitter {
544568
.selectAll(':scope > g.graph')
545569
.data(
546570
(d: FunctionPlotOptions) => {
547-
return d.data.map(datumDefaults)
571+
return d.data
548572
},
549573
(d: any) => {
550574
// The key is the function set or other value that uniquely identifies the datum.

src/datum-defaults.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ export default function datumDefaults(d: FunctionPlotDatum): FunctionPlotDatum {
1212
d.sampler = d.graphType !== 'interval' ? 'builtIn' : 'interval'
1313
}
1414

15-
// TODO: handle default fnType
16-
// default `fnType` is linear
17-
if (!('fnType' in d)) {
15+
if (!('fnType' in d) && (d.graphType == 'polyline' || d.graphType == 'interval' || d.graphType == 'scatter')) {
1816
d.fnType = 'linear'
1917
}
2018

src/datum-validation.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { FunctionPlotDatum } from './types.js'
2+
import { assert } from './utils.mjs'
3+
4+
export default function datumValidation(d: FunctionPlotDatum) {
5+
validateGraphType(d)
6+
validateFnType(d)
7+
}
8+
9+
function validateGraphType(d: FunctionPlotDatum) {
10+
// defaulted to 'interval' in datumDefaults.
11+
assert('graphType' in d, `graphType isn't defined`)
12+
}
13+
14+
function validateFnType(d: FunctionPlotDatum) {
15+
const invalid = `invalid option fnType=${d.fnType} with graphType=${d.graphType}`
16+
if (d.fnType === 'linear') {
17+
assert(d.graphType === 'polyline' || d.graphType === 'interval' || d.graphType === 'scatter', invalid)
18+
}
19+
if (d.fnType === 'parametric' || d.fnType === 'polar' || d.fnType === 'vector') {
20+
assert(d.graphType === 'polyline', invalid)
21+
}
22+
if (d.fnType === 'points') {
23+
assert(d.graphType === 'polyline' || d.graphType === 'scatter', invalid)
24+
}
25+
if (d.fnType === 'implicit') {
26+
assert(d.graphType === 'interval', invalid)
27+
}
28+
}

src/graph-types/interval.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { infinity, color } from '../utils.mjs'
55

66
import { Chart } from '../index.js'
77
import { Interval, FunctionPlotDatum, FunctionPlotScale, LinearFunction } from '../types.js'
8+
import { IntervalSamplerResult } from '../samplers/types.js'
89

910
function clampRange(minWidthHeight: number, vLo: number, vHi: number, gLo: number, gHi: number) {
1011
// issue 69
@@ -83,7 +84,7 @@ export default function interval(chart: Chart) {
8384
const el = ((plotLine as any).el = d3Select(this))
8485
const index = d.index
8586
const closed = d.closed
86-
let evaluatedData
87+
let evaluatedData: IntervalSamplerResult
8788
if (d.fnType === 'linear' && typeof (d as LinearFunction).fn === 'string' && d.sampler === 'asyncInterval') {
8889
evaluatedData = await asyncIntervalEvaluate(chart, d)
8990
} else {
@@ -92,7 +93,7 @@ export default function interval(chart: Chart) {
9293
const innerSelection = el.selectAll(':scope > path.line').data(evaluatedData)
9394

9495
// the min height/width of the rects drawn by the path generator
95-
const minWidthHeight = Math.max(evaluatedData[0].scaledDx, 1)
96+
const minWidthHeight = Math.max((evaluatedData[0] as any).scaledDx, 1)
9697

9798
const cls = `line line-${index}`
9899
const innerSelectionEnter = innerSelection.enter().append('path').attr('class', cls).attr('fill', 'none')

src/types.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ export interface LinearDatum {
239239
fnType?: 'linear'
240240
}
241241

242-
export type LinearFunction = LinearDatum & AbstractFunctionDatum
242+
export type LinearFunction = AbstractFunctionDatum & LinearDatum
243243

244244
export interface ImplicitDatum {
245245
/**
@@ -250,9 +250,14 @@ export interface ImplicitDatum {
250250
/**
251251
*/
252252
fnType: 'implicit'
253+
254+
/**
255+
* The graphType for an implicit function is always 'interval'
256+
*/
257+
graphType: 'interval'
253258
}
254259

255-
export type ImplicitFunction = ImplicitDatum & AbstractFunctionDatum
260+
export type ImplicitFunction = AbstractFunctionDatum & ImplicitDatum
256261

257262
export interface PolarDatum {
258263
/**
@@ -263,7 +268,7 @@ export interface PolarDatum {
263268
fnType: 'polar'
264269
}
265270

266-
export type PolarFunction = PolarDatum & AbstractFunctionDatum
271+
export type PolarFunction = AbstractFunctionDatum & PolarDatum
267272

268273
export interface ParametricDatum {
269274
/**
@@ -279,7 +284,7 @@ export interface ParametricDatum {
279284
fnType: 'parametric'
280285
}
281286

282-
export type ParametricFunction = ParametricDatum & AbstractFunctionDatum
287+
export type ParametricFunction = AbstractFunctionDatum & ParametricDatum
283288

284289
export interface PointDatum {
285290
/**
@@ -290,7 +295,7 @@ export interface PointDatum {
290295
fnType: 'points'
291296
}
292297

293-
export type PointFunction = PointDatum & AbstractFunctionDatum
298+
export type PointFunction = AbstractFunctionDatum & PointDatum
294299

295300
export interface VectorDatum {
296301
/**
@@ -306,7 +311,7 @@ export interface VectorDatum {
306311
fnType: 'vector'
307312
}
308313

309-
export type VectorFunction = VectorDatum & AbstractFunctionDatum
314+
export type VectorFunction = AbstractFunctionDatum & VectorDatum
310315

311316
export interface TextDatum {
312317
graphType: 'text'
@@ -322,7 +327,7 @@ export interface TextDatum {
322327
location?: [number, number]
323328
}
324329

325-
export type TextFunction = TextDatum & AbstractFunctionDatum
330+
export type TextFunction = AbstractFunctionDatum & TextDatum
326331

327332
export type FunctionPlotDatum =
328333
| AbstractFunctionDatum

src/utils.mjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,17 @@ export function color(data, index) {
9393
}
9494

9595
/**
96-
* Infinity is a value that is close to Infinity but not Infinity, it can fit in a JS number.
96+
* infinity is a value that is close to Infinity but not Infinity, it can fit in a JS number.
9797
*/
9898
export function infinity() {
9999
return 9007199254740991
100100
}
101+
102+
/**
103+
* asserts makes an simple assertion and throws `Error(message)` if the assertion failed
104+
*/
105+
export function assert(assertion, message) {
106+
if (!assertion) {
107+
throw new Error(message)
108+
}
109+
}

test/e2e/graphs.test.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import puppeteer from 'puppeteer'
1+
import puppeteer, { Page } from 'puppeteer'
22
import { expect, describe, it, beforeAll } from '@jest/globals'
33
import { toMatchImageSnapshot } from 'jest-image-snapshot'
44

@@ -12,18 +12,23 @@ const matchSnapshotConfig = {
1212
failureThresholdType: 'percent'
1313
}
1414

15+
async function getPage() {
16+
const browser = await puppeteer.launch({ headless: 'new' })
17+
const page = await browser.newPage()
18+
await page.setViewport({
19+
width: 1000,
20+
height: 1000,
21+
deviceScaleFactor: 2
22+
})
23+
await page.goto('http://localhost:4444/jest-function-plot.html')
24+
return page
25+
}
26+
1527
describe('Function Plot', () => {
16-
async function getPage() {
17-
const browser = await puppeteer.launch({ headless: 'new' })
18-
const page = await browser.newPage()
19-
await page.setViewport({
20-
width: 1000,
21-
height: 1000,
22-
deviceScaleFactor: 2
23-
})
24-
await page.goto('http://localhost:4444/jest-function-plot.html')
25-
return page
26-
}
28+
let page: Page
29+
beforeAll(async function () {
30+
page = await getPage()
31+
})
2732

2833
function stripWrappingFunction(fnString: string) {
2934
fnString = fnString.replace(/^\s*function\s*\(\)\s*\{/, '')
@@ -33,7 +38,6 @@ describe('Function Plot', () => {
3338

3439
snippets.forEach((snippet) => {
3540
it(snippet.testName, async () => {
36-
const page = await getPage()
3741
await page.evaluate(stripWrappingFunction(snippet.fn.toString()))
3842
// When a function that's evaluated asynchronously runs
3943
// it's possible that the rendering didn't happen yet.
@@ -52,7 +56,6 @@ describe('Function Plot', () => {
5256
})
5357

5458
it('update the graph using multiple renders', async () => {
55-
const page = await getPage()
5659
const firstRender = `
5760
const dualRender = {
5861
target: '#playground',

0 commit comments

Comments
 (0)