From 1c9cce5005caf79d64edfce5c48bd1ae55a68e66 Mon Sep 17 00:00:00 2001 From: Mauricio Poppe Date: Fri, 15 Dec 2023 19:08:31 -0500 Subject: [PATCH] SamplerType is a subset of the only params neeeded from the Chart --- src/chart.ts | 8 +++--- src/evaluate.ts | 29 ++++++++++++++++----- src/helpers/secant.ts | 38 ++++++++++++--------------- src/samplers/builtIn.ts | 55 ++++++++++++++++++++-------------------- src/samplers/interval.ts | 45 ++++++++++++++++---------------- src/samplers/types.ts | 13 ++++++++++ src/types.ts | 8 ++++-- src/utils.ts | 6 ++--- 8 files changed, 114 insertions(+), 88 deletions(-) create mode 100644 src/samplers/types.ts diff --git a/src/chart.ts b/src/chart.ts index e542279..a57415e 100644 --- a/src/chart.ts +++ b/src/chart.ts @@ -1,6 +1,6 @@ import { line as d3Line, Line } from 'd3-shape' import { format as d3Format } from 'd3-format' -import { scaleLinear as d3ScaleLinear, scaleLog as d3ScaleLog, ScaleLinear, ScaleLogarithmic } from 'd3-scale' +import { scaleLinear as d3ScaleLinear, scaleLog as d3ScaleLog } from 'd3-scale' import { axisLeft as d3AxisLeft, axisBottom as d3AxisBottom, Axis } from 'd3-axis' import { zoom as d3Zoom } from 'd3-zoom' // @ts-ignore @@ -8,7 +8,7 @@ import { select as d3Select, pointer as d3Pointer } from 'd3-selection' import { interpolateRound as d3InterpolateRound } from 'd3-interpolate' import EventEmitter from 'events' -import { FunctionPlotOptions, FunctionPlotDatum } from './types' +import { FunctionPlotOptions, FunctionPlotDatum, FunctionPlotScale } from './types' import annotations from './helpers/annotations' import mousetip from './tip' @@ -39,8 +39,8 @@ export interface ChartMeta { */ height?: number zoomBehavior?: any - xScale?: ScaleLinear | ScaleLogarithmic - yScale?: ScaleLinear | ScaleLogarithmic + xScale?: FunctionPlotScale + yScale?: FunctionPlotScale xAxis?: Axis yAxis?: Axis xDomain?: number[] diff --git a/src/evaluate.ts b/src/evaluate.ts index 7353045..bd15cfe 100644 --- a/src/evaluate.ts +++ b/src/evaluate.ts @@ -1,13 +1,11 @@ import globals from './globals' -import { interval, builtIn } from './samplers' +import interval from './samplers/interval' +import builtIn from './samplers/builtIn' import { Chart } from './index' import { FunctionPlotDatum } from './types' -const evalTypeFn = { - interval, - builtIn -} +type SamplerTypeFn = typeof interval | typeof builtIn /** * Computes the endpoints x_lo, x_hi of the range @@ -34,10 +32,27 @@ function computeEndpoints(scale: any, d: any): [number, number] { */ function evaluate(chart: Chart, d: FunctionPlotDatum) { const range = computeEndpoints(chart.meta.xScale, d) - const evalFn = evalTypeFn[d.sampler] + + let samplerFn: SamplerTypeFn + if (d.sampler === 'builtIn') { + samplerFn = builtIn + } else if (d.sampler === 'interval') { + samplerFn = interval + } else { + throw new Error(`Invalid sampler function ${d.sampler}`) + } + const nSamples = d.nSamples || Math.min(globals.MAX_ITERATIONS, globals.DEFAULT_ITERATIONS || chart.meta.width * 2) - const data = evalFn(chart, d, range, nSamples) + const data = samplerFn({ + d, + range, + xScale: chart.meta.xScale, + yScale: chart.meta.yScale, + xAxis: chart.options.xAxis, + yAxis: chart.options.yAxis, + nSamples + }) // NOTE: it's impossible to listen for the first eval event // as the event is already fired when a listener is attached chart.emit('eval', data, d.index, d.isHelper) diff --git a/src/helpers/secant.ts b/src/helpers/secant.ts index 094c6ef..e233d8b 100644 --- a/src/helpers/secant.ts +++ b/src/helpers/secant.ts @@ -1,13 +1,13 @@ -import {select as d3Select, Selection} from 'd3-selection' +import { select as d3Select, Selection } from 'd3-selection' import { builtIn as builtInEvaluator } from './eval' import datumDefaults from '../datum-defaults' import { polyline } from '../graph-types/' -import { Chart } from "../index"; +import { Chart } from '../index' import { FunctionPlotDatumScope, FunctionPlotDatum, FunctionPlotDatumSecant } from '../types' -export default function secant (chart: Chart) { +export default function secant(chart: Chart) { const secantDefaults = datumDefaults({ isHelper: true, skipTip: true, @@ -16,11 +16,11 @@ export default function secant (chart: Chart) { graphType: 'polyline' }) - function computeSlope (scope: FunctionPlotDatumScope) { + function computeSlope(scope: FunctionPlotDatumScope) { scope.m = (scope.y1 - scope.y0) / (scope.x1 - scope.x0) } - function updateLine (d: FunctionPlotDatum, secant: FunctionPlotDatumSecant) { + function updateLine(d: FunctionPlotDatum, secant: FunctionPlotDatumSecant) { if (!('x0' in secant)) { throw Error('secant must have the property `x0` defined') } @@ -29,20 +29,20 @@ export default function secant (chart: Chart) { const x0 = secant.x0 const x1 = typeof secant.x1 === 'number' ? secant.x1 : Infinity Object.assign(secant.scope, { - x0: x0, - x1: x1, + x0, + x1, y0: builtInEvaluator(d, 'fn', { x: x0 }), y1: builtInEvaluator(d, 'fn', { x: x1 }) }) computeSlope(secant.scope) } - function setFn (d: FunctionPlotDatum, secant: FunctionPlotDatumSecant) { + function setFn(d: FunctionPlotDatum, secant: FunctionPlotDatumSecant) { updateLine(d, secant) secant.fn = 'm * (x - x0) + y0' } - function setMouseListener (d: FunctionPlotDatum, secantObject: FunctionPlotDatumSecant) { + function setMouseListener(d: FunctionPlotDatum, secantObject: FunctionPlotDatumSecant) { const self = this if (secantObject.updateOnMouseMove && !secantObject.$$mouseListener) { secantObject.$$mouseListener = function ({ x }: any) { @@ -54,12 +54,12 @@ export default function secant (chart: Chart) { } } - function computeLines (d: FunctionPlotDatum) { + function computeLines(d: FunctionPlotDatum) { const self = this const data = [] d.secants = d.secants || [] for (let i = 0; i < d.secants.length; i += 1) { - const secant = d.secants[i] = Object.assign({}, secantDefaults, d.secants[i]) + const secant = (d.secants[i] = Object.assign({}, secantDefaults, d.secants[i])) // necessary to make the secant have the same color as d secant.index = d.index if (!secant.fn) { @@ -71,25 +71,19 @@ export default function secant (chart: Chart) { return data } - const secant = function (selection: Selection) { + function secant(selection: Selection) { selection.each(function (d) { const el = d3Select(this) const data = computeLines.call(selection, d) - const innerSelection = el.selectAll('g.secant') - .data(data) + const innerSelection = el.selectAll('g.secant').data(data) - const innerSelectionEnter = innerSelection.enter() - .append('g') - .attr('class', 'secant') + const innerSelectionEnter = innerSelection.enter().append('g').attr('class', 'secant') // enter + update - innerSelection.merge(innerSelectionEnter) - .call(polyline(chart)) + innerSelection.merge(innerSelectionEnter).call(polyline(chart)) // change the opacity of the secants - innerSelection.merge(innerSelectionEnter) - .selectAll('path') - .attr('opacity', 0.5) + innerSelection.merge(innerSelectionEnter).selectAll('path').attr('opacity', 0.5) // exit innerSelection.exit().remove() diff --git a/src/samplers/builtIn.ts b/src/samplers/builtIn.ts index b467366..deaa5f4 100644 --- a/src/samplers/builtIn.ts +++ b/src/samplers/builtIn.ts @@ -3,8 +3,8 @@ import clamp from 'clamp' import utils from '../utils' import { builtIn as evaluate } from '../helpers/eval' -import { Chart } from '../chart' -import { FunctionPlotDatum } from '../types' +import { FunctionPlotDatum, FunctionPlotScale } from '../types' +import { SamplerParams, SamplerFn } from './types' function checkAsymptote( d0: number[], @@ -45,17 +45,15 @@ function checkAsymptote( /** * Splits the evaluated data into arrays, each array is separated by any asymptote found * through the process of detecting slope/sign brusque changes - * @param chart - * @param d - * @param data + * * @returns {Array[]} */ -function split(chart: Chart, d: FunctionPlotDatum, data: number[][]) { +function split(d: FunctionPlotDatum, data: number[][], yScale: FunctionPlotScale): Array { let i, oldSign let deltaX let st = [] const sets = [] - const domain = chart.meta.yScale.domain() + const domain = yScale.domain() const yMin = domain[0] const yMax = domain[1] @@ -105,48 +103,48 @@ function split(chart: Chart, d: FunctionPlotDatum, data: number[][]) { return sets } -function linear(chart: Chart, d: FunctionPlotDatum, range: [number, number], n: number) { - const allX = utils.space(chart.options.xAxis.type, range, n) - const yDomain = chart.meta.yScale.domain() +function linear(samplerParams: SamplerParams): Array { + const allX = utils.space(samplerParams.xAxis, samplerParams.range, samplerParams.nSamples) + const yDomain = samplerParams.yScale.domain() const yDomainMargin = yDomain[1] - yDomain[0] const yMin = yDomain[0] - yDomainMargin * 1e5 const yMax = yDomain[1] + yDomainMargin * 1e5 let data = [] for (let i = 0; i < allX.length; i += 1) { const x = allX[i] - const y = evaluate(d, 'fn', { x }) + const y = evaluate(samplerParams.d, 'fn', { x }) if (utils.isValidNumber(x) && utils.isValidNumber(y)) { data.push([x, clamp(y, yMin, yMax)]) } } - data = split(chart, d, data) + data = split(samplerParams.d, data, samplerParams.yScale) return data } -function parametric(chart: Chart, d: FunctionPlotDatum, range: [number, number], nSamples: number) { +function parametric(samplerParams: SamplerParams): Array { // range is mapped to canvas coordinates from the input // for parametric plots the range will tell the start/end points of the `t` param - const parametricRange = d.range || [0, 2 * Math.PI] - const tCoords = utils.space(chart.options.xAxis.type, parametricRange, nSamples) + const parametricRange = samplerParams.d.range || [0, 2 * Math.PI] + const tCoords = utils.space(samplerParams.xAxis, parametricRange, samplerParams.nSamples) const samples = [] for (let i = 0; i < tCoords.length; i += 1) { const t = tCoords[i] - const x = evaluate(d, 'x', { t }) - const y = evaluate(d, 'y', { t }) + const x = evaluate(samplerParams.d, 'x', { t }) + const y = evaluate(samplerParams.d, 'y', { t }) samples.push([x, y]) } return [samples] } -function polar(chart: Chart, d: FunctionPlotDatum, range: [number, number], nSamples: number) { +function polar(samplerParams: SamplerParams): Array { // range is mapped to canvas coordinates from the input // for polar plots the range will tell the start/end points of the `theta` param - const polarRange = d.range || [-Math.PI, Math.PI] - const thetaSamples = utils.space(chart.options.xAxis.type, polarRange, nSamples) + const polarRange = samplerParams.d.range || [-Math.PI, Math.PI] + const thetaSamples = utils.space(samplerParams.xAxis, polarRange, samplerParams.nSamples) const samples = [] for (let i = 0; i < thetaSamples.length; i += 1) { const theta = thetaSamples[i] - const r = evaluate(d, 'r', { theta }) + const r = evaluate(samplerParams.d, 'r', { theta }) const x = r * Math.cos(theta) const y = r * Math.sin(theta) samples.push([x, y]) @@ -154,16 +152,17 @@ function polar(chart: Chart, d: FunctionPlotDatum, range: [number, number], nSam return [samples] } -function points(chart: Chart, d: FunctionPlotDatum, range: [number, number], nSamples: number) { - return [d.points] +function points(samplerParams: SamplerParams): Array { + return [samplerParams.d.points] } -function vector(chart: Chart, d: FunctionPlotDatum, range: [number, number], nSamples: number) { +function vector(sampleParams: SamplerParams): Array { + const d = sampleParams.d d.offset = d.offset || [0, 0] return [[d.offset, [d.vector[0] + d.offset[0], d.vector[1] + d.offset[1]]]] } -const sampler = function (chart: Chart, d: FunctionPlotDatum, range: [number, number], nSamples: number) { +const sampler: SamplerFn = function sampler(samplerParams: SamplerParams): Array { const fnTypes = { parametric, polar, @@ -171,11 +170,11 @@ const sampler = function (chart: Chart, d: FunctionPlotDatum, range: [number, nu vector, linear } - if (!(d.fnType in fnTypes)) { - throw Error(d.fnType + ' is not supported in the `builtIn` sampler') + if (!(samplerParams.d.fnType in fnTypes)) { + throw Error(samplerParams.d.fnType + ' is not supported in the `builtIn` sampler') } // @ts-ignore - return fnTypes[d.fnType].apply(null, arguments) + return fnTypes[samplerParams.d.fnType].apply(null, arguments) } export default sampler diff --git a/src/samplers/interval.ts b/src/samplers/interval.ts index 57292d7..58a40ae 100644 --- a/src/samplers/interval.ts +++ b/src/samplers/interval.ts @@ -1,24 +1,25 @@ import intervalArithmeticEval, { Interval } from 'interval-arithmetic-eval' -import { Chart } from '../chart' -import { FunctionPlotDatum } from '../types' import { interval as evaluate } from '../helpers/eval' import utils from '../utils' +import { FunctionPlotDatum } from '../types' +import { SamplerParams, SamplerFn } from './types' + // disable the use of typed arrays in interval-arithmetic to improve the performance ;(intervalArithmeticEval as any).policies.disableRounding() -function interval1d(chart: Chart, d: FunctionPlotDatum, range: [number, number], nSamples: number) { - const xCoords = utils.space(chart.options.xAxis.type, range, nSamples) - const xScale = chart.meta.xScale - const yScale = chart.meta.yScale +function interval1d(samplerParams: SamplerParams): Array { + const xCoords = utils.space(samplerParams.xAxis, samplerParams.range, samplerParams.nSamples) + const xScale = samplerParams.xScale + const yScale = samplerParams.yScale const yMin = yScale.domain()[0] const yMax = yScale.domain()[1] const samples = [] let i for (i = 0; i < xCoords.length - 1; i += 1) { const x = { lo: xCoords[i], hi: xCoords[i + 1] } - const y = evaluate(d, 'fn', { x }) + const y = evaluate(samplerParams.d, 'fn', { x }) if (!Interval.isEmpty(y) && !Interval.isWhole(y)) { samples.push([x, y]) } @@ -69,8 +70,8 @@ function smallRect(x: Interval, y: Interval) { return Interval.width(x) < rectEps } -function quadTree(x: Interval, y: Interval, meta: FunctionPlotDatum) { - const sample = evaluate(meta, 'fn', { x, y }) +function quadTree(x: Interval, y: Interval, d: FunctionPlotDatum) { + const sample = evaluate(d, 'fn', { x, y }) const fulfills = Interval.zeroIn(sample) if (!fulfills) { return this @@ -86,36 +87,36 @@ function quadTree(x: Interval, y: Interval, meta: FunctionPlotDatum) { const north = { lo: midY, hi: y.hi } const south = { lo: y.lo, hi: midY } - quadTree.call(this, east, north, meta) - quadTree.call(this, east, south, meta) - quadTree.call(this, west, north, meta) - quadTree.call(this, west, south, meta) + quadTree.call(this, east, north, d) + quadTree.call(this, east, south, d) + quadTree.call(this, west, north, d) + quadTree.call(this, west, south, d) } -function interval2d(chart: Chart, meta: FunctionPlotDatum) { - const xScale = chart.meta.xScale - const xDomain = chart.meta.xScale.domain() - const yDomain = chart.meta.yScale.domain() +function interval2d(samplerParams: SamplerParams): Array { + const xScale = samplerParams.xScale + const xDomain = samplerParams.xScale.domain() + const yDomain = samplerParams.yScale.domain() const x = { lo: xDomain[0], hi: xDomain[1] } const y = { lo: yDomain[0], hi: yDomain[1] } const samples: any = [] // 1 px rectEps = xScale.invert(1) - xScale.invert(0) - quadTree.call(samples, x, y, meta) + quadTree.call(samples, x, y, samplerParams.d) samples.scaledDx = 1 return [samples] } -const sampler = function (chart: Chart, d: FunctionPlotDatum, range: [number, number], nSamples: number) { +const sampler: SamplerFn = function sampler(samplerParams: SamplerParams): Array { const fnTypes = { implicit: interval2d, linear: interval1d } - if (!Object.hasOwn(fnTypes, d.fnType)) { - throw Error(d.fnType + ' is not supported in the `interval` sampler') + if (!Object.hasOwn(fnTypes, samplerParams.d.fnType)) { + throw Error(samplerParams.d.fnType + ' is not supported in the `interval` sampler') } // @ts-ignore - return fnTypes[d.fnType].apply(null, arguments) + return fnTypes[samplerParams.d.fnType].apply(null, arguments) } export default sampler diff --git a/src/samplers/types.ts b/src/samplers/types.ts new file mode 100644 index 0000000..4dbaa51 --- /dev/null +++ b/src/samplers/types.ts @@ -0,0 +1,13 @@ +import { FunctionPlotDatum, FunctionPlotScale, FunctionPlotOptionsAxis } from '../types' + +export type SamplerParams = { + d: FunctionPlotDatum + range: [number, number] + xScale: FunctionPlotScale + yScale: FunctionPlotScale + xAxis: FunctionPlotOptionsAxis + yAxis: FunctionPlotOptionsAxis + nSamples: number +} + +export type SamplerFn = (samplerParams: SamplerParams) => Array diff --git a/src/types.ts b/src/types.ts index 47eb957..3424a9d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,12 @@ +import { ScaleLinear, ScaleLogarithmic } from 'd3-scale' + export interface Interval { lo: number hi: number } +export type FunctionPlotScale = ScaleLinear | ScaleLogarithmic + export interface FunctionPlotOptionsAxis { /** * Initial ends of the axis @@ -304,12 +308,12 @@ export interface FunctionPlotOptions { /** * The x-axis domain, internally state used to preserve the x-domain across multiple calls to function plot */ - xDomain?: number[] + xDomain?: [number, number] /** * The y-axis domain, internally state used to preserve the y-domain across multiple calls to function plot */ - yDomain?: number[] + yDomain?: [number, number] /** * The tip configuration diff --git a/src/utils.ts b/src/utils.ts index 161ee79..a6b5436 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ import globals from './globals' -import { FunctionPlotDatum } from './types' +import { FunctionPlotDatum, FunctionPlotOptionsAxis } from './types' const utils = { linspace: function (lo: number, hi: number, n: number): number[] { @@ -16,10 +16,10 @@ const utils = { return typeof v === 'number' && !isNaN(v) }, - space: function (type: string, range: [number, number], n: number) { + space: function (axis: FunctionPlotOptionsAxis, range: [number, number], n: number) { const lo = range[0] const hi = range[1] - if (type === 'log') { + if (axis.type === 'log') { return this.logspace(Math.log10(lo), Math.log10(hi), n) } // default is linear