@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.
Files changed (67) hide show
  1. package/README.md +6 -6
  2. package/dist/BandSeries.vue.js +52 -0
  3. package/dist/BandSeries.vue2.js +4 -0
  4. package/dist/BarSeries.vue.js +15 -13
  5. package/dist/CandlestickSeries.vue.js +31 -29
  6. package/dist/ChartContainer.vue.js +75 -68
  7. package/dist/LineSeries.vue.js +19 -17
  8. package/dist/composables.js +46 -36
  9. package/dist/core/dist/animation/config.js +92 -75
  10. package/dist/core/dist/animation/viewport-engine.js +155 -96
  11. package/dist/core/dist/axis/dom-labels.js +56 -51
  12. package/dist/core/dist/chart/badge-animator.js +30 -22
  13. package/dist/core/dist/chart/edge-indicators.js +22 -21
  14. package/dist/core/dist/chart/last-value.js +33 -26
  15. package/dist/core/dist/chart/scale-guards.js +23 -5
  16. package/dist/core/dist/chart/x-guards.js +37 -0
  17. package/dist/core/dist/chart.js +1286 -842
  18. package/dist/core/dist/data/decimation.js +58 -39
  19. package/dist/core/dist/data/sync.js +37 -31
  20. package/dist/core/dist/navigator/controller.js +41 -41
  21. package/dist/core/dist/perf/perf-hud.js +3 -3
  22. package/dist/core/dist/scales/tick-tracker.js +2 -2
  23. package/dist/core/dist/scales/x-scale.js +167 -53
  24. package/dist/core/dist/scales/y-scale.js +36 -34
  25. package/dist/core/dist/series/band.js +723 -0
  26. package/dist/core/dist/series/bar.js +297 -291
  27. package/dist/core/dist/series/base-multi-layer.js +283 -237
  28. package/dist/core/dist/series/candlestick.js +32 -31
  29. package/dist/core/dist/series/heatmap.js +5 -5
  30. package/dist/core/dist/series/line-intro.js +47 -37
  31. package/dist/core/dist/series/line.js +489 -448
  32. package/dist/core/dist/series/painters/builtins.js +4 -4
  33. package/dist/core/dist/series/painters/canvas-path.js +64 -52
  34. package/dist/core/dist/series/register-builtins.js +9 -8
  35. package/dist/core/dist/snapshots.js +59 -46
  36. package/dist/core/dist/utils/point.js +10 -0
  37. package/dist/index.d.ts +552 -129
  38. package/dist/index.js +195 -191
  39. package/dist/ui/Crosshair.vue.js +51 -27
  40. package/dist/ui/InfoBar.vue.js +98 -79
  41. package/dist/ui/Marker.vue.js +14 -11
  42. package/dist/ui/Navigator.vue.js +23 -23
  43. package/dist/ui/ReferenceLine.vue.js +19 -16
  44. package/dist/ui/Tooltip.vue.js +116 -84
  45. package/dist/ui/XAxis.vue.js +48 -0
  46. package/dist/ui/XAxis.vue2.js +4 -0
  47. package/dist/ui/YAxis.vue.js +44 -28
  48. package/dist/ui/YLabel.vue.js +82 -85
  49. package/package.json +2 -2
  50. package/src/BandSeries.vue +92 -0
  51. package/src/BarSeries.vue +13 -6
  52. package/src/CandlestickSeries.vue +9 -5
  53. package/src/ChartContainer.vue +20 -0
  54. package/src/LineSeries.vue +13 -6
  55. package/src/composables.ts +23 -4
  56. package/src/index.ts +16 -1
  57. package/src/ui/Crosshair.vue +41 -1
  58. package/src/ui/InfoBar.vue +22 -2
  59. package/src/ui/Marker.vue +5 -1
  60. package/src/ui/Navigator.vue +2 -1
  61. package/src/ui/ReferenceLine.vue +5 -1
  62. package/src/ui/Tooltip.vue +43 -6
  63. package/src/ui/{TimeAxis.vue → XAxis.vue} +18 -5
  64. package/src/ui/YAxis.vue +66 -12
  65. package/src/ui/YLabel.vue +37 -18
  66. package/dist/ui/TimeAxis.vue.js +0 -44
  67. package/dist/ui/TimeAxis.vue2.js +0 -4
@@ -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
- {{ chart.yScale.formatY(position.y) }}
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
@@ -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: OHLCData | TimePoint): data is OHLCData {
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.timeScale.formatX(displayTime, dataInterval) }}
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
  );
@@ -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=""
@@ -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
  );
@@ -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 = mediaSize.width - chart.yAxisWidth;
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
- const tooltipWidth = 160;
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: OHLCData | TimePoint): data is OHLCData {
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
- {{ formatDate(displayTime, { locale: chart.timeScale.getLocale(), timeZone: chart.timeScale.getTimeZone() }) }}
184
- {{ chart.timeScale.formatX(displayTime, dataInterval) }}
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: (chart.getMediaSize().width - chart.yAxisWidth) + 'px',
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 { computed, onMounted, onUnmounted, ref, watch } from 'vue';
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: (onLeft ? layout.yAxisWidth : 0) + 'px',
61
+ left: layout.leftAxisWidth + 'px',
49
62
  bottom: '0',
50
- right: (onLeft ? 0 : layout.yAxisWidth) + 'px',
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
- const syncFormat = () => chart.yScale.setFormat(props.format ?? null);
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(() => chart.yScale.setFormat(null));
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(() => chart.setYAxisLabelDensity({ labelCount: null, minLabelSpacing: null }));
86
+ watch(() => [props.labelCount, props.minLabelSpacing, props.scale, known.value], applyDensity);
87
+ onUnmounted(resetDensity);
40
88
 
41
89
  let cleanup: (() => void) | null = null;
42
- onMounted(() => {
90
+ const mountLabels = () => {
91
+ cleanup?.();
92
+ cleanup = null;
43
93
  if (containerRef.value === null) return;
44
- cleanup = mountAxisLabels({ chart, container: containerRef.value, axis: 'y' });
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
- [layout.yAxisPosition]: '0',
111
+ [column.position]: '0',
58
112
  top: '0',
59
113
  bottom: layout.xAxisHeight + 'px',
60
- width: layout.yAxisWidth + 'px',
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 resolveSeriesId(chart, props.seriesId);
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 chart.yScale.valueToY(positionValue.value);
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 = chart.yScale.getRange();
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: (chart.yAxisPosition === 'left' ? 0 : chart.yAxisWidth) + 'px',
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
- [chart.yAxisPosition]: '4px',
308
+ [side]: '4px',
290
309
  top: y + 'px',
291
310
  transform: 'translateY(-50%)',
292
311
  opacity: opacity,
@@ -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
- };
@@ -1,4 +0,0 @@
1
- import f from "./TimeAxis.vue.js";
2
- export {
3
- f as default
4
- };