@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.
Files changed (55) hide show
  1. package/README.md +4 -4
  2. package/dist/BandSeries.vue.js +20 -18
  3. package/dist/BarSeries.vue.js +15 -13
  4. package/dist/CandlestickSeries.vue.js +31 -29
  5. package/dist/ChartContainer.vue.js +77 -70
  6. package/dist/LineSeries.vue.js +19 -17
  7. package/dist/composables.js +46 -36
  8. package/dist/core/dist/animation/viewport-engine.js +155 -96
  9. package/dist/core/dist/axis/dom-labels.js +56 -51
  10. package/dist/core/dist/chart/scale-guards.js +23 -5
  11. package/dist/core/dist/chart/x-guards.js +37 -0
  12. package/dist/core/dist/chart.js +1286 -843
  13. package/dist/core/dist/data/sync.js +37 -31
  14. package/dist/core/dist/navigator/controller.js +37 -37
  15. package/dist/core/dist/perf/perf-hud.js +3 -3
  16. package/dist/core/dist/scales/tick-tracker.js +2 -2
  17. package/dist/core/dist/scales/x-scale.js +167 -53
  18. package/dist/core/dist/scales/y-scale.js +36 -34
  19. package/dist/core/dist/series/band.js +266 -213
  20. package/dist/core/dist/series/bar.js +297 -291
  21. package/dist/core/dist/series/base-multi-layer.js +283 -237
  22. package/dist/core/dist/series/candlestick.js +1 -0
  23. package/dist/core/dist/series/line.js +21 -20
  24. package/dist/core/dist/snapshots.js +55 -43
  25. package/dist/index.d.ts +323 -58
  26. package/dist/index.js +9 -9
  27. package/dist/ui/Crosshair.vue.js +51 -27
  28. package/dist/ui/InfoBar.vue.js +37 -37
  29. package/dist/ui/Marker.vue.js +14 -11
  30. package/dist/ui/Navigator.vue.js +23 -23
  31. package/dist/ui/ReferenceLine.vue.js +19 -16
  32. package/dist/ui/Tooltip.vue.js +91 -86
  33. package/dist/ui/XAxis.vue.js +48 -0
  34. package/dist/ui/XAxis.vue2.js +4 -0
  35. package/dist/ui/YAxis.vue.js +44 -28
  36. package/dist/ui/YLabel.vue.js +78 -74
  37. package/package.json +2 -2
  38. package/src/BandSeries.vue +11 -6
  39. package/src/BarSeries.vue +13 -6
  40. package/src/CandlestickSeries.vue +9 -5
  41. package/src/ChartContainer.vue +20 -0
  42. package/src/LineSeries.vue +13 -6
  43. package/src/composables.ts +23 -4
  44. package/src/index.ts +8 -1
  45. package/src/ui/Crosshair.vue +41 -1
  46. package/src/ui/InfoBar.vue +2 -1
  47. package/src/ui/Marker.vue +5 -1
  48. package/src/ui/Navigator.vue +2 -1
  49. package/src/ui/ReferenceLine.vue +5 -1
  50. package/src/ui/Tooltip.vue +7 -4
  51. package/src/ui/{TimeAxis.vue → XAxis.vue} +18 -5
  52. package/src/ui/YAxis.vue +66 -12
  53. package/src/ui/YLabel.vue +35 -5
  54. package/dist/ui/TimeAxis.vue.js +0 -44
  55. package/dist/ui/TimeAxis.vue2.js +0 -4
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
  );
@@ -10,6 +10,7 @@ import {
10
10
  computeTooltipPosition,
11
11
  formatCompact,
12
12
  formatDate,
13
+ formatHeaderX,
13
14
  formatPriceAdaptive,
14
15
  resolveCandlestickBodyColor,
15
16
  } from '@wick-charts/core';
