emberwick 0.3.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 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 |
@@ -248,6 +252,7 @@ off() // unsubscribe
248
252
  | `'markerHover'` | The marker under the pointer, or `null` when none is |
249
253
  | `'markerClick'` | The clicked marker. Only fires on a hit, never with `null` |
250
254
  | `'visibleRange'` | `{ from, to, fromTime, toTime, barCount, spacing, settled }` |
255
+ | `'replay'` | `{ active, playing, index, length, progress, speed, time, bar, atEnd }` |
251
256
 
252
257
  A drag that happens to end on top of a marker does not fire `'markerClick'` —
253
258
  panning and clicking stay distinct.
@@ -289,6 +294,84 @@ payload on demand if you would rather poll than subscribe.
289
294
 
290
295
  ---
291
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
+
373
+ ---
374
+
292
375
  ## Annotations
293
376
 
294
377
  Three independent collections, each replaced wholesale. Zones paint behind the
@@ -566,6 +649,7 @@ import {
566
649
  TimeScale, PriceScale, // scales (advanced)
567
650
  Smoothed, Tween, Inertia, // motion primitives
568
651
  LiveCandle,
652
+ Replay, MIN_SPEED, MAX_SPEED, // bar-by-bar playback
569
653
  easeOutCubic, easeInOutCubic,
570
654
  mulberry32, // seeded PRNG
571
655
  version,
package/index.d.ts CHANGED
@@ -289,6 +289,80 @@ export declare class PriceScale {
289
289
  readonly hi: number
290
290
  }
291
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
+
292
366
  /* ----------------------------------------------------------------- chart -- */
293
367
 
294
368
  export declare class Chart {
@@ -317,6 +391,21 @@ export declare class Chart {
317
391
  /** Topmost marker under a plot-relative point, else null. */
318
392
  markerAt(x: number, y: number): ResolvedMarker | null
319
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
+
320
409
  setTheme(theme: Partial<Theme>): void
321
410
  setPriceMode(mode: PriceMode): void
322
411
  setAnimate(on: boolean): void
@@ -342,6 +431,7 @@ export declare class Chart {
342
431
  subscribe(event: 'markerClick', fn: (marker: ResolvedMarker) => void): () => void
343
432
  subscribe(event: 'markerHover', fn: (marker: ResolvedMarker | null) => void): () => void
344
433
  subscribe(event: 'visibleRange', fn: (range: VisibleRangePayload) => void): () => void
434
+ subscribe(event: 'replay', fn: (state: ReplayState) => void): () => void
345
435
 
346
436
  /** Removes listeners, canvases and the render loop. */
347
437
  destroy(): void