@johnhenry/math-sample 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -2
- package/dist/expr.d.ts +26 -0
- package/dist/expr.d.ts.map +1 -0
- package/dist/expr.js +24 -0
- package/dist/expr.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/sample.d.ts +114 -0
- package/dist/sample.d.ts.map +1 -0
- package/dist/sample.js +308 -0
- package/dist/sample.js.map +1 -0
- package/dist/svg.d.ts +37 -0
- package/dist/svg.d.ts.map +1 -0
- package/dist/svg.js +82 -0
- package/dist/svg.js.map +1 -0
- package/dist/ticks.d.ts +17 -0
- package/dist/ticks.d.ts.map +1 -0
- package/dist/ticks.js +47 -0
- package/dist/ticks.js.map +1 -0
- package/dist/viewport.d.ts +59 -0
- package/dist/viewport.d.ts.map +1 -0
- package/dist/viewport.js +111 -0
- package/dist/viewport.js.map +1 -0
- package/package.json +70 -4
- package/src/expr.ts +39 -0
- package/src/index.ts +36 -0
- package/src/sample.ts +391 -0
- package/src/svg.ts +124 -0
- package/src/ticks.ts +46 -0
- package/src/viewport.ts +150 -0
package/README.md
CHANGED
|
@@ -1,3 +1,137 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @johnhenry/math-sample
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Sampling for plots, as a library with no DOM in it. Give it a `(x) => number`
|
|
4
|
+
(or an `@johnhenry/math` expression) and a viewport; get back `Float64Array`s
|
|
5
|
+
with the poles and jumps already turned into gaps, plus the viewport maths,
|
|
6
|
+
"nice" ticks and SVG path output a plot needs. Pure ESM, zero runtime
|
|
7
|
+
dependencies, no globals beyond the language: it runs unchanged in a Web
|
|
8
|
+
Worker, so a plotter can send it *sources and a viewport*, and transfer the
|
|
9
|
+
arrays back.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @johnhenry/math-sample
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { sampleAdaptive, samplesToSvgPath, axesToSvg, niceTicks } from "@johnhenry/math-sample";
|
|
17
|
+
import { compileExpression } from "@johnhenry/math-sample/expr"; // needs @johnhenry/math
|
|
18
|
+
|
|
19
|
+
const f = compileExpression("a*sin(x)", { params: { a: 2 } });
|
|
20
|
+
const view = { xMin: -10, xMax: 10, yMin: -6, yMax: 6 };
|
|
21
|
+
|
|
22
|
+
const s = sampleAdaptive(f, { min: view.xMin, max: view.xMax }, 400, {
|
|
23
|
+
visibleYRange: { min: view.yMin, max: view.yMax },
|
|
24
|
+
});
|
|
25
|
+
s.x; s.y; // Float64Array, NaN in y = gap
|
|
26
|
+
samplesToSvgPath(s, view, 600, 300); // "M0.00 150.00 L1.50 ..."
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## API
|
|
30
|
+
|
|
31
|
+
### Samples
|
|
32
|
+
|
|
33
|
+
`Samples` is `{ x: Float64Array; y: Float64Array }`. A row with `y = NaN` is a
|
|
34
|
+
gap: do not draw between its neighbours. Gaps are never first, last or
|
|
35
|
+
adjacent to another gap, so `runsOf(samples)` (half-open row ranges) is exactly
|
|
36
|
+
the polylines to draw.
|
|
37
|
+
|
|
38
|
+
| Function | Does |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `sampleUniform(f, domain, n, options?)` | `n` evenly spaced evaluations, both ends included |
|
|
41
|
+
| `sampleAdaptive(f, domain, n, options?)` | the same base grid, with cells bisected where the curve bends |
|
|
42
|
+
| `runsOf(samples)` | `[start, end)` row ranges of consecutive data |
|
|
43
|
+
| `findDiscontinuities(samples)` | `{ before, after }` points either side of every gap |
|
|
44
|
+
| `findCrossings(samples, signedDistance)` | interpolated crossings of a boundary (`y => y` is the x-axis); not across gaps |
|
|
45
|
+
| `findRoots(f, domain, { resolution? })` | sign changes on a grid, polished by bisection; poles rejected |
|
|
46
|
+
| `findIntersections(f, g, domain, opts?)` | roots of `f - g`, each with `y = f(x)` |
|
|
47
|
+
| `toPoints(samples)` | `[x, y \| null][]`, JSON-safe |
|
|
48
|
+
|
|
49
|
+
`f` is any `(x: number) => number`. Domains are `{ min, max }` (finite, `min < max`);
|
|
50
|
+
`n` is an integer >= 2. Bad input throws `RangeError`.
|
|
51
|
+
|
|
52
|
+
**Options (all samplers):**
|
|
53
|
+
|
|
54
|
+
- `visibleYRange: { min, max }`: the y-range on screen. A sample far outside it
|
|
55
|
+
(a pole's finite but enormous shoulders) is treated as a gap, and the curve
|
|
56
|
+
is capped with an interpolated point just inside the edge, so it runs up to
|
|
57
|
+
the border of the plot instead of stopping short. Also sets the default
|
|
58
|
+
tolerance and, for `sampleAdaptive`, enables jump detection (`sampleUniform` does not detect jumps).
|
|
59
|
+
- `signal` (`{ aborted: boolean }`, so any `AbortSignal`) and `isCancelled: () => boolean`:
|
|
60
|
+
checked every 256 evaluations; either one makes the sampler throw
|
|
61
|
+
`SampleCancelledError`. `isCancelled: () => generation !== latest` is the
|
|
62
|
+
generation-counter pattern for pan/zoom.
|
|
63
|
+
|
|
64
|
+
**Options (`sampleAdaptive`):**
|
|
65
|
+
|
|
66
|
+
- `maxDepth` (4): bisections per base cell.
|
|
67
|
+
- `tolerance`: a cell is split while its midpoint is further than this, in y
|
|
68
|
+
units, from the straight line through its ends. Default `1e-4` of the visible
|
|
69
|
+
y span, so zooming in refines more and zooming out less; with no
|
|
70
|
+
`visibleYRange`, `1e-3`.
|
|
71
|
+
- `jumpFraction` (0.5 with `visibleYRange`; off without): a cell still this far
|
|
72
|
+
apart (as a fraction of the y span) at `maxDepth`, whose height has not
|
|
73
|
+
shrunk as it was halved, is a jump (a step, a pole) and becomes a gap. A
|
|
74
|
+
steep slope loses height as it is halved, so it stays connected. `Infinity` turns it off.
|
|
75
|
+
|
|
76
|
+
### Viewport
|
|
77
|
+
|
|
78
|
+
`Viewport` is `{ xMin, xMax, yMin, yMax }`. Everything is unit-agnostic: use CSS
|
|
79
|
+
pixels or device pixels, as long as width and height agree.
|
|
80
|
+
|
|
81
|
+
`toScreenX/toScreenY(v, viewport, size)`, `toDataX/toDataY(s, viewport, size)`,
|
|
82
|
+
`panViewport(v, dxPx, dyPx, w, h)` (content follows the pointer),
|
|
83
|
+
`zoomViewport(v, factor, sx, sy, w, h)` (the data under the cursor stays put),
|
|
84
|
+
`viewportFromAnchor(...)`, `wheelZoomFactor(deltaY, step?)`,
|
|
85
|
+
`pinchZoomFactor(startDist, currentDist)`, `fitViewport(x, y, padding?)`, and
|
|
86
|
+
`backingStoreSize(cssW, cssH, dpr)` which returns `{ width, height, scale }` for
|
|
87
|
+
sizing a canvas: set `canvas.width/height` to those, `ctx.scale(scale, scale)`,
|
|
88
|
+
and draw in CSS pixels. A non-finite or non-positive `dpr` counts as 1.
|
|
89
|
+
|
|
90
|
+
### Ticks
|
|
91
|
+
|
|
92
|
+
`niceTicks(min, max, target = 6)` gives round values (1, 2 or 5 times a power of
|
|
93
|
+
ten) with no float drift (`0.3`, not `0.30000000000000004`). `tickStep` is the
|
|
94
|
+
step it chose, and `formatTick(v, step)` labels with matching decimals.
|
|
95
|
+
Degenerate or non-finite ranges give `[]`.
|
|
96
|
+
|
|
97
|
+
### SVG
|
|
98
|
+
|
|
99
|
+
`samplesToSvgPath(samples, viewport, w, h, { digits = 2 })` returns path data,
|
|
100
|
+
one `M ... L ...` subpath per run. `axesToSvg(viewport, w, h, { targetTickCount, stroke, labelFill, fontSize })`
|
|
101
|
+
returns `<line>`/`<text>` elements with inline attributes (axes at zero, or
|
|
102
|
+
hugging the nearest edge with labels flipped inward). `svgDocument(w, h, content)`
|
|
103
|
+
wraps them.
|
|
104
|
+
|
|
105
|
+
### Expressions (`@johnhenry/math-sample/expr`)
|
|
106
|
+
|
|
107
|
+
`compileExpression(exprOrSource, { variable = "x", params = {} })` returns a
|
|
108
|
+
`(x) => number` from an `@johnhenry/math` `Expr` or source string. This is the
|
|
109
|
+
only entry that needs `@johnhenry/math`, which is an **optional peer
|
|
110
|
+
dependency**; the main entry never imports it. In a Worker: post the source
|
|
111
|
+
string and params, compile there, sample, and `postMessage` the arrays with
|
|
112
|
+
`[s.x.buffer, s.y.buffer]` as transferables.
|
|
113
|
+
|
|
114
|
+
## What it does not do (yet)
|
|
115
|
+
|
|
116
|
+
- **Implicit multiplication is opt-in.** `2x` fails to parse by default; write
|
|
117
|
+
`2*x`, or pass `compileExpression(src, { implicitMultiplication: true })`
|
|
118
|
+
(needs `@johnhenry/math` 0.1.0+; multi-letter variable names such as `theta`
|
|
119
|
+
then become products, use `x_1`).
|
|
120
|
+
- **1D `y = f(x)` only.** Implicit/interval curves, parametric and polar,
|
|
121
|
+
surfaces, ODEs and series samplers are planned as later parts.
|
|
122
|
+
- **Gaps from steep or fast-oscillating functions.** Jump detection is a
|
|
123
|
+
heuristic; a function that oscillates faster than the grid resolves can read
|
|
124
|
+
as a jump. Raise the resolution or `maxDepth`, or set `jumpFraction: Infinity`.
|
|
125
|
+
- **Double-root and tangent roots are missed** by `findRoots` (no sign change).
|
|
126
|
+
- **No memoization or debouncing.** Callers own caching; use `isCancelled` to
|
|
127
|
+
drop stale work.
|
|
128
|
+
- **Typed output is `Float64Array` only.** Convert with `Float32Array.from(s.y)`
|
|
129
|
+
if you need less.
|
|
130
|
+
|
|
131
|
+
## Tests
|
|
132
|
+
|
|
133
|
+
`npm test -w @johnhenry/math-sample`. Golden values from NumPy, SciPy
|
|
134
|
+
(`brentq`) and SymPy (pole locations) are committed in `test/fixtures/goldens.json`;
|
|
135
|
+
regenerate with `scripts/generate-goldens.py` (the tests never call Python).
|
|
136
|
+
Behaviour was designed against [mallory](https://github.com/johnhenry/mallory)'s
|
|
137
|
+
samplers (MIT, same author); the code here is a rewrite.
|
package/dist/expr.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Expr } from "@johnhenry/math";
|
|
2
|
+
import type { Fn1 } from "./sample.ts";
|
|
3
|
+
export interface CompileOptions {
|
|
4
|
+
/** The free variable that becomes the argument. Default `"x"`. */
|
|
5
|
+
variable?: string;
|
|
6
|
+
/** Fixed values for every other free variable (slider values, say). */
|
|
7
|
+
params?: Record<string, number>;
|
|
8
|
+
/**
|
|
9
|
+
* Read juxtaposition as multiplication in source text (`2x`, `3sin(x)`, `(x+1)(x-1)`), the way people
|
|
10
|
+
* type maths. Default `false`. Needs `@johnhenry/math` 0.1.0 or later; multi-letter variable names are
|
|
11
|
+
* unavailable when it is on (see `ParseOptions` in `@johnhenry/math`). Ignored when `expr` is already a parsed `Expr`.
|
|
12
|
+
*/
|
|
13
|
+
implicitMultiplication?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Compile an `@johnhenry/math` expression (source text or a parsed `Expr`)
|
|
17
|
+
* into a plain `(x) => number` the samplers take. The AST is walked once; the
|
|
18
|
+
* closure is cheap to call thousands of times. This is the only module that
|
|
19
|
+
* needs `@johnhenry/math` (an optional peer dependency), so a Worker can
|
|
20
|
+
* receive an expression string plus params, compile it here, and sample.
|
|
21
|
+
*
|
|
22
|
+
* Source text uses `@johnhenry/math`'s grammar: write `2*x`, or pass
|
|
23
|
+
* `implicitMultiplication: true` to accept `2x`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function compileExpression(expr: Expr | string, options?: CompileOptions): Fn1;
|
|
26
|
+
//# sourceMappingURL=expr.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expr.d.ts","sourceRoot":"","sources":["../src/expr.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,EAAY,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAEvC,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,GAAG,CAYxF"}
|
package/dist/expr.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Symbolic } from "@johnhenry/math";
|
|
2
|
+
/**
|
|
3
|
+
* Compile an `@johnhenry/math` expression (source text or a parsed `Expr`)
|
|
4
|
+
* into a plain `(x) => number` the samplers take. The AST is walked once; the
|
|
5
|
+
* closure is cheap to call thousands of times. This is the only module that
|
|
6
|
+
* needs `@johnhenry/math` (an optional peer dependency), so a Worker can
|
|
7
|
+
* receive an expression string plus params, compile it here, and sample.
|
|
8
|
+
*
|
|
9
|
+
* Source text uses `@johnhenry/math`'s grammar: write `2*x`, or pass
|
|
10
|
+
* `implicitMultiplication: true` to accept `2x`.
|
|
11
|
+
*/
|
|
12
|
+
export function compileExpression(expr, options = {}) {
|
|
13
|
+
const variable = options.variable ?? "x";
|
|
14
|
+
const tree = typeof expr === "string"
|
|
15
|
+
? Symbolic.parse(expr, { implicitMultiplication: options.implicitMultiplication === true })
|
|
16
|
+
: expr;
|
|
17
|
+
const compiled = Symbolic.compile(tree);
|
|
18
|
+
const env = { ...options.params, [variable]: 0 };
|
|
19
|
+
return (x) => {
|
|
20
|
+
env[variable] = x;
|
|
21
|
+
return compiled(env);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=expr.js.map
|
package/dist/expr.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expr.js","sourceRoot":"","sources":["../src/expr.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAgBtD;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAmB,EAAE,UAA0B,EAAE;IACjF,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,GAAG,CAAC;IACzC,MAAM,IAAI,GACR,OAAO,IAAI,KAAK,QAAQ;QACtB,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,sBAAsB,EAAE,OAAO,CAAC,sBAAsB,KAAK,IAAI,EAAE,CAAC;QAC3F,CAAC,CAAC,IAAI,CAAC;IACX,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,GAAG,GAA2B,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;IACzE,OAAO,CAAC,CAAC,EAAE,EAAE;QACX,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAClB,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC;IACvB,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { type AdaptiveOptions, type CancelSignal, type Domain, type Fn1, findCrossings, findDiscontinuities, findIntersections, findRoots, type Point, type RootOptions, runsOf, SampleCancelledError, type SampleOptions, type Samples, sampleAdaptive, sampleUniform, toPoints, type YRange, } from "./sample.ts";
|
|
2
|
+
export { type AxesOptions, axesToSvg, type PathOptions, samplesToSvgPath, svgDocument, type XY } from "./svg.ts";
|
|
3
|
+
export { formatTick, niceTicks, tickStep } from "./ticks.ts";
|
|
4
|
+
export { backingStoreSize, fitViewport, panViewport, pinchZoomFactor, toDataX, toDataY, toScreenX, toScreenY, type Viewport, viewportFromAnchor, wheelZoomFactor, zoomViewport, } from "./viewport.ts";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,GAAG,EACR,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,SAAS,EACT,KAAK,KAAK,EACV,KAAK,WAAW,EAChB,MAAM,EACN,oBAAoB,EACpB,KAAK,aAAa,EAClB,KAAK,OAAO,EACZ,cAAc,EACd,aAAa,EACb,QAAQ,EACR,KAAK,MAAM,GACZ,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,KAAK,WAAW,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,gBAAgB,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,MAAM,UAAU,CAAC;AACjH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,WAAW,EACX,eAAe,EACf,OAAO,EACP,OAAO,EACP,SAAS,EACT,SAAS,EACT,KAAK,QAAQ,EACb,kBAAkB,EAClB,eAAe,EACf,YAAY,GACb,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { findCrossings, findDiscontinuities, findIntersections, findRoots, runsOf, SampleCancelledError, sampleAdaptive, sampleUniform, toPoints, } from "./sample.js";
|
|
2
|
+
export { axesToSvg, samplesToSvgPath, svgDocument } from "./svg.js";
|
|
3
|
+
export { formatTick, niceTicks, tickStep } from "./ticks.js";
|
|
4
|
+
export { backingStoreSize, fitViewport, panViewport, pinchZoomFactor, toDataX, toDataY, toScreenX, toScreenY, viewportFromAnchor, wheelZoomFactor, zoomViewport, } from "./viewport.js";
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,SAAS,EAGT,MAAM,EACN,oBAAoB,EAGpB,cAAc,EACd,aAAa,EACb,QAAQ,GAET,MAAM,aAAa,CAAC;AACrB,OAAO,EAAoB,SAAS,EAAoB,gBAAgB,EAAE,WAAW,EAAW,MAAM,UAAU,CAAC;AACjH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,WAAW,EACX,eAAe,EACf,OAAO,EACP,OAAO,EACP,SAAS,EACT,SAAS,EAET,kBAAkB,EAClB,eAAe,EACf,YAAY,GACb,MAAM,eAAe,CAAC"}
|
package/dist/sample.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** A real function of one variable, e.g. a compiled expression. */
|
|
2
|
+
export type Fn1 = (x: number) => number;
|
|
3
|
+
/** A closed interval of the x-axis. */
|
|
4
|
+
export interface Domain {
|
|
5
|
+
min: number;
|
|
6
|
+
max: number;
|
|
7
|
+
}
|
|
8
|
+
/** The y-range currently on screen; lets the sampler cap blow-ups and scale its tolerance. */
|
|
9
|
+
export interface YRange {
|
|
10
|
+
min: number;
|
|
11
|
+
max: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Sampled curve as two parallel typed arrays. A row whose `y` is `NaN` is a
|
|
15
|
+
* gap: the curve is not drawn between the rows on either side of it (a pole,
|
|
16
|
+
* a domain edge, a jump). Gaps are never first, last, or adjacent to another
|
|
17
|
+
* gap.
|
|
18
|
+
*/
|
|
19
|
+
export interface Samples {
|
|
20
|
+
x: Float64Array;
|
|
21
|
+
y: Float64Array;
|
|
22
|
+
}
|
|
23
|
+
/** Anything that looks like an `AbortSignal`; kept structural so the package needs no DOM or Node types. */
|
|
24
|
+
export interface CancelSignal {
|
|
25
|
+
readonly aborted: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface SampleOptions {
|
|
28
|
+
/**
|
|
29
|
+
* The y-range on screen. Samples far outside it (a pole's shoulders, which
|
|
30
|
+
* are finite but huge) are treated as gaps, and the curve is capped with an
|
|
31
|
+
* interpolated point at the edge of the range. Also sets the default
|
|
32
|
+
* adaptive tolerance, and (for `sampleAdaptive` only; `sampleUniform` does
|
|
33
|
+
* not detect jumps) enables jump detection.
|
|
34
|
+
*/
|
|
35
|
+
visibleYRange?: YRange;
|
|
36
|
+
/** Abort when `signal.aborted` becomes true. Checked every few hundred evaluations. */
|
|
37
|
+
signal?: CancelSignal;
|
|
38
|
+
/** Polled alongside `signal`; return true to abort (the generation-counter pattern: `() => gen !== latest`). */
|
|
39
|
+
isCancelled?: () => boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface AdaptiveOptions extends SampleOptions {
|
|
42
|
+
/** How many times one base-grid cell may be bisected. Default 4. */
|
|
43
|
+
maxDepth?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Bisect a cell while its midpoint sits further than this (in y units) from
|
|
46
|
+
* the straight line between its ends. Default: `1e-4` of the visible y span
|
|
47
|
+
* when `visibleYRange` is given, else `1e-3`.
|
|
48
|
+
*/
|
|
49
|
+
tolerance?: number;
|
|
50
|
+
/**
|
|
51
|
+
* A cell that is still this far apart at `maxDepth` (as a fraction of the
|
|
52
|
+
* visible y span) is a jump, not a steep slope, and becomes a gap. Default
|
|
53
|
+
* `0.5` when `visibleYRange` is given; with no range there is no scale to
|
|
54
|
+
* judge against and jumps are not detected. `Infinity` disables it. To tell
|
|
55
|
+
* a jump from a steep slope, the height must also persist (at least 80%)
|
|
56
|
+
* from the parent cell to the finest one. A function that oscillates faster
|
|
57
|
+
* than the grid can resolve can still look like a jump; raise the
|
|
58
|
+
* resolution or `maxDepth`.
|
|
59
|
+
*/
|
|
60
|
+
jumpFraction?: number;
|
|
61
|
+
}
|
|
62
|
+
/** Thrown by the samplers when `signal` or `isCancelled` reports cancellation. */
|
|
63
|
+
export declare class SampleCancelledError extends Error {
|
|
64
|
+
constructor();
|
|
65
|
+
}
|
|
66
|
+
/** Evaluate `f` at `resolution` evenly spaced points over `domain` (both ends included). */
|
|
67
|
+
export declare function sampleUniform(f: Fn1, domain: Domain, resolution: number, options?: SampleOptions): Samples;
|
|
68
|
+
/**
|
|
69
|
+
* Like {@link sampleUniform}, but bisects each cell of the base grid where the
|
|
70
|
+
* curve bends: a cell is split while its midpoint deviates from the straight
|
|
71
|
+
* line through its ends by more than the tolerance, up to `maxDepth` times. A
|
|
72
|
+
* straight stretch costs nothing extra; a sharp bend or narrow spike that a
|
|
73
|
+
* uniform grid would straddle is resolved. Cells that still jump at the
|
|
74
|
+
* deepest level become gaps (see `jumpFraction`).
|
|
75
|
+
*/
|
|
76
|
+
export declare function sampleAdaptive(f: Fn1, domain: Domain, baseResolution: number, options?: AdaptiveOptions): Samples;
|
|
77
|
+
/** Half-open row ranges `[start, end)` of consecutive data rows, i.e. the polylines to draw. */
|
|
78
|
+
export declare function runsOf(samples: Samples): [number, number][];
|
|
79
|
+
/**
|
|
80
|
+
* The two edges of every gap: the last point before it and the first after.
|
|
81
|
+
* Where the curve really breaks lies between them; the distance is at most one
|
|
82
|
+
* sampling cell, so mark both edges rather than guessing a position.
|
|
83
|
+
*/
|
|
84
|
+
export declare function findDiscontinuities(samples: Samples): {
|
|
85
|
+
before: Point;
|
|
86
|
+
after: Point;
|
|
87
|
+
}[];
|
|
88
|
+
export interface Point {
|
|
89
|
+
x: number;
|
|
90
|
+
y: number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Where the polyline crosses a boundary. `signedDistance(y)` is negative on
|
|
94
|
+
* one side and non-negative on the other, and linear in y (`y` for the x-axis,
|
|
95
|
+
* `y - c` for the line y = c), so zero can be interpolated between two rows.
|
|
96
|
+
* Pairs across a gap never count.
|
|
97
|
+
*/
|
|
98
|
+
export declare function findCrossings(samples: Samples, signedDistance: (y: number) => number): Point[];
|
|
99
|
+
export interface RootOptions extends SampleOptions {
|
|
100
|
+
/** Grid points scanned for sign changes. Default 1000. */
|
|
101
|
+
resolution?: number;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Roots of `f` on `domain`: scan a uniform grid for sign changes (or exact
|
|
105
|
+
* zeros) and polish each by bisection. Poles (sign change through infinity)
|
|
106
|
+
* are not roots and are rejected. Roots where `f` only touches zero without
|
|
107
|
+
* changing sign (such as x^2 at 0) fall between grid points and are missed.
|
|
108
|
+
*/
|
|
109
|
+
export declare function findRoots(f: Fn1, domain: Domain, options?: RootOptions): number[];
|
|
110
|
+
/** Where `f` and `g` meet on `domain`: the roots of `f - g`, each with `y = f(x)`. Same caveats as {@link findRoots}. */
|
|
111
|
+
export declare function findIntersections(f: Fn1, g: Fn1, domain: Domain, options?: RootOptions): Point[];
|
|
112
|
+
/** `[x, y]` pairs as plain arrays, with `y = null` at gaps (JSON-safe, unlike NaN). */
|
|
113
|
+
export declare function toPoints(samples: Samples): [number, number | null][];
|
|
114
|
+
//# sourceMappingURL=sample.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sample.d.ts","sourceRoot":"","sources":["../src/sample.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;AAExC,uCAAuC;AACvC,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,8FAA8F;AAC9F,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,CAAC,EAAE,YAAY,CAAC;IAChB,CAAC,EAAE,YAAY,CAAC;CACjB;AAED,4GAA4G;AAC5G,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uFAAuF;IACvF,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,gHAAgH;IAChH,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACpD,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,kFAAkF;AAClF,qBAAa,oBAAqB,SAAQ,KAAK;;CAK9C;AAsGD,4FAA4F;AAC5F,wBAAgB,aAAa,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAY9G;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CA6DrH;AAED,gGAAgG;AAChG,wBAAgB,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAa3D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG;IAAE,MAAM,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,EAAE,CAUvF;AAED,MAAM,WAAW,KAAK;IACpB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,KAAK,EAAE,CAe9F;AAED,MAAM,WAAW,WAAY,SAAQ,aAAa;IAChD,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,MAAM,EAAE,CAyCrF;AAED,yHAAyH;AACzH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,KAAK,EAAE,CAEpG;AAED,uFAAuF;AACvF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,EAAE,CAOpE"}
|
package/dist/sample.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/** Thrown by the samplers when `signal` or `isCancelled` reports cancellation. */
|
|
2
|
+
export class SampleCancelledError extends Error {
|
|
3
|
+
constructor() {
|
|
4
|
+
super("sampling cancelled");
|
|
5
|
+
this.name = "SampleCancelledError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
const CANCEL_CHECK_INTERVAL = 256;
|
|
9
|
+
const DEFAULT_MAX_DEPTH = 4;
|
|
10
|
+
const FALLBACK_TOLERANCE = 1e-3;
|
|
11
|
+
const RELATIVE_TOLERANCE = 1e-4;
|
|
12
|
+
const DEFAULT_JUMP_FRACTION = 0.5;
|
|
13
|
+
const JUMP_PERSISTENCE = 0.8;
|
|
14
|
+
function checkDomain(domain, resolution) {
|
|
15
|
+
if (!Number.isFinite(domain.min) || !Number.isFinite(domain.max) || !(domain.max > domain.min)) {
|
|
16
|
+
throw new RangeError(`domain must be finite with min < max, got [${domain.min}, ${domain.max}]`);
|
|
17
|
+
}
|
|
18
|
+
if (!Number.isInteger(resolution) || resolution < 2) {
|
|
19
|
+
throw new RangeError(`resolution must be an integer >= 2, got ${resolution}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function guardedEval(f, options) {
|
|
23
|
+
const { signal, isCancelled } = options;
|
|
24
|
+
if (!signal && !isCancelled)
|
|
25
|
+
return f;
|
|
26
|
+
let count = 0;
|
|
27
|
+
const poll = () => {
|
|
28
|
+
if (signal?.aborted || isCancelled?.())
|
|
29
|
+
throw new SampleCancelledError();
|
|
30
|
+
};
|
|
31
|
+
poll();
|
|
32
|
+
return (x) => {
|
|
33
|
+
if (++count % CANCEL_CHECK_INTERVAL === 0)
|
|
34
|
+
poll();
|
|
35
|
+
return f(x);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function isOffRange(y, range) {
|
|
39
|
+
if (!range)
|
|
40
|
+
return false;
|
|
41
|
+
const span = range.max - range.min;
|
|
42
|
+
return Math.abs(y) > Math.abs(range.min) + Math.abs(range.max) + 5 * span;
|
|
43
|
+
}
|
|
44
|
+
/** Where segment a-b crosses the nearer edge of `range`, inset 3% so end markers are not clipped. Null if undefined. */
|
|
45
|
+
function edgeCrossing(ax, ay, bx, by, range) {
|
|
46
|
+
if (!range || !Number.isFinite(ay) || !Number.isFinite(by) || ay === by)
|
|
47
|
+
return null;
|
|
48
|
+
const span = range.max - range.min;
|
|
49
|
+
const edge = by > ay ? range.max - 0.03 * span : range.min + 0.03 * span;
|
|
50
|
+
const t = (edge - ay) / (by - ay);
|
|
51
|
+
if (!Number.isFinite(t) || t < 0 || t > 1)
|
|
52
|
+
return null;
|
|
53
|
+
return [ax + t * (bx - ax), edge];
|
|
54
|
+
}
|
|
55
|
+
/** Turn raw points (NaN / Infinity / off-range = invalid) into the gap-marked `Samples` form. */
|
|
56
|
+
function toSamples(rx, ry, range) {
|
|
57
|
+
const ox = [];
|
|
58
|
+
const oy = [];
|
|
59
|
+
let prevValid = false;
|
|
60
|
+
let px = 0;
|
|
61
|
+
let py = 0;
|
|
62
|
+
let havePrev = false;
|
|
63
|
+
for (let i = 0; i < rx.length; i++) {
|
|
64
|
+
const x = rx[i];
|
|
65
|
+
const y = ry[i];
|
|
66
|
+
const finite = Number.isFinite(y);
|
|
67
|
+
const valid = finite && !isOffRange(y, range);
|
|
68
|
+
if (valid) {
|
|
69
|
+
if (!prevValid && havePrev) {
|
|
70
|
+
const e = edgeCrossing(px, py, x, y, range);
|
|
71
|
+
if (e) {
|
|
72
|
+
ox.push(e[0]);
|
|
73
|
+
oy.push(e[1]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
ox.push(x);
|
|
77
|
+
oy.push(y);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
if (oy.length > 0 && !Number.isNaN(oy[oy.length - 1])) {
|
|
81
|
+
if (prevValid && finite) {
|
|
82
|
+
const e = edgeCrossing(px, py, x, y, range);
|
|
83
|
+
if (e) {
|
|
84
|
+
ox.push(e[0]);
|
|
85
|
+
oy.push(e[1]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
ox.push(x);
|
|
89
|
+
oy.push(Number.NaN);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
px = x;
|
|
93
|
+
py = y;
|
|
94
|
+
prevValid = valid;
|
|
95
|
+
havePrev = true;
|
|
96
|
+
}
|
|
97
|
+
if (oy.length > 0 && Number.isNaN(oy[oy.length - 1])) {
|
|
98
|
+
ox.pop();
|
|
99
|
+
oy.pop();
|
|
100
|
+
}
|
|
101
|
+
return { x: Float64Array.from(ox), y: Float64Array.from(oy) };
|
|
102
|
+
}
|
|
103
|
+
/** Evaluate `f` at `resolution` evenly spaced points over `domain` (both ends included). */
|
|
104
|
+
export function sampleUniform(f, domain, resolution, options = {}) {
|
|
105
|
+
checkDomain(domain, resolution);
|
|
106
|
+
const g = guardedEval(f, options);
|
|
107
|
+
const rx = new Array(resolution);
|
|
108
|
+
const ry = new Array(resolution);
|
|
109
|
+
const span = domain.max - domain.min;
|
|
110
|
+
for (let i = 0; i < resolution; i++) {
|
|
111
|
+
const x = i === resolution - 1 ? domain.max : domain.min + (i / (resolution - 1)) * span;
|
|
112
|
+
rx[i] = x;
|
|
113
|
+
ry[i] = g(x);
|
|
114
|
+
}
|
|
115
|
+
return toSamples(rx, ry, options.visibleYRange);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Like {@link sampleUniform}, but bisects each cell of the base grid where the
|
|
119
|
+
* curve bends: a cell is split while its midpoint deviates from the straight
|
|
120
|
+
* line through its ends by more than the tolerance, up to `maxDepth` times. A
|
|
121
|
+
* straight stretch costs nothing extra; a sharp bend or narrow spike that a
|
|
122
|
+
* uniform grid would straddle is resolved. Cells that still jump at the
|
|
123
|
+
* deepest level become gaps (see `jumpFraction`).
|
|
124
|
+
*/
|
|
125
|
+
export function sampleAdaptive(f, domain, baseResolution, options = {}) {
|
|
126
|
+
checkDomain(domain, baseResolution);
|
|
127
|
+
const range = options.visibleYRange;
|
|
128
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
129
|
+
const tolerance = options.tolerance ?? (range ? (range.max - range.min) * RELATIVE_TOLERANCE : FALLBACK_TOLERANCE);
|
|
130
|
+
const jumpFraction = options.jumpFraction ?? (range ? DEFAULT_JUMP_FRACTION : Number.POSITIVE_INFINITY);
|
|
131
|
+
const jumpAbs = range ? jumpFraction * (range.max - range.min) : Number.POSITIVE_INFINITY;
|
|
132
|
+
const g = guardedEval(f, options);
|
|
133
|
+
const rx = [];
|
|
134
|
+
const ry = [];
|
|
135
|
+
// Pushes `a` and the refinement points strictly between a and b (not b).
|
|
136
|
+
const refine = (ax, ay, bx, by, depth, parentDy) => {
|
|
137
|
+
if (!Number.isFinite(ay) || !Number.isFinite(by) || isOffRange(ay, range) || isOffRange(by, range)) {
|
|
138
|
+
rx.push(ax);
|
|
139
|
+
ry.push(ay);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (depth >= maxDepth) {
|
|
143
|
+
rx.push(ax);
|
|
144
|
+
ry.push(ay);
|
|
145
|
+
const dy = Math.abs(by - ay);
|
|
146
|
+
// A jump keeps its height as the cell halves; a steep slope loses about half of it per halving.
|
|
147
|
+
if (dy > jumpAbs && dy >= JUMP_PERSISTENCE * parentDy) {
|
|
148
|
+
rx.push((ax + bx) / 2);
|
|
149
|
+
ry.push(Number.NaN);
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const mx = (ax + bx) / 2;
|
|
154
|
+
const my = g(mx);
|
|
155
|
+
// A non-finite midpoint is a singularity between a and b: record it as the gap it is.
|
|
156
|
+
if (!Number.isFinite(my)) {
|
|
157
|
+
rx.push(ax, mx);
|
|
158
|
+
ry.push(ay, my);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (Math.abs(my - (ay + by) / 2) <= tolerance) {
|
|
162
|
+
rx.push(ax);
|
|
163
|
+
ry.push(ay);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const dy = Math.abs(by - ay);
|
|
167
|
+
refine(ax, ay, mx, my, depth + 1, dy);
|
|
168
|
+
refine(mx, my, bx, by, depth + 1, dy);
|
|
169
|
+
};
|
|
170
|
+
const span = domain.max - domain.min;
|
|
171
|
+
let lx = domain.min;
|
|
172
|
+
let ly = g(lx);
|
|
173
|
+
for (let i = 1; i < baseResolution; i++) {
|
|
174
|
+
const x = i === baseResolution - 1 ? domain.max : domain.min + (i / (baseResolution - 1)) * span;
|
|
175
|
+
const y = g(x);
|
|
176
|
+
refine(lx, ly, x, y, 0, Number.POSITIVE_INFINITY);
|
|
177
|
+
lx = x;
|
|
178
|
+
ly = y;
|
|
179
|
+
}
|
|
180
|
+
rx.push(lx);
|
|
181
|
+
ry.push(ly);
|
|
182
|
+
return toSamples(rx, ry, range);
|
|
183
|
+
}
|
|
184
|
+
/** Half-open row ranges `[start, end)` of consecutive data rows, i.e. the polylines to draw. */
|
|
185
|
+
export function runsOf(samples) {
|
|
186
|
+
const runs = [];
|
|
187
|
+
let start = -1;
|
|
188
|
+
for (let i = 0; i < samples.y.length; i++) {
|
|
189
|
+
if (Number.isNaN(samples.y[i])) {
|
|
190
|
+
if (start >= 0)
|
|
191
|
+
runs.push([start, i]);
|
|
192
|
+
start = -1;
|
|
193
|
+
}
|
|
194
|
+
else if (start < 0) {
|
|
195
|
+
start = i;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (start >= 0)
|
|
199
|
+
runs.push([start, samples.y.length]);
|
|
200
|
+
return runs;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The two edges of every gap: the last point before it and the first after.
|
|
204
|
+
* Where the curve really breaks lies between them; the distance is at most one
|
|
205
|
+
* sampling cell, so mark both edges rather than guessing a position.
|
|
206
|
+
*/
|
|
207
|
+
export function findDiscontinuities(samples) {
|
|
208
|
+
const out = [];
|
|
209
|
+
for (let i = 1; i < samples.y.length - 1; i++) {
|
|
210
|
+
if (!Number.isNaN(samples.y[i]))
|
|
211
|
+
continue;
|
|
212
|
+
out.push({
|
|
213
|
+
before: { x: samples.x[i - 1], y: samples.y[i - 1] },
|
|
214
|
+
after: { x: samples.x[i + 1], y: samples.y[i + 1] },
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Where the polyline crosses a boundary. `signedDistance(y)` is negative on
|
|
221
|
+
* one side and non-negative on the other, and linear in y (`y` for the x-axis,
|
|
222
|
+
* `y - c` for the line y = c), so zero can be interpolated between two rows.
|
|
223
|
+
* Pairs across a gap never count.
|
|
224
|
+
*/
|
|
225
|
+
export function findCrossings(samples, signedDistance) {
|
|
226
|
+
const out = [];
|
|
227
|
+
for (const [start, end] of runsOf(samples)) {
|
|
228
|
+
for (let i = start + 1; i < end; i++) {
|
|
229
|
+
const d0 = signedDistance(samples.y[i - 1]);
|
|
230
|
+
const d1 = signedDistance(samples.y[i]);
|
|
231
|
+
if (d0 >= 0 === d1 >= 0)
|
|
232
|
+
continue;
|
|
233
|
+
const t = d0 / (d0 - d1);
|
|
234
|
+
out.push({
|
|
235
|
+
x: samples.x[i - 1] + t * (samples.x[i] - samples.x[i - 1]),
|
|
236
|
+
y: samples.y[i - 1] + t * (samples.y[i] - samples.y[i - 1]),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Roots of `f` on `domain`: scan a uniform grid for sign changes (or exact
|
|
244
|
+
* zeros) and polish each by bisection. Poles (sign change through infinity)
|
|
245
|
+
* are not roots and are rejected. Roots where `f` only touches zero without
|
|
246
|
+
* changing sign (such as x^2 at 0) fall between grid points and are missed.
|
|
247
|
+
*/
|
|
248
|
+
export function findRoots(f, domain, options = {}) {
|
|
249
|
+
const n = options.resolution ?? 1000;
|
|
250
|
+
checkDomain(domain, n);
|
|
251
|
+
const g = guardedEval(f, options);
|
|
252
|
+
const span = domain.max - domain.min;
|
|
253
|
+
const roots = [];
|
|
254
|
+
let ax = domain.min;
|
|
255
|
+
let ay = g(ax);
|
|
256
|
+
if (ay === 0)
|
|
257
|
+
roots.push(ax);
|
|
258
|
+
for (let i = 1; i < n; i++) {
|
|
259
|
+
const bx = i === n - 1 ? domain.max : domain.min + (i / (n - 1)) * span;
|
|
260
|
+
const by = g(bx);
|
|
261
|
+
if (by === 0) {
|
|
262
|
+
roots.push(bx);
|
|
263
|
+
}
|
|
264
|
+
else if (Number.isFinite(ay) && Number.isFinite(by) && ay !== 0 && ay < 0 !== by < 0) {
|
|
265
|
+
let lo = ax;
|
|
266
|
+
let hi = bx;
|
|
267
|
+
let flo = ay;
|
|
268
|
+
for (let k = 0; k < 100; k++) {
|
|
269
|
+
const mid = (lo + hi) / 2;
|
|
270
|
+
if (mid === lo || mid === hi)
|
|
271
|
+
break;
|
|
272
|
+
const fm = g(mid);
|
|
273
|
+
if (fm === 0) {
|
|
274
|
+
lo = hi = mid;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
if (flo < 0 === fm < 0) {
|
|
278
|
+
lo = mid;
|
|
279
|
+
flo = fm;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
hi = mid;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const r = (lo + hi) / 2;
|
|
286
|
+
const fr = g(r);
|
|
287
|
+
if (Math.abs(fr) <= Math.max(Math.abs(ay), Math.abs(by)))
|
|
288
|
+
roots.push(r);
|
|
289
|
+
}
|
|
290
|
+
ax = bx;
|
|
291
|
+
ay = by;
|
|
292
|
+
}
|
|
293
|
+
return roots;
|
|
294
|
+
}
|
|
295
|
+
/** Where `f` and `g` meet on `domain`: the roots of `f - g`, each with `y = f(x)`. Same caveats as {@link findRoots}. */
|
|
296
|
+
export function findIntersections(f, g, domain, options = {}) {
|
|
297
|
+
return findRoots((x) => f(x) - g(x), domain, options).map((x) => ({ x, y: f(x) }));
|
|
298
|
+
}
|
|
299
|
+
/** `[x, y]` pairs as plain arrays, with `y = null` at gaps (JSON-safe, unlike NaN). */
|
|
300
|
+
export function toPoints(samples) {
|
|
301
|
+
const out = new Array(samples.x.length);
|
|
302
|
+
for (let i = 0; i < out.length; i++) {
|
|
303
|
+
const y = samples.y[i];
|
|
304
|
+
out[i] = [samples.x[i], Number.isNaN(y) ? null : y];
|
|
305
|
+
}
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
//# sourceMappingURL=sample.js.map
|