@wick-charts/vue 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ function r(n) {
2
+ return Math.max(1, Math.round(n));
3
+ }
4
+ function t(n) {
5
+ return r(n) % 2 === 1 ? 0.5 : 0;
6
+ }
7
+ export {
8
+ t as crispCenterOffset,
9
+ r as crispLineWidth
10
+ };
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.
@@ -725,6 +760,15 @@ export declare interface AnimationsConfig {
725
760
  * @default {@link DEFAULT_TICKS_MS}
726
761
  */
727
762
  ticks?: AnimationTime;
763
+ /**
764
+ * Gridline layer opacity: the opening reveal on first paint and the
765
+ * fade out when `grid.visible` flips off. Independent of
766
+ * {@link ticks}, which owns the per-tick cross-fade on relabel.
767
+ * `false` makes the grid appear and disappear instantly.
768
+ *
769
+ * @default {@link DEFAULT_GRID_MS}
770
+ */
771
+ grid?: AnimationTime;
728
772
  };
729
773
  /**
730
774
  * Series-visibility toggle duration. Drives BOTH the renderer's alpha
@@ -2117,6 +2161,13 @@ export declare class ChartInstance extends EventEmitter<ChartEvents> implements
2117
2161
  color: string;
2118
2162
  }[] | null;
2119
2163
  getSeriesIds(): string[];
2164
+ /**
2165
+ * Whether pan/zoom gestures currently apply — true when any visible series
2166
+ * has a time axis. Part of the {@link PanZoomTarget} surface: on a
2167
+ * spatial-only chart (pie, heatmap) the interaction layer stops capturing
2168
+ * wheel and touch events, so the page scrolls normally under the cursor.
2169
+ */
2170
+ canPanZoom(): boolean;
2120
2171
  /**
2121
2172
  * Filter `getSeriesIds()` by renderer type. A concrete kind (`'pie'`,
2122
2173
  * `'heatmap'`, a custom kind, …) returns series of that exact kind;
@@ -2234,6 +2285,14 @@ export declare class ChartInstance extends EventEmitter<ChartEvents> implements
2234
2285
  setGrid(grid: {
2235
2286
  visible: boolean;
2236
2287
  }): void;
2288
+ /**
2289
+ * Set or clear the edge fade masks (see {@link ChartOptions.fade}).
2290
+ * Content the main layer draws inside a zone dissolves toward that edge.
2291
+ * `right` omitted re-arms its default (track the Y-axis width); pass
2292
+ * `{ top: 0, right: 0, left: 0 }` to turn every mask off. Takes effect on
2293
+ * the next render frame.
2294
+ */
2295
+ setFade(fade: ChartOptions['fade']): void;
2237
2296
  /**
2238
2297
  * Set the visual state for one side of the chart. Typically called in
2239
2298
  * response to the `onEdgeReached` callback:
@@ -2374,6 +2433,18 @@ export declare interface ChartOptions {
2374
2433
  grid?: {
2375
2434
  visible: boolean;
2376
2435
  };
2436
+ /**
2437
+ * Soft alpha fade applied at the edges of the plot area. Anything the main
2438
+ * layer draws inside a zone (series, grid, time-region bands) dissolves to
2439
+ * transparent as it approaches the edge: horizontally sliding under the
2440
+ * Y-axis labels on the right (`right`, **on by default**), toward a
2441
+ * floating Title / InfoBar at the top (`top`), or out the left edge
2442
+ * (`left`). The mask *erases* pixels rather than painting a cover color,
2443
+ * so it stays correct over any container background — including the
2444
+ * default CSS gradient. Live — `chart.setFade()` updates it after
2445
+ * construction.
2446
+ */
2447
+ fade?: FadeConfig;
2377
2448
  /**
2378
2449
  * Animation control. Grouped as `axis: { y, x, ticks }` (axis-side
2379
2450
  * behaviour), `toggle` (series visibility — alpha + Y refit), and
@@ -2741,6 +2812,27 @@ declare class EventEmitter<Events = Record<string, Listener>> {
2741
2812
  removeAllListeners(): void;
2742
2813
  }
2743
2814
 
2815
+ /** Shape of {@link ChartOptions.fade}. */
2816
+ declare interface FadeConfig {
2817
+ /** Fade-zone height in CSS pixels, measured down from the top of the plot
2818
+ * area. `0` / omitted disables the mask. */
2819
+ top?: number;
2820
+ /**
2821
+ * Total width in CSS pixels of the horizontal dissolve at the right edge.
2822
+ * The ramp finishes just inside the Y-axis column — 12px past the pane
2823
+ * edge, before the right-anchored label glyphs start — so content melts
2824
+ * on approach (pan / tail-scroll exit) and is fully gone before it can
2825
+ * cross any axis text; the rest of the width runs backward into the pane
2826
+ * as a soft lead-in. **Defaults to a 60px ramp** — on out of the box;
2827
+ * `0` disables. A no-op while the Y axis is hidden.
2828
+ */
2829
+ right?: number;
2830
+ /** Fade-zone width in CSS pixels at the left pane edge — content panning
2831
+ * out to the left dissolves instead of hard-clipping at the canvas
2832
+ * boundary. `0` / omitted disables. */
2833
+ left?: number;
2834
+ }
2835
+
2744
2836
  /**
2745
2837
  * Plain staggered fade-in, no geometry motion. Works for both bar and
2746
2838
  * candlestick series.
@@ -3680,6 +3772,13 @@ declare interface PanZoomTarget {
3680
3772
  * `chartWidth` participates in soft-bound math the same way as in `pan`.
3681
3773
  */
