@wick-charts/vue 0.7.0 → 0.8.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.
@@ -1,85 +1,85 @@
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";
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 D, createVNode as Q, createCommentVNode as X } from "vue";
2
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({
3
+ import { resolveAnimate as ee, resolveYLabelSeriesId as te, yLabelFractionDigits as ae, firstVisibleValue as le, BadgeAnimator as ne } from "../core/dist/chart/badge-animator.js";
4
+ import { useChartInstance as oe } from "../context.js";
5
+ import ue from "./NumberFlow.vue.js";
6
+ const fe = /* @__PURE__ */ O({
7
7
  __name: "YLabel",
8
8
  props: {
9
9
  seriesId: {},
10
10
  color: {},
11
- format: { type: Function },
12
- animate: { type: [Boolean, Object] },
11
+ format: {},
12
+ animate: { type: [Boolean, Object], default: void 0 },
13
13
  scale: {}
14
14
  },
15
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));
16
+ const u = E, e = oe(), M = W(), N = t(() => typeof M.default == "function"), l = w(0), r = w(null);
17
+ let m = null, g = null, i = null;
18
+ const R = t(() => ee(u.animate));
19
19
  function S() {
20
- s = null;
21
- const a = o.value, u = a !== null ? e.getStackedLastValue(a) : null;
22
- if (a === null || !u) {
20
+ i = null;
21
+ const a = o.value, d = a !== null ? e.getStackedLastValue(a) : null;
22
+ if (a === null || !d) {
23
23
  m = null, g = null;
24
24
  return;
25
25
  }
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,
26
+ const c = d.value;
27
+ (m === null || g !== a) && (m = new ne(c), g = a);
28
+ const { glide: $, countUp: j, settleMs: G } = R.value, I = e.getIntroFront(a), v = m.frame({
29
+ target: c,
30
30
  // Only needed for the intro count-up; skip the visible-range binary search
31
31
  // entirely once the reveal has settled (the hot streaming path).
32
- firstVisible: I < 1 ? ae(e, a, f) : f,
32
+ firstVisible: I < 1 ? le(e, a, c) : c,
33
33
  introFront: I,
34
34
  now: performance.now(),
35
35
  glide: $,
36
36
  countUp: j,
37
37
  settleMs: G,
38
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));
39
+ }), p = r.value;
40
+ (p === null || p.positionValue !== v.positionValue || p.textValue !== v.textValue || p.opacity !== v.opacity) && (r.value = { positionValue: v.positionValue, textValue: v.textValue, opacity: v.opacity }), v.animating && i === null && (i = requestAnimationFrame(S));
41
41
  }
42
- function D() {
43
- s === null && (s = requestAnimationFrame(S));
42
+ function T() {
43
+ i === null && (i = requestAnimationFrame(S));
44
44
  }
45
- const p = () => {
46
- l.value++, D();
45
+ const f = () => {
46
+ l.value++, T();
47
47
  };
48
48
  H(() => {
49
- e.setYLabel(!0), e.on("overlayChange", p), e.on("viewportChange", p), e.getSeriesIds().length > 0 && p();
49
+ e.setYLabel(!0), e.on("overlayChange", f), e.on("viewportChange", f), e.getSeriesIds().length > 0 && f();
50
50
  }), J(() => {
51
- e.setYLabel(!1), e.off("overlayChange", p), e.off("viewportChange", p), s !== null && cancelAnimationFrame(s), s = null;
51
+ e.setYLabel(!1), e.off("overlayChange", f), e.off("viewportChange", f), i !== null && cancelAnimationFrame(i), i = null;
52
52
  });
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;
53
+ const o = t(() => (l.value, te(e, u.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)), s = t(() => (l.value, e.getTheme())), C = t(() => (l.value, u.scale !== void 0 ? u.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(() => r.value !== null ? r.value.positionValue : n.value?.value ?? 0), P = t(() => r.value !== null ? r.value.textValue : n.value?.value ?? 0), b = t(() => r.value !== null ? r.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 s.value.yLabel.neutralBackground;
55
+ if (!n.value.isLive) return s.value.axis.textColor;
56
+ if (u.color) return u.color;
57
57
  switch (F.value) {
58
58
  case "up":
59
- return v.value.yLabel.upBackground;
59
+ return s.value.yLabel.upBackground;
60
60
  case "down":
61
- return v.value.yLabel.downBackground;
61
+ return s.value.yLabel.downBackground;
62
62
  default:
63
- return v.value.yLabel.neutralBackground;
63
+ return s.value.yLabel.neutralBackground;
64
64
  }
65
65
  }), L = t(() => {
66
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;
67
+ const a = x.value.getRange();
68
+ return ae(a.max - a.min);
69
69
  }), k = t(() => {
70
- if (r.format) return r.format;
70
+ if (u.format) return u.format;
71
71
  l.value;
72
72
  const a = x.value.getFormat();
73
73
  if (a) return a;
74
- const u = new Intl.NumberFormat("en-US", {
74
+ const d = new Intl.NumberFormat("en-US", {
75
75
  minimumFractionDigits: L.value,
76
76
  maximumFractionDigits: L.value,
77
77
  useGrouping: !1
78
78
  });
79
- return (f) => u.format(f);
79
+ return (c) => d.format(c);
80
80
  });
81
- return (a, u) => n.value !== null ? (A(), B(Y, { key: 0 }, [
82
- R.value ? K(a.$slots, "default", {
81
+ return (a, d) => n.value !== null ? (A(), B(Y, { key: 0 }, [
82
+ N.value ? K(a.$slots, "default", {
83
83
  key: 0,
84
84
  value: n.value.value,
85
85
  y: h.value,
@@ -92,8 +92,8 @@ const ce = /* @__PURE__ */ O({
92
92
  _("div", {
93
93
  style: z({
94
94
  position: "absolute",
95
- left: M(e).plotLeft + "px",
96
- right: M(e).getAxisColumnWidth("right") + "px",
95
+ left: D(e).plotLeft + "px",
96
+ right: D(e).getAxisColumnWidth("right") + "px",
97
97
  top: h.value + "px",
98
98
  height: "0",
99
99
  borderTop: "1px dashed " + V.value,
@@ -112,16 +112,16 @@ const ce = /* @__PURE__ */ O({
112
112
  pointerEvents: "auto",
113
113
  zIndex: 3,
114
114
  background: V.value,
115
- color: v.value.yLabel.textColor,
116
- fontSize: v.value.yLabel.fontSize + "px",
117
- fontFamily: v.value.typography.fontFamily,
115
+ color: s.value.yLabel.textColor,
116
+ fontSize: s.value.yLabel.fontSize + "px",
117
+ fontFamily: s.value.typography.fontFamily,
118
118
  padding: "3px 8px",
119
119
  borderRadius: "3px",
120
120
  whiteSpace: "nowrap",
121
121
  transition: "background-color 0.3s ease"
122
122
  })
123
123
  }, [
124
- Q(oe, {
124
+ Q(ue, {
125
125
  value: P.value,
126
126
  format: k.value,
127
127
  "spin-duration": 350
@@ -132,5 +132,5 @@ const ce = /* @__PURE__ */ O({
132
132
  }
133
133
  });
134
134
  export {
135
- ce as default
135
+ fe as default
136
136
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wick-charts/vue",
3
- "version": "0.7.0",
4
- "description": "High-performance canvas timeseries charts for Vue — candlestick, line, bar, pie. Tree-shakeable, zero runtime deps.",
3
+ "version": "0.8.1",
4
+ "description": "Realtime, animated canvas charts for Vue: candlestick, line, bar, heatmap, pie and more. Tree-shakeable, zero runtime deps.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -14,10 +14,16 @@
14
14
  "charts",
15
15
  "charting",
16
16
  "canvas",
17
+ "realtime",
18
+ "streaming",
19
+ "animation",
17
20
  "candlestick",
18
21
  "line-chart",
22
+ "area-chart",
19
23
  "bar-chart",
24
+ "heatmap",
20
25
  "pie-chart",
26
+ "sparkline",
21
27
  "timeseries",
22
28
  "vue",
23
29
  "visualization"
@@ -45,7 +51,7 @@
45
51
  "vue": ">=3.3.0"
46
52
  },
47
53
  "devDependencies": {
48
- "@wick-charts/core": "^0.7.0"
54
+ "@wick-charts/core": "^0.8.1"
49
55
  },
50
56
  "scripts": {
51
57
  "build": "vite build"
@@ -36,13 +36,14 @@ const seriesId = ref<string | null>(null);
36
36
  // reconciliation (plus per-layer labels/colors) so streaming eases instead of
37
37
  // snapping Y every tick.
38
38
  let prevSync: SeriesSyncState[] = [];
39
+ let layerCount = 0;
39
40
 
40
41
  function applyData(id: string, data: BandData): void {
41
42
  prevSync = syncLayers({ chart, id, data, prev: prevSync });
42
43
  }
43
44
 
44
- onMounted(() => {
45
- const layerCount = toLayers(props.data).length;
45
+ function createSeries(data: BandData): void {
46
+ layerCount = toLayers(data).length;
46
47
  const id = chart.addSeries(BandSeriesDef, {
47
48
  ...props.options,
48
49
  scale: props.scale,
@@ -52,8 +53,12 @@ onMounted(() => {
52
53
  seriesId.value = id;
53
54
  prevSync = new Array(layerCount).fill(EMPTY_SYNC_STATE);
54
55
  // Lazy watcher — apply initial data here so static-data mounts render without a no-op first frame.
55
- applyData(id, props.data);
56
+ applyData(id, data);
56
57
  chart.setSeriesVisible(id, props.visible);
58
+ }
59
+
60
+ onMounted(() => {
61
+ createSeries(props.data);
57
62
  });
58
63
 
59
64
  onUnmounted(() => {
@@ -66,6 +71,14 @@ watch(
66
71
  const id = seriesId.value;
67
72
  if (!id) return;
68
73
 
74
+ // The layer count is fixed when a series is created; a new count re-creates it, as in React.
75
+ if (toLayers(data).length !== layerCount) {
76
+ chart.removeSeries(id);
77
+ createSeries(data);
78
+
79
+ return;
80
+ }
81
+
69
82
  applyData(id, data);
70
83
  },
71
84
  );
package/src/BarSeries.vue CHANGED
@@ -32,13 +32,14 @@ const seriesId = ref<string | null>(null);
32
32
  // reconciliation (plus per-layer labels/colors) so streaming eases instead of
33
33
  // snapping Y every tick.
34
34
  let prevSync: SeriesSyncState[] = [];
35
+ let layerCount = 0;
35
36
 
36
37
  function applyData(id: string, data: MultiLayerData): void {
37
38
  prevSync = syncLayers({ chart, id, data, prev: prevSync });
38
39
  }
39
40
 
40
- onMounted(() => {
41
- const layerCount = toLayers(props.data).length;
41
+ function createSeries(data: MultiLayerData): void {
42
+ layerCount = toLayers(data).length;
42
43
  const id = chart.addSeries(BarSeriesDef, {
43
44
  ...props.options,
44
45
  scale: props.scale,
@@ -48,8 +49,12 @@ onMounted(() => {
48
49
  seriesId.value = id;
49
50
  prevSync = new Array(layerCount).fill(EMPTY_SYNC_STATE);
50
51
  // Lazy watcher — apply initial data here so static-data mounts render without a no-op first frame.
51
- applyData(id, props.data);
52
+ applyData(id, data);
52
53
  chart.setSeriesVisible(id, props.visible);
54
+ }
55
+
56
+ onMounted(() => {
57
+ createSeries(props.data);
53
58
  });
54
59
 
55
60
  onUnmounted(() => {
@@ -62,6 +67,14 @@ watch(
62
67
  const id = seriesId.value;
63
68
  if (!id) return;
64
69
 
70
+ // The layer count is fixed when a series is created; a new count re-creates it, as in React.
71
+ if (toLayers(data).length !== layerCount) {
72
+ chart.removeSeries(id);
73
+ createSeries(data);
74
+
75
+ return;
76
+ }
77
+
65
78
  applyData(id, data);
66
79
  },
67
80
  );
@@ -62,19 +62,23 @@ const props = withDefaults(
62
62
  */
63
63
  viewport?: {
64
64
  /**
65
- * Width of the visible window in data bars, set on the first data load
66
- * to `maxVisibleBars * dataInterval`. While the dataset is smaller than
67
- * this width, streaming ticks render into the empty right-side gap and
68
- * the viewport stays put; once the data reaches the right edge, the
69
- * viewport pans forward to keep the latest bar pinned (tail-scroll).
70
- * Default: 200.
65
+ * Most bars the chart's own fit shows. The first data load fits every bar
66
+ * up to this count; while the data holds fewer, streamed bars widen the
67
+ * window, and once it is full the window keeps its width and follows the
68
+ * latest bar. `fitContent()` and an explicit bar count are not capped.
69
+ * Default: 200. Values below 2 are clamped.
71
70
  */
72
71
  maxVisibleBars?: number;
73
72
  /**
74
73
  * Initial visible range applied before the first paint with data. Same
75
74
  * shape as the imperative `chart.setVisibleRange` — pass a bar count
76
75
  * (e.g. `35`), an explicit `{from, to}` window, or `{from, bars}` for
77
- * a warm-up pair. Captured at mount.
76
+ * a warm-up pair.
77
+ *
78
+ * It stays the chart's window until the user zooms or pans or the app
79
+ * sets a range, so a series that loads later or data set again keeps it.
80
+ * A bar count widens with streamed bars until it holds that many; a
81
+ * `{from, …}` window ends once streaming moves it. Captured at mount.
78
82
  */
79
83
  initialRange?: VisibleRangeSpec;
80
84
  };
@@ -86,7 +90,7 @@ const props = withDefaults(
86
90
  * two-way binding; a same-value literal (compared structurally) is a
87
91
  * no-op, so an inline object doesn't re-apply on every render.
88
92
  *
89
- * For a one-shot initial window that isn't re-applied on every prop
93
+ * For an initial window that isn't re-applied on every prop
90
94
  * change, use `viewport.initialRange` instead.
91
95
  */
92
96
  visibleRange?: VisibleRangeSpec;
@@ -211,6 +215,8 @@ const props = withDefaults(
211
215
  // distinguishable from an explicit `:fade="false"` all-off opt-out.
212
216
  // Declaring a default (even `undefined`) suppresses the cast.
213
217
  fade: undefined,
218
+ // Absent must reach the core as `undefined` (animations on), as in React and Svelte.
219
+ animations: undefined,
214
220
  },
215
221
  );
216
222
 
@@ -32,13 +32,14 @@ const seriesId = ref<string | null>(null);
32
32
  // reconciliation (plus per-layer labels/colors) so streaming eases instead of
33
33
  // snapping Y every tick.
34
34
  let prevSync: SeriesSyncState[] = [];
35
+ let layerCount = 0;
35
36
 
36
37
  function applyData(id: string, data: MultiLayerData): void {
37
38
  prevSync = syncLayers({ chart, id, data, prev: prevSync });
38
39
  }
39
40
 
40
- onMounted(() => {
41
- const layerCount = toLayers(props.data).length;
41
+ function createSeries(data: MultiLayerData): void {
42
+ layerCount = toLayers(data).length;
42
43
  const id = chart.addSeries(LineSeriesDef, {
43
44
  ...props.options,
44
45
  scale: props.scale,
@@ -48,8 +49,12 @@ onMounted(() => {
48
49
  seriesId.value = id;
49
50
  prevSync = new Array(layerCount).fill(EMPTY_SYNC_STATE);
50
51
  // Lazy watcher — apply initial data here so static-data mounts render without a no-op first frame.
51
- applyData(id, props.data);
52
+ applyData(id, data);
52
53
  chart.setSeriesVisible(id, props.visible);
54
+ }
55
+
56
+ onMounted(() => {
57
+ createSeries(props.data);
53
58
  });
54
59
 
55
60
  onUnmounted(() => {
@@ -62,6 +67,14 @@ watch(
62
67
  const id = seriesId.value;
63
68
  if (!id) return;
64
69
 
70
+ // The layer count is fixed when a series is created; a new count re-creates it, as in React.
71
+ if (toLayers(data).length !== layerCount) {
72
+ chart.removeSeries(id);
73
+ createSeries(data);
74
+
75
+ return;
76
+ }
77
+
65
78
  applyData(id, data);
66
79
  },
67
80
  );
package/src/ui/YLabel.vue CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  prefersReducedMotion,
8
8
  resolveAnimate,
9
9
  resolveYLabelSeriesId,
10
+ yLabelFractionDigits,
10
11
  } from '@wick-charts/core';
11
12
  import { computed, onMounted, onUnmounted, ref, useSlots } from 'vue';
12
13
 
@@ -21,28 +22,32 @@ export type { YLabelAnimate, YLabelAnimateOptions } from '@wick-charts/core';
21
22
  /** Direction of the current value vs. previous close. Drives the badge color in the default UI. */
22
23
  export type YLabelDirection = 'up' | 'down' | 'neutral';
23
24
 
24
- const props = defineProps<{
25
- /**
26
- * Owning series id. **Optional** — when omitted, the first visible
27
- * single-layer time series is picked, falling back to the first visible
28
- * multi-layer time series.
29
- */
30
- seriesId?: string;
31
- color?: string;
32
- /** Custom formatter; routed through NumberFlow's `formatter` so the digit animation still plays. Default: the scale's `<YAxis format>`, else a range-adaptive number. */
33
- format?: ValueFormatter;
34
- /**
35
- * Badge motion. `true` (default) glides + counts up + fades in with the
36
- * intro; `false` snaps with no easing; an object tunes individual parts.
37
- */
38
- animate?: YLabelAnimate;
39
- /**
40
- * Named Y scale (`ChartContainer` `scales`) the value maps through. Default:
41
- * the series' own scale. The badge sits on that scale's axis side — the
42
- * default axis side for an overlay scale.
43
- */
44
- scale?: string;
45
- }>();
25
+ const props = withDefaults(
26
+ defineProps<{
27
+ /**
28
+ * Owning series id. **Optional** — when omitted, the first visible
29
+ * single-layer time series is picked, falling back to the first visible
30
+ * multi-layer time series.
31
+ */
32
+ seriesId?: string;
33
+ color?: string;
34
+ /** Custom formatter; routed through NumberFlow's `formatter` so the digit animation still plays. Default: the scale's `<YAxis format>`, else a range-adaptive number. */
35
+ format?: ValueFormatter;
36
+ /**
37
+ * Badge motion. `true` (default) glides + counts up + fades in with the
38
+ * intro; `false` snaps with no easing; an object tunes individual parts.
39
+ */
40
+ animate?: YLabelAnimate;
41
+ /**
42
+ * Named Y scale (`ChartContainer` `scales`) the value maps through. Default:
43
+ * the series' own scale. The badge sits on that scale's axis side — the
44
+ * default axis side for an overlay scale.
45
+ */
46
+ scale?: string;
47
+ }>(),
48
+ // Vue casts an absent Boolean prop to `false`; an absent `animate` must stay `undefined` (on).
49
+ { animate: undefined },
50
+ );
46
51
 
47
52
  defineSlots<{
48
53
  default?(ctx: {
@@ -250,12 +255,8 @@ const bgColor = computed<string>(() => {
250
255
  const fractionDigits = computed(() => {
251
256
  void bump.value;
252
257
  const yRange = yScale.value.getRange();
253
- const range = yRange.max - yRange.min;
254
- if (range < 0.1) return 6;
255
- if (range < 10) return 4;
256
- if (range < 1000) return 2;
257
258
 
258
- return 0;
259
+ return yLabelFractionDigits(yRange.max - yRange.min);
259
260
  });
260
261
 
261
262
  const effectiveFormat = computed<ValueFormatter>(() => {