@wick-charts/vue 0.6.2 → 0.6.4
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 -2
- package/dist/BandSeries.vue.js +50 -0
- package/dist/BandSeries.vue2.js +4 -0
- package/dist/ChartContainer.vue.js +45 -39
- package/dist/composables.js +39 -26
- package/dist/core/dist/animation/config.js +92 -75
- package/dist/core/dist/axis/dom-labels.js +56 -43
- package/dist/core/dist/chart/badge-animator.js +30 -22
- package/dist/core/dist/chart/edge-indicators.js +22 -21
- package/dist/core/dist/chart/last-value.js +33 -26
- package/dist/core/dist/chart/options.js +5 -5
- package/dist/core/dist/chart/scale-guards.js +26 -0
- package/dist/core/dist/chart/y-target.js +46 -19
- package/dist/core/dist/chart.js +670 -518
- package/dist/core/dist/components/crosshair.js +8 -8
- package/dist/core/dist/components/grid.js +26 -17
- package/dist/core/dist/components/marker.js +28 -28
- package/dist/core/dist/components/reference-line.js +16 -16
- package/dist/core/dist/data/decimation.js +58 -39
- package/dist/core/dist/interactions/handler.js +3 -2
- package/dist/core/dist/interactions/zoom.js +6 -5
- package/dist/core/dist/navigator/controller.js +47 -44
- package/dist/core/dist/perf/perf-hud.js +3 -3
- package/dist/core/dist/scales/log-scale.js +76 -0
- package/dist/core/dist/scales/y-scale.js +134 -35
- package/dist/core/dist/series/band.js +670 -0
- package/dist/core/dist/series/bar-intro.js +11 -11
- package/dist/core/dist/series/bar.js +301 -251
- package/dist/core/dist/series/base-multi-layer.js +100 -76
- package/dist/core/dist/series/candlestick.js +238 -235
- package/dist/core/dist/series/heatmap.js +5 -5
- package/dist/core/dist/series/line-intro.js +50 -40
- package/dist/core/dist/series/line.js +561 -483
- package/dist/core/dist/series/painters/builtins.js +4 -4
- package/dist/core/dist/series/painters/canvas-path.js +64 -52
- package/dist/core/dist/series/register-builtins.js +9 -8
- package/dist/core/dist/series/scale-stack.js +25 -0
- package/dist/core/dist/snapshots.js +36 -35
- package/dist/core/dist/tooltip-position.js +5 -5
- package/dist/core/dist/utils/point.js +10 -0
- package/dist/index.d.ts +501 -128
- package/dist/index.js +195 -189
- package/dist/ui/Crosshair.vue.js +14 -14
- package/dist/ui/HeatmapTooltip.vue.js +6 -6
- package/dist/ui/InfoBar.vue.js +100 -81
- package/dist/ui/PieTooltip.vue.js +4 -4
- package/dist/ui/TimeAxis.vue.js +23 -22
- package/dist/ui/Tooltip.vue.js +140 -111
- package/dist/ui/YAxis.vue.js +25 -24
- package/dist/ui/YLabel.vue.js +63 -70
- package/package.json +2 -2
- package/src/BandSeries.vue +87 -0
- package/src/ChartContainer.vue +20 -8
- package/src/composables.ts +35 -1
- package/src/index.ts +13 -0
- package/src/ui/Crosshair.vue +2 -2
- package/src/ui/InfoBar.vue +20 -1
- package/src/ui/TimeAxis.vue +7 -4
- package/src/ui/Tooltip.vue +40 -3
- package/src/ui/YAxis.vue +5 -3
- package/src/ui/YLabel.vue +5 -16
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { BandData, BandSeriesOptions, SeriesSyncState } from '@wick-charts/core';
|
|
3
|
+
import { BandSeriesDef, EMPTY_SYNC_STATE, syncLayers, toLayers } from '@wick-charts/core';
|
|
4
|
+
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
|
5
|
+
|
|
6
|
+
import { useChartInstance } from './context';
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(
|
|
9
|
+
defineProps<{
|
|
10
|
+
/**
|
|
11
|
+
* Band data — one band per layer. A single band passes a flat
|
|
12
|
+
* `{ time, low, high }[]`; several bands pass `BandPoint[][]`. To name/color
|
|
13
|
+
* a band, pass `{ label, color?, data }` (or an array of them). Time accepts
|
|
14
|
+
* ms or `Date`.
|
|
15
|
+
*/
|
|
16
|
+
data: BandData;
|
|
17
|
+
/** Visual options override — fill, edge stroke, curve, entrance / intro / smoothing. */
|
|
18
|
+
options?: Partial<BandSeriesOptions>;
|
|
19
|
+
/** Stable series ID — same value across remounts. */
|
|
20
|
+
id?: string;
|
|
21
|
+
/** Show/hide the series without unmounting it — excludes it from the Y-range fit and tooltip/legend. Default `true`. Live. */
|
|
22
|
+
visible?: boolean;
|
|
23
|
+
}>(),
|
|
24
|
+
// Vue's type-based `defineProps` casts an absent Boolean-typed prop to
|
|
25
|
+
// `false` (not `undefined`) without an explicit default — see the
|
|
26
|
+
// `interactive` fix in ChartContainer.vue for the full explanation.
|
|
27
|
+
{ visible: true },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const chart = useChartInstance();
|
|
31
|
+
const seriesId = ref<string | null>(null);
|
|
32
|
+
// Per-layer sync state — drives the shared append/keepLast/update/replace
|
|
33
|
+
// reconciliation (plus per-layer labels/colors) so streaming eases instead of
|
|
34
|
+
// snapping Y every tick.
|
|
35
|
+
let prevSync: SeriesSyncState[] = [];
|
|
36
|
+
|
|
37
|
+
function applyData(id: string, data: BandData): void {
|
|
38
|
+
prevSync = syncLayers({ chart, id, data, prev: prevSync });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onMounted(() => {
|
|
42
|
+
const layerCount = toLayers(props.data).length;
|
|
43
|
+
const id = chart.addSeries(BandSeriesDef, {
|
|
44
|
+
...props.options,
|
|
45
|
+
layers: layerCount,
|
|
46
|
+
id: props.id,
|
|
47
|
+
});
|
|
48
|
+
seriesId.value = id;
|
|
49
|
+
prevSync = new Array(layerCount).fill(EMPTY_SYNC_STATE);
|
|
50
|
+
// Lazy watcher — apply initial data here so static-data mounts render without a no-op first frame.
|
|
51
|
+
applyData(id, props.data);
|
|
52
|
+
chart.setSeriesVisible(id, props.visible);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
onUnmounted(() => {
|
|
56
|
+
if (seriesId.value) chart.removeSeries(seriesId.value);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
watch(
|
|
60
|
+
() => props.data,
|
|
61
|
+
(data) => {
|
|
62
|
+
const id = seriesId.value;
|
|
63
|
+
if (!id) return;
|
|
64
|
+
|
|
65
|
+
applyData(id, data);
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
watch(
|
|
70
|
+
() => props.options,
|
|
71
|
+
(options) => {
|
|
72
|
+
if (seriesId.value && options) {
|
|
73
|
+
chart.updateSeriesOptions(seriesId.value, options);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{ deep: true },
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
watch(
|
|
80
|
+
() => props.visible,
|
|
81
|
+
(visible) => {
|
|
82
|
+
if (seriesId.value) chart.setSeriesVisible(seriesId.value, visible);
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<template><span v-if="false" /></template>
|
package/src/ChartContainer.vue
CHANGED
|
@@ -81,7 +81,7 @@ const props = withDefaults(
|
|
|
81
81
|
* change, use `viewport.initialRange` instead.
|
|
82
82
|
*/
|
|
83
83
|
visibleRange?: VisibleRangeSpec;
|
|
84
|
-
/** Show the chart background gradient. Live. Defaults to
|
|
84
|
+
/** Show the chart background gradient. Live. Defaults to false. */
|
|
85
85
|
gradient?: boolean;
|
|
86
86
|
/**
|
|
87
87
|
* Enable zoom, pan, and crosshair interactions. Defaults to true.
|
|
@@ -96,16 +96,20 @@ const props = withDefaults(
|
|
|
96
96
|
* Soft fade-out at the top of the plot area, so series content dissolves
|
|
97
97
|
* under a floating `<Title>` / `<InfoBar>` instead of colliding with it.
|
|
98
98
|
* The mask erases canvas alpha rather than painting a cover color, so it
|
|
99
|
-
* stays correct over
|
|
99
|
+
* stays correct over a background gradient. Live.
|
|
100
100
|
*
|
|
101
101
|
* Two directions, independently controlled:
|
|
102
102
|
*
|
|
103
103
|
* **X (under the axis)** — panning content slides under the Y-axis
|
|
104
104
|
* column and dissolves instead of hard-clipping at the pane edge. **On
|
|
105
105
|
* by default** as a 60px ramp that finishes just inside the axis
|
|
106
|
-
* column, before the label glyphs
|
|
107
|
-
*
|
|
108
|
-
*
|
|
106
|
+
* column, before the label glyphs — on whichever side
|
|
107
|
+
* `axis.y.position` puts it (a left-axis ramp ends at the pane edge
|
|
108
|
+
* instead). The axis side's key (`{ right }` by default, `{ left }` for
|
|
109
|
+
* a left axis) overrides the total ramp width in CSS px (`0` disables);
|
|
110
|
+
* the other key adds a plain zone at the opposite pane edge (default
|
|
111
|
+
* off). A chart with only pie / heatmap series has no axis column, so
|
|
112
|
+
* both keys add plain zones there.
|
|
109
113
|
*
|
|
110
114
|
* **Top (under the header)** — opt-in. `true` enables the auto zone
|
|
111
115
|
* (measured header + 24px run-out, half the header fold-in released so
|
|
@@ -185,7 +189,7 @@ const props = withDefaults(
|
|
|
185
189
|
}>(),
|
|
186
190
|
{
|
|
187
191
|
theme: () => catppuccin.theme,
|
|
188
|
-
gradient:
|
|
192
|
+
gradient: false,
|
|
189
193
|
// Vue's type-based `defineProps` casts an absent Boolean-typed prop to
|
|
190
194
|
// `false` (not `undefined`) — without this explicit default, every chart
|
|
191
195
|
// that didn't pass `:interactive="true"` silently got zero pan/zoom/
|
|
@@ -229,6 +233,13 @@ provide(NavigatorAnchorKey, navigatorAnchor);
|
|
|
229
233
|
let resizeObserver: ResizeObserver | null = null;
|
|
230
234
|
const topOverlayHeight = ref(0);
|
|
231
235
|
|
|
236
|
+
// Watching `chart` hands a rebuilt instance the current height too.
|
|
237
|
+
watch(
|
|
238
|
+
() => [chart.value, props.headerLayout === 'overlay' ? topOverlayHeight.value : 0] as const,
|
|
239
|
+
([instance, height]) => instance?.setHeaderHeight(height),
|
|
240
|
+
{ immediate: true },
|
|
241
|
+
);
|
|
242
|
+
|
|
232
243
|
/** Run-out below the measured header (CSS px) for the auto `fade` zone —
|
|
233
244
|
* content starts dissolving this far before it slides under the header. */
|
|
234
245
|
const FADE_AUTO_BAND = 24;
|
|
@@ -310,8 +321,9 @@ function applyFade() {
|
|
|
310
321
|
return;
|
|
311
322
|
}
|
|
312
323
|
|
|
313
|
-
// Omitting
|
|
314
|
-
// explicit value (or the all-off branch
|
|
324
|
+
// Omitting a side keeps the core's auto default (the under-the-axis ramp
|
|
325
|
+
// on the Y-axis side); only an explicit value (or the all-off branch
|
|
326
|
+
// above) overrides it.
|
|
315
327
|
const next: FadeConfig = { top: resolvedFadeTop.value };
|
|
316
328
|
if (typeof props.fade === 'object') {
|
|
317
329
|
if (props.fade.right !== undefined) next.right = props.fade.right;
|
package/src/composables.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChartInstance, CrosshairPosition, VisibleRange, YRange } from '@wick-charts/core';
|
|
1
|
+
import type { ChartInstance, CrosshairPosition, VisibleRange, YAxisPosition, YRange } from '@wick-charts/core';
|
|
2
2
|
import { type Ref, onMounted, onUnmounted, ref } from 'vue';
|
|
3
3
|
|
|
4
4
|
export function useVisibleRange(chart: ChartInstance): Ref<VisibleRange> {
|
|
@@ -21,6 +21,40 @@ export function useYRange(chart: ChartInstance): Ref<YRange> {
|
|
|
21
21
|
return range;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export interface AxisLayout {
|
|
25
|
+
yAxisPosition: YAxisPosition;
|
|
26
|
+
yAxisWidth: number;
|
|
27
|
+
xAxisHeight: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readAxisLayout(chart: ChartInstance): AxisLayout {
|
|
31
|
+
return { yAxisPosition: chart.yAxisPosition, yAxisWidth: chart.yAxisWidth, xAxisHeight: chart.xAxisHeight };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The axis gutters overlays anchor to — refreshed on a Y-axis side or gutter size change. */
|
|
35
|
+
export function useAxisLayout(chart: ChartInstance): Ref<AxisLayout> {
|
|
36
|
+
const layout = ref<AxisLayout>(readAxisLayout(chart)) as Ref<AxisLayout>;
|
|
37
|
+
const handler = () => {
|
|
38
|
+
const next = readAxisLayout(chart);
|
|
39
|
+
const prev = layout.value;
|
|
40
|
+
const unchanged =
|
|
41
|
+
prev.yAxisPosition === next.yAxisPosition &&
|
|
42
|
+
prev.yAxisWidth === next.yAxisWidth &&
|
|
43
|
+
prev.xAxisHeight === next.xAxisHeight;
|
|
44
|
+
if (unchanged) return;
|
|
45
|
+
|
|
46
|
+
layout.value = next;
|
|
47
|
+
};
|
|
48
|
+
onMounted(() => {
|
|
49
|
+
// The axis watcher may have landed a new config between setup and mount.
|
|
50
|
+
handler();
|
|
51
|
+
chart.on('viewportChange', handler);
|
|
52
|
+
});
|
|
53
|
+
onUnmounted(() => chart.off('viewportChange', handler));
|
|
54
|
+
|
|
55
|
+
return layout;
|
|
56
|
+
}
|
|
57
|
+
|
|
24
58
|
export function useLastYValue(chart: ChartInstance, seriesId: string): Ref<{ value: number; isLive: boolean } | null> {
|
|
25
59
|
let snapshot = chart.getLastValue(seriesId);
|
|
26
60
|
// Track the pixel Y the snapshot maps to so a pan/zoom/resize that leaves the
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,11 @@ export type {
|
|
|
13
13
|
AnimationsConfig,
|
|
14
14
|
AxisBound,
|
|
15
15
|
AxisConfig,
|
|
16
|
+
BandData,
|
|
17
|
+
BandEntryAnimation,
|
|
18
|
+
BandPoint,
|
|
19
|
+
BandPointInput,
|
|
20
|
+
BandSeriesOptions,
|
|
16
21
|
BarElementTransform,
|
|
17
22
|
BarEntryAnimation,
|
|
18
23
|
BarIntroDirectives,
|
|
@@ -51,6 +56,7 @@ export type {
|
|
|
51
56
|
HeatmapSeriesOptions,
|
|
52
57
|
HeatmapSeriesRenderer,
|
|
53
58
|
HoverInfo,
|
|
59
|
+
LayerSnapshot,
|
|
54
60
|
LegendItem,
|
|
55
61
|
LineIntroDirectives,
|
|
56
62
|
LineIntroFn,
|
|
@@ -61,6 +67,7 @@ export type {
|
|
|
61
67
|
LineSeriesOptions,
|
|
62
68
|
LoadingIndicatorArgs,
|
|
63
69
|
LoadingIndicatorFn,
|
|
70
|
+
LogScaleOptions,
|
|
64
71
|
MarkerConfig,
|
|
65
72
|
MarkerShape,
|
|
66
73
|
MultiLayerData,
|
|
@@ -86,6 +93,7 @@ export type {
|
|
|
86
93
|
ReferenceLineStyle,
|
|
87
94
|
RenderPadding,
|
|
88
95
|
RoundedRectArgs,
|
|
96
|
+
ScaleTickArgs,
|
|
89
97
|
SeriesCreateEnv,
|
|
90
98
|
SeriesDefinition,
|
|
91
99
|
SeriesHoverInfo,
|
|
@@ -128,10 +136,13 @@ export type {
|
|
|
128
136
|
WaveIntroTransform,
|
|
129
137
|
XAxisConfig,
|
|
130
138
|
YAxisConfig,
|
|
139
|
+
YAxisPosition,
|
|
131
140
|
YRange,
|
|
141
|
+
YScaleTransform,
|
|
132
142
|
} from '@wick-charts/core';
|
|
133
143
|
// Painters — custom per-element drawing for bar / candle / line series.
|
|
134
144
|
export {
|
|
145
|
+
BandSeriesDef,
|
|
135
146
|
BarSeriesDef,
|
|
136
147
|
CandlestickSeriesDef,
|
|
137
148
|
ChartInstance,
|
|
@@ -172,6 +183,7 @@ export {
|
|
|
172
183
|
isTimeSeriesRenderer,
|
|
173
184
|
lavenderMist,
|
|
174
185
|
lightPink,
|
|
186
|
+
logScale,
|
|
175
187
|
materialPalenight,
|
|
176
188
|
minimalLight,
|
|
177
189
|
mintBreeze,
|
|
@@ -214,6 +226,7 @@ export {
|
|
|
214
226
|
wipeIntro,
|
|
215
227
|
} from '@wick-charts/core';
|
|
216
228
|
|
|
229
|
+
export { default as BandSeries } from './BandSeries.vue';
|
|
217
230
|
export { default as BarSeries } from './BarSeries.vue';
|
|
218
231
|
export { default as CandlestickSeries } from './CandlestickSeries.vue';
|
|
219
232
|
// Vue components
|
package/src/ui/Crosshair.vue
CHANGED
|
@@ -56,11 +56,11 @@ const labelStyle = computed(() => ({
|
|
|
56
56
|
|
|
57
57
|
<template>
|
|
58
58
|
<template v-if="position">
|
|
59
|
-
<!-- Y label on
|
|
59
|
+
<!-- Y label on the value axis -->
|
|
60
60
|
<div
|
|
61
61
|
:style="{
|
|
62
62
|
position: 'absolute',
|
|
63
|
-
|
|
63
|
+
[chart.yAxisPosition]: '0',
|
|
64
64
|
top: position.mediaY + 'px',
|
|
65
65
|
transform: 'translateY(-50%)',
|
|
66
66
|
...labelStyle,
|
package/src/ui/InfoBar.vue
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {
|
|
3
|
+
type BandPoint,
|
|
3
4
|
type OHLCData,
|
|
4
5
|
type SeriesSnapshot,
|
|
5
6
|
type SnapshotSort,
|
|
@@ -98,9 +99,13 @@ const displayTime = computed(() => {
|
|
|
98
99
|
const slots = useSlots();
|
|
99
100
|
const hasCustomSlot = computed(() => typeof slots.default === 'function');
|
|
100
101
|
|
|
101
|
-
function isOHLC(data:
|
|
102
|
+
function isOHLC(data: SeriesSnapshot['data']): data is OHLCData {
|
|
102
103
|
return 'open' in data;
|
|
103
104
|
}
|
|
105
|
+
|
|
106
|
+
function isBand(data: SeriesSnapshot['data']): data is BandPoint {
|
|
107
|
+
return 'high' in data && !isOHLC(data);
|
|
108
|
+
}
|
|
104
109
|
</script>
|
|
105
110
|
|
|
106
111
|
<template>
|
|
@@ -174,6 +179,20 @@ function isOHLC(data: OHLCData | TimePoint): data is OHLCData {
|
|
|
174
179
|
</span>
|
|
175
180
|
</template>
|
|
176
181
|
</span>
|
|
182
|
+
<span v-else-if="isBand(s.data)" :style="{ display: 'inline-flex', alignItems: 'center', gap: '3px' }">
|
|
183
|
+
<span
|
|
184
|
+
:style="{
|
|
185
|
+
width: '6px',
|
|
186
|
+
height: '6px',
|
|
187
|
+
borderRadius: '1px',
|
|
188
|
+
background: s.color,
|
|
189
|
+
flexShrink: 0,
|
|
190
|
+
}"
|
|
191
|
+
/>
|
|
192
|
+
<span :style="{ color: s.color, fontWeight: 500 }">
|
|
193
|
+
{{ effectiveFormat((s.data as BandPoint).low, 'low') }} – {{ effectiveFormat((s.data as BandPoint).high, 'high') }}
|
|
194
|
+
</span>
|
|
195
|
+
</span>
|
|
177
196
|
<span v-else :style="{ display: 'inline-flex', alignItems: 'center', gap: '3px' }">
|
|
178
197
|
<span
|
|
179
198
|
:style="{
|
package/src/ui/TimeAxis.vue
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { 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
6
|
import { useChartInstance } from '../context';
|
|
6
7
|
|
|
7
8
|
const props = defineProps<{
|
|
@@ -12,6 +13,8 @@ const props = defineProps<{
|
|
|
12
13
|
}>();
|
|
13
14
|
|
|
14
15
|
const chart = useChartInstance();
|
|
16
|
+
const layout = useAxisLayout(chart);
|
|
17
|
+
const onLeft = computed(() => layout.value.yAxisPosition === 'left');
|
|
15
18
|
const containerRef = ref<HTMLDivElement | null>(null);
|
|
16
19
|
|
|
17
20
|
const applyDensity = () => {
|
|
@@ -42,10 +45,10 @@ onUnmounted(() => {
|
|
|
42
45
|
ref="containerRef"
|
|
43
46
|
:style="{
|
|
44
47
|
position: 'absolute',
|
|
45
|
-
left:
|
|
48
|
+
left: (onLeft ? layout.yAxisWidth : 0) + 'px',
|
|
46
49
|
bottom: '0',
|
|
47
|
-
right:
|
|
48
|
-
height:
|
|
50
|
+
right: (onLeft ? 0 : layout.yAxisWidth) + 'px',
|
|
51
|
+
height: layout.xAxisHeight + 'px',
|
|
49
52
|
pointerEvents: 'none',
|
|
50
53
|
display: 'flex',
|
|
51
54
|
alignItems: 'center',
|
package/src/ui/Tooltip.vue
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {
|
|
3
|
+
type BandPoint,
|
|
3
4
|
type OHLCData,
|
|
4
5
|
type SeriesSnapshot,
|
|
5
6
|
type SnapshotSort,
|
|
@@ -104,16 +105,18 @@ watchEffect((onCleanup) => {
|
|
|
104
105
|
const floatingPos = computed<{ left: number; top: number } | null>(() => {
|
|
105
106
|
if (!crosshair.value || snapshots.value.length === 0) return null;
|
|
106
107
|
const mediaSize = chart.getMediaSize();
|
|
108
|
+
const chartLeft = chart.plotLeft;
|
|
107
109
|
const chartWidth = mediaSize.width - chart.yAxisWidth;
|
|
108
110
|
const chartHeight = mediaSize.height - chart.xAxisHeight;
|
|
109
111
|
|
|
110
112
|
if (hasCustomSlot.value) {
|
|
111
113
|
const size = measuredSize.value;
|
|
112
|
-
if (!size) return { left:
|
|
114
|
+
if (!size) return { left: chartLeft, top: 0 };
|
|
113
115
|
|
|
114
116
|
return computeTooltipPosition({
|
|
115
117
|
x: crosshair.value.mediaX,
|
|
116
118
|
y: crosshair.value.mediaY,
|
|
119
|
+
chartLeft,
|
|
117
120
|
chartWidth,
|
|
118
121
|
chartHeight,
|
|
119
122
|
tooltipWidth: size.width,
|
|
@@ -123,12 +126,15 @@ const floatingPos = computed<{ left: number; top: number } | null>(() => {
|
|
|
123
126
|
|
|
124
127
|
const hasOHLC = snapshots.value.some((s) => 'open' in s.data);
|
|
125
128
|
const lineCount = snapshots.value.filter((s) => !('open' in s.data)).length;
|
|
126
|
-
|
|
129
|
+
// A band row carries two numbers, so it needs a wider box than a line row.
|
|
130
|
+
const hasBand = snapshots.value.some((s) => 'high' in s.data && !('open' in s.data));
|
|
131
|
+
const tooltipWidth = hasBand ? 220 : 160;
|
|
127
132
|
const tooltipHeight = hasOHLC ? 140 : 40 + lineCount * 22;
|
|
128
133
|
|
|
129
134
|
return computeTooltipPosition({
|
|
130
135
|
x: crosshair.value.mediaX,
|
|
131
136
|
y: crosshair.value.mediaY,
|
|
137
|
+
chartLeft,
|
|
132
138
|
chartWidth,
|
|
133
139
|
chartHeight,
|
|
134
140
|
tooltipWidth,
|
|
@@ -138,9 +144,13 @@ const floatingPos = computed<{ left: number; top: number } | null>(() => {
|
|
|
138
144
|
|
|
139
145
|
const measured = computed(() => !hasCustomSlot.value || measuredSize.value !== null);
|
|
140
146
|
|
|
141
|
-
function isOHLC(data:
|
|
147
|
+
function isOHLC(data: SeriesSnapshot['data']): data is OHLCData {
|
|
142
148
|
return 'open' in data;
|
|
143
149
|
}
|
|
150
|
+
|
|
151
|
+
function isBand(data: SeriesSnapshot['data']): data is BandPoint {
|
|
152
|
+
return 'high' in data && !isOHLC(data);
|
|
153
|
+
}
|
|
144
154
|
</script>
|
|
145
155
|
|
|
146
156
|
<template>
|
|
@@ -212,6 +222,33 @@ function isOHLC(data: OHLCData | TimePoint): data is OHLCData {
|
|
|
212
222
|
</span>
|
|
213
223
|
</template>
|
|
214
224
|
</div>
|
|
225
|
+
<div
|
|
226
|
+
v-else-if="isBand(s.data)"
|
|
227
|
+
:style="{ display: 'flex', alignItems: 'center', gap: '8px', padding: '2px 0' }"
|
|
228
|
+
>
|
|
229
|
+
<span
|
|
230
|
+
:style="{
|
|
231
|
+
width: '8px',
|
|
232
|
+
height: '8px',
|
|
233
|
+
borderRadius: '2px',
|
|
234
|
+
background: s.color,
|
|
235
|
+
flexShrink: 0,
|
|
236
|
+
}"
|
|
237
|
+
/>
|
|
238
|
+
<span
|
|
239
|
+
:style="{
|
|
240
|
+
opacity: 0.6,
|
|
241
|
+
flex: '1',
|
|
242
|
+
minWidth: 0,
|
|
243
|
+
overflow: 'hidden',
|
|
244
|
+
textOverflow: 'ellipsis',
|
|
245
|
+
whiteSpace: 'nowrap',
|
|
246
|
+
}"
|
|
247
|
+
>{{ s.label ?? 'Range' }}</span>
|
|
248
|
+
<span :style="{ fontWeight: 600, color: s.color, whiteSpace: 'nowrap' }">
|
|
249
|
+
{{ effectiveFormat((s.data as BandPoint).low, 'low') }} – {{ effectiveFormat((s.data as BandPoint).high, 'high') }}
|
|
250
|
+
</span>
|
|
251
|
+
</div>
|
|
215
252
|
<div v-else :style="{ display: 'flex', alignItems: 'center', gap: '8px', padding: '2px 0' }">
|
|
216
253
|
<span
|
|
217
254
|
:style="{
|
package/src/ui/YAxis.vue
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { type ValueFormatter, mountAxisLabels } from '@wick-charts/core';
|
|
3
3
|
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
|
4
4
|
|
|
5
|
+
import { useAxisLayout } from '../composables';
|
|
5
6
|
import { useChartInstance } from '../context';
|
|
6
7
|
|
|
7
8
|
const props = defineProps<{
|
|
@@ -17,6 +18,7 @@ const props = defineProps<{
|
|
|
17
18
|
}>();
|
|
18
19
|
|
|
19
20
|
const chart = useChartInstance();
|
|
21
|
+
const layout = useAxisLayout(chart);
|
|
20
22
|
const containerRef = ref<HTMLDivElement | null>(null);
|
|
21
23
|
|
|
22
24
|
// Route the formatter through yScale so Crosshair / YLabel fallback use
|
|
@@ -52,10 +54,10 @@ onUnmounted(() => {
|
|
|
52
54
|
ref="containerRef"
|
|
53
55
|
:style="{
|
|
54
56
|
position: 'absolute',
|
|
55
|
-
|
|
57
|
+
[layout.yAxisPosition]: '0',
|
|
56
58
|
top: '0',
|
|
57
|
-
bottom:
|
|
58
|
-
width:
|
|
59
|
+
bottom: layout.xAxisHeight + 'px',
|
|
60
|
+
width: layout.yAxisWidth + 'px',
|
|
59
61
|
pointerEvents: 'none',
|
|
60
62
|
}"
|
|
61
63
|
/>
|
package/src/ui/YLabel.vue
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {
|
|
3
3
|
BadgeAnimator,
|
|
4
|
-
type ChartInstance,
|
|
5
4
|
type ValueFormatter,
|
|
6
5
|
type YLabelAnimate,
|
|
7
6
|
firstVisibleValue,
|
|
8
7
|
prefersReducedMotion,
|
|
9
8
|
resolveAnimate,
|
|
9
|
+
resolveYLabelSeriesId,
|
|
10
10
|
} from '@wick-charts/core';
|
|
11
11
|
import { computed, onMounted, onUnmounted, ref, useSlots } from 'vue';
|
|
12
12
|
|
|
@@ -145,21 +145,10 @@ onUnmounted(() => {
|
|
|
145
145
|
rafId = null;
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
-
function resolveSeriesId(c: ChartInstance, explicit: string | undefined): string | null {
|
|
149
|
-
if (explicit !== undefined) return explicit;
|
|
150
|
-
|
|
151
|
-
const singleLayer = c.getSeriesIdsByType('time', { visibleOnly: true, singleLayerOnly: true });
|
|
152
|
-
if (singleLayer.length > 0) return singleLayer[0];
|
|
153
|
-
|
|
154
|
-
const anyTime = c.getSeriesIdsByType('time', { visibleOnly: true });
|
|
155
|
-
|
|
156
|
-
return anyTime.length > 0 ? anyTime[0] : null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
148
|
const resolvedId = computed<string | null>(() => {
|
|
160
149
|
void bump.value;
|
|
161
150
|
|
|
162
|
-
return
|
|
151
|
+
return resolveYLabelSeriesId(chart, props.seriesId);
|
|
163
152
|
});
|
|
164
153
|
|
|
165
154
|
const last = computed<{ value: number; isLive: boolean } | null>(() => {
|
|
@@ -273,8 +262,8 @@ const effectiveFormat = computed<ValueFormatter>(() => {
|
|
|
273
262
|
<div
|
|
274
263
|
:style="{
|
|
275
264
|
position: 'absolute',
|
|
276
|
-
left: '
|
|
277
|
-
right: chart.yAxisWidth + 'px',
|
|
265
|
+
left: chart.plotLeft + 'px',
|
|
266
|
+
right: (chart.yAxisPosition === 'left' ? 0 : chart.yAxisWidth) + 'px',
|
|
278
267
|
top: y + 'px',
|
|
279
268
|
height: '0',
|
|
280
269
|
borderTop: '1px dashed ' + bgColor,
|
|
@@ -286,7 +275,7 @@ const effectiveFormat = computed<ValueFormatter>(() => {
|
|
|
286
275
|
<div
|
|
287
276
|
:style="{
|
|
288
277
|
position: 'absolute',
|
|
289
|
-
|
|
278
|
+
[chart.yAxisPosition]: '4px',
|
|
290
279
|
top: y + 'px',
|
|
291
280
|
transform: 'translateY(-50%)',
|
|
292
281
|
opacity: opacity,
|