@wil-works/evoke-charts 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -67,7 +67,40 @@ function pickTooltipRows(params) {
67
67
  });
68
68
  }
69
69
  //#endregion
70
+ //#region src/extent.js
71
+ function minOf(values, ...extras) {
72
+ let min = Infinity;
73
+ for (let i = 0; i < values.length; i++) {
74
+ const v = values[i];
75
+ if (Number.isNaN(v)) return NaN;
76
+ if (v < min) min = v;
77
+ }
78
+ for (let i = 0; i < extras.length; i++) {
79
+ const v = extras[i];
80
+ if (Number.isNaN(v)) return NaN;
81
+ if (v < min) min = v;
82
+ }
83
+ return min;
84
+ }
85
+ function maxOf(values, ...extras) {
86
+ let max = -Infinity;
87
+ for (let i = 0; i < values.length; i++) {
88
+ const v = values[i];
89
+ if (Number.isNaN(v)) return NaN;
90
+ if (v > max) max = v;
91
+ }
92
+ for (let i = 0; i < extras.length; i++) {
93
+ const v = extras[i];
94
+ if (Number.isNaN(v)) return NaN;
95
+ if (v > max) max = v;
96
+ }
97
+ return max;
98
+ }
99
+ //#endregion
70
100
  //#region src/types.js