3682
3774
  zoomAt(centerTime: number, factor: number, chartWidth?: number): void;
3775
+ /**
3776
+ * Whether pan/zoom gestures currently apply to this target. A spatial-only
3777
+ * chart (pie, heatmap) has no time axis to gesture on, so the interaction
3778
+ * layer leaves wheel and touch events to the page — scrolling past the
3779
+ * chart keeps working. Targets that omit the hook are always pan/zoomable.
3780
+ */
3781
+ canPanZoom?(): boolean;
3683
3782
  }
3684
3783
 
3685
3784
  /**
@@ -5009,7 +5108,17 @@ export declare class XScale {
5009
5108
  formatX(timestamp: number, interval: number): string;
5010
5109
  private resetHysteresis;
5011
5110
  timeToX(time: number): number;
5111
+ /** X in physical (bitmap) pixels, snapped to the pixel grid — for
5112
+ * axis-aligned edges that have to stay crisp. */
5012
5113
  timeToBitmapX(time: number): number;
5114
+ /** Same in physical pixels, off the grid — see {@link YScale.valueToBitmapYExact}. */
5115
+ timeToBitmapXExact(time: number): number;
5116
+ /**
5117
+ * X position in CSS pixels of the *stroke center* the canvas grid draws for
5118
+ * `time`. DOM axis labels position off this instead of raw {@link timeToX}
5119
+ * so text and gridline share one pixel grid.
5120
+ */
5121
+ timeToSnappedX(time: number): number;
5013
5122
  xToTime(x: number): number;
5014
5123
  pixelDeltaToTimeDelta(pixelDelta: number): number;
5015
5124
  barWidthMedia(dataInterval: number): number;
