@wil-works/evoke-charts 0.5.0 → 0.7.0

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/dist/index.mjs CHANGED
@@ -1,4 +1,6 @@
1
- import { $ as INTERACTION, A as squarifyTreemap, B as calendarHitTest, C as chordHitTest, D as computeSunburstGeometry, E as boxplotHitTest, F as matrixFieldExtent, G as getPadding, H as getHeatmapCategories, I as scatterGroupColors, J as resolveTickExtendedRange, K as getTheme, L as scatterPointPositions, M as computeFacetGrids, N as computeMatrixCells, O as sunburstValue, P as computePieMaxRadius, Q as DEFAULT_I18N_ZH, R as getLegendBounds, S as arcHitTest, T as vennHitTest, U as xToCategoryIndex, V as funnelStepColor, W as createAnimation, X as CHART_PALETTES, Y as updateAnimation, Z as resolveChartPalette, _ as getSliderGeometry, b as zoomToSlice, et as applyWheelZoom, f as chartOptionsSchema, g as getDataZoomConfig, h as createSvgRecorder, j as buildPieSlices, k as computeBins, m as renderChart, n as buildChartPrompt, nt as resolveCursor, p as validateOptions, q as isMissingValue, s as generateChartSpec, t as lintChartSpec, tt as pickTooltipRows, u as parseDataTable, v as windowToX, w as sankeyHitTest, x as ganttHitTest, y as xToPercent, z as getToolboxBounds } from "./ai-BmXDig6M.js";
1
+ import { $ as isMissingValue, A as vennHitTest, B as doughnutInnerRadius, C as windowToX, D as arcHitTest, E as ganttHitTest, F as squarifyTreemap, G as getToolboxBounds, H as scatterGroupColors, I as buildPieSlices, J as getHeatmapCategories, K as calendarHitTest, L as computeFacetGrids, M as computeSunburstGeometry, N as sunburstValue, O as chordHitTest, P as computeBins, Q as getTheme, R as computeMatrixCells, S as getSliderGeometry, T as zoomToSlice, U as scatterPointPositions, V as matrixFieldExtent, W as getLegendBounds, X as createAnimation, Y as xToCategoryIndex, Z as getPadding, _ as chartOptionsSchema, at as INTERACTION, b as createSvgRecorder, ct as resolveCursor, et as resolveTickExtendedRange, h as parseDataTable, it as minOf, j as boxplotHitTest, k as sankeyHitTest, l as generateChartSpec, n as buildChartPrompt, nt as waterfallSteps, ot as applyWheelZoom, q as funnelStepColor, rt as maxOf, st as pickTooltipRows, t as lintChartSpec, tt as updateAnimation, v as validateOptions, w as xToPercent, x as getDataZoomConfig, y as renderChart, z as computePieMaxRadius } from "./ai-cRi95pS2.js";
2
+ import { a as staggerProgress, i as prefersReducedMotion, n as clearSeriesPalette, r as _plugin_vue_export_helper_default, t as applySeriesPalette } from "./palette-lwVAltky.js";
3
+ import { S as DEFAULT_I18N_ZH, b as resolveChartPalette, y as CHART_PALETTES } from "./schema-CM4hKlpc.js";
2
4
  import { Fragment, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, h, inject, nextTick, normalizeClass, normalizeStyle, onMounted, onUnmounted, openBlock, provide, ref, renderList, renderSlot, resolveDynamicComponent, toDisplayString, watch, withCtx } from "vue";
3
5
  //#region src/connect.js
4
6
  var connectGroups = /* @__PURE__ */ new Map();
@@ -22,15 +24,8 @@ function broadcastConnect(group, source, apply) {
22
24
  });
23
25
  }
24
26
  //#endregion
25
- //#region \0plugin-vue:export-helper
26
- var _plugin_vue_export_helper_default = (sfc, props) => {
27
- const target = sfc.__vccOpts || sfc;
28
- for (const [key, val] of props) target[key] = val;
29
- return target;
30
- };
31
- //#endregion
32
27
  //#region src/chart.vue
33
- var _hoisted_1 = ["aria-label"];
28
+ var _hoisted_1 = ["tabindex", "aria-label"];
34
29
  var _hoisted_2 = {
35
30
  key: 1,
36
31
  class: "ev-chart__overlay"
@@ -121,7 +116,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
121
116
  switch (opt.type) {
122
117
  case "pie":
123
118
  case "doughnut":
124
- case "rose": return !opt.pieData || opt.pieData.length === 0;
119
+ case "rose": return !opt.pieData || opt.pieData.length === 0 || opt.pieData.every((d) => !(d.value > 0));
125
120
  case "scatter": return !opt.scatterData || opt.scatterData.length === 0;
126
121
  case "funnel": return !opt.funnelData || opt.funnelData.length === 0;
127
122
  case "gauge": return !opt.gauge;
@@ -140,7 +135,12 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
140
135
  case "sankey":
141
136
  case "chord":
142
137
  case "arc": return !(opt.sankeyData || opt.chordData || opt.arcData)?.nodes?.length;
143
- case "venn": return !opt.vennData || opt.vennData.length === 0;
138
+ case "venn":
139
+ if (!opt.vennData || opt.vennData.length === 0) return true;
140
+ {
141
+ const circles = opt.vennData.filter((d) => !d.sets || d.sets.length === 1);
142
+ return circles.length === 0 || circles.length > 3;
143
+ }
144
144
  case "gantt": return !opt.ganttData || opt.ganttData.length === 0;
145
145
  case "scatter-matrix": return !opt.matrixFields?.length || !opt.matrixData?.length;
146
146
  default: return !opt.labels?.length || !opt.series?.length;
@@ -236,11 +236,20 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
236
236
  data: s.data.slice(startIdx, endIdx)
237
237
  }));
238
238
  if (base.candleData) sliced.candleData = base.candleData.slice(startIdx, endIdx);
239
+ if (base.volumeData) sliced.volumeData = base.volumeData.slice(startIdx, endIdx);
239
240
  sliced.__sliceStart = startIdx;
