wick-charts 0.5.0 → 0.7.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 +79 -2
- package/dist/index.d.ts +59 -1
- package/dist/index.js +118 -3
- package/dist/renderer.d.ts +7 -0
- package/dist/renderer.js +19 -8
- package/dist/types.d.ts +14 -3
- package/dist/valueAxis.d.ts +44 -0
- package/dist/valueAxis.js +52 -0
- package/dist/viewport.d.ts +11 -0
- package/dist/viewport.js +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,9 @@ npm install wick-charts
|
|
|
19
19
|
- [Candle data](#candle-data)
|
|
20
20
|
- [Line charts](#line-charts)
|
|
21
21
|
- [Styling](#styling)
|
|
22
|
+
- [Inverting the value axis](#inverting-the-value-axis)
|
|
22
23
|
- [Reading chart state](#reading-chart-state)
|
|
24
|
+
- [Setting the visible range](#setting-the-visible-range)
|
|
23
25
|
- [Loading more history on demand](#loading-more-history-on-demand)
|
|
24
26
|
- [Extending: plugins](#extending-plugins)
|
|
25
27
|
- [Multi-pane indicators](#multi-pane-indicators)
|
|
@@ -227,6 +229,39 @@ type-checks `style` against
|
|
|
227
229
|
`CandlestickStyle`; the more general `new WickChart(canvas, { type: 'candlestick', style })`
|
|
228
230
|
also works but doesn't — see "Series types" below for why, if you're curious.
|
|
229
231
|
|
|
232
|
+
### Inverting the value axis
|
|
233
|
+
|
|
234
|
+
`invertValueAxis` mirrors the value axis top-to-bottom — every pane's higher values render
|
|
235
|
+
lower on screen instead of higher, useful for a "what if this series had moved the opposite
|
|
236
|
+
way" view:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
const chart = createCandlestickChart(canvas, { invertValueAxis: true });
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Unlike the style options above, it's meant to be flipped live rather than fixed at
|
|
243
|
+
construction — `setInvertValueAxis(boolean)`/`isValueAxisInverted()` let a UI toggle it on an
|
|
244
|
+
existing chart without losing the current pan/zoom position or manual value-range override,
|
|
245
|
+
the same way `setPluginVisible` toggles a plugin without losing its state:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
toggleButton.addEventListener('click', () => {
|
|
249
|
+
chart.setInvertValueAxis(!chart.isValueAxisInverted());
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Only where each value renders is mirrored — the underlying data isn't. A candle's open/close
|
|
254
|
+
relationship (and therefore its up/down color) still reflects the real values, `formatLegend`
|
|
255
|
+
still shows the real OHLC numbers, and dragging the price axis or panning vertically still
|
|
256
|
+
feels like "grab and slide" in the same screen direction as before; only the sign of what that
|
|
257
|
+
drag does to the value range flips internally to keep it feeling that way. Applies to every
|
|
258
|
+
pane in the stack (see "Multi-pane indicators" below) consistently, not just the main one.
|
|
259
|
+
|
|
260
|
+
One known exception: candlestick's volume bars aren't mapped through the value-axis scale at
|
|
261
|
+
all (they're drawn in a fixed-height strip anchored to the bottom of the pane, independent of
|
|
262
|
+
price — see "Series types" under Architecture), so they stay bottom-anchored regardless of
|
|
263
|
+
`invertValueAxis` rather than flipping to the top with everything else.
|
|
264
|
+
|
|
230
265
|
### Reading chart state
|
|
231
266
|
|
|
232
267
|
Useful for building UI around the canvas (a legend, a toolbar, a "jump to latest" button)
|
|
@@ -235,10 +270,47 @@ without reaching into the chart's internals:
|
|
|
235
270
|
```ts
|
|
236
271
|
chart.getPointCount(); // total candles loaded (not just visible)
|
|
237
272
|
chart.getVisibleRange(); // { startIndex, endIndex, visibleCount }
|
|
273
|
+
chart.getVisibleTimeRange(); // { from, to } in unix seconds, or null with no data
|
|
238
274
|
chart.getValueRangeOverride(); // { min, max } once the user has dragged the price axis, else null
|
|
239
275
|
chart.getHoveredPoint(); // the candle under the cursor/finger, or null
|
|
240
276
|
```
|
|
241
277
|
|
|
278
|
+
### Setting the visible range
|
|
279
|
+
|
|
280
|
+
The write side of `getVisibleRange`/`getVisibleTimeRange` — jump the pan/zoom window
|
|
281
|
+
programmatically instead of only ever through a drag/scroll gesture:
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
chart.setVisibleRange({ startIndex: 50, endIndex: 100 }); // index-based, like getVisibleRange()
|
|
285
|
+
chart.setVisibleTimeRange({ from: '2024-02-01T00:00:00Z', to: '2024-03-01T00:00:00Z' });
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`setVisibleTimeRange` is the one to reach for when syncing one chart's pan/zoom onto another
|
|
289
|
+
**independent** `WickChart` instance that shares a time axis — a common pattern for a price
|
|
290
|
+
chart and an indicator chart panned together, or any "these views move as one" UI. Indices
|
|
291
|
+
aren't safe for this: two charts may have loaded different amounts of history via
|
|
292
|
+
`setDataLoader`, so the same index means a different candle in each, while the same time
|
|
293
|
+
always means the same point (or the nearest one either chart actually has loaded). A time
|
|
294
|
+
value in `from`/`to` accepts every shape `Candle.time` does (unix seconds/ms, ISO string,
|
|
295
|
+
`{ businessDay }}`); `getVisibleTimeRange()` always returns plain unix seconds.
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
// Keep `follower` in lockstep with `driver` — see demo/sync.html for a
|
|
299
|
+
// complete two-chart example, including what to do about there being no
|
|
300
|
+
// pan/zoom change event yet (see "Status" below).
|
|
301
|
+
function syncLoop() {
|
|
302
|
+
const range = driver.getVisibleTimeRange();
|
|
303
|
+
if (range) follower.setVisibleTimeRange(range);
|
|
304
|
+
requestAnimationFrame(syncLoop);
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Both setters clamp an out-of-range request instead of throwing (the same way a drag/zoom
|
|
309
|
+
gesture can never overscroll past the loaded data) and clear the current hover, since a jump
|
|
310
|
+
is a discontinuous change — a hover position computed for the window before it no longer
|
|
311
|
+
lines up with anything until the pointer moves again. Neither touches the value axis (manual
|
|
312
|
+
price-range override, invert) — only the time window moves.
|
|
313
|
+
|
|
242
314
|
### Loading more history on demand
|
|
243
315
|
|
|
244
316
|
`setDataLoader` lets you start with a small window and stream in more as the user pans toward
|
|
@@ -644,8 +716,13 @@ concrete drawing tool ships yet, only the mechanism a trend line or similar woul
|
|
|
644
716
|
on. `addPane`/`removePane` let a plugin-drawn indicator (RSI, MACD, ...) reserve its own
|
|
645
717
|
horizontal strip with an independent value axis — see "Multi-pane indicators" above; volume
|
|
646
718
|
still shares the candlestick pane rather than getting its own, since it draws through the
|
|
647
|
-
series itself, not a pane-targeted plugin.
|
|
648
|
-
|
|
719
|
+
series itself, not a pane-targeted plugin. `invertValueAxis`/`setInvertValueAxis` mirror the
|
|
720
|
+
whole stack's value axis top-to-bottom without touching the underlying data — see "Inverting
|
|
721
|
+
the value axis" above. `setVisibleRange`/`setVisibleTimeRange` let the pan/zoom window be set
|
|
722
|
+
programmatically (see "Setting the visible range" above) — there's no pan/zoom *change* event
|
|
723
|
+
yet, so keeping one chart synced to another (`demo/sync.html`) means polling
|
|
724
|
+
`getVisibleTimeRange()` (e.g. once per animation frame) rather than reacting to a callback.
|
|
725
|
+
See [CHANGELOG.md](./CHANGELOG.md) for what shipped in each release.
|
|
649
726
|
|
|
650
727
|
## License
|
|
651
728
|
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ 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
4
|
import type { LineStyle } from './series/line.js';
|
|
5
|
-
import type { Candle, LinePoint, PaneOptions, WickChartOptions, SeriesPoint, ValueRange } from './types.js';
|
|
5
|
+
import type { Candle, LinePoint, PaneOptions, WickChartOptions, SeriesPoint, ValueRange, WickTime } from './types.js';
|
|
6
6
|
export type { BusinessDay, Candle, LinePoint, PaneOptions, WickChartOptions, WickTime, SeriesPoint, UnixMillis, ValueRange, } from './types.js';
|
|
7
7
|
export type { DataLoader, DataRequest } from './dataSource.js';
|
|
8
8
|
export type { ChartPlugin, ChartPointerEvent, PluginRenderApi } from './plugins/types.js';
|
|
@@ -76,6 +76,11 @@ export declare class WickChart<TPoint extends SeriesPoint = Candle> {
|
|
|
76
76
|
* every threshold crossing until `setData` resets it (a fresh dataset
|
|
77
77
|
* may come from a different source that does have more). */
|
|
78
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;
|
|
79
84
|
constructor(canvas: HTMLCanvasElement, options?: WickChartOptions);
|
|
80
85
|
setData(points: TPoint[]): this;
|
|
81
86
|
/**
|
|
@@ -150,6 +155,48 @@ export declare class WickChart<TPoint extends SeriesPoint = Candle> {
|
|
|
150
155
|
endIndex: number;
|
|
151
156
|
visibleCount: number;
|
|
152
157
|
};
|
|
158
|
+
/**
|
|
159
|
+
* Jumps the visible pan/zoom window directly to `[startIndex, endIndex)`
|
|
160
|
+
* — the programmatic, index-based counterpart to `getVisibleRange()`'s
|
|
161
|
+
* own shape, for anything that already knows the target in index terms
|
|
162
|
+
* (a minimap click, a saved bookmark). Clamped the same way a drag/zoom
|
|
163
|
+
* gesture already is (see `Viewport.setVisibleIndexRange`) rather than
|
|
164
|
+
* throwing on an out-of-range request. Clears the current hover, the
|
|
165
|
+
* same way `setData()` does — a jump is a discontinuous change, and a
|
|
166
|
+
* hover position computed for the window before it is no longer
|
|
167
|
+
* meaningful until the pointer actually moves again.
|
|
168
|
+
*
|
|
169
|
+
* Indices aren't a safe way to sync two independent `WickChart`
|
|
170
|
+
* instances sharing a time axis — they may have loaded different
|
|
171
|
+
* amounts of history via `setDataLoader`, so the same index means a
|
|
172
|
+
* different point in each. Use `setVisibleTimeRange` for that instead.
|
|
173
|
+
*/
|
|
174
|
+
setVisibleRange(range: {
|
|
175
|
+
startIndex: number;
|
|
176
|
+
endIndex: number;
|
|
177
|
+
}): this;
|
|
178
|
+
/**
|
|
179
|
+
* The time-based counterpart to `setVisibleRange` — jumps to whatever
|
|
180
|
+
* window of the currently loaded data falls within `[from, to]`
|
|
181
|
+
* (inclusive both ends), resolved against this chart's own loaded
|
|
182
|
+
* points. This is what makes syncing one chart's pan/zoom onto another
|
|
183
|
+
* independent `WickChart` instance sharing a time axis possible: read
|
|
184
|
+
* the source chart's `getVisibleTimeRange()` and pass it straight to
|
|
185
|
+
* this one, and the two stay in sync by time even if they've loaded
|
|
186
|
+
* different amounts of history. A no-op if no data has been loaded yet.
|
|
187
|
+
*/
|
|
188
|
+
setVisibleTimeRange(range: {
|
|
189
|
+
from: WickTime;
|
|
190
|
+
to: WickTime;
|
|
191
|
+
}): this;
|
|
192
|
+
/** The currently visible window's time span, in unix seconds — `null`
|
|
193
|
+
* when there's no data to report one for. The time-based counterpart to
|
|
194
|
+
* `getVisibleRange()`, and the read side `setVisibleTimeRange` is meant
|
|
195
|
+
* to be paired with for syncing one chart's pan/zoom onto another. */
|
|
196
|
+
getVisibleTimeRange(): {
|
|
197
|
+
from: number;
|
|
198
|
+
to: number;
|
|
199
|
+
} | null;
|
|
153
200
|
/** The value axis's manual range once the user has dragged or scaled it
|
|
154
201
|
* — `null` if the axis is still auto-fitting to whatever's visible
|
|
155
202
|
* (the default until the user first touches it vertically). */
|
|
@@ -157,6 +204,17 @@ export declare class WickChart<TPoint extends SeriesPoint = Candle> {
|
|
|
157
204
|
/** The point currently under the cursor (crosshair/legend target), or
|
|
158
205
|
* `null` when nothing is hovered. */
|
|
159
206
|
getHoveredPoint(): TPoint | null;
|
|
207
|
+
/**
|
|
208
|
+
* Mirrors the value axis top-to-bottom (or restores it) and re-renders —
|
|
209
|
+
* see `WickChartOptions.invertValueAxis` for what this actually changes.
|
|
210
|
+
* A live toggle, unlike most other style options: pan/zoom/valueRangeOverride
|
|
211
|
+
* state is untouched, so a "flip" button can call this on an existing
|
|
212
|
+
* chart without losing the user's current view, the same way
|
|
213
|
+
* `setPluginVisible` toggles a plugin without losing its state.
|
|
214
|
+
*/
|
|
215
|
+
setInvertValueAxis(inverted: boolean): this;
|
|
216
|
+
/** Whether the value axis is currently mirrored — see `setInvertValueAxis`. */
|
|
217
|
+
isValueAxisInverted(): boolean;
|
|
160
218
|
/** Removes all attached listeners. Call on unmount — the mouseup
|
|
161
219
|
* listener is on `window` (so drags don't get stuck if the cursor
|
|
162
220
|
* leaves the canvas mid-drag) and won't be garbage-collected on its own. */
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ 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';
|
|
@@ -44,6 +45,39 @@ const LONG_PRESS_MS = 350;
|
|
|
44
45
|
* as a real drag, not a hold — cancels the pending long-press timer so a
|
|
45
46
|
* fast pan gesture never flips into scrub mid-motion. */
|
|
46
47
|
const LONG_PRESS_MOVE_TOLERANCE_PX = 10;
|
|
48
|
+
/** First index in `times` (ascending) whose value is `>= target`, or
|
|
49
|
+
* `times.length` if every value is smaller — the standard binary
|
|
50
|
+
* lower-bound, O(log n) rather than a linear scan over what can be a
|
|
51
|
+
* multi-thousand-point loaded series. Used by `setVisibleTimeRange` to
|
|
52
|
+
* resolve a `from` time to its start index. */
|
|
53
|
+
function lowerBound(times, target) {
|
|
54
|
+
let lo = 0;
|
|
55
|
+
let hi = times.length;
|
|
56
|
+
while (lo < hi) {
|
|
57
|
+
const mid = (lo + hi) >>> 1;
|
|
58
|
+
if (times[mid] < target)
|
|
59
|
+
lo = mid + 1;
|
|
60
|
+
else
|
|
61
|
+
hi = mid;
|
|
62
|
+
}
|
|
63
|
+
return lo;
|
|
64
|
+
}
|
|
65
|
+
/** First index in `times` (ascending) whose value is `> target`, or
|
|
66
|
+
* `times.length` if none is — the exclusive end boundary for a `to` time,
|
|
67
|
+
* so a range `[lowerBound(from), upperBound(to))` includes every point
|
|
68
|
+
* with a time in `[from, to]` inclusive on both ends. */
|
|
69
|
+
function upperBound(times, target) {
|
|
70
|
+
let lo = 0;
|
|
71
|
+
let hi = times.length;
|
|
72
|
+
while (lo < hi) {
|
|
73
|
+
const mid = (lo + hi) >>> 1;
|
|
74
|
+
if (times[mid] <= target)
|
|
75
|
+
lo = mid + 1;
|
|
76
|
+
else
|
|
77
|
+
hi = mid;
|
|
78
|
+
}
|
|
79
|
+
return lo;
|
|
80
|
+
}
|
|
47
81
|
/**
|
|
48
82
|
* Interactive chart: drag to pan, wheel to zoom, drag the price-axis strip
|
|
49
83
|
* to rescale it, hover a point for a legend. What gets plotted (candles
|
|
@@ -302,6 +336,7 @@ export class WickChart {
|
|
|
302
336
|
};
|
|
303
337
|
this.seriesDefinition = getSeries(options?.type ?? 'candlestick');
|
|
304
338
|
this.renderer = new ChartRenderer(canvas, this.seriesDefinition, options);
|
|
339
|
+
this.invertValueAxis = options?.invertValueAxis ?? false;
|
|
305
340
|
this.viewport = new Viewport(0);
|
|
306
341
|
// Without this, a touch drag on the canvas also scrolls/zooms the page
|
|
307
342
|
// underneath it — the browser's native touch gestures and this class's
|
|
@@ -450,6 +485,62 @@ export class WickChart {
|
|
|
450
485
|
visibleCount: this.viewport.visibleCount,
|
|
451
486
|
};
|
|
452
487
|
}
|
|
488
|
+
/**
|
|
489
|
+
* Jumps the visible pan/zoom window directly to `[startIndex, endIndex)`
|
|
490
|
+
* — the programmatic, index-based counterpart to `getVisibleRange()`'s
|
|
491
|
+
* own shape, for anything that already knows the target in index terms
|
|
492
|
+
* (a minimap click, a saved bookmark). Clamped the same way a drag/zoom
|
|
493
|
+
* gesture already is (see `Viewport.setVisibleIndexRange`) rather than
|
|
494
|
+
* throwing on an out-of-range request. Clears the current hover, the
|
|
495
|
+
* same way `setData()` does — a jump is a discontinuous change, and a
|
|
496
|
+
* hover position computed for the window before it is no longer
|
|
497
|
+
* meaningful until the pointer actually moves again.
|
|
498
|
+
*
|
|
499
|
+
* Indices aren't a safe way to sync two independent `WickChart`
|
|
500
|
+
* instances sharing a time axis — they may have loaded different
|
|
501
|
+
* amounts of history via `setDataLoader`, so the same index means a
|
|
502
|
+
* different point in each. Use `setVisibleTimeRange` for that instead.
|
|
503
|
+
*/
|
|
504
|
+
setVisibleRange(range) {
|
|
505
|
+
this.viewport.setVisibleIndexRange(range.startIndex, range.endIndex, this.sorted.length);
|
|
506
|
+
this.hoverIndex = null;
|
|
507
|
+
this.hoverY = null;
|
|
508
|
+
this.scheduleRender();
|
|
509
|
+
return this;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* The time-based counterpart to `setVisibleRange` — jumps to whatever
|
|
513
|
+
* window of the currently loaded data falls within `[from, to]`
|
|
514
|
+
* (inclusive both ends), resolved against this chart's own loaded
|
|
515
|
+
* points. This is what makes syncing one chart's pan/zoom onto another
|
|
516
|
+
* independent `WickChart` instance sharing a time axis possible: read
|
|
517
|
+
* the source chart's `getVisibleTimeRange()` and pass it straight to
|
|
518
|
+
* this one, and the two stay in sync by time even if they've loaded
|
|
519
|
+
* different amounts of history. A no-op if no data has been loaded yet.
|
|
520
|
+
*/
|
|
521
|
+
setVisibleTimeRange(range) {
|
|
522
|
+
if (this.times.length === 0)
|
|
523
|
+
return this;
|
|
524
|
+
const fromSeconds = toUnixSeconds(range.from);
|
|
525
|
+
const toSeconds = toUnixSeconds(range.to);
|
|
526
|
+
return this.setVisibleRange({
|
|
527
|
+
startIndex: lowerBound(this.times, fromSeconds),
|
|
528
|
+
endIndex: upperBound(this.times, toSeconds),
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
/** The currently visible window's time span, in unix seconds — `null`
|
|
532
|
+
* when there's no data to report one for. The time-based counterpart to
|
|
533
|
+
* `getVisibleRange()`, and the read side `setVisibleTimeRange` is meant
|
|
534
|
+
* to be paired with for syncing one chart's pan/zoom onto another. */
|
|
535
|
+
getVisibleTimeRange() {
|
|
536
|
+
if (this.sorted.length === 0)
|
|
537
|
+
return null;
|
|
538
|
+
const startIdx = Math.max(0, Math.floor(this.viewport.startIndex));
|
|
539
|
+
const endIdx = Math.min(this.sorted.length, Math.ceil(this.viewport.endIndex));
|
|
540
|
+
if (endIdx <= startIdx)
|
|
541
|
+
return null;
|
|
542
|
+
return { from: this.times[startIdx], to: this.times[endIdx - 1] };
|
|
543
|
+
}
|
|
453
544
|
/** The value axis's manual range once the user has dragged or scaled it
|
|
454
545
|
* — `null` if the axis is still auto-fitting to whatever's visible
|
|
455
546
|
* (the default until the user first touches it vertically). */
|
|
@@ -464,6 +555,24 @@ export class WickChart {
|
|
|
464
555
|
getHoveredPoint() {
|
|
465
556
|
return this.hoverIndex === null ? null : (this.sorted[this.hoverIndex] ?? null);
|
|
466
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Mirrors the value axis top-to-bottom (or restores it) and re-renders —
|
|
560
|
+
* see `WickChartOptions.invertValueAxis` for what this actually changes.
|
|
561
|
+
* A live toggle, unlike most other style options: pan/zoom/valueRangeOverride
|
|
562
|
+
* state is untouched, so a "flip" button can call this on an existing
|
|
563
|
+
* chart without losing the user's current view, the same way
|
|
564
|
+
* `setPluginVisible` toggles a plugin without losing its state.
|
|
565
|
+
*/
|
|
566
|
+
setInvertValueAxis(inverted) {
|
|
567
|
+
this.invertValueAxis = inverted;
|
|
568
|
+
this.renderer.setInvertValueAxis(inverted);
|
|
569
|
+
this.scheduleRender();
|
|
570
|
+
return this;
|
|
571
|
+
}
|
|
572
|
+
/** Whether the value axis is currently mirrored — see `setInvertValueAxis`. */
|
|
573
|
+
isValueAxisInverted() {
|
|
574
|
+
return this.invertValueAxis;
|
|
575
|
+
}
|
|
467
576
|
/** Removes all attached listeners. Call on unmount — the mouseup
|
|
468
577
|
* listener is on `window` (so drags don't get stuck if the cursor
|
|
469
578
|
* leaves the canvas mid-drag) and won't be garbage-collected on its own. */
|
|
@@ -527,7 +636,13 @@ export class WickChart {
|
|
|
527
636
|
// Dragging down moves the visible value window down (content
|
|
528
637
|
// follows the cursor), matching the horizontal drag's "grab and
|
|
529
638
|
// slide" feel — see the pan call above for the mirrored X case.
|
|
530
|
-
|
|
639
|
+
// invertValueAxis flips which value-space direction "down the
|
|
640
|
+
// screen" corresponds to (see src/valueAxis.ts), so the sign here
|
|
641
|
+
// has to flip too or an inverted chart would pan backwards relative
|
|
642
|
+
// to the drag — negated, not re-derived, since the *magnitude* is
|
|
643
|
+
// identical either way.
|
|
644
|
+
const directionSign = this.invertValueAxis ? -1 : 1;
|
|
645
|
+
this.viewport.panValueRange(deltaYDevice * valuePerPixel * directionSign);
|
|
531
646
|
}
|
|
532
647
|
this.scheduleRender();
|
|
533
648
|
}
|
|
@@ -674,7 +789,7 @@ export class WickChart {
|
|
|
674
789
|
const chartHeight = this.mainPaneHeight();
|
|
675
790
|
if (!range || chartHeight <= 0)
|
|
676
791
|
return null;
|
|
677
|
-
return range.min
|
|
792
|
+
return pixelToValue(y, range.min, range.max, chartHeight, this.invertValueAxis);
|
|
678
793
|
}
|
|
679
794
|
/** x pixel -> global (possibly fractional) index — the exact inverse of
|
|
680
795
|
* the renderer's own `xForIndex`, so a pointer event lines up with
|
|
@@ -702,7 +817,7 @@ export class WickChart {
|
|
|
702
817
|
const chartHeight = this.mainPaneHeight();
|
|
703
818
|
if (!range || chartHeight <= 0)
|
|
704
819
|
return null;
|
|
705
|
-
return
|
|
820
|
+
return valueToPixel(value, range.min, range.max, chartHeight, this.invertValueAxis);
|
|
706
821
|
}
|
|
707
822
|
pointerEventAt(x, y) {
|
|
708
823
|
return {
|
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
|
|
@@ -249,9 +258,10 @@ export class ChartRenderer {
|
|
|
249
258
|
},
|
|
250
259
|
indexForX,
|
|
251
260
|
// Exact inverse of the mapping above: subtract the pane's top offset
|
|
252
|
-
// before inverting the same
|
|
253
|
-
//
|
|
254
|
-
|
|
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),
|
|
255
265
|
visibleStartIndex,
|
|
256
266
|
visibleEndIndex,
|
|
257
267
|
allPoints,
|
|
@@ -358,8 +368,9 @@ export class ChartRenderer {
|
|
|
358
368
|
ctx.restore();
|
|
359
369
|
if (priceLineVisible) {
|
|
360
370
|
// Exact inverse of the value->y mapping createScale set up for this
|
|
361
|
-
// frame — same
|
|
362
|
-
|
|
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);
|
|
363
374
|
this.renderPriceLabelChip(formatPrice(value, priceStep), hoverY, chartWidth);
|
|
364
375
|
}
|
|
365
376
|
this.renderTimeLabelChip(formatHoverTime(timeSeconds), x, chartHeight, canvas.width);
|
package/dist/types.d.ts
CHANGED
|
@@ -173,9 +173,9 @@ export interface WickChartOptions {
|
|
|
173
173
|
/**
|
|
174
174
|
* Which registered series type to render this chart as (see
|
|
175
175
|
* `registerSeries` in `src/series/registry.ts`). Defaults to
|
|
176
|
-
* `'candlestick'
|
|
177
|
-
* new one is a matter of implementing `SeriesDefinition` and
|
|
178
|
-
* 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.
|
|
179
179
|
*/
|
|
180
180
|
type?: string;
|
|
181
181
|
/** Background color of the canvas. Defaults to transparent. Chart-wide
|
|
@@ -199,4 +199,15 @@ export interface WickChartOptions {
|
|
|
199
199
|
crosshair?: ChartCrosshairOptions;
|
|
200
200
|
/** Hover legend coloring. Merged over the built-in defaults field by field. */
|
|
201
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;
|
|
202
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
|
+
}
|
package/dist/viewport.d.ts
CHANGED
|
@@ -33,6 +33,17 @@ export declare class Viewport {
|
|
|
33
33
|
* keeping the point at `anchorIndex` under the same relative position —
|
|
34
34
|
* the standard "zoom toward the cursor" feel. */
|
|
35
35
|
zoom(factor: number, anchorIndex: number, totalCount: number): void;
|
|
36
|
+
/**
|
|
37
|
+
* Replaces the visible window outright with `[startIndex, endIndex)`,
|
|
38
|
+
* clamped the same way `pan`/`zoom` already are (a floor on
|
|
39
|
+
* `visibleCount` so the window never collapses to nothing, and never
|
|
40
|
+
* extends past `[0, totalCount]`). The primitive a programmatic "jump to
|
|
41
|
+
* this range" builds on (see `WickChart.setVisibleRange`/
|
|
42
|
+
* `setVisibleTimeRange`) — unlike `pan`/`zoom`, which shift or scale the
|
|
43
|
+
* *current* window for a continuous gesture, this discards it and starts
|
|
44
|
+
* fresh from whatever was requested.
|
|
45
|
+
*/
|
|
46
|
+
setVisibleIndexRange(startIndex: number, endIndex: number, totalCount: number): void;
|
|
36
47
|
/** Multiplies the value-scale factor, clamped to a sane range so the
|
|
37
48
|
* value axis can't be dragged into showing nothing or clipping data.
|
|
38
49
|
* Only affects the auto-fit path — a no-op once `valueRangeOverride` is
|
package/dist/viewport.js
CHANGED
|
@@ -50,6 +50,22 @@ export class Viewport {
|
|
|
50
50
|
const maxStart = Math.max(0, totalCount - this.visibleCount);
|
|
51
51
|
this.startIndex = clamp(anchorIndex - anchorRatio * newVisibleCount, 0, maxStart);
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Replaces the visible window outright with `[startIndex, endIndex)`,
|
|
55
|
+
* clamped the same way `pan`/`zoom` already are (a floor on
|
|
56
|
+
* `visibleCount` so the window never collapses to nothing, and never
|
|
57
|
+
* extends past `[0, totalCount]`). The primitive a programmatic "jump to
|
|
58
|
+
* this range" builds on (see `WickChart.setVisibleRange`/
|
|
59
|
+
* `setVisibleTimeRange`) — unlike `pan`/`zoom`, which shift or scale the
|
|
60
|
+
* *current* window for a continuous gesture, this discards it and starts
|
|
61
|
+
* fresh from whatever was requested.
|
|
62
|
+
*/
|
|
63
|
+
setVisibleIndexRange(startIndex, endIndex, totalCount) {
|
|
64
|
+
const requestedCount = endIndex - startIndex;
|
|
65
|
+
this.visibleCount = clamp(requestedCount, MIN_VISIBLE_COUNT, Math.max(totalCount, MIN_VISIBLE_COUNT));
|
|
66
|
+
const maxStart = Math.max(0, totalCount - this.visibleCount);
|
|
67
|
+
this.startIndex = clamp(startIndex, 0, maxStart);
|
|
68
|
+
}
|
|
53
69
|
/** Multiplies the value-scale factor, clamped to a sane range so the
|
|
54
70
|
* value axis can't be dragged into showing nothing or clipping data.
|
|
55
71
|
* Only affects the auto-fit path — a no-op once `valueRangeOverride` is
|