emberwick 0.2.0 → 0.4.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 +122 -3
- package/index.d.ts +120 -2
- package/index.js +375 -13
- package/index.js.map +1 -1
- package/package.json +1 -1
- package/umd/emberwick.umd.js +1 -1
- package/umd/emberwick.umd.js.map +1 -1
package/README.md
CHANGED
|
@@ -225,6 +225,10 @@ const chart = createChart(el, {
|
|
|
225
225
|
| `setAnimate(bool)` | Toggle live-candle easing |
|
|
226
226
|
| `setMagnet(bool)` | Toggle crosshair OHLC snapping |
|
|
227
227
|
| `snapToRealtime()` | Jump back to the newest bar and re-enable autoscale |
|
|
228
|
+
| `startReplay(options?)` | Begin bar-by-bar playback. Returns the `Replay`, or `null` if there is nothing to replay |
|
|
229
|
+
| `stopReplay()` | Leave replay and reveal the whole dataset again |
|
|
230
|
+
| `replayState()` | Current playback state. `{ active: false, ... }` when not replaying |
|
|
231
|
+
| `chart.replay` | Getter — the active `Replay` controller, or `null` |
|
|
228
232
|
| `toImage()` | PNG data URL of the composited layers |
|
|
229
233
|
| `destroy()` | Remove listeners, stop the loop, drop canvases |
|
|
230
234
|
| `chart.fps` | Getter — measured frames per second |
|
|
@@ -240,16 +244,132 @@ const off = chart.subscribe('crosshair', (payload) => {
|
|
|
240
244
|
off() // unsubscribe
|
|
241
245
|
```
|
|
242
246
|
|
|
247
|
+
| Event | Payload |
|
|
248
|
+
|---|---|
|
|
243
249
|
| Event | Payload |
|
|
244
250
|
|---|---|
|
|
245
251
|
| `'crosshair'` | `{ index, bar, price }`, or `null` when the pointer leaves the plot |
|
|
246
252
|
| `'markerHover'` | The marker under the pointer, or `null` when none is |
|
|
247
253
|
| `'markerClick'` | The clicked marker. Only fires on a hit, never with `null` |
|
|
248
|
-
| `'visibleRange'` |
|
|
254
|
+
| `'visibleRange'` | `{ from, to, fromTime, toTime, barCount, spacing, settled }` |
|
|
255
|
+
| `'replay'` | `{ active, playing, index, length, progress, speed, time, bar, atEnd }` |
|
|
249
256
|
|
|
250
257
|
A drag that happens to end on top of a marker does not fire `'markerClick'` —
|
|
251
258
|
panning and clicking stay distinct.
|
|
252
259
|
|
|
260
|
+
### Tracking the visible range
|
|
261
|
+
|
|
262
|
+
`'visibleRange'` is a **state** event rather than a notification, which makes it
|
|
263
|
+
usable without any debouncing of your own:
|
|
264
|
+
|
|
265
|
+
- A new subscriber is called **immediately** with the current window, so it
|
|
266
|
+
never has to wait for the user to pan before it knows what is on screen.
|
|
267
|
+
- It then fires **only when the window actually changes**. Indices are
|
|
268
|
+
integers, so a slow pan at 9px/bar produces roughly one event every nine
|
|
269
|
+
frames, not one per frame.
|
|
270
|
+
- `spacing` is reported but is deliberately *not* part of the change test — it
|
|
271
|
+
is a float that moves every frame of an eased zoom, and keying on it would
|
|
272
|
+
turn this into a 60/sec firehose. Use it for level-of-detail decisions.
|
|
273
|
+
- `settled` *is* part of the change test, so the final event of a gesture
|
|
274
|
+
always arrives with `settled: true`. That makes "wait until the view stops
|
|
275
|
+
moving, then do the expensive thing" a safe pattern.
|
|
276
|
+
|
|
277
|
+
```js
|
|
278
|
+
const off = chart.subscribe('visibleRange', async (r) => {
|
|
279
|
+
// paginate backwards when the user approaches the left edge
|
|
280
|
+
if (r.from < 50 && r.settled && r.fromTime) {
|
|
281
|
+
const older = await myApi.bars({ to: r.fromTime, limit: 500 })
|
|
282
|
+
chart.setData(older.concat(chart.bars))
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Syncing a second chart is the other common use — feed `fromTime`/`toTime`
|
|
288
|
+
straight into the other instance. And `chart.visibleRange()` returns the same
|
|
289
|
+
payload on demand if you would rather poll than subscribe.
|
|
290
|
+
|
|
291
|
+
> The chart also paginates backwards **on its own** through
|
|
292
|
+
> `feed.getBars({ to })` whenever you have attached a feed. This event is for
|
|
293
|
+
> when you want to drive that yourself, or to drive something other than data.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Replay
|
|
298
|
+
|
|
299
|
+
Play a fixed dataset back bar by bar — backtesting playback, a market-open
|
|
300
|
+
recap, a training drill.
|
|
301
|
+
|
|
302
|
+
```js
|
|
303
|
+
chart.setData(bars)
|
|
304
|
+
|
|
305
|
+
const replay = chart.startReplay({ from: 200, speed: 4 })
|
|
306
|
+
replay.play()
|
|
307
|
+
|
|
308
|
+
chart.subscribe('replay', (s) => {
|
|
309
|
+
scrubber.value = s.index
|
|
310
|
+
clock.textContent = new Date(s.time).toLocaleTimeString()
|
|
311
|
+
if (s.atEnd) playBtn.textContent = 'Restart'
|
|
312
|
+
})
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
The chart is never put into a special mode. The controller keeps the dataset
|
|
316
|
+
aside and hands the chart only the **revealed prefix**, so scales, crosshair,
|
|
317
|
+
annotations and `'visibleRange'` behave exactly as they do on live data that
|
|
318
|
+
happens to end at the cursor.
|
|
319
|
+
|
|
320
|
+
Revealing the next bar goes through the same path a feed tick takes, so the
|
|
321
|
+
candle grows in and the axis glides. Scrubbing swaps the prefix and jumps —
|
|
322
|
+
easing a scrub would read as lag, the same rule the pan gesture follows.
|
|
323
|
+
|
|
324
|
+
### `chart.startReplay(options?)`
|
|
325
|
+
|
|
326
|
+
| Option | Default | Notes |
|
|
327
|
+
|---|---|---|
|
|
328
|
+
| `bars` | the chart's current bars | The dataset to replay. Never mutated |
|
|
329
|
+
| `from` | midpoint | Starting cursor index |
|
|
330
|
+
| `speed` | `1` | Multiplier, clamped to `0.25`–`500` |
|
|
331
|
+
| `baseInterval` | `1000` | Real ms one bar takes at 1× |
|
|
332
|
+
| `loop` | `false` | Restart at the end instead of stopping |
|
|
333
|
+
| `follow` | `true` | Re-anchor the right edge on the cursor when scrubbing |
|
|
334
|
+
|
|
335
|
+
Returns the `Replay`, or `null` when there are fewer than two bars. While a
|
|
336
|
+
replay is active an attached feed is ignored, so live ticks cannot fight the
|
|
337
|
+
cursor; `stopReplay()` restores the full dataset and resumes normal service.
|
|
338
|
+
|
|
339
|
+
### Transport
|
|
340
|
+
|
|
341
|
+
Every method returns the controller, so calls chain.
|
|
342
|
+
|
|
343
|
+
| Method | Description |
|
|
344
|
+
|---|---|
|
|
345
|
+
| `play()` / `pause()` / `toggle()` | Pressing play at the end restarts from the beginning |
|
|
346
|
+
| `seek(index)` | Move the cursor. Out-of-range values clamp |
|
|
347
|
+
| `step(n = 1)` | Relative move; `step(-1)` goes back a bar |
|
|
348
|
+
| `toStart()` / `toEnd()` | Jump to either end |
|
|
349
|
+
| `setSpeed(x)` | Clamped to `0.25`–`500`. Never bursts bars on a rate change |
|
|
350
|
+
| `setLoop(bool)` | Toggle looping |
|
|
351
|
+
|
|
352
|
+
Readable state: `index`, `length`, `progress` (0–1), `speed`, `time`, `bar`,
|
|
353
|
+
`atEnd`, `playing`, and `interval` (real ms between bars at the current
|
|
354
|
+
speed).
|
|
355
|
+
|
|
356
|
+
### The `'replay'` event
|
|
357
|
+
|
|
358
|
+
`{ active, playing, index, length, progress, speed, time, bar, atEnd }`.
|
|
359
|
+
|
|
360
|
+
Like `'visibleRange'` it is a **state** event: a new subscriber is called
|
|
361
|
+
immediately, and after `stopReplay()` it fires once with `active: false` so a
|
|
362
|
+
UI can reset itself without special-casing teardown. `chart.replayState()`
|
|
363
|
+
returns the same payload on demand.
|
|
364
|
+
|
|
365
|
+
**Markers after the cursor are hidden, not clamped.** Time→index resolution
|
|
366
|
+
snaps to the nearest bar, so without that filter every future trade would pile
|
|
367
|
+
onto the newest revealed candle — and a replay that shows you tomorrow's
|
|
368
|
+
entries is worse than no replay at all.
|
|
369
|
+
|
|
370
|
+
The cursor never goes below index 1: the scales infer the timeframe from the
|
|
371
|
+
first pair of bars.
|
|
372
|
+
|
|
253
373
|
---
|
|
254
374
|
|
|
255
375
|
## Annotations
|
|
@@ -529,6 +649,7 @@ import {
|
|
|
529
649
|
TimeScale, PriceScale, // scales (advanced)
|
|
530
650
|
Smoothed, Tween, Inertia, // motion primitives
|
|
531
651
|
LiveCandle,
|
|
652
|
+
Replay, MIN_SPEED, MAX_SPEED, // bar-by-bar playback
|
|
532
653
|
easeOutCubic, easeInOutCubic,
|
|
533
654
|
mulberry32, // seeded PRNG
|
|
534
655
|
version,
|
|
@@ -607,8 +728,6 @@ src/App.jsx, src/styles.css the playground (not published)
|
|
|
607
728
|
|
|
608
729
|
Honest list of what isn't there yet:
|
|
609
730
|
|
|
610
|
-
- **`subscribe('visibleRange', fn)` never fires.** The event is accepted and
|
|
611
|
-
the handler is stored, but nothing emits it. Use `'crosshair'` for now.
|
|
612
731
|
- **No OHLCV legend in the package.** `subscribe('crosshair', fn)` gives you
|
|
613
732
|
the hovered bar; rendering the readout is still yours to do.
|
|
614
733
|
- **Markers are not draggable.** They are hit-tested for hover and click, but
|
package/index.d.ts
CHANGED
|
@@ -179,6 +179,28 @@ export interface CrosshairPayload {
|
|
|
179
179
|
price: number
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/** Payload of the 'visibleRange' event, and the return of chart.visibleRange(). */
|
|
183
|
+
export interface VisibleRangePayload {
|
|
184
|
+
/** First visible bar index, clamped to the loaded data. */
|
|
185
|
+
from: number
|
|
186
|
+
/** Last visible bar index, clamped to the loaded data. */
|
|
187
|
+
to: number
|
|
188
|
+
/** `bars[from].time`, or null when no bars are loaded. */
|
|
189
|
+
fromTime: number | null
|
|
190
|
+
/** `bars[to].time`, or null when no bars are loaded. */
|
|
191
|
+
toTime: number | null
|
|
192
|
+
/** Total bars currently loaded. */
|
|
193
|
+
barCount: number
|
|
194
|
+
/** Current pixels per bar — useful for level-of-detail decisions. */
|
|
195
|
+
spacing: number
|
|
196
|
+
/**
|
|
197
|
+
* False while the view is still easing. The last event of a gesture always
|
|
198
|
+
* arrives with `settled: true`, so it is safe to defer expensive work
|
|
199
|
+
* (a fetch, a re-aggregation) until you see it.
|
|
200
|
+
*/
|
|
201
|
+
settled: boolean
|
|
202
|
+
}
|
|
203
|
+
|
|
182
204
|
/* ------------------------------------------------------------------ feed -- */
|
|
183
205
|
|
|
184
206
|
export interface GetBarsRequest {
|
|
@@ -267,6 +289,80 @@ export declare class PriceScale {
|
|
|
267
289
|
readonly hi: number
|
|
268
290
|
}
|
|
269
291
|
|
|
292
|
+
/* ---------------------------------------------------------------- replay -- */
|
|
293
|
+
|
|
294
|
+
export interface ReplayOptions {
|
|
295
|
+
/** Dataset to replay. Defaults to the chart's current bars. */
|
|
296
|
+
bars?: Bar[]
|
|
297
|
+
/** Starting cursor index. Default: the midpoint of the dataset. */
|
|
298
|
+
from?: number
|
|
299
|
+
/** Rate multiplier, clamped to 0.25–500. Default 1. */
|
|
300
|
+
speed?: number
|
|
301
|
+
/** Real milliseconds one bar takes at 1×. Default 1000. */
|
|
302
|
+
baseInterval?: number
|
|
303
|
+
/** Restart from the beginning instead of stopping at the end. Default false. */
|
|
304
|
+
loop?: boolean
|
|
305
|
+
/** Re-anchor the right edge on the cursor when scrubbing. Default true. */
|
|
306
|
+
follow?: boolean
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Payload of the 'replay' event, and the return of chart.replayState(). */
|
|
310
|
+
export interface ReplayState {
|
|
311
|
+
/** False when the chart is not replaying; every other field is then inert. */
|
|
312
|
+
active: boolean
|
|
313
|
+
playing: boolean
|
|
314
|
+
/** Cursor: index of the newest revealed bar. -1 when inactive. */
|
|
315
|
+
index: number
|
|
316
|
+
/** Size of the dataset being replayed. */
|
|
317
|
+
length: number
|
|
318
|
+
/** 0 at the first playable bar, 1 at the last. */
|
|
319
|
+
progress: number
|
|
320
|
+
speed: number
|
|
321
|
+
/** `time` of the bar at the cursor. */
|
|
322
|
+
time: number | null
|
|
323
|
+
bar: Bar | null
|
|
324
|
+
atEnd: boolean
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Bar-by-bar playback over a fixed dataset. Obtained from
|
|
329
|
+
* `chart.startReplay()` or `chart.replay`; not constructed directly.
|
|
330
|
+
*/
|
|
331
|
+
export declare class Replay {
|
|
332
|
+
readonly source: Bar[]
|
|
333
|
+
readonly length: number
|
|
334
|
+
readonly lastIndex: number
|
|
335
|
+
readonly atEnd: boolean
|
|
336
|
+
readonly bar: Bar | null
|
|
337
|
+
readonly time: number | null
|
|
338
|
+
readonly progress: number
|
|
339
|
+
/** Real ms between bars at the current speed. */
|
|
340
|
+
readonly interval: number
|
|
341
|
+
index: number
|
|
342
|
+
speed: number
|
|
343
|
+
playing: boolean
|
|
344
|
+
looping: boolean
|
|
345
|
+
follow: boolean
|
|
346
|
+
baseInterval: number
|
|
347
|
+
|
|
348
|
+
play(): this
|
|
349
|
+
pause(): this
|
|
350
|
+
toggle(): this
|
|
351
|
+
/** Clamped to 0.25–500. */
|
|
352
|
+
setSpeed(speed: number): this
|
|
353
|
+
setLoop(on: boolean): this
|
|
354
|
+
/** Move the cursor. Out-of-range values clamp. */
|
|
355
|
+
seek(index: number): this
|
|
356
|
+
step(n?: number): this
|
|
357
|
+
toStart(): this
|
|
358
|
+
toEnd(): this
|
|
359
|
+
state(): ReplayState
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Speed bounds accepted by `setSpeed`. */
|
|
363
|
+
export declare const MIN_SPEED: number
|
|
364
|
+
export declare const MAX_SPEED: number
|
|
365
|
+
|
|
270
366
|
/* ----------------------------------------------------------------- chart -- */
|
|
271
367
|
|
|
272
368
|
export declare class Chart {
|
|
@@ -295,6 +391,21 @@ export declare class Chart {
|
|
|
295
391
|
/** Topmost marker under a plot-relative point, else null. */
|
|
296
392
|
markerAt(x: number, y: number): ResolvedMarker | null
|
|
297
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Start bar-by-bar playback. With no `bars`, the chart's current data is
|
|
396
|
+
* the dataset. Returns null if there are fewer than two bars to replay.
|
|
397
|
+
*
|
|
398
|
+
* chart.startReplay({ from: 200, speed: 4 })
|
|
399
|
+
* chart.replay.play()
|
|
400
|
+
*/
|
|
401
|
+
startReplay(options?: ReplayOptions): Replay | null
|
|
402
|
+
/** Leave replay and reveal the whole dataset again. */
|
|
403
|
+
stopReplay(): void
|
|
404
|
+
/** The active controller, or null. */
|
|
405
|
+
readonly replay: Replay | null
|
|
406
|
+
/** Current playback state; `{ active: false, ... }` when not replaying. */
|
|
407
|
+
replayState(): ReplayState
|
|
408
|
+
|
|
298
409
|
setTheme(theme: Partial<Theme>): void
|
|
299
410
|
setPriceMode(mode: PriceMode): void
|
|
300
411
|
setAnimate(on: boolean): void
|
|
@@ -306,14 +417,21 @@ export declare class Chart {
|
|
|
306
417
|
/** Rolling frames-per-second of the render loop. */
|
|
307
418
|
readonly fps: number
|
|
308
419
|
|
|
420
|
+
/** The window currently on screen. Cheap enough to poll. */
|
|
421
|
+
visibleRange(): VisibleRangePayload
|
|
422
|
+
|
|
309
423
|
/**
|
|
310
424
|
* Subscribe to a chart event. Returns an unsubscribe function.
|
|
311
|
-
*
|
|
425
|
+
*
|
|
426
|
+
* 'visibleRange' is a state event rather than a notification: a new
|
|
427
|
+
* subscriber is called immediately with the current window, and then only
|
|
428
|
+
* when that window actually changes — so no debouncing is required.
|
|
312
429
|
*/
|
|
313
430
|
subscribe(event: 'crosshair', fn: (payload: CrosshairPayload | null) => void): () => void
|
|
314
431
|
subscribe(event: 'markerClick', fn: (marker: ResolvedMarker) => void): () => void
|
|
315
432
|
subscribe(event: 'markerHover', fn: (marker: ResolvedMarker | null) => void): () => void
|
|
316
|
-
subscribe(event: 'visibleRange', fn: (range:
|
|
433
|
+
subscribe(event: 'visibleRange', fn: (range: VisibleRangePayload) => void): () => void
|
|
434
|
+
subscribe(event: 'replay', fn: (state: ReplayState) => void): () => void
|
|
317
435
|
|
|
318
436
|
/** Removes listeners, canvases and the render loop. */
|
|
319
437
|
destroy(): void
|