@wil-works/evoke-charts 0.4.0 → 0.5.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/ai.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as SPEC_RULES, c as inferColumns, d as parseNumeric, f as chartOptionsSchema, i as formatExamples, l as looksLikeTime, n as buildChartPrompt, o as detectIntent, p as validateOptions, r as SPEC_EXAMPLES, s as generateChartSpec, t as lintChartSpec, u as parseDataTable } from "./ai-BdpMydhW.js";
1
+ import { a as SPEC_RULES, c as inferColumns, d as parseNumeric, f as chartOptionsSchema, i as formatExamples, l as looksLikeTime, n as buildChartPrompt, o as detectIntent, p as validateOptions, r as SPEC_EXAMPLES, s as generateChartSpec, t as lintChartSpec, u as parseDataTable } from "./ai-BmXDig6M.js";
2
2
  export { SPEC_EXAMPLES, SPEC_RULES, buildChartPrompt, chartOptionsSchema, detectIntent, formatExamples, generateChartSpec, inferColumns, lintChartSpec, looksLikeTime, parseDataTable, parseNumeric, validateOptions };
@@ -1,5 +1,5 @@
1
1
 
2
- .ev-chart[data-v-04ce6a3d] {
2
+ .ev-chart[data-v-df8a90cc] {
3
3
  position: relative;
4
4
  /* 容器不带边框:是否加框、加多粗由宿主决定,图表只负责内容;
5
5
  圆角裁剪下沉到 canvas 与浮层自身(border-radius: inherit) */
@@ -9,7 +9,7 @@
9
9
  }
10
10
 
11
11
  /* 关键:canvas 必须能接收鼠标事件 */
12
- .ev-chart__canvas[data-v-04ce6a3d] {
12
+ .ev-chart__canvas[data-v-df8a90cc] {
13
13
  position: absolute;
14
14
  top: 0;
15
15
  left: 0;
@@ -23,14 +23,14 @@
23
23
 
24
24
  /* HTML 覆盖层:只在内容层,不与 canvas 抢交互(子元素可自行 pointer-events:auto),
25
25
  也不压过 tooltip(z-index 100) */
26
- .ev-chart__overlay[data-v-04ce6a3d] {
26
+ .ev-chart__overlay[data-v-df8a90cc] {
27
27
  position: absolute;
28
28
  inset: 0;
29
29
  z-index: 1;
30
30
  pointer-events: none;
31
31
  border-radius: inherit;
32
32
  }
33
- .ev-chart__tooltip[data-v-04ce6a3d] {
33
+ .ev-chart__tooltip[data-v-df8a90cc] {
34
34
  position: absolute;
35
35
  pointer-events: none;
36
36
  z-index: 100;
@@ -50,47 +50,47 @@
50
50
  }
51
51
 
52
52
  /* tooltip 淡入淡出(仅 opacity,位置由 left/top transition 负责) */
53
- .ev-chart-tooltip-enter-active[data-v-04ce6a3d],
54
- .ev-chart-tooltip-leave-active[data-v-04ce6a3d] {
53
+ .ev-chart-tooltip-enter-active[data-v-df8a90cc],
54
+ .ev-chart-tooltip-leave-active[data-v-df8a90cc] {
55
55
  transition: var(--ev-tooltip-fade-transition);
56
56
  }
57
57
 
58
58
  /* 进入期间禁用位置 transition,避免首次出现时从旧位置滑入 */
59
- .ev-chart-tooltip-enter-active[data-v-04ce6a3d] {
59
+ .ev-chart-tooltip-enter-active[data-v-df8a90cc] {
60
60
  transition:
61
61
  var(--ev-tooltip-fade-transition),
62
62
  left 0s,
63
63
  top 0s;
64
64
  }
65
- .ev-chart-tooltip-enter-from[data-v-04ce6a3d],
66
- .ev-chart-tooltip-leave-to[data-v-04ce6a3d] {
65
+ .ev-chart-tooltip-enter-from[data-v-df8a90cc],
66
+ .ev-chart-tooltip-leave-to[data-v-df8a90cc] {
67
67
  opacity: 0;
68
68
  }
69
69
 
70
70
  /* v-html 注入的内容不带 scoped 属性,需用 :deep() 穿透 */
71
- [data-v-04ce6a3d] .ev-chart__tooltip-title {
71
+ [data-v-df8a90cc] .ev-chart__tooltip-title {
72
72
  color: var(--ev-text-color-secondary, #6b7280);
73
73
  font-size: 12px;
74
74
  margin-bottom: 6px;
75
75
  font-weight: 500;
76
76
  }
77
- [data-v-04ce6a3d] .ev-chart__tooltip-item {
77
+ [data-v-df8a90cc] .ev-chart__tooltip-item {
78
78
  display: flex;
79
79
  align-items: center;
80
80
  gap: 8px;
81
81
  padding: 2px 0;
82
82
  }
83
- [data-v-04ce6a3d] .ev-chart__tooltip-dot {
83
+ [data-v-df8a90cc] .ev-chart__tooltip-dot {
84
84
  width: 8px;
85
85
  height: 8px;
86
86
  border-radius: 50%;
87
87
  flex-shrink: 0;
88
88
  display: inline-block;
89
89
  }
90
- [data-v-04ce6a3d] .ev-chart__tooltip-name {
90
+ [data-v-df8a90cc] .ev-chart__tooltip-name {
91
91
  color: var(--ev-text-color-primary, #111827);
92
92
  }
93
- [data-v-04ce6a3d] .ev-chart__tooltip-value {
93
+ [data-v-df8a90cc] .ev-chart__tooltip-value {
94
94
  color: var(--ev-text-color-primary, #111827);
95
95
  font-weight: 600;
96
96
  margin-left: auto;
@@ -99,7 +99,7 @@
99
99
  }
100
100
 
101
101
  /* ══════ 空数据占位 ══════ */
102
- .ev-chart__empty[data-v-04ce6a3d] {
102
+ .ev-chart__empty[data-v-df8a90cc] {
103
103
  position: absolute;
104
104
  inset: 0;
105
105
  border-radius: inherit;
@@ -112,7 +112,7 @@
112
112
  gap: 14px;
113
113
  user-select: none;
114
114
  }
115
- .ev-chart__empty-icon-wrap[data-v-04ce6a3d] {
115
+ .ev-chart__empty-icon-wrap[data-v-df8a90cc] {
116
116
  display: flex;
117
117
  align-items: center;
118
118
  justify-content: center;
@@ -121,19 +121,19 @@
121
121
  border-radius: 16px;
122
122
  background: var(--ev-fill-color-light, #f5f7fa);
123
123
  }
124
- .ev-chart__empty-img[data-v-04ce6a3d] {
124
+ .ev-chart__empty-img[data-v-df8a90cc] {
125
125
  width: 32px;
126
126
  height: 32px;
127
127
  opacity: 0.4;
128
128
  }
129
- .ev-chart__empty-text[data-v-04ce6a3d] {
129
+ .ev-chart__empty-text[data-v-df8a90cc] {
130
130
  letter-spacing: 0.04em;
131
131
  font-weight: 500;
132
132
  font-size: 13px;
133
133
  }
134
134
 
135
135
  /* ══════ 加载中骨架 ══════ */
136
- .ev-chart__loading[data-v-04ce6a3d] {
136
+ .ev-chart__loading[data-v-df8a90cc] {
137
137
  position: absolute;
138
138
  inset: 0;
139
139
  border-radius: inherit;
@@ -142,20 +142,20 @@
142
142
  justify-content: center;
143
143
  background: var(--ev-bg-color, #ffffff);
144
144
  }
145
- .ev-chart__loading-bars[data-v-04ce6a3d] {
145
+ .ev-chart__loading-bars[data-v-df8a90cc] {
146
146
  display: flex;
147
147
  align-items: flex-end;
148
148
  gap: 6px;
149
149
  height: 60px;
150
150
  }
151
- .ev-chart__loading-bar[data-v-04ce6a3d] {
151
+ .ev-chart__loading-bar[data-v-df8a90cc] {
152
152
  width: 10px;
153
153
  height: 40px;
154
154
  border-radius: 4px;
155
155
  background: var(--ev-border-color, #e2e8f0);
156
- animation: ev-chart-shimmer-04ce6a3d 1.2s ease-in-out infinite;
156
+ animation: ev-chart-shimmer-df8a90cc 1.2s ease-in-out infinite;
157
157
  }
158
- @keyframes ev-chart-shimmer-04ce6a3d {
158
+ @keyframes ev-chart-shimmer-df8a90cc {
159
159
  0%,
160
160
  100% {
161
161
  transform: scaleY(0.4);
@@ -168,7 +168,7 @@
168
168
  }
169
169
 
170
170
  /* ══════ 错误边界 ══════ */
171
- .ev-chart__error[data-v-04ce6a3d] {
171
+ .ev-chart__error[data-v-df8a90cc] {
172
172
  position: absolute;
173
173
  inset: 0;
174
174
  border-radius: inherit;
@@ -182,13 +182,13 @@
182
182
  padding: 20px;
183
183
  text-align: center;
184
184
  }
185
- .ev-chart__error-icon[data-v-04ce6a3d] {
185
+ .ev-chart__error-icon[data-v-df8a90cc] {
186
186
  display: flex;
187
187
  align-items: center;
188
188
  justify-content: center;
189
189
  font-size: 30px;
190
190
  }
191
- .ev-chart__error-text[data-v-04ce6a3d] {
191
+ .ev-chart__error-text[data-v-df8a90cc] {
192
192
  font-size: 12px;
193
193
  line-height: 1.5;
194
194
  max-width: 80%;
@@ -196,17 +196,17 @@
196
196
  }
197
197
 
198
198
  /* ══════ 进入/离开淡入淡出 ══════ */
199
- .ev-chart-fade-enter-active[data-v-04ce6a3d],
200
- .ev-chart-fade-leave-active[data-v-04ce6a3d] {
199
+ .ev-chart-fade-enter-active[data-v-df8a90cc],
200
+ .ev-chart-fade-leave-active[data-v-df8a90cc] {
201
201
  transition: opacity 0.2s ease;
202
202
  }
203
- .ev-chart-fade-enter-from[data-v-04ce6a3d],
204
- .ev-chart-fade-leave-to[data-v-04ce6a3d] {
203
+ .ev-chart-fade-enter-from[data-v-df8a90cc],
204
+ .ev-chart-fade-leave-to[data-v-df8a90cc] {
205
205
  opacity: 0;
206
206
  }
207
207
 
208
208
  /* ══════ 无障碍:sr-only ══════ */
209
- .ev-chart__sr-only[data-v-04ce6a3d] {
209
+ .ev-chart__sr-only[data-v-df8a90cc] {
210
210
  position: absolute;
211
211
  width: 1px;
212
212
  height: 1px;
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 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";
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();
@@ -126,6 +126,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
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,12 @@ 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": return !opt.vennData || opt.vennData.length === 0;
144
+ case "gantt": return !opt.ganttData || opt.ganttData.length === 0;
145
+ case "scatter-matrix": return !opt.matrixFields?.length || !opt.matrixData?.length;
139
146
  default: return !opt.labels?.length || !opt.series?.length;
140
147
  }
141
148
  });
@@ -159,6 +166,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
159
166
  let hoverIndex = -1;
160
167
  let hoverAnimProgress = 0;
161
168
  let hoverAnimFrameId = null;
169
+ let hoverAnimDone = null;
162
170
  const hiddenSeries = ref(/* @__PURE__ */ new Set());
163
171
  const focusSeries = ref(null);
164
172
  const emphasisSeriesName = computed(() => {
@@ -443,12 +451,44 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
443
451
  mouseY,
444
452
  showCrosshair: hoverIndex >= 0,
445
453
  hoverAnimProgress,
454
+ focusAnimProgress,
446
455
  zoomRange: isZoomEnabled() ? zoomRange.value : void 0,
447
456
  brushRect: brushRect.value,
448
457
  focusSeries: focusSeries.value,
449
458
  hoveredToolbox: hoveredToolbox.value
450
459
  };
451
460
  }
461
+ let focusAnimProgress = 1;
462
+ let focusAnimFrameId = null;
463
+ function animateFocusProgress() {
464
+ if (focusAnimFrameId !== null) {
465
+ cancelAnimationFrame(focusAnimFrameId);
466
+ focusAnimFrameId = null;
467
+ }
468
+ const target = focusSeries.value ? 1 : 0;
469
+ if (props.options.animation?.enabled === false || target === focusAnimProgress) {
470
+ focusAnimProgress = target;
471
+ redraw();
472
+ return;
473
+ }
474
+ const start = focusAnimProgress;
475
+ const startTime = performance.now();
476
+ const duration = 180;
477
+ function frame(now) {
478
+ const tRaw = Math.min((now - startTime) / duration, 1);
479
+ focusAnimProgress = start + (target - start) * (1 - Math.pow(1 - tRaw, 3));
480
+ redraw();
481
+ if (tRaw < 1) focusAnimFrameId = requestAnimationFrame(frame);
482
+ else focusAnimFrameId = null;
483
+ }
484
+ focusAnimFrameId = requestAnimationFrame(frame);
485
+ }
486
+ function stopFocusAnimation() {
487
+ if (focusAnimFrameId !== null) {
488
+ cancelAnimationFrame(focusAnimFrameId);
489
+ focusAnimFrameId = null;
490
+ }
491
+ }
452
492
  function snapshotSeriesData(opt) {
453
493
  const map = /* @__PURE__ */ new Map();
454
494
  (opt.series || []).forEach((s) => map.set(s.name, s.data.map((v) => isMissingValue(v) ? 0 : v)));
@@ -564,31 +604,40 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
564
604
  cancelAnimationFrame(hoverAnimFrameId);
565
605
  hoverAnimFrameId = null;
566
606
  }
607
+ hoverAnimDone = null;
567
608
  }
568
609
  function usesHoverAnimation() {
569
610
  const t = props.options.type;
570
- return t === "pie" || t === "doughnut" || t === "rose" || t === "sunburst" || t === "treemap";
611
+ return t === "pie" || t === "doughnut" || t === "rose" || t === "sunburst" || t === "treemap" || t === "radar";
612
+ }
613
+ function animatesHoverEnter() {
614
+ const t = props.options.type;
615
+ return t === "pie" || t === "doughnut" || t === "rose" || t === "radar" || t === "venn" || t === "sankey" || t === "chord" || t === "arc";
571
616
  }
572
617
  function clearHover() {
573
618
  if (hoverIndex === -1) return false;
574
- hoverIndex = -1;
575
- const t = props.options.type;
576
- if (t === "pie" || t === "doughnut" || t === "rose") startHoverAnimation(-1);
619
+ if (animatesHoverEnter()) startHoverAnimation(-1, () => {
620
+ hoverIndex = -1;
621
+ redraw();
622
+ });
577
623
  else {
624
+ hoverIndex = -1;
578
625
  stopHoverAnimation();
579
626
  hoverAnimProgress = 0;
580
627
  redraw();
581
628
  }
582
629
  return true;
583
630
  }
584
- function startHoverAnimation(dir) {
585
- if (!usesHoverAnimation()) return;
631
+ function startHoverAnimation(dir, onDone) {
632
+ if (!animatesHoverEnter()) return;
586
633
  if (props.options.animation?.enabled === false) {
587
634
  hoverAnimProgress = dir > 0 ? 1 : 0;
588
635
  redraw();
636
+ if (onDone) onDone();
589
637
  return;
590
638
  }
591
639
  stopHoverAnimation();
640
+ hoverAnimDone = onDone || null;
592
641
  const duration = 220;
593
642
  const startTime = performance.now();
594
643
  const startProgress = hoverAnimProgress;
@@ -604,7 +653,12 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
604
653
  }
605
654
  renderChart(canvasRef.value, buildRenderParams(1));
606
655
  if (tRaw < 1) hoverAnimFrameId = requestAnimationFrame(frame);
607
- else hoverAnimFrameId = null;
656
+ else {
657
+ hoverAnimFrameId = null;
658
+ const done = hoverAnimDone;
659
+ hoverAnimDone = null;
660
+ if (done) done();
661
+ }
608
662
  }
609
663
  hoverAnimFrameId = requestAnimationFrame(frame);
610
664
  }
@@ -624,7 +678,12 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
624
678
  "rose",
625
679
  "radar",
626
680
  "funnel",
627
- "scatter"
681
+ "scatter",
682
+ "sankey",
683
+ "chord",
684
+ "arc",
685
+ "venn",
686
+ "candle"
628
687
  ];
629
688
  function checkLegendHit(x, y) {
630
689
  const canvas = canvasRef.value;
@@ -751,7 +810,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
751
810
  const i = Math.floor(relativeY / stepHeight);
752
811
  if (i < 0 || i >= drawData.length) return null;
753
812
  const data = drawData[i];
754
- const color = data.color || theme.colors[allFunnel.indexOf(data) % theme.colors.length];
813
+ const color = funnelStepColor(data, allFunnel.indexOf(data) < 0 ? i : allFunnel.indexOf(data), theme);
755
814
  return {
756
815
  index: i,
757
816
  params: {
@@ -794,27 +853,61 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
794
853
  };
795
854
  }
796
855
  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];
856
+ const visible = (options.boxData || []).filter((b) => !(b.group && hiddenSeries.value.has(b.group)) && !hiddenSeries.value.has(b.label));
857
+ if (visible.length === 0) return null;
858
+ const showOutliers = options.showOutliers !== false;
859
+ const allValues = visible.flatMap((b) => [
860
+ b.min,
861
+ b.max,
862
+ ...showOutliers ? b.outliers || [] : []
863
+ ]);
864
+ 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);
866
+ return boxplotHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value, {
867
+ min: ext.min,
868
+ max: ext.max
869
+ });
870
+ }
871
+ 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);
873
+ if (options.type === "venn") return vennHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries.value);
874
+ 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);
877
+ if (options.type === "scatter-matrix") {
878
+ const fields = options.matrixFields || [];
879
+ const records = options.matrixData || [];
880
+ if (fields.length === 0 || records.length === 0) return null;
881
+ const cells = computeMatrixCells(plotArea, fields);
882
+ const cell = cells.find((c) => canvasX >= c.x && canvasX <= c.x + c.width && canvasY >= c.y && canvasY <= c.y + c.height);
883
+ if (!cell || cell.row === cell.col) return null;
884
+ const extX = matrixFieldExtent(records, cell.xField);
885
+ const extY = matrixFieldExtent(records, cell.yField);
886
+ const px = (v) => cell.x + 5 + (v - extX.min) / (extX.max - extX.min) * (cell.width - 10);
887
+ const py = (v) => cell.y + cell.height - 5 - (v - extY.min) / (extY.max - extY.min) * (cell.height - 10);
888
+ let nearest = -1;
889
+ let minDist = Infinity;
890
+ records.forEach((record, ri) => {
891
+ const xv = record[cell.xField];
892
+ const yv = record[cell.yField];
893
+ if (typeof xv !== "number" || typeof yv !== "number") return;
894
+ const dist = Math.sqrt((canvasX - px(xv)) ** 2 + (canvasY - py(yv)) ** 2);
895
+ if (dist < minDist) {
896
+ minDist = dist;
897
+ nearest = ri;
898
+ }
899
+ });
900
+ if (nearest < 0 || minDist > 18) return null;
901
+ const record = records[nearest];
902
+ const cellIndex = cells.indexOf(cell);
804
903
  return {
805
- index: i,
904
+ index: cellIndex,
806
905
  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(),
906
+ seriesName: record.label || `${cell.yField} × ${cell.xField}`,
907
+ name: `${cell.xField}=${record[cell.xField]}, ${cell.yField}=${record[cell.yField]}`,
908
+ value: record[cell.yField],
909
+ color: record.color || theme.colors[0],
910
+ dataIndex: cellIndex,
818
911
  seriesIndex: 0
819
912
  }
820
913
  };
@@ -886,22 +979,56 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
886
979
  if (options.type === "scatter") {
887
980
  const scatterData = options.scatterData || [];
888
981
  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;
982
+ if (options.facet === true) {
983
+ const grid = computeFacetGrids(scatterData, plotArea, options);
984
+ if (!grid) return null;
985
+ for (const facet of grid.facets) {
986
+ const a = facet.area;
987
+ if (canvasX < a.x || canvasX > a.x + a.width || canvasY < a.y || canvasY > a.y + a.height) continue;
988
+ let nearestLocal = -1;
989
+ let minDistance = Infinity;
990
+ facet.data.forEach((point, i) => {
991
+ const ppx = facet.toX(point.x);
992
+ const ppy = facet.toY(point.y);
993
+ const dist = Math.sqrt((canvasX - ppx) ** 2 + (canvasY - ppy) ** 2);
994
+ if (dist < minDistance) {
995
+ minDistance = dist;
996
+ nearestLocal = i;
997
+ }
998
+ });
999
+ if (nearestLocal < 0 || minDistance > 24) return null;
1000
+ const point = facet.data[nearestLocal];
1001
+ const globalIdx = scatterData.indexOf(point);
1002
+ const color = point.color || theme.colors[globalIdx % theme.colors.length];
1003
+ return {
1004
+ index: globalIdx,
1005
+ params: {
1006
+ seriesName: point.group || point.label || `Point ${globalIdx + 1}`,
1007
+ name: `(${point.x}, ${point.y})`,
1008
+ value: point.y,
1009
+ color,
1010
+ dataIndex: globalIdx,
1011
+ seriesIndex: 0
1012
+ }
1013
+ };
1014
+ }
1015
+ return null;
1016
+ }
893
1017
  const yValues = scatterData.map((d) => d.y);
894
1018
  const axisConfig = options.yAxis || {};
895
1019
  const rawMin = axisConfig.min ?? Math.min(...yValues);
896
1020
  const rawMax = axisConfig.max ?? Math.max(...yValues);
897
1021
  const yExt = resolveTickExtendedRange(rawMin, rawMax, axisConfig.ticks || 5);
898
- const yPadding = (yExt.max - yExt.min) * .05;
1022
+ const positions = scatterPointPositions(scatterData, {
1023
+ min: yExt.min,
1024
+ max: yExt.max
1025
+ }, plotArea, options);
1026
+ const groupColors = scatterGroupColors(scatterData, theme);
899
1027
  let nearestIndex = -1;
900
1028
  let minDistance = Infinity;
901
1029
  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;
1030
+ if (hiddenSeries.value.has(point.label || `P${i + 1}`) || point.group && hiddenSeries.value.has(point.group)) return;
1031
+ const [px, py] = positions[i];
905
1032
  const dist = Math.sqrt((canvasX - px) ** 2 + (canvasY - py) ** 2);
906
1033
  if (dist < minDistance) {
907
1034
  minDistance = dist;
@@ -910,11 +1037,11 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
910
1037
  });
911
1038
  if (nearestIndex >= 0 && minDistance < 30) {
912
1039
  const point = scatterData[nearestIndex];
913
- const color = point.color || theme.colors[nearestIndex % theme.colors.length];
1040
+ const color = point.color || (point.group && groupColors.has(point.group) ? groupColors.get(point.group) : theme.colors[nearestIndex % theme.colors.length]);
914
1041
  return {
915
1042
  index: nearestIndex,
916
1043
  params: {
917
- seriesName: point.label || `Point ${nearestIndex + 1}`,
1044
+ seriesName: point.label || point.group || `Point ${nearestIndex + 1}`,
918
1045
  name: `(${point.x}, ${point.y})`,
919
1046
  value: point.y,
920
1047
  color,
@@ -944,6 +1071,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
944
1071
  candle.high,
945
1072
  candle.low
946
1073
  ],
1074
+ volume: (options.volumeData || [])[i],
947
1075
  color,
948
1076
  dataIndex: i,
949
1077
  seriesIndex: 0
@@ -1062,6 +1190,16 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1062
1190
  const centerY = plotArea.y + plotArea.height / 2;
1063
1191
  const radius = Math.max(40, Math.min(plotArea.width, plotArea.height) / 2 - 30);
1064
1192
  const angleStep = Math.PI * 2 / indicators.length;
1193
+ const labelOffset = options.radarLabelOffset ?? 12;
1194
+ let labelHit = -1;
1195
+ indicators.forEach((indicator, i) => {
1196
+ if (labelHit >= 0) return;
1197
+ const angle = i * angleStep - Math.PI / 2;
1198
+ const lx = centerX + Math.cos(angle) * (radius + labelOffset);
1199
+ const ly = centerY + Math.sin(angle) * (radius + labelOffset);
1200
+ if (Math.sqrt((canvasX - lx) ** 2 + (canvasY - ly) ** 2) <= 18) labelHit = i;
1201
+ });
1202
+ if (labelHit >= 0) return radarAxisHover(labelHit, indicators, series, options, theme);
1065
1203
  let nearestIndex = -1;
1066
1204
  let minDist = Infinity;
1067
1205
  indicators.forEach((_, i) => {
@@ -1078,18 +1216,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1078
1216
  }
1079
1217
  });
1080
1218
  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
- };
1219
+ return radarAxisHover(nearestIndex, indicators, series, options, theme);
1093
1220
  }
1094
1221
  const labels = options.labels || [];
1095
1222
  if (labels.length === 0) return null;
@@ -1097,6 +1224,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1097
1224
  if (dataIndex < 0 || dataIndex >= labels.length) return null;
1098
1225
  const visibleSeries = (options.series || []).filter((s) => !hiddenSeries.value.has(s.name));
1099
1226
  if (visibleSeries.length === 0) return null;
1227
+ const volume = (options.volumeData || [])[dataIndex];
1100
1228
  if (props.options.tooltip?.showAllSeries !== false && visibleSeries.length > 1) {
1101
1229
  if (visibleSeries.every((s) => isMissingValue(s.data[dataIndex]))) return null;
1102
1230
  return {
@@ -1105,6 +1233,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1105
1233
  seriesName: s.name,
1106
1234
  name: labels[dataIndex],
1107
1235
  value: s.data[dataIndex],
1236
+ volume,
1108
1237
  color: s.color || theme.colors[options.series.indexOf(s) % theme.colors.length],
1109
1238
  dataIndex: dataIndex + sliceStartOffset(),
1110
1239
  seriesIndex: sIdx
@@ -1120,6 +1249,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1120
1249
  seriesName: series.name,
1121
1250
  name: labels[dataIndex],
1122
1251
  value: series.data[dataIndex],
1252
+ volume,
1123
1253
  color,
1124
1254
  dataIndex: dataIndex + sliceStartOffset(),
1125
1255
  seriesIndex: 0
@@ -1127,6 +1257,20 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1127
1257
  };
1128
1258
  }
1129
1259
  }
1260
+ /** 雷达轴悬浮:返回该维度上全部可见系列的 tooltip 行 */
1261
+ function radarAxisHover(axisIndex, indicators, series, options, theme) {
1262
+ return {
1263
+ index: axisIndex,
1264
+ params: series.map((s, sIdx) => ({
1265
+ seriesName: s.name,
1266
+ name: indicators[axisIndex].name,
1267
+ value: s.data[axisIndex],
1268
+ color: s.color || theme.colors[(options.radarSeries || []).indexOf(s) % theme.colors.length],
1269
+ dataIndex: axisIndex,
1270
+ seriesIndex: sIdx
1271
+ }))
1272
+ };
1273
+ }
1130
1274
  function setZoomRange(next) {
1131
1275
  const minSpan = INTERACTION.zoom.minSpan;
1132
1276
  let { start, end } = next;
@@ -1444,7 +1588,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1444
1588
  const nextFocus = props.options.legend?.hoverEmphasis !== false && legendName ? legendName : null;
1445
1589
  if (nextFocus !== focusSeries.value) {
1446
1590
  focusSeries.value = nextFocus;
1447
- redraw();
1591
+ animateFocusProgress();
1448
1592
  }
1449
1593
  }
1450
1594
  if (legendName) {
@@ -1454,13 +1598,13 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1454
1598
  }
1455
1599
  if (e.pointerType === "touch") return;
1456
1600
  const result = getHoveredData(e.clientX, e.clientY);
1457
- const isPieLike = props.options.type === "pie" || props.options.type === "doughnut" || props.options.type === "rose";
1601
+ const enterAnimated = animatesHoverEnter();
1458
1602
  if (result && props.options.tooltip?.show !== false) {
1459
1603
  const newIndex = result.index;
1460
1604
  if (newIndex !== hoverIndex) {
1461
1605
  const prevIndex = hoverIndex;
1462
1606
  hoverIndex = newIndex;
1463
- if (isPieLike) {
1607
+ if (enterAnimated) {
1464
1608
  if (prevIndex === -1) startHoverAnimation(1);
1465
1609
  else {
1466
1610
  stopHoverAnimation();
@@ -1484,7 +1628,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1484
1628
  });
1485
1629
  ariaLiveText.value = `${emitParams.seriesName}: ${formatTooltipValue(emitParams.value)}`;
1486
1630
  }
1487
- }
1631
+ } else if (enterAnimated && hoverAnimProgress < 1) startHoverAnimation(1);
1488
1632
  applyTooltipContent(result.params);
1489
1633
  if (props.options.tooltip?.trigger === "click") return;
1490
1634
  lastMouseClientX = e.clientX;
@@ -1771,6 +1915,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
1771
1915
  stopHoverAnimation();
1772
1916
  stopTweenAnimation();
1773
1917
  stopSceneTimer();
1918
+ stopFocusAnimation();
1774
1919
  if (tooltipRafId !== null) {
1775
1920
  cancelAnimationFrame(tooltipRafId);
1776
1921
  tooltipRafId = null;
@@ -2087,7 +2232,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
2087
2232
  ], 12, _hoisted_1);
2088
2233
  };
2089
2234
  }
2090
- }), [["__scopeId", "data-v-04ce6a3d"]]);
2235
+ }), [["__scopeId", "data-v-df8a90cc"]]);
2091
2236
  //#endregion
2092
2237
  //#region src/useChart.js
2093
2238
  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.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Evoke Charts — 零依赖 Canvas 自绘图表库(Vue3),Evoke 生态独立图表包",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",