Skip to content

packages/core/src/features/viz/lib/financial-autoscale.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 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 built with @pibbl/core 0.0.2, revision dd39ce2. ALPHA — NOT FOR PRODUCTION USE.