101
+ var UP_COLOR = "#dc2626";
102
+ var DOWN_COLOR = "#16a34a";
103
+ var VOLUME_SERIES_NAME = "成交量";
71
104
  var DEFAULT_I18N_ZH = {
72
105
  tooltip: {
73
106
  open: "开",
@@ -434,6 +467,89 @@ function mixColor(color, ratio, target) {
434
467
  function isMissingValue(v) {
435
468
  return v === null || v === void 0 || Number.isNaN(v);
436
469
  }
470
+ function truncateLabel(canvasCtx, label, maxWidth) {
471
+ if (canvasCtx.measureText(label).width <= maxWidth) return label;
472
+ let lo = 0, hi = label.length;
473
+ while (lo < hi) {
474
+ const mid = Math.ceil((lo + hi) / 2);
475
+ const truncated = label.slice(0, mid) + "…";
476
+ if (canvasCtx.measureText(truncated).width > maxWidth) hi = mid - 1;
477
+ else lo = mid;
478
+ }
479
+ return label.slice(0, lo) + "…";
480
+ }
481
+ function waterfallSteps(deltas, totalIndices) {
482
+ const totalIdx = new Set(totalIndices || []);
483
+ const steps = [];
484
+ let cumulative = 0;
485
+ (deltas || []).forEach((raw, i) => {
486
+ const missing = isMissingValue(raw);
487
+ const v = missing ? 0 : raw;
488
+ if (totalIdx.has(i)) {
489
+ steps.push({
490
+ value: v,
491
+ from: 0,
492
+ to: v,
493
+ isTotal: true,
494
+ missing
495
+ });
496
+ cumulative = v;
497
+ } else {
498
+ steps.push({
499
+ value: v,
500
+ from: cumulative,
501
+ to: cumulative + v,
502
+ isTotal: false,
503
+ missing
504
+ });
505
+ cumulative += v;
506
+ }
507
+ });
508
+ return steps;
509
+ }
510
+ function minMaxDecimatePoints(points, columns) {
511
+ const out = [];
512
+ const per = points.length / columns;
513
+ let pendingGap = false;
514
+ for (let b = 0; b < columns; b++) {
515
+ const start = Math.floor(b * per);
516
+ const end = Math.min(points.length, Math.max(start + 1, Math.floor((b + 1) * per)));
517
+ let first = -1;
518
+ let last = -1;
519
+ let minI = -1;
520
+ let maxI = -1;
521
+ for (let i = start; i < end; i++) {
522
+ const p = points[i];
523
+ if (!p) continue;
524
+ if (first < 0) first = i;
525
+ last = i;
526
+ if (minI < 0 || p[1] < points[minI][1]) minI = i;
527
+ if (maxI < 0 || p[1] > points[maxI][1]) maxI = i;
528
+ }
529
+ if (first < 0) {
530
+ if (out.length > 0) pendingGap = true;
531
+ continue;
532
+ }
533
+ if (pendingGap) {
534
+ out.push({
535
+ p: null,
536
+ i: first
537
+ });
538
+ pendingGap = false;
539
+ }
540
+ const picks = [.../* @__PURE__ */ new Set([
541
+ first,
542
+ minI,
543
+ maxI,
544
+ last
545
+ ])].sort((a, b) => a - b);
546
+ for (const idx of picks) out.push({
547
+ p: points[idx],
548
+ i: idx
549
+ });
550
+ }
551
+ return out;
552
+ }
437
553
  function resolveConnectNulls(options, series) {
438
554
  return series?.connectNulls ?? options.connectNulls ?? false;
439
555
  }
@@ -710,7 +826,7 @@ function getPadding(options, containerWidth = 600) {
710
826
  if (options.type === "gantt") {
711
827
  const pad2 = resolveUserPadding(options);
712
828
  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;
829
+ const labelW = names2.length ? Math.max(80, Math.min(180, Math.round(maxOf(names2.map((n) => estimateTextWidth(n, 12))) + 16))) : 100;
714
830
  const top2 = (pad2.top ?? 18) + titleBlockHeight(options) + (legendPosition === "top" ? LEGEND_BAND + legendRowsExtra : 0);
715
831
  const bottom2 = (pad2.bottom ?? 46) + (legendPosition === "bottom" ? LEGEND_BAND + legendRowsExtra : 0);
716
832
  const left2 = pad2.left ?? labelW;
@@ -914,22 +1030,14 @@ function calculateRange(options, hiddenSeries, useRightAxis) {
914
1030
  const seriesList = (options.series || []).filter((s) => !hiddenSeries.has(s.name) && (useRightAxis ? s.yAxis === "right" : s.yAxis !== "right"));
915
1031
  if (options.type === "waterfall") {
916
1032
  const wf = options.waterfall || {};
917
- const totalIdx = new Set(wf.totalIndices || []);
918
1033
  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);
1034
+ for (const step of waterfallSteps(deltas, wf.totalIndices)) {
1035
+ const points = step.isTotal ? [step.to] : [step.from, step.to];
1036
+ for (const p of points) {
1037
+ if (p > max) max = p;
1038
+ if (p < min) min = p;
931
1039
  }
932
- });
1040
+ }
933
1041
  } else if (options.type === "boxplot") (options.boxData || []).forEach((b) => {
934
1042
  if (b.group && hiddenSeries.has(b.group)) return;
935
1043
  if (b.max > max) max = b.max;
@@ -1102,22 +1210,11 @@ function updateAnimation(state) {
1102
1210
  if (state.startTime === 0) state.startTime = Date.now();
1103
1211
  const elapsed = Date.now() - state.startTime;
1104
1212
  const t = Math.min(elapsed / state.duration, 1);
1105
- state.progress = easings[state.easing](t);
1213
+ state.progress = (easings[state.easing] || easings.easeOut)(t);
1106
1214
  return t < 1;
1107
1215
  }
1108
1216
  //#endregion
1109
1217
  //#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
1218
  /**
1122
1219
  * 抽稀刻度:保留首末两端覆盖,步长取整数倍使数量 ≤ maxCount。
1123
1220
  * nice 算法的 step 候选会让实际刻度数超出请求数(如 ticks:3 得到 5 档),
@@ -1295,8 +1392,8 @@ function renderTimeXAxis(ctx) {
1295
1392
  const xAxisConfig = options.xAxis || {};
1296
1393
  const valid = parseTimeLabels(options.labels || []).filter((t) => !Number.isNaN(t));
1297
1394
  if (valid.length === 0) return;
1298
- const min = xAxisConfig.min ?? Math.min(...valid);
1299
- const max = xAxisConfig.max ?? Math.max(...valid);
1395
+ const min = xAxisConfig.min ?? minOf(valid);
1396
+ const max = xAxisConfig.max ?? maxOf(valid);
1300
1397
  const span = max - min || 1;
1301
1398
  const { values: tickTs, unit } = calculateTimeTicks(min, max, Math.max(3, Math.min(8, Math.floor(plotArea.width / 90))));
1302
1399
  const toX = (ts) => plotArea.x + (ts - min) / span * plotArea.width;
@@ -1336,8 +1433,8 @@ function timeToX(options, plotArea, index) {
1336
1433
  const timestamps = parseTimeLabels(options.labels || []);
1337
1434
  const valid = timestamps.filter((t) => !Number.isNaN(t));
1338
1435
  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;
1436
+ const min = options.xAxis?.min ?? minOf(valid);
1437
+ const span = (options.xAxis?.max ?? maxOf(valid)) - min || 1;
1341
1438
  const ts = timestamps[index];
1342
1439
  if (Number.isNaN(ts)) return plotArea.x + index / ((options.labels?.length || 1) - 1 || 1) * plotArea.width;
1343
1440
  return plotArea.x + (ts - min) / span * plotArea.width;
@@ -1822,7 +1919,7 @@ function funnelStepColor(data, index, theme) {
1822
1919
  function computeFunnelGeometry(plotArea, options, theme) {
1823
1920
  const all = options.funnelData || [];
1824
1921
  const drawn = options.pyramid === true ? [...all].reverse() : all;
1825
- const maxValue = drawn.length ? Math.max(...drawn.map((d) => d.value)) : 0;
1922
+ const maxValue = drawn.length ? maxOf(drawn.map((d) => d.value)) : 0;
1826
1923
  const maxWidth = plotArea.width * .8;
1827
1924
  const centerX = plotArea.x + plotArea.width / 2;
1828
1925
  const topY = plotArea.y + 8;
@@ -1867,7 +1964,7 @@ function funnelPercent(value, maxValue) {
1867
1964
  return `${Math.round(value / maxValue * 1e3) / 10}%`;
1868
1965
  }
1869
1966
  function renderFunnelChart(ctx) {
1870
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hiddenSeries } = ctx;
1967
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1, hiddenSeries } = ctx;
1871
1968
  const funnelData = (options.funnelData || []).filter((d) => !hiddenSeries.has(d.label || ""));
1872
1969
  if (funnelData.length === 0) return;
1873
1970
  const geo = computeFunnelGeometry(plotArea, {
@@ -1889,7 +1986,7 @@ function renderFunnelChart(ctx) {
1889
1986
  canvasCtx.lineTo(centerX + bottomW / 2, y + stepHeight);
1890
1987
  canvasCtx.lineTo(centerX - bottomW / 2, y + stepHeight);
1891
1988
  canvasCtx.closePath();
1892
- canvasCtx.fillStyle = i === hoverIndex ? mixColor(step.color, .08, "#000000") : step.color;
1989
+ canvasCtx.fillStyle = i === hoverIndex ? mixColor(step.color, .08 * hoverAnimProgress, "#000000") : step.color;
1893
1990
  canvasCtx.fill();
1894
1991
  canvasCtx.strokeStyle = theme.backgroundColor;
1895
1992
  canvasCtx.lineWidth = 2;
@@ -1966,11 +2063,12 @@ function renderFunnelChart(ctx) {
1966
2063
  */
1967
2064
  function renderGaugeChart(ctx) {
1968
2065
  const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex } = ctx;
1969
- const gauge = options.gauge;
2066
+ const rawGauge = options.gauge;
2067
+ const gauge = typeof rawGauge === "number" ? { value: rawGauge } : rawGauge;
1970
2068
  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));
2069
+ const min = gauge.min ?? 0;
2070
+ const max = gauge.max ?? 100;
2071
+ const value = typeof gauge.value === "number" && Number.isFinite(gauge.value) ? Math.max(min, Math.min(max, gauge.value)) : min;
1974
2072
  const toRad = (deg) => -deg * Math.PI / 180;
1975
2073
  const startAngle = toRad(gauge.startAngle ?? 220);
1976
2074
  let normalizedEnd = toRad(gauge.endAngle ?? -40);
@@ -1991,7 +2089,7 @@ function renderGaugeChart(ctx) {
1991
2089
  canvasCtx.lineCap = gauge.cornerRadius === "butt" ? "butt" : "round";
1992
2090
  canvasCtx.stroke();
1993
2091
  canvasCtx.restore();
1994
- const animatedRatio = (value - min) / (max - min) * progress;
2092
+ const animatedRatio = (max - min !== 0 ? (value - min) / (max - min) : 0) * progress;
1995
2093
  const valueAngle = startAngle + (normalizedEnd - startAngle) * animatedRatio;
1996
2094
  let progressColor = theme.colors[0];
1997
2095
  if (Array.isArray(gauge.color)) {
@@ -2127,7 +2225,9 @@ function calendarPad(n) {
2127
2225
  }
2128
2226
  /**
2129
2227
  * 日历热力布局:start/end 缺省取数据极值并对齐周边界(weekStart 默认周一);
2130
- * 数值等宽分 4 档色阶(0/缺测为空档);cellSize 按可用空间自适应。
2228
+ * 数值等宽分 4 档色阶(0/缺测为空档);granularity: 'day'(列=周 行=星期,
2229
+ * 默认)/ 'week' / 'month'(聚合为单行周合计 / 月合计,求和口径);
2230
+ * 格子为矩形,横向铺满绘图区(宽高独立自适应)。
2131
2231
  */
2132
2232
  function computeCalendarLayout(plotArea, options, theme) {
2133
2233
  const data = options.calendarData || [];
@@ -2138,6 +2238,8 @@ function computeCalendarLayout(plotArea, options, theme) {
2138
2238
  const t = Date.parse(v);
2139
2239
  return Number.isNaN(t) ? null : calendarNoon(t);
2140
2240
  };
2241
+ const gran = cal.granularity === "week" || cal.granularity === "month" ? cal.granularity : "day";
2242
+ const weekStart = cal.weekStart ?? 1;
2141
2243
  const valueMap = /* @__PURE__ */ new Map();
2142
2244
  let minTs = Infinity;
2143
2245
  let maxTs = -Infinity;
@@ -2152,67 +2254,128 @@ function computeCalendarLayout(plotArea, options, theme) {
2152
2254
  let startTs = toTs(cal.start) ?? minTs;
2153
2255
  let endTs = toTs(cal.end) ?? maxTs;
2154
2256
  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
2257
  const ramps = cal.colors || (!isLightColor(theme.backgroundColor) ? CALENDAR_RAMP_DARK : CALENDAR_RAMP_LIGHT);
2258
+ const todayTs = calendarNoon(cal.today !== void 0 ? toTs(cal.today) ?? Date.now() : Date.now());
2259
+ const buckets = [];
2260
+ const pushBucket = (label, tsList, col, row) => {
2261
+ let value = null;
2262
+ for (const ts of tsList) if (valueMap.has(ts)) {
2263
+ const v = valueMap.get(ts);
2264
+ value = typeof value === "number" ? value + v : v;
2265
+ }
2266
+ buckets.push({
2267
+ label,
2268
+ col,
2269
+ row,
2270
+ value: typeof value === "number" ? value : null,
2271
+ today: tsList.includes(todayTs)
2272
+ });
2273
+ };
2274
+ if (gran === "day") {
2275
+ const shift = (new Date(startTs).getDay() - weekStart + 7) % 7;
2276
+ const firstWeek = startTs - shift * CALENDAR_DAY;
2277
+ const tailShift = (new Date(endTs).getDay() - weekStart + 7) % 7;
2278
+ const lastWeek = endTs + (6 - tailShift) * CALENDAR_DAY;
2279
+ const cols = Math.round((lastWeek - firstWeek) / CALENDAR_DAY / 7);
2280
+ if (cols <= 0) return null;
2281
+ let prevMonth = -1;
2282
+ const monthLabels = [];
2283
+ for (let col = 0; col < cols; col++) {
2284
+ const firstDay = new Date(firstWeek + col * 7 * CALENDAR_DAY);
2285
+ if (firstDay.getMonth() !== prevMonth) {
2286
+ if (col > 0 || firstDay.getDate() <= 7) monthLabels.push({
2287
+ label: `${firstDay.getMonth() + 1}月`,
2288
+ col
2289
+ });
2290
+ prevMonth = firstDay.getMonth();
2291
+ }
2292
+ for (let row = 0; row < 7; row++) {
2293
+ const ts = firstWeek + (col * 7 + row) * CALENDAR_DAY;
2294
+ if (ts < startTs || ts > endTs) continue;
2295
+ const d = new Date(ts);
2296
+ pushBucket(`${d.getFullYear()}-${calendarPad(d.getMonth() + 1)}-${calendarPad(d.getDate())}`, [ts], col, row);
2297
+ }
2298
+ }
2299
+ var bucketLabels = monthLabels;
2300
+ var rows = 7;
2301
+ } else {
2302
+ const order = [];
2303
+ const sums = /* @__PURE__ */ new Map();
2304
+ for (let ts = startTs; ts <= endTs; ts += CALENDAR_DAY) {
2305
+ const d = new Date(ts);
2306
+ let key;
2307
+ let label;
2308
+ if (gran === "week") {
2309
+ const shift2 = (d.getDay() - weekStart + 7) % 7;
2310
+ const weekTs = ts - shift2 * CALENDAR_DAY;
2311
+ key = `w${weekTs}`;
2312
+ const wd = new Date(weekTs);
2313
+ label = `${wd.getMonth() + 1}月${wd.getDate()}日周`;
2314
+ } else {
2315
+ key = `m${d.getFullYear()}-${d.getMonth()}`;
2316
+ label = `${d.getFullYear()}-${calendarPad(d.getMonth() + 1)}`;
2317
+ }
2318
+ if (!sums.has(key)) {
2319
+ sums.set(key, null);
2320
+ order.push({
2321
+ key,
2322
+ label
2323
+ });
2324
+ }
2325
+ const v = valueMap.get(ts);
2326
+ if (v !== void 0) {
2327
+ const prev = sums.get(key);
2328
+ sums.set(key, typeof prev === "number" ? prev + v : v);
2329
+ }
2330
+ }
2331
+ order.forEach((b, i) => {
2332
+ const value = sums.get(b.key);
2333
+ buckets.push({
2334
+ label: b.label,
2335
+ col: i,
2336
+ row: 0,
2337
+ value: typeof value === "number" ? value : null,
2338
+ today: false
2339
+ });
2340
+ });
2341
+ bucketLabels = gran === "month" ? order.map((b, i) => ({
2342
+ label: b.label,
2343
+ col: i
2344
+ })) : order.map((b, i) => ({
2345
+ label: b.label,
2346
+ col: i,
2347
+ month: parseInt(b.label, 10)
2348
+ })).filter((b, i, arr) => i === 0 || b.month !== arr[i - 1].month);
2349
+ rows = 1;
2350
+ }
2163
2351
  let vMax = 0;
2164
- valueMap.forEach((v) => {
2165
- if (typeof v === "number" && v > vMax) vMax = v;
2352
+ buckets.forEach((b) => {
2353
+ if (typeof b.value === "number" && b.value > vMax) vMax = b.value;
2166
2354
  });
2167
2355
  const levelOf = (v) => {
2168
2356
  if (typeof v !== "number" || !(v > 0) || !(vMax > 0)) return 0;
2169
2357
  return Math.max(1, Math.min(4, Math.ceil(v / (vMax / 4))));
2170
2358
  };
2171
2359
  const labelTop = 18;
2172
- const leftBand = 28;
2360
+ const leftBand = gran === "day" ? 28 : 6;
2173
2361
  const gap = Math.max(1, Math.min(8, cal.cellGap ?? 3));
2174
2362
  const scaleH = cal.showScale === false ? 0 : 22;
2363
+ const cols = Math.max(1, maxOf(buckets.map((b) => b.col + 1)));
2175
2364
  const availW = plotArea.width - leftBand;
2176
2365
  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);
2366
+ const cellW = Math.max(6, (availW - gap * (cols - 1)) / cols);
2367
+ const cellH = Math.max(6, (availH - gap * (rows - 1)) / rows);
2368
+ const originX = plotArea.x + leftBand;
2180
2369
  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
- }
2370
+ const cells = buckets.map((b, i) => ({
2371
+ ...b,
2372
+ x: originX + b.col * (cellW + gap),
2373
+ y: originY + b.row * (cellH + gap),
2374
+ w: cellW,
2375
+ h: cellH,
2376
+ color: ramps[levelOf(b.value)],
2377
+ index: i
2378
+ }));
2216
2379
  const weekdayLabels = cal.weekdayLabels || [
2217
2380
  "日",
2218
2381
  "一",
@@ -2223,20 +2386,26 @@ function computeCalendarLayout(plotArea, options, theme) {
2223
2386
  "六"
2224
2387
  ];
2225
2388
  const showRows = [];
2226
- for (let row = 0; row < 7; row++) {
2389
+ if (gran === "day") for (let row = 0; row < 7; row++) {
2227
2390
  const name = weekdayLabels[(weekStart + row) % 7];
2228
2391
  if (name === "一" || name === "三" || name === "五" || cal.showAllWeekdays === true) showRows.push({
2229
2392
  row,
2230
2393
  name,
2231
- y: originY + row * (cell + gap) + cell / 2
2394
+ y: originY + row * (cellH + gap) + cellH / 2
2232
2395
  });
2233
2396
  }
2234
2397
  return {
2235
2398
  cells,
2236
2399
  cols,
2237
- cell,
2400
+ rows,
2401
+ cellW,
2402
+ cellH,
2238
2403
  gap,
2239
- monthLabels,
2404
+ granularity: gran,
2405
+ monthLabels: bucketLabels.map((m) => ({
2406
+ label: m.label,
2407
+ x: originX + m.col * (cellW + gap)
2408
+ })),
2240
2409
  showRows,
2241
2410
  ramps,
2242
2411
  originX,
@@ -2251,17 +2420,17 @@ function renderCalendarHeatmapChart(ctx) {
2251
2420
  const cal = options.calendar || {};
2252
2421
  layout.cells.forEach((c, i) => {
2253
2422
  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;
2423
+ const w = c.w * colT;
2424
+ const h = c.h * colT;
2256
2425
  const isHover = i === hoverIndex;
2257
2426
  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;
2427
+ roundRect(canvasCtx, c.x + (c.w - w) / 2, c.y + (c.h - h) / 2, w, h, Math.min(3, w / 4, h / 4));
2428
+ canvasCtx.fillStyle = isHover ? mixColor(c.color, .18 * hoverAnimProgressOf(ctx), "#000000") : c.color;
2260
2429
  canvasCtx.fill();
2261
2430
  if (c.today) {
2262
2431
  canvasCtx.strokeStyle = theme.colors[0];
2263
2432
  canvasCtx.lineWidth = 1.5;
2264
- roundRect(canvasCtx, c.x - 1.5, c.y - 1.5, c.size + 3, c.size + 3, 4);
2433
+ roundRect(canvasCtx, c.x - 1.5, c.y - 1.5, c.w + 3, c.h + 3, 4);
2265
2434
  canvasCtx.stroke();
2266
2435
  }
2267
2436
  canvasCtx.restore();
@@ -2296,10 +2465,13 @@ function renderCalendarHeatmapChart(ctx) {
2296
2465
  canvasCtx.restore();
2297
2466
  }
2298
2467
  }
2468
+ function hoverAnimProgressOf(ctx) {
2469
+ return ctx.hoverAnimProgress === void 0 ? 1 : ctx.hoverAnimProgress;
2470
+ }
2299
2471
  function calendarHitTest(canvasX, canvasY, plotArea, options, theme) {
2300
2472
  const layout = computeCalendarLayout(plotArea, options, theme);
2301
2473
  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 {
2474
+ 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
2475
  index: c.index,
2304
2476
  params: {
2305
2477
  seriesName: c.label,
@@ -2331,15 +2503,16 @@ function getHeatmapCategories(options) {
2331
2503
  };
2332
2504
  }
2333
2505
  function renderHeatmapChart(ctx) {
2334
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex } = ctx;
2506
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1 } = ctx;
2507
+ const hovT = hoverAnimProgress;
2335
2508
  const heatmapData = options.heatmapData || [];
2336
2509
  if (heatmapData.length === 0) return;
2337
2510
  const heatmapConfig = options.heatmap || {};
2338
2511
  const colorScale = heatmapConfig.colorScale || [...CHART_COLORS.heatmapScale];
2339
2512
  const { xCategories, yCategories } = getHeatmapCategories(options);
2340
2513
  const values = heatmapData.map((d) => d.value);
2341
- const minValue = Math.min(...values);
2342
- const maxValue = Math.max(...values);
2514
+ const minValue = minOf(values);
2515
+ const maxValue = maxOf(values);
2343
2516
  const valueRange = maxValue - minValue || 1;
2344
2517
  const cellWidth = plotArea.width / xCategories.length;
2345
2518
  const cellHeight = plotArea.height / yCategories.length;
@@ -2362,7 +2535,8 @@ function renderHeatmapChart(ctx) {
2362
2535
  const padding = 1;
2363
2536
  roundRect(canvasCtx, x + padding, y + padding, cellWidth - 2, cellHeight - 2, 3);
2364
2537
  canvasCtx.fill();
2365
- if (isHover) {
2538
+ if (isHover && hovT > .01) {
2539
+ canvasCtx.globalAlpha = hovT;
2366
2540
  canvasCtx.strokeStyle = theme.textColor;
2367
2541
  canvasCtx.lineWidth = 2;
2368
2542
  canvasCtx.stroke();
@@ -2446,10 +2620,10 @@ function candleVolumeLayout(plotArea, options, hiddenSeries) {
2446
2620
  * 量带绘制(K 线 / 折线分时共用):flags 为每根的涨跌布尔(决定颜色),
2447
2621
  * baseY 为量带底、bandTop 为量带顶;不画轴(量纲从属),最高柱顶标 compact max。
2448
2622
  */
2449
- function renderVolumeBand(ctx, volumeData, flags, baseY, bandTop, barWidth, slotWidth, plotWidth, plotX, theme, progress, hoverIndex, upColor = "#dc2626", downColor = "#16a34a") {
2623
+ function renderVolumeBand(ctx, volumeData, flags, baseY, bandTop, barWidth, slotWidth, plotWidth, plotX, theme, progress, hoverIndex, upColor = UP_COLOR, downColor = DOWN_COLOR) {
2450
2624
  const { ctx: canvasCtx, hiddenSeries } = ctx;
2451
2625
  if (hiddenSeries && hiddenSeries.has("成交量")) return;
2452
- const volMax = Math.max(...volumeData.filter((v) => Number.isFinite(v)), 1);
2626
+ const volMax = maxOf(volumeData.filter((v) => Number.isFinite(v)), 1);
2453
2627
  const bandHeight = Math.max(4, baseY - bandTop);
2454
2628
  volumeData.forEach((vol, i) => {
2455
2629
  if (!Number.isFinite(vol)) return;
@@ -2484,7 +2658,8 @@ function renderVolumeBand(ctx, volumeData, flags, baseY, bandTop, barWidth, slot
2484
2658
  * 带不画轴(量纲从属)。图例「成交量」点选后量带隐去、K 线回铺全高。
2485
2659
  */
2486
2660
  function renderCandleChart(ctx, yRange) {
2487
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hiddenSeries } = ctx;
2661
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1, hiddenSeries } = ctx;
2662
+ const hovT = hoverAnimProgress;
2488
2663
  const candleData = options.candleData || [];
2489
2664
  if (candleData.length === 0) return;
2490
2665
  const upColor = options.candleUpColor || "#dc2626";
@@ -2513,9 +2688,10 @@ function renderCandleChart(ctx, yRange) {
2513
2688
  const closeY = yFor(candle.close);
2514
2689
  const bodyTop = Math.min(openY, closeY);
2515
2690
  const bodyHeight = Math.max(1, Math.abs(closeY - openY));
2516
- canvasCtx.fillStyle = isHover ? color + "dd" : color;
2691
+ canvasCtx.fillStyle = isHover ? mixColor(color, .12 * hovT, "#000000") : color;
2517
2692
  canvasCtx.fillRect(x - candleWidth / 2, bodyTop, candleWidth, bodyHeight);
2518
- if (isHover) {
2693
+ if (isHover && hovT > .01) {
2694
+ canvasCtx.globalAlpha = hovT;
2519
2695
  canvasCtx.strokeStyle = theme.textColor;
2520
2696
  canvasCtx.lineWidth = 1.5;
2521
2697
  canvasCtx.strokeRect(x - candleWidth / 2, bodyTop, candleWidth, bodyHeight);
@@ -2656,10 +2832,10 @@ function collectLegendItems(options, theme, hiddenSeries) {
2656
2832
  if (options.type === "candle") {
2657
2833
  const items = [];
2658
2834
  if ((options.volumeData || []).length > 0) items.push({
2659
- name: "成交量",
2660
- label: fmt("成交量"),
2835
+ name: VOLUME_SERIES_NAME,
2836
+ label: fmt(VOLUME_SERIES_NAME),
2661
2837
  color: theme.textColorSecondary,
2662
- hidden: hiddenSeries.has("成交量")
2838
+ hidden: hiddenSeries.has(VOLUME_SERIES_NAME)
2663
2839
  });
2664
2840
  if (Array.isArray(options.candleMa)) options.candleMa.forEach((days, i) => {
2665
2841
  const name = `MA${days}`;
@@ -2679,10 +2855,10 @@ function collectLegendItems(options, theme, hiddenSeries) {
2679
2855
  hidden: hiddenSeries.has(s.name)
2680
2856
  }));
2681
2857
  if ((options.type === "line" || options.type === "area") && (options.volumeData || []).length > 0) items.push({
2682
- name: "成交量",
2683
- label: fmt("成交量"),
2858
+ name: VOLUME_SERIES_NAME,
2859
+ label: fmt(VOLUME_SERIES_NAME),
2684
2860
  color: theme.textColorSecondary,
2685
- hidden: hiddenSeries.has("成交量")
2861
+ hidden: hiddenSeries.has(VOLUME_SERIES_NAME)
2686
2862
  });
2687
2863
  return items;
2688
2864
  }
@@ -2746,12 +2922,7 @@ function computeLegendLayout(ctx, options, plotArea, containerWidth, containerHe
2746
2922
  };
2747
2923
  }
2748
2924
  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;
2925
+ return estimateTextWidth(text, 12);
2755
2926
  }
2756
2927
  function renderLegend(ctx) {
2757
2928
  const { ctx: canvasCtx, theme, options, hiddenSeries, width, height, plotArea } = ctx;
@@ -2929,7 +3100,7 @@ function computePieMaxRadius(pieData, options, plotArea) {
2929
3100
  const pct = (d.value / total * 100).toFixed(1);
2930
3101
  return percentMode ? `${pct}%` : `${d.label || ""} (${pct}%)`;
2931
3102
  });
2932
- const horizNeed = 31 + Math.max(40, ...texts.map((t) => estimateTextWidth(t, 12))) + 10;
3103
+ const horizNeed = 31 + Math.max(40, maxOf(texts.map((t) => estimateTextWidth(t, 12)))) + 10;
2933
3104
  return Math.max(30, Math.min(Math.min(plotArea.width, plotArea.height) / 2 - 20, plotArea.width / 2 - horizNeed));
2934
3105
  }
2935
3106
  function alphaHex(opacity) {
@@ -2993,14 +3164,22 @@ function renderLineChart(ctx, yRange, side = "left") {
2993
3164
  points.push([x, y]);
2994
3165
  });
2995
3166
  allPoints.push(points.map((p) => p || [NaN, NaN]));
3167
+ let drawPoints = points;
3168
+ let drawIdxs = points.map((_, i) => i);
3169
+ const columnBudget = Math.max(2, Math.floor(plotArea.width));
3170
+ if (points.length > columnBudget * 2) {
3171
+ const decimated = minMaxDecimatePoints(points, columnBudget);
3172
+ drawPoints = decimated.map((e) => e.p);
3173
+ drawIdxs = decimated.map((e) => e.i);
3174
+ }
2996
3175
  const connect = resolveConnectNulls(options, series);
2997
3176
  const segments = [];
2998
3177
  let currentPts = [];
2999
3178
  let currentIdxs = [];
3000
- points.forEach((p, i) => {
3179
+ drawPoints.forEach((p, k) => {
3001
3180
  if (p) {
3002
3181
  currentPts.push(p);
3003
- currentIdxs.push(i);
3182
+ currentIdxs.push(drawIdxs[k]);
3004
3183
  } else if (currentPts.length > 0 && !connect) {
3005
3184
  segments.push({
3006
3185
  pts: currentPts,
@@ -3104,9 +3283,9 @@ function renderLineChart(ctx, yRange, side = "left") {
3104
3283
  if (!series.area && options.type !== "area") canvasCtx.restore();
3105
3284
  if (series.showSymbol === true && series.symbol !== "none") {
3106
3285
  const baseR = series.symbolSize ?? 4;
3107
- points.forEach((p, i) => {
3286
+ drawPoints.forEach((p, k) => {
3108
3287
  if (!p) return;
3109
- const isHover = i === hoverIndex;
3288
+ const isHover = drawIdxs[k] === hoverIndex;
3110
3289
  const radius = isHover ? baseR + 2 : baseR;
3111
3290
  canvasCtx.beginPath();
3112
3291
  drawSymbol(canvasCtx, series.symbol, p[0], p[1], radius);
@@ -3141,7 +3320,7 @@ function renderLineChart(ctx, yRange, side = "left") {
3141
3320
  canvasCtx.textBaseline = "bottom";
3142
3321
  const rects = [];
3143
3322
  const texts = [];
3144
- points.forEach((p, i) => {
3323
+ drawPoints.forEach((p, k) => {
3145
3324
  if (!p) {
3146
3325
  rects.push({
3147
3326
  x: 0,
@@ -3152,7 +3331,7 @@ function renderLineChart(ctx, yRange, side = "left") {
3152
3331
  texts.push("");
3153
3332
  return;
3154
3333
  }
3155
- const text = valueFormatter(series.data[i]);
3334
+ const text = valueFormatter(series.data[drawIdxs[k]]);
3156
3335
  const w = canvasCtx.measureText(text).width;
3157
3336
  rects.push({
3158
3337
  x: p[0] - w / 2 - 2,
@@ -3163,8 +3342,8 @@ function renderLineChart(ctx, yRange, side = "left") {
3163
3342
  texts.push(text);
3164
3343
  });
3165
3344
  const visible = layoutLabelsAvoidOverlap(rects);
3166
- points.forEach((p, i) => {
3167
- if (p && visible[i]) canvasCtx.fillText(texts[i], p[0], p[1] - 8);
3345
+ drawPoints.forEach((p, k) => {
3346
+ if (p && visible[k]) canvasCtx.fillText(texts[k], p[0], p[1] - 8);
3168
3347
  });
3169
3348
  canvasCtx.restore();
3170
3349
  }
@@ -3173,7 +3352,8 @@ function renderLineChart(ctx, yRange, side = "left") {
3173
3352
  return allPoints;
3174
3353
  }
3175
3354
  function renderBarChart(ctx, yRange, stacked = false) {
3176
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hiddenSeries, valueFormatter } = ctx;
3355
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1, hiddenSeries, valueFormatter } = ctx;
3356
+ const hovT = hoverAnimProgress;
3177
3357
  const visibleSeries = (options.series || []).filter((s) => !hiddenSeries.has(s.name));
3178
3358
  if (visibleSeries.length === 0) return;
3179
3359
  const labels = options.labels || [];
@@ -3245,10 +3425,11 @@ function renderBarChart(ctx, yRange, stacked = false) {
3245
3425
  canvasCtx.beginPath();
3246
3426
  canvasCtx.rect(barX, seg.y, barWidth, seg.height);
3247
3427
  }
3248
- canvasCtx.fillStyle = isHover ? seg.color + "dd" : seg.color;
3428
+ canvasCtx.fillStyle = isHover ? mixColor(seg.color, .12 * hovT, "#000000") : seg.color;
3249
3429
  canvasCtx.fill();
3250
- if (isHover && (seg.isTopPositive || seg.isBottomNegative)) {
3251
- canvasCtx.fillStyle = "rgba(255, 255, 255, 0.3)";
3430
+ if (isHover && hovT > .01 && (seg.isTopPositive || seg.isBottomNegative)) {
3431
+ canvasCtx.globalAlpha = seg.alpha * .3 * hovT;
3432
+ canvasCtx.fillStyle = "#ffffff";
3252
3433
  const stripY = seg.value >= 0 ? seg.y : seg.y + seg.height - Math.min(6, seg.height);
3253
3434
  canvasCtx.fillRect(barX, stripY, barWidth, Math.min(6, seg.height));
3254
3435
  }
@@ -3300,10 +3481,11 @@ function renderBarChart(ctx, yRange, stacked = false) {
3300
3481
  canvasCtx.globalAlpha = alpha;
3301
3482
  if (barHeight > .5) {
3302
3483
  drawRoundedBarEnd(canvasCtx, x, barTop, barWidth, barHeight, borderRadius, value >= 0 ? "top" : "bottom");
3303
- canvasCtx.fillStyle = isHover ? color + "dd" : color;
3484
+ canvasCtx.fillStyle = isHover ? mixColor(color, .12 * hovT, "#000000") : color;
3304
3485
  canvasCtx.fill();
3305
- if (isHover) {
3306
- canvasCtx.fillStyle = "rgba(255, 255, 255, 0.3)";
3486
+ if (isHover && hovT > .01) {
3487
+ canvasCtx.globalAlpha = .3 * hovT;
3488
+ canvasCtx.fillStyle = "#ffffff";
3307
3489
  const stripY = value >= 0 ? barTop : barTop + barHeight - Math.min(10, barHeight);
3308
3490
  canvasCtx.fillRect(x, stripY, barWidth, Math.min(10, barHeight));
3309
3491
  }
@@ -3365,7 +3547,8 @@ function drawRoundedBarEnd(canvasCtx, x, y, width, height, radius, end) {
3365
3547
  canvasCtx.closePath();
3366
3548
  }
3367
3549
  function renderHorizontalBarChart(ctx, xRange) {
3368
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hiddenSeries, valueFormatter } = ctx;
3550
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1, hiddenSeries, valueFormatter } = ctx;
3551
+ const hovT = hoverAnimProgress;
3369
3552
  const visibleSeries = (options.series || []).filter((s) => !hiddenSeries.has(s.name));
3370
3553
  if (visibleSeries.length === 0) return;
3371
3554
  const seriesCount = visibleSeries.length;
@@ -3410,10 +3593,11 @@ function renderHorizontalBarChart(ctx, xRange) {
3410
3593
  canvasCtx.lineTo(barLeft + barWidth, y + barHeight);
3411
3594
  }
3412
3595
  canvasCtx.closePath();
3413
- canvasCtx.fillStyle = isHover ? color + "dd" : color;
3596
+ canvasCtx.fillStyle = isHover ? mixColor(color, .12 * hovT, "#000000") : color;
3414
3597
  canvasCtx.fill();
3415
- if (isHover) {
3416
- canvasCtx.fillStyle = "rgba(255, 255, 255, 0.3)";
3598
+ if (isHover && hovT > .01) {
3599
+ canvasCtx.globalAlpha = .3 * hovT;
3600
+ canvasCtx.fillStyle = "#ffffff";
3417
3601
  const stripX = value >= 0 ? barLeft + barWidth - Math.min(10, barWidth) : barLeft;
3418
3602
  canvasCtx.fillRect(stripX, y, Math.min(10, barWidth), barHeight);
3419
3603
  }
@@ -3434,15 +3618,7 @@ function renderHorizontalBarChart(ctx, xRange) {
3434
3618
  (options.labels || []).forEach((label, i) => {
3435
3619
  const y = plotArea.y + plotArea.height - (i + .5) * categoryWidth;
3436
3620
  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
- }
3621
+ formattedLabel = truncateLabel(canvasCtx, formattedLabel, maxCatWidth);
3446
3622
  canvasCtx.fillText(formattedLabel, plotArea.x - 10, y);
3447
3623
  });
3448
3624
  canvasCtx.restore();
@@ -3494,7 +3670,7 @@ function renderPieChart(ctx, isDoughnut, isRose = false) {
3494
3670
  const centerX = plotArea.x + plotArea.width / 2;
3495
3671
  const centerY = plotArea.y + plotArea.height / 2;
3496
3672
  const maxRadius = computePieMaxRadius(pieData, options, plotArea);
3497
- const innerRadius = isDoughnut ? Math.max(0, Math.min(maxRadius - 5, maxRadius * (options.innerRadius || .6))) : 0;
3673
+ const innerRadius = isDoughnut ? doughnutInnerRadius(maxRadius, options) : 0;
3498
3674
  let startAngle = -Math.PI / 2;
3499
3675
  const labelItems = [];
3500
3676
  pieData.forEach((data, index) => {
@@ -3502,7 +3678,7 @@ function renderPieChart(ctx, isDoughnut, isRose = false) {
3502
3678
  const sliceAngle = data.value / total * Math.PI * 2 * progress;
3503
3679
  const endAngle = startAngle + sliceAngle;
3504
3680
  const isHover = index === hoverIndex;
3505
- const roseRadius = isRose ? maxRadius * Math.sqrt(data.value / Math.max(...pieData.map((d) => d.value))) : maxRadius;
3681
+ const roseRadius = isRose ? maxRadius * Math.sqrt(data.value / maxOf(pieData.map((d) => d.value))) : maxRadius;
3506
3682
  const hoverDistance = isRose ? Math.max(10, roseRadius * .15) : 12;
3507
3683
  const offset = isHover ? hoverDistance * hoverAnimProgress : 0;
3508
3684
  const radius = isRose ? roseRadius * progress : maxRadius;
@@ -3569,10 +3745,13 @@ function renderPieChart(ctx, isDoughnut, isRose = false) {
3569
3745
  canvasCtx.restore();
3570
3746
  }
3571
3747
  }
3748
+ function doughnutInnerRadius(maxRadius, options) {
3749
+ return Math.max(0, Math.min(maxRadius - 5, maxRadius * (options.innerRadius || .6)));
3750
+ }
3572
3751
  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);
3752
+ const xValues = scatterData.map((d) => d.x).filter((v) => typeof v === "number" && Number.isFinite(v));
3753
+ const xMin = xValues.length ? minOf(xValues) : 0;
3754
+ const xMax = xValues.length ? maxOf(xValues) : 1;
3576
3755
  const xRange = xMax - xMin || 1;
3577
3756
  const xPadding = xRange * .05;
3578
3757
  const yPadding = (yRange.max - yRange.min) * .05;
@@ -3776,12 +3955,12 @@ function computeFacetGrids(scatterData, plotArea, options) {
3776
3955
  const col = i % cols;
3777
3956
  const row = Math.floor(i / cols);
3778
3957
  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);
3958
+ const xs = data.map((d) => d.x).filter((v) => typeof v === "number" && Number.isFinite(v));
3959
+ const ys = data.map((d) => d.y).filter((v) => typeof v === "number" && Number.isFinite(v));
3960
+ const xMin = xs.length ? minOf(xs) : 0;
3961
+ const xMax = xs.length ? maxOf(xs) : 1;
3962
+ const yMin = ys.length ? minOf(ys) : 0;
3963
+ const yMax = ys.length ? maxOf(ys) : 1;
3785
3964
  const xPad = (xMax - xMin || 1) * .08;
3786
3965
  const yPad = (yMax - yMin || 1) * .08;
3787
3966
  const outer = {
@@ -3900,8 +4079,8 @@ function matrixFieldExtent(records, field) {
3900
4079
  min: 0,
3901
4080
  max: 1
3902
4081
  };
3903
- const min = Math.min(...values);
3904
- const max = Math.max(...values);
4082
+ const min = minOf(values);
4083
+ const max = maxOf(values);
3905
4084
  const pad = (max - min || 1) * .08;
3906
4085
  return {
3907
4086
  min: min - pad,
@@ -4134,26 +4313,11 @@ function renderRadarChart(ctx) {
4134
4313
  if (options.radarRingFill === true) {
4135
4314
  canvasCtx.save();
4136
4315
  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;
4316
+ for (let level = 5; level >= 1; level--) {
4317
+ canvasCtx.globalAlpha = level % 2 === 1 ? .5 : .22;
4141
4318
  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");
4319
+ canvasCtx.arc(centerX, centerY, radius * level / 5, 0, Math.PI * 2);
4320
+ canvasCtx.fill();
4157
4321
  }
4158
4322
  canvasCtx.restore();
4159
4323
  }
@@ -4357,8 +4521,8 @@ function computeBins(options, hiddenSeries) {
4357
4521
  const allValues = [];
4358
4522
  visibleSeries.forEach((s) => allValues.push(...s.data.filter((v) => !isMissingValue(v))));
4359
4523
  if (allValues.length === 0) return null;
4360
- const dataMin = Math.min(...allValues);
4361
- const range = Math.max(...allValues) - dataMin || 1;
4524
+ const dataMin = minOf(allValues);
4525
+ const range = maxOf(allValues) - dataMin || 1;
4362
4526
  let bins;
4363
4527
  if (binConfig.bins && binConfig.bins.length > 1) bins = binConfig.bins;
4364
4528
  else {
@@ -4388,7 +4552,7 @@ function renderBinChart(ctx) {
4388
4552
  const { bins, frequencies, displayValues } = result;
4389
4553
  const dataMin = bins[0];
4390
4554
  const range = bins[bins.length - 1] - dataMin || 1;
4391
- const maxFreq = Math.max(...displayValues, 1);
4555
+ const maxFreq = maxOf(displayValues, 1);
4392
4556
  const binCount = bins.length - 1;
4393
4557
  const barGap = 2;
4394
4558
  const barWidth = (plotArea.width - barGap * (binCount - 1)) / binCount;
@@ -4451,8 +4615,8 @@ function renderBulletChart(ctx) {
4451
4615
  allValues.push(r.from, r.to);
4452
4616
  });
4453
4617
  });
4454
- const maxVal = Math.max(...allValues, 0);
4455
- const minVal = Math.min(...allValues, 0);
4618
+ const maxVal = maxOf(allValues, 0);
4619
+ const minVal = minOf(allValues, 0);
4456
4620
  const range = maxVal - minVal || 1;
4457
4621
  const padMin = minVal >= 0 ? 0 : minVal - range * .1;
4458
4622
  const padMax = maxVal + range * .1;
@@ -4665,8 +4829,8 @@ function renderSparklineChart(ctx) {
4665
4829
  if (data.length === 0) return;
4666
4830
  const valid = data.filter((v) => !isMissingValue(v));
4667
4831
  if (valid.length === 0) return;
4668
- const min = Math.min(...valid);
4669
- const range = Math.max(...valid) - min || 1;
4832
+ const min = minOf(valid);
4833
+ const range = maxOf(valid) - min || 1;
4670
4834
  const points = [];
4671
4835
  const pointByIndex = [];
4672
4836
  data.forEach((v, i) => {
@@ -4761,40 +4925,27 @@ function renderSparklineChart(ctx) {
4761
4925
  //#endregion
4762
4926
  //#region src/renderer/charts-extra.js
4763
4927
  function renderWaterfallChart(ctx, yRange) {
4764
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, valueFormatter, hiddenSeries } = ctx;
4928
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1, valueFormatter, hiddenSeries } = ctx;
4929
+ const hovT = hoverAnimProgress;
4765
4930
  const wf = options.waterfall || {};
4766
4931
  const labels = options.labels || [];
4767
4932
  const deltas = (options.series || []).filter((s) => !hiddenSeries.has(s.name))[0]?.data || [];
4768
4933
  if (labels.length === 0 || deltas.length === 0) return;
4769
- const totalIdx = new Set(wf.totalIndices || []);
4934
+ new Set(wf.totalIndices || []);
4770
4935
  const increaseColor = wf.increaseColor || "#dc2626";
4771
4936
  const decreaseColor = wf.decreaseColor || "#16a34a";
4772
4937
  const totalColor = wf.totalColor || theme.colors[0];
4773
4938
  const categoryWidth = plotArea.width / labels.length;
4774
4939
  const barWidth = Math.min(categoryWidth * .6, 64);
4775
4940
  const yFor = (v) => plotArea.y + plotArea.height - (v - yRange.min) / (yRange.max - yRange.min) * plotArea.height;
4776
- let cumulative = 0;
4941
+ const steps = waterfallSteps(deltas, wf.totalIndices);
4777
4942
  let prevEdgeX = null;
4778
4943
  let prevExitY = null;
4779
4944
  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
- }
4945
+ const step = steps[i];
4946
+ if (!step || step.missing) return;
4947
+ const { from, to, isTotal } = step;
4948
+ const color = isTotal ? totalColor : step.value >= 0 ? increaseColor : decreaseColor;
4798
4949
  const cx = plotArea.x + (i + .5) * categoryWidth;
4799
4950
  const x = cx - barWidth / 2;
4800
4951
  const isHover = i === hoverIndex;
@@ -4825,12 +4976,13 @@ function renderWaterfallChart(ctx, yRange) {
4825
4976
  canvasCtx.quadraticCurveTo(x + barWidth, top, x + barWidth, top + r);
4826
4977
  canvasCtx.lineTo(x + barWidth, top + height);
4827
4978
  canvasCtx.closePath();
4828
- canvasCtx.fillStyle = isHover ? color + "dd" : color;
4979
+ canvasCtx.fillStyle = isHover ? mixColor(color, .12 * hovT, "#000000") : color;
4829
4980
  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));
4981
+ if (isHover && hovT > .01) {
4982
+ canvasCtx.globalAlpha = .3 * hovT;
4983
+ canvasCtx.fillStyle = "#ffffff";
4984
+ canvasCtx.fillRect(x, top, barWidth, Math.min(6, height));
4985
+ }
4834
4986
  }
4835
4987
  canvasCtx.restore();
4836
4988
  if (options.showValues && progress > .9 && height > .5) {
@@ -4839,11 +4991,11 @@ function renderWaterfallChart(ctx, yRange) {
4839
4991
  canvasCtx.font = "11px Inter, sans-serif";
4840
4992
  canvasCtx.textAlign = "center";
4841
4993
  canvasCtx.textBaseline = "bottom";
4842
- canvasCtx.fillText(valueFormatter(v), cx, top - 4);
4994
+ canvasCtx.fillText(valueFormatter(step.value), cx, top - 4);
4843
4995
  canvasCtx.restore();
4844
4996
  }
4845
4997
  prevEdgeX = x + barWidth;
4846
- prevExitY = yFor(cumulative);
4998
+ prevExitY = yFor(step.to);
4847
4999
  });
4848
5000
  }
4849
5001
  /**
@@ -4927,7 +5079,8 @@ function computeBoxplotGeometry(plotArea, options, theme, hiddenSeries, valueRan
4927
5079
  };
4928
5080
  }
4929
5081
  function renderBoxplotChart(ctx, yRange) {
4930
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hiddenSeries } = ctx;
5082
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1, hiddenSeries } = ctx;
5083
+ const hovT = hoverAnimProgress;
4931
5084
  const geo = computeBoxplotGeometry(plotArea, options, theme, hiddenSeries, yRange);
4932
5085
  if (!geo) return;
4933
5086
  const { boxes, horizontal } = geo;
@@ -4935,7 +5088,9 @@ function renderBoxplotChart(ctx, yRange) {
4935
5088
  box.b;
4936
5089
  const isHover = box.index === hoverIndex;
4937
5090
  const color = box.color;
4938
- const lineWidth = isHover ? 2 : 1.5;
5091
+ const lineWidth = 1.5 + .5 * (isHover ? hovT : 0);
5092
+ const fillAlpha = .2 + .13 * (isHover ? hovT : 0);
5093
+ const fillHex = Math.round(fillAlpha * 255).toString(16).padStart(2, "0");
4939
5094
  canvasCtx.save();
4940
5095
  canvasCtx.strokeStyle = color;
4941
5096
  canvasCtx.fillStyle = color;
@@ -4958,10 +5113,10 @@ function renderBoxplotChart(ctx, yRange) {
4958
5113
  canvasCtx.moveTo(wHi, yMed - cap / 2);
4959
5114
  canvasCtx.lineTo(wHi, yMed + cap / 2);
4960
5115
  canvasCtx.stroke();
4961
- canvasCtx.fillStyle = color + (isHover ? "55" : "33");
5116
+ canvasCtx.fillStyle = color + fillHex;
4962
5117
  canvasCtx.fillRect(qLo, yLo, Math.max(1, qHi - qLo), box.thickness);
4963
5118
  canvasCtx.strokeRect(qLo, yLo, Math.max(1, qHi - qLo), box.thickness);
4964
- canvasCtx.lineWidth = isHover ? 2.5 : 2;
5119
+ canvasCtx.lineWidth = 2 + .5 * (isHover ? hovT : 0);
4965
5120
  canvasCtx.beginPath();
4966
5121
  canvasCtx.moveTo(box.cx + (box.med - box.cx) * progress, yLo);
4967
5122
  canvasCtx.lineTo(box.cx + (box.med - box.cx) * progress, yLo + box.thickness);
@@ -4969,7 +5124,7 @@ function renderBoxplotChart(ctx, yRange) {
4969
5124
  box.outliers.forEach((o) => {
4970
5125
  const ox = box.cx + (o - box.cx) * progress;
4971
5126
  canvasCtx.beginPath();
4972
- canvasCtx.arc(ox, yMed, isHover ? 3.5 : 2.5, 0, Math.PI * 2);
5127
+ canvasCtx.arc(ox, yMed, 2.5 + (isHover ? hovT : 0), 0, Math.PI * 2);
4973
5128
  canvasCtx.fillStyle = color;
4974
5129
  canvasCtx.fill();
4975
5130
  });
@@ -4999,10 +5154,10 @@ function renderBoxplotChart(ctx, yRange) {
4999
5154
  canvasCtx.moveTo(box.cx - box.capWidth / 2, yMax);
5000
5155
  canvasCtx.lineTo(box.cx + box.capWidth / 2, yMax);
5001
5156
  canvasCtx.stroke();
5002
- canvasCtx.fillStyle = color + (isHover ? "55" : "33");
5157
+ canvasCtx.fillStyle = color + fillHex;
5003
5158
  canvasCtx.fillRect(xLo, yQ3, box.thickness, Math.max(1, yQ1 - yQ3));
5004
5159
  canvasCtx.strokeRect(xLo, yQ3, box.thickness, Math.max(1, yQ1 - yQ3));
5005
- canvasCtx.lineWidth = isHover ? 2.5 : 2;
5160
+ canvasCtx.lineWidth = 2 + .5 * (isHover ? hovT : 0);
5006
5161
  canvasCtx.beginPath();
5007
5162
  canvasCtx.moveTo(xLo, yMed);
5008
5163
  canvasCtx.lineTo(xLo + box.thickness, yMed);
@@ -5010,7 +5165,7 @@ function renderBoxplotChart(ctx, yRange) {
5010
5165
  box.outliers.forEach((o) => {
5011
5166
  const oy = box.cy + (o - box.cy) * progress;
5012
5167
  canvasCtx.beginPath();
5013
- canvasCtx.arc(box.cx, oy, isHover ? 3.5 : 2.5, 0, Math.PI * 2);
5168
+ canvasCtx.arc(box.cx, oy, 2.5 + (isHover ? hovT : 0), 0, Math.PI * 2);
5014
5169
  canvasCtx.fillStyle = color;
5015
5170
  canvasCtx.fill();
5016
5171
  });
@@ -5036,15 +5191,7 @@ function renderBoxplotChart(ctx, yRange) {
5036
5191
  seen.add(box.catIndex);
5037
5192
  let label = box.b.label;
5038
5193
  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
- }
5194
+ label = truncateLabel(canvasCtx, label, maxWidth);
5048
5195
  const cx = plotArea.x + (box.catIndex + .5) * geo.categoryWidth;
5049
5196
  canvasCtx.fillText(label, cx, plotArea.y + plotArea.height + 8);
5050
5197
  });
@@ -5061,15 +5208,7 @@ function renderBoxplotChart(ctx, yRange) {
5061
5208
  seen.add(box.catIndex);
5062
5209
  let label = box.b.label;
5063
5210
  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
- }
5211
+ label = truncateLabel(canvasCtx, label, maxWidth);
5073
5212
  const cy = plotArea.y + plotArea.height - (box.catIndex + .5) * geo.categoryWidth;
5074
5213
  canvasCtx.fillText(label, plotArea.x - 10, cy);
5075
5214
  });
@@ -5202,6 +5341,10 @@ function computeSunburstGeometry(plotArea, options, theme, valueFormatter) {
5202
5341
  valueFormatter
5203
5342
  };
5204
5343
  }
5344
+ /** 能用正文色(浅色主题近黑)就用正文色,底色深到不可读才反白(用户定则) */
5345
+ function sunburstLabelColor(bg, theme) {
5346
+ return isLightColor(bg) ? theme.textColor : getContrastText(bg);
5347
+ }
5205
5348
  function renderSunburstChart(ctx) {
5206
5349
  const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, valueFormatter, hoverAnimProgress = 1 } = ctx;
5207
5350
  const geo = computeSunburstGeometry(plotArea, options, theme, valueFormatter);
@@ -5249,7 +5392,7 @@ function renderSunburstChart(ctx) {
5249
5392
  name,
5250
5393
  value,
5251
5394
  bold: seg.depth === 0,
5252
- fill: getContrastText(color),
5395
+ fill: sunburstLabelColor(color, theme),
5253
5396
  alpha: alphaAt(i)
5254
5397
  });
5255
5398
  });
@@ -5274,7 +5417,8 @@ function renderSunburstChart(ctx) {
5274
5417
  });
5275
5418
  }
5276
5419
  function renderMixedChart(ctx, leftRange, rightRange) {
5277
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hiddenSeries } = ctx;
5420
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1, hiddenSeries } = ctx;
5421
+ const hovT = hoverAnimProgress;
5278
5422
  const labels = options.labels || [];
5279
5423
  const barSeries = (options.series || []).filter((s) => !hiddenSeries.has(s.name)).filter((s) => (s.chartType || "line") === "bar");
5280
5424
  if (barSeries.length > 0 && labels.length > 0) {
@@ -5309,7 +5453,7 @@ function renderMixedChart(ctx, leftRange, rightRange) {
5309
5453
  canvasCtx.quadraticCurveTo(x + barWidth, barTop, x + barWidth, barTop + r);
5310
5454
  canvasCtx.lineTo(x + barWidth, barTop + barHeight);
5311
5455
  canvasCtx.closePath();
5312
- canvasCtx.fillStyle = isHover ? color + "dd" : color + "cc";
5456
+ canvasCtx.fillStyle = isHover ? mixColor(color, .12 * hovT, "#000000") + "cc" : color + "cc";
5313
5457
  canvasCtx.fill();
5314
5458
  canvasCtx.restore();
5315
5459
  }
@@ -5356,10 +5500,7 @@ function computeSankeyLayout(plotArea, options, theme, hiddenSeries) {
5356
5500
  outSum.set(l.source, (outSum.get(l.source) || 0) + l.value);
5357
5501
  inSum.set(l.target, (inSum.get(l.target) || 0) + l.value);
5358
5502
  });
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
- });
5503
+ 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
5504
  const depth = new Map(allNodes.map((n) => [n.name, 0]));
5364
5505
  for (let round = 0; round < allNodes.length; round++) {
5365
5506
  let changed = false;
@@ -5372,8 +5513,14 @@ function computeSankeyLayout(plotArea, options, theme, hiddenSeries) {
5372
5513
  });
5373
5514
  if (!changed) break;
5374
5515
  }
5375
- const maxDepth = Math.max(...depth.values());
5376
- const valueMax = Math.max(1, ...allNodes.map((n) => n.value || 0), ...links.map((l) => l.value));
5516
+ const maxDepth = maxOf([...depth.values()]);
5517
+ if ((options.sankey?.nodeAlign ?? "justify") === "justify") {
5518
+ const hasOut = new Set(links.map((l) => l.source));
5519
+ allNodes.forEach((n) => {
5520
+ if (!hasOut.has(n.name)) depth.set(n.name, maxDepth);
5521
+ });
5522
+ }
5523
+ const valueMax = Math.max(1, maxOf([...nodeValues.values()]), maxOf(links.map((l) => l.value)));
5377
5524
  const columnCount = maxDepth + 1;
5378
5525
  const scale = (plotArea.height * .82 - NODE_GAP * Math.max(0, columnCount - 1)) / valueMax;
5379
5526
  const xOfColumn = (col) => plotArea.x + col * (plotArea.width - 14) / Math.max(1, columnCount - 1);
@@ -5383,8 +5530,8 @@ function computeSankeyLayout(plotArea, options, theme, hiddenSeries) {
5383
5530
  color: n.color || theme.colors[i % theme.colors.length],
5384
5531
  col: depth.get(n.name) || 0,
5385
5532
  x: xOfColumn(depth.get(n.name) || 0),
5386
- value: n.value || 0,
5387
- height: Math.max(2, (n.value || 0) * scale)
5533
+ value: nodeValues.get(n.name) || 0,
5534
+ height: Math.max(2, (nodeValues.get(n.name) || 0) * scale)
5388
5535
  }));
5389
5536
  const byCol = /* @__PURE__ */ new Map();
5390
5537
  nodes.forEach((n) => {
@@ -5527,8 +5674,7 @@ function renderSankeyChart(ctx) {
5527
5674
  });
5528
5675
  }
5529
5676
  /** 桑基命中:先节点矩形、后流带(平滑步进插值带内即命中) */
5530
- function sankeyHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries) {
5531
- const layout = computeSankeyLayout(plotArea, options, theme, hiddenSeries);
5677
+ function sankeyHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries, layout = computeSankeyLayout(plotArea, options, theme, hiddenSeries)) {
5532
5678
  if (!layout) return null;
5533
5679
  const { nodes, ribbons } = layout;
5534
5680
  for (let i = 0; i < nodes.length; i++) {
@@ -5575,9 +5721,10 @@ function computeVennLayout(plotArea, options, theme) {
5575
5721
  })).filter((d) => d.sets.length === 1);
5576
5722
  if (circles.length === 0 || circles.length > 3) return null;
5577
5723
  const interRows = rows.filter((d) => d.sets && d.sets.length === 2);
5578
- const valueMax = Math.max(...circles.map((d) => d.value || 0));
5724
+ const valueMax = maxOf(circles.map((d) => d.value || 0));
5579
5725
  if (!(valueMax > 0)) return null;
5580
- const R = Math.min(plotArea.width, plotArea.height) * .38;
5726
+ const setCount = circles.length;
5727
+ const R = Math.min(plotArea.height * (setCount >= 3 ? .3 : .46), plotArea.width * (setCount >= 3 ? .2 : .24));
5581
5728
  const k = R * R / valueMax;
5582
5729
  const radiusOf = (v) => Math.sqrt((v || 0) * k / Math.PI);
5583
5730
  circles.forEach((c, i) => {
@@ -5698,7 +5845,8 @@ function renderVennChart(ctx) {
5698
5845
  if (progress < .9) return;
5699
5846
  canvasCtx.save();
5700
5847
  canvasCtx.font = "600 12px Inter, sans-serif";
5701
- canvasCtx.fillStyle = layout.hollow ? theme.textColor : getContrastText(c.color);
5848
+ const labelColor = isLightColor(c.color) ? theme.textColor : getContrastText(c.color);
5849
+ canvasCtx.fillStyle = layout.hollow ? theme.textColor : labelColor;
5702
5850
  canvasCtx.textAlign = "center";
5703
5851
  canvasCtx.textBaseline = "middle";
5704
5852
  canvasCtx.fillText(c.name, c.x, c.y - c.r * .4);
@@ -5775,7 +5923,7 @@ function computeChordLayout(plotArea, options, theme, hiddenSeries) {
5775
5923
  const cy = plotArea.y + plotArea.height / 2;
5776
5924
  const R = Math.max(40, Math.min(plotArea.width, plotArea.height) / 2 - 30);
5777
5925
  const mode = options.chordMode === "curve" ? "curve" : "band";
5778
- const valueMax = Math.max(1, ...links.map((l) => l.value));
5926
+ const valueMax = Math.max(1, maxOf(links.map((l) => l.value)));
5779
5927
  const byValue = options.chordByValue === true;
5780
5928
  const nodeValue = (n) => {
5781
5929
  if (byValue) {
@@ -5814,8 +5962,8 @@ function computeChordLayout(plotArea, options, theme, hiddenSeries) {
5814
5962
  const src = arcByName.get(link.source);
5815
5963
  const tgt = arcByName.get(link.target);
5816
5964
  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);
5965
+ const s0 = mode === "curve" ? src.midAngle : src.startAngle + (outOffset.get(src.name) || 0);
5966
+ const t0 = mode === "curve" ? tgt.midAngle : tgt.startAngle + (outOffset.get(tgt.name) || 0);
5819
5967
  outOffset.set(src.name, (outOffset.get(src.name) || 0) + angularW);
5820
5968
  outOffset.set(tgt.name, (outOffset.get(tgt.name) || 0) + angularW);
5821
5969
  const [ax, ay] = polar(s0, R);
@@ -5998,9 +6146,11 @@ function chordHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries)
5998
6146
  function computeArcLayout(plotArea, options, theme, hiddenSeries) {
5999
6147
  const { nodes, links } = normalizeRelation(options.arcData, hiddenSeries);
6000
6148
  if (nodes.length === 0) return null;
6001
- const axisY = plotArea.y + Math.max(30, (plotArea.height - 40) * .52);
6149
+ const labelBand = 30;
6150
+ const topGap = 24;
6151
+ const axisY = plotArea.y + Math.max(40, plotArea.height - labelBand);
6002
6152
  const step = plotArea.width / nodes.length;
6003
- const valueMax = Math.max(1, ...links.map((l) => l.value));
6153
+ const valueMax = Math.max(1, maxOf(links.map((l) => l.value)));
6004
6154
  const nodePts = nodes.map((n, i) => ({
6005
6155
  node: n,
6006
6156
  name: n.name,
@@ -6018,7 +6168,7 @@ function computeArcLayout(plotArea, options, theme, hiddenSeries) {
6018
6168
  const dist = Math.abs(b.x - a.x);
6019
6169
  const left = a.x <= b.x ? a : b;
6020
6170
  const right = a.x <= b.x ? b : a;
6021
- const cy = Math.min(dist * .45, (axisY - plotArea.y - 12) / .75);
6171
+ const cy = Math.min(dist * .45, (axisY - plotArea.y - topGap) / .75);
6022
6172
  const width = 1.5 + link.value / valueMax * 4.5;
6023
6173
  const samples = [];
6024
6174
  const x1 = left.x;
@@ -6090,18 +6240,9 @@ function renderArcChart(ctx) {
6090
6240
  canvasCtx.lineTo(n.x, axisY + 16);
6091
6241
  canvasCtx.stroke();
6092
6242
  if (progress >= .9) {
6093
- let label = n.name;
6094
6243
  canvasCtx.font = "12px Inter, sans-serif";
6095
6244
  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
- }
6245
+ const label = truncateLabel(canvasCtx, n.name, maxW);
6105
6246
  canvasCtx.fillStyle = theme.textColorSecondary;
6106
6247
  canvasCtx.textAlign = "center";
6107
6248
  canvasCtx.textBaseline = "top";
@@ -6173,11 +6314,11 @@ function computeGanttLayout(plotArea, options, theme, hiddenSeries, valueFormatt
6173
6314
  endTs: toTs(t.end)
6174
6315
  })).filter((t) => t.startTs !== null && t.endTs !== null);
6175
6316
  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)));
6317
+ const labelW = Math.max(LABEL_MIN, Math.min(LABEL_MAX, Math.round(maxOf(parsed.map((t) => estimateTextWidth(t.name || "", 12))) + 16)));
6177
6318
  const chartX = plotArea.x;
6178
6319
  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)));
6320
+ let tmin = minOf(parsed.map((t) => Math.min(t.startTs, t.endTs)));
6321
+ let tmax = maxOf(parsed.map((t) => Math.max(t.startTs, t.endTs)));
6181
6322
  if (options.ganttToday) {
6182
6323
  const today = toTs(options.ganttToday);
6183
6324
  if (today !== null) {
@@ -6238,7 +6379,7 @@ function computeGanttLayout(plotArea, options, theme, hiddenSeries, valueFormatt
6238
6379
  };
6239
6380
  }
6240
6381
  function renderGanttChart(ctx) {
6241
- const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hiddenSeries } = ctx;
6382
+ const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hoverAnimProgress = 1, hiddenSeries } = ctx;
6242
6383
  const layout = computeGanttLayout(plotArea, options, theme, hiddenSeries);
6243
6384
  if (!layout) return;
6244
6385
  const { rows, deps, chartX, chartW, rowH, barH } = layout;
@@ -6253,6 +6394,7 @@ function renderGanttChart(ctx) {
6253
6394
  });
6254
6395
  if (hoverIndex >= 0 && hoverIndex < rows.length) {
6255
6396
  canvasCtx.save();
6397
+ canvasCtx.globalAlpha = hoverAnimProgress;
6256
6398
  canvasCtx.fillStyle = theme.highlightColor;
6257
6399
  canvasCtx.fillRect(chartX, plotArea.y + hoverIndex * rowH, chartW, rowH);
6258
6400
  canvasCtx.restore();
@@ -6386,21 +6528,12 @@ function renderGanttChart(ctx) {
6386
6528
  canvasCtx.textBaseline = "middle";
6387
6529
  let label = r.name || "";
6388
6530
  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
- }
6531
+ label = truncateLabel(canvasCtx, label, maxW);
6398
6532
  canvasCtx.fillText(label, chartX - 10, plotArea.y + i * rowH + rowH / 2);
6399
6533
  canvasCtx.restore();
6400
6534
  });
6401
6535
  }
6402
- function ganttHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries) {
6403
- const layout = computeGanttLayout(plotArea, options, theme, hiddenSeries);
6536
+ function ganttHitTest(canvasX, canvasY, plotArea, options, theme, hiddenSeries, layout = computeGanttLayout(plotArea, options, theme, hiddenSeries)) {
6404
6537
  if (!layout) return null;
6405
6538
  const { rows } = layout;
6406
6539
  const relY = canvasY - plotArea.y;
@@ -6469,7 +6602,7 @@ function renderDataZoomSlider(ctx, zoom) {
6469
6602
  const previewSeries = (options.series || []).filter((s) => !ctx.hiddenSeries.has(s.name))[0];
6470
6603
  if (previewSeries && previewSeries.data.length > 0) {
6471
6604
  const data = previewSeries.data;
6472
- const max = Math.max(...data.map((v) => v === null || Number.isNaN(v) ? 0 : v), 1);
6605
+ const max = maxOf(data.map((v) => v === null || Number.isNaN(v) ? 0 : v), 1);
6473
6606
  const barAreaHeight = geo.height - 18;
6474
6607
  const barWidth = Math.max(1, geo.width / data.length - 1);
6475
6608
  data.forEach((raw, i) => {
@@ -6771,6 +6904,11 @@ function createSvgRecorder(real) {
6771
6904
  "font-family": font.family,
6772
6905
  "fill": typeof style.fillStyle === "string" ? style.fillStyle : "#000"
6773
6906
  };
6907
+ if (matrix[1] !== 0 || matrix[2] !== 0) {
6908
+ attrs.x = x.toFixed(2);
6909
+ attrs.y = y.toFixed(2);
6910
+ attrs.transform = `matrix(${matrix.map((v) => +v.toFixed(4)).join(" ")})`;
6911
+ }
6774
6912
  if (style.textAlign === "center") attrs["text-anchor"] = "middle";
6775
6913
  else if (style.textAlign === "right" || style.textAlign === "end") attrs["text-anchor"] = "end";
6776
6914
  if (style.textBaseline === "middle") attrs["dominant-baseline"] = "central";
@@ -6934,7 +7072,7 @@ function createSvgRecorder(real) {
6934
7072
  }
6935
7073
  function toSvg(width, height, backgroundColor) {
6936
7074
  const body = elements.map((el) => {
6937
- const attrs = Object.entries(el.attrs).map(([k, v]) => `${k}="${v}"`).join(" ");
7075
+ const attrs = Object.entries(el.attrs).map(([k, v]) => `${k}="${escapeXml(String(v))}"`).join(" ");
6938
7076
  if (el.tag === "text") return `<text ${attrs}>${escapeXml(el.text || "")}</text>`;
6939
7077
  return `<${el.tag} ${attrs}/>`;
6940
7078
  }).join("\n");
@@ -6956,7 +7094,14 @@ function runLayers(renderCtx, at) {
6956
7094
  }
6957
7095
  function renderChart(canvas, params) {
6958
7096
  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);
7097
+ const logApplied = applyLogTransform(rawOptions);
7098
+ const options = (logApplied.series || []).some((s) => !s.data) ? {
7099
+ ...logApplied,
7100
+ series: logApplied.series.map((s) => s.data ? s : {
7101
+ ...s,
7102
+ data: []
7103
+ })
7104
+ } : logApplied;
6960
7105
  const ctx = externalCtx || canvas.getContext("2d");
6961
7106
  const width = canvas.width / dpr;
6962
7107
  const height = canvas.height / dpr;
@@ -7017,10 +7162,10 @@ function renderChart(canvas, params) {
7017
7162
  });
7018
7163
  break;
7019
7164
  }
7020
- const yValues = scatterData.map((d) => d.y);
7165
+ const yValues = scatterData.map((d) => d.y).filter((v) => typeof v === "number" && Number.isFinite(v));
7021
7166
  const yRange = renderYAxis(renderCtx, "left", {
7022
- min: Math.min(...yValues),
7023
- max: Math.max(...yValues)
7167
+ min: yValues.length ? minOf(yValues) : 0,
7168
+ max: yValues.length ? maxOf(yValues) : 1
7024
7169
  });
7025
7170
  renderXAxis(renderCtx);
7026
7171
  points = renderScatterChart(renderCtx, yRange);
@@ -7050,8 +7195,8 @@ function renderChart(canvas, params) {
7050
7195
  ...renderCtx,
7051
7196
  plotArea: axisArea
7052
7197
  }, "left", {
7053
- min: Math.min(...allValues),
7054
- max: Math.max(...allValues)
7198
+ min: minOf(allValues),
7199
+ max: maxOf(allValues)
7055
7200
  });
7056
7201
  renderXAxis(renderCtx);
7057
7202
  renderCandleChart(renderCtx, yRange);
@@ -7068,7 +7213,15 @@ function renderChart(canvas, params) {
7068
7213
  renderChordChart(renderCtx);
7069
7214
  break;
7070
7215
  case "arc":
7071
- renderArcChart(renderCtx);
7216
+ if (options.arcCircular === true) renderChordChart({
7217
+ ...renderCtx,
7218
+ options: {
7219
+ ...options,
7220
+ chordData: options.arcData,
7221
+ chordMode: "curve"
7222
+ }
7223
+ });
7224
+ else renderArcChart(renderCtx);
7072
7225
  break;
7073
7226
  case "gantt":
7074
7227
  renderGanttChart(renderCtx);
@@ -7115,12 +7268,12 @@ function renderChart(canvas, params) {
7115
7268
  ]);
7116
7269
  let range;
7117
7270
  if (horizontal) range = renderHorizontalAxis(renderCtx, {
7118
- min: Math.min(...allValues),
7119
- max: Math.max(...allValues)
7271
+ min: minOf(allValues),
7272
+ max: maxOf(allValues)
7120
7273
  });
7121
7274
  else range = renderYAxis(renderCtx, "left", {
7122
- min: Math.min(...allValues),
7123
- max: Math.max(...allValues)
7275
+ min: minOf(allValues),
7276
+ max: maxOf(allValues)
7124
7277
  });
7125
7278
  leftRange = range;
7126
7279
  renderBoxplotChart(renderCtx, range);
@@ -7292,7 +7445,12 @@ var chartOptionsSchema = {
7292
7445
  show: { type: "boolean" },
7293
7446
  position: {
7294
7447
  type: "string",
7295
- enum: ["top", "bottom"]
7448
+ enum: [
7449
+ "top",
7450
+ "bottom",
7451
+ "left",
7452
+ "right"
7453
+ ]
7296
7454
  },
7297
7455
  interactive: { type: "boolean" },
7298
7456
  hoverEmphasis: { type: "boolean" }
@@ -7322,6 +7480,10 @@ var chartOptionsSchema = {
7322
7480
  type: "object",
7323
7481
  properties: {
7324
7482
  show: { type: "boolean" },
7483
+ type: {
7484
+ type: "string",
7485
+ enum: ["category", "time"]
7486
+ },
7325
7487
  min: { type: "number" },
7326
7488
  max: { type: "number" },
7327
7489
  ticks: { type: "integer" },
@@ -7341,6 +7503,10 @@ var chartOptionsSchema = {
7341
7503
  type: "object",
7342
7504
  properties: {
7343
7505
  show: { type: "boolean" },
7506
+ type: {
7507
+ type: "string",
7508
+ enum: ["value", "log"]
7509
+ },
7344
7510
  min: { type: "number" },
7345
7511
  max: { type: "number" },
7346
7512
  ticks: { type: "integer" },
@@ -7481,6 +7647,8 @@ var chartOptionsSchema = {
7481
7647
  }
7482
7648
  },
7483
7649
  annotation: { type: "object" },
7650
+ markLines: { type: "array" },
7651
+ markAreas: { type: "array" },
7484
7652
  markLine: { type: "array" },
7485
7653
  markArea: { type: "array" },
7486
7654
  scenes: {
@@ -7503,20 +7671,110 @@ var chartOptionsSchema = {
7503
7671
  }
7504
7672
  },
7505
7673
  pieData: { type: "array" },
7674
+ piePercentMode: { type: "boolean" },
7675
+ pieHideThreshold: { type: "number" },
7676
+ innerRadius: { type: "number" },
7506
7677
  radarSeries: { type: "array" },
7507
7678
  radarIndicators: { type: "array" },
7679
+ radarStacked: { type: "boolean" },
7680
+ radarRingFill: { type: "boolean" },
7681
+ radarLabelOffset: { type: "number" },
7508
7682
  scatterData: { type: "array" },
7683
+ quadrant: {
7684
+ type: "object",
7685
+ properties: {
7686
+ show: { type: "boolean" },
7687
+ xMid: { type: "number" },
7688
+ yMid: { type: "number" },
7689
+ labels: { type: "array" }
7690
+ }
7691
+ },
7692
+ scatterTrendline: { type: "string" },
7693
+ trendlinePerGroup: { type: "boolean" },
7694
+ jitter: { type: "number" },
7695
+ pointLabels: { type: "boolean" },
7696
+ facet: { type: "boolean" },
7509
7697
  funnelData: { type: "array" },
7510
7698
  funnelMinRatio: { type: "number" },
7699
+ pyramid: { type: "boolean" },
7511
7700
  gauge: { oneOf: [{ type: "number" }, { type: "object" }] },
7512
7701
  heatmapData: { type: "array" },
7702
+ heatmap: {
7703
+ type: "object",
7704
+ properties: {
7705
+ colorScale: { type: "array" },
7706
+ colorBar: { type: "boolean" }
7707
+ }
7708
+ },
7709
+ heatmapColorBar: { type: "boolean" },
7710
+ heatmapSortBy: {
7711
+ type: "string",
7712
+ enum: ["name", "value"]
7713
+ },
7513
7714
  candleData: { type: "array" },
7514
7715
  volumeData: { type: "array" },
7515
7716
  candleMa: { type: "array" },
7717
+ candleMaColors: { type: "array" },
7718
+ candleUpColor: { type: "string" },
7719
+ candleDownColor: { type: "string" },
7516
7720
  calendarData: { type: "array" },
7517
- calendar: { type: "object" },
7721
+ calendar: {
7722
+ type: "object",
7723
+ properties: {
7724
+ start: { type: "string" },
7725
+ end: { type: "string" },
7726
+ weekStart: { type: "integer" },
7727
+ today: { type: "string" },
7728
+ granularity: {
7729
+ type: "string",
7730
+ enum: [
7731
+ "day",
7732
+ "week",
7733
+ "month"
7734
+ ]
7735
+ },
7736
+ colors: { type: "array" },
7737
+ cellGap: { type: "number" },
7738
+ showScale: { type: "boolean" },
7739
+ weekdayLabels: { type: "array" },
7740
+ showAllWeekdays: { type: "boolean" }
7741
+ }
7742
+ },
7518
7743
  bulletData: { type: "array" },
7519
7744
  boxData: { type: "array" },
7745
+ boxHorizontal: { type: "boolean" },
7746
+ showOutliers: { type: "boolean" },
7747
+ binConfig: {
7748
+ type: "object",
7749
+ properties: {
7750
+ bins: { type: "array" },
7751
+ binCount: { type: "integer" },
7752
+ density: { type: "boolean" }
7753
+ }
7754
+ },
7755
+ treemapData: { type: "array" },
7756
+ sunburstData: { type: "array" },
7757
+ valueFormat: {
7758
+ type: "object",
7759
+ properties: {
7760
+ decimals: { type: "integer" },
7761
+ thousandSeparator: { type: "string" },
7762
+ prefix: { type: "string" },
7763
+ suffix: { type: "string" },
7764
+ abbreviate: { type: "boolean" }
7765
+ }
7766
+ },
7767
+ showValues: { type: "boolean" },
7768
+ borderRadius: { type: "number" },
7769
+ step: { type: "boolean" },
7770
+ smooth: { type: "boolean" },
7771
+ stackAreas: { type: "boolean" },
7772
+ connectNulls: { type: "boolean" },
7773
+ crosshairUnified: { type: "boolean" },
7774
+ waterfall: {
7775
+ type: "object",
7776
+ properties: { totalIndices: { type: "array" } }
7777
+ },
7520
7778
  sankeyData: {
7521
7779
  type: "object",
7522
7780
  properties: {
@@ -7524,6 +7782,13 @@ var chartOptionsSchema = {
7524
7782
  links: { type: "array" }
7525
7783
  }
7526
7784
  },
7785
+ sankey: {
7786
+ type: "object",
7787
+ properties: { nodeAlign: {
7788
+ type: "string",
7789
+ enum: ["justify", "left"]
7790
+ } }
7791
+ },
7527
7792
  chordData: {
7528
7793
  type: "object",
7529
7794
  properties: {
@@ -7539,7 +7804,10 @@ var chartOptionsSchema = {
7539
7804
  }
7540
7805
  },
7541
7806
  vennData: { type: "array" },
7807
+ vennHollow: { type: "boolean" },
7542
7808
  ganttData: { type: "array" },
7809
+ ganttToday: { type: "string" },
7810
+ arcCircular: { type: "boolean" },
7543
7811
  matrixFields: { type: "array" },
7544
7812
  matrixData: { type: "array" },
7545
7813
  loading: { type: "boolean" },
@@ -7553,7 +7821,9 @@ var chartOptionsSchema = {
7553
7821
  },
7554
7822
  logAxis: { type: "boolean" },
7555
7823
  valueFormatter: { type: "function" },
7556
- timeAxis: { type: "boolean" }
7824
+ timeAxis: { type: "boolean" },
7825
+ sparklineSmooth: { type: "boolean" },
7826
+ sparklineArea: { type: "boolean" }
7557
7827
  }
7558
7828
  };
7559
7829
  var TYPE_CHECKS = {
@@ -8020,9 +8290,9 @@ function generateChartSpec(data, hint = {}) {
8020
8290
  if (splitIdx >= 0) {
8021
8291
  spec.type = "mixed";
8022
8292
  spec.yAxisRight = { show: true };
8023
- const peakOf = (s) => Math.max(0, ...s.data.filter((v) => typeof v === "number" && Number.isFinite(v)).map(Math.abs));
8293
+ const peakOf = (s) => Math.max(0, maxOf(s.data.filter((v) => typeof v === "number" && Number.isFinite(v)).map(Math.abs)));
8024
8294
  const peakMax = peakOf(seriesList[splitIdx]);
8025
- const peakMin = Math.min(...seriesList.filter((_, i) => i !== splitIdx).map(peakOf).filter((v) => v > 0));
8295
+ const peakMin = minOf(seriesList.filter((_, i) => i !== splitIdx).map(peakOf).filter((v) => v > 0));
8026
8296
  const ratioText = Number.isFinite(peakMin) && peakMin > 0 ? `(最高约为最低的 ${fmtRatio(peakMax / peakMin)} 倍)` : "";
8027
8297
  report.push({
8028
8298
  level: "info",
@@ -8066,8 +8336,8 @@ function planDualAxis(seriesList) {
8066
8336
  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
8337
  const valid = maxes.filter((m) => m > 0);
8068
8338
  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;
8339
+ const max = maxOf(maxes);
8340
+ return max / minOf(valid) >= 100 ? maxes.indexOf(max) : -1;
8071
8341
  }
8072
8342
  function fmtRatio(ratio) {
8073
8343
  if (ratio >= 1e8) return `${(ratio / 1e8).toFixed(1)}亿`;
@@ -8094,6 +8364,16 @@ var SPEC_RULES = [
8094
8364
  "series[].data 长度必须与 labels 一致,缺失值用 null",
8095
8365
  "双轴:两个量纲或量级悬殊的度量用 type mixed——大量度 series 用 chartType bar 且 yAxis left,小量度 chartType line 且 yAxis right,并置顶层 yAxisRight",
8096
8366
  "palette 仅在需求要求固定配色(不随宿主主题换肤)时设置,取值限定 schema enum 中的色系 id;与 theme.colors 同时出现时以 theme.colors 为准",
8367
+ "弧长连接图关系密集或需要闭合回路视角时置 arcCircular: true,数据仍走 arcData",
8368
+ "日历热力需要周/月聚合视角时置 calendar.granularity(day 每日 / week / month,周月为求和聚合)",
8369
+ "数值格式化优先用 valueFormat 快捷对象(decimals/thousandSeparator/prefix/suffix/abbreviate),确需函数式定制才用 valueFormatter",
8370
+ "散点增强:要看分布分界用 quadrant(均值十字 + 象限标签),看相关走向用 scatterTrendline: 'linear'(多组对比配 trendlinePerGroup: true),同值重叠用 jitter,需逐点标注用 pointLabels: true,多变量按 group 拆格用 facet: true",
8371
+ "雷达增强:radarRingFill: true 画同心环带底色;多系列重叠看构成用 radarStacked: true",
8372
+ "K 线:volumeData 与 candleData 等长即带量副图,candleMa(如 [5, 10])叠均线并可配 candleMaColors,涨跌色用 candleUpColor / candleDownColor",
8373
+ "桑基列对齐缺省 justify(末端贴右缘),仅按拓扑深度排布的需求才设 sankey: { nodeAlign: 'left' }",
8374
+ "箱线横向排布用 boxHorizontal: true;直方图分箱用 binConfig(binCount 或 bins,密度视角 density);热力排序视角用 heatmapSortBy: 'value'",
8375
+ "折线增强:多系列面积构成用 stackAreas: true,阶梯走势用 step: true,null 断点需跨接用 connectNulls: true,逐点标值用 showValues,阈值参考线用 markLines,事件区间强调用 markAreas",
8376
+ "瀑布图合计列用 waterfall.totalIndices 标记;漏斗尾段压缩用 funnelMinRatio;饼图环形化用 type: 'doughnut' 配 innerRadius",
8097
8377
  "叙述注解 annotations 单图不超过 3 处,emphasis 焦点最多 1 个系列",
8098
8378
  "不使用 schema 之外的字段",
8099
8379
  `支持的图表类型:${chartOptionsSchema.properties.type.enum.join(" / ")}`
@@ -8249,6 +8529,102 @@ var SPEC_EXAMPLES = [
8249
8529
  yAxisRight: { show: true },
8250
8530
  legend: { show: true }
8251
8531
  }
8532
+ },
8533
+ {
8534
+ requirement: "按周汇总看打卡活跃度",
8535
+ spec: {
8536
+ type: "calendar-heatmap",
8537
+ title: "打卡活跃度(按周聚合)",
8538
+ calendarData: [
8539
+ {
8540
+ date: "2026-06-01",
8541
+ value: 3
8542
+ },
8543
+ {
8544
+ date: "2026-06-02",
8545
+ value: 5
8546
+ },
8547
+ {
8548
+ date: "2026-06-05",
8549
+ value: 8
8550
+ }
8551
+ ],
8552
+ calendar: {
8553
+ start: "2026-06-01",
8554
+ end: "2026-08-31",
8555
+ granularity: "week"
8556
+ }
8557
+ }
8558
+ },
8559
+ {
8560
+ requirement: "服务间调用关系,环形布局更好看出闭环",
8561
+ spec: {
8562
+ type: "arc",
8563
+ title: "服务间调用拓扑",
8564
+ arcCircular: true,
8565
+ arcData: {
8566
+ nodes: [
8567
+ { name: "网关" },
8568
+ { name: "订单服务" },
8569
+ { name: "支付服务" },
8570
+ { name: "消息中心" }
8571
+ ],
8572
+ links: [
8573
+ {
8574
+ source: "网关",
8575
+ target: "订单服务",
8576
+ value: 46
8577
+ },
8578
+ {
8579
+ source: "订单服务",
8580
+ target: "支付服务",
8581
+ value: 38
8582
+ },
8583
+ {
8584
+ source: "支付服务",
8585
+ target: "消息中心",
8586
+ value: 22
8587
+ }
8588
+ ]
8589
+ }
8590
+ }
8591
+ },
8592
+ {
8593
+ requirement: "各渠道投放费用和转化效果的关系,看分界和走向",
8594
+ spec: {
8595
+ type: "scatter",
8596
+ title: "投放-转化四象限",
8597
+ scatterData: [
8598
+ {
8599
+ x: 120,
8600
+ y: 32,
8601
+ label: "搜索"
8602
+ },
8603
+ {
8604
+ x: 260,
8605
+ y: 58,
8606
+ label: "信息流"
8607
+ },
8608
+ {
8609
+ x: 90,
8610
+ y: 18,
8611
+ label: "社群"
8612
+ },
8613
+ {
8614
+ x: 340,
8615
+ y: 71,
8616
+ label: "直播"
8617
+ }
8618
+ ],
8619
+ quadrant: { labels: [
8620
+ "低投高转",
8621
+ "高投高转",
8622
+ "低投低转",
8623
+ "高投低转"
8624
+ ] },
8625
+ scatterTrendline: "linear",
8626
+ pointLabels: true
8627
+ }
8252
8628
  }
8253
8629
  ];
8254
8630
  function formatExamples(maxCount = SPEC_EXAMPLES.length) {
@@ -8405,7 +8781,7 @@ function lintChartSpec(spec, opts = {}) {
8405
8781
  if (fixed.type === "bar" && Array.isArray(fixed.series) && fixed.series.length === 1 && Array.isArray(fixed.labels)) {
8406
8782
  const width = opts.width ?? DEFAULT_SIZE.width;
8407
8783
  const labels = fixed.labels.map(String);
8408
- const maxLabelWidth = Math.max(...labels.map((l) => estimateTextWidth(l, 12)), 0);
8784
+ const maxLabelWidth = maxOf(labels.map((l) => estimateTextWidth(l, 12)), 0);
8409
8785
  const slots = Math.max(1, (width - 80) / Math.max(1, labels.length));
8410
8786
  if (labels.length >= 7 && maxLabelWidth > slots * .9) {
8411
8787
  fixed.type = "horizontal-bar";
@@ -8451,4 +8827,4 @@ function lintChartSpec(spec, opts = {}) {
8451
8827
  };
8452
8828
  }
8453
8829
  //#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 };
8830
+ export { resolveChartPalette as $, squarifyTreemap as A, getToolboxBounds as B, chordHitTest as C, computeSunburstGeometry as D, boxplotHitTest as E, doughnutInnerRadius as F, createAnimation as G, funnelStepColor as H, matrixFieldExtent as I, isMissingValue as J, getPadding as K, scatterGroupColors as L, computeFacetGrids as M, computeMatrixCells as N, sunburstValue as O, computePieMaxRadius as P, CHART_PALETTES as Q, scatterPointPositions as R, arcHitTest as S, vennHitTest as T, getHeatmapCategories as U, calendarHitTest as V, xToCategoryIndex as W, updateAnimation as X, resolveTickExtendedRange as Y, waterfallSteps as Z, getSliderGeometry as _, SPEC_RULES as a, INTERACTION as at, zoomToSlice as b, inferColumns as c, resolveCursor as ct, parseNumeric as d, DEFAULT_I18N_ZH as et, chartOptionsSchema as f, getDataZoomConfig as g, createSvgRecorder as h, formatExamples as i, minOf as it, buildPieSlices as j, computeBins as k, looksLikeTime as l, renderChart as m, buildChartPrompt as n, UP_COLOR as nt, detectIntent as o, applyWheelZoom as ot, validateOptions as p, getTheme as q, SPEC_EXAMPLES as r, maxOf as rt, generateChartSpec as s, pickTooltipRows as st, lintChartSpec as t, DOWN_COLOR as tt, parseDataTable as u, windowToX as v, sankeyHitTest as w, ganttHitTest as x, xToPercent as y, getLegendBounds as z };