Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new methods to do datum defaults and validation #325

Merged
merged 1 commit into from
Nov 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 35 additions & 11 deletions src/chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import annotations from './helpers/annotations.js'
import mousetip from './tip.js'
import helpers from './helpers/index.js'
import datumDefaults from './datum-defaults.js'
import datumValidation from './datum-validation.js'
import globals from './globals.mjs'

export interface ChartMetaMargin {
Expand Down Expand Up @@ -156,6 +157,8 @@ export class Chart extends EventEmitter.EventEmitter {
*/
plot() {
this.emit('before:plot')
this.setDefaultOptions()
this.validateOptions()
this.buildInternalVars()
this.render()
this.emit('after:plot')
Expand All @@ -182,6 +185,31 @@ export class Chart extends EventEmitter.EventEmitter {
return cachedInstance
}

private setDefaultOptions() {
this.options.x = this.options.x || {}
this.options.x.type = this.options.x.type || 'linear'

this.options.y = this.options.y || {}
this.options.y.type = this.options.y.type || 'linear'

for (let d of this.options.data) {
datumDefaults(d)
}
}

/**
* Validate options provides best effort runtime validation of the options.
*/
private validateOptions() {
try {
for (let datum of this.options.data) {
datumValidation(datum)
}
} catch (e) {
throw new Error(`detected invalid options: ${e}`, e)
}
}

private buildInternalVars() {
const margin = (this.meta.margin = { left: 40, right: 20, top: 20, bottom: 20 })
// if there's a title make the top margin bigger
Expand Down Expand Up @@ -215,13 +243,7 @@ export class Chart extends EventEmitter.EventEmitter {
return (self.meta.height * xDiff) / self.meta.width
}

this.options.x = this.options.x || {}
this.options.x.type = this.options.x.type || 'linear'

this.options.y = this.options.y || {}
this.options.y.type = this.options.y.type || 'linear'

const xDomain = (this.meta.xDomain = (function (axis: FunctionPlotOptionsAxis) {
const xDomain = (function (axis: FunctionPlotOptionsAxis): [number, number] {
if (axis.domain) {
return axis.domain
}
Expand All @@ -232,9 +254,10 @@ export class Chart extends EventEmitter.EventEmitter {
return [1, 10]
}
throw Error('axis type ' + axis.type + ' unsupported')
})(this.options.x))
})(this.options.x)
this.meta.xDomain = xDomain

const yDomain = (this.meta.yDomain = (function (axis: FunctionPlotOptionsAxis) {
const yDomain = (function (axis: FunctionPlotOptionsAxis): [number, number] {
if (axis.domain) {
return axis.domain
}
Expand All @@ -245,7 +268,8 @@ export class Chart extends EventEmitter.EventEmitter {
return [1, 10]
}
throw Error('axis type ' + axis.type + ' unsupported')
})(this.options.y))
})(this.options.y)
this.meta.yDomain = yDomain

if (!this.meta.xScale) {
this.meta.xScale = getD3Scale(this.options.x.type)()
Expand Down Expand Up @@ -544,7 +568,7 @@ export class Chart extends EventEmitter.EventEmitter {
.selectAll(':scope > g.graph')
.data(
(d: FunctionPlotOptions) => {
return d.data.map(datumDefaults)
return d.data
},
(d: any) => {
// The key is the function set or other value that uniquely identifies the datum.
Expand Down
4 changes: 1 addition & 3 deletions src/datum-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ export default function datumDefaults(d: FunctionPlotDatum): FunctionPlotDatum {
d.sampler = d.graphType !== 'interval' ? 'builtIn' : 'interval'
}

// TODO: handle default fnType
// default `fnType` is linear
if (!('fnType' in d)) {
if (!('fnType' in d) && (d.graphType == 'polyline' || d.graphType == 'interval' || d.graphType == 'scatter')) {
d.fnType = 'linear'
}

Expand Down
28 changes: 28 additions & 0 deletions src/datum-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { FunctionPlotDatum } from './types.js'
import { assert } from './utils.mjs'

export default function datumValidation(d: FunctionPlotDatum) {
validateGraphType(d)
validateFnType(d)
}

function validateGraphType(d: FunctionPlotDatum) {
// defaulted to 'interval' in datumDefaults.
assert('graphType' in d, `graphType isn't defined`)
}

function validateFnType(d: FunctionPlotDatum) {
const invalid = `invalid option fnType=${d.fnType} with graphType=${d.graphType}`
if (d.fnType === 'linear') {
assert(d.graphType === 'polyline' || d.graphType === 'interval' || d.graphType === 'scatter', invalid)
}
if (d.fnType === 'parametric' || d.fnType === 'polar' || d.fnType === 'vector') {
assert(d.graphType === 'polyline', invalid)
}
if (d.fnType === 'points') {
assert(d.graphType === 'polyline' || d.graphType === 'scatter', invalid)
}
if (d.fnType === 'implicit') {
assert(d.graphType === 'interval', invalid)
}
}
5 changes: 3 additions & 2 deletions src/graph-types/interval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { infinity, color } from '../utils.mjs'

import { Chart } from '../index.js'
import { Interval, FunctionPlotDatum, FunctionPlotScale, LinearFunction } from '../types.js'
import { IntervalSamplerResult } from '../samplers/types.js'

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

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

const cls = `line line-${index}`
const innerSelectionEnter = innerSelection.enter().append('path').attr('class', cls).attr('fill', 'none')
Expand Down
19 changes: 12 additions & 7 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export interface LinearDatum {
fnType?: 'linear'
}

export type LinearFunction = LinearDatum & AbstractFunctionDatum
export type LinearFunction = AbstractFunctionDatum & LinearDatum

export interface ImplicitDatum {
/**
Expand All @@ -250,9 +250,14 @@ export interface ImplicitDatum {
/**
*/
fnType: 'implicit'

/**
* The graphType for an implicit function is always 'interval'
*/
graphType: 'interval'
}

export type ImplicitFunction = ImplicitDatum & AbstractFunctionDatum
export type ImplicitFunction = AbstractFunctionDatum & ImplicitDatum

export interface PolarDatum {
/**
Expand All @@ -263,7 +268,7 @@ export interface PolarDatum {
fnType: 'polar'
}

export type PolarFunction = PolarDatum & AbstractFunctionDatum
export type PolarFunction = AbstractFunctionDatum & PolarDatum

export interface ParametricDatum {
/**
Expand All @@ -279,7 +284,7 @@ export interface ParametricDatum {
fnType: 'parametric'
}

export type ParametricFunction = ParametricDatum & AbstractFunctionDatum
export type ParametricFunction = AbstractFunctionDatum & ParametricDatum

export interface PointDatum {
/**
Expand All @@ -290,7 +295,7 @@ export interface PointDatum {
fnType: 'points'
}

export type PointFunction = PointDatum & AbstractFunctionDatum
export type PointFunction = AbstractFunctionDatum & PointDatum

export interface VectorDatum {
/**
Expand All @@ -306,7 +311,7 @@ export interface VectorDatum {
fnType: 'vector'
}

export type VectorFunction = VectorDatum & AbstractFunctionDatum
export type VectorFunction = AbstractFunctionDatum & VectorDatum

export interface TextDatum {
graphType: 'text'
Expand All @@ -322,7 +327,7 @@ export interface TextDatum {
location?: [number, number]
}

export type TextFunction = TextDatum & AbstractFunctionDatum
export type TextFunction = AbstractFunctionDatum & TextDatum

export type FunctionPlotDatum =
| AbstractFunctionDatum
Expand Down
11 changes: 10 additions & 1 deletion src/utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,17 @@ export function color(data, index) {
}

/**
* Infinity is a value that is close to Infinity but not Infinity, it can fit in a JS number.
* infinity is a value that is close to Infinity but not Infinity, it can fit in a JS number.
*/
export function infinity() {
return 9007199254740991
}

/**
* asserts makes an simple assertion and throws `Error(message)` if the assertion failed
*/
export function assert(assertion, message) {
if (!assertion) {
throw new Error(message)
}
}
31 changes: 17 additions & 14 deletions test/e2e/graphs.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import puppeteer from 'puppeteer'
import puppeteer, { Page } from 'puppeteer'
import { expect, describe, it, beforeAll } from '@jest/globals'
import { toMatchImageSnapshot } from 'jest-image-snapshot'

Expand All @@ -12,18 +12,23 @@ const matchSnapshotConfig = {
failureThresholdType: 'percent'
}

async function getPage() {
const browser = await puppeteer.launch({ headless: 'new' })
const page = await browser.newPage()
await page.setViewport({
width: 1000,
height: 1000,
deviceScaleFactor: 2
})
await page.goto('http://localhost:4444/jest-function-plot.html')
return page
}

describe('Function Plot', () => {
async function getPage() {
const browser = await puppeteer.launch({ headless: 'new' })
const page = await browser.newPage()
await page.setViewport({
width: 1000,
height: 1000,
deviceScaleFactor: 2
})
await page.goto('http://localhost:4444/jest-function-plot.html')
return page
}
let page: Page
beforeAll(async function () {
page = await getPage()
})

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

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

it('update the graph using multiple renders', async () => {
const page = await getPage()
const firstRender = `
const dualRender = {
target: '#playground',
Expand Down
Loading