@wick-charts/vue 0.6.3 → 0.7.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 (67) hide show
  1. package/README.md +6 -6
  2. package/dist/BandSeries.vue.js +52 -0
  3. package/dist/BandSeries.vue2.js +4 -0
  4. package/dist/BarSeries.vue.js +15 -13
  5. package/dist/CandlestickSeries.vue.js +31 -29
  6. package/dist/ChartContainer.vue.js +75 -68
  7. package/dist/LineSeries.vue.js +19 -17
  8. package/dist/composables.js +46 -36
  9. package/dist/core/dist/animation/config.js +92 -75
  10. package/dist/core/dist/animation/viewport-engine.js +155 -96
  11. package/dist/core/dist/axis/dom-labels.js +56 -51
  12. package/dist/core/dist/chart/badge-animator.js +30 -22
  13. package/dist/core/dist/chart/edge-indicators.js +22 -21
  14. package/dist/core/dist/chart/last-value.js +33 -26
  15. package/dist/core/dist/chart/scale-guards.js +23 -5
  16. package/dist/core/dist/chart/x-guards.js +37 -0
  17. package/dist/core/dist/chart.js +1286 -842
  18. package/dist/core/dist/data/decimation.js +58 -39
  19. package/dist/core/dist/data/sync.js +37 -31
  20. package/dist/core/dist/navigator/controller.js +41 -41
  21. package/dist/core/dist/perf/perf-hud.js +3 -3
  22. package/dist/core/dist/scales/tick-tracker.js +2 -2
  23. package/dist/core/dist/scales/x-scale.js +167 -53
  24. package/dist/core/dist/scales/y-scale.js +36 -34
  25. package/dist/core/dist/series/band.js +723 -0
  26. package/dist/core/dist/series/bar.js +297 -291
  27. package/dist/core/dist/series/base-multi-layer.js +283 -237
  28. package/dist/core/dist/series/candlestick.js +32 -31
  29. package/dist/core/dist/series/heatmap.js +5 -5
  30. package/dist/core/dist/series/line-intro.js +47 -37
  31. package/dist/core/dist/series/line.js +489 -448
  32. package/dist/core/dist/series/painters/builtins.js +4 -4
  33. package/dist/core/dist/series/painters/canvas-path.js +64 -52
  34. package/dist/core/dist/series/register-builtins.js +9 -8
  35. package/dist/core/dist/snapshots.js +59 -46
  36. package/dist/core/dist/utils/point.js +10 -0
  37. package/dist/index.d.ts +552 -129
  38. package/dist/index.js +195 -191
  39. package/dist/ui/Crosshair.vue.js +51 -27
  40. package/dist/ui/InfoBar.vue.js +98 -79
  41. package/dist/ui/Marker.vue.js +14 -11
  42. package/dist/ui/Navigator.vue.js +23 -23
  43. package/dist/ui/ReferenceLine.vue.js +19 -16
  44. package/dist/ui/Tooltip.vue.js +116 -84
  45. package/dist/ui/XAxis.vue.js +48 -0
  46. package/dist/ui/XAxis.vue2.js +4 -0
  47. package/dist/ui/YAxis.vue.js +44 -28
  48. package/dist/ui/YLabel.vue.js +82 -85
  49. package/package.json +2 -2
  50. package/src/BandSeries.vue +92 -0
  51. package/src/BarSeries.vue +13 -6
  52. package/src/CandlestickSeries.vue +9 -5
  53. package/src/ChartContainer.vue +20 -0
  54. package/src/LineSeries.vue +13 -6
  55. package/src/composables.ts +23 -4
  56. package/src/index.ts +16 -1
  57. package/src/ui/Crosshair.vue +41 -1
  58. package/src/ui/InfoBar.vue +22 -2
  59. package/src/ui/Marker.vue +5 -1
  60. package/src/ui/Navigator.vue +2 -1
  61. package/src/ui/ReferenceLine.vue +5 -1
  62. package/src/ui/Tooltip.vue +43 -6
  63. package/src/ui/{TimeAxis.vue → XAxis.vue} +18 -5
  64. package/src/ui/YAxis.vue +66 -12
  65. package/src/ui/YLabel.vue +37 -18
  66. package/dist/ui/TimeAxis.vue.js +0 -44
  67. package/dist/ui/TimeAxis.vue2.js +0 -4
