@wil-works/evoke-charts 0.5.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 { $ 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 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,7 +121,7 @@ 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;
@@ -140,7 +140,12 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
140
140
  case "sankey":
141
141
  case "chord":
142
142
  case "arc": return !(opt.sankeyData || opt.chordData || opt.arcData)?.nodes?.length;
143
- case "venn": return !opt.vennData || opt.vennData.length === 0;
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
+ }
144
149
  case "gantt": return !opt.ganttData || opt.ganttData.length === 0;
145
150
  case "scatter-matrix": return !opt.matrixFields?.length || !opt.matrixData?.length;
146
151
  default: return !opt.labels?.length || !opt.series?.length;
@@ -236,11 +241,20 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
236
241
  data: s.data.slice(startIdx, endIdx)
237
242
  }));
238
243
  if (base.candleData) sliced.candleData = base.candleData.slice(startIdx, endIdx);
244
+ if (base.volumeData) sliced.volumeData = base.volumeData.slice(startIdx, endIdx);
239
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
+ }
240
254
  } else if (base.scatterData && base.scatterData.length > 0) {
241
255
  const xs = base.scatterData.map((d) => d.x);
242
- const xMin = Math.min(...xs);
243
- const span = Math.max(...xs) - xMin || 1;
256
+ const xMin = minOf(xs);
257
+ const span = maxOf(xs) - xMin || 1;
244
258
  const lo = xMin + range.start / 100 * span;
245
259
  const hi = xMin + range.end / 100 * span;
246
260
  const filtered = base.scatterData.filter((d) => d.x >= lo && d.x <= hi);
@@ -334,11 +348,11 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
334
348
  const el = tooltipRef.value;
335
349
  const tw = el?.offsetWidth || 120;
336
350
  const th = el?.offsetHeight || 40;
337
- const gap = 12;
351
+ const gap = INTERACTION.tooltip.cursorGap;
338
352
  const margin = 2;
339
353
  let legendBottom = margin;
340
354
  const legend = props.options.legend || {};
341
- 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;
342
356
  const spaceRight = containerRect.width - relX - gap;
343
357
  const spaceLeft = relX - gap;
344
358
  let left;
@@ -408,7 +422,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
408
422
  lastHeight = height;
409
423
  canvas.width = width * dpr.value;
410
424
  canvas.height = height * dpr.value;
411
- const overlayPadding = getPadding(effectiveOptions.value, width);
425
+ const overlayPadding = paddingFor(effectiveOptions.value, width);
412
426
  overlayInfo.value = {
413
427
  plotArea: {
414
428
  x: overlayPadding.left,
@@ -458,7 +472,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
458
472
  hoveredToolbox: hoveredToolbox.value
459
473
  };
460
474
  }
461
- let focusAnimProgress = 1;
475
+ let focusAnimProgress = emphasisSeriesName.value ? 1 : 0;
462
476
  let focusAnimFrameId = null;
463
477
  function animateFocusProgress() {
464
478
  if (focusAnimFrameId !== null) {
@@ -491,7 +505,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
491
505
  }
492
506
  function snapshotSeriesData(opt) {
493
507
  const map = /* @__PURE__ */ new Map();
494
- (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)));
495
509
  return map;
496
510
  }
