Skip to content

packages/core/src/features/viz/lib/monotone-curve.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

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 built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.