@@ -106,7 +107,7 @@ const floatingPos = computed<{ left: number; top: number } | null>(() => {
106
107
  if (!crosshair.value || snapshots.value.length === 0) return null;
107
108
  const mediaSize = chart.getMediaSize();
108
109
  const chartLeft = chart.plotLeft;
109
- const chartWidth = mediaSize.width - chart.yAxisWidth;
110
+ const chartWidth = chart.getLayout().chartArea.width;
110
111
  const chartHeight = mediaSize.height - chart.xAxisHeight;
111
112
 
112
113
  if (hasCustomSlot.value) {
@@ -187,8 +188,10 @@ function isBand(data: SeriesSnapshot['data']): data is BandPoint {
187
188
  letterSpacing: '0.02em',
188
189
  }"
189
190
  >
190
- {{ formatDate(displayTime, { locale: chart.timeScale.getLocale(), timeZone: chart.timeScale.getTimeZone() }) }}
191
- {{ 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 }) }}
192
195
  </div>
193
196
 
194
197
  <template v-for="s in snapshots" :key="s.id">
@@ -288,7 +291,7 @@ function isBand(data: SeriesSnapshot['data']): data is BandPoint {
288
291
  fontVariantNumeric: 'tabular-nums',
289
292
  color: theme.tooltip.textColor,
290
293
  width: 'max-content',
291
- maxWidth: (chart.getMediaSize().width - chart.yAxisWidth) + 'px',
294
+ maxWidth: chart.getLayout().chartArea.width + 'px',
292
295
  boxSizing: 'border-box',
293
296
  zIndex: 10,
294
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
@@ -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<{
@@ -169,6 +175,24 @@ const theme = computed(() => {
169
175
  return chart.getTheme();
170
176
  });
171
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
+
172
196
  // Position eases (the glide); the digit value steps discretely (once per real
173
197
  // point, not per frame) so the roll doesn't churn. Before the first rAF frame
174
198
  // lands, fall back to the settled value (hidden if an intro is still sweeping).
@@ -195,7 +219,7 @@ const y = computed(() => {
195
219
  void bump.value;
196
220
  if (last.value === null) return 0;
197
221
 
198
- return chart.yScale.valueToY(positionValue.value);
222
+ return yScale.value.valueToY(positionValue.value);
199
223
  });
200
224
 
201
225
  // Direction / color track the settled last value (not the mid-glide display
@@ -225,7 +249,7 @@ const bgColor = computed<string>(() => {
225
249
 
226
250
  const fractionDigits = computed(() => {
227
251
  void bump.value;
228
- const yRange = chart.yScale.getRange();
252
+ const yRange = yScale.value.getRange();
229
253
  const range = yRange.max - yRange.min;
230
254
  if (range < 0.1) return 6;
231
255
  if (range < 10) return 4;
@@ -236,6 +260,12 @@ const fractionDigits = computed(() => {
236
260
 
237
261
  const effectiveFormat = computed<ValueFormatter>(() => {
238
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
+
239
269
  const nf = new Intl.NumberFormat('en-US', {
240
270
  minimumFractionDigits: fractionDigits.value,
241
271
  maximumFractionDigits: fractionDigits.value,
@@ -263,7 +293,7 @@ const effectiveFormat = computed<ValueFormatter>(() => {
263
293
  :style="{
264
294
  position: 'absolute',
265
295
  left: chart.plotLeft + 'px',
266
- right: (chart.yAxisPosition === 'left' ? 0 : chart.yAxisWidth) + 'px',
296
+ right: chart.getAxisColumnWidth('right') + 'px',
267
297
  top: y + 'px',
268
298
  height: '0',
269
299
  borderTop: '1px dashed ' + bgColor,
@@ -275,7 +305,7 @@ const effectiveFormat = computed<ValueFormatter>(() => {
275
305
  <div
276
306
  :style="{
277
307
  position: 'absolute',
278
- [chart.yAxisPosition]: '4px',
308
+ [side]: '4px',
279
309
  top: y + 'px',
280
310
  transform: 'translateY(-50%)',
281
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
- };