497
511
  function sameStructure(a, b) {
@@ -505,7 +519,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
505
519
  const series = (next.series || []).map((s) => {
506
520
  const old = prev.get(s.name);
507
521
  if (!old) return s;
508
- const data = s.data.map((v, i) => {
522
+ const data = (s.data || []).map((v, i) => {
509
523
  if (isMissingValue(v)) return v;
510
524
  const from = old[i + offset] ?? 0;
511
525
  return from + (v - from) * t;
@@ -521,7 +535,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
521
535
  };
522
536
  }
523
537
  function dataSignature(opt) {
524
- 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);
525
539
  return `${(opt.series || []).length}\u7CFB\u5217/${(opt.labels || []).length}\u7C7B\u76EE/\u03A3${Math.round(total)}`;
526
540
  }
527
541
  function stopTweenAnimation() {
@@ -539,7 +553,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
539
553
  if (!(nextOptions.series || []).some((s) => {
540
554
  const old = prevSnapshot.get(s.name);
541
555
  if (!old) return false;
542
- 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));
543
557
  })) return false;
544
558
  stopAnimation();
545
559
  stopHoverAnimation();
@@ -604,15 +618,47 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
604
618
  cancelAnimationFrame(hoverAnimFrameId);
605
619
  hoverAnimFrameId = null;
606
620
  }
621
+ if (hoverRedrawFrameId !== null) {
622
+ cancelAnimationFrame(hoverRedrawFrameId);
623
+ hoverRedrawFrameId = null;
624
+ }
607
625
  hoverAnimDone = null;
608
626
  }
627
+ let hoverRedrawFrameId = null;
628
+ function scheduleHoverRedraw() {
629
+ if (hoverRedrawFrameId !== null) return;
630
+ hoverRedrawFrameId = requestAnimationFrame(() => {
631
+ hoverRedrawFrameId = null;
632
+ redraw();
633
+ });
634
+ }
609
635
  function usesHoverAnimation() {
610
636
  const t = props.options.type;
611
637
  return t === "pie" || t === "doughnut" || t === "rose" || t === "sunburst" || t === "treemap" || t === "radar";
612
638
  }
613
639
  function animatesHoverEnter() {
614
640
  const t = props.options.type;
615
- return t === "pie" || t === "doughnut" || t === "rose" || t === "radar" || t === "venn" || t === "sankey" || t === "chord" || t === "arc";
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);
616
662
  }
617
663
  function clearHover() {
618
664
  if (hoverIndex === -1) return false;
@@ -695,15 +741,14 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
695
741
  const theme = getTheme(isDark, props.options.theme, props.options.palette);
696
742
  const width = rect.width;
697
743
  const height = rect.height;
698
- const padding = getPadding(effectiveOptions.value, width);
744
+ const padding = paddingFor(effectiveOptions.value, width);
699
745
  const plotArea = {
700
746
  x: padding.left,
701
747
  y: padding.top,
702
748
  width: width - padding.left - padding.right,
703
749
  height: height - padding.top - padding.bottom
704
750
  };
705
- const ctx2d = canvas.getContext("2d");
706
- 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);
707
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;
708
753
  return null;
709
754
  }
@@ -748,7 +793,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
748
793
  const options = effectiveOptions.value;
749
794
  const width = rect.width;
750
795
  const height = rect.height;
751
- const padding = getPadding(options, width);
796
+ const padding = paddingFor(options, width);
752
797
  const plotArea = {
753
798
  x: padding.left,
754
799
  y: padding.top,
@@ -770,12 +815,12 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
770
815
  const dx = canvasX - centerX;
771
816
  const dy = canvasY - centerY;
772
817
  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;
818
+ const innerRadius = options.type === "doughnut" ? doughnutInnerRadius(baseRadius, options) : 0;
774
819
  let angle = Math.atan2(dy, dx) + Math.PI / 2;
775
820
  if (angle < 0) angle += Math.PI * 2;
776
821
  const total = slices.reduce((sum, d) => sum + d.value, 0);
777
822
  let currentAngle = 0;
778
- const maxVal = Math.max(...slices.map((d) => d.value));
823
+ const maxVal = maxOf(slices.map((d) => d.value));
779
824
  for (let i = 0; i < slices.length; i++) {
780
825
  const sliceAngle = slices[i].value / total * Math.PI * 2;
781
826
  if (angle >= currentAngle && angle < currentAngle + sliceAngle) {
@@ -831,14 +876,10 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
831
876
  const i = Math.floor((canvasX - plotArea.x) / categoryWidth);
832
877
  if (i < 0 || i >= labels2.length) return null;
833
878
  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);
879
+ const step = waterfallSteps(deltas, wf.totalIndices)[i];
880
+ const v = step.value;
881
+ const cumulative = step.to;
882
+ const isTotal = step.isTotal;
842
883
  const color = isTotal ? wf.totalColor || theme.colors[0] : v >= 0 ? wf.increaseColor || "#dc2626" : wf.decreaseColor || "#16a34a";
843
884
  return {
844
885
  index: i,
@@ -862,18 +903,25 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
862
903
  ...showOutliers ? b.outliers || [] : []
863
904
  ]);
864
905
  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);
