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