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,20 @@
1
+ /**
2
+ * JS placeholder for domain→pixel scaling.
3
+ *
4
+ * This mirrors `Scale` in crates/wickchart-core/src/lib.rs exactly. It's
5
+ * the seam where the WASM build gets wired in: once the crate is compiled
6
+ * with wasm-pack and published as an internal dependency, `LinearScale`
7
+ * gets replaced by a thin wrapper around the WASM `Scale` for series above
8
+ * whatever point size profiling says the JS↔WASM call overhead pays for
9
+ * itself. Small series stay on this plain implementation — there's no
10
+ * reason to pay a WASM boundary cost to scale a handful of points.
11
+ */
12
+ export declare class LinearScale {
13
+ private domainMin;
14
+ private domainMax;
15
+ private rangeMin;
16
+ private rangeMax;
17
+ constructor(domainMin: number, domainMax: number, rangeMin: number, rangeMax: number);
18
+ map(value: number): number;
19
+ mapMany(values: number[]): number[];
20
+ }
package/dist/scale.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * JS placeholder for domain→pixel scaling.
3
+ *
4
+ * This mirrors `Scale` in crates/wickchart-core/src/lib.rs exactly. It's
5
+ * the seam where the WASM build gets wired in: once the crate is compiled
6
+ * with wasm-pack and published as an internal dependency, `LinearScale`
7
+ * gets replaced by a thin wrapper around the WASM `Scale` for series above
8
+ * whatever point size profiling says the JS↔WASM call overhead pays for
9
+ * itself. Small series stay on this plain implementation — there's no
10
+ * reason to pay a WASM boundary cost to scale a handful of points.
11
+ */
12
+ export class LinearScale {
13
+ constructor(domainMin, domainMax, rangeMin, rangeMax) {
14
+ this.domainMin = domainMin;
15
+ this.domainMax = domainMax;
16
+ this.rangeMin = rangeMin;
17
+ this.rangeMax = rangeMax;
18
+ }
19
+ map(value) {
20
+ const span = this.domainMax - this.domainMin;
21
+ if (span === 0)
22
+ return this.rangeMin;
23
+ const t = (value - this.domainMin) / span;
24
+ return this.rangeMin + t * (this.rangeMax - this.rangeMin);
25
+ }
26
+ mapMany(values) {
27
+ return values.map((v) => this.map(v));
28
+ }
29
+ }
@@ -0,0 +1,20 @@
1
+ import type { Candle } from '../types.js';
2
+ import type { SeriesDefinition } from './types.js';
3
+ export interface CandlestickStyle {
4
+ /** Candle body/wick color for up (close >= open) bars. */
5
+ upColor: string;
6
+ /** Candle body/wick color for down (close < open) bars. */
7
+ downColor: string;
8
+ /** Candle body/wick width as a fraction of the available per-candle slot
9
+ * width (the rest is inter-candle gap). Defaults to 0.6. */
10
+ bodyWidthRatio: number;
11
+ /** Fraction of the chart's full height that volume bars occupy, measured
12
+ * up from the bottom. Candles still use the full height for their own
13
+ * price scale regardless of this value — the bars sit in this bottom
14
+ * margin, layered underneath. Defaults to 0.2. */
15
+ volumeAreaHeightRatio: number;
16
+ /** Opacity (0-1) of the volume bars, so they read as a backdrop rather
17
+ * than competing with the candles drawn over them. Defaults to 0.5. */
18
+ volumeBarOpacity: number;
19
+ }
20
+ export declare const candlestickSeries: SeriesDefinition<Candle, CandlestickStyle>;
@@ -0,0 +1,88 @@
1
+ import { fitRange } from '../priceRange.js';
2
+ import { registerSeries } from './registry.js';
3
+ const DEFAULT_STYLE = {
4
+ upColor: '#26a69a',
5
+ downColor: '#ef5350',
6
+ bodyWidthRatio: 0.6,
7
+ volumeAreaHeightRatio: 0.2,
8
+ volumeBarOpacity: 0.5,
9
+ };
10
+ function getValueRange(visible, scaleFactor) {
11
+ const rawMin = Math.min(...visible.map((c) => c.low));
12
+ const rawMax = Math.max(...visible.map((c) => c.high));
13
+ return fitRange(rawMin, rawMax, scaleFactor);
14
+ }
15
+ /** Draws a volume bar per candle that has one, scaled against the largest
16
+ * volume currently visible. A no-op — nothing reserved, nothing drawn —
17
+ * when not a single visible candle has `volume` set, so charts built from
18
+ * OHLC-only data look exactly as they did before this existed. */
19
+ function drawVolumeBars(context, style) {
20
+ const { ctx, visible, startIndex, xForIndex, slotWidth, chartHeight } = context;
21
+ const maxVolume = visible.reduce((max, c) => (c.volume !== undefined ? Math.max(max, c.volume) : max), 0);
22
+ if (maxVolume <= 0)
23
+ return;
24
+ const areaHeight = chartHeight * style.volumeAreaHeightRatio;
25
+ const bodyWidth = Math.max(1, slotWidth * style.bodyWidthRatio);
26
+ ctx.globalAlpha = style.volumeBarOpacity;
27
+ visible.forEach((candle, i) => {
28
+ if (candle.volume === undefined)
29
+ return;
30
+ const x = xForIndex(startIndex + i);
31
+ const barHeight = Math.max(1, (candle.volume / maxVolume) * areaHeight);
32
+ ctx.fillStyle = candle.close >= candle.open ? style.upColor : style.downColor;
33
+ ctx.fillRect(x - bodyWidth / 2, chartHeight - barHeight, bodyWidth, barHeight);
34
+ });
35
+ ctx.globalAlpha = 1;
36
+ }
37
+ function draw(context, style) {
38
+ const { ctx, visible, startIndex, xForIndex, slotWidth, yScale } = context;
39
+ const bodyWidth = Math.max(1, slotWidth * style.bodyWidthRatio);
40
+ // Drawn first so the (opaque) candles render on top of the (translucent)
41
+ // volume bars where the two overlap near the bottom of the chart.
42
+ drawVolumeBars(context, style);
43
+ // Batched through mapMany (one call per array) rather than four map()
44
+ // calls per candle in the loop below — the batch is what lets the WASM
45
+ // path pay the JS<->WASM boundary cost once per frame instead of once
46
+ // per point.
47
+ const yHighs = yScale.mapMany(visible.map((c) => c.high));
48
+ const yLows = yScale.mapMany(visible.map((c) => c.low));
49
+ const yOpens = yScale.mapMany(visible.map((c) => c.open));
50
+ const yCloses = yScale.mapMany(visible.map((c) => c.close));
51
+ visible.forEach((candle, i) => {
52
+ const x = xForIndex(startIndex + i);
53
+ const isUp = candle.close >= candle.open;
54
+ ctx.strokeStyle = ctx.fillStyle = isUp ? style.upColor : style.downColor;
55
+ ctx.beginPath();
56
+ ctx.moveTo(x, yHighs[i]);
57
+ ctx.lineTo(x, yLows[i]);
58
+ ctx.stroke();
59
+ const yOpen = yOpens[i];
60
+ const yClose = yCloses[i];
61
+ const top = Math.min(yOpen, yClose);
62
+ const bodyHeight = Math.max(1, Math.abs(yClose - yOpen));
63
+ ctx.fillRect(x - bodyWidth / 2, top, bodyWidth, bodyHeight);
64
+ });
65
+ }
66
+ function formatLegend(candle) {
67
+ const parts = [
68
+ `O ${candle.open.toLocaleString('en-US')}`,
69
+ `H ${candle.high.toLocaleString('en-US')}`,
70
+ `L ${candle.low.toLocaleString('en-US')}`,
71
+ `C ${candle.close.toLocaleString('en-US')}`,
72
+ ];
73
+ if (candle.volume !== undefined) {
74
+ parts.push(`Vol ${candle.volume.toLocaleString('en-US')}`);
75
+ }
76
+ return parts;
77
+ }
78
+ export const candlestickSeries = {
79
+ type: 'candlestick',
80
+ defaultStyle: DEFAULT_STYLE,
81
+ getValueRange,
82
+ draw,
83
+ formatLegend,
84
+ };
85
+ // Registered as a module-level side effect so importing this file (which
86
+ // src/index.ts always does) is enough to make 'candlestick' available —
87
+ // callers never register the built-in type themselves.
88
+ registerSeries(candlestickSeries);
@@ -0,0 +1,13 @@
1
+ import type { SeriesPoint } from '../types.js';
2
+ import type { SeriesDefinition } from './types.js';
3
+ /** Registers a series type, making it available to `new WickChart(canvas,
4
+ * { type: definition.type })`. Call this once per definition — typically as
5
+ * a module-level side effect in the file that defines it (see
6
+ * `src/series/candlestick.ts`) so importing the module is enough to make
7
+ * the type available. */
8
+ export declare function registerSeries<TPoint extends SeriesPoint, TStyle>(definition: SeriesDefinition<TPoint, TStyle>): void;
9
+ /** Resolves a `type` string to its registered definition. Throws rather
10
+ * than returning `undefined` — an unknown type is a caller mistake (typo,
11
+ * or forgetting to import the module that registers it), not a state
12
+ * `WickChart` should silently tolerate. */
13
+ export declare function getSeries<TPoint extends SeriesPoint, TStyle>(type: string): SeriesDefinition<TPoint, TStyle>;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Keyed by `SeriesDefinition.type`. Deliberately untyped-at-rest
3
+ * (`SeriesDefinition<any, any>`) — a registry that must hold arbitrarily
4
+ * different point/style shapes side by side can't statically relate a
5
+ * runtime string key to a specific `TPoint`/`TStyle` pair, so the type
6
+ * safety is pushed to the two functions below instead: `registerSeries`
7
+ * checks a definition against the type parameters it's called with, and
8
+ * `getSeries`'s caller asserts what it expects back (exactly like
9
+ * `JSON.parse`'s return type, or a DI container's `resolve<T>()`).
10
+ */
11
+ const registry = new Map();
12
+ /** Registers a series type, making it available to `new WickChart(canvas,
13
+ * { type: definition.type })`. Call this once per definition — typically as
14
+ * a module-level side effect in the file that defines it (see
15
+ * `src/series/candlestick.ts`) so importing the module is enough to make
16
+ * the type available. */
17
+ export function registerSeries(definition) {
18
+ registry.set(definition.type, definition);
19
+ }
20
+ /** Resolves a `type` string to its registered definition. Throws rather
21
+ * than returning `undefined` — an unknown type is a caller mistake (typo,
22
+ * or forgetting to import the module that registers it), not a state
23
+ * `WickChart` should silently tolerate. */
24
+ export function getSeries(type) {
25
+ const definition = registry.get(type);
26
+ if (!definition) {
27
+ throw new Error(`wick-charts: unknown series type "${type}" — is it registered (registerSeries) and imported?`);
28
+ }
29
+ return definition;
30
+ }
@@ -0,0 +1,56 @@
1
+ import type { Scale } from '../hybridScale.js';
2
+ import type { SeriesPoint, ValueRange } from '../types.js';
3
+ export type { ValueRange } from '../types.js';
4
+ /**
5
+ * Everything a series's `draw` needs to turn its visible points into
6
+ * pixels, precomputed once per frame by `ChartRenderer` so every series
7
+ * type shares the exact same geometry (no series recomputes its own x
8
+ * positions or re-derives the y scale).
9
+ */
10
+ export interface SeriesDrawContext<TPoint extends SeriesPoint> {
11
+ ctx: CanvasRenderingContext2D;
12
+ /** The points currently in view, already sliced from the full sorted set. */
13
+ visible: TPoint[];
14
+ /** Global (full sorted-array) index of `visible[0]` — add a local offset
15
+ * to it before calling `xForIndex`. */
16
+ startIndex: number;
17
+ /** Global-index -> x pixel. Already accounts for the viewport's
18
+ * (possibly fractional) pan position, so panning stays pixel-smooth. */
19
+ xForIndex: (globalIndex: number) => number;
20
+ /** Pixel width of one candle/point's slot — series that draw a body
21
+ * width or bar width derive it from this. */
22
+ slotWidth: number;
23
+ /** Value (price) -> y pixel for the current frame's domain. */
24
+ yScale: Scale;
25
+ chartHeight: number;
26
+ }
27
+ /**
28
+ * The single seam a new chart type has to implement. `WickChart` and
29
+ * `ChartRenderer` are written only against this interface — pan/zoom,
30
+ * touch/mouse handling, on-demand data loading, and axis rendering never
31
+ * need to know which concrete series is active. Adding a new chart type
32
+ * (line, area, bar, ...) means writing one file that implements this
33
+ * interface and calling `registerSeries` on it — see
34
+ * `src/series/candlestick.ts` for the reference implementation and
35
+ * `src/series/registry.ts` for how `type` strings resolve to a definition.
36
+ */
37
+ export interface SeriesDefinition<TPoint extends SeriesPoint, TStyle> {
38
+ /** Unique key — what `WickChartOptions.type` matches against. */
39
+ readonly type: string;
40
+ /** Style used when the caller doesn't override it via `WickChartOptions.style`. */
41
+ readonly defaultStyle: TStyle;
42
+ /** Computes the y-domain to auto-fit for the currently visible points,
43
+ * before the user has manually panned/scaled the value axis (see
44
+ * `Viewport.valueRangeOverride`). `scaleFactor` is the user's manual
45
+ * vertical-zoom multiplier and should widen/narrow the fitted range
46
+ * around its center, not replace it. */
47
+ getValueRange(visible: TPoint[], scaleFactor: number): ValueRange;
48
+ /** Draws the visible points for one frame. Must not read or mutate
49
+ * anything outside `context` and `style` — `ChartRenderer` owns the
50
+ * canvas lifecycle (clear/background/axes) around this call. */
51
+ draw(context: SeriesDrawContext<TPoint>, style: TStyle): void;
52
+ /** Builds the hover/crosshair legend text for one point, one string per
53
+ * segment (joined with spacing by the renderer). Omit to draw the
54
+ * crosshair line with no legend text. */
55
+ formatLegend?(point: TPoint, style: TStyle): string[];
56
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ import { vi } from 'vitest';
2
+ /** Minimal fake of the subset of CanvasRenderingContext2D the renderer
3
+ * actually calls — jsdom implements `<canvas>` as an element but not its
4
+ * 2D drawing context, so real code under test needs this stood in for
5
+ * `getContext('2d')`. */
6
+ export interface FakeContext2D {
7
+ clearRect: ReturnType<typeof vi.fn>;
8
+ fillRect: ReturnType<typeof vi.fn>;
9
+ beginPath: ReturnType<typeof vi.fn>;
10
+ closePath: ReturnType<typeof vi.fn>;
11
+ moveTo: ReturnType<typeof vi.fn>;
12
+ lineTo: ReturnType<typeof vi.fn>;
13
+ stroke: ReturnType<typeof vi.fn>;
14
+ fill: ReturnType<typeof vi.fn>;
15
+ fillText: ReturnType<typeof vi.fn>;
16
+ measureText: ReturnType<typeof vi.fn>;
17
+ save: ReturnType<typeof vi.fn>;
18
+ restore: ReturnType<typeof vi.fn>;
19
+ setLineDash: ReturnType<typeof vi.fn>;
20
+ fillStyle: string;
21
+ strokeStyle: string;
22
+ font: string;
23
+ textAlign: string;
24
+ textBaseline: string;
25
+ globalAlpha: number;
26
+ lineWidth: number;
27
+ }
28
+ export declare function createFakeContext(): FakeContext2D;
29
+ /** A real jsdom `<canvas>` element (so `addEventListener`/`dispatchEvent`/
30
+ * `getBoundingClientRect` all behave like a normal DOM node) with its 2D
31
+ * context stubbed to `createFakeContext()`'s spies. `width`/`height` are
32
+ * the backing-store size; `getBoundingClientRect` is stubbed to the same
33
+ * values by default (devicePixelRatio 1) — pass a different `cssWidth`/
34
+ * `cssHeight` to simulate a scaled display. */
35
+ export declare function createTestCanvas(width?: number, height?: number, cssWidth?: number, cssHeight?: number): {
36
+ canvas: HTMLCanvasElement;
37
+ ctx: FakeContext2D;
38
+ };
@@ -0,0 +1,50 @@
1
+ import { vi } from 'vitest';
2
+ export function createFakeContext() {
3
+ return {
4
+ clearRect: vi.fn(),
5
+ fillRect: vi.fn(),
6
+ beginPath: vi.fn(),
7
+ closePath: vi.fn(),
8
+ moveTo: vi.fn(),
9
+ lineTo: vi.fn(),
10
+ stroke: vi.fn(),
11
+ fill: vi.fn(),
12
+ fillText: vi.fn(),
13
+ measureText: vi.fn().mockReturnValue({ width: 40 }),
14
+ save: vi.fn(),
15
+ restore: vi.fn(),
16
+ setLineDash: vi.fn(),
17
+ fillStyle: '',
18
+ strokeStyle: '',
19
+ font: '',
20
+ textAlign: '',
21
+ textBaseline: '',
22
+ globalAlpha: 1,
23
+ lineWidth: 1,
24
+ };
25
+ }
26
+ /** A real jsdom `<canvas>` element (so `addEventListener`/`dispatchEvent`/
27
+ * `getBoundingClientRect` all behave like a normal DOM node) with its 2D
28
+ * context stubbed to `createFakeContext()`'s spies. `width`/`height` are
29
+ * the backing-store size; `getBoundingClientRect` is stubbed to the same
30
+ * values by default (devicePixelRatio 1) — pass a different `cssWidth`/
31
+ * `cssHeight` to simulate a scaled display. */
32
+ export function createTestCanvas(width = 800, height = 400, cssWidth = width, cssHeight = height) {
33
+ const canvas = document.createElement('canvas');
34
+ canvas.width = width;
35
+ canvas.height = height;
36
+ const ctx = createFakeContext();
37
+ vi.spyOn(canvas, 'getContext').mockReturnValue(ctx);
38
+ vi.spyOn(canvas, 'getBoundingClientRect').mockReturnValue({
39
+ left: 0,
40
+ top: 0,
41
+ right: cssWidth,
42
+ bottom: cssHeight,
43
+ width: cssWidth,
44
+ height: cssHeight,
45
+ x: 0,
46
+ y: 0,
47
+ toJSON: () => ({}),
48
+ });
49
+ return { canvas, ctx };
50
+ }
package/dist/time.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { WickTime } from './types.js';
2
+ /**
3
+ * Normalizes any supported `WickTime` shape to unix seconds — the single
4
+ * unit every downstream consumer (sorting, scaling, rendering) works in.
5
+ */
6
+ export declare function toUnixSeconds(time: WickTime): number;
package/dist/time.js ADDED
@@ -0,0 +1,58 @@
1
+ function isUnixMillis(time) {
2
+ return typeof time === 'object' && time !== null && 'unixMs' in time;
3
+ }
4
+ function isBusinessDay(time) {
5
+ return typeof time === 'object' && time !== null && 'businessDay' in time;
6
+ }
7
+ function businessDayToUnixSeconds({ year, month, day }) {
8
+ // BusinessDay has no time-of-day — anchor it at UTC midnight.
9
+ return Date.UTC(year, month - 1, day) / 1000;
10
+ }
11
+ /**
12
+ * Registered in the order they should be checked. To support a new source
13
+ * format (e.g. a vendor that sends `{ ns: bigint }`), add a strategy here —
14
+ * nothing else in the codebase needs to change.
15
+ */
16
+ const strategies = [
17
+ {
18
+ name: 'unix-seconds',
19
+ test: (time) => typeof time === 'number',
20
+ toUnixSeconds: (time) => time,
21
+ },
22
+ {
23
+ name: 'unix-millis',
24
+ test: isUnixMillis,
25
+ toUnixSeconds: (time) => time.unixMs / 1000,
26
+ },
27
+ {
28
+ name: 'business-day',
29
+ test: isBusinessDay,
30
+ toUnixSeconds: (time) => businessDayToUnixSeconds(time.businessDay),
31
+ },
32
+ {
33
+ name: 'iso-string',
34
+ test: (time) => typeof time === 'string',
35
+ toUnixSeconds: (time) => {
36
+ const parsedMs = Date.parse(time);
37
+ if (Number.isNaN(parsedMs)) {
38
+ throw new Error(`wick-charts: could not parse time string "${String(time)}" as ISO 8601`);
39
+ }
40
+ return parsedMs / 1000;
41
+ },
42
+ },
43
+ ];
44
+ /**
45
+ * Normalizes any supported `WickTime` shape to unix seconds — the single
46
+ * unit every downstream consumer (sorting, scaling, rendering) works in.
47
+ */
48
+ export function toUnixSeconds(time) {
49
+ const strategy = strategies.find((s) => s.test(time));
50
+ if (!strategy) {
51
+ throw new Error(`wick-charts: unrecognized time value ${JSON.stringify(time)}`);
52
+ }
53
+ const seconds = strategy.toUnixSeconds(time);
54
+ if (!Number.isFinite(seconds)) {
55
+ throw new Error(`wick-charts: "${strategy.name}" strategy produced a non-finite time for ${JSON.stringify(time)}`);
56
+ }
57
+ return seconds;
58
+ }
@@ -0,0 +1,151 @@
1
+ /** A calendar day with no time-of-day component. */
2
+ export interface BusinessDay {
3
+ year: number;
4
+ month: number;
5
+ day: number;
6
+ }
7
+ /** Explicit unix-milliseconds wrapper — disambiguates from the implicit
8
+ * unix-*seconds* convention that a bare `number` carries (see src/time.ts).
9
+ * Millisecond timestamps are a common source format in the wild. */
10
+ export interface UnixMillis {
11
+ unixMs: number;
12
+ }
13
+ /**
14
+ * A point in time for any plotted point (candle, line point, etc.).
15
+ * Deliberately a union of several shapes instead of one
16
+ * flexible-but-ambiguous type, because time representation is not
17
+ * universal across data sources:
18
+ * - a bare `number`: unix timestamp in **seconds**
19
+ * - `{ unixMs }`: unix timestamp in **milliseconds**
20
+ * - `{ businessDay }`: a calendar day with no time-of-day
21
+ * - a `string`: ISO 8601
22
+ *
23
+ * Adding a new source format means adding one case to `src/time.ts`'s
24
+ * strategy list — this type and the call sites that consume `SeriesPoint`
25
+ * never need to change.
26
+ */
27
+ export type WickTime = number | string | UnixMillis | {
28
+ businessDay: BusinessDay;
29
+ };
30
+ /**
31
+ * The minimum shape every plotted point must have, regardless of chart
32
+ * type — a time to place it on the x-axis. `Candle` (OHLC) is one instance
33
+ * of this; a future line/area/bar series point is another. Everything in
34
+ * the engine that doesn't need to know *what* is plotted (pan/zoom, event
35
+ * handling, data loading, merging) is written against this shape, not
36
+ * against `Candle` — see `src/series/types.ts` for where the per-type
37
+ * behavior (drawing, value-range, legend text) actually lives.
38
+ */
39
+ export interface SeriesPoint {
40
+ time: WickTime;
41
+ }
42
+ /**
43
+ * A y-domain — a value axis range. The single `{min, max}` shape shared by
44
+ * `SeriesDefinition.getValueRange`'s result, `Viewport.valueRangeOverride`,
45
+ * and `src/priceRange.ts`'s `fitRange` helper, so all three talk about "the
46
+ * currently plotted range" the same way regardless of which series (or
47
+ * whether the user has manually overridden the axis) produced it.
48
+ */
49
+ export interface ValueRange {
50
+ min: number;
51
+ max: number;
52
+ }
53
+ export interface Candle extends SeriesPoint {
54
+ open: number;
55
+ high: number;
56
+ low: number;
57
+ close: number;
58
+ volume?: number;
59
+ }
60
+ /** Text styling shared by every label the chart draws — axis ticks,
61
+ * crosshair axis labels, and the hover legend. `axisSize`/`legendSize` are
62
+ * separate since the legend has historically been drawn one px larger to
63
+ * stand out as the "primary" hover readout; override either independently. */
64
+ export interface ChartFontOptions {
65
+ /** CSS font family. Defaults to `'sans-serif'`. */
66
+ family?: string;
67
+ /** Size, in px, of price/time axis tick labels and crosshair axis labels. Defaults to 10. */
68
+ axisSize?: number;
69
+ /** Size, in px, of the hover legend text. Defaults to 11. */
70
+ legendSize?: number;
71
+ }
72
+ /** Sizing and coloring for the price/time axis strips and their grid lines
73
+ * — engine-level, not part of any series's own style, since every series
74
+ * type shares the same two axes regardless of what it plots. */
75
+ export interface ChartAxisOptions {
76
+ /** Width, in px, of the price-axis strip on the right. Defaults to 64. */
77
+ priceWidth?: number;
78
+ /** Height, in px, of the time-axis strip at the bottom. Defaults to 24. */
79
+ timeHeight?: number;
80
+ /** Target number of price-axis tick labels. Defaults to 5. */
81
+ priceTickCount?: number;
82
+ /** Maximum number of time-axis tick labels. Defaults to 6. */
83
+ timeMaxTicks?: number;
84
+ /** Tick label color. Defaults to `'#787878'`. */
85
+ textColor?: string;
86
+ /** Color of the two axis boundary lines. Defaults to `'#33333333'`. */
87
+ lineColor?: string;
88
+ /** Color of the horizontal price gridlines. Defaults to `'#2a2a2a55'`. */
89
+ gridLineColor?: string;
90
+ }
91
+ /** Coloring and padding for the hover crosshair's lines and its two
92
+ * highlighted axis-label chips. */
93
+ export interface ChartCrosshairOptions {
94
+ /** Dashed crosshair line color. Defaults to `'#9090904d'`. */
95
+ lineColor?: string;
96
+ /** Background fill of the price/time label chips. Defaults to `'#3a3a3a'`. */
97
+ labelBackground?: string;
98
+ /** Text color inside the label chips. Defaults to `'#f0f0f0'`. */
99
+ labelTextColor?: string;
100
+ /** Horizontal padding, in px, inside each label chip. Defaults to 4. */
101
+ labelPaddingX?: number;
102
+ /** Vertical padding, in px, inside each label chip. Defaults to 3. */
103
+ labelPaddingY?: number;
104
+ }
105
+ /** Styling for the hover legend — a tooltip that follows the cursor/finger
106
+ * showing the OHLC(+volume) breakdown for the hovered point, offset up and
107
+ * to the right of it and clamped so it never runs off the chart edge. */
108
+ export interface ChartLegendOptions {
109
+ /** Legend text color. Defaults to `'#f0f0f0'`. */
110
+ textColor?: string;
111
+ /** Tooltip background fill. Defaults to `'#3a3a3a'`. */
112
+ background?: string;
113
+ /** Horizontal padding, in px, inside the tooltip. Defaults to 8. */
114
+ paddingX?: number;
115
+ /** Vertical padding, in px, inside the tooltip. Defaults to 6. */
116
+ paddingY?: number;
117
+ /** Gap, in px, between the hovered pixel and the tooltip's nearest edge.
118
+ * Defaults to 12. */
119
+ cursorGap?: number;
120
+ }
121
+ export interface WickChartOptions {
122
+ /**
123
+ * Which registered series type to render this chart as (see
124
+ * `registerSeries` in `src/series/registry.ts`). Defaults to
125
+ * `'candlestick'`, the only type built into the library today — adding a
126
+ * new one is a matter of implementing `SeriesDefinition` and registering
127
+ * it, without changing `WickChart` or `ChartRenderer` at all.
128
+ */
129
+ type?: string;
130
+ /** Background color of the canvas. Defaults to transparent. Chart-wide
131
+ * (the renderer clears/fills the whole canvas with it), not part of any
132
+ * series's own style. */
133
+ background?: string;
134
+ /**
135
+ * Style overrides specific to the chosen `type` — shape depends on which
136
+ * series is active (candlestick's is `CandlestickStyle`). Merged over the
137
+ * series definition's `defaultStyle`.
138
+ */
139
+ style?: Record<string, unknown>;
140
+ /** Font family/sizes for every label the chart draws. Merged over the
141
+ * built-in defaults field by field — set only what you want to change. */
142
+ font?: ChartFontOptions;
143
+ /** Price/time axis sizing, tick counts, and coloring. Merged over the
144
+ * built-in defaults field by field. */
145
+ axis?: ChartAxisOptions;
146
+ /** Hover crosshair line/label coloring and padding. Merged over the
147
+ * built-in defaults field by field. */
148
+ crosshair?: ChartCrosshairOptions;
149
+ /** Hover legend coloring. Merged over the built-in defaults field by field. */
150
+ legend?: ChartLegendOptions;
151
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ import type { ValueRange } from './types.js';
2
+ /**
3
+ * Pure pan/zoom/value-scale state — no DOM, no canvas. `WickChart` owns
4
+ * translating pixel deltas (drag distance, wheel delta) into calls here;
5
+ * this class only owns the resulting numbers, which keeps it unit-testable
6
+ * without a canvas. Generic across series types: "value" here is whatever
7
+ * the active `SeriesDefinition.getValueRange` returns for the y-axis —
8
+ * price for candlesticks, but no different in kind for a future line or
9
+ * bar series's own value domain.
10
+ */
11
+ export declare class Viewport {
12
+ startIndex: number;
13
+ visibleCount: number;
14
+ /** 1 = auto-fit value range. >1 widens it (the series looks
15
+ * shorter/compressed). <1 narrows it (the series looks taller), clamped
16
+ * so real data never clips off-screen. Sign of drag->factor mapping
17
+ * lives in WickChart, not here. */
18
+ valueScaleFactor: number;
19
+ /** Manually-set value range from a vertical drag or value-axis scale.
20
+ * `null` until the user first touches the value axis — while `null` the
21
+ * renderer auto-fits (see `SeriesDefinition.getValueRange`) using
22
+ * `valueScaleFactor` alone. Once set, auto-fit stops applying: the user
23
+ * has taken explicit control of the axis, so the chart stops
24
+ * recentering it under them. */
25
+ valueRangeOverride: ValueRange | null;
26
+ constructor(totalCount: number, visibleCount?: number);
27
+ get endIndex(): number;
28
+ /** Shifts the visible window. Positive `deltaPoints` moves forward in
29
+ * time (later points come into view on the right). Clamped so the
30
+ * window never leaves [0, totalCount] — no overscroll past the data. */
31
+ pan(deltaPoints: number, totalCount: number): void;
32
+ /** Scales the visible window by `factor` (>1 zooms out, <1 zooms in),
33
+ * keeping the point at `anchorIndex` under the same relative position —
34
+ * the standard "zoom toward the cursor" feel. */
35
+ zoom(factor: number, anchorIndex: number, totalCount: number): void;
36
+ /** Multiplies the value-scale factor, clamped to a sane range so the
37
+ * value axis can't be dragged into showing nothing or clipping data.
38
+ * Only affects the auto-fit path — a no-op once `valueRangeOverride` is
39
+ * set, at which point `scaleValueRange` takes over. */
40
+ scaleValue(factor: number): void;
41
+ /** Switches the value axis to manual mode, pinned at `range`. Call once,
42
+ * lazily, the first time the user drags vertically — see `WickChart`. */
43
+ setValueRangeOverride(range: ValueRange): void;
44
+ /** Shifts the manual value range by an absolute amount (same units as
45
+ * the plotted value). No-op until `setValueRangeOverride` has been
46
+ * called at least once — there is nothing to shift relative to
47
+ * otherwise. */
48
+ panValueRange(deltaAbsolute: number): void;
49
+ /** Scales the manual value range around its own center. No-op until
50
+ * `setValueRangeOverride` has been called at least once. */
51
+ scaleValueRange(factor: number): void;
52
+ }