@wick-charts/vue 0.5.1 → 0.6.1

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.
Files changed (82) hide show
  1. package/README.md +45 -6
  2. package/dist/BarSeries.vue.js +26 -20
  3. package/dist/CandlestickSeries.vue.js +30 -24
  4. package/dist/ChartContainer.vue.js +125 -72
  5. package/dist/HeatmapSeries.vue.js +40 -0
  6. package/dist/HeatmapSeries.vue2.js +4 -0
  7. package/dist/LineSeries.vue.js +26 -20
  8. package/dist/PieSeries.vue.js +24 -18
  9. package/dist/core/dist/animation/animator.js +21 -21
  10. package/dist/core/dist/animation/config.js +114 -87
  11. package/dist/core/dist/animation/easing.js +3 -1
  12. package/dist/core/dist/animation/intro-wave.js +68 -0
  13. package/dist/core/dist/axis/dom-labels.js +13 -14
  14. package/dist/core/dist/chart/badge-animator.js +56 -0
  15. package/dist/core/dist/chart/edge-indicators.js +28 -13
  16. package/dist/core/dist/chart/last-value.js +18 -17
  17. package/dist/core/dist/chart/options.js +27 -16
  18. package/dist/core/dist/chart/y-target.js +13 -12
  19. package/dist/core/dist/chart.js +868 -441
  20. package/dist/core/dist/components/annotation-chip.js +28 -0
  21. package/dist/core/dist/components/crosshair.js +8 -6
  22. package/dist/core/dist/components/edge-indicator.js +58 -26
  23. package/dist/core/dist/components/loading-indicator.js +46 -0
  24. package/dist/core/dist/components/marker.js +49 -31
  25. package/dist/core/dist/components/reference-line.js +27 -36
  26. package/dist/core/dist/components/time-region.js +30 -28
  27. package/dist/core/dist/data/store.js +38 -19
  28. package/dist/core/dist/data/sync.js +60 -38
  29. package/dist/core/dist/heatmap-tooltip.js +16 -0
  30. package/dist/core/dist/interactions/handler.js +86 -46
  31. package/dist/core/dist/perf/perf-hud.js +5 -5
  32. package/dist/core/dist/scales/x-scale.js +85 -37
  33. package/dist/core/dist/scales/y-scale.js +38 -27
  34. package/dist/core/dist/series/bar-intro.js +18 -0
  35. package/dist/core/dist/series/bar.js +337 -205
  36. package/dist/core/dist/series/base-multi-layer.js +224 -96
  37. package/dist/core/dist/series/candlestick-intro.js +57 -0
  38. package/dist/core/dist/series/candlestick.js +454 -276
  39. package/dist/core/dist/series/heatmap.js +387 -0
  40. package/dist/core/dist/series/line-intro.js +85 -0
  41. package/dist/core/dist/series/line.js +590 -282
  42. package/dist/core/dist/series/painters/builtins.js +2 -2
  43. package/dist/core/dist/series/pie.js +396 -259
  44. package/dist/core/dist/series/register-builtins.js +8 -7
  45. package/dist/core/dist/series/types.js +6 -0
  46. package/dist/core/dist/series/wave-intro.js +26 -0
  47. package/dist/core/dist/theme/create.js +69 -63
  48. package/dist/core/dist/theme/themes/mint-breeze.js +2 -2
  49. package/dist/core/dist/theme/themes/sand-dune.js +2 -2
  50. package/dist/core/dist/utils/color.js +109 -34
  51. package/dist/core/dist/utils/deep-equal.js +18 -0
  52. package/dist/core/dist/utils/time.js +25 -19
  53. package/dist/index.d.ts +1494 -122
  54. package/dist/index.js +177 -130
  55. package/dist/ui/Crosshair.vue.js +20 -21
  56. package/dist/ui/HeatmapTooltip.vue.js +96 -0
  57. package/dist/ui/HeatmapTooltip.vue2.js +4 -0
  58. package/dist/ui/InfoBar.vue.js +46 -47
  59. package/dist/ui/Legend.vue.js +80 -79
  60. package/dist/ui/PieLegend.vue.js +5 -5
  61. package/dist/ui/PieTooltip.vue.js +77 -67
  62. package/dist/ui/Tooltip.vue.js +60 -60
  63. package/dist/ui/YAxis.vue.js +2 -2
  64. package/dist/ui/YLabel.vue.js +94 -61
  65. package/package.json +2 -2
  66. package/src/BarSeries.vue +23 -7
  67. package/src/CandlestickSeries.vue +23 -7
  68. package/src/ChartContainer.vue +250 -13
  69. package/src/HeatmapSeries.vue +69 -0
  70. package/src/LineSeries.vue +23 -7
  71. package/src/PieSeries.vue +22 -6
  72. package/src/index.ts +84 -3
  73. package/src/ui/Crosshair.vue +1 -2
  74. package/src/ui/HeatmapTooltip.vue +144 -0
  75. package/src/ui/InfoBar.vue +1 -2
  76. package/src/ui/Legend.vue +13 -0
  77. package/src/ui/Marker.vue +1 -1
  78. package/src/ui/PieTooltip.vue +10 -0
  79. package/src/ui/ReferenceLine.vue +2 -2
  80. package/src/ui/TimeRegion.vue +2 -2
  81. package/src/ui/Tooltip.vue +2 -2
  82. package/src/ui/YLabel.vue +123 -8
