@wil-works/evoke-charts 0.4.0 → 0.6.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,4 @@
1
- import { A as xToCategoryIndex, B as INTERACTION, C as computeBins, D as computePieMaxRadius, E as buildPieSlices, F as resolveTickExtendedRange, H as pickTooltipRows, I as updateAnimation, L as CHART_PALETTES, M as getPadding, N as getTheme, O as getLegendBounds, P as isMissingValue, R as resolveChartPalette, S as sunburstValue, T as getHeatmapCategories, U as resolveCursor, V as applyWheelZoom, _ as getSliderGeometry, b as zoomToSlice, f as chartOptionsSchema, g as getDataZoomConfig, h as createSvgRecorder, j as createAnimation, k as getToolboxBounds, m as renderChart, n as buildChartPrompt, p as validateOptions, s as generateChartSpec, t as lintChartSpec, u as parseDataTable, v as windowToX, w as squarifyTreemap, x as computeSunburstGeometry, y as xToPercent, z as DEFAULT_I18N_ZH } from "./ai-BdpMydhW.js";
1
+ import { $ as resolveChartPalette, A as squarifyTreemap, B as getToolboxBounds, C as chordHitTest, D as computeSunburstGeometry, E as boxplotHitTest, F as doughnutInnerRadius, G as createAnimation, H as funnelStepColor, I as matrixFieldExtent, J as isMissingValue, K as getPadding, L as scatterGroupColors, M as computeFacetGrids, N as computeMatrixCells, O as sunburstValue, P as computePieMaxRadius, Q as CHART_PALETTES, R as scatterPointPositions, S as arcHitTest, T as vennHitTest, U as getHeatmapCategories, V as calendarHitTest, W as xToCategoryIndex, X as updateAnimation, Y as resolveTickExtendedRange, Z as waterfallSteps, _ as getSliderGeometry, at as INTERACTION, b as zoomToSlice, ct as resolveCursor, et as DEFAULT_I18N_ZH, f as chartOptionsSchema, g as getDataZoomConfig, h as createSvgRecorder, it as minOf, j as buildPieSlices, k as computeBins, m as renderChart, n as buildChartPrompt, ot as applyWheelZoom, p as validateOptions, q as getTheme, rt as maxOf, s as generateChartSpec, st as pickTooltipRows, t as lintChartSpec, u as parseDataTable, v as windowToX, w as sankeyHitTest, x as ganttHitTest, y as xToPercent, z as getLegendBounds } from "./ai-B5Fk3lBQ.js";
2
2
  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
3
  //#region src/connect.js
4
4
  var connectGroups = /* @__PURE__ */ new Map();
@@ -30,7 +30,7 @@ var _plugin_vue_export_helper_default = (sfc, props) => {
30
30
  };
31
31
  //#endregion
32
32
  //#region src/chart.vue
33
- var _hoisted_1 = ["aria-label"];
33
+ var _hoisted_1 = ["tabindex", "aria-label"];
34
34
  var _hoisted_2 = {
35
35
  key: 1,
36
36
  class: "ev-chart__overlay"
@@ -121,11 +121,12 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
121
121
  switch (opt.type) {
122
122
  case "pie":
123
123
  case "doughnut":
124
- case "rose": return !opt.pieData || opt.pieData.length === 0;
124
+ case "rose": return !opt.pieData || opt.pieData.length === 0 || opt.pieData.every((d) => !(d.value > 0));
125
125
  case "scatter": return !opt.scatterData || opt.scatterData.length === 0;
126
126
  case "funnel": return !opt.funnelData || opt.funnelData.length === 0;
127
127
  case "gauge": return !opt.gauge;
128
128
  case "heatmap": return !opt.heatmapData || opt.heatmapData.length === 0;
129
+ case "calendar-heatmap": return !opt.calendarData || opt.calendarData.length === 0;
129
130
  case "candle": return !opt.candleData || opt.candleData.length === 0;
130
131
  case "bullet": return !opt.bulletData || opt.bulletData.length === 0;
131
132
  case "treemap": return !opt.treemapData || opt.treemapData.length === 0;
@@ -136,6 +137,17 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
136
137
  case "bin": return !opt.series || opt.series.length === 0 || opt.series.every((s) => !s.data || s.data.length === 0);
137
138
  case "sparkline": return !opt.series || opt.series.length === 0 || !opt.series[0]?.data?.length;
138
139
  case "radar": return !opt.radarIndicators?.length || !opt.radarSeries?.length;
140
+ case "sankey":
141
+ case "chord":
142
+ case "arc": return !(opt.sankeyData || opt.chordData || opt.arcData)?.nodes?.length;
143
+ case "venn":
144
+ if (!opt.vennData || opt.vennData.length === 0) return true;
145
+ {
146
+ const circles = opt.vennData.filter((d) => !d.sets || d.sets.length === 1);
147
+ return circles.length === 0 || circles.length > 3;
148
+ }
149
+ case "gantt": return !opt.ganttData || opt.ganttData.length === 0;
150
+ case "scatter-matrix": return !opt.matrixFields?.length || !opt.matrixData?.length;
139
151
  default: return !opt.labels?.length || !opt.series?.length;
140
152
  }
141
153
  });
@@ -159,6 +171,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
159
171
  let hoverIndex = -1;
160
172
  let hoverAnimProgress = 0;
161
173
  let hoverAnimFrameId = null;
174
+ let hoverAnimDone = null;
162
175
  const hiddenSeries = ref(/* @__PURE__ */ new Set());
163
176
  const focusSeries = ref(null);
164
177
  const emphasisSeriesName = computed(() => {
@@ -228,11 +241,20 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
228
241
  data: s.data.slice(startIdx, endIdx)
229
242
  }));
230
243
  if (base.candleData) sliced.candleData = base.candleData.slice(startIdx, endIdx);
244
+ if (base.volumeData) sliced.volumeData = base.volumeData.slice(startIdx, endIdx);
231
245
  sliced.__sliceStart = startIdx;
