Skip to content

Commit 651aa08

Browse files
committed
Refactor graph types to classes.
Having classes instead of plain objects allows having aliases for known graph types, it also allows the addition of new graphs if they follow the same structure as other graph types. Example of the new API: ```javascript functionPlot({ target: '#playground', data: [ functionPlot.interval({ fn: 'x^2' }), functionPlot.scatter({ fn: 'sin(x)', nSamples: 50 }), functionPlot.polyline({ fn: 'x^3' }), functionPlot.text({ text: 'foo', location: [1, 2] }) ] }) ``` The existing object API is still supported and transformed into the classes, moving forward, the class API is preferred.
1 parent adafb75 commit 651aa08

18 files changed

+463
-287
lines changed

site/playground-rerender.html

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,13 @@
4242

4343
upd.addEventListener('click', evt => {
4444
obj.data[0] = { fn: 'x', nSamples: 10, graphType: 'scatter' }
45-
46-
// obj.data[0].nSamples = 10
47-
// obj.data[0].graphType = 'scatter'
4845
console.log(obj)
4946
functionPlot(obj)
5047
})
5148

5249
redefine.addEventListener('click', evt => {
53-
obj.data = [{
54-
fn: 'x', nSamples: 10, graphType: 'scatter'
55-
}]
56-
console.log(obj)
50+
obj.data = [functionPlot.text({ text: 'foo', location: [1, 2] })]
51+
console.log(obj.data)
5752
functionPlot(obj)
5853
})
5954

site/playground.html

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@
2929
functionPlot({
3030
target: '#playground',
3131
data: [
32-
{ fn: 'x^2', nSamples: 4000, sampler: 'asyncInterval' },
33-
{ fn: 'sin(x)', nSamples: 4000, sampler: 'asyncInterval' },
34-
{ fn: '1/x', nSamples: 4000, sampler: 'asyncInterval' }
32+
functionPlot.interval({ fn: 'x^2' }),
33+
functionPlot.scatter({ fn: 'sin(x)', nSamples: 50 }),
34+
functionPlot.polyline({ fn: 'x^3' }),
35+
functionPlot.text({ text: 'foo', location: [1, 2] })
3536
]
3637
})
3738
// functionPlot({

src/chart.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ import { select as d3Select, pointer as d3Pointer } from 'd3-selection'
77
import { interpolateRound as d3InterpolateRound } from 'd3-interpolate'
88
import EventEmitter from 'events'
99

10-
import { FunctionPlotOptions, FunctionPlotDatum, FunctionPlotScale, FunctionPlotOptionsAxis } from './types.js'
10+
import { FunctionPlotDatum, FunctionPlotOptions, FunctionPlotScale, FunctionPlotOptionsAxis } from './types.js'
11+
12+
import { Mark } from './graph-types/mark.js'
13+
import { interval, polyline, scatter, text } from './graph-types/index.js'
1114