@@ -5,9 +5,15 @@ import {
5
5
  ChartInstance,
6
6
  type ChartOptions,
7
7
  type ChartTheme,
8
+ type CrosshairPosition,
8
9
  type EdgeReachedInfo,
10
+ type FadeConfig,
11
+ type PointClickInfo,
12
+ type SeriesHoverInfo,
13
+ type VisibleRange,
9
14
  type VisibleRangeSpec,
10
15
  catppuccin,
16
+ deepEqual,
11
17
  } from '@wick-charts/core';
12
18
  import { computed, nextTick, onMounted, onUnmounted, provide, ref, shallowRef, watch } from 'vue';
13
19
 
@@ -25,7 +31,9 @@ type PerfOption = NonNullable<ChartOptions['perf']>;
25
31
 
26
32
  const props = withDefaults(
27
33
  defineProps<{
34
+ /** Visual theme. Live — changing this at runtime updates all themed elements. */
28
35
  theme?: ChartTheme;
36
+ /** Grouped axis configuration (Y/X visibility, bounds, sizing). Live. */
29
37
  axis?: AxisConfig;
30
38
  /**
31
39
  * Viewport padding. `top`/`bottom` are in pixels. `left`/`right` accept either pixels (`50`)
@@ -61,14 +69,55 @@ const props = withDefaults(
61
69
  */
62
70
  initialRange?: VisibleRangeSpec;
63
71
  };
64
- /** Show the chart background gradient. Defaults to true. */
72
+ /**
73
+ * Controlled visible range. Same shape as the imperative
74
+ * `chart.setVisibleRange` — a bar count, an explicit `{from, to}` window,
75
+ * or `{from, bars}`. Every reference/value change applies via
76
+ * `chart.setVisibleRange`, so pair it with `onVisibleRangeChange` for a
77
+ * two-way binding; a same-value literal (compared structurally) is a
78
+ * no-op, so an inline object doesn't re-apply on every render.
79
+ *
80
+ * For a one-shot initial window that isn't re-applied on every prop
81
+ * change, use `viewport.initialRange` instead.
82
+ */
83
+ visibleRange?: VisibleRangeSpec;
84
+ /** Show the chart background gradient. Live. Defaults to true. */
65
85
  gradient?: boolean;
66
- /** Enable zoom, pan, and crosshair interactions. Defaults to true. */
86
+ /**
87
+ * Enable zoom, pan, and crosshair interactions. Defaults to true.
88
+ *
89
+ * Mount-only — core has no `setInteractive`, so changing this prop after
90
+ * mount is ignored.
91
+ */
67
92
  interactive?: boolean;
68
- /** Background grid configuration. Default: `{ visible: true }`. */
93
+ /** Background grid configuration. Live. Default: `{ visible: true }`. */
69
94
  grid?: { visible: boolean };
70
95
  /**
71
- * How `<Title>` and `<InfoBar>` are positioned relative to the canvas.
96
+ * Soft fade-out at the top of the plot area, so series content dissolves
97
+ * under a floating `<Title>` / `<InfoBar>` instead of colliding with it.
98
+ * The mask erases canvas alpha rather than painting a cover color, so it
99
+ * stays correct over the default background gradient. Live.
100
+ *
101
+ * Two directions, independently controlled:
102
+ *
103
+ * **X (under the axis)** — panning content slides under the Y-axis
104
+ * column and dissolves instead of hard-clipping at the pane edge. **On
105
+ * by default** as a 60px ramp that finishes just inside the axis
106
+ * column, before the label glyphs; `{ right }` overrides the total
107
+ * ramp width in CSS px (`0` disables). `{ left }` adds the mirror zone
108
+ * at the left pane edge (default off).
109
+ *
110
+ * **Top (under the header)** — opt-in. `true` enables the auto zone
111
+ * (measured header + 24px run-out, half the header fold-in released so
112
+ * data rides up into the zone at rest), `{ top }` sets the zone height
113
+ * explicitly, and `{ overlap }` tunes how many px of the fold-in to
114
+ * release (0 keeps the strict fold-in). Off unless one of those is set.
115
+ *
116
+ * `false` — every mask off, strict edges everywhere.
117
+ */
118
+ fade?: boolean | { top?: number; overlap?: number; right?: number; left?: number };
119
+ /**
120
+ * How `<Title>` and `<InfoBar>` are positioned relative to the canvas. Live.
72
121
  * - `'overlay'` (default): absolute overlays on top of the canvas.
73
122
  * - `'inline'`: flex siblings above the canvas — the canvas (and grid) shift down.
74
123
  */
@@ -79,10 +128,10 @@ const props = withDefaults(
79
128
  * `<CandlestickSeries>` / `<BarSeries>` override these chart-level
80
129
  * defaults unless the category here is explicitly `false`.
81
130
  *
82
- * **Init-only by reference identity.** A new `animations` reference
83
- * recreates the underlying `ChartInstance`. Hoist the object outside
84
- * the render scope — an inline literal tears the chart down on every
85
- * re-render.
131
+ * **Init-only, but diffed by value, not reference.** A same-value
132
+ * inline object literal is a no-op — only a genuine change to the
133
+ * resolved config recreates the underlying `ChartInstance`, since the
134
+ * animation engine doesn't support live reconfiguration yet.
86
135
  */
87
136
  animations?: boolean | AnimationsConfig;
88
137
  /**
@@ -97,11 +146,58 @@ const props = withDefaults(
97
146
  * the prop identity later is ignored.
98
147
  */
99
148
  onEdgeReached?: (info: EdgeReachedInfo) => void;
149
+ /**
150
+ * Fired on a click (or tap) on the chart canvas that isn't the tail end
151
+ * of a pan drag. `info.spatialHit` resolves a pie/heatmap/custom-spatial
152
+ * series directly under the pointer; for a time-series series, resolve
153
+ * the point yourself from `info.time` (`chart.getDataAtTime` /
154
+ * `buildHoverSnapshots`). Live — the latest callback is always used.
155
+ */
156
+ onPointClick?: (info: PointClickInfo) => void;
157
+ /**
158
+ * Fired on a double-click on the chart canvas. The chart also responds
159
+ * by calling `fitContent()`. Live — the latest callback is always used.
160
+ */
161
+ onPointDoubleClick?: (info: PointClickInfo) => void;
162
+ /**
163
+ * Fired when the spatially-hovered series/index changes (pie, heatmap,
164
+ * or a custom spatial kind) — `null` when the pointer leaves every hit
165
+ * area. Live — the latest callback is always used.
166
+ */
167
+ onSeriesHover?: (hit: SeriesHoverInfo | null) => void;
168
+ /**
169
+ * Fired once the underlying `ChartInstance` is constructed — on mount,
170
+ * and again if the chart is ever rebuilt (a genuine `animations` value
171
+ * change). Live — the latest callback is always used for the next rebuild.
172
+ */
173
+ onReady?: (chart: ChartInstance) => void;
174
+ /**
175
+ * Fired whenever the committed visible range changes — pan, zoom,
176
+ * `fitContent()`, a data update that shifts the tail-scroll window, or a
177
+ * `visibleRange` prop change. Live — the latest callback is always used.
178
+ */
179
+ onVisibleRangeChange?: (range: VisibleRange) => void;
180
+ /**
181
+ * Fired on every crosshair move, with `null` when the pointer leaves the
182
+ * chart. Live — the latest callback is always used.
183
+ */
184
+ onCrosshairMove?: (position: CrosshairPosition | null) => void;
100
185
  }>(),
101
186
  {
102
187
  theme: () => catppuccin.theme,
103
188
  gradient: true,
189
+ // Vue's type-based `defineProps` casts an absent Boolean-typed prop to
190
+ // `false` (not `undefined`) — without this explicit default, every chart
191
+ // that didn't pass `:interactive="true"` silently got zero pan/zoom/
192
+ // crosshair/click interactivity. `gradient` dodges the same trap only
193
+ // because it already had a default.
194
+ interactive: true,
104
195
  headerLayout: 'overlay',
196
+ // Same Boolean-cast trap, opposite goal: an absent `fade` must stay
197
+ // `undefined` (keep the core's default X mask armed) and remain
198
+ // distinguishable from an explicit `:fade="false"` all-off opt-out.
199
+ // Declaring a default (even `undefined`) suppresses the cast.
200
+ fade: undefined,
105
201
  },
106
202
  );
