wick-charts 0.3.0

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +545 -0
  3. package/dist/axis.d.ts +21 -0
  4. package/dist/axis.js +44 -0
  5. package/dist/dataSource.d.ts +24 -0
  6. package/dist/dataSource.js +1 -0
  7. package/dist/hitTest.d.ts +31 -0
  8. package/dist/hitTest.js +46 -0
  9. package/dist/hybridScale.d.ts +25 -0
  10. package/dist/hybridScale.js +41 -0
  11. package/dist/index.d.ts +225 -0
  12. package/dist/index.js +715 -0
  13. package/dist/mergeSeries.d.ts +14 -0
  14. package/dist/mergeSeries.js +21 -0
  15. package/dist/plugins/types.d.ts +140 -0
  16. package/dist/plugins/types.js +1 -0
  17. package/dist/priceAxis.d.ts +7 -0
  18. package/dist/priceAxis.js +49 -0
  19. package/dist/priceRange.d.ts +12 -0
  20. package/dist/priceRange.js +16 -0
  21. package/dist/renderer.d.ts +79 -0
  22. package/dist/renderer.js +318 -0
  23. package/dist/scale.d.ts +20 -0
  24. package/dist/scale.js +29 -0
  25. package/dist/series/candlestick.d.ts +20 -0
  26. package/dist/series/candlestick.js +88 -0
  27. package/dist/series/registry.d.ts +13 -0
  28. package/dist/series/registry.js +30 -0
  29. package/dist/series/types.d.ts +56 -0
  30. package/dist/series/types.js +1 -0
  31. package/dist/testHelpers.d.ts +38 -0
  32. package/dist/testHelpers.js +50 -0
  33. package/dist/time.d.ts +6 -0
  34. package/dist/time.js +58 -0
  35. package/dist/types.d.ts +151 -0
  36. package/dist/types.js +1 -0
  37. package/dist/viewport.d.ts +52 -0
  38. package/dist/viewport.js +87 -0
  39. package/dist/wasm.d.ts +29 -0
  40. package/dist/wasm.js +35 -0
  41. package/dist/wasmImporter.d.ts +6 -0
  42. package/dist/wasmImporter.js +7 -0
  43. package/package.json +39 -0
  44. package/wasm-pkg/package.json +21 -0
  45. package/wasm-pkg/wickchart_core.d.ts +59 -0
  46. package/wasm-pkg/wickchart_core.js +227 -0
  47. package/wasm-pkg/wickchart_core_bg.wasm +0 -0
  48. package/wasm-pkg/wickchart_core_bg.wasm.d.ts +11 -0
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Perpendicular distance, in px, from `(px, py)` to the line segment
3
+ * `(x1, y1)`-`(x2, y2)` — clamped to the segment itself, so a point beyond
4
+ * either endpoint measures against that endpoint rather than the infinite
5
+ * line the segment sits on.
6
+ *
7
+ * Every interactive plugin that draws a line-shaped thing (a trend line, a
8
+ * ray, an edge of a rectangle) eventually needs to answer "did the user
9
+ * click on/near the thing I already placed" so it can be selected, dragged,
10
+ * or deleted — and the endpoint-clamping is easy to get subtly wrong when
11
+ * re-derived per plugin, so it lives here once instead.
12
+ */
13
+ export declare function distanceToSegment(px: number, py: number, x1: number, y1: number, x2: number, y2: number): number;
14
+ /**
15
+ * `true` if `(px, py)` is within `tolerancePx` of the segment
16
+ * `(x1, y1)`-`(x2, y2)`. Defaults to 6px — comfortably clickable with a
17
+ * mouse pointer, still forgiving enough for an imprecise fingertip on
18
+ * touch. Call it from `onPointerDown`/`onPointerMove` with the event's
19
+ * `x`/`y` and the shape's own endpoints converted to pixels via the
20
+ * event's `xForIndex`/`yForValue` (a shape stored in data space survives
21
+ * pan/zoom; converting it to pixels at hit-test time, rather than storing
22
+ * pixels directly, is what keeps a placed line lined up with the candles
23
+ * it was drawn against after the user scrolls or zooms).
24
+ */
25
+ export declare function hitTestSegment(px: number, py: number, x1: number, y1: number, x2: number, y2: number, tolerancePx?: number): boolean;
26
+ /**
27
+ * `true` if `(px, py)` is within `tolerancePx` of the point `(x, y)` — for
28
+ * hit-testing a marker, a drag handle, or a single endpoint of a shape
29
+ * rather than an edge.
30
+ */
31
+ export declare function hitTestPoint(px: number, py: number, x: number, y: number, tolerancePx?: number): boolean;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Perpendicular distance, in px, from `(px, py)` to the line segment
3
+ * `(x1, y1)`-`(x2, y2)` — clamped to the segment itself, so a point beyond
4
+ * either endpoint measures against that endpoint rather than the infinite
5
+ * line the segment sits on.
6
+ *
7
+ * Every interactive plugin that draws a line-shaped thing (a trend line, a
8
+ * ray, an edge of a rectangle) eventually needs to answer "did the user
9
+ * click on/near the thing I already placed" so it can be selected, dragged,
10
+ * or deleted — and the endpoint-clamping is easy to get subtly wrong when
11
+ * re-derived per plugin, so it lives here once instead.
12
+ */
13
+ export function distanceToSegment(px, py, x1, y1, x2, y2) {
14
+ const dx = x2 - x1;
15
+ const dy = y2 - y1;
16
+ const lengthSquared = dx * dx + dy * dy;
17
+ if (lengthSquared === 0)
18
+ return Math.hypot(px - x1, py - y1); // a zero-length "segment" is just a point
19
+ let t = ((px - x1) * dx + (py - y1) * dy) / lengthSquared;
20
+ t = Math.max(0, Math.min(1, t));
21
+ const closestX = x1 + t * dx;
22
+ const closestY = y1 + t * dy;
23
+ return Math.hypot(px - closestX, py - closestY);
24
+ }
25
+ /**
26
+ * `true` if `(px, py)` is within `tolerancePx` of the segment
27
+ * `(x1, y1)`-`(x2, y2)`. Defaults to 6px — comfortably clickable with a
28
+ * mouse pointer, still forgiving enough for an imprecise fingertip on
29
+ * touch. Call it from `onPointerDown`/`onPointerMove` with the event's
30
+ * `x`/`y` and the shape's own endpoints converted to pixels via the
31
+ * event's `xForIndex`/`yForValue` (a shape stored in data space survives
32
+ * pan/zoom; converting it to pixels at hit-test time, rather than storing
33
+ * pixels directly, is what keeps a placed line lined up with the candles
34
+ * it was drawn against after the user scrolls or zooms).
35
+ */
36
+ export function hitTestSegment(px, py, x1, y1, x2, y2, tolerancePx = 6) {
37
+ return distanceToSegment(px, py, x1, y1, x2, y2) <= tolerancePx;
38
+ }
39
+ /**
40
+ * `true` if `(px, py)` is within `tolerancePx` of the point `(x, y)` — for
41
+ * hit-testing a marker, a drag handle, or a single endpoint of a shape
42
+ * rather than an edge.
43
+ */
44
+ export function hitTestPoint(px, py, x, y, tolerancePx = 6) {
45
+ return Math.hypot(px - x, py - y) <= tolerancePx;
46
+ }
@@ -0,0 +1,25 @@
1
+ import type { WasmModule } from './wasm.js';
2
+ /** Below this many points, a `LinearScale.mapMany` call is already just a
3
+ * handful of multiplications — crossing into WASM would spend more time on
4
+ * the JS↔WASM boundary than the JS version spends on the whole computation. */
5
+ export declare const WASM_SCALE_THRESHOLD = 500;
6
+ export interface Scale {
7
+ map(value: number): number;
8
+ mapMany(values: number[]): number[];
9
+ }
10
+ export interface DisposableScale {
11
+ scale: Scale;
12
+ dispose(): void;
13
+ }
14
+ /**
15
+ * Picks JS or WASM for a domain→range mapping based on how many points
16
+ * will go through it. `wasmModule` defaults to whatever `loadWasm` (see
17
+ * wasm.ts) has cached so far — pass it explicitly in tests instead of
18
+ * depending on that module-level cache.
19
+ *
20
+ * Always returns a `dispose()` — a no-op for the JS path, a real WASM
21
+ * memory free for the WASM path — so call sites can treat both uniformly
22
+ * (`try { ... } finally { result.dispose() }`) without branching on which
23
+ * one they got.
24
+ */
25
+ export declare function createScale(domainMin: number, domainMax: number, rangeMin: number, rangeMax: number, pointCount: number, wasmModule?: WasmModule | null): DisposableScale;
@@ -0,0 +1,41 @@
1
+ import { LinearScale } from './scale.js';
2
+ import { getCachedWasmModule } from './wasm.js';
3
+ /** Below this many points, a `LinearScale.mapMany` call is already just a
4
+ * handful of multiplications — crossing into WASM would spend more time on
5
+ * the JS↔WASM boundary than the JS version spends on the whole computation. */
6
+ export const WASM_SCALE_THRESHOLD = 500;
7
+ /** A `Scale` backed by a `wasm-bindgen`-generated instance, freeing its
8
+ * WASM-side memory via `dispose()` — callers must call this once done
9
+ * (the renderer does so in a `finally`) since nothing else will. */
10
+ class WasmBackedScale {
11
+ constructor(wasm, domainMin, domainMax, rangeMin, rangeMax) {
12
+ this.inner = new wasm.Scale(domainMin, domainMax, rangeMin, rangeMax);
13
+ }
14
+ map(value) {
15
+ return this.inner.map(value);
16
+ }
17
+ mapMany(values) {
18
+ return Array.from(this.inner.map_many(Float64Array.from(values)));
19
+ }
20
+ dispose() {
21
+ this.inner.free();
22
+ }
23
+ }
24
+ /**
25
+ * Picks JS or WASM for a domain→range mapping based on how many points
26
+ * will go through it. `wasmModule` defaults to whatever `loadWasm` (see
27
+ * wasm.ts) has cached so far — pass it explicitly in tests instead of
28
+ * depending on that module-level cache.
29
+ *
30
+ * Always returns a `dispose()` — a no-op for the JS path, a real WASM
31
+ * memory free for the WASM path — so call sites can treat both uniformly
32
+ * (`try { ... } finally { result.dispose() }`) without branching on which
33
+ * one they got.
34
+ */
35
+ export function createScale(domainMin, domainMax, rangeMin, rangeMax, pointCount, wasmModule = getCachedWasmModule()) {
36
+ if (wasmModule && pointCount >= WASM_SCALE_THRESHOLD) {
37
+ const scale = new WasmBackedScale(wasmModule, domainMin, domainMax, rangeMin, rangeMax);
38
+ return { scale, dispose: () => scale.dispose() };
39
+ }
40
+ return { scale: new LinearScale(domainMin, domainMax, rangeMin, rangeMax), dispose: () => { } };
41
+ }
@@ -0,0 +1,225 @@
1
+ import type { DataLoader } from './dataSource.js';
2
+ import type { ChartPlugin } from './plugins/types.js';
3
+ import type { CandlestickStyle } from './series/candlestick.js';
4
+ import type { Candle, WickChartOptions, SeriesPoint, ValueRange } from './types.js';
5
+ export type { BusinessDay, Candle, WickChartOptions, WickTime, SeriesPoint, UnixMillis, ValueRange } from './types.js';
6
+ export type { DataLoader, DataRequest } from './dataSource.js';
7
+ export type { ChartPlugin, ChartPointerEvent, PluginRenderApi } from './plugins/types.js';
8
+ export { distanceToSegment, hitTestPoint, hitTestSegment } from './hitTest.js';
9
+ export type { Scale } from './hybridScale.js';
10
+ export { mergeSeriesPoints } from './mergeSeries.js';
11
+ export { registerSeries, getSeries } from './series/registry.js';
12
+ export type { SeriesDefinition, SeriesDrawContext } from './series/types.js';
13
+ export type { CandlestickStyle } from './series/candlestick.js';
14
+ export { candlestickSeries } from './series/candlestick.js';
15
+ export { LinearScale } from './scale.js';
16
+ export { toUnixSeconds } from './time.js';
17
+ export { Viewport } from './viewport.js';
18
+ export { getCachedWasmModule, loadWasm } from './wasm.js';
19
+ /**
20
+ * Interactive chart: drag to pan, wheel to zoom, drag the price-axis strip
21
+ * to rescale it, hover a point for a legend. What gets plotted (candles
22
+ * today; a future line/area/bar series) is decided entirely by
23
+ * `options.type` and the `SeriesDefinition` registered under it — this
24
+ * class only owns generic engine concerns (viewport math, mouse/touch/wheel
25
+ * handling, on-demand data loading, render scheduling) and never touches a
26
+ * point's fields directly. Construct once per canvas; call `destroy()`
27
+ * when done with it (unmount) to remove the window-level mouseup listener.
28
+ */
29
+ export declare class WickChart<TPoint extends SeriesPoint = Candle> {
30
+ private canvas;
31
+ private renderer;
32
+ private seriesDefinition;
33
+ private sorted;
34
+ private times;
35
+ private viewport;
36
+ private hoverIndex;
37
+ /** Device-pixel y of the pointer/finger that produced `hoverIndex` — the
38
+ * crosshair's horizontal line follows this directly, not any property of
39
+ * the hovered point itself (see `ChartRenderer.renderCrosshairAndLegend`
40
+ * for why: pinning it to, say, the candle's close would leave the line
41
+ * motionless while the pointer moves within that candle's column). */
42
+ private hoverY;
43
+ private plugins;
44
+ /** The plugin whose `onPointerDown` returned `true` for the pointer
45
+ * currently down, or `null` when no plugin has claimed the current
46
+ * gesture (the common case — the chart handles it itself). */
47
+ private activeGesturePlugin;
48
+ private dragMode;
49
+ private lastX;
50
+ private lastY;
51
+ private renderScheduled;
52
+ private pendingAnimationFrame;
53
+ /** Distance (CSS px) between two touches on the previous touchmove —
54
+ * `null` whenever fewer than two fingers are down. Compared frame to
55
+ * frame (not against a fixed start value) so it composes naturally with
56
+ * the same incremental-delta style `applyPanDelta`/`applyValueScaleDelta`
57
+ * already use. */
58
+ private pinchLastDistance;
59
+ /** Where the current single-finger touch landed — compared against the
60
+ * live position to tell a hold from a drag; see LONG_PRESS_MOVE_TOLERANCE_PX. */
61
+ private touchStartX;
62
+ private touchStartY;
63
+ private longPressTimer;
64
+ private loader;
65
+ private loadThreshold;
66
+ private loading;
67
+ /** Set once a loader for a direction returns empty — stops re-asking at
68
+ * every threshold crossing until `setData` resets it (a fresh dataset
69
+ * may come from a different source that does have more). */
70
+ private exhausted;
71
+ constructor(canvas: HTMLCanvasElement, options?: WickChartOptions);
72
+ setData(points: TPoint[]): this;
73
+ /**
74
+ * Registers a callback the chart asks for more points when the visible
75
+ * window gets within `threshold` points of either edge of what's
76
+ * currently loaded. The chart never fetches on its own — it only decides
77
+ * *when* more data is needed and merges what the loader returns; the
78
+ * loader owns *how* (REST call, cache, websocket replay, whatever).
79
+ */
80
+ setDataLoader(loader: DataLoader<TPoint>, threshold?: number): this;
81
+ /** Registers a plugin (marker, annotation, drawing tool, ...) drawn on
82
+ * top of the chart every frame after the series and axes — see
83
+ * `src/plugins/types.ts`. Adding overlay features this way, rather than
84
+ * by extending `WickChart` itself, is what keeps the core closed to
85
+ * modification: a marker implementation never needs to touch this file. */
86
+ addPlugin(plugin: ChartPlugin<TPoint>): this;
87
+ removePlugin(plugin: ChartPlugin<TPoint>): this;
88
+ /** Every currently-registered plugin, in registration order — for an app
89
+ * building a management UI (a list of attached indicators/drawing tools
90
+ * with visibility toggles or delete buttons) without maintaining its own
91
+ * parallel bookkeeping of every `addPlugin` call. A copy, not a live
92
+ * view: mutating the returned array doesn't affect the chart. */
93
+ getPlugins(): readonly ChartPlugin<TPoint>[];
94
+ /** Shows or hides every plugin whose `id` matches (see `ChartPlugin.id`)
95
+ * and re-renders. A no-op, not an error, if nothing matches — plugins
96
+ * with no `id` set are never matched. */
97
+ setPluginVisible(id: string, visible: boolean): this;
98
+ render(): void;
99
+ /** Coalesces render() calls into at most one per animation frame. Mouse
100
+ * events (drag, wheel) can fire far faster than the display refreshes —
101
+ * calling render() directly from each one redraws the full canvas once
102
+ * per event instead of once per frame, which is what actually causes
103
+ * dragging to feel janky, not anything data-loading does. */
104
+ private scheduleRender;
105
+ /** How many points are currently loaded (not just visible) — grows as
106
+ * `setDataLoader`'s loader supplies more history. */
107
+ getPointCount(): number;
108
+ /** The currently visible window, in point indices into the full loaded
109
+ * series. Useful for building UI around the chart (a minimap, a "jump to
110
+ * latest" button) without reaching into private state. */
111
+ getVisibleRange(): {
112
+ startIndex: number;
113
+ endIndex: number;
114
+ visibleCount: number;
115
+ };
116
+ /** The value axis's manual range once the user has dragged or scaled it
117
+ * — `null` if the axis is still auto-fitting to whatever's visible
118
+ * (the default until the user first touches it vertically). */
119
+ getValueRangeOverride(): ValueRange | null;
120
+ /** The point currently under the cursor (crosshair/legend target), or
121
+ * `null` when nothing is hovered. */
122
+ getHoveredPoint(): TPoint | null;
123
+ /** Removes all attached listeners. Call on unmount — the mouseup
124
+ * listener is on `window` (so drags don't get stuck if the cursor
125
+ * leaves the canvas mid-drag) and won't be garbage-collected on its own. */
126
+ destroy(): void;
127
+ private attachEvents;
128
+ private onMouseDown;
129
+ private onMouseMove;
130
+ private onMouseUp;
131
+ private onMouseLeave;
132
+ /** Shared by both mouse drag and single-finger touch drag: shifts the
133
+ * visible time window and, once the value axis is in manual mode, the
134
+ * visible value window too — see the "pan" branch `onMouseMove` used to
135
+ * inline before mouse and touch needed the exact same math. */
136
+ private applyPanDelta;
137
+ /** Shared by both mouse drag and single-finger touch drag on the
138
+ * price-axis strip. */
139
+ private applyValueScaleDelta;
140
+ private onTouchStart;
141
+ private onTouchMove;
142
+ private onTouchEnd;
143
+ private clearLongPressTimer;
144
+ private touchDistance;
145
+ private onWheel;
146
+ private updateHover;
147
+ /** Checks whether the visible window is close enough to either edge of
148
+ * the loaded data to ask `this.loader` for more. Safe to call after
149
+ * every render — guarded so it never fires two overlapping requests for
150
+ * the same direction or re-asks a direction that already came back
151
+ * empty. */
152
+ private maybeLoadMore;
153
+ private requestMore;
154
+ private applyLoadedPoints;
155
+ /** Switches the price/value axis to manual mode if it hasn't been
156
+ * already, seeding it from the current auto-fit range so the first pixel
157
+ * of a drag doesn't jump. No-op on subsequent calls (already manual). */
158
+ private ensureValueRangeOverride;
159
+ /** The value range the *next* render would use — whatever's already
160
+ * manually overridden, or a fresh auto-fit computed the same way
161
+ * `ChartRenderer.render` does. Used outside of a render pass itself, by
162
+ * anything that needs to convert a pixel position to a data value
163
+ * on-demand (`valueForY`, dispatched pointer events) rather than only
164
+ * during `render()`. `null` when there's nothing to compute one from. */
165
+ private frameValueRange;
166
+ /** y pixel -> value in the range the next render would use. `null` if
167
+ * there's no data or no usable chart area to compute one against — see
168
+ * `ChartPointerEvent.value`. */
169
+ private valueForY;
170
+ /** x pixel -> global (possibly fractional) index — the exact inverse of
171
+ * the renderer's own `xForIndex`, so a pointer event lines up with
172
+ * wherever the chart itself would draw that index. */
173
+ private indexForX;
174
+ /** Global (possibly fractional) index -> x pixel — the exact inverse of
175
+ * `indexForX` above, and the same formula `ChartRenderer.render` draws
176
+ * with for the current viewport. Exposed on `ChartPointerEvent` so a
177
+ * plugin can convert a shape it's storing in data space back to pixels
178
+ * for hit-testing, without duplicating this math itself. */
179
+ private xForIndex;
180
+ /** Value in the range the next render would use -> y pixel — the exact
181
+ * inverse of `valueForY` above. `null` under the same conditions
182
+ * `valueForY` returns `null` for. */
183
+ private yForValue;
184
+ private pointerEventAt;
185
+ /** Same as `pointerEventAt`, but starting from `lastX`/`lastY` (raw
186
+ * `clientX`/`clientY`, tracked on every pointer move) instead of
187
+ * already-converted chart-area pixels — for the two touch-end paths
188
+ * where there's no current touch position to read coordinates from. */
189
+ private pointerEventAtLast;
190
+ /** Offers a pointer-down at `(x, y)` (chart-area pixels) to each plugin
191
+ * in reverse-registration order, stopping at the first one whose
192
+ * `onPointerDown` returns `true`. That plugin becomes
193
+ * `activeGesturePlugin` for the rest of the gesture; returns whether
194
+ * anyone claimed it, so callers know whether to skip their own default
195
+ * pan/price-scale handling. */
196
+ private dispatchPointerDown;
197
+ /** Position in canvas backing-store pixels, accounting for the gap
198
+ * between the canvas's CSS display size and its drawing-buffer size
199
+ * (e.g. when the canvas width attribute is device-pixel-ratio scaled).
200
+ * Takes any `{clientX, clientY}` point rather than `MouseEvent`
201
+ * specifically, since a `Touch` (or a synthesized pinch midpoint) has
202
+ * the same two fields and needs the exact same conversion. */
203
+ private cursorPosition;
204
+ /** CSS-pixel-to-backing-store-pixel ratio for the X axis — same
205
+ * conversion `cursorPosition` applies to absolute coordinates, extracted
206
+ * so pixel *deltas* (drag distance, wheel deltaX) can be converted too. */
207
+ private devicePixelScaleX;
208
+ private devicePixelScaleY;
209
+ }
210
+ /**
211
+ * `new WickChart(canvas, { type: 'candlestick', style: {...} })` type-checks
212
+ * even if `style` has nothing to do with `CandlestickStyle` — `type` is a
213
+ * runtime string the registry resolves, so nothing ties it to a specific
214
+ * `TStyle` at the type level (see `src/series/registry.ts`). This factory
215
+ * pins both `TPoint` (`Candle`) and `TStyle` (`CandlestickStyle`) for the
216
+ * one series built into the library, so `style` is fully checked here.
217
+ *
218
+ * A new series type gets the same treatment: export an equivalent
219
+ * `create<Name>Chart` next to it (in your own module, or a file like this
220
+ * one) rather than widening `WickChartOptions` itself — that keeps every
221
+ * series's style shape independent of every other's.
222
+ */
223
+ export declare function createCandlestickChart(canvas: HTMLCanvasElement, options?: Omit<WickChartOptions, 'type' | 'style'> & {
224
+ style?: Partial<CandlestickStyle>;
225
+ }): WickChart<Candle>;