@wick-charts/vue 0.5.0 → 0.6.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.
Files changed (80) hide show
  1. package/README.md +18 -3
  2. package/dist/BarSeries.vue.js +26 -20
  3. package/dist/CandlestickSeries.vue.js +30 -24
  4. package/dist/ChartContainer.vue.js +88 -65
  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/y-target.js +13 -12
  18. package/dist/core/dist/chart.js +908 -626
  19. package/dist/core/dist/components/annotation-chip.js +28 -0
  20. package/dist/core/dist/components/edge-indicator.js +58 -26
  21. package/dist/core/dist/components/loading-indicator.js +46 -0
  22. package/dist/core/dist/components/marker.js +49 -31
  23. package/dist/core/dist/components/reference-line.js +27 -36
  24. package/dist/core/dist/components/time-region.js +30 -28
  25. package/dist/core/dist/data/store.js +38 -19
  26. package/dist/core/dist/data/sync.js +60 -38
  27. package/dist/core/dist/heatmap-tooltip.js +16 -0
  28. package/dist/core/dist/interactions/handler.js +67 -43
  29. package/dist/core/dist/perf/perf-hud.js +2 -2
  30. package/dist/core/dist/scales/x-scale.js +85 -37
  31. package/dist/core/dist/scales/y-scale.js +38 -27
  32. package/dist/core/dist/series/bar-intro.js +18 -0
  33. package/dist/core/dist/series/bar.js +337 -205
  34. package/dist/core/dist/series/base-multi-layer.js +224 -96
  35. package/dist/core/dist/series/candlestick-intro.js +57 -0
  36. package/dist/core/dist/series/candlestick.js +454 -276
  37. package/dist/core/dist/series/heatmap.js +387 -0
  38. package/dist/core/dist/series/line-intro.js +85 -0
  39. package/dist/core/dist/series/line.js +590 -282
  40. package/dist/core/dist/series/painters/builtins.js +2 -2
  41. package/dist/core/dist/series/pie.js +396 -259
  42. package/dist/core/dist/series/register-builtins.js +8 -7
  43. package/dist/core/dist/series/types.js +6 -0
  44. package/dist/core/dist/series/wave-intro.js +26 -0
  45. package/dist/core/dist/theme/create.js +69 -63
  46. package/dist/core/dist/theme/themes/mint-breeze.js +2 -2
  47. package/dist/core/dist/theme/themes/sand-dune.js +2 -2
  48. package/dist/core/dist/utils/color.js +109 -34
  49. package/dist/core/dist/utils/deep-equal.js +18 -0
  50. package/dist/core/dist/utils/time.js +25 -19
  51. package/dist/index.d.ts +1411 -122
  52. package/dist/index.js +177 -130
  53. package/dist/ui/Crosshair.vue.js +20 -21
  54. package/dist/ui/HeatmapTooltip.vue.js +96 -0
  55. package/dist/ui/HeatmapTooltip.vue2.js +4 -0
  56. package/dist/ui/InfoBar.vue.js +46 -47
  57. package/dist/ui/Legend.vue.js +80 -79
  58. package/dist/ui/PieLegend.vue.js +5 -5
  59. package/dist/ui/PieTooltip.vue.js +77 -67
  60. package/dist/ui/Tooltip.vue.js +60 -60
  61. package/dist/ui/YAxis.vue.js +2 -2
  62. package/dist/ui/YLabel.vue.js +94 -61
  63. package/package.json +2 -2
  64. package/src/BarSeries.vue +23 -7
  65. package/src/CandlestickSeries.vue +23 -7
  66. package/src/ChartContainer.vue +134 -12
  67. package/src/HeatmapSeries.vue +69 -0
  68. package/src/LineSeries.vue +23 -7
  69. package/src/PieSeries.vue +22 -6
  70. package/src/index.ts +84 -3
  71. package/src/ui/Crosshair.vue +1 -2
  72. package/src/ui/HeatmapTooltip.vue +144 -0
  73. package/src/ui/InfoBar.vue +1 -2
  74. package/src/ui/Legend.vue +13 -0
  75. package/src/ui/Marker.vue +1 -1
  76. package/src/ui/PieTooltip.vue +10 -0
  77. package/src/ui/ReferenceLine.vue +2 -2
  78. package/src/ui/TimeRegion.vue +2 -2
  79. package/src/ui/Tooltip.vue +2 -2
  80. package/src/ui/YLabel.vue +123 -8