241
+ } else if (base.candleData) {
242
+ const cn = base.candleData.length;
243
+ if (cn > 0) {
244
+ const { startIdx, endIdx } = zoomToSlice(range, cn);
245
+ sliced.candleData = base.candleData.slice(startIdx, endIdx);
246
+ if (base.volumeData) sliced.volumeData = base.volumeData.slice(startIdx, endIdx);
247
+ sliced.__sliceStart = startIdx;
248
+ }
240
249
  } else if (base.scatterData && base.scatterData.length > 0) {
241
250
  const xs = base.scatterData.map((d) => d.x);
242
- const xMin = Math.min(...xs);
243
- const span = Math.max(...xs) - xMin || 1;
251
+ const xMin = minOf(xs);
252
+ const span = maxOf(xs) - xMin || 1;
244
253
  const lo = xMin + range.start / 100 * span;
245
254
  const hi = xMin + range.end / 100 * span;
246
255
  const filtered = base.scatterData.filter((d) => d.x >= lo && d.x <= hi);
@@ -334,11 +343,11 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
334
343
  const el = tooltipRef.value;
335
344
  const tw = el?.offsetWidth || 120;
336
345
  const th = el?.offsetHeight || 40;
337
- const gap = 12;
346
+ const gap = INTERACTION.tooltip.cursorGap;
338
347
  const margin = 2;
339
348
  let legendBottom = margin;
340
349
  const legend = props.options.legend || {};
341
- if (legend.show !== false && (legend.position || "bottom") === "top") legendBottom = getPadding(props.options, containerRect.width).top;
350
+ if (legend.show !== false && (legend.position || "bottom") === "top") legendBottom = paddingFor(props.options, containerRect.width).top;
342
351
  const spaceRight = containerRect.width - relX - gap;
343
352
  const spaceLeft = relX - gap;
344
353
  let left;
@@ -408,7 +417,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
408
417
  lastHeight = height;
409
418
  canvas.width = width * dpr.value;
410
419
  canvas.height = height * dpr.value;
411
- const overlayPadding = getPadding(effectiveOptions.value, width);
420
+ const overlayPadding = paddingFor(effectiveOptions.value, width);
412
421
  overlayInfo.value = {
413
422
  plotArea: {
414
423
  x: overlayPadding.left,
@@ -420,10 +429,18 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
420
429
  options: props.options
421
430
  };
422
431
  const animCfg = animOverride || props.options.animation;
423
- const animEnabled = animate && animCfg?.enabled !== false;
432
+ const animEnabled = animate && animCfg?.enabled !== false && !prefersReducedMotion();
433
+ const stagger = staggerOf(effectiveOptions.value);
424
434
  animationState = createAnimation(animCfg);
425
435
  animationState.isAnimating = animEnabled;
426
- if (animEnabled) {
436
+ if (animEnabled && stagger > 0 && hasInterpolatable(effectiveOptions.value)) {
437
+ isEnterAnimating = true;
438
+ runDataTween(zeroSnapshot(effectiveOptions.value), animCfg, stagger, () => {
439
+ isEnterAnimating = false;
440
+ emit("animation-end");
441
+ emit("ready");
442
+ });
443
+ } else if (animEnabled) {
427
444
  isEnterAnimating = true;
428
445
  animateFrame();
429
446
  } else {
@@ -458,7 +475,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
458
475
  hoveredToolbox: hoveredToolbox.value
459
476
  };
460
477
  }
461
- let focusAnimProgress = 1;
478
+ let focusAnimProgress = emphasisSeriesName.value ? 1 : 0;
462
479
  let focusAnimFrameId = null;
463
480
  function animateFocusProgress() {
464
481
  if (focusAnimFrameId !== null) {
@@ -490,39 +507,68 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
490
507
  }
491
508
  }
492
509
  function snapshotSeriesData(opt) {
493
- const map = /* @__PURE__ */ new Map();
494
- (opt.series || []).forEach((s) => map.set(s.name, s.data.map((v) => isMissingValue(v) ? 0 : v)));
495
- return map;
510
+ const series = /* @__PURE__ */ new Map();
511
+ (opt.series || []).forEach((s) => series.set(s.name, (s.data || []).map((v) => isMissingValue(v) ? 0 : v)));
512
+ const pieData = /* @__PURE__ */ new Map();
513
+ (opt.pieData || []).forEach((d) => pieData.set(d && d.name, Number.isFinite(Number(d && d.value)) ? Number(d.value) : 0));
514
+ return {
515
+ series,
516
+ pieData
517
+ };
518
+ }
519
+ /** 有没有可插值的数值面(series / pieData 任一),都没有就不该走数据级补间 */
520
+ function hasInterpolatable(opt) {
521
+ if ((opt.series || []).some((s) => Array.isArray(s.data) && s.data.length)) return true;
522
+ return (opt.pieData || []).length > 0;
496
523
  }
497
524
  function sameStructure(a, b) {
498
525
  const la = a.labels || [];
499
526
  const lb = b.labels || [];
500
527
  if (la.length !== lb.length || la.some((v, i) => v !== lb[i])) return false;
501
- return (a.series || []).map((s) => s.name).join("|") === (b.series || []).map((s) => s.name).join("|");
528
+ if ((a.series || []).map((s) => s.name).join("|") !== (b.series || []).map((s) => s.name).join("|")) return false;
529
+ return (a.pieData || []).map((d) => d && d.name).join("|") === (b.pieData || []).map((d) => d && d.name).join("|");
502
530
  }
503
- function interpolateOptions(prev, next, t) {
531
+ function interpolateOptions(prev, next, t, stagger = 0) {
504
532
  const offset = next.__sliceStart || 0;
533
+ const cols = Math.max(1, (next.labels || []).length);
505
534
  const series = (next.series || []).map((s) => {
506
- const old = prev.get(s.name);
535
+ const old = prev.series.get(s.name);
507
536
  if (!old) return s;
508
- const data = s.data.map((v, i) => {
537
+ const data = (s.data || []).map((v, i) => {
509
538
  if (isMissingValue(v)) return v;
510
539
  const from = old[i + offset] ?? 0;
511
- return from + (v - from) * t;
540
+ const ti = staggerProgress(t, i, cols, stagger);
541
+ return from + (v - from) * ti;
512
542
  });
513
543
  return {
514
544
  ...s,
515
545
  data
516
546
  };
517
547
  });
518
- return {
548
+ const out = {
519
549
  ...next,
520
550
  series
521
551
  };
552
+ if (Array.isArray(next.pieData)) {
553
+ const sliceCount = Math.max(1, next.pieData.length);
554
+ out.pieData = next.pieData.map((d, i) => {
555
+ const to = Number(d && d.value);
556
+ if (!Number.isFinite(to)) return d;
557
+ const from = prev.pieData.get(d && d.name) ?? 0;
558
+ const ti = staggerProgress(t, i, sliceCount, stagger);
559
+ return {
560
+ ...d,
561
+ value: from + (to - from) * ti
562
+ };
563
+ });
564
+ }
565
+ return out;
522
566
  }
523
567
  function dataSignature(opt) {
524
- const total = (opt.series || []).reduce((sum, s) => sum + s.data.reduce((acc, v) => acc + (isMissingValue(v) ? 0 : v), 0), 0);
525
- return `${(opt.series || []).length}\u7CFB\u5217/${(opt.labels || []).length}\u7C7B\u76EE/\u03A3${Math.round(total)}`;
568
+ const seriesTotal = (opt.series || []).reduce((sum, s) => sum + (s.data || []).reduce((acc, v) => acc + (isMissingValue(v) ? 0 : v), 0), 0);
569
+ const pieTotal = (opt.pieData || []).reduce((sum, d) => sum + (Number.isFinite(Number(d && d.value)) ? Math.abs(Number(d.value)) : 0), 0);
570
+ const pieCount = (opt.pieData || []).length;
571
+ return `${(opt.series || []).length}\u7CFB\u5217/${pieCount}\u997C\u9879/${(opt.labels || []).length}\u7C7B\u76EE/\u03A3${Math.round(seriesTotal + pieTotal)}`;
526
572
  }
527
573
  function stopTweenAnimation() {
528
574
  if (tweenFrameId !== null) {
@@ -531,28 +577,32 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
531
577
  }
532
578
  tweenState = null;
533
579
  }
534
- function startTweenIfNeeded(prevOptions) {
535
- const nextOptions = props.options;
536
- if (nextOptions.animation?.enabled === false) return false;
537
- if (!sameStructure(prevOptions, nextOptions)) return false;
538
- const prevSnapshot = snapshotSeriesData(prevOptions);
539
- if (!(nextOptions.series || []).some((s) => {
540
- const old = prevSnapshot.get(s.name);
541
- if (!old) return false;
542
- return s.data.some((v, i) => (isMissingValue(v) ? 0 : v) !== (old[i] ?? 0));
543
- })) return false;
580
+ /** 错峰占比(0 = 关闭,沿用统一 progress 的进场动画) */
581
+ function staggerOf(opt) {
582
+ const v = opt && opt.animation ? opt.animation.stagger : void 0;
583
+ return Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 0;
584
+ }
585
+ /** 全零数据快照 — 分段进场的起点(逐条长到目标值) */
586
+ function zeroSnapshot(opt) {
587
+ const series = /* @__PURE__ */ new Map();
588
+ (opt.series || []).forEach((s) => series.set(s.name, (s.data || []).map(() => 0)));
589
+ const pieData = /* @__PURE__ */ new Map();
590
+ (opt.pieData || []).forEach((d) => pieData.set(d && d.name, 0));
591
+ return {
592
+ series,
593
+ pieData
594
+ };
595
+ }
596
+ /** 数据补间主循环 — 更新补间与分段进场共用,按类目错峰插值后以 progress = 1 重绘 */
597
+ function runDataTween(fromSnapshot, cfg, stagger, onDone) {
544
598
  stopAnimation();
545
599
  stopHoverAnimation();
546
- isEnterAnimating = false;
547
600
  stopTweenAnimation();
548
- if (!canvasRef.value) return false;
549
- tweenState = createAnimation(nextOptions.animation);
550
- const fromSig = dataSignature(prevOptions);
551
- const toSig = dataSignature(nextOptions);
601
+ tweenState = createAnimation(cfg);
552
602
  function frame() {
553
603
  if (!tweenState) return;
554
604
  const isAnimating = updateAnimation(tweenState);
555
- const tweened = interpolateOptions(prevSnapshot, effectiveOptions.value, tweenState.progress);
605
+ const tweened = interpolateOptions(fromSnapshot, effectiveOptions.value, tweenState.progress, stagger);
556
606
  try {
557
607
  renderChart(canvasRef.value, buildRenderParams(1, tweened));
558
608
  } catch (err) {
@@ -563,16 +613,40 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
563
613
  else {
564
614
  tweenFrameId = null;
565
615
  tweenState = null;
566
- emit("data-update", {
567
- from: fromSig,
568
- to: toSig
569
- });
570
- emit("animation-end");
616
+ if (onDone) onDone();
571
617
  }
572
618
  }
573
619
  tweenFrameId = requestAnimationFrame(frame);
574
620
  return true;
575
621
  }
622
+ function startTweenIfNeeded(prevOptions) {
623
+ const nextOptions = props.options;
624
+ if (nextOptions.animation?.enabled === false) return false;
625
+ if (prefersReducedMotion()) return false;
626
+ if (!hasInterpolatable(nextOptions) || !sameStructure(prevOptions, nextOptions)) return false;
627
+ const prevSnapshot = snapshotSeriesData(prevOptions);
628
+ const seriesChanged = (nextOptions.series || []).some((s) => {
629
+ const old = prevSnapshot.series.get(s.name);
630
+ if (!old) return false;
631
+ return (s.data || []).some((v, i) => (isMissingValue(v) ? 0 : v) !== (old[i] ?? 0));
632
+ });
633
+ const pieChanged = (nextOptions.pieData || []).some((d) => {
634
+ const old = prevSnapshot.pieData.get(d && d.name);
635
+ const v = Number(d && d.value);
636
+ return old !== void 0 && Number.isFinite(v) && v !== old;
637
+ });
638
+ if (!seriesChanged && !pieChanged) return false;
639
+ isEnterAnimating = false;
640
+ const fromSig = dataSignature(prevOptions);
641
+ const toSig = dataSignature(nextOptions);
642
+ return runDataTween(prevSnapshot, nextOptions.animation, staggerOf(nextOptions), () => {
643
+ emit("data-update", {
644
+ from: fromSig,
645
+ to: toSig
646
+ });
647
+ emit("animation-end");
648
+ });
649
+ }
576
650
  function handleRenderError(err) {
577
651
  const msg = err instanceof Error ? err.message : "Chart render error";
578
652
  internalError.value = msg;
@@ -604,15 +678,47 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
604
678
  cancelAnimationFrame(hoverAnimFrameId);
605
679
  hoverAnimFrameId = null;
606
680
  }
681
+ if (hoverRedrawFrameId !== null) {
682
+ cancelAnimationFrame(hoverRedrawFrameId);
683
+ hoverRedrawFrameId = null;
684
+ }
607
685
  hoverAnimDone = null;
608
686
  }
687
+ let hoverRedrawFrameId = null;
688
+ function scheduleHoverRedraw() {
689
+ if (hoverRedrawFrameId !== null) return;
690
+ hoverRedrawFrameId = requestAnimationFrame(() => {
691
+ hoverRedrawFrameId = null;
692
+ redraw();
693
+ });
694
+ }
609
695
  function usesHoverAnimation() {
610
696
  const t = props.options.type;
611
697
  return t === "pie" || t === "doughnut" || t === "rose" || t === "sunburst" || t === "treemap" || t === "radar";
612
698
  }
613
699
  function animatesHoverEnter() {
614
700
  const t = props.options.type;
615
- return t === "pie" || t === "doughnut" || t === "rose" || t === "radar" || t === "venn" || t === "sankey" || t === "chord" || t === "arc";
701
+ return [
702
+ "pie",
703
+ "doughnut",
704
+ "rose",
705
+ "radar",
706
+ "venn",
707
+ "sankey",
708
+ "chord",
709
+ "arc",
710
+ "bar",
711
+ "stacked-bar",
712
+ "horizontal-bar",
713
+ "waterfall",
714
+ "mixed",
715
+ "boxplot",
716
+ "candle",
717
+ "heatmap",
718
+ "calendar-heatmap",
719
+ "funnel",
720
+ "gantt"
721
+ ].includes(t);
616
722
  }
617
723
  function clearHover() {
618
724
  if (hoverIndex === -1) return false;
@@ -695,15 +801,14 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
695
801
  const theme = getTheme(isDark, props.options.theme, props.options.palette);
696
802
  const width = rect.width;
697
803
  const height = rect.height;
698
- const padding = getPadding(effectiveOptions.value, width);
804
+ const padding = paddingFor(effectiveOptions.value, width);
699
805
  const plotArea = {
700
806
  x: padding.left,
701
807
  y: padding.top,
702
808
  width: width - padding.left - padding.right,
703
809
  height: height - padding.top - padding.bottom
704
810
  };
705
- const ctx2d = canvas.getContext("2d");
706
- const bounds = getLegendBounds(ctx2d, effectiveOptions.value, plotArea, width, height, theme, hiddenSeries.value);
811
+ const bounds = legendBoundsFor(canvas.getContext("2d"), effectiveOptions.value, plotArea, width, height, theme, hiddenSeries.value, isDark);
707
812
  for (const bound of bounds) if (canvasX >= bound.x && canvasX <= bound.x + bound.width && canvasY >= bound.y && canvasY <= bound.y + bound.height) return bound.name;
708
813
  return null;
709
814
  }
@@ -748,7 +853,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
748
853
  const options = effectiveOptions.value;
749
854
  const width = rect.width;
750
855
  const height = rect.height;
751
- const padding = getPadding(options, width);
856
+ const padding = paddingFor(options, width);
752
857
  const plotArea = {
753
858
  x: padding.left,
754
859
  y: padding.top,
@@ -770,12 +875,12 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
770
875
  const dx = canvasX - centerX;
771
876
  const dy = canvasY - centerY;
772
877
  const distance = Math.sqrt(dx * dx + dy * dy);
773
- const innerRadius = options.type === "doughnut" ? Math.max(0, Math.min(baseRadius - 5, baseRadius * (options.innerRadius || .6))) : 0;
878
+ const innerRadius = options.type === "doughnut" ? doughnutInnerRadius(baseRadius, options) : 0;
774
879
  let angle = Math.atan2(dy, dx) + Math.PI / 2;
775
880
  if (angle < 0) angle += Math.PI * 2;
776
881
  const total = slices.reduce((sum, d) => sum + d.value, 0);
777
882
  let currentAngle = 0;
778
- const maxVal = Math.max(...slices.map((d) => d.value));
883
+ const maxVal = maxOf(slices.map((d) => d.value));
779
884
  for (let i = 0; i < slices.length; i++) {
780
885
  const sliceAngle = slices[i].value / total * Math.PI * 2;
781
886
  if (angle >= currentAngle && angle < currentAngle + sliceAngle) {
@@ -831,14 +936,10 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
831
936
  const i = Math.floor((canvasX - plotArea.x) / categoryWidth);
832
937
  if (i < 0 || i >= labels2.length) return null;
833
938
  const wf = options.waterfall || {};
834
- const totalIdx = new Set(wf.totalIndices || []);
835
- const v = deltas[i] || 0;
836
- let cumulative = 0;
837
- for (let k = 0; k <= i; k++) {
838
- const vk = deltas[k] || 0;
839
- cumulative = totalIdx.has(k) ? vk : cumulative + vk;
840
- }
841
- const isTotal = totalIdx.has(i);
939
+ const step = waterfallSteps(deltas, wf.totalIndices)[i];
940
+ const v = step.value;
941
+ const cumulative = step.to;
942
+ const isTotal = step.isTotal;
842
943
  const color = isTotal ? wf.totalColor || theme.colors[0] : v >= 0 ? wf.increaseColor || "#dc2626" : wf.decreaseColor || "#16a34a";
843
944
  return {
844
945
  index: i,
@@ -860,20 +961,27 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
860
961
  b.min,
861
962
  b.max,
862
963
  ...showOutliers ? b.outliers || [] : []
863
- ]);
964
+ ]).filter((v) => typeof v === "number" && Number.isFinite(v));
864
965
  const axisCfg = options.boxHorizontal ? options.xAxis || {} : options.yAxis || {};
865
- const ext = resolveTickExtendedRange(axisCfg.min ?? Math.min(...allValues), axisCfg.max ?? Math.max(...allValues), axisCfg.ticks || 5);
966
+ const ext = resolveTickExtendedRange(axisCfg.min ?? (allValues.length ? minOf(allValues) : 0), axisCfg.max ?? (allValues.length ? maxOf(allValues) : 1), axisCfg.ticks || 5);
866
967
  return boxplotHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value, {
867
968
  min: ext.min,
868
969
  max: ext.max
869
970
  });
870
971
  }
871
972
  if (options.type === "calendar-heatmap") return calendarHitTest(canvasX, canvasY, plotArea, options, theme);
872
- if (options.type === "sankey") return sankeyHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value);
973
+ if (options.type === "sankey") return sankeyHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value, sankeyLayoutFor(plotArea, options, theme, hiddenSeries.value, isDark));
873
974
  if (options.type === "venn") return vennHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value);
874
975
  if (options.type === "chord") return chordHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value);
875
- if (options.type === "arc") return arcHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value);
876
- if (options.type === "gantt") return ganttHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value);
976
+ if (options.type === "arc") {
977
+ if (options.arcCircular === true) return chordHitTest(canvasX, canvasY, plotArea, {
978
+ ...options,
979
+ chordData: options.arcData,
980
+ chordMode: "curve"
981
+ }, theme, hiddenSeries.value);
982
+ return arcHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value);
983
+ }
984
+ if (options.type === "gantt") return ganttHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value, ganttLayoutFor(plotArea, options, theme, hiddenSeries.value, isDark));
877
985
  if (options.type === "scatter-matrix") {
878
986
  const fields = options.matrixFields || [];
879
987
  const records = options.matrixData || [];
@@ -1014,12 +1122,12 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1014
1122
  }
