@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/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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {
|
|
3
|
+
type BandPoint,
|
|
3
4
|
type OHLCData,
|
|
4
5
|
type SeriesSnapshot,
|
|
5
6
|
type SnapshotSort,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
buildHoverSnapshots,
|
|
9
10
|
buildLastSnapshots,
|
|
10
11
|
formatCompact,
|
|
12
|
+
formatHeaderX,
|
|
11
13
|
formatPriceAdaptive,
|
|
12
14
|
resolveCandlestickBodyColor,
|
|
13
15
|
} from '@wick-charts/core';
|
|
@@ -98,9 +100,13 @@ const displayTime = computed(() => {
|
|
|
98
100
|
const slots = useSlots();
|
|
99
101
|
const hasCustomSlot = computed(() => typeof slots.default === 'function');
|
|
100
102
|
|
|
101
|
-
function isOHLC(data:
|
|
103
|
+
function isOHLC(data: SeriesSnapshot['data']): data is OHLCData {
|
|
102
104
|
return 'open' in data;
|
|
103
105
|
}
|
|
106
|
+
|
|
107
|
+
function isBand(data: SeriesSnapshot['data']): data is BandPoint {
|
|
108
|
+
return 'high' in data && !isOHLC(data);
|
|
109
|
+
}
|
|
104
110
|
</script>
|
|
105
111
|
|
|
106
112
|
<template>
|
|
@@ -139,7 +145,7 @@ function isOHLC(data: OHLCData | TimePoint): data is OHLCData {
|
|
|
139
145
|
<slot v-if="hasCustomSlot" :snapshots="snapshots" :time="displayTime" :is-hover="isHover" />
|
|
140
146
|
<template v-else>
|
|
141
147
|
<span :style="{ color: theme.axis.textColor, marginRight: '2px' }">
|
|
142
|
-
{{ chart
|
|
148
|
+
{{ formatHeaderX(chart, { time: displayTime, snapshots, interval: dataInterval }) }}
|
|
143
149
|
</span>
|
|
144
150
|
<template v-for="s in snapshots" :key="s.id">
|
|
145
151
|
<span v-if="isOHLC(s.data)" :style="{ display: 'inline-flex', alignItems: 'center', gap: '4px' }">
|
|
@@ -174,6 +180,20 @@ function isOHLC(data: OHLCData | TimePoint): data is OHLCData {
|
|
|
174
180
|
</span>
|
|
175
181
|
</template>
|
|
176
182
|
</span>
|
|
183
|
+
<span v-else-if="isBand(s.data)" :style="{ display: 'inline-flex', alignItems: 'center', gap: '3px' }">
|
|
184
|
+
<span
|
|
185
|
+
:style="{
|
|
186
|
+
width: '6px',
|
|
187
|
+
height: '6px',
|
|
188
|
+
borderRadius: '1px',
|
|
189
|
+
background: s.color,
|
|
190
|
+
flexShrink: 0,
|
|
191
|
+
}"
|
|
192
|
+
/>
|
|
193
|
+
<span :style="{ color: s.color, fontWeight: 500 }">
|
|
194
|
+
{{ effectiveFormat((s.data as BandPoint).low, 'low') }} – {{ effectiveFormat((s.data as BandPoint).high, 'high') }}
|
|
195
|
+
</span>
|
|
196
|
+
</span>
|
|
177
197
|
<span v-else :style="{ display: 'inline-flex', alignItems: 'center', gap: '3px' }">
|
|
178
198
|
<span
|
|
179
199
|
:style="{
|
package/src/ui/Marker.vue
CHANGED
|
@@ -19,6 +19,8 @@ const props = defineProps<{
|
|
|
19
19
|
label?: string;
|
|
20
20
|
/** Color override. Falls back to the series color (when `seriesId` is set), then the theme line color. */
|
|
21
21
|
color?: string;
|
|
22
|
+
/** Named Y scale (`ChartContainer` `scales`) the `value` maps through. Default: the `seriesId` series' scale, else the default scale. */
|
|
23
|
+
scale?: string;
|
|
22
24
|
}>();
|
|
23
25
|
|
|
24
26
|
const chart = useChartInstance();
|
|
@@ -33,6 +35,7 @@ onMounted(() => {
|
|
|
33
35
|
pulse: props.pulse,
|
|
34
36
|
label: props.label,
|
|
35
37
|
color: props.color,
|
|
38
|
+
scale: props.scale,
|
|
36
39
|
});
|
|
37
40
|
});
|
|
38
41
|
|
|
@@ -42,7 +45,7 @@ onUnmounted(() => {
|
|
|
42
45
|
});
|
|
43
46
|
|
|
44
47
|
watch(
|
|
45
|
-
() => [props.time, props.value, props.seriesId, props.shape, props.pulse, props.label, props.color],
|
|
48
|
+
() => [props.time, props.value, props.seriesId, props.shape, props.pulse, props.label, props.color, props.scale],
|
|
46
49
|
() => {
|
|
47
50
|
if (markerId === null) return;
|
|
48
51
|
|
|
@@ -54,6 +57,7 @@ watch(
|
|
|
54
57
|
pulse: props.pulse,
|
|
55
58
|
label: props.label,
|
|
56
59
|
color: props.color,
|
|
60
|
+
scale: props.scale,
|
|
57
61
|
});
|
|
58
62
|
},
|
|
59
63
|
);
|
package/src/ui/Navigator.vue
CHANGED
|
@@ -25,6 +25,7 @@ const stripRef = ref<HTMLDivElement | null>(null);
|
|
|
25
25
|
const controller = shallowRef<NavigatorController | null>(null);
|
|
26
26
|
|
|
27
27
|
const resolvedHeight = computed(() => props.height ?? themeRef.value.navigator.height);
|
|
28
|
+
const timeAxis = computed(() => chartRef.value?.checkTimeOnlyFeature('Navigator') ?? false);
|
|
28
29
|
|
|
29
30
|
// Mount the controller once the strip div exists and the chart is available.
|
|
30
31
|
// Watch both — either may arrive after the other across Teleport boundaries.
|
|
@@ -63,7 +64,7 @@ onBeforeUnmount(() => {
|
|
|
63
64
|
</script>
|
|
64
65
|
|
|
65
66
|
<template>
|
|
66
|
-
<Teleport v-if="anchor" :to="anchor">
|
|
67
|
+
<Teleport v-if="anchor && timeAxis" :to="anchor">
|
|
67
68
|
<div
|
|
68
69
|
ref="stripRef"
|
|
69
70
|
data-chart-navigator=""
|
package/src/ui/ReferenceLine.vue
CHANGED
|
@@ -17,6 +17,8 @@ const props = defineProps<{
|
|
|
17
17
|
style?: ReferenceLineStyle;
|
|
18
18
|
/** Stroke width in CSS pixels. Default: `1`. */
|
|
19
19
|
width?: number;
|
|
20
|
+
/** Named Y scale (`ChartContainer` `scales`) the `value` maps through. Default: the default scale. */
|
|
21
|
+
scale?: string;
|
|
20
22
|
}>();
|
|
21
23
|
|
|
22
24
|
const chart = useChartInstance();
|
|
@@ -30,6 +32,7 @@ onMounted(() => {
|
|
|
30
32
|
label: props.label,
|
|
31
33
|
style: props.style,
|
|
32
34
|
width: props.width,
|
|
35
|
+
scale: props.scale,
|
|
33
36
|
});
|
|
34
37
|
});
|
|
35
38
|
|
|
@@ -39,7 +42,7 @@ onUnmounted(() => {
|
|
|
39
42
|
});
|
|
40
43
|
|
|
41
44
|
watch(
|
|
42
|
-
() => [props.value, props.time, props.color, props.label, props.style, props.width],
|
|
45
|
+
() => [props.value, props.time, props.color, props.label, props.style, props.width, props.scale],
|
|
43
46
|
() => {
|
|
44
47
|
if (lineId === null) return;
|
|
45
48
|
|
|
@@ -50,6 +53,7 @@ watch(
|
|
|
50
53
|
label: props.label,
|
|
51
54
|
style: props.style,
|
|
52
55
|
width: props.width,
|
|
56
|
+
scale: props.scale,
|
|
53
57
|
});
|
|
54
58
|
},
|
|
55
59
|
);
|
package/src/ui/Tooltip.vue
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {
|
|
3
|
+
type BandPoint,
|
|
3
4
|
type OHLCData,
|
|
4
5
|
type SeriesSnapshot,
|
|
5
6
|
type SnapshotSort,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
computeTooltipPosition,
|
|
10
11
|
formatCompact,
|
|
11
12
|
formatDate,
|
|
13
|
+
formatHeaderX,
|
|
12
14
|
formatPriceAdaptive,
|
|
13
15
|
resolveCandlestickBodyColor,
|
|
14
16
|
} from '@wick-charts/core';
|
|
@@ -105,7 +107,7 @@ const floatingPos = computed<{ left: number; top: number } | null>(() => {
|
|
|
105
107
|
if (!crosshair.value || snapshots.value.length === 0) return null;
|
|
106
108
|
const mediaSize = chart.getMediaSize();
|
|
107
109
|
const chartLeft = chart.plotLeft;
|
|
108
|
-
const chartWidth =
|
|
110
|
+
const chartWidth = chart.getLayout().chartArea.width;
|
|
109
111
|
const chartHeight = mediaSize.height - chart.xAxisHeight;
|
|
110
112
|
|
|
111
113
|
if (hasCustomSlot.value) {
|
|
@@ -125,7 +127,9 @@ const floatingPos = computed<{ left: number; top: number } | null>(() => {
|
|
|
125
127
|
|
|
126
128
|
const hasOHLC = snapshots.value.some((s) => 'open' in s.data);
|
|
127
129
|
const lineCount = snapshots.value.filter((s) => !('open' in s.data)).length;
|
|
128
|
-
|
|
130
|
+
// A band row carries two numbers, so it needs a wider box than a line row.
|
|
131
|
+
const hasBand = snapshots.value.some((s) => 'high' in s.data && !('open' in s.data));
|
|
132
|
+
const tooltipWidth = hasBand ? 220 : 160;
|
|
129
133
|
const tooltipHeight = hasOHLC ? 140 : 40 + lineCount * 22;
|
|
130
134
|
|
|
131
135
|
return computeTooltipPosition({
|
|
@@ -141,9 +145,13 @@ const floatingPos = computed<{ left: number; top: number } | null>(() => {
|
|
|
141
145
|
|
|
142
146
|
const measured = computed(() => !hasCustomSlot.value || measuredSize.value !== null);
|
|
143
147
|
|
|
144
|
-
function isOHLC(data:
|
|
148
|
+
function isOHLC(data: SeriesSnapshot['data']): data is OHLCData {
|
|
145
149
|
return 'open' in data;
|
|
146
150
|
}
|
|
151
|
+
|
|
152
|
+
function isBand(data: SeriesSnapshot['data']): data is BandPoint {
|
|
153
|
+
return 'high' in data && !isOHLC(data);
|
|
154
|
+
}
|
|
147
155
|
</script>
|
|
148
156
|
|
|
149
157
|
<template>
|
|
@@ -180,8 +188,10 @@ function isOHLC(data: OHLCData | TimePoint): data is OHLCData {
|
|
|
180
188
|
letterSpacing: '0.02em',
|
|
181
189
|
}"
|
|
182
190
|
>
|
|
183
|
-
|
|
184
|
-
|
|
191
|
+
<template v-if="chart.timeScale.getType() === 'time'">
|
|
192
|
+
{{ formatDate(displayTime, { locale: chart.timeScale.getLocale(), timeZone: chart.timeScale.getTimeZone() }) }}
|
|
193
|
+
</template>
|
|
194
|
+
{{ formatHeaderX(chart, { time: displayTime, snapshots, interval: dataInterval }) }}
|
|
185
195
|
</div>
|
|
186
196
|
|
|
187
197
|
<template v-for="s in snapshots" :key="s.id">
|
|
@@ -215,6 +225,33 @@ function isOHLC(data: OHLCData | TimePoint): data is OHLCData {
|
|
|
215
225
|
</span>
|
|
216
226
|
</template>
|
|
217
227
|
</div>
|
|
228
|
+
<div
|
|
229
|
+
v-else-if="isBand(s.data)"
|
|
230
|
+
:style="{ display: 'flex', alignItems: 'center', gap: '8px', padding: '2px 0' }"
|
|
231
|
+
>
|
|
232
|
+
<span
|
|
233
|
+
:style="{
|
|
234
|
+
width: '8px',
|
|
235
|
+
height: '8px',
|
|
236
|
+
borderRadius: '2px',
|
|
237
|
+
background: s.color,
|
|
238
|
+
flexShrink: 0,
|
|
239
|
+
}"
|
|
240
|
+
/>
|
|
241
|
+
<span
|
|
242
|
+
:style="{
|
|
243
|
+
opacity: 0.6,
|
|
244
|
+
flex: '1',
|
|
245
|
+
minWidth: 0,
|
|
246
|
+
overflow: 'hidden',
|
|
247
|
+
textOverflow: 'ellipsis',
|
|
248
|
+
whiteSpace: 'nowrap',
|
|
249
|
+
}"
|
|
250
|
+
>{{ s.label ?? 'Range' }}</span>
|
|
251
|
+
<span :style="{ fontWeight: 600, color: s.color, whiteSpace: 'nowrap' }">
|
|
252
|
+
{{ effectiveFormat((s.data as BandPoint).low, 'low') }} – {{ effectiveFormat((s.data as BandPoint).high, 'high') }}
|
|
253
|
+
</span>
|
|
254
|
+
</div>
|
|
218
255
|
<div v-else :style="{ display: 'flex', alignItems: 'center', gap: '8px', padding: '2px 0' }">
|
|
219
256
|
<span
|
|
220
257
|
:style="{
|
|
@@ -254,7 +291,7 @@ function isOHLC(data: OHLCData | TimePoint): data is OHLCData {
|
|
|
254
291
|
fontVariantNumeric: 'tabular-nums',
|
|
255
292
|
color: theme.tooltip.textColor,
|
|
256
293
|
width: 'max-content',
|
|
257
|
-
maxWidth:
|
|
294
|
+
maxWidth: chart.getLayout().chartArea.width + 'px',
|
|
258
295
|
boxSizing: 'border-box',
|
|
259
296
|
zIndex: 10,
|
|
260
297
|
visibility: measuredSize !== null ? 'visible' : 'hidden',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { mountAxisLabels } from '@wick-charts/core';
|
|
3
|
-
import {
|
|
2
|
+
import { type XAxisFormatter, mountAxisLabels } from '@wick-charts/core';
|
|
3
|
+
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
|
4
4
|
|
|
5
5
|
import { useAxisLayout } from '../composables';
|
|
6
6
|
import { useChartInstance } from '../context';
|
|
@@ -10,11 +10,16 @@ const props = defineProps<{
|
|
|
10
10
|
labelCount?: number;
|
|
11
11
|
/** Minimum pixel gap between adjacent labels (hard floor). Overrides chart-level. */
|
|
12
12
|
minLabelSpacing?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Custom label formatter. `x` is epoch ms on a time axis, the number on a
|
|
15
|
+
* numeric axis, the category name on a category axis; `step` is the tick
|
|
16
|
+
* spacing. Also formats the crosshair label and the tooltip / info-bar headers.
|
|
17
|
+
*/
|
|
18
|
+
format?: XAxisFormatter;
|
|
13
19
|
}>();
|
|
14
20
|
|
|
15
21
|
const chart = useChartInstance();
|
|
16
22
|
const layout = useAxisLayout(chart);
|
|
17
|
-
const onLeft = computed(() => layout.value.yAxisPosition === 'left');
|
|
18
23
|
const containerRef = ref<HTMLDivElement | null>(null);
|
|
19
24
|
|
|
20
25
|
const applyDensity = () => {
|
|
@@ -25,8 +30,16 @@ const applyDensity = () => {
|
|
|
25
30
|
};
|
|
26
31
|
applyDensity();
|
|
27
32
|
watch(() => [props.labelCount, props.minLabelSpacing], applyDensity);
|
|
33
|
+
|
|
34
|
+
chart.setXAxisFormat(props.format ?? null);
|
|
35
|
+
watch(
|
|
36
|
+
() => props.format,
|
|
37
|
+
(format) => chart.setXAxisFormat(format ?? null),
|
|
38
|
+
);
|
|
39
|
+
|
|
28
40
|
onUnmounted(() => {
|
|
29
41
|
chart.setTimeAxisLabelDensity({ labelCount: null, minLabelSpacing: null });
|
|
42
|
+
chart.setXAxisFormat(null);
|
|
30
43
|
});
|
|
31
44
|
|
|
32
45
|
let cleanup: (() => void) | null = null;
|
|
@@ -45,9 +58,9 @@ onUnmounted(() => {
|
|
|
45
58
|
ref="containerRef"
|
|
46
59
|
:style="{
|
|
47
60
|
position: 'absolute',
|
|
48
|
-
left:
|
|
61
|
+
left: layout.leftAxisWidth + 'px',
|
|
49
62
|
bottom: '0',
|
|
50
|
-
right:
|
|
63
|
+
right: layout.rightAxisWidth + 'px',
|
|
51
64
|
height: layout.xAxisHeight + 'px',
|
|
52
65
|
pointerEvents: 'none',
|
|
53
66
|
display: 'flex',
|
package/src/ui/YAxis.vue
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { type ValueFormatter, mountAxisLabels } from '@wick-charts/core';
|
|
3
|
-
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
|
3
|
+
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
4
4
|
|
|
5
|
-
import { useAxisLayout } from '../composables';
|
|
5
|
+
import { useAxisLayout, useYRange } from '../composables';
|
|
6
6
|
import { useChartInstance } from '../context';
|
|
7
7
|
|
|
8
8
|
const props = defineProps<{
|
|
@@ -15,34 +15,87 @@ const props = defineProps<{
|
|
|
15
15
|
labelCount?: number;
|
|
16
16
|
/** Minimum pixel gap between adjacent labels (hard floor). Overrides chart-level. */
|
|
17
17
|
minLabelSpacing?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Named Y scale (`ChartContainer` `scales`) this axis labels. Default: the
|
|
20
|
+
* default scale. An overlay scale has no column, so only `format` and the
|
|
21
|
+
* label density apply.
|
|
22
|
+
*/
|
|
23
|
+
scale?: string;
|
|
18
24
|
}>();
|
|
19
25
|
|
|
20
26
|
const chart = useChartInstance();
|
|
21
27
|
const layout = useAxisLayout(chart);
|
|
28
|
+
// `setScales` emits `viewportChange`, which this ref follows.
|
|
29
|
+
const yRange = useYRange(chart);
|
|
22
30
|
const containerRef = ref<HTMLDivElement | null>(null);
|
|
23
31
|
|
|
32
|
+
// An id `scales` doesn't define (yet) must not fall through to the default scale.
|
|
33
|
+
const known = computed(() => {
|
|
34
|
+
void yRange.value;
|
|
35
|
+
|
|
36
|
+
return props.scale === undefined || chart.getYScaleIds().includes(props.scale);
|
|
37
|
+
});
|
|
38
|
+
const column = computed(() => {
|
|
39
|
+
const { yAxisPosition, yAxisWidth } = layout.value;
|
|
40
|
+
if (props.scale === undefined) return { position: yAxisPosition, width: yAxisWidth };
|
|
41
|
+
|
|
42
|
+
return known.value ? chart.getYScaleColumn(props.scale) : null;
|
|
43
|
+
});
|
|
44
|
+
|
|
24
45
|
// Route the formatter through yScale so Crosshair / YLabel fallback use
|
|
25
46
|
// the same function as the axis labels.
|
|
26
|
-
|
|
47
|
+
// The scale whose formatter this axis set, so a scale switch and unmount clear that one.
|
|
48
|
+
let formatApplied: { scale: string | undefined } | null = null;
|
|
49
|
+
const clearFormat = () => {
|
|
50
|
+
if (formatApplied === null) return;
|
|
51
|
+
|
|
52
|
+
chart.setYAxisFormat({ format: null, scale: formatApplied.scale });
|
|
53
|
+
formatApplied = null;
|
|
54
|
+
};
|
|
55
|
+
const syncFormat = () => {
|
|
56
|
+
clearFormat();
|
|
57
|
+
if (!known.value) return;
|
|
58
|
+
|
|
59
|
+
chart.setYAxisFormat({ format: props.format ?? null, scale: props.scale });
|
|
60
|
+
formatApplied = { scale: props.scale };
|
|
61
|
+
};
|
|
27
62
|
onMounted(syncFormat);
|
|
28
|
-
watch(() => props.format, syncFormat);
|
|
29
|
-
onUnmounted(
|
|
63
|
+
watch(() => [props.format, props.scale, known.value], syncFormat);
|
|
64
|
+
onUnmounted(clearFormat);
|
|
65
|
+
|
|
66
|
+
// The scale whose density this axis set, so a scale switch and unmount reset that one.
|
|
67
|
+
let densityApplied: { scale: string | undefined } | null = null;
|
|
68
|
+
const resetDensity = () => {
|
|
69
|
+
if (densityApplied === null) return;
|
|
30
70
|
|
|
71
|
+
chart.setYAxisLabelDensity({ labelCount: null, minLabelSpacing: null, scale: densityApplied.scale });
|
|
72
|
+
densityApplied = null;
|
|
73
|
+
};
|
|
31
74
|
const applyDensity = () => {
|
|
75
|
+
if (densityApplied !== null && densityApplied.scale !== props.scale) resetDensity();
|
|
76
|
+
if (!known.value) return;
|
|
77
|
+
|
|
32
78
|
chart.setYAxisLabelDensity({
|
|
33
79
|
labelCount: props.labelCount ?? null,
|
|
34
80
|
minLabelSpacing: props.minLabelSpacing ?? null,
|
|
81
|
+
scale: props.scale,
|
|
35
82
|
});
|
|
83
|
+
densityApplied = { scale: props.scale };
|
|
36
84
|
};
|
|
37
85
|
applyDensity();
|
|
38
|
-
watch(() => [props.labelCount, props.minLabelSpacing], applyDensity);
|
|
39
|
-
onUnmounted(
|
|
86
|
+
watch(() => [props.labelCount, props.minLabelSpacing, props.scale, known.value], applyDensity);
|
|
87
|
+
onUnmounted(resetDensity);
|
|
40
88
|
|
|
41
89
|
let cleanup: (() => void) | null = null;
|
|
42
|
-
|
|
90
|
+
const mountLabels = () => {
|
|
91
|
+
cleanup?.();
|
|
92
|
+
cleanup = null;
|
|
43
93
|
if (containerRef.value === null) return;
|
|
44
|
-
|
|
45
|
-
});
|
|
94
|
+
|
|
95
|
+
cleanup = mountAxisLabels({ chart, container: containerRef.value, axis: 'y', scale: props.scale });
|
|
96
|
+
};
|
|
97
|
+
onMounted(mountLabels);
|
|
98
|
+
watch([containerRef, () => props.scale], mountLabels, { flush: 'post' });
|
|
46
99
|
onUnmounted(() => {
|
|
47
100
|
cleanup?.();
|
|
48
101
|
cleanup = null;
|
|
@@ -51,13 +104,14 @@ onUnmounted(() => {
|
|
|
51
104
|
|
|
52
105
|
<template>
|
|
53
106
|
<div
|
|
107
|
+
v-if="column"
|
|
54
108
|
ref="containerRef"
|
|
55
109
|
:style="{
|
|
56
110
|
position: 'absolute',
|
|
57
|
-
[
|
|
111
|
+
[column.position]: '0',
|
|
58
112
|
top: '0',
|
|
59
113
|
bottom: layout.xAxisHeight + 'px',
|
|
60
|
-
width:
|
|
114
|
+
width: column.width + 'px',
|
|
61
115
|
pointerEvents: 'none',
|
|
62
116
|
}"
|
|
63
117
|
/>
|
package/src/ui/YLabel.vue
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {
|
|
3
3
|
BadgeAnimator,
|
|
4
|
-
type ChartInstance,
|
|
5
4
|
type ValueFormatter,
|
|
6
5
|
type YLabelAnimate,
|
|
7
6
|
firstVisibleValue,
|
|
8
7
|
prefersReducedMotion,
|
|
9
8
|
resolveAnimate,
|
|
9
|
+
resolveYLabelSeriesId,
|
|
10
10
|
} from '@wick-charts/core';
|
|
11
11
|
import { computed, onMounted, onUnmounted, ref, useSlots } from 'vue';
|
|
12
12
|
|
|
@@ -29,13 +29,19 @@ const props = defineProps<{
|
|
|
29
29
|
*/
|
|
30
30
|
seriesId?: string;
|
|
31
31
|
color?: string;
|
|
32
|
-
/** Custom formatter; routed through NumberFlow's `formatter` so the digit animation still plays. */
|
|
32
|
+
/** Custom formatter; routed through NumberFlow's `formatter` so the digit animation still plays. Default: the scale's `<YAxis format>`, else a range-adaptive number. */
|
|
33
33
|
format?: ValueFormatter;
|
|
34
34
|
/**
|
|
35
35
|
* Badge motion. `true` (default) glides + counts up + fades in with the
|
|
36
36
|
* intro; `false` snaps with no easing; an object tunes individual parts.
|
|
37
37
|
*/
|
|
38
38
|
animate?: YLabelAnimate;
|
|
39
|
+
/**
|
|
40
|
+
* Named Y scale (`ChartContainer` `scales`) the value maps through. Default:
|
|
41
|
+
* the series' own scale. The badge sits on that scale's axis side — the
|
|
42
|
+
* default axis side for an overlay scale.
|
|
43
|
+
*/
|
|
44
|
+
scale?: string;
|
|
39
45
|
}>();
|
|
40
46
|
|
|
41
47
|
defineSlots<{
|
|
@@ -145,21 +151,10 @@ onUnmounted(() => {
|
|
|
145
151
|
rafId = null;
|
|
146
152
|
});
|
|
147
153
|
|
|
148
|
-
function resolveSeriesId(c: ChartInstance, explicit: string | undefined): string | null {
|
|
149
|
-
if (explicit !== undefined) return explicit;
|
|
150
|
-
|
|
151
|
-
const singleLayer = c.getSeriesIdsByType('time', { visibleOnly: true, singleLayerOnly: true });
|
|
152
|
-
if (singleLayer.length > 0) return singleLayer[0];
|
|
153
|
-
|
|
154
|
-
const anyTime = c.getSeriesIdsByType('time', { visibleOnly: true });
|
|
155
|
-
|
|
156
|
-
return anyTime.length > 0 ? anyTime[0] : null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
154
|
const resolvedId = computed<string | null>(() => {
|
|
160
155
|
void bump.value;
|
|
161
156
|
|
|
162
|
-
return
|
|
157
|
+
return resolveYLabelSeriesId(chart, props.seriesId);
|
|
163
158
|
});
|
|
164
159
|
|
|
165
160
|
const last = computed<{ value: number; isLive: boolean } | null>(() => {
|
|
@@ -180,6 +175,24 @@ const theme = computed(() => {
|
|
|
180
175
|
return chart.getTheme();
|
|
181
176
|
});
|
|
182
177
|
|
|
178
|
+
const scaleId = computed<string | undefined>(() => {
|
|
179
|
+
void bump.value;
|
|
180
|
+
if (props.scale !== undefined) return props.scale;
|
|
181
|
+
|
|
182
|
+
return resolvedId.value !== null ? chart.getSeriesScale(resolvedId.value) : undefined;
|
|
183
|
+
});
|
|
184
|
+
const yScale = computed(() => {
|
|
185
|
+
void bump.value;
|
|
186
|
+
|
|
187
|
+
return chart.getYScale(scaleId.value);
|
|
188
|
+
});
|
|
189
|
+
// An overlay scale has no column; its badge sits on the default axis side.
|
|
190
|
+
const side = computed(() => {
|
|
191
|
+
void bump.value;
|
|
192
|
+
|
|
193
|
+
return chart.getYScaleColumn(scaleId.value)?.position ?? chart.yAxisPosition;
|
|
194
|
+
});
|
|
195
|
+
|
|
183
196
|
// Position eases (the glide); the digit value steps discretely (once per real
|
|
184
197
|
// point, not per frame) so the roll doesn't churn. Before the first rAF frame
|
|
185
198
|
// lands, fall back to the settled value (hidden if an intro is still sweeping).
|
|
@@ -206,7 +219,7 @@ const y = computed(() => {
|
|
|
206
219
|
void bump.value;
|
|
207
220
|
if (last.value === null) return 0;
|
|
208
221
|
|
|
209
|
-
return
|
|
222
|
+
return yScale.value.valueToY(positionValue.value);
|
|
210
223
|
});
|
|
211
224
|
|
|
212
225
|
// Direction / color track the settled last value (not the mid-glide display
|
|
@@ -236,7 +249,7 @@ const bgColor = computed<string>(() => {
|
|
|
236
249
|
|
|
237
250
|
const fractionDigits = computed(() => {
|
|
238
251
|
void bump.value;
|
|
239
|
-
const yRange =
|
|
252
|
+
const yRange = yScale.value.getRange();
|
|
240
253
|
const range = yRange.max - yRange.min;
|
|
241
254
|
if (range < 0.1) return 6;
|
|
242
255
|
if (range < 10) return 4;
|
|
@@ -247,6 +260,12 @@ const fractionDigits = computed(() => {
|
|
|
247
260
|
|
|
248
261
|
const effectiveFormat = computed<ValueFormatter>(() => {
|
|
249
262
|
if (props.format) return props.format;
|
|
263
|
+
|
|
264
|
+
// `<YAxis format>` puts its formatter on the scale; the badge follows it.
|
|
265
|
+
void bump.value;
|
|
266
|
+
const scaleFormat = yScale.value.getFormat();
|
|
267
|
+
if (scaleFormat) return scaleFormat;
|
|
268
|
+
|
|
250
269
|
const nf = new Intl.NumberFormat('en-US', {
|
|
251
270
|
minimumFractionDigits: fractionDigits.value,
|
|
252
271
|
maximumFractionDigits: fractionDigits.value,
|
|
@@ -274,7 +293,7 @@ const effectiveFormat = computed<ValueFormatter>(() => {
|
|
|
274
293
|
:style="{
|
|
275
294
|
position: 'absolute',
|
|
276
295
|
left: chart.plotLeft + 'px',
|
|
277
|
-
right:
|
|
296
|
+
right: chart.getAxisColumnWidth('right') + 'px',
|
|
278
297
|
top: y + 'px',
|
|
279
298
|
height: '0',
|
|
280
299
|
borderTop: '1px dashed ' + bgColor,
|
|
@@ -286,7 +305,7 @@ const effectiveFormat = computed<ValueFormatter>(() => {
|
|
|
286
305
|
<div
|
|
287
306
|
:style="{
|
|
288
307
|
position: 'absolute',
|
|
289
|
-
[
|
|
308
|
+
[side]: '4px',
|
|
290
309
|
top: y + 'px',
|
|
291
310
|
transform: 'translateY(-50%)',
|
|
292
311
|
opacity: opacity,
|
package/dist/ui/TimeAxis.vue.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { defineComponent as c, computed as p, ref as m, watch as f, onUnmounted as u, onMounted as x, openBlock as b, createElementBlock as y, normalizeStyle as h, unref as o } from "vue";
|
|
2
|
-
import { mountAxisLabels as L } from "../core/dist/axis/dom-labels.js";
|
|
3
|
-
import { useAxisLayout as d } from "../composables.js";
|
|
4
|
-
import { useChartInstance as g } from "../context.js";
|
|
5
|
-
const D = /* @__PURE__ */ c({
|
|
6
|
-
__name: "TimeAxis",
|
|
7
|
-
props: {
|
|
8
|
-
labelCount: {},
|
|
9
|
-
minLabelSpacing: {}
|
|
10
|
-
},
|
|
11
|
-
setup(r) {
|
|
12
|
-
const e = r, n = g(), t = d(n), a = p(() => t.value.yAxisPosition === "left"), l = m(null), s = () => {
|
|
13
|
-
n.setTimeAxisLabelDensity({
|
|
14
|
-
labelCount: e.labelCount ?? null,
|
|
15
|
-
minLabelSpacing: e.minLabelSpacing ?? null
|
|
16
|
-
});
|
|
17
|
-
};
|
|
18
|
-
s(), f(() => [e.labelCount, e.minLabelSpacing], s), u(() => {
|
|
19
|
-
n.setTimeAxisLabelDensity({ labelCount: null, minLabelSpacing: null });
|
|
20
|
-
});
|
|
21
|
-
let i = null;
|
|
22
|
-
return x(() => {
|
|
23
|
-
l.value !== null && (i = L({ chart: n, container: l.value, axis: "x" }));
|
|
24
|
-
}), u(() => {
|
|
25
|
-
i?.(), i = null;
|
|
26
|
-
}), (A, v) => (b(), y("div", {
|
|
27
|
-
ref_key: "containerRef",
|
|
28
|
-
ref: l,
|
|
29
|
-
style: h({
|
|
30
|
-
position: "absolute",
|
|
31
|
-
left: (a.value ? o(t).yAxisWidth : 0) + "px",
|
|
32
|
-
bottom: "0",
|
|
33
|
-
right: (a.value ? 0 : o(t).yAxisWidth) + "px",
|
|
34
|
-
height: o(t).xAxisHeight + "px",
|
|
35
|
-
pointerEvents: "none",
|
|
36
|
-
display: "flex",
|
|
37
|
-
alignItems: "center"
|
|
38
|
-
})
|
|
39
|
-
}, null, 4));
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
export {
|
|
43
|
-
D as default
|
|
44
|
-
};
|
package/dist/ui/TimeAxis.vue2.js
DELETED