packages/core/src/features/viz/lib/financial-autoscale.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
1 import type { LineAccessor, LinearDomain } from "./types.js";
2 import type { CandlestickSeries } from "./candlestick-series.js";
3 import type { LinearScale } from "./linear-scale-component.js";
4 import { finiteCoordinate, pointAccessor, pointData } from "./point-data.js";
5
6 /** Inputs that identify complete OHLC candle values. See {@link visibleOhlcPriceExtent}. */
7 export interface VisibleOhlcPriceExtentOptions<D> {
8 /** UTC-millisecond property or callback. */
9 readonly time: LineAccessor<D>;
10 /** Opening-price property or callback. */
11 readonly open: LineAccessor<D>;
12 /** High-price property or callback. */
13 readonly high: LineAccessor<D>;
14 /** Low-price property or callback. */
15 readonly low: LineAccessor<D>;
16 /** Closing-price property or callback. */
17 readonly close: LineAccessor<D>;
18 /**
19 * Maps an exact timestamp into the coordinate system used by visibleRange.
20 * @param time - Finite UTC-millisecond timestamp from the current source row.
21 * @returns A finite position, or undefined to exclude the row.
22 * @see ResolvedTradingTimeScale
23 */
24 readonly positionAtTime: (time: number) => number | undefined;
25 /** Two finite endpoints in the positionAtTime coordinate system; either order is accepted. */
26 readonly visibleRange: LinearDomain;
27 /**
28 * Excludes an original row before any accessor runs.
29 * @param datum - Original source row.
30 * @param index - Source index.
31 * @param data - Complete original source array.
32 * @returns Whether this row participates.
33 */
34 readonly defined?: (datum: D, index: number, data: readonly D[]) => boolean;
35 }
36
37 /** Options for expanding a continuous numeric domain. See {@link padLinearDomain}. */
38 export interface PadLinearDomainOptions {
39 /** Fraction of the effective span added to each side; defaults to 0.1. */
40 readonly padding?: number;
41 /** Positive span floor before padding. Flat domains use a deterministic fallback when omitted. */
42 readonly minimumSpan?: number;
43 }
44
45 function pair(value: readonly number[], name: string): LinearDomain {
46 if (!Array.isArray(value) || value.length !== 2 || !value.every(Number.isFinite))
47 throw new RangeError(`${name} requires two finite endpoints.`);
48 return Object.freeze([value[0]!, value[1]!]);
49 }
50
51 /**
52 * Finds the ascending low/high extent of complete OHLC rows whose mapped center is visible. See
53 * {@link CandlestickSeries}.
54 *
55 * The supplied position function keeps elapsed-time and fractional trading-slot visibility
56 * explicit. It does not sort or mutate data, interpolate timestamps, or use painted pixels.
57 * @param data - Original OHLC records in application-owned source order.
58 * @param options - Named accessors and one caller-defined visibility coordinate system.
59 * @returns Frozen ascending low/high endpoints, or undefined when no complete visible candle exists.
60 * @see CandlestickSeries
61 */
62 export function visibleOhlcPriceExtent<D>(
63 data: readonly D[],
64 options: VisibleOhlcPriceExtentOptions<D>,
65 ): LinearDomain | undefined {
66 const rows = pointData(data, "visibleOhlcPriceExtent");
67 if (typeof options.positionAtTime !== "function")
68 throw new TypeError("visibleOhlcPriceExtent positionAtTime must be a function.");
69 const visibleRange = pair(options.visibleRange, "visibleOhlcPriceExtent visibleRange");
70 const minimum = Math.min(...visibleRange), maximum = Math.max(...visibleRange);
71 const time = pointAccessor(options.time), open = pointAccessor(options.open),
72 high = pointAccessor(options.high), low = pointAccessor(options.low), close = pointAccessor(options.close);
73 let minimumPrice = Infinity, maximumPrice = -Infinity;
74 for (let index = 0; index < rows.length; index++) {
75 const datum = rows[index]!;
76 if (options.defined && !options.defined(datum, index, rows)) continue;
77 const timestamp = time(datum, index, rows);
78 if (!finiteCoordinate(timestamp)) continue;
79 const position = options.positionAtTime(timestamp);
80 if (position === undefined) continue;
81 if (!finiteCoordinate(position))
82 throw new RangeError("visibleOhlcPriceExtent positionAtTime must return a finite number or undefined.");
83 if (position < minimum || position > maximum) continue;
84 const opening = open(datum, index, rows), highest = high(datum, index, rows),
85 lowest = low(datum, index, rows), closing = close(datum, index, rows);
86 if (!finiteCoordinate(opening) || !finiteCoordinate(highest) ||
87 !finiteCoordinate(lowest) || !finiteCoordinate(closing)) continue;
88 if (lowest > Math.min(opening, closing) || highest < Math.max(opening, closing))
89 throw new RangeError(
90 `visibleOhlcPriceExtent: malformed OHLC at source index ${index}; require low <= open/close <= high.`,
91 );
92 minimumPrice = Math.min(minimumPrice, lowest);
93 maximumPrice = Math.max(maximumPrice, highest);
94 }
95 return minimumPrice === Infinity
96 ? undefined
97 : Object.freeze([minimumPrice, maximumPrice]);
98 }
99
100 /**
101 * Expands a finite numeric domain by equal proportional padding on both sides. See
102 * {@link LinearScale}.
103 *
104 * Flat domains first receive minimumSpan or max(abs(value) * 0.02, 1), then padding.
105 * Input direction is preserved and output endpoints are frozen.
106 * @param domain - Two finite numeric endpoints, including equal values.
107 * @param options - Padding fraction and optional positive effective-span floor.
108 * @returns A distinct finite padded domain in the input direction.
109 * @see LinearScale
110 */
111 export function padLinearDomain(
112 domain: LinearDomain,
113 options: PadLinearDomainOptions = {},
114 ): LinearDomain {
115 const input = pair(domain, "padLinearDomain domain");
116 const padding = options.padding ?? 0.1;
117 if (typeof padding !== "number" || !Number.isFinite(padding) || padding < 0)
118 throw new RangeError("padLinearDomain padding must be finite and nonnegative.");
119 const suppliedMinimum = options.minimumSpan;
120 if (suppliedMinimum !== undefined &&
121 (typeof suppliedMinimum !== "number" || !Number.isFinite(suppliedMinimum) || suppliedMinimum <= 0))
122 throw new RangeError("padLinearDomain minimumSpan must be finite and positive.");
123 const ascending = input[0] <= input[1];
124 const lower = Math.min(...input), upper = Math.max(...input);
125 const rawSpan = upper - lower;
126 let span = rawSpan;
127 if (rawSpan === 0) span = suppliedMinimum ?? Math.max(Math.abs(lower) * 0.02, 1);
128 else if (suppliedMinimum !== undefined) span = Math.max(rawSpan, suppliedMinimum);
129 const midpoint = lower + rawSpan / 2;
130 const half = span / 2 + span * padding;
131 const start = midpoint - half, end = midpoint + half;
132 if (!Number.isFinite(start) || !Number.isFinite(end) || start === end)
133 throw new RangeError("padLinearDomain result must have distinct finite endpoints.");
134 return Object.freeze(ascending ? [start, end] : [end, start]);
135 }
136
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision dd39ce2. ALPHA — NOT FOR PRODUCTION USE.