107
203
 
@@ -133,9 +229,49 @@ provide(NavigatorAnchorKey, navigatorAnchor);
133
229
  let resizeObserver: ResizeObserver | null = null;
134
230
  const topOverlayHeight = ref(0);
135
231
 
232
+ /** Run-out below the measured header (CSS px) for the auto `fade` zone —
233
+ * content starts dissolving this far before it slides under the header. */
234
+ const FADE_AUTO_BAND = 24;
235
+
236
+ // Fade resolution — shared by the header fold-in below and `applyFade`.
237
+ // Vue casts an absent Boolean-typed prop to `false`, which conveniently is
238
+ // also the "off" value here. `overlap` deliberately applies only while the
239
+ // fade is on: without the mask, letting data ride under the header would
240
+ // just recreate the collision the strict fold-in exists to prevent.
241
+ // The top mask is opt-in: `true`, `{ top }`, or `{ overlap }` arms it (a
242
+ // pure X config like `{ right: 0 }` does not).
243
+ const fadeEnabled = computed(() => {
244
+ if (props.fade === false) return false;
245
+
246
+ return typeof props.fade === 'object'
247
+ ? props.fade.top !== undefined || props.fade.overlap !== undefined
248
+ : props.fade === true;
249
+ });
250
+ // Auto overlap: half the measured header — with the full fold-in intact the
251
+ // zone would cover only padding and enabling the fade would change nothing
252
+ // visible at rest. Explicit `overlap` (including 0) wins.
253
+ const fadeOverlap = computed(() => {
254
+ if (!fadeEnabled.value) return 0;
255
+
256
+ const explicit = typeof props.fade === 'object' ? props.fade.overlap : undefined;
257
+ if (explicit !== undefined) return Math.max(0, explicit);
258
+
259
+ return topOverlayHeight.value / 2;
260
+ });
261
+ const resolvedFadeTop = computed(() => {
262
+ if (!fadeEnabled.value) return 0;
263
+ if (typeof props.fade === 'object' && props.fade.top !== undefined) return props.fade.top;
264
+
265
+ const measured = props.headerLayout === 'overlay' ? topOverlayHeight.value : 0;
266
+
267
+ return measured > 0 ? measured + FADE_AUTO_BAND : FADE_AUTO_BAND;
268
+ });
269
+
136
270
  // Inline mode: browser flex already reserves header height, so folding it
