@funnest/charts 0.4.0 → 0.5.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/dist/chart.d.ts CHANGED
@@ -141,6 +141,8 @@ export declare class Chart {
141
141
  private priceAxisWidth;
142
142
  private frame;
143
143
  private crosshair;
144
+ /** Time (UTC ms) of a crosshair mirrored from a linked chart; see setSyncedCrosshair(). */
145
+ private syncedTime;
144
146
  private lastCrosshairKey;
145
147
  private lastRangeKey;
146
148
  private lastLayoutKey;
@@ -193,6 +195,30 @@ export declare class Chart {
193
195
  scrollBars(bars: number): void;
194
196
  /** True when the newest bar is visible. */
195
197
  isAtRealtime(): boolean;
198
+ /**
199
+ * Mirrors another chart's crosshair: a vertical line at `time` (UTC ms) on the bar that
200
+ * contains it, with the time badge but no price line. `crosshairMove` fires with
201
+ * `source: 'sync'`, so legends follow along. A pointer on this chart takes precedence.
202
+ * Pass null to clear.
203
+ */
204
+ setSyncedCrosshair(time: number | null): void;
205
+ /**
206
+ * The time span (UTC ms) between the plot's left and right edges. Edges past the data
207
+ * (the empty space right of the last bar) are extrapolated by the bar interval.
208
+ */
209
+ getVisibleTimeRange(): {
210
+ from: number;
211
+ to: number;
212
+ } | null;
213
+ /** Scrolls and zooms so the plot spans `from`…`to` (UTC ms), e.g. to match a linked chart. */
214
+ setVisibleTimeRange(range: {
215
+ from: number;
216
+ to: number;
217
+ }): void;
218
+ /** Fractional bar index → time, interpolating between bars and extrapolating past either end. */
219
+ private indexToTime;
220
+ /** Inverse of indexToTime(). */
221
+ private timeToIndex;
196
222
  /** Snapshot of the chart (without the crosshair) at device resolution. */
197
223
  takeScreenshot(): HTMLCanvasElement;
198
224
  /**
@@ -286,6 +312,8 @@ export declare class Chart {
286
312
  private afterFrame;
287
313
  private requestHistory;
288
314
  private resolveCrosshair;
315
+ /** The bar containing `time`, as a crosshair without a price (nothing when it's off the data). */
316
+ private resolveSyncedCrosshair;
289
317
  private paneAt;
290
318
  private setTooltip;
291
319
  private executionTooltip;
package/dist/index.js CHANGED
@@ -1681,6 +1681,7 @@ var DEFAULT_CHART_SETTINGS = {
1681
1681
  gridHorizontal: true,
1682
1682
  gridVertical: true,
1683
1683
  volume: true,
1684
+ navigationButtons: true,
1684
1685
  upColor: null,
1685
1686
  downColor: null
1686
1687
  };
@@ -2239,6 +2240,17 @@ function drawOverlay(layer, m, ch, tooltip = null) {
2239
2240
  if (tooltip) drawTooltip(p, f, tooltip);
2240
2241
  if (!ch) return;
2241
2242
  p.vLine(ch.x, 0, f.panesHeight, pal.crosshair, DASH);
2243
+ if (ch.source !== "sync") drawCrosshairValue(p, m, ch);
2244
+ const timeLabel = m.formatCrosshairTime(ch.time);
2245
+ const w = p.measure(timeLabel) + 16;
2246
+ const cx = Math.min(f.plotWidth - w / 2, Math.max(w / 2, ch.x));
2247
+ p.roundRect(cx - w / 2, f.panesHeight + 4, w, f.timeAxisHeight - 8, 4, pal.text);
2248
+ p.text(timeLabel, cx, f.panesHeight + f.timeAxisHeight / 2, pal.background, "center");
2249
+ }
2250
+ function drawCrosshairValue(p, m, ch) {
2251
+ const f = m.frame;
2252
+ const pal = f.palette;
2253
+ const ctx = p.ctx;
2242
2254
  p.hLine(ch.y, 0, f.plotWidth, pal.crosshair, DASH);
2243
2255
  if (ch.source === "touch") {
2244
2256
  ctx.beginPath();
@@ -2255,11 +2267,6 @@ function drawOverlay(layer, m, ch, tooltip = null) {
2255
2267
  const by = Math.min(f.panesHeight - BADGE_HEIGHT / 2, Math.max(BADGE_HEIGHT / 2, ch.y));
2256
2268
  p.roundRect(f.plotWidth + 2, by - BADGE_HEIGHT / 2, f.width - f.plotWidth - 4, BADGE_HEIGHT, 4, pal.text);
2257
2269
  p.text(valueLabel, f.plotWidth + 8, by, pal.background);
2258
- const timeLabel = m.formatCrosshairTime(ch.time);
2259
- const w = p.measure(timeLabel) + 16;
2260
- const cx = Math.min(f.plotWidth - w / 2, Math.max(w / 2, ch.x));
2261
- p.roundRect(cx - w / 2, f.panesHeight + 4, w, f.timeAxisHeight - 8, 4, pal.text);
2262
- p.text(timeLabel, cx, f.panesHeight + f.timeAxisHeight / 2, pal.background, "center");
2263
2270
  }
2264
2271
 
2265
2272
  // src/resolution.ts
@@ -2385,6 +2392,8 @@ var Chart = class {
2385
2392
  priceAxisWidth = MIN_PRICE_AXIS_WIDTH;
2386
2393
  frame = null;
2387
2394
  crosshair = null;
2395
+ /** Time (UTC ms) of a crosshair mirrored from a linked chart; see setSyncedCrosshair(). */
2396
+ syncedTime = null;
2388
2397
  lastCrosshairKey = "";
2389
2398
  lastRangeKey = "";
2390
2399
  lastLayoutKey = "";
@@ -2611,6 +2620,61 @@ var Chart = class {
2611
2620
  isAtRealtime() {
2612
2621
  return this.store.length === 0 || this.timeScale.visibleRange().to >= this.store.length - 1;
2613
2622
  }
2623
+ /**
2624
+ * Mirrors another chart's crosshair: a vertical line at `time` (UTC ms) on the bar that
2625
+ * contains it, with the time badge but no price line. `crosshairMove` fires with
2626
+ * `source: 'sync'`, so legends follow along. A pointer on this chart takes precedence.
2627
+ * Pass null to clear.
2628
+ */
2629
+ setSyncedCrosshair(time) {
2630
+ if (time === this.syncedTime) return;
2631
+ this.syncedTime = time;
2632
+ this.invalidate(false);
2633
+ }
2634
+ /**
2635
+ * The time span (UTC ms) between the plot's left and right edges. Edges past the data
2636
+ * (the empty space right of the last bar) are extrapolated by the bar interval.
2637
+ */
2638
+ getVisibleTimeRange() {
2639
+ const ts = this.timeScale;
2640
+ if (this.store.length === 0 || ts.width <= 0) return null;
2641
+ return { from: this.indexToTime(ts.xToIndexFloat(0)), to: this.indexToTime(ts.xToIndexFloat(ts.width)) };
2642
+ }
2643
+ /** Scrolls and zooms so the plot spans `from`…`to` (UTC ms), e.g. to match a linked chart. */
2644
+ setVisibleTimeRange(range) {
2645
+ const ts = this.timeScale;
2646
+ if (this.store.length === 0 || ts.width <= 0 || !(range.to > range.from)) return;
2647
+ const from = this.timeToIndex(range.from);
2648
+ const to = this.timeToIndex(range.to);
2649
+ if (!(to > from)) return;
2650
+ ts.barSpacing = ts.width / (to - from);
2651
+ ts.rightOffset = to - 0.5 - (this.store.length - 1);
2652
+ ts.clamp();
2653
+ this.invalidate();
2654
+ }
2655
+ /** Fractional bar index → time, interpolating between bars and extrapolating past either end. */
2656
+ indexToTime(index) {
2657
+ const bars = this.store.bars;
2658
+ const last = bars.length - 1;
2659
+ const step = this.barInterval();
2660
+ if (index <= 0) return bars[0].time + index * step;
2661
+ if (index >= last) return bars[last].time + (index - last) * step;
2662
+ const i = Math.floor(index);
2663
+ const a = bars[i].time;
2664
+ return a + (bars[i + 1].time - a) * (index - i);
2665
+ }
2666
+ /** Inverse of indexToTime(). */
2667
+ timeToIndex(time) {
2668
+ const bars = this.store.bars;
2669
+ const last = bars.length - 1;
2670
+ const step = this.barInterval();
2671
+ if (time <= bars[0].time) return (time - bars[0].time) / step;
2672
+ if (time >= bars[last].time) return last + (time - bars[last].time) / step;
2673
+ const i = this.store.indexAtOrBefore(time);
2674
+ const a = bars[i].time;
2675
+ const b = bars[i + 1].time;
2676
+ return i + (b > a ? (time - a) / (b - a) : 0);
2677
+ }
2614
2678
  /** Snapshot of the chart (without the crosshair) at device resolution. */
2615
2679
  takeScreenshot() {
2616
2680
  if (this.rafId || this.mainDirty) this.render();
@@ -3232,8 +3296,9 @@ var Chart = class {
3232
3296
  resolveCrosshair(frame) {
3233
3297
  const ch = this.crosshair;
3234
3298
  const bars = this.store.bars;
3235
- if (!ch || bars.length === 0) return null;
3236
- if (ch.source === "mouse" && (ch.x < 0 || ch.x > frame.plotWidth || ch.y < 0 || ch.y > frame.panesHeight)) return null;
3299
+ if (bars.length === 0) return null;
3300
+ const outside = ch?.source === "mouse" && (ch.x < 0 || ch.x > frame.plotWidth || ch.y < 0 || ch.y > frame.panesHeight);
3301
+ if (!ch || outside) return this.syncedTime === null ? null : this.resolveSyncedCrosshair(this.syncedTime);
3237
3302
  const index = Math.min(bars.length - 1, Math.max(0, this.timeScale.xToIndex(ch.x)));
3238
3303
  const bar = bars[index];
3239
3304
  const x = this.timeScale.indexToX(index);
@@ -3243,6 +3308,18 @@ var Chart = class {
3243
3308
  const value = pane.kind === "main" ? roundToTick(raw, this.tickSize) : raw;
3244
3309
  return { index, time: bar.time, bar, value, pane: pane.id, x, y, source: ch.source };
3245
3310
  }
3311
+ /** The bar containing `time`, as a crosshair without a price (nothing when it's off the data). */
3312
+ resolveSyncedCrosshair(time) {
3313
+ const bars = this.store.bars;
3314
+ const last = bars[bars.length - 1];
3315
+ if (time < bars[0].time || time >= last.time + this.barInterval()) return null;
3316
+ const index = this.store.indexAtOrBefore(time);
3317
+ const bar = bars[index];
3318
+ if (!bar) return null;
3319
+ const x = this.timeScale.indexToX(index);
3320
+ if (x < 0 || x > this.timeScale.width) return null;
3321
+ return { index, time: bar.time, bar, value: bar.close, pane: "main", x, y: -1, source: "sync" };
3322
+ }
3246
3323
  paneAt(frame, y) {
3247
3324
  return frame.panes.find((p) => y >= p.top && y < p.top + p.height) ?? frame.panes[frame.panes.length - 1];
3248
3325
  }
@@ -67,7 +67,8 @@ export interface CrosshairInfo {
67
67
  pane: PaneId;
68
68
  x: number;
69
69
  y: number;
70
- source: 'mouse' | 'touch';
70
+ /** 'sync': mirrored from a linked chart (setSyncedCrosshair) — time only, `y` is -1. */
71
+ source: 'mouse' | 'touch' | 'sync';
71
72
  }
72
73
  /** Fill details shown next to an execution marker. */
73
74
  export interface Tooltip {
@@ -12,6 +12,8 @@ export interface ChartSettings {
12
12
  gridVertical: boolean;
13
13
  /** Volume pane under the price pane. */
14
14
  volume: boolean;
15
+ /** Zoom/scroll buttons at the bottom center of the chart (@funnest/charts-ui). */
16
+ navigationButtons: boolean;
15
17
  /** Up/down candle colors; null follows the theme. */
16
18
  upColor: string | null;
17
19
  downColor: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnest/charts",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Canvas charting engine for financial charts",
5
5
  "keywords": [
6
6
  "chart",