906
+ const ext = resolveTickExtendedRange(axisCfg.min ?? minOf(allValues), axisCfg.max ?? maxOf(allValues), axisCfg.ticks || 5);
866
907
  return boxplotHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value, {
867
908
  min: ext.min,
868
909
  max: ext.max
869
910
  });
870
911
  }
871
912
  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);
913
+ if (options.type === "sankey") return sankeyHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value, sankeyLayoutFor(plotArea, options, theme, hiddenSeries.value, isDark));
873
914
  if (options.type === "venn") return vennHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value);
874
915
  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);
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));
877
925
  if (options.type === "scatter-matrix") {
878
926
  const fields = options.matrixFields || [];
879
927
  const records = options.matrixData || [];
@@ -1014,12 +1062,12 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1014
1062
  }
1015
1063
  return null;
1016
1064
  }
1017
- const yValues = scatterData.map((d) => d.y);
1065
+ const yValues = scatterData.map((d) => d.y).filter((v) => typeof v === "number" && Number.isFinite(v));
1018
1066
  const axisConfig = options.yAxis || {};
1019
- const rawMin = axisConfig.min ?? Math.min(...yValues);
1020
- 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);
1021
1069
  const yExt = resolveTickExtendedRange(rawMin, rawMax, axisConfig.ticks || 5);