137
271
  // into padding.top would double-shift the data. Only overlay needs the fold.
138
- const headerExtra = computed(() => (props.headerLayout === 'overlay' ? topOverlayHeight.value : 0));
272
+ const headerExtra = computed(() =>
273
+ props.headerLayout === 'overlay' ? Math.max(0, topOverlayHeight.value - fadeOverlap.value) : 0,
274
+ );
139
275
 
140
276
  function applyPadding() {
141
277
  if (!chart.value) return;
@@ -147,6 +283,43 @@ function applyPadding() {
147
283
  chart.value.setPadding(merged);
148
284
  }
149
285
 
286
+ /** Seed the constructor's fade config so the very first frame is already
287
+ * correct — `applyFade` lands the header-measured top zone right after, but
288
+ * an opted-out chart must not flash the default X mask for one paint. */
289
+ function seedFadeOption(options: ChartOptions): void {
290
+ if (props.fade === false) {
291
+ options.fade = { top: 0, right: 0, left: 0 };
292
+
293
+ return;
294
+ }
295
+ if (typeof props.fade !== 'object') return;
296
+
297
+ const seed: FadeConfig = {};
298
+ if (props.fade.top !== undefined) seed.top = props.fade.top;
299
+ if (props.fade.right !== undefined) seed.right = props.fade.right;
300
+ if (props.fade.left !== undefined) seed.left = props.fade.left;
301
+ options.fade = seed;
302
+ }
303
+
304
+ function applyFade() {
305
+ if (!chart.value) return;
306
+
307
+ if (props.fade === false) {
308
+ chart.value.setFade({ top: 0, right: 0, left: 0 });
309
+
310
+ return;
311
+ }
312
+
313
+ // Omitting `right` keeps the core's under-the-axis default armed; only an
314
+ // explicit value (or the all-off branch above) overrides it.
315
+ const next: FadeConfig = { top: resolvedFadeTop.value };
316
+ if (typeof props.fade === 'object') {
317
+ if (props.fade.right !== undefined) next.right = props.fade.right;
318
+ if (props.fade.left !== undefined) next.left = props.fade.left;
319
+ }
320
+ chart.value.setFade(next);
321
+ }
322
+
150
323
  // Wires up (or tears down) the header-height observer for the current
151
324
  // `headerLayout`. Called on mount AND whenever `headerLayout` flips at
152
325
  // runtime — overlay needs a live measurement, inline lets browser flex
@@ -174,6 +347,21 @@ function syncHeaderObserver() {
174
347
  resizeObserver.observe(headerEl);
175
348
  }
176
349
 
350
+ // `props` is a reactive proxy — reading `props.onX` inside these listeners
351
+ // always sees the caller's latest callback, no ref-latch needed the way
352
+ // React does. Shared between the mount path and the animations-triggered
353
+ // rebuild below so the two don't drift.
354
+ function subscribeDeclarativeEvents(instance: ChartInstance): void {
355
+ instance.on('pointClick', (info) => props.onPointClick?.(info));
356
+ instance.on('pointDoubleClick', (info) => props.onPointDoubleClick?.(info));
357
+ instance.on('seriesHover', (hit) => props.onSeriesHover?.(hit));
358
+ instance.on('crosshairMove', (position) => props.onCrosshairMove?.(position));
359
+ const emitVisibleRangeChange = () => props.onVisibleRangeChange?.(instance.getVisibleRange());
360
+ instance.on('viewportChange', emitVisibleRangeChange);
361
+ instance.on('dataUpdate', emitVisibleRangeChange);
362
+ instance.on('seriesChange', emitVisibleRangeChange);
363
+ }
364
+
177
365
  onMounted(async () => {
178
366
  if (!containerRef.value) return;
179
367
 
@@ -187,7 +375,10 @@ onMounted(async () => {
187
375
  if (perfAtMount !== undefined) options.perf = perfAtMount;
188
376
  if (onEdgeReachedAtMount) options.onEdgeReached = onEdgeReachedAtMount;
189
377
  if (props.animations !== undefined) options.animations = props.animations;
378
+ seedFadeOption(options);
190
379
  chart.value = new ChartInstance(containerRef.value, options);
380
+ subscribeDeclarativeEvents(chart.value);
381
+ props.onReady?.(chart.value);
191
382
 
192
383
  await nextTick();
193
384
  syncHeaderObserver();
@@ -236,9 +427,11 @@ watch(
236
427
  },
237
428
  );
238
429
 
239
- // Init-only: post-mount `animations` reference change tears down the
240
- // instance and rebuilds with the new config. Reference equality matters
241
- // — passing an inline literal recreates on every render.
430
+ // Init-only: a post-mount `animations` VALUE change tears down the instance
431
+ // and rebuilds with the new config. `deepEqual` (not reference equality)
432
+ // gates the rebuild — a caller's brand-new inline literal with the same
433
+ // values is a no-op, since Vue's `watch` fires this callback on every
434
+ // reference change regardless of content.
242
435
  //
243
436
  // Children get the chart via `useChartInstance()` which snapshots
244
437
  // `chartRef.value` once at setup time. To force them to re-setup against
@@ -247,9 +440,12 @@ watch(
247
440
  // Vue commits the unmount before the new ChartInstance is constructed.
248
441
  // When `chart.value` becomes the new instance, the slot re-mounts and
249
442
  // every child's `setup()` runs again against the fresh chart.
443
+ let lastAnimations = props.animations;
250
444
  watch(
251
445
  () => props.animations,
252
446
  async (next) => {
447
+ if (deepEqual(next, lastAnimations)) return;
448
+ lastAnimations = next;
253
449
  if (!chart.value || !containerRef.value) return;
254
450
  chart.value.destroy();
255
451
  chart.value = null;
@@ -264,10 +460,45 @@ watch(
264
460
  if (perfAtMount !== undefined) opts.perf = perfAtMount;
265
461
  if (onEdgeReachedAtMount) opts.onEdgeReached = onEdgeReachedAtMount;
266
462
  if (next !== undefined) opts.animations = next;
463
+ seedFadeOption(opts);
267
464
  chart.value = new ChartInstance(containerRef.value, opts);
465
+ subscribeDeclarativeEvents(chart.value);
466
+ props.onReady?.(chart.value);
268
467
  },
269
468
  );
270
469
 
470
+ // Controlled visible range: only re-apply when the resolved value actually
471
+ // differs from what we last applied ourselves, so feeding the range from
472
+ // `onVisibleRangeChange` back into this prop (the standard two-way-binding
473
+ // shape) doesn't re-trigger `setVisibleRange` with an unchanged spec on
474
+ // every render. The latch remembers which instance it applied to — a chart
475
+ // rebuild (an `animations` change) must receive the range again even though
476
+ // the spec's value hasn't moved.
477
+ let lastAppliedVisibleRange: { instance: ChartInstance; spec: VisibleRangeSpec } | undefined;
478
+ watch(
479
+ () => [chart.value, props.visibleRange] as const,
480
+ async ([instance, next]) => {
481
+ if (!instance || next === undefined) return;
482
+ if (
483
+ lastAppliedVisibleRange !== undefined &&
484
+ lastAppliedVisibleRange.instance === instance &&
485
+ deepEqual(lastAppliedVisibleRange.spec, next)
486
+ ) {
487
+ return;
488
+ }
489
+
490
+ lastAppliedVisibleRange = { instance, spec: next };
491
+ // On the chart's first tick, a series child (e.g. `<CandlestickSeries>`)
492
+ // hasn't mounted and seeded its data yet, so the engine is still on the
493
+ // placeholder `dataInterval` (60s) — a `{from, to}` spec sized for the
494
+ // real interval would fail the 2-bar-minimum guard. Awaiting a tick lets
495
+ // the child's data-seed (and its `detectInterval` call) land first.
496
+ await nextTick();
497
+ instance.setVisibleRange(next);
498
+ },
499
+ { immediate: true },
500
+ );
501
+
271
502
  // Re-apply padding on any input that affects it — including `headerExtra`,
272
503
  // so a runtime `headerLayout` toggle re-runs even when `padding` is undefined
273
504
  // (the default top=20 still needs the fold-in/out).
@@ -282,6 +513,12 @@ watch(
282
513
  applyPadding,
283
514
  );
284
515
 
516
+ // Fade rides the chart identity too, so a rebuilt instance (an `animations`
517
+ // change) receives the current zones again; `setFade` no-ops on unchanged
518
+ // values, so the extra fires are free. `immediate` covers the mount path —
519
+ // the first run lands right after `chart.value` is set.
520
+ watch(() => [chart.value, resolvedFadeTop.value, props.fade] as const, applyFade, { immediate: true });
521
+
285
522
  const rootStyle = computed(() => {
286
523
  const t = themeRef.value;
287
524
  const [gtop, gbot] = t?.chartGradient ?? ['transparent', 'transparent'];
@@ -289,7 +526,7 @@ const rootStyle = computed(() => {
289
526
  const background = props.gradient ? `linear-gradient(to bottom, ${gtop} 0%, ${bg} 70%, ${gbot} 100%)` : bg;
290
527
 
291
528
  return (
292
- 'position: relative; display: flex; flex-direction: column; width: 100%; height: 100%; overflow: hidden; ' +
529
+ 'position: relative; display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 240px; overflow: hidden; ' +
293
530
  `background: ${background}`
294
531
  );
295
532
  });
@@ -0,0 +1,69 @@
1
+ <script setup lang="ts">
2
+ import type { HeatmapCellData, HeatmapSeriesOptions } from '@wick-charts/core';
3
+ import { HeatmapSeriesDef } from '@wick-charts/core';
4
+ import { onMounted, onUnmounted, ref, watch } from 'vue';
5
+
6
+ import { useChartInstance } from './context';
7
+
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ data: HeatmapCellData[];
11
+ options?: Partial<HeatmapSeriesOptions>;
12
+ /** Stable series ID — same value across remounts. */
13
+ id?: string;
14
+ /** Show/hide the series without unmounting it — excludes it from the Y-range fit and tooltip/legend. Default `true`. Live. */
15
+ visible?: boolean;
16
+ }>(),
17
+ // Vue's type-based `defineProps` casts an absent Boolean-typed prop to
18
+ // `false` (not `undefined`) without an explicit default — see the
19
+ // `interactive` fix in ChartContainer.vue for the full explanation.
20
+ { visible: true },
21
+ );
22
+
23
+ const chart = useChartInstance();
24
+ const seriesId = ref<string | null>(null);
25
+
26
+ onMounted(() => {
27
+ const id = chart.addSeries(HeatmapSeriesDef, { ...props.options, id: props.id });
28
+ seriesId.value = id;
29
+ // Lazy watcher — apply initial data here so static-data mounts render without a no-op first frame.
30
+ if (props.data.length > 0) {
31
+ chart.setSeriesData(id, props.data);
32
+ }
33
+ chart.setSeriesVisible(id, props.visible);
34
+ });
35
+
36
+ onUnmounted(() => {
37
+ if (seriesId.value) chart.removeSeries(seriesId.value);
38
+ });
39
+
40
+ watch(
41
+ () => props.data,
42
+ (data) => {
43
+ // No length guard: an empty array is a legitimate update (filter yields
44
+ // no rows) and must clear the previous cells, matching the React wrapper.
45
+ if (seriesId.value) {
46
+ chart.setSeriesData(seriesId.value, data);
47
+ }
48
+ },
49
+ );
50
+
51
+ watch(
52
+ () => props.options,
53
+ (options) => {
54
+ if (seriesId.value && options) {
55
+ chart.updateSeriesOptions(seriesId.value, options);
56
+ }
57
+ },
58
+ { deep: true },
59
+ );
60
+
61
+ watch(
62
+ () => props.visible,
63
+ (visible) => {
64
+ if (seriesId.value) chart.setSeriesVisible(seriesId.value, visible);
65
+ },
66
+ );
67
+ </script>
68
+
69
+ <template><span v-if="false" /></template>
@@ -5,13 +5,21 @@ import { onMounted, onUnmounted, ref, watch } from 'vue';
5
5
 
6
6
  import { useChartInstance } from './context';
7
7
 
8
- const props = defineProps<{
9
- /** Flat `TimePoint[]`, `TimePoint[][]`, or named `{ label, color?, data }` layers. Time accepts ms or `Date`. */
10
- data: MultiLayerData;
11
- options?: Partial<LineSeriesOptions>;
12
- /** Stable series ID — same value across remounts. */
13
- id?: string;
14
- }>();
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ /** Flat `TimePoint[]`, `TimePoint[][]`, or named `{ label, color?, data }` layers. Time accepts ms or `Date`. */
11
+ data: MultiLayerData;
12
+ options?: Partial<LineSeriesOptions>;
13
+ /** Stable series ID — same value across remounts. */
14
+ id?: string;
15
+ /** Show/hide the series without unmounting it — excludes it from the Y-range fit and tooltip/legend. Default `true`. Live. */
16
+ visible?: boolean;
17
+ }>(),
18
+ // Vue's type-based `defineProps` casts an absent Boolean-typed prop to
19
+ // `false` (not `undefined`) without an explicit default — see the
20
+ // `interactive` fix in ChartContainer.vue for the full explanation.
21
+ { visible: true },
22
+ );
15
23
 
16
24
  const chart = useChartInstance();
17
25
  const seriesId = ref<string | null>(null);
@@ -35,6 +43,7 @@ onMounted(() => {
35
43
  prevSync = new Array(layerCount).fill(EMPTY_SYNC_STATE);
36
44
  // Lazy watcher — apply initial data here so static-data mounts render without a no-op first frame.
37
45
  applyData(id, props.data);
46
+ chart.setSeriesVisible(id, props.visible);
38
47
  });
39
48
 
40
49
  onUnmounted(() => {
@@ -60,6 +69,13 @@ watch(
60
69
  },
61
70
  { deep: true },
62
71
  );
72
+
73
+ watch(
74
+ () => props.visible,
75
+ (visible) => {
76
+ if (seriesId.value) chart.setSeriesVisible(seriesId.value, visible);
77
+ },
78
+ );
63
79
  </script>
64
80
 
65
81
  <template><span v-if="false" /></template>
package/src/PieSeries.vue CHANGED
@@ -5,12 +5,20 @@ import { onMounted, onUnmounted, ref, watch } from 'vue';
5
5
 
6
6
  import { useChartInstance } from './context';
7
7
 
8
- const props = defineProps<{
9
- data: PieSliceData[];
10
- options?: Partial<PieSeriesOptions>;
11
- /** Stable series ID — same value across remounts. */
12
- id?: string;
13
- }>();
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ data: PieSliceData[];
11
+ options?: Partial<PieSeriesOptions>;
12
+ /** Stable series ID — same value across remounts. */
13
+ id?: string;
14
+ /** Show/hide the series without unmounting it — excludes it from the Y-range fit and tooltip/legend. Default `true`. Live. */
15
+ visible?: boolean;
16
+ }>(),
17
+ // Vue's type-based `defineProps` casts an absent Boolean-typed prop to
18
+ // `false` (not `undefined`) without an explicit default — see the
19
+ // `interactive` fix in ChartContainer.vue for the full explanation.
20
+ { visible: true },
21
+ );
14
22
 
15
23
  const chart = useChartInstance();
16
24
  const seriesId = ref<string | null>(null);
@@ -22,6 +30,7 @@ onMounted(() => {
22
30
  if (props.data.length > 0) {
23
31
  chart.setSeriesData(id, props.data);
24
32
  }
33
+ chart.setSeriesVisible(id, props.visible);
25
34
  });
26
35
 
27
36
  onUnmounted(() => {
@@ -46,6 +55,13 @@ watch(
46
55
  },
47
56
  { deep: true },
48
57
  );
58
+
59
+ watch(
60
+ () => props.visible,
61
+ (visible) => {
62
+ if (seriesId.value) chart.setSeriesVisible(seriesId.value, visible);
63
+ },
64
+ );
49
65
  </script>
50
66
 
51
67
  <template><span v-if="false" /></template>