@wick-charts/vue 0.6.0 → 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 +27 -3
- package/dist/ChartContainer.vue.js +99 -69
- package/dist/core/dist/chart/options.js +27 -16
- package/dist/core/dist/chart.js +566 -366
- package/dist/core/dist/components/crosshair.js +8 -6
- package/dist/core/dist/interactions/handler.js +36 -20
- package/dist/index.d.ts +90 -0
- package/package.json +2 -2
- package/src/ChartContainer.vue +116 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
function
|
|
2
|
-
const { context: t, bitmapSize:
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
t.
|
|
1
|
+
function u(m) {
|
|
2
|
+
const { scope: x, bitmapX: o, bitmapY: i, theme: M, pane: a } = m, { context: t, bitmapSize: h, horizontalPixelRatio: e, verticalPixelRatio: p } = x, n = o <= a.width, r = i <= a.height;
|
|
3
|
+
if (!n && !r) return;
|
|
4
|
+
const s = Math.max(1, Math.round(p)), c = Math.max(1, Math.round(e));
|
|
5
|
+
t.strokeStyle = M.crosshair.color, t.lineWidth = Math.max(s, c), t.setLineDash([4 * e, 4 * e]);
|
|
6
|
+
const l = Math.round(o) + (c % 2 === 1 ? 0.5 : 0), d = Math.round(i) + (s % 2 === 1 ? 0.5 : 0);
|
|
7
|
+
t.beginPath(), n && (t.moveTo(l, 0), t.lineTo(l, h.height)), r && (t.moveTo(0, d), t.lineTo(h.width, d)), t.stroke(), t.setLineDash([]);
|
|
6
8
|
}
|
|
7
9
|
export {
|
|
8
|
-
|
|
10
|
+
u as renderCrosshair
|
|
9
11
|
};
|
|
@@ -6,8 +6,8 @@ function i(o, e) {
|
|
|
6
6
|
const t = o.clientX - e.clientX, s = o.clientY - e.clientY;
|
|
7
7
|
return Math.hypot(t, s);
|
|
8
8
|
}
|
|
9
|
-
const d = 5,
|
|
10
|
-
class
|
|
9
|
+
const d = 5, m = 500;
|
|
10
|
+
class T extends a {
|
|
11
11
|
zoom;
|
|
12
12
|
pan;
|
|
13
13
|
canvas;
|
|
@@ -15,13 +15,27 @@ class M extends a {
|
|
|
15
15
|
yScale;
|
|
16
16
|
target;
|
|
17
17
|
constructor(e, t, s, n) {
|
|
18
|
-
super(), this.canvas = e, this.target = t, this.timeScale = s, this.yScale = n, this.zoom = new r(t, s), this.pan = new u(t, s, e), e.style.cursor = "crosshair",
|
|
18
|
+
super(), this.canvas = e, this.target = t, this.timeScale = s, this.yScale = n, this.zoom = new r(t, s), this.pan = new u(t, s, e), e.style.cursor = "crosshair", this.syncPanZoomMode(), e.addEventListener("wheel", this.onWheel, { passive: !1 }), e.addEventListener("mousedown", this.onMouseDown), e.addEventListener("mousemove", this.onMouseMove), e.addEventListener("mouseup", this.onMouseUp), e.addEventListener("mouseleave", this.onMouseLeave), e.addEventListener("click", this.onClick), e.addEventListener("dblclick", this.onDblClick), e.addEventListener("touchstart", this.onTouchStart, { passive: !1 }), e.addEventListener("touchmove", this.onTouchMove, { passive: !1 }), e.addEventListener("touchend", this.onTouchEnd), e.addEventListener("touchcancel", this.onTouchCancel);
|
|
19
|
+
}
|
|
20
|
+
/** Live read of the target's capability — series can be added or removed at any time. */
|
|
21
|
+
panZoomEnabled() {
|
|
22
|
+
return this.target.canPanZoom?.() ?? !0;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Reflect the target's pan/zoom capability into the canvas CSS. While
|
|
26
|
+
* pan/zoomable, `touch-action: none` hands every touch gesture to the
|
|
27
|
+
* chart; on a spatial-only target native scrolling stays enabled so the
|
|
28
|
+
* chart doesn't trap the page. The host re-invokes this whenever the
|
|
29
|
+
* series mix changes.
|
|
30
|
+
*/
|
|
31
|
+
syncPanZoomMode() {
|
|
32
|
+
this.canvas.style.touchAction = this.panZoomEnabled() ? "none" : "auto";
|
|
19
33
|
}
|
|
20
34
|
onWheel = (e) => {
|
|
21
|
-
this.zoom.handleWheel(e);
|
|
35
|
+
this.panZoomEnabled() && this.zoom.handleWheel(e);
|
|
22
36
|
};
|
|
23
37
|
onMouseDown = (e) => {
|
|
24
|
-
this.zoom.cancelPendingRebound(), this.pan.handleMouseDown(e), this.downClient = { x: e.clientX, y: e.clientY };
|
|
38
|
+
this.zoom.cancelPendingRebound(), this.panZoomEnabled() && this.pan.handleMouseDown(e), this.downClient = { x: e.clientX, y: e.clientY };
|
|
25
39
|
};
|
|
26
40
|
onMouseMove = (e) => {
|
|
27
41
|
this.pan.isDragging() && this.pan.handleMouseMove(e), this.emitCrosshair(e.offsetX, e.offsetY);
|
|
@@ -54,36 +68,38 @@ class M extends a {
|
|
|
54
68
|
// synthesize a phantom 'click'.
|
|
55
69
|
gestureHadTwoFingers = !1;
|
|
56
70
|
onTouchStart = (e) => {
|
|
57
|
-
e.preventDefault(), this.zoom.cancelPendingRebound(), this.touchCount = e.touches.length, e.touches.length === 1 ? (this.pan.handleMouseDown({
|
|
71
|
+
this.panZoomEnabled() && (e.preventDefault(), this.zoom.cancelPendingRebound(), this.touchCount = e.touches.length, e.touches.length === 1 ? (this.pan.handleMouseDown({
|
|
58
72
|
button: 0,
|
|
59
73
|
clientX: e.touches[0].clientX
|
|
60
|
-
}), this.downClient = { x: e.touches[0].clientX, y: e.touches[0].clientY }, this.touchDownTime = Date.now(), this.gestureHadTwoFingers = !1) : e.touches.length === 2 && (this.lastTouchDist = i(e.touches[0], e.touches[1]), this.gestureHadTwoFingers = !0);
|
|
74
|
+
}), this.downClient = { x: e.touches[0].clientX, y: e.touches[0].clientY }, this.touchDownTime = Date.now(), this.gestureHadTwoFingers = !1) : e.touches.length === 2 && (this.lastTouchDist = i(e.touches[0], e.touches[1]), this.gestureHadTwoFingers = !0));
|
|
61
75
|
};
|
|
62
76
|
onTouchMove = (e) => {
|
|
63
|
-
if (
|
|
64
|
-
this.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
if (this.panZoomEnabled()) {
|
|
78
|
+
if (e.preventDefault(), e.touches.length === 1 && this.touchCount === 1)
|
|
79
|
+
this.pan.handleMouseMove({
|
|
80
|
+
clientX: e.touches[0].clientX
|
|
81
|
+
});
|
|
82
|
+
else if (e.touches.length === 2) {
|
|
83
|
+
const t = i(e.touches[0], e.touches[1]), s = (e.touches[0].clientX + e.touches[1].clientX) / 2, n = this.canvas.getBoundingClientRect();
|
|
84
|
+
if (t > 0 && this.lastTouchDist > 0) {
|
|
85
|
+
const h = l(this.lastTouchDist / t, 0.1, 10), c = this.timeScale.xToTime(s - n.left);
|
|
86
|
+
this.target.zoomAt(c, h, this.timeScale.getMediaWidth());
|
|
87
|
+
}
|
|
88
|
+
this.lastTouchDist = t;
|
|
72
89
|
}
|
|
73
|
-
this.lastTouchDist = t;
|
|
74
90
|
}
|
|
75
91
|
};
|
|
76
92
|
onTouchEnd = (e) => {
|
|
77
93
|
if (e.touches.length === 0) {
|
|
78
94
|
const t = e.changedTouches[0];
|
|
79
|
-
if (this.touchCount === 1 && !this.gestureHadTwoFingers && t && !this.movedBeyondClickTolerance(t.clientX, t.clientY) && Date.now() - this.touchDownTime <=
|
|
95
|
+
if (this.touchCount === 1 && !this.gestureHadTwoFingers && t && !this.movedBeyondClickTolerance(t.clientX, t.clientY) && Date.now() - this.touchDownTime <= m) {
|
|
80
96
|
const s = this.canvas.getBoundingClientRect();
|
|
81
97
|
this.emit("click", this.posFromOffset(t.clientX - s.left, t.clientY - s.top));
|
|
82
98
|
}
|
|
83
99
|
this.pan.handleMouseUp(), this.touchCount = 0, this.lastTouchDist = 0, this.downClient = null, this.gestureHadTwoFingers = !1, this.emit("crosshairMove", null);
|
|
84
100
|
return;
|
|
85
101
|
}
|
|
86
|
-
e.touches.length === 1 && (this.touchCount = 1, this.lastTouchDist = 0, this.pan.handleMouseDown({ button: 0, clientX: e.touches[0].clientX }), this.downClient = { x: e.touches[0].clientX, y: e.touches[0].clientY }, this.touchDownTime = Date.now());
|
|
102
|
+
e.touches.length === 1 && this.touchCount === 2 && (this.touchCount = 1, this.lastTouchDist = 0, this.pan.handleMouseDown({ button: 0, clientX: e.touches[0].clientX }), this.downClient = { x: e.touches[0].clientX, y: e.touches[0].clientY }, this.touchDownTime = Date.now());
|
|
87
103
|
};
|
|
88
104
|
onTouchCancel = () => {
|
|
89
105
|
this.pan.handleMouseUp(), this.touchCount = 0, this.lastTouchDist = 0, this.downClient = null, this.gestureHadTwoFingers = !1, this.emit("crosshairMove", null);
|
|
@@ -104,5 +120,5 @@ class M extends a {
|
|
|
104
120
|
}
|
|
105
121
|
}
|
|
106
122
|
export {
|
|
107
|
-
|
|
123
|
+
T as InteractionHandler
|
|
108
124
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ import { Ref } from 'vue';
|
|
|
6
6
|
import { ShallowRef } from 'vue';
|
|
7
7
|
|
|
8
8
|
declare const __VLS_component: DefineComponent<__VLS_Props_3, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, PublicProps, Readonly<__VLS_Props_3> & Readonly<{}>, {
|
|
9
|
+
fade: boolean | {
|
|
10
|
+
top?: number;
|
|
11
|
+
overlap?: number;
|
|
12
|
+
right?: number;
|
|
13
|
+
left?: number;
|
|
14
|
+
};
|
|
9
15
|
theme: ChartTheme;
|
|
10
16
|
gradient: boolean;
|
|
11
17
|
interactive: boolean;
|
|
@@ -275,6 +281,35 @@ declare type __VLS_Props_3 = {
|
|
|
275
281
|
grid?: {
|
|
276
282
|
visible: boolean;
|
|
277
283
|
};
|
|
284
|
+
/**
|
|
285
|
+
* Soft fade-out at the top of the plot area, so series content dissolves
|
|
286
|
+
* under a floating `<Title>` / `<InfoBar>` instead of colliding with it.
|
|
287
|
+
* The mask erases canvas alpha rather than painting a cover color, so it
|
|
288
|
+
* stays correct over the default background gradient. Live.
|
|
289
|
+
*
|
|
290
|
+
* Two directions, independently controlled:
|
|
291
|
+
*
|
|
292
|
+
* **X (under the axis)** — panning content slides under the Y-axis
|
|
293
|
+
* column and dissolves instead of hard-clipping at the pane edge. **On
|
|
294
|
+
* by default** as a 60px ramp that finishes just inside the axis
|
|
295
|
+
* column, before the label glyphs; `{ right }` overrides the total
|
|
296
|
+
* ramp width in CSS px (`0` disables). `{ left }` adds the mirror zone
|
|
297
|
+
* at the left pane edge (default off).
|
|
298
|
+
*
|
|
299
|
+
* **Top (under the header)** — opt-in. `true` enables the auto zone
|
|
300
|
+
* (measured header + 24px run-out, half the header fold-in released so
|
|
301
|
+
* data rides up into the zone at rest), `{ top }` sets the zone height
|
|
302
|
+
* explicitly, and `{ overlap }` tunes how many px of the fold-in to
|
|
303
|
+
* release (0 keeps the strict fold-in). Off unless one of those is set.
|
|
304
|
+
*
|
|
305
|
+
* `false` — every mask off, strict edges everywhere.
|
|
306
|
+
*/
|
|
307
|
+
fade?: boolean | {
|
|
308
|
+
top?: number;
|
|
309
|
+
overlap?: number;
|
|
310
|
+
right?: number;
|
|
311
|
+
left?: number;
|
|
312
|
+
};
|
|
278
313
|
/**
|
|
279
314
|
* How `<Title>` and `<InfoBar>` are positioned relative to the canvas. Live.
|
|
280
315
|
* - `'overlay'` (default): absolute overlays on top of the canvas.
|
|
@@ -2117,6 +2152,13 @@ export declare class ChartInstance extends EventEmitter<ChartEvents> implements
|
|
|
2117
2152
|
color: string;
|
|
2118
2153
|
}[] | null;
|
|
2119
2154
|
getSeriesIds(): string[];
|
|
2155
|
+
/**
|
|
2156
|
+
* Whether pan/zoom gestures currently apply — true when any visible series
|
|
2157
|
+
* has a time axis. Part of the {@link PanZoomTarget} surface: on a
|
|
2158
|
+
* spatial-only chart (pie, heatmap) the interaction layer stops capturing
|
|
2159
|
+
* wheel and touch events, so the page scrolls normally under the cursor.
|
|
2160
|
+
*/
|
|
2161
|
+
canPanZoom(): boolean;
|
|
2120
2162
|
/**
|
|
2121
2163
|
* Filter `getSeriesIds()` by renderer type. A concrete kind (`'pie'`,
|
|
2122
2164
|
* `'heatmap'`, a custom kind, …) returns series of that exact kind;
|
|
@@ -2234,6 +2276,14 @@ export declare class ChartInstance extends EventEmitter<ChartEvents> implements
|
|
|
2234
2276
|
setGrid(grid: {
|
|
2235
2277
|
visible: boolean;
|
|
2236
2278
|
}): void;
|
|
2279
|
+
/**
|
|
2280
|
+
* Set or clear the edge fade masks (see {@link ChartOptions.fade}).
|
|
2281
|
+
* Content the main layer draws inside a zone dissolves toward that edge.
|
|
2282
|
+
* `right` omitted re-arms its default (track the Y-axis width); pass
|
|
2283
|
+
* `{ top: 0, right: 0, left: 0 }` to turn every mask off. Takes effect on
|
|
2284
|
+
* the next render frame.
|
|
2285
|
+
*/
|
|
2286
|
+
setFade(fade: ChartOptions['fade']): void;
|
|
2237
2287
|
/**
|
|
2238
2288
|
* Set the visual state for one side of the chart. Typically called in
|
|
2239
2289
|
* response to the `onEdgeReached` callback:
|
|
@@ -2374,6 +2424,18 @@ export declare interface ChartOptions {
|
|
|
2374
2424
|
grid?: {
|
|
2375
2425
|
visible: boolean;
|
|
2376
2426
|
};
|
|
2427
|
+
/**
|
|
2428
|
+
* Soft alpha fade applied at the edges of the plot area. Anything the main
|
|
2429
|
+
* layer draws inside a zone (series, grid, time-region bands) dissolves to
|
|
2430
|
+
* transparent as it approaches the edge: horizontally sliding under the
|
|
2431
|
+
* Y-axis labels on the right (`right`, **on by default**), toward a
|
|
2432
|
+
* floating Title / InfoBar at the top (`top`), or out the left edge
|
|
2433
|
+
* (`left`). The mask *erases* pixels rather than painting a cover color,
|
|
2434
|
+
* so it stays correct over any container background — including the
|
|
2435
|
+
* default CSS gradient. Live — `chart.setFade()` updates it after
|
|
2436
|
+
* construction.
|
|
2437
|
+
*/
|
|
2438
|
+
fade?: FadeConfig;
|
|
2377
2439
|
/**
|
|
2378
2440
|
* Animation control. Grouped as `axis: { y, x, ticks }` (axis-side
|
|
2379
2441
|
* behaviour), `toggle` (series visibility — alpha + Y refit), and
|
|
@@ -2741,6 +2803,27 @@ declare class EventEmitter<Events = Record<string, Listener>> {
|
|
|
2741
2803
|
removeAllListeners(): void;
|
|
2742
2804
|
}
|
|
2743
2805
|
|
|
2806
|
+
/** Shape of {@link ChartOptions.fade}. */
|
|
2807
|
+
declare interface FadeConfig {
|
|
2808
|
+
/** Fade-zone height in CSS pixels, measured down from the top of the plot
|
|
2809
|
+
* area. `0` / omitted disables the mask. */
|
|
2810
|
+
top?: number;
|
|
2811
|
+
/**
|
|
2812
|
+
* Total width in CSS pixels of the horizontal dissolve at the right edge.
|
|
2813
|
+
* The ramp finishes just inside the Y-axis column — 12px past the pane
|
|
2814
|
+
* edge, before the right-anchored label glyphs start — so content melts
|
|
2815
|
+
* on approach (pan / tail-scroll exit) and is fully gone before it can
|
|
2816
|
+
* cross any axis text; the rest of the width runs backward into the pane
|
|
2817
|
+
* as a soft lead-in. **Defaults to a 60px ramp** — on out of the box;
|
|
2818
|
+
* `0` disables. A no-op while the Y axis is hidden.
|
|
2819
|
+
*/
|
|
2820
|
+
right?: number;
|
|
2821
|
+
/** Fade-zone width in CSS pixels at the left pane edge — content panning
|
|
2822
|
+
* out to the left dissolves instead of hard-clipping at the canvas
|
|
2823
|
+
* boundary. `0` / omitted disables. */
|
|
2824
|
+
left?: number;
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2744
2827
|
/**
|
|
2745
2828
|
* Plain staggered fade-in, no geometry motion. Works for both bar and
|
|
2746
2829
|
* candlestick series.
|
|
@@ -3680,6 +3763,13 @@ declare interface PanZoomTarget {
|
|
|
3680
3763
|
* `chartWidth` participates in soft-bound math the same way as in `pan`.
|
|
3681
3764
|
*/
|
|
3682
3765
|
zoomAt(centerTime: number, factor: number, chartWidth?: number): void;
|
|
3766
|
+
/**
|
|
3767
|
+
* Whether pan/zoom gestures currently apply to this target. A spatial-only
|
|
3768
|
+
* chart (pie, heatmap) has no time axis to gesture on, so the interaction
|
|
3769
|
+
* layer leaves wheel and touch events to the page — scrolling past the
|
|
3770
|
+
* chart keeps working. Targets that omit the hook are always pan/zoomable.
|
|
3771
|
+
*/
|
|
3772
|
+
canPanZoom?(): boolean;
|
|
3683
3773
|
}
|
|
3684
3774
|
|
|
3685
3775
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wick-charts/vue",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "High-performance canvas timeseries charts for Vue — candlestick, line, bar, pie. Tree-shakeable, zero runtime deps.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"vue": ">=3.3.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@wick-charts/core": "^0.6.
|
|
48
|
+
"@wick-charts/core": "^0.6.1"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "vite build"
|
package/src/ChartContainer.vue
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type ChartTheme,
|
|
8
8
|
type CrosshairPosition,
|
|
9
9
|
type EdgeReachedInfo,
|
|
10
|
+
type FadeConfig,
|
|
10
11
|
type PointClickInfo,
|
|
11
12
|
type SeriesHoverInfo,
|
|
12
13
|
type VisibleRange,
|
|
@@ -91,6 +92,30 @@ const props = withDefaults(
|
|
|
91
92
|
interactive?: boolean;
|
|
92
93
|
/** Background grid configuration. Live. Default: `{ visible: true }`. */
|
|
93
94
|
grid?: { visible: boolean };
|
|
95
|
+
/**
|
|
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 };
|
|
94
119
|
/**
|
|
95
120
|
* How `<Title>` and `<InfoBar>` are positioned relative to the canvas. Live.
|
|
96
121
|
* - `'overlay'` (default): absolute overlays on top of the canvas.
|
|
@@ -168,6 +193,11 @@ const props = withDefaults(
|
|
|
168
193
|
// because it already had a default.
|
|
169
194
|
interactive: true,
|
|
170
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,
|
|
171
201
|
},
|
|
172
202
|
);
|
|
173
203
|
|
|
@@ -199,9 +229,49 @@ provide(NavigatorAnchorKey, navigatorAnchor);
|
|
|
199
229
|
let resizeObserver: ResizeObserver | null = null;
|
|
200
230
|
const topOverlayHeight = ref(0);
|
|
201
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
|
+
|
|
202
270
|
// Inline mode: browser flex already reserves header height, so folding it
|
|
203
271
|
// into padding.top would double-shift the data. Only overlay needs the fold.
|
|
204
|
-
const headerExtra = computed(() =>
|
|
272
|
+
const headerExtra = computed(() =>
|
|
273
|
+
props.headerLayout === 'overlay' ? Math.max(0, topOverlayHeight.value - fadeOverlap.value) : 0,
|
|
274
|
+
);
|
|
205
275
|
|
|
206
276
|
function applyPadding() {
|
|
207
277
|
if (!chart.value) return;
|
|
@@ -213,6 +283,43 @@ function applyPadding() {
|
|
|
213
283
|
chart.value.setPadding(merged);
|
|
214
284
|
}
|
|
215
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
|
+
|
|
216
323
|
// Wires up (or tears down) the header-height observer for the current
|
|
217
324
|
// `headerLayout`. Called on mount AND whenever `headerLayout` flips at
|
|
218
325
|
// runtime — overlay needs a live measurement, inline lets browser flex
|
|
@@ -268,6 +375,7 @@ onMounted(async () => {
|
|
|
268
375
|
if (perfAtMount !== undefined) options.perf = perfAtMount;
|
|
269
376
|
if (onEdgeReachedAtMount) options.onEdgeReached = onEdgeReachedAtMount;
|
|
270
377
|
if (props.animations !== undefined) options.animations = props.animations;
|
|
378
|
+
seedFadeOption(options);
|
|
271
379
|
chart.value = new ChartInstance(containerRef.value, options);
|
|
272
380
|
subscribeDeclarativeEvents(chart.value);
|
|
273
381
|
props.onReady?.(chart.value);
|
|
@@ -352,6 +460,7 @@ watch(
|
|
|
352
460
|
if (perfAtMount !== undefined) opts.perf = perfAtMount;
|
|
353
461
|
if (onEdgeReachedAtMount) opts.onEdgeReached = onEdgeReachedAtMount;
|
|
354
462
|
if (next !== undefined) opts.animations = next;
|
|
463
|
+
seedFadeOption(opts);
|
|
355
464
|
chart.value = new ChartInstance(containerRef.value, opts);
|
|
356
465
|
subscribeDeclarativeEvents(chart.value);
|
|
357
466
|
props.onReady?.(chart.value);
|
|
@@ -404,6 +513,12 @@ watch(
|
|
|
404
513
|
applyPadding,
|
|
405
514
|
);
|
|
406
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
|
+
|
|
407
522
|
const rootStyle = computed(() => {
|
|
408
523
|
const t = themeRef.value;
|
|
409
524
|
const [gtop, gbot] = t?.chartGradient ?? ['transparent', 'transparent'];
|