@wick-charts/vue 0.5.1 → 0.6.1
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 +45 -6
- package/dist/BarSeries.vue.js +26 -20
- package/dist/CandlestickSeries.vue.js +30 -24
- package/dist/ChartContainer.vue.js +125 -72
- package/dist/HeatmapSeries.vue.js +40 -0
- package/dist/HeatmapSeries.vue2.js +4 -0
- package/dist/LineSeries.vue.js +26 -20
- package/dist/PieSeries.vue.js +24 -18
- package/dist/core/dist/animation/animator.js +21 -21
- package/dist/core/dist/animation/config.js +114 -87
- package/dist/core/dist/animation/easing.js +3 -1
- package/dist/core/dist/animation/intro-wave.js +68 -0
- package/dist/core/dist/axis/dom-labels.js +13 -14
- package/dist/core/dist/chart/badge-animator.js +56 -0
- package/dist/core/dist/chart/edge-indicators.js +28 -13
- package/dist/core/dist/chart/last-value.js +18 -17
- package/dist/core/dist/chart/options.js +27 -16
- package/dist/core/dist/chart/y-target.js +13 -12
- package/dist/core/dist/chart.js +868 -441
- package/dist/core/dist/components/annotation-chip.js +28 -0
- package/dist/core/dist/components/crosshair.js +8 -6
- package/dist/core/dist/components/edge-indicator.js +58 -26
- package/dist/core/dist/components/loading-indicator.js +46 -0
- package/dist/core/dist/components/marker.js +49 -31
- package/dist/core/dist/components/reference-line.js +27 -36
- package/dist/core/dist/components/time-region.js +30 -28
- package/dist/core/dist/data/store.js +38 -19
- package/dist/core/dist/data/sync.js +60 -38
- package/dist/core/dist/heatmap-tooltip.js +16 -0
- package/dist/core/dist/interactions/handler.js +86 -46
- package/dist/core/dist/perf/perf-hud.js +5 -5
- package/dist/core/dist/scales/x-scale.js +85 -37
- package/dist/core/dist/scales/y-scale.js +38 -27
- package/dist/core/dist/series/bar-intro.js +18 -0
- package/dist/core/dist/series/bar.js +337 -205
- package/dist/core/dist/series/base-multi-layer.js +224 -96
- package/dist/core/dist/series/candlestick-intro.js +57 -0
- package/dist/core/dist/series/candlestick.js +454 -276
- package/dist/core/dist/series/heatmap.js +387 -0
- package/dist/core/dist/series/line-intro.js +85 -0
- package/dist/core/dist/series/line.js +590 -282
- package/dist/core/dist/series/painters/builtins.js +2 -2
- package/dist/core/dist/series/pie.js +396 -259
- package/dist/core/dist/series/register-builtins.js +8 -7
- package/dist/core/dist/series/types.js +6 -0
- package/dist/core/dist/series/wave-intro.js +26 -0
- package/dist/core/dist/theme/create.js +69 -63
- package/dist/core/dist/theme/themes/mint-breeze.js +2 -2
- package/dist/core/dist/theme/themes/sand-dune.js +2 -2
- package/dist/core/dist/utils/color.js +109 -34
- package/dist/core/dist/utils/deep-equal.js +18 -0
- package/dist/core/dist/utils/time.js +25 -19
- package/dist/index.d.ts +1494 -122
- package/dist/index.js +177 -130
- package/dist/ui/Crosshair.vue.js +20 -21
- package/dist/ui/HeatmapTooltip.vue.js +96 -0
- package/dist/ui/HeatmapTooltip.vue2.js +4 -0
- package/dist/ui/InfoBar.vue.js +46 -47
- package/dist/ui/Legend.vue.js +80 -79
- package/dist/ui/PieLegend.vue.js +5 -5
- package/dist/ui/PieTooltip.vue.js +77 -67
- package/dist/ui/Tooltip.vue.js +60 -60
- package/dist/ui/YAxis.vue.js +2 -2
- package/dist/ui/YLabel.vue.js +94 -61
- package/package.json +2 -2
- package/src/BarSeries.vue +23 -7
- package/src/CandlestickSeries.vue +23 -7
- package/src/ChartContainer.vue +250 -13
- package/src/HeatmapSeries.vue +69 -0
- package/src/LineSeries.vue +23 -7
- package/src/PieSeries.vue +22 -6
- package/src/index.ts +84 -3
- package/src/ui/Crosshair.vue +1 -2
- package/src/ui/HeatmapTooltip.vue +144 -0
- package/src/ui/InfoBar.vue +1 -2
- package/src/ui/Legend.vue +13 -0
- package/src/ui/Marker.vue +1 -1
- package/src/ui/PieTooltip.vue +10 -0
- package/src/ui/ReferenceLine.vue +2 -2
- package/src/ui/TimeRegion.vue +2 -2
- package/src/ui/Tooltip.vue +2 -2
- package/src/ui/YLabel.vue +123 -8
package/src/ChartContainer.vue
CHANGED
|
@@ -5,9 +5,15 @@ import {
|
|
|
5
5
|
ChartInstance,
|
|
6
6
|
type ChartOptions,
|
|
7
7
|
type ChartTheme,
|
|
8
|
+
type CrosshairPosition,
|
|
8
9
|
type EdgeReachedInfo,
|
|
10
|
+
type FadeConfig,
|
|
11
|
+
type PointClickInfo,
|
|
12
|
+
type SeriesHoverInfo,
|
|
13
|
+
type VisibleRange,
|
|
9
14
|
type VisibleRangeSpec,
|
|
10
15
|
catppuccin,
|
|
16
|
+
deepEqual,
|
|
11
17
|
} from '@wick-charts/core';
|
|
12
18
|
import { computed, nextTick, onMounted, onUnmounted, provide, ref, shallowRef, watch } from 'vue';
|
|
13
19
|
|
|
@@ -25,7 +31,9 @@ type PerfOption = NonNullable<ChartOptions['perf']>;
|
|
|
25
31
|
|
|
26
32
|
const props = withDefaults(
|
|
27
33
|
defineProps<{
|
|
34
|
+
/** Visual theme. Live — changing this at runtime updates all themed elements. */
|
|
28
35
|
theme?: ChartTheme;
|
|
36
|
+
/** Grouped axis configuration (Y/X visibility, bounds, sizing). Live. */
|
|
29
37
|
axis?: AxisConfig;
|
|
30
38
|
/**
|
|
31
39
|
* Viewport padding. `top`/`bottom` are in pixels. `left`/`right` accept either pixels (`50`)
|
|
@@ -61,14 +69,55 @@ const props = withDefaults(
|
|
|
61
69
|
*/
|
|
62
70
|
initialRange?: VisibleRangeSpec;
|
|
63
71
|
};
|
|
64
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Controlled visible range. Same shape as the imperative
|
|
74
|
+
* `chart.setVisibleRange` — a bar count, an explicit `{from, to}` window,
|
|
75
|
+
* or `{from, bars}`. Every reference/value change applies via
|
|
76
|
+
* `chart.setVisibleRange`, so pair it with `onVisibleRangeChange` for a
|
|
77
|
+
* two-way binding; a same-value literal (compared structurally) is a
|
|
78
|
+
* no-op, so an inline object doesn't re-apply on every render.
|
|
79
|
+
*
|
|
80
|
+
* For a one-shot initial window that isn't re-applied on every prop
|
|
81
|
+
* change, use `viewport.initialRange` instead.
|
|
82
|
+
*/
|
|
83
|
+
visibleRange?: VisibleRangeSpec;
|
|
84
|
+
/** Show the chart background gradient. Live. Defaults to true. */
|
|
65
85
|
gradient?: boolean;
|
|
66
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Enable zoom, pan, and crosshair interactions. Defaults to true.
|
|
88
|
+
*
|
|
89
|
+
* Mount-only — core has no `setInteractive`, so changing this prop after
|
|
90
|
+
* mount is ignored.
|
|
91
|
+
*/
|
|
67
92
|
interactive?: boolean;
|
|
68
|
-
/** Background grid configuration. Default: `{ visible: true }`. */
|
|
93
|
+
/** Background grid configuration. Live. Default: `{ visible: true }`. */
|
|
69
94
|
grid?: { visible: boolean };
|
|
70
95
|
/**
|
|
71
|
-
*
|
|
96
|
+
* Soft fade-out at the top of the plot area, so series content dissolves
|
|
97
|
+
* under a floating `<Title>` / `<InfoBar>` instead of colliding with it.
|
|
98
|
+
* The mask erases canvas alpha rather than painting a cover color, so it
|
|
99
|
+
* stays correct over the default background gradient. Live.
|
|
100
|
+
*
|
|
101
|
+
* Two directions, independently controlled:
|
|
102
|
+
*
|
|
103
|
+
* **X (under the axis)** — panning content slides under the Y-axis
|
|
104
|
+
* column and dissolves instead of hard-clipping at the pane edge. **On
|
|
105
|
+
* by default** as a 60px ramp that finishes just inside the axis
|
|
106
|
+
* column, before the label glyphs; `{ right }` overrides the total
|
|
107
|
+
* ramp width in CSS px (`0` disables). `{ left }` adds the mirror zone
|
|
108
|
+
* at the left pane edge (default off).
|
|
109
|
+
*
|
|
110
|
+
* **Top (under the header)** — opt-in. `true` enables the auto zone
|
|
111
|
+
* (measured header + 24px run-out, half the header fold-in released so
|
|
112
|
+
* data rides up into the zone at rest), `{ top }` sets the zone height
|
|
113
|
+
* explicitly, and `{ overlap }` tunes how many px of the fold-in to
|
|
114
|
+
* release (0 keeps the strict fold-in). Off unless one of those is set.
|
|
115
|
+
*
|
|
116
|
+
* `false` — every mask off, strict edges everywhere.
|
|
117
|
+
*/
|
|
118
|
+
fade?: boolean | { top?: number; overlap?: number; right?: number; left?: number };
|
|
119
|
+
/**
|
|
120
|
+
* How `<Title>` and `<InfoBar>` are positioned relative to the canvas. Live.
|
|
72
121
|
* - `'overlay'` (default): absolute overlays on top of the canvas.
|
|
73
122
|
* - `'inline'`: flex siblings above the canvas — the canvas (and grid) shift down.
|
|
74
123
|
*/
|
|
@@ -79,10 +128,10 @@ const props = withDefaults(
|
|
|
79
128
|
* `<CandlestickSeries>` / `<BarSeries>` override these chart-level
|
|
80
129
|
* defaults unless the category here is explicitly `false`.
|
|
81
130
|
*
|
|
82
|
-
* **Init-only by reference
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
131
|
+
* **Init-only, but diffed by value, not reference.** A same-value
|
|
132
|
+
* inline object literal is a no-op — only a genuine change to the
|
|
133
|
+
* resolved config recreates the underlying `ChartInstance`, since the
|
|
134
|
+
* animation engine doesn't support live reconfiguration yet.
|
|
86
135
|
*/
|
|
87
136
|
animations?: boolean | AnimationsConfig;
|
|
88
137
|
/**
|
|
@@ -97,11 +146,58 @@ const props = withDefaults(
|
|
|
97
146
|
* the prop identity later is ignored.
|
|
98
147
|
*/
|
|
99
148
|
onEdgeReached?: (info: EdgeReachedInfo) => void;
|
|
149
|
+
/**
|
|
150
|
+
* Fired on a click (or tap) on the chart canvas that isn't the tail end
|
|
151
|
+
* of a pan drag. `info.spatialHit` resolves a pie/heatmap/custom-spatial
|
|
152
|
+
* series directly under the pointer; for a time-series series, resolve
|
|
153
|
+
* the point yourself from `info.time` (`chart.getDataAtTime` /
|
|
154
|
+
* `buildHoverSnapshots`). Live — the latest callback is always used.
|
|
155
|
+
*/
|
|
156
|
+
onPointClick?: (info: PointClickInfo) => void;
|
|
157
|
+
/**
|
|
158
|
+
* Fired on a double-click on the chart canvas. The chart also responds
|
|
159
|
+
* by calling `fitContent()`. Live — the latest callback is always used.
|
|
160
|
+
*/
|
|
161
|
+
onPointDoubleClick?: (info: PointClickInfo) => void;
|
|
162
|
+
/**
|
|
163
|
+
* Fired when the spatially-hovered series/index changes (pie, heatmap,
|
|
164
|
+
* or a custom spatial kind) — `null` when the pointer leaves every hit
|
|
165
|
+
* area. Live — the latest callback is always used.
|
|
166
|
+
*/
|
|
167
|
+
onSeriesHover?: (hit: SeriesHoverInfo | null) => void;
|
|
168
|
+
/**
|
|
169
|
+
* Fired once the underlying `ChartInstance` is constructed — on mount,
|
|
170
|
+
* and again if the chart is ever rebuilt (a genuine `animations` value
|
|
171
|
+
* change). Live — the latest callback is always used for the next rebuild.
|
|
172
|
+
*/
|
|
173
|
+
onReady?: (chart: ChartInstance) => void;
|
|
174
|
+
/**
|
|
175
|
+
* Fired whenever the committed visible range changes — pan, zoom,
|
|
176
|
+
* `fitContent()`, a data update that shifts the tail-scroll window, or a
|
|
177
|
+
* `visibleRange` prop change. Live — the latest callback is always used.
|
|
178
|
+
*/
|
|
179
|
+
onVisibleRangeChange?: (range: VisibleRange) => void;
|
|
180
|
+
/**
|
|
181
|
+
* Fired on every crosshair move, with `null` when the pointer leaves the
|
|
182
|
+
* chart. Live — the latest callback is always used.
|
|
183
|
+
*/
|
|
184
|
+
onCrosshairMove?: (position: CrosshairPosition | null) => void;
|
|
100
185
|
}>(),
|
|
101
186
|
{
|
|
102
187
|
theme: () => catppuccin.theme,
|
|
103
188
|
gradient: true,
|
|
189
|
+
// Vue's type-based `defineProps` casts an absent Boolean-typed prop to
|
|
190
|
+
// `false` (not `undefined`) — without this explicit default, every chart
|
|
191
|
+
// that didn't pass `:interactive="true"` silently got zero pan/zoom/
|
|
192
|
+
// crosshair/click interactivity. `gradient` dodges the same trap only
|
|
193
|
+
// because it already had a default.
|
|
194
|
+
interactive: true,
|
|
104
195
|
headerLayout: 'overlay',
|
|
196
|
+
// Same Boolean-cast trap, opposite goal: an absent `fade` must stay
|
|
197
|
+
// `undefined` (keep the core's default X mask armed) and remain
|
|
198
|
+
// distinguishable from an explicit `:fade="false"` all-off opt-out.
|
|
199
|
+
// Declaring a default (even `undefined`) suppresses the cast.
|
|
200
|
+
fade: undefined,
|
|
105
201
|
},
|
|
106
202
|
);
|
|
107
203
|
|
|
@@ -133,9 +229,49 @@ provide(NavigatorAnchorKey, navigatorAnchor);
|
|
|
133
229
|
let resizeObserver: ResizeObserver | null = null;
|
|
134
230
|
const topOverlayHeight = ref(0);
|
|
135
231
|
|
|
232
|
+
/** Run-out below the measured header (CSS px) for the auto `fade` zone —
|
|
233
|
+
* content starts dissolving this far before it slides under the header. */
|
|
234
|
+
const FADE_AUTO_BAND = 24;
|
|
235
|
+
|
|
236
|
+
// Fade resolution — shared by the header fold-in below and `applyFade`.
|
|
237
|
+
// Vue casts an absent Boolean-typed prop to `false`, which conveniently is
|
|
238
|
+
// also the "off" value here. `overlap` deliberately applies only while the
|
|
239
|
+
// fade is on: without the mask, letting data ride under the header would
|
|
240
|
+
// just recreate the collision the strict fold-in exists to prevent.
|
|
241
|
+
// The top mask is opt-in: `true`, `{ top }`, or `{ overlap }` arms it (a
|
|
242
|
+
// pure X config like `{ right: 0 }` does not).
|
|
243
|
+
const fadeEnabled = computed(() => {
|
|
244
|
+
if (props.fade === false) return false;
|
|
245
|
+
|
|
246
|
+
return typeof props.fade === 'object'
|
|
247
|
+
? props.fade.top !== undefined || props.fade.overlap !== undefined
|
|
248
|
+
: props.fade === true;
|
|
249
|
+
});
|
|
250
|
+
// Auto overlap: half the measured header — with the full fold-in intact the
|
|
251
|
+
// zone would cover only padding and enabling the fade would change nothing
|
|
252
|
+
// visible at rest. Explicit `overlap` (including 0) wins.
|
|
253
|
+
const fadeOverlap = computed(() => {
|
|
254
|
+
if (!fadeEnabled.value) return 0;
|
|
255
|
+
|
|
256
|
+
const explicit = typeof props.fade === 'object' ? props.fade.overlap : undefined;
|
|
257
|
+
if (explicit !== undefined) return Math.max(0, explicit);
|
|
258
|
+
|
|
259
|
+
return topOverlayHeight.value / 2;
|
|
260
|
+
});
|
|
261
|
+
const resolvedFadeTop = computed(() => {
|
|
262
|
+
if (!fadeEnabled.value) return 0;
|
|
263
|
+
if (typeof props.fade === 'object' && props.fade.top !== undefined) return props.fade.top;
|
|
264
|
+
|
|
265
|
+
const measured = props.headerLayout === 'overlay' ? topOverlayHeight.value : 0;
|
|
266
|
+
|
|
267
|
+
return measured > 0 ? measured + FADE_AUTO_BAND : FADE_AUTO_BAND;
|
|
268
|
+
});
|
|
269
|
+
|
|
136
270
|
// Inline mode: browser flex already reserves header height, so folding it
|
|
137
271
|
// into padding.top would double-shift the data. Only overlay needs the fold.
|
|
138
|
-
const headerExtra = computed(() =>
|
|
272
|
+
const headerExtra = computed(() =>
|
|
273
|
+
props.headerLayout === 'overlay' ? Math.max(0, topOverlayHeight.value - fadeOverlap.value) : 0,
|
|
274
|
+
);
|
|
139
275
|
|
|
140
276
|
function applyPadding() {
|
|
141
277
|
if (!chart.value) return;
|
|
@@ -147,6 +283,43 @@ function applyPadding() {
|
|
|
147
283
|
chart.value.setPadding(merged);
|
|
148
284
|
}
|
|
149
285
|
|
|
286
|
+
/** Seed the constructor's fade config so the very first frame is already
|
|
287
|
+
* correct — `applyFade` lands the header-measured top zone right after, but
|
|
288
|
+
* an opted-out chart must not flash the default X mask for one paint. */
|
|
289
|
+
function seedFadeOption(options: ChartOptions): void {
|
|
290
|
+
if (props.fade === false) {
|
|
291
|
+
options.fade = { top: 0, right: 0, left: 0 };
|
|
292
|
+
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (typeof props.fade !== 'object') return;
|
|
296
|
+
|
|
297
|
+
const seed: FadeConfig = {};
|
|
298
|
+
if (props.fade.top !== undefined) seed.top = props.fade.top;
|
|
299
|
+
if (props.fade.right !== undefined) seed.right = props.fade.right;
|
|
300
|
+
if (props.fade.left !== undefined) seed.left = props.fade.left;
|
|
301
|
+
options.fade = seed;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function applyFade() {
|
|
305
|
+
if (!chart.value) return;
|
|
306
|
+
|
|
307
|
+
if (props.fade === false) {
|
|
308
|
+
chart.value.setFade({ top: 0, right: 0, left: 0 });
|
|
309
|
+
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Omitting `right` keeps the core's under-the-axis default armed; only an
|
|
314
|
+
// explicit value (or the all-off branch above) overrides it.
|
|
315
|
+
const next: FadeConfig = { top: resolvedFadeTop.value };
|
|
316
|
+
if (typeof props.fade === 'object') {
|
|
317
|
+
if (props.fade.right !== undefined) next.right = props.fade.right;
|
|
318
|
+
if (props.fade.left !== undefined) next.left = props.fade.left;
|
|
319
|
+
}
|
|
320
|
+
chart.value.setFade(next);
|
|
321
|
+
}
|
|
322
|
+
|
|
150
323
|
// Wires up (or tears down) the header-height observer for the current
|
|
151
324
|
// `headerLayout`. Called on mount AND whenever `headerLayout` flips at
|
|
152
325
|
// runtime — overlay needs a live measurement, inline lets browser flex
|
|
@@ -174,6 +347,21 @@ function syncHeaderObserver() {
|
|
|
174
347
|
resizeObserver.observe(headerEl);
|
|
175
348
|
}
|
|
176
349
|
|
|
350
|
+
// `props` is a reactive proxy — reading `props.onX` inside these listeners
|
|
351
|
+
// always sees the caller's latest callback, no ref-latch needed the way
|
|
352
|
+
// React does. Shared between the mount path and the animations-triggered
|
|
353
|
+
// rebuild below so the two don't drift.
|
|
354
|
+
function subscribeDeclarativeEvents(instance: ChartInstance): void {
|
|
355
|
+
instance.on('pointClick', (info) => props.onPointClick?.(info));
|
|
356
|
+
instance.on('pointDoubleClick', (info) => props.onPointDoubleClick?.(info));
|
|
357
|
+
instance.on('seriesHover', (hit) => props.onSeriesHover?.(hit));
|
|
358
|
+
instance.on('crosshairMove', (position) => props.onCrosshairMove?.(position));
|
|
359
|
+
const emitVisibleRangeChange = () => props.onVisibleRangeChange?.(instance.getVisibleRange());
|
|
360
|
+
instance.on('viewportChange', emitVisibleRangeChange);
|
|
361
|
+
instance.on('dataUpdate', emitVisibleRangeChange);
|
|
362
|
+
instance.on('seriesChange', emitVisibleRangeChange);
|
|
363
|
+
}
|
|
364
|
+
|
|
177
365
|
onMounted(async () => {
|
|
178
366
|
if (!containerRef.value) return;
|
|
179
367
|
|
|
@@ -187,7 +375,10 @@ onMounted(async () => {
|
|
|
187
375
|
if (perfAtMount !== undefined) options.perf = perfAtMount;
|
|
188
376
|
if (onEdgeReachedAtMount) options.onEdgeReached = onEdgeReachedAtMount;
|
|
189
377
|
if (props.animations !== undefined) options.animations = props.animations;
|
|
378
|
+
seedFadeOption(options);
|
|
190
379
|
chart.value = new ChartInstance(containerRef.value, options);
|
|
380
|
+
subscribeDeclarativeEvents(chart.value);
|
|
381
|
+
props.onReady?.(chart.value);
|
|
191
382
|
|
|
192
383
|
await nextTick();
|
|
193
384
|
syncHeaderObserver();
|
|
@@ -236,9 +427,11 @@ watch(
|
|
|
236
427
|
},
|
|
237
428
|
);
|
|
238
429
|
|
|
239
|
-
// Init-only: post-mount `animations`
|
|
240
|
-
//
|
|
241
|
-
// —
|
|
430
|
+
// Init-only: a post-mount `animations` VALUE change tears down the instance
|
|
431
|
+
// and rebuilds with the new config. `deepEqual` (not reference equality)
|
|
432
|
+
// gates the rebuild — a caller's brand-new inline literal with the same
|
|
433
|
+
// values is a no-op, since Vue's `watch` fires this callback on every
|
|
434
|
+
// reference change regardless of content.
|
|
242
435
|
//
|
|
243
436
|
// Children get the chart via `useChartInstance()` which snapshots
|
|
244
437
|
// `chartRef.value` once at setup time. To force them to re-setup against
|
|
@@ -247,9 +440,12 @@ watch(
|
|
|
247
440
|
// Vue commits the unmount before the new ChartInstance is constructed.
|
|
248
441
|
// When `chart.value` becomes the new instance, the slot re-mounts and
|
|
249
442
|
// every child's `setup()` runs again against the fresh chart.
|
|
443
|
+
let lastAnimations = props.animations;
|
|
250
444
|
watch(
|
|
251
445
|
() => props.animations,
|
|
252
446
|
async (next) => {
|
|
447
|
+
if (deepEqual(next, lastAnimations)) return;
|
|
448
|
+
lastAnimations = next;
|
|
253
449
|
if (!chart.value || !containerRef.value) return;
|
|
254
450
|
chart.value.destroy();
|
|
255
451
|
chart.value = null;
|
|
@@ -264,10 +460,45 @@ watch(
|
|
|
264
460
|
if (perfAtMount !== undefined) opts.perf = perfAtMount;
|
|
265
461
|
if (onEdgeReachedAtMount) opts.onEdgeReached = onEdgeReachedAtMount;
|
|
266
462
|
if (next !== undefined) opts.animations = next;
|
|
463
|
+
seedFadeOption(opts);
|
|
267
464
|
chart.value = new ChartInstance(containerRef.value, opts);
|
|
465
|
+
subscribeDeclarativeEvents(chart.value);
|
|
466
|
+
props.onReady?.(chart.value);
|
|
268
467
|
},
|
|
269
468
|
);
|
|
270
469
|
|
|
470
|
+
// Controlled visible range: only re-apply when the resolved value actually
|
|
471
|
+
// differs from what we last applied ourselves, so feeding the range from
|
|
472
|
+
// `onVisibleRangeChange` back into this prop (the standard two-way-binding
|
|
473
|
+
// shape) doesn't re-trigger `setVisibleRange` with an unchanged spec on
|
|
474
|
+
// every render. The latch remembers which instance it applied to — a chart
|
|
475
|
+
// rebuild (an `animations` change) must receive the range again even though
|
|
476
|
+
// the spec's value hasn't moved.
|
|
477
|
+
let lastAppliedVisibleRange: { instance: ChartInstance; spec: VisibleRangeSpec } | undefined;
|
|
478
|
+
watch(
|
|
479
|
+
() => [chart.value, props.visibleRange] as const,
|
|
480
|
+
async ([instance, next]) => {
|
|
481
|
+
if (!instance || next === undefined) return;
|
|
482
|
+
if (
|
|
483
|
+
lastAppliedVisibleRange !== undefined &&
|
|
484
|
+
lastAppliedVisibleRange.instance === instance &&
|
|
485
|
+
deepEqual(lastAppliedVisibleRange.spec, next)
|
|
486
|
+
) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
lastAppliedVisibleRange = { instance, spec: next };
|
|
491
|
+
// On the chart's first tick, a series child (e.g. `<CandlestickSeries>`)
|
|
492
|
+
// hasn't mounted and seeded its data yet, so the engine is still on the
|
|
493
|
+
// placeholder `dataInterval` (60s) — a `{from, to}` spec sized for the
|
|
494
|
+
// real interval would fail the 2-bar-minimum guard. Awaiting a tick lets
|
|
495
|
+
// the child's data-seed (and its `detectInterval` call) land first.
|
|
496
|
+
await nextTick();
|
|
497
|
+
instance.setVisibleRange(next);
|
|
498
|
+
},
|
|
499
|
+
{ immediate: true },
|
|
500
|
+
);
|
|
501
|
+
|
|
271
502
|
// Re-apply padding on any input that affects it — including `headerExtra`,
|
|
272
503
|
// so a runtime `headerLayout` toggle re-runs even when `padding` is undefined
|
|
273
504
|
// (the default top=20 still needs the fold-in/out).
|
|
@@ -282,6 +513,12 @@ watch(
|
|
|
282
513
|
applyPadding,
|
|
283
514
|
);
|
|
284
515
|
|
|
516
|
+
// Fade rides the chart identity too, so a rebuilt instance (an `animations`
|
|
517
|
+
// change) receives the current zones again; `setFade` no-ops on unchanged
|
|
518
|
+
// values, so the extra fires are free. `immediate` covers the mount path —
|
|
519
|
+
// the first run lands right after `chart.value` is set.
|
|
520
|
+
watch(() => [chart.value, resolvedFadeTop.value, props.fade] as const, applyFade, { immediate: true });
|
|
521
|
+
|
|
285
522
|
const rootStyle = computed(() => {
|
|
286
523
|
const t = themeRef.value;
|
|
287
524
|
const [gtop, gbot] = t?.chartGradient ?? ['transparent', 'transparent'];
|
|
@@ -289,7 +526,7 @@ const rootStyle = computed(() => {
|
|
|
289
526
|
const background = props.gradient ? `linear-gradient(to bottom, ${gtop} 0%, ${bg} 70%, ${gbot} 100%)` : bg;
|
|
290
527
|
|
|
291
528
|
return (
|
|
292
|
-
'position: relative; display: flex; flex-direction: column; width: 100%; height: 100%; overflow: hidden; ' +
|
|
529
|
+
'position: relative; display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 240px; overflow: hidden; ' +
|
|
293
530
|
`background: ${background}`
|
|
294
531
|
);
|
|
295
532
|
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { HeatmapCellData, HeatmapSeriesOptions } from '@wick-charts/core';
|
|
3
|
+
import { HeatmapSeriesDef } 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
|
+
data: HeatmapCellData[];
|
|
11
|
+
options?: Partial<HeatmapSeriesOptions>;
|
|
12
|
+
/** Stable series ID — same value across remounts. */
|
|
13
|
+
id?: string;
|
|
14
|
+
/** Show/hide the series without unmounting it — excludes it from the Y-range fit and tooltip/legend. Default `true`. Live. */
|
|
15
|
+
visible?: boolean;
|
|
16
|
+
}>(),
|
|
17
|
+
// Vue's type-based `defineProps` casts an absent Boolean-typed prop to
|
|
18
|
+
// `false` (not `undefined`) without an explicit default — see the
|
|
19
|
+
// `interactive` fix in ChartContainer.vue for the full explanation.
|
|
20
|
+
{ visible: true },
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const chart = useChartInstance();
|
|
24
|
+
const seriesId = ref<string | null>(null);
|
|
25
|
+
|
|
26
|
+
onMounted(() => {
|
|
27
|
+
const id = chart.addSeries(HeatmapSeriesDef, { ...props.options, id: props.id });
|
|
28
|
+
seriesId.value = id;
|
|
29
|
+
// Lazy watcher — apply initial data here so static-data mounts render without a no-op first frame.
|
|
30
|
+
if (props.data.length > 0) {
|
|
31
|
+
chart.setSeriesData(id, props.data);
|
|
32
|
+
}
|
|
33
|
+
chart.setSeriesVisible(id, props.visible);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
onUnmounted(() => {
|
|
37
|
+
if (seriesId.value) chart.removeSeries(seriesId.value);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
watch(
|
|
41
|
+
() => props.data,
|
|
42
|
+
(data) => {
|
|
43
|
+
// No length guard: an empty array is a legitimate update (filter yields
|
|
44
|
+
// no rows) and must clear the previous cells, matching the React wrapper.
|
|
45
|
+
if (seriesId.value) {
|
|
46
|
+
chart.setSeriesData(seriesId.value, data);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
watch(
|
|
52
|
+
() => props.options,
|
|
53
|
+
(options) => {
|
|
54
|
+
if (seriesId.value && options) {
|
|
55
|
+
chart.updateSeriesOptions(seriesId.value, options);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{ deep: true },
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
watch(
|
|
62
|
+
() => props.visible,
|
|
63
|
+
(visible) => {
|
|
64
|
+
if (seriesId.value) chart.setSeriesVisible(seriesId.value, visible);
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<template><span v-if="false" /></template>
|
package/src/LineSeries.vue
CHANGED
|
@@ -5,13 +5,21 @@ import { onMounted, onUnmounted, ref, watch } from 'vue';
|
|
|
5
5
|
|
|
6
6
|
import { useChartInstance } from './context';
|
|
7
7
|
|
|
8
|
-
const props =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
const props = withDefaults(
|
|
9
|
+
defineProps<{
|
|
10
|
+
/** Flat `TimePoint[]`, `TimePoint[][]`, or named `{ label, color?, data }` layers. Time accepts ms or `Date`. */
|
|
11
|
+
data: MultiLayerData;
|
|
12
|
+
options?: Partial<LineSeriesOptions>;
|
|
13
|
+
/** Stable series ID — same value across remounts. */
|
|
14
|
+
id?: string;
|
|
15
|
+
/** Show/hide the series without unmounting it — excludes it from the Y-range fit and tooltip/legend. Default `true`. Live. */
|
|
16
|
+
visible?: boolean;
|
|
17
|
+
}>(),
|
|
18
|
+
// Vue's type-based `defineProps` casts an absent Boolean-typed prop to
|
|
19
|
+
// `false` (not `undefined`) without an explicit default — see the
|
|
20
|
+
// `interactive` fix in ChartContainer.vue for the full explanation.
|
|
21
|
+
{ visible: true },
|
|
22
|
+
);
|
|
15
23
|
|
|
16
24
|
const chart = useChartInstance();
|
|
17
25
|
const seriesId = ref<string | null>(null);
|
|
@@ -35,6 +43,7 @@ onMounted(() => {
|
|
|
35
43
|
prevSync = new Array(layerCount).fill(EMPTY_SYNC_STATE);
|
|
36
44
|
// Lazy watcher — apply initial data here so static-data mounts render without a no-op first frame.
|
|
37
45
|
applyData(id, props.data);
|
|
46
|
+
chart.setSeriesVisible(id, props.visible);
|
|
38
47
|
});
|
|
39
48
|
|
|
40
49
|
onUnmounted(() => {
|
|
@@ -60,6 +69,13 @@ watch(
|
|
|
60
69
|
},
|
|
61
70
|
{ deep: true },
|
|
62
71
|
);
|
|
72
|
+
|
|
73
|
+
watch(
|
|
74
|
+
() => props.visible,
|
|
75
|
+
(visible) => {
|
|
76
|
+
if (seriesId.value) chart.setSeriesVisible(seriesId.value, visible);
|
|
77
|
+
},
|
|
78
|
+
);
|
|
63
79
|
</script>
|
|
64
80
|
|
|
65
81
|
<template><span v-if="false" /></template>
|
package/src/PieSeries.vue
CHANGED
|
@@ -5,12 +5,20 @@ import { onMounted, onUnmounted, ref, watch } from 'vue';
|
|
|
5
5
|
|
|
6
6
|
import { useChartInstance } from './context';
|
|
7
7
|
|
|
8
|
-
const props =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
const props = withDefaults(
|
|
9
|
+
defineProps<{
|
|
10
|
+
data: PieSliceData[];
|
|
11
|
+
options?: Partial<PieSeriesOptions>;
|
|
12
|
+
/** Stable series ID — same value across remounts. */
|
|
13
|
+
id?: string;
|
|
14
|
+
/** Show/hide the series without unmounting it — excludes it from the Y-range fit and tooltip/legend. Default `true`. Live. */
|
|
15
|
+
visible?: boolean;
|
|
16
|
+
}>(),
|
|
17
|
+
// Vue's type-based `defineProps` casts an absent Boolean-typed prop to
|
|
18
|
+
// `false` (not `undefined`) without an explicit default — see the
|
|
19
|
+
// `interactive` fix in ChartContainer.vue for the full explanation.
|
|
20
|
+
{ visible: true },
|
|
21
|
+
);
|
|
14
22
|
|
|
15
23
|
const chart = useChartInstance();
|
|
16
24
|
const seriesId = ref<string | null>(null);
|
|
@@ -22,6 +30,7 @@ onMounted(() => {
|
|
|
22
30
|
if (props.data.length > 0) {
|
|
23
31
|
chart.setSeriesData(id, props.data);
|
|
24
32
|
}
|
|
33
|
+
chart.setSeriesVisible(id, props.visible);
|
|
25
34
|
});
|
|
26
35
|
|
|
27
36
|
onUnmounted(() => {
|
|
@@ -46,6 +55,13 @@ watch(
|
|
|
46
55
|
},
|
|
47
56
|
{ deep: true },
|
|
48
57
|
);
|
|
58
|
+
|
|
59
|
+
watch(
|
|
60
|
+
() => props.visible,
|
|
61
|
+
(visible) => {
|
|
62
|
+
if (seriesId.value) chart.setSeriesVisible(seriesId.value, visible);
|
|
63
|
+
},
|
|
64
|
+
);
|
|
49
65
|
</script>
|
|
50
66
|
|
|
51
67
|
<template><span v-if="false" /></template>
|