@wil-works/evoke-charts 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/{ai-BmXDig6M.js → ai-B5Fk3lBQ.js} +686 -310
- package/dist/ai.mjs +1 -1
- package/dist/evoke-charts.css +37 -31
- package/dist/index.mjs +280 -60
- package/package.json +1 -1
|
@@ -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(
|
|
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
|
-
|
|
920
|
-
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
if (
|
|
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 ??
|
|
1299
|
-
const max = xAxisConfig.max ??
|
|
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 ??
|
|
1340
|
-
const span = (options.xAxis?.max ??
|
|
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 ?
|
|
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
|
|
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
|
|
1972
|
-
const max = gauge.max
|
|
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/缺测为空档);
|
|
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
|
-
|
|
2165
|
-
if (typeof
|
|
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
|
|
2178
|
-
const
|
|
2179
|
-
const originX = plotArea.x + leftBand
|
|
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
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
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 * (
|
|
2394
|
+
y: originY + row * (cellH + gap) + cellH / 2
|
|
2232
2395
|
});
|
|
2233
2396
|
}
|
|
2234
2397
|
return {
|
|
2235
2398
|
cells,
|
|
2236
2399
|
cols,
|
|
2237
|
-
|
|
2400
|
+
rows,
|
|
2401
|
+
cellW,
|
|
2402
|
+
cellH,
|
|
2238
2403
|
gap,
|
|
2239
|
-
|
|
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
|
|
2255
|
-
const
|
|
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 +
|
|
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.
|
|
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.
|
|
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 =
|
|
2342
|
-
const maxValue =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
3179
|
+
drawPoints.forEach((p, k) => {
|
|
3001
3180
|
if (p) {
|
|
3002
3181
|
currentPts.push(p);
|
|
3003
|
-
currentIdxs.push(
|
|
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
|
-
|
|
3286
|
+
drawPoints.forEach((p, k) => {
|
|
3108
3287
|
if (!p) return;
|
|
3109
|
-
const isHover =
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
3167
|
-
if (p && visible[
|
|
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
|
|
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.
|
|
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
|
|
3484
|
+
canvasCtx.fillStyle = isHover ? mixColor(color, .12 * hovT, "#000000") : color;
|
|
3304
3485
|
canvasCtx.fill();
|
|
3305
|
-
if (isHover) {
|
|
3306
|
-
canvasCtx.
|
|
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
|
|
3596
|
+
canvasCtx.fillStyle = isHover ? mixColor(color, .12 * hovT, "#000000") : color;
|
|
3414
3597
|
canvasCtx.fill();
|
|
3415
|
-
if (isHover) {
|
|
3416
|
-
canvasCtx.
|
|
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
|
-
|
|
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 ?
|
|
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 /
|
|
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 =
|
|
3575
|
-
const xMax =
|
|
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 =
|
|
3782
|
-
const xMax =
|
|
3783
|
-
const yMin =
|
|
3784
|
-
const yMax =
|
|
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 =
|
|
3904
|
-
const max =
|
|
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
|
-
|
|
4138
|
-
|
|
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
|
-
|
|
4143
|
-
|
|
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 =
|
|
4361
|
-
const range =
|
|
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 =
|
|
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 =
|
|
4455
|
-
const minVal =
|
|
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 =
|
|
4669
|
-
const range =
|
|
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
|
-
|
|
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
|
-
|
|
4941
|
+
const steps = waterfallSteps(deltas, wf.totalIndices);
|
|
4777
4942
|
let prevEdgeX = null;
|
|
4778
4943
|
let prevExitY = null;
|
|
4779
4944
|
labels.forEach((_, i) => {
|
|
4780
|
-
const
|
|
4781
|
-
if (
|
|
4782
|
-
const
|
|
4783
|
-
const
|
|
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
|
|
4979
|
+
canvasCtx.fillStyle = isHover ? mixColor(color, .12 * hovT, "#000000") : color;
|
|
4829
4980
|
canvasCtx.fill();
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
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(
|
|
4994
|
+
canvasCtx.fillText(valueFormatter(step.value), cx, top - 4);
|
|
4843
4995
|
canvasCtx.restore();
|
|
4844
4996
|
}
|
|
4845
4997
|
prevEdgeX = x + barWidth;
|
|
4846
|
-
prevExitY = yFor(
|
|
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 ?
|
|
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 +
|
|
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 =
|
|
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 ?
|
|
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 +
|
|
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 =
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 + "
|
|
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.
|
|
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 =
|
|
5376
|
-
|
|
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.
|
|
5387
|
-
height: Math.max(2, (n.
|
|
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 =
|
|
5724
|
+
const valueMax = maxOf(circles.map((d) => d.value || 0));
|
|
5579
5725
|
if (!(valueMax > 0)) return null;
|
|
5580
|
-
const
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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 -
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
6180
|
-
let tmax =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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:
|
|
7023
|
-
max:
|
|
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:
|
|
7054
|
-
max:
|
|
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
|
-
|
|
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:
|
|
7119
|
-
max:
|
|
7271
|
+
min: minOf(allValues),
|
|
7272
|
+
max: maxOf(allValues)
|
|
7120
7273
|
});
|
|
7121
7274
|
else range = renderYAxis(renderCtx, "left", {
|
|
7122
|
-
min:
|
|
7123
|
-
max:
|
|
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: [
|
|
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: {
|
|
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,
|
|
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 =
|
|
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 =
|
|
8070
|
-
return max /
|
|
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 =
|
|
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 {
|
|
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 };
|