@wick-charts/vue 0.6.4 → 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 +4 -4
- package/dist/BandSeries.vue.js +20 -18
- package/dist/BarSeries.vue.js +15 -13
- package/dist/CandlestickSeries.vue.js +31 -29
- package/dist/ChartContainer.vue.js +77 -70
- package/dist/LineSeries.vue.js +19 -17
- package/dist/composables.js +46 -36
- 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/scale-guards.js +23 -5
- package/dist/core/dist/chart/x-guards.js +37 -0
- package/dist/core/dist/chart.js +1286 -843
- package/dist/core/dist/data/sync.js +37 -31
- package/dist/core/dist/navigator/controller.js +37 -37
- 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 +266 -213
- 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 +1 -0
- package/dist/core/dist/series/line.js +21 -20
- package/dist/core/dist/snapshots.js +55 -43
- package/dist/index.d.ts +323 -58
- package/dist/index.js +9 -9
- package/dist/ui/Crosshair.vue.js +51 -27
- package/dist/ui/InfoBar.vue.js +37 -37
- 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 +91 -86
- 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 +78 -74
- package/package.json +2 -2
- package/src/BandSeries.vue +11 -6
- 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 +8 -1
- package/src/ui/Crosshair.vue +41 -1
- package/src/ui/InfoBar.vue +2 -1
- 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 +7 -4
- package/src/ui/{TimeAxis.vue → XAxis.vue} +18 -5
- package/src/ui/YAxis.vue +66 -12
- package/src/ui/YLabel.vue +35 -5
- package/dist/ui/TimeAxis.vue.js +0 -44
- package/dist/ui/TimeAxis.vue2.js +0 -4
package/dist/ui/YLabel.vue.js
CHANGED
|
@@ -1,95 +1,99 @@
|
|
|
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
|
|
17
|
-
const
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
|
|
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));
|
|
19
|
+
function S() {
|
|
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
|
-
(
|
|
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:
|
|
32
|
-
introFront:
|
|
32
|
+
firstVisible: I < 1 ? ae(e, a, f) : f,
|
|
33
|
+
introFront: I,
|
|
33
34
|
now: performance.now(),
|
|
34
|
-
glide:
|
|
35
|
-
countUp:
|
|
36
|
-
settleMs:
|
|
37
|
-
reducedMotion:
|
|
38
|
-
}), d =
|
|
39
|
-
(d === null || d.positionValue !==
|
|
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
|
-
const
|
|
45
|
-
|
|
45
|
+
const p = () => {
|
|
46
|
+
l.value++, D();
|
|
46
47
|
};
|
|
47
|
-
|
|
48
|
-
e.setYLabel(!0), e.on("overlayChange",
|
|
49
|
-
}),
|
|
50
|
-
e.setYLabel(!1), e.off("overlayChange",
|
|
48
|
+
H(() => {
|
|
49
|
+
e.setYLabel(!0), e.on("overlayChange", p), e.on("viewportChange", p), e.getSeriesIds().length > 0 && p();
|
|
50
|
+
}), J(() => {
|
|
51
|
+
e.setYLabel(!1), e.off("overlayChange", p), e.off("viewportChange", p), s !== null && cancelAnimationFrame(s), s = null;
|
|
51
52
|
});
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
if (!
|
|
55
|
-
if (
|
|
56
|
-
switch (
|
|
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) {
|
|
57
58
|
case "up":
|
|
58
|
-
return
|
|
59
|
+
return v.value.yLabel.upBackground;
|
|
59
60
|
case "down":
|
|
60
|
-
return
|
|
61
|
+
return v.value.yLabel.downBackground;
|
|
61
62
|
default:
|
|
62
|
-
return
|
|
63
|
+
return v.value.yLabel.neutralBackground;
|
|
63
64
|
}
|
|
64
|
-
}),
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
return
|
|
68
|
-
}),
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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,
|
|
73
77
|
useGrouping: !1
|
|
74
78
|
});
|
|
75
|
-
return (
|
|
79
|
+
return (f) => u.format(f);
|
|
76
80
|
});
|
|
77
|
-
return (
|
|
78
|
-
|
|
81
|
+
return (a, u) => n.value !== null ? (A(), B(Y, { key: 0 }, [
|
|
82
|
+
R.value ? K(a.$slots, "default", {
|
|
79
83
|
key: 0,
|
|
80
|
-
value:
|
|
84
|
+
value: n.value.value,
|
|
81
85
|
y: h.value,
|
|
82
86
|
bgColor: V.value,
|
|
83
|
-
isLive:
|
|
84
|
-
direction:
|
|
85
|
-
format:
|
|
87
|
+
isLive: n.value.isLive,
|
|
88
|
+
direction: F.value,
|
|
89
|
+
format: k.value,
|
|
86
90
|
opacity: b.value
|
|
87
|
-
}) : (
|
|
91
|
+
}) : (A(), B(Y, { key: 1 }, [
|
|
88
92
|
_("div", {
|
|
89
|
-
style:
|
|
93
|
+
style: z({
|
|
90
94
|
position: "absolute",
|
|
91
|
-
left:
|
|
92
|
-
right: (
|
|
95
|
+
left: M(e).plotLeft + "px",
|
|
96
|
+
right: M(e).getAxisColumnWidth("right") + "px",
|
|
93
97
|
top: h.value + "px",
|
|
94
98
|
height: "0",
|
|
95
99
|
borderTop: "1px dashed " + V.value,
|
|
@@ -99,34 +103,34 @@ const ie = /* @__PURE__ */ $({
|
|
|
99
103
|
})
|
|
100
104
|
}, null, 4),
|
|
101
105
|
_("div", {
|
|
102
|
-
style:
|
|
106
|
+
style: z({
|
|
103
107
|
position: "absolute",
|
|
104
|
-
[
|
|
108
|
+
[U.value]: "4px",
|
|
105
109
|
top: h.value + "px",
|
|
106
110
|
transform: "translateY(-50%)",
|
|
107
111
|
opacity: b.value,
|
|
108
112
|
pointerEvents: "auto",
|
|
109
113
|
zIndex: 3,
|
|
110
114
|
background: V.value,
|
|
111
|
-
color:
|
|
112
|
-
fontSize:
|
|
113
|
-
fontFamily:
|
|
115
|
+
color: v.value.yLabel.textColor,
|
|
116
|
+
fontSize: v.value.yLabel.fontSize + "px",
|
|
117
|
+
fontFamily: v.value.typography.fontFamily,
|
|
114
118
|
padding: "3px 8px",
|
|
115
119
|
borderRadius: "3px",
|
|
116
120
|
whiteSpace: "nowrap",
|
|
117
121
|
transition: "background-color 0.3s ease"
|
|
118
122
|
})
|
|
119
123
|
}, [
|
|
120
|
-
|
|
121
|
-
value:
|
|
122
|
-
format:
|
|
124
|
+
Q(oe, {
|
|
125
|
+
value: P.value,
|
|
126
|
+
format: k.value,
|
|
123
127
|
"spin-duration": 350
|
|
124
128
|
}, null, 8, ["value", "format"])
|
|
125
129
|
], 4)
|
|
126
130
|
], 64))
|
|
127
|
-
], 64)) :
|
|
131
|
+
], 64)) : X("", !0);
|
|
128
132
|
}
|
|
129
133
|
});
|
|
130
134
|
export {
|
|
131
|
-
|
|
135
|
+
ce as default
|
|
132
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"
|
package/src/BandSeries.vue
CHANGED
|
@@ -11,7 +11,8 @@ const props = withDefaults(
|
|
|
11
11
|
* Band data — one band per layer. A single band passes a flat
|
|
12
12
|
* `{ time, low, high }[]`; several bands pass `BandPoint[][]`. To name/color
|
|
13
13
|
* a band, pass `{ label, color?, data }` (or an array of them). Time accepts
|
|
14
|
-
* ms or `Date`.
|
|
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 }`.
|
|
15
16
|
*/
|
|
16
17
|
data: BandData;
|
|
17
18
|
/** Visual options override — fill, edge stroke, curve, entrance / intro / smoothing. */
|
|
@@ -20,6 +21,8 @@ const props = withDefaults(
|
|
|
20
21
|
id?: string;
|
|
21
22
|
/** Show/hide the series without unmounting it — excludes it from the Y-range fit and tooltip/legend. Default `true`. Live. */
|
|
22
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;
|
|
23
26
|
}>(),
|
|
24
27
|
// Vue's type-based `defineProps` casts an absent Boolean-typed prop to
|
|
25
28
|
// `false` (not `undefined`) without an explicit default — see the
|
|
@@ -42,6 +45,7 @@ onMounted(() => {
|
|
|
42
45
|
const layerCount = toLayers(props.data).length;
|
|
43
46
|
const id = chart.addSeries(BandSeriesDef, {
|
|
44
47
|
...props.options,
|
|
48
|
+
scale: props.scale,
|
|
45
49
|
layers: layerCount,
|
|
46
50
|
id: props.id,
|
|
47
51
|
});
|
|
@@ -66,12 +70,13 @@ watch(
|
|
|
66
70
|
},
|
|
67
71
|
);
|
|
68
72
|
|
|
73
|
+
// `scale` rides the same update, so a stacking change is checked against the new scale.
|
|
69
74
|
watch(
|
|
70
|
-
() => props.options,
|
|
71
|
-
(options) => {
|
|
72
|
-
if (seriesId.value
|
|
73
|
-
|
|
74
|
-
}
|
|
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 });
|
|
75
80
|
},
|
|
76
81
|
{ deep: true },
|
|
77
82
|
);
|
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
|
@@ -17,6 +17,7 @@ export type {
|
|
|
17
17
|
BandEntryAnimation,
|
|
18
18
|
BandPoint,
|
|
19
19
|
BandPointInput,
|
|
20
|
+
BandSampleInput,
|
|
20
21
|
BandSeriesOptions,
|
|
21
22
|
BarElementTransform,
|
|
22
23
|
BarEntryAnimation,
|
|
@@ -64,6 +65,7 @@ export type {
|
|
|
64
65
|
LineIntroValueArgs,
|
|
65
66
|
LinePaintArgs,
|
|
66
67
|
LinePainter,
|
|
68
|
+
LinePointInput,
|
|
67
69
|
LineSeriesOptions,
|
|
68
70
|
LoadingIndicatorArgs,
|
|
69
71
|
LoadingIndicatorFn,
|
|
@@ -135,6 +137,11 @@ export type {
|
|
|
135
137
|
WaveIntroFrame,
|
|
136
138
|
WaveIntroTransform,
|
|
137
139
|
XAxisConfig,
|
|
140
|
+
XAxisFormatter,
|
|
141
|
+
XAxisPadding,
|
|
142
|
+
XAxisType,
|
|
143
|
+
XBandPointInput,
|
|
144
|
+
XPointInput,
|
|
138
145
|
YAxisConfig,
|
|
139
146
|
YAxisPosition,
|
|
140
147
|
YRange,
|
|
@@ -247,10 +254,10 @@ export { default as NumberFlow } from './ui/NumberFlow.vue';
|
|
|
247
254
|
export { default as PieLegend } from './ui/PieLegend.vue';
|
|
248
255
|
export { default as PieTooltip } from './ui/PieTooltip.vue';
|
|
249
256
|
export { default as ReferenceLine } from './ui/ReferenceLine.vue';
|
|
250
|
-
export { default as TimeAxis, default as XAxis } from './ui/TimeAxis.vue';
|
|
251
257
|
export { default as TimeRegion } from './ui/TimeRegion.vue';
|
|
252
258
|
export { default as Title } from './ui/Title.vue';
|
|
253
259
|
// UI overlays
|
|
254
260
|
export { default as Tooltip } from './ui/Tooltip.vue';
|
|
261
|
+
export { default as TimeAxis, default as XAxis } from './ui/XAxis.vue';
|
|
255
262
|
export { default as YAxis } from './ui/YAxis.vue';
|
|
256
263
|
export { default as YLabel } from './ui/YLabel.vue';
|
package/src/ui/Crosshair.vue
CHANGED
|
@@ -35,6 +35,32 @@ const tickInterval = computed(() => {
|
|
|
35
35
|
return chart.timeScale.niceTickValues(dataInterval.value).tickInterval || dataInterval.value;
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
// One value label per named scale's axis column, each read through its own scale.
|
|
39
|
+
// `bump` re-labels a still crosshair after a formatter or scale change.
|
|
40
|
+
const defaultLabel = computed(() => {
|
|
41
|
+
void bump.value;
|
|
42
|
+
const pos = position.value;
|
|
43
|
+
|
|
44
|
+
return pos === null ? '' : chart.yScale.formatY(pos.y);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const scaleLabels = computed(() => {
|
|
48
|
+
void bump.value;
|
|
49
|
+
const pos = position.value;
|
|
50
|
+
if (pos === null) return [];
|
|
51
|
+
|
|
52
|
+
return chart.getYScaleIds().flatMap((scale) => {
|
|
53
|
+
const column = chart.getYScaleColumn(scale);
|
|
54
|
+
if (column === null) return [];
|
|
55
|
+
|
|
56
|
+
const yScale = chart.getYScale(scale);
|
|
57
|
+
|
|
58
|
+
return [{ scale, position: column.position, text: yScale.formatY(yScale.yToValue(pos.mediaY)) }];
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
// A named scale can own the default axis's side when the default axis is hidden.
|
|
62
|
+
const defaultSideTaken = computed(() => scaleLabels.value.some((label) => label.position === chart.yAxisPosition));
|
|
63
|
+
|
|
38
64
|
const labelStyle = computed(() => ({
|
|
39
65
|
// Blend the theme's labelBackground at 80% opacity so the axis grid
|
|
40
66
|
// shows through — keeps the badge from looking like an opaque block
|
|
@@ -58,6 +84,7 @@ const labelStyle = computed(() => ({
|
|
|
58
84
|
<template v-if="position">
|
|
59
85
|
<!-- Y label on the value axis -->
|
|
60
86
|
<div
|
|
87
|
+
v-if="!defaultSideTaken"
|
|
61
88
|
:style="{
|
|
62
89
|
position: 'absolute',
|
|
63
90
|
[chart.yAxisPosition]: '0',
|
|
@@ -66,7 +93,20 @@ const labelStyle = computed(() => ({
|
|
|
66
93
|
...labelStyle,
|
|
67
94
|
}"
|
|
68
95
|
>
|
|
69
|
-
{{
|
|
96
|
+
{{ defaultLabel }}
|
|
97
|
+
</div>
|
|
98
|
+
<div
|
|
99
|
+
v-for="label in scaleLabels"
|
|
100
|
+
:key="label.scale"
|
|
101
|
+
:style="{
|
|
102
|
+
position: 'absolute',
|
|
103
|
+
[label.position]: '0',
|
|
104
|
+
top: position.mediaY + 'px',
|
|
105
|
+
transform: 'translateY(-50%)',
|
|
106
|
+
...labelStyle,
|
|
107
|
+
}"
|
|
108
|
+
>
|
|
109
|
+
{{ label.text }}
|
|
70
110
|
</div>
|
|
71
111
|
<!-- Time label on bottom axis -->
|
|
72
112
|
<div
|
package/src/ui/InfoBar.vue
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
buildHoverSnapshots,
|
|
10
10
|
buildLastSnapshots,
|
|
11
11
|
formatCompact,
|
|
12
|
+
formatHeaderX,
|
|
12
13
|
formatPriceAdaptive,
|
|
13
14
|
resolveCandlestickBodyColor,
|
|
14
15
|
} from '@wick-charts/core';
|
|
@@ -144,7 +145,7 @@ function isBand(data: SeriesSnapshot['data']): data is BandPoint {
|
|
|
144
145
|
<slot v-if="hasCustomSlot" :snapshots="snapshots" :time="displayTime" :is-hover="isHover" />
|
|
145
146
|
<template v-else>
|
|
146
147
|
<span :style="{ color: theme.axis.textColor, marginRight: '2px' }">
|
|
147
|
-
{{ chart
|
|
148
|
+
{{ formatHeaderX(chart, { time: displayTime, snapshots, interval: dataInterval }) }}
|
|
148
149
|
</span>
|
|
149
150
|
<template v-for="s in snapshots" :key="s.id">
|
|
150
151
|
<span v-if="isOHLC(s.data)" :style="{ display: 'inline-flex', alignItems: 'center', gap: '4px' }">
|