246
+ } else if (base.candleData) {
247
+ const cn = base.candleData.length;
248
+ if (cn > 0) {
249
+ const { startIdx, endIdx } = zoomToSlice(range, cn);
250
+ sliced.candleData = base.candleData.slice(startIdx, endIdx);
251
+ if (base.volumeData) sliced.volumeData = base.volumeData.slice(startIdx, endIdx);
252
+ sliced.__sliceStart = startIdx;
253
+ }
232
254
  } else if (base.scatterData && base.scatterData.length > 0) {
233
255
  const xs = base.scatterData.map((d) => d.x);
234
- const xMin = Math.min(...xs);
235
- const span = Math.max(...xs) - xMin || 1;
256
+ const xMin = minOf(xs);
257
+ const span = maxOf(xs) - xMin || 1;
236
258
  const lo = xMin + range.start / 100 * span;
237
259
  const hi = xMin + range.end / 100 * span;
238
260
  const filtered = base.scatterData.filter((d) => d.x >= lo && d.x <= hi);
@@ -326,11 +348,11 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
326
348
  const el = tooltipRef.value;
327
349
  const tw = el?.offsetWidth || 120;
328
350
  const th = el?.offsetHeight || 40;
329
- const gap = 12;
351
+ const gap = INTERACTION.tooltip.cursorGap;
330
352
  const margin = 2;
331
353
  let legendBottom = margin;
332
354
  const legend = props.options.legend || {};
333
- if (legend.show !== false && (legend.position || "bottom") === "top") legendBottom = getPadding(props.options, containerRect.width).top;
355
+ if (legend.show !== false && (legend.position || "bottom") === "top") legendBottom = paddingFor(props.options, containerRect.width).top;
334
356
  const spaceRight = containerRect.width - relX - gap;
335
357
  const spaceLeft = relX - gap;
336
358
  let left;
@@ -400,7 +422,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
400
422
  lastHeight = height;
401
423
  canvas.width = width * dpr.value;
402
424
  canvas.height = height * dpr.value;
403
- const overlayPadding = getPadding(effectiveOptions.value, width);
425
+ const overlayPadding = paddingFor(effectiveOptions.value, width);
404
426
  overlayInfo.value = {
405
427
  plotArea: {
406
428
  x: overlayPadding.left,
@@ -443,15 +465,47 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
443
465
  mouseY,
444
466
  showCrosshair: hoverIndex >= 0,
445
467
  hoverAnimProgress,
468
+ focusAnimProgress,
446
469
  zoomRange: isZoomEnabled() ? zoomRange.value : void 0,
447
470
  brushRect: brushRect.value,
448
471
  focusSeries: focusSeries.value,
449
472
  hoveredToolbox: hoveredToolbox.value
450
473
  };
451
474
  }
475
+ let focusAnimProgress = emphasisSeriesName.value ? 1 : 0;
476
+ let focusAnimFrameId = null;
477
+ function animateFocusProgress() {
478
+ if (focusAnimFrameId !== null) {
479
+ cancelAnimationFrame(focusAnimFrameId);
480
+ focusAnimFrameId = null;
481
+ }
482
+ const target = focusSeries.value ? 1 : 0;
483
+ if (props.options.animation?.enabled === false || target === focusAnimProgress) {
484
+ focusAnimProgress = target;
485
+ redraw();
486
+ return;
487
+ }
488
+ const start = focusAnimProgress;
489
+ const startTime = performance.now();
490
+ const duration = 180;
491
+ function frame(now) {
492
+ const tRaw = Math.min((now - startTime) / duration, 1);
493
+ focusAnimProgress = start + (target - start) * (1 - Math.pow(1 - tRaw, 3));
494
+ redraw();
495
+ if (tRaw < 1) focusAnimFrameId = requestAnimationFrame(frame);
496
+ else focusAnimFrameId = null;
497
+ }
498
+ focusAnimFrameId = requestAnimationFrame(frame);
499
+ }
500
+ function stopFocusAnimation() {
501
+ if (focusAnimFrameId !== null) {
502
+ cancelAnimationFrame(focusAnimFrameId);
503
+ focusAnimFrameId = null;
504
+ }
505
+ }
452
506
  function snapshotSeriesData(opt) {
453
507
  const map = /* @__PURE__ */ new Map();
454
- (opt.series || []).forEach((s) => map.set(s.name, s.data.map((v) => isMissingValue(v) ? 0 : v)));
508
+ (opt.series || []).forEach((s) => map.set(s.name, (s.data || []).map((v) => isMissingValue(v) ? 0 : v)));
455
509
  return map;
456
510
  }
457
511
  function sameStructure(a, b) {
@@ -465,7 +519,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
465
519
  const series = (next.series || []).map((s) => {
466
520
  const old = prev.get(s.name);
467
521
  if (!old) return s;
468
- const data = s.data.map((v, i) => {
522
+ const data = (s.data || []).map((v, i) => {
469
523
  if (isMissingValue(v)) return v;
470
524
  const from = old[i + offset] ?? 0;
471
525
  return from + (v - from) * t;
@@ -481,7 +535,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
481
535
  };
482
536
  }
483
537
  function dataSignature(opt) {
484
- const total = (opt.series || []).reduce((sum, s) => sum + s.data.reduce((acc, v) => acc + (isMissingValue(v) ? 0 : v), 0), 0);
538
+ const total = (opt.series || []).reduce((sum, s) => sum + (s.data || []).reduce((acc, v) => acc + (isMissingValue(v) ? 0 : v), 0), 0);
485
539
  return `${(opt.series || []).length}\u7CFB\u5217/${(opt.labels || []).length}\u7C7B\u76EE/\u03A3${Math.round(total)}`;
486
540
  }
487
541
  function stopTweenAnimation() {
@@ -499,7 +553,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
499
553
  if (!(nextOptions.series || []).some((s) => {
500
554
  const old = prevSnapshot.get(s.name);
501
555
  if (!old) return false;
502
- return s.data.some((v, i) => (isMissingValue(v) ? 0 : v) !== (old[i] ?? 0));
556
+ return (s.data || []).some((v, i) => (isMissingValue(v) ? 0 : v) !== (old[i] ?? 0));
503
557
  })) return false;
504
558
  stopAnimation();
505
559
  stopHoverAnimation();
@@ -564,31 +618,72 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
564
618
  cancelAnimationFrame(hoverAnimFrameId);
565
619
  hoverAnimFrameId = null;
566
620
  }
621
+ if (hoverRedrawFrameId !== null) {
622
+ cancelAnimationFrame(hoverRedrawFrameId);
623
+ hoverRedrawFrameId = null;
624
+ }
625
+ hoverAnimDone = null;
626
+ }
627
+ let hoverRedrawFrameId = null;
628
+ function scheduleHoverRedraw() {
629
+ if (hoverRedrawFrameId !== null) return;
630
+ hoverRedrawFrameId = requestAnimationFrame(() => {
631
+ hoverRedrawFrameId = null;
632
+ redraw();
633
+ });
567
634
  }
568
635
  function usesHoverAnimation() {
569
636
  const t = props.options.type;
570
- return t === "pie" || t === "doughnut" || t === "rose" || t === "sunburst" || t === "treemap";
637
+ return t === "pie" || t === "doughnut" || t === "rose" || t === "sunburst" || t === "treemap" || t === "radar";
638
+ }
639
+ function animatesHoverEnter() {
640
+ const t = props.options.type;
641
+ return [
642
+ "pie",
643
+ "doughnut",
644
+ "rose",
645
+ "radar",
646
+ "venn",
647
+ "sankey",
648
+ "chord",
649
+ "arc",
650
+ "bar",
651
+ "stacked-bar",
652
+ "horizontal-bar",
653
+ "waterfall",
654
+ "mixed",
655
+ "boxplot",
656
+ "candle",
657
+ "heatmap",
658
+ "calendar-heatmap",
659
+ "funnel",
660
+ "gantt"
661
+ ].includes(t);
571
662
  }
572
663
  function clearHover() {
573
664
  if (hoverIndex === -1) return false;
574
- hoverIndex = -1;
575
- const t = props.options.type;
576
- if (t === "pie" || t === "doughnut" || t === "rose") startHoverAnimation(-1);
665
+ if (animatesHoverEnter()) startHoverAnimation(-1, () => {
666
+ hoverIndex = -1;
667
+ redraw();
668
+ });
577
669
  else {
670
+ hoverIndex = -1;
578
671
  stopHoverAnimation();
579
672
  hoverAnimProgress = 0;
580
673
  redraw();
581
674
  }
582
675
  return true;
583
676
  }
584
- function startHoverAnimation(dir) {
585
- if (!usesHoverAnimation()) return;
677
+ function startHoverAnimation(dir, onDone) {
678
+ if (!animatesHoverEnter()) return;
586
679
  if (props.options.animation?.enabled === false) {
587
680
  hoverAnimProgress = dir > 0 ? 1 : 0;
588
681
  redraw();
682
+ if (onDone) onDone();
589
683
  return;
590
684
  }
591
685
  stopHoverAnimation();
686
+ hoverAnimDone = onDone || null;
592
687
  const duration = 220;
593
688
  const startTime = performance.now();
594
689
  const startProgress = hoverAnimProgress;
@@ -604,7 +699,12 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
604
699
  }
605
700
  renderChart(canvasRef.value, buildRenderParams(1));
606
701
  if (tRaw < 1) hoverAnimFrameId = requestAnimationFrame(frame);
607
- else hoverAnimFrameId = null;
702
+ else {
703
+ hoverAnimFrameId = null;
704
+ const done = hoverAnimDone;
705
+ hoverAnimDone = null;
706
+ if (done) done();
707
+ }
608
708
  }
609
709
  hoverAnimFrameId = requestAnimationFrame(frame);
610
710
  }
@@ -624,7 +724,12 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
624
724
  "rose",
625
725
  "radar",
626
726
  "funnel",
627
- "scatter"
727
+ "scatter",
728
+ "sankey",
729
+ "chord",
730
+ "arc",
731
+ "venn",
732
+ "candle"
628
733
  ];
629
734
  function checkLegendHit(x, y) {
630
735
  const canvas = canvasRef.value;
@@ -636,15 +741,14 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
636
741
  const theme = getTheme(isDark, props.options.theme, props.options.palette);
637
742
  const width = rect.width;
638
743
  const height = rect.height;
639
- const padding = getPadding(effectiveOptions.value, width);
744
+ const padding = paddingFor(effectiveOptions.value, width);
640
745
  const plotArea = {
641
746
  x: padding.left,
642
747
  y: padding.top,
643
748
  width: width - padding.left - padding.right,
644
749
  height: height - padding.top - padding.bottom
645
750
  };
646
- const ctx2d = canvas.getContext("2d");
647
- const bounds = getLegendBounds(ctx2d, effectiveOptions.value, plotArea, width, height, theme, hiddenSeries.value);
751
+ const bounds = legendBoundsFor(canvas.getContext("2d"), effectiveOptions.value, plotArea, width, height, theme, hiddenSeries.value, isDark);
648
752
  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;
649
753
  return null;
650
754
  }
@@ -689,7 +793,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
689
793
  const options = effectiveOptions.value;
690
794
  const width = rect.width;
691
795
  const height = rect.height;
692
- const padding = getPadding(options, width);
796
+ const padding = paddingFor(options, width);
693
797
  const plotArea = {
694
798
  x: padding.left,
695
799
  y: padding.top,
@@ -711,12 +815,12 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
711
815
  const dx = canvasX - centerX;
712
816
  const dy = canvasY - centerY;
713
817
  const distance = Math.sqrt(dx * dx + dy * dy);
714
- const innerRadius = options.type === "doughnut" ? Math.max(0, Math.min(baseRadius - 5, baseRadius * (options.innerRadius || .6))) : 0;
818
+ const innerRadius = options.type === "doughnut" ? doughnutInnerRadius(baseRadius, options) : 0;
715
819
  let angle = Math.atan2(dy, dx) + Math.PI / 2;
716
820
  if (angle < 0) angle += Math.PI * 2;
717
821
  const total = slices.reduce((sum, d) => sum + d.value, 0);
718
822
  let currentAngle = 0;
719
- const maxVal = Math.max(...slices.map((d) => d.value));
823
+ const maxVal = maxOf(slices.map((d) => d.value));
720
824
  for (let i = 0; i < slices.length; i++) {
721
825
  const sliceAngle = slices[i].value / total * Math.PI * 2;
722
826
  if (angle >= currentAngle && angle < currentAngle + sliceAngle) {
@@ -751,7 +855,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
751
855
  const i = Math.floor(relativeY / stepHeight);
752
856
  if (i < 0 || i >= drawData.length) return null;
753
857
  const data = drawData[i];
754
- const color = data.color || theme.colors[allFunnel.indexOf(data) % theme.colors.length];
858
+ const color = funnelStepColor(data, allFunnel.indexOf(data) < 0 ? i : allFunnel.indexOf(data), theme);
755
859
  return {
756
860
  index: i,
757
861
  params: {
@@ -772,14 +876,10 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
772
876
  const i = Math.floor((canvasX - plotArea.x) / categoryWidth);
773
877
  if (i < 0 || i >= labels2.length) return null;
774
878
  const wf = options.waterfall || {};
775
- const totalIdx = new Set(wf.totalIndices || []);
776
- const v = deltas[i] || 0;
777
- let cumulative = 0;
778
- for (let k = 0; k <= i; k++) {
779
- const vk = deltas[k] || 0;
780
- cumulative = totalIdx.has(k) ? vk : cumulative + vk;
781
- }
782
- const isTotal = totalIdx.has(i);
879
+ const step = waterfallSteps(deltas, wf.totalIndices)[i];
880
+ const v = step.value;
881
+ const cumulative = step.to;
882
+ const isTotal = step.isTotal;
783
883
  const color = isTotal ? wf.totalColor || theme.colors[0] : v >= 0 ? wf.increaseColor || "#dc2626" : wf.decreaseColor || "#16a34a";
784
884
  return {
785
885
  index: i,
@@ -794,27 +894,68 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
794
894
  };
795
895
  }
796
896
  if (options.type === "boxplot") {
797
- const boxData = options.boxData || [];
798
- if (boxData.length === 0) return null;
799
- const categoryWidth = plotArea.width / boxData.length;
800
- const i = Math.floor((canvasX - plotArea.x) / categoryWidth);
801
- if (i < 0 || i >= boxData.length) return null;
802
- const b = boxData[i];
803
- const color = b.color || theme.colors[i % theme.colors.length];
897
+ const visible = (options.boxData || []).filter((b) => !(b.group && hiddenSeries.value.has(b.group)) && !hiddenSeries.value.has(b.label));
898
+ if (visible.length === 0) return null;
899
+ const showOutliers = options.showOutliers !== false;
900
+ const allValues = visible.flatMap((b) => [
901
+ b.min,
902
+ b.max,
903
+ ...showOutliers ? b.outliers || [] : []
904
+ ]);
905
+ const axisCfg = options.boxHorizontal ? options.xAxis || {} : options.yAxis || {};
906
+ const ext = resolveTickExtendedRange(axisCfg.min ?? minOf(allValues), axisCfg.max ?? maxOf(allValues), axisCfg.ticks || 5);
907
+ return boxplotHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value, {
908
+ min: ext.min,
909
+ max: ext.max
910
+ });
911
+ }
912
+ if (options.type === "calendar-heatmap") return calendarHitTest(canvasX, canvasY, plotArea, options, theme);
913
+ if (options.type === "sankey") return sankeyHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value, sankeyLayoutFor(plotArea, options, theme, hiddenSeries.value, isDark));
914
+ if (options.type === "venn") return vennHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value);
915
+ if (options.type === "chord") return chordHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value);
916
+ if (options.type === "arc") {
917
+ if (options.arcCircular === true) return chordHitTest(canvasX, canvasY, plotArea, {
918
+ ...options,
919
+ chordData: options.arcData,
920
+ chordMode: "curve"
921
+ }, theme, hiddenSeries.value);
922
+ return arcHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value);
923
+ }
924
+ if (options.type === "gantt") return ganttHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value, ganttLayoutFor(plotArea, options, theme, hiddenSeries.value, isDark));
925
+ if (options.type === "scatter-matrix") {
926
+ const fields = options.matrixFields || [];
927
+ const records = options.matrixData || [];
928
+ if (fields.length === 0 || records.length === 0) return null;
929
+ const cells = computeMatrixCells(plotArea, fields);
930
+ const cell = cells.find((c) => canvasX >= c.x && canvasX <= c.x + c.width && canvasY >= c.y && canvasY <= c.y + c.height);
931
+ if (!cell || cell.row === cell.col) return null;
932
+ const extX = matrixFieldExtent(records, cell.xField);
933
+ const extY = matrixFieldExtent(records, cell.yField);
934
+ const px = (v) => cell.x + 5 + (v - extX.min) / (extX.max - extX.min) * (cell.width - 10);
935
+ const py = (v) => cell.y + cell.height - 5 - (v - extY.min) / (extY.max - extY.min) * (cell.height - 10);
936
+ let nearest = -1;
937
+ let minDist = Infinity;
938
+ records.forEach((record, ri) => {
939
+ const xv = record[cell.xField];
940
+ const yv = record[cell.yField];
941
+ if (typeof xv !== "number" || typeof yv !== "number") return;
942
+ const dist = Math.sqrt((canvasX - px(xv)) ** 2 + (canvasY - py(yv)) ** 2);
943
+ if (dist < minDist) {
944
+ minDist = dist;
945
+ nearest = ri;
946
+ }
947
+ });
948
+ if (nearest < 0 || minDist > 18) return null;
949
+ const record = records[nearest];
950
+ const cellIndex = cells.indexOf(cell);
804
951
  return {
805
- index: i,
952
+ index: cellIndex,
806
953
  params: {
807
- seriesName: b.label,
808
- name: `${b.label}\uFF08\u4E2D\u4F4D\u6570 ${b.median}\uFF09`,
809
- value: [
810
- b.min,
811
- b.q1,
812
- b.median,
813
- b.q3,
814
- b.max
815
- ],
816
- color,
817
- dataIndex: i + sliceStartOffset(),
954
+ seriesName: record.label || `${cell.yField} × ${cell.xField}`,
955
+ name: `${cell.xField}=${record[cell.xField]}, ${cell.yField}=${record[cell.yField]}`,
956
+ value: record[cell.yField],
957
+ color: record.color || theme.colors[0],
958
+ dataIndex: cellIndex,
818
959
  seriesIndex: 0
819
960
  }
820
961
  };
@@ -886,22 +1027,56 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
886
1027
  if (options.type === "scatter") {
887
1028
  const scatterData = options.scatterData || [];
888
1029
  if (scatterData.length === 0) return null;
889
- const xValues = scatterData.map((d) => d.x);
890
- const xMin = Math.min(...xValues);
891
- const xRange = Math.max(...xValues) - xMin || 1;
892
- const xPadding = xRange * .05;
893
- const yValues = scatterData.map((d) => d.y);
1030
+ if (options.facet === true) {
1031
+ const grid = computeFacetGrids(scatterData, plotArea, options);
1032
+ if (!grid) return null;
1033
+ for (const facet of grid.facets) {
1034
+ const a = facet.area;
1035
+ if (canvasX < a.x || canvasX > a.x + a.width || canvasY < a.y || canvasY > a.y + a.height) continue;
1036
+ let nearestLocal = -1;
1037
+ let minDistance = Infinity;
1038
+ facet.data.forEach((point, i) => {
1039
+ const ppx = facet.toX(point.x);
1040
+ const ppy = facet.toY(point.y);
1041
+ const dist = Math.sqrt((canvasX - ppx) ** 2 + (canvasY - ppy) ** 2);
1042
+ if (dist < minDistance) {
1043
+ minDistance = dist;
1044
+ nearestLocal = i;
1045
+ }
1046
+ });
1047
+ if (nearestLocal < 0 || minDistance > 24) return null;
1048
+ const point = facet.data[nearestLocal];
1049
+ const globalIdx = scatterData.indexOf(point);
1050
+ const color = point.color || theme.colors[globalIdx % theme.colors.length];
1051
+ return {
1052
+ index: globalIdx,
1053
+ params: {
1054
+ seriesName: point.group || point.label || `Point ${globalIdx + 1}`,
1055
+ name: `(${point.x}, ${point.y})`,
1056
+ value: point.y,
1057
+ color,
1058
+ dataIndex: globalIdx,
1059
+ seriesIndex: 0
1060
+ }
1061
+ };
1062
+ }
1063
+ return null;
1064
+ }
1065
+ const yValues = scatterData.map((d) => d.y).filter((v) => typeof v === "number" && Number.isFinite(v));
894
1066
  const axisConfig = options.yAxis || {};
895
- const rawMin = axisConfig.min ?? Math.min(...yValues);
896
- const rawMax = axisConfig.max ?? Math.max(...yValues);
1067
+ const rawMin = axisConfig.min ?? (yValues.length ? minOf(yValues) : 0);
1068
+ const rawMax = axisConfig.max ?? (yValues.length ? maxOf(yValues) : 1);
897
1069
  const yExt = resolveTickExtendedRange(rawMin, rawMax, axisConfig.ticks || 5);
898
- const yPadding = (yExt.max - yExt.min) * .05;
1070
+ const positions = scatterPositionsFor(scatterData, {
1071
+ min: yExt.min,
1072
+ max: yExt.max
1073
+ }, plotArea, options);
1074
+ const groupColors = scatterGroupColors(scatterData, theme);
899
1075
  let nearestIndex = -1;
900
1076
  let minDistance = Infinity;
901
1077
  scatterData.forEach((point, i) => {
902
- const px = plotArea.x + (point.x - xMin + xPadding) / (xRange + xPadding * 2) * plotArea.width;
903
- const normalizedY = (point.y - yExt.min + yPadding) / (yExt.max - yExt.min + yPadding * 2);
904
- const py = plotArea.y + plotArea.height - normalizedY * plotArea.height;
1078
+ if (hiddenSeries.value.has(point.label || `P${i + 1}`) || point.group && hiddenSeries.value.has(point.group)) return;
1079
+ const [px, py] = positions[i];
905
1080
  const dist = Math.sqrt((canvasX - px) ** 2 + (canvasY - py) ** 2);
906
1081
  if (dist < minDistance) {
907
1082
  minDistance = dist;
@@ -910,11 +1085,11 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
910
1085
  });
911
1086
  if (nearestIndex >= 0 && minDistance < 30) {
912
1087
  const point = scatterData[nearestIndex];
913
- const color = point.color || theme.colors[nearestIndex % theme.colors.length];
1088
+ const color = point.color || (point.group && groupColors.has(point.group) ? groupColors.get(point.group) : theme.colors[nearestIndex % theme.colors.length]);
914
1089
  return {
915
1090
  index: nearestIndex,
916
1091
  params: {
917
- seriesName: point.label || `Point ${nearestIndex + 1}`,
1092
+ seriesName: point.label || point.group || `Point ${nearestIndex + 1}`,
918
1093
  name: `(${point.x}, ${point.y})`,
919
1094
  value: point.y,
920
1095
  color,
@@ -944,6 +1119,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
944
1119
  candle.high,
945
1120
  candle.low
946
1121
  ],
1122
+ volume: (options.volumeData || [])[i],
947
1123
  color,
948
1124
  dataIndex: i,
949
1125
  seriesIndex: 0
@@ -1062,13 +1238,23 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1062
1238
  const centerY = plotArea.y + plotArea.height / 2;
1063
1239
  const radius = Math.max(40, Math.min(plotArea.width, plotArea.height) / 2 - 30);
1064
1240
  const angleStep = Math.PI * 2 / indicators.length;
1241
+ const labelOffset = options.radarLabelOffset ?? 12;
1242
+ let labelHit = -1;
1243
+ indicators.forEach((indicator, i) => {
1244
+ if (labelHit >= 0) return;
1245
+ const angle = i * angleStep - Math.PI / 2;
1246
+ const lx = centerX + Math.cos(angle) * (radius + labelOffset);
1247
+ const ly = centerY + Math.sin(angle) * (radius + labelOffset);
1248
+ if (Math.sqrt((canvasX - lx) ** 2 + (canvasY - ly) ** 2) <= 18) labelHit = i;
1249
+ });
1250
+ if (labelHit >= 0) return radarAxisHover(labelHit, indicators, series, options, theme);
1065
1251
  let nearestIndex = -1;
1066
1252
  let minDist = Infinity;
1067
1253
  indicators.forEach((_, i) => {
1068
1254
  const angle = i * angleStep - Math.PI / 2;
1069
1255
  const ind = indicators[i];
1070
1256
  const span = ind.max - (ind.min || 0);
1071
- const maxR = span > 0 ? Math.max(...series.map((s) => radius * (Math.max(0, (s.data[i] || 0) - (ind.min || 0)) / span))) : 0;
1257
+ const maxR = span > 0 ? maxOf(series.map((s) => radius * (Math.max(0, (s.data[i] || 0) - (ind.min || 0)) / span))) : 0;
1072
1258
  const px = centerX + Math.cos(angle) * maxR;
1073
1259
  const py = centerY + Math.sin(angle) * maxR;
1074
1260
  const dist = Math.sqrt((canvasX - px) ** 2 + (canvasY - py) ** 2);
@@ -1078,18 +1264,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1078
1264
  }
1079
1265
  });
1080
1266
  if (nearestIndex < 0 || minDist > 30) return null;
1081
- const params = series.map((s, sIdx) => ({
1082
- seriesName: s.name,
1083
- name: indicators[nearestIndex].name,
1084
- value: s.data[nearestIndex],
1085
- color: s.color || theme.colors[options.series.indexOf(s) % theme.colors.length],
1086
- dataIndex: nearestIndex,
1087
- seriesIndex: sIdx
1088
- }));
1089
- return {
1090
- index: nearestIndex,
1091
- params
1092
- };
1267
+ return radarAxisHover(nearestIndex, indicators, series, options, theme);
1093
1268
  }
1094
1269
  const labels = options.labels || [];
1095
1270
  if (labels.length === 0) return null;
@@ -1097,6 +1272,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1097
1272
  if (dataIndex < 0 || dataIndex >= labels.length) return null;
1098
1273
  const visibleSeries = (options.series || []).filter((s) => !hiddenSeries.value.has(s.name));
1099
1274
  if (visibleSeries.length === 0) return null;
1275
+ const volume = (options.volumeData || [])[dataIndex];
1100
1276
  if (props.options.tooltip?.showAllSeries !== false && visibleSeries.length > 1) {
1101
1277
  if (visibleSeries.every((s) => isMissingValue(s.data[dataIndex]))) return null;
1102
1278
  return {
@@ -1105,6 +1281,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1105
1281
  seriesName: s.name,
1106
1282
  name: labels[dataIndex],
1107
1283
  value: s.data[dataIndex],
1284
+ volume,
1108
1285
  color: s.color || theme.colors[options.series.indexOf(s) % theme.colors.length],
1109
1286
  dataIndex: dataIndex + sliceStartOffset(),
1110
1287
  seriesIndex: sIdx
@@ -1120,6 +1297,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1120
1297
  seriesName: series.name,
1121
1298
  name: labels[dataIndex],
1122
1299
  value: series.data[dataIndex],
1300
+ volume,
1123
1301
  color,
1124
1302
  dataIndex: dataIndex + sliceStartOffset(),
1125
1303
  seriesIndex: 0
@@ -1127,6 +1305,20 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1127
1305
  };
1128
1306
  }
1129
1307
  }
1308
+ /** 雷达轴悬浮:返回该维度上全部可见系列的 tooltip 行 */
1309
+ function radarAxisHover(axisIndex, indicators, series, options, theme) {
1310
+ return {
1311
+ index: axisIndex,
1312
+ params: series.map((s, sIdx) => ({
1313
+ seriesName: s.name,
1314
+ name: indicators[axisIndex].name,
1315
+ value: s.data[axisIndex],
1316
+ color: s.color || theme.colors[(options.radarSeries || []).indexOf(s) % theme.colors.length],
1317
+ dataIndex: axisIndex,
1318
+ seriesIndex: sIdx
1319
+ }))
1320
+ };
1321
+ }
1130
1322
  function setZoomRange(next) {
1131
1323
  const minSpan = INTERACTION.zoom.minSpan;
1132
1324
  let { start, end } = next;
@@ -1153,7 +1345,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1153
1345
  const canvas = canvasRef.value;
1154
1346
  if (!canvas) return null;
1155
1347
  const rect = canvas.getBoundingClientRect();
1156
- const padding = getPadding(effectiveOptions.value, rect.width);
1348
+ const padding = paddingFor(effectiveOptions.value, rect.width);
1157
1349
  const plotArea = {
1158
1350
  x: padding.left,
1159
1351
  y: padding.top,
@@ -1217,7 +1409,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1217
1409
  const rect = canvas.getBoundingClientRect();
1218
1410
  const x = e.clientX - rect.left;
1219
1411
  const y = e.clientY - rect.top;
1220
- const padding = getPadding(effectiveOptions.value, rect.width);
1412
+ const padding = paddingFor(effectiveOptions.value, rect.width);
1221
1413
  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)) {
1222
1414
  touchPanning = true;
1223
1415
  touchPanStartX = x;
@@ -1240,7 +1432,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1240
1432
  const x = e.clientX - rect.left;
1241
1433
  const y = e.clientY - rect.top;
1242
1434
  if (props.options.brush?.enabled === true) {
1243
- const padding = getPadding(effectiveOptions.value, rect.width);
1435
+ const padding = paddingFor(effectiveOptions.value, rect.width);
1244
1436
  const plotArea = {
1245
1437
  x: padding.left,
1246
1438
  y: padding.top,
@@ -1335,7 +1527,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1335
1527
  const labels = options.labels || [];
1336
1528
  if (canvas && labels.length > 0) {
1337
1529
  const rect = canvas.getBoundingClientRect();
1338
- const padding = getPadding(options, rect.width);
1530
+ const padding = paddingFor(options, rect.width);
1339
1531
  const plotArea = {
1340
1532
  x: padding.left,
1341
1533
  y: padding.top,
@@ -1378,7 +1570,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1378
1570
  const rect = canvas.getBoundingClientRect();
1379
1571
  const x = e.clientX - rect.left;
1380
1572
  const y = e.clientY - rect.top;
1381
- const padding = getPadding(effectiveOptions.value, rect.width);
1573
+ const padding = paddingFor(effectiveOptions.value, rect.width);
1382
1574
  const plotArea = {
1383
1575
  x: padding.left,
1384
1576
  y: padding.top,
@@ -1444,7 +1636,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1444
1636
  const nextFocus = props.options.legend?.hoverEmphasis !== false && legendName ? legendName : null;
1445
1637
  if (nextFocus !== focusSeries.value) {
1446
1638
  focusSeries.value = nextFocus;
1447
- redraw();
1639
+ animateFocusProgress();
1448
1640
  }
1449
1641
  }
1450
1642
  if (legendName) {
@@ -1454,13 +1646,13 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1454
1646
  }
1455
1647
  if (e.pointerType === "touch") return;
1456
1648
  const result = getHoveredData(e.clientX, e.clientY);
1457
- const isPieLike = props.options.type === "pie" || props.options.type === "doughnut" || props.options.type === "rose";
1649
+ const enterAnimated = animatesHoverEnter();
1458
1650
  if (result && props.options.tooltip?.show !== false) {
1459
1651
  const newIndex = result.index;
1460
1652
  if (newIndex !== hoverIndex) {
1461
1653
  const prevIndex = hoverIndex;
1462
1654
  hoverIndex = newIndex;
1463
- if (isPieLike) {
1655
+ if (enterAnimated) {
1464
1656
  if (prevIndex === -1) startHoverAnimation(1);
1465
1657
  else {
1466
1658
  stopHoverAnimation();
@@ -1470,8 +1662,8 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1470
1662
  } else if (usesHoverAnimation()) {
1471
1663
  stopHoverAnimation();
1472
1664
  hoverAnimProgress = 1;
1473
- redraw();
1474
- } else redraw();
1665
+ scheduleHoverRedraw();
1666
+ } else scheduleHoverRedraw();
1475
1667
  if (prevIndex === -1) {
1476
1668
  const emitParams = Array.isArray(result.params) ? result.params[0] : result.params;
1477
1669
  emit("hover", {
@@ -1484,7 +1676,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1484
1676
  });
1485
1677
  ariaLiveText.value = `${emitParams.seriesName}: ${formatTooltipValue(emitParams.value)}`;
1486
1678
  }
1487
- }
1679
+ } else if (enterAnimated && hoverAnimProgress < 1) startHoverAnimation(1);
1488
1680
  applyTooltipContent(result.params);
1489
1681
  if (props.options.tooltip?.trigger === "click") return;
1490
1682
  lastMouseClientX = e.clientX;
@@ -1531,6 +1723,9 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1531
1723
  }
1532
1724
  return Number.isInteger(value) ? value.toString() : parseFloat(value.toFixed(4)).toString();
1533
1725
  }
1726
+ function escapeHtml(value) {
1727
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1728
+ }
1534
1729
  function applyTooltipContent(params) {
1535
1730
  const formatter = props.options.tooltip?.formatter;
1536
1731
  if (formatter) {
@@ -1541,23 +1736,23 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1541
1736
  const title = params[0]?.name || "";
1542
1737
  const rows = pickTooltipRows(params).map((p) => `
1543
1738
  <div class="ev-chart__tooltip-item">
1544
- <span class="ev-chart__tooltip-dot" style="background: ${p.color}"></span>
1545
- <span class="ev-chart__tooltip-name">${p.seriesName}</span>
1546
- <span class="ev-chart__tooltip-value">${formatTooltipValue(p.value)}</span>
1739
+ <span class="ev-chart__tooltip-dot" style="background: ${escapeHtml(p.color)}"></span>
1740
+ <span class="ev-chart__tooltip-name">${escapeHtml(p.seriesName)}</span>
1741
+ <span class="ev-chart__tooltip-value">${escapeHtml(formatTooltipValue(p.value))}</span>
1547
1742
  </div>
1548
1743
  `).join("");
1549
1744
  tooltipContent.value = `
1550
- <div class="ev-chart__tooltip-title">${title}</div>
1745
+ <div class="ev-chart__tooltip-title">${escapeHtml(title)}</div>
1551
1746
  ${rows}
1552
1747
  `;
1553
1748
  } else {
1554
1749
  const p = params;
1555
1750
  tooltipContent.value = `
1556
- <div class="ev-chart__tooltip-title">${p.name}</div>
1751
+ <div class="ev-chart__tooltip-title">${escapeHtml(p.name)}</div>
1557
1752
  <div class="ev-chart__tooltip-item">
1558
- <span class="ev-chart__tooltip-dot" style="background: ${p.color}"></span>
1559
- <span class="ev-chart__tooltip-name">${p.seriesName}</span>
1560
- <span class="ev-chart__tooltip-value">${formatTooltipValue(p.value)}</span>
1753
+ <span class="ev-chart__tooltip-dot" style="background: ${escapeHtml(p.color)}"></span>
1754
+ <span class="ev-chart__tooltip-name">${escapeHtml(p.seriesName)}</span>
1755
+ <span class="ev-chart__tooltip-value">${escapeHtml(formatTooltipValue(p.value))}</span>
1561
1756
  </div>
1562
1757
  `;
1563
1758
  }
@@ -1574,7 +1769,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1574
1769
  else if (checkToolboxHit(e.clientX, e.clientY)) zone = "toolbox";
1575
1770
  else if (checkLegendHit(e.clientX, e.clientY)) zone = "legend";
1576
1771
  else {
1577
- const padding = getPadding(effectiveOptions.value, rect.width);
1772
+ const padding = paddingFor(effectiveOptions.value, rect.width);
1578
1773
  if (x >= padding.left && x <= rect.width - padding.right && y >= padding.top && y <= rect.height - padding.bottom) zone = "plot";
1579
1774
  else {
1580
1775
  const slider = getSliderGeo();
@@ -1615,6 +1810,67 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1615
1810
  ariaLiveText.value = "";
1616
1811
  }
1617
1812
  }
1813
+ const KEYNAV_TYPES = /* @__PURE__ */ new Set([
1814
+ "line",
1815
+ "area",
1816
+ "bar",
1817
+ "stacked-bar",
1818
+ "waterfall",
1819
+ "mixed",
1820
+ "horizontal-bar"
1821
+ ]);
1822
+ let keyboardClientX = -1;
1823
+ let keyboardClientY = -1;
1824
+ function handleChartKeydown(e) {
1825
+ const options = props.options;
1826
+ if (!KEYNAV_TYPES.has(options.type)) return;
1827
+ const count = (options.labels || []).length;
1828
+ if (count === 0) return;
1829
+ if (e.key === "Enter" || e.key === " ") {
1830
+ e.preventDefault();
1831
+ if (hoverIndex < 0 || keyboardClientX < 0) return;
1832
+ const result = getHoveredData(keyboardClientX, keyboardClientY);
1833
+ if (result) {
1834
+ const params = Array.isArray(result.params) ? result.params[0] : result.params;
1835
+ emit("click", params);
1836
+ }
1837
+ return;
1838
+ }
1839
+ const horizontal = options.type === "horizontal-bar";
1840
+ let i = hoverIndex;
1841
+ if (e.key === "Home") i = 0;
1842
+ else if (e.key === "End") i = count - 1;
1843
+ else if (horizontal && e.key === "ArrowUp") i = hoverIndex < 0 ? 0 : Math.min(count - 1, hoverIndex + 1);
1844
+ else if (horizontal && e.key === "ArrowDown") i = hoverIndex < 0 ? 0 : Math.max(0, hoverIndex - 1);
1845
+ else if (!horizontal && e.key === "ArrowRight") i = hoverIndex < 0 ? 0 : Math.min(count - 1, hoverIndex + 1);
1846
+ else if (!horizontal && e.key === "ArrowLeft") i = hoverIndex < 0 ? 0 : Math.max(0, hoverIndex - 1);
1847
+ else return;
1848
+ e.preventDefault();
1849
+ if (i === hoverIndex) return;
1850
+ const canvas = canvasRef.value;
1851
+ if (!canvas) return;
1852
+ const rect = canvas.getBoundingClientRect();
1853
+ const padding = paddingFor(effectiveOptions.value, rect.width);
1854
+ const plotWidth = rect.width - padding.left - padding.right;
1855
+ const plotHeight = rect.height - padding.top - padding.bottom;
1856
+ const clientX = horizontal ? rect.left + padding.left + plotWidth / 2 : rect.left + padding.left + (i + .5) * (plotWidth / count);
1857
+ const clientY = horizontal ? rect.top + padding.top + plotHeight - (i + .5) * (plotHeight / count) : rect.top + padding.top + plotHeight / 2;
1858
+ keyboardClientX = clientX;
1859
+ keyboardClientY = clientY;
1860
+ mouseX = clientX - rect.left;
1861
+ mouseY = clientY - rect.top;
1862
+ handlePointerMove({
1863
+ clientX,
1864
+ clientY,
1865
+ pointerType: "mouse"
1866
+ });
1867
+ }
1868
+ function handleFocusOut() {
1869
+ if (clearTransientState()) {
1870
+ emit("unhover");
1871
+ ariaLiveText.value = "";
1872
+ }
1873
+ }
1618
1874
  let resizeObserver = null;
1619
1875
  function setupResizeObserver() {
1620
1876
  if (!props.responsive) return;
@@ -1662,7 +1918,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1662
1918
  labels: [...opt.labels || []],
1663
1919
  series: (opt.series || []).map((s) => ({
1664
1920
  ...s,
1665
- data: [...s.data]
1921
+ data: [...s.data || []]
1666
1922
  }))
1667
1923
  };
1668
1924
  }
@@ -1691,9 +1947,17 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1691
1947
  watch(() => props.options, (next) => {
1692
1948
  cachedDataExtent = null;
1693
1949
  cachedPlotArea = null;
1950
+ cachedPadding = null;
1951
+ scatterHitCache = null;
1952
+ legendBoundsCache = null;
1953
+ ganttLayoutCache = null;
1954
+ sankeyLayoutCache = null;
1694
1955
  internalError.value = null;
1695
1956
  runDevValidation();
1696
- focusSeries.value = emphasisSeriesName.value;
1957
+ if (focusSeries.value !== emphasisSeriesName.value) {
1958
+ focusSeries.value = emphasisSeriesName.value;
1959
+ animateFocusProgress();
1960
+ }
1697
1961
  syncScenes();
1698
1962
  const zoom = getDataZoomConfig(props.options);
1699
1963
  const zoomKey = zoom ? JSON.stringify({
@@ -1747,6 +2011,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1747
2011
  };
1748
2012
  let unregisterConnector = null;
1749
2013
  onMounted(() => {
2014
+ focusSeries.value = emphasisSeriesName.value;
1750
2015
  render(true);
1751
2016
  syncScenes();
1752
2017
  setupResizeObserver();
@@ -1771,6 +2036,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1771
2036
  stopHoverAnimation();
1772
2037
  stopTweenAnimation();
1773
2038
  stopSceneTimer();
2039
+ stopFocusAnimation();
1774
2040
  if (tooltipRafId !== null) {
1775
2041
  cancelAnimationFrame(tooltipRafId);
1776
2042
  tooltipRafId = null;
@@ -1779,11 +2045,98 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1779
2045
  });
1780
2046
  let cachedDataExtent = null;
1781
2047
  let cachedPlotArea = null;
2048
+ let cachedPadding = null;
2049
+ function paddingFor(options, width) {
2050
+ if (cachedPadding && cachedPadding.options === options && cachedPadding.width === width) return cachedPadding.value;
2051
+ const value = getPadding(options, width);
2052
+ cachedPadding = {
2053
+ options,
2054
+ width,
2055
+ value
2056
+ };
2057
+ return value;
2058
+ }
2059
+ let scatterHitCache = null;
2060
+ function scatterPositionsFor(scatterData, yRange, plotArea, options) {
2061
+ const c = scatterHitCache;
2062
+ 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;
2063
+ const positions = scatterPointPositions(scatterData, yRange, plotArea, options);
2064
+ scatterHitCache = {
2065
+ scatterData,
2066
+ options,
2067
+ yMin: yRange.min,
2068
+ yMax: yRange.max,
2069
+ px: plotArea.x,
2070
+ py: plotArea.y,
2071
+ pw: plotArea.width,
2072
+ ph: plotArea.height,
2073
+ positions
2074
+ };
2075
+ return positions;
2076
+ }
2077
+ let legendBoundsCache = null;
2078
+ function legendBoundsFor(ctx2d, options, plotArea, width, height, theme, hiddenSeries, isDark) {
2079
+ const c = legendBoundsCache;
2080
+ 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;
2081
+ const bounds = getLegendBounds(ctx2d, options, plotArea, width, height, theme, hiddenSeries);
2082
+ legendBoundsCache = {
2083
+ options,
2084
+ width,
2085
+ height,
2086
+ dark: isDark,
2087
+ hidden: hiddenSeries,
2088
+ px: plotArea.x,
2089
+ py: plotArea.y,
2090
+ pw: plotArea.width,
2091
+ ph: plotArea.height,
2092
+ bounds
2093
+ };
2094
+ return bounds;
2095
+ }
2096
+ let ganttLayoutCache = null;
2097
+ function ganttLayoutFor(plotArea, options, theme, hiddenSeries, isDark) {
2098
+ const c = ganttLayoutCache;
2099
+ 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;
2100
+ const layout = computeGanttLayout(plotArea, options, theme, hiddenSeries);
2101
+ ganttLayoutCache = {
2102
+ options,
2103
+ dark: isDark,
2104
+ hidden: hiddenSeries,
2105
+ px: plotArea.x,
2106
+ py: plotArea.y,
2107
+ pw: plotArea.width,
2108
+ ph: plotArea.height,
2109
+ layout
2110
+ };
2111
+ return layout;
2112
+ }
2113
+ let sankeyLayoutCache = null;
2114
+ function sankeyLayoutFor(plotArea, options, theme, hiddenSeries, isDark) {
2115
+ const c = sankeyLayoutCache;
2116
+ 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;
2117
+ const layout = computeSankeyLayout(plotArea, options, theme, hiddenSeries);
2118
+ sankeyLayoutCache = {
2119
+ options,
2120
+ dark: isDark,
2121
+ hidden: hiddenSeries,
2122
+ px: plotArea.x,
2123
+ py: plotArea.y,
2124
+ pw: plotArea.width,
2125
+ ph: plotArea.height,
2126
+ layout
2127
+ };
2128
+ return layout;
2129
+ }
1782
2130
  __expose({
1783
2131
  refresh: () => render(true),
1784
2132
  update(newOptions) {
1785
2133
  Object.assign(props.options, newOptions);
1786
2134
  cachedDataExtent = null;
2135
+ cachedPadding = null;
2136
+ scatterHitCache = null;
2137
+ legendBoundsCache = null;
2138
+ ganttLayoutCache = null;
2139
+ sankeyLayoutCache = null;
1787
2140
  specVersion.value++;
1788
2141
  debouncedRender(true);
1789
2142
  },
@@ -1793,9 +2146,18 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1793
2146
  destroy() {
1794
2147
  cleanupResizeObserver();
1795
2148
  cleanupDarkModeObserver();
2149
+ window.removeEventListener("pointerup", onWindowPointerUp);
2150
+ window.removeEventListener("keydown", handleEscapeKey);
2151
+ containerRef.value?.removeEventListener("wheel", handleWheel);
2152
+ if (unregisterConnector) {
2153
+ unregisterConnector();
2154
+ unregisterConnector = null;
2155
+ }
1796
2156
  stopAnimation();
1797
2157
  stopHoverAnimation();
2158
+ stopTweenAnimation();
1798
2159
  stopSceneTimer();
2160
+ stopFocusAnimation();
1799
2161
  if (tooltipRafId !== null) {
1800
2162
  cancelAnimationFrame(tooltipRafId);
1801
2163
  tooltipRafId = null;
@@ -1871,7 +2233,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1871
2233
  };
1872
2234
  const width = canvas.width / dpr.value;
1873
2235
  const height = canvas.height / dpr.value;
1874
- const padding = getPadding(effectiveOptions.value, width);
2236
+ const padding = paddingFor(effectiveOptions.value, width);
1875
2237
  cachedPlotArea = {
1876
2238
  x: padding.left,
1877
2239
  y: padding.top,
@@ -2005,7 +2367,10 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
2005
2367
  class: "ev-chart",
2006
2368
  style: normalizeStyle([containerStyle.value, { cursor: cursorStyle.value }]),
2007
2369
  role: "img",
2008
- "aria-label": props.options.ariaLabel || ariaLabelText.value
2370
+ tabindex: isEmpty.value || internalError.value ? -1 : 0,
2371
+ "aria-label": props.options.ariaLabel || ariaLabelText.value,
2372
+ onKeydown: handleChartKeydown,
2373
+ onFocusout: handleFocusOut
2009
2374
  }, [
2010
2375
  !isEmpty.value && !internalError.value ? (openBlock(), createElementBlock("canvas", {
2011
2376
  key: 0,
@@ -2084,10 +2449,10 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
2084
2449
  _: 1
2085
2450
  }),
2086
2451
  createElementVNode("div", _hoisted_11, toDisplayString(ariaLiveText.value), 1)
2087
- ], 12, _hoisted_1);
2452
+ ], 44, _hoisted_1);
2088
2453
  };
2089
2454
  }
2090
- }), [["__scopeId", "data-v-04ce6a3d"]]);
2455
+ }), [["__scopeId", "data-v-62da63e1"]]);
2091
2456
  //#endregion
2092
2457
  //#region src/useChart.js
2093
2458
  var USE_CHART_KEY = /* @__PURE__ */ Symbol("ev-chart-context");