@wick-charts/vue 0.2.2 → 0.2.3
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 +28 -3
- package/src/BarSeries.vue +59 -0
- package/src/CandlestickSeries.vue +71 -0
- package/src/ChartContainer.vue +141 -0
- package/src/LineSeries.vue +59 -0
- package/src/PieSeries.vue +50 -0
- package/src/composables.ts +60 -0
- package/src/context.ts +31 -0
- package/src/index.ts +104 -0
- package/src/shims-vue.d.ts +6 -0
- package/src/ui/Crosshair.vue +60 -0
- package/src/ui/InfoBar.vue +194 -0
- package/src/ui/Legend.vue +302 -0
- package/src/ui/NumberFlow.vue +98 -0
- package/src/ui/PieLegend.vue +126 -0
- package/src/ui/PieTooltip.vue +200 -0
- package/src/ui/TimeAxis.vue +92 -0
- package/src/ui/Title.vue +57 -0
- package/src/ui/Tooltip.vue +260 -0
- package/src/ui/YAxis.vue +100 -0
- package/src/ui/YLabel.vue +194 -0
package/package.json
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wick-charts/vue",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "High-performance canvas timeseries charts for Vue — candlestick, line, bar, pie. Tree-shakeable, zero runtime deps.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/mo4islona/wick-charts.git",
|
|
9
|
+
"directory": "packages/vue"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://mo4islona.github.io/wick-charts/",
|
|
12
|
+
"bugs": "https://github.com/mo4islona/wick-charts/issues",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"charts",
|
|
15
|
+
"charting",
|
|
16
|
+
"canvas",
|
|
17
|
+
"candlestick",
|
|
18
|
+
"line-chart",
|
|
19
|
+
"bar-chart",
|
|
20
|
+
"pie-chart",
|
|
21
|
+
"timeseries",
|
|
22
|
+
"vue",
|
|
23
|
+
"visualization"
|
|
24
|
+
],
|
|
4
25
|
"type": "module",
|
|
5
26
|
"module": "./dist/index.js",
|
|
6
27
|
"types": "./dist/index.d.ts",
|
|
@@ -11,7 +32,11 @@
|
|
|
11
32
|
}
|
|
12
33
|
},
|
|
13
34
|
"files": [
|
|
14
|
-
"dist"
|
|
35
|
+
"dist",
|
|
36
|
+
"src",
|
|
37
|
+
"LICENSE",
|
|
38
|
+
"!src/__tests__",
|
|
39
|
+
"!src/**/*.test.*"
|
|
15
40
|
],
|
|
16
41
|
"publishConfig": {
|
|
17
42
|
"access": "public"
|
|
@@ -20,7 +45,7 @@
|
|
|
20
45
|
"vue": ">=3.3.0"
|
|
21
46
|
},
|
|
22
47
|
"devDependencies": {
|
|
23
|
-
"@wick-charts/core": "^0.2.
|
|
48
|
+
"@wick-charts/core": "^0.2.3"
|
|
24
49
|
},
|
|
25
50
|
"scripts": {
|
|
26
51
|
"build": "vite build"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { BarSeriesOptions, TimePoint } from '@wick-charts/core';
|
|
3
|
+
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
|
4
|
+
|
|
5
|
+
import { useChartInstance } from './context';
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
data: TimePoint[][];
|
|
9
|
+
options?: Partial<BarSeriesOptions>;
|
|
10
|
+
/** Stable series ID — same value across remounts. */
|
|
11
|
+
id?: string;
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
const chart = useChartInstance();
|
|
15
|
+
const seriesId = ref<string | null>(null);
|
|
16
|
+
|
|
17
|
+
onMounted(() => {
|
|
18
|
+
const id = chart.addBarSeries({
|
|
19
|
+
...props.options,
|
|
20
|
+
layers: props.data.length,
|
|
21
|
+
id: props.id,
|
|
22
|
+
});
|
|
23
|
+
seriesId.value = id;
|
|
24
|
+
// 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
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
onUnmounted(() => {
|
|
33
|
+
if (seriesId.value) chart.removeSeries(seriesId.value);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
watch(
|
|
37
|
+
() => props.data,
|
|
38
|
+
(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
|
+
});
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
watch(
|
|
49
|
+
() => props.options,
|
|
50
|
+
(options) => {
|
|
51
|
+
if (seriesId.value && options) {
|
|
52
|
+
chart.updateSeriesOptions(seriesId.value, options);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{ deep: true },
|
|
56
|
+
);
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<template><span v-if="false" /></template>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CandlestickSeriesOptions, OHLCInput } from '@wick-charts/core';
|
|
3
|
+
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
|
4
|
+
|
|
5
|
+
import { useChartInstance } from './context';
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
data: OHLCInput[];
|
|
9
|
+
options?: Partial<CandlestickSeriesOptions>;
|
|
10
|
+
/** Stable series ID — same value across remounts. */
|
|
11
|
+
id?: string;
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
const chart = useChartInstance();
|
|
15
|
+
const seriesId = ref<string | null>(null);
|
|
16
|
+
let prevLen = 0;
|
|
17
|
+
|
|
18
|
+
onMounted(() => {
|
|
19
|
+
const id = chart.addCandlestickSeries({ ...props.options, id: props.id });
|
|
20
|
+
seriesId.value = id;
|
|
21
|
+
// Initial data load — Vue's `watch` is lazy by default, so the watcher
|
|
22
|
+
// below only fires on subsequent `data` prop mutations. Explicitly apply
|
|
23
|
+
// 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
|
+
});
|
|
29
|
+
|
|
30
|
+
onUnmounted(() => {
|
|
31
|
+
if (seriesId.value) chart.removeSeries(seriesId.value);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
watch(
|
|
35
|
+
() => props.data,
|
|
36
|
+
(data) => {
|
|
37
|
+
const id = seriesId.value;
|
|
38
|
+
if (!id) return;
|
|
39
|
+
|
|
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;
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
watch(
|
|
61
|
+
() => props.options,
|
|
62
|
+
(options) => {
|
|
63
|
+
if (seriesId.value && options) {
|
|
64
|
+
chart.updateSeriesOptions(seriesId.value, options);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{ deep: true },
|
|
68
|
+
);
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<template><span v-if="false" /></template>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { type AxisConfig, ChartInstance, type ChartTheme, darkTheme } from '@wick-charts/core';
|
|
3
|
+
import { computed, nextTick, onMounted, onUnmounted, provide, ref, shallowRef, watch } from 'vue';
|
|
4
|
+
|
|
5
|
+
import { ChartKey, InfoBarAnchorKey, LegendAnchorKey, LegendRightAnchorKey, ThemeKey, TitleAnchorKey } from './context';
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(
|
|
8
|
+
defineProps<{
|
|
9
|
+
theme?: ChartTheme;
|
|
10
|
+
axis?: AxisConfig;
|
|
11
|
+
/** Background grid configuration. Default: `{ visible: true }`. */
|
|
12
|
+
grid?: { visible: boolean };
|
|
13
|
+
}>(),
|
|
14
|
+
{
|
|
15
|
+
theme: () => darkTheme,
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const containerRef = ref<HTMLDivElement>();
|
|
20
|
+
const chart = shallowRef<ChartInstance | null>(null);
|
|
21
|
+
const themeRef = shallowRef<ChartTheme>(props.theme);
|
|
22
|
+
|
|
23
|
+
// Anchors for hoisted overlays. Title / InfoBar teleport into a top
|
|
24
|
+
// overlay anchor that is positioned *absolute* at the top of the canvas
|
|
25
|
+
// block, so the canvas (and its grid) spans the full container height. The
|
|
26
|
+
// combined anchor height is measured and fed into `chart.setPadding({ top })`
|
|
27
|
+
// so data stays below the header.
|
|
28
|
+
const topOverlayRef = ref<HTMLElement | null>(null);
|
|
29
|
+
const titleAnchor = ref<HTMLElement | null>(null);
|
|
30
|
+
const infoBarAnchor = ref<HTMLElement | null>(null);
|
|
31
|
+
const legendAnchor = ref<HTMLElement | null>(null);
|
|
32
|
+
const legendRightAnchor = ref<HTMLElement | null>(null);
|
|
33
|
+
|
|
34
|
+
provide(ChartKey, chart);
|
|
35
|
+
provide(ThemeKey, themeRef);
|
|
36
|
+
provide(TitleAnchorKey, titleAnchor);
|
|
37
|
+
provide(InfoBarAnchorKey, infoBarAnchor);
|
|
38
|
+
provide(LegendAnchorKey, legendAnchor);
|
|
39
|
+
provide(LegendRightAnchorKey, legendRightAnchor);
|
|
40
|
+
|
|
41
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
42
|
+
let topOverlayHeight = 0;
|
|
43
|
+
|
|
44
|
+
function applyPadding() {
|
|
45
|
+
if (!chart.value) return;
|
|
46
|
+
chart.value.setPadding({ top: 20 + topOverlayHeight });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
onMounted(async () => {
|
|
50
|
+
if (!containerRef.value) return;
|
|
51
|
+
const options: any = {};
|
|
52
|
+
if (props.axis) options.axis = props.axis;
|
|
53
|
+
if (props.theme) options.theme = props.theme;
|
|
54
|
+
if (props.grid !== undefined) options.grid = props.grid;
|
|
55
|
+
chart.value = new ChartInstance(containerRef.value, options);
|
|
56
|
+
|
|
57
|
+
await nextTick();
|
|
58
|
+
if (!topOverlayRef.value) return;
|
|
59
|
+
const measure = () => {
|
|
60
|
+
topOverlayHeight = topOverlayRef.value?.getBoundingClientRect().height ?? 0;
|
|
61
|
+
applyPadding();
|
|
62
|
+
};
|
|
63
|
+
measure();
|
|
64
|
+
resizeObserver = new ResizeObserver(measure);
|
|
65
|
+
resizeObserver.observe(topOverlayRef.value);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
onUnmounted(() => {
|
|
69
|
+
resizeObserver?.disconnect();
|
|
70
|
+
resizeObserver = null;
|
|
71
|
+
chart.value?.destroy();
|
|
72
|
+
chart.value = null;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
watch(
|
|
76
|
+
() => props.theme,
|
|
77
|
+
(newTheme) => {
|
|
78
|
+
if (chart.value && newTheme) {
|
|
79
|
+
chart.value.setTheme(newTheme);
|
|
80
|
+
themeRef.value = newTheme;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
watch(
|
|
86
|
+
() => props.axis,
|
|
87
|
+
(newAxis) => {
|
|
88
|
+
if (chart.value && newAxis) chart.value.setAxis(newAxis);
|
|
89
|
+
},
|
|
90
|
+
{ deep: true },
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
watch(
|
|
94
|
+
() => props.grid?.visible,
|
|
95
|
+
() => {
|
|
96
|
+
if (chart.value && props.grid !== undefined) chart.value.setGrid(props.grid);
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const rootStyle = computed(() => {
|
|
101
|
+
const t = themeRef.value;
|
|
102
|
+
const [gtop, gbot] = t?.chartGradient ?? ['transparent', 'transparent'];
|
|
103
|
+
const bg = t?.background ?? 'transparent';
|
|
104
|
+
return (
|
|
105
|
+
'position: relative; display: flex; flex-direction: column; width: 100%; height: 100%; overflow: hidden; ' +
|
|
106
|
+
`background: linear-gradient(to bottom, ${gtop} 0%, ${bg} 70%, ${gbot} 100%)`
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<template>
|
|
112
|
+
<div :style="rootStyle">
|
|
113
|
+
<div style="display: flex; flex-direction: row; flex: 1; min-height: 0">
|
|
114
|
+
<div
|
|
115
|
+
ref="containerRef"
|
|
116
|
+
style="position: relative; flex: 1; min-width: 0; min-height: 0; overflow: hidden"
|
|
117
|
+
>
|
|
118
|
+
<!-- Top overlay (Title + InfoBar) — stacked absolute inside the
|
|
119
|
+
canvas block so the grid extends full-height behind them. -->
|
|
120
|
+
<div
|
|
121
|
+
ref="topOverlayRef"
|
|
122
|
+
data-chart-top-overlay=""
|
|
123
|
+
style="position: absolute; top: 0; left: 0; right: 0; z-index: 2; pointer-events: none; display: flex; flex-direction: column"
|
|
124
|
+
>
|
|
125
|
+
<div ref="titleAnchor" data-chart-title-anchor="" />
|
|
126
|
+
<div ref="infoBarAnchor" data-tooltip-legend-anchor="" />
|
|
127
|
+
</div>
|
|
128
|
+
<div
|
|
129
|
+
v-if="chart"
|
|
130
|
+
data-chart-series-overlay=""
|
|
131
|
+
style="position: absolute; inset: 0; pointer-events: none; z-index: 3"
|
|
132
|
+
>
|
|
133
|
+
<slot />
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<div ref="legendRightAnchor" data-legend-right-anchor="" style="flex: 0 0 auto" />
|
|
137
|
+
</div>
|
|
138
|
+
<div ref="legendAnchor" data-legend-anchor="" style="flex: 0 0 auto" />
|
|
139
|
+
</div>
|
|
140
|
+
</template>
|
|
141
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { LineSeriesOptions, TimePoint } from '@wick-charts/core';
|
|
3
|
+
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
|
4
|
+
|
|
5
|
+
import { useChartInstance } from './context';
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
data: TimePoint[][];
|
|
9
|
+
options?: Partial<LineSeriesOptions>;
|
|
10
|
+
/** Stable series ID — same value across remounts. */
|
|
11
|
+
id?: string;
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
const chart = useChartInstance();
|
|
15
|
+
const seriesId = ref<string | null>(null);
|
|
16
|
+
|
|
17
|
+
onMounted(() => {
|
|
18
|
+
const id = chart.addLineSeries({
|
|
19
|
+
...props.options,
|
|
20
|
+
layers: props.data.length,
|
|
21
|
+
id: props.id,
|
|
22
|
+
});
|
|
23
|
+
seriesId.value = id;
|
|
24
|
+
// 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
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
onUnmounted(() => {
|
|
33
|
+
if (seriesId.value) chart.removeSeries(seriesId.value);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
watch(
|
|
37
|
+
() => props.data,
|
|
38
|
+
(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
|
+
});
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
watch(
|
|
49
|
+
() => props.options,
|
|
50
|
+
(options) => {
|
|
51
|
+
if (seriesId.value && options) {
|
|
52
|
+
chart.updateSeriesOptions(seriesId.value, options);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{ deep: true },
|
|
56
|
+
);
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<template><span v-if="false" /></template>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PieSeriesOptions, PieSliceData } from '@wick-charts/core';
|
|
3
|
+
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
|
4
|
+
|
|
5
|
+
import { useChartInstance } from './context';
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
data: PieSliceData[];
|
|
9
|
+
options?: Partial<PieSeriesOptions>;
|
|
10
|
+
/** Stable series ID — same value across remounts. */
|
|
11
|
+
id?: string;
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
const chart = useChartInstance();
|
|
15
|
+
const seriesId = ref<string | null>(null);
|
|
16
|
+
|
|
17
|
+
onMounted(() => {
|
|
18
|
+
const id = chart.addPieSeries({ ...props.options, id: props.id });
|
|
19
|
+
seriesId.value = id;
|
|
20
|
+
// Lazy watcher — apply initial data here so static-data mounts render without a no-op first frame.
|
|
21
|
+
if (props.data.length > 0) {
|
|
22
|
+
chart.setSeriesData(id, props.data);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
onUnmounted(() => {
|
|
27
|
+
if (seriesId.value) chart.removeSeries(seriesId.value);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
watch(
|
|
31
|
+
() => props.data,
|
|
32
|
+
(data) => {
|
|
33
|
+
if (seriesId.value && data.length > 0) {
|
|
34
|
+
chart.setSeriesData(seriesId.value, data);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
watch(
|
|
40
|
+
() => props.options,
|
|
41
|
+
(options) => {
|
|
42
|
+
if (seriesId.value && options) {
|
|
43
|
+
chart.updateSeriesOptions(seriesId.value, options);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{ deep: true },
|
|
47
|
+
);
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<template><span v-if="false" /></template>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ChartInstance, CrosshairPosition, VisibleRange, YRange } from '@wick-charts/core';
|
|
2
|
+
import { type Ref, onMounted, onUnmounted, ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
export function useVisibleRange(chart: ChartInstance): Ref<VisibleRange> {
|
|
5
|
+
const range = ref<VisibleRange>(chart.getVisibleRange()) as Ref<VisibleRange>;
|
|
6
|
+
const handler = () => {
|
|
7
|
+
range.value = chart.getVisibleRange();
|
|
8
|
+
};
|
|
9
|
+
onMounted(() => chart.on('viewportChange', handler));
|
|
10
|
+
onUnmounted(() => chart.off('viewportChange', handler));
|
|
11
|
+
return range;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useYRange(chart: ChartInstance): Ref<YRange> {
|
|
15
|
+
const range = ref<YRange>(chart.getYRange()) as Ref<YRange>;
|
|
16
|
+
const handler = () => {
|
|
17
|
+
range.value = chart.getYRange();
|
|
18
|
+
};
|
|
19
|
+
onMounted(() => chart.on('viewportChange', handler));
|
|
20
|
+
onUnmounted(() => chart.off('viewportChange', handler));
|
|
21
|
+
return range;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useLastYValue(chart: ChartInstance, seriesId: string): Ref<{ value: number; isLive: boolean } | null> {
|
|
25
|
+
const val = ref<{ value: number; isLive: boolean } | null>(chart.getLastValue(seriesId));
|
|
26
|
+
const handler = () => {
|
|
27
|
+
const next = chart.getLastValue(seriesId);
|
|
28
|
+
if (val.value?.value === next?.value && val.value?.isLive === next?.isLive) return;
|
|
29
|
+
val.value = next;
|
|
30
|
+
};
|
|
31
|
+
onMounted(() => {
|
|
32
|
+
chart.on('dataUpdate', handler);
|
|
33
|
+
chart.on('viewportChange', handler);
|
|
34
|
+
});
|
|
35
|
+
onUnmounted(() => {
|
|
36
|
+
chart.off('dataUpdate', handler);
|
|
37
|
+
chart.off('viewportChange', handler);
|
|
38
|
+
});
|
|
39
|
+
return val;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function usePreviousClose(chart: ChartInstance, seriesId: string): Ref<number | null> {
|
|
43
|
+
const price = ref<number | null>(chart.getPreviousClose(seriesId));
|
|
44
|
+
const handler = () => {
|
|
45
|
+
price.value = chart.getPreviousClose(seriesId);
|
|
46
|
+
};
|
|
47
|
+
onMounted(() => chart.on('dataUpdate', handler));
|
|
48
|
+
onUnmounted(() => chart.off('dataUpdate', handler));
|
|
49
|
+
return price;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function useCrosshairPosition(chart: ChartInstance): Ref<CrosshairPosition | null> {
|
|
53
|
+
const pos = ref<CrosshairPosition | null>(chart.getCrosshairPosition()) as Ref<CrosshairPosition | null>;
|
|
54
|
+
const handler = () => {
|
|
55
|
+
pos.value = chart.getCrosshairPosition();
|
|
56
|
+
};
|
|
57
|
+
onMounted(() => chart.on('crosshairMove', handler));
|
|
58
|
+
onUnmounted(() => chart.off('crosshairMove', handler));
|
|
59
|
+
return pos;
|
|
60
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ChartInstance, ChartTheme } from '@wick-charts/core';
|
|
2
|
+
import { type InjectionKey, type Ref, type ShallowRef, inject } from 'vue';
|
|
3
|
+
|
|
4
|
+
export const ChartKey: InjectionKey<ShallowRef<ChartInstance | null>> = Symbol('wick-chart');
|
|
5
|
+
export const ThemeKey: InjectionKey<ShallowRef<ChartTheme>> = Symbol('wick-theme');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* DOM anchor for a hoisted overlay component (Title, InfoBar, Legend).
|
|
9
|
+
* The child component teleports its output into the anchor the container
|
|
10
|
+
* provides, so the chart owns its own header / info-bar / footer layout.
|
|
11
|
+
*/
|
|
12
|
+
export const TitleAnchorKey: InjectionKey<Ref<HTMLElement | null>> = Symbol('wick-title-anchor');
|
|
13
|
+
export const InfoBarAnchorKey: InjectionKey<Ref<HTMLElement | null>> = Symbol('wick-info-bar-anchor');
|
|
14
|
+
export const LegendAnchorKey: InjectionKey<Ref<HTMLElement | null>> = Symbol('wick-legend-anchor');
|
|
15
|
+
export const LegendRightAnchorKey: InjectionKey<Ref<HTMLElement | null>> = Symbol('wick-legend-right-anchor');
|
|
16
|
+
|
|
17
|
+
export function useChartInstance(): ChartInstance {
|
|
18
|
+
const chartRef = inject(ChartKey);
|
|
19
|
+
if (!chartRef || !chartRef.value) {
|
|
20
|
+
throw new Error('useChartInstance must be used within <ChartContainer>');
|
|
21
|
+
}
|
|
22
|
+
return chartRef.value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useTheme(): ShallowRef<ChartTheme> {
|
|
26
|
+
const themeRef = inject(ThemeKey);
|
|
27
|
+
if (!themeRef) {
|
|
28
|
+
throw new Error('useTheme must be used within <ChartContainer>');
|
|
29
|
+
}
|
|
30
|
+
return themeRef;
|
|
31
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Re-export core
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
AxisBound,
|
|
5
|
+
AxisConfig,
|
|
6
|
+
BarSeriesOptions,
|
|
7
|
+
/** @deprecated Use {@link StackingMode} instead. */
|
|
8
|
+
BarStacking,
|
|
9
|
+
BuildHoverSnapshotsArgs,
|
|
10
|
+
BuildLastSnapshotsArgs,
|
|
11
|
+
CandlestickSeriesOptions,
|
|
12
|
+
ChartLayout,
|
|
13
|
+
ChartOptions,
|
|
14
|
+
ChartTheme,
|
|
15
|
+
CrosshairPosition,
|
|
16
|
+
HoverInfo,
|
|
17
|
+
LegendItem,
|
|
18
|
+
/** @deprecated Use {@link TimePoint} instead. */
|
|
19
|
+
LineData,
|
|
20
|
+
LineSeriesOptions,
|
|
21
|
+
OHLCData,
|
|
22
|
+
OHLCInput,
|
|
23
|
+
PieSeriesOptions,
|
|
24
|
+
PieSliceData,
|
|
25
|
+
SeriesSnapshot,
|
|
26
|
+
SeriesType,
|
|
27
|
+
SliceInfo,
|
|
28
|
+
SnapshotSort,
|
|
29
|
+
StackingMode,
|
|
30
|
+
ThemeConfig,
|
|
31
|
+
ThemePreset,
|
|
32
|
+
TimePoint,
|
|
33
|
+
TimePointInput,
|
|
34
|
+
TimeValue,
|
|
35
|
+
TooltipField,
|
|
36
|
+
TooltipFormatter,
|
|
37
|
+
TooltipPosition,
|
|
38
|
+
TooltipPositionArgs,
|
|
39
|
+
Typography,
|
|
40
|
+
ValueFormatter,
|
|
41
|
+
VisibleRange,
|
|
42
|
+
XAxisConfig,
|
|
43
|
+
YAxisConfig,
|
|
44
|
+
YRange,
|
|
45
|
+
} from '@wick-charts/core';
|
|
46
|
+
export {
|
|
47
|
+
ChartInstance,
|
|
48
|
+
andromeda,
|
|
49
|
+
ayuMirage,
|
|
50
|
+
buildHoverSnapshots,
|
|
51
|
+
buildLastSnapshots,
|
|
52
|
+
catppuccin,
|
|
53
|
+
computeTooltipPosition,
|
|
54
|
+
createTheme,
|
|
55
|
+
darkTheme,
|
|
56
|
+
detectInterval,
|
|
57
|
+
dracula,
|
|
58
|
+
formatCompact,
|
|
59
|
+
formatDate,
|
|
60
|
+
formatPriceAdaptive,
|
|
61
|
+
formatTime,
|
|
62
|
+
githubLight,
|
|
63
|
+
gruvbox,
|
|
64
|
+
handwritten,
|
|
65
|
+
highContrast,
|
|
66
|
+
lavenderMist,
|
|
67
|
+
lightPink,
|
|
68
|
+
lightTheme,
|
|
69
|
+
materialPalenight,
|
|
70
|
+
minimalLight,
|
|
71
|
+
mintBreeze,
|
|
72
|
+
monokaiPro,
|
|
73
|
+
nightOwl,
|
|
74
|
+
normalizeTime,
|
|
75
|
+
oneDarkPro,
|
|
76
|
+
panda,
|
|
77
|
+
peachCream,
|
|
78
|
+
quietLight,
|
|
79
|
+
rosePineDawn,
|
|
80
|
+
sandDune,
|
|
81
|
+
solarizedLight,
|
|
82
|
+
} from '@wick-charts/core';
|
|
83
|
+
|
|
84
|
+
export { default as BarSeries } from './BarSeries.vue';
|
|
85
|
+
export { default as CandlestickSeries } from './CandlestickSeries.vue';
|
|
86
|
+
// Vue components
|
|
87
|
+
export { default as ChartContainer } from './ChartContainer.vue';
|
|
88
|
+
export { useCrosshairPosition, useLastYValue, usePreviousClose, useVisibleRange, useYRange } from './composables';
|
|
89
|
+
// Composables and context
|
|
90
|
+
export { useChartInstance, useTheme } from './context';
|
|
91
|
+
export { default as LineSeries } from './LineSeries.vue';
|
|
92
|
+
export { default as PieSeries } from './PieSeries.vue';
|
|
93
|
+
export { default as Crosshair } from './ui/Crosshair.vue';
|
|
94
|
+
export { default as InfoBar } from './ui/InfoBar.vue';
|
|
95
|
+
export { default as Legend } from './ui/Legend.vue';
|
|
96
|
+
export { default as NumberFlow } from './ui/NumberFlow.vue';
|
|
97
|
+
export { default as PieLegend } from './ui/PieLegend.vue';
|
|
98
|
+
export { default as PieTooltip } from './ui/PieTooltip.vue';
|
|
99
|
+
export { default as TimeAxis } from './ui/TimeAxis.vue';
|
|
100
|
+
export { default as Title } from './ui/Title.vue';
|
|
101
|
+
// UI overlays
|
|
102
|
+
export { default as Tooltip } from './ui/Tooltip.vue';
|
|
103
|
+
export { default as YAxis } from './ui/YAxis.vue';
|
|
104
|
+
export { default as YLabel } from './ui/YLabel.vue';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { formatTime } from '@wick-charts/core';
|
|
3
|
+
import { computed } from 'vue';
|
|
4
|
+
|
|
5
|
+
import { useCrosshairPosition } from '../composables';
|
|
6
|
+
import { useChartInstance } from '../context';
|
|
7
|
+
|
|
8
|
+
const chart = useChartInstance();
|
|
9
|
+
const position = useCrosshairPosition(chart);
|
|
10
|
+
|
|
11
|
+
const theme = computed(() => chart.getTheme());
|
|
12
|
+
const dataInterval = computed(() => chart.getDataInterval());
|
|
13
|
+
|
|
14
|
+
const labelStyle = computed(() => ({
|
|
15
|
+
// Blend the theme's labelBackground at 80% opacity so the axis grid
|
|
16
|
+
// shows through — keeps the badge from looking like an opaque block
|
|
17
|
+
// while still masking whatever tick sits underneath.
|
|
18
|
+
background: `color-mix(in srgb, ${theme.value.crosshair.labelBackground} 80%, transparent)`,
|
|
19
|
+
color: theme.value.crosshair.labelTextColor,
|
|
20
|
+
fontSize: theme.value.typography.axisFontSize + 'px',
|
|
21
|
+
fontFamily: theme.value.typography.fontFamily,
|
|
22
|
+
fontVariantNumeric: 'tabular-nums' as const,
|
|
23
|
+
padding: '2px 6px',
|
|
24
|
+
borderRadius: '2px',
|
|
25
|
+
whiteSpace: 'nowrap' as const,
|
|
26
|
+
pointerEvents: 'none' as const,
|
|
27
|
+
// Sit above axis ticks (z:0) but below the YLabel badge (z:3) so the
|
|
28
|
+
// live last-value stays visible when the crosshair crosses its row.
|
|
29
|
+
zIndex: 2,
|
|
30
|
+
}));
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<template v-if="position">
|
|
35
|
+
<!-- Y label on right axis -->
|
|
36
|
+
<div
|
|
37
|
+
:style="{
|
|
38
|
+
position: 'absolute',
|
|
39
|
+
right: '0',
|
|
40
|
+
top: position.mediaY + 'px',
|
|
41
|
+
transform: 'translateY(-50%)',
|
|
42
|
+
...labelStyle,
|
|
43
|
+
}"
|
|
44
|
+
>
|
|
45
|
+
{{ chart.yScale.formatY(position.y) }}
|
|
46
|
+
</div>
|
|
47
|
+
<!-- Time label on bottom axis -->
|
|
48
|
+
<div
|
|
49
|
+
:style="{
|
|
50
|
+
position: 'absolute',
|
|
51
|
+
bottom: '0',
|
|
52
|
+
left: position.mediaX + 'px',
|
|
53
|
+
transform: 'translateX(-50%)',
|
|
54
|
+
...labelStyle,
|
|
55
|
+
}"
|
|
56
|
+
>
|
|
57
|
+
{{ formatTime(position.time, dataInterval) }}
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
</template>
|