@wick-charts/vue 0.3.5 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wick-charts/vue",
3
- "version": "0.3.5",
3
+ "version": "0.4.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": {
@@ -8,7 +8,7 @@
8
8
  "url": "git+https://github.com/mo4islona/wick-charts.git",
9
9
  "directory": "packages/vue"
10
10
  },
11
- "homepage": "https://mo4islona.github.io/wick-charts/",
11
+ "homepage": "https://wick-charts.eeff.io/",
12
12
  "bugs": "https://github.com/mo4islona/wick-charts/issues",
13
13
  "keywords": [
14
14
  "charts",
@@ -45,7 +45,7 @@
45
45
  "vue": ">=3.3.0"
46
46
  },
47
47
  "devDependencies": {
48
- "@wick-charts/core": "^0.3.5"
48
+ "@wick-charts/core": "^0.4.0"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "vite build"
package/src/BarSeries.vue CHANGED
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
- import type { BarSeriesOptions, TimePoint } from '@wick-charts/core';
2
+ import type { BarSeriesOptions, SeriesSyncState, TimePoint } from '@wick-charts/core';
3
+ import { EMPTY_SYNC_STATE, syncSeriesLayer } from '@wick-charts/core';
3
4
  import { onMounted, onUnmounted, ref, watch } from 'vue';
4
5
 
5
6
  import { useChartInstance } from './context';
@@ -13,20 +14,28 @@ const props = defineProps<{
13
14
 
14
15
  const chart = useChartInstance();
15
16
  const seriesId = ref<string | null>(null);
17
+ // Per-layer sync state — drives the shared append/keepLast/update/replace
18
+ // reconciliation so streaming eases instead of snapping Y every tick.
19
+ let prevSync: SeriesSyncState[] = [];
20
+
21
+ function applyData(id: string, data: TimePoint[][]): void {
22
+ chart.batch(() => {
23
+ for (let i = 0; i < data.length; i++) {
24
+ prevSync[i] = syncSeriesLayer({ chart, id, data: data[i], prev: prevSync[i] ?? EMPTY_SYNC_STATE, layerIndex: i });
25
+ }
26
+ });
27
+ }
16
28
 
17
29
  onMounted(() => {
18
- const id = chart.addBarSeries({
30
+ const id = chart.addSeries('bar', {
19
31
  ...props.options,
20
32
  layers: props.data.length,
21
33
  id: props.id,
22
34
  });
23
35
  seriesId.value = id;
36
+ prevSync = props.data.map(() => EMPTY_SYNC_STATE);
24
37
  // Lazy watcher — apply initial data here so static-data mounts render without a no-op first frame.
25
- chart.batch(() => {
26
- for (let i = 0; i < props.data.length; i++) {
27
- chart.setSeriesData(id, props.data[i], i);
28
- }
29
- });
38
+ applyData(id, props.data);
30
39
  });
31
40
 
32
41
  onUnmounted(() => {
@@ -36,12 +45,10 @@ onUnmounted(() => {
36
45
  watch(
37
46
  () => props.data,
38
47
  (data) => {
39
- if (!seriesId.value) return;
40
- chart.batch(() => {
41
- for (let i = 0; i < data.length; i++) {
42
- chart.setSeriesData(seriesId.value!, data[i], i);
43
- }
44
- });
48
+ const id = seriesId.value;
49
+ if (!id) return;
50
+
51
+ applyData(id, data);
45
52
  },
46
53
  );
47
54
 
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
- import type { CandlestickSeriesOptions, OHLCInput } from '@wick-charts/core';
2
+ import type { CandlestickSeriesOptions, OHLCInput, SeriesSyncState } from '@wick-charts/core';
3
+ import { EMPTY_SYNC_STATE, syncSeriesLayer } from '@wick-charts/core';
3
4
  import { onMounted, onUnmounted, ref, watch } from 'vue';
4
5
 
5
6
  import { useChartInstance } from './context';
@@ -13,18 +14,18 @@ const props = defineProps<{
13
14
 
14
15
  const chart = useChartInstance();
15
16
  const seriesId = ref<string | null>(null);
16
- let prevLen = 0;
17
+ // Sync state — drives the shared append/keepLast/update/replace reconciliation
18
+ // so a rolling-window stream eases instead of snapping Y (and a 6–20 candle
19
+ // burst takes the smooth append path, matching React's 20-candle threshold).
20
+ let prevSync: SeriesSyncState = EMPTY_SYNC_STATE;
17
21
 
18
22
  onMounted(() => {
19
- const id = chart.addCandlestickSeries({ ...props.options, id: props.id });
23
+ const id = chart.addSeries('candlestick', { ...props.options, id: props.id });
20
24
  seriesId.value = id;
21
25
  // Initial data load — Vue's `watch` is lazy by default, so the watcher
22
26
  // below only fires on subsequent `data` prop mutations. Explicitly apply
23
27
  // the first value here so components with static data render immediately.
24
- if (props.data.length > 0) {
25
- chart.setSeriesData(id, props.data);
26
- prevLen = props.data.length;
27
- }
28
+ prevSync = syncSeriesLayer({ chart, id, data: props.data, prev: prevSync });
28
29
  });
29
30
 
30
31
  onUnmounted(() => {
@@ -37,23 +38,7 @@ watch(
37
38
  const id = seriesId.value;
38
39
  if (!id) return;
39
40
 
40
- if (data.length === 0) {
41
- chart.setSeriesData(id, []);
42
- prevLen = 0;
43
- return;
44
- }
45
-
46
- if (prevLen === 0 || data.length < prevLen || data.length - prevLen > 5) {
47
- chart.setSeriesData(id, data);
48
- } else if (data.length === prevLen) {
49
- chart.updateData(id, data[data.length - 1]);
50
- } else {
51
- for (let i = prevLen; i < data.length; i++) {
52
- chart.appendData(id, data[i]);
53
- }
54
- }
55
-
56
- prevLen = data.length;
41
+ prevSync = syncSeriesLayer({ chart, id, data, prev: prevSync });
57
42
  },
58
43
  );
59
44
 
@@ -5,6 +5,8 @@ import {
5
5
  ChartInstance,
6
6
  type ChartOptions,
7
7
  type ChartTheme,
8
+ type EdgeReachedInfo,
9
+ type VisibleRangeSpec,
8
10
  catppuccin,
9
11
  } from '@wick-charts/core';
10
12
  import { computed, nextTick, onMounted, onUnmounted, provide, ref, shallowRef, watch } from 'vue';
@@ -37,6 +39,28 @@ const props = withDefaults(
37
39
  right?: number | { intervals: number };
38
40
  left?: number | { intervals: number };
39
41
  };
42
+ /**
43
+ * Viewport-level streaming behavior. Captured at mount only — changing
44
+ * this prop after the chart is created is ignored.
45
+ */
46
+ viewport?: {
47
+ /**
48
+ * Width of the visible window in data bars, set on the first data load
49
+ * to `maxVisibleBars * dataInterval`. While the dataset is smaller than
50
+ * this width, streaming ticks render into the empty right-side gap and
51
+ * the viewport stays put; once the data reaches the right edge, the
52
+ * viewport pans forward to keep the latest bar pinned (tail-scroll).
53
+ * Default: 200.
54
+ */
55
+ maxVisibleBars?: number;
56
+ /**
57
+ * Initial visible range applied before the first paint with data. Same
58
+ * shape as the imperative `chart.setVisibleRange` — pass a bar count
59
+ * (e.g. `35`), an explicit `{from, to}` window, or `{from, bars}` for
60
+ * a warm-up pair. Captured at mount.
61
+ */
62
+ initialRange?: VisibleRangeSpec;
63
+ };
40
64
  /** Show the chart background gradient. Defaults to true. */
41
65
  gradient?: boolean;
42
66
  /** Enable zoom, pan, and crosshair interactions. Defaults to true. */
@@ -50,16 +74,15 @@ const props = withDefaults(
50
74
  */
51
75
  headerLayout?: 'overlay' | 'inline';
52
76
  /**
53
- * Chart-level animation configuration. See `AnimationsConfig` for the full shape.
77
+ * Animation control. `true` / omitted uses built-in defaults; `false`
78
+ * disables every category. Per-series options on `<LineSeries>` /
79
+ * `<CandlestickSeries>` / `<BarSeries>` override these chart-level
80
+ * defaults unless the category here is explicitly `false`.
54
81
  *
55
- * Two layers — chart-level (this prop) sets defaults for every series; per-series
56
- * options on `<LineSeries>`/`<CandlestickSeries>`/`<BarSeries>` override that
57
- * default for that one series.
58
- *
59
- * Shorthands: `true` / omitted — built-in defaults; `false` — disables every
60
- * animation category; `{ points: false }` / `{ viewport: false }` disables a
61
- * category. Updating this prop calls `chart.setAnimations(...)` so the new
62
- * durations take effect on the next animation / render.
82
+ * **Init-only by reference identity.** A new `animations` reference
83
+ * recreates the underlying `ChartInstance`. Hoist the object outside
84
+ * the render scope — an inline literal tears the chart down on every
85
+ * re-render.
63
86
  */
64
87
  animations?: boolean | AnimationsConfig;
65
88
  /**
@@ -67,6 +90,13 @@ const props = withDefaults(
67
90
  * later changes are ignored.
68
91
  */
69
92
  perf?: PerfOption;
93
+ /**
94
+ * Fired after the user releases a pan/zoom gesture that pulled the viewport
95
+ * past a data edge by more than ~10% of the visible range. Hosts typically
96
+ * respond by prefetching more history. Captured at mount only; changing
97
+ * the prop identity later is ignored.
98
+ */
99
+ onEdgeReached?: (info: EdgeReachedInfo) => void;
70
100
  }>(),
71
101
  {
72
102
  theme: () => catppuccin.theme,
@@ -82,6 +112,8 @@ const themeRef = shallowRef<ChartTheme>(props.theme);
82
112
  // Capture perf at mount only — mirrors React's perfRef so a new object
83
113
  // identity later doesn't recreate the chart.
84
114
  const perfAtMount = props.perf;
115
+ // Same mount-only capture for the edge callback — the chart binds it once.
116
+ const onEdgeReachedAtMount = props.onEdgeReached;
85
117
 
86
118
  const topOverlayRef = ref<HTMLElement | null>(null);
87
119
  const titleAnchor = ref<HTMLElement | null>(null);
@@ -149,9 +181,11 @@ onMounted(async () => {
149
181
  if (props.axis) options.axis = props.axis;
150
182
  if (props.theme) options.theme = props.theme;
151
183
  if (props.padding) options.padding = props.padding;
184
+ if (props.viewport) options.viewport = props.viewport;
152
185
  if (props.interactive !== undefined) options.interactive = props.interactive;
153
186
  if (props.grid !== undefined) options.grid = props.grid;
154
187
  if (perfAtMount !== undefined) options.perf = perfAtMount;
188
+ if (onEdgeReachedAtMount) options.onEdgeReached = onEdgeReachedAtMount;
155
189
  if (props.animations !== undefined) options.animations = props.animations;
156
190
  chart.value = new ChartInstance(containerRef.value, options);
157
191
 
@@ -202,14 +236,35 @@ watch(
202
236
  },
203
237
  );
204
238
 
205
- // Stringify the animations config so callers can pass a fresh object identity
206
- // without thrashing animator state when nothing has actually changed.
239
+ // Init-only: post-mount `animations` reference change tears down the
240
+ // instance and rebuilds with the new config. Reference equality matters
241
+ // — passing an inline literal recreates on every render.
242
+ //
243
+ // Children get the chart via `useChartInstance()` which snapshots
244
+ // `chartRef.value` once at setup time. To force them to re-setup against
245
+ // the new instance, we flip `chart.value` to `null` first — the `v-if`
246
+ // guard in the template unmounts the slot — then `await nextTick()` so
247
+ // Vue commits the unmount before the new ChartInstance is constructed.
248
+ // When `chart.value` becomes the new instance, the slot re-mounts and
249
+ // every child's `setup()` runs again against the fresh chart.
207
250
  watch(
208
- () => JSON.stringify(props.animations),
209
- () => {
210
- if (chart.value && props.animations !== undefined) {
211
- chart.value.setAnimations(props.animations);
212
- }
251
+ () => props.animations,
252
+ async (next) => {
253
+ if (!chart.value || !containerRef.value) return;
254
+ chart.value.destroy();
255
+ chart.value = null;
256
+ await nextTick();
257
+ const opts: ChartOptions = {};
258
+ if (props.axis) opts.axis = props.axis;
259
+ if (props.theme) opts.theme = props.theme;
260
+ if (props.padding) opts.padding = props.padding;
261
+ if (props.viewport) opts.viewport = props.viewport;
262
+ if (props.interactive !== undefined) opts.interactive = props.interactive;
263
+ if (props.grid !== undefined) opts.grid = props.grid;
264
+ if (perfAtMount !== undefined) opts.perf = perfAtMount;
265
+ if (onEdgeReachedAtMount) opts.onEdgeReached = onEdgeReachedAtMount;
266
+ if (next !== undefined) opts.animations = next;
267
+ chart.value = new ChartInstance(containerRef.value, opts);
213
268
  },
214
269
  );
215
270
 
@@ -231,9 +286,7 @@ const rootStyle = computed(() => {
231
286
  const t = themeRef.value;
232
287
  const [gtop, gbot] = t?.chartGradient ?? ['transparent', 'transparent'];
233
288
  const bg = t?.background ?? 'transparent';
234
- const background = props.gradient
235
- ? `linear-gradient(to bottom, ${gtop} 0%, ${bg} 70%, ${gbot} 100%)`
236
- : bg;
289
+ const background = props.gradient ? `linear-gradient(to bottom, ${gtop} 0%, ${bg} 70%, ${gbot} 100%)` : bg;
237
290
 
238
291
  return (
239
292
  'position: relative; display: flex; flex-direction: column; width: 100%; height: 100%; overflow: hidden; ' +
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
- import type { LineSeriesOptions, TimePoint } from '@wick-charts/core';
2
+ import type { LineSeriesOptions, SeriesSyncState, TimePoint } from '@wick-charts/core';
3
+ import { EMPTY_SYNC_STATE, syncSeriesLayer } from '@wick-charts/core';
3
4
  import { onMounted, onUnmounted, ref, watch } from 'vue';
4
5
 
5
6
  import { useChartInstance } from './context';
@@ -13,20 +14,28 @@ const props = defineProps<{
13
14
 
14
15
  const chart = useChartInstance();
15
16
  const seriesId = ref<string | null>(null);
17
+ // Per-layer sync state — drives the shared append/keepLast/update/replace
18
+ // reconciliation so streaming eases instead of snapping Y every tick.
19
+ let prevSync: SeriesSyncState[] = [];
20
+
21
+ function applyData(id: string, data: TimePoint[][]): void {
22
+ chart.batch(() => {
23
+ for (let i = 0; i < data.length; i++) {
24
+ prevSync[i] = syncSeriesLayer({ chart, id, data: data[i], prev: prevSync[i] ?? EMPTY_SYNC_STATE, layerIndex: i });
25
+ }
26
+ });
27
+ }
16
28
 
17
29
  onMounted(() => {
18
- const id = chart.addLineSeries({
30
+ const id = chart.addSeries('line', {
19
31
  ...props.options,
20
32
  layers: props.data.length,
21
33
  id: props.id,
22
34
  });
23
35
  seriesId.value = id;
36
+ prevSync = props.data.map(() => EMPTY_SYNC_STATE);
24
37
  // Lazy watcher — apply initial data here so static-data mounts render without a no-op first frame.
25
- chart.batch(() => {
26
- for (let i = 0; i < props.data.length; i++) {
27
- chart.setSeriesData(id, props.data[i], i);
28
- }
29
- });
38
+ applyData(id, props.data);
30
39
  });
31
40
 
32
41
  onUnmounted(() => {
@@ -36,12 +45,10 @@ onUnmounted(() => {
36
45
  watch(
37
46
  () => props.data,
38
47
  (data) => {
39
- if (!seriesId.value) return;
40
- chart.batch(() => {
41
- for (let i = 0; i < data.length; i++) {
42
- chart.setSeriesData(seriesId.value!, data[i], i);
43
- }
44
- });
48
+ const id = seriesId.value;
49
+ if (!id) return;
50
+
51
+ applyData(id, data);
45
52
  },
46
53
  );
47
54
 
package/src/PieSeries.vue CHANGED
@@ -15,7 +15,7 @@ const chart = useChartInstance();
15
15
  const seriesId = ref<string | null>(null);
16
16
 
17
17
  onMounted(() => {
18
- const id = chart.addPieSeries({ ...props.options, id: props.id });
18
+ const id = chart.addSeries('pie', { ...props.options, id: props.id });
19
19
  seriesId.value = id;
20
20
  // Lazy watcher — apply initial data here so static-data mounts render without a no-op first frame.
21
21
  if (props.data.length > 0) {
package/src/index.ts CHANGED
@@ -13,7 +13,6 @@ export type {
13
13
  AxisConfig,
14
14
  BarSeriesOptions,
15
15
  /** @deprecated Use {@link StackingMode} instead. */
16
- BarStacking,
17
16
  BuildHoverSnapshotsArgs,
18
17
  BuildLastSnapshotsArgs,
19
18
  CandlestickSeriesOptions,
@@ -24,7 +23,6 @@ export type {
24
23
  HoverInfo,
25
24
  LegendItem,
26
25
  /** @deprecated Use {@link TimePoint} instead. */
27
- LineData,
28
26
  LineSeriesOptions,
29
27
  NavigatorCandlePoint,
30
28
  NavigatorControllerParams,
@@ -68,7 +66,6 @@ export {
68
66
  catppuccin,
69
67
  computeTooltipPosition,
70
68
  createTheme,
71
- darkTheme,
72
69
  detectInterval,
73
70
  dracula,
74
71
  formatCompact,
@@ -81,7 +78,6 @@ export {
81
78
  highContrast,
82
79
  lavenderMist,
83
80
  lightPink,
84
- lightTheme,
85
81
  materialPalenight,
86
82
  minimalLight,
87
83
  mintBreeze,
@@ -1,8 +1,7 @@
1
1
  <script setup lang="ts">
2
- import { formatTime, resolveAxisFontSize, resolveAxisTextColor } from '@wick-charts/core';
3
- import { computed, onUnmounted, watch } from 'vue';
2
+ import { mountAxisLabels } from '@wick-charts/core';
3
+ import { onMounted, onUnmounted, ref, watch } from 'vue';
4
4
 
5
- import { useVisibleRange } from '../composables';
6
5
  import { useChartInstance } from '../context';
7
6
 
8
7
  const props = defineProps<{
@@ -12,13 +11,8 @@ const props = defineProps<{
12
11
  minLabelSpacing?: number;
13
12
  }>();
14
13
 
15
- interface TrackedTick {
16
- opacity: number;
17
- addedAt: number;
18
- }
19
-
20
14
  const chart = useChartInstance();
21
- const visibleRange = useVisibleRange(chart);
15
+ const containerRef = ref<HTMLDivElement | null>(null);
22
16
 
23
17
  const applyDensity = () => {
24
18
  chart.setTimeAxisLabelDensity({
@@ -32,51 +26,20 @@ onUnmounted(() => {
32
26
  chart.setTimeAxisLabelDensity({ labelCount: null, minLabelSpacing: null });
33
27
  });
34
28
 
35
- const tickMap = new Map<number, TrackedTick>();
36
-
37
- const theme = computed(() => chart.getTheme());
38
-
39
- const tickData = computed(() => {
40
- // Access visibleRange.value to track dependency
41
- void visibleRange.value;
42
-
43
- const dataInterval = chart.getDataInterval();
44
- const { ticks: currentTicks, tickInterval } = chart.timeScale.niceTickValues(dataInterval);
45
- const currentSet = new Set(currentTicks);
46
- const now = performance.now();
47
-
48
- // Mark current ticks as visible
49
- for (const t of currentTicks) {
50
- if (!tickMap.has(t)) {
51
- tickMap.set(t, { opacity: 1, addedAt: now });
52
- } else {
53
- tickMap.get(t)!.opacity = 1;
54
- }
55
- }
56
-
57
- // Mark missing ticks for fade-out
58
- for (const [t, entry] of tickMap) {
59
- if (!currentSet.has(t)) {
60
- entry.opacity = 0;
61
- }
62
- }
63
-
64
- // Clean up old faded-out ticks
65
- for (const [t, entry] of tickMap) {
66
- if (entry.opacity === 0 && now - entry.addedAt > 5000) {
67
- tickMap.delete(t);
68
- }
69
- }
70
-
71
- return {
72
- allTicks: Array.from(tickMap.entries()),
73
- tickInterval,
74
- };
29
+ let cleanup: (() => void) | null = null;
30
+ onMounted(() => {
31
+ if (containerRef.value === null) return;
32
+ cleanup = mountAxisLabels({ chart, container: containerRef.value, axis: 'x' });
33
+ });
34
+ onUnmounted(() => {
35
+ cleanup?.();
36
+ cleanup = null;
75
37
  });
76
38
  </script>
77
39
 
78
40
  <template>
79
41
  <div
42
+ ref="containerRef"
80
43
  :style="{
81
44
  position: 'absolute',
82
45
  left: '0',
@@ -87,25 +50,5 @@ const tickData = computed(() => {
87
50
  display: 'flex',
88
51
  alignItems: 'center',
89
52
  }"
90
- >
91
- <span
92
- v-for="[time, entry] in tickData.allTicks"
93
- :key="time"
94
- :style="{
95
- position: 'absolute',
96
- left: chart.timeScale.timeToX(time) + 'px',
97
- transform: 'translateX(-50%)',
98
- color: resolveAxisTextColor(theme, 'x'),
99
- fontSize: resolveAxisFontSize(theme, 'x') + 'px',
100
- fontFamily: theme.typography.fontFamily,
101
- userSelect: 'none',
102
- whiteSpace: 'nowrap',
103
- opacity: entry.opacity,
104
- transition: 'opacity 0.25s ease',
105
- willChange: 'opacity',
106
- }"
107
- >
108
- {{ formatTime(time, tickData.tickInterval) }}
109
- </span>
110
- </div>
53
+ />
111
54
  </template>
package/src/ui/Title.vue CHANGED
@@ -59,7 +59,7 @@ if (!theme) {
59
59
  display: 'flex',
60
60
  alignItems: 'baseline',
61
61
  gap: '6px',
62
- padding: '6px 8px 4px',
62
+ padding: '6px 8px 0',
63
63
  flexShrink: 0,
64
64
  fontFamily: theme.typography.fontFamily,
65
65
  fontSize: theme.typography.fontSize + 'px',
package/src/ui/YAxis.vue CHANGED
@@ -1,8 +1,7 @@
1
1
  <script setup lang="ts">
2
- import { resolveAxisFontSize, resolveAxisTextColor, type ValueFormatter } from '@wick-charts/core';
3
- import { computed, onMounted, onUnmounted, watch } from 'vue';
2
+ import { type ValueFormatter, mountAxisLabels } from '@wick-charts/core';
3
+ import { onMounted, onUnmounted, ref, watch } from 'vue';
4
4
 
5
- import { useVisibleRange } from '../composables';
6
5
  import { useChartInstance } from '../context';
7
6
 
8
7
  const props = defineProps<{
@@ -17,14 +16,8 @@ const props = defineProps<{
17
16
  minLabelSpacing?: number;
18
17
  }>();
19
18
 
20
- interface TrackedTick {
21
- opacity: number;
22
- addedAt: number;
23
- }
24
-
25
19
  const chart = useChartInstance();
26
- // Subscribe to visible range to trigger re-renders on viewport changes
27
- const visibleRange = useVisibleRange(chart);
20
+ const containerRef = ref<HTMLDivElement | null>(null);
28
21
 
29
22
  // Route the formatter through yScale so Crosshair / YLabel fallback use
30
23
  // the same function as the axis labels.
@@ -39,53 +32,24 @@ const applyDensity = () => {
39
32
  minLabelSpacing: props.minLabelSpacing ?? null,
40
33
  });
41
34
  };
42
- // Run during setup (before first paint) to match TimeAxis.vue and avoid a
43
- // post-mount density change that would cause a visible tick "jump".
44
35
  applyDensity();
45
36
  watch(() => [props.labelCount, props.minLabelSpacing], applyDensity);
46
37
  onUnmounted(() => chart.setYAxisLabelDensity({ labelCount: null, minLabelSpacing: null }));
47
38
 
48
- const tickMap = new Map<number, TrackedTick>();
49
-
50
- const theme = computed(() => chart.getTheme());
51
-
52
- const allTicks = computed(() => {
53
- // Access visibleRange.value to track dependency
54
- void visibleRange.value;
55
-
56
- const currentTicks = chart.yScale.niceTickValues();
57
- const currentSet = new Set(currentTicks);
58
- const now = performance.now();
59
-
60
- // Mark current ticks as visible
61
- for (const p of currentTicks) {
62
- if (!tickMap.has(p)) {
63
- tickMap.set(p, { opacity: 1, addedAt: now });
64
- } else {
65
- tickMap.get(p)!.opacity = 1;
66
- }
67
- }
68
-
69
- // Mark missing ticks for fade-out
70
- for (const [p, entry] of tickMap) {
71
- if (!currentSet.has(p)) {
72
- entry.opacity = 0;
73
- }
74
- }
75
-
76
- // Clean up old faded-out ticks
77
- for (const [p, entry] of tickMap) {
78
- if (entry.opacity === 0 && now - entry.addedAt > 5000) {
79
- tickMap.delete(p);
80
- }
81
- }
82
-
83
- return Array.from(tickMap.entries());
39
+ let cleanup: (() => void) | null = null;
40
+ onMounted(() => {
41
+ if (containerRef.value === null) return;
42
+ cleanup = mountAxisLabels({ chart, container: containerRef.value, axis: 'y' });
43
+ });
44
+ onUnmounted(() => {
45
+ cleanup?.();
46
+ cleanup = null;
84
47
  });
85
48
  </script>
86
49
 
87
50
  <template>
88
51
  <div
52
+ ref="containerRef"
89
53
  :style="{
90
54
  position: 'absolute',
91
55
  right: '0',
@@ -94,26 +58,5 @@ const allTicks = computed(() => {
94
58
  width: chart.yAxisWidth + 'px',
95
59
  pointerEvents: 'none',
96
60
  }"
97
- >
98
- <span
99
- v-for="[price, entry] in allTicks"
100
- :key="price"
101
- :style="{
102
- position: 'absolute',
103
- right: '8px',
104
- top: chart.yScale.valueToY(price) + 'px',
105
- transform: 'translateY(-50%)',
106
- color: resolveAxisTextColor(theme, 'y'),
107
- fontSize: resolveAxisFontSize(theme, 'y') + 'px',
108
- fontFamily: theme.typography.fontFamily,
109
- fontVariantNumeric: 'tabular-nums',
110
- userSelect: 'none',
111
- opacity: entry.opacity,
112
- transition: 'opacity 0.25s ease',
113
- willChange: 'opacity',
114
- }"
115
- >
116
- {{ chart.yScale.formatY(price) }}
117
- </span>
118
- </div>
61
+ />
119
62
  </template>