wick-charts 0.6.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 +43 -1
- package/dist/index.d.ts +43 -1
- package/dist/index.js +89 -0
- package/dist/viewport.d.ts +11 -0
- package/dist/viewport.js +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ npm install wick-charts
|
|
|
21
21
|
- [Styling](#styling)
|
|
22
22
|
- [Inverting the value axis](#inverting-the-value-axis)
|
|
23
23
|
- [Reading chart state](#reading-chart-state)
|
|
24
|
+
- [Setting the visible range](#setting-the-visible-range)
|
|
24
25
|
- [Loading more history on demand](#loading-more-history-on-demand)
|
|
25
26
|
- [Extending: plugins](#extending-plugins)
|
|
26
27
|
- [Multi-pane indicators](#multi-pane-indicators)
|
|
@@ -269,10 +270,47 @@ without reaching into the chart's internals:
|
|
|
269
270
|
```ts
|
|
270
271
|
chart.getPointCount(); // total candles loaded (not just visible)
|
|
271
272
|
chart.getVisibleRange(); // { startIndex, endIndex, visibleCount }
|
|
273
|
+
chart.getVisibleTimeRange(); // { from, to } in unix seconds, or null with no data
|
|
272
274
|
chart.getValueRangeOverride(); // { min, max } once the user has dragged the price axis, else null
|
|
273
275
|
chart.getHoveredPoint(); // the candle under the cursor/finger, or null
|
|
274
276
|
```
|
|
275
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
|
+
|
|
276
314
|
### Loading more history on demand
|
|
277
315
|
|
|
278
316
|
`setDataLoader` lets you start with a small window and stream in more as the user pans toward
|
|
@@ -680,7 +718,11 @@ horizontal strip with an independent value axis — see "Multi-pane indicators"
|
|
|
680
718
|
still shares the candlestick pane rather than getting its own, since it draws through the
|
|
681
719
|
series itself, not a pane-targeted plugin. `invertValueAxis`/`setInvertValueAxis` mirror the
|
|
682
720
|
whole stack's value axis top-to-bottom without touching the underlying data — see "Inverting
|
|
683
|
-
the value axis" above.
|
|
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.
|
|
684
726
|
|
|
685
727
|
## License
|
|
686
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';
|
|
@@ -155,6 +155,48 @@ export declare class WickChart<TPoint extends SeriesPoint = Candle> {
|
|
|
155
155
|
endIndex: number;
|
|
156
156
|
visibleCount: number;
|
|
157
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;
|
|
158
200
|
/** The value axis's manual range once the user has dragged or scaled it
|
|
159
201
|
* — `null` if the axis is still auto-fitting to whatever's visible
|
|
160
202
|
* (the default until the user first touches it vertically). */
|
package/dist/index.js
CHANGED
|
@@ -45,6 +45,39 @@ const LONG_PRESS_MS = 350;
|
|
|
45
45
|
* as a real drag, not a hold — cancels the pending long-press timer so a
|
|
46
46
|
* fast pan gesture never flips into scrub mid-motion. */
|
|
47
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
|
+
}
|
|
48
81
|
/**
|
|
49
82
|
* Interactive chart: drag to pan, wheel to zoom, drag the price-axis strip
|
|
50
83
|
* to rescale it, hover a point for a legend. What gets plotted (candles
|
|
@@ -452,6 +485,62 @@ export class WickChart {
|
|
|
452
485
|
visibleCount: this.viewport.visibleCount,
|
|
453
486
|
};
|
|
454
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
|
+
}
|
|
455
544
|
/** The value axis's manual range once the user has dragged or scaled it
|
|
456
545
|
* — `null` if the axis is still auto-fitting to whatever's visible
|
|
457
546
|
* (the default until the user first touches it vertically). */
|
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
|