Skip to content

Commit

Permalink
SamplerType is a subset of the only params neeeded from the Chart
Browse files Browse the repository at this point in the history
  • Loading branch information
mauriciopoppe committed Dec 16, 2023
1 parent c155d2c commit 1c9cce5
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 88 deletions.
8 changes: 4 additions & 4 deletions src/chart.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
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
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'
Expand Down Expand Up @@ -39,8 +39,8 @@ export interface ChartMeta {
*/
height?: number
zoomBehavior?: any
xScale?: ScaleLinear<number, number> | ScaleLogarithmic<number, number>
yScale?: ScaleLinear<number, number> | ScaleLogarithmic<number, number>
xScale?: FunctionPlotScale
yScale?: FunctionPlotScale
xAxis?: Axis<any>
yAxis?: Axis<any>
xDomain?: number[]
Expand Down
29 changes: 22 additions & 7 deletions src/evaluate.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand Down
38 changes: 16 additions & 22 deletions src/helpers/secant.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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')
}
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -71,25 +71,19 @@ export default function secant (chart: Chart) {
return data
}

const secant = function (selection: Selection<any, FunctionPlotDatum, any, any>) {
function secant(selection: Selection<any, FunctionPlotDatum, any, any>) {
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()
Expand Down
55 changes: 27 additions & 28 deletions src/samplers/builtIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand Down Expand Up @@ -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<any> {
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]

Expand Down Expand Up @@ -105,77 +103,78 @@ 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<any> {
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<any> {
// 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<any> {
// 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])
}
return [samples]
}

function points(chart: Chart, d: FunctionPlotDatum, range: [number, number], nSamples: number) {
return [d.points]
function points(samplerParams: SamplerParams): Array<any> {
return [samplerParams.d.points]
}

function vector(chart: Chart, d: FunctionPlotDatum, range: [number, number], nSamples: number) {
function vector(sampleParams: SamplerParams): Array<any> {
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<any> {
const fnTypes = {
parametric,
polar,
points,
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
Loading

0 comments on commit 1c9cce5

Please sign in to comment.