1215
import annotations from './helpers/annotations.js'
1316
import mousetip from './tip.js'
14-
import helpers from './helpers/index.js'
17+
import { helpers } from './helpers/index.js'
1518
import datumDefaults from './datum-defaults.js'
1619
import datumValidation from './datum-validation.js'
1720
import globals from './globals.mjs'
@@ -565,7 +568,7 @@ export class Chart extends EventEmitter.EventEmitter {
565568
const graphsEnter = graphs.enter().append('g').attr('class', 'graph')
566569

567570
// enter + update
568-
graphs.merge(graphsEnter).each(function (d: FunctionPlotDatum, index: number) {
571+
graphs.merge(graphsEnter).each(function (d: Mark | FunctionPlotDatum, index: number) {
569572
// additional options needed in the graph-types/helpers
570573
d.index = index
571574

@@ -574,7 +577,31 @@ export class Chart extends EventEmitter.EventEmitter {
574577
d.generation = self.generation
575578

576579
const selection = d3Select(this)
577-
selection.call(globals.graphTypes[d.graphType](self))
580+
581+
// To preserve compatibility with v1. Convert the plain object into a mark.
582+
let mark: Mark
583+
if (!(d instanceof Mark)) {
584+
if (d.graphType === 'interval') {
585+
mark = interval(d)
586+
} else if (d.graphType === 'polyline') {
587+
mark = polyline(d)
588+
} else if (d.graphType === 'scatter') {
589+
mark = scatter(d)
590+
} else if (d.graphType === 'text') {
591+
mark = text(d)
592+
} else {
593+
throw new Error(
594+
`Cannot convert datum=${d} to be graph of type Interval, Polyline, Scatter or Text.\n` +
595+
`Check that a datum is an instance of Mark or that it can converted to any graph type above.`
596+
)
597+
}
598+
} else {
599+
mark = d
600+
}
601+
602+
mark.chart = self
603+
mark.render(selection)
604+
578605
selection.call(helpers(self))
579606
})
580607
this.generation += 1
@@ -731,8 +758,9 @@ export class Chart extends EventEmitter.EventEmitter {
731758
const xScaleClone = transform.rescaleX(self.meta.zoomBehavior.xScale).interpolate(d3InterpolateRound)
732759
const yScaleClone = transform.rescaleY(self.meta.zoomBehavior.yScale).interpolate(d3InterpolateRound)
733760

734-
// update the scales's metadata
735-
// NOTE: setting self.meta.xScale = self.meta.zoomBehavior.xScale creates artifacts and weird lines
761+
// update the scales' metadata
762+
// setting self.meta.xScale = self.meta.zoomBehavior.xScale creates artifacts and weird lines
763+
// so that's why the domain and range are cloned and then assigned.
736764
self.meta.xScale
737765
.domain(xScaleClone.domain())
738766
// @ts-ignore domain always returns typeof this.meta.yDomain

src/globals.mjs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ const Globals = {
1919
DEFAULT_ITERATIONS: null,
2020
TIP_X_EPS: 1,
2121
MAX_ITERATIONS: 0,
22-
graphTypes: {},
2322

2423
/** @type {null | any} */
2524
_workerPool: null,
@@ -39,12 +38,4 @@ const Globals = {
3938

4039
Globals.MAX_ITERATIONS = Globals.DEFAULT_WIDTH * 10
4140

42-
function registerGraphType(graphType, graphTypeBulder) {
43-
if (Object.hasOwn(Globals.graphTypes, graphType)) {
44-
throw new Error(`registerGraphType: graphType ${graphType} is already registered.`)
45-
}
46-
Globals.graphTypes[graphType] = graphTypeBulder
47-
}
48-
49-
export { registerGraphType }
5041
export default Globals

src/graph-types/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import polyline from './polyline.js'
2-
import interval from './interval.js'
3-
import scatter from './scatter.js'
4-
import text from './text.js'
1+
import { Interval, interval } from './interval.js'
2+
import { Polyline, polyline } from './polyline.js'
3+
import { Scatter, scatter } from './scatter.js'
4+
import { Text, text } from './text.js'
55

6-
export { polyline, interval, scatter, text }
6+
export { Polyline, polyline, Scatter, scatter, Interval, interval, Text, text }

src/graph-types/interval.ts

Lines changed: 56 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { select as d3Select, Selection } from 'd3-selection'
33
import { asyncIntervalEvaluate, intervalEvaluate } from '../evaluate-datum.js'
44
import { infinity, color } from '../utils.mjs'
55

6-
import { Chart } from '../index.js'
7-
import { Interval, FunctionPlotDatum, FunctionPlotScale, LinearFunction } from '../types.js'
6+
import { Mark } from './mark.js'
7+
import { Interval as TInterval, FunctionPlotDatum, FunctionPlotScale } from '../types.js'
88
import { IntervalSamplerResult } from '../samplers/types.js'
99

1010
function clampRange(minWidthHeight: number, vLo: number, vHi: number, gLo: number, gHi: number) {
@@ -39,7 +39,7 @@ export function createPathD(
3939
xScale: FunctionPlotScale,
4040
yScale: FunctionPlotScale,
4141
minWidthHeight: number,
42-
points: Array<[Interval, Interval]>,
42+
points: Array<[TInterval, TInterval]>,
4343
closed: boolean
4444
) {
4545
let path = ''
@@ -75,54 +75,67 @@ export function createPathD(
7575
return path
7676
}
7777

78-
export default function interval(chart: Chart) {
79-
const xScale = chart.meta.xScale
80-
const yScale = chart.meta.yScale
78+
export class Interval extends Mark {
79+
fn?: any
80+
closed: boolean
81+
fnType: string
82+
sampler: string
83+
range?: [number, number]
84+
nSamples: number
8185

82-
function plotLine(selection: Selection<any, FunctionPlotDatum, any, any>) {
83-
selection.each(async function (d) {
84-
const el = ((plotLine as any).el = d3Select(this))
85-
const index = d.index
86-
const closed = d.closed
87-
let evaluatedData: IntervalSamplerResult
88-
if (d.fnType === 'linear' && typeof (d as LinearFunction).fn === 'string' && d.sampler === 'asyncInterval') {
89-
evaluatedData = await asyncIntervalEvaluate(chart, d)
90-
} else {
91-
evaluatedData = intervalEvaluate(chart, d)
92-
}
93-
const innerSelection = el.selectAll(':scope > path.line').data(evaluatedData)
86+
constructor(options: any) {
87+
super(options)
88+
this.fn = options.fn
89+
this.fnType = options.fnType || 'linear'
90+
this.sampler = options.sampler || 'interval'
91+
this.closed = options.closed
92+
this.range = options.range
93+
this.nSamples = options.nSamples
94+
}
9495

95-
// the min height/width of the rects drawn by the path generator
96-
const minWidthHeight = Math.max((evaluatedData[0] as any).scaledDx, 1)
96+
async render(selection: Selection<any, FunctionPlotDatum, any, any>) {
97+
const index = this.index
98+
const closed = this.closed
99+
let evaluatedData: IntervalSamplerResult
100+
if (this.fnType === 'linear' && typeof this.fn === 'string' && this.sampler === 'asyncInterval') {
101+
evaluatedData = await asyncIntervalEvaluate(this.chart, this as any)
102+
} else {
103+
evaluatedData = intervalEvaluate(this.chart, this as any)
104+
}
105+
const innerSelection = selection.selectAll(':scope > path.line').data(evaluatedData)
97106

98-
const cls = `line line-${index}`
99-
const innerSelectionEnter = innerSelection.enter().append('path').attr('class', cls).attr('fill', 'none')
107+
// the min height/width of the rects drawn by the path generator
108+
const minWidthHeight = Math.max((evaluatedData[0] as any).scaledDx, 1)
100109

101-
// enter + update
102-
const selection = innerSelection
103-
.merge(innerSelectionEnter)
104-
.attr('stroke-width', minWidthHeight)
105-
.attr('stroke', color(d, index) as any)
106-
.attr('opacity', closed ? 0.5 : 1)
107-
.attr('d', function (d: Array<[Interval, Interval]>) {
108-
return createPathD(xScale, yScale, minWidthHeight, d, closed)
109-
})
110+
const cls = `line line-${index}`
111+
const innerSelectionEnter = innerSelection.enter().append('path').attr('class', cls).attr('fill', 'none')
110112

111-
if (d.attr) {
112-
for (const k in d.attr) {
113-
// If the attribute to modify is class then append the default class
114-
// or otherwise the d3 selection won't work.
115-
let val = d.attr[k]
116-
if (k === 'class') {
117-
val = `${cls} ${d.attr[k]}`
118-
}
119-
selection.attr(k, val)
113+
// enter + update
114+
innerSelection
115+
.merge(innerSelectionEnter)
116+
.attr('stroke-width', minWidthHeight)
117+
.attr('stroke', color(this, index) as any)
118+
.attr('opacity', closed ? 0.5 : 1)
119+
.attr('d', (d: Array<[TInterval, TInterval]>) => {
120+
return createPathD(this.chart.meta.xScale, this.chart.meta.yScale, minWidthHeight, d, closed)
121+
})
122+
123+
if (this.attr) {
124+
for (const k in this.attr) {
125+
// If the attribute to modify is class then append the default class
126+
// or otherwise the d3 selection won't work.
127+
let val = this.attr[k]
128+
if (k === 'class') {
129+
val = `${cls} ${this.attr[k]}`
120130
}
131+
selection.attr(k, val)
121132
}
133+
}
122134

123-
innerSelection.exit().remove()
124-
})
135+
innerSelection.exit().remove()
125136
}
137+
}
126138

127-
return plotLine
139+
export function interval(options: any) {
140+
return new Interval(options)
128141
}

src/graph-types/mark.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Chart } from '../index.js'
2+
3+
interface Attr {
4+
[key: string]: any
5+
}
6+
7+
export class Mark {
8+
id: string
9+
index: number
10+
color: string
11+
attr: Attr
12+
13+
chart: Chart
14+
15+
constructor(options: any) {
16+
this.id = options.id
17+
this.index = options.index
18+
this.attr = options.attr
19+
this.color = options.color
20+
}
21+
22+
render(selection: any) {}
23+
}

0 commit comments

Comments
 (0)