@@ -1,139 +1,136 @@
1
- import { defineComponent as q, useSlots as j, computed as n, ref as w, onMounted as G, onUnmounted as W, openBlock as A, createElementBlock as B, Fragment as _, renderSlot as H, createElementVNode as T, normalizeStyle as z, unref as g, createVNode as J, createCommentVNode as K } from "vue";
2
- import { prefersReducedMotion as Q } from "../core/dist/animation/reduced-motion.js";
3
- import { resolveAnimate as X, firstVisibleValue as Z, BadgeAnimator as ee } from "../core/dist/chart/badge-animator.js";
4
- import { useChartInstance as te } from "../context.js";
5
- import ne from "./NumberFlow.vue.js";
6
- const ie = /* @__PURE__ */ q({
1
+ import { defineComponent as O, useSlots as W, computed as t, ref as w, onMounted as H, onUnmounted as J, openBlock as A, createElementBlock as B, Fragment as Y, renderSlot as K, createElementVNode as _, normalizeStyle as z, unref as M, createVNode as Q, createCommentVNode as X } from "vue";
2
+ import { prefersReducedMotion as Z } from "../core/dist/animation/reduced-motion.js";
3
+ import { resolveAnimate as ee, resolveYLabelSeriesId as te, firstVisibleValue as ae, BadgeAnimator as le } from "../core/dist/chart/badge-animator.js";
4
+ import { useChartInstance as ne } from "../context.js";
5
+ import oe from "./NumberFlow.vue.js";
6
+ const ce = /* @__PURE__ */ O({
7
7
  __name: "YLabel",
8
8
  props: {
9
9
  seriesId: {},
10
10
  color: {},
11
11
  format: { type: Function },
12
- animate: { type: [Boolean, Object] }
12
+ animate: { type: [Boolean, Object] },
13
+ scale: {}
13
14
  },
14
- setup(Y) {
15
- const f = Y, e = te(), E = j(), M = n(() => typeof E.default == "function"), i = w(0), o = w(null);
16
- let m = null, x = null, u = null;
17
- const N = n(() => X(f.animate));
15
+ setup(E) {
16
+ const r = E, e = ne(), N = W(), R = t(() => typeof N.default == "function"), l = w(0), i = w(null);
17
+ let m = null, g = null, s = null;
18
+ const T = t(() => ee(r.animate));
18
19
  function S() {
19
- u = null;
20
- const t = s.value, l = t !== null ? e.getStackedLastValue(t) : null;
21
- if (t === null || !l) {
22
- m = null, x = null;
20
+ s = null;
21
+ const a = o.value, u = a !== null ? e.getStackedLastValue(a) : null;
22
+ if (a === null || !u) {
23
+ m = null, g = null;
23
24
  return;
24
25
  }
25
- const v = l.value;
26
- (m === null || x !== t) && (m = new ee(v), x = t);
27
- const { glide: d, countUp: U, settleMs: $ } = N.value, I = e.getIntroFront(t), c = m.frame({
28
- target: v,
26
+ const f = u.value;
27
+ (m === null || g !== a) && (m = new le(f), g = a);
28
+ const { glide: $, countUp: j, settleMs: G } = T.value, I = e.getIntroFront(a), c = m.frame({
29
+ target: f,
29
30
  // Only needed for the intro count-up; skip the visible-range binary search
30
31
  // entirely once the reveal has settled (the hot streaming path).
31
- firstVisible: I < 1 ? Z(e, t, v) : v,
32
+ firstVisible: I < 1 ? ae(e, a, f) : f,
32
33
  introFront: I,
33
34
  now: performance.now(),
34
- glide: d,
35
- countUp: U,
36
- settleMs: $,
37
- reducedMotion: Q()
38
- }), y = o.value;
39
- (y === null || y.positionValue !== c.positionValue || y.textValue !== c.textValue || y.opacity !== c.opacity) && (o.value = { positionValue: c.positionValue, textValue: c.textValue, opacity: c.opacity }), c.animating && u === null && (u = requestAnimationFrame(S));
35
+ glide: $,
36
+ countUp: j,
37
+ settleMs: G,
38
+ reducedMotion: Z()
39
+ }), d = i.value;
40
+ (d === null || d.positionValue !== c.positionValue || d.textValue !== c.textValue || d.opacity !== c.opacity) && (i.value = { positionValue: c.positionValue, textValue: c.textValue, opacity: c.opacity }), c.animating && s === null && (s = requestAnimationFrame(S));
40
41
  }
41
- function O() {
42
- u === null && (u = requestAnimationFrame(S));
42
+ function D() {
43
+ s === null && (s = requestAnimationFrame(S));
43
44
  }
44
45
  const p = () => {
45
- i.value++, O();
46
+ l.value++, D();
46
47
  };
47
- G(() => {
48
+ H(() => {
48
49
  e.setYLabel(!0), e.on("overlayChange", p), e.on("viewportChange", p), e.getSeriesIds().length > 0 && p();
49
- }), W(() => {
50
- e.setYLabel(!1), e.off("overlayChange", p), e.off("viewportChange", p), u !== null && cancelAnimationFrame(u), u = null;
50
+ }), J(() => {
51
+ e.setYLabel(!1), e.off("overlayChange", p), e.off("viewportChange", p), s !== null && cancelAnimationFrame(s), s = null;
51
52
  });
52
- function R(t, l) {
53
- if (l !== void 0) return l;
54
- const v = t.getSeriesIdsByType("time", { visibleOnly: !0, singleLayerOnly: !0 });
55
- if (v.length > 0) return v[0];
56
- const d = t.getSeriesIdsByType("time", { visibleOnly: !0 });
57
- return d.length > 0 ? d[0] : null;
58
- }
59
- const s = n(() => (i.value, R(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())), D = n(() => o.value !== null ? o.value.positionValue : a.value?.value ?? 0), P = n(() => o.value !== null ? o.value.textValue : a.value?.value ?? 0), h = n(() => o.value !== null ? o.value.opacity : s.value === null ? 1 : e.getIntroFront(s.value) < 1 ? 0 : 1), V = n(() => (i.value, a.value === null ? 0 : e.yScale.valueToY(D.value))), k = n(() => a.value === null || b.value === null ? "neutral" : a.value.value > b.value ? "up" : a.value.value < b.value ? "down" : "neutral"), L = n(() => {
60
- if (a.value === null) return r.value.yLabel.neutralBackground;
61
- if (!a.value.isLive) return r.value.axis.textColor;
62
- if (f.color) return f.color;
63
- switch (k.value) {
53
+ const o = t(() => (l.value, te(e, r.seriesId))), n = t(() => (l.value, o.value !== null ? e.getStackedLastValue(o.value) : null)), y = t(() => (l.value, o.value !== null ? e.getPreviousClose(o.value) : null)), v = t(() => (l.value, e.getTheme())), C = t(() => (l.value, r.scale !== void 0 ? r.scale : o.value !== null ? e.getSeriesScale(o.value) : void 0)), x = t(() => (l.value, e.getYScale(C.value))), U = t(() => (l.value, e.getYScaleColumn(C.value)?.position ?? e.yAxisPosition)), q = t(() => i.value !== null ? i.value.positionValue : n.value?.value ?? 0), P = t(() => i.value !== null ? i.value.textValue : n.value?.value ?? 0), b = t(() => i.value !== null ? i.value.opacity : o.value === null ? 1 : e.getIntroFront(o.value) < 1 ? 0 : 1), h = t(() => (l.value, n.value === null ? 0 : x.value.valueToY(q.value))), F = t(() => n.value === null || y.value === null ? "neutral" : n.value.value > y.value ? "up" : n.value.value < y.value ? "down" : "neutral"), V = t(() => {
54
+ if (n.value === null) return v.value.yLabel.neutralBackground;
55
+ if (!n.value.isLive) return v.value.axis.textColor;
56
+ if (r.color) return r.color;
57
+ switch (F.value) {
64
58
  case "up":
65
- return r.value.yLabel.upBackground;
59
+ return v.value.yLabel.upBackground;
66
60
  case "down":
67
- return r.value.yLabel.downBackground;
61
+ return v.value.yLabel.downBackground;
68
62
  default:
69
- return r.value.yLabel.neutralBackground;
63
+ return v.value.yLabel.neutralBackground;
70
64
  }
71
- }), C = n(() => {
72
- i.value;
73
- const t = e.yScale.getRange(), l = t.max - t.min;
74
- return l < 0.1 ? 6 : l < 10 ? 4 : l < 1e3 ? 2 : 0;
75
- }), F = n(() => {
76
- if (f.format) return f.format;
77
- const t = new Intl.NumberFormat("en-US", {
78
- minimumFractionDigits: C.value,
79
- maximumFractionDigits: C.value,
65
+ }), L = t(() => {
66
+ l.value;
67
+ const a = x.value.getRange(), u = a.max - a.min;
68
+ return u < 0.1 ? 6 : u < 10 ? 4 : u < 1e3 ? 2 : 0;
69
+ }), k = t(() => {
70
+ if (r.format) return r.format;
71
+ l.value;
72
+ const a = x.value.getFormat();
73
+ if (a) return a;
74
+ const u = new Intl.NumberFormat("en-US", {
75
+ minimumFractionDigits: L.value,
76
+ maximumFractionDigits: L.value,
80
77
  useGrouping: !1
81
78
  });
82
- return (l) => t.format(l);
79
+ return (f) => u.format(f);
83
80
  });
84
- return (t, l) => a.value !== null ? (A(), B(_, { key: 0 }, [
85
- M.value ? H(t.$slots, "default", {
81
+ return (a, u) => n.value !== null ? (A(), B(Y, { key: 0 }, [
82
+ R.value ? K(a.$slots, "default", {
86
83
  key: 0,
87
- value: a.value.value,
88
- y: V.value,
89
- bgColor: L.value,
90
- isLive: a.value.isLive,
91
- direction: k.value,
92
- format: F.value,
93
- opacity: h.value
94
- }) : (A(), B(_, { key: 1 }, [
95
- T("div", {
84
+ value: n.value.value,
85
+ y: h.value,
86
+ bgColor: V.value,
87
+ isLive: n.value.isLive,
88
+ direction: F.value,
89
+ format: k.value,
90
+ opacity: b.value
91
+ }) : (A(), B(Y, { key: 1 }, [
92
+ _("div", {
96
93
  style: z({
97
94
  position: "absolute",
98
- left: g(e).plotLeft + "px",
99
- right: (g(e).yAxisPosition === "left" ? 0 : g(e).yAxisWidth) + "px",
100
- top: V.value + "px",
95
+ left: M(e).plotLeft + "px",
96
+ right: M(e).getAxisColumnWidth("right") + "px",
97
+ top: h.value + "px",
101
98
  height: "0",
102
- borderTop: "1px dashed " + L.value,
103
- opacity: h.value * 0.5,
99
+ borderTop: "1px dashed " + V.value,
100
+ opacity: b.value * 0.5,
104
101
  pointerEvents: "none",
105
102
  zIndex: 2
106
103
  })
107
104
  }, null, 4),
108
- T("div", {
105
+ _("div", {
109
106
  style: z({
110
107
  position: "absolute",
111
- [g(e).yAxisPosition]: "4px",
112
- top: V.value + "px",
108
+ [U.value]: "4px",
109
+ top: h.value + "px",
113
110
  transform: "translateY(-50%)",
114
- opacity: h.value,
111
+ opacity: b.value,
115
112
  pointerEvents: "auto",
116
113
  zIndex: 3,
117
- background: L.value,
118
- color: r.value.yLabel.textColor,
119
- fontSize: r.value.yLabel.fontSize + "px",
120
- fontFamily: r.value.typography.fontFamily,
114
+ background: V.value,
115
+ color: v.value.yLabel.textColor,
116
+ fontSize: v.value.yLabel.fontSize + "px",
117
+ fontFamily: v.value.typography.fontFamily,
121
118
  padding: "3px 8px",
122
119
  borderRadius: "3px",
123
120
  whiteSpace: "nowrap",
124
121
  transition: "background-color 0.3s ease"
125
122
  })
126
123
  }, [
127
- J(ne, {
124
+ Q(oe, {
128
125
  value: P.value,
129
- format: F.value,
126
+ format: k.value,
130
127
  "spin-duration": 350
131
128
  }, null, 8, ["value", "format"])
132
129
  ], 4)
133
130
  ], 64))
134
- ], 64)) : K("", !0);
131
+ ], 64)) : X("", !0);
135
132
  }
136
133
  });
137
134
  export {
138
- ie as default
135
+ ce as default
139
136
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wick-charts/vue",
3
- "version": "0.6.3",
3
+ "version": "0.7.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.6.3"
48
+ "@wick-charts/core": "^0.7.0"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "vite build"
@@ -0,0 +1,92 @@
1
+ <script setup lang="ts">
2
+ import type { BandData, BandSeriesOptions, SeriesSyncState } from '@wick-charts/core';
3
+ import { BandSeriesDef, EMPTY_SYNC_STATE, syncLayers, toLayers } 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
+ /**
11
+ * Band data — one band per layer. A single band passes a flat
12
+ * `{ time, low, high }[]`; several bands pass `BandPoint[][]`. To name/color
13
+ * a band, pass `{ label, color?, data }` (or an array of them). Time accepts
14
+ * ms or `Date`. On a numeric or category X axis (`axis.x.type`) samples
15
+ * carry `x` in place of `time`: `{ x: 'EU', low, high }`.
16
+ */
17
+ data: BandData;
18
+ /** Visual options override — fill, edge stroke, curve, entrance / intro / smoothing. */
19
+ options?: Partial<BandSeriesOptions>;
20
+ /** Stable series ID — same value across remounts. */
21
+ id?: string;
22
+ /** Show/hide the series without unmounting it — excludes it from the Y-range fit and tooltip/legend. Default `true`. Live. */
23
+ visible?: boolean;
24
+ /** Named Y scale from the container's `scales` — autoscales on its own, sharing the time axis. Default: the default scale. Live. */
25
+ scale?: string;
26
+ }>(),
27
+ // Vue's type-based `defineProps` casts an absent Boolean-typed prop to
28
+ // `false` (not `undefined`) without an explicit default — see the
29
+ // `interactive` fix in ChartContainer.vue for the full explanation.
30
+ { visible: true },
31
+ );
32
+
33
+ const chart = useChartInstance();
34
+ const seriesId = ref<string | null>(null);
35
+ // Per-layer sync state — drives the shared append/keepLast/update/replace
36
+ // reconciliation (plus per-layer labels/colors) so streaming eases instead of
37
+ // snapping Y every tick.
38
+ let prevSync: SeriesSyncState[] = [];
39
+
40
+ function applyData(id: string, data: BandData): void {
41
+ prevSync = syncLayers({ chart, id, data, prev: prevSync });
42
+ }
43
+
44
+ onMounted(() => {
45
+ const layerCount = toLayers(props.data).length;
46
+ const id = chart.addSeries(BandSeriesDef, {
47
+ ...props.options,
48
+ scale: props.scale,
49
+ layers: layerCount,
50
+ id: props.id,
51
+ });
52
+ seriesId.value = id;
53
+ prevSync = new Array(layerCount).fill(EMPTY_SYNC_STATE);
54
+ // Lazy watcher — apply initial data here so static-data mounts render without a no-op first frame.
55
+ applyData(id, props.data);
56
+ chart.setSeriesVisible(id, props.visible);
57
+ });
58
+
59
+ onUnmounted(() => {
60
+ if (seriesId.value) chart.removeSeries(seriesId.value);
61
+ });
62
+
63
+ watch(
64
+ () => props.data,
65
+ (data) => {
66
+ const id = seriesId.value;
67
+ if (!id) return;
68
+
69
+ applyData(id, data);
70
+ },
71
+ );
72
+
73
+ // `scale` rides the same update, so a stacking change is checked against the new scale.
74
+ watch(
75
+ () => [props.options, props.scale] as const,
76
+ ([options, scale]) => {
77
+ if (!seriesId.value) return;
78
+
79
+ chart.updateSeriesOptions(seriesId.value, options ? { ...options, scale } : { scale });
80
+ },
81
+ { deep: true },
82
+ );
83
+
84
+ watch(
85
+ () => props.visible,
86
+ (visible) => {
87
+ if (seriesId.value) chart.setSeriesVisible(seriesId.value, visible);
88
+ },
89
+ );
90
+ </script>
91
+
92
+ <template><span v-if="false" /></template>
package/src/BarSeries.vue CHANGED
@@ -7,13 +7,18 @@ import { useChartInstance } from './context';
7
7
 
8
8
  const props = withDefaults(
9
9
  defineProps<{
10
- /** Flat `TimePoint[]`, `TimePoint[][]`, or named `{ label, color?, data }` layers. Time accepts ms or `Date`. */
10
+ /**
11
+ * Flat `TimePoint[]`, `TimePoint[][]`, or named `{ label, color?, data }` layers. Time accepts ms or `Date`.
12
+ * On a numeric or category X axis (`axis.x.type`) points carry `x` in place of `time`.
13
+ */
11
14
  data: MultiLayerData;
12
15
  options?: Partial<BarSeriesOptions>;
13
16
  /** Stable series ID — same value across remounts. */
14
17
  id?: string;
15
18
  /** Show/hide the series without unmounting it — excludes it from the Y-range fit and tooltip/legend. Default `true`. Live. */
16
19
  visible?: boolean;
20
+ /** Named Y scale from the container's `scales` — autoscales on its own, sharing the time axis. Default: the default scale. Live. */
21
+ scale?: string;
17
22
  }>(),
18
23
  // Vue's type-based `defineProps` casts an absent Boolean-typed prop to
19
24
  // `false` (not `undefined`) without an explicit default — see the
@@ -36,6 +41,7 @@ onMounted(() => {
36
41
  const layerCount = toLayers(props.data).length;
37
42
  const id = chart.addSeries(BarSeriesDef, {
38
43
  ...props.options,
44
+ scale: props.scale,
39
45
  layers: layerCount,
40
46
  id: props.id,
41
47
  });
@@ -60,12 +66,13 @@ watch(
60
66
  },
61
67
  );
62
68
 
69
+ // `scale` rides the same update, so a stacking change is checked against the new scale.
63
70
  watch(
64
- () => props.options,
65
- (options) => {
66
- if (seriesId.value && options) {
67
- chart.updateSeriesOptions(seriesId.value, options);
68
- }
71
+ () => [props.options, props.scale] as const,
72
+ ([options, scale]) => {
73
+ if (!seriesId.value) return;
74
+
75
+ chart.updateSeriesOptions(seriesId.value, options ? { ...options, scale } : { scale });
69
76
  },
70
77
  { deep: true },
71
78
  );
@@ -14,6 +14,8 @@ const props = withDefaults(
14
14
  id?: string;
15
15
  /** Show/hide the series without unmounting it — excludes it from the Y-range fit and tooltip/legend. Default `true`. Live. */
16
16
  visible?: boolean;
17
+ /** Named Y scale from the container's `scales` — autoscales on its own, sharing the time axis. Default: the default scale. Live. */
18
+ scale?: string;
17
19
  }>(),
18
20
  // Vue's type-based `defineProps` casts an absent Boolean-typed prop to
19
21
  // `false` (not `undefined`) without an explicit default — see the
@@ -37,6 +39,7 @@ function applyData(id: string, data: CandlestickData): void {
37
39
  onMounted(() => {
38
40
  const id = chart.addSeries(CandlestickSeriesDef, {
39
41
  ...props.options,
42
+ scale: props.scale,
40
43
  id: props.id,
41
44
  label: toLayers<OHLCInput>(props.data)[0].label,
42
45
  });
@@ -62,12 +65,13 @@ watch(
62
65
  },
63
66
  );
64
67
 
68
+ // `scale` rides the same update, so a stacking change is checked against the new scale.
65
69
  watch(
66
- () => props.options,
67
- (options) => {
68
- if (seriesId.value && options) {
69
- chart.updateSeriesOptions(seriesId.value, options);
70
- }
70
+ () => [props.options, props.scale] as const,
71
+ ([options, scale]) => {
72
+ if (!seriesId.value) return;
73
+
74
+ chart.updateSeriesOptions(seriesId.value, options ? { ...options, scale } : { scale });
71
75
  },
72
76
  { deep: true },
73
77
  );
@@ -12,6 +12,7 @@ import {
12
12
  type SeriesHoverInfo,
13
13
  type VisibleRange,
14
14
  type VisibleRangeSpec,
15
+ type YAxisConfig,
15
16
  catppuccin,
16
17
  deepEqual,
17
18
  } from '@wick-charts/core';
@@ -35,6 +36,14 @@ const props = withDefaults(
35
36
  theme?: ChartTheme;
36
37
  /** Grouped axis configuration (Y/X visibility, bounds, sizing). Live. */
37
38
  axis?: AxisConfig;
39
+ /**
40
+ * Named secondary Y scales, keyed by id — a series opts in with its `scale`
41
+ * prop and autoscales on its own, sharing the time axis. Same fields as
42
+ * `axis.y`; `position` defaults to the side opposite the default axis. One
43
+ * axis column fits on each side — render it with `<YAxis scale="id" />`.
44
+ * Live.
45
+ */
46
+ scales?: Record<string, YAxisConfig>;
38
47
  /**
39
48
  * Viewport padding. `top`/`bottom` are in pixels. `left`/`right` accept either pixels (`50`)
40
49
  * or data intervals (`{ intervals: 3 }`). Set to 0 for edge-to-edge sparklines. Updates are
@@ -379,6 +388,7 @@ onMounted(async () => {
379
388
 
380
389
  const options: ChartOptions = {};
381
390
  if (props.axis) options.axis = props.axis;
391
+ if (props.scales) options.scales = props.scales;
382
392
  if (props.theme) options.theme = props.theme;
383
393
  if (props.padding) options.padding = props.padding;
384
394
  if (props.viewport) options.viewport = props.viewport;
@@ -432,6 +442,15 @@ watch(
432
442
  { deep: true },
433
443
  );
434
444
 
445
+ // The core skips a deep-equal replay, so a fresh inline literal doesn't refit.
446
+ watch(
447
+ () => props.scales,
448
+ (newScales) => {
449
+ chart.value?.setScales(newScales);
450
+ },
451
+ { deep: true },
452
+ );
453
+
435
454
  watch(
436
455
  () => props.grid?.visible,
437
456
  () => {
@@ -464,6 +483,7 @@ watch(
464
483
  await nextTick();
465
484
  const opts: ChartOptions = {};
466
485
  if (props.axis) opts.axis = props.axis;
486
+ if (props.scales) opts.scales = props.scales;
467
487
  if (props.theme) opts.theme = props.theme;
468
488
  if (props.padding) opts.padding = props.padding;
469
489
  if (props.viewport) opts.viewport = props.viewport;
@@ -7,13 +7,18 @@ import { useChartInstance } from './context';
7
7
 
8
8
  const props = withDefaults(
9
9
  defineProps<{
10
- /** Flat `TimePoint[]`, `TimePoint[][]`, or named `{ label, color?, data }` layers. Time accepts ms or `Date`. */
10
+ /**
11
+ * Flat `TimePoint[]`, `TimePoint[][]`, or named `{ label, color?, data }` layers. Time accepts ms or `Date`.
12
+ * On a numeric or category X axis (`axis.x.type`) points carry `x` in place of `time`.
13
+ */
11
14
  data: MultiLayerData;
12
15
  options?: Partial<LineSeriesOptions>;
13
16
  /** Stable series ID — same value across remounts. */
14
17
  id?: string;
15
18
  /** Show/hide the series without unmounting it — excludes it from the Y-range fit and tooltip/legend. Default `true`. Live. */
16
19
  visible?: boolean;
20
+ /** Named Y scale from the container's `scales` — autoscales on its own, sharing the time axis. Default: the default scale. Live. */
21
+ scale?: string;
17
22
  }>(),
18
23
  // Vue's type-based `defineProps` casts an absent Boolean-typed prop to
19
24
  // `false` (not `undefined`) without an explicit default — see the
@@ -36,6 +41,7 @@ onMounted(() => {
36
41
  const layerCount = toLayers(props.data).length;
37
42
  const id = chart.addSeries(LineSeriesDef, {
38
43
  ...props.options,
44
+ scale: props.scale,
39
45
  layers: layerCount,
40
46
  id: props.id,
41
47
  });
@@ -60,12 +66,13 @@ watch(
60
66
  },
61
67
  );
62
68
 
69
+ // `scale` rides the same update, so a stacking change is checked against the new scale.
63
70
  watch(
64
- () => props.options,
65
- (options) => {
66
- if (seriesId.value && options) {
67
- chart.updateSeriesOptions(seriesId.value, options);
68
- }
71
+ () => [props.options, props.scale] as const,
72
+ ([options, scale]) => {
73
+ if (!seriesId.value) return;
74
+
75
+ chart.updateSeriesOptions(seriesId.value, options ? { ...options, scale } : { scale });
69
76
  },
70
77
  { deep: true },
71
78
  );
@@ -25,10 +25,24 @@ export interface AxisLayout {
25
25
  yAxisPosition: YAxisPosition;
26
26
  yAxisWidth: number;
27
27
  xAxisHeight: number;
28
+ /** Width of the Y-axis column on each side — the default axis's or a named scale's, `0` for none. */
29
+ leftAxisWidth: number;
30
+ rightAxisWidth: number;
31
+ /** Named scale owning each side's column, `null` for the default axis or none — a swap moves the columns. */
32
+ leftAxisScale: string | null;
33
+ rightAxisScale: string | null;
28
34
  }
29
35
 
30
36
  function readAxisLayout(chart: ChartInstance): AxisLayout {
31
- return { yAxisPosition: chart.yAxisPosition, yAxisWidth: chart.yAxisWidth, xAxisHeight: chart.xAxisHeight };
37
+ return {
38
+ yAxisPosition: chart.yAxisPosition,
39
+ yAxisWidth: chart.yAxisWidth,
40
+ xAxisHeight: chart.xAxisHeight,
41
+ leftAxisWidth: chart.getAxisColumnWidth('left'),
42
+ rightAxisWidth: chart.getAxisColumnWidth('right'),
43
+ leftAxisScale: chart.getAxisColumnScale('left'),
44
+ rightAxisScale: chart.getAxisColumnScale('right'),
45
+ };
32
46
  }
33
47
 
34
48
  /** The axis gutters overlays anchor to — refreshed on a Y-axis side or gutter size change. */
@@ -40,7 +54,11 @@ export function useAxisLayout(chart: ChartInstance): Ref<AxisLayout> {
40
54
  const unchanged =
41
55
  prev.yAxisPosition === next.yAxisPosition &&
42
56
  prev.yAxisWidth === next.yAxisWidth &&
43
- prev.xAxisHeight === next.xAxisHeight;
57
+ prev.xAxisHeight === next.xAxisHeight &&
58
+ prev.leftAxisWidth === next.leftAxisWidth &&
59
+ prev.rightAxisWidth === next.rightAxisWidth &&
60
+ prev.leftAxisScale === next.leftAxisScale &&
61
+ prev.rightAxisScale === next.rightAxisScale;
44
62
  if (unchanged) return;
45
63
 
46
64
  layout.value = next;
@@ -57,15 +75,16 @@ export function useAxisLayout(chart: ChartInstance): Ref<AxisLayout> {
57
75
 
58
76
  export function useLastYValue(chart: ChartInstance, seriesId: string): Ref<{ value: number; isLive: boolean } | null> {
59
77
  let snapshot = chart.getLastValue(seriesId);
78
+ const yScale = () => chart.getYScale(chart.getSeriesScale(seriesId));
60
79
  // Track the pixel Y the snapshot maps to so a pan/zoom/resize that leaves the
61
80
  // value unchanged but shifts the badge still re-emits — a consumer-positioned
62
81
  // badge reads `yScale.valueToY` off this ref. Comparing prev/next against the
63
82
  // live yScale alone would always be equal.
64
- let lastY = snapshot ? chart.yScale.valueToY(snapshot.value) : null;
83
+ let lastY = snapshot ? yScale().valueToY(snapshot.value) : null;
65
84
  const val = ref<{ value: number; isLive: boolean } | null>(snapshot);
66
85
  const handler = () => {
67
86
  const next = chart.getLastValue(seriesId);
68
- const nextY = next ? chart.yScale.valueToY(next.value) : null;
87
+ const nextY = next ? yScale().valueToY(next.value) : null;
69
88
  if (snapshot?.value === next?.value && snapshot?.isLive === next?.isLive && lastY === nextY) return;
70
89
 
71
90
  snapshot = next;
package/src/index.ts CHANGED
@@ -13,6 +13,12 @@ export type {
13
13
  AnimationsConfig,
14
14
  AxisBound,
15
15
  AxisConfig,
16
+ BandData,
17
+ BandEntryAnimation,
18
+ BandPoint,
19
+ BandPointInput,
20
+ BandSampleInput,
21
+ BandSeriesOptions,
16
22
  BarElementTransform,
17
23
  BarEntryAnimation,
18
24
  BarIntroDirectives,
@@ -51,6 +57,7 @@ export type {
51
57
  HeatmapSeriesOptions,
52
58
  HeatmapSeriesRenderer,
53
59
  HoverInfo,
60
+ LayerSnapshot,
54
61
  LegendItem,
55
62
  LineIntroDirectives,
56
63
  LineIntroFn,
@@ -58,6 +65,7 @@ export type {
58
65
  LineIntroValueArgs,
59
66
  LinePaintArgs,
60
67
  LinePainter,
68
+ LinePointInput,
61
69
  LineSeriesOptions,
62
70
  LoadingIndicatorArgs,
63
71
  LoadingIndicatorFn,
@@ -129,6 +137,11 @@ export type {
129
137
  WaveIntroFrame,
130
138
  WaveIntroTransform,
131
139
  XAxisConfig,
140
+ XAxisFormatter,
141
+ XAxisPadding,
142
+ XAxisType,
143
+ XBandPointInput,
144
+ XPointInput,
132
145
  YAxisConfig,
133
146
  YAxisPosition,
134
147
  YRange,
@@ -136,6 +149,7 @@ export type {
136
149
  } from '@wick-charts/core';
137
150
  // Painters — custom per-element drawing for bar / candle / line series.
138
151
  export {
152
+ BandSeriesDef,
139
153
  BarSeriesDef,
140
154
  CandlestickSeriesDef,
141
155
  ChartInstance,
@@ -219,6 +233,7 @@ export {
219
233
  wipeIntro,
220
234
  } from '@wick-charts/core';
221
235
 
236
+ export { default as BandSeries } from './BandSeries.vue';
222
237
  export { default as BarSeries } from './BarSeries.vue';
223
238
  export { default as CandlestickSeries } from './CandlestickSeries.vue';
224
239
  // Vue components
@@ -239,10 +254,10 @@ export { default as NumberFlow } from './ui/NumberFlow.vue';
239
254
  export { default as PieLegend } from './ui/PieLegend.vue';
240
255
  export { default as PieTooltip } from './ui/PieTooltip.vue';
241
256
  export { default as ReferenceLine } from './ui/ReferenceLine.vue';
242
- export { default as TimeAxis, default as XAxis } from './ui/TimeAxis.vue';
243
257
  export { default as TimeRegion } from './ui/TimeRegion.vue';
244
258
  export { default as Title } from './ui/Title.vue';
245
259
  // UI overlays
246
260
  export { default as Tooltip } from './ui/Tooltip.vue';
261
+ export { default as TimeAxis, default as XAxis } from './ui/XAxis.vue';
247
262
  export { default as YAxis } from './ui/YAxis.vue';
248
263
  export { default as YLabel } from './ui/YLabel.vue';