packages/core/src/features/viz/lib/monotone-curve.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
1 import {
2 createSeriesCurve,
3 type SeriesCurve,
4 type SeriesCurvePoint,
5 } from "./curve-definition.js";
6
7 function endpointSlope(
8 h0: number,
9 h1: number,
10 first: number,
11 second: number,
12 ): number {
13 let slope = ((2 * h0 + h1) * first - h0 * second) / (h0 + h1);
14 if (Math.sign(slope) !== Math.sign(first)) return 0;
15 if (
16 Math.sign(first) !== Math.sign(second) &&
17 Math.abs(slope) > Math.abs(3 * first)
18 )
19 slope = 3 * first;
20 return slope;
21 }
22
23 function monotoneSegments(points: readonly SeriesCurvePoint[]) {
24 if (points.length < 2) return [];
25 const direction = points[1][0] > points[0][0] ? 1 : -1;
26 const x = points.map((point) => direction * point[0]);
27 const h: number[] = [];
28 const delta: number[] = [];
29 for (let i = 0; i < points.length - 1; i++) {
30 const width = x[i + 1] - x[i];
31 if (!Number.isFinite(width) || width <= 0)
32 throw new RangeError(
33 "monotoneX requires strictly ascending or descending finite x coordinates.",
34 );
35 h.push(width);
36 delta.push((points[i + 1][1] - points[i][1]) / width);
37 }
38 const slopes = new Array<number>(points.length);
39 if (points.length === 2) slopes[0] = slopes[1] = delta[0];
40 else {
41 slopes[0] = endpointSlope(h[0], h[1], delta[0], delta[1]);
42 slopes[points.length - 1] = endpointSlope(
43 h.at(-1)!,
44 h.at(-2)!,
45 delta.at(-1)!,
46 delta.at(-2)!,
47 );
48 for (let i = 1; i < points.length - 1; i++) {
49 const previous = delta[i - 1],
50 next = delta[i];
51 slopes[i] =
52 previous === 0 || next === 0 || Math.sign(previous) !== Math.sign(next)
53 ? 0
54 : (3 * (h[i - 1] + h[i])) /
55 ((2 * h[i] + h[i - 1]) / previous + (h[i] + 2 * h[i - 1]) / next);
56 }
57 }
58 return points.slice(1).map((end, index) => {
59 const start = points[index],
60 width = h[index];
61 const control1: SeriesCurvePoint = [
62 start[0] + (end[0] - start[0]) / 3,
63 start[1] + (slopes[index] * width) / 3,
64 ];
65 const control2: SeriesCurvePoint = [
66 end[0] - (end[0] - start[0]) / 3,
67 end[1] - (slopes[index + 1] * width) / 3,
68 ];
69 if (![...control1, ...control2, ...end].every(Number.isFinite))
70 throw new RangeError("monotoneX generated non-finite cubic controls.");
71 return Object.freeze({ end, control1, control2 });
72 });
73 }
74
75 const monotone = /* @__PURE__ */ createSeriesCurve(monotoneSegments);
76
77 /**
78 * Returns the immutable monotone-X cubic Hermite definition.
79 *
80 * It uses weighted harmonic interior slopes and limited one-sided endpoint slopes, preserving every observation,
81 * flat segment, and local extremum without sorting data. Each run must have strictly ascending
82 * or strictly descending x coordinates.
83 *
84 * @returns The named {@link SeriesCurve} definition.
85 */
86 export function monotoneX(): SeriesCurve {
87 return monotone;
88 }
89
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.