@wick-charts/vue 0.8.0 → 0.8.2
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 +2 -0
- package/dist/BandSeries.vue.js +43 -32
- package/dist/BarSeries.vue.js +44 -33
- package/dist/CandlestickSeries.vue.js +24 -23
- package/dist/ChartContainer.vue.js +1 -1
- package/dist/HeatmapSeries.vue.js +21 -20
- package/dist/LineSeries.vue.js +43 -32
- package/dist/PieSeries.vue.js +25 -24
- package/dist/composables.js +63 -36
- package/dist/core/dist/axis/dom-labels.js +64 -53
- package/dist/core/dist/chart/badge-animator.js +38 -28
- package/dist/core/dist/chart/options.js +1 -1
- package/dist/core/dist/chart/y-target.js +38 -32
- package/dist/core/dist/chart.js +852 -709
- package/dist/core/dist/data/decimation.js +52 -51
- package/dist/core/dist/data/store.js +38 -20
- package/dist/core/dist/data/sync.js +97 -59
- package/dist/core/dist/navigator/controller.js +11 -11
- package/dist/core/dist/perf/perf-hud.js +3 -3
- package/dist/core/dist/scales/tick-tracker.js +2 -1
- package/dist/core/dist/scales/time-ticks.js +99 -0
- package/dist/core/dist/scales/x-scale.js +33 -27
- package/dist/core/dist/series/band.js +83 -83
- package/dist/core/dist/series/bar.js +240 -211
- package/dist/core/dist/series/base-multi-layer.js +288 -240
- package/dist/core/dist/series/candlestick.js +168 -168
- package/dist/core/dist/series/line.js +545 -455
- package/dist/core/dist/snapshots.js +109 -86
- package/dist/core/dist/utils/point.js +4 -0
- package/dist/core/dist/utils/time.js +42 -26
- package/dist/index.d.ts +67 -28
- package/dist/ui/InfoBar.vue.js +69 -69
- package/dist/ui/NumberFlow.vue.js +36 -27
- package/dist/ui/Tooltip.vue.js +128 -131
- package/dist/ui/YLabel.vue.js +47 -47
- package/package.json +2 -2
- package/src/BandSeries.vue +20 -5
- package/src/BarSeries.vue +20 -5
- package/src/CandlestickSeries.vue +5 -4
- package/src/ChartContainer.vue +15 -9
- package/src/HeatmapSeries.vue +5 -4
- package/src/LineSeries.vue +20 -5
- package/src/PieSeries.vue +6 -5
- package/src/composables.ts +64 -1
- package/src/ui/InfoBar.vue +11 -3
- package/src/ui/NumberFlow.vue +17 -3
- package/src/ui/Tooltip.vue +23 -12
- package/src/ui/YLabel.vue +28 -27
package/src/BarSeries.vue
CHANGED
|
@@ -3,6 +3,7 @@ import type { BarSeriesOptions, MultiLayerData, SeriesSyncState } from '@wick-ch
|
|
|
3
3
|
import { BarSeriesDef, EMPTY_SYNC_STATE, syncLayers, toLayers } from '@wick-charts/core';
|
|
4
4
|
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
|
5
5
|
|
|
6
|
+
import { rawSeriesData, seriesDataSource } from './composables';
|
|
6
7
|
import { useChartInstance } from './context';
|
|
7
8
|
|
|
8
9
|
const props = withDefaults(
|
|
@@ -32,13 +33,14 @@ const seriesId = ref<string | null>(null);
|
|
|
32
33
|
// reconciliation (plus per-layer labels/colors) so streaming eases instead of
|
|
33
34
|
// snapping Y every tick.
|
|
34
35
|
let prevSync: SeriesSyncState[] = [];
|
|
36
|
+
let layerCount = 0;
|
|
35
37
|
|
|
36
38
|
function applyData(id: string, data: MultiLayerData): void {
|
|
37
39
|
prevSync = syncLayers({ chart, id, data, prev: prevSync });
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
function createSeries(data: MultiLayerData): void {
|
|
43
|
+
layerCount = toLayers(data).length;
|
|
42
44
|
const id = chart.addSeries(BarSeriesDef, {
|
|
43
45
|
...props.options,
|
|
44
46
|
scale: props.scale,
|
|
@@ -48,8 +50,12 @@ onMounted(() => {
|
|
|
48
50
|
seriesId.value = id;
|
|
49
51
|
prevSync = new Array(layerCount).fill(EMPTY_SYNC_STATE);
|
|
50
52
|
// Lazy watcher — apply initial data here so static-data mounts render without a no-op first frame.
|
|
51
|
-
applyData(id,
|
|
53
|
+
applyData(id, data);
|
|
52
54
|
chart.setSeriesVisible(id, props.visible);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onMounted(() => {
|
|
58
|
+
createSeries(rawSeriesData(props.data));
|
|
53
59
|
});
|
|
54
60
|
|
|
55
61
|
onUnmounted(() => {
|
|
@@ -57,11 +63,20 @@ onUnmounted(() => {
|
|
|
57
63
|
});
|
|
58
64
|
|
|
59
65
|
watch(
|
|
60
|
-
() => props.data,
|
|
61
|
-
(
|
|
66
|
+
seriesDataSource(() => props.data),
|
|
67
|
+
() => {
|
|
68
|
+
const data = rawSeriesData(props.data);
|
|
62
69
|
const id = seriesId.value;
|
|
63
70
|
if (!id) return;
|
|
64
71
|
|
|
72
|
+
// The layer count is fixed when a series is created; a new count re-creates it, as in React.
|
|
73
|
+
if (toLayers(data).length !== layerCount) {
|
|
74
|
+
chart.removeSeries(id);
|
|
75
|
+
createSeries(data);
|
|
76
|
+
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
65
80
|
applyData(id, data);
|
|
66
81
|
},
|
|
67
82
|
);
|
|
@@ -3,6 +3,7 @@ import type { CandlestickData, CandlestickSeriesOptions, OHLCInput, SeriesSyncSt
|
|
|
3
3
|
import { CandlestickSeriesDef, EMPTY_SYNC_STATE, syncSeriesLayer, toLayers } from '@wick-charts/core';
|
|
4
4
|
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
|
5
5
|
|
|
6
|
+
import { rawSeriesData, seriesDataSource } from './composables';
|
|
6
7
|
import { useChartInstance } from './context';
|
|
7
8
|
|
|
8
9
|
const props = withDefaults(
|
|
@@ -47,7 +48,7 @@ onMounted(() => {
|
|
|
47
48
|
// Initial data load — Vue's `watch` is lazy by default, so the watcher
|
|
48
49
|
// below only fires on subsequent `data` prop mutations. Explicitly apply
|
|
49
50
|
// the first value here so components with static data render immediately.
|
|
50
|
-
applyData(id, props.data);
|
|
51
|
+
applyData(id, rawSeriesData(props.data));
|
|
51
52
|
chart.setSeriesVisible(id, props.visible);
|
|
52
53
|
});
|
|
53
54
|
|
|
@@ -56,12 +57,12 @@ onUnmounted(() => {
|
|
|
56
57
|
});
|
|
57
58
|
|
|
58
59
|
watch(
|
|
59
|
-
() => props.data,
|
|
60
|
-
(
|
|
60
|
+
seriesDataSource(() => props.data),
|
|
61
|
+
() => {
|
|
61
62
|
const id = seriesId.value;
|
|
62
63
|
if (!id) return;
|
|
63
64
|
|
|
64
|
-
applyData(id, data);
|
|
65
|
+
applyData(id, rawSeriesData(props.data));
|
|
65
66
|
},
|
|
66
67
|
);
|
|
67
68
|
|
package/src/ChartContainer.vue
CHANGED
|
@@ -48,7 +48,7 @@ const props = withDefaults(
|
|
|
48
48
|
* Viewport padding. `top`/`bottom` are in pixels. `left`/`right` accept either pixels (`50`)
|
|
49
49
|
* or data intervals (`{ intervals: 3 }`). Set to 0 for edge-to-edge sparklines. Updates are
|
|
50
50
|
* applied reactively when this prop or the measured header height changes.
|
|
51
|
-
* Defaults: `{ top: 20, bottom: 20, right: { intervals: 3 }, left: { intervals: 0 } }`.
|
|
51
|
+
* Defaults: `{ top: 20, bottom: 20, right: { intervals: 3 }, left: { intervals: 0.5 } }`.
|
|
52
52
|
*/
|
|
53
53
|
padding?: {
|
|
54
54
|
top?: number;
|
|
@@ -62,19 +62,23 @@ const props = withDefaults(
|
|
|
62
62
|
*/
|
|
63
63
|
viewport?: {
|
|
64
64
|
/**
|
|
65
|
-
*
|
|
66
|
-
* to
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* Default: 200.
|
|
65
|
+
* Most bars the chart's own fit shows. The first data load fits every bar
|
|
66
|
+
* up to this count; while the data holds fewer, streamed bars widen the
|
|
67
|
+
* window, and once it is full the window keeps its width and follows the
|
|
68
|
+
* latest bar. `fitContent()` and an explicit bar count are not capped.
|
|
69
|
+
* Default: 200. Values below 2 are clamped.
|
|
71
70
|
*/
|
|
72
71
|
maxVisibleBars?: number;
|
|
73
72
|
/**
|
|
74
73
|
* Initial visible range applied before the first paint with data. Same
|
|
75
74
|
* shape as the imperative `chart.setVisibleRange` — pass a bar count
|
|
76
75
|
* (e.g. `35`), an explicit `{from, to}` window, or `{from, bars}` for
|
|
77
|
-
* a warm-up pair.
|
|
76
|
+
* a warm-up pair.
|
|
77
|
+
*
|
|
78
|
+
* It stays the chart's window until the user zooms or pans or the app
|
|
79
|
+
* sets a range, so a series that loads later or data set again keeps it.
|
|
80
|
+
* A bar count widens with streamed bars until it holds that many; a
|
|
81
|
+
* `{from, …}` window ends once streaming moves it. Captured at mount.
|
|
78
82
|
*/
|
|
79
83
|
initialRange?: VisibleRangeSpec;
|
|
80
84
|
};
|
|
@@ -86,7 +90,7 @@ const props = withDefaults(
|
|
|
86
90
|
* two-way binding; a same-value literal (compared structurally) is a
|
|
87
91
|
* no-op, so an inline object doesn't re-apply on every render.
|
|
88
92
|
*
|
|
89
|
-
* For
|
|
93
|
+
* For an initial window that isn't re-applied on every prop
|
|
90
94
|
* change, use `viewport.initialRange` instead.
|
|
91
95
|
*/
|
|
92
96
|
visibleRange?: VisibleRangeSpec;
|
|
@@ -211,6 +215,8 @@ const props = withDefaults(
|
|
|
211
215
|
// distinguishable from an explicit `:fade="false"` all-off opt-out.
|
|
212
216
|
// Declaring a default (even `undefined`) suppresses the cast.
|
|
213
217
|
fade: undefined,
|
|
218
|
+
// Absent must reach the core as `undefined` (animations on), as in React and Svelte.
|
|
219
|
+
animations: undefined,
|
|
214
220
|
},
|
|
215
221
|
);
|
|
216
222
|
|
package/src/HeatmapSeries.vue
CHANGED
|
@@ -3,6 +3,7 @@ import type { HeatmapCellData, HeatmapSeriesOptions } from '@wick-charts/core';
|
|
|
3
3
|
import { HeatmapSeriesDef } from '@wick-charts/core';
|
|
4
4
|
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
|
5
5
|
|
|
6
|
+
import { rawSeriesData, seriesDataSource } from './composables';
|
|
6
7
|
import { useChartInstance } from './context';
|
|
7
8
|
|
|
8
9
|
const props = withDefaults(
|
|
@@ -28,7 +29,7 @@ onMounted(() => {
|
|
|
28
29
|
seriesId.value = id;
|
|
29
30
|
// Lazy watcher — apply initial data here so static-data mounts render without a no-op first frame.
|
|
30
31
|
if (props.data.length > 0) {
|
|
31
|
-
chart.setSeriesData(id, props.data);
|
|
32
|
+
chart.setSeriesData(id, rawSeriesData(props.data));
|
|
32
33
|
}
|
|
33
34
|
chart.setSeriesVisible(id, props.visible);
|
|
34
35
|
});
|
|
@@ -38,12 +39,12 @@ onUnmounted(() => {
|
|
|
38
39
|
});
|
|
39
40
|
|
|
40
41
|
watch(
|
|
41
|
-
() => props.data,
|
|
42
|
-
(
|
|
42
|
+
seriesDataSource(() => props.data),
|
|
43
|
+
() => {
|
|
43
44
|
// No length guard: an empty array is a legitimate update (filter yields
|
|
44
45
|
// no rows) and must clear the previous cells, matching the React wrapper.
|
|
45
46
|
if (seriesId.value) {
|
|
46
|
-
chart.setSeriesData(seriesId.value, data);
|
|
47
|
+
chart.setSeriesData(seriesId.value, rawSeriesData(props.data));
|
|
47
48
|
}
|
|
48
49
|
},
|
|
49
50
|
);
|
package/src/LineSeries.vue
CHANGED
|
@@ -3,6 +3,7 @@ import type { LineSeriesOptions, MultiLayerData, SeriesSyncState } from '@wick-c
|
|
|
3
3
|
import { EMPTY_SYNC_STATE, LineSeriesDef, syncLayers, toLayers } from '@wick-charts/core';
|
|
4
4
|
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
|
5
5
|
|
|
6
|
+
import { rawSeriesData, seriesDataSource } from './composables';
|
|
6
7
|
import { useChartInstance } from './context';
|
|
7
8
|
|
|
8
9
|
const props = withDefaults(
|
|
@@ -32,13 +33,14 @@ const seriesId = ref<string | null>(null);
|
|
|
32
33
|
// reconciliation (plus per-layer labels/colors) so streaming eases instead of
|
|
33
34
|
// snapping Y every tick.
|
|
34
35
|
let prevSync: SeriesSyncState[] = [];
|
|
36
|
+
let layerCount = 0;
|
|
35
37
|
|
|
36
38
|
function applyData(id: string, data: MultiLayerData): void {
|
|
37
39
|
prevSync = syncLayers({ chart, id, data, prev: prevSync });
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
function createSeries(data: MultiLayerData): void {
|
|
43
|
+
layerCount = toLayers(data).length;
|
|
42
44
|
const id = chart.addSeries(LineSeriesDef, {
|
|
43
45
|
...props.options,
|
|
44
46
|
scale: props.scale,
|
|
@@ -48,8 +50,12 @@ onMounted(() => {
|
|
|
48
50
|
seriesId.value = id;
|
|
49
51
|
prevSync = new Array(layerCount).fill(EMPTY_SYNC_STATE);
|
|
50
52
|
// Lazy watcher — apply initial data here so static-data mounts render without a no-op first frame.
|
|
51
|
-
applyData(id,
|
|
53
|
+
applyData(id, data);
|
|
52
54
|
chart.setSeriesVisible(id, props.visible);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onMounted(() => {
|
|
58
|
+
createSeries(rawSeriesData(props.data));
|
|
53
59
|
});
|
|
54
60
|
|
|
55
61
|
onUnmounted(() => {
|
|
@@ -57,11 +63,20 @@ onUnmounted(() => {
|
|
|
57
63
|
});
|
|
58
64
|
|
|
59
65
|
watch(
|
|
60
|
-
() => props.data,
|
|
61
|
-
(
|
|
66
|
+
seriesDataSource(() => props.data),
|
|
67
|
+
() => {
|
|
68
|
+
const data = rawSeriesData(props.data);
|
|
62
69
|
const id = seriesId.value;
|
|
63
70
|
if (!id) return;
|
|
64
71
|
|
|
72
|
+
// The layer count is fixed when a series is created; a new count re-creates it, as in React.
|
|
73
|
+
if (toLayers(data).length !== layerCount) {
|
|
74
|
+
chart.removeSeries(id);
|
|
75
|
+
createSeries(data);
|
|
76
|
+
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
65
80
|
applyData(id, data);
|
|
66
81
|
},
|
|
67
82
|
);
|
package/src/PieSeries.vue
CHANGED
|
@@ -3,6 +3,7 @@ import type { PieSeriesOptions, PieSliceData } from '@wick-charts/core';
|
|
|
3
3
|
import { PieSeriesDef } from '@wick-charts/core';
|
|
4
4
|
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
|
5
5
|
|
|
6
|
+
import { rawSeriesData, seriesDataSource } from './composables';
|
|
6
7
|
import { useChartInstance } from './context';
|
|
7
8
|
|
|
8
9
|
const props = withDefaults(
|
|
@@ -28,7 +29,7 @@ onMounted(() => {
|
|
|
28
29
|
seriesId.value = id;
|
|
29
30
|
// Lazy watcher — apply initial data here so static-data mounts render without a no-op first frame.
|
|
30
31
|
if (props.data.length > 0) {
|
|
31
|
-
chart.setSeriesData(id, props.data);
|
|
32
|
+
chart.setSeriesData(id, rawSeriesData(props.data));
|
|
32
33
|
}
|
|
33
34
|
chart.setSeriesVisible(id, props.visible);
|
|
34
35
|
});
|
|
@@ -38,10 +39,10 @@ onUnmounted(() => {
|
|
|
38
39
|
});
|
|
39
40
|
|
|
40
41
|
watch(
|
|
41
|
-
() => props.data,
|
|
42
|
-
(
|
|
43
|
-
if (seriesId.value && data.length > 0) {
|
|
44
|
-
chart.setSeriesData(seriesId.value, data);
|
|
42
|
+
seriesDataSource(() => props.data),
|
|
43
|
+
() => {
|
|
44
|
+
if (seriesId.value && props.data.length > 0) {
|
|
45
|
+
chart.setSeriesData(seriesId.value, rawSeriesData(props.data));
|
|
45
46
|
}
|
|
46
47
|
},
|
|
47
48
|
);
|
package/src/composables.ts
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
import type { ChartInstance, CrosshairPosition, VisibleRange, YAxisPosition, YRange } from '@wick-charts/core';
|
|
2
|
-
import { type Ref, onMounted, onUnmounted, ref } from 'vue';
|
|
2
|
+
import { type Ref, onMounted, onUnmounted, ref, toRaw } from 'vue';
|
|
3
|
+
|
|
4
|
+
/** A `{ label?, color?, data }` layer — an object carrying a `data` array. */
|
|
5
|
+
function isLayerObject(value: unknown): value is { data: unknown[] } {
|
|
6
|
+
return (
|
|
7
|
+
typeof value === 'object' && value !== null && !Array.isArray(value) && Array.isArray(Reflect.get(value, 'data'))
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Each layer's point array, read through whatever Proxy the prop arrived in. */
|
|
12
|
+
function layerArrays(data: unknown): unknown[][] {
|
|
13
|
+
if (isLayerObject(data)) return [data.data];
|
|
14
|
+
if (!Array.isArray(data)) return [];
|
|
15
|
+
|
|
16
|
+
const layered = Array.isArray(data[0]) || isLayerObject(data[0]);
|
|
17
|
+
if (!layered) return [data];
|
|
18
|
+
|
|
19
|
+
return data.map((layer) => (isLayerObject(layer) ? layer.data : (layer as unknown[])));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A series `data` prop with Vue's reactive Proxies taken off the top level and
|
|
24
|
+
* each layer, so the chart reads plain arrays: a `ref` / `reactive` array of
|
|
25
|
+
* 100k points read through its Proxy costs the engine several times more.
|
|
26
|
+
*/
|
|
27
|
+
export function rawSeriesData<D>(data: D): D {
|
|
28
|
+
const raw = toRaw(data);
|
|
29
|
+
const rawLayer = (layer: unknown) => {
|
|
30
|
+
if (!isLayerObject(layer)) return toRaw(layer);
|
|
31
|
+
|
|
32
|
+
const plain = toRaw(layer);
|
|
33
|
+
|
|
34
|
+
return { ...plain, data: toRaw(plain.data) };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (!Array.isArray(raw)) return rawLayer(raw) as D;
|
|
38
|
+
|
|
39
|
+
const layered = Array.isArray(raw[0]) || isLayerObject(raw[0]);
|
|
40
|
+
if (!layered) return raw;
|
|
41
|
+
|
|
42
|
+
return raw.map(rawLayer) as D;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Watch source for a series `data` prop. Besides a new array, it re-fires on
|
|
47
|
+
* an in-place `push` / `splice` or an edit of a layer's last point on a
|
|
48
|
+
* reactive array — it reads each layer's length and last point through the
|
|
49
|
+
* Proxy instead of deep-watching every point. A `shallowRef` still needs a
|
|
50
|
+
* new array (or `triggerRef`).
|
|
51
|
+
*/
|
|
52
|
+
export function seriesDataSource(getData: () => unknown): () => unknown[] {
|
|
53
|
+
return () => {
|
|
54
|
+
const data = getData();
|
|
55
|
+
const deps: unknown[] = [data];
|
|
56
|
+
for (const points of layerArrays(data)) {
|
|
57
|
+
deps.push(points, points.length);
|
|
58
|
+
|
|
59
|
+
const last = points[points.length - 1];
|
|
60
|
+
if (typeof last === 'object' && last !== null) deps.push(...Object.values(last));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return deps;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
3
66
|
|
|
4
67
|
export function useVisibleRange(chart: ChartInstance): Ref<VisibleRange> {
|
|
5
68
|
const range = ref<VisibleRange>(chart.getVisibleRange()) as Ref<VisibleRange>;
|
package/src/ui/InfoBar.vue
CHANGED
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
type OHLCData,
|
|
5
5
|
type SeriesSnapshot,
|
|
6
6
|
type SnapshotSort,
|
|
7
|
-
type TimePoint,
|
|
8
7
|
type TooltipFormatter,
|
|
9
8
|
buildHoverSnapshots,
|
|
10
9
|
buildLastSnapshots,
|
|
11
10
|
formatCompact,
|
|
12
11
|
formatHeaderX,
|
|
13
12
|
formatPriceAdaptive,
|
|
13
|
+
formatRowValue,
|
|
14
14
|
resolveCandlestickBodyColor,
|
|
15
15
|
} from '@wick-charts/core';
|
|
16
16
|
import { computed, inject, onMounted, onUnmounted, ref, useSlots } from 'vue';
|
|
@@ -56,12 +56,15 @@ const onOverlayChange = () => {
|
|
|
56
56
|
};
|
|
57
57
|
onMounted(() => {
|
|
58
58
|
chart.on('overlayChange', onOverlayChange);
|
|
59
|
+
// Rows read animated values (smoothing, fades, percent shares).
|
|
60
|
+
chart.on('seriesFrame', onOverlayChange);
|
|
59
61
|
// Catch-up: a sibling series' mount effect may have registered data in the
|
|
60
62
|
// same synchronous flush, so poke the computed now.
|
|
61
63
|
if (chart.getSeriesIds().length > 0) bump.value++;
|
|
62
64
|
});
|
|
63
65
|
onUnmounted(() => {
|
|
64
66
|
chart.off('overlayChange', onOverlayChange);
|
|
67
|
+
chart.off('seriesFrame', onOverlayChange);
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
// Hover-over-the-y-axis gap: the overlay canvas includes the y-axis strip,
|
|
@@ -89,7 +92,12 @@ const snapshots = computed<readonly SeriesSnapshot[]>(() =>
|
|
|
89
92
|
isHover.value ? (hoverSnapshots.value as readonly SeriesSnapshot[]) : lastSnapshots.value,
|
|
90
93
|
);
|
|
91
94
|
|
|
92
|
-
|
|
95
|
+
// `chart` is not reactive: `bump` re-reads the interval once data lands after mount.
|
|
96
|
+
const dataInterval = computed(() => {
|
|
97
|
+
void bump.value;
|
|
98
|
+
|
|
99
|
+
return chart.getDataInterval();
|
|
100
|
+
});
|
|
93
101
|
const displayTime = computed(() => {
|
|
94
102
|
if (snapshots.value.length === 0) return 0;
|
|
95
103
|
if (isHover.value && crosshair.value) return crosshair.value.time;
|
|
@@ -205,7 +213,7 @@ function isBand(data: SeriesSnapshot['data']): data is BandPoint {
|
|
|
205
213
|
}"
|
|
206
214
|
/>
|
|
207
215
|
<span :style="{ color: s.color, fontWeight: 500 }">
|
|
208
|
-
{{
|
|
216
|
+
{{ formatRowValue(s, (v) => effectiveFormat(v, 'value')) }}
|
|
209
217
|
</span>
|
|
210
218
|
</span>
|
|
211
219
|
</template>
|
package/src/ui/NumberFlow.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted, ref } from 'vue';
|
|
2
|
+
import { computed, onMounted, ref, watch } from 'vue';
|
|
3
3
|
|
|
4
4
|
const props = withDefaults(
|
|
5
5
|
defineProps<{
|
|
@@ -11,6 +11,7 @@ const props = withDefaults(
|
|
|
11
11
|
*/
|
|
12
12
|
format?: ((value: number) => string) | Intl.NumberFormatOptions;
|
|
13
13
|
locale?: string;
|
|
14
|
+
/** Per-digit spin duration in ms. A value that arrives sooner than this after the previous one snaps instead. */
|
|
14
15
|
spinDuration?: number;
|
|
15
16
|
}>(),
|
|
16
17
|
{
|
|
@@ -50,6 +51,19 @@ const parts = computed<CharPart[]>(() => {
|
|
|
50
51
|
return result;
|
|
51
52
|
});
|
|
52
53
|
|
|
54
|
+
// A spin retargeted mid-turn shows two half digits, so values faster than a spin snap.
|
|
55
|
+
const duration = ref(props.spinDuration);
|
|
56
|
+
let changedAt = Number.NEGATIVE_INFINITY;
|
|
57
|
+
watch(
|
|
58
|
+
() => effectiveFormat.value(props.value),
|
|
59
|
+
() => {
|
|
60
|
+
const now = performance.now();
|
|
61
|
+
const tooFast = now - changedAt < props.spinDuration;
|
|
62
|
+
changedAt = now;
|
|
63
|
+
duration.value = tooFast ? 0 : props.spinDuration;
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
53
67
|
const digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
|
54
68
|
</script>
|
|
55
69
|
|
|
@@ -77,8 +91,8 @@ const digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
|
|
77
91
|
display: 'flex',
|
|
78
92
|
flexDirection: 'column',
|
|
79
93
|
transform: 'translateY(' + (-parseInt(part.value) * 1.2) + 'em)',
|
|
80
|
-
transition: mounted
|
|
81
|
-
? 'transform ' +
|
|
94
|
+
transition: mounted && duration > 0
|
|
95
|
+
? 'transform ' + duration + 'ms cubic-bezier(0.16, 1, 0.3, 1)'
|
|
82
96
|
: 'none',
|
|
83
97
|
}"
|
|
84
98
|
>
|
package/src/ui/Tooltip.vue
CHANGED
|
@@ -4,14 +4,13 @@ import {
|
|
|
4
4
|
type OHLCData,
|
|
5
5
|
type SeriesSnapshot,
|
|
6
6
|
type SnapshotSort,
|
|
7
|
-
type TimePoint,
|
|
8
7
|
type TooltipFormatter,
|
|
9
8
|
buildHoverSnapshots,
|
|
10
9
|
computeTooltipPosition,
|
|
11
10
|
formatCompact,
|
|
12
|
-
formatDate,
|
|
13
|
-
formatHeaderX,
|
|
14
11
|
formatPriceAdaptive,
|
|
12
|
+
formatRowValue,
|
|
13
|
+
formatTooltipHeader,
|
|
15
14
|
resolveCandlestickBodyColor,
|
|
16
15
|
} from '@wick-charts/core';
|
|
17
16
|
import { computed, onMounted, onUnmounted, ref, useSlots, watchEffect } from 'vue';
|
|
@@ -47,14 +46,26 @@ const onOverlayChange = () => {
|
|
|
47
46
|
};
|
|
48
47
|
onMounted(() => {
|
|
49
48
|
chart.on('overlayChange', onOverlayChange);
|
|
49
|
+
// Rows read animated values (smoothing, fades, percent shares).
|
|
50
|
+
chart.on('seriesFrame', onOverlayChange);
|
|
50
51
|
if (chart.getSeriesIds().length > 0) bump.value++;
|
|
51
52
|
});
|
|
52
53
|
onUnmounted(() => {
|
|
53
54
|
chart.off('overlayChange', onOverlayChange);
|
|
55
|
+
chart.off('seriesFrame', onOverlayChange);
|
|
54
56
|
});
|
|
55
57
|
|
|
56
|
-
const theme = computed(() =>
|
|
57
|
-
|
|
58
|
+
const theme = computed(() => {
|
|
59
|
+
void bump.value;
|
|
60
|
+
|
|
61
|
+
return chart.getTheme();
|
|
62
|
+
});
|
|
63
|
+
// `chart` is not reactive: `bump` re-reads the interval once data lands after mount.
|
|
64
|
+
const dataInterval = computed(() => {
|
|
65
|
+
void bump.value;
|
|
66
|
+
|
|
67
|
+
return chart.getDataInterval();
|
|
68
|
+
});
|
|
58
69
|
|
|
59
70
|
const snapshots = computed<readonly SeriesSnapshot[]>(() => {
|
|
60
71
|
void bump.value;
|
|
@@ -127,9 +138,12 @@ const floatingPos = computed<{ left: number; top: number } | null>(() => {
|
|
|
127
138
|
|
|
128
139
|
const hasOHLC = snapshots.value.some((s) => 'open' in s.data);
|
|
129
140
|
const lineCount = snapshots.value.filter((s) => !('open' in s.data)).length;
|
|
130
|
-
// A band row carries two numbers,
|
|
141
|
+
// A band row carries two numbers, and a percent-stack row a share and a value, so both need a wider box.
|
|
131
142
|
const hasBand = snapshots.value.some((s) => 'high' in s.data && !('open' in s.data));
|
|
132
|
-
const
|
|
143
|
+
const hasShare = snapshots.value.some((s) => s.share !== undefined);
|
|
144
|
+
let tooltipWidth = 160;
|
|
145
|
+
if (hasBand) tooltipWidth = 220;
|
|
146
|
+
if (hasShare) tooltipWidth = 240;
|
|
133
147
|
const tooltipHeight = hasOHLC ? 140 : 40 + lineCount * 22;
|
|
134
148
|
|
|
135
149
|
return computeTooltipPosition({
|
|
@@ -188,10 +202,7 @@ function isBand(data: SeriesSnapshot['data']): data is BandPoint {
|
|
|
188
202
|
letterSpacing: '0.02em',
|
|
189
203
|
}"
|
|
190
204
|
>
|
|
191
|
-
|
|
192
|
-
{{ formatDate(displayTime, { locale: chart.timeScale.getLocale(), timeZone: chart.timeScale.getTimeZone() }) }}
|
|
193
|
-
</template>
|
|
194
|
-
{{ formatHeaderX(chart, { time: displayTime, snapshots, interval: dataInterval }) }}
|
|
205
|
+
{{ formatTooltipHeader(chart, { time: displayTime, snapshots, interval: dataInterval }) }}
|
|
195
206
|
</div>
|
|
196
207
|
|
|
197
208
|
<template v-for="s in snapshots" :key="s.id">
|
|
@@ -264,7 +275,7 @@ function isBand(data: SeriesSnapshot['data']): data is BandPoint {
|
|
|
264
275
|
/>
|
|
265
276
|
<span :style="{ opacity: 0.6, flex: '1' }">{{ s.label ?? 'Value' }}</span>
|
|
266
277
|
<span :style="{ fontWeight: 600, color: s.color }">
|
|
267
|
-
{{
|
|
278
|
+
{{ formatRowValue(s, (v) => effectiveFormat(v, 'value')) }}
|
|
268
279
|
</span>
|
|
269
280
|
</div>
|
|
270
281
|
</template>
|
package/src/ui/YLabel.vue
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
prefersReducedMotion,
|
|
8
8
|
resolveAnimate,
|
|
9
9
|
resolveYLabelSeriesId,
|
|
10
|
+
yLabelFractionDigits,
|
|
10
11
|
} from '@wick-charts/core';
|
|
11
12
|
import { computed, onMounted, onUnmounted, ref, useSlots } from 'vue';
|
|
12
13
|
|
|
@@ -21,28 +22,32 @@ export type { YLabelAnimate, YLabelAnimateOptions } from '@wick-charts/core';
|
|
|
21
22
|
/** Direction of the current value vs. previous close. Drives the badge color in the default UI. */
|
|
22
23
|
export type YLabelDirection = 'up' | 'down' | 'neutral';
|
|
23
24
|
|
|
24
|
-
const props =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
25
|
+
const props = withDefaults(
|
|
26
|
+
defineProps<{
|
|
27
|
+
/**
|
|
28
|
+
* Owning series id. **Optional** — when omitted, the first visible
|
|
29
|
+
* single-layer time series is picked, falling back to the first visible
|
|
30
|
+
* multi-layer time series.
|
|
31
|
+
*/
|
|
32
|
+
seriesId?: string;
|
|
33
|
+
color?: string;
|
|
34
|
+
/** Custom formatter; routed through NumberFlow's `formatter` so the digit animation still plays. Default: the scale's `<YAxis format>`, else a range-adaptive number. */
|
|
35
|
+
format?: ValueFormatter;
|
|
36
|
+
/**
|
|
37
|
+
* Badge motion. `true` (default) glides + counts up + fades in with the
|
|
38
|
+
* intro; `false` snaps with no easing; an object tunes individual parts.
|
|
39
|
+
*/
|
|
40
|
+
animate?: YLabelAnimate;
|
|
41
|
+
/**
|
|
42
|
+
* Named Y scale (`ChartContainer` `scales`) the value maps through. Default:
|
|
43
|
+
* the series' own scale. The badge sits on that scale's axis side — the
|
|
44
|
+
* default axis side for an overlay scale.
|
|
45
|
+
*/
|
|
46
|
+
scale?: string;
|
|
47
|
+
}>(),
|
|
48
|
+
// Vue casts an absent Boolean prop to `false`; an absent `animate` must stay `undefined` (on).
|
|
49
|
+
{ animate: undefined },
|
|
50
|
+
);
|
|
46
51
|
|
|
47
52
|
defineSlots<{
|
|
48
53
|
default?(ctx: {
|
|
@@ -250,12 +255,8 @@ const bgColor = computed<string>(() => {
|
|
|
250
255
|
const fractionDigits = computed(() => {
|
|
251
256
|
void bump.value;
|
|
252
257
|
const yRange = yScale.value.getRange();
|
|
253
|
-
const range = yRange.max - yRange.min;
|
|
254
|
-
if (range < 0.1) return 6;
|
|
255
|
-
if (range < 10) return 4;
|
|
256
|
-
if (range < 1000) return 2;
|
|
257
258
|
|
|
258
|
-
return
|
|
259
|
+
return yLabelFractionDigits(yRange.max - yRange.min);
|
|
259
260
|
});
|
|
260
261
|
|
|
261
262
|
const effectiveFormat = computed<ValueFormatter>(() => {
|