1022
- const positions = scatterPointPositions(scatterData, {
1070
+ const positions = scatterPositionsFor(scatterData, {
1023
1071
  min: yExt.min,
1024
1072
  max: yExt.max
1025
1073
  }, plotArea, options);
@@ -1206,7 +1254,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1206
1254
  const angle = i * angleStep - Math.PI / 2;
1207
1255
  const ind = indicators[i];
1208
1256
  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;
1257
+ const maxR = span > 0 ? maxOf(series.map((s) => radius * (Math.max(0, (s.data[i] || 0) - (ind.min || 0)) / span))) : 0;
1210
1258
  const px = centerX + Math.cos(angle) * maxR;
1211
1259
  const py = centerY + Math.sin(angle) * maxR;
1212
1260
  const dist = Math.sqrt((canvasX - px) ** 2 + (canvasY - py) ** 2);
@@ -1297,7 +1345,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1297
1345
  const canvas = canvasRef.value;
1298
1346
  if (!canvas) return null;
1299
1347
  const rect = canvas.getBoundingClientRect();
1300
- const padding = getPadding(effectiveOptions.value, rect.width);
1348
+ const padding = paddingFor(effectiveOptions.value, rect.width);
1301
1349
  const plotArea = {
1302
1350
  x: padding.left,
1303
1351
  y: padding.top,
@@ -1361,7 +1409,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1361
1409
  const rect = canvas.getBoundingClientRect();
1362
1410
  const x = e.clientX - rect.left;
1363
1411
  const y = e.clientY - rect.top;
1364
- const padding = getPadding(effectiveOptions.value, rect.width);
1412
+ const padding = paddingFor(effectiveOptions.value, rect.width);
1365
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)) {
1366
1414
  touchPanning = true;
1367
1415
  touchPanStartX = x;
@@ -1384,7 +1432,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1384
1432
  const x = e.clientX - rect.left;
1385
1433
  const y = e.clientY - rect.top;
1386
1434
  if (props.options.brush?.enabled === true) {
1387
- const padding = getPadding(effectiveOptions.value, rect.width);
1435
+ const padding = paddingFor(effectiveOptions.value, rect.width);
1388
1436
  const plotArea = {
1389
1437
  x: padding.left,
1390
1438
  y: padding.top,
@@ -1479,7 +1527,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1479
1527
  const labels = options.labels || [];
1480
1528
  if (canvas && labels.length > 0) {
1481
1529
  const rect = canvas.getBoundingClientRect();
1482
- const padding = getPadding(options, rect.width);
1530
+ const padding = paddingFor(options, rect.width);
1483
1531
  const plotArea = {
1484
1532
  x: padding.left,
1485
1533
  y: padding.top,
@@ -1522,7 +1570,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1522
1570
  const rect = canvas.getBoundingClientRect();
1523
1571
  const x = e.clientX - rect.left;
1524
1572
  const y = e.clientY - rect.top;
1525
- const padding = getPadding(effectiveOptions.value, rect.width);
1573
+ const padding = paddingFor(effectiveOptions.value, rect.width);
1526
1574
  const plotArea = {
1527
1575
  x: padding.left,
1528
1576
  y: padding.top,
@@ -1614,8 +1662,8 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1614
1662
  } else if (usesHoverAnimation()) {
1615
1663
  stopHoverAnimation();
1616
1664
  hoverAnimProgress = 1;
1617
- redraw();
1618
- } else redraw();
1665
+ scheduleHoverRedraw();
1666
+ } else scheduleHoverRedraw();
1619
1667
  if (prevIndex === -1) {
1620
1668
  const emitParams = Array.isArray(result.params) ? result.params[0] : result.params;
1621
1669
  emit("hover", {
@@ -1675,6 +1723,9 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1675
1723
  }
1676
1724
  return Number.isInteger(value) ? value.toString() : parseFloat(value.toFixed(4)).toString();
1677
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
+ }
1678
1729
  function applyTooltipContent(params) {
1679
1730
  const formatter = props.options.tooltip?.formatter;
1680
1731
  if (formatter) {
@@ -1685,23 +1736,23 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1685
1736
  const title = params[0]?.name || "";
1686
1737
  const rows = pickTooltipRows(params).map((p) => `
1687
1738
  <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>
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>
1691
1742
  </div>
1692
1743
  `).join("");
1693
1744
  tooltipContent.value = `
1694
- <div class="ev-chart__tooltip-title">${title}</div>
1745
+ <div class="ev-chart__tooltip-title">${escapeHtml(title)}</div>
1695
1746
  ${rows}
1696
1747
  `;
1697
1748
  } else {
1698
1749
  const p = params;
1699
1750
  tooltipContent.value = `
1700
- <div class="ev-chart__tooltip-title">${p.name}</div>
1751
+ <div class="ev-chart__tooltip-title">${escapeHtml(p.name)}</div>
1701
1752
  <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>
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>
1705
1756
  </div>
1706
1757
  `;
1707
1758
  }
@@ -1718,7 +1769,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1718
1769
  else if (checkToolboxHit(e.clientX, e.clientY)) zone = "toolbox";
1719
1770
  else if (checkLegendHit(e.clientX, e.clientY)) zone = "legend";
1720
1771
  else {
1721
- const padding = getPadding(effectiveOptions.value, rect.width);
1772
+ const padding = paddingFor(effectiveOptions.value, rect.width);
1722
1773
  if (x >= padding.left && x <= rect.width - padding.right && y >= padding.top && y <= rect.height - padding.bottom) zone = "plot";
1723
1774
  else {
1724
1775
  const slider = getSliderGeo();
@@ -1759,6 +1810,67 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1759
1810
  ariaLiveText.value = "";
1760
1811
  }
1761
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
+ }
1762
1874
  let resizeObserver = null;
1763
1875
  function setupResizeObserver() {
1764
1876
  if (!props.responsive) return;
@@ -1806,7 +1918,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1806
1918
  labels: [...opt.labels || []],
1807
1919
  series: (opt.series || []).map((s) => ({
1808
1920
  ...s,
1809
- data: [...s.data]
1921
+ data: [...s.data || []]
1810
1922
  }))
1811
1923
  };
1812
1924
  }
@@ -1835,9 +1947,17 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1835
1947
  watch(() => props.options, (next) => {
1836
1948
  cachedDataExtent = null;
1837
1949
  cachedPlotArea = null;
1950
+ cachedPadding = null;
1951
+ scatterHitCache = null;
1952
+ legendBoundsCache = null;
1953
+ ganttLayoutCache = null;
1954
+ sankeyLayoutCache = null;
1838
1955
  internalError.value = null;
1839
1956
  runDevValidation();
1840
- focusSeries.value = emphasisSeriesName.value;
1957
+ if (focusSeries.value !== emphasisSeriesName.value) {
1958
+ focusSeries.value = emphasisSeriesName.value;
1959
+ animateFocusProgress();
1960
+ }
1841
1961
  syncScenes();
1842
1962
  const zoom = getDataZoomConfig(props.options);
1843
1963
  const zoomKey = zoom ? JSON.stringify({
@@ -1891,6 +2011,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1891
2011
  };
1892
2012
  let unregisterConnector = null;
1893
2013
  onMounted(() => {
2014
+ focusSeries.value = emphasisSeriesName.value;
1894
2015
  render(true);
1895
2016
  syncScenes();
1896
2017
  setupResizeObserver();
@@ -1924,11 +2045,98 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1924
2045
  });
1925
2046
  let cachedDataExtent = null;
1926
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
+ }
1927
2130
  __expose({
1928
2131
  refresh: () => render(true),
1929
2132
  update(newOptions) {
1930
2133
  Object.assign(props.options, newOptions);
1931
2134
  cachedDataExtent = null;
2135
+ cachedPadding = null;
2136
+ scatterHitCache = null;
2137
+ legendBoundsCache = null;
2138
+ ganttLayoutCache = null;
2139
+ sankeyLayoutCache = null;
1932
2140
  specVersion.value++;
1933
2141
  debouncedRender(true);
1934
2142
  },
@@ -1938,9 +2146,18 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1938
2146
  destroy() {
1939
2147
  cleanupResizeObserver();
1940
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
+ }
1941
2156
  stopAnimation();
1942
2157
  stopHoverAnimation();
2158
+ stopTweenAnimation();
1943
2159
  stopSceneTimer();
2160
+ stopFocusAnimation();
1944
2161
  if (tooltipRafId !== null) {
1945
2162
  cancelAnimationFrame(tooltipRafId);
1946
2163
  tooltipRafId = null;
@@ -2016,7 +2233,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
2016
2233
  };
2017
2234
  const width = canvas.width / dpr.value;
2018
2235
  const height = canvas.height / dpr.value;
2019
- const padding = getPadding(effectiveOptions.value, width);
2236
+ const padding = paddingFor(effectiveOptions.value, width);
2020
2237
  cachedPlotArea = {
2021
2238
  x: padding.left,
2022
2239
  y: padding.top,
@@ -2150,7 +2367,10 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
2150
2367
  class: "ev-chart",
2151
2368
  style: normalizeStyle([containerStyle.value, { cursor: cursorStyle.value }]),
2152
2369
  role: "img",
2153
- "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
2154
2374
  }, [
2155
2375
  !isEmpty.value && !internalError.value ? (openBlock(), createElementBlock("canvas", {
2156
2376
  key: 0,
@@ -2229,10 +2449,10 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
2229
2449
  _: 1
2230
2450
  }),
2231
2451
  createElementVNode("div", _hoisted_11, toDisplayString(ariaLiveText.value), 1)
2232
- ], 12, _hoisted_1);
2452
+ ], 44, _hoisted_1);
2233
2453
  };
2234
2454
  }
2235
- }), [["__scopeId", "data-v-df8a90cc"]]);
2455
+ }), [["__scopeId", "data-v-62da63e1"]]);
2236
2456
  //#endregion
2237
2457
  //#region src/useChart.js
2238
2458
  var USE_CHART_KEY = /* @__PURE__ */ Symbol("ev-chart-context");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wil-works/evoke-charts",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Evoke Charts — 零依赖 Canvas 自绘图表库(Vue3),Evoke 生态独立图表包",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",