@wil-works/evoke-charts 0.2.0 → 0.3.1
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 +5 -1
- package/dist/evoke-charts.css +47 -32
- package/dist/index.mjs +1966 -258
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fragment, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, h, inject, nextTick, normalizeClass, normalizeStyle, onMounted, onUnmounted, openBlock, provide, ref, renderList, resolveDynamicComponent, toDisplayString, watch, withCtx } from "vue";
|
|
1
|
+
import { Fragment, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, h, inject, nextTick, normalizeClass, normalizeStyle, onMounted, onUnmounted, openBlock, provide, ref, renderList, renderSlot, resolveDynamicComponent, toDisplayString, watch, withCtx } from "vue";
|
|
2
2
|
//#region src/types.js
|
|
3
3
|
var DEFAULT_I18N_ZH = {
|
|
4
4
|
tooltip: {
|
|
@@ -121,19 +121,62 @@ function estimateTextWidth(text, fontSize = 12) {
|
|
|
121
121
|
}
|
|
122
122
|
return width;
|
|
123
123
|
}
|
|
124
|
+
/** 解析 `#rgb` / `#rrggbb` / `rgb()` / `rgba()` 为通道数组;无法识别返回 null */
|
|
125
|
+
function parseColorChannels(color) {
|
|
126
|
+
if (typeof color !== "string") return null;
|
|
127
|
+
const text = color.trim();
|
|
128
|
+
const hex = text.replace(/^#/, "");
|
|
129
|
+
if (/^[0-9a-fA-F]{3}$/.test(hex)) return [
|
|
130
|
+
0,
|
|
131
|
+
1,
|
|
132
|
+
2
|
|
133
|
+
].map((i) => parseInt(hex[i] + hex[i], 16));
|
|
134
|
+
if (/^[0-9a-fA-F]{6}$/.test(hex)) return [
|
|
135
|
+
0,
|
|
136
|
+
2,
|
|
137
|
+
4
|
|
138
|
+
].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
|
139
|
+
const rgbMatch = text.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);
|
|
140
|
+
if (rgbMatch) return [
|
|
141
|
+
1,
|
|
142
|
+
2,
|
|
143
|
+
3
|
|
144
|
+
].map((i) => Math.round(parseFloat(rgbMatch[i])));
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
/** 浅色底判断(YIQ):底色可能来自令牌、混合出的 `rgb()` 或自定义主题 */
|
|
148
|
+
function isLightColor(color) {
|
|
149
|
+
const ch = parseColorChannels(color);
|
|
150
|
+
if (!ch) return true;
|
|
151
|
+
return (ch[0] * 299 + ch[1] * 587 + ch[2] * 114) / 1e3 >= 150;
|
|
152
|
+
}
|
|
153
|
+
function relativeLuminance(ch) {
|
|
154
|
+
const [r, g, b] = ch.map((v) => {
|
|
155
|
+
const s = v / 255;
|
|
156
|
+
return s <= .03928 ? s / 12.92 : Math.pow((s + .055) / 1.055, 2.4);
|
|
157
|
+
});
|
|
158
|
+
return .2126 * r + .7152 * g + .0722 * b;
|
|
159
|
+
}
|
|
160
|
+
/** 底色上的文字取反色:白字/深字谁对比度高用谁——中间调底色(如逐层提亮后的蓝)
|
|
161
|
+
* 用亮度阈值会误判成白字,导致浅底白字看不清 */
|
|
124
162
|
function getContrastText(bgColor) {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
163
|
+
const ch = parseColorChannels(bgColor);
|
|
164
|
+
if (!ch) return "#111827";
|
|
165
|
+
const luminance = relativeLuminance(ch);
|
|
166
|
+
const againstWhite = 1.05 / (luminance + .05);
|
|
167
|
+
return (luminance + .05) / (relativeLuminance([
|
|
168
|
+
17,
|
|
169
|
+
24,
|
|
170
|
+
39
|
|
171
|
+
]) + .05) >= againstWhite ? "#111827" : "#ffffff";
|
|
172
|
+
}
|
|
173
|
+
/** 颜色向 target 混合(ratio 0-1),返回 `#rrggbb`:同色系逐层提亮/压暗用 */
|
|
174
|
+
function mixColor(color, ratio, target) {
|
|
175
|
+
const from = parseColorChannels(color);
|
|
176
|
+
const to = parseColorChannels(target);
|
|
177
|
+
if (!from || !to) return color;
|
|
178
|
+
const r = Math.max(0, Math.min(1, ratio));
|
|
179
|
+
return `#${from.map((v, i) => Math.round(v + (to[i] - v) * r)).map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
137
180
|
}
|
|
138
181
|
function isMissingValue(v) {
|
|
139
182
|
return v === null || v === void 0 || Number.isNaN(v);
|
|
@@ -320,6 +363,15 @@ function estimateYAxisLeft(options) {
|
|
|
320
363
|
const axisConfig = options.yAxis || {};
|
|
321
364
|
if (axisConfig.type === "log") return 65;
|
|
322
365
|
try {
|
|
366
|
+
if (options.type === "horizontal-bar") {
|
|
367
|
+
const formatter = axisConfig.formatter;
|
|
368
|
+
const maxW = (options.labels || []).reduce((m, l) => {
|
|
369
|
+
const label = formatter ? String(formatter(l)) : String(l);
|
|
370
|
+
return Math.max(m, estimateTextWidth(label, 12));
|
|
371
|
+
}, 0);
|
|
372
|
+
if (!maxW) return 65;
|
|
373
|
+
return Math.max(40, Math.min(140, Math.round(maxW + 26)));
|
|
374
|
+
}
|
|
323
375
|
const range = calculateRange(options, /* @__PURE__ */ new Set(), false);
|
|
324
376
|
const min = axisConfig.min ?? range?.min;
|
|
325
377
|
const max = axisConfig.max ?? range?.max;
|
|
@@ -420,10 +472,12 @@ function getPadding(options, containerWidth = 600) {
|
|
|
420
472
|
}
|
|
421
473
|
const pad = resolveUserPadding(options);
|
|
422
474
|
const xAxisHidden = options.xAxis?.show === false;
|
|
475
|
+
const yAxisWidth = options.yAxis?.width;
|
|
476
|
+
const yAxisRightWidth = options.yAxisRight?.width;
|
|
423
477
|
let top = (pad.top ?? 18) + titleBlockHeight(options) + (legendPosition === "top" ? LEGEND_BAND + legendRowsExtra : 0);
|
|
424
478
|
let bottom = (pad.bottom ?? (xAxisHidden ? 12 : 46)) + (legendPosition === "bottom" ? LEGEND_BAND + legendRowsExtra : 0);
|
|
425
|
-
let left = pad.left ?? estimateYAxisLeft(options);
|
|
426
|
-
let right = pad.right ?? (hasYAxisRight ? 65 : 24);
|
|
479
|
+
let left = pad.left ?? (typeof yAxisWidth === "number" && yAxisWidth > 0 ? yAxisWidth : estimateYAxisLeft(options));
|
|
480
|
+
let right = pad.right ?? (typeof yAxisRightWidth === "number" && yAxisRightWidth > 0 ? yAxisRightWidth : hasYAxisRight ? 65 : 24);
|
|
427
481
|
if (legendPosition === "left") left += 80;
|
|
428
482
|
if (legendPosition === "right") right += 80;
|
|
429
483
|
if (hasXAxis && hasXAxisTitle) return {
|
|
@@ -639,12 +693,15 @@ function calculateRange(options, hiddenSeries, useRightAxis) {
|
|
|
639
693
|
const range = max - min || 1;
|
|
640
694
|
if (min >= 0) return {
|
|
641
695
|
min: 0,
|
|
642
|
-
max: max + range * .1
|
|
643
|
-
};
|
|
644
|
-
else return {
|
|
645
|
-
min: min - range * .1,
|
|
646
|
-
max: max + range * .1
|
|
696
|
+
max: max + Math.max(range * .1, max * .05)
|
|
647
697
|
};
|
|
698
|
+
else {
|
|
699
|
+
const pad = Math.max(range * .1, Math.max(Math.abs(max), Math.abs(min)) * .05);
|
|
700
|
+
return {
|
|
701
|
+
min: min - pad,
|
|
702
|
+
max: max + pad
|
|
703
|
+
};
|
|
704
|
+
}
|
|
648
705
|
}
|
|
649
706
|
function applyLogTransform(options) {
|
|
650
707
|
if (options.yAxis?.type !== "log") return options;
|
|
@@ -769,6 +826,20 @@ function truncateLabel(canvasCtx, label, maxWidth) {
|
|
|
769
826
|
}
|
|
770
827
|
return label.slice(0, lo) + "…";
|
|
771
828
|
}
|
|
829
|
+
/**
|
|
830
|
+
* 抽稀刻度:保留首末两端覆盖,步长取整数倍使数量 ≤ maxCount。
|
|
831
|
+
* nice 算法的 step 候选会让实际刻度数超出请求数(如 ticks:3 得到 5 档),
|
|
832
|
+
* 显式 ticks 视为硬上限,超出的中间档按均匀步长抽稀(round 值不丢)。
|
|
833
|
+
*/
|
|
834
|
+
function thinTickValues(values, maxCount) {
|
|
835
|
+
const limit = Math.max(2, maxCount);
|
|
836
|
+
if (values.length <= limit) return values;
|
|
837
|
+
let stride = Math.ceil((values.length - 1) / (limit - 1));
|
|
838
|
+
while (stride < values.length - 1 && (values.length - 1) % stride !== 0) stride += 1;
|
|
839
|
+
const thinned = [];
|
|
840
|
+
for (let i = 0; i < values.length; i += stride) thinned.push(values[i]);
|
|
841
|
+
return thinned;
|
|
842
|
+
}
|
|
772
843
|
function drawAxisLine(canvasCtx, cfg, theme, x1, y1, x2, y2) {
|
|
773
844
|
if (cfg?.axisLine?.show === false) return;
|
|
774
845
|
canvasCtx.save();
|
|
@@ -795,7 +866,10 @@ function drawAxisTick(canvasCtx, cfg, theme, x, y, dx, dy) {
|
|
|
795
866
|
function renderYAxis(ctx, side = "left", dataRange) {
|
|
796
867
|
const { ctx: canvasCtx, theme, plotArea, options, width } = ctx;
|
|
797
868
|
const axisConfig = side === "left" ? options.yAxis || {} : options.yAxisRight || {};
|
|
798
|
-
const
|
|
869
|
+
const requestedTicks = axisConfig.ticks || 5;
|
|
870
|
+
const maxByHeight = Math.max(2, Math.floor(plotArea.height / 24) + 1);
|
|
871
|
+
const tickCap = axisConfig.ticks != null ? Math.min(requestedTicks, maxByHeight) : maxByHeight;
|
|
872
|
+
const needsThin = axisConfig.ticks != null || maxByHeight < requestedTicks;
|
|
799
873
|
const isLog = side === "left" && axisConfig.type === "log";
|
|
800
874
|
let computedMax;
|
|
801
875
|
let computedMin;
|
|
@@ -810,7 +884,8 @@ function renderYAxis(ctx, side = "left", dataRange) {
|
|
|
810
884
|
}
|
|
811
885
|
}
|
|
812
886
|
const max = axisConfig.max ?? computedMax;
|
|
813
|
-
const { min: actualMin, max: actualMax, tickValues } = resolveTickExtendedRange(axisConfig.min ?? computedMin, max,
|
|
887
|
+
const { min: actualMin, max: actualMax, tickValues: niceTicks } = resolveTickExtendedRange(axisConfig.min ?? computedMin, max, requestedTicks);
|
|
888
|
+
const tickValues = needsThin ? thinTickValues(niceTicks, tickCap) : niceTicks;
|
|
814
889
|
canvasCtx.save();
|
|
815
890
|
canvasCtx.strokeStyle = theme.gridColor;
|
|
816
891
|
canvasCtx.fillStyle = theme.textColorSecondary;
|
|
@@ -1041,6 +1116,10 @@ function xToCategoryIndex(options, plotArea, labels, x) {
|
|
|
1041
1116
|
return Math.round((x - plotArea.x) / step);
|
|
1042
1117
|
}
|
|
1043
1118
|
function renderAnnotations(ctx, yRange) {
|
|
1119
|
+
renderLegacyAnnotation(ctx, yRange);
|
|
1120
|
+
renderNarrativeAnnotations(ctx, yRange);
|
|
1121
|
+
}
|
|
1122
|
+
function renderLegacyAnnotation(ctx, yRange) {
|
|
1044
1123
|
const { ctx: canvasCtx, theme, plotArea, options } = ctx;
|
|
1045
1124
|
const annotation = options.annotation;
|
|
1046
1125
|
if (!annotation) return;
|
|
@@ -1103,6 +1182,196 @@ function renderAnnotations(ctx, yRange) {
|
|
|
1103
1182
|
canvasCtx.restore();
|
|
1104
1183
|
});
|
|
1105
1184
|
}
|
|
1185
|
+
var ANNOTATION_ANCHORS = {
|
|
1186
|
+
top: [
|
|
1187
|
+
0,
|
|
1188
|
+
-1,
|
|
1189
|
+
"center",
|
|
1190
|
+
"bottom"
|
|
1191
|
+
],
|
|
1192
|
+
bottom: [
|
|
1193
|
+
0,
|
|
1194
|
+
1,
|
|
1195
|
+
"center",
|
|
1196
|
+
"top"
|
|
1197
|
+
],
|
|
1198
|
+
left: [
|
|
1199
|
+
-1,
|
|
1200
|
+
0,
|
|
1201
|
+
"end",
|
|
1202
|
+
"middle"
|
|
1203
|
+
],
|
|
1204
|
+
right: [
|
|
1205
|
+
1,
|
|
1206
|
+
0,
|
|
1207
|
+
"start",
|
|
1208
|
+
"middle"
|
|
1209
|
+
],
|
|
1210
|
+
"top-left": [
|
|
1211
|
+
-1,
|
|
1212
|
+
-1,
|
|
1213
|
+
"end",
|
|
1214
|
+
"bottom"
|
|
1215
|
+
],
|
|
1216
|
+
"top-right": [
|
|
1217
|
+
1,
|
|
1218
|
+
-1,
|
|
1219
|
+
"start",
|
|
1220
|
+
"bottom"
|
|
1221
|
+
],
|
|
1222
|
+
"bottom-left": [
|
|
1223
|
+
-1,
|
|
1224
|
+
1,
|
|
1225
|
+
"end",
|
|
1226
|
+
"top"
|
|
1227
|
+
],
|
|
1228
|
+
"bottom-right": [
|
|
1229
|
+
1,
|
|
1230
|
+
1,
|
|
1231
|
+
"start",
|
|
1232
|
+
"top"
|
|
1233
|
+
]
|
|
1234
|
+
};
|
|
1235
|
+
function resolveAnnotationX(ctx, x, xPx) {
|
|
1236
|
+
if (typeof xPx === "number") return xPx;
|
|
1237
|
+
const { plotArea, options } = ctx;
|
|
1238
|
+
const labels = options.labels || [];
|
|
1239
|
+
const idx = typeof x === "number" ? x : labels.indexOf(x);
|
|
1240
|
+
if (idx < 0) return NaN;
|
|
1241
|
+
return categoryToX(options, plotArea, labels, idx);
|
|
1242
|
+
}
|
|
1243
|
+
function resolveAnnotationY(ctx, y, yPx, yRange) {
|
|
1244
|
+
if (typeof yPx === "number") return yPx;
|
|
1245
|
+
if (typeof y !== "number" || !yRange) return NaN;
|
|
1246
|
+
const { plotArea } = ctx;
|
|
1247
|
+
return plotArea.y + plotArea.height - (y - yRange.min) / (yRange.max - yRange.min) * plotArea.height;
|
|
1248
|
+
}
|
|
1249
|
+
function annotationSeriesColor(ctx, ref) {
|
|
1250
|
+
const { theme, options } = ctx;
|
|
1251
|
+
const series = options.series || [];
|
|
1252
|
+
if (typeof ref === "number") return series[ref]?.color || theme.colors[ref] || theme.colors[0] || "#175DFF";
|
|
1253
|
+
if (typeof ref === "string") {
|
|
1254
|
+
const i = series.findIndex((s) => s.name === ref);
|
|
1255
|
+
if (i >= 0) return series[i].color || theme.colors[i] || theme.colors[0] || "#175DFF";
|
|
1256
|
+
}
|
|
1257
|
+
return theme.colors[0] || "#175DFF";
|
|
1258
|
+
}
|
|
1259
|
+
function renderNarrativeAnnotations(ctx, yRange) {
|
|
1260
|
+
const { ctx: canvasCtx, theme, plotArea, options } = ctx;
|
|
1261
|
+
const list = options.annotations || [];
|
|
1262
|
+
if (list.length === 0) return;
|
|
1263
|
+
const secondary = theme.textColorSecondary;
|
|
1264
|
+
list.forEach((a) => {
|
|
1265
|
+
if (!a || !a.type) return;
|
|
1266
|
+
if (a.type === "region") {
|
|
1267
|
+
const x1 = resolveAnnotationX(ctx, a.from);
|
|
1268
|
+
const x2 = resolveAnnotationX(ctx, a.to);
|
|
1269
|
+
if (!Number.isFinite(x1) || !Number.isFinite(x2)) return;
|
|
1270
|
+
const labels = options.labels || [];
|
|
1271
|
+
const band = labels.length > 0 ? plotArea.width / labels.length : 0;
|
|
1272
|
+
const left = Math.min(x1, x2) - band / 2;
|
|
1273
|
+
const width = Math.abs(x2 - x1) + band;
|
|
1274
|
+
canvasCtx.save();
|
|
1275
|
+
canvasCtx.globalAlpha = .06;
|
|
1276
|
+
canvasCtx.fillStyle = a.color || theme.colors[0] || "#175DFF";
|
|
1277
|
+
canvasCtx.fillRect(left, plotArea.y, width, plotArea.height);
|
|
1278
|
+
canvasCtx.restore();
|
|
1279
|
+
if (a.label) {
|
|
1280
|
+
canvasCtx.save();
|
|
1281
|
+
canvasCtx.fillStyle = secondary;
|
|
1282
|
+
canvasCtx.font = "italic 400 11px Inter, sans-serif";
|
|
1283
|
+
canvasCtx.textAlign = "left";
|
|
1284
|
+
canvasCtx.textBaseline = "top";
|
|
1285
|
+
canvasCtx.fillText(a.label, left + 4, plotArea.y + 6);
|
|
1286
|
+
canvasCtx.restore();
|
|
1287
|
+
}
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
const px = resolveAnnotationX(ctx, a.x, a.xPx);
|
|
1291
|
+
const py = resolveAnnotationY(ctx, a.y, a.yPx, yRange);
|
|
1292
|
+
if (!Number.isFinite(px) || !Number.isFinite(py)) return;
|
|
1293
|
+
const color = a.color || annotationSeriesColor(ctx, a.series);
|
|
1294
|
+
canvasCtx.save();
|
|
1295
|
+
if (a.type === "point") {
|
|
1296
|
+
canvasCtx.fillStyle = color;
|
|
1297
|
+
canvasCtx.beginPath();
|
|
1298
|
+
canvasCtx.arc(px, py, 3, 0, Math.PI * 2);
|
|
1299
|
+
canvasCtx.fill();
|
|
1300
|
+
canvasCtx.globalAlpha = .85;
|
|
1301
|
+
canvasCtx.strokeStyle = color;
|
|
1302
|
+
canvasCtx.lineWidth = 1.5;
|
|
1303
|
+
canvasCtx.beginPath();
|
|
1304
|
+
canvasCtx.arc(px, py, 6, 0, Math.PI * 2);
|
|
1305
|
+
canvasCtx.stroke();
|
|
1306
|
+
canvasCtx.restore();
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (a.type === "delta") {
|
|
1310
|
+
const up = a.direction !== "down";
|
|
1311
|
+
const deltaColor = a.color || (up ? options.candleUpColor || "#dc2626" : options.candleDownColor || "#16a34a");
|
|
1312
|
+
const text = a.text || a.label || "";
|
|
1313
|
+
canvasCtx.fillStyle = deltaColor;
|
|
1314
|
+
canvasCtx.beginPath();
|
|
1315
|
+
if (up) {
|
|
1316
|
+
canvasCtx.moveTo(px, py - 16);
|
|
1317
|
+
canvasCtx.lineTo(px - 5, py - 9);
|
|
1318
|
+
canvasCtx.lineTo(px + 5, py - 9);
|
|
1319
|
+
} else {
|
|
1320
|
+
canvasCtx.moveTo(px, py + 16);
|
|
1321
|
+
canvasCtx.lineTo(px - 5, py + 9);
|
|
1322
|
+
canvasCtx.lineTo(px + 5, py + 9);
|
|
1323
|
+
}
|
|
1324
|
+
canvasCtx.closePath();
|
|
1325
|
+
canvasCtx.fill();
|
|
1326
|
+
if (text) {
|
|
1327
|
+
canvasCtx.fillStyle = deltaColor;
|
|
1328
|
+
canvasCtx.font = "600 11px Inter, sans-serif";
|
|
1329
|
+
canvasCtx.textAlign = "center";
|
|
1330
|
+
if (up) {
|
|
1331
|
+
canvasCtx.textBaseline = "bottom";
|
|
1332
|
+
canvasCtx.fillText(text, px, py - 20);
|
|
1333
|
+
} else {
|
|
1334
|
+
canvasCtx.textBaseline = "top";
|
|
1335
|
+
canvasCtx.fillText(text, px, py + 20);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
canvasCtx.restore();
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
if (a.type === "callout") {
|
|
1342
|
+
const [dx, dy, align, baseline] = ANNOTATION_ANCHORS[a.anchor] || ANNOTATION_ANCHORS["top-right"];
|
|
1343
|
+
const lx = px + dx * 16 + (a.offsetX || 0);
|
|
1344
|
+
const ly = py + dy * 16 + (a.offsetY || 0);
|
|
1345
|
+
canvasCtx.fillStyle = color;
|
|
1346
|
+
canvasCtx.strokeStyle = color;
|
|
1347
|
+
canvasCtx.lineWidth = 1;
|
|
1348
|
+
canvasCtx.setLineDash([3, 3]);
|
|
1349
|
+
canvasCtx.beginPath();
|
|
1350
|
+
canvasCtx.arc(px, py, 2, 0, Math.PI * 2);
|
|
1351
|
+
canvasCtx.fill();
|
|
1352
|
+
canvasCtx.beginPath();
|
|
1353
|
+
canvasCtx.moveTo(px + dx * 2, py + dy * 2);
|
|
1354
|
+
canvasCtx.lineTo(lx - dx * 2, ly - dy * 2);
|
|
1355
|
+
canvasCtx.stroke();
|
|
1356
|
+
canvasCtx.setLineDash([]);
|
|
1357
|
+
if (a.label) {
|
|
1358
|
+
canvasCtx.fillStyle = a.textColor || secondary;
|
|
1359
|
+
canvasCtx.font = "italic 400 11px Inter, sans-serif";
|
|
1360
|
+
canvasCtx.textAlign = align;
|
|
1361
|
+
canvasCtx.textBaseline = baseline;
|
|
1362
|
+
canvasCtx.fillText(a.label, lx + dx * 2, ly + dy * 2);
|
|
1363
|
+
}
|
|
1364
|
+
canvasCtx.restore();
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
canvasCtx.fillStyle = a.color || secondary;
|
|
1368
|
+
canvasCtx.font = `italic ${a.fontWeight || 400} 11px Inter, sans-serif`;
|
|
1369
|
+
canvasCtx.textAlign = "center";
|
|
1370
|
+
canvasCtx.textBaseline = "bottom";
|
|
1371
|
+
canvasCtx.fillText(a.text || a.label || "", px + (a.offsetX || 0), py + (a.offsetY !== void 0 ? a.offsetY : -6));
|
|
1372
|
+
canvasCtx.restore();
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1106
1375
|
function renderHoverHighlight(ctx, points) {
|
|
1107
1376
|
if (ctx.hoverIndex < 0) return;
|
|
1108
1377
|
const { ctx: canvasCtx, theme, plotArea } = ctx;
|
|
@@ -1267,6 +1536,12 @@ function collectLegendItems(options, theme, hiddenSeries) {
|
|
|
1267
1536
|
color: d.color || theme.colors[i % theme.colors.length],
|
|
1268
1537
|
hidden: hiddenSeries.has(d.label || "")
|
|
1269
1538
|
}));
|
|
1539
|
+
if (options.type === "scatter") return (options.scatterData || []).map((d, i) => ({
|
|
1540
|
+
name: d.label || `P${i + 1}`,
|
|
1541
|
+
label: fmt(d.label || `P${i + 1}`),
|
|
1542
|
+
color: d.color || theme.colors[i % theme.colors.length],
|
|
1543
|
+
hidden: hiddenSeries.has(d.label || `P${i + 1}`)
|
|
1544
|
+
}));
|
|
1270
1545
|
return (options.series || []).map((s, i) => ({
|
|
1271
1546
|
name: s.name,
|
|
1272
1547
|
label: fmt(s.name),
|
|
@@ -1445,12 +1720,69 @@ function renderToolbox(ctx, hoveredFeature) {
|
|
|
1445
1720
|
canvasCtx.restore();
|
|
1446
1721
|
});
|
|
1447
1722
|
}
|
|
1723
|
+
/**
|
|
1724
|
+
* items: [{ angle, r, text }](angle 扇区中角,r 引线锚点半径)
|
|
1725
|
+
* cfg: { canvasCtx, centerX, centerY, plotArea, theme } + 可选覆写
|
|
1726
|
+
*/
|
|
1727
|
+
function drawCalloutLabels(items, cfg) {
|
|
1728
|
+
const { canvasCtx, centerX, centerY, plotArea, theme, radialLen = 14, stubLen = 12, textGap = 5, minGap = 16, maxShift = 10, font = "12px Inter, sans-serif", textColor = theme.textColor, lineColor = theme.textColorSecondary } = cfg;
|
|
1729
|
+
if (!canvasCtx || items.length === 0) return;
|
|
1730
|
+
const topLimit = plotArea.y + 4;
|
|
1731
|
+
const bottomLimit = plotArea.y + plotArea.height - 4;
|
|
1732
|
+
const layoutSide = (side, sign) => {
|
|
1733
|
+
const sorted = side.map((it) => ({
|
|
1734
|
+
...it,
|
|
1735
|
+
y: centerY + Math.sin(it.angle) * (it.r + radialLen),
|
|
1736
|
+
visible: true
|
|
1737
|
+
})).sort((a, b) => a.y - b.y);
|
|
1738
|
+
let lastY = -Infinity;
|
|
1739
|
+
sorted.forEach((it) => {
|
|
1740
|
+
let y = it.y;
|
|
1741
|
+
if (y < lastY + minGap) {
|
|
1742
|
+
const shifted = lastY + minGap;
|
|
1743
|
+
if (shifted - it.y <= maxShift && shifted <= bottomLimit) y = shifted;
|
|
1744
|
+
else {
|
|
1745
|
+
it.visible = false;
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
if (y > bottomLimit || y < topLimit) {
|
|
1750
|
+
it.visible = false;
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
it.y = y;
|
|
1754
|
+
lastY = y;
|
|
1755
|
+
});
|
|
1756
|
+
sorted.forEach((it) => {
|
|
1757
|
+
if (!it.visible) return;
|
|
1758
|
+
const dirX = Math.cos(it.angle);
|
|
1759
|
+
const dirY = Math.sin(it.angle);
|
|
1760
|
+
const p1x = centerX + dirX * (it.r + radialLen);
|
|
1761
|
+
const p1y = centerY + dirY * (it.r + radialLen);
|
|
1762
|
+
const p2x = p1x + sign * stubLen;
|
|
1763
|
+
canvasCtx.save();
|
|
1764
|
+
canvasCtx.strokeStyle = lineColor;
|
|
1765
|
+
canvasCtx.lineWidth = 1;
|
|
1766
|
+
canvasCtx.beginPath();
|
|
1767
|
+
canvasCtx.moveTo(centerX + dirX * it.r, centerY + dirY * it.r);
|
|
1768
|
+
canvasCtx.lineTo(p1x, p1y);
|
|
1769
|
+
canvasCtx.lineTo(p2x, it.y);
|
|
1770
|
+
canvasCtx.stroke();
|
|
1771
|
+
canvasCtx.restore();
|
|
1772
|
+
canvasCtx.save();
|
|
1773
|
+
canvasCtx.fillStyle = textColor;
|
|
1774
|
+
canvasCtx.font = font;
|
|
1775
|
+
canvasCtx.textAlign = sign === 1 ? "left" : "right";
|
|
1776
|
+
canvasCtx.textBaseline = "middle";
|
|
1777
|
+
canvasCtx.fillText(it.text, p2x + sign * textGap, it.y);
|
|
1778
|
+
canvasCtx.restore();
|
|
1779
|
+
});
|
|
1780
|
+
};
|
|
1781
|
+
layoutSide(items.filter((it) => Math.cos(it.angle) >= 0), 1);
|
|
1782
|
+
layoutSide(items.filter((it) => Math.cos(it.angle) < 0), -1);
|
|
1783
|
+
}
|
|
1448
1784
|
//#endregion
|
|
1449
1785
|
//#region src/renderer/charts-basic.js
|
|
1450
|
-
var PIE_RADIAL_LEN = 14;
|
|
1451
|
-
var PIE_STUB_LEN = 12;
|
|
1452
|
-
var PIE_TEXT_GAP = 5;
|
|
1453
|
-
var PIE_MAX_SHIFT = 10;
|
|
1454
1786
|
function computePieMaxRadius(pieData, options, plotArea) {
|
|
1455
1787
|
const total = pieData.reduce((sum, d) => sum + d.value, 0);
|
|
1456
1788
|
if (total <= 0) return 30;
|
|
@@ -2069,70 +2401,21 @@ function renderPieChart(ctx, isDoughnut, isRose = false) {
|
|
|
2069
2401
|
if (progress > .9) {
|
|
2070
2402
|
const percentage = (data.value / total * 100).toFixed(1);
|
|
2071
2403
|
const text = percentMode ? `${percentage}%` : `${data.label || ""} (${percentage}%)`;
|
|
2072
|
-
const r = isRose ? roseRadius : maxRadius;
|
|
2073
2404
|
labelItems.push({
|
|
2074
2405
|
angle: midAngle,
|
|
2075
|
-
r,
|
|
2076
|
-
|
|
2077
|
-
text,
|
|
2078
|
-
y: centerY + Math.sin(midAngle) * (r + PIE_RADIAL_LEN),
|
|
2079
|
-
visible: true
|
|
2406
|
+
r: isRose ? roseRadius : maxRadius,
|
|
2407
|
+
text
|
|
2080
2408
|
});
|
|
2081
2409
|
}
|
|
2082
2410
|
startAngle = endAngle;
|
|
2083
2411
|
});
|
|
2084
|
-
if (labelItems.length > 0 && progress > .9) {
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
sorted.forEach((it) => {
|
|
2092
|
-
let y = it.y;
|
|
2093
|
-
if (y < lastY + minGap) {
|
|
2094
|
-
const shifted = lastY + minGap;
|
|
2095
|
-
if (shifted - it.y <= PIE_MAX_SHIFT && shifted <= bottomLimit) y = shifted;
|
|
2096
|
-
else {
|
|
2097
|
-
it.visible = false;
|
|
2098
|
-
return;
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
if (y > bottomLimit || y < topLimit) {
|
|
2102
|
-
it.visible = false;
|
|
2103
|
-
return;
|
|
2104
|
-
}
|
|
2105
|
-
it.y = y;
|
|
2106
|
-
lastY = y;
|
|
2107
|
-
});
|
|
2108
|
-
sorted.forEach((it) => {
|
|
2109
|
-
if (!it.visible) return;
|
|
2110
|
-
const dirX = Math.cos(it.angle);
|
|
2111
|
-
const dirY = Math.sin(it.angle);
|
|
2112
|
-
const p1x = centerX + dirX * (it.r + PIE_RADIAL_LEN);
|
|
2113
|
-
const p1y = centerY + dirY * (it.r + PIE_RADIAL_LEN);
|
|
2114
|
-
const p2x = p1x + sign * PIE_STUB_LEN;
|
|
2115
|
-
canvasCtx.save();
|
|
2116
|
-
canvasCtx.strokeStyle = theme.textColorSecondary;
|
|
2117
|
-
canvasCtx.lineWidth = 1;
|
|
2118
|
-
canvasCtx.beginPath();
|
|
2119
|
-
canvasCtx.moveTo(centerX + dirX * it.r, centerY + dirY * it.r);
|
|
2120
|
-
canvasCtx.lineTo(p1x, p1y);
|
|
2121
|
-
canvasCtx.lineTo(p2x, it.y);
|
|
2122
|
-
canvasCtx.stroke();
|
|
2123
|
-
canvasCtx.restore();
|
|
2124
|
-
canvasCtx.save();
|
|
2125
|
-
canvasCtx.fillStyle = theme.textColor;
|
|
2126
|
-
canvasCtx.font = "12px Inter, sans-serif";
|
|
2127
|
-
canvasCtx.textAlign = sign === 1 ? "left" : "right";
|
|
2128
|
-
canvasCtx.textBaseline = "middle";
|
|
2129
|
-
canvasCtx.fillText(it.text, p2x + sign * PIE_TEXT_GAP, it.y);
|
|
2130
|
-
canvasCtx.restore();
|
|
2131
|
-
});
|
|
2132
|
-
};
|
|
2133
|
-
layoutSide(labelItems.filter((it) => Math.cos(it.angle) >= 0), 1);
|
|
2134
|
-
layoutSide(labelItems.filter((it) => Math.cos(it.angle) < 0), -1);
|
|
2135
|
-
}
|
|
2412
|
+
if (labelItems.length > 0 && progress > .9) drawCalloutLabels(labelItems, {
|
|
2413
|
+
canvasCtx,
|
|
2414
|
+
centerX,
|
|
2415
|
+
centerY,
|
|
2416
|
+
plotArea,
|
|
2417
|
+
theme
|
|
2418
|
+
});
|
|
2136
2419
|
if (isDoughnut && options.gauge?.value !== void 0) {
|
|
2137
2420
|
canvasCtx.save();
|
|
2138
2421
|
canvasCtx.fillStyle = theme.textColor;
|
|
@@ -2175,6 +2458,10 @@ function renderScatterChart(ctx, yRange) {
|
|
|
2175
2458
|
const points = [];
|
|
2176
2459
|
const yFactor = Math.min(ctx.progress * 1.2, 1);
|
|
2177
2460
|
scatterData.forEach((point, i) => {
|
|
2461
|
+
if (ctx.hiddenSeries.has(point.label || `P${i + 1}`)) {
|
|
2462
|
+
points.push([NaN, NaN]);
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2178
2465
|
const x = scale.toX(point.x);
|
|
2179
2466
|
const finalY = scale.toY(point.y);
|
|
2180
2467
|
const animatedY = plotArea.y + plotArea.height - (plotArea.y + plotArea.height - finalY) * yFactor;
|
|
@@ -2327,6 +2614,20 @@ function renderRadarChart(ctx) {
|
|
|
2327
2614
|
canvasCtx.stroke();
|
|
2328
2615
|
}
|
|
2329
2616
|
canvasCtx.restore();
|
|
2617
|
+
const sharedMax = indicators[0].max;
|
|
2618
|
+
const sharedMin = indicators[0].min || 0;
|
|
2619
|
+
if (sharedMax != null && indicators.every((ind) => (ind.max ?? sharedMax) === sharedMax && (ind.min || 0) === sharedMin)) {
|
|
2620
|
+
canvasCtx.save();
|
|
2621
|
+
canvasCtx.fillStyle = theme.textColorSecondary;
|
|
2622
|
+
canvasCtx.font = "10px Inter, sans-serif";
|
|
2623
|
+
canvasCtx.textAlign = "left";
|
|
2624
|
+
canvasCtx.textBaseline = "middle";
|
|
2625
|
+
for (let level = 1; level <= 5; level++) {
|
|
2626
|
+
const v = sharedMin + (sharedMax - sharedMin) * level / 5;
|
|
2627
|
+
canvasCtx.fillText(Number.isInteger(sharedMax) ? String(Math.round(v)) : v.toFixed(1), centerX + 5, centerY - radius * level / 5);
|
|
2628
|
+
}
|
|
2629
|
+
canvasCtx.restore();
|
|
2630
|
+
}
|
|
2330
2631
|
canvasCtx.save();
|
|
2331
2632
|
canvasCtx.fillStyle = theme.textColorSecondary;
|
|
2332
2633
|
canvasCtx.font = "12px Inter, sans-serif";
|
|
@@ -2380,7 +2681,15 @@ function renderRadarChart(ctx) {
|
|
|
2380
2681
|
else canvasCtx.lineTo(p[0], p[1]);
|
|
2381
2682
|
});
|
|
2382
2683
|
canvasCtx.closePath();
|
|
2383
|
-
|
|
2684
|
+
if (/^#[0-9a-fA-F]{6}$/.test(color)) {
|
|
2685
|
+
const grad = canvasCtx.createLinearGradient(0, centerY - radius, 0, centerY + radius);
|
|
2686
|
+
grad.addColorStop(0, color + (hoverIndex >= 0 ? "59" : "40"));
|
|
2687
|
+
grad.addColorStop(1, color + "08");
|
|
2688
|
+
canvasCtx.fillStyle = grad;
|
|
2689
|
+
} else {
|
|
2690
|
+
canvasCtx.fillStyle = color;
|
|
2691
|
+
canvasCtx.globalAlpha = alpha * .18;
|
|
2692
|
+
}
|
|
2384
2693
|
canvasCtx.fill();
|
|
2385
2694
|
canvasCtx.restore();
|
|
2386
2695
|
}
|
|
@@ -2393,15 +2702,17 @@ function renderRadarChart(ctx) {
|
|
|
2393
2702
|
});
|
|
2394
2703
|
canvasCtx.closePath();
|
|
2395
2704
|
canvasCtx.strokeStyle = color;
|
|
2396
|
-
canvasCtx.lineWidth = hoverIndex >= 0 ? 2.5 :
|
|
2705
|
+
canvasCtx.lineWidth = hoverIndex >= 0 ? 2.5 : 1.5;
|
|
2397
2706
|
canvasCtx.stroke();
|
|
2398
2707
|
canvasCtx.restore();
|
|
2399
2708
|
canvasCtx.save();
|
|
2400
2709
|
canvasCtx.globalAlpha = alpha;
|
|
2710
|
+
const showSymbol = s.showSymbol === true;
|
|
2401
2711
|
points.forEach((p, i) => {
|
|
2402
2712
|
const isHover = i === hoverIndex;
|
|
2713
|
+
if (!showSymbol && !isHover) return;
|
|
2403
2714
|
canvasCtx.beginPath();
|
|
2404
|
-
canvasCtx.arc(p[0], p[1], isHover ? 6 :
|
|
2715
|
+
canvasCtx.arc(p[0], p[1], isHover ? 6 : 3, 0, Math.PI * 2);
|
|
2405
2716
|
canvasCtx.fillStyle = isHover ? theme.backgroundColor : color;
|
|
2406
2717
|
canvasCtx.fill();
|
|
2407
2718
|
canvasCtx.strokeStyle = color;
|
|
@@ -2436,11 +2747,14 @@ function renderFunnelChart(ctx) {
|
|
|
2436
2747
|
const stepHeight = (plotArea.y + plotArea.height - 8 - topY) / funnelData.length;
|
|
2437
2748
|
const maxValue = Math.max(...funnelData.map((d) => d.value));
|
|
2438
2749
|
const drawData = options.pyramid === true ? [...funnelData].reverse() : funnelData;
|
|
2750
|
+
const labelAlpha = Math.min(1, Math.max(0, (progress - .72) / .28));
|
|
2751
|
+
const outsideLabels = [];
|
|
2439
2752
|
drawData.forEach((data, i) => {
|
|
2440
2753
|
const color = data.color || theme.colors[funnelData.indexOf(data) % theme.colors.length];
|
|
2441
2754
|
const isHover = i === hoverIndex;
|
|
2442
|
-
const
|
|
2443
|
-
const
|
|
2755
|
+
const easedWidth = 1 - Math.pow(1 - progress, 3);
|
|
2756
|
+
const currentWidth = data.value / maxValue * maxWidth * easedWidth;
|
|
2757
|
+
const nextWidth = (drawData[i + 1]?.value ?? data.value * .5) / maxValue * maxWidth * easedWidth;
|
|
2444
2758
|
const y = topY + i * stepHeight;
|
|
2445
2759
|
const yOffset = isHover ? -3 : 0;
|
|
2446
2760
|
canvasCtx.save();
|
|
@@ -2455,48 +2769,67 @@ function renderFunnelChart(ctx) {
|
|
|
2455
2769
|
canvasCtx.strokeStyle = theme.backgroundColor;
|
|
2456
2770
|
canvasCtx.lineWidth = 2;
|
|
2457
2771
|
canvasCtx.stroke();
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2772
|
+
canvasCtx.restore();
|
|
2773
|
+
if (labelAlpha <= 0) return;
|
|
2774
|
+
const labelY = y + stepHeight / 2 + yOffset;
|
|
2775
|
+
const percentage = (data.value / drawData[0].value * 100).toFixed(1);
|
|
2776
|
+
const label = `${data.label}`;
|
|
2777
|
+
const valueText = `${data.value} (${percentage}%)`;
|
|
2778
|
+
canvasCtx.save();
|
|
2779
|
+
canvasCtx.font = "bold 13px Inter, sans-serif";
|
|
2780
|
+
const labelWidth = canvasCtx.measureText(label).width;
|
|
2781
|
+
canvasCtx.font = "11px Inter, sans-serif";
|
|
2782
|
+
const valueWidth = canvasCtx.measureText(valueText).width;
|
|
2783
|
+
const minTextWidth = Math.max(labelWidth, valueWidth) + 28;
|
|
2784
|
+
if ((currentWidth + nextWidth) / 2 >= minTextWidth && stepHeight >= 36) {
|
|
2785
|
+
canvasCtx.globalAlpha = labelAlpha;
|
|
2786
|
+
canvasCtx.fillStyle = theme.backgroundColor;
|
|
2463
2787
|
canvasCtx.font = "bold 13px Inter, sans-serif";
|
|
2464
|
-
|
|
2788
|
+
canvasCtx.textAlign = "center";
|
|
2789
|
+
canvasCtx.textBaseline = "middle";
|
|
2790
|
+
canvasCtx.fillText(label, centerX, labelY - 9);
|
|
2465
2791
|
canvasCtx.font = "11px Inter, sans-serif";
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2792
|
+
canvasCtx.fillText(valueText, centerX, labelY + 9);
|
|
2793
|
+
} else outsideLabels.push({
|
|
2794
|
+
labelY,
|
|
2795
|
+
lineEndX: centerX + currentWidth / 2,
|
|
2796
|
+
label,
|
|
2797
|
+
valueText,
|
|
2798
|
+
labelTextWidth: Math.max(labelWidth, valueWidth)
|
|
2799
|
+
});
|
|
2800
|
+
canvasCtx.restore();
|
|
2801
|
+
});
|
|
2802
|
+
outsideLabels.sort((a, b) => a.labelY - b.labelY);
|
|
2803
|
+
const textX = plotArea.x + plotArea.width - 4;
|
|
2804
|
+
let lastBottom = -Infinity;
|
|
2805
|
+
outsideLabels.map((o) => {
|
|
2806
|
+
const top = Math.max(o.labelY - 13, lastBottom + 2);
|
|
2807
|
+
lastBottom = top + 36;
|
|
2808
|
+
return {
|
|
2809
|
+
...o,
|
|
2810
|
+
top
|
|
2811
|
+
};
|
|
2812
|
+
}).forEach((o) => {
|
|
2813
|
+
canvasCtx.save();
|
|
2814
|
+
canvasCtx.globalAlpha = labelAlpha;
|
|
2815
|
+
canvasCtx.strokeStyle = theme.textColorSecondary;
|
|
2816
|
+
canvasCtx.lineWidth = 1;
|
|
2817
|
+
canvasCtx.setLineDash([3, 3]);
|
|
2818
|
+
canvasCtx.beginPath();
|
|
2819
|
+
canvasCtx.moveTo(o.lineEndX, o.labelY);
|
|
2820
|
+
canvasCtx.lineTo(textX - o.labelTextWidth - 10, o.top + 12);
|
|
2821
|
+
canvasCtx.stroke();
|
|
2822
|
+
canvasCtx.setLineDash([]);
|
|
2823
|
+
const nameY = o.top + 12;
|
|
2824
|
+
canvasCtx.fillStyle = theme.textColor;
|
|
2825
|
+
canvasCtx.font = "bold 12px Inter, sans-serif";
|
|
2826
|
+
canvasCtx.textAlign = "right";
|
|
2827
|
+
canvasCtx.textBaseline = "bottom";
|
|
2828
|
+
canvasCtx.fillText(o.label, textX, nameY);
|
|
2829
|
+
canvasCtx.font = "11px Inter, sans-serif";
|
|
2830
|
+
canvasCtx.fillStyle = theme.textColorSecondary;
|
|
2831
|
+
canvasCtx.textBaseline = "top";
|
|
2832
|
+
canvasCtx.fillText(o.valueText, textX, nameY + 2);
|
|
2500
2833
|
canvasCtx.restore();
|
|
2501
2834
|
});
|
|
2502
2835
|
}
|
|
@@ -2980,42 +3313,40 @@ function squarifyTreemap(nodes, x, y, w, h, depth, result) {
|
|
|
2980
3313
|
});
|
|
2981
3314
|
return result;
|
|
2982
3315
|
}
|
|
3316
|
+
function isRectWithinFocus(rect, focus) {
|
|
3317
|
+
return rect.depth >= focus.depth && rect.x >= focus.x - .5 && rect.y >= focus.y - .5 && rect.x + rect.w <= focus.x + focus.w + .5 && rect.y + rect.h <= focus.y + focus.h + .5;
|
|
3318
|
+
}
|
|
2983
3319
|
function renderTreemapChart(ctx) {
|
|
2984
|
-
const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, valueFormatter } = ctx;
|
|
3320
|
+
const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, valueFormatter, hoverAnimProgress = 1 } = ctx;
|
|
2985
3321
|
const rootData = options.treemapData || [];
|
|
2986
3322
|
if (rootData.length === 0) return;
|
|
2987
3323
|
const rects = [];
|
|
2988
3324
|
squarifyTreemap(rootData, plotArea.x, plotArea.y, plotArea.width, plotArea.height, 0, rects);
|
|
2989
3325
|
const totalValue = rootData.reduce((s, n) => s + treemapValueOf(n), 0);
|
|
2990
3326
|
const scale = progress;
|
|
2991
|
-
|
|
3327
|
+
const focus = hoverIndex >= 0 && hoverIndex < rects.length ? rects[hoverIndex] : null;
|
|
3328
|
+
const dimAlpha = 1 - .75 * hoverAnimProgress;
|
|
3329
|
+
function drawRect(rect, isFocusRoot) {
|
|
2992
3330
|
const cx = rect.x + rect.w / 2;
|
|
2993
3331
|
const cy = rect.y + rect.h / 2;
|
|
2994
3332
|
const aw = rect.w * scale;
|
|
2995
3333
|
const ah = rect.h * scale;
|
|
2996
3334
|
const ax = cx - aw / 2;
|
|
2997
3335
|
const ay = cy - ah / 2;
|
|
2998
|
-
const
|
|
3336
|
+
const base = rect.node.color || theme.colors[(rect.colorSeed + rect.depth) % theme.colors.length];
|
|
3337
|
+
const color = isFocusRoot ? mixColor(base, .08, "#000000") : base;
|
|
2999
3338
|
canvasCtx.save();
|
|
3000
|
-
canvasCtx.
|
|
3339
|
+
canvasCtx.globalAlpha = !focus || isRectWithinFocus(rect, focus) ? 1 : dimAlpha;
|
|
3340
|
+
canvasCtx.fillStyle = isFocusRoot ? color : color + (rect.depth === 0 ? "" : "cc");
|
|
3001
3341
|
roundRect(canvasCtx, ax, ay, Math.max(0, aw), Math.max(0, ah), 3);
|
|
3002
3342
|
canvasCtx.fill();
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
canvasCtx.stroke();
|
|
3007
|
-
} else {
|
|
3008
|
-
canvasCtx.shadowColor = color;
|
|
3009
|
-
canvasCtx.shadowBlur = 10;
|
|
3010
|
-
canvasCtx.strokeStyle = theme.textColor;
|
|
3011
|
-
canvasCtx.lineWidth = 2.5;
|
|
3012
|
-
canvasCtx.stroke();
|
|
3013
|
-
canvasCtx.shadowBlur = 0;
|
|
3014
|
-
canvasCtx.stroke();
|
|
3015
|
-
}
|
|
3343
|
+
canvasCtx.strokeStyle = theme.backgroundColor;
|
|
3344
|
+
canvasCtx.lineWidth = 2;
|
|
3345
|
+
canvasCtx.stroke();
|
|
3016
3346
|
canvasCtx.restore();
|
|
3017
3347
|
if (progress > .8 && aw > 50 && ah > 24) {
|
|
3018
3348
|
canvasCtx.save();
|
|
3349
|
+
canvasCtx.globalAlpha = !focus || isRectWithinFocus(rect, focus) ? 1 : dimAlpha;
|
|
3019
3350
|
canvasCtx.fillStyle = getContrastText(color);
|
|
3020
3351
|
if (rect.node.children && rect.node.children.length > 0) {
|
|
3021
3352
|
canvasCtx.font = "bold 11px Inter, sans-serif";
|
|
@@ -3036,10 +3367,7 @@ function renderTreemapChart(ctx) {
|
|
|
3036
3367
|
canvasCtx.restore();
|
|
3037
3368
|
}
|
|
3038
3369
|
}
|
|
3039
|
-
rects.forEach((rect
|
|
3040
|
-
if (i !== hoverIndex) drawRect(rect, false);
|
|
3041
|
-
});
|
|
3042
|
-
if (hoverIndex >= 0 && hoverIndex < rects.length) drawRect(rects[hoverIndex], true);
|
|
3370
|
+
rects.forEach((rect) => drawRect(rect, focus === rect));
|
|
3043
3371
|
}
|
|
3044
3372
|
function renderSparklineChart(ctx) {
|
|
3045
3373
|
const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex } = ctx;
|
|
@@ -3319,12 +3647,26 @@ function computeSunburstDepth(nodes) {
|
|
|
3319
3647
|
});
|
|
3320
3648
|
return max;
|
|
3321
3649
|
}
|
|
3322
|
-
|
|
3323
|
-
|
|
3650
|
+
var SUNBURST_HOLE_RATIO = .22;
|
|
3651
|
+
var SUNBURST_LIGHT_STEP = .26;
|
|
3652
|
+
var SUNBURST_LIGHT_MAX = .6;
|
|
3653
|
+
var SUNBURST_DARK_STEP = .14;
|
|
3654
|
+
var SUNBURST_DARK_MAX = .34;
|
|
3655
|
+
/** 父节点可省略 value(由子孙汇总);直接读 n.value 会得到 NaN,
|
|
3656
|
+
* 让后续兄弟分支的角度全部失效、只剩第一个分支可见 */
|
|
3657
|
+
function sunburstValue(node) {
|
|
3658
|
+
if (typeof node.value === "number" && Number.isFinite(node.value)) return node.value;
|
|
3659
|
+
if (node.children && node.children.length > 0) return node.children.reduce((s, c) => s + sunburstValue(c), 0);
|
|
3660
|
+
return 0;
|
|
3661
|
+
}
|
|
3662
|
+
function layoutSunburst(nodes, r0, ringWidth, depth, startAngle, colorOffset, result, sweep = Math.PI * 2, parentIndex = -1) {
|
|
3663
|
+
const total = nodes.reduce((s, n) => s + sunburstValue(n), 0);
|
|
3324
3664
|
if (total <= 0) return result;
|
|
3325
3665
|
let a = startAngle;
|
|
3326
3666
|
nodes.forEach((n, i) => {
|
|
3327
|
-
const
|
|
3667
|
+
const slot = depth === 0 ? (colorOffset + i) % 8 : colorOffset % 8;
|
|
3668
|
+
const span = sweep * sunburstValue(n) / total;
|
|
3669
|
+
const selfIndex = result.length;
|
|
3328
3670
|
result.push({
|
|
3329
3671
|
startAngle: a,
|
|
3330
3672
|
endAngle: a + span,
|
|
@@ -3332,25 +3674,72 @@ function layoutSunburst(nodes, r0, ringWidth, depth, startAngle, colorOffset, re
|
|
|
3332
3674
|
r0,
|
|
3333
3675
|
r1: r0 + ringWidth,
|
|
3334
3676
|
node: n,
|
|
3677
|
+
value: sunburstValue(n),
|
|
3335
3678
|
depth,
|
|
3336
|
-
colorIndex:
|
|
3679
|
+
colorIndex: slot,
|
|
3680
|
+
parentIndex
|
|
3337
3681
|
});
|
|
3338
|
-
if (n.children && n.children.length > 0) layoutSunburst(n.children, r0 + ringWidth, ringWidth, depth + 1, a,
|
|
3682
|
+
if (n.children && n.children.length > 0) layoutSunburst(n.children, r0 + ringWidth, ringWidth, depth + 1, a, slot, result, span, selfIndex);
|
|
3339
3683
|
a += span;
|
|
3340
3684
|
});
|
|
3341
3685
|
return result;
|
|
3342
3686
|
}
|
|
3343
|
-
function
|
|
3344
|
-
|
|
3687
|
+
function isSunburstDescendant(segments, index, ancestorIndex) {
|
|
3688
|
+
let cursor = index;
|
|
3689
|
+
while (cursor >= 0) {
|
|
3690
|
+
if (cursor === ancestorIndex) return true;
|
|
3691
|
+
cursor = segments[cursor].parentIndex;
|
|
3692
|
+
}
|
|
3693
|
+
return false;
|
|
3694
|
+
}
|
|
3695
|
+
/** 环带颜色:同色系按深度向背景方向混合,外圈大面积不刺眼 */
|
|
3696
|
+
function sunburstDepthColor(base, depth, theme) {
|
|
3697
|
+
if (depth <= 0) return base;
|
|
3698
|
+
const darker = !isLightColor(theme.backgroundColor);
|
|
3699
|
+
return mixColor(base, Math.min(darker ? SUNBURST_DARK_MAX : SUNBURST_LIGHT_MAX, depth * (darker ? SUNBURST_DARK_STEP : SUNBURST_LIGHT_STEP)), darker ? "#000000" : "#ffffff");
|
|
3700
|
+
}
|
|
3701
|
+
function sunburstNodeColor(seg, theme) {
|
|
3702
|
+
const base = seg.node.color || theme.colors[seg.colorIndex % theme.colors.length];
|
|
3703
|
+
if (seg.node.color && seg.depth > 0) return seg.node.color;
|
|
3704
|
+
return sunburstDepthColor(base, seg.depth, theme);
|
|
3705
|
+
}
|
|
3706
|
+
/** 旭日图几何:渲染与悬浮命中共用一份口径(环厚、留白、半径预算) */
|
|
3707
|
+
function computeSunburstGeometry(plotArea, options, theme, valueFormatter) {
|
|
3345
3708
|
const data = options.sunburstData || [];
|
|
3346
|
-
|
|
3709
|
+
const depthCount = computeSunburstDepth(data);
|
|
3710
|
+
if (data.length === 0 || depthCount === 0) return null;
|
|
3347
3711
|
const centerX = plotArea.x + plotArea.width / 2;
|
|
3348
3712
|
const centerY = plotArea.y + plotArea.height / 2;
|
|
3349
|
-
const maxR = Math.max(40, Math.min(plotArea.width, plotArea.height) / 2 -
|
|
3350
|
-
const innerHole = Math.max(0, maxR *
|
|
3351
|
-
const
|
|
3352
|
-
const segments = layoutSunburst(data, innerHole,
|
|
3713
|
+
const maxR = Math.max(40, Math.min(plotArea.width, plotArea.height) / 2 - 20);
|
|
3714
|
+
const innerHole = Math.max(0, maxR * SUNBURST_HOLE_RATIO);
|
|
3715
|
+
const ringWidth = (maxR - innerHole) / depthCount;
|
|
3716
|
+
const segments = layoutSunburst(data, innerHole, ringWidth, 0, -Math.PI / 2, 0, []);
|
|
3717
|
+
segments.forEach((seg) => {
|
|
3718
|
+
seg.color = sunburstNodeColor(seg, theme);
|
|
3719
|
+
});
|
|
3720
|
+
return {
|
|
3721
|
+
centerX,
|
|
3722
|
+
centerY,
|
|
3723
|
+
maxR,
|
|
3724
|
+
innerHole,
|
|
3725
|
+
ringWidth,
|
|
3726
|
+
depthCount,
|
|
3727
|
+
segments,
|
|
3728
|
+
showValues: options.showValues === true,
|
|
3729
|
+
valueFormatter
|
|
3730
|
+
};
|
|
3731
|
+
}
|
|
3732
|
+
function renderSunburstChart(ctx) {
|
|
3733
|
+
const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, valueFormatter, hoverAnimProgress = 1 } = ctx;
|
|
3734
|
+
const geo = computeSunburstGeometry(plotArea, options, theme, valueFormatter);
|
|
3735
|
+
if (!geo) return;
|
|
3736
|
+
const { centerX, centerY, maxR, ringWidth, depthCount, segments, showValues } = geo;
|
|
3353
3737
|
const sweep = -Math.PI / 2 + Math.PI * 2 * progress;
|
|
3738
|
+
const grow = .94 + .06 * progress;
|
|
3739
|
+
const focusIndex = hoverIndex >= 0 && hoverIndex < segments.length ? hoverIndex : -1;
|
|
3740
|
+
const dimAlpha = 1 - .75 * hoverAnimProgress;
|
|
3741
|
+
const alphaAt = (i) => focusIndex < 0 || isSunburstDescendant(segments, i, focusIndex) ? 1 : dimAlpha;
|
|
3742
|
+
const labels = [];
|
|
3354
3743
|
canvasCtx.save();
|
|
3355
3744
|
canvasCtx.beginPath();
|
|
3356
3745
|
canvasCtx.moveTo(centerX, centerY);
|
|
@@ -3358,45 +3747,58 @@ function renderSunburstChart(ctx) {
|
|
|
3358
3747
|
canvasCtx.closePath();
|
|
3359
3748
|
canvasCtx.clip();
|
|
3360
3749
|
segments.forEach((seg, i) => {
|
|
3361
|
-
const color = seg.
|
|
3362
|
-
const isHover = i === hoverIndex;
|
|
3750
|
+
const color = i === focusIndex ? mixColor(seg.color, .08, "#000000") : seg.color;
|
|
3363
3751
|
canvasCtx.save();
|
|
3752
|
+
canvasCtx.globalAlpha = alphaAt(i);
|
|
3364
3753
|
canvasCtx.beginPath();
|
|
3365
|
-
canvasCtx.arc(centerX, centerY, seg.r1, seg.startAngle, seg.endAngle);
|
|
3366
|
-
canvasCtx.arc(centerX, centerY, seg.r0, seg.endAngle, seg.startAngle, true);
|
|
3754
|
+
canvasCtx.arc(centerX, centerY, seg.r1 * grow, seg.startAngle, seg.endAngle);
|
|
3755
|
+
canvasCtx.arc(centerX, centerY, seg.r0 * grow, seg.endAngle, seg.startAngle, true);
|
|
3367
3756
|
canvasCtx.closePath();
|
|
3368
|
-
canvasCtx.fillStyle =
|
|
3757
|
+
canvasCtx.fillStyle = color;
|
|
3369
3758
|
canvasCtx.fill();
|
|
3370
3759
|
canvasCtx.strokeStyle = theme.backgroundColor;
|
|
3371
3760
|
canvasCtx.lineWidth = 2;
|
|
3372
3761
|
canvasCtx.stroke();
|
|
3373
|
-
if (isHover) {
|
|
3374
|
-
canvasCtx.strokeStyle = theme.textColor;
|
|
3375
|
-
canvasCtx.lineWidth = 2;
|
|
3376
|
-
canvasCtx.stroke();
|
|
3377
|
-
}
|
|
3378
3762
|
canvasCtx.restore();
|
|
3763
|
+
if (progress <= .9) return;
|
|
3764
|
+
const name = seg.node.name || "";
|
|
3765
|
+
if (!name) return;
|
|
3379
3766
|
const span = seg.endAngle - seg.startAngle;
|
|
3380
3767
|
const midR = (seg.r0 + seg.r1) / 2;
|
|
3381
3768
|
const chord = midR * span;
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
}
|
|
3396
|
-
canvasCtx.restore();
|
|
3397
|
-
}
|
|
3769
|
+
if (ringWidth < 12 || chord < 11) return;
|
|
3770
|
+
const value = showValues && ringWidth >= 26 ? valueFormatter(seg.value) : null;
|
|
3771
|
+
if (Math.max(estimateTextWidth(name, 11), value ? estimateTextWidth(value, 10) : 0) > ringWidth + 20) return;
|
|
3772
|
+
labels.push({
|
|
3773
|
+
x: centerX + Math.cos(seg.midAngle) * midR,
|
|
3774
|
+
y: centerY + Math.sin(seg.midAngle) * midR,
|
|
3775
|
+
midAngle: seg.midAngle,
|
|
3776
|
+
name,
|
|
3777
|
+
value,
|
|
3778
|
+
bold: seg.depth === 0,
|
|
3779
|
+
fill: getContrastText(color),
|
|
3780
|
+
alpha: alphaAt(i)
|
|
3781
|
+
});
|
|
3398
3782
|
});
|
|
3399
3783
|
canvasCtx.restore();
|
|
3784
|
+
labels.forEach((l) => {
|
|
3785
|
+
canvasCtx.save();
|
|
3786
|
+
canvasCtx.globalAlpha = l.alpha;
|
|
3787
|
+
canvasCtx.fillStyle = l.fill;
|
|
3788
|
+
canvasCtx.font = l.bold ? "bold 11px Inter, sans-serif" : "11px Inter, sans-serif";
|
|
3789
|
+
canvasCtx.textAlign = "center";
|
|
3790
|
+
canvasCtx.textBaseline = "middle";
|
|
3791
|
+
canvasCtx.translate(l.x, l.y);
|
|
3792
|
+
let rot = l.midAngle;
|
|
3793
|
+
if (rot > Math.PI / 2 || rot < -Math.PI / 2) rot += Math.PI;
|
|
3794
|
+
canvasCtx.rotate(rot);
|
|
3795
|
+
canvasCtx.fillText(l.name, 0, l.value ? -7 : 0);
|
|
3796
|
+
if (l.value) {
|
|
3797
|
+
canvasCtx.font = "10px Inter, sans-serif";
|
|
3798
|
+
canvasCtx.fillText(l.value, 0, 7);
|
|
3799
|
+
}
|
|
3800
|
+
canvasCtx.restore();
|
|
3801
|
+
});
|
|
3400
3802
|
}
|
|
3401
3803
|
function renderMixedChart(ctx, leftRange, rightRange) {
|
|
3402
3804
|
const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hiddenSeries } = ctx;
|
|
@@ -3966,6 +4368,13 @@ function createSvgRecorder(real) {
|
|
|
3966
4368
|
}
|
|
3967
4369
|
//#endregion
|
|
3968
4370
|
//#region src/renderer/index.js
|
|
4371
|
+
function runLayers(renderCtx, at) {
|
|
4372
|
+
const layers = renderCtx.options.layers;
|
|
4373
|
+
if (!Array.isArray(layers)) return;
|
|
4374
|
+
layers.forEach((layer) => {
|
|
4375
|
+
if (layer && typeof layer.draw === "function" && (layer.at || "front") === at) layer.draw(renderCtx.ctx, renderCtx);
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
3969
4378
|
function renderChart(canvas, params) {
|
|
3970
4379
|
const { options: rawOptions, ctx: externalCtx, dpr = window.devicePixelRatio || 1, progress = 1, hoverIndex = -1, hiddenSeries = /* @__PURE__ */ new Set(), mouseX = -1, mouseY = -1, showCrosshair = false, hoverAnimProgress = 1, zoomRange, brushRect, focusSeries = null, hoveredToolbox = null } = params;
|
|
3971
4380
|
const options = applyLogTransform(rawOptions);
|
|
@@ -4003,6 +4412,7 @@ function renderChart(canvas, params) {
|
|
|
4003
4412
|
focusSeries
|
|
4004
4413
|
};
|
|
4005
4414
|
renderTitle(renderCtx, options.subtitle ? 12 : 15);
|
|
4415
|
+
runLayers(renderCtx, "back");
|
|
4006
4416
|
let points = [];
|
|
4007
4417
|
let leftRange;
|
|
4008
4418
|
switch (options.type) {
|
|
@@ -4146,7 +4556,8 @@ function renderChart(canvas, params) {
|
|
|
4146
4556
|
break;
|
|
4147
4557
|
}
|
|
4148
4558
|
}
|
|
4149
|
-
|
|
4559
|
+
runLayers(renderCtx, "after-series");
|
|
4560
|
+
if (leftRange && (options.annotation || options.annotations && options.annotations.length > 0)) renderAnnotations(renderCtx, leftRange);
|
|
4150
4561
|
if ((options.type === "line" || options.type === "area") && points.length > 0) renderHoverHighlight(renderCtx, points);
|
|
4151
4562
|
if (points.length > 0) renderCrosshair(renderCtx, points);
|
|
4152
4563
|
if (zoomRange) renderDataZoomSlider(renderCtx, zoomRange);
|
|
@@ -4164,6 +4575,7 @@ function renderChart(canvas, params) {
|
|
|
4164
4575
|
ctx.strokeRect(x, y, w, h);
|
|
4165
4576
|
ctx.restore();
|
|
4166
4577
|
}
|
|
4578
|
+
runLayers(renderCtx, "front");
|
|
4167
4579
|
if (options.type !== "sparkline") renderLegend(renderCtx);
|
|
4168
4580
|
if (options.type !== "sparkline") renderToolbox(renderCtx, hoveredToolbox);
|
|
4169
4581
|
ctx.restore();
|
|
@@ -4191,6 +4603,376 @@ function broadcastConnect(group, source, apply) {
|
|
|
4191
4603
|
if (target !== source) apply(target);
|
|
4192
4604
|
});
|
|
4193
4605
|
}
|
|
4606
|
+
var chartOptionsSchema = {
|
|
4607
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
4608
|
+
title: "EvChart Options",
|
|
4609
|
+
type: "object",
|
|
4610
|
+
required: ["type"],
|
|
4611
|
+
properties: {
|
|
4612
|
+
type: {
|
|
4613
|
+
type: "string",
|
|
4614
|
+
enum: [
|
|
4615
|
+
"line",
|
|
4616
|
+
"area",
|
|
4617
|
+
"bar",
|
|
4618
|
+
"stacked-bar",
|
|
4619
|
+
"horizontal-bar",
|
|
4620
|
+
"pie",
|
|
4621
|
+
"doughnut",
|
|
4622
|
+
"rose",
|
|
4623
|
+
"radar",
|
|
4624
|
+
"scatter",
|
|
4625
|
+
"funnel",
|
|
4626
|
+
"gauge",
|
|
4627
|
+
"heatmap",
|
|
4628
|
+
"candle",
|
|
4629
|
+
"bin",
|
|
4630
|
+
"bullet",
|
|
4631
|
+
"treemap",
|
|
4632
|
+
"sparkline",
|
|
4633
|
+
"waterfall",
|
|
4634
|
+
"boxplot",
|
|
4635
|
+
"sunburst",
|
|
4636
|
+
"mixed"
|
|
4637
|
+
]
|
|
4638
|
+
},
|
|
4639
|
+
title: { type: "string" },
|
|
4640
|
+
subtitle: { type: "string" },
|
|
4641
|
+
labels: { type: "array" },
|
|
4642
|
+
series: {
|
|
4643
|
+
type: "array",
|
|
4644
|
+
items: {
|
|
4645
|
+
type: "object",
|
|
4646
|
+
required: ["name", "data"],
|
|
4647
|
+
properties: {
|
|
4648
|
+
name: { type: "string" },
|
|
4649
|
+
data: { type: "array" },
|
|
4650
|
+
color: { type: "string" },
|
|
4651
|
+
showSymbol: { type: "boolean" },
|
|
4652
|
+
lineWidth: { type: "number" },
|
|
4653
|
+
smooth: { type: "boolean" },
|
|
4654
|
+
yAxis: {
|
|
4655
|
+
type: "string",
|
|
4656
|
+
enum: ["left", "right"]
|
|
4657
|
+
},
|
|
4658
|
+
type: { type: "string" },
|
|
4659
|
+
chartType: {
|
|
4660
|
+
type: "string",
|
|
4661
|
+
enum: ["bar", "line"]
|
|
4662
|
+
},
|
|
4663
|
+
area: { type: "boolean" }
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4666
|
+
},
|
|
4667
|
+
legend: {
|
|
4668
|
+
type: "object",
|
|
4669
|
+
properties: {
|
|
4670
|
+
show: { type: "boolean" },
|
|
4671
|
+
position: {
|
|
4672
|
+
type: "string",
|
|
4673
|
+
enum: ["top", "bottom"]
|
|
4674
|
+
},
|
|
4675
|
+
interactive: { type: "boolean" },
|
|
4676
|
+
hoverEmphasis: { type: "boolean" }
|
|
4677
|
+
}
|
|
4678
|
+
},
|
|
4679
|
+
tooltip: {
|
|
4680
|
+
type: "object",
|
|
4681
|
+
properties: {
|
|
4682
|
+
show: { type: "boolean" },
|
|
4683
|
+
trigger: {
|
|
4684
|
+
type: "string",
|
|
4685
|
+
enum: ["hover", "click"]
|
|
4686
|
+
},
|
|
4687
|
+
showAllSeries: { type: "boolean" },
|
|
4688
|
+
formatter: { type: "function" }
|
|
4689
|
+
}
|
|
4690
|
+
},
|
|
4691
|
+
animation: {
|
|
4692
|
+
type: "object",
|
|
4693
|
+
properties: {
|
|
4694
|
+
enabled: { type: "boolean" },
|
|
4695
|
+
duration: { type: "number" },
|
|
4696
|
+
easing: { type: "string" }
|
|
4697
|
+
}
|
|
4698
|
+
},
|
|
4699
|
+
xAxis: {
|
|
4700
|
+
type: "object",
|
|
4701
|
+
properties: {
|
|
4702
|
+
show: { type: "boolean" },
|
|
4703
|
+
min: { type: "number" },
|
|
4704
|
+
max: { type: "number" },
|
|
4705
|
+
ticks: { type: "integer" },
|
|
4706
|
+
grid: {
|
|
4707
|
+
type: "object",
|
|
4708
|
+
properties: {
|
|
4709
|
+
show: { type: "boolean" },
|
|
4710
|
+
style: {
|
|
4711
|
+
type: "string",
|
|
4712
|
+
enum: ["solid", "dashed"]
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
},
|
|
4718
|
+
yAxis: {
|
|
4719
|
+
type: "object",
|
|
4720
|
+
properties: {
|
|
4721
|
+
show: { type: "boolean" },
|
|
4722
|
+
min: { type: "number" },
|
|
4723
|
+
max: { type: "number" },
|
|
4724
|
+
ticks: { type: "integer" },
|
|
4725
|
+
width: { type: "number" },
|
|
4726
|
+
grid: {
|
|
4727
|
+
type: "object",
|
|
4728
|
+
properties: {
|
|
4729
|
+
show: { type: "boolean" },
|
|
4730
|
+
style: {
|
|
4731
|
+
type: "string",
|
|
4732
|
+
enum: ["solid", "dashed"]
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
},
|
|
4738
|
+
yAxisRight: { $ref: "#/properties/yAxis" },
|
|
4739
|
+
padding: { oneOf: [{ type: "number" }, {
|
|
4740
|
+
type: "object",
|
|
4741
|
+
properties: {
|
|
4742
|
+
top: { type: "number" },
|
|
4743
|
+
right: { type: "number" },
|
|
4744
|
+
bottom: { type: "number" },
|
|
4745
|
+
left: { type: "number" }
|
|
4746
|
+
}
|
|
4747
|
+
}] },
|
|
4748
|
+
dataZoom: {
|
|
4749
|
+
type: "object",
|
|
4750
|
+
properties: {
|
|
4751
|
+
enabled: { type: "boolean" },
|
|
4752
|
+
start: { type: "number" },
|
|
4753
|
+
end: { type: "number" },
|
|
4754
|
+
position: {
|
|
4755
|
+
type: "string",
|
|
4756
|
+
enum: ["bottom", "top"]
|
|
4757
|
+
},
|
|
4758
|
+
height: { type: "number" },
|
|
4759
|
+
mouseWheel: { type: "boolean" }
|
|
4760
|
+
}
|
|
4761
|
+
},
|
|
4762
|
+
toolbox: {
|
|
4763
|
+
type: "object",
|
|
4764
|
+
properties: {
|
|
4765
|
+
show: { type: "boolean" },
|
|
4766
|
+
filename: { type: "string" }
|
|
4767
|
+
}
|
|
4768
|
+
},
|
|
4769
|
+
theme: { type: "object" },
|
|
4770
|
+
layers: {
|
|
4771
|
+
type: "array",
|
|
4772
|
+
items: {
|
|
4773
|
+
type: "object",
|
|
4774
|
+
properties: {
|
|
4775
|
+
at: {
|
|
4776
|
+
type: "string",
|
|
4777
|
+
enum: [
|
|
4778
|
+
"back",
|
|
4779
|
+
"after-series",
|
|
4780
|
+
"front"
|
|
4781
|
+
]
|
|
4782
|
+
},
|
|
4783
|
+
draw: { type: "function" }
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
},
|
|
4787
|
+
emphasis: {
|
|
4788
|
+
type: "object",
|
|
4789
|
+
properties: {
|
|
4790
|
+
series: { oneOf: [{ type: "string" }, { type: "integer" }] },
|
|
4791
|
+
dimOthers: { type: "boolean" }
|
|
4792
|
+
}
|
|
4793
|
+
},
|
|
4794
|
+
annotations: {
|
|
4795
|
+
type: "array",
|
|
4796
|
+
items: {
|
|
4797
|
+
type: "object",
|
|
4798
|
+
oneOf: [{
|
|
4799
|
+
required: [
|
|
4800
|
+
"type",
|
|
4801
|
+
"x",
|
|
4802
|
+
"y"
|
|
4803
|
+
],
|
|
4804
|
+
properties: { type: { enum: [
|
|
4805
|
+
"text",
|
|
4806
|
+
"callout",
|
|
4807
|
+
"delta",
|
|
4808
|
+
"point"
|
|
4809
|
+
] } }
|
|
4810
|
+
}, {
|
|
4811
|
+
required: [
|
|
4812
|
+
"type",
|
|
4813
|
+
"from",
|
|
4814
|
+
"to"
|
|
4815
|
+
],
|
|
4816
|
+
properties: { type: { enum: ["region"] } }
|
|
4817
|
+
}],
|
|
4818
|
+
properties: {
|
|
4819
|
+
type: {
|
|
4820
|
+
type: "string",
|
|
4821
|
+
enum: [
|
|
4822
|
+
"text",
|
|
4823
|
+
"callout",
|
|
4824
|
+
"region",
|
|
4825
|
+
"delta",
|
|
4826
|
+
"point"
|
|
4827
|
+
]
|
|
4828
|
+
},
|
|
4829
|
+
x: {},
|
|
4830
|
+
y: { type: "number" },
|
|
4831
|
+
from: {},
|
|
4832
|
+
to: {},
|
|
4833
|
+
at: {},
|
|
4834
|
+
label: { type: "string" },
|
|
4835
|
+
text: { type: "string" },
|
|
4836
|
+
anchor: {
|
|
4837
|
+
type: "string",
|
|
4838
|
+
enum: [
|
|
4839
|
+
"top",
|
|
4840
|
+
"bottom",
|
|
4841
|
+
"left",
|
|
4842
|
+
"right",
|
|
4843
|
+
"top-left",
|
|
4844
|
+
"top-right",
|
|
4845
|
+
"bottom-left",
|
|
4846
|
+
"bottom-right"
|
|
4847
|
+
]
|
|
4848
|
+
},
|
|
4849
|
+
color: { type: "string" },
|
|
4850
|
+
offsetX: { type: "number" },
|
|
4851
|
+
offsetY: { type: "number" },
|
|
4852
|
+
xPx: { type: "number" },
|
|
4853
|
+
yPx: { type: "number" }
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
},
|
|
4857
|
+
annotation: { type: "object" },
|
|
4858
|
+
markLine: { type: "array" },
|
|
4859
|
+
markArea: { type: "array" },
|
|
4860
|
+
scenes: {
|
|
4861
|
+
type: "object",
|
|
4862
|
+
properties: {
|
|
4863
|
+
autoplay: { type: "boolean" },
|
|
4864
|
+
loop: { type: "boolean" },
|
|
4865
|
+
items: {
|
|
4866
|
+
type: "array",
|
|
4867
|
+
items: {
|
|
4868
|
+
type: "object",
|
|
4869
|
+
required: ["patch"],
|
|
4870
|
+
properties: {
|
|
4871
|
+
patch: { type: "object" },
|
|
4872
|
+
duration: { type: "number" },
|
|
4873
|
+
hold: { type: "number" }
|
|
4874
|
+
}
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
},
|
|
4879
|
+
pieData: { type: "array" },
|
|
4880
|
+
radarSeries: { type: "array" },
|
|
4881
|
+
radarIndicators: { type: "array" },
|
|
4882
|
+
scatterData: { type: "array" },
|
|
4883
|
+
funnelData: { type: "array" },
|
|
4884
|
+
gauge: { oneOf: [{ type: "number" }, { type: "object" }] },
|
|
4885
|
+
heatmapData: { type: "array" },
|
|
4886
|
+
candleData: { type: "array" },
|
|
4887
|
+
bulletData: { type: "array" },
|
|
4888
|
+
boxData: { type: "array" },
|
|
4889
|
+
loading: { type: "boolean" },
|
|
4890
|
+
emptyText: { type: "string" },
|
|
4891
|
+
ariaLabel: { type: "string" },
|
|
4892
|
+
i18n: { type: "object" },
|
|
4893
|
+
connectGroup: { type: "string" },
|
|
4894
|
+
brush: {
|
|
4895
|
+
type: "object",
|
|
4896
|
+
properties: { enabled: { type: "boolean" } }
|
|
4897
|
+
},
|
|
4898
|
+
logAxis: { type: "boolean" },
|
|
4899
|
+
valueFormatter: { type: "function" },
|
|
4900
|
+
timeAxis: { type: "boolean" }
|
|
4901
|
+
}
|
|
4902
|
+
};
|
|
4903
|
+
var TYPE_CHECKS = {
|
|
4904
|
+
string: (v) => typeof v === "string",
|
|
4905
|
+
number: (v) => typeof v === "number" && Number.isFinite(v),
|
|
4906
|
+
integer: (v) => typeof v === "number" && Number.isInteger(v),
|
|
4907
|
+
boolean: (v) => typeof v === "boolean",
|
|
4908
|
+
object: (v) => v !== null && typeof v === "object" && !Array.isArray(v),
|
|
4909
|
+
array: (v) => Array.isArray(v),
|
|
4910
|
+
function: (v) => typeof v === "function"
|
|
4911
|
+
};
|
|
4912
|
+
function typeMatches(node, value) {
|
|
4913
|
+
if (Array.isArray(node.type)) return node.type.some((t) => TYPE_CHECKS[t]?.(value));
|
|
4914
|
+
const check = TYPE_CHECKS[node.type];
|
|
4915
|
+
return check ? check(value) : true;
|
|
4916
|
+
}
|
|
4917
|
+
function validateNode(node, value, path, warnings) {
|
|
4918
|
+
if (node == null || value === void 0 || value === null) return;
|
|
4919
|
+
if (node.oneOf) {
|
|
4920
|
+
if (!node.oneOf.some((branch) => {
|
|
4921
|
+
const before = warnings.length;
|
|
4922
|
+
validateNode(branch, value, path, warnings);
|
|
4923
|
+
const ok = warnings.length === before;
|
|
4924
|
+
warnings.length = before;
|
|
4925
|
+
return ok;
|
|
4926
|
+
})) {
|
|
4927
|
+
warnings.push({
|
|
4928
|
+
path,
|
|
4929
|
+
message: "类型不匹配(应为 oneOf 之一)"
|
|
4930
|
+
});
|
|
4931
|
+
return;
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
if (node.type && !typeMatches(node, value)) {
|
|
4935
|
+
warnings.push({
|
|
4936
|
+
path,
|
|
4937
|
+
message: `应为 ${node.type},实际为 ${Array.isArray(value) ? "array" : typeof value}`
|
|
4938
|
+
});
|
|
4939
|
+
return;
|
|
4940
|
+
}
|
|
4941
|
+
if (node.enum && !node.enum.includes(value)) {
|
|
4942
|
+
warnings.push({
|
|
4943
|
+
path,
|
|
4944
|
+
message: `应为 ${node.enum.join(" / ")} 之一,实际为 ${String(value)}`
|
|
4945
|
+
});
|
|
4946
|
+
return;
|
|
4947
|
+
}
|
|
4948
|
+
if (node.type === "object") {
|
|
4949
|
+
(node.required || []).forEach((key) => {
|
|
4950
|
+
if (value[key] === void 0) warnings.push({
|
|
4951
|
+
path: `${path}.${key}`,
|
|
4952
|
+
message: "缺少必填字段"
|
|
4953
|
+
});
|
|
4954
|
+
});
|
|
4955
|
+
if (node.properties) Object.entries(node.properties).forEach(([key, sub]) => {
|
|
4956
|
+
if (value[key] !== void 0) validateNode(sub, value[key], `${path}.${key}`, warnings);
|
|
4957
|
+
});
|
|
4958
|
+
}
|
|
4959
|
+
if (node.type === "array" && node.items && Array.isArray(value)) value.forEach((item, i) => validateNode(node.items, item, `${path}[${i}]`, warnings));
|
|
4960
|
+
}
|
|
4961
|
+
function validateOptions(options) {
|
|
4962
|
+
const warnings = [];
|
|
4963
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) return {
|
|
4964
|
+
ok: false,
|
|
4965
|
+
warnings: [{
|
|
4966
|
+
path: "options",
|
|
4967
|
+
message: "应为对象"
|
|
4968
|
+
}]
|
|
4969
|
+
};
|
|
4970
|
+
validateNode(chartOptionsSchema, options, "options", warnings);
|
|
4971
|
+
return {
|
|
4972
|
+
ok: warnings.length === 0,
|
|
4973
|
+
warnings
|
|
4974
|
+
};
|
|
4975
|
+
}
|
|
4194
4976
|
//#endregion
|
|
4195
4977
|
//#region \0plugin-vue:export-helper
|
|
4196
4978
|
var _plugin_vue_export_helper_default = (sfc, props) => {
|
|
@@ -4202,23 +4984,27 @@ var _plugin_vue_export_helper_default = (sfc, props) => {
|
|
|
4202
4984
|
//#region src/chart.vue
|
|
4203
4985
|
var _hoisted_1 = ["aria-label"];
|
|
4204
4986
|
var _hoisted_2 = {
|
|
4987
|
+
key: 1,
|
|
4988
|
+
class: "ev-chart__overlay"
|
|
4989
|
+
};
|
|
4990
|
+
var _hoisted_3 = {
|
|
4205
4991
|
key: 0,
|
|
4206
4992
|
class: "ev-chart__empty"
|
|
4207
4993
|
};
|
|
4208
|
-
var
|
|
4209
|
-
var
|
|
4210
|
-
var
|
|
4994
|
+
var _hoisted_4 = { class: "ev-chart__empty-icon-wrap" };
|
|
4995
|
+
var _hoisted_5 = { class: "ev-chart__empty-text" };
|
|
4996
|
+
var _hoisted_6 = {
|
|
4211
4997
|
key: 0,
|
|
4212
4998
|
class: "ev-chart__loading"
|
|
4213
4999
|
};
|
|
4214
|
-
var
|
|
4215
|
-
var
|
|
5000
|
+
var _hoisted_7 = { class: "ev-chart__loading-bars" };
|
|
5001
|
+
var _hoisted_8 = {
|
|
4216
5002
|
key: 0,
|
|
4217
5003
|
class: "ev-chart__error"
|
|
4218
5004
|
};
|
|
4219
|
-
var
|
|
4220
|
-
var
|
|
4221
|
-
var
|
|
5005
|
+
var _hoisted_9 = { class: "ev-chart__error-text" };
|
|
5006
|
+
var _hoisted_10 = ["innerHTML"];
|
|
5007
|
+
var _hoisted_11 = {
|
|
4222
5008
|
class: "ev-chart__sr-only",
|
|
4223
5009
|
role: "status",
|
|
4224
5010
|
"aria-live": "polite"
|
|
@@ -4256,7 +5042,8 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
4256
5042
|
"animation-end",
|
|
4257
5043
|
"data-update",
|
|
4258
5044
|
"brush-select",
|
|
4259
|
-
"zoom"
|
|
5045
|
+
"zoom",
|
|
5046
|
+
"scene-change"
|
|
4260
5047
|
],
|
|
4261
5048
|
setup(__props, { expose: __expose, emit: __emit }) {
|
|
4262
5049
|
const DataLine = () => h("svg", {
|
|
@@ -4326,6 +5113,12 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
4326
5113
|
let hoverAnimFrameId = null;
|
|
4327
5114
|
const hiddenSeries = ref(/* @__PURE__ */ new Set());
|
|
4328
5115
|
const focusSeries = ref(null);
|
|
5116
|
+
const emphasisSeriesName = computed(() => {
|
|
5117
|
+
const em = props.options.emphasis;
|
|
5118
|
+
if (!em || em.series === void 0 || em.dimOthers === false) return null;
|
|
5119
|
+
if (typeof em.series === "number") return props.options.series?.[em.series]?.name ?? null;
|
|
5120
|
+
return em.series || null;
|
|
5121
|
+
});
|
|
4329
5122
|
const hoveredToolbox = ref(null);
|
|
4330
5123
|
let mouseX = -1;
|
|
4331
5124
|
let mouseY = -1;
|
|
@@ -4355,33 +5148,102 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
4355
5148
|
const brushRect = ref(null);
|
|
4356
5149
|
let suppressClick = false;
|
|
4357
5150
|
let brushDragged = false;
|
|
5151
|
+
const specVersion = ref(0);
|
|
5152
|
+
const overlayInfo = ref({
|
|
5153
|
+
plotArea: {
|
|
5154
|
+
x: 0,
|
|
5155
|
+
y: 0,
|
|
5156
|
+
width: 0,
|
|
5157
|
+
height: 0
|
|
5158
|
+
},
|
|
5159
|
+
theme: getTheme(false, void 0),
|
|
5160
|
+
options: null
|
|
5161
|
+
});
|
|
4358
5162
|
const effectiveOptions = computed(() => {
|
|
5163
|
+
specVersion.value;
|
|
4359
5164
|
const opt = props.options;
|
|
4360
|
-
|
|
5165
|
+
const scene = opt.scenes?.items?.[sceneIndex.value];
|
|
5166
|
+
const base = scene?.patch ? {
|
|
5167
|
+
...opt,
|
|
5168
|
+
...scene.patch
|
|
5169
|
+
} : opt;
|
|
5170
|
+
if (!getDataZoomConfig(base)) return base;
|
|
4361
5171
|
const range = zoomRange.value;
|
|
4362
|
-
if (range.start <= 0 && range.end >= 100) return
|
|
4363
|
-
const sliced = { ...
|
|
4364
|
-
const n = (
|
|
5172
|
+
if (range.start <= 0 && range.end >= 100) return base;
|
|
5173
|
+
const sliced = { ...base };
|
|
5174
|
+
const n = (base.labels || []).length;
|
|
4365
5175
|
if (n > 0) {
|
|
4366
5176
|
const { startIdx, endIdx } = zoomToSlice(range, n);
|
|
4367
|
-
sliced.labels = (
|
|
4368
|
-
if (
|
|
5177
|
+
sliced.labels = (base.labels || []).slice(startIdx, endIdx);
|
|
5178
|
+
if (base.series) sliced.series = base.series.map((s) => ({
|
|
4369
5179
|
...s,
|
|
4370
5180
|
data: s.data.slice(startIdx, endIdx)
|
|
4371
5181
|
}));
|
|
4372
|
-
if (
|
|
5182
|
+
if (base.candleData) sliced.candleData = base.candleData.slice(startIdx, endIdx);
|
|
4373
5183
|
sliced.__sliceStart = startIdx;
|
|
4374
|
-
} else if (
|
|
4375
|
-
const xs =
|
|
5184
|
+
} else if (base.scatterData && base.scatterData.length > 0) {
|
|
5185
|
+
const xs = base.scatterData.map((d) => d.x);
|
|
4376
5186
|
const xMin = Math.min(...xs);
|
|
4377
5187
|
const span = Math.max(...xs) - xMin || 1;
|
|
4378
5188
|
const lo = xMin + range.start / 100 * span;
|
|
4379
5189
|
const hi = xMin + range.end / 100 * span;
|
|
4380
|
-
const filtered =
|
|
4381
|
-
sliced.scatterData = filtered.length > 0 ? filtered :
|
|
5190
|
+
const filtered = base.scatterData.filter((d) => d.x >= lo && d.x <= hi);
|
|
5191
|
+
sliced.scatterData = filtered.length > 0 ? filtered : base.scatterData;
|
|
4382
5192
|
}
|
|
4383
5193
|
return sliced;
|
|
4384
5194
|
});
|
|
5195
|
+
const sceneIndex = ref(0);
|
|
5196
|
+
let sceneTimer = null;
|
|
5197
|
+
let sceneAnimOverride = null;
|
|
5198
|
+
function stopSceneTimer() {
|
|
5199
|
+
if (sceneTimer !== null) {
|
|
5200
|
+
clearTimeout(sceneTimer);
|
|
5201
|
+
sceneTimer = null;
|
|
5202
|
+
}
|
|
5203
|
+
}
|
|
5204
|
+
function applyScene(i, opts = {}) {
|
|
5205
|
+
const items = props.options.scenes?.items || [];
|
|
5206
|
+
if (items.length === 0) return;
|
|
5207
|
+
const next = Math.max(0, Math.min(i, items.length - 1));
|
|
5208
|
+
if (next === sceneIndex.value && !opts.force) return;
|
|
5209
|
+
sceneIndex.value = next;
|
|
5210
|
+
const item = items[next];
|
|
5211
|
+
sceneAnimOverride = item?.duration ? {
|
|
5212
|
+
...props.options.animation || {},
|
|
5213
|
+
duration: item.duration
|
|
5214
|
+
} : null;
|
|
5215
|
+
emit("scene-change", {
|
|
5216
|
+
index: next,
|
|
5217
|
+
total: items.length
|
|
5218
|
+
});
|
|
5219
|
+
render(true, sceneAnimOverride);
|
|
5220
|
+
scheduleSceneAdvance();
|
|
5221
|
+
}
|
|
5222
|
+
function scheduleSceneAdvance() {
|
|
5223
|
+
stopSceneTimer();
|
|
5224
|
+
const scenes = props.options.scenes;
|
|
5225
|
+
if (!scenes?.autoplay) return;
|
|
5226
|
+
const item = scenes.items?.[sceneIndex.value];
|
|
5227
|
+
if (!item) return;
|
|
5228
|
+
const wait = Math.max(0, (item.duration ?? 1200) + (item.hold ?? 0));
|
|
5229
|
+
sceneTimer = setTimeout(() => {
|
|
5230
|
+
const total = props.options.scenes?.items?.length ?? 0;
|
|
5231
|
+
if (total === 0) return;
|
|
5232
|
+
if (sceneIndex.value >= total - 1) {
|
|
5233
|
+
if (props.options.scenes?.loop) applyScene(0, { force: true });
|
|
5234
|
+
} else applyScene(sceneIndex.value + 1);
|
|
5235
|
+
}, wait);
|
|
5236
|
+
}
|
|
5237
|
+
function syncScenes() {
|
|
5238
|
+
const total = props.options.scenes?.items?.length ?? 0;
|
|
5239
|
+
if (total === 0) {
|
|
5240
|
+
stopSceneTimer();
|
|
5241
|
+
if (sceneIndex.value !== 0) sceneIndex.value = 0;
|
|
5242
|
+
return;
|
|
5243
|
+
}
|
|
5244
|
+
if (sceneIndex.value >= total) sceneIndex.value = 0;
|
|
5245
|
+
scheduleSceneAdvance();
|
|
5246
|
+
}
|
|
4385
5247
|
function sliceStartOffset() {
|
|
4386
5248
|
return effectiveOptions.value.__sliceStart || 0;
|
|
4387
5249
|
}
|
|
@@ -4427,17 +5289,24 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
4427
5289
|
const spaceBelow = containerRect.height - relY - gap;
|
|
4428
5290
|
const spaceAbove = relY - gap - legendBottom;
|
|
4429
5291
|
let top;
|
|
4430
|
-
if (
|
|
4431
|
-
|
|
4432
|
-
|
|
5292
|
+
if (th + 4 <= containerRect.height) {
|
|
5293
|
+
if (spaceBelow >= th) top = relY + gap;
|
|
5294
|
+
else if (spaceAbove >= th) top = relY - gap - th;
|
|
5295
|
+
else top = spaceBelow >= spaceAbove ? containerRect.height - th - margin : legendBottom;
|
|
5296
|
+
top = Math.max(legendBottom, Math.min(top, containerRect.height - th - margin));
|
|
5297
|
+
} else {
|
|
5298
|
+
const pointViewportY = containerRect.top + relY;
|
|
5299
|
+
const belowInViewport = pointViewportY + gap + th <= window.innerHeight - 8;
|
|
5300
|
+
const aboveInViewport = pointViewportY - gap - th >= 8;
|
|
5301
|
+
top = belowInViewport || !aboveInViewport ? relY + gap : relY - gap - th;
|
|
5302
|
+
}
|
|
4433
5303
|
left = Math.max(margin, Math.min(left, containerRect.width - tw - margin));
|
|
4434
|
-
top = Math.max(legendBottom, Math.min(top, containerRect.height - th - margin));
|
|
4435
5304
|
tooltipPlacement.value = "top";
|
|
4436
5305
|
tooltipX.value = left;
|
|
4437
5306
|
tooltipY.value = top;
|
|
4438
5307
|
}
|
|
4439
5308
|
const canvasStyle = computed(() => ({ cursor: hoverIndex >= 0 ? "pointer" : "default" }));
|
|
4440
|
-
function render(animate = true) {
|
|
5309
|
+
function render(animate = true, animOverride = null) {
|
|
4441
5310
|
const canvas = canvasRef.value;
|
|
4442
5311
|
if (!canvas) return;
|
|
4443
5312
|
const container = containerRef.value;
|
|
@@ -4480,8 +5349,20 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
4480
5349
|
lastHeight = height;
|
|
4481
5350
|
canvas.width = width * dpr.value;
|
|
4482
5351
|
canvas.height = height * dpr.value;
|
|
4483
|
-
const
|
|
4484
|
-
|
|
5352
|
+
const overlayPadding = getPadding(effectiveOptions.value, width);
|
|
5353
|
+
overlayInfo.value = {
|
|
5354
|
+
plotArea: {
|
|
5355
|
+
x: overlayPadding.left,
|
|
5356
|
+
y: overlayPadding.top,
|
|
5357
|
+
width: width - overlayPadding.left - overlayPadding.right,
|
|
5358
|
+
height: height - overlayPadding.top - overlayPadding.bottom
|
|
5359
|
+
},
|
|
5360
|
+
theme: getTheme(document.documentElement.classList.contains("dark"), effectiveOptions.value.theme),
|
|
5361
|
+
options: props.options
|
|
5362
|
+
};
|
|
5363
|
+
const animCfg = animOverride || props.options.animation;
|
|
5364
|
+
const animEnabled = animate && animCfg?.enabled !== false;
|
|
5365
|
+
animationState = createAnimation(animCfg);
|
|
4485
5366
|
animationState.isAnimating = animEnabled;
|
|
4486
5367
|
if (animEnabled) {
|
|
4487
5368
|
isEnterAnimating = true;
|
|
@@ -4633,9 +5514,24 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
4633
5514
|
hoverAnimFrameId = null;
|
|
4634
5515
|
}
|
|
4635
5516
|
}
|
|
4636
|
-
function
|
|
5517
|
+
function usesHoverAnimation() {
|
|
4637
5518
|
const t = props.options.type;
|
|
4638
|
-
|
|
5519
|
+
return t === "pie" || t === "doughnut" || t === "rose" || t === "sunburst" || t === "treemap";
|
|
5520
|
+
}
|
|
5521
|
+
function clearHover() {
|
|
5522
|
+
if (hoverIndex === -1) return false;
|
|
5523
|
+
hoverIndex = -1;
|
|
5524
|
+
const t = props.options.type;
|
|
5525
|
+
if (t === "pie" || t === "doughnut" || t === "rose") startHoverAnimation(-1);
|
|
5526
|
+
else {
|
|
5527
|
+
stopHoverAnimation();
|
|
5528
|
+
hoverAnimProgress = 0;
|
|
5529
|
+
redraw();
|
|
5530
|
+
}
|
|
5531
|
+
return true;
|
|
5532
|
+
}
|
|
5533
|
+
function startHoverAnimation(dir) {
|
|
5534
|
+
if (!usesHoverAnimation()) return;
|
|
4639
5535
|
if (props.options.animation?.enabled === false) {
|
|
4640
5536
|
hoverAnimProgress = dir > 0 ? 1 : 0;
|
|
4641
5537
|
redraw();
|
|
@@ -4676,7 +5572,8 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
4676
5572
|
"doughnut",
|
|
4677
5573
|
"rose",
|
|
4678
5574
|
"radar",
|
|
4679
|
-
"funnel"
|
|
5575
|
+
"funnel",
|
|
5576
|
+
"scatter"
|
|
4680
5577
|
];
|
|
4681
5578
|
function checkLegendHit(x, y) {
|
|
4682
5579
|
const canvas = canvasRef.value;
|
|
@@ -4869,20 +5766,14 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
4869
5766
|
};
|
|
4870
5767
|
}
|
|
4871
5768
|
if (options.type === "sunburst") {
|
|
4872
|
-
const
|
|
4873
|
-
if (
|
|
4874
|
-
const centerX
|
|
4875
|
-
const centerY = plotArea.y + plotArea.height / 2;
|
|
4876
|
-
const maxR = Math.max(40, Math.min(plotArea.width, plotArea.height) / 2 - 10);
|
|
4877
|
-
const innerHole = Math.max(0, maxR * .18);
|
|
4878
|
-
const depthCount = computeSunburstDepth(data);
|
|
4879
|
-
const ringWidth = (maxR - innerHole) / depthCount;
|
|
5769
|
+
const geo = computeSunburstGeometry(plotArea, options, theme, formatTooltipValue);
|
|
5770
|
+
if (!geo) return null;
|
|
5771
|
+
const { centerX, centerY, segments } = geo;
|
|
4880
5772
|
const dx = canvasX - centerX;
|
|
4881
5773
|
const dy = canvasY - centerY;
|
|
4882
5774
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
4883
5775
|
let hoverAngle = Math.atan2(dy, dx);
|
|
4884
5776
|
if (hoverAngle < -Math.PI / 2) hoverAngle += Math.PI * 2;
|
|
4885
|
-
const segments = layoutSunburst(data, innerHole, ringWidth, 0, -Math.PI / 2, 0, []);
|
|
4886
5777
|
for (let i = 0; i < segments.length; i++) {
|
|
4887
5778
|
const seg = segments[i];
|
|
4888
5779
|
if (distance >= seg.r0 && distance <= seg.r1 && hoverAngle >= seg.startAngle && hoverAngle < seg.endAngle) return {
|
|
@@ -4890,8 +5781,8 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
4890
5781
|
params: {
|
|
4891
5782
|
seriesName: seg.node.name,
|
|
4892
5783
|
name: seg.node.name,
|
|
4893
|
-
value: seg.node
|
|
4894
|
-
color: seg.
|
|
5784
|
+
value: sunburstValue(seg.node),
|
|
5785
|
+
color: seg.color,
|
|
4895
5786
|
dataIndex: i,
|
|
4896
5787
|
seriesIndex: 0
|
|
4897
5788
|
}
|
|
@@ -5502,18 +6393,15 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5502
6393
|
redraw();
|
|
5503
6394
|
}
|
|
5504
6395
|
const legendName = checkLegendHit(e.clientX, e.clientY);
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
focusSeries.value
|
|
5508
|
-
|
|
6396
|
+
if (emphasisSeriesName.value) {} else {
|
|
6397
|
+
const nextFocus = props.options.legend?.hoverEmphasis !== false && legendName ? legendName : null;
|
|
6398
|
+
if (nextFocus !== focusSeries.value) {
|
|
6399
|
+
focusSeries.value = nextFocus;
|
|
6400
|
+
redraw();
|
|
6401
|
+
}
|
|
5509
6402
|
}
|
|
5510
6403
|
if (legendName) {
|
|
5511
|
-
|
|
5512
|
-
hoverIndex = -1;
|
|
5513
|
-
const t = props.options.type;
|
|
5514
|
-
if (t === "pie" || t === "doughnut" || t === "rose") startHoverAnimation(-1);
|
|
5515
|
-
else redraw();
|
|
5516
|
-
}
|
|
6404
|
+
clearHover();
|
|
5517
6405
|
tooltipVisible.value = false;
|
|
5518
6406
|
return;
|
|
5519
6407
|
}
|
|
@@ -5532,6 +6420,10 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5532
6420
|
hoverAnimProgress = .35;
|
|
5533
6421
|
startHoverAnimation(1);
|
|
5534
6422
|
}
|
|
6423
|
+
} else if (usesHoverAnimation()) {
|
|
6424
|
+
stopHoverAnimation();
|
|
6425
|
+
hoverAnimProgress = 1;
|
|
6426
|
+
redraw();
|
|
5535
6427
|
} else redraw();
|
|
5536
6428
|
if (prevIndex === -1) {
|
|
5537
6429
|
const emitParams = Array.isArray(result.params) ? result.params[0] : result.params;
|
|
@@ -5556,10 +6448,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5556
6448
|
});
|
|
5557
6449
|
tooltipVisible.value = true;
|
|
5558
6450
|
} else {
|
|
5559
|
-
if (
|
|
5560
|
-
hoverIndex = -1;
|
|
5561
|
-
if (isPieLike) startHoverAnimation(-1);
|
|
5562
|
-
else redraw();
|
|
6451
|
+
if (clearHover()) {
|
|
5563
6452
|
emit("unhover");
|
|
5564
6453
|
ariaLiveText.value = "";
|
|
5565
6454
|
}
|
|
@@ -5634,18 +6523,14 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5634
6523
|
tooltipVisible.value = false;
|
|
5635
6524
|
mouseX = -1;
|
|
5636
6525
|
mouseY = -1;
|
|
5637
|
-
if (focusSeries.value !== null) focusSeries.value = null;
|
|
6526
|
+
if (focusSeries.value !== null && !emphasisSeriesName.value) focusSeries.value = null;
|
|
5638
6527
|
if (hoveredToolbox.value !== null) hoveredToolbox.value = null;
|
|
5639
6528
|
if (dragMode === "brush") {
|
|
5640
6529
|
brushRect.value = null;
|
|
5641
6530
|
redraw();
|
|
5642
6531
|
}
|
|
5643
6532
|
dragMode = "none";
|
|
5644
|
-
if (
|
|
5645
|
-
hoverIndex = -1;
|
|
5646
|
-
const t = props.options.type;
|
|
5647
|
-
if (t === "pie" || t === "doughnut" || t === "rose") startHoverAnimation(-1);
|
|
5648
|
-
else redraw();
|
|
6533
|
+
if (clearHover()) {
|
|
5649
6534
|
emit("unhover");
|
|
5650
6535
|
ariaLiveText.value = "";
|
|
5651
6536
|
}
|
|
@@ -5701,11 +6586,35 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5701
6586
|
}))
|
|
5702
6587
|
};
|
|
5703
6588
|
}
|
|
6589
|
+
function cloneSpec(value) {
|
|
6590
|
+
if (value === null || typeof value !== "object") return value;
|
|
6591
|
+
if (Array.isArray(value)) return value.map(cloneSpec);
|
|
6592
|
+
const out = {};
|
|
6593
|
+
Object.keys(value).forEach((k) => {
|
|
6594
|
+
out[k] = cloneSpec(value[k]);
|
|
6595
|
+
});
|
|
6596
|
+
return out;
|
|
6597
|
+
}
|
|
6598
|
+
const IS_DEV = typeof import.meta !== "undefined" && false;
|
|
6599
|
+
const devWarned = /* @__PURE__ */ new Set();
|
|
6600
|
+
function runDevValidation() {
|
|
6601
|
+
if (!IS_DEV) return;
|
|
6602
|
+
const { warnings } = validateOptions(props.options);
|
|
6603
|
+
warnings.slice(0, 8).forEach((w) => {
|
|
6604
|
+
const key = `${w.path}:${w.message}`;
|
|
6605
|
+
if (devWarned.has(key)) return;
|
|
6606
|
+
devWarned.add(key);
|
|
6607
|
+
console.warn(`[EvChart] ${w.path} ${w.message}`);
|
|
6608
|
+
});
|
|
6609
|
+
}
|
|
5704
6610
|
let prevOptionsSnapshot = null;
|
|
5705
6611
|
watch(() => props.options, (next) => {
|
|
5706
6612
|
cachedDataExtent = null;
|
|
5707
6613
|
cachedPlotArea = null;
|
|
5708
6614
|
internalError.value = null;
|
|
6615
|
+
runDevValidation();
|
|
6616
|
+
focusSeries.value = emphasisSeriesName.value;
|
|
6617
|
+
syncScenes();
|
|
5709
6618
|
const zoom = getDataZoomConfig(props.options);
|
|
5710
6619
|
const zoomKey = zoom ? JSON.stringify({
|
|
5711
6620
|
e: zoom.enabled,
|
|
@@ -5759,6 +6668,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5759
6668
|
let unregisterConnector = null;
|
|
5760
6669
|
onMounted(() => {
|
|
5761
6670
|
render(true);
|
|
6671
|
+
syncScenes();
|
|
5762
6672
|
setupResizeObserver();
|
|
5763
6673
|
setupDarkModeObserver();
|
|
5764
6674
|
window.addEventListener("pointerup", onWindowPointerUp);
|
|
@@ -5778,6 +6688,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5778
6688
|
stopAnimation();
|
|
5779
6689
|
stopHoverAnimation();
|
|
5780
6690
|
stopTweenAnimation();
|
|
6691
|
+
stopSceneTimer();
|
|
5781
6692
|
if (tooltipRafId !== null) {
|
|
5782
6693
|
cancelAnimationFrame(tooltipRafId);
|
|
5783
6694
|
tooltipRafId = null;
|
|
@@ -5791,6 +6702,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5791
6702
|
update(newOptions) {
|
|
5792
6703
|
Object.assign(props.options, newOptions);
|
|
5793
6704
|
cachedDataExtent = null;
|
|
6705
|
+
specVersion.value++;
|
|
5794
6706
|
debouncedRender(true);
|
|
5795
6707
|
},
|
|
5796
6708
|
toDataURL(type = "image/png", quality = 1) {
|
|
@@ -5801,6 +6713,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5801
6713
|
cleanupDarkModeObserver();
|
|
5802
6714
|
stopAnimation();
|
|
5803
6715
|
stopHoverAnimation();
|
|
6716
|
+
stopSceneTimer();
|
|
5804
6717
|
if (tooltipRafId !== null) {
|
|
5805
6718
|
cancelAnimationFrame(tooltipRafId);
|
|
5806
6719
|
tooltipRafId = null;
|
|
@@ -5890,6 +6803,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5890
6803
|
...props.options.theme,
|
|
5891
6804
|
...theme
|
|
5892
6805
|
};
|
|
6806
|
+
specVersion.value++;
|
|
5893
6807
|
redraw();
|
|
5894
6808
|
},
|
|
5895
6809
|
exportSVG(options = {}) {
|
|
@@ -5932,6 +6846,59 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5932
6846
|
getOption() {
|
|
5933
6847
|
return props.options;
|
|
5934
6848
|
},
|
|
6849
|
+
getSpec() {
|
|
6850
|
+
return cloneSpec(props.options);
|
|
6851
|
+
},
|
|
6852
|
+
setSpec(spec) {
|
|
6853
|
+
if (spec === null || typeof spec !== "object") return;
|
|
6854
|
+
const next = cloneSpec(spec);
|
|
6855
|
+
Object.keys(props.options).forEach((k) => delete props.options[k]);
|
|
6856
|
+
Object.assign(props.options, next);
|
|
6857
|
+
hiddenSeries.value = /* @__PURE__ */ new Set();
|
|
6858
|
+
focusSeries.value = null;
|
|
6859
|
+
brushRect.value = null;
|
|
6860
|
+
hoverIndex = -1;
|
|
6861
|
+
mouseX = -1;
|
|
6862
|
+
mouseY = -1;
|
|
6863
|
+
stopSceneTimer();
|
|
6864
|
+
sceneIndex.value = 0;
|
|
6865
|
+
sceneAnimOverride = null;
|
|
6866
|
+
const zoom = getDataZoomConfig(next);
|
|
6867
|
+
zoomRange.value = {
|
|
6868
|
+
start: zoom?.start ?? 0,
|
|
6869
|
+
end: zoom?.end ?? 100
|
|
6870
|
+
};
|
|
6871
|
+
lastZoomKey = zoom ? JSON.stringify({
|
|
6872
|
+
e: zoom.enabled,
|
|
6873
|
+
s: zoom.start,
|
|
6874
|
+
e2: zoom.end,
|
|
6875
|
+
p: zoom.position,
|
|
6876
|
+
h: zoom.height
|
|
6877
|
+
}) : "";
|
|
6878
|
+
cachedDataExtent = null;
|
|
6879
|
+
cachedPlotArea = null;
|
|
6880
|
+
specVersion.value++;
|
|
6881
|
+
debouncedRender(true);
|
|
6882
|
+
},
|
|
6883
|
+
nextScene() {
|
|
6884
|
+
applyScene(sceneIndex.value + 1);
|
|
6885
|
+
},
|
|
6886
|
+
prevScene() {
|
|
6887
|
+
applyScene(sceneIndex.value - 1);
|
|
6888
|
+
},
|
|
6889
|
+
gotoScene(i) {
|
|
6890
|
+
applyScene(i);
|
|
6891
|
+
},
|
|
6892
|
+
getSceneIndex() {
|
|
6893
|
+
return sceneIndex.value;
|
|
6894
|
+
},
|
|
6895
|
+
getEffectiveSpec() {
|
|
6896
|
+
const spec = cloneSpec(effectiveOptions.value);
|
|
6897
|
+
Object.keys(spec).forEach((k) => {
|
|
6898
|
+
if (k.startsWith("__")) delete spec[k];
|
|
6899
|
+
});
|
|
6900
|
+
return spec;
|
|
6901
|
+
},
|
|
5935
6902
|
setDataZoomRange(start, end) {
|
|
5936
6903
|
if (!isZoomEnabled()) {
|
|
5937
6904
|
console.warn("[EvChart] setDataZoomRange: dataZoom is not enabled");
|
|
@@ -5969,12 +6936,17 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5969
6936
|
onDblclick: handleDblClick,
|
|
5970
6937
|
onPointerleave: handlePointerLeave
|
|
5971
6938
|
}, null, 36)) : createCommentVNode("", true),
|
|
6939
|
+
!isEmpty.value && !props.options.loading && !internalError.value && _ctx.$slots.overlay ? (openBlock(), createElementBlock("div", _hoisted_2, [renderSlot(_ctx.$slots, "overlay", {
|
|
6940
|
+
plotArea: overlayInfo.value.plotArea,
|
|
6941
|
+
theme: overlayInfo.value.theme,
|
|
6942
|
+
options: props.options
|
|
6943
|
+
}, void 0, true)])) : createCommentVNode("", true),
|
|
5972
6944
|
createVNode(Transition, { name: "ev-chart-fade" }, {
|
|
5973
|
-
default: withCtx(() => [isEmpty.value && !props.options.loading && !internalError.value ? (openBlock(), createElementBlock("div",
|
|
6945
|
+
default: withCtx(() => [isEmpty.value && !props.options.loading && !internalError.value ? (openBlock(), createElementBlock("div", _hoisted_3, [createElementVNode("div", _hoisted_4, [(openBlock(), createBlock(resolveDynamicComponent(DataLine), { class: "ev-chart__empty-img" }))]), createElementVNode("span", _hoisted_5, toDisplayString(emptyTextI18n.value), 1)])) : createCommentVNode("", true)]),
|
|
5974
6946
|
_: 1
|
|
5975
6947
|
}),
|
|
5976
6948
|
createVNode(Transition, { name: "ev-chart-fade" }, {
|
|
5977
|
-
default: withCtx(() => [props.options.loading && !internalError.value ? (openBlock(), createElementBlock("div",
|
|
6949
|
+
default: withCtx(() => [props.options.loading && !internalError.value ? (openBlock(), createElementBlock("div", _hoisted_6, [createElementVNode("div", _hoisted_7, [(openBlock(), createElementBlock(Fragment, null, renderList(6, (i) => {
|
|
5978
6950
|
return createElementVNode("div", {
|
|
5979
6951
|
key: i,
|
|
5980
6952
|
class: "ev-chart__loading-bar",
|
|
@@ -5984,7 +6956,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5984
6956
|
_: 1
|
|
5985
6957
|
}),
|
|
5986
6958
|
createVNode(Transition, { name: "ev-chart-fade" }, {
|
|
5987
|
-
default: withCtx(() => [internalError.value ? (openBlock(), createElementBlock("div",
|
|
6959
|
+
default: withCtx(() => [internalError.value ? (openBlock(), createElementBlock("div", _hoisted_8, [_cache[0] || (_cache[0] = createElementVNode("span", { class: "ev-chart__error-icon" }, [createElementVNode("svg", {
|
|
5988
6960
|
viewBox: "0 0 24 24",
|
|
5989
6961
|
width: "1em",
|
|
5990
6962
|
height: "1em",
|
|
@@ -6012,7 +6984,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
6012
6984
|
x2: "12",
|
|
6013
6985
|
y2: "16.51"
|
|
6014
6986
|
})
|
|
6015
|
-
])], -1)), createElementVNode("span",
|
|
6987
|
+
])], -1)), createElementVNode("span", _hoisted_9, toDisplayString(internalError.value), 1)])) : createCommentVNode("", true)]),
|
|
6016
6988
|
_: 1
|
|
6017
6989
|
}),
|
|
6018
6990
|
createVNode(Transition, { name: "ev-chart-tooltip" }, {
|
|
@@ -6024,14 +6996,14 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
6024
6996
|
style: normalizeStyle(tooltipStyle.value),
|
|
6025
6997
|
role: "tooltip",
|
|
6026
6998
|
innerHTML: tooltipContent.value
|
|
6027
|
-
}, null, 14,
|
|
6999
|
+
}, null, 14, _hoisted_10)) : createCommentVNode("", true)]),
|
|
6028
7000
|
_: 1
|
|
6029
7001
|
}),
|
|
6030
|
-
createElementVNode("div",
|
|
7002
|
+
createElementVNode("div", _hoisted_11, toDisplayString(ariaLiveText.value), 1)
|
|
6031
7003
|
], 12, _hoisted_1);
|
|
6032
7004
|
};
|
|
6033
7005
|
}
|
|
6034
|
-
}), [["__scopeId", "data-v-
|
|
7006
|
+
}), [["__scopeId", "data-v-0c5d944e"]]);
|
|
6035
7007
|
//#endregion
|
|
6036
7008
|
//#region src/useChart.js
|
|
6037
7009
|
var USE_CHART_KEY = /* @__PURE__ */ Symbol("ev-chart-context");
|
|
@@ -6184,6 +7156,742 @@ function clearSeriesPalette(options = {}) {
|
|
|
6184
7156
|
return true;
|
|
6185
7157
|
}
|
|
6186
7158
|
//#endregion
|
|
7159
|
+
//#region src/ai/table.js
|
|
7160
|
+
var NUM_CLEAN = /[,\s_]/g;
|
|
7161
|
+
function parseNumeric(raw) {
|
|
7162
|
+
if (typeof raw === "number") return Number.isFinite(raw) ? raw : null;
|
|
7163
|
+
if (typeof raw !== "string") return null;
|
|
7164
|
+
let s = raw.trim();
|
|
7165
|
+
if (!s) return null;
|
|
7166
|
+
let sign = 1;
|
|
7167
|
+
if (/^\(.*\)$/.test(s)) {
|
|
7168
|
+
sign = -1;
|
|
7169
|
+
s = s.slice(1, -1);
|
|
7170
|
+
}
|
|
7171
|
+
s = s.replace(NUM_CLEAN, "").replace(/^[¥$€£]/, "").replace(/[¥$€£]$/, "");
|
|
7172
|
+
let mult = 1;
|
|
7173
|
+
if (/亿$/.test(s)) {
|
|
7174
|
+
mult = 1e8;
|
|
7175
|
+
s = s.slice(0, -1);
|
|
7176
|
+
} else if (/万$/.test(s)) {
|
|
7177
|
+
mult = 1e4;
|
|
7178
|
+
s = s.slice(0, -1);
|
|
7179
|
+
} else if (/[Kk]$/.test(s)) {
|
|
7180
|
+
mult = 1e3;
|
|
7181
|
+
s = s.slice(0, -1);
|
|
7182
|
+
}
|
|
7183
|
+
if (/%$/.test(s)) s = s.slice(0, -1);
|
|
7184
|
+
if (!/^[-+]?\d+(\.\d+)?$/.test(s)) return null;
|
|
7185
|
+
return sign * parseFloat(s) * mult;
|
|
7186
|
+
}
|
|
7187
|
+
function looksLikeTime(v) {
|
|
7188
|
+
if (typeof v !== "string") return false;
|
|
7189
|
+
const s = v.trim();
|
|
7190
|
+
if (/^\d{4}[-/年.]\d{1,2}月?(?:[-/.]\d{1,2}日?)?$/.test(s)) return true;
|
|
7191
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(s)) return true;
|
|
7192
|
+
if (/^\d{1,2}:\d{2}(:\d{2})?$/.test(s)) return true;
|
|
7193
|
+
return false;
|
|
7194
|
+
}
|
|
7195
|
+
function splitLine(line, delim) {
|
|
7196
|
+
const out = [];
|
|
7197
|
+
let cur = "";
|
|
7198
|
+
let inQuotes = false;
|
|
7199
|
+
for (let i = 0; i < line.length; i++) {
|
|
7200
|
+
const ch = line[i];
|
|
7201
|
+
if (ch === "\"") {
|
|
7202
|
+
if (inQuotes && line[i + 1] === "\"") {
|
|
7203
|
+
cur += "\"";
|
|
7204
|
+
i++;
|
|
7205
|
+
} else inQuotes = !inQuotes;
|
|
7206
|
+
} else if (ch === delim && !inQuotes) {
|
|
7207
|
+
out.push(cur.trim());
|
|
7208
|
+
cur = "";
|
|
7209
|
+
} else cur += ch;
|
|
7210
|
+
}
|
|
7211
|
+
out.push(cur.trim());
|
|
7212
|
+
return out;
|
|
7213
|
+
}
|
|
7214
|
+
function pickDelimiter(line) {
|
|
7215
|
+
const candidates = [
|
|
7216
|
+
" ",
|
|
7217
|
+
";",
|
|
7218
|
+
",",
|
|
7219
|
+
","
|
|
7220
|
+
];
|
|
7221
|
+
let best = ",";
|
|
7222
|
+
let bestCount = 0;
|
|
7223
|
+
candidates.forEach((d) => {
|
|
7224
|
+
const n = splitLine(line, d).length;
|
|
7225
|
+
if (n > bestCount) {
|
|
7226
|
+
bestCount = n;
|
|
7227
|
+
best = d;
|
|
7228
|
+
}
|
|
7229
|
+
});
|
|
7230
|
+
return best;
|
|
7231
|
+
}
|
|
7232
|
+
function rowToObj(headers, row) {
|
|
7233
|
+
const obj = {};
|
|
7234
|
+
headers.forEach((h, i) => {
|
|
7235
|
+
obj[h] = row[i] === void 0 ? null : row[i];
|
|
7236
|
+
});
|
|
7237
|
+
return obj;
|
|
7238
|
+
}
|
|
7239
|
+
function parseDataTable(input) {
|
|
7240
|
+
if (Array.isArray(input)) {
|
|
7241
|
+
if (input.length === 0) return null;
|
|
7242
|
+
const first = input[0];
|
|
7243
|
+
if (first !== null && typeof first === "object" && !Array.isArray(first)) {
|
|
7244
|
+
const headers = [];
|
|
7245
|
+
input.forEach((row) => {
|
|
7246
|
+
Object.keys(row).forEach((k) => {
|
|
7247
|
+
if (!headers.includes(k)) headers.push(k);
|
|
7248
|
+
});
|
|
7249
|
+
});
|
|
7250
|
+
return {
|
|
7251
|
+
headers,
|
|
7252
|
+
rows: input
|
|
7253
|
+
};
|
|
7254
|
+
}
|
|
7255
|
+
if (Array.isArray(first)) {
|
|
7256
|
+
if (input.length < 1) return null;
|
|
7257
|
+
const width = input.reduce((m, r) => Math.max(m, r.length), 0);
|
|
7258
|
+
if (width < 2) return null;
|
|
7259
|
+
const head = input[0].map((v) => v === null || v === void 0 ? "" : String(v).trim());
|
|
7260
|
+
if (head.filter((v) => parseNumeric(v) !== null).length === 0) return {
|
|
7261
|
+
headers: head,
|
|
7262
|
+
rows: input.slice(1).map((r) => rowToObj(head, r))
|
|
7263
|
+
};
|
|
7264
|
+
const headers = Array.from({ length: width }, (_, i) => `列${i + 1}`);
|
|
7265
|
+
return {
|
|
7266
|
+
headers,
|
|
7267
|
+
rows: input.map((r) => rowToObj(headers, r))
|
|
7268
|
+
};
|
|
7269
|
+
}
|
|
7270
|
+
return null;
|
|
7271
|
+
}
|
|
7272
|
+
if (typeof input === "string") {
|
|
7273
|
+
const lines = input.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
7274
|
+
if (lines.length < 2) return null;
|
|
7275
|
+
const delim = pickDelimiter(lines[0]);
|
|
7276
|
+
const grid = lines.map((l) => splitLine(l, delim));
|
|
7277
|
+
const head = grid[0];
|
|
7278
|
+
if (head.filter((v) => parseNumeric(v) !== null).length === 0 && head.length >= 2) return {
|
|
7279
|
+
headers: head,
|
|
7280
|
+
rows: grid.slice(1).map((r) => rowToObj(head, r))
|
|
7281
|
+
};
|
|
7282
|
+
const width = grid.reduce((m, r) => Math.max(m, r.length), 0);
|
|
7283
|
+
const headers = Array.from({ length: width }, (_, i) => `列${i + 1}`);
|
|
7284
|
+
return {
|
|
7285
|
+
headers,
|
|
7286
|
+
rows: grid.map((r) => rowToObj(headers, r))
|
|
7287
|
+
};
|
|
7288
|
+
}
|
|
7289
|
+
return null;
|
|
7290
|
+
}
|
|
7291
|
+
function inferColumns(table) {
|
|
7292
|
+
const { headers, rows } = table;
|
|
7293
|
+
return headers.map((name) => {
|
|
7294
|
+
const values = rows.map((r) => r[name] === void 0 ? null : r[name]);
|
|
7295
|
+
const present = values.filter((v) => v !== null && v !== "").length;
|
|
7296
|
+
const numericHits = values.filter((v) => parseNumeric(v) !== null).length;
|
|
7297
|
+
if (present > 0 && numericHits / present >= .6) return {
|
|
7298
|
+
name,
|
|
7299
|
+
type: "number",
|
|
7300
|
+
values: values.map((v) => parseNumeric(v))
|
|
7301
|
+
};
|
|
7302
|
+
const timeHits = values.filter((v) => looksLikeTime(v)).length;
|
|
7303
|
+
if (present > 0 && timeHits / present >= .6) return {
|
|
7304
|
+
name,
|
|
7305
|
+
type: "time",
|
|
7306
|
+
values
|
|
7307
|
+
};
|
|
7308
|
+
return {
|
|
7309
|
+
name,
|
|
7310
|
+
type: "category",
|
|
7311
|
+
values
|
|
7312
|
+
};
|
|
7313
|
+
});
|
|
7314
|
+
}
|
|
7315
|
+
function distinctCount(values) {
|
|
7316
|
+
return new Set(values.filter((v) => v !== null && v !== "")).size;
|
|
7317
|
+
}
|
|
7318
|
+
//#endregion
|
|
7319
|
+
//#region src/ai/spec.js
|
|
7320
|
+
var INTENT_KEYWORDS = {
|
|
7321
|
+
trend: [
|
|
7322
|
+
"趋势",
|
|
7323
|
+
"走势",
|
|
7324
|
+
"变化",
|
|
7325
|
+
"随时间",
|
|
7326
|
+
"增长",
|
|
7327
|
+
"trend"
|
|
7328
|
+
],
|
|
7329
|
+
share: [
|
|
7330
|
+
"占比",
|
|
7331
|
+
"构成",
|
|
7332
|
+
"份额",
|
|
7333
|
+
"比例",
|
|
7334
|
+
"组成",
|
|
7335
|
+
"share"
|
|
7336
|
+
],
|
|
7337
|
+
rank: [
|
|
7338
|
+
"排行",
|
|
7339
|
+
"排名",
|
|
7340
|
+
"前几",
|
|
7341
|
+
"highest",
|
|
7342
|
+
"rank"
|
|
7343
|
+
],
|
|
7344
|
+
compare: [
|
|
7345
|
+
"对比",
|
|
7346
|
+
"比较",
|
|
7347
|
+
"对照",
|
|
7348
|
+
"相比",
|
|
7349
|
+
"compare"
|
|
7350
|
+
],
|
|
7351
|
+
relation: [
|
|
7352
|
+
"关系",
|
|
7353
|
+
"相关",
|
|
7354
|
+
"散点",
|
|
7355
|
+
"分布",
|
|
7356
|
+
"relation",
|
|
7357
|
+
"scatter"
|
|
7358
|
+
],
|
|
7359
|
+
stack: [
|
|
7360
|
+
"叠加",
|
|
7361
|
+
"堆叠",
|
|
7362
|
+
"累计",
|
|
7363
|
+
"stack"
|
|
7364
|
+
]
|
|
7365
|
+
};
|
|
7366
|
+
function detectIntent(text) {
|
|
7367
|
+
if (!text || typeof text !== "string") return null;
|
|
7368
|
+
for (const intent of [
|
|
7369
|
+
"trend",
|
|
7370
|
+
"stack",
|
|
7371
|
+
"share",
|
|
7372
|
+
"rank",
|
|
7373
|
+
"relation",
|
|
7374
|
+
"compare"
|
|
7375
|
+
]) if (INTENT_KEYWORDS[intent].some((kw) => text.includes(kw))) return intent;
|
|
7376
|
+
return null;
|
|
7377
|
+
}
|
|
7378
|
+
function fmtValue(v) {
|
|
7379
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return String(v);
|
|
7380
|
+
if (Math.abs(v) >= 1e8) return `${(v / 1e8).toFixed(1)}亿`;
|
|
7381
|
+
if (Math.abs(v) >= 1e4) return `${(v / 1e4).toFixed(1)}万`;
|
|
7382
|
+
return `${Math.round(v * 100) / 100}`;
|
|
7383
|
+
}
|
|
7384
|
+
function buildNarrative(spec, labels, seriesList) {
|
|
7385
|
+
const annotations = [];
|
|
7386
|
+
let best = null;
|
|
7387
|
+
seriesList.forEach((s, si) => {
|
|
7388
|
+
s.data.forEach((v, di) => {
|
|
7389
|
+
if (typeof v === "number" && Number.isFinite(v) && (!best || v > best.value)) best = {
|
|
7390
|
+
value: v,
|
|
7391
|
+
label: labels[di],
|
|
7392
|
+
series: s.name,
|
|
7393
|
+
si,
|
|
7394
|
+
di
|
|
7395
|
+
};
|
|
7396
|
+
});
|
|
7397
|
+
});
|
|
7398
|
+
if (best) annotations.push({
|
|
7399
|
+
type: "callout",
|
|
7400
|
+
x: best.label,
|
|
7401
|
+
y: best.value,
|
|
7402
|
+
series: best.series,
|
|
7403
|
+
label: `峰值 ${fmtValue(best.value)}`,
|
|
7404
|
+
anchor: "top-right"
|
|
7405
|
+
});
|
|
7406
|
+
const first = seriesList[0];
|
|
7407
|
+
if (first && first.data.length >= 2) {
|
|
7408
|
+
const prev = first.data[first.data.length - 2];
|
|
7409
|
+
const last = first.data[first.data.length - 1];
|
|
7410
|
+
if (typeof prev === "number" && typeof last === "number" && prev !== 0) {
|
|
7411
|
+
const pct = (last - prev) / Math.abs(prev);
|
|
7412
|
+
if (Math.abs(pct) >= .01) annotations.push({
|
|
7413
|
+
type: "delta",
|
|
7414
|
+
x: labels[labels.length - 1],
|
|
7415
|
+
y: last,
|
|
7416
|
+
series: first.name,
|
|
7417
|
+
direction: pct >= 0 ? "up" : "down",
|
|
7418
|
+
text: `环比 ${pct >= 0 ? "+" : ""}${(pct * 100).toFixed(0)}%`
|
|
7419
|
+
});
|
|
7420
|
+
}
|
|
7421
|
+
}
|
|
7422
|
+
if (annotations.length > 0) spec.annotations = annotations.slice(0, 3);
|
|
7423
|
+
}
|
|
7424
|
+
function generateChartSpec(data, hint = {}) {
|
|
7425
|
+
const table = parseDataTable(data);
|
|
7426
|
+
if (!table) return {
|
|
7427
|
+
spec: null,
|
|
7428
|
+
report: [{
|
|
7429
|
+
level: "error",
|
|
7430
|
+
message: "无法识别的数据格式:支持 CSV/TSV 文本、对象数组、二维数组"
|
|
7431
|
+
}]
|
|
7432
|
+
};
|
|
7433
|
+
const cols = inferColumns(table);
|
|
7434
|
+
const numbers = cols.filter((c) => c.type === "number");
|
|
7435
|
+
const times = cols.filter((c) => c.type === "time");
|
|
7436
|
+
const cats = cols.filter((c) => c.type === "category" && distinctCount(c.values) > 0);
|
|
7437
|
+
const dimension = times[0] || cats[0] || null;
|
|
7438
|
+
const labels = dimension ? dimension.values.map((v) => String(v)) : table.rows.map((_, i) => String(i + 1));
|
|
7439
|
+
const intent = hint.intent || detectIntent([hint.title, hint.hint].filter(Boolean).join(" ")) || defaultIntent({
|
|
7440
|
+
times: times.length,
|
|
7441
|
+
numbers: numbers.length,
|
|
7442
|
+
cats: cats.length,
|
|
7443
|
+
labels: labels.length
|
|
7444
|
+
});
|
|
7445
|
+
const report = [{
|
|
7446
|
+
level: "info",
|
|
7447
|
+
message: `识别到 ${numbers.length} 个数值列、${times.length} 个时间列、${cats.length} 个类目列;意图「${intent}」`
|
|
7448
|
+
}];
|
|
7449
|
+
const spec = {};
|
|
7450
|
+
if (hint.title) spec.title = hint.title;
|
|
7451
|
+
if (intent === "relation" && numbers.length >= 2) {
|
|
7452
|
+
const cat = cats[0];
|
|
7453
|
+
spec.type = "scatter";
|
|
7454
|
+
spec.scatterData = table.rows.map((_, i) => ({
|
|
7455
|
+
x: numbers[0].values[i],
|
|
7456
|
+
y: numbers[1].values[i],
|
|
7457
|
+
label: cat ? String(cat.values[i]) : String(i + 1)
|
|
7458
|
+
}));
|
|
7459
|
+
report.push({
|
|
7460
|
+
level: "info",
|
|
7461
|
+
message: `散点图:${numbers[0].name} → x,${numbers[1].name} → y`
|
|
7462
|
+
});
|
|
7463
|
+
return {
|
|
7464
|
+
spec,
|
|
7465
|
+
report
|
|
7466
|
+
};
|
|
7467
|
+
}
|
|
7468
|
+
if (numbers.length === 0) return {
|
|
7469
|
+
spec: null,
|
|
7470
|
+
report: [{
|
|
7471
|
+
level: "error",
|
|
7472
|
+
message: "数据中没有可用的数值列(列需 ≥60% 为可解析数字)"
|
|
7473
|
+
}]
|
|
7474
|
+
};
|
|
7475
|
+
const seriesList = numbers.slice(0, 8).map((c) => ({
|
|
7476
|
+
name: c.name,
|
|
7477
|
+
data: c.values
|
|
7478
|
+
}));
|
|
7479
|
+
if (numbers.length > 8) report.push({
|
|
7480
|
+
level: "warn",
|
|
7481
|
+
message: `数值列超过 8 个,仅取前 8 列(${seriesList.map((s) => s.name).join("、")})`
|
|
7482
|
+
});
|
|
7483
|
+
if (intent === "share") {
|
|
7484
|
+
if (seriesList.length === 1 && labels.length <= 8) {
|
|
7485
|
+
const pieData = labels.map((name, i) => ({
|
|
7486
|
+
name,
|
|
7487
|
+
value: seriesList[0].data[i]
|
|
7488
|
+
})).filter((d) => typeof d.value === "number" && d.value > 0);
|
|
7489
|
+
spec.type = "pie";
|
|
7490
|
+
spec.pieData = pieData;
|
|
7491
|
+
report.push({
|
|
7492
|
+
level: "info",
|
|
7493
|
+
message: `饼图:${seriesList[0].name} 按类目占比(${pieData.length} 项)`
|
|
7494
|
+
});
|
|
7495
|
+
return {
|
|
7496
|
+
spec,
|
|
7497
|
+
report
|
|
7498
|
+
};
|
|
7499
|
+
}
|
|
7500
|
+
if (seriesList.length >= 2 && seriesList.length <= 8) {
|
|
7501
|
+
const sums = seriesList.map((s) => s.data.reduce((acc, v) => typeof v === "number" && Number.isFinite(v) ? acc + v : acc, 0));
|
|
7502
|
+
if (sums.every((v) => v > 0)) {
|
|
7503
|
+
spec.type = "pie";
|
|
7504
|
+
spec.pieData = seriesList.map((s, i) => ({
|
|
7505
|
+
name: s.name,
|
|
7506
|
+
value: sums[i]
|
|
7507
|
+
}));
|
|
7508
|
+
report.push({
|
|
7509
|
+
level: "info",
|
|
7510
|
+
message: `饼图:按度量合计占比(${sums.length} 项)`
|
|
7511
|
+
});
|
|
7512
|
+
return {
|
|
7513
|
+
spec,
|
|
7514
|
+
report
|
|
7515
|
+
};
|
|
7516
|
+
}
|
|
7517
|
+
}
|
|
7518
|
+
}
|
|
7519
|
+
const labelWidth = labels.reduce((m, l) => Math.max(m, l.length), 0);
|
|
7520
|
+
const wantRank = intent === "rank" || intent === "compare" && seriesList.length === 1 && labels.length >= 9;
|
|
7521
|
+
if (seriesList.length === 1 && (wantRank || labels.length >= 13 || labelWidth >= 5 && labels.length >= 7)) {
|
|
7522
|
+
spec.type = "horizontal-bar";
|
|
7523
|
+
spec.labels = labels;
|
|
7524
|
+
spec.series = seriesList;
|
|
7525
|
+
report.push({
|
|
7526
|
+
level: "info",
|
|
7527
|
+
message: "横向条形图:单度量分类对比,类目名走纵列更易读"
|
|
7528
|
+
});
|
|
7529
|
+
finishCartesian(spec, hint, labels, seriesList, report);
|
|
7530
|
+
return {
|
|
7531
|
+
spec,
|
|
7532
|
+
report
|
|
7533
|
+
};
|
|
7534
|
+
}
|
|
7535
|
+
if (intent === "stack" && seriesList.length >= 2) {
|
|
7536
|
+
spec.type = "stacked-bar";
|
|
7537
|
+
report.push({
|
|
7538
|
+
level: "info",
|
|
7539
|
+
message: "堆叠柱状图:多度量叠加看构成"
|
|
7540
|
+
});
|
|
7541
|
+
} else if ((intent === "compare" || intent === "rank") && seriesList.length <= 3 && labels.length <= 12) {
|
|
7542
|
+
spec.type = "bar";
|
|
7543
|
+
report.push({
|
|
7544
|
+
level: "info",
|
|
7545
|
+
message: "柱状图:类目间量值对比"
|
|
7546
|
+
});
|
|
7547
|
+
} else {
|
|
7548
|
+
spec.type = "line";
|
|
7549
|
+
report.push({
|
|
7550
|
+
level: "info",
|
|
7551
|
+
message: "折线图:维度有序(时间/类目),看走势与多系列对比"
|
|
7552
|
+
});
|
|
7553
|
+
}
|
|
7554
|
+
spec.labels = labels;
|
|
7555
|
+
spec.series = seriesList;
|
|
7556
|
+
if (seriesList.length > 1) spec.legend = { show: true };
|
|
7557
|
+
finishCartesian(spec, hint, labels, seriesList, report);
|
|
7558
|
+
return {
|
|
7559
|
+
spec,
|
|
7560
|
+
report
|
|
7561
|
+
};
|
|
7562
|
+
}
|
|
7563
|
+
function finishCartesian(spec, hint, labels, seriesList, report) {
|
|
7564
|
+
if (hint.narrative) {
|
|
7565
|
+
buildNarrative(spec, labels, seriesList);
|
|
7566
|
+
if (spec.annotations) report.push({
|
|
7567
|
+
level: "info",
|
|
7568
|
+
message: `叙述注解 ${spec.annotations.length} 处(峰值 callout / 末点环比 delta)`
|
|
7569
|
+
});
|
|
7570
|
+
}
|
|
7571
|
+
}
|
|
7572
|
+
function defaultIntent({ times, numbers, cats, labels }) {
|
|
7573
|
+
if (times > 0 && numbers > 0) return "trend";
|
|
7574
|
+
if (numbers >= 2 && cats === 0) return "relation";
|
|
7575
|
+
if (numbers === 1 && (cats > 0 || labels > 0)) return "compare";
|
|
7576
|
+
return "trend";
|
|
7577
|
+
}
|
|
7578
|
+
var SPEC_RULES = [
|
|
7579
|
+
"type 必填,取值必须是 schema enum 中的图表类型",
|
|
7580
|
+
"series[].data 长度必须与 labels 一致,缺失值用 null",
|
|
7581
|
+
"叙述注解 annotations 单图不超过 3 处,emphasis 焦点最多 1 个系列",
|
|
7582
|
+
"不使用 schema 之外的字段",
|
|
7583
|
+
`支持的图表类型:${chartOptionsSchema.properties.type.enum.join(" / ")}`
|
|
7584
|
+
];
|
|
7585
|
+
//#endregion
|
|
7586
|
+
//#region src/ai/examples.js
|
|
7587
|
+
var SPEC_EXAMPLES = [
|
|
7588
|
+
{
|
|
7589
|
+
requirement: "看上半年各渠道销售额走势",
|
|
7590
|
+
spec: {
|
|
7591
|
+
type: "line",
|
|
7592
|
+
title: "渠道月度销售额",
|
|
7593
|
+
labels: [
|
|
7594
|
+
"1月",
|
|
7595
|
+
"2月",
|
|
7596
|
+
"3月",
|
|
7597
|
+
"4月",
|
|
7598
|
+
"5月",
|
|
7599
|
+
"6月"
|
|
7600
|
+
],
|
|
7601
|
+
series: [{
|
|
7602
|
+
name: "线上商城",
|
|
7603
|
+
data: [
|
|
7604
|
+
320,
|
|
7605
|
+
356,
|
|
7606
|
+
401,
|
|
7607
|
+
388,
|
|
7608
|
+
520,
|
|
7609
|
+
560
|
|
7610
|
+
]
|
|
7611
|
+
}, {
|
|
7612
|
+
name: "线下门店",
|
|
7613
|
+
data: [
|
|
7614
|
+
280,
|
|
7615
|
+
302,
|
|
7616
|
+
346,
|
|
7617
|
+
331,
|
|
7618
|
+
352,
|
|
7619
|
+
428
|
|
7620
|
+
]
|
|
7621
|
+
}],
|
|
7622
|
+
legend: { show: true }
|
|
7623
|
+
}
|
|
7624
|
+
},
|
|
7625
|
+
{
|
|
7626
|
+
requirement: "各渠道销售额占比",
|
|
7627
|
+
spec: {
|
|
7628
|
+
type: "pie",
|
|
7629
|
+
title: "渠道销售额占比",
|
|
7630
|
+
pieData: [
|
|
7631
|
+
{
|
|
7632
|
+
name: "线上商城",
|
|
7633
|
+
value: 2545
|
|
7634
|
+
},
|
|
7635
|
+
{
|
|
7636
|
+
name: "线下门店",
|
|
7637
|
+
value: 2039
|
|
7638
|
+
},
|
|
7639
|
+
{
|
|
7640
|
+
name: "社群团购",
|
|
7641
|
+
value: 844
|
|
7642
|
+
}
|
|
7643
|
+
]
|
|
7644
|
+
}
|
|
7645
|
+
},
|
|
7646
|
+
{
|
|
7647
|
+
requirement: "各门店销量排行",
|
|
7648
|
+
spec: {
|
|
7649
|
+
type: "horizontal-bar",
|
|
7650
|
+
title: "门店销量排行",
|
|
7651
|
+
labels: [
|
|
7652
|
+
"线下体验店",
|
|
7653
|
+
"线上旗舰店",
|
|
7654
|
+
"社群团购点",
|
|
7655
|
+
"仓储会员店",
|
|
7656
|
+
"社区便利店"
|
|
7657
|
+
],
|
|
7658
|
+
series: [{
|
|
7659
|
+
name: "销量",
|
|
7660
|
+
data: [
|
|
7661
|
+
120,
|
|
7662
|
+
98,
|
|
7663
|
+
80,
|
|
7664
|
+
60,
|
|
7665
|
+
52
|
|
7666
|
+
]
|
|
7667
|
+
}]
|
|
7668
|
+
}
|
|
7669
|
+
},
|
|
7670
|
+
{
|
|
7671
|
+
requirement: "广告投放与成交金额的关系",
|
|
7672
|
+
spec: {
|
|
7673
|
+
type: "scatter",
|
|
7674
|
+
title: "投放-成交关系",
|
|
7675
|
+
scatterData: [
|
|
7676
|
+
{
|
|
7677
|
+
x: 100,
|
|
7678
|
+
y: 12,
|
|
7679
|
+
label: "华东"
|
|
7680
|
+
},
|
|
7681
|
+
{
|
|
7682
|
+
x: 200,
|
|
7683
|
+
y: 26,
|
|
7684
|
+
label: "华南"
|
|
7685
|
+
},
|
|
7686
|
+
{
|
|
7687
|
+
x: 300,
|
|
7688
|
+
y: 31,
|
|
7689
|
+
label: "华北"
|
|
7690
|
+
}
|
|
7691
|
+
]
|
|
7692
|
+
}
|
|
7693
|
+
}
|
|
7694
|
+
];
|
|
7695
|
+
function formatExamples(maxCount = SPEC_EXAMPLES.length) {
|
|
7696
|
+
return SPEC_EXAMPLES.slice(0, maxCount).map((e) => `需求:${e.requirement}\nSpec:${JSON.stringify(e.spec)}`).join("\n\n");
|
|
7697
|
+
}
|
|
7698
|
+
//#endregion
|
|
7699
|
+
//#region src/ai/prompt.js
|
|
7700
|
+
var PREVIEW_ROWS = 8;
|
|
7701
|
+
function buildChartPrompt({ data, requirement, extraRules = [], examples = true } = {}) {
|
|
7702
|
+
const table = parseDataTable(data);
|
|
7703
|
+
let digest;
|
|
7704
|
+
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} 行` : ""}`;
|
|
7705
|
+
else if (data !== void 0 && data !== null) digest = typeof data === "string" ? data : JSON.stringify(data);
|
|
7706
|
+
else digest = "(未提供数据,请基于需求虚构合理示例数据)";
|
|
7707
|
+
const rules = [...SPEC_RULES, ...extraRules];
|
|
7708
|
+
const sections = [
|
|
7709
|
+
"你是图表配置生成器。根据下面的数据与需求,产出 EvChart 的 options JSON(下称 Spec),它将被 <ev-chart :options> 直接渲染。",
|
|
7710
|
+
`## Options Schema\n${JSON.stringify(chartOptionsSchema)}`,
|
|
7711
|
+
`## 数据预览\n${digest}`
|
|
7712
|
+
];
|
|
7713
|
+
if (examples) sections.push(`## 示例\n${formatExamples()}`);
|
|
7714
|
+
sections.push(requirement ? `## 需求\n${requirement}` : "## 需求\n根据数据形状自行选择最合适的图表类型,并拟一个不超过 12 字的标题。");
|
|
7715
|
+
sections.push(`## 硬性规则\n${rules.map((r, i) => `${i + 1}. ${r}`).join("\n")}`);
|
|
7716
|
+
return sections.join("\n\n");
|
|
7717
|
+
}
|
|
7718
|
+
//#endregion
|
|
7719
|
+
//#region src/ai/lint.js
|
|
7720
|
+
var DEFAULT_SIZE = {
|
|
7721
|
+
width: 800,
|
|
7722
|
+
height: 450
|
|
7723
|
+
};
|
|
7724
|
+
function checkTextOverflow(svg, width, height) {
|
|
7725
|
+
const issues = [];
|
|
7726
|
+
let count = 0;
|
|
7727
|
+
for (const m of svg.matchAll(/<text x="([-\d.]+)" y="([-\d.]+)"/g)) {
|
|
7728
|
+
const x = parseFloat(m[1]);
|
|
7729
|
+
const y = parseFloat(m[2]);
|
|
7730
|
+
if (x < -1 || x > width + 1 || y < 0 || y > height + 1) count++;
|
|
7731
|
+
}
|
|
7732
|
+
if (count > 0) issues.push({
|
|
7733
|
+
level: "warn",
|
|
7734
|
+
message: `${count} 处文本超出画布边界(可能被裁剪)`,
|
|
7735
|
+
rule: "text-overflow"
|
|
7736
|
+
});
|
|
7737
|
+
return issues;
|
|
7738
|
+
}
|
|
7739
|
+
function checkTextOverlap(svg) {
|
|
7740
|
+
const issues = [];
|
|
7741
|
+
const texts = [];
|
|
7742
|
+
for (const m of svg.matchAll(/<text x="([-\d.]+)" y="([-\d.]+)" font-size="([\d.]+)"([^>]*)>([^<]*)<\/text>/g)) {
|
|
7743
|
+
const x = parseFloat(m[1]);
|
|
7744
|
+
const y = parseFloat(m[2]);
|
|
7745
|
+
const fs = parseFloat(m[3]);
|
|
7746
|
+
const attrs = m[4];
|
|
7747
|
+
const content = m[5].trim();
|
|
7748
|
+
if (!content) continue;
|
|
7749
|
+
const w = estimateTextWidth(content, fs);
|
|
7750
|
+
let left = x;
|
|
7751
|
+
if (/text-anchor="middle"/.test(attrs)) left = x - w / 2;
|
|
7752
|
+
else if (/text-anchor="end"/.test(attrs)) left = x - w;
|
|
7753
|
+
texts.push({
|
|
7754
|
+
left,
|
|
7755
|
+
right: left + w,
|
|
7756
|
+
y,
|
|
7757
|
+
fs
|
|
7758
|
+
});
|
|
7759
|
+
}
|
|
7760
|
+
let count = 0;
|
|
7761
|
+
for (let i = 0; i < texts.length; i++) for (let j = i + 1; j < texts.length; j++) {
|
|
7762
|
+
const a = texts[i];
|
|
7763
|
+
const b = texts[j];
|
|
7764
|
+
if (Math.abs(a.y - b.y) >= Math.max(a.fs, b.fs) * .8) continue;
|
|
7765
|
+
if (Math.min(a.right, b.right) - Math.max(a.left, b.left) > Math.min(a.right - a.left, b.right - b.left) * .3) count++;
|
|
7766
|
+
}
|
|
7767
|
+
if (count > 0) issues.push({
|
|
7768
|
+
level: "warn",
|
|
7769
|
+
message: `${count} 处文本相互重叠(标签或注解可能互相遮挡)`,
|
|
7770
|
+
rule: "text-overlap"
|
|
7771
|
+
});
|
|
7772
|
+
return issues;
|
|
7773
|
+
}
|
|
7774
|
+
function luminance(color) {
|
|
7775
|
+
let s = String(color).trim();
|
|
7776
|
+
if (/^#[0-9a-fA-F]{6}$/.test(s)) s = s.slice(1);
|
|
7777
|
+
else if (/^#[0-9a-fA-F]{3}$/.test(s)) s = s.slice(1).split("").map((c) => c + c).join("");
|
|
7778
|
+
else return null;
|
|
7779
|
+
const n = parseInt(s, 16);
|
|
7780
|
+
const chan = [
|
|
7781
|
+
n >> 16 & 255,
|
|
7782
|
+
n >> 8 & 255,
|
|
7783
|
+
n & 255
|
|
7784
|
+
].map((v) => {
|
|
7785
|
+
const c = v / 255;
|
|
7786
|
+
return c <= .03928 ? c / 12.92 : Math.pow((c + .055) / 1.055, 2.4);
|
|
7787
|
+
});
|
|
7788
|
+
return .2126 * chan[0] + .7152 * chan[1] + .0722 * chan[2];
|
|
7789
|
+
}
|
|
7790
|
+
function contrastRatio(a, b) {
|
|
7791
|
+
const la = luminance(a);
|
|
7792
|
+
const lb = luminance(b);
|
|
7793
|
+
if (la === null || lb === null) return null;
|
|
7794
|
+
return (Math.max(la, lb) + .05) / (Math.min(la, lb) + .05);
|
|
7795
|
+
}
|
|
7796
|
+
function checkPaletteContrast(spec) {
|
|
7797
|
+
const issues = [];
|
|
7798
|
+
if (!spec.theme || !Array.isArray(spec.theme.colors)) return issues;
|
|
7799
|
+
[false, true].forEach((dark) => {
|
|
7800
|
+
const theme = getTheme(dark, spec.theme);
|
|
7801
|
+
spec.theme.colors.forEach((c) => {
|
|
7802
|
+
const ratio = contrastRatio(c, theme.backgroundColor);
|
|
7803
|
+
if (ratio !== null && ratio < 3) issues.push({
|
|
7804
|
+
level: "warn",
|
|
7805
|
+
message: `自定义色板${dark ? "暗色" : "浅色"}模式对比度不足 3:1:${c}(${ratio.toFixed(1)})`,
|
|
7806
|
+
rule: "low-contrast"
|
|
7807
|
+
});
|
|
7808
|
+
});
|
|
7809
|
+
});
|
|
7810
|
+
return issues;
|
|
7811
|
+
}
|
|
7812
|
+
function lintChartSpec(spec, opts = {}) {
|
|
7813
|
+
const issues = [];
|
|
7814
|
+
const fixed = { ...spec };
|
|
7815
|
+
if (!fixed || typeof fixed !== "object") return {
|
|
7816
|
+
issues: [{
|
|
7817
|
+
level: "error",
|
|
7818
|
+
message: "Spec 必须是对象"
|
|
7819
|
+
}],
|
|
7820
|
+
spec: fixed
|
|
7821
|
+
};
|
|
7822
|
+
const { warnings } = validateOptions(fixed);
|
|
7823
|
+
warnings.forEach((w) => issues.push({
|
|
7824
|
+
level: "error",
|
|
7825
|
+
message: `${w.path} ${w.message}`,
|
|
7826
|
+
path: w.path
|
|
7827
|
+
}));
|
|
7828
|
+
if (issues.some((i) => i.level === "error" && i.path === "options.type")) return {
|
|
7829
|
+
issues,
|
|
7830
|
+
spec: fixed
|
|
7831
|
+
};
|
|
7832
|
+
if (Array.isArray(fixed.annotations) && fixed.annotations.length > 3) {
|
|
7833
|
+
fixed.annotations = fixed.annotations.slice(0, 3);
|
|
7834
|
+
issues.push({
|
|
7835
|
+
level: "info",
|
|
7836
|
+
message: "注解超过 3 处,已按焦点预算裁剪",
|
|
7837
|
+
rule: "annotation-budget"
|
|
7838
|
+
});
|
|
7839
|
+
}
|
|
7840
|
+
if ((fixed.type === "pie" || fixed.type === "doughnut") && Array.isArray(fixed.pieData) && fixed.pieData.length > 8) issues.push({
|
|
7841
|
+
level: "warn",
|
|
7842
|
+
message: `饼图 ${fixed.pieData.length} 个扇区过多(>8),建议改用横向条形图`,
|
|
7843
|
+
rule: "pie-slices"
|
|
7844
|
+
});
|
|
7845
|
+
issues.push(...checkPaletteContrast(fixed));
|
|
7846
|
+
if (fixed.type === "bar" && Array.isArray(fixed.series) && fixed.series.length === 1 && Array.isArray(fixed.labels)) {
|
|
7847
|
+
const width = opts.width ?? DEFAULT_SIZE.width;
|
|
7848
|
+
const labels = fixed.labels.map(String);
|
|
7849
|
+
const maxLabelWidth = Math.max(...labels.map((l) => estimateTextWidth(l, 12)), 0);
|
|
7850
|
+
const slots = Math.max(1, (width - 80) / Math.max(1, labels.length));
|
|
7851
|
+
if (labels.length >= 7 && maxLabelWidth > slots * .9) {
|
|
7852
|
+
fixed.type = "horizontal-bar";
|
|
7853
|
+
issues.push({
|
|
7854
|
+
level: "info",
|
|
7855
|
+
message: "类目名过宽拥挤,已自动改为横向条形图",
|
|
7856
|
+
rule: "bar-to-horizontal"
|
|
7857
|
+
});
|
|
7858
|
+
}
|
|
7859
|
+
}
|
|
7860
|
+
if (typeof document !== "undefined" && fixed.type) try {
|
|
7861
|
+
const width = opts.width ?? DEFAULT_SIZE.width;
|
|
7862
|
+
const height = opts.height ?? DEFAULT_SIZE.height;
|
|
7863
|
+
const canvas = document.createElement("canvas");
|
|
7864
|
+
canvas.width = width;
|
|
7865
|
+
canvas.height = height;
|
|
7866
|
+
const real = canvas.getContext("2d");
|
|
7867
|
+
if (real) {
|
|
7868
|
+
const recorder = createSvgRecorder(real);
|
|
7869
|
+
renderChart(canvas, {
|
|
7870
|
+
options: fixed,
|
|
7871
|
+
ctx: recorder.ctx,
|
|
7872
|
+
dpr: 1,
|
|
7873
|
+
progress: 1,
|
|
7874
|
+
hoverIndex: -1,
|
|
7875
|
+
mouseX: -1,
|
|
7876
|
+
mouseY: -1,
|
|
7877
|
+
showCrosshair: false
|
|
7878
|
+
});
|
|
7879
|
+
issues.push(...checkTextOverflow(recorder.toSvg(width, height, "#ffffff"), width, height));
|
|
7880
|
+
issues.push(...checkTextOverlap(recorder.toSvg(width, height, "#ffffff")));
|
|
7881
|
+
}
|
|
7882
|
+
} catch {
|
|
7883
|
+
issues.push({
|
|
7884
|
+
level: "info",
|
|
7885
|
+
message: "无头渲染自检不可用,已跳过几何检查",
|
|
7886
|
+
rule: "headless-skipped"
|
|
7887
|
+
});
|
|
7888
|
+
}
|
|
7889
|
+
return {
|
|
7890
|
+
issues,
|
|
7891
|
+
spec: fixed
|
|
7892
|
+
};
|
|
7893
|
+
}
|
|
7894
|
+
//#endregion
|
|
6187
7895
|
//#region src/index.js
|
|
6188
7896
|
var components = { EvChart: chart_default };
|
|
6189
7897
|
function install(app, _options = {}) {
|
|
@@ -6192,4 +7900,4 @@ function install(app, _options = {}) {
|
|
|
6192
7900
|
var EvokeCharts = { install };
|
|
6193
7901
|
var src_default = { install };
|
|
6194
7902
|
//#endregion
|
|
6195
|
-
export { chart_default as EvChart, EvokeCharts, applySeriesPalette, clearSeriesPalette, components, src_default as default, injectChartContext, install, provideChartContext, useChart };
|
|
7903
|
+
export { chart_default as EvChart, EvokeCharts, applySeriesPalette, buildChartPrompt, chartOptionsSchema, clearSeriesPalette, components, src_default as default, generateChartSpec, injectChartContext, install, lintChartSpec, parseDataTable, provideChartContext, useChart, validateOptions };
|