emberwick 0.2.0 → 0.3.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 +38 -3
- package/index.d.ts +30 -2
- package/index.js +50 -0
- 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
|
@@ -240,16 +240,53 @@ const off = chart.subscribe('crosshair', (payload) => {
|
|
|
240
240
|
off() // unsubscribe
|
|
241
241
|
```
|
|
242
242
|
|
|
243
|
+
| Event | Payload |
|
|
244
|
+
|---|---|
|
|
243
245
|
| Event | Payload |
|
|
244
246
|
|---|---|
|
|
245
247
|
| `'crosshair'` | `{ index, bar, price }`, or `null` when the pointer leaves the plot |
|
|
246
248
|
| `'markerHover'` | The marker under the pointer, or `null` when none is |
|
|
247
249
|
| `'markerClick'` | The clicked marker. Only fires on a hit, never with `null` |
|
|
248
|
-
| `'visibleRange'` |
|
|
250
|
+
| `'visibleRange'` | `{ from, to, fromTime, toTime, barCount, spacing, settled }` |
|
|
249
251
|
|
|
250
252
|
A drag that happens to end on top of a marker does not fire `'markerClick'` —
|
|
251
253
|
panning and clicking stay distinct.
|
|
252
254
|
|
|
255
|
+
### Tracking the visible range
|
|
256
|
+
|
|
257
|
+
`'visibleRange'` is a **state** event rather than a notification, which makes it
|
|
258
|
+
usable without any debouncing of your own:
|
|
259
|
+
|
|
260
|
+
- A new subscriber is called **immediately** with the current window, so it
|
|
261
|
+
never has to wait for the user to pan before it knows what is on screen.
|
|
262
|
+
- It then fires **only when the window actually changes**. Indices are
|
|
263
|
+
integers, so a slow pan at 9px/bar produces roughly one event every nine
|
|
264
|
+
frames, not one per frame.
|
|
265
|
+
- `spacing` is reported but is deliberately *not* part of the change test — it
|
|
266
|
+
is a float that moves every frame of an eased zoom, and keying on it would
|
|
267
|
+
turn this into a 60/sec firehose. Use it for level-of-detail decisions.
|
|
268
|
+
- `settled` *is* part of the change test, so the final event of a gesture
|
|
269
|
+
always arrives with `settled: true`. That makes "wait until the view stops
|
|
270
|
+
moving, then do the expensive thing" a safe pattern.
|
|
271
|
+
|
|
272
|
+
```js
|
|
273
|
+
const off = chart.subscribe('visibleRange', async (r) => {
|
|
274
|
+
// paginate backwards when the user approaches the left edge
|
|
275
|
+
if (r.from < 50 && r.settled && r.fromTime) {
|
|
276
|
+
const older = await myApi.bars({ to: r.fromTime, limit: 500 })
|
|
277
|
+
chart.setData(older.concat(chart.bars))
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Syncing a second chart is the other common use — feed `fromTime`/`toTime`
|
|
283
|
+
straight into the other instance. And `chart.visibleRange()` returns the same
|
|
284
|
+
payload on demand if you would rather poll than subscribe.
|
|
285
|
+
|
|
286
|
+
> The chart also paginates backwards **on its own** through
|
|
287
|
+
> `feed.getBars({ to })` whenever you have attached a feed. This event is for
|
|
288
|
+
> when you want to drive that yourself, or to drive something other than data.
|
|
289
|
+
|
|
253
290
|
---
|
|
254
291
|
|
|
255
292
|
## Annotations
|
|
@@ -607,8 +644,6 @@ src/App.jsx, src/styles.css the playground (not published)
|
|
|
607
644
|
|
|
608
645
|
Honest list of what isn't there yet:
|
|
609
646
|
|
|
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
647
|
- **No OHLCV legend in the package.** `subscribe('crosshair', fn)` gives you
|
|
613
648
|
the hovered bar; rendering the readout is still yours to do.
|
|
614
649
|
- **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 {
|
|
@@ -306,14 +328,20 @@ export declare class Chart {
|
|
|
306
328
|
/** Rolling frames-per-second of the render loop. */
|
|
307
329
|
readonly fps: number
|
|
308
330
|
|
|
331
|
+
/** The window currently on screen. Cheap enough to poll. */
|
|
332
|
+
visibleRange(): VisibleRangePayload
|
|
333
|
+
|
|
309
334
|
/**
|
|
310
335
|
* Subscribe to a chart event. Returns an unsubscribe function.
|
|
311
|
-
*
|
|
336
|
+
*
|
|
337
|
+
* 'visibleRange' is a state event rather than a notification: a new
|
|
338
|
+
* subscriber is called immediately with the current window, and then only
|
|
339
|
+
* when that window actually changes — so no debouncing is required.
|
|
312
340
|
*/
|
|
313
341
|
subscribe(event: 'crosshair', fn: (payload: CrosshairPayload | null) => void): () => void
|
|
314
342
|
subscribe(event: 'markerClick', fn: (marker: ResolvedMarker) => void): () => void
|
|
315
343
|
subscribe(event: 'markerHover', fn: (marker: ResolvedMarker | null) => void): () => void
|
|
316
|
-
subscribe(event: 'visibleRange', fn: (range:
|
|
344
|
+
subscribe(event: 'visibleRange', fn: (range: VisibleRangePayload) => void): () => void
|
|
317
345
|
|
|
318
346
|
/** Removes listeners, canvases and the render loop. */
|
|
319
347
|
destroy(): void
|
package/index.js
CHANGED
|
@@ -1394,8 +1394,57 @@ class Chart {
|
|
|
1394
1394
|
const set = this._listeners[event];
|
|
1395
1395
|
if (!set) throw new Error(`Chart: unknown event "${event}"`);
|
|
1396
1396
|
set.add(fn);
|
|
1397
|
+
if (event === "visibleRange") {
|
|
1398
|
+
const payload = this.visibleRange();
|
|
1399
|
+
this._rangeKey = this._rangeIdentity(payload);
|
|
1400
|
+
fn(payload);
|
|
1401
|
+
}
|
|
1397
1402
|
return () => set.delete(fn);
|
|
1398
1403
|
}
|
|
1404
|
+
// ------------------------------------------------------------------ range --
|
|
1405
|
+
/**
|
|
1406
|
+
* The window currently on screen. Cheap enough to poll, though
|
|
1407
|
+
* subscribe('visibleRange', fn) is the better way to track it.
|
|
1408
|
+
*/
|
|
1409
|
+
visibleRange() {
|
|
1410
|
+
const { from, to } = this.ts.visibleRange();
|
|
1411
|
+
return this._rangePayload(from, to);
|
|
1412
|
+
}
|
|
1413
|
+
_rangePayload(from, to) {
|
|
1414
|
+
const n = this.bars.length;
|
|
1415
|
+
return {
|
|
1416
|
+
from,
|
|
1417
|
+
to,
|
|
1418
|
+
fromTime: n ? this.bars[from].time : null,
|
|
1419
|
+
toTime: n ? this.bars[to].time : null,
|
|
1420
|
+
barCount: n,
|
|
1421
|
+
spacing: this.ts.spacing,
|
|
1422
|
+
settled: this.ts.settled
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
_rangeIdentity(p) {
|
|
1426
|
+
return `${p.from}:${p.to}:${p.fromTime}:${p.toTime}:${p.settled ? 1 : 0}`;
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Fires only when the window actually changed, so a consumer can hang a
|
|
1430
|
+
* fetch off it without debouncing. Two deliberate choices:
|
|
1431
|
+
*
|
|
1432
|
+
* - `spacing` is NOT part of the identity. It is a float that moves on every
|
|
1433
|
+
* frame of an eased zoom, so keying on it would make this a 60/sec
|
|
1434
|
+
* firehose. It is still reported, for level-of-detail decisions.
|
|
1435
|
+
* - `settled` IS part of the identity, so the final event of a gesture
|
|
1436
|
+
* always arrives with settled:true. Without it, code that defers expensive
|
|
1437
|
+
* work until the view stops moving would wait forever.
|
|
1438
|
+
*/
|
|
1439
|
+
_emitVisibleRange(from, to) {
|
|
1440
|
+
const set = this._listeners.visibleRange;
|
|
1441
|
+
if (!set.size) return;
|
|
1442
|
+
const payload = this._rangePayload(from, to);
|
|
1443
|
+
const key = this._rangeIdentity(payload);
|
|
1444
|
+
if (key === this._rangeKey) return;
|
|
1445
|
+
this._rangeKey = key;
|
|
1446
|
+
for (const fn of set) fn(payload);
|
|
1447
|
+
}
|
|
1399
1448
|
// ----------------------------------------------------------- annotations --
|
|
1400
1449
|
/** Replace every marker. Each `time` is resolved to its nearest bar. */
|
|
1401
1450
|
setMarkers(markers) {
|
|
@@ -1482,6 +1531,7 @@ class Chart {
|
|
|
1482
1531
|
if (redrawAll || dirty.has("overlay")) {
|
|
1483
1532
|
drawCrosshair(this.layers.ctx.overlay, state);
|
|
1484
1533
|
}
|
|
1534
|
+
this._emitVisibleRange(from, to);
|
|
1485
1535
|
return animating;
|
|
1486
1536
|
}
|
|
1487
1537
|
// ------------------------------------------------------------------- api --
|