@wil-works/evoke-charts 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import { C as DOWN_COLOR, D as getSeriesColors, E as formatValue, O as readChartToken, T as VOLUME_SERIES_NAME, b as resolveChartPalette, n as validateOptions3d, t as chart3dOptionsSchema, w as UP_COLOR, x as CHART_COLORS, y as CHART_PALETTES } from "./schema-CM4hKlpc.js";
1
2
  //#region src/interactions.js
2
3
  var INTERACTION = {
3
4
  focusDimAlpha: .22,
@@ -67,302 +68,34 @@ function pickTooltipRows(params) {
67
68
  });
68
69
  }
69
70
  //#endregion
70
- //#region src/types.js
71
- var DEFAULT_I18N_ZH = {
72
- tooltip: {
73
- open: "开",
74
- close: "收",
75
- high: "高",
76
- low: "低",
77
- total: "总计",
78
- average: "平均"
79
- },
80
- legend: {
81
- show: "显示",
82
- hide: "隐藏"
83
- },
84
- noData: "暂无数据"
85
- };
86
- /**
87
- * 系列色令牌槽位 — 每槽按顺序尝试读取专用数据色令牌(--ev-color-series-N),
88
- * 槽 1 回退 --ev-color-primary(品牌主色跟随);
89
- * 任一槽无令牌时该槽回落内置成套色板(与语义状态色解耦的数据色板)。
90
- * CHART_COLORS 仅作为 SSR / 无 DOM 环境的静态兜底。
91
- */
92
- var SERIES_COLOR_SLOTS = [
93
- ["--ev-color-series-1", "--ev-color-primary"],
94
- ["--ev-color-series-2"],
95
- ["--ev-color-series-3"],
96
- ["--ev-color-series-4"],
97
- ["--ev-color-series-5"],
98
- ["--ev-color-series-6"],
99
- ["--ev-color-series-7"],
100
- ["--ev-color-series-8"]
101
- ];
102
- function readChartToken(name, fallback) {
103
- try {
104
- if (typeof document === "undefined") return fallback;
105
- return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
106
- } catch (e) {
107
- return fallback;
71
+ //#region src/extent.js
72
+ function minOf(values, ...extras) {
73
+ let min = Infinity;
74
+ for (let i = 0; i < values.length; i++) {
75
+ const v = values[i];
76
+ if (Number.isNaN(v)) return NaN;
77
+ if (v < min) min = v;
108
78
  }
109
- }
110
- function getSeriesColors(fallbackColors) {
111
- return SERIES_COLOR_SLOTS.map((tokens, i) => {
112
- for (const t of tokens) {
113
- const v = readChartToken(t, "");
114
- if (v) return v;
115
- }
116
- return fallbackColors[i];
117
- });
118
- }
119
- var CHART_COLORS = {
120
- /**
121
- * 主色板 — 品牌蓝锚定的成套数据色板(与语义状态色解耦):
122
- * 色相按取色顺序相邻差 ≥ 30°,明度带 44–68,青绿/金黄/天蓝/珊瑚/紫/石板
123
- * 取材 AntV 经典系并经品牌蓝调协调;暗色板同色相提亮。
124
- */
125
- primary: [
126
- "#175DFF",
127
- "#5AD8A6",
128
- "#F6BD16",
129
- "#6DC8EC",
130
- "#E8684A",
131
- "#9270CA",
132
- "#FF9D4D",
133
- "#5D7092"
134
- ],
135
- /** 浅色系列 — primary 的 light-9/light-8/light-7 阶梯 */
136
- light: [
137
- "#e8f0ff",
138
- "#d0e1ff",
139
- "#b9d2ff"
140
- ],
141
- /** 暗色模式系列 — 同色相提亮(对齐 evoke 暗色主色) */
142
- dark: [
143
- "#4d8bff",
144
- "#5ad8a6",
145
- "#f6bd16",
146
- "#6dc8ec",
147
- "#f08568",
148
- "#a585e8",
149
- "#ff9d4d",
150
- "#8da3bf"
151
- ],
152
- /** 热力图默认色阶(天亮蓝由浅到深) */
153
- heatmapScale: [
154
- "#f0f5ff",
155
- "#c6d9ff",
156
- "#8ab0ff",
157
- "#4d86ff",
158
- "#175DFF",
159
- "#0040c2"
160
- ]
161
- };
162
- function formatValue(value, options) {
163
- if (value === null || value === void 0 || Number.isNaN(value)) return "-";
164
- const { decimals = 0, thousandSeparator = ",", prefix = "", suffix = "", abbreviate = false } = options || {};
165
- let formatted;
166
- if (abbreviate) {
167
- const abs = Math.abs(value);
168
- if (abs >= 1e8) formatted = (value / 1e8).toFixed(2) + "亿";
169
- else if (abs >= 1e4) formatted = (value / 1e4).toFixed(2) + "万";
170
- else if (abs >= 1e3) formatted = (value / 1e3).toFixed(2) + "K";
171
- else formatted = value.toFixed(decimals);
172
- } else {
173
- const fixed = value.toFixed(decimals);
174
- if (thousandSeparator && decimals === 0) formatted = fixed.replace(/\B(?=(\d{3})+(?!\d))/g, thousandSeparator);
175
- else if (thousandSeparator) {
176
- const [intPart, decPart] = fixed.split(".");
177
- formatted = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousandSeparator) + (decPart ? "." + decPart : "");
178
- } else formatted = fixed;
79
+ for (let i = 0; i < extras.length; i++) {
80
+ const v = extras[i];
81
+ if (Number.isNaN(v)) return NaN;
82
+ if (v < min) min = v;
179
83
  }
180
- return prefix + formatted + suffix;
84
+ return min;
181
85
  }
182
- //#endregion
183
- //#region src/palettes.js
184
- var CHART_PALETTES = [
185
- {
186
- id: "classic",
187
- name: "经典",
188
- scene: "当前缺省色板的固化版,品牌蓝锚定;想保住默认观感不随换肤时用",
189
- light: [
190
- "#175DFF",
191
- "#5AD8A6",
192
- "#F6BD16",
193
- "#6DC8EC",
194
- "#E8684A",
195
- "#9270CA",
196
- "#FF9D4D",
197
- "#5D7092"
198
- ],
199
- dark: [
200
- "#4d8bff",
201
- "#5ad8a6",
202
- "#f6bd16",
203
- "#6dc8ec",
204
- "#f08568",
205
- "#a585e8",
206
- "#ff9d4d",
207
- "#8da3bf"
208
- ]
209
- },
210
- {
211
- id: "aurora",
212
- name: "极光",
213
- scene: "科技 SaaS / 数据产品——冷调现代,蓝绿紫主打的仪表盘观感",
214
- light: [
215
- "#3B82F6",
216
- "#10B981",
217
- "#F59E0B",
218
- "#8B5CF6",
219
- "#EC4899",
220
- "#06B6D4",
221
- "#F97316",
222
- "#64748B"
223
- ],
224
- dark: [
225
- "#60A5FA",
226
- "#34D399",
227
- "#FBBF24",
228
- "#A78BFA",
229
- "#F472B6",
230
- "#22D3EE",
231
- "#FB923C",
232
- "#94A3B8"
233
- ]
234
- },
235
- {
236
- id: "sunset",
237
- name: "落日",
238
- scene: "消费零售 / 生活方式 / 营销复盘——暖橙珊瑚主导,热情有温度",
239
- light: [
240
- "#F97316",
241
- "#E11D48",
242
- "#EAB308",
243
- "#14B8A6",
244
- "#8B5CF6",
245
- "#FB7185",
246
- "#0EA5E9",
247
- "#A8A29E"
248
- ],
249
- dark: [
250
- "#FB923C",
251
- "#FB7185",
252
- "#FACC15",
253
- "#2DD4BF",
254
- "#A78BFA",
255
- "#FDA4AF",
256
- "#38BDF8",
257
- "#BDB4AC"
258
- ]
259
- },
260
- {
261
- id: "morandi",
262
- name: "莫兰迪",
263
- scene: "人文 / 咨询报告 / 印刷排版——低饱和灰调,长文配图不抢字",
264
- light: [
265
- "#A3AEBB",
266
- "#C2A878",
267
- "#B08890",
268
- "#86A69D",
269
- "#9A8FA8",
270
- "#B7C4A0",
271
- "#C98F6B",
272
- "#8A8F98"
273
- ],
274
- dark: [
275
- "#B4BEC9",
276
- "#D4BC8E",
277
- "#C29AA3",
278
- "#97B5AC",
279
- "#ACA2BC",
280
- "#C5D0AF",
281
- "#D8A685",
282
- "#9CA1A9"
283
- ]
284
- },
285
- {
286
- id: "forest",
287
- name: "林间",
288
- scene: "健康 / 环保 / 农业供应链——自然绿主导,ESG 与可持续叙事",
289
- light: [
290
- "#16A34A",
291
- "#0EA5E9",
292
- "#CA8A04",
293
- "#7C3AED",
294
- "#DC2626",
295
- "#0D9488",
296
- "#D97706",
297
- "#64748B"
298
- ],
299
- dark: [
300
- "#22C55E",
301
- "#38BDF8",
302
- "#EAB308",
303
- "#8B5CF6",
304
- "#EF4444",
305
- "#14B8A6",
306
- "#F59E0B",
307
- "#94A3B8"
308
- ]
309
- },
310
- {
311
- id: "ink",
312
- name: "墨蓝",
313
- scene: "金融 / 政企 / 严肃年报——克制深稳,深蓝为骨、暖色点缀",
314
- light: [
315
- "#1E40AF",
316
- "#0F766E",
317
- "#B45309",
318
- "#6D28D9",
319
- "#BE123C",
320
- "#0369A1",
321
- "#4D7C0F",
322
- "#6B7280"
323
- ],
324
- dark: [
325
- "#6E9BF7",
326
- "#2DD4BF",
327
- "#D97706",
328
- "#8B5CF6",
329
- "#FB7185",
330
- "#38BDF8",
331
- "#84CC16",
332
- "#9CA3AF"
333
- ]
334
- },
335
- {
336
- id: "candy",
337
- name: "糖果",
338
- scene: "营销活动 / 年轻品牌 / 大屏展示——高饱和明快,视觉抓人",
339
- light: [
340
- "#EC4899",
341
- "#8B5CF6",
342
- "#F59E0B",
343
- "#22C55E",
344
- "#06B6D4",
345
- "#EF4444",
346
- "#6366F1",
347
- "#9CA3AF"
348
- ],
349
- dark: [
350
- "#F472B6",
351
- "#A78BFA",
352
- "#FBBF24",
353
- "#4ADE80",
354
- "#22D3EE",
355
- "#F87171",
356
- "#818CF8",
357
- "#A1A1AA"
358
- ]
86
+ function maxOf(values, ...extras) {
87
+ let max = -Infinity;
88
+ for (let i = 0; i < values.length; i++) {
89
+ const v = values[i];
90
+ if (Number.isNaN(v)) return NaN;
91
+ if (v > max) max = v;
359
92
  }
360
- ];
361
- /** 按 id 取色系在指定明暗模式下的 8 槽色值;未知 id 返回 null */
362
- function resolveChartPalette(id, isDark) {
363
- const palette = CHART_PALETTES.find((p) => p.id === id);
364
- if (!palette) return null;
365
- return (isDark ? palette.dark : palette.light).slice(0, 8);
93
+ for (let i = 0; i < extras.length; i++) {
94
+ const v = extras[i];
95
+ if (Number.isNaN(v)) return NaN;
96
+ if (v > max) max = v;
97
+ }
98
+ return max;
366
99
  }
367
100
  //#endregion
368
101
  //#region src/renderer/core.js
@@ -434,6 +167,89 @@ function mixColor(color, ratio, target) {
434
167
  function isMissingValue(v) {
435
168
  return v === null || v === void 0 || Number.isNaN(v);
436
169
  }
170
+ function truncateLabel(canvasCtx, label, maxWidth) {
171
+ if (canvasCtx.measureText(label).width <= maxWidth) return label;
172
+ let lo = 0, hi = label.length;
173
+ while (lo < hi) {
174
+ const mid = Math.ceil((lo + hi) / 2);
175
+ const truncated = label.slice(0, mid) + "…";
176
+ if (canvasCtx.measureText(truncated).width > maxWidth) hi = mid - 1;
177
+ else lo = mid;
178
+ }
179
+ return label.slice(0, lo) + "…";
180
+ }
181
+ function waterfallSteps(deltas, totalIndices) {
182
+ const totalIdx = new Set(totalIndices || []);
183
+ const steps = [];
184
+ let cumulative = 0;
185
+ (deltas || []).forEach((raw, i) => {
186
+ const missing = isMissingValue(raw);
187
+ const v = missing ? 0 : raw;
188
+ if (totalIdx.has(i)) {
189
+ steps.push({
190
+ value: v,
191
+ from: 0,
192
+ to: v,
193
+ isTotal: true,
194
+ missing
195
+ });
196
+ cumulative = v;
197
+ } else {
198
+ steps.push({
199
+ value: v,
200
+ from: cumulative,
201
+ to: cumulative + v,
202
+ isTotal: false,
203
+ missing
204
+ });
205
+ cumulative += v;
206
+ }
207
+ });
208
+ return steps;
209
+ }
210
+ function minMaxDecimatePoints(points, columns) {
211
+ const out = [];
212
+ const per = points.length / columns;
213
+ let pendingGap = false;
214
+ for (let b = 0; b < columns; b++) {
215
+ const start = Math.floor(b * per);
216
+ const end = Math.min(points.length, Math.max(start + 1, Math.floor((b + 1) * per)));
217
+ let first = -1;
218
+ let last = -1;
219
+ let minI = -1;
220
+ let maxI = -1;
221
+ for (let i = start; i < end; i++) {
222
+ const p = points[i];
223
+ if (!p) continue;
224
+ if (first < 0) first = i;
225
+ last = i;
226
+ if (minI < 0 || p[1] < points[minI][1]) minI = i;
227
+ if (maxI < 0 || p[1] > points[maxI][1]) maxI = i;
228
+ }
229
+ if (first < 0) {
230
+ if (out.length > 0) pendingGap = true;
231
+ continue;
232
+ }
233
+ if (pendingGap) {
234
+ out.push({
235
+ p: null,
236
+ i: first
237
+ });
238
+ pendingGap = false;
239
+ }
240
+ const picks = [.../* @__PURE__ */ new Set([
241
+ first,
242
+ minI,
243
+ maxI,
244
+ last
245
+ ])].sort((a, b) => a - b);
246
+ for (const idx of picks) out.push({
247
+ p: points[idx],
248
+ i: idx
249
+ });
250
+ }
251
+ return out;
252
+ }
437
253
  function resolveConnectNulls(options, series) {
438
254
  return series?.connectNulls ?? options.connectNulls ?? false;
439
255
  }
@@ -710,7 +526,7 @@ function getPadding(options, containerWidth = 600) {
710
526
  if (options.type === "gantt") {
711
527
  const pad2 = resolveUserPadding(options);
712
528
  const names2 = (options.ganttData || []).map((t) => t.name || "");
713
- const labelW = names2.length ? Math.max(80, Math.min(180, Math.round(Math.max(...names2.map((n) => estimateTextWidth(n, 12))) + 16))) : 100;
529
+ const labelW = names2.length ? Math.max(80, Math.min(180, Math.round(maxOf(names2.map((n) => estimateTextWidth(n, 12))) + 16))) : 100;
714
530
  const top2 = (pad2.top ?? 18) + titleBlockHeight(options) + (legendPosition === "top" ? LEGEND_BAND + legendRowsExtra : 0);
715
531
  const bottom2 = (pad2.bottom ?? 46) + (legendPosition === "bottom" ? LEGEND_BAND + legendRowsExtra : 0);
716
532
  const left2 = pad2.left ?? labelW;
@@ -735,9 +551,11 @@ function getPadding(options, containerWidth = 600) {
735
551
  if (options.heatmap?.colorBar === true || options.heatmapColorBar === true) right2 += 44;
736
552
  }
737
553
  if (options.type === "bin") {
738
- const allValues = [];
739
- (options.series || []).forEach((s) => allValues.push(...s.data.filter((v) => !isMissingValue(v))));
740
- const maxFreq = allValues.length;
554
+ let maxFreq = 0;
555
+ (options.series || []).forEach((s) => {
556
+ const data = s.data || [];
557
+ for (let i = 0; i < data.length; i++) if (!isMissingValue(data[i])) maxFreq++;
558
+ });
741
559
  const tickLabelWidth = String(maxFreq).length * 7;
742
560
  left2 = pad.left ?? Math.min(90, Math.max(36, tickLabelWidth + 16));
743
561
  bottom2 = Math.max(bottom2, 30);
@@ -914,22 +732,14 @@ function calculateRange(options, hiddenSeries, useRightAxis) {
914
732
  const seriesList = (options.series || []).filter((s) => !hiddenSeries.has(s.name) && (useRightAxis ? s.yAxis === "right" : s.yAxis !== "right"));
915
733
  if (options.type === "waterfall") {
916
734
  const wf = options.waterfall || {};
917
- const totalIdx = new Set(wf.totalIndices || []);
918
735
  const deltas = (options.series || []).filter((s) => !hiddenSeries.has(s.name))[0]?.data || [];
919
- let cumulative = 0;
920
- deltas.forEach((rawV, i) => {
921
- const v = isMissingValue(rawV) ? 0 : rawV;
922
- if (totalIdx.has(i)) {
923
- if (v > max) max = v;
924
- if (v < min) min = v;
925
- cumulative = v;
926
- } else {
927
- const from = cumulative;
928
- cumulative = cumulative + v;
929
- if (Math.max(from, cumulative) > max) max = Math.max(from, cumulative);
930
- if (Math.min(from, cumulative) < min) min = Math.min(from, cumulative);
736
+ for (const step of waterfallSteps(deltas, wf.totalIndices)) {
737
+ const points = step.isTotal ? [step.to] : [step.from, step.to];
738
+ for (const p of points) {
739
+ if (p > max) max = p;
740
+ if (p < min) min = p;
931
741
  }
932
- });
742
+ }
933
743
  } else if (options.type === "boxplot") (options.boxData || []).forEach((b) => {
934
744
  if (b.group && hiddenSeries.has(b.group)) return;
935
745
  if (b.max > max) max = b.max;
@@ -1102,22 +912,11 @@ function updateAnimation(state) {
1102
912
  if (state.startTime === 0) state.startTime = Date.now();
1103
913
  const elapsed = Date.now() - state.startTime;
1104
914
  const t = Math.min(elapsed / state.duration, 1);
1105
- state.progress = easings[state.easing](t);
915
+ state.progress = (easings[state.easing] || easings.easeOut)(t);
1106
916
  return t < 1;
1107
917
  }
1108
918
  //#endregion
1109
919
  //#region src/renderer/axes.js
1110
- function truncateLabel(canvasCtx, label, maxWidth) {
1111
- if (canvasCtx.measureText(label).width <= maxWidth) return label;
1112
- let lo = 0, hi = label.length;
1113
- while (lo < hi) {
1114
- const mid = Math.ceil((lo + hi) / 2);
1115
- const truncated = label.slice(0, mid) + "…";
1116
- if (canvasCtx.measureText(truncated).width > maxWidth) hi = mid - 1;
1117
- else lo = mid;
1118
- }
1119
- return label.slice(0, lo) + "…";
1120
- }
1121
920
  /**
1122
921
  * 抽稀刻度:保留首末两端覆盖,步长取整数倍使数量 ≤ maxCount。
1123
922
  * nice 算法的 step 候选会让实际刻度数超出请求数(如 ticks:3 得到 5 档),
@@ -1295,8 +1094,8 @@ function renderTimeXAxis(ctx) {
1295
1094
  const xAxisConfig = options.xAxis || {};
1296
1095
  const valid = parseTimeLabels(options.labels || []).filter((t) => !Number.isNaN(t));
1297
1096
  if (valid.length === 0) return;
1298
- const min = xAxisConfig.min ?? Math.min(...valid);
1299
- const max = xAxisConfig.max ?? Math.max(...valid);
1097
+ const min = xAxisConfig.min ?? minOf(valid);
1098
+ const max = xAxisConfig.max ?? maxOf(valid);
1300
1099
  const span = max - min || 1;
1301
1100
  const { values: tickTs, unit } = calculateTimeTicks(min, max, Math.max(3, Math.min(8, Math.floor(plotArea.width / 90))));
1302
1101
  const toX = (ts) => plotArea.x + (ts - min) / span * plotArea.width;
@@ -1336,8 +1135,8 @@ function timeToX(options, plotArea, index) {
1336
1135
  const timestamps = parseTimeLabels(options.labels || []);
1337
1136
  const valid = timestamps.filter((t) => !Number.isNaN(t));
1338
1137
  if (valid.length === 0) return plotArea.x;
1339
- const min = options.xAxis?.min ?? Math.min(...valid);
1340
- const span = (options.xAxis?.max ?? Math.max(...valid)) - min || 1;
1138
+ const min = options.xAxis?.min ?? minOf(valid);
1139
+ const span = (options.xAxis?.max ?? maxOf(valid)) - min || 1;
1341
1140
  const ts = timestamps[index];
1342
1141
  if (Number.isNaN(ts)) return plotArea.x + index / ((options.labels?.length || 1) - 1 || 1) * plotArea.width;
1343
1142
  return plotArea.x + (ts - min) / span * plotArea.width;
@@ -1822,7 +1621,7 @@ function funnelStepColor(data, index, theme) {
1822
1621
  function computeFunnelGeometry(plotArea, options, theme) {
1823
1622
  const all = options.funnelData || [];
1824
1623
  const drawn = options.pyramid === true ? [...all].reverse() : all;
1825
- const maxValue = drawn.length ? Math.max(...drawn.map((d) => d.value)) : 0;
1624
+ const maxValue = drawn.length ? maxOf(drawn.map((d) => d.value)) : 0;
1826
1625
  const maxWidth = plotArea.width * .8;
1827
1626
  const centerX = plotArea.x + plotArea.width / 2;
1828
1627
  const topY = plotArea.y + 8;
@@ -1867,7 +1666,7 @@ function funnelPercent(value, maxValue) {
1867
1666
  return `${Math.round(value / maxValue * 1e3) / 10}%`;
1868
1667
  }
1869
1668
  function renderFunnelChart(ctx) {
1870
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hiddenSeries } = ctx;
1669
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1, hiddenSeries } = ctx;
1871
1670
  const funnelData = (options.funnelData || []).filter((d) => !hiddenSeries.has(d.label || ""));
1872
1671
  if (funnelData.length === 0) return;
1873
1672
  const geo = computeFunnelGeometry(plotArea, {
@@ -1889,7 +1688,7 @@ function renderFunnelChart(ctx) {
1889
1688
  canvasCtx.lineTo(centerX + bottomW / 2, y + stepHeight);
1890
1689
  canvasCtx.lineTo(centerX - bottomW / 2, y + stepHeight);
1891
1690
  canvasCtx.closePath();
1892
- canvasCtx.fillStyle = i === hoverIndex ? mixColor(step.color, .08, "#000000") : step.color;
1691
+ canvasCtx.fillStyle = i === hoverIndex ? mixColor(step.color, .08 * hoverAnimProgress, "#000000") : step.color;
1893
1692
  canvasCtx.fill();
1894
1693
  canvasCtx.strokeStyle = theme.backgroundColor;
1895
1694
  canvasCtx.lineWidth = 2;
@@ -1966,11 +1765,12 @@ function renderFunnelChart(ctx) {
1966
1765
  */
1967
1766
  function renderGaugeChart(ctx) {
1968
1767
  const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex } = ctx;
1969
- const gauge = options.gauge;
1768
+ const rawGauge = options.gauge;
1769
+ const gauge = typeof rawGauge === "number" ? { value: rawGauge } : rawGauge;
1970
1770
  if (!gauge || typeof gauge !== "object") return;
1971
- const min = gauge.min || 0;
1972
- const max = gauge.max || 100;
1973
- const value = Math.max(min, Math.min(max, gauge.value));
1771
+ const min = gauge.min ?? 0;
1772
+ const max = gauge.max ?? 100;
1773
+ const value = typeof gauge.value === "number" && Number.isFinite(gauge.value) ? Math.max(min, Math.min(max, gauge.value)) : min;
1974
1774
  const toRad = (deg) => -deg * Math.PI / 180;
1975
1775
  const startAngle = toRad(gauge.startAngle ?? 220);
1976
1776
  let normalizedEnd = toRad(gauge.endAngle ?? -40);
@@ -1991,7 +1791,7 @@ function renderGaugeChart(ctx) {
1991
1791
  canvasCtx.lineCap = gauge.cornerRadius === "butt" ? "butt" : "round";
1992
1792
  canvasCtx.stroke();
1993
1793
  canvasCtx.restore();
1994
- const animatedRatio = (value - min) / (max - min) * progress;
1794
+ const animatedRatio = (max - min !== 0 ? (value - min) / (max - min) : 0) * progress;
1995
1795
  const valueAngle = startAngle + (normalizedEnd - startAngle) * animatedRatio;
1996
1796
  let progressColor = theme.colors[0];
1997
1797
  if (Array.isArray(gauge.color)) {
@@ -2127,7 +1927,9 @@ function calendarPad(n) {
2127
1927
  }
2128
1928
  /**
2129
1929
  * 日历热力布局:start/end 缺省取数据极值并对齐周边界(weekStart 默认周一);
2130
- * 数值等宽分 4 档色阶(0/缺测为空档);cellSize 按可用空间自适应。
1930
+ * 数值等宽分 4 档色阶(0/缺测为空档);granularity: 'day'(列=周 行=星期,
1931
+ * 默认)/ 'week' / 'month'(聚合为单行周合计 / 月合计,求和口径);
1932
+ * 格子为矩形,横向铺满绘图区(宽高独立自适应)。
2131
1933
  */
2132
1934
  function computeCalendarLayout(plotArea, options, theme) {
2133
1935
  const data = options.calendarData || [];
@@ -2138,6 +1940,8 @@ function computeCalendarLayout(plotArea, options, theme) {
2138
1940
  const t = Date.parse(v);
2139
1941
  return Number.isNaN(t) ? null : calendarNoon(t);
2140
1942
  };
1943
+ const gran = cal.granularity === "week" || cal.granularity === "month" ? cal.granularity : "day";
1944
+ const weekStart = cal.weekStart ?? 1;
2141
1945
  const valueMap = /* @__PURE__ */ new Map();
2142
1946
  let minTs = Infinity;
2143
1947
  let maxTs = -Infinity;
@@ -2152,67 +1956,128 @@ function computeCalendarLayout(plotArea, options, theme) {
2152
1956
  let startTs = toTs(cal.start) ?? minTs;
2153
1957
  let endTs = toTs(cal.end) ?? maxTs;
2154
1958
  if (endTs < startTs) [startTs, endTs] = [endTs, startTs];
2155
- const weekStart = cal.weekStart ?? 1;
2156
- const shift = (new Date(startTs).getDay() - weekStart + 7) % 7;
2157
- const firstWeek = startTs - shift * CALENDAR_DAY;
2158
- const tailShift = (new Date(endTs).getDay() - weekStart + 7) % 7;
2159
- const lastWeek = endTs + (6 - tailShift) * CALENDAR_DAY;
2160
- const cols = Math.round((lastWeek - firstWeek) / CALENDAR_DAY / 7);
2161
- if (cols <= 0) return null;
2162
1959
  const ramps = cal.colors || (!isLightColor(theme.backgroundColor) ? CALENDAR_RAMP_DARK : CALENDAR_RAMP_LIGHT);
1960
+ const todayTs = calendarNoon(cal.today !== void 0 ? toTs(cal.today) ?? Date.now() : Date.now());
1961
+ const buckets = [];
1962
+ const pushBucket = (label, tsList, col, row) => {
1963
+ let value = null;
1964
+ for (const ts of tsList) if (valueMap.has(ts)) {
1965
+ const v = valueMap.get(ts);
1966
+ value = typeof value === "number" ? value + v : v;
1967
+ }
1968
+ buckets.push({
1969
+ label,
1970
+ col,
1971
+ row,
1972
+ value: typeof value === "number" ? value : null,
1973
+ today: tsList.includes(todayTs)
1974
+ });
1975
+ };
1976
+ if (gran === "day") {
1977
+ const shift = (new Date(startTs).getDay() - weekStart + 7) % 7;
1978
+ const firstWeek = startTs - shift * CALENDAR_DAY;
1979
+ const tailShift = (new Date(endTs).getDay() - weekStart + 7) % 7;
1980
+ const lastWeek = endTs + (6 - tailShift) * CALENDAR_DAY;
1981
+ const cols = Math.round((lastWeek - firstWeek) / CALENDAR_DAY / 7);
1982
+ if (cols <= 0) return null;
1983
+ let prevMonth = -1;
1984
+ const monthLabels = [];
1985
+ for (let col = 0; col < cols; col++) {
1986
+ const firstDay = new Date(firstWeek + col * 7 * CALENDAR_DAY);
1987
+ if (firstDay.getMonth() !== prevMonth) {
1988
+ if (col > 0 || firstDay.getDate() <= 7) monthLabels.push({
1989
+ label: `${firstDay.getMonth() + 1}月`,
1990
+ col
1991
+ });
1992
+ prevMonth = firstDay.getMonth();
1993
+ }
1994
+ for (let row = 0; row < 7; row++) {
1995
+ const ts = firstWeek + (col * 7 + row) * CALENDAR_DAY;
1996
+ if (ts < startTs || ts > endTs) continue;
1997
+ const d = new Date(ts);
1998
+ pushBucket(`${d.getFullYear()}-${calendarPad(d.getMonth() + 1)}-${calendarPad(d.getDate())}`, [ts], col, row);
1999
+ }
2000
+ }
2001
+ var bucketLabels = monthLabels;
2002
+ var rows = 7;
2003
+ } else {
2004
+ const order = [];
2005
+ const sums = /* @__PURE__ */ new Map();
2006
+ for (let ts = startTs; ts <= endTs; ts += CALENDAR_DAY) {
2007
+ const d = new Date(ts);
2008
+ let key;
2009
+ let label;
2010
+ if (gran === "week") {
2011
+ const shift2 = (d.getDay() - weekStart + 7) % 7;
2012
+ const weekTs = ts - shift2 * CALENDAR_DAY;
2013
+ key = `w${weekTs}`;
2014
+ const wd = new Date(weekTs);
2015
+ label = `${wd.getMonth() + 1}月${wd.getDate()}日周`;
2016
+ } else {
2017
+ key = `m${d.getFullYear()}-${d.getMonth()}`;
2018
+ label = `${d.getFullYear()}-${calendarPad(d.getMonth() + 1)}`;
2019
+ }
2020
+ if (!sums.has(key)) {
2021
+ sums.set(key, null);
2022
+ order.push({
2023
+ key,
2024
+ label
2025
+ });
2026
+ }
2027
+ const v = valueMap.get(ts);
2028
+ if (v !== void 0) {
2029
+ const prev = sums.get(key);
2030
+ sums.set(key, typeof prev === "number" ? prev + v : v);
2031
+ }
2032
+ }
2033
+ order.forEach((b, i) => {
2034
+ const value = sums.get(b.key);
2035
+ buckets.push({
2036
+ label: b.label,
2037
+ col: i,
2038
+ row: 0,
2039
+ value: typeof value === "number" ? value : null,
2040
+ today: false
2041
+ });
2042
+ });
2043
+ bucketLabels = gran === "month" ? order.map((b, i) => ({
2044
+ label: b.label,
2045
+ col: i
2046
+ })) : order.map((b, i) => ({
2047
+ label: b.label,
2048
+ col: i,
2049
+ month: parseInt(b.label, 10)
2050
+ })).filter((b, i, arr) => i === 0 || b.month !== arr[i - 1].month);
2051
+ rows = 1;
2052
+ }
2163
2053
  let vMax = 0;
2164
- valueMap.forEach((v) => {
2165
- if (typeof v === "number" && v > vMax) vMax = v;
2054
+ buckets.forEach((b) => {
2055
+ if (typeof b.value === "number" && b.value > vMax) vMax = b.value;
2166
2056
  });
2167
2057
  const levelOf = (v) => {
2168
2058
  if (typeof v !== "number" || !(v > 0) || !(vMax > 0)) return 0;
2169
2059
  return Math.max(1, Math.min(4, Math.ceil(v / (vMax / 4))));
2170
2060
  };
2171
2061
  const labelTop = 18;
2172
- const leftBand = 28;
2062
+ const leftBand = gran === "day" ? 28 : 6;
2173
2063
  const gap = Math.max(1, Math.min(8, cal.cellGap ?? 3));
2174
2064
  const scaleH = cal.showScale === false ? 0 : 22;
2065
+ const cols = Math.max(1, maxOf(buckets.map((b) => b.col + 1)));
2175
2066
  const availW = plotArea.width - leftBand;
2176
2067
  const availH = plotArea.height - labelTop - scaleH;
2177
- const cell = Math.max(5, Math.min((availH - gap * 6) / 7, (availW - gap * (cols - 1)) / cols));
2178
- const gridW = cols * cell + (cols - 1) * gap;
2179
- const originX = plotArea.x + leftBand + Math.max(0, (availW - gridW) / 2);
2068
+ const cellW = Math.max(6, (availW - gap * (cols - 1)) / cols);
2069
+ const cellH = Math.max(6, (availH - gap * (rows - 1)) / rows);
2070
+ const originX = plotArea.x + leftBand;
2180
2071
  const originY = plotArea.y + labelTop;
2181
- const todayTs = calendarNoon(cal.today !== void 0 ? toTs(cal.today) ?? Date.now() : Date.now());
2182
- const cells = [];
2183
- const monthLabels = [];
2184
- let prevMonth = -1;
2185
- for (let col = 0; col < cols; col++) {
2186
- const x = originX + col * (cell + gap);
2187
- const firstDay = new Date(firstWeek + col * 7 * CALENDAR_DAY);
2188
- if (firstDay.getMonth() !== prevMonth) {
2189
- if (col > 0 || firstDay.getDate() <= 7) monthLabels.push({
2190
- label: `${firstDay.getMonth() + 1}月`,
2191
- x
2192
- });
2193
- prevMonth = firstDay.getMonth();
2194
- }
2195
- for (let row = 0; row < 7; row++) {
2196
- const ts = firstWeek + (col * 7 + row) * CALENDAR_DAY;
2197
- if (ts < startTs || ts > endTs) continue;
2198
- const value = valueMap.get(ts);
2199
- const level = levelOf(value);
2200
- const d = new Date(ts);
2201
- cells.push({
2202
- ts,
2203
- col,
2204
- row,
2205
- x,
2206
- y: originY + row * (cell + gap),
2207
- size: cell,
2208
- value: typeof value === "number" ? value : null,
2209
- color: ramps[level],
2210
- today: ts === todayTs,
2211
- label: `${d.getFullYear()}-${calendarPad(d.getMonth() + 1)}-${calendarPad(d.getDate())}`,
2212
- index: cells.length
2213
- });
2214
- }
2215
- }
2072
+ const cells = buckets.map((b, i) => ({
2073
+ ...b,
2074
+ x: originX + b.col * (cellW + gap),
2075
+ y: originY + b.row * (cellH + gap),
2076
+ w: cellW,
2077
+ h: cellH,
2078
+ color: ramps[levelOf(b.value)],
2079
+ index: i
2080
+ }));
2216
2081
  const weekdayLabels = cal.weekdayLabels || [
2217
2082
  "日",
2218
2083
  "一",
@@ -2223,20 +2088,26 @@ function computeCalendarLayout(plotArea, options, theme) {
2223
2088
  "六"
2224
2089
  ];
2225
2090
  const showRows = [];
2226
- for (let row = 0; row < 7; row++) {
2091
+ if (gran === "day") for (let row = 0; row < 7; row++) {
2227
2092
  const name = weekdayLabels[(weekStart + row) % 7];
2228
2093
  if (name === "一" || name === "三" || name === "五" || cal.showAllWeekdays === true) showRows.push({
2229
2094
  row,
2230
2095
  name,
2231
- y: originY + row * (cell + gap) + cell / 2
2096
+ y: originY + row * (cellH + gap) + cellH / 2
2232
2097
  });
2233
2098
  }
2234
2099
  return {
2235
2100
  cells,
2236
2101
  cols,
2237
- cell,
2102
+ rows,
2103
+ cellW,
2104
+ cellH,
2238
2105
  gap,
2239
- monthLabels,
2106
+ granularity: gran,
2107
+ monthLabels: bucketLabels.map((m) => ({
2108
+ label: m.label,
2109
+ x: originX + m.col * (cellW + gap)
2110
+ })),
2240
2111
  showRows,
2241
2112
  ramps,
2242
2113
  originX,
@@ -2251,17 +2122,17 @@ function renderCalendarHeatmapChart(ctx) {
2251
2122
  const cal = options.calendar || {};
2252
2123
  layout.cells.forEach((c, i) => {
2253
2124
  const colT = Math.max(0, Math.min(1, progress * 1.8 - c.col / Math.max(1, layout.cols) * .8));
2254
- const size = c.size * colT;
2255
- const offset = (c.size - size) / 2;
2125
+ const w = c.w * colT;
2126
+ const h = c.h * colT;
2256
2127
  const isHover = i === hoverIndex;
2257
2128
  canvasCtx.save();
2258
- roundRect(canvasCtx, c.x + offset, c.y + offset, size, size, Math.min(3, size / 4));
2259
- canvasCtx.fillStyle = isHover ? mixColor(c.color, .18, "#000000") : c.color;
2129
+ roundRect(canvasCtx, c.x + (c.w - w) / 2, c.y + (c.h - h) / 2, w, h, Math.min(3, w / 4, h / 4));
2130
+ canvasCtx.fillStyle = isHover ? mixColor(c.color, .18 * hoverAnimProgressOf(ctx), "#000000") : c.color;
2260
2131
  canvasCtx.fill();
2261
2132
  if (c.today) {
2262
2133
  canvasCtx.strokeStyle = theme.colors[0];
2263
2134
  canvasCtx.lineWidth = 1.5;
2264
- roundRect(canvasCtx, c.x - 1.5, c.y - 1.5, c.size + 3, c.size + 3, 4);
2135
+ roundRect(canvasCtx, c.x - 1.5, c.y - 1.5, c.w + 3, c.h + 3, 4);
2265
2136
  canvasCtx.stroke();
2266
2137
  }
2267
2138
  canvasCtx.restore();
@@ -2296,10 +2167,13 @@ function renderCalendarHeatmapChart(ctx) {
2296
2167
  canvasCtx.restore();
2297
2168
  }
2298
2169
  }
2170
+ function hoverAnimProgressOf(ctx) {
2171
+ return ctx.hoverAnimProgress === void 0 ? 1 : ctx.hoverAnimProgress;
2172
+ }
2299
2173
  function calendarHitTest(canvasX, canvasY, plotArea, options, theme) {
2300
2174
  const layout = computeCalendarLayout(plotArea, options, theme);
2301
2175
  if (!layout) return null;
2302
- for (const c of layout.cells) if (canvasX >= c.x && canvasX <= c.x + c.size && canvasY >= c.y && canvasY <= c.y + c.size) return {
2176
+ for (const c of layout.cells) if (canvasX >= c.x && canvasX <= c.x + c.w && canvasY >= c.y && canvasY <= c.y + c.h) return {
2303
2177
  index: c.index,
2304
2178
  params: {
2305
2179
  seriesName: c.label,
@@ -2331,20 +2205,22 @@ function getHeatmapCategories(options) {
2331
2205
  };
2332
2206
  }
2333
2207
  function renderHeatmapChart(ctx) {
2334
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex } = ctx;
2208
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1 } = ctx;
2209
+ const hovT = hoverAnimProgress;
2335
2210
  const heatmapData = options.heatmapData || [];
2336
2211
  if (heatmapData.length === 0) return;
2337
2212
  const heatmapConfig = options.heatmap || {};
2338
2213
  const colorScale = heatmapConfig.colorScale || [...CHART_COLORS.heatmapScale];
2339
2214
  const { xCategories, yCategories } = getHeatmapCategories(options);
2340
- const values = heatmapData.map((d) => d.value);
2341
- const minValue = Math.min(...values);
2342
- const maxValue = Math.max(...values);
2215
+ const values = heatmapData.map((d) => d.value).filter((v) => typeof v === "number" && Number.isFinite(v));
2216
+ const minValue = values.length ? minOf(values) : 0;
2217
+ const maxValue = values.length ? maxOf(values) : 1;
2343
2218
  const valueRange = maxValue - minValue || 1;
2344
2219
  const cellWidth = plotArea.width / xCategories.length;
2345
2220
  const cellHeight = plotArea.height / yCategories.length;
2346
2221
  function getColor(value, customColor) {
2347
2222
  if (customColor) return customColor;
2223
+ if (typeof value !== "number" || !Number.isFinite(value)) return colorScale[0];
2348
2224
  const ratio = (value - minValue) / valueRange;
2349
2225
  const index = Math.min(colorScale.length - 1, Math.floor(ratio * colorScale.length));
2350
2226
  return colorScale[index];
@@ -2362,7 +2238,8 @@ function renderHeatmapChart(ctx) {
2362
2238
  const padding = 1;
2363
2239
  roundRect(canvasCtx, x + padding, y + padding, cellWidth - 2, cellHeight - 2, 3);
2364
2240
  canvasCtx.fill();
2365
- if (isHover) {
2241
+ if (isHover && hovT > .01) {
2242
+ canvasCtx.globalAlpha = hovT;
2366
2243
  canvasCtx.strokeStyle = theme.textColor;
2367
2244
  canvasCtx.lineWidth = 2;
2368
2245
  canvasCtx.stroke();
@@ -2446,10 +2323,10 @@ function candleVolumeLayout(plotArea, options, hiddenSeries) {
2446
2323
  * 量带绘制(K 线 / 折线分时共用):flags 为每根的涨跌布尔(决定颜色),
2447
2324
  * baseY 为量带底、bandTop 为量带顶;不画轴(量纲从属),最高柱顶标 compact max。
2448
2325
  */
2449
- function renderVolumeBand(ctx, volumeData, flags, baseY, bandTop, barWidth, slotWidth, plotWidth, plotX, theme, progress, hoverIndex, upColor = "#dc2626", downColor = "#16a34a") {
2326
+ function renderVolumeBand(ctx, volumeData, flags, baseY, bandTop, barWidth, slotWidth, plotWidth, plotX, theme, progress, hoverIndex, upColor = UP_COLOR, downColor = DOWN_COLOR) {
2450
2327
  const { ctx: canvasCtx, hiddenSeries } = ctx;
2451
2328
  if (hiddenSeries && hiddenSeries.has("成交量")) return;
2452
- const volMax = Math.max(...volumeData.filter((v) => Number.isFinite(v)), 1);
2329
+ const volMax = maxOf(volumeData.filter((v) => Number.isFinite(v)), 1);
2453
2330
  const bandHeight = Math.max(4, baseY - bandTop);
2454
2331
  volumeData.forEach((vol, i) => {
2455
2332
  if (!Number.isFinite(vol)) return;
@@ -2484,7 +2361,8 @@ function renderVolumeBand(ctx, volumeData, flags, baseY, bandTop, barWidth, slot
2484
2361
  * 带不画轴(量纲从属)。图例「成交量」点选后量带隐去、K 线回铺全高。
2485
2362
  */
2486
2363
  function renderCandleChart(ctx, yRange) {
2487
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hiddenSeries } = ctx;
2364
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1, hiddenSeries } = ctx;
2365
+ const hovT = hoverAnimProgress;
2488
2366
  const candleData = options.candleData || [];
2489
2367
  if (candleData.length === 0) return;
2490
2368
  const upColor = options.candleUpColor || "#dc2626";
@@ -2513,9 +2391,10 @@ function renderCandleChart(ctx, yRange) {
2513
2391
  const closeY = yFor(candle.close);
2514
2392
  const bodyTop = Math.min(openY, closeY);
2515
2393
  const bodyHeight = Math.max(1, Math.abs(closeY - openY));
2516
- canvasCtx.fillStyle = isHover ? color + "dd" : color;
2394
+ canvasCtx.fillStyle = isHover ? mixColor(color, .12 * hovT, "#000000") : color;
2517
2395
  canvasCtx.fillRect(x - candleWidth / 2, bodyTop, candleWidth, bodyHeight);
2518
- if (isHover) {
2396
+ if (isHover && hovT > .01) {
2397
+ canvasCtx.globalAlpha = hovT;
2519
2398
  canvasCtx.strokeStyle = theme.textColor;
2520
2399
  canvasCtx.lineWidth = 1.5;
2521
2400
  canvasCtx.strokeRect(x - candleWidth / 2, bodyTop, candleWidth, bodyHeight);
@@ -2656,10 +2535,10 @@ function collectLegendItems(options, theme, hiddenSeries) {
2656
2535
  if (options.type === "candle") {
2657
2536
  const items = [];
2658
2537
  if ((options.volumeData || []).length > 0) items.push({
2659
- name: "成交量",
2660
- label: fmt("成交量"),
2538
+ name: VOLUME_SERIES_NAME,
2539
+ label: fmt(VOLUME_SERIES_NAME),
2661
2540
  color: theme.textColorSecondary,
2662
- hidden: hiddenSeries.has("成交量")
2541
+ hidden: hiddenSeries.has(VOLUME_SERIES_NAME)
2663
2542
  });
2664
2543
  if (Array.isArray(options.candleMa)) options.candleMa.forEach((days, i) => {
2665
2544
  const name = `MA${days}`;
@@ -2679,10 +2558,10 @@ function collectLegendItems(options, theme, hiddenSeries) {
2679
2558
  hidden: hiddenSeries.has(s.name)
2680
2559
  }));
2681
2560
  if ((options.type === "line" || options.type === "area") && (options.volumeData || []).length > 0) items.push({
2682
- name: "成交量",
2683
- label: fmt("成交量"),
2561
+ name: VOLUME_SERIES_NAME,
2562
+ label: fmt(VOLUME_SERIES_NAME),
2684
2563
  color: theme.textColorSecondary,
2685
- hidden: hiddenSeries.has("成交量")
2564
+ hidden: hiddenSeries.has(VOLUME_SERIES_NAME)
2686
2565
  });
2687
2566
  return items;
2688
2567
  }
@@ -2746,12 +2625,7 @@ function computeLegendLayout(ctx, options, plotArea, containerWidth, containerHe
2746
2625
  };
2747
2626
  }
2748
2627
  function estimateWidth(text) {
2749
- let width = 0;
2750
- for (const ch of text) {
2751
- const code = ch.codePointAt(0) || 0;
2752
- width += code >= 11904 && code <= 40959 || code >= 63744 && code <= 64255 || code >= 65280 && code <= 65376 || code >= 12288 && code <= 12351 ? 12 : 12 * .62;
2753
- }
2754
- return width;
2628
+ return estimateTextWidth(text, 12);
2755
2629
  }
2756
2630
  function renderLegend(ctx) {
2757
2631
  const { ctx: canvasCtx, theme, options, hiddenSeries, width, height, plotArea } = ctx;
@@ -2929,7 +2803,7 @@ function computePieMaxRadius(pieData, options, plotArea) {
2929
2803
  const pct = (d.value / total * 100).toFixed(1);
2930
2804
  return percentMode ? `${pct}%` : `${d.label || ""} (${pct}%)`;
2931
2805
  });
2932
- const horizNeed = 31 + Math.max(40, ...texts.map((t) => estimateTextWidth(t, 12))) + 10;
2806
+ const horizNeed = 31 + Math.max(40, maxOf(texts.map((t) => estimateTextWidth(t, 12)))) + 10;
2933
2807
  return Math.max(30, Math.min(Math.min(plotArea.width, plotArea.height) / 2 - 20, plotArea.width / 2 - horizNeed));
2934
2808
  }
2935
2809
  function alphaHex(opacity) {
@@ -2993,14 +2867,22 @@ function renderLineChart(ctx, yRange, side = "left") {
2993
2867
  points.push([x, y]);
2994
2868
  });
2995
2869
  allPoints.push(points.map((p) => p || [NaN, NaN]));
2870
+ let drawPoints = points;
2871
+ let drawIdxs = points.map((_, i) => i);
2872
+ const columnBudget = Math.max(2, Math.floor(plotArea.width));
2873
+ if (points.length > columnBudget * 2) {
2874
+ const decimated = minMaxDecimatePoints(points, columnBudget);
2875
+ drawPoints = decimated.map((e) => e.p);
2876
+ drawIdxs = decimated.map((e) => e.i);
2877
+ }
2996
2878
  const connect = resolveConnectNulls(options, series);
2997
2879
  const segments = [];
2998
2880
  let currentPts = [];
2999
2881
  let currentIdxs = [];
3000
- points.forEach((p, i) => {
2882
+ drawPoints.forEach((p, k) => {
3001
2883
  if (p) {
3002
2884
  currentPts.push(p);
3003
- currentIdxs.push(i);
2885
+ currentIdxs.push(drawIdxs[k]);
3004
2886
  } else if (currentPts.length > 0 && !connect) {
3005
2887
  segments.push({
3006
2888
  pts: currentPts,
@@ -3060,7 +2942,7 @@ function renderLineChart(ctx, yRange, side = "left") {
3060
2942
  canvasCtx.clip();
3061
2943
  segments.forEach(({ pts: seg, idxs }) => {
3062
2944
  canvasCtx.beginPath();
3063
- const baseValue = isStackedArea && filteredIndex > 0 ? stackedSeries.slice(0, filteredIndex).reduce((sum, s) => sum + (s.data[0] || 0), 0) : yRange.min;
2945
+ const baseValue = isStackedArea && filteredIndex > 0 ? stackedSeries.slice(0, filteredIndex).reduce((sum, s) => sum + (s.data[idxs[0]] || 0), 0) : yRange.min;
3064
2946
  const baseY = isStackedArea && filteredIndex > 0 ? plotArea.y + plotArea.height - (baseValue - yRange.min) * progress / (yRange.max - yRange.min) * plotArea.height : plotArea.y + plotArea.height;
3065
2947
  canvasCtx.moveTo(seg[0][0], baseY);
3066
2948
  canvasCtx.lineTo(seg[0][0], seg[0][1]);
@@ -3104,9 +2986,9 @@ function renderLineChart(ctx, yRange, side = "left") {
3104
2986
  if (!series.area && options.type !== "area") canvasCtx.restore();
3105
2987
  if (series.showSymbol === true && series.symbol !== "none") {
3106
2988
  const baseR = series.symbolSize ?? 4;
3107
- points.forEach((p, i) => {
2989
+ drawPoints.forEach((p, k) => {
3108
2990
  if (!p) return;
3109
- const isHover = i === hoverIndex;
2991
+ const isHover = drawIdxs[k] === hoverIndex;
3110
2992
  const radius = isHover ? baseR + 2 : baseR;
3111
2993
  canvasCtx.beginPath();
3112
2994
  drawSymbol(canvasCtx, series.symbol, p[0], p[1], radius);
@@ -3141,7 +3023,7 @@ function renderLineChart(ctx, yRange, side = "left") {
3141
3023
  canvasCtx.textBaseline = "bottom";
3142
3024
  const rects = [];
3143
3025
  const texts = [];
3144
- points.forEach((p, i) => {
3026
+ drawPoints.forEach((p, k) => {
3145
3027
  if (!p) {
3146
3028
  rects.push({
3147
3029
  x: 0,
@@ -3152,7 +3034,7 @@ function renderLineChart(ctx, yRange, side = "left") {
3152
3034
  texts.push("");
3153
3035
  return;
3154
3036
  }
3155
- const text = valueFormatter(series.data[i]);
3037
+ const text = valueFormatter(series.data[drawIdxs[k]]);
3156
3038
  const w = canvasCtx.measureText(text).width;
3157
3039
  rects.push({
3158
3040
  x: p[0] - w / 2 - 2,
@@ -3163,8 +3045,8 @@ function renderLineChart(ctx, yRange, side = "left") {
3163
3045
  texts.push(text);
3164
3046
  });
3165
3047
  const visible = layoutLabelsAvoidOverlap(rects);
3166
- points.forEach((p, i) => {
3167
- if (p && visible[i]) canvasCtx.fillText(texts[i], p[0], p[1] - 8);
3048
+ drawPoints.forEach((p, k) => {
3049
+ if (p && visible[k]) canvasCtx.fillText(texts[k], p[0], p[1] - 8);
3168
3050
  });
3169
3051
  canvasCtx.restore();
3170
3052
  }
@@ -3173,7 +3055,8 @@ function renderLineChart(ctx, yRange, side = "left") {
3173
3055
  return allPoints;
3174
3056
  }
3175
3057
  function renderBarChart(ctx, yRange, stacked = false) {
3176
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hiddenSeries, valueFormatter } = ctx;
3058
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1, hiddenSeries, valueFormatter } = ctx;
3059
+ const hovT = hoverAnimProgress;
3177
3060
  const visibleSeries = (options.series || []).filter((s) => !hiddenSeries.has(s.name));
3178
3061
  if (visibleSeries.length === 0) return;
3179
3062
  const labels = options.labels || [];
@@ -3245,10 +3128,11 @@ function renderBarChart(ctx, yRange, stacked = false) {
3245
3128
  canvasCtx.beginPath();
3246
3129
  canvasCtx.rect(barX, seg.y, barWidth, seg.height);
3247
3130
  }
3248
- canvasCtx.fillStyle = isHover ? seg.color + "dd" : seg.color;
3131
+ canvasCtx.fillStyle = isHover ? mixColor(seg.color, .12 * hovT, "#000000") : seg.color;
3249
3132
  canvasCtx.fill();
3250
- if (isHover && (seg.isTopPositive || seg.isBottomNegative)) {
3251
- canvasCtx.fillStyle = "rgba(255, 255, 255, 0.3)";
3133
+ if (isHover && hovT > .01 && (seg.isTopPositive || seg.isBottomNegative)) {
3134
+ canvasCtx.globalAlpha = seg.alpha * .3 * hovT;
3135
+ canvasCtx.fillStyle = "#ffffff";
3252
3136
  const stripY = seg.value >= 0 ? seg.y : seg.y + seg.height - Math.min(6, seg.height);
3253
3137
  canvasCtx.fillRect(barX, stripY, barWidth, Math.min(6, seg.height));
3254
3138
  }
@@ -3300,10 +3184,11 @@ function renderBarChart(ctx, yRange, stacked = false) {
3300
3184
  canvasCtx.globalAlpha = alpha;
3301
3185
  if (barHeight > .5) {
3302
3186
  drawRoundedBarEnd(canvasCtx, x, barTop, barWidth, barHeight, borderRadius, value >= 0 ? "top" : "bottom");
3303
- canvasCtx.fillStyle = isHover ? color + "dd" : color;
3187
+ canvasCtx.fillStyle = isHover ? mixColor(color, .12 * hovT, "#000000") : color;
3304
3188
  canvasCtx.fill();
3305
- if (isHover) {
3306
- canvasCtx.fillStyle = "rgba(255, 255, 255, 0.3)";
3189
+ if (isHover && hovT > .01) {
3190
+ canvasCtx.globalAlpha = .3 * hovT;
3191
+ canvasCtx.fillStyle = "#ffffff";
3307
3192
  const stripY = value >= 0 ? barTop : barTop + barHeight - Math.min(10, barHeight);
3308
3193
  canvasCtx.fillRect(x, stripY, barWidth, Math.min(10, barHeight));
3309
3194
  }
@@ -3365,7 +3250,8 @@ function drawRoundedBarEnd(canvasCtx, x, y, width, height, radius, end) {
3365
3250
  canvasCtx.closePath();
3366
3251
  }
3367
3252
  function renderHorizontalBarChart(ctx, xRange) {
3368
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hiddenSeries, valueFormatter } = ctx;
3253
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1, hiddenSeries, valueFormatter } = ctx;
3254
+ const hovT = hoverAnimProgress;
3369
3255
  const visibleSeries = (options.series || []).filter((s) => !hiddenSeries.has(s.name));
3370
3256
  if (visibleSeries.length === 0) return;
3371
3257
  const seriesCount = visibleSeries.length;
@@ -3410,10 +3296,11 @@ function renderHorizontalBarChart(ctx, xRange) {
3410
3296
  canvasCtx.lineTo(barLeft + barWidth, y + barHeight);
3411
3297
  }
3412
3298
  canvasCtx.closePath();
3413
- canvasCtx.fillStyle = isHover ? color + "dd" : color;
3299
+ canvasCtx.fillStyle = isHover ? mixColor(color, .12 * hovT, "#000000") : color;
3414
3300
  canvasCtx.fill();
3415
- if (isHover) {
3416
- canvasCtx.fillStyle = "rgba(255, 255, 255, 0.3)";
3301
+ if (isHover && hovT > .01) {
3302
+ canvasCtx.globalAlpha = .3 * hovT;
3303
+ canvasCtx.fillStyle = "#ffffff";
3417
3304
  const stripX = value >= 0 ? barLeft + barWidth - Math.min(10, barWidth) : barLeft;
3418
3305
  canvasCtx.fillRect(stripX, y, Math.min(10, barWidth), barHeight);
3419
3306
  }
@@ -3434,15 +3321,7 @@ function renderHorizontalBarChart(ctx, xRange) {
3434
3321
  (options.labels || []).forEach((label, i) => {
3435
3322
  const y = plotArea.y + plotArea.height - (i + .5) * categoryWidth;
3436
3323
  let formattedLabel = options.yAxis?.formatter ? options.yAxis.formatter(label) : label;
3437
- if (canvasCtx.measureText(formattedLabel).width > maxCatWidth) {
3438
- let lo = 0, hi = formattedLabel.length;
3439
- while (lo < hi) {
3440
- const mid = Math.ceil((lo + hi) / 2);
3441
- if (canvasCtx.measureText(formattedLabel.slice(0, mid) + "…").width > maxCatWidth) hi = mid - 1;
3442
- else lo = mid;
3443
- }
3444
- formattedLabel = formattedLabel.slice(0, lo) + "…";
3445
- }
3324
+ formattedLabel = truncateLabel(canvasCtx, formattedLabel, maxCatWidth);
3446
3325
  canvasCtx.fillText(formattedLabel, plotArea.x - 10, y);
3447
3326
  });
3448
3327
  canvasCtx.restore();
@@ -3494,7 +3373,7 @@ function renderPieChart(ctx, isDoughnut, isRose = false) {
3494
3373
  const centerX = plotArea.x + plotArea.width / 2;
3495
3374
  const centerY = plotArea.y + plotArea.height / 2;
3496
3375
  const maxRadius = computePieMaxRadius(pieData, options, plotArea);
3497
- const innerRadius = isDoughnut ? Math.max(0, Math.min(maxRadius - 5, maxRadius * (options.innerRadius || .6))) : 0;
3376
+ const innerRadius = isDoughnut ? doughnutInnerRadius(maxRadius, options) : 0;
3498
3377
  let startAngle = -Math.PI / 2;
3499
3378
  const labelItems = [];
3500
3379
  pieData.forEach((data, index) => {
@@ -3502,7 +3381,7 @@ function renderPieChart(ctx, isDoughnut, isRose = false) {
3502
3381
  const sliceAngle = data.value / total * Math.PI * 2 * progress;
3503
3382
  const endAngle = startAngle + sliceAngle;
3504
3383
  const isHover = index === hoverIndex;
3505
- const roseRadius = isRose ? maxRadius * Math.sqrt(data.value / Math.max(...pieData.map((d) => d.value))) : maxRadius;
3384
+ const roseRadius = isRose ? maxRadius * Math.sqrt(data.value / maxOf(pieData.map((d) => d.value))) : maxRadius;
3506
3385
  const hoverDistance = isRose ? Math.max(10, roseRadius * .15) : 12;
3507
3386
  const offset = isHover ? hoverDistance * hoverAnimProgress : 0;
3508
3387
  const radius = isRose ? roseRadius * progress : maxRadius;
@@ -3569,10 +3448,13 @@ function renderPieChart(ctx, isDoughnut, isRose = false) {
3569
3448
  canvasCtx.restore();
3570
3449
  }
3571
3450
  }
3451
+ function doughnutInnerRadius(maxRadius, options) {
3452
+ return Math.max(0, Math.min(maxRadius - 5, maxRadius * (options.innerRadius || .6)));
3453
+ }
3572
3454
  function createScatterScale(scatterData, yRange, plotArea) {
3573
- const xValues = scatterData.map((d) => d.x);
3574
- const xMin = Math.min(...xValues);
3575
- const xMax = Math.max(...xValues);
3455
+ const xValues = scatterData.map((d) => d.x).filter((v) => typeof v === "number" && Number.isFinite(v));
3456
+ const xMin = xValues.length ? minOf(xValues) : 0;
3457
+ const xMax = xValues.length ? maxOf(xValues) : 1;
3576
3458
  const xRange = xMax - xMin || 1;
3577
3459
  const xPadding = xRange * .05;
3578
3460
  const yPadding = (yRange.max - yRange.min) * .05;
@@ -3776,12 +3658,12 @@ function computeFacetGrids(scatterData, plotArea, options) {
3776
3658
  const col = i % cols;
3777
3659
  const row = Math.floor(i / cols);
3778
3660
  const data = grouped.get(name);
3779
- const xs = data.map((d) => d.x);
3780
- const ys = data.map((d) => d.y);
3781
- const xMin = Math.min(...xs);
3782
- const xMax = Math.max(...xs);
3783
- const yMin = Math.min(...ys);
3784
- const yMax = Math.max(...ys);
3661
+ const xs = data.map((d) => d.x).filter((v) => typeof v === "number" && Number.isFinite(v));
3662
+ const ys = data.map((d) => d.y).filter((v) => typeof v === "number" && Number.isFinite(v));
3663
+ const xMin = xs.length ? minOf(xs) : 0;
3664
+ const xMax = xs.length ? maxOf(xs) : 1;
3665
+ const yMin = ys.length ? minOf(ys) : 0;
3666
+ const yMax = ys.length ? maxOf(ys) : 1;
3785
3667
  const xPad = (xMax - xMin || 1) * .08;
3786
3668
  const yPad = (yMax - yMin || 1) * .08;
3787
3669
  const outer = {
@@ -3855,7 +3737,7 @@ function renderScatterFacetChart(ctx, yRange) {
3855
3737
  const y = area.y + area.height - (area.y + area.height - finalY) * yFactor;
3856
3738
  const globalIdx = scatterData.indexOf(point);
3857
3739
  const size = (point.size || 5) * Math.min(yFactor * 1.5, 1);
3858
- const color = point.color || theme.colors[i % theme.colors.length];
3740
+ const color = point.color || theme.colors[globalIdx % theme.colors.length];
3859
3741
  const isHover = globalIdx === hoverIndex;
3860
3742
  canvasCtx.beginPath();
3861
3743
  canvasCtx.arc(x, y, isHover ? size + 2 : size, 0, Math.PI * 2);
@@ -3900,8 +3782,8 @@ function matrixFieldExtent(records, field) {
3900
3782
  min: 0,
3901
3783
  max: 1
3902
3784
  };
3903
- const min = Math.min(...values);
3904
- const max = Math.max(...values);
3785
+ const min = minOf(values);
3786
+ const max = maxOf(values);
3905
3787
  const pad = (max - min || 1) * .08;
3906
3788
  return {
3907
3789
  min: min - pad,
@@ -4134,26 +4016,11 @@ function renderRadarChart(ctx) {
4134
4016
  if (options.radarRingFill === true) {
4135
4017
  canvasCtx.save();
4136
4018
  canvasCtx.fillStyle = theme.gridColor;
4137
- canvasCtx.globalAlpha = .45;
4138
- for (let level = 1; level <= 5; level += 2) {
4139
- const outer = radius * level / 5;
4140
- const inner = radius * (level - 1) / 5;
4019
+ for (let level = 5; level >= 1; level--) {
4020
+ canvasCtx.globalAlpha = level % 2 === 1 ? .5 : .22;
4141
4021
  canvasCtx.beginPath();
4142
- for (let i = 0; i <= indicators.length; i++) {
4143
- const angle = i * angleStep - Math.PI / 2;
4144
- const x = centerX + Math.cos(angle) * outer;
4145
- const y = centerY + Math.sin(angle) * outer;
4146
- if (i === 0) canvasCtx.moveTo(x, y);
4147
- else canvasCtx.lineTo(x, y);
4148
- }
4149
- canvasCtx.closePath();
4150
- canvasCtx.moveTo(centerX + inner, centerY);
4151
- for (let i = indicators.length; i >= 0; i--) {
4152
- const angle = i * angleStep - Math.PI / 2;
4153
- canvasCtx.lineTo(centerX + Math.cos(angle) * inner, centerY + Math.sin(angle) * inner);
4154
- }
4155
- canvasCtx.closePath();
4156
- canvasCtx.fill("evenodd");
4022
+ canvasCtx.arc(centerX, centerY, radius * level / 5, 0, Math.PI * 2);
4023
+ canvasCtx.fill();
4157
4024
  }
4158
4025
  canvasCtx.restore();
4159
4026
  }
@@ -4355,10 +4222,13 @@ function computeBins(options, hiddenSeries) {
4355
4222
  const binConfig = options.binConfig || {};
4356
4223
  const visibleSeries = (options.series || []).filter((s) => !hiddenSeries.has(s.name));
4357
4224
  const allValues = [];
4358
- visibleSeries.forEach((s) => allValues.push(...s.data.filter((v) => !isMissingValue(v))));
4225
+ visibleSeries.forEach((s) => {
4226
+ const data = s.data || [];
4227
+ for (let i = 0; i < data.length; i++) if (!isMissingValue(data[i])) allValues.push(data[i]);
4228
+ });
4359
4229
  if (allValues.length === 0) return null;
4360
- const dataMin = Math.min(...allValues);
4361
- const range = Math.max(...allValues) - dataMin || 1;
4230
+ const dataMin = minOf(allValues);
4231
+ const range = maxOf(allValues) - dataMin || 1;
4362
4232
  let bins;
4363
4233
  if (binConfig.bins && binConfig.bins.length > 1) bins = binConfig.bins;
4364
4234
  else {
@@ -4388,7 +4258,7 @@ function renderBinChart(ctx) {
4388
4258
  const { bins, frequencies, displayValues } = result;
4389
4259
  const dataMin = bins[0];
4390
4260
  const range = bins[bins.length - 1] - dataMin || 1;
4391
- const maxFreq = Math.max(...displayValues, 1);
4261
+ const maxFreq = maxOf(displayValues, 1);
4392
4262
  const binCount = bins.length - 1;
4393
4263
  const barGap = 2;
4394
4264
  const barWidth = (plotArea.width - barGap * (binCount - 1)) / binCount;
@@ -4451,8 +4321,8 @@ function renderBulletChart(ctx) {
4451
4321
  allValues.push(r.from, r.to);
4452
4322
  });
4453
4323
  });
4454
- const maxVal = Math.max(...allValues, 0);
4455
- const minVal = Math.min(...allValues, 0);
4324
+ const maxVal = maxOf(allValues, 0);
4325
+ const minVal = minOf(allValues, 0);
4456
4326
  const range = maxVal - minVal || 1;
4457
4327
  const padMin = minVal >= 0 ? 0 : minVal - range * .1;
4458
4328
  const padMax = maxVal + range * .1;
@@ -4665,8 +4535,8 @@ function renderSparklineChart(ctx) {
4665
4535
  if (data.length === 0) return;
4666
4536
  const valid = data.filter((v) => !isMissingValue(v));
4667
4537
  if (valid.length === 0) return;
4668
- const min = Math.min(...valid);
4669
- const range = Math.max(...valid) - min || 1;
4538
+ const min = minOf(valid);
4539
+ const range = maxOf(valid) - min || 1;
4670
4540
  const points = [];
4671
4541
  const pointByIndex = [];
4672
4542
  data.forEach((v, i) => {
@@ -4761,40 +4631,27 @@ function renderSparklineChart(ctx) {
4761
4631
  //#endregion
4762
4632
  //#region src/renderer/charts-extra.js
4763
4633
  function renderWaterfallChart(ctx, yRange) {
4764
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, valueFormatter, hiddenSeries } = ctx;
4634
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1, valueFormatter, hiddenSeries } = ctx;
4635
+ const hovT = hoverAnimProgress;
4765
4636
  const wf = options.waterfall || {};
4766
4637
  const labels = options.labels || [];
4767
4638
  const deltas = (options.series || []).filter((s) => !hiddenSeries.has(s.name))[0]?.data || [];
4768
4639
  if (labels.length === 0 || deltas.length === 0) return;
4769
- const totalIdx = new Set(wf.totalIndices || []);
4640
+ new Set(wf.totalIndices || []);
4770
4641
  const increaseColor = wf.increaseColor || "#dc2626";
4771
4642
  const decreaseColor = wf.decreaseColor || "#16a34a";
4772
4643
  const totalColor = wf.totalColor || theme.colors[0];
4773
4644
  const categoryWidth = plotArea.width / labels.length;
4774
4645
  const barWidth = Math.min(categoryWidth * .6, 64);
4775
4646
  const yFor = (v) => plotArea.y + plotArea.height - (v - yRange.min) / (yRange.max - yRange.min) * plotArea.height;
4776
- let cumulative = 0;
4647
+ const steps = waterfallSteps(deltas, wf.totalIndices);
4777
4648
  let prevEdgeX = null;
4778
4649
  let prevExitY = null;
4779
4650
  labels.forEach((_, i) => {
4780
- const raw = deltas[i];
4781
- if (isMissingValue(raw)) return;
4782
- const v = raw;
4783
- const isTotal = totalIdx.has(i);
4784
- let from;
4785
- let to;
4786
- let color;
4787
- if (isTotal) {
4788
- from = 0;
4789
- to = v;
4790
- cumulative = v;
4791
- color = totalColor;
4792
- } else {
4793
- from = cumulative;
4794
- to = cumulative + v;
4795
- cumulative = to;
4796
- color = v >= 0 ? increaseColor : decreaseColor;
4797
- }
4651
+ const step = steps[i];
4652
+ if (!step || step.missing) return;
4653
+ const { from, to, isTotal } = step;
4654
+ const color = isTotal ? totalColor : step.value >= 0 ? increaseColor : decreaseColor;
4798
4655
  const cx = plotArea.x + (i + .5) * categoryWidth;
4799
4656
  const x = cx - barWidth / 2;
4800
4657
  const isHover = i === hoverIndex;
@@ -4825,12 +4682,13 @@ function renderWaterfallChart(ctx, yRange) {
4825
4682
  canvasCtx.quadraticCurveTo(x + barWidth, top, x + barWidth, top + r);
4826
4683
  canvasCtx.lineTo(x + barWidth, top + height);
4827
4684
  canvasCtx.closePath();
4828
- canvasCtx.fillStyle = isHover ? color + "dd" : color;
4685
+ canvasCtx.fillStyle = isHover ? mixColor(color, .12 * hovT, "#000000") : color;
4829
4686
  canvasCtx.fill();
4830
- }
4831
- if (isHover && height > .5) {
4832
- canvasCtx.fillStyle = "rgba(255, 255, 255, 0.3)";
4833
- canvasCtx.fillRect(x, top, barWidth, Math.min(6, height));
4687
+ if (isHover && hovT > .01) {
4688
+ canvasCtx.globalAlpha = .3 * hovT;
4689
+ canvasCtx.fillStyle = "#ffffff";
4690
+ canvasCtx.fillRect(x, top, barWidth, Math.min(6, height));
4691
+ }
4834
4692
  }
4835
4693
  canvasCtx.restore();
4836
4694
  if (options.showValues && progress > .9 && height > .5) {
@@ -4839,11 +4697,11 @@ function renderWaterfallChart(ctx, yRange) {
4839
4697
  canvasCtx.font = "11px Inter, sans-serif";
4840
4698
  canvasCtx.textAlign = "center";
4841
4699
  canvasCtx.textBaseline = "bottom";
4842
- canvasCtx.fillText(valueFormatter(v), cx, top - 4);
4700
+ canvasCtx.fillText(valueFormatter(step.value), cx, top - 4);
4843
4701
  canvasCtx.restore();
4844
4702
  }
4845
4703
  prevEdgeX = x + barWidth;
4846
- prevExitY = yFor(cumulative);
4704
+ prevExitY = yFor(step.to);
4847
4705
  });
4848
4706
  }
4849
4707
  /**
@@ -4927,7 +4785,8 @@ function computeBoxplotGeometry(plotArea, options, theme, hiddenSeries, valueRan
4927
4785
  };
4928
4786
  }
4929
4787
  function renderBoxplotChart(ctx, yRange) {
4930
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hiddenSeries } = ctx;
4788
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1, hiddenSeries } = ctx;
4789
+ const hovT = hoverAnimProgress;
4931
4790
  const geo = computeBoxplotGeometry(plotArea, options, theme, hiddenSeries, yRange);
4932
4791
  if (!geo) return;
4933
4792
  const { boxes, horizontal } = geo;
@@ -4935,7 +4794,9 @@ function renderBoxplotChart(ctx, yRange) {
4935
4794
  box.b;
4936
4795
  const isHover = box.index === hoverIndex;
4937
4796
  const color = box.color;
4938
- const lineWidth = isHover ? 2 : 1.5;
4797
+ const lineWidth = 1.5 + .5 * (isHover ? hovT : 0);
4798
+ const fillAlpha = .2 + .13 * (isHover ? hovT : 0);
4799
+ const fillHex = Math.round(fillAlpha * 255).toString(16).padStart(2, "0");
4939
4800
  canvasCtx.save();
4940
4801
  canvasCtx.strokeStyle = color;
4941
4802
  canvasCtx.fillStyle = color;
@@ -4958,10 +4819,10 @@ function renderBoxplotChart(ctx, yRange) {
4958
4819
  canvasCtx.moveTo(wHi, yMed - cap / 2);
4959
4820
  canvasCtx.lineTo(wHi, yMed + cap / 2);
4960
4821
  canvasCtx.stroke();
4961
- canvasCtx.fillStyle = color + (isHover ? "55" : "33");
4822
+ canvasCtx.fillStyle = color + fillHex;
4962
4823
  canvasCtx.fillRect(qLo, yLo, Math.max(1, qHi - qLo), box.thickness);
4963
4824
  canvasCtx.strokeRect(qLo, yLo, Math.max(1, qHi - qLo), box.thickness);
4964
- canvasCtx.lineWidth = isHover ? 2.5 : 2;
4825
+ canvasCtx.lineWidth = 2 + .5 * (isHover ? hovT : 0);
4965
4826
  canvasCtx.beginPath();
4966
4827
  canvasCtx.moveTo(box.cx + (box.med - box.cx) * progress, yLo);
4967
4828
  canvasCtx.lineTo(box.cx + (box.med - box.cx) * progress, yLo + box.thickness);
@@ -4969,7 +4830,7 @@ function renderBoxplotChart(ctx, yRange) {
4969
4830
  box.outliers.forEach((o) => {
4970
4831
  const ox = box.cx + (o - box.cx) * progress;
4971
4832
  canvasCtx.beginPath();
4972
- canvasCtx.arc(ox, yMed, isHover ? 3.5 : 2.5, 0, Math.PI * 2);
4833
+ canvasCtx.arc(ox, yMed, 2.5 + (isHover ? hovT : 0), 0, Math.PI * 2);
4973
4834
  canvasCtx.fillStyle = color;
4974
4835
  canvasCtx.fill();
4975
4836
  });
@@ -4999,10 +4860,10 @@ function renderBoxplotChart(ctx, yRange) {
4999
4860
  canvasCtx.moveTo(box.cx - box.capWidth / 2, yMax);
5000
4861
  canvasCtx.lineTo(box.cx + box.capWidth / 2, yMax);
5001
4862
  canvasCtx.stroke();
5002
- canvasCtx.fillStyle = color + (isHover ? "55" : "33");
4863
+ canvasCtx.fillStyle = color + fillHex;
5003
4864
  canvasCtx.fillRect(xLo, yQ3, box.thickness, Math.max(1, yQ1 - yQ3));
5004
4865
  canvasCtx.strokeRect(xLo, yQ3, box.thickness, Math.max(1, yQ1 - yQ3));
5005
- canvasCtx.lineWidth = isHover ? 2.5 : 2;
4866
+ canvasCtx.lineWidth = 2 + .5 * (isHover ? hovT : 0);
5006
4867
  canvasCtx.beginPath();
5007
4868
  canvasCtx.moveTo(xLo, yMed);
5008
4869
  canvasCtx.lineTo(xLo + box.thickness, yMed);
@@ -5010,7 +4871,7 @@ function renderBoxplotChart(ctx, yRange) {
5010
4871
  box.outliers.forEach((o) => {
5011
4872
  const oy = box.cy + (o - box.cy) * progress;
5012
4873
  canvasCtx.beginPath();
5013
- canvasCtx.arc(box.cx, oy, isHover ? 3.5 : 2.5, 0, Math.PI * 2);
4874
+ canvasCtx.arc(box.cx, oy, 2.5 + (isHover ? hovT : 0), 0, Math.PI * 2);
5014
4875
  canvasCtx.fillStyle = color;
5015
4876
  canvasCtx.fill();
5016
4877
  });
@@ -5036,15 +4897,7 @@ function renderBoxplotChart(ctx, yRange) {
5036
4897
  seen.add(box.catIndex);
5037
4898
  let label = box.b.label;
5038
4899
  const maxWidth = geo.categoryWidth - 8;
5039
- if (canvasCtx.measureText(label).width > maxWidth) {
5040
- let lo = 0, hi = label.length;
5041
- while (lo < hi) {
5042
- const mid = Math.ceil((lo + hi) / 2);
5043
- if (canvasCtx.measureText(label.slice(0, mid) + "…").width > maxWidth) hi = mid - 1;
5044
- else lo = mid;
5045
- }
5046
- label = label.slice(0, lo) + "…";
5047
- }
4900
+ label = truncateLabel(canvasCtx, label, maxWidth);
5048
4901
  const cx = plotArea.x + (box.catIndex + .5) * geo.categoryWidth;
5049
4902
  canvasCtx.fillText(label, cx, plotArea.y + plotArea.height + 8);
5050
4903
  });
@@ -5061,15 +4914,7 @@ function renderBoxplotChart(ctx, yRange) {
5061
4914
  seen.add(box.catIndex);
5062
4915
  let label = box.b.label;
5063
4916
  const maxWidth = plotArea.x - 12;
5064
- if (canvasCtx.measureText(label).width > maxWidth) {
5065
- let lo = 0, hi = label.length;
5066
- while (lo < hi) {
5067
- const mid = Math.ceil((lo + hi) / 2);
5068
- if (canvasCtx.measureText(label.slice(0, mid) + "…").width > maxWidth) hi = mid - 1;
5069
- else lo = mid;
5070
- }
5071
- label = label.slice(0, lo) + "…";
5072
- }
4917
+ label = truncateLabel(canvasCtx, label, maxWidth);
5073
4918
  const cy = plotArea.y + plotArea.height - (box.catIndex + .5) * geo.categoryWidth;
5074
4919
  canvasCtx.fillText(label, plotArea.x - 10, cy);
5075
4920
  });
@@ -5202,6 +5047,10 @@ function computeSunburstGeometry(plotArea, options, theme, valueFormatter) {
5202
5047
  valueFormatter
5203
5048
  };
5204
5049
  }
5050
+ /** 能用正文色(浅色主题近黑)就用正文色,底色深到不可读才反白(用户定则) */
5051
+ function sunburstLabelColor(bg, theme) {
5052
+ return isLightColor(bg) ? theme.textColor : getContrastText(bg);
5053
+ }
5205
5054
  function renderSunburstChart(ctx) {
5206
5055
  const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, valueFormatter, hoverAnimProgress = 1 } = ctx;
5207
5056
  const geo = computeSunburstGeometry(plotArea, options, theme, valueFormatter);
@@ -5249,7 +5098,7 @@ function renderSunburstChart(ctx) {
5249
5098
  name,
5250
5099
  value,
5251
5100
  bold: seg.depth === 0,
5252
- fill: getContrastText(color),
5101
+ fill: sunburstLabelColor(color, theme),
5253
5102
  alpha: alphaAt(i)
5254
5103
  });
5255
5104
  });
@@ -5274,7 +5123,8 @@ function renderSunburstChart(ctx) {
5274
5123
  });
5275
5124
  }
5276
5125
  function renderMixedChart(ctx, leftRange, rightRange) {
5277
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hiddenSeries } = ctx;
5126
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1, hiddenSeries } = ctx;
5127
+ const hovT = hoverAnimProgress;
5278
5128
  const labels = options.labels || [];
5279
5129
  const barSeries = (options.series || []).filter((s) => !hiddenSeries.has(s.name)).filter((s) => (s.chartType || "line") === "bar");
5280
5130
  if (barSeries.length > 0 && labels.length > 0) {
@@ -5309,7 +5159,7 @@ function renderMixedChart(ctx, leftRange, rightRange) {
5309
5159
  canvasCtx.quadraticCurveTo(x + barWidth, barTop, x + barWidth, barTop + r);
5310
5160
  canvasCtx.lineTo(x + barWidth, barTop + barHeight);
5311
5161
  canvasCtx.closePath();
5312
- canvasCtx.fillStyle = isHover ? color + "dd" : color + "cc";
5162
+ canvasCtx.fillStyle = isHover ? mixColor(color, .12 * hovT, "#000000") + "cc" : color + "cc";
5313
5163
  canvasCtx.fill();
5314
5164
  canvasCtx.restore();
5315
5165
  }
@@ -5356,10 +5206,7 @@ function computeSankeyLayout(plotArea, options, theme, hiddenSeries) {
5356
5206
  outSum.set(l.source, (outSum.get(l.source) || 0) + l.value);
5357
5207
  inSum.set(l.target, (inSum.get(l.target) || 0) + l.value);
5358
5208
  });
5359
- allNodes.forEach((n) => {
5360
- if (typeof n.value === "number" && n.value > 0) return;
5361
- n.value = Math.max(inSum.get(n.name) || 0, outSum.get(n.name) || 0);
5362
- });
5209
+ const nodeValues = new Map(allNodes.map((n) => [n.name, typeof n.value === "number" && n.value > 0 ? n.value : Math.max(inSum.get(n.name) || 0, outSum.get(n.name) || 0)]));
5363
5210
  const depth = new Map(allNodes.map((n) => [n.name, 0]));
5364
5211
  for (let round = 0; round < allNodes.length; round++) {
5365
5212
  let changed = false;
@@ -5372,8 +5219,14 @@ function computeSankeyLayout(plotArea, options, theme, hiddenSeries) {
5372
5219
  });
5373
5220
  if (!changed) break;
5374
5221
  }
5375
- const maxDepth = Math.max(...depth.values());
5376
- const valueMax = Math.max(1, ...allNodes.map((n) => n.value || 0), ...links.map((l) => l.value));
5222
+ const maxDepth = maxOf([...depth.values()]);
5223
+ if ((options.sankey?.nodeAlign ?? "justify") === "justify") {
5224
+ const hasOut = new Set(links.map((l) => l.source));
5225
+ allNodes.forEach((n) => {
5226
+ if (!hasOut.has(n.name)) depth.set(n.name, maxDepth);
5227
+ });
5228
+ }
5229
+ const valueMax = Math.max(1, maxOf([...nodeValues.values()]), maxOf(links.map((l) => l.value)));
5377
5230
  const columnCount = maxDepth + 1;
5378
5231
  const scale = (plotArea.height * .82 - NODE_GAP * Math.max(0, columnCount - 1)) / valueMax;
5379
5232
  const xOfColumn = (col) => plotArea.x + col * (plotArea.width - 14) / Math.max(1, columnCount - 1);
@@ -5383,8 +5236,8 @@ function computeSankeyLayout(plotArea, options, theme, hiddenSeries) {
5383
5236
  color: n.color || theme.colors[i % theme.colors.length],
5384
5237
  col: depth.get(n.name) || 0,
5385
5238
  x: xOfColumn(depth.get(n.name) || 0),
5386
- value: n.value || 0,
5387
- height: Math.max(2, (n.value || 0) * scale)
5239
+ value: nodeValues.get(n.name) || 0,
5240
+ height: Math.max(2, (nodeValues.get(n.name) || 0) * scale)
5388
5241
  }));
5389
5242
  const byCol = /* @__PURE__ */ new Map();
5390
5243
  nodes.forEach((n) => {
@@ -5527,8 +5380,7 @@ function renderSankeyChart(ctx) {
5527
5380
  });
5528
5381
  }
5529
5382
  /** 桑基命中:先节点矩形、后流带(平滑步进插值带内即命中) */
5530
- function sankeyHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries) {
5531
- const layout = computeSankeyLayout(plotArea, options, theme, hiddenSeries);
5383
+ function sankeyHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries, layout = computeSankeyLayout(plotArea, options, theme, hiddenSeries)) {
5532
5384
  if (!layout) return null;
5533
5385
  const { nodes, ribbons } = layout;
5534
5386
  for (let i = 0; i < nodes.length; i++) {
@@ -5575,9 +5427,10 @@ function computeVennLayout(plotArea, options, theme) {
5575
5427
  })).filter((d) => d.sets.length === 1);
5576
5428
  if (circles.length === 0 || circles.length > 3) return null;
5577
5429
  const interRows = rows.filter((d) => d.sets && d.sets.length === 2);
5578
- const valueMax = Math.max(...circles.map((d) => d.value || 0));
5430
+ const valueMax = maxOf(circles.map((d) => d.value || 0));
5579
5431
  if (!(valueMax > 0)) return null;
5580
- const R = Math.min(plotArea.width, plotArea.height) * .38;
5432
+ const setCount = circles.length;
5433
+ const R = Math.min(plotArea.height * (setCount >= 3 ? .3 : .46), plotArea.width * (setCount >= 3 ? .2 : .24));
5581
5434
  const k = R * R / valueMax;
5582
5435
  const radiusOf = (v) => Math.sqrt((v || 0) * k / Math.PI);
5583
5436
  circles.forEach((c, i) => {
@@ -5698,7 +5551,8 @@ function renderVennChart(ctx) {
5698
5551
  if (progress < .9) return;
5699
5552
  canvasCtx.save();
5700
5553
  canvasCtx.font = "600 12px Inter, sans-serif";
5701
- canvasCtx.fillStyle = layout.hollow ? theme.textColor : getContrastText(c.color);
5554
+ const labelColor = isLightColor(c.color) ? theme.textColor : getContrastText(c.color);
5555
+ canvasCtx.fillStyle = layout.hollow ? theme.textColor : labelColor;
5702
5556
  canvasCtx.textAlign = "center";
5703
5557
  canvasCtx.textBaseline = "middle";
5704
5558
  canvasCtx.fillText(c.name, c.x, c.y - c.r * .4);
@@ -5775,7 +5629,7 @@ function computeChordLayout(plotArea, options, theme, hiddenSeries) {
5775
5629
  const cy = plotArea.y + plotArea.height / 2;
5776
5630
  const R = Math.max(40, Math.min(plotArea.width, plotArea.height) / 2 - 30);
5777
5631
  const mode = options.chordMode === "curve" ? "curve" : "band";
5778
- const valueMax = Math.max(1, ...links.map((l) => l.value));
5632
+ const valueMax = Math.max(1, maxOf(links.map((l) => l.value)));
5779
5633
  const byValue = options.chordByValue === true;
5780
5634
  const nodeValue = (n) => {
5781
5635
  if (byValue) {
@@ -5814,8 +5668,8 @@ function computeChordLayout(plotArea, options, theme, hiddenSeries) {
5814
5668
  const src = arcByName.get(link.source);
5815
5669
  const tgt = arcByName.get(link.target);
5816
5670
  const angularW = link.value / valueMax * (R * .14) / R;
5817
- const s0 = src.startAngle + (outOffset.get(src.name) || 0);
5818
- const t0 = tgt.startAngle + (outOffset.get(tgt.name) || 0);
5671
+ const s0 = mode === "curve" ? src.midAngle : src.startAngle + (outOffset.get(src.name) || 0);
5672
+ const t0 = mode === "curve" ? tgt.midAngle : tgt.startAngle + (outOffset.get(tgt.name) || 0);
5819
5673
  outOffset.set(src.name, (outOffset.get(src.name) || 0) + angularW);
5820
5674
  outOffset.set(tgt.name, (outOffset.get(tgt.name) || 0) + angularW);
5821
5675
  const [ax, ay] = polar(s0, R);
@@ -5998,9 +5852,11 @@ function chordHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries)
5998
5852
  function computeArcLayout(plotArea, options, theme, hiddenSeries) {
5999
5853
  const { nodes, links } = normalizeRelation(options.arcData, hiddenSeries);
6000
5854
  if (nodes.length === 0) return null;
6001
- const axisY = plotArea.y + Math.max(30, (plotArea.height - 40) * .52);
5855
+ const labelBand = 30;
5856
+ const topGap = 24;
5857
+ const axisY = plotArea.y + Math.max(40, plotArea.height - labelBand);
6002
5858
  const step = plotArea.width / nodes.length;
6003
- const valueMax = Math.max(1, ...links.map((l) => l.value));
5859
+ const valueMax = Math.max(1, maxOf(links.map((l) => l.value)));
6004
5860
  const nodePts = nodes.map((n, i) => ({
6005
5861
  node: n,
6006
5862
  name: n.name,
@@ -6018,7 +5874,7 @@ function computeArcLayout(plotArea, options, theme, hiddenSeries) {
6018
5874
  const dist = Math.abs(b.x - a.x);
6019
5875
  const left = a.x <= b.x ? a : b;
6020
5876
  const right = a.x <= b.x ? b : a;
6021
- const cy = Math.min(dist * .45, (axisY - plotArea.y - 12) / .75);
5877
+ const cy = Math.min(dist * .45, (axisY - plotArea.y - topGap) / .75);
6022
5878
  const width = 1.5 + link.value / valueMax * 4.5;
6023
5879
  const samples = [];
6024
5880
  const x1 = left.x;
@@ -6090,18 +5946,9 @@ function renderArcChart(ctx) {
6090
5946
  canvasCtx.lineTo(n.x, axisY + 16);
6091
5947
  canvasCtx.stroke();
6092
5948
  if (progress >= .9) {
6093
- let label = n.name;
6094
5949
  canvasCtx.font = "12px Inter, sans-serif";
6095
5950
  const maxW = plotArea.width / nodes.length - 6;
6096
- if (canvasCtx.measureText(label).width > maxW) {
6097
- let lo = 0, hi = label.length;
6098
- while (lo < hi) {
6099
- const mid = Math.ceil((lo + hi) / 2);
6100
- if (canvasCtx.measureText(label.slice(0, mid) + "…").width > maxW) hi = mid - 1;
6101
- else lo = mid;
6102
- }
6103
- label = label.slice(0, lo) + "…";
6104
- }
5951
+ const label = truncateLabel(canvasCtx, n.name, maxW);
6105
5952
  canvasCtx.fillStyle = theme.textColorSecondary;
6106
5953
  canvasCtx.textAlign = "center";
6107
5954
  canvasCtx.textBaseline = "top";
@@ -6173,11 +6020,11 @@ function computeGanttLayout(plotArea, options, theme, hiddenSeries, valueFormatt
6173
6020
  endTs: toTs(t.end)
6174
6021
  })).filter((t) => t.startTs !== null && t.endTs !== null);
6175
6022
  if (parsed.length === 0) return null;
6176
- const labelW = Math.max(LABEL_MIN, Math.min(LABEL_MAX, Math.round(Math.max(...parsed.map((t) => estimateTextWidth(t.name || "", 12))) + 16)));
6023
+ const labelW = Math.max(LABEL_MIN, Math.min(LABEL_MAX, Math.round(maxOf(parsed.map((t) => estimateTextWidth(t.name || "", 12))) + 16)));
6177
6024
  const chartX = plotArea.x;
6178
6025
  const chartW = plotArea.width;
6179
- let tmin = Math.min(...parsed.map((t) => Math.min(t.startTs, t.endTs)));
6180
- let tmax = Math.max(...parsed.map((t) => Math.max(t.startTs, t.endTs)));
6026
+ let tmin = minOf(parsed.map((t) => Math.min(t.startTs, t.endTs)));
6027
+ let tmax = maxOf(parsed.map((t) => Math.max(t.startTs, t.endTs)));
6181
6028
  if (options.ganttToday) {
6182
6029
  const today = toTs(options.ganttToday);
6183
6030
  if (today !== null) {
@@ -6238,7 +6085,7 @@ function computeGanttLayout(plotArea, options, theme, hiddenSeries, valueFormatt
6238
6085
  };
6239
6086
  }
6240
6087
  function renderGanttChart(ctx) {
6241
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hiddenSeries } = ctx;
6088
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1, hiddenSeries } = ctx;
6242
6089
  const layout = computeGanttLayout(plotArea, options, theme, hiddenSeries);
6243
6090
  if (!layout) return;
6244
6091
  const { rows, deps, chartX, chartW, rowH, barH } = layout;
@@ -6253,6 +6100,7 @@ function renderGanttChart(ctx) {
6253
6100
  });
6254
6101
  if (hoverIndex >= 0 && hoverIndex < rows.length) {
6255
6102
  canvasCtx.save();
6103
+ canvasCtx.globalAlpha = hoverAnimProgress;
6256
6104
  canvasCtx.fillStyle = theme.highlightColor;
6257
6105
  canvasCtx.fillRect(chartX, plotArea.y + hoverIndex * rowH, chartW, rowH);
6258
6106
  canvasCtx.restore();
@@ -6386,21 +6234,12 @@ function renderGanttChart(ctx) {
6386
6234
  canvasCtx.textBaseline = "middle";
6387
6235
  let label = r.name || "";
6388
6236
  const maxW = layout.labelW - 16;
6389
- if (canvasCtx.measureText(label).width > maxW) {
6390
- let lo = 0, hi = label.length;
6391
- while (lo < hi) {
6392
- const mid = Math.ceil((lo + hi) / 2);
6393
- if (canvasCtx.measureText(label.slice(0, mid) + "…").width > maxW) hi = mid - 1;
6394
- else lo = mid;
6395
- }
6396
- label = label.slice(0, lo) + "…";
6397
- }
6237
+ label = truncateLabel(canvasCtx, label, maxW);
6398
6238
  canvasCtx.fillText(label, chartX - 10, plotArea.y + i * rowH + rowH / 2);
6399
6239
  canvasCtx.restore();
6400
6240
  });
6401
6241
  }
6402
- function ganttHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries) {
6403
- const layout = computeGanttLayout(plotArea, options, theme, hiddenSeries);
6242
+ function ganttHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries, layout = computeGanttLayout(plotArea, options, theme, hiddenSeries)) {
6404
6243
  if (!layout) return null;
6405
6244
  const { rows } = layout;
6406
6245
  const relY = canvasY - plotArea.y;
@@ -6469,7 +6308,7 @@ function renderDataZoomSlider(ctx, zoom) {
6469
6308
  const previewSeries = (options.series || []).filter((s) => !ctx.hiddenSeries.has(s.name))[0];
6470
6309
  if (previewSeries && previewSeries.data.length > 0) {
6471
6310
  const data = previewSeries.data;
6472
- const max = Math.max(...data.map((v) => v === null || Number.isNaN(v) ? 0 : v), 1);
6311
+ const max = maxOf(data.map((v) => v === null || Number.isNaN(v) ? 0 : v), 1);
6473
6312
  const barAreaHeight = geo.height - 18;
6474
6313
  const barWidth = Math.max(1, geo.width / data.length - 1);
6475
6314
  data.forEach((raw, i) => {
@@ -6771,6 +6610,11 @@ function createSvgRecorder(real) {
6771
6610
  "font-family": font.family,
6772
6611
  "fill": typeof style.fillStyle === "string" ? style.fillStyle : "#000"
6773
6612
  };
6613
+ if (matrix[1] !== 0 || matrix[2] !== 0) {
6614
+ attrs.x = x.toFixed(2);
6615
+ attrs.y = y.toFixed(2);
6616
+ attrs.transform = `matrix(${matrix.map((v) => +v.toFixed(4)).join(" ")})`;
6617
+ }
6774
6618
  if (style.textAlign === "center") attrs["text-anchor"] = "middle";
6775
6619
  else if (style.textAlign === "right" || style.textAlign === "end") attrs["text-anchor"] = "end";
6776
6620
  if (style.textBaseline === "middle") attrs["dominant-baseline"] = "central";
@@ -6920,7 +6764,7 @@ function createSvgRecorder(real) {
6920
6764
  function gradientDefs() {
6921
6765
  return gradients.map((g, i) => {
6922
6766
  const stops = g.stops.map((s) => {
6923
- let color = s.color;
6767
+ let color = escapeXml(String(s.color));
6924
6768
  let opacity = 1;
6925
6769
  const m = /^(#[0-9a-fA-F]{6})([0-9a-fA-F]{2})$/.exec(s.color);
6926
6770
  if (m) {
@@ -6934,7 +6778,7 @@ function createSvgRecorder(real) {
6934
6778
  }
6935
6779
  function toSvg(width, height, backgroundColor) {
6936
6780
  const body = elements.map((el) => {
6937
- const attrs = Object.entries(el.attrs).map(([k, v]) => `${k}="${v}"`).join(" ");
6781
+ const attrs = Object.entries(el.attrs).map(([k, v]) => `${k}="${escapeXml(String(v))}"`).join(" ");
6938
6782
  if (el.tag === "text") return `<text ${attrs}>${escapeXml(el.text || "")}</text>`;
6939
6783
  return `<${el.tag} ${attrs}/>`;
6940
6784
  }).join("\n");
@@ -6956,7 +6800,14 @@ function runLayers(renderCtx, at) {
6956
6800
  }
6957
6801
  function renderChart(canvas, params) {
6958
6802
  const { options: rawOptions, ctx: externalCtx, dpr = window.devicePixelRatio || 1, progress = 1, hoverIndex = -1, hoverAnimProgress = 1, focusAnimProgress = 1, hiddenSeries = /* @__PURE__ */ new Set(), mouseX = -1, mouseY = -1, showCrosshair = false, zoomRange, brushRect, focusSeries = null, hoveredToolbox = null } = params;
6959
- const options = applyLogTransform(rawOptions);
6803
+ const logApplied = applyLogTransform(rawOptions);
6804
+ const options = (logApplied.series || []).some((s) => !s.data) ? {
6805
+ ...logApplied,
6806
+ series: logApplied.series.map((s) => s.data ? s : {
6807
+ ...s,
6808
+ data: []
6809
+ })
6810
+ } : logApplied;
6960
6811
  const ctx = externalCtx || canvas.getContext("2d");
6961
6812
  const width = canvas.width / dpr;
6962
6813
  const height = canvas.height / dpr;
@@ -7017,10 +6868,10 @@ function renderChart(canvas, params) {
7017
6868
  });
7018
6869
  break;
7019
6870
  }
7020
- const yValues = scatterData.map((d) => d.y);
6871
+ const yValues = scatterData.map((d) => d.y).filter((v) => typeof v === "number" && Number.isFinite(v));
7021
6872
  const yRange = renderYAxis(renderCtx, "left", {
7022
- min: Math.min(...yValues),
7023
- max: Math.max(...yValues)
6873
+ min: yValues.length ? minOf(yValues) : 0,
6874
+ max: yValues.length ? maxOf(yValues) : 1
7024
6875
  });
7025
6876
  renderXAxis(renderCtx);
7026
6877
  points = renderScatterChart(renderCtx, yRange);
@@ -7050,8 +6901,8 @@ function renderChart(canvas, params) {
7050
6901
  ...renderCtx,
7051
6902
  plotArea: axisArea
7052
6903
  }, "left", {
7053
- min: Math.min(...allValues),
7054
- max: Math.max(...allValues)
6904
+ min: minOf(allValues),
6905
+ max: maxOf(allValues)
7055
6906
  });
7056
6907
  renderXAxis(renderCtx);
7057
6908
  renderCandleChart(renderCtx, yRange);
@@ -7068,7 +6919,15 @@ function renderChart(canvas, params) {
7068
6919
  renderChordChart(renderCtx);
7069
6920
  break;
7070
6921
  case "arc":
7071
- renderArcChart(renderCtx);
6922
+ if (options.arcCircular === true) renderChordChart({
6923
+ ...renderCtx,
6924
+ options: {
6925
+ ...options,
6926
+ chordData: options.arcData,
6927
+ chordMode: "curve"
6928
+ }
6929
+ });
6930
+ else renderArcChart(renderCtx);
7072
6931
  break;
7073
6932
  case "gantt":
7074
6933
  renderGanttChart(renderCtx);
@@ -7112,15 +6971,17 @@ function renderChart(canvas, params) {
7112
6971
  b.min,
7113
6972
  b.max,
7114
6973
  ...showOutliers ? b.outliers || [] : []
7115
- ]);
6974
+ ]).filter((v) => typeof v === "number" && Number.isFinite(v));
6975
+ const valueLo = allValues.length ? minOf(allValues) : 0;
6976
+ const valueHi = allValues.length ? maxOf(allValues) : 1;
7116
6977
  let range;
7117
6978
  if (horizontal) range = renderHorizontalAxis(renderCtx, {
7118
- min: Math.min(...allValues),
7119
- max: Math.max(...allValues)
6979
+ min: valueLo,
6980
+ max: valueHi
7120
6981
  });
7121
6982
  else range = renderYAxis(renderCtx, "left", {
7122
- min: Math.min(...allValues),
7123
- max: Math.max(...allValues)
6983
+ min: valueLo,
6984
+ max: valueHi
7124
6985
  });
7125
6986
  leftRange = range;
7126
6987
  renderBoxplotChart(renderCtx, range);
@@ -7292,7 +7153,12 @@ var chartOptionsSchema = {
7292
7153
  show: { type: "boolean" },
7293
7154
  position: {
7294
7155
  type: "string",
7295
- enum: ["top", "bottom"]
7156
+ enum: [
7157
+ "top",
7158
+ "bottom",
7159
+ "left",
7160
+ "right"
7161
+ ]
7296
7162
  },
7297
7163
  interactive: { type: "boolean" },
7298
7164
  hoverEmphasis: { type: "boolean" }
@@ -7315,13 +7181,18 @@ var chartOptionsSchema = {
7315
7181
  properties: {
7316
7182
  enabled: { type: "boolean" },
7317
7183
  duration: { type: "number" },
7318
- easing: { type: "string" }
7184
+ easing: { type: "string" },
7185
+ stagger: { type: "number" }
7319
7186
  }
7320
7187
  },
7321
7188
  xAxis: {
7322
7189
  type: "object",
7323
7190
  properties: {
7324
7191
  show: { type: "boolean" },
7192
+ type: {
7193
+ type: "string",
7194
+ enum: ["category", "time"]
7195
+ },
7325
7196
  min: { type: "number" },
7326
7197
  max: { type: "number" },
7327
7198
  ticks: { type: "integer" },
@@ -7341,6 +7212,10 @@ var chartOptionsSchema = {
7341
7212
  type: "object",
7342
7213
  properties: {
7343
7214
  show: { type: "boolean" },
7215
+ type: {
7216
+ type: "string",
7217
+ enum: ["value", "log"]
7218
+ },
7344
7219
  min: { type: "number" },
7345
7220
  max: { type: "number" },
7346
7221
  ticks: { type: "integer" },
@@ -7481,6 +7356,8 @@ var chartOptionsSchema = {
7481
7356
  }
7482
7357
  },
7483
7358
  annotation: { type: "object" },
7359
+ markLines: { type: "array" },
7360
+ markAreas: { type: "array" },
7484
7361
  markLine: { type: "array" },
7485
7362
  markArea: { type: "array" },
7486
7363
  scenes: {
@@ -7503,20 +7380,110 @@ var chartOptionsSchema = {
7503
7380
  }
7504
7381
  },
7505
7382
  pieData: { type: "array" },
7383
+ piePercentMode: { type: "boolean" },
7384
+ pieHideThreshold: { type: "number" },
7385
+ innerRadius: { type: "number" },
7506
7386
  radarSeries: { type: "array" },
7507
7387
  radarIndicators: { type: "array" },
7388
+ radarStacked: { type: "boolean" },
7389
+ radarRingFill: { type: "boolean" },
7390
+ radarLabelOffset: { type: "number" },
7508
7391
  scatterData: { type: "array" },
7392
+ quadrant: {
7393
+ type: "object",
7394
+ properties: {
7395
+ show: { type: "boolean" },
7396
+ xMid: { type: "number" },
7397
+ yMid: { type: "number" },
7398
+ labels: { type: "array" }
7399
+ }
7400
+ },
7401
+ scatterTrendline: { type: "string" },
7402
+ trendlinePerGroup: { type: "boolean" },
7403
+ jitter: { type: "number" },
7404
+ pointLabels: { type: "boolean" },
7405
+ facet: { type: "boolean" },
7509
7406
  funnelData: { type: "array" },
7510
7407
  funnelMinRatio: { type: "number" },
7408
+ pyramid: { type: "boolean" },
7511
7409
  gauge: { oneOf: [{ type: "number" }, { type: "object" }] },
7512
7410
  heatmapData: { type: "array" },
7411
+ heatmap: {
7412
+ type: "object",
7413
+ properties: {
7414
+ colorScale: { type: "array" },
7415
+ colorBar: { type: "boolean" }
7416
+ }
7417
+ },
7418
+ heatmapColorBar: { type: "boolean" },
7419
+ heatmapSortBy: {
7420
+ type: "string",
7421
+ enum: ["name", "value"]
7422
+ },
7513
7423
  candleData: { type: "array" },
7514
7424
  volumeData: { type: "array" },
7515
7425
  candleMa: { type: "array" },
7426
+ candleMaColors: { type: "array" },
7427
+ candleUpColor: { type: "string" },
7428
+ candleDownColor: { type: "string" },
7516
7429
  calendarData: { type: "array" },
7517
- calendar: { type: "object" },
7430
+ calendar: {
7431
+ type: "object",
7432
+ properties: {
7433
+ start: { type: "string" },
7434
+ end: { type: "string" },
7435
+ weekStart: { type: "integer" },
7436
+ today: { type: "string" },
7437
+ granularity: {
7438
+ type: "string",
7439
+ enum: [
7440
+ "day",
7441
+ "week",
7442
+ "month"
7443
+ ]
7444
+ },
7445
+ colors: { type: "array" },
7446
+ cellGap: { type: "number" },
7447
+ showScale: { type: "boolean" },
7448
+ weekdayLabels: { type: "array" },
7449
+ showAllWeekdays: { type: "boolean" }
7450
+ }
7451
+ },
7518
7452
  bulletData: { type: "array" },
7519
7453
  boxData: { type: "array" },
7454
+ boxHorizontal: { type: "boolean" },
7455
+ showOutliers: { type: "boolean" },
7456
+ binConfig: {
7457
+ type: "object",
7458
+ properties: {
7459
+ bins: { type: "array" },
7460
+ binCount: { type: "integer" },
7461
+ density: { type: "boolean" }
7462
+ }
7463
+ },
7464
+ treemapData: { type: "array" },
7465
+ sunburstData: { type: "array" },
7466
+ valueFormat: {
7467
+ type: "object",
7468
+ properties: {
7469
+ decimals: { type: "integer" },
7470
+ thousandSeparator: { type: "string" },
7471
+ prefix: { type: "string" },
7472
+ suffix: { type: "string" },
7473
+ abbreviate: { type: "boolean" }
7474
+ }
7475
+ },
7476
+ showValues: { type: "boolean" },
7477
+ borderRadius: { type: "number" },
7478
+ step: { type: "boolean" },
7479
+ smooth: { type: "boolean" },
7480
+ stackAreas: { type: "boolean" },
7481
+ connectNulls: { type: "boolean" },
7482
+ crosshairUnified: { type: "boolean" },
7483
+ waterfall: {
7484
+ type: "object",
7485
+ properties: { totalIndices: { type: "array" } }
7486
+ },
7520
7487
  sankeyData: {
7521
7488
  type: "object",
7522
7489
  properties: {
@@ -7524,6 +7491,13 @@ var chartOptionsSchema = {
7524
7491
  links: { type: "array" }
7525
7492
  }
7526
7493
  },
7494
+ sankey: {
7495
+ type: "object",
7496
+ properties: { nodeAlign: {
7497
+ type: "string",
7498
+ enum: ["justify", "left"]
7499
+ } }
7500
+ },
7527
7501
  chordData: {
7528
7502
  type: "object",
7529
7503
  properties: {
@@ -7539,7 +7513,10 @@ var chartOptionsSchema = {
7539
7513
  }
7540
7514
  },
7541
7515
  vennData: { type: "array" },
7516
+ vennHollow: { type: "boolean" },
7542
7517
  ganttData: { type: "array" },
7518
+ ganttToday: { type: "string" },
7519
+ arcCircular: { type: "boolean" },
7543
7520
  matrixFields: { type: "array" },
7544
7521
  matrixData: { type: "array" },
7545
7522
  loading: { type: "boolean" },
@@ -7553,7 +7530,9 @@ var chartOptionsSchema = {
7553
7530
  },
7554
7531
  logAxis: { type: "boolean" },
7555
7532
  valueFormatter: { type: "function" },
7556
- timeAxis: { type: "boolean" }
7533
+ timeAxis: { type: "boolean" },
7534
+ sparklineSmooth: { type: "boolean" },
7535
+ sparklineArea: { type: "boolean" }
7557
7536
  }
7558
7537
  };
7559
7538
  var TYPE_CHECKS = {
@@ -7800,6 +7779,192 @@ function distinctCount(values) {
7800
7779
  return new Set(values.filter((v) => v !== null && v !== "")).size;
7801
7780
  }
7802
7781
  //#endregion
7782
+ //#region src/ai/threed.js
7783
+ var THREE_HINT = /三维|立体|3d/i;
7784
+ function wantsThreed(hint = {}) {
7785
+ if (hint.intent === "threed") return true;
7786
+ const text = [hint.title, hint.hint].filter(Boolean).join(" ");
7787
+ return THREE_HINT.test(text);
7788
+ }
7789
+ function pivotGrid(cx, cy, cz) {
7790
+ const rows = cx.values.length;
7791
+ if (rows < 4) return null;
7792
+ const xs = [...new Set(cx.values.filter(Number.isFinite))].sort((a, b) => a - b);
7793
+ const ys = [...new Set(cy.values.filter(Number.isFinite))].sort((a, b) => a - b);
7794
+ if (xs.length < 2 || ys.length < 2) return null;
7795
+ if (xs.length * ys.length !== rows) return null;
7796
+ if (xs.length > 64 || ys.length > 64) return {
7797
+ tooLarge: true,
7798
+ x: xs.length,
7799
+ y: ys.length
7800
+ };
7801
+ const z = Array.from({ length: ys.length }, () => new Array(xs.length).fill(null));
7802
+ const seen = /* @__PURE__ */ new Set();
7803
+ for (let i = 0; i < rows; i++) {
7804
+ const xi = xs.indexOf(cx.values[i]);
7805
+ const yi = ys.indexOf(cy.values[i]);
7806
+ if (xi < 0 || yi < 0) return null;
7807
+ const key = yi * xs.length + xi;
7808
+ if (seen.has(key)) return null;
7809
+ seen.add(key);
7810
+ z[yi][xi] = cz.values[i];
7811
+ }
7812
+ return {
7813
+ x: xs,
7814
+ y: ys,
7815
+ z
7816
+ };
7817
+ }
7818
+ function planThreed({ hint, intent, numbers, times, cats, table, labels }) {
7819
+ const asked = wantsThreed(hint);
7820
+ const natural = !asked && cats.length === 0 && times.length === 0 && numbers.length >= 3;
7821
+ if (!asked && !natural) return null;
7822
+ if (cats.length === 0 && times.length === 0) {
7823
+ if (numbers.length < 3) return {
7824
+ spec: null,
7825
+ report: [{
7826
+ level: "warn",
7827
+ message: "三维散点/曲面需要至少三个数值维度,已回退二维选型"
7828
+ }]
7829
+ };
7830
+ const [cx, cy, cz] = numbers;
7831
+ const grid = pivotGrid(cx, cy, cz);
7832
+ if (grid && !grid.tooLarge) return {
7833
+ spec: {
7834
+ type: "surface3d",
7835
+ surfaceData: {
7836
+ x: grid.x,
7837
+ y: grid.y,
7838
+ z: grid.z
7839
+ },
7840
+ xAxis: { name: cx.name },
7841
+ yAxis: { name: cy.name },
7842
+ zAxis: { name: cz.name }
7843
+ },
7844
+ report: [{
7845
+ level: "info",
7846
+ message: `三维曲面:${cx.name} × ${cy.name} 构成 ${grid.x.length}×${grid.y.length} 完整高度场(${cz.name})`
7847
+ }]
7848
+ };
7849
+ const report = [];
7850
+ if (grid && grid.tooLarge) report.push({
7851
+ level: "warn",
7852
+ message: `数值网格 ${grid.x}×${grid.y} 过大(任一边 > 64),曲面 spec 会爆炸,已改用三维散点`
7853
+ });
7854
+ return {
7855
+ spec: {
7856
+ type: "scatter3d",
7857
+ scatterData: table.rows.map((_, i) => ({
7858
+ x: cx.values[i],
7859
+ y: cy.values[i],
7860
+ z: cz.values[i]
7861
+ })),
7862
+ xAxis: { name: cx.name },
7863
+ yAxis: { name: cy.name },
7864
+ zAxis: { name: cz.name }
7865
+ },
7866
+ report: [...report, {
7867
+ level: "info",
7868
+ message: `三维散点:${cx.name} / ${cy.name} / ${cz.name} 三轴空间(二维散点会丢一维)`
7869
+ }]
7870
+ };
7871
+ }
7872
+ if (numbers.length === 0) return null;
7873
+ if (intent === "relation" && numbers.length >= 3) {
7874
+ const [cx, cy, cz] = numbers;
7875
+ const cat = cats[0];
7876
+ return {
7877
+ spec: {
7878
+ type: "scatter3d",
7879
+ scatterData: table.rows.map((_, i) => ({
7880
+ x: cx.values[i],
7881
+ y: cy.values[i],
7882
+ z: cz.values[i],
7883
+ label: cat ? String(cat.values[i]) : String(i + 1)
7884
+ })),
7885
+ xAxis: { name: cx.name },
7886
+ yAxis: { name: cy.name },
7887
+ zAxis: { name: cz.name }
7888
+ },
7889
+ report: [{
7890
+ level: "info",
7891
+ message: `三维散点:${cx.name} / ${cy.name} / ${cz.name} 三轴空间,${cat ? `「${cat.name}」作点标签` : "行号作点标签"}`
7892
+ }]
7893
+ };
7894
+ }
7895
+ const seriesList = numbers.slice(0, 8).map((c) => ({
7896
+ name: c.name,
7897
+ data: c.values
7898
+ }));
7899
+ const report = [];
7900
+ if (numbers.length > 8) report.push({
7901
+ level: "warn",
7902
+ message: `数值列超过 8 个,仅取前 8 列(${seriesList.map((s) => s.name).join("、")})`
7903
+ });
7904
+ if (intent === "share") {
7905
+ if (seriesList.length === 1 && labels.length <= 8) {
7906
+ const pieData = labels.map((name, i) => ({
7907
+ name,
7908
+ value: seriesList[0].data[i]
7909
+ })).filter((d) => typeof d.value === "number" && d.value > 0);
7910
+ return {
7911
+ spec: {
7912
+ type: "pie3d",
7913
+ pieData
7914
+ },
7915
+ report: [...report, {
7916
+ level: "info",
7917
+ message: `三维饼:${seriesList[0].name} 按类目占比(${pieData.length} 项)`
7918
+ }]
7919
+ };
7920
+ }
7921
+ if (seriesList.length >= 2 && seriesList.length <= 8) {
7922
+ const sums = seriesList.map((s) => s.data.reduce((acc, v) => typeof v === "number" && Number.isFinite(v) ? acc + v : acc, 0));
7923
+ if (sums.every((v) => v > 0)) return {
7924
+ spec: {
7925
+ type: "pie3d",
7926
+ pieData: seriesList.map((s, i) => ({
7927
+ name: s.name,
7928
+ value: sums[i]
7929
+ }))
7930
+ },
7931
+ report: [...report, {
7932
+ level: "info",
7933
+ message: `三维饼:按度量合计占比(${sums.length} 项)`
7934
+ }]
7935
+ };
7936
+ }
7937
+ }
7938
+ const text = [hint.title, hint.hint].filter(Boolean).join("");
7939
+ const isTrend = !/柱|条形/.test(text) && (times.length > 0 || intent === "trend");
7940
+ const spec = {
7941
+ type: isTrend ? "line3d" : "bar3d",
7942
+ labels,
7943
+ series: seriesList
7944
+ };
7945
+ if (seriesList.length > 1) spec.legend = { show: true };
7946
+ if (seriesList.length === 1) spec.zAxis = { name: seriesList[0].name };
7947
+ report.push({
7948
+ level: "info",
7949
+ message: isTrend ? "三维空间折线:维度有序看走势,落地投影与面带保留空间形态" : "三维柱林:类目 × 系列的空间量值对比"
7950
+ });
7951
+ return {
7952
+ spec,
7953
+ report
7954
+ };
7955
+ }
7956
+ var SPEC_RULES_3D = [
7957
+ "type 必填,取值限定三维 schema enum:bar3d / line3d / scatter3d / surface3d / pie3d",
7958
+ "bar3d / line3d 用 labels(X 类目)+ series(Y 系列,data 为标量数组),series[].data 长度必须与 labels 一致,系列建议 ≤5(过多互相遮挡)",
7959
+ "scatter3d 用 scatterData:{ x, y, z, label? } 三元组数组,三轴均为数值",
7960
+ "surface3d 用 surfaceData:{ x: 列值数组, y: 行值数组, z: 行×列矩阵 },z.length 必须等于 y.length 且每行长度等于 x.length",
7961
+ "pie3d 用 pieData:{ name, value } 数组,扇区 ≤8 项,正值才参与",
7962
+ "渲染组件是 <ev-chart3d>(来自 @wil-works/evoke-charts/3d 子入口);二维专属字段(annotations / dataZoom / yAxisRight / markLines 等)在三维 schema 中不存在,不要使用",
7963
+ "相机交互默认已含拖拽环绕、滚轮缩放与双击复位,一般无需配置 camera;展厅氛围场景才设 camera.autoRotate: true",
7964
+ "palette / theme / colors 与二维同一套:色系 id 限定三维 schema enum;theme.colors 与 palette 同时出现时以 theme.colors 为准",
7965
+ "数值格式化用 valueFormatter(函数)或 i18n 文案对象;不使用三维 schema 之外的字段"
7966
+ ];
7967
+ //#endregion
7803
7968
  //#region src/ai/spec.js
7804
7969
  var INTENT_KEYWORDS = {
7805
7970
  trend: [
@@ -7932,6 +8097,25 @@ function generateChartSpec(data, hint = {}) {
7932
8097
  }];
7933
8098
  const spec = {};
7934
8099
  if (hint.title) spec.title = hint.title;
8100
+ const three = planThreed({
8101
+ hint,
8102
+ intent,
8103
+ numbers,
8104
+ times,
8105
+ cats,
8106
+ table,
8107
+ labels
8108
+ });
8109
+ if (three) {
8110
+ report.push(...three.report);
8111
+ if (three.spec) {
8112
+ Object.assign(spec, three.spec);
8113
+ return {
8114
+ spec,
8115
+ report
8116
+ };
8117
+ }
8118
+ }
7935
8119
  if (intent === "relation" && numbers.length >= 2) {
7936
8120
  const cat = cats[0];
7937
8121
  spec.type = "scatter";
@@ -8020,9 +8204,9 @@ function generateChartSpec(data, hint = {}) {
8020
8204
  if (splitIdx >= 0) {
8021
8205
  spec.type = "mixed";
8022
8206
  spec.yAxisRight = { show: true };
8023
- const peakOf = (s) => Math.max(0, ...s.data.filter((v) => typeof v === "number" && Number.isFinite(v)).map(Math.abs));
8207
+ const peakOf = (s) => Math.max(0, maxOf(s.data.filter((v) => typeof v === "number" && Number.isFinite(v)).map(Math.abs)));
8024
8208
  const peakMax = peakOf(seriesList[splitIdx]);
8025
- const peakMin = Math.min(...seriesList.filter((_, i) => i !== splitIdx).map(peakOf).filter((v) => v > 0));
8209
+ const peakMin = minOf(seriesList.filter((_, i) => i !== splitIdx).map(peakOf).filter((v) => v > 0));
8026
8210
  const ratioText = Number.isFinite(peakMin) && peakMin > 0 ? `(最高约为最低的 ${fmtRatio(peakMax / peakMin)} 倍)` : "";
8027
8211
  report.push({
8028
8212
  level: "info",
@@ -8066,8 +8250,8 @@ function planDualAxis(seriesList) {
8066
8250
  const maxes = seriesList.map((s) => s.data.reduce((m, v) => typeof v === "number" && Number.isFinite(v) ? Math.max(m, Math.abs(v)) : m, 0));
8067
8251
  const valid = maxes.filter((m) => m > 0);
8068
8252
  if (seriesList.length < 2 || valid.length < 2) return -1;
8069
- const max = Math.max(...maxes);
8070
- return max / Math.min(...valid) >= 100 ? maxes.indexOf(max) : -1;
8253
+ const max = maxOf(maxes);
8254
+ return max / minOf(valid) >= 100 ? maxes.indexOf(max) : -1;
8071
8255
  }
8072
8256
  function fmtRatio(ratio) {
8073
8257
  if (ratio >= 1e8) return `${(ratio / 1e8).toFixed(1)}亿`;
@@ -8094,6 +8278,16 @@ var SPEC_RULES = [
8094
8278
  "series[].data 长度必须与 labels 一致,缺失值用 null",
8095
8279
  "双轴:两个量纲或量级悬殊的度量用 type mixed——大量度 series 用 chartType bar 且 yAxis left,小量度 chartType line 且 yAxis right,并置顶层 yAxisRight",
8096
8280
  "palette 仅在需求要求固定配色(不随宿主主题换肤)时设置,取值限定 schema enum 中的色系 id;与 theme.colors 同时出现时以 theme.colors 为准",
8281
+ "弧长连接图关系密集或需要闭合回路视角时置 arcCircular: true,数据仍走 arcData",
8282
+ "日历热力需要周/月聚合视角时置 calendar.granularity(day 每日 / week / month,周月为求和聚合)",
8283
+ "数值格式化优先用 valueFormat 快捷对象(decimals/thousandSeparator/prefix/suffix/abbreviate),确需函数式定制才用 valueFormatter",
8284
+ "散点增强:要看分布分界用 quadrant(均值十字 + 象限标签),看相关走向用 scatterTrendline: 'linear'(多组对比配 trendlinePerGroup: true),同值重叠用 jitter,需逐点标注用 pointLabels: true,多变量按 group 拆格用 facet: true",
8285
+ "雷达增强:radarRingFill: true 画同心环带底色;多系列重叠看构成用 radarStacked: true",
8286
+ "K 线:volumeData 与 candleData 等长即带量副图,candleMa(如 [5, 10])叠均线并可配 candleMaColors,涨跌色用 candleUpColor / candleDownColor",
8287
+ "桑基列对齐缺省 justify(末端贴右缘),仅按拓扑深度排布的需求才设 sankey: { nodeAlign: 'left' }",
8288
+ "箱线横向排布用 boxHorizontal: true;直方图分箱用 binConfig(binCount 或 bins,密度视角 density);热力排序视角用 heatmapSortBy: 'value'",
8289
+ "折线增强:多系列面积构成用 stackAreas: true,阶梯走势用 step: true,null 断点需跨接用 connectNulls: true,逐点标值用 showValues,阈值参考线用 markLines,事件区间强调用 markAreas",
8290
+ "瀑布图合计列用 waterfall.totalIndices 标记;漏斗尾段压缩用 funnelMinRatio;饼图环形化用 type: 'doughnut' 配 innerRadius",
8097
8291
  "叙述注解 annotations 单图不超过 3 处,emphasis 焦点最多 1 个系列",
8098
8292
  "不使用 schema 之外的字段",
8099
8293
  `支持的图表类型:${chartOptionsSchema.properties.type.enum.join(" / ")}`
@@ -8249,27 +8443,252 @@ var SPEC_EXAMPLES = [
8249
8443
  yAxisRight: { show: true },
8250
8444
  legend: { show: true }
8251
8445
  }
8446
+ },
8447
+ {
8448
+ requirement: "按周汇总看打卡活跃度",
8449
+ spec: {
8450
+ type: "calendar-heatmap",
8451
+ title: "打卡活跃度(按周聚合)",
8452
+ calendarData: [
8453
+ {
8454
+ date: "2026-06-01",
8455
+ value: 3
8456
+ },
8457
+ {
8458
+ date: "2026-06-02",
8459
+ value: 5
8460
+ },
8461
+ {
8462
+ date: "2026-06-05",
8463
+ value: 8
8464
+ }
8465
+ ],
8466
+ calendar: {
8467
+ start: "2026-06-01",
8468
+ end: "2026-08-31",
8469
+ granularity: "week"
8470
+ }
8471
+ }
8472
+ },
8473
+ {
8474
+ requirement: "服务间调用关系,环形布局更好看出闭环",
8475
+ spec: {
8476
+ type: "arc",
8477
+ title: "服务间调用拓扑",
8478
+ arcCircular: true,
8479
+ arcData: {
8480
+ nodes: [
8481
+ { name: "网关" },
8482
+ { name: "订单服务" },
8483
+ { name: "支付服务" },
8484
+ { name: "消息中心" }
8485
+ ],
8486
+ links: [
8487
+ {
8488
+ source: "网关",
8489
+ target: "订单服务",
8490
+ value: 46
8491
+ },
8492
+ {
8493
+ source: "订单服务",
8494
+ target: "支付服务",
8495
+ value: 38
8496
+ },
8497
+ {
8498
+ source: "支付服务",
8499
+ target: "消息中心",
8500
+ value: 22
8501
+ }
8502
+ ]
8503
+ }
8504
+ }
8505
+ },
8506
+ {
8507
+ requirement: "各渠道投放费用和转化效果的关系,看分界和走向",
8508
+ spec: {
8509
+ type: "scatter",
8510
+ title: "投放-转化四象限",
8511
+ scatterData: [
8512
+ {
8513
+ x: 120,
8514
+ y: 32,
8515
+ label: "搜索"
8516
+ },
8517
+ {
8518
+ x: 260,
8519
+ y: 58,
8520
+ label: "信息流"
8521
+ },
8522
+ {
8523
+ x: 90,
8524
+ y: 18,
8525
+ label: "社群"
8526
+ },
8527
+ {
8528
+ x: 340,
8529
+ y: 71,
8530
+ label: "直播"
8531
+ }
8532
+ ],
8533
+ quadrant: { labels: [
8534
+ "低投高转",
8535
+ "高投高转",
8536
+ "低投低转",
8537
+ "高投低转"
8538
+ ] },
8539
+ scatterTrendline: "linear",
8540
+ pointLabels: true
8541
+ }
8252
8542
  }
8253
8543
  ];
8254
8544
  function formatExamples(maxCount = SPEC_EXAMPLES.length) {
8255
8545
  return SPEC_EXAMPLES.slice(0, maxCount).map((e) => `需求:${e.requirement}\nSpec:${JSON.stringify(e.spec)}`).join("\n\n");
8256
8546
  }
8547
+ var SPEC_EXAMPLES_3D = [
8548
+ {
8549
+ requirement: "三维看温度、湿度与设备故障率的关系",
8550
+ spec: {
8551
+ type: "scatter3d",
8552
+ title: "温度-湿度-故障率空间分布",
8553
+ scatterData: [
8554
+ {
8555
+ x: 22,
8556
+ y: 40,
8557
+ z: 1.2,
8558
+ label: "产线 A"
8559
+ },
8560
+ {
8561
+ x: 28,
8562
+ y: 55,
8563
+ z: 2.8,
8564
+ label: "产线 B"
8565
+ },
8566
+ {
8567
+ x: 31,
8568
+ y: 62,
8569
+ z: 4.5,
8570
+ label: "产线 C"
8571
+ },
8572
+ {
8573
+ x: 25,
8574
+ y: 48,
8575
+ z: 1.9,
8576
+ label: "产线 D"
8577
+ }
8578
+ ],
8579
+ xAxis: { name: "温度(℃)" },
8580
+ yAxis: { name: "湿度(%)" },
8581
+ zAxis: { name: "故障率(%)" }
8582
+ }
8583
+ },
8584
+ {
8585
+ requirement: "转速和扭矩共同影响下的产出效率曲面",
8586
+ spec: {
8587
+ type: "surface3d",
8588
+ title: "转速-扭矩-效率高度场",
8589
+ surfaceData: {
8590
+ x: [
8591
+ 800,
8592
+ 1200,
8593
+ 1600,
8594
+ 2e3
8595
+ ],
8596
+ y: [
8597
+ 100,
8598
+ 150,
8599
+ 200
8600
+ ],
8601
+ z: [
8602
+ [
8603
+ 61,
8604
+ 64,
8605
+ 66,
8606
+ 63
8607
+ ],
8608
+ [
8609
+ 67,
8610
+ 72,
8611
+ 75,
8612
+ 70
8613
+ ],
8614
+ [
8615
+ 64,
8616
+ 69,
8617
+ 71,
8618
+ 66
8619
+ ]
8620
+ ]
8621
+ },
8622
+ xAxis: { name: "转速(rpm)" },
8623
+ yAxis: { name: "扭矩(N·m)" },
8624
+ zAxis: { name: "效率(%)" }
8625
+ }
8626
+ },
8627
+ {
8628
+ requirement: "三维柱林对比四个季度的区域销量",
8629
+ spec: {
8630
+ type: "bar3d",
8631
+ title: "区域季度销量柱林",
8632
+ labels: [
8633
+ "一季度",
8634
+ "二季度",
8635
+ "三季度",
8636
+ "四季度"
8637
+ ],
8638
+ series: [
8639
+ {
8640
+ name: "华东",
8641
+ data: [
8642
+ 320,
8643
+ 356,
8644
+ 401,
8645
+ 388
8646
+ ]
8647
+ },
8648
+ {
8649
+ name: "华南",
8650
+ data: [
8651
+ 280,
8652
+ 302,
8653
+ 346,
8654
+ 331
8655
+ ]
8656
+ },
8657
+ {
8658
+ name: "华北",
8659
+ data: [
8660
+ 240,
8661
+ 262,
8662
+ 288,
8663
+ 305
8664
+ ]
8665
+ }
8666
+ ],
8667
+ legend: { show: true },
8668
+ zAxis: { name: "销量(万)" }
8669
+ }
8670
+ }
8671
+ ];
8672
+ function formatExamples3d(maxCount = SPEC_EXAMPLES_3D.length) {
8673
+ return SPEC_EXAMPLES_3D.slice(0, maxCount).map((e) => `需求:${e.requirement}\nSpec:${JSON.stringify(e.spec)}`).join("\n\n");
8674
+ }
8257
8675
  //#endregion
8258
8676
  //#region src/ai/prompt.js
8259
8677
  var PREVIEW_ROWS = 8;
8260
- function buildChartPrompt({ data, requirement, extraRules = [], examples = true } = {}) {
8678
+ function buildChartPrompt({ data, requirement, extraRules = [], examples = true, mode = "2d" } = {}) {
8679
+ const is3d = mode === "3d";
8261
8680
  const table = parseDataTable(data);
8262
8681
  let digest;
8263
8682
  if (table) digest = `${table.headers.join(" | ")}\n${table.rows.slice(0, PREVIEW_ROWS).map((r) => table.headers.map((h) => r[h] === null || r[h] === void 0 ? "" : String(r[h])).join(" | ")).join("\n")}${table.rows.length > PREVIEW_ROWS ? `\n…共 ${table.rows.length} 行` : ""}`;
8264
8683
  else if (data !== void 0 && data !== null) digest = typeof data === "string" ? data : JSON.stringify(data);
8265
8684
  else digest = "(未提供数据,请基于需求虚构合理示例数据)";
8266
- const rules = [...SPEC_RULES, ...extraRules];
8685
+ const rules = [...is3d ? SPEC_RULES_3D : SPEC_RULES, ...extraRules];
8267
8686
  const sections = [
8268
- "你是图表配置生成器。根据下面的数据与需求,产出 EvChart 的 options JSON(下称 Spec),它将被 <ev-chart :options> 直接渲染。",
8269
- `## Options Schema\n${JSON.stringify(chartOptionsSchema)}`,
8687
+ is3d ? "你是三维图表配置生成器。根据下面的数据与需求,产出 EvChart3d 的 options JSON(下称 Spec),它将被 <ev-chart3d :options> 直接渲染(组件来自 @wil-works/evoke-charts/3d 子入口)。" : "你是图表配置生成器。根据下面的数据与需求,产出 EvChart 的 options JSON(下称 Spec),它将被 <ev-chart :options> 直接渲染。",
8688
+ `## Options Schema\n${JSON.stringify(is3d ? chart3dOptionsSchema : chartOptionsSchema)}`,
8270
8689
  `## 数据预览\n${digest}`
8271
8690
  ];
8272
- if (examples) sections.push(`## 示例\n${formatExamples()}`);
8691
+ if (examples) sections.push(`## 示例\n${is3d ? formatExamples3d() : formatExamples()}`);
8273
8692
  sections.push(requirement ? `## 需求\n${requirement}` : "## 需求\n根据数据形状自行选择最合适的图表类型,并拟一个不超过 12 字的标题。");
8274
8693
  sections.push(`## 硬性规则\n${rules.map((r, i) => `${i + 1}. ${r}`).join("\n")}`);
8275
8694
  return sections.join("\n\n");
@@ -8378,6 +8797,23 @@ function lintChartSpec(spec, opts = {}) {
8378
8797
  }],
8379
8798
  spec: fixed
8380
8799
  };
8800
+ if (typeof fixed.type === "string" && /3d$/.test(fixed.type)) {
8801
+ const { warnings } = validateOptions3d(fixed);
8802
+ warnings.forEach((w) => issues.push({
8803
+ level: "error",
8804
+ message: `${w.path} ${w.message}`,
8805
+ path: w.path
8806
+ }));
8807
+ if (!issues.length) issues.push({
8808
+ level: "info",
8809
+ message: "三维 spec 已过 schema 校验(几何层自检仅覆盖二维图型)",
8810
+ rule: "threed-schema-only"
8811
+ });
8812
+ return {
8813
+ issues,
8814
+ spec: fixed
8815
+ };
8816
+ }
8381
8817
  const { warnings } = validateOptions(fixed);
8382
8818
  warnings.forEach((w) => issues.push({
8383
8819
  level: "error",
@@ -8405,7 +8841,7 @@ function lintChartSpec(spec, opts = {}) {
8405
8841
  if (fixed.type === "bar" && Array.isArray(fixed.series) && fixed.series.length === 1 && Array.isArray(fixed.labels)) {
8406
8842
  const width = opts.width ?? DEFAULT_SIZE.width;
8407
8843
  const labels = fixed.labels.map(String);
8408
- const maxLabelWidth = Math.max(...labels.map((l) => estimateTextWidth(l, 12)), 0);
8844
+ const maxLabelWidth = maxOf(labels.map((l) => estimateTextWidth(l, 12)), 0);
8409
8845
  const slots = Math.max(1, (width - 80) / Math.max(1, labels.length));
8410
8846
  if (labels.length >= 7 && maxLabelWidth > slots * .9) {
8411
8847
  fixed.type = "horizontal-bar";
@@ -8451,4 +8887,4 @@ function lintChartSpec(spec, opts = {}) {
8451
8887
  };
8452
8888
  }
8453
8889
  //#endregion
8454
- export { INTERACTION as $, squarifyTreemap as A, calendarHitTest as B, chordHitTest as C, computeSunburstGeometry as D, boxplotHitTest as E, matrixFieldExtent as F, getPadding as G, getHeatmapCategories as H, scatterGroupColors as I, resolveTickExtendedRange as J, getTheme as K, scatterPointPositions as L, computeFacetGrids as M, computeMatrixCells as N, sunburstValue as O, computePieMaxRadius as P, DEFAULT_I18N_ZH as Q, getLegendBounds as R, arcHitTest as S, vennHitTest as T, xToCategoryIndex as U, funnelStepColor as V, createAnimation as W, CHART_PALETTES as X, updateAnimation as Y, resolveChartPalette as Z, getSliderGeometry as _, SPEC_RULES as a, zoomToSlice as b, inferColumns as c, parseNumeric as d, applyWheelZoom as et, chartOptionsSchema as f, getDataZoomConfig as g, createSvgRecorder as h, formatExamples as i, buildPieSlices as j, computeBins as k, looksLikeTime as l, renderChart as m, buildChartPrompt as n, resolveCursor as nt, detectIntent as o, validateOptions as p, isMissingValue as q, SPEC_EXAMPLES as r, generateChartSpec as s, lintChartSpec as t, pickTooltipRows as tt, parseDataTable as u, windowToX as v, sankeyHitTest as w, ganttHitTest as x, xToPercent as y, getToolboxBounds as z };
8890
+ export { isMissingValue as $, vennHitTest as A, doughnutInnerRadius as B, windowToX as C, arcHitTest as D, ganttHitTest as E, squarifyTreemap as F, getToolboxBounds as G, scatterGroupColors as H, buildPieSlices as I, getHeatmapCategories as J, calendarHitTest as K, computeFacetGrids as L, computeSunburstGeometry as M, sunburstValue as N, chordHitTest as O, computeBins as P, getTheme as Q, computeMatrixCells as R, getSliderGeometry as S, zoomToSlice as T, scatterPointPositions as U, matrixFieldExtent as V, getLegendBounds as W, createAnimation as X, xToCategoryIndex as Y, getPadding as Z, chartOptionsSchema as _, formatExamples as a, INTERACTION as at, createSvgRecorder as b, detectIntent as c, resolveCursor as ct, planThreed as d, resolveTickExtendedRange as et, wantsThreed as f, parseNumeric as g, parseDataTable as h, SPEC_EXAMPLES_3D as i, minOf as it, boxplotHitTest as j, sankeyHitTest as k, generateChartSpec as l, looksLikeTime as m, buildChartPrompt as n, waterfallSteps as nt, formatExamples3d as o, applyWheelZoom as ot, inferColumns as p, funnelStepColor as q, SPEC_EXAMPLES as r, maxOf as rt, SPEC_RULES as s, pickTooltipRows as st, lintChartSpec as t, updateAnimation as tt, SPEC_RULES_3D as u, validateOptions as v, xToPercent as w, getDataZoomConfig as x, renderChart as y, computePieMaxRadius as z };