wick-charts 0.3.0 → 0.5.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 +129 -22
- package/dist/index.d.ts +60 -2
- package/dist/index.js +88 -9
- package/dist/paneLayout.d.ts +38 -0
- package/dist/paneLayout.js +49 -0
- package/dist/plugins/types.d.ts +11 -0
- package/dist/renderer.d.ts +27 -1
- package/dist/renderer.js +147 -38
- package/dist/series/line.d.ts +14 -0
- package/dist/series/line.js +65 -0
- package/dist/types.d.ts +51 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,10 +17,12 @@ 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)
|
|
21
22
|
- [Reading chart state](#reading-chart-state)
|
|
22
23
|
- [Loading more history on demand](#loading-more-history-on-demand)
|
|
23
24
|
- [Extending: plugins](#extending-plugins)
|
|
25
|
+
- [Multi-pane indicators](#multi-pane-indicators)
|
|
24
26
|
- [Cleanup](#cleanup)
|
|
25
27
|
- [Architecture](#architecture)
|
|
26
28
|
- [Development](#development)
|
|
@@ -135,6 +137,39 @@ it — a dataset with no `volume` at all renders exactly as if the feature didn'
|
|
|
135
137
|
array) is safe. It resets pan/zoom/hover state — call it for a genuinely new dataset, and use
|
|
136
138
|
`setDataLoader()` (below) to extend the current one instead.
|
|
137
139
|
|
|
140
|
+
### Line charts
|
|
141
|
+
|
|
142
|
+
For a plain time series with no OHLC shape — an equity curve, a metric over time, anything
|
|
143
|
+
that's just one number per point — `createLineChart` is the line-series equivalent of
|
|
144
|
+
`createCandlestickChart` above. A line point is `{ time, value }`:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { createLineChart } from 'wick-charts';
|
|
148
|
+
|
|
149
|
+
const chart = createLineChart(canvas, {
|
|
150
|
+
style: { lineColor: '#2196f3', lineWidth: 1.5 }, // both shown here are the defaults
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
chart.setData([
|
|
154
|
+
{ time: '2024-01-01T00:00:00Z', value: 100 },
|
|
155
|
+
{ time: '2024-01-02T00:00:00Z', value: 103.4 },
|
|
156
|
+
{ time: '2024-01-03T00:00:00Z', value: 101.8 },
|
|
157
|
+
// ...
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
chart.render();
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Everything else — pan/zoom/hover, `setDataLoader`, `addPlugin`, `addPane`, `background`/
|
|
164
|
+
`font`/`axis`/`crosshair`/`legend` styling — works exactly as it does for a candlestick chart,
|
|
165
|
+
since none of it is specific to what's actually plotted (see "Series types" under
|
|
166
|
+
Architecture). `value` accepts `NaN` (or any non-finite number) as an explicit gap: the line
|
|
167
|
+
breaks there and resumes at the next real value, rather than plotting a bogus point or
|
|
168
|
+
throwing — useful for a series with missing data at some points without pre-filtering it
|
|
169
|
+
yourself. The hover legend shows `Value <number>` (or `Value —` for a hovered gap) in place of
|
|
170
|
+
candlestick's OHLC breakdown; `new WickChart(canvas, { type: 'line' })` also works, the same
|
|
171
|
+
untyped escape hatch `type: 'candlestick'` has, if you'd rather not import the factory.
|
|
172
|
+
|
|
138
173
|
### Styling
|
|
139
174
|
|
|
140
175
|
Every visual aspect of the chart is an option — nothing is a fixed constant you can't reach.
|
|
@@ -354,6 +389,58 @@ or validated for uniqueness, just compared with `===` when you call `setPluginVi
|
|
|
354
389
|
plugin with no `id` still works exactly as before; it just can't be targeted that way, only by
|
|
355
390
|
holding onto its reference and calling `removePlugin` directly.
|
|
356
391
|
|
|
392
|
+
### Multi-pane indicators
|
|
393
|
+
|
|
394
|
+
An oscillator like RSI or MACD has a value domain that has nothing to do with price (RSI's
|
|
395
|
+
fixed `[0, 100]`, say) — drawing it as a `ChartPlugin` overlay in the price pane would either
|
|
396
|
+
get swamped by the candles or need a hand-rolled rescale hack. `addPane` reserves a horizontal
|
|
397
|
+
strip below the main price pane with its own independent value axis, and a plugin's `paneId`
|
|
398
|
+
routes its `draw()` there instead of the price pane:
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
chart.addPane({
|
|
402
|
+
id: 'rsi',
|
|
403
|
+
heightRatio: 0.25, // share of the total plotting height this pane occupies; defaults to 0.25
|
|
404
|
+
getValueRange: () => ({ min: 0, max: 100 }), // this pane's own value-axis domain, called every frame
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
chart.addPlugin({
|
|
408
|
+
paneId: 'rsi', // routes this plugin into the 'rsi' pane instead of the price pane
|
|
409
|
+
draw({ ctx, allPoints, visibleStartIndex, visibleEndIndex, xForIndex, yForValue }) {
|
|
410
|
+
const rsi = computeRsi(allPoints.map((c) => c.close), 14); // your own indicator math — see below
|
|
411
|
+
ctx.strokeStyle = '#bb86fc';
|
|
412
|
+
ctx.beginPath();
|
|
413
|
+
for (let i = visibleStartIndex; i < visibleEndIndex; i++) {
|
|
414
|
+
const x = xForIndex(i);
|
|
415
|
+
const y = yForValue(rsi[i]); // mapped against *this pane's* [0, 100], not price
|
|
416
|
+
i === visibleStartIndex ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
|
417
|
+
}
|
|
418
|
+
ctx.stroke();
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
The pane itself draws nothing but a separator line and its own right-side axis (ticks resolved
|
|
424
|
+
from `getValueRange()`, styled through the same `axis` options as the price axis) — exactly the
|
|
425
|
+
same "core provides layout, the app provides the math" split plugins already use for indicator
|
|
426
|
+
*overlays*, just for indicators that need their own scale instead of sharing the price one. See
|
|
427
|
+
`demo/index.html` for a complete worked example (an RSI pane built entirely in the demo's own
|
|
428
|
+
code, same as the moving-average overlay above it — see "Indicators" below for why neither
|
|
429
|
+
ships in the library itself).
|
|
430
|
+
|
|
431
|
+
Every declared pane stacks below the previous one in call order, each shrinking the main
|
|
432
|
+
pane's share of the plotting height; `removePane(id)` gives that space back. A plugin whose
|
|
433
|
+
`paneId` doesn't match any currently-added pane falls back to drawing in the price pane rather
|
|
434
|
+
than silently disappearing — useful if you remove a pane before removing the plugins that
|
|
435
|
+
targeted it. `getPanes()` returns every declared pane's `id`/`heightRatio`, the same
|
|
436
|
+
snapshot-for-building-a-management-UI idea `getPlugins()` already offers for plugins.
|
|
437
|
+
|
|
438
|
+
The hover crosshair's dashed vertical line spans every pane so a hovered candle lines up
|
|
439
|
+
across the whole stack; the horizontal line, price-label chip, and OHLC legend stay scoped to
|
|
440
|
+
the price pane — an indicator pane's own hover readout, if you want one, is something its own
|
|
441
|
+
plugin draws (it has the same `xForIndex`/`yForValue` a price-pane plugin does, just mapped
|
|
442
|
+
against that pane's own value domain and pixel rect).
|
|
443
|
+
|
|
357
444
|
### Cleanup
|
|
358
445
|
|
|
359
446
|
Call `chart.destroy()` when you're done with a chart (component unmount, etc.) — it removes a
|
|
@@ -402,31 +489,35 @@ for *this* series (a line series wouldn't have a body width or volume bars to co
|
|
|
402
489
|
|
|
403
490
|
### Series types
|
|
404
491
|
|
|
405
|
-
|
|
406
|
-
`WickChart` and `ChartRenderer` are generic over a point shape
|
|
407
|
-
`time`) and delegate every type-specific decision — how to compute
|
|
408
|
-
how to draw the visible points, what a hover legend says — to a
|
|
492
|
+
Candlestick and line are the two chart types today, and nothing above `src/series/` treats
|
|
493
|
+
either specially. `WickChart` and `ChartRenderer` are generic over a point shape
|
|
494
|
+
(`SeriesPoint` — just a `time`) and delegate every type-specific decision — how to compute
|
|
495
|
+
the value-axis range, how to draw the visible points, what a hover legend says — to a
|
|
409
496
|
`SeriesDefinition` (see `src/series/types.ts`) resolved at construction time from
|
|
410
497
|
`options.type` via a small registry (`src/series/registry.ts`). `src/series/candlestick.ts`
|
|
411
|
-
|
|
412
|
-
why importing `wick-charts` at all is enough to make
|
|
413
|
-
registering anything.
|
|
414
|
-
|
|
415
|
-
Adding a
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
498
|
+
and `src/series/line.ts` both register themselves (as `'candlestick'`/`'line'`) on import,
|
|
499
|
+
which is why importing `wick-charts` at all is enough to make either type available without
|
|
500
|
+
the caller registering anything.
|
|
501
|
+
|
|
502
|
+
Adding a chart type (area, bar, ...) means writing one new file that implements
|
|
503
|
+
`SeriesDefinition<TPoint, TStyle>` and calling `registerSeries` on it — `Viewport`, event
|
|
504
|
+
handling, data loading, and WASM scale dispatch are all untouched, and every existing chart
|
|
505
|
+
of another type keeps working exactly as before; `src/series/line.ts` is a second, smaller
|
|
506
|
+
worked example of this alongside candlestick's own. This is the extension point the `type`
|
|
507
|
+
option and `style` option are built around: `style` is whatever shape the chosen series's
|
|
508
|
+
`defaultStyle` declares (candlestick's is `{ upColor, downColor, ... }`, line's is `{
|
|
509
|
+
lineColor, lineWidth }`), merged over that default rather than hardcoded into the chart
|
|
510
|
+
itself.
|
|
422
511
|
|
|
423
512
|
`options.type` is a plain string the registry resolves at runtime, so `new WickChart(canvas,
|
|
424
513
|
{ type: 'candlestick', style: {...} })` type-checks even if `style` has nothing to do with
|
|
425
514
|
`CandlestickStyle` — nothing ties a runtime string to a specific `TPoint`/`TStyle` pair at the
|
|
426
|
-
type level. `createCandlestickChart()` (in `src/index.ts`)
|
|
427
|
-
|
|
428
|
-
should export an equivalent `create<Name>Chart` next
|
|
429
|
-
`WickChartOptions` itself, so each series's style shape stays
|
|
515
|
+
type level. `createCandlestickChart()`/`createLineChart()` (both in `src/index.ts`) are the
|
|
516
|
+
fix for the two built-in types: a thin wrapper per series that pins both generics so its
|
|
517
|
+
`style` is fully checked. A new series should export an equivalent `create<Name>Chart` next
|
|
518
|
+
to it rather than widening `WickChartOptions` itself, so each series's style shape stays
|
|
519
|
+
independent of every other's — line's factory is the second proof this pattern holds up, not
|
|
520
|
+
just a one-off written for candlestick.
|
|
430
521
|
|
|
431
522
|
### Plugins (markers, annotations, drawing tools)
|
|
432
523
|
|
|
@@ -469,6 +560,18 @@ two things `draw()` alone can't give it, both added specifically to make that bu
|
|
|
469
560
|
`ChartRenderer.render` uses, recomputed on demand since pointer events happen between
|
|
470
561
|
frames, not during one.
|
|
471
562
|
|
|
563
|
+
`ChartPlugin.paneId` is a third, narrower option on top of the two above — it doesn't change
|
|
564
|
+
what a plugin implements, only which pane's `PluginRenderApi` it receives. `ChartRenderer`
|
|
565
|
+
builds one `PluginRenderApi` per pane per frame (`buildPluginApi`, sharing a `FrameGeometry` for
|
|
566
|
+
the parts every pane has in common — the time axis and the frame-ended guard) and routes each
|
|
567
|
+
plugin to the one matching its `paneId`, defaulting to the main pane. A pane-targeted plugin's
|
|
568
|
+
`yForValue`/`valueForY` are pane-local (mapped against that pane's own `getValueRange()`) but
|
|
569
|
+
still return/accept absolute canvas pixels, exactly like the main pane's — so a plugin never
|
|
570
|
+
needs to know whether it's drawing in the price pane or a declared one, only which `paneId` it
|
|
571
|
+
was given. Pointer gestures (`onPointerDown`/etc.) aren't pane-aware yet: they're still offered
|
|
572
|
+
chart-wide the same way regardless of any plugin's `paneId`, matching the mechanism's original
|
|
573
|
+
scope (drawing tools on the price series) rather than a limitation specific to panes.
|
|
574
|
+
|
|
472
575
|
### Indicators (moving averages, Bollinger Bands, ...): deliberately not included
|
|
473
576
|
|
|
474
577
|
wick-charts ships the extension point (`ChartPlugin`, `allPoints`, `xForIndex`/`yForValue`)
|
|
@@ -533,12 +636,16 @@ hover state — and on-demand history loading via `setDataLoader`. Per-candle vo
|
|
|
533
636
|
the bottom fifth of the chart when a candle has `volume`, and are entirely omitted (nothing
|
|
534
637
|
drawn, nothing reserved) for data that doesn't.
|
|
535
638
|
Coordinate scaling runs on WASM once a frame's point count crosses the threshold, JS below
|
|
536
|
-
it. Candlestick
|
|
639
|
+
it. Candlestick and line are the two registered series types so far (`createCandlestickChart`/
|
|
640
|
+
`createLineChart`); the plugin extension point (draw
|
|
537
641
|
overlays plus, now, claimable pointer gestures for interactive tools — see "Plugins" above)
|
|
538
642
|
has no built-in users (see "Indicators" above for why) beyond `demo/index.html`'s example. No
|
|
539
643
|
concrete drawing tool ships yet, only the mechanism a trend line or similar would be built
|
|
540
|
-
on.
|
|
541
|
-
|
|
644
|
+
on. `addPane`/`removePane` let a plugin-drawn indicator (RSI, MACD, ...) reserve its own
|
|
645
|
+
horizontal strip with an independent value axis — see "Multi-pane indicators" above; volume
|
|
646
|
+
still shares the candlestick pane rather than getting its own, since it draws through the
|
|
647
|
+
series itself, not a pane-targeted plugin. See [CHANGELOG.md](./CHANGELOG.md) for what shipped
|
|
648
|
+
in each release.
|
|
542
649
|
|
|
543
650
|
## License
|
|
544
651
|
|
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';
|
|
@@ -41,6 +44,11 @@ export declare class WickChart<TPoint extends SeriesPoint = Candle> {
|
|
|
41
44
|
* motionless while the pointer moves within that candle's column). */
|
|
42
45
|
private hoverY;
|
|
43
46
|
private plugins;
|
|
47
|
+
/** Indicator/oscillator panes declared via `addPane`, defaults already
|
|
48
|
+
* resolved — see `ResolvedPaneOptions`. Empty until an app adds one; a
|
|
49
|
+
* chart that never calls `addPane` renders exactly as it did before
|
|
50
|
+
* panes existed (single price pane filling the whole plotting height). */
|
|
51
|
+
private panes;
|
|
44
52
|
/** The plugin whose `onPointerDown` returned `true` for the pointer
|
|
45
53
|
* currently down, or `null` when no plugin has claimed the current
|
|
46
54
|
* gesture (the common case — the chart handles it itself). */
|
|
@@ -95,6 +103,35 @@ export declare class WickChart<TPoint extends SeriesPoint = Candle> {
|
|
|
95
103
|
* and re-renders. A no-op, not an error, if nothing matches — plugins
|
|
96
104
|
* with no `id` set are never matched. */
|
|
97
105
|
setPluginVisible(id: string, visible: boolean): this;
|
|
106
|
+
/**
|
|
107
|
+
* Reserves a horizontal strip below the main price pane (and below any
|
|
108
|
+
* previously-added pane — panes stack in call order) for an indicator or
|
|
109
|
+
* oscillator, drawn entirely by `ChartPlugin`s registered with a
|
|
110
|
+
* matching `paneId` (see `ChartPlugin.paneId`). The pane itself computes
|
|
111
|
+
* nothing: `options.getValueRange` supplies whatever value-axis domain
|
|
112
|
+
* makes sense for what will be plotted into it (a fixed `[0, 100]` for
|
|
113
|
+
* RSI, an auto-fit range closed over a MACD series a plugin already
|
|
114
|
+
* tracks, ...) — the same "core provides layout, the app provides the
|
|
115
|
+
* math" split `addPlugin` already uses for indicator overlays on the
|
|
116
|
+
* main pane. A no-op on layout until at least one plugin actually
|
|
117
|
+
* targets this pane's `id`; an empty pane still reserves its space and
|
|
118
|
+
* draws its own axis, just with nothing inside it.
|
|
119
|
+
*/
|
|
120
|
+
addPane(options: PaneOptions): this;
|
|
121
|
+
/** Removes a previously-added pane by `id` and re-renders. A no-op, not
|
|
122
|
+
* an error, if nothing matches. Plugins still targeting the removed
|
|
123
|
+
* pane's `id` via `ChartPlugin.paneId` fall back to drawing in the main
|
|
124
|
+
* pane rather than being silently dropped — see the doc comment on
|
|
125
|
+
* `ChartPlugin.paneId`. */
|
|
126
|
+
removePane(id: string): this;
|
|
127
|
+
/** Every currently-declared pane's `id` and resolved `heightRatio`, in
|
|
128
|
+
* stacking order (top to bottom, main pane excluded since it always
|
|
129
|
+
* exists and always sits first) — for an app building a management UI
|
|
130
|
+
* around indicator panes without maintaining its own parallel list. */
|
|
131
|
+
getPanes(): readonly {
|
|
132
|
+
id: string;
|
|
133
|
+
heightRatio: number;
|
|
134
|
+
}[];
|
|
98
135
|
render(): void;
|
|
99
136
|
/** Coalesces render() calls into at most one per animation frame. Mouse
|
|
100
137
|
* events (drag, wheel) can fire far faster than the display refreshes —
|
|
@@ -163,6 +200,17 @@ export declare class WickChart<TPoint extends SeriesPoint = Candle> {
|
|
|
163
200
|
* on-demand (`valueForY`, dispatched pointer events) rather than only
|
|
164
201
|
* during `render()`. `null` when there's nothing to compute one from. */
|
|
165
202
|
private frameValueRange;
|
|
203
|
+
/** The main price pane's own pixel height for the *next* render — as
|
|
204
|
+
* opposed to `this.renderer.chartHeight`, which is the whole pane
|
|
205
|
+
* stack's height (main pane plus every declared indicator pane below
|
|
206
|
+
* it). Every pixel<->value conversion outside of `ChartRenderer.render`
|
|
207
|
+
* itself (price-axis drag-to-scale, `ChartPointerEvent.value`, ...) is
|
|
208
|
+
* about the main pane specifically — pointer gestures and price-axis
|
|
209
|
+
* dragging aren't pane-aware yet, so they only ever mean the main price
|
|
210
|
+
* pane — and has to divide by this, not the full stack, or dragging
|
|
211
|
+
* would run at the wrong speed (or a hovered value would come out
|
|
212
|
+
* wrong) as soon as an app adds its first indicator pane. */
|
|
213
|
+
private mainPaneHeight;
|
|
166
214
|
/** y pixel -> value in the range the next render would use. `null` if
|
|
167
215
|
* there's no data or no usable chart area to compute one against — see
|
|
168
216
|
* `ChartPointerEvent.value`. */
|
|
@@ -223,3 +271,13 @@ export declare class WickChart<TPoint extends SeriesPoint = Candle> {
|
|
|
223
271
|
export declare function createCandlestickChart(canvas: HTMLCanvasElement, options?: Omit<WickChartOptions, 'type' | 'style'> & {
|
|
224
272
|
style?: Partial<CandlestickStyle>;
|
|
225
273
|
}): WickChart<Candle>;
|
|
274
|
+
/**
|
|
275
|
+
* The second series type's equivalent of `createCandlestickChart` above —
|
|
276
|
+
* pins `TPoint` (`LinePoint`) and `TStyle` (`LineStyle`) so `style` is
|
|
277
|
+
* fully checked here the same way, rather than accepted as the untyped
|
|
278
|
+
* `Record<string, unknown>` `WickChartOptions.style` allows for `new
|
|
279
|
+
* WickChart(canvas, { type: 'line', style })`.
|
|
280
|
+
*/
|
|
281
|
+
export declare function createLineChart(canvas: HTMLCanvasElement, options?: Omit<WickChartOptions, 'type' | 'style'> & {
|
|
282
|
+
style?: Partial<LineStyle>;
|
|
283
|
+
}): WickChart<LinePoint>;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mergeSeriesPoints } from './mergeSeries.js';
|
|
2
|
+
import { computePaneLayout } from './paneLayout.js';
|
|
2
3
|
import { ChartRenderer } from './renderer.js';
|
|
3
4
|
import { getSeries } from './series/registry.js';
|
|
4
5
|
import { toUnixSeconds } from './time.js';
|
|
@@ -8,13 +9,14 @@ import { importRealWasm } from './wasmImporter.js';
|
|
|
8
9
|
export { distanceToSegment, hitTestPoint, hitTestSegment } from './hitTest.js';
|
|
9
10
|
export { mergeSeriesPoints } from './mergeSeries.js';
|
|
10
11
|
export { registerSeries, getSeries } from './series/registry.js';
|
|
11
|
-
// Also registers
|
|
12
|
-
// src/series/candlestick.ts and src/series/registry.ts.
|
|
13
|
-
// gets the same treatment: implement SeriesDefinition,
|
|
14
|
-
// have the consuming app import it directly before
|
|
15
|
-
// that type), and `type: '<its key>'` becomes
|
|
16
|
-
// to this file.
|
|
12
|
+
// Also registers 'candlestick'/'line' as a module-load side effect — see
|
|
13
|
+
// src/series/candlestick.ts, src/series/line.ts, and src/series/registry.ts.
|
|
14
|
+
// A new series type gets the same treatment: implement SeriesDefinition,
|
|
15
|
+
// export it here (or have the consuming app import it directly before
|
|
16
|
+
// constructing a chart of that type), and `type: '<its key>'` becomes
|
|
17
|
+
// usable with no other change to this file.
|
|
17
18
|
export { candlestickSeries } from './series/candlestick.js';
|
|
19
|
+
export { lineSeries } from './series/line.js';
|
|
18
20
|
export { LinearScale } from './scale.js';
|
|
19
21
|
export { toUnixSeconds } from './time.js';
|
|
20
22
|
export { Viewport } from './viewport.js';
|
|
@@ -26,6 +28,14 @@ const DEFAULT_VISIBLE_POINTS = 120;
|
|
|
26
28
|
/** How close (in points) the visible window has to get to either edge of
|
|
27
29
|
* the loaded data before `setDataLoader`'s loader is asked for more. */
|
|
28
30
|
const DEFAULT_LOAD_THRESHOLD = 20;
|
|
31
|
+
/** `PaneOptions.heightRatio`'s default — see `addPane`. */
|
|
32
|
+
const DEFAULT_PANE_HEIGHT_RATIO = 0.25;
|
|
33
|
+
/** `PaneOptions.getValueRange`'s default — see `addPane`. Fixed at
|
|
34
|
+
* `[0, 1]` rather than auto-fitting to anything, since the pane has no
|
|
35
|
+
* data of its own to fit to: only a plugin drawing into it knows what
|
|
36
|
+
* range makes sense, which is exactly why `getValueRange` exists to be
|
|
37
|
+
* overridden. */
|
|
38
|
+
const DEFAULT_PANE_VALUE_RANGE = { min: 0, max: 1 };
|
|
29
39
|
/** How long a single finger has to stay down before a still-in-progress
|
|
30
40
|
* 'pan' touch switches to 'scrub' mode (touch has no hover, so this is its
|
|
31
41
|
* substitute — hold to inspect a point instead of panning past it). */
|
|
@@ -57,6 +67,11 @@ export class WickChart {
|
|
|
57
67
|
* motionless while the pointer moves within that candle's column). */
|
|
58
68
|
this.hoverY = null;
|
|
59
69
|
this.plugins = [];
|
|
70
|
+
/** Indicator/oscillator panes declared via `addPane`, defaults already
|
|
71
|
+
* resolved — see `ResolvedPaneOptions`. Empty until an app adds one; a
|
|
72
|
+
* chart that never calls `addPane` renders exactly as it did before
|
|
73
|
+
* panes existed (single price pane filling the whole plotting height). */
|
|
74
|
+
this.panes = [];
|
|
60
75
|
/** The plugin whose `onPointerDown` returned `true` for the pointer
|
|
61
76
|
* currently down, or `null` when no plugin has claimed the current
|
|
62
77
|
* gesture (the common case — the chart handles it itself). */
|
|
@@ -353,6 +368,46 @@ export class WickChart {
|
|
|
353
368
|
this.scheduleRender();
|
|
354
369
|
return this;
|
|
355
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Reserves a horizontal strip below the main price pane (and below any
|
|
373
|
+
* previously-added pane — panes stack in call order) for an indicator or
|
|
374
|
+
* oscillator, drawn entirely by `ChartPlugin`s registered with a
|
|
375
|
+
* matching `paneId` (see `ChartPlugin.paneId`). The pane itself computes
|
|
376
|
+
* nothing: `options.getValueRange` supplies whatever value-axis domain
|
|
377
|
+
* makes sense for what will be plotted into it (a fixed `[0, 100]` for
|
|
378
|
+
* RSI, an auto-fit range closed over a MACD series a plugin already
|
|
379
|
+
* tracks, ...) — the same "core provides layout, the app provides the
|
|
380
|
+
* math" split `addPlugin` already uses for indicator overlays on the
|
|
381
|
+
* main pane. A no-op on layout until at least one plugin actually
|
|
382
|
+
* targets this pane's `id`; an empty pane still reserves its space and
|
|
383
|
+
* draws its own axis, just with nothing inside it.
|
|
384
|
+
*/
|
|
385
|
+
addPane(options) {
|
|
386
|
+
this.panes.push({
|
|
387
|
+
id: options.id,
|
|
388
|
+
heightRatio: options.heightRatio ?? DEFAULT_PANE_HEIGHT_RATIO,
|
|
389
|
+
getValueRange: options.getValueRange ?? (() => DEFAULT_PANE_VALUE_RANGE),
|
|
390
|
+
});
|
|
391
|
+
this.scheduleRender();
|
|
392
|
+
return this;
|
|
393
|
+
}
|
|
394
|
+
/** Removes a previously-added pane by `id` and re-renders. A no-op, not
|
|
395
|
+
* an error, if nothing matches. Plugins still targeting the removed
|
|
396
|
+
* pane's `id` via `ChartPlugin.paneId` fall back to drawing in the main
|
|
397
|
+
* pane rather than being silently dropped — see the doc comment on
|
|
398
|
+
* `ChartPlugin.paneId`. */
|
|
399
|
+
removePane(id) {
|
|
400
|
+
this.panes = this.panes.filter((pane) => pane.id !== id);
|
|
401
|
+
this.scheduleRender();
|
|
402
|
+
return this;
|
|
403
|
+
}
|
|
404
|
+
/** Every currently-declared pane's `id` and resolved `heightRatio`, in
|
|
405
|
+
* stacking order (top to bottom, main pane excluded since it always
|
|
406
|
+
* exists and always sits first) — for an app building a management UI
|
|
407
|
+
* around indicator panes without maintaining its own parallel list. */
|
|
408
|
+
getPanes() {
|
|
409
|
+
return this.panes.map(({ id, heightRatio }) => ({ id, heightRatio }));
|
|
410
|
+
}
|
|
356
411
|
render() {
|
|
357
412
|
this.renderer.render({
|
|
358
413
|
sorted: this.sorted,
|
|
@@ -361,6 +416,7 @@ export class WickChart {
|
|
|
361
416
|
hoverIndex: this.hoverIndex,
|
|
362
417
|
hoverY: this.hoverY,
|
|
363
418
|
plugins: this.plugins,
|
|
419
|
+
panes: this.panes,
|
|
364
420
|
});
|
|
365
421
|
this.maybeLoadMore();
|
|
366
422
|
}
|
|
@@ -463,7 +519,7 @@ export class WickChart {
|
|
|
463
519
|
// have opposite sign.
|
|
464
520
|
this.viewport.pan(-deltaXDevice / slotWidth, this.sorted.length);
|
|
465
521
|
}
|
|
466
|
-
const chartHeight = this.
|
|
522
|
+
const chartHeight = this.mainPaneHeight();
|
|
467
523
|
if (chartHeight > 0 && this.viewport.valueRangeOverride) {
|
|
468
524
|
const deltaYDevice = deltaYCss * this.devicePixelScaleY();
|
|
469
525
|
const { min, max } = this.viewport.valueRangeOverride;
|
|
@@ -597,12 +653,25 @@ export class WickChart {
|
|
|
597
653
|
return null;
|
|
598
654
|
return this.seriesDefinition.getValueRange(visible, this.viewport.valueScaleFactor);
|
|
599
655
|
}
|
|
656
|
+
/** The main price pane's own pixel height for the *next* render — as
|
|
657
|
+
* opposed to `this.renderer.chartHeight`, which is the whole pane
|
|
658
|
+
* stack's height (main pane plus every declared indicator pane below
|
|
659
|
+
* it). Every pixel<->value conversion outside of `ChartRenderer.render`
|
|
660
|
+
* itself (price-axis drag-to-scale, `ChartPointerEvent.value`, ...) is
|
|
661
|
+
* about the main pane specifically — pointer gestures and price-axis
|
|
662
|
+
* dragging aren't pane-aware yet, so they only ever mean the main price
|
|
663
|
+
* pane — and has to divide by this, not the full stack, or dragging
|
|
664
|
+
* would run at the wrong speed (or a hovered value would come out
|
|
665
|
+
* wrong) as soon as an app adds its first indicator pane. */
|
|
666
|
+
mainPaneHeight() {
|
|
667
|
+
return computePaneLayout(this.panes, this.renderer.chartHeight).main.height;
|
|
668
|
+
}
|
|
600
669
|
/** y pixel -> value in the range the next render would use. `null` if
|
|
601
670
|
* there's no data or no usable chart area to compute one against — see
|
|
602
671
|
* `ChartPointerEvent.value`. */
|
|
603
672
|
valueForY(y) {
|
|
604
673
|
const range = this.frameValueRange();
|
|
605
|
-
const chartHeight = this.
|
|
674
|
+
const chartHeight = this.mainPaneHeight();
|
|
606
675
|
if (!range || chartHeight <= 0)
|
|
607
676
|
return null;
|
|
608
677
|
return range.min + (1 - y / chartHeight) * (range.max - range.min);
|
|
@@ -630,7 +699,7 @@ export class WickChart {
|
|
|
630
699
|
* `valueForY` returns `null` for. */
|
|
631
700
|
yForValue(value) {
|
|
632
701
|
const range = this.frameValueRange();
|
|
633
|
-
const chartHeight = this.
|
|
702
|
+
const chartHeight = this.mainPaneHeight();
|
|
634
703
|
if (!range || chartHeight <= 0)
|
|
635
704
|
return null;
|
|
636
705
|
return chartHeight * (1 - (value - range.min) / (range.max - range.min));
|
|
@@ -713,3 +782,13 @@ export class WickChart {
|
|
|
713
782
|
export function createCandlestickChart(canvas, options) {
|
|
714
783
|
return new WickChart(canvas, { ...options, type: 'candlestick' });
|
|
715
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* The second series type's equivalent of `createCandlestickChart` above —
|
|
787
|
+
* pins `TPoint` (`LinePoint`) and `TStyle` (`LineStyle`) so `style` is
|
|
788
|
+
* fully checked here the same way, rather than accepted as the untyped
|
|
789
|
+
* `Record<string, unknown>` `WickChartOptions.style` allows for `new
|
|
790
|
+
* WickChart(canvas, { type: 'line', style })`.
|
|
791
|
+
*/
|
|
792
|
+
export function createLineChart(canvas, options) {
|
|
793
|
+
return new WickChart(canvas, { ...options, type: 'line' });
|
|
794
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure layout math for stacking a chart's panes vertically — no canvas, no
|
|
3
|
+
* DOM, fully unit-testable in isolation from `ChartRenderer`. The main
|
|
4
|
+
* (price) pane is never passed in here: it's whatever height remains after
|
|
5
|
+
* every declared pane's `heightRatio` share of the total plotting height
|
|
6
|
+
* (canvas height minus the time-axis strip) is subtracted, which is why it
|
|
7
|
+
* always exists even with zero declared panes.
|
|
8
|
+
*/
|
|
9
|
+
/** One caller-declared pane's layout inputs — a subset of `PaneOptions`
|
|
10
|
+
* (see `src/types.ts`), kept separate so this module doesn't need to know
|
|
11
|
+
* about `getValueRange`/`valueRange` at all. */
|
|
12
|
+
export interface PaneLayoutInput {
|
|
13
|
+
id: string;
|
|
14
|
+
/** Fraction of the total plotting height this pane occupies, before
|
|
15
|
+
* clamping. Clamped into `[MIN_PANE_HEIGHT_RATIO, MAX_TOTAL_PANE_RATIO]`
|
|
16
|
+
* as a share of the whole stack — see `computePaneLayout`. */
|
|
17
|
+
heightRatio: number;
|
|
18
|
+
}
|
|
19
|
+
/** One pane's resolved pixel rect within the plotting area (i.e. relative
|
|
20
|
+
* to the top of the chart, above the time-axis strip — the same origin
|
|
21
|
+
* `ChartRenderer.chartHeight` already uses). */
|
|
22
|
+
export interface PaneRect {
|
|
23
|
+
id: string;
|
|
24
|
+
top: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolves every declared pane's pixel rect plus the main pane's, stacked
|
|
29
|
+
* top (price pane) to bottom (declared panes, in call order) inside
|
|
30
|
+
* `totalHeight` px. Declared ratios are scaled down proportionally (not
|
|
31
|
+
* clamped one by one, which would silently change the relative sizing
|
|
32
|
+
* between panes) whenever their sum would leave the main pane below
|
|
33
|
+
* `MIN_PANE_HEIGHT_RATIO` of the stack.
|
|
34
|
+
*/
|
|
35
|
+
export declare function computePaneLayout(panes: PaneLayoutInput[], totalHeight: number): {
|
|
36
|
+
main: PaneRect;
|
|
37
|
+
panes: PaneRect[];
|
|
38
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure layout math for stacking a chart's panes vertically — no canvas, no
|
|
3
|
+
* DOM, fully unit-testable in isolation from `ChartRenderer`. The main
|
|
4
|
+
* (price) pane is never passed in here: it's whatever height remains after
|
|
5
|
+
* every declared pane's `heightRatio` share of the total plotting height
|
|
6
|
+
* (canvas height minus the time-axis strip) is subtracted, which is why it
|
|
7
|
+
* always exists even with zero declared panes.
|
|
8
|
+
*/
|
|
9
|
+
/** Floor on any single pane's share of the stack, main pane included — a
|
|
10
|
+
* pane asked to render at near-zero height is worse than one slightly
|
|
11
|
+
* taller than requested; nothing usable can be drawn (axis ticks, a
|
|
12
|
+
* legible plot) below this. */
|
|
13
|
+
const MIN_PANE_HEIGHT_RATIO = 0.05;
|
|
14
|
+
/** Ceiling on how much of the total plotting height every declared pane
|
|
15
|
+
* *combined* may claim, leaving at least this much for the main pane even
|
|
16
|
+
* if the sum of requested ratios would otherwise consume it entirely. */
|
|
17
|
+
const MAX_TOTAL_PANE_RATIO = 0.8;
|
|
18
|
+
/**
|
|
19
|
+
* Resolves every declared pane's pixel rect plus the main pane's, stacked
|
|
20
|
+
* top (price pane) to bottom (declared panes, in call order) inside
|
|
21
|
+
* `totalHeight` px. Declared ratios are scaled down proportionally (not
|
|
22
|
+
* clamped one by one, which would silently change the relative sizing
|
|
23
|
+
* between panes) whenever their sum would leave the main pane below
|
|
24
|
+
* `MIN_PANE_HEIGHT_RATIO` of the stack.
|
|
25
|
+
*/
|
|
26
|
+
export function computePaneLayout(panes, totalHeight) {
|
|
27
|
+
const height = Math.max(0, totalHeight);
|
|
28
|
+
if (panes.length === 0) {
|
|
29
|
+
return { main: { id: 'main', top: 0, height }, panes: [] };
|
|
30
|
+
}
|
|
31
|
+
const rawRatios = panes.map((p) => Math.max(MIN_PANE_HEIGHT_RATIO, p.heightRatio));
|
|
32
|
+
const rawTotal = rawRatios.reduce((sum, r) => sum + r, 0);
|
|
33
|
+
// Scale every declared pane down by the same factor if together they'd
|
|
34
|
+
// eat more than MAX_TOTAL_PANE_RATIO of the stack — proportional, so a
|
|
35
|
+
// pane asking for twice another's height still ends up twice as tall.
|
|
36
|
+
const scale = rawTotal > MAX_TOTAL_PANE_RATIO ? MAX_TOTAL_PANE_RATIO / rawTotal : 1;
|
|
37
|
+
const ratios = rawRatios.map((r) => r * scale);
|
|
38
|
+
let top = 0;
|
|
39
|
+
const mainHeight = height * (1 - ratios.reduce((sum, r) => sum + r, 0));
|
|
40
|
+
const main = { id: 'main', top, height: mainHeight };
|
|
41
|
+
top += mainHeight;
|
|
42
|
+
const rects = panes.map((pane, i) => {
|
|
43
|
+
const paneHeight = height * ratios[i];
|
|
44
|
+
const rect = { id: pane.id, top, height: paneHeight };
|
|
45
|
+
top += paneHeight;
|
|
46
|
+
return rect;
|
|
47
|
+
});
|
|
48
|
+
return { main, panes: rects };
|
|
49
|
+
}
|
package/dist/plugins/types.d.ts
CHANGED
|
@@ -117,6 +117,17 @@ export interface ChartPlugin<TPoint extends SeriesPoint = SeriesPoint> {
|
|
|
117
117
|
* directly and call `chart.render()`.
|
|
118
118
|
*/
|
|
119
119
|
visible?: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Routes this plugin's `draw()` into a specific pane instead of the main
|
|
122
|
+
* price pane — the id must match one passed to `WickChart.addPane` (see
|
|
123
|
+
* `PaneOptions.id` in `src/types.ts`). Omitted, or set to `'main'`,
|
|
124
|
+
* keeps today's behavior: the plugin draws in the main price pane. A
|
|
125
|
+
* `paneId` that doesn't match any currently-added pane is treated the
|
|
126
|
+
* same as `'main'` (a plugin never silently stops drawing just because
|
|
127
|
+
* its pane was removed before it was) — call `removePlugin` yourself if
|
|
128
|
+
* that's not what you want when a pane goes away.
|
|
129
|
+
*/
|
|
130
|
+
paneId?: string;
|
|
120
131
|
draw(api: PluginRenderApi<TPoint>): void;
|
|
121
132
|
/**
|
|
122
133
|
* Called on pointer down inside the chart's plotting area (not the
|
package/dist/renderer.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ChartPlugin } from './plugins/types.js';
|
|
2
2
|
import type { SeriesDefinition } from './series/types.js';
|
|
3
|
-
import type { WickChartOptions, SeriesPoint } from './types.js';
|
|
3
|
+
import type { ResolvedPaneOptions, WickChartOptions, SeriesPoint } from './types.js';
|
|
4
4
|
import type { Viewport } from './viewport.js';
|
|
5
5
|
export interface RenderInput<TPoint extends SeriesPoint> {
|
|
6
6
|
/** Every point, sorted ascending by normalized time. */
|
|
@@ -16,6 +16,11 @@ export interface RenderInput<TPoint extends SeriesPoint> {
|
|
|
16
16
|
* position rather than any property of the hovered point itself. */
|
|
17
17
|
hoverY: number | null;
|
|
18
18
|
plugins: ChartPlugin<TPoint>[];
|
|
19
|
+
/** Indicator/oscillator panes declared via `WickChart.addPane`, resolved
|
|
20
|
+
* (defaults applied) — see `ResolvedPaneOptions`. Empty by default, in
|
|
21
|
+
* which case the main pane alone fills the whole plotting height exactly
|
|
22
|
+
* as it did before panes existed. */
|
|
23
|
+
panes: ResolvedPaneOptions[];
|
|
19
24
|
}
|
|
20
25
|
/**
|
|
21
26
|
* The chart engine's renderer: canvas lifecycle, axes, crosshair, and
|
|
@@ -53,11 +58,32 @@ export declare class ChartRenderer<TPoint extends SeriesPoint> {
|
|
|
53
58
|
private axisFont;
|
|
54
59
|
private legendFont;
|
|
55
60
|
render(input: RenderInput<TPoint>): void;
|
|
61
|
+
/**
|
|
62
|
+
* Builds the `PluginRenderApi` for one pane — the main price pane or a
|
|
63
|
+
* declared indicator pane, identical shape either way — from that pane's
|
|
64
|
+
* own rect/value-domain/scale plus whatever `geometry` every pane shares
|
|
65
|
+
* for this frame (shared because there is only one time axis, and one
|
|
66
|
+
* frame-ended flag, for the whole stack; see `FrameGeometry`).
|
|
67
|
+
*/
|
|
68
|
+
private buildPluginApi;
|
|
56
69
|
/** The decimal precision `formatPrice` should use for the current price
|
|
57
70
|
* range — shared by the axis ticks and the crosshair's price label so
|
|
58
71
|
* both display the same value with the same rounding. */
|
|
59
72
|
private currentPriceStep;
|
|
73
|
+
/**
|
|
74
|
+
* Draws one pane's right-side value axis: boundary line, horizontal grid
|
|
75
|
+
* lines, and tick labels. Used for both the main price pane and every
|
|
76
|
+
* indicator pane — `topOffset` shifts everything down by that pane's own
|
|
77
|
+
* position in the stack (0 for the main pane, which sits at the top), so
|
|
78
|
+
* `yScale` only ever has to know about its own pane-local [0, chartHeight]
|
|
79
|
+
* range and never about where that pane lives in the full canvas.
|
|
80
|
+
*/
|
|
60
81
|
private renderPriceAxis;
|
|
82
|
+
/** The horizontal rule separating an indicator pane from whatever sits
|
|
83
|
+
* above it (the main pane, or the previous indicator pane) — the same
|
|
84
|
+
* `axis.lineColor` boundary style `renderTimeAxis` already draws between
|
|
85
|
+
* the plotting area and the time-axis strip. */
|
|
86
|
+
private renderPaneSeparator;
|
|
61
87
|
private renderTimeAxis;
|
|
62
88
|
private renderCrosshairAndLegend;
|
|
63
89
|
/** The OHLC(+volume) tooltip — floats near the hovered pixel like a
|
package/dist/renderer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatAxisLabel, formatHoverTime, pickTickIndices } from './axis.js';
|
|
2
2
|
import { createScale } from './hybridScale.js';
|
|
3
|
+
import { computePaneLayout } from './paneLayout.js';
|
|
3
4
|
import { formatPrice, niceTicks } from './priceAxis.js';
|
|
4
5
|
const DEFAULT_BACKGROUND = 'transparent';
|
|
5
6
|
const DEFAULT_FONT = {
|
|
@@ -81,18 +82,25 @@ export class ChartRenderer {
|
|
|
81
82
|
}
|
|
82
83
|
render(input) {
|
|
83
84
|
const { ctx, canvas, background, seriesDefinition, style } = this;
|
|
84
|
-
const { sorted, times, viewport, hoverIndex, hoverY, plugins } = input;
|
|
85
|
+
const { sorted, times, viewport, hoverIndex, hoverY, plugins, panes } = input;
|
|
85
86
|
const width = canvas.width;
|
|
86
87
|
const height = canvas.height;
|
|
87
88
|
const chartWidth = this.chartWidth;
|
|
88
|
-
|
|
89
|
+
// Full stack height: the main price pane plus every declared indicator
|
|
90
|
+
// pane below it. `this.chartHeight` predates panes and named what's
|
|
91
|
+
// now only true with zero of them — kept as the property name (public
|
|
92
|
+
// API reads it through) but renamed locally here since most of this
|
|
93
|
+
// method cares about one pane's height, not the stack's.
|
|
94
|
+
const stackHeight = this.chartHeight;
|
|
89
95
|
ctx.clearRect(0, 0, width, height);
|
|
90
96
|
if (background !== 'transparent') {
|
|
91
97
|
ctx.fillStyle = background;
|
|
92
98
|
ctx.fillRect(0, 0, width, height);
|
|
93
99
|
}
|
|
94
|
-
if (sorted.length === 0 || chartWidth <= 0 ||
|
|
100
|
+
if (sorted.length === 0 || chartWidth <= 0 || stackHeight <= 0)
|
|
95
101
|
return;
|
|
102
|
+
const { main: mainRect, panes: paneRects } = computePaneLayout(panes, stackHeight);
|
|
103
|
+
const chartHeight = mainRect.height;
|
|
96
104
|
const startIdx = Math.max(0, Math.floor(viewport.startIndex));
|
|
97
105
|
const endIdx = Math.min(sorted.length, Math.ceil(viewport.endIndex));
|
|
98
106
|
const visible = sorted.slice(startIdx, endIdx);
|
|
@@ -105,51 +113,92 @@ export class ChartRenderer {
|
|
|
105
113
|
// Whichever it picks, `dispose()` must run once we're done reading
|
|
106
114
|
// from it (a no-op on the JS path, a real WASM memory free otherwise).
|
|
107
115
|
const { scale: yScale, dispose: disposeYScale } = createScale(valueMin, valueMax, chartHeight, 0, visible.length);
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
116
|
+
// Every indicator pane gets the exact same treatment as the main pane
|
|
117
|
+
// — its own value domain (from `PaneOptions.getValueRange`) and its
|
|
118
|
+
// own JS/WASM scale over its own pixel height — kept alive for the
|
|
119
|
+
// whole frame alongside `yScale`, since plugins targeting a pane draw
|
|
120
|
+
// only after every pane's axis has already been rendered.
|
|
121
|
+
const paneScales = paneRects.map((rect, i) => {
|
|
122
|
+
const pane = panes[i];
|
|
123
|
+
const { min, max } = pane.getValueRange();
|
|
124
|
+
const { scale, dispose } = createScale(min, max, rect.height, 0, visible.length);
|
|
125
|
+
return { pane, rect, min, max, scale, dispose };
|
|
126
|
+
});
|
|
127
|
+
// Flipped in `finally`, right before every scale above frees its WASM
|
|
128
|
+
// backing memory (a no-op on the JS path). Guards `yForValue` below so
|
|
129
|
+
// a plugin that stashes it and calls it later gets a clear thrown
|
|
130
|
+
// error instead of touching freed memory — see the interface-level
|
|
131
|
+
// warning on `PluginRenderApi`. An object (not a plain `let`) so every
|
|
132
|
+
// pane's plugin-api closure, built by `buildPluginApi` below, shares
|
|
133
|
+
// the same flag instead of each capturing its own.
|
|
134
|
+
const frameState = { ended: false };
|
|
114
135
|
try {
|
|
115
136
|
const slotWidth = chartWidth / viewport.visibleCount;
|
|
116
137
|
// x position for a *global* sorted-array index — honors the (possibly
|
|
117
138
|
// fractional) viewport.startIndex so panning is pixel-smooth, not
|
|
118
|
-
// stepped a whole point at a time.
|
|
139
|
+
// stepped a whole point at a time. Shared by every pane: there is
|
|
140
|
+
// only one time axis for the whole stack.
|
|
119
141
|
const xForIndex = (globalIndex) => (globalIndex - viewport.startIndex) * slotWidth + slotWidth / 2;
|
|
120
|
-
|
|
142
|
+
// Exact inverse of xForIndex above — solving
|
|
143
|
+
// `x = (index - viewport.startIndex) * slotWidth + slotWidth / 2` for `index`.
|
|
144
|
+
const indexForX = (x) => viewport.startIndex + (x - slotWidth / 2) / slotWidth;
|
|
145
|
+
// save/restore isolates whatever canvas state a series's draw()
|
|
146
|
+
// touches (lineWidth, line dash, ...) from the axis/crosshair/plugin
|
|
147
|
+
// drawing that follows — the same isolation each plugin already gets
|
|
148
|
+
// around its own draw() call below. Without this, a property no
|
|
149
|
+
// series happened to set before (lineSeries.draw() is the first
|
|
150
|
+
// built-in one to set ctx.lineWidth) would silently leak into every
|
|
151
|
+
// subsequent stroke() this frame, including axis boundary lines,
|
|
152
|
+
// grid lines, and the crosshair.
|
|
153
|
+
ctx.save();
|
|
154
|
+
try {
|
|
155
|
+
seriesDefinition.draw({ ctx, visible, startIndex: startIdx, xForIndex, slotWidth, yScale, chartHeight }, style);
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
ctx.restore();
|
|
159
|
+
}
|
|
121
160
|
const priceStep = this.currentPriceStep(valueMin, valueMax);
|
|
122
|
-
this.renderPriceAxis(valueMin, valueMax, priceStep, yScale, chartWidth, chartHeight);
|
|
123
|
-
|
|
161
|
+
this.renderPriceAxis(valueMin, valueMax, priceStep, yScale, chartWidth, chartHeight, mainRect.top);
|
|
162
|
+
for (const { rect, min, max, scale } of paneScales) {
|
|
163
|
+
this.renderPaneSeparator(rect.top, chartWidth);
|
|
164
|
+
const step = this.currentPriceStep(min, max);
|
|
165
|
+
this.renderPriceAxis(min, max, step, scale, chartWidth, rect.height, rect.top);
|
|
166
|
+
}
|
|
167
|
+
this.renderTimeAxis(times, startIdx, visible.length, stackHeight, chartWidth, xForIndex);
|
|
124
168
|
if (hoverIndex !== null && hoverIndex >= startIdx && hoverIndex < endIdx) {
|
|
125
|
-
|
|
169
|
+
// The dashed vertical line spans the whole stack (every pane); the
|
|
170
|
+
// horizontal line, price-label chip, and OHLC legend stay scoped
|
|
171
|
+
// to the main pane only — an indicator pane's own hover readout,
|
|
172
|
+
// if it wants one, is the job of whatever plugin draws into it.
|
|
173
|
+
this.renderCrosshairAndLegend(sorted[hoverIndex], xForIndex(hoverIndex), times[hoverIndex], hoverY, valueMin, valueMax, priceStep, chartWidth, chartHeight, stackHeight);
|
|
126
174
|
}
|
|
127
175
|
if (plugins.length > 0) {
|
|
128
|
-
|
|
129
|
-
|
|
176
|
+
// Everything every pane's PluginRenderApi shares — only the pane's
|
|
177
|
+
// own rect/value-domain/scale differ between `buildPluginApi`
|
|
178
|
+
// calls, so bundling the rest here keeps that call to a handful of
|
|
179
|
+
// pane-specific arguments instead of ten positional ones repeated
|
|
180
|
+
// per pane.
|
|
181
|
+
const frameGeometry = {
|
|
130
182
|
chartWidth,
|
|
131
|
-
chartHeight,
|
|
132
183
|
xForIndex,
|
|
133
|
-
|
|
134
|
-
if (frameEnded) {
|
|
135
|
-
throw new Error('wick-charts: PluginRenderApi.yForValue called after its frame ended — ' +
|
|
136
|
-
'only call it synchronously inside ChartPlugin.draw()');
|
|
137
|
-
}
|
|
138
|
-
return yScale.map(value);
|
|
139
|
-
},
|
|
140
|
-
// Exact inverse of xForIndex above — solving
|
|
141
|
-
// `x = (index - viewport.startIndex) * slotWidth + slotWidth / 2` for `index`.
|
|
142
|
-
indexForX: (x) => viewport.startIndex + (x - slotWidth / 2) / slotWidth,
|
|
143
|
-
// Exact inverse of the value->y mapping createScale set up for this
|
|
144
|
-
// frame (domain [valueMin, valueMax] -> range [chartHeight, 0]).
|
|
145
|
-
valueForY: (y) => valueMin + (1 - y / chartHeight) * (valueMax - valueMin),
|
|
184
|
+
indexForX,
|
|
146
185
|
visibleStartIndex: startIdx,
|
|
147
186
|
visibleEndIndex: endIdx,
|
|
148
187
|
allPoints: sorted,
|
|
188
|
+
frameState,
|
|
149
189
|
};
|
|
190
|
+
const mainApi = this.buildPluginApi(mainRect, valueMin, valueMax, yScale, frameGeometry);
|
|
191
|
+
const paneApiById = new Map();
|
|
192
|
+
for (const { pane, rect, min, max, scale } of paneScales) {
|
|
193
|
+
paneApiById.set(pane.id, this.buildPluginApi(rect, min, max, scale, frameGeometry));
|
|
194
|
+
}
|
|
150
195
|
for (const plugin of plugins) {
|
|
151
196
|
if (plugin.visible === false)
|
|
152
197
|
continue;
|
|
198
|
+
// A paneId with no matching pane (e.g. the pane it targeted was
|
|
199
|
+
// since removed) falls back to the main pane rather than being
|
|
200
|
+
// silently skipped — see the doc comment on `ChartPlugin.paneId`.
|
|
201
|
+
const api = plugin.paneId && plugin.paneId !== 'main' ? (paneApiById.get(plugin.paneId) ?? mainApi) : mainApi;
|
|
153
202
|
// save/restore isolates each plugin's canvas state (strokeStyle,
|
|
154
203
|
// lineDash, ...) from the next one — a plugin that forgets to
|
|
155
204
|
// clean up after itself can't bleed style into whatever draws
|
|
@@ -169,10 +218,45 @@ export class ChartRenderer {
|
|
|
169
218
|
}
|
|
170
219
|
}
|
|
171
220
|
finally {
|
|
172
|
-
|
|
221
|
+
frameState.ended = true;
|
|
173
222
|
disposeYScale();
|
|
223
|
+
for (const { dispose } of paneScales)
|
|
224
|
+
dispose();
|
|
174
225
|
}
|
|
175
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Builds the `PluginRenderApi` for one pane — the main price pane or a
|
|
229
|
+
* declared indicator pane, identical shape either way — from that pane's
|
|
230
|
+
* own rect/value-domain/scale plus whatever `geometry` every pane shares
|
|
231
|
+
* for this frame (shared because there is only one time axis, and one
|
|
232
|
+
* frame-ended flag, for the whole stack; see `FrameGeometry`).
|
|
233
|
+
*/
|
|
234
|
+
buildPluginApi(rect, valueMin, valueMax, scale, geometry) {
|
|
235
|
+
const { chartWidth, xForIndex, indexForX, visibleStartIndex, visibleEndIndex, allPoints, frameState } = geometry;
|
|
236
|
+
return {
|
|
237
|
+
ctx: this.ctx,
|
|
238
|
+
chartWidth,
|
|
239
|
+
chartHeight: rect.height,
|
|
240
|
+
xForIndex,
|
|
241
|
+
yForValue: (value) => {
|
|
242
|
+
if (frameState.ended) {
|
|
243
|
+
throw new Error('wick-charts: PluginRenderApi.yForValue called after its frame ended — ' +
|
|
244
|
+
'only call it synchronously inside ChartPlugin.draw()');
|
|
245
|
+
}
|
|
246
|
+
// Local pane-space y (scale's range is [rect.height, 0]) shifted
|
|
247
|
+
// into absolute canvas pixels by the pane's own top offset.
|
|
248
|
+
return rect.top + scale.map(value);
|
|
249
|
+
},
|
|
250
|
+
indexForX,
|
|
251
|
+
// Exact inverse of the mapping above: subtract the pane's top offset
|
|
252
|
+
// before inverting the same [valueMin, valueMax] -> [rect.height, 0]
|
|
253
|
+
// mapping createScale set up for it.
|
|
254
|
+
valueForY: (y) => valueMin + (1 - (y - rect.top) / rect.height) * (valueMax - valueMin),
|
|
255
|
+
visibleStartIndex,
|
|
256
|
+
visibleEndIndex,
|
|
257
|
+
allPoints,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
176
260
|
/** The decimal precision `formatPrice` should use for the current price
|
|
177
261
|
* range — shared by the axis ticks and the crosshair's price label so
|
|
178
262
|
* both display the same value with the same rounding. */
|
|
@@ -180,21 +264,30 @@ export class ChartRenderer {
|
|
|
180
264
|
const ticks = niceTicks(priceMin, priceMax, this.axis.priceTickCount);
|
|
181
265
|
return ticks.length > 1 ? ticks[1] - ticks[0] : 0;
|
|
182
266
|
}
|
|
183
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Draws one pane's right-side value axis: boundary line, horizontal grid
|
|
269
|
+
* lines, and tick labels. Used for both the main price pane and every
|
|
270
|
+
* indicator pane — `topOffset` shifts everything down by that pane's own
|
|
271
|
+
* position in the stack (0 for the main pane, which sits at the top), so
|
|
272
|
+
* `yScale` only ever has to know about its own pane-local [0, chartHeight]
|
|
273
|
+
* range and never about where that pane lives in the full canvas.
|
|
274
|
+
*/
|
|
275
|
+
renderPriceAxis(priceMin, priceMax, step, yScale, chartWidth, chartHeight, topOffset) {
|
|
184
276
|
const { ctx, axis } = this;
|
|
185
277
|
const ticks = niceTicks(priceMin, priceMax, axis.priceTickCount);
|
|
186
278
|
ctx.strokeStyle = axis.lineColor;
|
|
187
279
|
ctx.beginPath();
|
|
188
|
-
ctx.moveTo(chartWidth + 0.5,
|
|
189
|
-
ctx.lineTo(chartWidth + 0.5, chartHeight);
|
|
280
|
+
ctx.moveTo(chartWidth + 0.5, topOffset);
|
|
281
|
+
ctx.lineTo(chartWidth + 0.5, topOffset + chartHeight);
|
|
190
282
|
ctx.stroke();
|
|
191
283
|
ctx.font = this.axisFont();
|
|
192
284
|
ctx.textAlign = 'left';
|
|
193
285
|
ctx.textBaseline = 'middle';
|
|
194
286
|
for (const value of ticks) {
|
|
195
|
-
const
|
|
196
|
-
if (
|
|
287
|
+
const localY = yScale.map(value);
|
|
288
|
+
if (localY < 0 || localY > chartHeight)
|
|
197
289
|
continue;
|
|
290
|
+
const y = topOffset + localY;
|
|
198
291
|
ctx.strokeStyle = axis.gridLineColor;
|
|
199
292
|
ctx.beginPath();
|
|
200
293
|
ctx.moveTo(0, y + 0.5);
|
|
@@ -204,6 +297,18 @@ export class ChartRenderer {
|
|
|
204
297
|
ctx.fillText(formatPrice(value, step), chartWidth + 6, y);
|
|
205
298
|
}
|
|
206
299
|
}
|
|
300
|
+
/** The horizontal rule separating an indicator pane from whatever sits
|
|
301
|
+
* above it (the main pane, or the previous indicator pane) — the same
|
|
302
|
+
* `axis.lineColor` boundary style `renderTimeAxis` already draws between
|
|
303
|
+
* the plotting area and the time-axis strip. */
|
|
304
|
+
renderPaneSeparator(top, chartWidth) {
|
|
305
|
+
const { ctx, axis } = this;
|
|
306
|
+
ctx.strokeStyle = axis.lineColor;
|
|
307
|
+
ctx.beginPath();
|
|
308
|
+
ctx.moveTo(0, top + 0.5);
|
|
309
|
+
ctx.lineTo(chartWidth, top + 0.5);
|
|
310
|
+
ctx.stroke();
|
|
311
|
+
}
|
|
207
312
|
renderTimeAxis(times, startIdx, visibleCount, chartHeight, chartWidth, xForIndex) {
|
|
208
313
|
const { ctx, axis } = this;
|
|
209
314
|
const visibleTimes = times.slice(startIdx, startIdx + visibleCount);
|
|
@@ -223,14 +328,18 @@ export class ChartRenderer {
|
|
|
223
328
|
ctx.fillText(label, x, chartHeight + 6);
|
|
224
329
|
}
|
|
225
330
|
}
|
|
226
|
-
renderCrosshairAndLegend(point, x, timeSeconds, hoverY, valueMin, valueMax, priceStep, chartWidth, chartHeight) {
|
|
331
|
+
renderCrosshairAndLegend(point, x, timeSeconds, hoverY, valueMin, valueMax, priceStep, chartWidth, chartHeight, stackHeight) {
|
|
227
332
|
const { ctx, canvas, seriesDefinition, style, crosshair } = this;
|
|
228
333
|
ctx.save();
|
|
229
334
|
ctx.strokeStyle = crosshair.lineColor;
|
|
230
335
|
ctx.setLineDash([4, 4]);
|
|
336
|
+
// Spans the whole pane stack (not just the main pane's own
|
|
337
|
+
// chartHeight) so hovering a candle lines up with the same column
|
|
338
|
+
// across every indicator pane below it — see the call site's comment
|
|
339
|
+
// in `render()` for why the horizontal line/legend don't follow suit.
|
|
231
340
|
ctx.beginPath();
|
|
232
341
|
ctx.moveTo(x, 0);
|
|
233
|
-
ctx.lineTo(x,
|
|
342
|
+
ctx.lineTo(x, stackHeight);
|
|
234
343
|
ctx.stroke();
|
|
235
344
|
// The horizontal line follows the actual cursor/finger position, not
|
|
236
345
|
// any property of the hovered point — pinning it to (say) the candle's
|
|
@@ -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
|
|
@@ -118,6 +130,45 @@ export interface ChartLegendOptions {
|
|
|
118
130
|
* Defaults to 12. */
|
|
119
131
|
cursorGap?: number;
|
|
120
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Declares one indicator/oscillator pane — a horizontal strip reserved
|
|
135
|
+
* below the main price pane, with its own value-axis domain independent of
|
|
136
|
+
* price (an RSI pane's fixed `[0, 100]`, a MACD pane auto-fit to whatever
|
|
137
|
+
* it's plotting). The pane itself draws nothing: content comes entirely
|
|
138
|
+
* from `ChartPlugin`s registered with a matching `paneId` (see
|
|
139
|
+
* `ChartPlugin.paneId` and `WickChart.addPane`) — the same "core provides
|
|
140
|
+
* layout, the app provides the math" split the plugin system already uses
|
|
141
|
+
* for indicator overlays on the main pane.
|
|
142
|
+
*/
|
|
143
|
+
export interface PaneOptions {
|
|
144
|
+
/** Stable identifier — matched against `ChartPlugin.paneId` to route a
|
|
145
|
+
* plugin's `draw()` into this pane instead of the main price pane.
|
|
146
|
+
* Uniqueness is the caller's responsibility; `addPane` doesn't enforce it. */
|
|
147
|
+
id: string;
|
|
148
|
+
/** Share of the total plotting height (canvas height minus the
|
|
149
|
+
* time-axis strip) this pane occupies. Every declared pane is scaled
|
|
150
|
+
* down proportionally (never one at a time, which would change their
|
|
151
|
+
* relative sizing) if their combined ratio would leave the main pane
|
|
152
|
+
* less than a fifth of the stack. Defaults to 0.25. */
|
|
153
|
+
heightRatio?: number;
|
|
154
|
+
/**
|
|
155
|
+
* This pane's own value-axis domain for the current frame, called once
|
|
156
|
+
* per render. Defaults to a fixed `{ min: 0, max: 1 }` if omitted, which
|
|
157
|
+
* is almost never meaningful — supply this for any real indicator pane
|
|
158
|
+
* (e.g. `() => ({ min: 0, max: 100 })` for RSI, or a closure over
|
|
159
|
+
* whatever series your own plugin is tracking for something auto-fit
|
|
160
|
+
* like MACD).
|
|
161
|
+
*/
|
|
162
|
+
getValueRange?: () => ValueRange;
|
|
163
|
+
}
|
|
164
|
+
/** `PaneOptions` with every optional field defaulted — what `WickChart`
|
|
165
|
+
* actually stores and hands to `ChartRenderer`, so the renderer never has
|
|
166
|
+
* to re-apply `??` defaults on every frame. */
|
|
167
|
+
export interface ResolvedPaneOptions {
|
|
168
|
+
id: string;
|
|
169
|
+
heightRatio: number;
|
|
170
|
+
getValueRange: () => ValueRange;
|
|
171
|
+
}
|
|
121
172
|
export interface WickChartOptions {
|
|
122
173
|
/**
|
|
123
174
|
* Which registered series type to render this chart as (see
|