@@ -5131,8 +5240,21 @@ export declare class YScale {
5131
5240
  setTickGenerator(fn: ValueTickGenerator | null): void;
5132
5241
  /** Convert a value to a Y position in CSS (media) pixels. */
5133
5242
  valueToY(value: number): number;
5134
- /** Convert a value to a Y position in physical (bitmap) pixels. */
5243
+ /** Convert a value to a Y position in physical (bitmap) pixels, snapped to
5244
+ * the pixel grid — for axis-aligned edges that have to stay crisp. */
5135
5245
  valueToBitmapY(value: number): number;
5246
+ /**
5247
+ * Same in physical pixels, off the grid. Anti-aliased geometry reads this
5248
+ * one: snapping buys a diagonal stroke no sharpness, and during a slow
5249
+ * animation it quantises the motion into whole-pixel jumps.
5250
+ */
5251
+ valueToBitmapYExact(value: number): number;
5252
+ /**
5253
+ * Y position in CSS pixels of the *stroke center* the canvas grid draws for
5254
+ * `value`. DOM axis labels position off this instead of raw {@link valueToY}
5255
+ * so text and gridline share one pixel grid.
5256
+ */
5257
+ valueToSnappedY(value: number): number;
5136
5258
  /** Convert a Y position in CSS pixels back to a value. */
5137
5259
  yToValue(y: number): number;
5138
5260
  /**
@@ -1,8 +1,9 @@
1
1
  import { defineComponent as q, useSlots as j, computed as n, ref as I, onMounted as G, onUnmounted as P, openBlock as w, createElementBlock as B, Fragment as A, renderSlot as W, createElementVNode as _, normalizeStyle as T, unref as H, createVNode as J, createCommentVNode as K } from "vue";
2
- import { resolveAnimate as Q, prefersReducedMotion as X, firstVisibleValue as Z, BadgeAnimator as ee } from "../core/dist/chart/badge-animator.js";
2
+ import { prefersReducedMotion as Q } from "../core/dist/animation/reduced-motion.js";
3
+ import { resolveAnimate as X, firstVisibleValue as Z, BadgeAnimator as ee } from "../core/dist/chart/badge-animator.js";
3
4
  import { useChartInstance as te } from "../context.js";
4
5
  import ne from "./NumberFlow.vue.js";
5
- const re = /* @__PURE__ */ q({
6
+ const ie = /* @__PURE__ */ q({
6
7
  __name: "YLabel",
7
8
  props: {
8
9
  seriesId: {},
@@ -12,18 +13,18 @@ const re = /* @__PURE__ */ q({
12
13
  },
13
14
  setup(z) {
14
15
  const f = z, e = te(), Y = j(), E = n(() => typeof Y.default == "function"), i = I(0), u = I(null);
15
- let m = null, g = null, o = null;
16
- const M = n(() => Q(f.animate));
17
- function S() {
16
+ let p = null, g = null, o = null;
17
+ const M = n(() => X(f.animate));
18
+ function L() {
18
19
  o = null;
19
20
  const t = s.value, l = t !== null ? e.getStackedLastValue(t) : null;
20
21
  if (t === null || !l) {
21
- m = null, g = null;
22
+ p = null, g = null;
22
23
  return;
23
24
  }
24
25
  const v = l.value;
25
- (m === null || g !== t) && (m = new ee(v), g = t);
26
- const { glide: d, countUp: U, settleMs: $ } = M.value, F = e.getIntroFront(t), c = m.frame({
26
+ (p === null || g !== t) && (p = new ee(v), g = t);
27
+ const { glide: d, countUp: U, settleMs: $ } = M.value, F = e.getIntroFront(t), c = p.frame({
27
28
  target: v,
28
29
  // Only needed for the intro count-up; skip the visible-range binary search
29
30
  // entirely once the reveal has settled (the hot streaming path).
@@ -33,20 +34,20 @@ const re = /* @__PURE__ */ q({
33
34
  glide: d,
34
35
  countUp: U,
35
36
  settleMs: $,
36
- reducedMotion: X()
37
+ reducedMotion: Q()
37
38
  }), y = u.value;
38
- (y === null || y.positionValue !== c.positionValue || y.textValue !== c.textValue || y.opacity !== c.opacity) && (u.value = { positionValue: c.positionValue, textValue: c.textValue, opacity: c.opacity }), c.animating && o === null && (o = requestAnimationFrame(S));
39
+ (y === null || y.positionValue !== c.positionValue || y.textValue !== c.textValue || y.opacity !== c.opacity) && (u.value = { positionValue: c.positionValue, textValue: c.textValue, opacity: c.opacity }), c.animating && o === null && (o = requestAnimationFrame(L));
39
40
  }
40
41
  function N() {
41
- o === null && (o = requestAnimationFrame(S));
42
+ o === null && (o = requestAnimationFrame(L));
42
43
  }
43
- const p = () => {
44
+ const m = () => {
44
45
  i.value++, N();
45
46
  };
46
47
  G(() => {
47
- e.setYLabel(!0), e.on("overlayChange", p), e.on("viewportChange", p), e.getSeriesIds().length > 0 && p();
48
+ e.setYLabel(!0), e.on("overlayChange", m), e.on("viewportChange", m), e.getSeriesIds().length > 0 && m();
48
49
  }), P(() => {
49
- e.setYLabel(!1), e.off("overlayChange", p), e.off("viewportChange", p), o !== null && cancelAnimationFrame(o), o = null;
50
+ e.setYLabel(!1), e.off("overlayChange", m), e.off("viewportChange", m), o !== null && cancelAnimationFrame(o), o = null;
50
51
  });
51
52
  function O(t, l) {
52
53
  if (l !== void 0) return l;
@@ -55,11 +56,11 @@ const re = /* @__PURE__ */ q({
55
56
  const d = t.getSeriesIdsByType("time", { visibleOnly: !0 });
56
57
  return d.length > 0 ? d[0] : null;
57
58
  }
58
- const s = n(() => (i.value, O(e, f.seriesId))), a = n(() => (i.value, s.value !== null ? e.getStackedLastValue(s.value) : null)), b = n(() => (i.value, s.value !== null ? e.getPreviousClose(s.value) : null)), r = n(() => (i.value, e.getTheme())), R = n(() => u.value !== null ? u.value.positionValue : a.value?.value ?? 0), D = n(() => u.value !== null ? u.value.textValue : a.value?.value ?? 0), h = n(() => u.value !== null ? u.value.opacity : s.value === null ? 1 : e.getIntroFront(s.value) < 1 ? 0 : 1), x = n(() => (i.value, a.value === null ? 0 : e.yScale.valueToY(R.value))), L = n(() => a.value === null || b.value === null ? "neutral" : a.value.value > b.value ? "up" : a.value.value < b.value ? "down" : "neutral"), V = n(() => {
59
+ const s = n(() => (i.value, O(e, f.seriesId))), a = n(() => (i.value, s.value !== null ? e.getStackedLastValue(s.value) : null)), b = n(() => (i.value, s.value !== null ? e.getPreviousClose(s.value) : null)), r = n(() => (i.value, e.getTheme())), R = n(() => u.value !== null ? u.value.positionValue : a.value?.value ?? 0), D = n(() => u.value !== null ? u.value.textValue : a.value?.value ?? 0), h = n(() => u.value !== null ? u.value.opacity : s.value === null ? 1 : e.getIntroFront(s.value) < 1 ? 0 : 1), x = n(() => (i.value, a.value === null ? 0 : e.yScale.valueToY(R.value))), S = n(() => a.value === null || b.value === null ? "neutral" : a.value.value > b.value ? "up" : a.value.value < b.value ? "down" : "neutral"), V = n(() => {
59
60
  if (a.value === null) return r.value.yLabel.neutralBackground;
60
61
  if (!a.value.isLive) return r.value.axis.textColor;
61
62
  if (f.color) return f.color;
62
- switch (L.value) {
63
+ switch (S.value) {
63
64
  case "up":
64
65
  return r.value.yLabel.upBackground;
65
66
  case "down":
@@ -87,7 +88,7 @@ const re = /* @__PURE__ */ q({
87
88
  y: x.value,
88
89
  bgColor: V.value,
89
90
  isLive: a.value.isLive,
90
- direction: L.value,
91
+ direction: S.value,
91
92
  format: C.value,
92
93
  opacity: h.value
93
94
  }) : (w(), B(A, { key: 1 }, [
@@ -134,5 +135,5 @@ const re = /* @__PURE__ */ q({
134
135
  }
135
136
  });
136
137
  export {
137
- re as default
138
+ ie as default
138
139
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wick-charts/vue",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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.0"
48
+ "@wick-charts/core": "^0.6.2"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "vite build"
@@ -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(() => (props.headerLayout === 'overlay' ? topOverlayHeight.value : 0));
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'];