@wick-charts/vue 0.5.1 → 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.
- package/README.md +18 -3
- package/dist/BarSeries.vue.js +26 -20
- package/dist/CandlestickSeries.vue.js +30 -24
- package/dist/ChartContainer.vue.js +88 -65
- package/dist/HeatmapSeries.vue.js +40 -0
- package/dist/HeatmapSeries.vue2.js +4 -0
- package/dist/LineSeries.vue.js +26 -20
- package/dist/PieSeries.vue.js +24 -18
- package/dist/core/dist/animation/animator.js +21 -21
- package/dist/core/dist/animation/config.js +114 -87
- package/dist/core/dist/animation/easing.js +3 -1
- package/dist/core/dist/animation/intro-wave.js +68 -0
- package/dist/core/dist/axis/dom-labels.js +13 -14
- package/dist/core/dist/chart/badge-animator.js +56 -0
- package/dist/core/dist/chart/edge-indicators.js +28 -13
- package/dist/core/dist/chart/last-value.js +18 -17
- package/dist/core/dist/chart/y-target.js +13 -12
- package/dist/core/dist/chart.js +679 -452
- package/dist/core/dist/components/annotation-chip.js +28 -0
- package/dist/core/dist/components/edge-indicator.js +58 -26
- package/dist/core/dist/components/loading-indicator.js +46 -0
- package/dist/core/dist/components/marker.js +49 -31
- package/dist/core/dist/components/reference-line.js +27 -36
- package/dist/core/dist/components/time-region.js +30 -28
- package/dist/core/dist/data/store.js +38 -19
- package/dist/core/dist/data/sync.js +60 -38
- package/dist/core/dist/heatmap-tooltip.js +16 -0
- package/dist/core/dist/interactions/handler.js +67 -43
- package/dist/core/dist/perf/perf-hud.js +5 -5
- package/dist/core/dist/scales/x-scale.js +85 -37
- package/dist/core/dist/scales/y-scale.js +38 -27
- package/dist/core/dist/series/bar-intro.js +18 -0
- package/dist/core/dist/series/bar.js +337 -205
- package/dist/core/dist/series/base-multi-layer.js +224 -96
- package/dist/core/dist/series/candlestick-intro.js +57 -0
- package/dist/core/dist/series/candlestick.js +454 -276
- package/dist/core/dist/series/heatmap.js +387 -0
- package/dist/core/dist/series/line-intro.js +85 -0
- package/dist/core/dist/series/line.js +590 -282
- package/dist/core/dist/series/painters/builtins.js +2 -2
- package/dist/core/dist/series/pie.js +396 -259
- package/dist/core/dist/series/register-builtins.js +8 -7
- package/dist/core/dist/series/types.js +6 -0
- package/dist/core/dist/series/wave-intro.js +26 -0
- package/dist/core/dist/theme/create.js +69 -63
- package/dist/core/dist/theme/themes/mint-breeze.js +2 -2
- package/dist/core/dist/theme/themes/sand-dune.js +2 -2
- package/dist/core/dist/utils/color.js +109 -34
- package/dist/core/dist/utils/deep-equal.js +18 -0
- package/dist/core/dist/utils/time.js +25 -19
- package/dist/index.d.ts +1405 -123
- package/dist/index.js +177 -130
- package/dist/ui/Crosshair.vue.js +20 -21
- package/dist/ui/HeatmapTooltip.vue.js +96 -0
- package/dist/ui/HeatmapTooltip.vue2.js +4 -0
- package/dist/ui/InfoBar.vue.js +46 -47
- package/dist/ui/Legend.vue.js +80 -79
- package/dist/ui/PieLegend.vue.js +5 -5
- package/dist/ui/PieTooltip.vue.js +77 -67
- package/dist/ui/Tooltip.vue.js +60 -60
- package/dist/ui/YAxis.vue.js +2 -2
- package/dist/ui/YLabel.vue.js +94 -61
- package/package.json +2 -2
- package/src/BarSeries.vue +23 -7
- package/src/CandlestickSeries.vue +23 -7
- package/src/ChartContainer.vue +134 -12
- package/src/HeatmapSeries.vue +69 -0
- package/src/LineSeries.vue +23 -7
- package/src/PieSeries.vue +22 -6
- package/src/index.ts +84 -3
- package/src/ui/Crosshair.vue +1 -2
- package/src/ui/HeatmapTooltip.vue +144 -0
- package/src/ui/InfoBar.vue +1 -2
- package/src/ui/Legend.vue +13 -0
- package/src/ui/Marker.vue +1 -1
- package/src/ui/PieTooltip.vue +10 -0
- package/src/ui/ReferenceLine.vue +2 -2
- package/src/ui/TimeRegion.vue +2 -2
- package/src/ui/Tooltip.vue +2 -2
- package/src/ui/YLabel.vue +123 -8
package/dist/ui/YLabel.vue.js
CHANGED
|
@@ -1,105 +1,138 @@
|
|
|
1
|
-
import { defineComponent as
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
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(
|
|
12
|
-
const
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
e.setYLabel(!0), e.on("overlayChange",
|
|
17
|
-
}),
|
|
18
|
-
e.setYLabel(!1), e.off("overlayChange",
|
|
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
|
|
51
|
+
function O(t, l) {
|
|
21
52
|
if (l !== void 0) return l;
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
const
|
|
25
|
-
return
|
|
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(() => (
|
|
28
|
-
if (
|
|
29
|
-
if (!
|
|
30
|
-
if (
|
|
31
|
-
switch (
|
|
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
|
|
64
|
+
return r.value.yLabel.upBackground;
|
|
34
65
|
case "down":
|
|
35
|
-
return
|
|
66
|
+
return r.value.yLabel.downBackground;
|
|
36
67
|
default:
|
|
37
|
-
return
|
|
68
|
+
return r.value.yLabel.neutralBackground;
|
|
38
69
|
}
|
|
39
|
-
}),
|
|
40
|
-
|
|
41
|
-
const
|
|
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
|
-
}),
|
|
44
|
-
if (
|
|
45
|
-
const
|
|
46
|
-
minimumFractionDigits:
|
|
47
|
-
maximumFractionDigits:
|
|
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) =>
|
|
81
|
+
return (l) => t.format(l);
|
|
51
82
|
});
|
|
52
|
-
return (
|
|
53
|
-
|
|
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:
|
|
56
|
-
y:
|
|
57
|
-
bgColor:
|
|
58
|
-
isLive:
|
|
59
|
-
direction:
|
|
60
|
-
format:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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:
|
|
67
|
-
top:
|
|
98
|
+
right: H(e).yAxisWidth + "px",
|
|
99
|
+
top: x.value + "px",
|
|
68
100
|
height: "0",
|
|
69
|
-
borderTop: "1px dashed " +
|
|
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
|
-
|
|
76
|
-
style:
|
|
107
|
+
_("div", {
|
|
108
|
+
style: T({
|
|
77
109
|
position: "absolute",
|
|
78
110
|
right: "4px",
|
|
79
|
-
top:
|
|
111
|
+
top: x.value + "px",
|
|
80
112
|
transform: "translateY(-50%)",
|
|
113
|
+
opacity: h.value,
|
|
81
114
|
pointerEvents: "auto",
|
|
82
115
|
zIndex: 3,
|
|
83
|
-
background:
|
|
84
|
-
color:
|
|
85
|
-
fontSize:
|
|
86
|
-
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
|
-
|
|
94
|
-
value:
|
|
95
|
-
format:
|
|
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)) :
|
|
133
|
+
], 64)) : K("", !0);
|
|
101
134
|
}
|
|
102
135
|
});
|
|
103
136
|
export {
|
|
104
|
-
|
|
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.
|
|
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.
|
|
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 =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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>
|
package/src/ChartContainer.vue
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
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`
|
|
240
|
-
//
|
|
241
|
-
// —
|
|
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>
|