@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.
- package/README.md +6 -6
- package/dist/BandSeries.vue.js +52 -0
- package/dist/BandSeries.vue2.js +4 -0
- package/dist/BarSeries.vue.js +15 -13
- package/dist/CandlestickSeries.vue.js +31 -29
- package/dist/ChartContainer.vue.js +75 -68
- package/dist/LineSeries.vue.js +19 -17
- package/dist/composables.js +46 -36
- package/dist/core/dist/animation/config.js +92 -75
- package/dist/core/dist/animation/viewport-engine.js +155 -96
- package/dist/core/dist/axis/dom-labels.js +56 -51
- package/dist/core/dist/chart/badge-animator.js +30 -22
- package/dist/core/dist/chart/edge-indicators.js +22 -21
- package/dist/core/dist/chart/last-value.js +33 -26
- package/dist/core/dist/chart/scale-guards.js +23 -5
- package/dist/core/dist/chart/x-guards.js +37 -0
- package/dist/core/dist/chart.js +1286 -842
- package/dist/core/dist/data/decimation.js +58 -39
- package/dist/core/dist/data/sync.js +37 -31
- package/dist/core/dist/navigator/controller.js +41 -41
- package/dist/core/dist/perf/perf-hud.js +3 -3
- package/dist/core/dist/scales/tick-tracker.js +2 -2
- package/dist/core/dist/scales/x-scale.js +167 -53
- package/dist/core/dist/scales/y-scale.js +36 -34
- package/dist/core/dist/series/band.js +723 -0
- package/dist/core/dist/series/bar.js +297 -291
- package/dist/core/dist/series/base-multi-layer.js +283 -237
- package/dist/core/dist/series/candlestick.js +32 -31
- package/dist/core/dist/series/heatmap.js +5 -5
- package/dist/core/dist/series/line-intro.js +47 -37
- package/dist/core/dist/series/line.js +489 -448
- package/dist/core/dist/series/painters/builtins.js +4 -4
- package/dist/core/dist/series/painters/canvas-path.js +64 -52
- package/dist/core/dist/series/register-builtins.js +9 -8
- package/dist/core/dist/snapshots.js +59 -46
- package/dist/core/dist/utils/point.js +10 -0
- package/dist/index.d.ts +552 -129
- package/dist/index.js +195 -191
- package/dist/ui/Crosshair.vue.js +51 -27
- package/dist/ui/InfoBar.vue.js +98 -79
- package/dist/ui/Marker.vue.js +14 -11
- package/dist/ui/Navigator.vue.js +23 -23
- package/dist/ui/ReferenceLine.vue.js +19 -16
- package/dist/ui/Tooltip.vue.js +116 -84
- package/dist/ui/XAxis.vue.js +48 -0
- package/dist/ui/XAxis.vue2.js +4 -0
- package/dist/ui/YAxis.vue.js +44 -28
- package/dist/ui/YLabel.vue.js +82 -85
- package/package.json +2 -2
- package/src/BandSeries.vue +92 -0
- package/src/BarSeries.vue +13 -6
- package/src/CandlestickSeries.vue +9 -5
- package/src/ChartContainer.vue +20 -0
- package/src/LineSeries.vue +13 -6
- package/src/composables.ts +23 -4
- package/src/index.ts +16 -1
- package/src/ui/Crosshair.vue +41 -1
- package/src/ui/InfoBar.vue +22 -2
- package/src/ui/Marker.vue +5 -1
- package/src/ui/Navigator.vue +2 -1
- package/src/ui/ReferenceLine.vue +5 -1
- package/src/ui/Tooltip.vue +43 -6
- package/src/ui/{TimeAxis.vue → XAxis.vue} +18 -5
- package/src/ui/YAxis.vue +66 -12
- package/src/ui/YLabel.vue +37 -18
- package/dist/ui/TimeAxis.vue.js +0 -44
- package/dist/ui/TimeAxis.vue2.js +0 -4
package/dist/ui/YLabel.vue.js
CHANGED
|
@@ -1,139 +1,136 @@
|
|
|
1
|
-
import { defineComponent as
|
|
2
|
-
import { prefersReducedMotion as
|
|
3
|
-
import { resolveAnimate as
|
|
4
|
-
import { useChartInstance as
|
|
5
|
-
import
|
|
6
|
-
const
|
|
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(
|
|
15
|
-
const
|
|
16
|
-
let m = null,
|
|
17
|
-
const
|
|
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
|
-
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
m = 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
|
|
26
|
-
(m === null ||
|
|
27
|
-
const { glide:
|
|
28
|
-
target:
|
|
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 ?
|
|
32
|
+
firstVisible: I < 1 ? ae(e, a, f) : f,
|
|
32
33
|
introFront: I,
|
|
33
34
|
now: performance.now(),
|
|
34
|
-
glide:
|
|
35
|
-
countUp:
|
|
36
|
-
settleMs:
|
|
37
|
-
reducedMotion:
|
|
38
|
-
}),
|
|
39
|
-
(
|
|
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
|
|
42
|
-
|
|
42
|
+
function D() {
|
|
43
|
+
s === null && (s = requestAnimationFrame(S));
|
|
43
44
|
}
|
|
44
45
|
const p = () => {
|
|
45
|
-
|
|
46
|
+
l.value++, D();
|
|
46
47
|
};
|
|
47
|
-
|
|
48
|
+
H(() => {
|
|
48
49
|
e.setYLabel(!0), e.on("overlayChange", p), e.on("viewportChange", p), e.getSeriesIds().length > 0 && p();
|
|
49
|
-
}),
|
|
50
|
-
e.setYLabel(!1), e.off("overlayChange", p), e.off("viewportChange", p),
|
|
50
|
+
}), J(() => {
|
|
51
|
+
e.setYLabel(!1), e.off("overlayChange", p), e.off("viewportChange", p), s !== null && cancelAnimationFrame(s), s = null;
|
|
51
52
|
});
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
|
|
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
|
|
59
|
+
return v.value.yLabel.upBackground;
|
|
66
60
|
case "down":
|
|
67
|
-
return
|
|
61
|
+
return v.value.yLabel.downBackground;
|
|
68
62
|
default:
|
|
69
|
-
return
|
|
63
|
+
return v.value.yLabel.neutralBackground;
|
|
70
64
|
}
|
|
71
|
-
}),
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
return
|
|
75
|
-
}),
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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 (
|
|
79
|
+
return (f) => u.format(f);
|
|
83
80
|
});
|
|
84
|
-
return (
|
|
85
|
-
|
|
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:
|
|
88
|
-
y:
|
|
89
|
-
bgColor:
|
|
90
|
-
isLive:
|
|
91
|
-
direction:
|
|
92
|
-
format:
|
|
93
|
-
opacity:
|
|
94
|
-
}) : (A(), B(
|
|
95
|
-
|
|
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:
|
|
99
|
-
right: (
|
|
100
|
-
top:
|
|
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 " +
|
|
103
|
-
opacity:
|
|
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
|
-
|
|
105
|
+
_("div", {
|
|
109
106
|
style: z({
|
|
110
107
|
position: "absolute",
|
|
111
|
-
[
|
|
112
|
-
top:
|
|
108
|
+
[U.value]: "4px",
|
|
109
|
+
top: h.value + "px",
|
|
113
110
|
transform: "translateY(-50%)",
|
|
114
|
-
opacity:
|
|
111
|
+
opacity: b.value,
|
|
115
112
|
pointerEvents: "auto",
|
|
116
113
|
zIndex: 3,
|
|
117
|
-
background:
|
|
118
|
-
color:
|
|
119
|
-
fontSize:
|
|
120
|
-
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
|
-
|
|
124
|
+
Q(oe, {
|
|
128
125
|
value: P.value,
|
|
129
|
-
format:
|
|
126
|
+
format: k.value,
|
|
130
127
|
"spin-duration": 350
|
|
131
128
|
}, null, 8, ["value", "format"])
|
|
132
129
|
], 4)
|
|
133
130
|
], 64))
|
|
134
|
-
], 64)) :
|
|
131
|
+
], 64)) : X("", !0);
|
|
135
132
|
}
|
|
136
133
|
});
|
|
137
134
|
export {
|
|
138
|
-
|
|
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.
|
|
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.
|
|
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
|
-
/**
|
|
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
|
|
67
|
-
|
|
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
|
|
69
|
-
|
|
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
|
);
|
package/src/ChartContainer.vue
CHANGED
|
@@ -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;
|
package/src/LineSeries.vue
CHANGED
|
@@ -7,13 +7,18 @@ import { useChartInstance } from './context';
|
|
|
7
7
|
|
|
8
8
|
const props = withDefaults(
|
|
9
9
|
defineProps<{
|
|
10
|
-
/**
|
|
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
|
|
67
|
-
|
|
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
|
);
|
package/src/composables.ts
CHANGED
|
@@ -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 {
|
|
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 ?
|
|
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 ?
|
|
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';
|