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.
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/axis.d.ts +21 -0
- package/dist/axis.js +44 -0
- package/dist/dataSource.d.ts +24 -0
- package/dist/dataSource.js +1 -0
- package/dist/hitTest.d.ts +31 -0
- package/dist/hitTest.js +46 -0
- package/dist/hybridScale.d.ts +25 -0
- package/dist/hybridScale.js +41 -0
- package/dist/index.d.ts +225 -0
- package/dist/index.js +715 -0
- package/dist/mergeSeries.d.ts +14 -0
- package/dist/mergeSeries.js +21 -0
- package/dist/plugins/types.d.ts +140 -0
- package/dist/plugins/types.js +1 -0
- package/dist/priceAxis.d.ts +7 -0
- package/dist/priceAxis.js +49 -0
- package/dist/priceRange.d.ts +12 -0
- package/dist/priceRange.js +16 -0
- package/dist/renderer.d.ts +79 -0
- package/dist/renderer.js +318 -0
- package/dist/scale.d.ts +20 -0
- package/dist/scale.js +29 -0
- package/dist/series/candlestick.d.ts +20 -0
- package/dist/series/candlestick.js +88 -0
- package/dist/series/registry.d.ts +13 -0
- package/dist/series/registry.js +30 -0
- package/dist/series/types.d.ts +56 -0
- package/dist/series/types.js +1 -0
- package/dist/testHelpers.d.ts +38 -0
- package/dist/testHelpers.js +50 -0
- package/dist/time.d.ts +6 -0
- package/dist/time.js +58 -0
- package/dist/types.d.ts +151 -0
- package/dist/types.js +1 -0
- package/dist/viewport.d.ts +52 -0
- package/dist/viewport.js +87 -0
- package/dist/wasm.d.ts +29 -0
- package/dist/wasm.js +35 -0
- package/dist/wasmImporter.d.ts +6 -0
- package/dist/wasmImporter.js +7 -0
- package/package.json +39 -0
- package/wasm-pkg/package.json +21 -0
- package/wasm-pkg/wickchart_core.d.ts +59 -0
- package/wasm-pkg/wickchart_core.js +227 -0
- package/wasm-pkg/wickchart_core_bg.wasm +0 -0
- package/wasm-pkg/wickchart_core_bg.wasm.d.ts +11 -0
package/dist/scale.d.ts
ADDED
|
@@ -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
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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|