wick-charts 0.4.0 → 0.6.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/README.md +96 -22
- package/dist/index.d.ts +31 -2
- package/dist/index.js +46 -9
- package/dist/renderer.d.ts +7 -0
- package/dist/renderer.js +34 -9
- package/dist/series/line.d.ts +14 -0
- package/dist/series/line.js +65 -0
- package/dist/types.d.ts +26 -3
- package/dist/valueAxis.d.ts +44 -0
- package/dist/valueAxis.js +52 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,9 @@ npm install wick-charts
|
|
|
17
17
|
- [Install](#install)
|
|
18
18
|
- [Quick start](#quick-start)
|
|
19
19
|
- [Candle data](#candle-data)
|
|
20
|
+
- [Line charts](#line-charts)
|
|
20
21
|
- [Styling](#styling)
|
|
22
|
+
- [Inverting the value axis](#inverting-the-value-axis)
|
|
21
23
|
- [Reading chart state](#reading-chart-state)
|
|
22
24
|
- [Loading more history on demand](#loading-more-history-on-demand)
|
|
23
25
|
- [Extending: plugins](#extending-plugins)
|
|
@@ -136,6 +138,39 @@ it — a dataset with no `volume` at all renders exactly as if the feature didn'
|
|
|
136
138
|
array) is safe. It resets pan/zoom/hover state — call it for a genuinely new dataset, and use
|
|
137
139
|
`setDataLoader()` (below) to extend the current one instead.
|
|
138
140
|
|
|
141
|
+
### Line charts
|
|
142
|
+
|
|
143
|
+
For a plain time series with no OHLC shape — an equity curve, a metric over time, anything
|
|
144
|
+
that's just one number per point — `createLineChart` is the line-series equivalent of
|
|
145
|
+
`createCandlestickChart` above. A line point is `{ time, value }`:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { createLineChart } from 'wick-charts';
|
|
149
|
+
|
|
150
|
+
const chart = createLineChart(canvas, {
|
|
151
|
+
style: { lineColor: '#2196f3', lineWidth: 1.5 }, // both shown here are the defaults
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
chart.setData([
|
|
155
|
+
{ time: '2024-01-01T00:00:00Z', value: 100 },
|
|
156
|
+
{ time: '2024-01-02T00:00:00Z', value: 103.4 },
|
|
157
|
+
{ time: '2024-01-03T00:00:00Z', value: 101.8 },
|
|
158
|
+
// ...
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
chart.render();
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Everything else — pan/zoom/hover, `setDataLoader`, `addPlugin`, `addPane`, `background`/
|
|
165
|
+
`font`/`axis`/`crosshair`/`legend` styling — works exactly as it does for a candlestick chart,
|
|
166
|
+
since none of it is specific to what's actually plotted (see "Series types" under
|
|
167
|
+
Architecture). `value` accepts `NaN` (or any non-finite number) as an explicit gap: the line
|
|
168
|
+
breaks there and resumes at the next real value, rather than plotting a bogus point or
|
|
169
|
+
throwing — useful for a series with missing data at some points without pre-filtering it
|
|
170
|
+
yourself. The hover legend shows `Value <number>` (or `Value —` for a hovered gap) in place of
|
|
171
|
+
candlestick's OHLC breakdown; `new WickChart(canvas, { type: 'line' })` also works, the same
|
|
172
|
+
untyped escape hatch `type: 'candlestick'` has, if you'd rather not import the factory.
|
|
173
|
+
|
|
139
174
|
### Styling
|
|
140
175
|
|
|
141
176
|
Every visual aspect of the chart is an option — nothing is a fixed constant you can't reach.
|
|
@@ -193,6 +228,39 @@ type-checks `style` against
|
|
|
193
228
|
`CandlestickStyle`; the more general `new WickChart(canvas, { type: 'candlestick', style })`
|
|
194
229
|
also works but doesn't — see "Series types" below for why, if you're curious.
|
|
195
230
|
|
|
231
|
+
### Inverting the value axis
|
|
232
|
+
|
|
233
|
+
`invertValueAxis` mirrors the value axis top-to-bottom — every pane's higher values render
|
|
234
|
+
lower on screen instead of higher, useful for a "what if this series had moved the opposite
|
|
235
|
+
way" view:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const chart = createCandlestickChart(canvas, { invertValueAxis: true });
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Unlike the style options above, it's meant to be flipped live rather than fixed at
|
|
242
|
+
construction — `setInvertValueAxis(boolean)`/`isValueAxisInverted()` let a UI toggle it on an
|
|
243
|
+
existing chart without losing the current pan/zoom position or manual value-range override,
|
|
244
|
+
the same way `setPluginVisible` toggles a plugin without losing its state:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
toggleButton.addEventListener('click', () => {
|
|
248
|
+
chart.setInvertValueAxis(!chart.isValueAxisInverted());
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Only where each value renders is mirrored — the underlying data isn't. A candle's open/close
|
|
253
|
+
relationship (and therefore its up/down color) still reflects the real values, `formatLegend`
|
|
254
|
+
still shows the real OHLC numbers, and dragging the price axis or panning vertically still
|
|
255
|
+
feels like "grab and slide" in the same screen direction as before; only the sign of what that
|
|
256
|
+
drag does to the value range flips internally to keep it feeling that way. Applies to every
|
|
257
|
+
pane in the stack (see "Multi-pane indicators" below) consistently, not just the main one.
|
|
258
|
+
|
|
259
|
+
One known exception: candlestick's volume bars aren't mapped through the value-axis scale at
|
|
260
|
+
all (they're drawn in a fixed-height strip anchored to the bottom of the pane, independent of
|
|
261
|
+
price — see "Series types" under Architecture), so they stay bottom-anchored regardless of
|
|
262
|
+
`invertValueAxis` rather than flipping to the top with everything else.
|
|
263
|
+
|
|
196
264
|
### Reading chart state
|
|
197
265
|
|
|
198
266
|
Useful for building UI around the canvas (a legend, a toolbar, a "jump to latest" button)
|
|
@@ -455,31 +523,35 @@ for *this* series (a line series wouldn't have a body width or volume bars to co
|
|
|
455
523
|
|
|
456
524
|
### Series types
|
|
457
525
|
|
|
458
|
-
|
|
459
|
-
`WickChart` and `ChartRenderer` are generic over a point shape
|
|
460
|
-
`time`) and delegate every type-specific decision — how to compute
|
|
461
|
-
how to draw the visible points, what a hover legend says — to a
|
|
526
|
+
Candlestick and line are the two chart types today, and nothing above `src/series/` treats
|
|
527
|
+
either specially. `WickChart` and `ChartRenderer` are generic over a point shape
|
|
528
|
+
(`SeriesPoint` — just a `time`) and delegate every type-specific decision — how to compute
|
|
529
|
+
the value-axis range, how to draw the visible points, what a hover legend says — to a
|
|
462
530
|
`SeriesDefinition` (see `src/series/types.ts`) resolved at construction time from
|
|
463
531
|
`options.type` via a small registry (`src/series/registry.ts`). `src/series/candlestick.ts`
|
|
464
|
-
|
|
465
|
-
why importing `wick-charts` at all is enough to make
|
|
466
|
-
registering anything.
|
|
467
|
-
|
|
468
|
-
Adding a
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
532
|
+
and `src/series/line.ts` both register themselves (as `'candlestick'`/`'line'`) on import,
|
|
533
|
+
which is why importing `wick-charts` at all is enough to make either type available without
|
|
534
|
+
the caller registering anything.
|
|
535
|
+
|
|
536
|
+
Adding a chart type (area, bar, ...) means writing one new file that implements
|
|
537
|
+
`SeriesDefinition<TPoint, TStyle>` and calling `registerSeries` on it — `Viewport`, event
|
|
538
|
+
handling, data loading, and WASM scale dispatch are all untouched, and every existing chart
|
|
539
|
+
of another type keeps working exactly as before; `src/series/line.ts` is a second, smaller
|
|
540
|
+
worked example of this alongside candlestick's own. This is the extension point the `type`
|
|
541
|
+
option and `style` option are built around: `style` is whatever shape the chosen series's
|
|
542
|
+
`defaultStyle` declares (candlestick's is `{ upColor, downColor, ... }`, line's is `{
|
|
543
|
+
lineColor, lineWidth }`), merged over that default rather than hardcoded into the chart
|
|
544
|
+
itself.
|
|
475
545
|
|
|
476
546
|
`options.type` is a plain string the registry resolves at runtime, so `new WickChart(canvas,
|
|
477
547
|
{ type: 'candlestick', style: {...} })` type-checks even if `style` has nothing to do with
|
|
478
548
|
`CandlestickStyle` — nothing ties a runtime string to a specific `TPoint`/`TStyle` pair at the
|
|
479
|
-
type level. `createCandlestickChart()` (in `src/index.ts`)
|
|
480
|
-
|
|
481
|
-
should export an equivalent `create<Name>Chart` next
|
|
482
|
-
`WickChartOptions` itself, so each series's style shape stays
|
|
549
|
+
type level. `createCandlestickChart()`/`createLineChart()` (both in `src/index.ts`) are the
|
|
550
|
+
fix for the two built-in types: a thin wrapper per series that pins both generics so its
|
|
551
|
+
`style` is fully checked. A new series should export an equivalent `create<Name>Chart` next
|
|
552
|
+
to it rather than widening `WickChartOptions` itself, so each series's style shape stays
|
|
553
|
+
independent of every other's — line's factory is the second proof this pattern holds up, not
|
|
554
|
+
just a one-off written for candlestick.
|
|
483
555
|
|
|
484
556
|
### Plugins (markers, annotations, drawing tools)
|
|
485
557
|
|
|
@@ -598,15 +670,17 @@ hover state — and on-demand history loading via `setDataLoader`. Per-candle vo
|
|
|
598
670
|
the bottom fifth of the chart when a candle has `volume`, and are entirely omitted (nothing
|
|
599
671
|
drawn, nothing reserved) for data that doesn't.
|
|
600
672
|
Coordinate scaling runs on WASM once a frame's point count crosses the threshold, JS below
|
|
601
|
-
it. Candlestick
|
|
673
|
+
it. Candlestick and line are the two registered series types so far (`createCandlestickChart`/
|
|
674
|
+
`createLineChart`); the plugin extension point (draw
|
|
602
675
|
overlays plus, now, claimable pointer gestures for interactive tools — see "Plugins" above)
|
|
603
676
|
has no built-in users (see "Indicators" above for why) beyond `demo/index.html`'s example. No
|
|
604
677
|
concrete drawing tool ships yet, only the mechanism a trend line or similar would be built
|
|
605
678
|
on. `addPane`/`removePane` let a plugin-drawn indicator (RSI, MACD, ...) reserve its own
|
|
606
679
|
horizontal strip with an independent value axis — see "Multi-pane indicators" above; volume
|
|
607
680
|
still shares the candlestick pane rather than getting its own, since it draws through the
|
|
608
|
-
series itself, not a pane-targeted plugin.
|
|
609
|
-
|
|
681
|
+
series itself, not a pane-targeted plugin. `invertValueAxis`/`setInvertValueAxis` mirror the
|
|
682
|
+
whole stack's value axis top-to-bottom without touching the underlying data — see "Inverting
|
|
683
|
+
the value axis" above. See [CHANGELOG.md](./CHANGELOG.md) for what shipped in each release.
|
|
610
684
|
|
|
611
685
|
## License
|
|
612
686
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { DataLoader } from './dataSource.js';
|
|
2
2
|
import type { ChartPlugin } from './plugins/types.js';
|
|
3
3
|
import type { CandlestickStyle } from './series/candlestick.js';
|
|
4
|
-
import type {
|
|
5
|
-
|
|
4
|
+
import type { LineStyle } from './series/line.js';
|
|
5
|
+
import type { Candle, LinePoint, PaneOptions, WickChartOptions, SeriesPoint, ValueRange } from './types.js';
|
|
6
|
+
export type { BusinessDay, Candle, LinePoint, PaneOptions, WickChartOptions, WickTime, SeriesPoint, UnixMillis, ValueRange, } from './types.js';
|
|
6
7
|
export type { DataLoader, DataRequest } from './dataSource.js';
|
|
7
8
|
export type { ChartPlugin, ChartPointerEvent, PluginRenderApi } from './plugins/types.js';
|
|
8
9
|
export { distanceToSegment, hitTestPoint, hitTestSegment } from './hitTest.js';
|
|
@@ -11,7 +12,9 @@ export { mergeSeriesPoints } from './mergeSeries.js';
|
|
|
11
12
|
export { registerSeries, getSeries } from './series/registry.js';
|
|
12
13
|
export type { SeriesDefinition, SeriesDrawContext } from './series/types.js';
|
|
13
14
|
export type { CandlestickStyle } from './series/candlestick.js';
|
|
15
|
+
export type { LineStyle } from './series/line.js';
|
|
14
16
|
export { candlestickSeries } from './series/candlestick.js';
|
|
17
|
+
export { lineSeries } from './series/line.js';
|
|
15
18
|
export { LinearScale } from './scale.js';
|
|
16
19
|
export { toUnixSeconds } from './time.js';
|
|
17
20
|
export { Viewport } from './viewport.js';
|
|
@@ -73,6 +76,11 @@ export declare class WickChart<TPoint extends SeriesPoint = Candle> {
|
|
|
73
76
|
* every threshold crossing until `setData` resets it (a fresh dataset
|
|
74
77
|
* may come from a different source that does have more). */
|
|
75
78
|
private exhausted;
|
|
79
|
+
/** Mirrors `this.renderer`'s own copy (set at construction, kept in sync
|
|
80
|
+
* by `setInvertValueAxis`) — needed here too since pointer-event value
|
|
81
|
+
* conversion and price-axis drag direction happen outside `render()`,
|
|
82
|
+
* where only `WickChart` (not `ChartRenderer`) is involved. */
|
|
83
|
+
private invertValueAxis;
|
|
76
84
|
constructor(canvas: HTMLCanvasElement, options?: WickChartOptions);
|
|
77
85
|
setData(points: TPoint[]): this;
|
|
78
86
|
/**
|
|
@@ -154,6 +162,17 @@ export declare class WickChart<TPoint extends SeriesPoint = Candle> {
|
|
|
154
162
|
/** The point currently under the cursor (crosshair/legend target), or
|
|
155
163
|
* `null` when nothing is hovered. */
|
|
156
164
|
getHoveredPoint(): TPoint | null;
|
|
165
|
+
/**
|
|
166
|
+
* Mirrors the value axis top-to-bottom (or restores it) and re-renders —
|
|
167
|
+
* see `WickChartOptions.invertValueAxis` for what this actually changes.
|
|
168
|
+
* A live toggle, unlike most other style options: pan/zoom/valueRangeOverride
|
|
169
|
+
* state is untouched, so a "flip" button can call this on an existing
|
|
170
|
+
* chart without losing the user's current view, the same way
|
|
171
|
+
* `setPluginVisible` toggles a plugin without losing its state.
|
|
172
|
+
*/
|
|
173
|
+
setInvertValueAxis(inverted: boolean): this;
|
|
174
|
+
/** Whether the value axis is currently mirrored — see `setInvertValueAxis`. */
|
|
175
|
+
isValueAxisInverted(): boolean;
|
|
157
176
|
/** Removes all attached listeners. Call on unmount — the mouseup
|
|
158
177
|
* listener is on `window` (so drags don't get stuck if the cursor
|
|
159
178
|
* leaves the canvas mid-drag) and won't be garbage-collected on its own. */
|
|
@@ -268,3 +287,13 @@ export declare class WickChart<TPoint extends SeriesPoint = Candle> {
|
|
|
268
287
|
export declare function createCandlestickChart(canvas: HTMLCanvasElement, options?: Omit<WickChartOptions, 'type' | 'style'> & {
|
|
269
288
|
style?: Partial<CandlestickStyle>;
|
|
270
289
|
}): WickChart<Candle>;
|
|
290
|
+
/**
|
|
291
|
+
* The second series type's equivalent of `createCandlestickChart` above —
|
|
292
|
+
* pins `TPoint` (`LinePoint`) and `TStyle` (`LineStyle`) so `style` is
|
|
293
|
+
* fully checked here the same way, rather than accepted as the untyped
|
|
294
|
+
* `Record<string, unknown>` `WickChartOptions.style` allows for `new
|
|
295
|
+
* WickChart(canvas, { type: 'line', style })`.
|
|
296
|
+
*/
|
|
297
|
+
export declare function createLineChart(canvas: HTMLCanvasElement, options?: Omit<WickChartOptions, 'type' | 'style'> & {
|
|
298
|
+
style?: Partial<LineStyle>;
|
|
299
|
+
}): WickChart<LinePoint>;
|
package/dist/index.js
CHANGED
|
@@ -3,19 +3,21 @@ import { computePaneLayout } from './paneLayout.js';
|
|
|
3
3
|
import { ChartRenderer } from './renderer.js';
|
|
4
4
|
import { getSeries } from './series/registry.js';
|
|
5
5
|
import { toUnixSeconds } from './time.js';
|
|
6
|
+
import { pixelToValue, valueToPixel } from './valueAxis.js';
|
|
6
7
|
import { Viewport } from './viewport.js';
|
|
7
8
|
import { loadWasm } from './wasm.js';
|
|
8
9
|
import { importRealWasm } from './wasmImporter.js';
|
|
9
10
|
export { distanceToSegment, hitTestPoint, hitTestSegment } from './hitTest.js';
|
|
10
11
|
export { mergeSeriesPoints } from './mergeSeries.js';
|
|
11
12
|
export { registerSeries, getSeries } from './series/registry.js';
|
|
12
|
-
// Also registers
|
|
13
|
-
// src/series/candlestick.ts and src/series/registry.ts.
|
|
14
|
-
// gets the same treatment: implement SeriesDefinition,
|
|
15
|
-
// have the consuming app import it directly before
|
|
16
|
-
// that type), and `type: '<its key>'` becomes
|
|
17
|
-
// to this file.
|
|
13
|
+
// Also registers 'candlestick'/'line' as a module-load side effect — see
|
|
14
|
+
// src/series/candlestick.ts, src/series/line.ts, and src/series/registry.ts.
|
|
15
|
+
// A new series type gets the same treatment: implement SeriesDefinition,
|
|
16
|
+
// export it here (or have the consuming app import it directly before
|
|
17
|
+
// constructing a chart of that type), and `type: '<its key>'` becomes
|
|
18
|
+
// usable with no other change to this file.
|
|
18
19
|
export { candlestickSeries } from './series/candlestick.js';
|
|
20
|
+
export { lineSeries } from './series/line.js';
|
|
19
21
|
export { LinearScale } from './scale.js';
|
|
20
22
|
export { toUnixSeconds } from './time.js';
|
|
21
23
|
export { Viewport } from './viewport.js';
|
|
@@ -301,6 +303,7 @@ export class WickChart {
|
|
|
301
303
|
};
|
|
302
304
|
this.seriesDefinition = getSeries(options?.type ?? 'candlestick');
|
|
303
305
|
this.renderer = new ChartRenderer(canvas, this.seriesDefinition, options);
|
|
306
|
+
this.invertValueAxis = options?.invertValueAxis ?? false;
|
|
304
307
|
this.viewport = new Viewport(0);
|
|
305
308
|
// Without this, a touch drag on the canvas also scrolls/zooms the page
|
|
306
309
|
// underneath it — the browser's native touch gestures and this class's
|
|
@@ -463,6 +466,24 @@ export class WickChart {
|
|
|
463
466
|
getHoveredPoint() {
|
|
464
467
|
return this.hoverIndex === null ? null : (this.sorted[this.hoverIndex] ?? null);
|
|
465
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Mirrors the value axis top-to-bottom (or restores it) and re-renders —
|
|
471
|
+
* see `WickChartOptions.invertValueAxis` for what this actually changes.
|
|
472
|
+
* A live toggle, unlike most other style options: pan/zoom/valueRangeOverride
|
|
473
|
+
* state is untouched, so a "flip" button can call this on an existing
|
|
474
|
+
* chart without losing the user's current view, the same way
|
|
475
|
+
* `setPluginVisible` toggles a plugin without losing its state.
|
|
476
|
+
*/
|
|
477
|
+
setInvertValueAxis(inverted) {
|
|
478
|
+
this.invertValueAxis = inverted;
|
|
479
|
+
this.renderer.setInvertValueAxis(inverted);
|
|
480
|
+
this.scheduleRender();
|
|
481
|
+
return this;
|
|
482
|
+
}
|
|
483
|
+
/** Whether the value axis is currently mirrored — see `setInvertValueAxis`. */
|
|
484
|
+
isValueAxisInverted() {
|
|
485
|
+
return this.invertValueAxis;
|
|
486
|
+
}
|
|
466
487
|
/** Removes all attached listeners. Call on unmount — the mouseup
|
|
467
488
|
* listener is on `window` (so drags don't get stuck if the cursor
|
|
468
489
|
* leaves the canvas mid-drag) and won't be garbage-collected on its own. */
|
|
@@ -526,7 +547,13 @@ export class WickChart {
|
|
|
526
547
|
// Dragging down moves the visible value window down (content
|
|
527
548
|
// follows the cursor), matching the horizontal drag's "grab and
|
|
528
549
|
// slide" feel — see the pan call above for the mirrored X case.
|
|
529
|
-
|
|
550
|
+
// invertValueAxis flips which value-space direction "down the
|
|
551
|
+
// screen" corresponds to (see src/valueAxis.ts), so the sign here
|
|
552
|
+
// has to flip too or an inverted chart would pan backwards relative
|
|
553
|
+
// to the drag — negated, not re-derived, since the *magnitude* is
|
|
554
|
+
// identical either way.
|
|
555
|
+
const directionSign = this.invertValueAxis ? -1 : 1;
|
|
556
|
+
this.viewport.panValueRange(deltaYDevice * valuePerPixel * directionSign);
|
|
530
557
|
}
|
|
531
558
|
this.scheduleRender();
|
|
532
559
|
}
|
|
@@ -673,7 +700,7 @@ export class WickChart {
|
|
|
673
700
|
const chartHeight = this.mainPaneHeight();
|
|
674
701
|
if (!range || chartHeight <= 0)
|
|
675
702
|
return null;
|
|
676
|
-
return range.min
|
|
703
|
+
return pixelToValue(y, range.min, range.max, chartHeight, this.invertValueAxis);
|
|
677
704
|
}
|
|
678
705
|
/** x pixel -> global (possibly fractional) index — the exact inverse of
|
|
679
706
|
* the renderer's own `xForIndex`, so a pointer event lines up with
|
|
@@ -701,7 +728,7 @@ export class WickChart {
|
|
|
701
728
|
const chartHeight = this.mainPaneHeight();
|
|
702
729
|
if (!range || chartHeight <= 0)
|
|
703
730
|
return null;
|
|
704
|
-
return
|
|
731
|
+
return valueToPixel(value, range.min, range.max, chartHeight, this.invertValueAxis);
|
|
705
732
|
}
|
|
706
733
|
pointerEventAt(x, y) {
|
|
707
734
|
return {
|
|
@@ -781,3 +808,13 @@ export class WickChart {
|
|
|
781
808
|
export function createCandlestickChart(canvas, options) {
|
|
782
809
|
return new WickChart(canvas, { ...options, type: 'candlestick' });
|
|
783
810
|
}
|
|
811
|
+
/**
|
|
812
|
+
* The second series type's equivalent of `createCandlestickChart` above —
|
|
813
|
+
* pins `TPoint` (`LinePoint`) and `TStyle` (`LineStyle`) so `style` is
|
|
814
|
+
* fully checked here the same way, rather than accepted as the untyped
|
|
815
|
+
* `Record<string, unknown>` `WickChartOptions.style` allows for `new
|
|
816
|
+
* WickChart(canvas, { type: 'line', style })`.
|
|
817
|
+
*/
|
|
818
|
+
export function createLineChart(canvas, options) {
|
|
819
|
+
return new WickChart(canvas, { ...options, type: 'line' });
|
|
820
|
+
}
|
package/dist/renderer.d.ts
CHANGED
|
@@ -48,7 +48,14 @@ export declare class ChartRenderer<TPoint extends SeriesPoint> {
|
|
|
48
48
|
private axis;
|
|
49
49
|
private crosshair;
|
|
50
50
|
private legend;
|
|
51
|
+
/** Unlike the style groups above, mutable after construction — see
|
|
52
|
+
* `setInvertValueAxis`. A live toggle, not a one-time style choice, is
|
|
53
|
+
* the whole point of this option (a "what if this series moved the
|
|
54
|
+
* opposite way" view a user flips on and off), so it doesn't get the
|
|
55
|
+
* "resolved once in the constructor" treatment those get. */
|
|
56
|
+
private invertValueAxis;
|
|
51
57
|
constructor(canvas: HTMLCanvasElement, seriesDefinition: SeriesDefinition<TPoint, unknown>, options?: WickChartOptions);
|
|
58
|
+
setInvertValueAxis(inverted: boolean): void;
|
|
52
59
|
/** Pixel width of the point-plotting area — excludes the price-axis
|
|
53
60
|
* strip on the right. Exposed so `WickChart` can convert cursor pixel
|
|
54
61
|
* positions to point indices / values for hit-testing and dragging. */
|
package/dist/renderer.js
CHANGED
|
@@ -2,6 +2,7 @@ import { formatAxisLabel, formatHoverTime, pickTickIndices } from './axis.js';
|
|
|
2
2
|
import { createScale } from './hybridScale.js';
|
|
3
3
|
import { computePaneLayout } from './paneLayout.js';
|
|
4
4
|
import { formatPrice, niceTicks } from './priceAxis.js';
|
|
5
|
+
import { pixelToValue, valueAxisPixelRange } from './valueAxis.js';
|
|
5
6
|
const DEFAULT_BACKGROUND = 'transparent';
|
|
6
7
|
const DEFAULT_FONT = {
|
|
7
8
|
family: 'sans-serif',
|
|
@@ -61,6 +62,10 @@ export class ChartRenderer {
|
|
|
61
62
|
this.axis = { ...DEFAULT_AXIS, ...options.axis };
|
|
62
63
|
this.crosshair = { ...DEFAULT_CROSSHAIR, ...options.crosshair };
|
|
63
64
|
this.legend = { ...DEFAULT_LEGEND, ...options.legend };
|
|
65
|
+
this.invertValueAxis = options.invertValueAxis ?? false;
|
|
66
|
+
}
|
|
67
|
+
setInvertValueAxis(inverted) {
|
|
68
|
+
this.invertValueAxis = inverted;
|
|
64
69
|
}
|
|
65
70
|
/** Pixel width of the point-plotting area — excludes the price-axis
|
|
66
71
|
* strip on the right. Exposed so `WickChart` can convert cursor pixel
|
|
@@ -112,16 +117,20 @@ export class ChartRenderer {
|
|
|
112
117
|
// JS below a few hundred points, WASM above — see hybridScale.ts.
|
|
113
118
|
// Whichever it picks, `dispose()` must run once we're done reading
|
|
114
119
|
// from it (a no-op on the JS path, a real WASM memory free otherwise).
|
|
115
|
-
|
|
120
|
+
// valueAxisPixelRange picks which pixel end is the domain minimum —
|
|
121
|
+
// the one thing invertValueAxis actually changes about this call.
|
|
122
|
+
const { scale: yScale, dispose: disposeYScale } = createScale(valueMin, valueMax, ...valueAxisPixelRange(chartHeight, this.invertValueAxis), visible.length);
|
|
116
123
|
// Every indicator pane gets the exact same treatment as the main pane
|
|
117
124
|
// — its own value domain (from `PaneOptions.getValueRange`) and its
|
|
118
125
|
// own JS/WASM scale over its own pixel height — kept alive for the
|
|
119
126
|
// whole frame alongside `yScale`, since plugins targeting a pane draw
|
|
120
|
-
// only after every pane's axis has already been rendered.
|
|
127
|
+
// only after every pane's axis has already been rendered. Inverted the
|
|
128
|
+
// same way as the main pane so a chart's invertValueAxis option
|
|
129
|
+
// mirrors its whole stack consistently, not just the price pane.
|
|
121
130
|
const paneScales = paneRects.map((rect, i) => {
|
|
122
131
|
const pane = panes[i];
|
|
123
132
|
const { min, max } = pane.getValueRange();
|
|
124
|
-
const { scale, dispose } = createScale(min, max, rect.height,
|
|
133
|
+
const { scale, dispose } = createScale(min, max, ...valueAxisPixelRange(rect.height, this.invertValueAxis), visible.length);
|
|
125
134
|
return { pane, rect, min, max, scale, dispose };
|
|
126
135
|
});
|
|
127
136
|
// Flipped in `finally`, right before every scale above frees its WASM
|
|
@@ -142,7 +151,21 @@ export class ChartRenderer {
|
|
|
142
151
|
// Exact inverse of xForIndex above — solving
|
|
143
152
|
// `x = (index - viewport.startIndex) * slotWidth + slotWidth / 2` for `index`.
|
|
144
153
|
const indexForX = (x) => viewport.startIndex + (x - slotWidth / 2) / slotWidth;
|
|
145
|
-
|
|
154
|
+
// save/restore isolates whatever canvas state a series's draw()
|
|
155
|
+
// touches (lineWidth, line dash, ...) from the axis/crosshair/plugin
|
|
156
|
+
// drawing that follows — the same isolation each plugin already gets
|
|
157
|
+
// around its own draw() call below. Without this, a property no
|
|
158
|
+
// series happened to set before (lineSeries.draw() is the first
|
|
159
|
+
// built-in one to set ctx.lineWidth) would silently leak into every
|
|
160
|
+
// subsequent stroke() this frame, including axis boundary lines,
|
|
161
|
+
// grid lines, and the crosshair.
|
|
162
|
+
ctx.save();
|
|
163
|
+
try {
|
|
164
|
+
seriesDefinition.draw({ ctx, visible, startIndex: startIdx, xForIndex, slotWidth, yScale, chartHeight }, style);
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
ctx.restore();
|
|
168
|
+
}
|
|
146
169
|
const priceStep = this.currentPriceStep(valueMin, valueMax);
|
|
147
170
|
this.renderPriceAxis(valueMin, valueMax, priceStep, yScale, chartWidth, chartHeight, mainRect.top);
|
|
148
171
|
for (const { rect, min, max, scale } of paneScales) {
|
|
@@ -235,9 +258,10 @@ export class ChartRenderer {
|
|
|
235
258
|
},
|
|
236
259
|
indexForX,
|
|
237
260
|
// Exact inverse of the mapping above: subtract the pane's top offset
|
|
238
|
-
// before inverting the same
|
|
239
|
-
//
|
|
240
|
-
|
|
261
|
+
// before inverting the same value<->pixel mapping createScale set up
|
|
262
|
+
// for it (see src/valueAxis.ts — same invertValueAxis flag, so this
|
|
263
|
+
// stays consistent with whichever direction the pane actually drew in).
|
|
264
|
+
valueForY: (y) => pixelToValue(y - rect.top, valueMin, valueMax, rect.height, this.invertValueAxis),
|
|
241
265
|
visibleStartIndex,
|
|
242
266
|
visibleEndIndex,
|
|
243
267
|
allPoints,
|
|
@@ -344,8 +368,9 @@ export class ChartRenderer {
|
|
|
344
368
|
ctx.restore();
|
|
345
369
|
if (priceLineVisible) {
|
|
346
370
|
// Exact inverse of the value->y mapping createScale set up for this
|
|
347
|
-
// frame — same
|
|
348
|
-
|
|
371
|
+
// frame — same helper (and same invertValueAxis flag) as
|
|
372
|
+
// PluginRenderApi.valueForY, see src/valueAxis.ts.
|
|
373
|
+
const value = pixelToValue(hoverY, valueMin, valueMax, chartHeight, this.invertValueAxis);
|
|
349
374
|
this.renderPriceLabelChip(formatPrice(value, priceStep), hoverY, chartWidth);
|
|
350
375
|
}
|
|
351
376
|
this.renderTimeLabelChip(formatHoverTime(timeSeconds), x, chartHeight, canvas.width);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LinePoint } from '../types.js';
|
|
2
|
+
import type { SeriesDefinition } from './types.js';
|
|
3
|
+
/** The second built-in series type — a plain single-value line, the
|
|
4
|
+
* simplest possible `SeriesDefinition` beyond candlestick's OHLC. Exists as
|
|
5
|
+
* much to exercise the registry with a genuinely different `TPoint`/`TStyle`
|
|
6
|
+
* pair as to be useful on its own; see `src/series/candlestick.ts` for the
|
|
7
|
+
* reference implementation this mirrors. */
|
|
8
|
+
export interface LineStyle {
|
|
9
|
+
/** Stroke color. Defaults to `'#2196f3'`. */
|
|
10
|
+
lineColor: string;
|
|
11
|
+
/** Stroke width, in px. Defaults to 1.5. */
|
|
12
|
+
lineWidth: number;
|
|
13
|
+
}
|
|
14
|
+
export declare const lineSeries: SeriesDefinition<LinePoint, LineStyle>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { fitRange } from '../priceRange.js';
|
|
2
|
+
import { registerSeries } from './registry.js';
|
|
3
|
+
const DEFAULT_STYLE = {
|
|
4
|
+
lineColor: '#2196f3',
|
|
5
|
+
lineWidth: 1.5,
|
|
6
|
+
};
|
|
7
|
+
/** Falls back to a fixed `[0, 1]` range when every visible point is a gap
|
|
8
|
+
* (`NaN`/non-finite `value`) — `Math.min()`/`Math.max()` over an empty
|
|
9
|
+
* array are `Infinity`/`-Infinity`, which would otherwise feed `fitRange`
|
|
10
|
+
* a `NaN` midpoint. Candlestick never needs this: `Candle`'s OHLC fields
|
|
11
|
+
* aren't optional or gap-tolerant the way a line point's `value` is. */
|
|
12
|
+
function getValueRange(visible, scaleFactor) {
|
|
13
|
+
const values = visible.map((p) => p.value).filter((v) => Number.isFinite(v));
|
|
14
|
+
if (values.length === 0)
|
|
15
|
+
return { min: 0, max: 1 };
|
|
16
|
+
return fitRange(Math.min(...values), Math.max(...values), scaleFactor);
|
|
17
|
+
}
|
|
18
|
+
function draw(context, style) {
|
|
19
|
+
const { ctx, visible, startIndex, xForIndex, yScale } = context;
|
|
20
|
+
if (visible.length === 0)
|
|
21
|
+
return;
|
|
22
|
+
// Batched through mapMany (one call per array) rather than once per
|
|
23
|
+
// point in the loop below — same reasoning as candlestick's draw(): it's
|
|
24
|
+
// what lets the WASM path pay the JS<->WASM boundary cost once per frame.
|
|
25
|
+
// NaN values map to NaN here, harmlessly — skipped below rather than
|
|
26
|
+
// filtered out first, so `ys[i]` still lines up with `visible[i]`.
|
|
27
|
+
const ys = yScale.mapMany(visible.map((p) => p.value));
|
|
28
|
+
ctx.strokeStyle = style.lineColor;
|
|
29
|
+
ctx.lineWidth = style.lineWidth;
|
|
30
|
+
ctx.beginPath();
|
|
31
|
+
// `drawing` tracks whether the path is mid-segment — a non-finite value
|
|
32
|
+
// (a gap in the data) breaks it, and the line resumes fresh at the next
|
|
33
|
+
// real value rather than jumping straight across the gap.
|
|
34
|
+
let drawing = false;
|
|
35
|
+
visible.forEach((point, i) => {
|
|
36
|
+
if (!Number.isFinite(point.value)) {
|
|
37
|
+
drawing = false;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const x = xForIndex(startIndex + i);
|
|
41
|
+
const y = ys[i];
|
|
42
|
+
if (drawing) {
|
|
43
|
+
ctx.lineTo(x, y);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
ctx.moveTo(x, y);
|
|
47
|
+
drawing = true;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
ctx.stroke();
|
|
51
|
+
}
|
|
52
|
+
function formatLegend(point) {
|
|
53
|
+
return [`Value ${Number.isFinite(point.value) ? point.value.toLocaleString('en-US') : '—'}`];
|
|
54
|
+
}
|
|
55
|
+
export const lineSeries = {
|
|
56
|
+
type: 'line',
|
|
57
|
+
defaultStyle: DEFAULT_STYLE,
|
|
58
|
+
getValueRange,
|
|
59
|
+
draw,
|
|
60
|
+
formatLegend,
|
|
61
|
+
};
|
|
62
|
+
// Registered as a module-level side effect, same as candlestickSeries — see
|
|
63
|
+
// src/series/candlestick.ts for why. src/index.ts imports this file so
|
|
64
|
+
// 'line' is available the moment the package itself is imported.
|
|
65
|
+
registerSeries(lineSeries);
|
package/dist/types.d.ts
CHANGED
|
@@ -57,6 +57,18 @@ export interface Candle extends SeriesPoint {
|
|
|
57
57
|
close: number;
|
|
58
58
|
volume?: number;
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* A single value plotted against time — the point shape for the built-in
|
|
62
|
+
* `'line'` series (see `src/series/line.ts`), and the simplest possible
|
|
63
|
+
* instance of `SeriesPoint` beyond `Candle`: a plain time series with
|
|
64
|
+
* nothing OHLC-specific about it. `value` is `NaN`-tolerant: a `NaN`
|
|
65
|
+
* (or non-finite) value is treated as a gap — the line breaks there and
|
|
66
|
+
* resumes at the next real value, rather than plotting a bogus point or
|
|
67
|
+
* throwing.
|
|
68
|
+
*/
|
|
69
|
+
export interface LinePoint extends SeriesPoint {
|
|
70
|
+
value: number;
|
|
71
|
+
}
|
|
60
72
|
/** Text styling shared by every label the chart draws — axis ticks,
|
|
61
73
|
* crosshair axis labels, and the hover legend. `axisSize`/`legendSize` are
|
|
62
74
|
* separate since the legend has historically been drawn one px larger to
|
|
@@ -161,9 +173,9 @@ export interface WickChartOptions {
|
|
|
161
173
|
/**
|
|
162
174
|
* Which registered series type to render this chart as (see
|
|
163
175
|
* `registerSeries` in `src/series/registry.ts`). Defaults to
|
|
164
|
-
* `'candlestick'
|
|
165
|
-
* new one is a matter of implementing `SeriesDefinition` and
|
|
166
|
-
* it, without changing `WickChart` or `ChartRenderer` at all.
|
|
176
|
+
* `'candlestick'`; `'line'` is the other type built into the library —
|
|
177
|
+
* adding a new one is a matter of implementing `SeriesDefinition` and
|
|
178
|
+
* registering it, without changing `WickChart` or `ChartRenderer` at all.
|
|
167
179
|
*/
|
|
168
180
|
type?: string;
|
|
169
181
|
/** Background color of the canvas. Defaults to transparent. Chart-wide
|
|
@@ -187,4 +199,15 @@ export interface WickChartOptions {
|
|
|
187
199
|
crosshair?: ChartCrosshairOptions;
|
|
188
200
|
/** Hover legend coloring. Merged over the built-in defaults field by field. */
|
|
189
201
|
legend?: ChartLegendOptions;
|
|
202
|
+
/**
|
|
203
|
+
* Mirrors the value axis top-to-bottom — every pane's higher values
|
|
204
|
+
* render lower on screen instead of higher, with no change to the
|
|
205
|
+
* underlying data (a candle's open/close relationship, and therefore
|
|
206
|
+
* its up/down color, is unaffected). Defaults to `false`. Useful for a
|
|
207
|
+
* "what if this series had moved the opposite way" view. Can also be
|
|
208
|
+
* toggled after construction via `WickChart.setInvertValueAxis` without
|
|
209
|
+
* losing pan/zoom state — see `src/valueAxis.ts` for the mapping this
|
|
210
|
+
* flips.
|
|
211
|
+
*/
|
|
212
|
+
invertValueAxis?: boolean;
|
|
190
213
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure pixel<->value mapping helpers for a chart's value axis, factored out
|
|
3
|
+
* so `WickChartOptions.invertValueAxis` (see `src/index.ts`) has exactly
|
|
4
|
+
* one place to change the y-direction convention instead of several
|
|
5
|
+
* independently re-derived copies of the same formula. Before this file
|
|
6
|
+
* existed, `ChartRenderer`'s hover-crosshair value readout, its per-pane
|
|
7
|
+
* `PluginRenderApi.valueForY`, and `WickChart`'s own pointer-event
|
|
8
|
+
* `value`/`yForValue` each inlined the same "pixel -> value" arithmetic
|
|
9
|
+
* separately — harmless while there was only ever one direction, but
|
|
10
|
+
* exactly the kind of duplication that turns "add an invert option" into
|
|
11
|
+
* "find and fix four copies of a formula, hope none were missed."
|
|
12
|
+
*
|
|
13
|
+
* The forward, per-point-in-a-frame hot path stays on `Scale`
|
|
14
|
+
* (`src/hybridScale.ts`, JS or WASM) for its own reasons — batched
|
|
15
|
+
* `mapMany`, no per-point allocation. `valueAxisPixelRange` only decides
|
|
16
|
+
* which pixel end a `Scale` should treat as the domain minimum, so
|
|
17
|
+
* inverting is a one-line change to how a `Scale` gets constructed rather
|
|
18
|
+
* than a second rendering path. `valueToPixel`/`pixelToValue` below are for
|
|
19
|
+
* the comparatively rare user-gesture paths (hover crosshair, pointer
|
|
20
|
+
* events, price-axis dragging) where a `Scale` instance either doesn't
|
|
21
|
+
* exist yet (these happen between frames) or isn't worth constructing for
|
|
22
|
+
* a single one-off conversion.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* The `[rangeMin, rangeMax]` pair to construct a value-axis `Scale` with:
|
|
26
|
+
* `createScale(domainMin, domainMax, ...valueAxisPixelRange(pixelSpan, inverted))`.
|
|
27
|
+
* Not inverted (the default for every chart): the domain maximum renders
|
|
28
|
+
* at pixel 0 (the top of the pane) and the minimum at `pixelSpan` (the
|
|
29
|
+
* bottom). Inverted: the same two pixels, swapped — every value renders
|
|
30
|
+
* mirrored top-to-bottom, with no change to the underlying data.
|
|
31
|
+
*/
|
|
32
|
+
export declare function valueAxisPixelRange(pixelSpan: number, inverted: boolean): [number, number];
|
|
33
|
+
/**
|
|
34
|
+
* value -> pixel, the exact forward direction `valueAxisPixelRange` sets a
|
|
35
|
+
* `Scale` up for. `pixelSpan` is the pane's own height (or a candidate
|
|
36
|
+
* one — this has no dependency on `Scale` or a live frame).
|
|
37
|
+
*/
|
|
38
|
+
export declare function valueToPixel(value: number, valueMin: number, valueMax: number, pixelSpan: number, inverted: boolean): number;
|
|
39
|
+
/**
|
|
40
|
+
* pixel -> value, the exact inverse of `valueToPixel` above —
|
|
41
|
+
* `valueToPixel(pixelToValue(pixel, ...), ...) === pixel` for any `pixel`
|
|
42
|
+
* in `[0, pixelSpan]`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function pixelToValue(pixel: number, valueMin: number, valueMax: number, pixelSpan: number, inverted: boolean): number;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure pixel<->value mapping helpers for a chart's value axis, factored out
|
|
3
|
+
* so `WickChartOptions.invertValueAxis` (see `src/index.ts`) has exactly
|
|
4
|
+
* one place to change the y-direction convention instead of several
|
|
5
|
+
* independently re-derived copies of the same formula. Before this file
|
|
6
|
+
* existed, `ChartRenderer`'s hover-crosshair value readout, its per-pane
|
|
7
|
+
* `PluginRenderApi.valueForY`, and `WickChart`'s own pointer-event
|
|
8
|
+
* `value`/`yForValue` each inlined the same "pixel -> value" arithmetic
|
|
9
|
+
* separately — harmless while there was only ever one direction, but
|
|
10
|
+
* exactly the kind of duplication that turns "add an invert option" into
|
|
11
|
+
* "find and fix four copies of a formula, hope none were missed."
|
|
12
|
+
*
|
|
13
|
+
* The forward, per-point-in-a-frame hot path stays on `Scale`
|
|
14
|
+
* (`src/hybridScale.ts`, JS or WASM) for its own reasons — batched
|
|
15
|
+
* `mapMany`, no per-point allocation. `valueAxisPixelRange` only decides
|
|
16
|
+
* which pixel end a `Scale` should treat as the domain minimum, so
|
|
17
|
+
* inverting is a one-line change to how a `Scale` gets constructed rather
|
|
18
|
+
* than a second rendering path. `valueToPixel`/`pixelToValue` below are for
|
|
19
|
+
* the comparatively rare user-gesture paths (hover crosshair, pointer
|
|
20
|
+
* events, price-axis dragging) where a `Scale` instance either doesn't
|
|
21
|
+
* exist yet (these happen between frames) or isn't worth constructing for
|
|
22
|
+
* a single one-off conversion.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* The `[rangeMin, rangeMax]` pair to construct a value-axis `Scale` with:
|
|
26
|
+
* `createScale(domainMin, domainMax, ...valueAxisPixelRange(pixelSpan, inverted))`.
|
|
27
|
+
* Not inverted (the default for every chart): the domain maximum renders
|
|
28
|
+
* at pixel 0 (the top of the pane) and the minimum at `pixelSpan` (the
|
|
29
|
+
* bottom). Inverted: the same two pixels, swapped — every value renders
|
|
30
|
+
* mirrored top-to-bottom, with no change to the underlying data.
|
|
31
|
+
*/
|
|
32
|
+
export function valueAxisPixelRange(pixelSpan, inverted) {
|
|
33
|
+
return inverted ? [0, pixelSpan] : [pixelSpan, 0];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* value -> pixel, the exact forward direction `valueAxisPixelRange` sets a
|
|
37
|
+
* `Scale` up for. `pixelSpan` is the pane's own height (or a candidate
|
|
38
|
+
* one — this has no dependency on `Scale` or a live frame).
|
|
39
|
+
*/
|
|
40
|
+
export function valueToPixel(value, valueMin, valueMax, pixelSpan, inverted) {
|
|
41
|
+
const t = (value - valueMin) / (valueMax - valueMin);
|
|
42
|
+
return inverted ? t * pixelSpan : (1 - t) * pixelSpan;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* pixel -> value, the exact inverse of `valueToPixel` above —
|
|
46
|
+
* `valueToPixel(pixelToValue(pixel, ...), ...) === pixel` for any `pixel`
|
|
47
|
+
* in `[0, pixelSpan]`.
|
|
48
|
+
*/
|
|
49
|
+
export function pixelToValue(pixel, valueMin, valueMax, pixelSpan, inverted) {
|
|
50
|
+
const t = inverted ? pixel / pixelSpan : 1 - pixel / pixelSpan;
|
|
51
|
+
return valueMin + t * (valueMax - valueMin);
|
|
52
|
+
}
|