@@ -1,105 +1,138 @@
1
- import { defineComponent as B, useSlots as F, computed as n, ref as _, onMounted as T, onUnmounted as z, openBlock as h, createElementBlock as b, Fragment as x, renderSlot as Y, createElementVNode as C, normalizeStyle as L, unref as E, createVNode as N, createCommentVNode as V } from "vue";
2
- import { useChartInstance as D } from "../context.js";
3
- import O from "./NumberFlow.vue.js";
4
- const A = /* @__PURE__ */ B({
1
+ import { defineComponent as q, useSlots as j, computed as n, ref as I, onMounted as G, onUnmounted as P, openBlock as w, createElementBlock as B, Fragment as A, renderSlot as W, createElementVNode as _, normalizeStyle as T, unref as H, createVNode as J, createCommentVNode as K } from "vue";
2
+ import { resolveAnimate as Q, prefersReducedMotion as X, firstVisibleValue as Z, BadgeAnimator as ee } from "../core/dist/chart/badge-animator.js";
3
+ import { useChartInstance as te } from "../context.js";
4
+ import ne from "./NumberFlow.vue.js";
5
+ const re = /* @__PURE__ */ q({
5
6
  __name: "YLabel",
6
7
  props: {
7
8
  seriesId: {},
8
9
  color: {},
9
- format: { type: Function }
10
+ format: { type: Function },
11
+ animate: { type: [Boolean, Object] }
10
12
  },
11
- setup(S) {
12
- const u = S, e = D(), k = F(), I = n(() => typeof k.default == "function"), r = _(0), i = () => {
13
- r.value++;
13
+ setup(z) {
14
+ const f = z, e = te(), Y = j(), E = n(() => typeof Y.default == "function"), i = I(0), u = I(null);
15
+ let m = null, g = null, o = null;
16
+ const M = n(() => Q(f.animate));
17
+ function S() {
18
+ o = null;
19
+ const t = s.value, l = t !== null ? e.getStackedLastValue(t) : null;
20
+ if (t === null || !l) {
21
+ m = null, g = null;
22
+ return;
23
+ }
24
+ const v = l.value;
25
+ (m === null || g !== t) && (m = new ee(v), g = t);
26
+ const { glide: d, countUp: U, settleMs: $ } = M.value, F = e.getIntroFront(t), c = m.frame({
27
+ target: v,
28
+ // Only needed for the intro count-up; skip the visible-range binary search
29
+ // entirely once the reveal has settled (the hot streaming path).
30
+ firstVisible: F < 1 ? Z(e, t, v) : v,
31
+ introFront: F,
32
+ now: performance.now(),
33
+ glide: d,
34
+ countUp: U,
35
+ settleMs: $,
36
+ reducedMotion: X()
37
+ }), y = u.value;
38
+ (y === null || y.positionValue !== c.positionValue || y.textValue !== c.textValue || y.opacity !== c.opacity) && (u.value = { positionValue: c.positionValue, textValue: c.textValue, opacity: c.opacity }), c.animating && o === null && (o = requestAnimationFrame(S));
39
+ }
40
+ function N() {
41
+ o === null && (o = requestAnimationFrame(S));
42
+ }
43
+ const p = () => {
44
+ i.value++, N();
14
45
  };
15
- T(() => {
16
- e.setYLabel(!0), e.on("overlayChange", i), e.on("viewportChange", i), e.getSeriesIds().length > 0 && r.value++;
17
- }), z(() => {
18
- e.setYLabel(!1), e.off("overlayChange", i), e.off("viewportChange", i);
46
+ G(() => {
47
+ e.setYLabel(!0), e.on("overlayChange", p), e.on("viewportChange", p), e.getSeriesIds().length > 0 && p();
48
+ }), P(() => {
49
+ e.setYLabel(!1), e.off("overlayChange", p), e.off("viewportChange", p), o !== null && cancelAnimationFrame(o), o = null;
19
50
  });
20
- function w(a, l) {
51
+ function O(t, l) {
21
52
  if (l !== void 0) return l;
22
- const g = a.getSeriesIdsByType("time", { visibleOnly: !0, singleLayerOnly: !0 });
23
- if (g.length > 0) return g[0];
24
- const y = a.getSeriesIdsByType("time", { visibleOnly: !0 });
25
- return y.length > 0 ? y[0] : null;
53
+ const v = t.getSeriesIdsByType("time", { visibleOnly: !0, singleLayerOnly: !0 });
54
+ if (v.length > 0) return v[0];
55
+ const d = t.getSeriesIdsByType("time", { visibleOnly: !0 });
56
+ return d.length > 0 ? d[0] : null;
26
57
  }
27
- const s = n(() => (r.value, w(e, u.seriesId))), t = n(() => (r.value, s.value !== null ? e.getStackedLastValue(s.value) : null)), v = n(() => (r.value, s.value !== null ? e.getPreviousClose(s.value) : null)), o = n(() => (r.value, e.getTheme())), c = n(() => (r.value, t.value === null ? 0 : e.yScale.valueToY(t.value.value))), f = n(() => t.value === null || v.value === null ? "neutral" : t.value.value > v.value ? "up" : t.value.value < v.value ? "down" : "neutral"), d = n(() => {
28
- if (t.value === null) return o.value.yLabel.neutralBackground;
29
- if (!t.value.isLive) return o.value.axis.textColor;
30
- if (u.color) return u.color;
31
- switch (f.value) {
58
+ const s = n(() => (i.value, O(e, f.seriesId))), a = n(() => (i.value, s.value !== null ? e.getStackedLastValue(s.value) : null)), b = n(() => (i.value, s.value !== null ? e.getPreviousClose(s.value) : null)), r = n(() => (i.value, e.getTheme())), R = n(() => u.value !== null ? u.value.positionValue : a.value?.value ?? 0), D = n(() => u.value !== null ? u.value.textValue : a.value?.value ?? 0), h = n(() => u.value !== null ? u.value.opacity : s.value === null ? 1 : e.getIntroFront(s.value) < 1 ? 0 : 1), x = n(() => (i.value, a.value === null ? 0 : e.yScale.valueToY(R.value))), L = n(() => a.value === null || b.value === null ? "neutral" : a.value.value > b.value ? "up" : a.value.value < b.value ? "down" : "neutral"), V = n(() => {
59
+ if (a.value === null) return r.value.yLabel.neutralBackground;
60
+ if (!a.value.isLive) return r.value.axis.textColor;
61
+ if (f.color) return f.color;
62
+ switch (L.value) {
32
63
  case "up":
33
- return o.value.yLabel.upBackground;
64
+ return r.value.yLabel.upBackground;
34
65
  case "down":
35
- return o.value.yLabel.downBackground;
66
+ return r.value.yLabel.downBackground;
36
67
  default:
37
- return o.value.yLabel.neutralBackground;
68
+ return r.value.yLabel.neutralBackground;
38
69
  }
39
- }), m = n(() => {
40
- r.value;
41
- const a = e.yScale.getRange(), l = a.max - a.min;
70
+ }), k = n(() => {
71
+ i.value;
72
+ const t = e.yScale.getRange(), l = t.max - t.min;
42
73
  return l < 0.1 ? 6 : l < 10 ? 4 : l < 1e3 ? 2 : 0;
43
- }), p = n(() => {
44
- if (u.format) return u.format;
45
- const a = new Intl.NumberFormat("en-US", {
46
- minimumFractionDigits: m.value,
47
- maximumFractionDigits: m.value,
74
+ }), C = n(() => {
75
+ if (f.format) return f.format;
76
+ const t = new Intl.NumberFormat("en-US", {
77
+ minimumFractionDigits: k.value,
78
+ maximumFractionDigits: k.value,
48
79
  useGrouping: !1
49
80
  });
50
- return (l) => a.format(l);
81
+ return (l) => t.format(l);
51
82
  });
52
- return (a, l) => t.value !== null ? (h(), b(x, { key: 0 }, [
53
- I.value ? Y(a.$slots, "default", {
83
+ return (t, l) => a.value !== null ? (w(), B(A, { key: 0 }, [
84
+ E.value ? W(t.$slots, "default", {
54
85
  key: 0,
55
- value: t.value.value,
56
- y: c.value,
57
- bgColor: d.value,
58
- isLive: t.value.isLive,
59
- direction: f.value,
60
- format: p.value
61
- }) : (h(), b(x, { key: 1 }, [
62
- C("div", {
63
- style: L({
86
+ value: a.value.value,
87
+ y: x.value,
88
+ bgColor: V.value,
89
+ isLive: a.value.isLive,
90
+ direction: L.value,
91
+ format: C.value,
92
+ opacity: h.value
93
+ }) : (w(), B(A, { key: 1 }, [
94
+ _("div", {
95
+ style: T({
64
96
  position: "absolute",
65
97
  left: "0",
66
- right: E(e).yAxisWidth + "px",
67
- top: c.value + "px",
98
+ right: H(e).yAxisWidth + "px",
99
+ top: x.value + "px",
68
100
  height: "0",
69
- borderTop: "1px dashed " + d.value,
70
- opacity: 0.5,
101
+ borderTop: "1px dashed " + V.value,
102
+ opacity: h.value * 0.5,
71
103
  pointerEvents: "none",
72
104
  zIndex: 2
73
105
  })
74
106
  }, null, 4),
75
- C("div", {
76
- style: L({
107
+ _("div", {
108
+ style: T({
77
109
  position: "absolute",
78
110
  right: "4px",
79
- top: c.value + "px",
111
+ top: x.value + "px",
80
112
  transform: "translateY(-50%)",
113
+ opacity: h.value,
81
114
  pointerEvents: "auto",
82
115
  zIndex: 3,
83
- background: d.value,
84
- color: o.value.yLabel.textColor,
85
- fontSize: o.value.yLabel.fontSize + "px",
86
- fontFamily: o.value.typography.fontFamily,
116
+ background: V.value,
117
+ color: r.value.yLabel.textColor,
118
+ fontSize: r.value.yLabel.fontSize + "px",
119
+ fontFamily: r.value.typography.fontFamily,
87
120
  padding: "3px 8px",
88
121
  borderRadius: "3px",
89
122
  whiteSpace: "nowrap",
90
123
  transition: "background-color 0.3s ease"
91
124
  })
92
125
  }, [
93
- N(O, {
94
- value: t.value.value,
95
- format: p.value,
126
+ J(ne, {
127
+ value: D.value,
128
+ format: C.value,
96
129
  "spin-duration": 350
97
130
  }, null, 8, ["value", "format"])
98
131
  ], 4)
99
132
  ], 64))
100
- ], 64)) : V("", !0);
133
+ ], 64)) : K("", !0);
101
134
  }
102
135
  });
103
136
  export {
104
- A as default
137
+ re as default
105
138
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wick-charts/vue",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "High-performance canvas timeseries charts for Vue — candlestick, line, bar, pie. Tree-shakeable, zero runtime deps.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -45,7 +45,7 @@
45
45
  "vue": ">=3.3.0"
46
46
  },
47
47
  "devDependencies": {
48
- "@wick-charts/core": "^0.5.0"
48
+ "@wick-charts/core": "^0.6.0"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "vite build"
package/src/BarSeries.vue CHANGED
@@ -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<BarSeriesOptions>;
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<BarSeriesOptions>;
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>
@@ -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 `OHLCInput[]`, or `{ label, data }` to name the stream. */
10
- data: CandlestickData;
11
- options?: Partial<CandlestickSeriesOptions>;
12
- /** Stable series ID — same value across remounts. */
13
- id?: string;
14
- }>();
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ /** Flat `OHLCInput[]`, or `{ label, data }` to name the stream. */
11
+ data: CandlestickData;
12
+ options?: Partial<CandlestickSeriesOptions>;
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);
@@ -37,6 +45,7 @@ onMounted(() => {
37
45
  // below only fires on subsequent `data` prop mutations. Explicitly apply
38
46
  // the first value here so components with static data render immediately.
39
47
  applyData(id, props.data);
48
+ chart.setSeriesVisible(id, props.visible);
40
49
  });
41
50
 
42
51
  onUnmounted(() => {
@@ -62,6 +71,13 @@ watch(
62
71
  },
63
72
  { deep: true },
64
73
  );
74
+
75
+ watch(
76
+ () => props.visible,
77
+ (visible) => {
78
+ if (seriesId.value) chart.setSeriesVisible(seriesId.value, visible);
79
+ },
80
+ );
65
81
  </script>
66
82
 
67
83
  <template><span v-if="false" /></template>
@@ -5,9 +5,14 @@ import {
5
5
  ChartInstance,
6
6
  type ChartOptions,
7
7
  type ChartTheme,
8
+ type CrosshairPosition,
8
9
  type EdgeReachedInfo,
10
+ type PointClickInfo,
11
+ type SeriesHoverInfo,
12
+ type VisibleRange,
9
13
  type VisibleRangeSpec,
10
14
  catppuccin,
15
+ deepEqual,
11
16
  } from '@wick-charts/core';
12
17
  import { computed, nextTick, onMounted, onUnmounted, provide, ref, shallowRef, watch } from 'vue';
13
18
 
@@ -25,7 +30,9 @@ type PerfOption = NonNullable<ChartOptions['perf']>;
25
30
 
26
31
  const props = withDefaults(
27
32
  defineProps<{
33
+ /** Visual theme. Live — changing this at runtime updates all themed elements. */
28
34
  theme?: ChartTheme;
35
+ /** Grouped axis configuration (Y/X visibility, bounds, sizing). Live. */
29
36
  axis?: AxisConfig;
30
37
  /**
31
38
  * Viewport padding. `top`/`bottom` are in pixels. `left`/`right` accept either pixels (`50`)
@@ -61,14 +68,31 @@ const props = withDefaults(
61
68
  */
62
69
  initialRange?: VisibleRangeSpec;
63
70
  };
64
- /** Show the chart background gradient. Defaults to true. */
71
+ /**
72
+ * Controlled visible range. Same shape as the imperative
73
+ * `chart.setVisibleRange` — a bar count, an explicit `{from, to}` window,
74
+ * or `{from, bars}`. Every reference/value change applies via
75
+ * `chart.setVisibleRange`, so pair it with `onVisibleRangeChange` for a
76
+ * two-way binding; a same-value literal (compared structurally) is a
77
+ * no-op, so an inline object doesn't re-apply on every render.
78
+ *
79
+ * For a one-shot initial window that isn't re-applied on every prop
80
+ * change, use `viewport.initialRange` instead.
81
+ */
82
+ visibleRange?: VisibleRangeSpec;
83
+ /** Show the chart background gradient. Live. Defaults to true. */
65
84
  gradient?: boolean;
66
- /** Enable zoom, pan, and crosshair interactions. Defaults to true. */
85
+ /**
86
+ * Enable zoom, pan, and crosshair interactions. Defaults to true.
87
+ *
88
+ * Mount-only — core has no `setInteractive`, so changing this prop after
89
+ * mount is ignored.
90
+ */
67
91
  interactive?: boolean;
68
- /** Background grid configuration. Default: `{ visible: true }`. */
92
+ /** Background grid configuration. Live. Default: `{ visible: true }`. */
69
93
  grid?: { visible: boolean };
70
94
  /**
71
- * How `<Title>` and `<InfoBar>` are positioned relative to the canvas.
95
+ * How `<Title>` and `<InfoBar>` are positioned relative to the canvas. Live.
72
96
  * - `'overlay'` (default): absolute overlays on top of the canvas.
73
97
  * - `'inline'`: flex siblings above the canvas — the canvas (and grid) shift down.
74
98
  */
@@ -79,10 +103,10 @@ const props = withDefaults(
79
103
  * `<CandlestickSeries>` / `<BarSeries>` override these chart-level
80
104
  * defaults unless the category here is explicitly `false`.
81
105
  *
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.
106
+ * **Init-only, but diffed by value, not reference.** A same-value
107
+ * inline object literal is a no-op — only a genuine change to the
108
+ * resolved config recreates the underlying `ChartInstance`, since the
109
+ * animation engine doesn't support live reconfiguration yet.
86
110
  */
87
111
  animations?: boolean | AnimationsConfig;
88
112
  /**
@@ -97,10 +121,52 @@ const props = withDefaults(
97
121
  * the prop identity later is ignored.
98
122
  */
99
123
  onEdgeReached?: (info: EdgeReachedInfo) => void;
124
+ /**
125
+ * Fired on a click (or tap) on the chart canvas that isn't the tail end
126
+ * of a pan drag. `info.spatialHit` resolves a pie/heatmap/custom-spatial
127
+ * series directly under the pointer; for a time-series series, resolve
128
+ * the point yourself from `info.time` (`chart.getDataAtTime` /
129
+ * `buildHoverSnapshots`). Live — the latest callback is always used.
130
+ */
131
+ onPointClick?: (info: PointClickInfo) => void;
132
+ /**
133
+ * Fired on a double-click on the chart canvas. The chart also responds
134
+ * by calling `fitContent()`. Live — the latest callback is always used.
135
+ */
136
+ onPointDoubleClick?: (info: PointClickInfo) => void;
137
+ /**
138
+ * Fired when the spatially-hovered series/index changes (pie, heatmap,
139
+ * or a custom spatial kind) — `null` when the pointer leaves every hit
140
+ * area. Live — the latest callback is always used.
141
+ */
142
+ onSeriesHover?: (hit: SeriesHoverInfo | null) => void;
143
+ /**
144
+ * Fired once the underlying `ChartInstance` is constructed — on mount,
145
+ * and again if the chart is ever rebuilt (a genuine `animations` value
146
+ * change). Live — the latest callback is always used for the next rebuild.
147
+ */
148
+ onReady?: (chart: ChartInstance) => void;
149
+ /**
150
+ * Fired whenever the committed visible range changes — pan, zoom,
151
+ * `fitContent()`, a data update that shifts the tail-scroll window, or a
152
+ * `visibleRange` prop change. Live — the latest callback is always used.
153
+ */
154
+ onVisibleRangeChange?: (range: VisibleRange) => void;
155
+ /**
156
+ * Fired on every crosshair move, with `null` when the pointer leaves the
157
+ * chart. Live — the latest callback is always used.
158
+ */
159
+ onCrosshairMove?: (position: CrosshairPosition | null) => void;
100
160
  }>(),
101
161
  {
102
162
  theme: () => catppuccin.theme,
103
163
  gradient: true,
164
+ // Vue's type-based `defineProps` casts an absent Boolean-typed prop to
165
+ // `false` (not `undefined`) — without this explicit default, every chart
166
+ // that didn't pass `:interactive="true"` silently got zero pan/zoom/
167
+ // crosshair/click interactivity. `gradient` dodges the same trap only
168
+ // because it already had a default.
169
+ interactive: true,
104
170
  headerLayout: 'overlay',
105
171
  },
106
172
  );
@@ -174,6 +240,21 @@ function syncHeaderObserver() {
174
240
  resizeObserver.observe(headerEl);
175
241
  }
176
242
 
243
+ // `props` is a reactive proxy — reading `props.onX` inside these listeners
244
+ // always sees the caller's latest callback, no ref-latch needed the way
245
+ // React does. Shared between the mount path and the animations-triggered
246
+ // rebuild below so the two don't drift.
247
+ function subscribeDeclarativeEvents(instance: ChartInstance): void {
248
+ instance.on('pointClick', (info) => props.onPointClick?.(info));
249
+ instance.on('pointDoubleClick', (info) => props.onPointDoubleClick?.(info));
250
+ instance.on('seriesHover', (hit) => props.onSeriesHover?.(hit));
251
+ instance.on('crosshairMove', (position) => props.onCrosshairMove?.(position));
252
+ const emitVisibleRangeChange = () => props.onVisibleRangeChange?.(instance.getVisibleRange());
253
+ instance.on('viewportChange', emitVisibleRangeChange);
254
+ instance.on('dataUpdate', emitVisibleRangeChange);
255
+ instance.on('seriesChange', emitVisibleRangeChange);
256
+ }
257
+
177
258
  onMounted(async () => {
178
259
  if (!containerRef.value) return;
179
260
 
@@ -188,6 +269,8 @@ onMounted(async () => {
188
269
  if (onEdgeReachedAtMount) options.onEdgeReached = onEdgeReachedAtMount;
189
270
  if (props.animations !== undefined) options.animations = props.animations;
190
271
  chart.value = new ChartInstance(containerRef.value, options);
272
+ subscribeDeclarativeEvents(chart.value);
273
+ props.onReady?.(chart.value);
191
274
 
192
275
  await nextTick();
193
276
  syncHeaderObserver();
@@ -236,9 +319,11 @@ watch(
236
319
  },
237
320
  );
238
321
 
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.
322
+ // Init-only: a post-mount `animations` VALUE change tears down the instance
323
+ // and rebuilds with the new config. `deepEqual` (not reference equality)
324
+ // gates the rebuild — a caller's brand-new inline literal with the same
325
+ // values is a no-op, since Vue's `watch` fires this callback on every
326
+ // reference change regardless of content.
242
327
  //
243
328
  // Children get the chart via `useChartInstance()` which snapshots
244
329
  // `chartRef.value` once at setup time. To force them to re-setup against
@@ -247,9 +332,12 @@ watch(
247
332
  // Vue commits the unmount before the new ChartInstance is constructed.
248
333
  // When `chart.value` becomes the new instance, the slot re-mounts and
249
334
  // every child's `setup()` runs again against the fresh chart.
335
+ let lastAnimations = props.animations;
250
336
  watch(
251
337
  () => props.animations,
252
338
  async (next) => {
339
+ if (deepEqual(next, lastAnimations)) return;
340
+ lastAnimations = next;
253
341
  if (!chart.value || !containerRef.value) return;
254
342
  chart.value.destroy();
255
343
  chart.value = null;
@@ -265,7 +353,41 @@ watch(
265
353
  if (onEdgeReachedAtMount) opts.onEdgeReached = onEdgeReachedAtMount;
266
354
  if (next !== undefined) opts.animations = next;
267
355
  chart.value = new ChartInstance(containerRef.value, opts);
356
+ subscribeDeclarativeEvents(chart.value);
357
+ props.onReady?.(chart.value);
358
+ },
359
+ );
360
+
361
+ // Controlled visible range: only re-apply when the resolved value actually
362
+ // differs from what we last applied ourselves, so feeding the range from
363
+ // `onVisibleRangeChange` back into this prop (the standard two-way-binding
364
+ // shape) doesn't re-trigger `setVisibleRange` with an unchanged spec on
365
+ // every render. The latch remembers which instance it applied to — a chart
366
+ // rebuild (an `animations` change) must receive the range again even though
367
+ // the spec's value hasn't moved.
368
+ let lastAppliedVisibleRange: { instance: ChartInstance; spec: VisibleRangeSpec } | undefined;
369
+ watch(
370
+ () => [chart.value, props.visibleRange] as const,
371
+ async ([instance, next]) => {
372
+ if (!instance || next === undefined) return;
373
+ if (
374
+ lastAppliedVisibleRange !== undefined &&
375
+ lastAppliedVisibleRange.instance === instance &&
376
+ deepEqual(lastAppliedVisibleRange.spec, next)
377
+ ) {
378
+ return;
379
+ }
380
+
381
+ lastAppliedVisibleRange = { instance, spec: next };
382
+ // On the chart's first tick, a series child (e.g. `<CandlestickSeries>`)
383
+ // hasn't mounted and seeded its data yet, so the engine is still on the
384
+ // placeholder `dataInterval` (60s) — a `{from, to}` spec sized for the
385
+ // real interval would fail the 2-bar-minimum guard. Awaiting a tick lets
386
+ // the child's data-seed (and its `detectInterval` call) land first.
387
+ await nextTick();
388
+ instance.setVisibleRange(next);
268
389
  },
390
+ { immediate: true },
269
391
  );
270
392
 
271
393
  // Re-apply padding on any input that affects it — including `headerExtra`,
@@ -289,7 +411,7 @@ const rootStyle = computed(() => {
289
411
  const background = props.gradient ? `linear-gradient(to bottom, ${gtop} 0%, ${bg} 70%, ${gbot} 100%)` : bg;
290
412
 
291
413
  return (
292
- 'position: relative; display: flex; flex-direction: column; width: 100%; height: 100%; overflow: hidden; ' +
414
+ 'position: relative; display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 240px; overflow: hidden; ' +
293
415
  `background: ${background}`
294
416
  );
295
417
  });
@@ -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>