1015
1123
  return null;
1016
1124
  }
1017
- const yValues = scatterData.map((d) => d.y);
1125
+ const yValues = scatterData.map((d) => d.y).filter((v) => typeof v === "number" && Number.isFinite(v));
1018
1126
  const axisConfig = options.yAxis || {};
1019
- const rawMin = axisConfig.min ?? Math.min(...yValues);
1020
- const rawMax = axisConfig.max ?? Math.max(...yValues);
1127
+ const rawMin = axisConfig.min ?? (yValues.length ? minOf(yValues) : 0);
1128
+ const rawMax = axisConfig.max ?? (yValues.length ? maxOf(yValues) : 1);
1021
1129
  const yExt = resolveTickExtendedRange(rawMin, rawMax, axisConfig.ticks || 5);
1022
- const positions = scatterPointPositions(scatterData, {
1130
+ const positions = scatterPositionsFor(scatterData, {
1023
1131
  min: yExt.min,
1024
1132
  max: yExt.max
1025
1133
  }, plotArea, options);
@@ -1156,7 +1264,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1156
1264
  params: visibleSeries2.map((s, sIdx) => ({
1157
1265
  seriesName: s.name,
1158
1266
  name: labels2[i] || `Point ${i + 1}`,
1159
- value: s.data[i],
1267
+ value: (s.data || [])[i],
1160
1268
  color: s.color || theme.colors[options.series.indexOf(s) % theme.colors.length],
1161
1269
  dataIndex: i,
1162
1270
  seriesIndex: sIdx
@@ -1175,7 +1283,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1175
1283
  params: visibleSeries2.map((s, sIdx) => ({
1176
1284
  seriesName: s.name,
1177
1285
  name: labels2[i],
1178
- value: s.data[i],
1286
+ value: (s.data || [])[i],
1179
1287
  color: s.color || theme.colors[options.series.indexOf(s) % theme.colors.length],
1180
1288
  dataIndex: i,
1181
1289
  seriesIndex: sIdx
@@ -1206,7 +1314,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1206
1314
  const angle = i * angleStep - Math.PI / 2;
1207
1315
  const ind = indicators[i];
1208
1316
  const span = ind.max - (ind.min || 0);
1209
- const maxR = span > 0 ? Math.max(...series.map((s) => radius * (Math.max(0, (s.data[i] || 0) - (ind.min || 0)) / span))) : 0;
1317
+ const maxR = span > 0 ? maxOf(series.map((s) => radius * (Math.max(0, (s.data[i] || 0) - (ind.min || 0)) / span))) : 0;
1210
1318
  const px = centerX + Math.cos(angle) * maxR;
1211
1319
  const py = centerY + Math.sin(angle) * maxR;
1212
1320
  const dist = Math.sqrt((canvasX - px) ** 2 + (canvasY - py) ** 2);
@@ -1226,13 +1334,13 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1226
1334
  if (visibleSeries.length === 0) return null;
1227
1335
  const volume = (options.volumeData || [])[dataIndex];
1228
1336
  if (props.options.tooltip?.showAllSeries !== false && visibleSeries.length > 1) {
1229
- if (visibleSeries.every((s) => isMissingValue(s.data[dataIndex]))) return null;
1337
+ if (visibleSeries.every((s) => isMissingValue((s.data || [])[dataIndex]))) return null;
1230
1338
  return {
1231
1339
  index: dataIndex,
1232
1340
  params: visibleSeries.map((s, sIdx) => ({
1233
1341
  seriesName: s.name,
1234
1342
  name: labels[dataIndex],
1235
- value: s.data[dataIndex],
1343
+ value: (s.data || [])[dataIndex],
1236
1344
  volume,
1237
1345
  color: s.color || theme.colors[options.series.indexOf(s) % theme.colors.length],
1238
1346
  dataIndex: dataIndex + sliceStartOffset(),
@@ -1241,14 +1349,14 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1241
1349
  };
1242
1350
  } else {
1243
1351
  const series = visibleSeries[0];
1244
- if (isMissingValue(series.data[dataIndex])) return null;
1352
+ if (isMissingValue((series.data || [])[dataIndex])) return null;
1245
1353
  const color = series.color || theme.colors[0];
1246
1354
  return {
1247
1355
  index: dataIndex,
1248
1356
  params: {
1249
1357
  seriesName: series.name,
1250
1358
  name: labels[dataIndex],
1251
- value: series.data[dataIndex],
1359
+ value: (series.data || [])[dataIndex],
1252
1360
  volume,
1253
1361
  color,
1254
1362
  dataIndex: dataIndex + sliceStartOffset(),
@@ -1297,7 +1405,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1297
1405
  const canvas = canvasRef.value;
1298
1406
  if (!canvas) return null;
1299
1407
  const rect = canvas.getBoundingClientRect();
1300
- const padding = getPadding(effectiveOptions.value, rect.width);
1408
+ const padding = paddingFor(effectiveOptions.value, rect.width);
1301
1409
  const plotArea = {
1302
1410
  x: padding.left,
1303
1411
  y: padding.top,
@@ -1361,7 +1469,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1361
1469
  const rect = canvas.getBoundingClientRect();
1362
1470
  const x = e.clientX - rect.left;
1363
1471
  const y = e.clientY - rect.top;
1364
- const padding = getPadding(effectiveOptions.value, rect.width);
1472
+ const padding = paddingFor(effectiveOptions.value, rect.width);
1365
1473
  if (x >= padding.left && x <= padding.left + (rect.width - padding.left - padding.right) && y >= padding.top && y <= padding.top + (rect.height - padding.top - padding.bottom)) {
1366
1474
  touchPanning = true;
1367
1475
  touchPanStartX = x;
@@ -1384,7 +1492,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1384
1492
  const x = e.clientX - rect.left;
1385
1493
  const y = e.clientY - rect.top;
1386
1494
  if (props.options.brush?.enabled === true) {
1387
- const padding = getPadding(effectiveOptions.value, rect.width);
1495
+ const padding = paddingFor(effectiveOptions.value, rect.width);
1388
1496
  const plotArea = {
1389
1497
  x: padding.left,
1390
1498
  y: padding.top,
@@ -1479,7 +1587,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1479
1587
  const labels = options.labels || [];
1480
1588
  if (canvas && labels.length > 0) {
1481
1589
  const rect = canvas.getBoundingClientRect();
1482
- const padding = getPadding(options, rect.width);
1590
+ const padding = paddingFor(options, rect.width);
1483
1591
  const plotArea = {
1484
1592
  x: padding.left,
1485
1593
  y: padding.top,
@@ -1522,7 +1630,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1522
1630
  const rect = canvas.getBoundingClientRect();
1523
1631
  const x = e.clientX - rect.left;
1524
1632
  const y = e.clientY - rect.top;
1525
- const padding = getPadding(effectiveOptions.value, rect.width);
1633
+ const padding = paddingFor(effectiveOptions.value, rect.width);
1526
1634
  const plotArea = {
1527
1635
  x: padding.left,
1528
1636
  y: padding.top,
@@ -1550,7 +1658,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1550
1658
  tooltipVisible.value = true;
1551
1659
  redraw();
1552
1660
  }
1553
- function handlePointerMove(e) {
1661
+ function handlePointerMove(e, fromKeyboard = false) {
1554
1662
  const canvas = canvasRef.value;
1555
1663
  if (canvas) {
1556
1664
  const rect = canvas.getBoundingClientRect();
@@ -1614,11 +1722,11 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1614
1722
  } else if (usesHoverAnimation()) {
1615
1723
  stopHoverAnimation();
1616
1724
  hoverAnimProgress = 1;
1617
- redraw();
1618
- } else redraw();
1619
- if (prevIndex === -1) {
1725
+ scheduleHoverRedraw();
1726
+ } else scheduleHoverRedraw();
1727
+ if (prevIndex === -1 || fromKeyboard) {
1620
1728
  const emitParams = Array.isArray(result.params) ? result.params[0] : result.params;
1621
- emit("hover", {
1729
+ if (prevIndex === -1) emit("hover", {
1622
1730
  seriesName: emitParams.seriesName,
1623
1731
  dataIndex: emitParams.dataIndex,
1624
1732
  value: emitParams.value,
@@ -1675,6 +1783,9 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1675
1783
  }
1676
1784
  return Number.isInteger(value) ? value.toString() : parseFloat(value.toFixed(4)).toString();
1677
1785
  }
1786
+ function escapeHtml(value) {
1787
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1788
+ }
1678
1789
  function applyTooltipContent(params) {
1679
1790
  const formatter = props.options.tooltip?.formatter;
1680
1791
  if (formatter) {
@@ -1685,23 +1796,23 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1685
1796
  const title = params[0]?.name || "";
1686
1797
  const rows = pickTooltipRows(params).map((p) => `
1687
1798
  <div class="ev-chart__tooltip-item">
1688
- <span class="ev-chart__tooltip-dot" style="background: ${p.color}"></span>
1689
- <span class="ev-chart__tooltip-name">${p.seriesName}</span>
1690
- <span class="ev-chart__tooltip-value">${formatTooltipValue(p.value)}</span>
1799
+ <span class="ev-chart__tooltip-dot" style="background: ${escapeHtml(p.color)}"></span>
1800
+ <span class="ev-chart__tooltip-name">${escapeHtml(p.seriesName)}</span>
1801
+ <span class="ev-chart__tooltip-value">${escapeHtml(formatTooltipValue(p.value))}</span>
1691
1802
  </div>
1692
1803
  `).join("");
1693
1804
  tooltipContent.value = `
1694
- <div class="ev-chart__tooltip-title">${title}</div>
1805
+ <div class="ev-chart__tooltip-title">${escapeHtml(title)}</div>
1695
1806
  ${rows}
1696
1807
  `;
1697
1808
  } else {
1698
1809
  const p = params;
1699
1810
  tooltipContent.value = `
1700
- <div class="ev-chart__tooltip-title">${p.name}</div>
1811
+ <div class="ev-chart__tooltip-title">${escapeHtml(p.name)}</div>
1701
1812
  <div class="ev-chart__tooltip-item">
1702
- <span class="ev-chart__tooltip-dot" style="background: ${p.color}"></span>
1703
- <span class="ev-chart__tooltip-name">${p.seriesName}</span>
1704
- <span class="ev-chart__tooltip-value">${formatTooltipValue(p.value)}</span>
1813
+ <span class="ev-chart__tooltip-dot" style="background: ${escapeHtml(p.color)}"></span>
1814
+ <span class="ev-chart__tooltip-name">${escapeHtml(p.seriesName)}</span>
1815
+ <span class="ev-chart__tooltip-value">${escapeHtml(formatTooltipValue(p.value))}</span>
1705
1816
  </div>
1706
1817
  `;
1707
1818
  }
@@ -1718,7 +1829,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1718
1829
  else if (checkToolboxHit(e.clientX, e.clientY)) zone = "toolbox";
1719
1830
  else if (checkLegendHit(e.clientX, e.clientY)) zone = "legend";
1720
1831
  else {
1721
- const padding = getPadding(effectiveOptions.value, rect.width);
1832
+ const padding = paddingFor(effectiveOptions.value, rect.width);
1722
1833
  if (x >= padding.left && x <= rect.width - padding.right && y >= padding.top && y <= rect.height - padding.bottom) zone = "plot";
1723
1834
  else {
1724
1835
  const slider = getSliderGeo();
@@ -1759,6 +1870,67 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1759
1870
  ariaLiveText.value = "";
1760
1871
  }
1761
1872
  }
1873
+ const KEYNAV_TYPES = /* @__PURE__ */ new Set([
1874
+ "line",
1875
+ "area",
1876
+ "bar",
1877
+ "stacked-bar",
1878
+ "waterfall",
1879
+ "mixed",
1880
+ "horizontal-bar"
1881
+ ]);
1882
+ let keyboardClientX = -1;
1883
+ let keyboardClientY = -1;
1884
+ function handleChartKeydown(e) {
1885
+ const options = props.options;
1886
+ if (!KEYNAV_TYPES.has(options.type)) return;
1887
+ const count = (effectiveOptions.value.labels || []).length;
1888
+ if (count === 0) return;
1889
+ if (e.key === "Enter" || e.key === " ") {
1890
+ e.preventDefault();
1891
+ if (hoverIndex < 0 || keyboardClientX < 0) return;
1892
+ const result = getHoveredData(keyboardClientX, keyboardClientY);
1893
+ if (result) {
1894
+ const params = Array.isArray(result.params) ? result.params[0] : result.params;
1895
+ emit("click", params);
1896
+ }
1897
+ return;
1898
+ }
1899
+ const horizontal = options.type === "horizontal-bar";
1900
+ let i = hoverIndex;
1901
+ if (e.key === "Home") i = 0;
1902
+ else if (e.key === "End") i = count - 1;
1903
+ else if (horizontal && e.key === "ArrowUp") i = hoverIndex < 0 ? 0 : Math.min(count - 1, hoverIndex + 1);
1904
+ else if (horizontal && e.key === "ArrowDown") i = hoverIndex < 0 ? 0 : Math.max(0, hoverIndex - 1);
1905
+ else if (!horizontal && e.key === "ArrowRight") i = hoverIndex < 0 ? 0 : Math.min(count - 1, hoverIndex + 1);
1906
+ else if (!horizontal && e.key === "ArrowLeft") i = hoverIndex < 0 ? 0 : Math.max(0, hoverIndex - 1);
1907
+ else return;
1908
+ e.preventDefault();
1909
+ if (i === hoverIndex) return;
1910
+ const canvas = canvasRef.value;
1911
+ if (!canvas) return;
1912
+ const rect = canvas.getBoundingClientRect();
1913
+ const padding = paddingFor(effectiveOptions.value, rect.width);
1914
+ const plotWidth = rect.width - padding.left - padding.right;
1915
+ const plotHeight = rect.height - padding.top - padding.bottom;
1916
+ const clientX = horizontal ? rect.left + padding.left + plotWidth / 2 : rect.left + padding.left + (i + .5) * (plotWidth / count);
1917
+ const clientY = horizontal ? rect.top + padding.top + plotHeight - (i + .5) * (plotHeight / count) : rect.top + padding.top + plotHeight / 2;
1918
+ keyboardClientX = clientX;
1919
+ keyboardClientY = clientY;
1920
+ mouseX = clientX - rect.left;
1921
+ mouseY = clientY - rect.top;
1922
+ handlePointerMove({
1923
+ clientX,
1924
+ clientY,
1925
+ pointerType: "mouse"
1926
+ }, true);
1927
+ }
1928
+ function handleFocusOut() {
1929
+ if (clearTransientState()) {
1930
+ emit("unhover");
1931
+ ariaLiveText.value = "";
1932
+ }
1933
+ }
1762
1934
  let resizeObserver = null;
1763
1935
  function setupResizeObserver() {
1764
1936
  if (!props.responsive) return;
@@ -1806,7 +1978,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1806
1978
  labels: [...opt.labels || []],
1807
1979
  series: (opt.series || []).map((s) => ({
1808
1980
  ...s,
1809
- data: [...s.data]
1981
+ data: [...s.data || []]
1810
1982
  }))
1811
1983
  };
1812
1984
  }
@@ -1815,10 +1987,18 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1815
1987
  if (Array.isArray(value)) return value.map(cloneSpec);
1816
1988
  const out = {};
1817
1989
  Object.keys(value).forEach((k) => {
1990
+ if (k === "__proto__") return;
1818
1991
  out[k] = cloneSpec(value[k]);
1819
1992
  });
1820
1993
  return out;
1821
1994
  }
1995
+ function assignSpec(target, source) {
1996
+ Object.keys(source).forEach((k) => {
1997
+ if (k === "__proto__") return;
1998
+ target[k] = source[k];
1999
+ });
2000
+ return target;
2001
+ }
1822
2002
  const IS_DEV = typeof import.meta !== "undefined" && false;
1823
2003
  const devWarned = /* @__PURE__ */ new Set();
1824
2004
  function runDevValidation() {
@@ -1833,11 +2013,13 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1833
2013
  }
1834
2014
  let prevOptionsSnapshot = null;
1835
2015
  watch(() => props.options, (next) => {
1836
- cachedDataExtent = null;
1837
- cachedPlotArea = null;
2016
+ invalidateHitCaches();
1838
2017
  internalError.value = null;
1839
2018
  runDevValidation();
1840
- focusSeries.value = emphasisSeriesName.value;
2019
+ if (focusSeries.value !== emphasisSeriesName.value) {
2020
+ focusSeries.value = emphasisSeriesName.value;
2021
+ animateFocusProgress();
2022
+ }
1841
2023
  syncScenes();
1842
2024
  const zoom = getDataZoomConfig(props.options);
1843
2025
  const zoomKey = zoom ? JSON.stringify({
@@ -1891,6 +2073,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1891
2073
  };
1892
2074
  let unregisterConnector = null;
1893
2075
  onMounted(() => {
2076
+ focusSeries.value = emphasisSeriesName.value;
1894
2077
  render(true);
1895
2078
  syncScenes();
1896
2079
  setupResizeObserver();
@@ -1924,11 +2107,102 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1924
2107
  });
1925
2108
  let cachedDataExtent = null;
1926
2109
  let cachedPlotArea = null;
2110
+ let cachedPadding = null;
2111
+ function paddingFor(options, width) {
2112
+ if (cachedPadding && cachedPadding.options === options && cachedPadding.width === width) return cachedPadding.value;
2113
+ const value = getPadding(options, width);
2114
+ cachedPadding = {
2115
+ options,
2116
+ width,
2117
+ value
2118
+ };
2119
+ return value;
2120
+ }
2121
+ let scatterHitCache = null;
2122
+ function scatterPositionsFor(scatterData, yRange, plotArea, options) {
2123
+ const c = scatterHitCache;
2124
+ if (c && c.scatterData === scatterData && c.options === options && c.yMin === yRange.min && c.yMax === yRange.max && c.px === plotArea.x && c.py === plotArea.y && c.pw === plotArea.width && c.ph === plotArea.height) return c.positions;
2125
+ const positions = scatterPointPositions(scatterData, yRange, plotArea, options);
2126
+ scatterHitCache = {
2127
+ scatterData,
2128
+ options,
2129
+ yMin: yRange.min,
2130
+ yMax: yRange.max,
2131
+ px: plotArea.x,
2132
+ py: plotArea.y,
2133
+ pw: plotArea.width,
2134
+ ph: plotArea.height,
2135
+ positions
2136
+ };
2137
+ return positions;
2138
+ }
2139
+ let legendBoundsCache = null;
2140
+ function legendBoundsFor(ctx2d, options, plotArea, width, height, theme, hiddenSeries, isDark) {
2141
+ const c = legendBoundsCache;
2142
+ if (c && c.options === options && c.width === width && c.height === height && c.dark === isDark && c.hidden === hiddenSeries && c.px === plotArea.x && c.py === plotArea.y && c.pw === plotArea.width && c.ph === plotArea.height) return c.bounds;
2143
+ const bounds = getLegendBounds(ctx2d, options, plotArea, width, height, theme, hiddenSeries);
2144
+ legendBoundsCache = {
2145
+ options,
2146
+ width,
2147
+ height,
2148
+ dark: isDark,
2149
+ hidden: hiddenSeries,
2150
+ px: plotArea.x,
2151
+ py: plotArea.y,
2152
+ pw: plotArea.width,
2153
+ ph: plotArea.height,
2154
+ bounds
2155
+ };
2156
+ return bounds;
2157
+ }
2158
+ let ganttLayoutCache = null;
2159
+ function ganttLayoutFor(plotArea, options, theme, hiddenSeries, isDark) {
2160
+ const c = ganttLayoutCache;
2161
+ if (c && c.options === options && c.dark === isDark && c.hidden === hiddenSeries && c.px === plotArea.x && c.py === plotArea.y && c.pw === plotArea.width && c.ph === plotArea.height) return c.layout;
2162
+ const layout = computeGanttLayout(plotArea, options, theme, hiddenSeries);
2163
+ ganttLayoutCache = {
2164
+ options,
2165
+ dark: isDark,
2166
+ hidden: hiddenSeries,
2167
+ px: plotArea.x,
2168
+ py: plotArea.y,
2169
+ pw: plotArea.width,
2170
+ ph: plotArea.height,
2171
+ layout
2172
+ };
2173
+ return layout;
2174
+ }
2175
+ let sankeyLayoutCache = null;
2176
+ function sankeyLayoutFor(plotArea, options, theme, hiddenSeries, isDark) {
2177
+ const c = sankeyLayoutCache;
2178
+ if (c && c.options === options && c.dark === isDark && c.hidden === hiddenSeries && c.px === plotArea.x && c.py === plotArea.y && c.pw === plotArea.width && c.ph === plotArea.height) return c.layout;
2179
+ const layout = computeSankeyLayout(plotArea, options, theme, hiddenSeries);
2180
+ sankeyLayoutCache = {
2181
+ options,
2182
+ dark: isDark,
2183
+ hidden: hiddenSeries,
2184
+ px: plotArea.x,
2185
+ py: plotArea.y,
2186
+ pw: plotArea.width,
2187
+ ph: plotArea.height,
2188
+ layout
2189
+ };
2190
+ return layout;
2191
+ }
2192
+ function invalidateHitCaches() {
2193
+ cachedDataExtent = null;
2194
+ cachedPlotArea = null;
2195
+ cachedPadding = null;
2196
+ scatterHitCache = null;
2197
+ legendBoundsCache = null;
2198
+ ganttLayoutCache = null;
2199
+ sankeyLayoutCache = null;
2200
+ }
1927
2201
  __expose({
1928
2202
  refresh: () => render(true),
1929
2203
  update(newOptions) {
1930
- Object.assign(props.options, newOptions);
1931
- cachedDataExtent = null;
2204
+ assignSpec(props.options, newOptions);
2205
+ invalidateHitCaches();
1932
2206
  specVersion.value++;
1933
2207
  debouncedRender(true);
1934
2208
  },
@@ -1938,9 +2212,18 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1938
2212
  destroy() {
1939
2213
  cleanupResizeObserver();
1940
2214
  cleanupDarkModeObserver();
2215
+ window.removeEventListener("pointerup", onWindowPointerUp);
2216
+ window.removeEventListener("keydown", handleEscapeKey);
2217
+ containerRef.value?.removeEventListener("wheel", handleWheel);
2218
+ if (unregisterConnector) {
2219
+ unregisterConnector();
2220
+ unregisterConnector = null;
2221
+ }
1941
2222
  stopAnimation();
1942
2223
  stopHoverAnimation();
2224
+ stopTweenAnimation();
1943
2225
  stopSceneTimer();
2226
+ stopFocusAnimation();
1944
2227
  if (tooltipRafId !== null) {
1945
2228
  cancelAnimationFrame(tooltipRafId);
1946
2229
  tooltipRafId = null;
@@ -1989,7 +2272,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1989
2272
  if (v < min) min = v;
1990
2273
  if (v > max) max = v;
1991
2274
  };
1992
- if (opt.series) opt.series.forEach((s) => s.data.forEach((v) => !isMissingValue(v) && consider(v)));
2275
+ if (opt.series) opt.series.forEach((s) => (s.data || []).forEach((v) => !isMissingValue(v) && consider(v)));
1993
2276
  if (opt.scatterData) opt.scatterData.forEach((p) => consider(p.y));
1994
2277
  if (opt.candleData) opt.candleData.forEach((c) => {
1995
2278
  consider(c.high);
@@ -2016,7 +2299,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
2016
2299
  };
2017
2300
  const width = canvas.width / dpr.value;
2018
2301
  const height = canvas.height / dpr.value;
2019
- const padding = getPadding(effectiveOptions.value, width);
2302
+ const padding = paddingFor(effectiveOptions.value, width);
2020
2303
  cachedPlotArea = {
2021
2304
  x: padding.left,
2022
2305
  y: padding.top,
@@ -2062,6 +2345,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
2062
2345
  mouseX: -1,
2063
2346
  mouseY: -1,
2064
2347
  showCrosshair: false,
2348
+ zoomRange: void 0,
2065
2349
  brushRect: null,
2066
2350
  hoveredToolbox: null,
2067
2351
  focusSeries: null
@@ -2082,7 +2366,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
2082
2366
  if (spec === null || typeof spec !== "object") return;
2083
2367
  const next = cloneSpec(spec);
2084
2368
  Object.keys(props.options).forEach((k) => delete props.options[k]);
2085
- Object.assign(props.options, next);
2369
+ assignSpec(props.options, next);
2086
2370
  hiddenSeries.value = /* @__PURE__ */ new Set();
2087
2371
  focusSeries.value = null;
2088
2372
  brushRect.value = null;
@@ -2104,8 +2388,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
2104
2388
  p: zoom.position,
2105
2389
  h: zoom.height
2106
2390
  }) : "";
2107
- cachedDataExtent = null;
2108
- cachedPlotArea = null;
2391
+ invalidateHitCaches();
2109
2392
  specVersion.value++;
2110
2393
  debouncedRender(true);
2111
2394
  },
@@ -2150,7 +2433,10 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
2150
2433
  class: "ev-chart",
2151
2434
  style: normalizeStyle([containerStyle.value, { cursor: cursorStyle.value }]),
2152
2435
  role: "img",
2153
- "aria-label": props.options.ariaLabel || ariaLabelText.value
2436
+ tabindex: isEmpty.value || internalError.value ? -1 : 0,
2437
+ "aria-label": props.options.ariaLabel || ariaLabelText.value,
2438
+ onKeydown: handleChartKeydown,
2439
+ onFocusout: handleFocusOut
2154
2440
  }, [
2155
2441
  !isEmpty.value && !internalError.value ? (openBlock(), createElementBlock("canvas", {
2156
2442
  key: 0,
@@ -2229,10 +2515,10 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
2229
2515
  _: 1
2230
2516
  }),
2231
2517
  createElementVNode("div", _hoisted_11, toDisplayString(ariaLiveText.value), 1)
2232
- ], 12, _hoisted_1);
2518
+ ], 44, _hoisted_1);
2233
2519
  };
2234
2520
  }
2235
- }), [["__scopeId", "data-v-df8a90cc"]]);
2521
+ }), [["__scopeId", "data-v-af1c6d5e"]]);
2236
2522
  //#endregion
2237
2523
  //#region src/useChart.js
2238
2524
  var USE_CHART_KEY = /* @__PURE__ */ Symbol("ev-chart-context");
@@ -2296,95 +2582,6 @@ function injectChartContext() {
2296
2582
  return inject(USE_CHART_KEY, null);
2297
2583
  }
2298
2584
  //#endregion
2299
- //#region src/palette.js
2300
- /**
2301
- * 配色方案应用工具 — 中性契约的官方实现
2302
- *
2303
- * 把一套数据色板写入 --ev-color-series-1..8 槽位令牌并派发 'ev-theme-change',
2304
- * 页面上的全部图表即时重绘。任何宿主(组件库 / 站点 / 独立页面)都可直接调用,
2305
- * 不必各自实现令牌写入与事件广播。
2306
- *
2307
- * palette 两种形态:
2308
- * - string[] 单套色值,明暗模式共用;
2309
- * - { light, dark } 浅/暗两组,随 html.dark 自动换挡(经注入样式表实现)。
2310
- *
2311
- * clearSeriesPalette() 移除覆写(内联属性 + 注入样式表),回到内置成套色板。
2312
- */
2313
- var SLOT_COUNT = 8;
2314
- var PALETTE_EVENT = "ev-theme-change";
2315
- var STYLE_ID = "data-ev-series-palette";
2316
- var paletteStyleEl = null;
2317
- function dispatchPaletteChange(dark) {
2318
- if (typeof document === "undefined") return;
2319
- const isDark = typeof dark === "boolean" ? dark : document.documentElement.classList.contains("dark");
2320
- document.documentElement.dispatchEvent(new CustomEvent(PALETTE_EVENT, {
2321
- bubbles: true,
2322
- detail: { dark: isDark }
2323
- }));
2324
- }
2325
- function removeInlineSlots() {
2326
- if (typeof document === "undefined") return;
2327
- for (let i = 1; i <= SLOT_COUNT; i++) document.documentElement.style.removeProperty(`--ev-color-series-${i}`);
2328
- }
2329
- function ensurePaletteStyleEl() {
2330
- if (typeof document === "undefined") return null;
2331
- if (!paletteStyleEl || !paletteStyleEl.isConnected) {
2332
- paletteStyleEl = document.createElement("style");
2333
- paletteStyleEl.setAttribute(STYLE_ID, "");
2334
- document.head.appendChild(paletteStyleEl);
2335
- }
2336
- return paletteStyleEl;
2337
- }
2338
- function normalizeSlots(colors) {
2339
- const out = {};
2340
- if (!Array.isArray(colors)) return out;
2341
- colors.slice(0, SLOT_COUNT).forEach((color, i) => {
2342
- if (color) out[`--ev-color-series-${i + 1}`] = color;
2343
- });
2344
- return out;
2345
- }
2346
- /**
2347
- * 应用一套数据色板
2348
- * @param {string[] | { light: string[], dark: string[] }} palette
2349
- * 数组:写入内联槽位(明暗共用);{ light, dark }:注入样式表,随 html.dark 自动换挡
2350
- * @param {{ dark?: boolean, target?: HTMLElement }} [options]
2351
- * dark 手动指定当前暗色态(默认探测 html.dark);target 改写注入目标
2352
- * @returns {boolean} 是否写入成功
2353
- */
2354
- function applySeriesPalette(palette, options = {}) {
2355
- if (typeof document === "undefined") return false;
2356
- const el = options.target ?? document.documentElement;
2357
- if (!el || !palette) return false;
2358
- if (Array.isArray(palette)) {
2359
- paletteStyleEl?.remove();
2360
- paletteStyleEl = null;
2361
- for (const [name, value] of Object.entries(normalizeSlots(palette))) el.style.setProperty(name, value);
2362
- dispatchPaletteChange(options.dark);
2363
- return true;
2364
- }
2365
- const { light, dark } = palette;
2366
- if (!Array.isArray(light) && !Array.isArray(dark)) return false;
2367
- removeInlineSlots();
2368
- const styleEl = ensurePaletteStyleEl();
2369
- if (styleEl) {
2370
- const vars = (colors) => colors.map((c, i) => `--ev-color-series-${i + 1}: ${c}`).join(";");
2371
- styleEl.textContent = `${light ? `:root { ${vars(light)}; }` : ""}${dark ? ` html.dark { ${vars(dark)}; }` : ""}`;
2372
- }
2373
- dispatchPaletteChange(options.dark);
2374
- return true;
2375
- }
2376
- /**
2377
- * 清除配色方案覆写(内联槽位 + 注入样式表),回到内置成套色板
2378
- */
2379
- function clearSeriesPalette(options = {}) {
2380
- if (typeof document === "undefined") return false;
2381
- paletteStyleEl?.remove();
2382
- paletteStyleEl = null;
2383
- removeInlineSlots();
2384
- dispatchPaletteChange(options.dark);
2385
- return true;
2386
- }
2387
- //#endregion
2388
2585
  //#region src/index.js
2389
2586
  var components = { EvChart: chart_default };
2390
2587
  function install(app, _options = {}) {