@wil-works/evoke-charts 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/evoke-charts.css +47 -32
- package/dist/index.mjs +1577 -51
- 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: {
|
|
@@ -320,6 +320,15 @@ function estimateYAxisLeft(options) {
|
|
|
320
320
|
const axisConfig = options.yAxis || {};
|
|
321
321
|
if (axisConfig.type === "log") return 65;
|
|
322
322
|
try {
|
|
323
|
+
if (options.type === "horizontal-bar") {
|
|
324
|
+
const formatter = axisConfig.formatter;
|
|
325
|
+
const maxW = (options.labels || []).reduce((m, l) => {
|
|
326
|
+
const label = formatter ? String(formatter(l)) : String(l);
|
|
327
|
+
return Math.max(m, estimateTextWidth(label, 12));
|
|
328
|
+
}, 0);
|
|
329
|
+
if (!maxW) return 65;
|
|
330
|
+
return Math.max(40, Math.min(140, Math.round(maxW + 26)));
|
|
331
|
+
}
|
|
323
332
|
const range = calculateRange(options, /* @__PURE__ */ new Set(), false);
|
|
324
333
|
const min = axisConfig.min ?? range?.min;
|
|
325
334
|
const max = axisConfig.max ?? range?.max;
|
|
@@ -420,10 +429,12 @@ function getPadding(options, containerWidth = 600) {
|
|
|
420
429
|
}
|
|
421
430
|
const pad = resolveUserPadding(options);
|
|
422
431
|
const xAxisHidden = options.xAxis?.show === false;
|
|
432
|
+
const yAxisWidth = options.yAxis?.width;
|
|
433
|
+
const yAxisRightWidth = options.yAxisRight?.width;
|
|
423
434
|
let top = (pad.top ?? 18) + titleBlockHeight(options) + (legendPosition === "top" ? LEGEND_BAND + legendRowsExtra : 0);
|
|
424
435
|
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);
|
|
436
|
+
let left = pad.left ?? (typeof yAxisWidth === "number" && yAxisWidth > 0 ? yAxisWidth : estimateYAxisLeft(options));
|
|
437
|
+
let right = pad.right ?? (typeof yAxisRightWidth === "number" && yAxisRightWidth > 0 ? yAxisRightWidth : hasYAxisRight ? 65 : 24);
|
|
427
438
|
if (legendPosition === "left") left += 80;
|
|
428
439
|
if (legendPosition === "right") right += 80;
|
|
429
440
|
if (hasXAxis && hasXAxisTitle) return {
|
|
@@ -639,12 +650,15 @@ function calculateRange(options, hiddenSeries, useRightAxis) {
|
|
|
639
650
|
const range = max - min || 1;
|
|
640
651
|
if (min >= 0) return {
|
|
641
652
|
min: 0,
|
|
642
|
-
max: max + range * .1
|
|
643
|
-
};
|
|
644
|
-
else return {
|
|
645
|
-
min: min - range * .1,
|
|
646
|
-
max: max + range * .1
|
|
653
|
+
max: max + Math.max(range * .1, max * .05)
|
|
647
654
|
};
|
|
655
|
+
else {
|
|
656
|
+
const pad = Math.max(range * .1, Math.max(Math.abs(max), Math.abs(min)) * .05);
|
|
657
|
+
return {
|
|
658
|
+
min: min - pad,
|
|
659
|
+
max: max + pad
|
|
660
|
+
};
|
|
661
|
+
}
|
|
648
662
|
}
|
|
649
663
|
function applyLogTransform(options) {
|
|
650
664
|
if (options.yAxis?.type !== "log") return options;
|
|
@@ -769,6 +783,20 @@ function truncateLabel(canvasCtx, label, maxWidth) {
|
|
|
769
783
|
}
|
|
770
784
|
return label.slice(0, lo) + "…";
|
|
771
785
|
}
|
|
786
|
+
/**
|
|
787
|
+
* 抽稀刻度:保留首末两端覆盖,步长取整数倍使数量 ≤ maxCount。
|
|
788
|
+
* nice 算法的 step 候选会让实际刻度数超出请求数(如 ticks:3 得到 5 档),
|
|
789
|
+
* 显式 ticks 视为硬上限,超出的中间档按均匀步长抽稀(round 值不丢)。
|
|
790
|
+
*/
|
|
791
|
+
function thinTickValues(values, maxCount) {
|
|
792
|
+
const limit = Math.max(2, maxCount);
|
|
793
|
+
if (values.length <= limit) return values;
|
|
794
|
+
let stride = Math.ceil((values.length - 1) / (limit - 1));
|
|
795
|
+
while (stride < values.length - 1 && (values.length - 1) % stride !== 0) stride += 1;
|
|
796
|
+
const thinned = [];
|
|
797
|
+
for (let i = 0; i < values.length; i += stride) thinned.push(values[i]);
|
|
798
|
+
return thinned;
|
|
799
|
+
}
|
|
772
800
|
function drawAxisLine(canvasCtx, cfg, theme, x1, y1, x2, y2) {
|
|
773
801
|
if (cfg?.axisLine?.show === false) return;
|
|
774
802
|
canvasCtx.save();
|
|
@@ -795,7 +823,10 @@ function drawAxisTick(canvasCtx, cfg, theme, x, y, dx, dy) {
|
|
|
795
823
|
function renderYAxis(ctx, side = "left", dataRange) {
|
|
796
824
|
const { ctx: canvasCtx, theme, plotArea, options, width } = ctx;
|
|
797
825
|
const axisConfig = side === "left" ? options.yAxis || {} : options.yAxisRight || {};
|
|
798
|
-
const
|
|
826
|
+
const requestedTicks = axisConfig.ticks || 5;
|
|
827
|
+
const maxByHeight = Math.max(2, Math.floor(plotArea.height / 24) + 1);
|
|
828
|
+
const tickCap = axisConfig.ticks != null ? Math.min(requestedTicks, maxByHeight) : maxByHeight;
|
|
829
|
+
const needsThin = axisConfig.ticks != null || maxByHeight < requestedTicks;
|
|
799
830
|
const isLog = side === "left" && axisConfig.type === "log";
|
|
800
831
|
let computedMax;
|
|
801
832
|
let computedMin;
|
|
@@ -810,7 +841,8 @@ function renderYAxis(ctx, side = "left", dataRange) {
|
|
|
810
841
|
}
|
|
811
842
|
}
|
|
812
843
|
const max = axisConfig.max ?? computedMax;
|
|
813
|
-
const { min: actualMin, max: actualMax, tickValues } = resolveTickExtendedRange(axisConfig.min ?? computedMin, max,
|
|
844
|
+
const { min: actualMin, max: actualMax, tickValues: niceTicks } = resolveTickExtendedRange(axisConfig.min ?? computedMin, max, requestedTicks);
|
|
845
|
+
const tickValues = needsThin ? thinTickValues(niceTicks, tickCap) : niceTicks;
|
|
814
846
|
canvasCtx.save();
|
|
815
847
|
canvasCtx.strokeStyle = theme.gridColor;
|
|
816
848
|
canvasCtx.fillStyle = theme.textColorSecondary;
|
|
@@ -1041,6 +1073,10 @@ function xToCategoryIndex(options, plotArea, labels, x) {
|
|
|
1041
1073
|
return Math.round((x - plotArea.x) / step);
|
|
1042
1074
|
}
|
|
1043
1075
|
function renderAnnotations(ctx, yRange) {
|
|
1076
|
+
renderLegacyAnnotation(ctx, yRange);
|
|
1077
|
+
renderNarrativeAnnotations(ctx, yRange);
|
|
1078
|
+
}
|
|
1079
|
+
function renderLegacyAnnotation(ctx, yRange) {
|
|
1044
1080
|
const { ctx: canvasCtx, theme, plotArea, options } = ctx;
|
|
1045
1081
|
const annotation = options.annotation;
|
|
1046
1082
|
if (!annotation) return;
|
|
@@ -1103,6 +1139,196 @@ function renderAnnotations(ctx, yRange) {
|
|
|
1103
1139
|
canvasCtx.restore();
|
|
1104
1140
|
});
|
|
1105
1141
|
}
|
|
1142
|
+
var ANNOTATION_ANCHORS = {
|
|
1143
|
+
top: [
|
|
1144
|
+
0,
|
|
1145
|
+
-1,
|
|
1146
|
+
"center",
|
|
1147
|
+
"bottom"
|
|
1148
|
+
],
|
|
1149
|
+
bottom: [
|
|
1150
|
+
0,
|
|
1151
|
+
1,
|
|
1152
|
+
"center",
|
|
1153
|
+
"top"
|
|
1154
|
+
],
|
|
1155
|
+
left: [
|
|
1156
|
+
-1,
|
|
1157
|
+
0,
|
|
1158
|
+
"end",
|
|
1159
|
+
"middle"
|
|
1160
|
+
],
|
|
1161
|
+
right: [
|
|
1162
|
+
1,
|
|
1163
|
+
0,
|
|
1164
|
+
"start",
|
|
1165
|
+
"middle"
|
|
1166
|
+
],
|
|
1167
|
+
"top-left": [
|
|
1168
|
+
-1,
|
|
1169
|
+
-1,
|
|
1170
|
+
"end",
|
|
1171
|
+
"bottom"
|
|
1172
|
+
],
|
|
1173
|
+
"top-right": [
|
|
1174
|
+
1,
|
|
1175
|
+
-1,
|
|
1176
|
+
"start",
|
|
1177
|
+
"bottom"
|
|
1178
|
+
],
|
|
1179
|
+
"bottom-left": [
|
|
1180
|
+
-1,
|
|
1181
|
+
1,
|
|
1182
|
+
"end",
|
|
1183
|
+
"top"
|
|
1184
|
+
],
|
|
1185
|
+
"bottom-right": [
|
|
1186
|
+
1,
|
|
1187
|
+
1,
|
|
1188
|
+
"start",
|
|
1189
|
+
"top"
|
|
1190
|
+
]
|
|
1191
|
+
};
|
|
1192
|
+
function resolveAnnotationX(ctx, x, xPx) {
|
|
1193
|
+
if (typeof xPx === "number") return xPx;
|
|
1194
|
+
const { plotArea, options } = ctx;
|
|
1195
|
+
const labels = options.labels || [];
|
|
1196
|
+
const idx = typeof x === "number" ? x : labels.indexOf(x);
|
|
1197
|
+
if (idx < 0) return NaN;
|
|
1198
|
+
return categoryToX(options, plotArea, labels, idx);
|
|
1199
|
+
}
|
|
1200
|
+
function resolveAnnotationY(ctx, y, yPx, yRange) {
|
|
1201
|
+
if (typeof yPx === "number") return yPx;
|
|
1202
|
+
if (typeof y !== "number" || !yRange) return NaN;
|
|
1203
|
+
const { plotArea } = ctx;
|
|
1204
|
+
return plotArea.y + plotArea.height - (y - yRange.min) / (yRange.max - yRange.min) * plotArea.height;
|
|
1205
|
+
}
|
|
1206
|
+
function annotationSeriesColor(ctx, ref) {
|
|
1207
|
+
const { theme, options } = ctx;
|
|
1208
|
+
const series = options.series || [];
|
|
1209
|
+
if (typeof ref === "number") return series[ref]?.color || theme.colors[ref] || theme.colors[0] || "#175DFF";
|
|
1210
|
+
if (typeof ref === "string") {
|
|
1211
|
+
const i = series.findIndex((s) => s.name === ref);
|
|
1212
|
+
if (i >= 0) return series[i].color || theme.colors[i] || theme.colors[0] || "#175DFF";
|
|
1213
|
+
}
|
|
1214
|
+
return theme.colors[0] || "#175DFF";
|
|
1215
|
+
}
|
|
1216
|
+
function renderNarrativeAnnotations(ctx, yRange) {
|
|
1217
|
+
const { ctx: canvasCtx, theme, plotArea, options } = ctx;
|
|
1218
|
+
const list = options.annotations || [];
|
|
1219
|
+
if (list.length === 0) return;
|
|
1220
|
+
const secondary = theme.textColorSecondary;
|
|
1221
|
+
list.forEach((a) => {
|
|
1222
|
+
if (!a || !a.type) return;
|
|
1223
|
+
if (a.type === "region") {
|
|
1224
|
+
const x1 = resolveAnnotationX(ctx, a.from);
|
|
1225
|
+
const x2 = resolveAnnotationX(ctx, a.to);
|
|
1226
|
+
if (!Number.isFinite(x1) || !Number.isFinite(x2)) return;
|
|
1227
|
+
const labels = options.labels || [];
|
|
1228
|
+
const band = labels.length > 0 ? plotArea.width / labels.length : 0;
|
|
1229
|
+
const left = Math.min(x1, x2) - band / 2;
|
|
1230
|
+
const width = Math.abs(x2 - x1) + band;
|
|
1231
|
+
canvasCtx.save();
|
|
1232
|
+
canvasCtx.globalAlpha = .06;
|
|
1233
|
+
canvasCtx.fillStyle = a.color || theme.colors[0] || "#175DFF";
|
|
1234
|
+
canvasCtx.fillRect(left, plotArea.y, width, plotArea.height);
|
|
1235
|
+
canvasCtx.restore();
|
|
1236
|
+
if (a.label) {
|
|
1237
|
+
canvasCtx.save();
|
|
1238
|
+
canvasCtx.fillStyle = secondary;
|
|
1239
|
+
canvasCtx.font = "italic 400 11px Inter, sans-serif";
|
|
1240
|
+
canvasCtx.textAlign = "left";
|
|
1241
|
+
canvasCtx.textBaseline = "top";
|
|
1242
|
+
canvasCtx.fillText(a.label, left + 4, plotArea.y + 6);
|
|
1243
|
+
canvasCtx.restore();
|
|
1244
|
+
}
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const px = resolveAnnotationX(ctx, a.x, a.xPx);
|
|
1248
|
+
const py = resolveAnnotationY(ctx, a.y, a.yPx, yRange);
|
|
1249
|
+
if (!Number.isFinite(px) || !Number.isFinite(py)) return;
|
|
1250
|
+
const color = a.color || annotationSeriesColor(ctx, a.series);
|
|
1251
|
+
canvasCtx.save();
|
|
1252
|
+
if (a.type === "point") {
|
|
1253
|
+
canvasCtx.fillStyle = color;
|
|
1254
|
+
canvasCtx.beginPath();
|
|
1255
|
+
canvasCtx.arc(px, py, 3, 0, Math.PI * 2);
|
|
1256
|
+
canvasCtx.fill();
|
|
1257
|
+
canvasCtx.globalAlpha = .85;
|
|
1258
|
+
canvasCtx.strokeStyle = color;
|
|
1259
|
+
canvasCtx.lineWidth = 1.5;
|
|
1260
|
+
canvasCtx.beginPath();
|
|
1261
|
+
canvasCtx.arc(px, py, 6, 0, Math.PI * 2);
|
|
1262
|
+
canvasCtx.stroke();
|
|
1263
|
+
canvasCtx.restore();
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
if (a.type === "delta") {
|
|
1267
|
+
const up = a.direction !== "down";
|
|
1268
|
+
const deltaColor = a.color || (up ? options.candleUpColor || "#dc2626" : options.candleDownColor || "#16a34a");
|
|
1269
|
+
const text = a.text || a.label || "";
|
|
1270
|
+
canvasCtx.fillStyle = deltaColor;
|
|
1271
|
+
canvasCtx.beginPath();
|
|
1272
|
+
if (up) {
|
|
1273
|
+
canvasCtx.moveTo(px, py - 16);
|
|
1274
|
+
canvasCtx.lineTo(px - 5, py - 9);
|
|
1275
|
+
canvasCtx.lineTo(px + 5, py - 9);
|
|
1276
|
+
} else {
|
|
1277
|
+
canvasCtx.moveTo(px, py + 16);
|
|
1278
|
+
canvasCtx.lineTo(px - 5, py + 9);
|
|
1279
|
+
canvasCtx.lineTo(px + 5, py + 9);
|
|
1280
|
+
}
|
|
1281
|
+
canvasCtx.closePath();
|
|
1282
|
+
canvasCtx.fill();
|
|
1283
|
+
if (text) {
|
|
1284
|
+
canvasCtx.fillStyle = deltaColor;
|
|
1285
|
+
canvasCtx.font = "600 11px Inter, sans-serif";
|
|
1286
|
+
canvasCtx.textAlign = "center";
|
|
1287
|
+
if (up) {
|
|
1288
|
+
canvasCtx.textBaseline = "bottom";
|
|
1289
|
+
canvasCtx.fillText(text, px, py - 20);
|
|
1290
|
+
} else {
|
|
1291
|
+
canvasCtx.textBaseline = "top";
|
|
1292
|
+
canvasCtx.fillText(text, px, py + 20);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
canvasCtx.restore();
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
if (a.type === "callout") {
|
|
1299
|
+
const [dx, dy, align, baseline] = ANNOTATION_ANCHORS[a.anchor] || ANNOTATION_ANCHORS["top-right"];
|
|
1300
|
+
const lx = px + dx * 16 + (a.offsetX || 0);
|
|
1301
|
+
const ly = py + dy * 16 + (a.offsetY || 0);
|
|
1302
|
+
canvasCtx.fillStyle = color;
|
|
1303
|
+
canvasCtx.strokeStyle = color;
|
|
1304
|
+
canvasCtx.lineWidth = 1;
|
|
1305
|
+
canvasCtx.setLineDash([3, 3]);
|
|
1306
|
+
canvasCtx.beginPath();
|
|
1307
|
+
canvasCtx.arc(px, py, 2, 0, Math.PI * 2);
|
|
1308
|
+
canvasCtx.fill();
|
|
1309
|
+
canvasCtx.beginPath();
|
|
1310
|
+
canvasCtx.moveTo(px + dx * 2, py + dy * 2);
|
|
1311
|
+
canvasCtx.lineTo(lx - dx * 2, ly - dy * 2);
|
|
1312
|
+
canvasCtx.stroke();
|
|
1313
|
+
canvasCtx.setLineDash([]);
|
|
1314
|
+
if (a.label) {
|
|
1315
|
+
canvasCtx.fillStyle = a.textColor || secondary;
|
|
1316
|
+
canvasCtx.font = "italic 400 11px Inter, sans-serif";
|
|
1317
|
+
canvasCtx.textAlign = align;
|
|
1318
|
+
canvasCtx.textBaseline = baseline;
|
|
1319
|
+
canvasCtx.fillText(a.label, lx + dx * 2, ly + dy * 2);
|
|
1320
|
+
}
|
|
1321
|
+
canvasCtx.restore();
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
canvasCtx.fillStyle = a.color || secondary;
|
|
1325
|
+
canvasCtx.font = `italic ${a.fontWeight || 400} 11px Inter, sans-serif`;
|
|
1326
|
+
canvasCtx.textAlign = "center";
|
|
1327
|
+
canvasCtx.textBaseline = "bottom";
|
|
1328
|
+
canvasCtx.fillText(a.text || a.label || "", px + (a.offsetX || 0), py + (a.offsetY !== void 0 ? a.offsetY : -6));
|
|
1329
|
+
canvasCtx.restore();
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1106
1332
|
function renderHoverHighlight(ctx, points) {
|
|
1107
1333
|
if (ctx.hoverIndex < 0) return;
|
|
1108
1334
|
const { ctx: canvasCtx, theme, plotArea } = ctx;
|
|
@@ -3966,6 +4192,13 @@ function createSvgRecorder(real) {
|
|
|
3966
4192
|
}
|
|
3967
4193
|
//#endregion
|
|
3968
4194
|
//#region src/renderer/index.js
|
|
4195
|
+
function runLayers(renderCtx, at) {
|
|
4196
|
+
const layers = renderCtx.options.layers;
|
|
4197
|
+
if (!Array.isArray(layers)) return;
|
|
4198
|
+
layers.forEach((layer) => {
|
|
4199
|
+
if (layer && typeof layer.draw === "function" && (layer.at || "front") === at) layer.draw(renderCtx.ctx, renderCtx);
|
|
4200
|
+
});
|
|
4201
|
+
}
|
|
3969
4202
|
function renderChart(canvas, params) {
|
|
3970
4203
|
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
4204
|
const options = applyLogTransform(rawOptions);
|
|
@@ -4003,6 +4236,7 @@ function renderChart(canvas, params) {
|
|
|
4003
4236
|
focusSeries
|
|
4004
4237
|
};
|
|
4005
4238
|
renderTitle(renderCtx, options.subtitle ? 12 : 15);
|
|
4239
|
+
runLayers(renderCtx, "back");
|
|
4006
4240
|
let points = [];
|
|
4007
4241
|
let leftRange;
|
|
4008
4242
|
switch (options.type) {
|
|
@@ -4146,7 +4380,8 @@ function renderChart(canvas, params) {
|
|
|
4146
4380
|
break;
|
|
4147
4381
|
}
|
|
4148
4382
|
}
|
|
4149
|
-
|
|
4383
|
+
runLayers(renderCtx, "after-series");
|
|
4384
|
+
if (leftRange && (options.annotation || options.annotations && options.annotations.length > 0)) renderAnnotations(renderCtx, leftRange);
|
|
4150
4385
|
if ((options.type === "line" || options.type === "area") && points.length > 0) renderHoverHighlight(renderCtx, points);
|
|
4151
4386
|
if (points.length > 0) renderCrosshair(renderCtx, points);
|
|
4152
4387
|
if (zoomRange) renderDataZoomSlider(renderCtx, zoomRange);
|
|
@@ -4164,6 +4399,7 @@ function renderChart(canvas, params) {
|
|
|
4164
4399
|
ctx.strokeRect(x, y, w, h);
|
|
4165
4400
|
ctx.restore();
|
|
4166
4401
|
}
|
|
4402
|
+
runLayers(renderCtx, "front");
|
|
4167
4403
|
if (options.type !== "sparkline") renderLegend(renderCtx);
|
|
4168
4404
|
if (options.type !== "sparkline") renderToolbox(renderCtx, hoveredToolbox);
|
|
4169
4405
|
ctx.restore();
|
|
@@ -4191,6 +4427,372 @@ function broadcastConnect(group, source, apply) {
|
|
|
4191
4427
|
if (target !== source) apply(target);
|
|
4192
4428
|
});
|
|
4193
4429
|
}
|
|
4430
|
+
var chartOptionsSchema = {
|
|
4431
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
4432
|
+
title: "EvChart Options",
|
|
4433
|
+
type: "object",
|
|
4434
|
+
required: ["type"],
|
|
4435
|
+
properties: {
|
|
4436
|
+
type: {
|
|
4437
|
+
type: "string",
|
|
4438
|
+
enum: [
|
|
4439
|
+
"line",
|
|
4440
|
+
"area",
|
|
4441
|
+
"bar",
|
|
4442
|
+
"stacked-bar",
|
|
4443
|
+
"horizontal-bar",
|
|
4444
|
+
"pie",
|
|
4445
|
+
"doughnut",
|
|
4446
|
+
"rose",
|
|
4447
|
+
"radar",
|
|
4448
|
+
"scatter",
|
|
4449
|
+
"funnel",
|
|
4450
|
+
"gauge",
|
|
4451
|
+
"heatmap",
|
|
4452
|
+
"candle",
|
|
4453
|
+
"bin",
|
|
4454
|
+
"bullet",
|
|
4455
|
+
"treemap",
|
|
4456
|
+
"sparkline",
|
|
4457
|
+
"waterfall",
|
|
4458
|
+
"boxplot",
|
|
4459
|
+
"sunburst",
|
|
4460
|
+
"mixed"
|
|
4461
|
+
]
|
|
4462
|
+
},
|
|
4463
|
+
title: { type: "string" },
|
|
4464
|
+
subtitle: { type: "string" },
|
|
4465
|
+
labels: { type: "array" },
|
|
4466
|
+
series: {
|
|
4467
|
+
type: "array",
|
|
4468
|
+
items: {
|
|
4469
|
+
type: "object",
|
|
4470
|
+
required: ["name", "data"],
|
|
4471
|
+
properties: {
|
|
4472
|
+
name: { type: "string" },
|
|
4473
|
+
data: { type: "array" },
|
|
4474
|
+
color: { type: "string" },
|
|
4475
|
+
showSymbol: { type: "boolean" },
|
|
4476
|
+
lineWidth: { type: "number" },
|
|
4477
|
+
smooth: { type: "boolean" },
|
|
4478
|
+
yAxis: {
|
|
4479
|
+
type: "string",
|
|
4480
|
+
enum: ["left", "right"]
|
|
4481
|
+
},
|
|
4482
|
+
type: { type: "string" },
|
|
4483
|
+
area: { type: "boolean" }
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
},
|
|
4487
|
+
legend: {
|
|
4488
|
+
type: "object",
|
|
4489
|
+
properties: {
|
|
4490
|
+
show: { type: "boolean" },
|
|
4491
|
+
position: {
|
|
4492
|
+
type: "string",
|
|
4493
|
+
enum: ["top", "bottom"]
|
|
4494
|
+
},
|
|
4495
|
+
interactive: { type: "boolean" },
|
|
4496
|
+
hoverEmphasis: { type: "boolean" }
|
|
4497
|
+
}
|
|
4498
|
+
},
|
|
4499
|
+
tooltip: {
|
|
4500
|
+
type: "object",
|
|
4501
|
+
properties: {
|
|
4502
|
+
show: { type: "boolean" },
|
|
4503
|
+
trigger: {
|
|
4504
|
+
type: "string",
|
|
4505
|
+
enum: ["hover", "click"]
|
|
4506
|
+
},
|
|
4507
|
+
showAllSeries: { type: "boolean" },
|
|
4508
|
+
formatter: { type: "function" }
|
|
4509
|
+
}
|
|
4510
|
+
},
|
|
4511
|
+
animation: {
|
|
4512
|
+
type: "object",
|
|
4513
|
+
properties: {
|
|
4514
|
+
enabled: { type: "boolean" },
|
|
4515
|
+
duration: { type: "number" },
|
|
4516
|
+
easing: { type: "string" }
|
|
4517
|
+
}
|
|
4518
|
+
},
|
|
4519
|
+
xAxis: {
|
|
4520
|
+
type: "object",
|
|
4521
|
+
properties: {
|
|
4522
|
+
show: { type: "boolean" },
|
|
4523
|
+
min: { type: "number" },
|
|
4524
|
+
max: { type: "number" },
|
|
4525
|
+
ticks: { type: "integer" },
|
|
4526
|
+
grid: {
|
|
4527
|
+
type: "object",
|
|
4528
|
+
properties: {
|
|
4529
|
+
show: { type: "boolean" },
|
|
4530
|
+
style: {
|
|
4531
|
+
type: "string",
|
|
4532
|
+
enum: ["solid", "dashed"]
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
},
|
|
4538
|
+
yAxis: {
|
|
4539
|
+
type: "object",
|
|
4540
|
+
properties: {
|
|
4541
|
+
show: { type: "boolean" },
|
|
4542
|
+
min: { type: "number" },
|
|
4543
|
+
max: { type: "number" },
|
|
4544
|
+
ticks: { type: "integer" },
|
|
4545
|
+
width: { type: "number" },
|
|
4546
|
+
grid: {
|
|
4547
|
+
type: "object",
|
|
4548
|
+
properties: {
|
|
4549
|
+
show: { type: "boolean" },
|
|
4550
|
+
style: {
|
|
4551
|
+
type: "string",
|
|
4552
|
+
enum: ["solid", "dashed"]
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
}
|
|
4557
|
+
},
|
|
4558
|
+
yAxisRight: { $ref: "#/properties/yAxis" },
|
|
4559
|
+
padding: { oneOf: [{ type: "number" }, {
|
|
4560
|
+
type: "object",
|
|
4561
|
+
properties: {
|
|
4562
|
+
top: { type: "number" },
|
|
4563
|
+
right: { type: "number" },
|
|
4564
|
+
bottom: { type: "number" },
|
|
4565
|
+
left: { type: "number" }
|
|
4566
|
+
}
|
|
4567
|
+
}] },
|
|
4568
|
+
dataZoom: {
|
|
4569
|
+
type: "object",
|
|
4570
|
+
properties: {
|
|
4571
|
+
enabled: { type: "boolean" },
|
|
4572
|
+
start: { type: "number" },
|
|
4573
|
+
end: { type: "number" },
|
|
4574
|
+
position: {
|
|
4575
|
+
type: "string",
|
|
4576
|
+
enum: ["bottom", "top"]
|
|
4577
|
+
},
|
|
4578
|
+
height: { type: "number" },
|
|
4579
|
+
mouseWheel: { type: "boolean" }
|
|
4580
|
+
}
|
|
4581
|
+
},
|
|
4582
|
+
toolbox: {
|
|
4583
|
+
type: "object",
|
|
4584
|
+
properties: {
|
|
4585
|
+
show: { type: "boolean" },
|
|
4586
|
+
filename: { type: "string" }
|
|
4587
|
+
}
|
|
4588
|
+
},
|
|
4589
|
+
theme: { type: "object" },
|
|
4590
|
+
layers: {
|
|
4591
|
+
type: "array",
|
|
4592
|
+
items: {
|
|
4593
|
+
type: "object",
|
|
4594
|
+
properties: {
|
|
4595
|
+
at: {
|
|
4596
|
+
type: "string",
|
|
4597
|
+
enum: [
|
|
4598
|
+
"back",
|
|
4599
|
+
"after-series",
|
|
4600
|
+
"front"
|
|
4601
|
+
]
|
|
4602
|
+
},
|
|
4603
|
+
draw: { type: "function" }
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
},
|
|
4607
|
+
emphasis: {
|
|
4608
|
+
type: "object",
|
|
4609
|
+
properties: {
|
|
4610
|
+
series: { oneOf: [{ type: "string" }, { type: "integer" }] },
|
|
4611
|
+
dimOthers: { type: "boolean" }
|
|
4612
|
+
}
|
|
4613
|
+
},
|
|
4614
|
+
annotations: {
|
|
4615
|
+
type: "array",
|
|
4616
|
+
items: {
|
|
4617
|
+
type: "object",
|
|
4618
|
+
oneOf: [{
|
|
4619
|
+
required: [
|
|
4620
|
+
"type",
|
|
4621
|
+
"x",
|
|
4622
|
+
"y"
|
|
4623
|
+
],
|
|
4624
|
+
properties: { type: { enum: [
|
|
4625
|
+
"text",
|
|
4626
|
+
"callout",
|
|
4627
|
+
"delta",
|
|
4628
|
+
"point"
|
|
4629
|
+
] } }
|
|
4630
|
+
}, {
|
|
4631
|
+
required: [
|
|
4632
|
+
"type",
|
|
4633
|
+
"from",
|
|
4634
|
+
"to"
|
|
4635
|
+
],
|
|
4636
|
+
properties: { type: { enum: ["region"] } }
|
|
4637
|
+
}],
|
|
4638
|
+
properties: {
|
|
4639
|
+
type: {
|
|
4640
|
+
type: "string",
|
|
4641
|
+
enum: [
|
|
4642
|
+
"text",
|
|
4643
|
+
"callout",
|
|
4644
|
+
"region",
|
|
4645
|
+
"delta",
|
|
4646
|
+
"point"
|
|
4647
|
+
]
|
|
4648
|
+
},
|
|
4649
|
+
x: {},
|
|
4650
|
+
y: { type: "number" },
|
|
4651
|
+
from: {},
|
|
4652
|
+
to: {},
|
|
4653
|
+
at: {},
|
|
4654
|
+
label: { type: "string" },
|
|
4655
|
+
text: { type: "string" },
|
|
4656
|
+
anchor: {
|
|
4657
|
+
type: "string",
|
|
4658
|
+
enum: [
|
|
4659
|
+
"top",
|
|
4660
|
+
"bottom",
|
|
4661
|
+
"left",
|
|
4662
|
+
"right",
|
|
4663
|
+
"top-left",
|
|
4664
|
+
"top-right",
|
|
4665
|
+
"bottom-left",
|
|
4666
|
+
"bottom-right"
|
|
4667
|
+
]
|
|
4668
|
+
},
|
|
4669
|
+
color: { type: "string" },
|
|
4670
|
+
offsetX: { type: "number" },
|
|
4671
|
+
offsetY: { type: "number" },
|
|
4672
|
+
xPx: { type: "number" },
|
|
4673
|
+
yPx: { type: "number" }
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
},
|
|
4677
|
+
annotation: { type: "object" },
|
|
4678
|
+
markLine: { type: "array" },
|
|
4679
|
+
markArea: { type: "array" },
|
|
4680
|
+
scenes: {
|
|
4681
|
+
type: "object",
|
|
4682
|
+
properties: {
|
|
4683
|
+
autoplay: { type: "boolean" },
|
|
4684
|
+
loop: { type: "boolean" },
|
|
4685
|
+
items: {
|
|
4686
|
+
type: "array",
|
|
4687
|
+
items: {
|
|
4688
|
+
type: "object",
|
|
4689
|
+
required: ["patch"],
|
|
4690
|
+
properties: {
|
|
4691
|
+
patch: { type: "object" },
|
|
4692
|
+
duration: { type: "number" },
|
|
4693
|
+
hold: { type: "number" }
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4697
|
+
}
|
|
4698
|
+
},
|
|
4699
|
+
pieData: { type: "array" },
|
|
4700
|
+
radarSeries: { type: "array" },
|
|
4701
|
+
radarIndicators: { type: "array" },
|
|
4702
|
+
scatterData: { type: "array" },
|
|
4703
|
+
funnelData: { type: "array" },
|
|
4704
|
+
gauge: { oneOf: [{ type: "number" }, { type: "object" }] },
|
|
4705
|
+
heatmapData: { type: "array" },
|
|
4706
|
+
candleData: { type: "array" },
|
|
4707
|
+
bulletData: { type: "array" },
|
|
4708
|
+
boxData: { type: "array" },
|
|
4709
|
+
loading: { type: "boolean" },
|
|
4710
|
+
emptyText: { type: "string" },
|
|
4711
|
+
ariaLabel: { type: "string" },
|
|
4712
|
+
i18n: { type: "object" },
|
|
4713
|
+
connectGroup: { type: "string" },
|
|
4714
|
+
brush: {
|
|
4715
|
+
type: "object",
|
|
4716
|
+
properties: { enabled: { type: "boolean" } }
|
|
4717
|
+
},
|
|
4718
|
+
logAxis: { type: "boolean" },
|
|
4719
|
+
valueFormatter: { type: "function" },
|
|
4720
|
+
timeAxis: { type: "boolean" }
|
|
4721
|
+
}
|
|
4722
|
+
};
|
|
4723
|
+
var TYPE_CHECKS = {
|
|
4724
|
+
string: (v) => typeof v === "string",
|
|
4725
|
+
number: (v) => typeof v === "number" && Number.isFinite(v),
|
|
4726
|
+
integer: (v) => typeof v === "number" && Number.isInteger(v),
|
|
4727
|
+
boolean: (v) => typeof v === "boolean",
|
|
4728
|
+
object: (v) => v !== null && typeof v === "object" && !Array.isArray(v),
|
|
4729
|
+
array: (v) => Array.isArray(v),
|
|
4730
|
+
function: (v) => typeof v === "function"
|
|
4731
|
+
};
|
|
4732
|
+
function typeMatches(node, value) {
|
|
4733
|
+
if (Array.isArray(node.type)) return node.type.some((t) => TYPE_CHECKS[t]?.(value));
|
|
4734
|
+
const check = TYPE_CHECKS[node.type];
|
|
4735
|
+
return check ? check(value) : true;
|
|
4736
|
+
}
|
|
4737
|
+
function validateNode(node, value, path, warnings) {
|
|
4738
|
+
if (node == null || value === void 0 || value === null) return;
|
|
4739
|
+
if (node.oneOf) {
|
|
4740
|
+
if (!node.oneOf.some((branch) => {
|
|
4741
|
+
const before = warnings.length;
|
|
4742
|
+
validateNode(branch, value, path, warnings);
|
|
4743
|
+
const ok = warnings.length === before;
|
|
4744
|
+
warnings.length = before;
|
|
4745
|
+
return ok;
|
|
4746
|
+
})) {
|
|
4747
|
+
warnings.push({
|
|
4748
|
+
path,
|
|
4749
|
+
message: "类型不匹配(应为 oneOf 之一)"
|
|
4750
|
+
});
|
|
4751
|
+
return;
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
if (node.type && !typeMatches(node, value)) {
|
|
4755
|
+
warnings.push({
|
|
4756
|
+
path,
|
|
4757
|
+
message: `应为 ${node.type},实际为 ${Array.isArray(value) ? "array" : typeof value}`
|
|
4758
|
+
});
|
|
4759
|
+
return;
|
|
4760
|
+
}
|
|
4761
|
+
if (node.enum && !node.enum.includes(value)) {
|
|
4762
|
+
warnings.push({
|
|
4763
|
+
path,
|
|
4764
|
+
message: `应为 ${node.enum.join(" / ")} 之一,实际为 ${String(value)}`
|
|
4765
|
+
});
|
|
4766
|
+
return;
|
|
4767
|
+
}
|
|
4768
|
+
if (node.type === "object") {
|
|
4769
|
+
(node.required || []).forEach((key) => {
|
|
4770
|
+
if (value[key] === void 0) warnings.push({
|
|
4771
|
+
path: `${path}.${key}`,
|
|
4772
|
+
message: "缺少必填字段"
|
|
4773
|
+
});
|
|
4774
|
+
});
|
|
4775
|
+
if (node.properties) Object.entries(node.properties).forEach(([key, sub]) => {
|
|
4776
|
+
if (value[key] !== void 0) validateNode(sub, value[key], `${path}.${key}`, warnings);
|
|
4777
|
+
});
|
|
4778
|
+
}
|
|
4779
|
+
if (node.type === "array" && node.items && Array.isArray(value)) value.forEach((item, i) => validateNode(node.items, item, `${path}[${i}]`, warnings));
|
|
4780
|
+
}
|
|
4781
|
+
function validateOptions(options) {
|
|
4782
|
+
const warnings = [];
|
|
4783
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) return {
|
|
4784
|
+
ok: false,
|
|
4785
|
+
warnings: [{
|
|
4786
|
+
path: "options",
|
|
4787
|
+
message: "应为对象"
|
|
4788
|
+
}]
|
|
4789
|
+
};
|
|
4790
|
+
validateNode(chartOptionsSchema, options, "options", warnings);
|
|
4791
|
+
return {
|
|
4792
|
+
ok: warnings.length === 0,
|
|
4793
|
+
warnings
|
|
4794
|
+
};
|
|
4795
|
+
}
|
|
4194
4796
|
//#endregion
|
|
4195
4797
|
//#region \0plugin-vue:export-helper
|
|
4196
4798
|
var _plugin_vue_export_helper_default = (sfc, props) => {
|
|
@@ -4202,23 +4804,27 @@ var _plugin_vue_export_helper_default = (sfc, props) => {
|
|
|
4202
4804
|
//#region src/chart.vue
|
|
4203
4805
|
var _hoisted_1 = ["aria-label"];
|
|
4204
4806
|
var _hoisted_2 = {
|
|
4807
|
+
key: 1,
|
|
4808
|
+
class: "ev-chart__overlay"
|
|
4809
|
+
};
|
|
4810
|
+
var _hoisted_3 = {
|
|
4205
4811
|
key: 0,
|
|
4206
4812
|
class: "ev-chart__empty"
|
|
4207
4813
|
};
|
|
4208
|
-
var
|
|
4209
|
-
var
|
|
4210
|
-
var
|
|
4814
|
+
var _hoisted_4 = { class: "ev-chart__empty-icon-wrap" };
|
|
4815
|
+
var _hoisted_5 = { class: "ev-chart__empty-text" };
|
|
4816
|
+
var _hoisted_6 = {
|
|
4211
4817
|
key: 0,
|
|
4212
4818
|
class: "ev-chart__loading"
|
|
4213
4819
|
};
|
|
4214
|
-
var
|
|
4215
|
-
var
|
|
4820
|
+
var _hoisted_7 = { class: "ev-chart__loading-bars" };
|
|
4821
|
+
var _hoisted_8 = {
|
|
4216
4822
|
key: 0,
|
|
4217
4823
|
class: "ev-chart__error"
|
|
4218
4824
|
};
|
|
4219
|
-
var
|
|
4220
|
-
var
|
|
4221
|
-
var
|
|
4825
|
+
var _hoisted_9 = { class: "ev-chart__error-text" };
|
|
4826
|
+
var _hoisted_10 = ["innerHTML"];
|
|
4827
|
+
var _hoisted_11 = {
|
|
4222
4828
|
class: "ev-chart__sr-only",
|
|
4223
4829
|
role: "status",
|
|
4224
4830
|
"aria-live": "polite"
|
|
@@ -4256,7 +4862,8 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
4256
4862
|
"animation-end",
|
|
4257
4863
|
"data-update",
|
|
4258
4864
|
"brush-select",
|
|
4259
|
-
"zoom"
|
|
4865
|
+
"zoom",
|
|
4866
|
+
"scene-change"
|
|
4260
4867
|
],
|
|
4261
4868
|
setup(__props, { expose: __expose, emit: __emit }) {
|
|
4262
4869
|
const DataLine = () => h("svg", {
|
|
@@ -4326,6 +4933,12 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
4326
4933
|
let hoverAnimFrameId = null;
|
|
4327
4934
|
const hiddenSeries = ref(/* @__PURE__ */ new Set());
|
|
4328
4935
|
const focusSeries = ref(null);
|
|
4936
|
+
const emphasisSeriesName = computed(() => {
|
|
4937
|
+
const em = props.options.emphasis;
|
|
4938
|
+
if (!em || em.series === void 0 || em.dimOthers === false) return null;
|
|
4939
|
+
if (typeof em.series === "number") return props.options.series?.[em.series]?.name ?? null;
|
|
4940
|
+
return em.series || null;
|
|
4941
|
+
});
|
|
4329
4942
|
const hoveredToolbox = ref(null);
|
|
4330
4943
|
let mouseX = -1;
|
|
4331
4944
|
let mouseY = -1;
|
|
@@ -4355,33 +4968,102 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
4355
4968
|
const brushRect = ref(null);
|
|
4356
4969
|
let suppressClick = false;
|
|
4357
4970
|
let brushDragged = false;
|
|
4971
|
+
const specVersion = ref(0);
|
|
4972
|
+
const overlayInfo = ref({
|
|
4973
|
+
plotArea: {
|
|
4974
|
+
x: 0,
|
|
4975
|
+
y: 0,
|
|
4976
|
+
width: 0,
|
|
4977
|
+
height: 0
|
|
4978
|
+
},
|
|
4979
|
+
theme: getTheme(false, void 0),
|
|
4980
|
+
options: null
|
|
4981
|
+
});
|
|
4358
4982
|
const effectiveOptions = computed(() => {
|
|
4983
|
+
specVersion.value;
|
|
4359
4984
|
const opt = props.options;
|
|
4360
|
-
|
|
4985
|
+
const scene = opt.scenes?.items?.[sceneIndex.value];
|
|
4986
|
+
const base = scene?.patch ? {
|
|
4987
|
+
...opt,
|
|
4988
|
+
...scene.patch
|
|
4989
|
+
} : opt;
|
|
4990
|
+
if (!getDataZoomConfig(base)) return base;
|
|
4361
4991
|
const range = zoomRange.value;
|
|
4362
|
-
if (range.start <= 0 && range.end >= 100) return
|
|
4363
|
-
const sliced = { ...
|
|
4364
|
-
const n = (
|
|
4992
|
+
if (range.start <= 0 && range.end >= 100) return base;
|
|
4993
|
+
const sliced = { ...base };
|
|
4994
|
+
const n = (base.labels || []).length;
|
|
4365
4995
|
if (n > 0) {
|
|
4366
4996
|
const { startIdx, endIdx } = zoomToSlice(range, n);
|
|
4367
|
-
sliced.labels = (
|
|
4368
|
-
if (
|
|
4997
|
+
sliced.labels = (base.labels || []).slice(startIdx, endIdx);
|
|
4998
|
+
if (base.series) sliced.series = base.series.map((s) => ({
|
|
4369
4999
|
...s,
|
|
4370
5000
|
data: s.data.slice(startIdx, endIdx)
|
|
4371
5001
|
}));
|
|
4372
|
-
if (
|
|
5002
|
+
if (base.candleData) sliced.candleData = base.candleData.slice(startIdx, endIdx);
|
|
4373
5003
|
sliced.__sliceStart = startIdx;
|
|
4374
|
-
} else if (
|
|
4375
|
-
const xs =
|
|
5004
|
+
} else if (base.scatterData && base.scatterData.length > 0) {
|
|
5005
|
+
const xs = base.scatterData.map((d) => d.x);
|
|
4376
5006
|
const xMin = Math.min(...xs);
|
|
4377
5007
|
const span = Math.max(...xs) - xMin || 1;
|
|
4378
5008
|
const lo = xMin + range.start / 100 * span;
|
|
4379
5009
|
const hi = xMin + range.end / 100 * span;
|
|
4380
|
-
const filtered =
|
|
4381
|
-
sliced.scatterData = filtered.length > 0 ? filtered :
|
|
5010
|
+
const filtered = base.scatterData.filter((d) => d.x >= lo && d.x <= hi);
|
|
5011
|
+
sliced.scatterData = filtered.length > 0 ? filtered : base.scatterData;
|
|
4382
5012
|
}
|
|
4383
5013
|
return sliced;
|
|
4384
5014
|
});
|
|
5015
|
+
const sceneIndex = ref(0);
|
|
5016
|
+
let sceneTimer = null;
|
|
5017
|
+
let sceneAnimOverride = null;
|
|
5018
|
+
function stopSceneTimer() {
|
|
5019
|
+
if (sceneTimer !== null) {
|
|
5020
|
+
clearTimeout(sceneTimer);
|
|
5021
|
+
sceneTimer = null;
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
function applyScene(i, opts = {}) {
|
|
5025
|
+
const items = props.options.scenes?.items || [];
|
|
5026
|
+
if (items.length === 0) return;
|
|
5027
|
+
const next = Math.max(0, Math.min(i, items.length - 1));
|
|
5028
|
+
if (next === sceneIndex.value && !opts.force) return;
|
|
5029
|
+
sceneIndex.value = next;
|
|
5030
|
+
const item = items[next];
|
|
5031
|
+
sceneAnimOverride = item?.duration ? {
|
|
5032
|
+
...props.options.animation || {},
|
|
5033
|
+
duration: item.duration
|
|
5034
|
+
} : null;
|
|
5035
|
+
emit("scene-change", {
|
|
5036
|
+
index: next,
|
|
5037
|
+
total: items.length
|
|
5038
|
+
});
|
|
5039
|
+
render(true, sceneAnimOverride);
|
|
5040
|
+
scheduleSceneAdvance();
|
|
5041
|
+
}
|
|
5042
|
+
function scheduleSceneAdvance() {
|
|
5043
|
+
stopSceneTimer();
|
|
5044
|
+
const scenes = props.options.scenes;
|
|
5045
|
+
if (!scenes?.autoplay) return;
|
|
5046
|
+
const item = scenes.items?.[sceneIndex.value];
|
|
5047
|
+
if (!item) return;
|
|
5048
|
+
const wait = Math.max(0, (item.duration ?? 1200) + (item.hold ?? 0));
|
|
5049
|
+
sceneTimer = setTimeout(() => {
|
|
5050
|
+
const total = props.options.scenes?.items?.length ?? 0;
|
|
5051
|
+
if (total === 0) return;
|
|
5052
|
+
if (sceneIndex.value >= total - 1) {
|
|
5053
|
+
if (props.options.scenes?.loop) applyScene(0, { force: true });
|
|
5054
|
+
} else applyScene(sceneIndex.value + 1);
|
|
5055
|
+
}, wait);
|
|
5056
|
+
}
|
|
5057
|
+
function syncScenes() {
|
|
5058
|
+
const total = props.options.scenes?.items?.length ?? 0;
|
|
5059
|
+
if (total === 0) {
|
|
5060
|
+
stopSceneTimer();
|
|
5061
|
+
if (sceneIndex.value !== 0) sceneIndex.value = 0;
|
|
5062
|
+
return;
|
|
5063
|
+
}
|
|
5064
|
+
if (sceneIndex.value >= total) sceneIndex.value = 0;
|
|
5065
|
+
scheduleSceneAdvance();
|
|
5066
|
+
}
|
|
4385
5067
|
function sliceStartOffset() {
|
|
4386
5068
|
return effectiveOptions.value.__sliceStart || 0;
|
|
4387
5069
|
}
|
|
@@ -4427,17 +5109,24 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
4427
5109
|
const spaceBelow = containerRect.height - relY - gap;
|
|
4428
5110
|
const spaceAbove = relY - gap - legendBottom;
|
|
4429
5111
|
let top;
|
|
4430
|
-
if (
|
|
4431
|
-
|
|
4432
|
-
|
|
5112
|
+
if (th + 4 <= containerRect.height) {
|
|
5113
|
+
if (spaceBelow >= th) top = relY + gap;
|
|
5114
|
+
else if (spaceAbove >= th) top = relY - gap - th;
|
|
5115
|
+
else top = spaceBelow >= spaceAbove ? containerRect.height - th - margin : legendBottom;
|
|
5116
|
+
top = Math.max(legendBottom, Math.min(top, containerRect.height - th - margin));
|
|
5117
|
+
} else {
|
|
5118
|
+
const pointViewportY = containerRect.top + relY;
|
|
5119
|
+
const belowInViewport = pointViewportY + gap + th <= window.innerHeight - 8;
|
|
5120
|
+
const aboveInViewport = pointViewportY - gap - th >= 8;
|
|
5121
|
+
top = belowInViewport || !aboveInViewport ? relY + gap : relY - gap - th;
|
|
5122
|
+
}
|
|
4433
5123
|
left = Math.max(margin, Math.min(left, containerRect.width - tw - margin));
|
|
4434
|
-
top = Math.max(legendBottom, Math.min(top, containerRect.height - th - margin));
|
|
4435
5124
|
tooltipPlacement.value = "top";
|
|
4436
5125
|
tooltipX.value = left;
|
|
4437
5126
|
tooltipY.value = top;
|
|
4438
5127
|
}
|
|
4439
5128
|
const canvasStyle = computed(() => ({ cursor: hoverIndex >= 0 ? "pointer" : "default" }));
|
|
4440
|
-
function render(animate = true) {
|
|
5129
|
+
function render(animate = true, animOverride = null) {
|
|
4441
5130
|
const canvas = canvasRef.value;
|
|
4442
5131
|
if (!canvas) return;
|
|
4443
5132
|
const container = containerRef.value;
|
|
@@ -4480,8 +5169,20 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
4480
5169
|
lastHeight = height;
|
|
4481
5170
|
canvas.width = width * dpr.value;
|
|
4482
5171
|
canvas.height = height * dpr.value;
|
|
4483
|
-
const
|
|
4484
|
-
|
|
5172
|
+
const overlayPadding = getPadding(effectiveOptions.value, width);
|
|
5173
|
+
overlayInfo.value = {
|
|
5174
|
+
plotArea: {
|
|
5175
|
+
x: overlayPadding.left,
|
|
5176
|
+
y: overlayPadding.top,
|
|
5177
|
+
width: width - overlayPadding.left - overlayPadding.right,
|
|
5178
|
+
height: height - overlayPadding.top - overlayPadding.bottom
|
|
5179
|
+
},
|
|
5180
|
+
theme: getTheme(document.documentElement.classList.contains("dark"), effectiveOptions.value.theme),
|
|
5181
|
+
options: props.options
|
|
5182
|
+
};
|
|
5183
|
+
const animCfg = animOverride || props.options.animation;
|
|
5184
|
+
const animEnabled = animate && animCfg?.enabled !== false;
|
|
5185
|
+
animationState = createAnimation(animCfg);
|
|
4485
5186
|
animationState.isAnimating = animEnabled;
|
|
4486
5187
|
if (animEnabled) {
|
|
4487
5188
|
isEnterAnimating = true;
|
|
@@ -5502,10 +6203,12 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5502
6203
|
redraw();
|
|
5503
6204
|
}
|
|
5504
6205
|
const legendName = checkLegendHit(e.clientX, e.clientY);
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
focusSeries.value
|
|
5508
|
-
|
|
6206
|
+
if (emphasisSeriesName.value) {} else {
|
|
6207
|
+
const nextFocus = props.options.legend?.hoverEmphasis !== false && legendName ? legendName : null;
|
|
6208
|
+
if (nextFocus !== focusSeries.value) {
|
|
6209
|
+
focusSeries.value = nextFocus;
|
|
6210
|
+
redraw();
|
|
6211
|
+
}
|
|
5509
6212
|
}
|
|
5510
6213
|
if (legendName) {
|
|
5511
6214
|
if (hoverIndex !== -1) {
|
|
@@ -5634,7 +6337,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5634
6337
|
tooltipVisible.value = false;
|
|
5635
6338
|
mouseX = -1;
|
|
5636
6339
|
mouseY = -1;
|
|
5637
|
-
if (focusSeries.value !== null) focusSeries.value = null;
|
|
6340
|
+
if (focusSeries.value !== null && !emphasisSeriesName.value) focusSeries.value = null;
|
|
5638
6341
|
if (hoveredToolbox.value !== null) hoveredToolbox.value = null;
|
|
5639
6342
|
if (dragMode === "brush") {
|
|
5640
6343
|
brushRect.value = null;
|
|
@@ -5701,11 +6404,35 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5701
6404
|
}))
|
|
5702
6405
|
};
|
|
5703
6406
|
}
|
|
6407
|
+
function cloneSpec(value) {
|
|
6408
|
+
if (value === null || typeof value !== "object") return value;
|
|
6409
|
+
if (Array.isArray(value)) return value.map(cloneSpec);
|
|
6410
|
+
const out = {};
|
|
6411
|
+
Object.keys(value).forEach((k) => {
|
|
6412
|
+
out[k] = cloneSpec(value[k]);
|
|
6413
|
+
});
|
|
6414
|
+
return out;
|
|
6415
|
+
}
|
|
6416
|
+
const IS_DEV = typeof import.meta !== "undefined" && false;
|
|
6417
|
+
const devWarned = /* @__PURE__ */ new Set();
|
|
6418
|
+
function runDevValidation() {
|
|
6419
|
+
if (!IS_DEV) return;
|
|
6420
|
+
const { warnings } = validateOptions(props.options);
|
|
6421
|
+
warnings.slice(0, 8).forEach((w) => {
|
|
6422
|
+
const key = `${w.path}:${w.message}`;
|
|
6423
|
+
if (devWarned.has(key)) return;
|
|
6424
|
+
devWarned.add(key);
|
|
6425
|
+
console.warn(`[EvChart] ${w.path} ${w.message}`);
|
|
6426
|
+
});
|
|
6427
|
+
}
|
|
5704
6428
|
let prevOptionsSnapshot = null;
|
|
5705
6429
|
watch(() => props.options, (next) => {
|
|
5706
6430
|
cachedDataExtent = null;
|
|
5707
6431
|
cachedPlotArea = null;
|
|
5708
6432
|
internalError.value = null;
|
|
6433
|
+
runDevValidation();
|
|
6434
|
+
focusSeries.value = emphasisSeriesName.value;
|
|
6435
|
+
syncScenes();
|
|
5709
6436
|
const zoom = getDataZoomConfig(props.options);
|
|
5710
6437
|
const zoomKey = zoom ? JSON.stringify({
|
|
5711
6438
|
e: zoom.enabled,
|
|
@@ -5759,6 +6486,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5759
6486
|
let unregisterConnector = null;
|
|
5760
6487
|
onMounted(() => {
|
|
5761
6488
|
render(true);
|
|
6489
|
+
syncScenes();
|
|
5762
6490
|
setupResizeObserver();
|
|
5763
6491
|
setupDarkModeObserver();
|
|
5764
6492
|
window.addEventListener("pointerup", onWindowPointerUp);
|
|
@@ -5778,6 +6506,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5778
6506
|
stopAnimation();
|
|
5779
6507
|
stopHoverAnimation();
|
|
5780
6508
|
stopTweenAnimation();
|
|
6509
|
+
stopSceneTimer();
|
|
5781
6510
|
if (tooltipRafId !== null) {
|
|
5782
6511
|
cancelAnimationFrame(tooltipRafId);
|
|
5783
6512
|
tooltipRafId = null;
|
|
@@ -5791,6 +6520,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5791
6520
|
update(newOptions) {
|
|
5792
6521
|
Object.assign(props.options, newOptions);
|
|
5793
6522
|
cachedDataExtent = null;
|
|
6523
|
+
specVersion.value++;
|
|
5794
6524
|
debouncedRender(true);
|
|
5795
6525
|
},
|
|
5796
6526
|
toDataURL(type = "image/png", quality = 1) {
|
|
@@ -5801,6 +6531,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5801
6531
|
cleanupDarkModeObserver();
|
|
5802
6532
|
stopAnimation();
|
|
5803
6533
|
stopHoverAnimation();
|
|
6534
|
+
stopSceneTimer();
|
|
5804
6535
|
if (tooltipRafId !== null) {
|
|
5805
6536
|
cancelAnimationFrame(tooltipRafId);
|
|
5806
6537
|
tooltipRafId = null;
|
|
@@ -5890,6 +6621,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5890
6621
|
...props.options.theme,
|
|
5891
6622
|
...theme
|
|
5892
6623
|
};
|
|
6624
|
+
specVersion.value++;
|
|
5893
6625
|
redraw();
|
|
5894
6626
|
},
|
|
5895
6627
|
exportSVG(options = {}) {
|
|
@@ -5932,6 +6664,59 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5932
6664
|
getOption() {
|
|
5933
6665
|
return props.options;
|
|
5934
6666
|
},
|
|
6667
|
+
getSpec() {
|
|
6668
|
+
return cloneSpec(props.options);
|
|
6669
|
+
},
|
|
6670
|
+
setSpec(spec) {
|
|
6671
|
+
if (spec === null || typeof spec !== "object") return;
|
|
6672
|
+
const next = cloneSpec(spec);
|
|
6673
|
+
Object.keys(props.options).forEach((k) => delete props.options[k]);
|
|
6674
|
+
Object.assign(props.options, next);
|
|
6675
|
+
hiddenSeries.value = /* @__PURE__ */ new Set();
|
|
6676
|
+
focusSeries.value = null;
|
|
6677
|
+
brushRect.value = null;
|
|
6678
|
+
hoverIndex = -1;
|
|
6679
|
+
mouseX = -1;
|
|
6680
|
+
mouseY = -1;
|
|
6681
|
+
stopSceneTimer();
|
|
6682
|
+
sceneIndex.value = 0;
|
|
6683
|
+
sceneAnimOverride = null;
|
|
6684
|
+
const zoom = getDataZoomConfig(next);
|
|
6685
|
+
zoomRange.value = {
|
|
6686
|
+
start: zoom?.start ?? 0,
|
|
6687
|
+
end: zoom?.end ?? 100
|
|
6688
|
+
};
|
|
6689
|
+
lastZoomKey = zoom ? JSON.stringify({
|
|
6690
|
+
e: zoom.enabled,
|
|
6691
|
+
s: zoom.start,
|
|
6692
|
+
e2: zoom.end,
|
|
6693
|
+
p: zoom.position,
|
|
6694
|
+
h: zoom.height
|
|
6695
|
+
}) : "";
|
|
6696
|
+
cachedDataExtent = null;
|
|
6697
|
+
cachedPlotArea = null;
|
|
6698
|
+
specVersion.value++;
|
|
6699
|
+
debouncedRender(true);
|
|
6700
|
+
},
|
|
6701
|
+
nextScene() {
|
|
6702
|
+
applyScene(sceneIndex.value + 1);
|
|
6703
|
+
},
|
|
6704
|
+
prevScene() {
|
|
6705
|
+
applyScene(sceneIndex.value - 1);
|
|
6706
|
+
},
|
|
6707
|
+
gotoScene(i) {
|
|
6708
|
+
applyScene(i);
|
|
6709
|
+
},
|
|
6710
|
+
getSceneIndex() {
|
|
6711
|
+
return sceneIndex.value;
|
|
6712
|
+
},
|
|
6713
|
+
getEffectiveSpec() {
|
|
6714
|
+
const spec = cloneSpec(effectiveOptions.value);
|
|
6715
|
+
Object.keys(spec).forEach((k) => {
|
|
6716
|
+
if (k.startsWith("__")) delete spec[k];
|
|
6717
|
+
});
|
|
6718
|
+
return spec;
|
|
6719
|
+
},
|
|
5935
6720
|
setDataZoomRange(start, end) {
|
|
5936
6721
|
if (!isZoomEnabled()) {
|
|
5937
6722
|
console.warn("[EvChart] setDataZoomRange: dataZoom is not enabled");
|
|
@@ -5969,12 +6754,17 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5969
6754
|
onDblclick: handleDblClick,
|
|
5970
6755
|
onPointerleave: handlePointerLeave
|
|
5971
6756
|
}, null, 36)) : createCommentVNode("", true),
|
|
6757
|
+
!isEmpty.value && !props.options.loading && !internalError.value && _ctx.$slots.overlay ? (openBlock(), createElementBlock("div", _hoisted_2, [renderSlot(_ctx.$slots, "overlay", {
|
|
6758
|
+
plotArea: overlayInfo.value.plotArea,
|
|
6759
|
+
theme: overlayInfo.value.theme,
|
|
6760
|
+
options: props.options
|
|
6761
|
+
}, void 0, true)])) : createCommentVNode("", true),
|
|
5972
6762
|
createVNode(Transition, { name: "ev-chart-fade" }, {
|
|
5973
|
-
default: withCtx(() => [isEmpty.value && !props.options.loading && !internalError.value ? (openBlock(), createElementBlock("div",
|
|
6763
|
+
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
6764
|
_: 1
|
|
5975
6765
|
}),
|
|
5976
6766
|
createVNode(Transition, { name: "ev-chart-fade" }, {
|
|
5977
|
-
default: withCtx(() => [props.options.loading && !internalError.value ? (openBlock(), createElementBlock("div",
|
|
6767
|
+
default: withCtx(() => [props.options.loading && !internalError.value ? (openBlock(), createElementBlock("div", _hoisted_6, [createElementVNode("div", _hoisted_7, [(openBlock(), createElementBlock(Fragment, null, renderList(6, (i) => {
|
|
5978
6768
|
return createElementVNode("div", {
|
|
5979
6769
|
key: i,
|
|
5980
6770
|
class: "ev-chart__loading-bar",
|
|
@@ -5984,7 +6774,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5984
6774
|
_: 1
|
|
5985
6775
|
}),
|
|
5986
6776
|
createVNode(Transition, { name: "ev-chart-fade" }, {
|
|
5987
|
-
default: withCtx(() => [internalError.value ? (openBlock(), createElementBlock("div",
|
|
6777
|
+
default: withCtx(() => [internalError.value ? (openBlock(), createElementBlock("div", _hoisted_8, [_cache[0] || (_cache[0] = createElementVNode("span", { class: "ev-chart__error-icon" }, [createElementVNode("svg", {
|
|
5988
6778
|
viewBox: "0 0 24 24",
|
|
5989
6779
|
width: "1em",
|
|
5990
6780
|
height: "1em",
|
|
@@ -6012,7 +6802,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
6012
6802
|
x2: "12",
|
|
6013
6803
|
y2: "16.51"
|
|
6014
6804
|
})
|
|
6015
|
-
])], -1)), createElementVNode("span",
|
|
6805
|
+
])], -1)), createElementVNode("span", _hoisted_9, toDisplayString(internalError.value), 1)])) : createCommentVNode("", true)]),
|
|
6016
6806
|
_: 1
|
|
6017
6807
|
}),
|
|
6018
6808
|
createVNode(Transition, { name: "ev-chart-tooltip" }, {
|
|
@@ -6024,14 +6814,14 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
6024
6814
|
style: normalizeStyle(tooltipStyle.value),
|
|
6025
6815
|
role: "tooltip",
|
|
6026
6816
|
innerHTML: tooltipContent.value
|
|
6027
|
-
}, null, 14,
|
|
6817
|
+
}, null, 14, _hoisted_10)) : createCommentVNode("", true)]),
|
|
6028
6818
|
_: 1
|
|
6029
6819
|
}),
|
|
6030
|
-
createElementVNode("div",
|
|
6820
|
+
createElementVNode("div", _hoisted_11, toDisplayString(ariaLiveText.value), 1)
|
|
6031
6821
|
], 12, _hoisted_1);
|
|
6032
6822
|
};
|
|
6033
6823
|
}
|
|
6034
|
-
}), [["__scopeId", "data-v-
|
|
6824
|
+
}), [["__scopeId", "data-v-ce034a2a"]]);
|
|
6035
6825
|
//#endregion
|
|
6036
6826
|
//#region src/useChart.js
|
|
6037
6827
|
var USE_CHART_KEY = /* @__PURE__ */ Symbol("ev-chart-context");
|
|
@@ -6184,6 +6974,742 @@ function clearSeriesPalette(options = {}) {
|
|
|
6184
6974
|
return true;
|
|
6185
6975
|
}
|
|
6186
6976
|
//#endregion
|
|
6977
|
+
//#region src/ai/table.js
|
|
6978
|
+
var NUM_CLEAN = /[,\s_]/g;
|
|
6979
|
+
function parseNumeric(raw) {
|
|
6980
|
+
if (typeof raw === "number") return Number.isFinite(raw) ? raw : null;
|
|
6981
|
+
if (typeof raw !== "string") return null;
|
|
6982
|
+
let s = raw.trim();
|
|
6983
|
+
if (!s) return null;
|
|
6984
|
+
let sign = 1;
|
|
6985
|
+
if (/^\(.*\)$/.test(s)) {
|
|
6986
|
+
sign = -1;
|
|
6987
|
+
s = s.slice(1, -1);
|
|
6988
|
+
}
|
|
6989
|
+
s = s.replace(NUM_CLEAN, "").replace(/^[¥$€£]/, "").replace(/[¥$€£]$/, "");
|
|
6990
|
+
let mult = 1;
|
|
6991
|
+
if (/亿$/.test(s)) {
|
|
6992
|
+
mult = 1e8;
|
|
6993
|
+
s = s.slice(0, -1);
|
|
6994
|
+
} else if (/万$/.test(s)) {
|
|
6995
|
+
mult = 1e4;
|
|
6996
|
+
s = s.slice(0, -1);
|
|
6997
|
+
} else if (/[Kk]$/.test(s)) {
|
|
6998
|
+
mult = 1e3;
|
|
6999
|
+
s = s.slice(0, -1);
|
|
7000
|
+
}
|
|
7001
|
+
if (/%$/.test(s)) s = s.slice(0, -1);
|
|
7002
|
+
if (!/^[-+]?\d+(\.\d+)?$/.test(s)) return null;
|
|
7003
|
+
return sign * parseFloat(s) * mult;
|
|
7004
|
+
}
|
|
7005
|
+
function looksLikeTime(v) {
|
|
7006
|
+
if (typeof v !== "string") return false;
|
|
7007
|
+
const s = v.trim();
|
|
7008
|
+
if (/^\d{4}[-/年.]\d{1,2}月?(?:[-/.]\d{1,2}日?)?$/.test(s)) return true;
|
|
7009
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(s)) return true;
|
|
7010
|
+
if (/^\d{1,2}:\d{2}(:\d{2})?$/.test(s)) return true;
|
|
7011
|
+
return false;
|
|
7012
|
+
}
|
|
7013
|
+
function splitLine(line, delim) {
|
|
7014
|
+
const out = [];
|
|
7015
|
+
let cur = "";
|
|
7016
|
+
let inQuotes = false;
|
|
7017
|
+
for (let i = 0; i < line.length; i++) {
|
|
7018
|
+
const ch = line[i];
|
|
7019
|
+
if (ch === "\"") {
|
|
7020
|
+
if (inQuotes && line[i + 1] === "\"") {
|
|
7021
|
+
cur += "\"";
|
|
7022
|
+
i++;
|
|
7023
|
+
} else inQuotes = !inQuotes;
|
|
7024
|
+
} else if (ch === delim && !inQuotes) {
|
|
7025
|
+
out.push(cur.trim());
|
|
7026
|
+
cur = "";
|
|
7027
|
+
} else cur += ch;
|
|
7028
|
+
}
|
|
7029
|
+
out.push(cur.trim());
|
|
7030
|
+
return out;
|
|
7031
|
+
}
|
|
7032
|
+
function pickDelimiter(line) {
|
|
7033
|
+
const candidates = [
|
|
7034
|
+
" ",
|
|
7035
|
+
";",
|
|
7036
|
+
",",
|
|
7037
|
+
","
|
|
7038
|
+
];
|
|
7039
|
+
let best = ",";
|
|
7040
|
+
let bestCount = 0;
|
|
7041
|
+
candidates.forEach((d) => {
|
|
7042
|
+
const n = splitLine(line, d).length;
|
|
7043
|
+
if (n > bestCount) {
|
|
7044
|
+
bestCount = n;
|
|
7045
|
+
best = d;
|
|
7046
|
+
}
|
|
7047
|
+
});
|
|
7048
|
+
return best;
|
|
7049
|
+
}
|
|
7050
|
+
function rowToObj(headers, row) {
|
|
7051
|
+
const obj = {};
|
|
7052
|
+
headers.forEach((h, i) => {
|
|
7053
|
+
obj[h] = row[i] === void 0 ? null : row[i];
|
|
7054
|
+
});
|
|
7055
|
+
return obj;
|
|
7056
|
+
}
|
|
7057
|
+
function parseDataTable(input) {
|
|
7058
|
+
if (Array.isArray(input)) {
|
|
7059
|
+
if (input.length === 0) return null;
|
|
7060
|
+
const first = input[0];
|
|
7061
|
+
if (first !== null && typeof first === "object" && !Array.isArray(first)) {
|
|
7062
|
+
const headers = [];
|
|
7063
|
+
input.forEach((row) => {
|
|
7064
|
+
Object.keys(row).forEach((k) => {
|
|
7065
|
+
if (!headers.includes(k)) headers.push(k);
|
|
7066
|
+
});
|
|
7067
|
+
});
|
|
7068
|
+
return {
|
|
7069
|
+
headers,
|
|
7070
|
+
rows: input
|
|
7071
|
+
};
|
|
7072
|
+
}
|
|
7073
|
+
if (Array.isArray(first)) {
|
|
7074
|
+
if (input.length < 1) return null;
|
|
7075
|
+
const width = input.reduce((m, r) => Math.max(m, r.length), 0);
|
|
7076
|
+
if (width < 2) return null;
|
|
7077
|
+
const head = input[0].map((v) => v === null || v === void 0 ? "" : String(v).trim());
|
|
7078
|
+
if (head.filter((v) => parseNumeric(v) !== null).length === 0) return {
|
|
7079
|
+
headers: head,
|
|
7080
|
+
rows: input.slice(1).map((r) => rowToObj(head, r))
|
|
7081
|
+
};
|
|
7082
|
+
const headers = Array.from({ length: width }, (_, i) => `列${i + 1}`);
|
|
7083
|
+
return {
|
|
7084
|
+
headers,
|
|
7085
|
+
rows: input.map((r) => rowToObj(headers, r))
|
|
7086
|
+
};
|
|
7087
|
+
}
|
|
7088
|
+
return null;
|
|
7089
|
+
}
|
|
7090
|
+
if (typeof input === "string") {
|
|
7091
|
+
const lines = input.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
7092
|
+
if (lines.length < 2) return null;
|
|
7093
|
+
const delim = pickDelimiter(lines[0]);
|
|
7094
|
+
const grid = lines.map((l) => splitLine(l, delim));
|
|
7095
|
+
const head = grid[0];
|
|
7096
|
+
if (head.filter((v) => parseNumeric(v) !== null).length === 0 && head.length >= 2) return {
|
|
7097
|
+
headers: head,
|
|
7098
|
+
rows: grid.slice(1).map((r) => rowToObj(head, r))
|
|
7099
|
+
};
|
|
7100
|
+
const width = grid.reduce((m, r) => Math.max(m, r.length), 0);
|
|
7101
|
+
const headers = Array.from({ length: width }, (_, i) => `列${i + 1}`);
|
|
7102
|
+
return {
|
|
7103
|
+
headers,
|
|
7104
|
+
rows: grid.map((r) => rowToObj(headers, r))
|
|
7105
|
+
};
|
|
7106
|
+
}
|
|
7107
|
+
return null;
|
|
7108
|
+
}
|
|
7109
|
+
function inferColumns(table) {
|
|
7110
|
+
const { headers, rows } = table;
|
|
7111
|
+
return headers.map((name) => {
|
|
7112
|
+
const values = rows.map((r) => r[name] === void 0 ? null : r[name]);
|
|
7113
|
+
const present = values.filter((v) => v !== null && v !== "").length;
|
|
7114
|
+
const numericHits = values.filter((v) => parseNumeric(v) !== null).length;
|
|
7115
|
+
if (present > 0 && numericHits / present >= .6) return {
|
|
7116
|
+
name,
|
|
7117
|
+
type: "number",
|
|
7118
|
+
values: values.map((v) => parseNumeric(v))
|
|
7119
|
+
};
|
|
7120
|
+
const timeHits = values.filter((v) => looksLikeTime(v)).length;
|
|
7121
|
+
if (present > 0 && timeHits / present >= .6) return {
|
|
7122
|
+
name,
|
|
7123
|
+
type: "time",
|
|
7124
|
+
values
|
|
7125
|
+
};
|
|
7126
|
+
return {
|
|
7127
|
+
name,
|
|
7128
|
+
type: "category",
|
|
7129
|
+
values
|
|
7130
|
+
};
|
|
7131
|
+
});
|
|
7132
|
+
}
|
|
7133
|
+
function distinctCount(values) {
|
|
7134
|
+
return new Set(values.filter((v) => v !== null && v !== "")).size;
|
|
7135
|
+
}
|
|
7136
|
+
//#endregion
|
|
7137
|
+
//#region src/ai/spec.js
|
|
7138
|
+
var INTENT_KEYWORDS = {
|
|
7139
|
+
trend: [
|
|
7140
|
+
"趋势",
|
|
7141
|
+
"走势",
|
|
7142
|
+
"变化",
|
|
7143
|
+
"随时间",
|
|
7144
|
+
"增长",
|
|
7145
|
+
"trend"
|
|
7146
|
+
],
|
|
7147
|
+
share: [
|
|
7148
|
+
"占比",
|
|
7149
|
+
"构成",
|
|
7150
|
+
"份额",
|
|
7151
|
+
"比例",
|
|
7152
|
+
"组成",
|
|
7153
|
+
"share"
|
|
7154
|
+
],
|
|
7155
|
+
rank: [
|
|
7156
|
+
"排行",
|
|
7157
|
+
"排名",
|
|
7158
|
+
"前几",
|
|
7159
|
+
"highest",
|
|
7160
|
+
"rank"
|
|
7161
|
+
],
|
|
7162
|
+
compare: [
|
|
7163
|
+
"对比",
|
|
7164
|
+
"比较",
|
|
7165
|
+
"对照",
|
|
7166
|
+
"相比",
|
|
7167
|
+
"compare"
|
|
7168
|
+
],
|
|
7169
|
+
relation: [
|
|
7170
|
+
"关系",
|
|
7171
|
+
"相关",
|
|
7172
|
+
"散点",
|
|
7173
|
+
"分布",
|
|
7174
|
+
"relation",
|
|
7175
|
+
"scatter"
|
|
7176
|
+
],
|
|
7177
|
+
stack: [
|
|
7178
|
+
"叠加",
|
|
7179
|
+
"堆叠",
|
|
7180
|
+
"累计",
|
|
7181
|
+
"stack"
|
|
7182
|
+
]
|
|
7183
|
+
};
|
|
7184
|
+
function detectIntent(text) {
|
|
7185
|
+
if (!text || typeof text !== "string") return null;
|
|
7186
|
+
for (const intent of [
|
|
7187
|
+
"trend",
|
|
7188
|
+
"stack",
|
|
7189
|
+
"share",
|
|
7190
|
+
"rank",
|
|
7191
|
+
"relation",
|
|
7192
|
+
"compare"
|
|
7193
|
+
]) if (INTENT_KEYWORDS[intent].some((kw) => text.includes(kw))) return intent;
|
|
7194
|
+
return null;
|
|
7195
|
+
}
|
|
7196
|
+
function fmtValue(v) {
|
|
7197
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return String(v);
|
|
7198
|
+
if (Math.abs(v) >= 1e8) return `${(v / 1e8).toFixed(1)}亿`;
|
|
7199
|
+
if (Math.abs(v) >= 1e4) return `${(v / 1e4).toFixed(1)}万`;
|
|
7200
|
+
return `${Math.round(v * 100) / 100}`;
|
|
7201
|
+
}
|
|
7202
|
+
function buildNarrative(spec, labels, seriesList) {
|
|
7203
|
+
const annotations = [];
|
|
7204
|
+
let best = null;
|
|
7205
|
+
seriesList.forEach((s, si) => {
|
|
7206
|
+
s.data.forEach((v, di) => {
|
|
7207
|
+
if (typeof v === "number" && Number.isFinite(v) && (!best || v > best.value)) best = {
|
|
7208
|
+
value: v,
|
|
7209
|
+
label: labels[di],
|
|
7210
|
+
series: s.name,
|
|
7211
|
+
si,
|
|
7212
|
+
di
|
|
7213
|
+
};
|
|
7214
|
+
});
|
|
7215
|
+
});
|
|
7216
|
+
if (best) annotations.push({
|
|
7217
|
+
type: "callout",
|
|
7218
|
+
x: best.label,
|
|
7219
|
+
y: best.value,
|
|
7220
|
+
series: best.series,
|
|
7221
|
+
label: `峰值 ${fmtValue(best.value)}`,
|
|
7222
|
+
anchor: "top-right"
|
|
7223
|
+
});
|
|
7224
|
+
const first = seriesList[0];
|
|
7225
|
+
if (first && first.data.length >= 2) {
|
|
7226
|
+
const prev = first.data[first.data.length - 2];
|
|
7227
|
+
const last = first.data[first.data.length - 1];
|
|
7228
|
+
if (typeof prev === "number" && typeof last === "number" && prev !== 0) {
|
|
7229
|
+
const pct = (last - prev) / Math.abs(prev);
|
|
7230
|
+
if (Math.abs(pct) >= .01) annotations.push({
|
|
7231
|
+
type: "delta",
|
|
7232
|
+
x: labels[labels.length - 1],
|
|
7233
|
+
y: last,
|
|
7234
|
+
series: first.name,
|
|
7235
|
+
direction: pct >= 0 ? "up" : "down",
|
|
7236
|
+
text: `环比 ${pct >= 0 ? "+" : ""}${(pct * 100).toFixed(0)}%`
|
|
7237
|
+
});
|
|
7238
|
+
}
|
|
7239
|
+
}
|
|
7240
|
+
if (annotations.length > 0) spec.annotations = annotations.slice(0, 3);
|
|
7241
|
+
}
|
|
7242
|
+
function generateChartSpec(data, hint = {}) {
|
|
7243
|
+
const table = parseDataTable(data);
|
|
7244
|
+
if (!table) return {
|
|
7245
|
+
spec: null,
|
|
7246
|
+
report: [{
|
|
7247
|
+
level: "error",
|
|
7248
|
+
message: "无法识别的数据格式:支持 CSV/TSV 文本、对象数组、二维数组"
|
|
7249
|
+
}]
|
|
7250
|
+
};
|
|
7251
|
+
const cols = inferColumns(table);
|
|
7252
|
+
const numbers = cols.filter((c) => c.type === "number");
|
|
7253
|
+
const times = cols.filter((c) => c.type === "time");
|
|
7254
|
+
const cats = cols.filter((c) => c.type === "category" && distinctCount(c.values) > 0);
|
|
7255
|
+
const dimension = times[0] || cats[0] || null;
|
|
7256
|
+
const labels = dimension ? dimension.values.map((v) => String(v)) : table.rows.map((_, i) => String(i + 1));
|
|
7257
|
+
const intent = hint.intent || detectIntent([hint.title, hint.hint].filter(Boolean).join(" ")) || defaultIntent({
|
|
7258
|
+
times: times.length,
|
|
7259
|
+
numbers: numbers.length,
|
|
7260
|
+
cats: cats.length,
|
|
7261
|
+
labels: labels.length
|
|
7262
|
+
});
|
|
7263
|
+
const report = [{
|
|
7264
|
+
level: "info",
|
|
7265
|
+
message: `识别到 ${numbers.length} 个数值列、${times.length} 个时间列、${cats.length} 个类目列;意图「${intent}」`
|
|
7266
|
+
}];
|
|
7267
|
+
const spec = {};
|
|
7268
|
+
if (hint.title) spec.title = hint.title;
|
|
7269
|
+
if (intent === "relation" && numbers.length >= 2) {
|
|
7270
|
+
const cat = cats[0];
|
|
7271
|
+
spec.type = "scatter";
|
|
7272
|
+
spec.scatterData = table.rows.map((_, i) => ({
|
|
7273
|
+
x: numbers[0].values[i],
|
|
7274
|
+
y: numbers[1].values[i],
|
|
7275
|
+
label: cat ? String(cat.values[i]) : String(i + 1)
|
|
7276
|
+
}));
|
|
7277
|
+
report.push({
|
|
7278
|
+
level: "info",
|
|
7279
|
+
message: `散点图:${numbers[0].name} → x,${numbers[1].name} → y`
|
|
7280
|
+
});
|
|
7281
|
+
return {
|
|
7282
|
+
spec,
|
|
7283
|
+
report
|
|
7284
|
+
};
|
|
7285
|
+
}
|
|
7286
|
+
if (numbers.length === 0) return {
|
|
7287
|
+
spec: null,
|
|
7288
|
+
report: [{
|
|
7289
|
+
level: "error",
|
|
7290
|
+
message: "数据中没有可用的数值列(列需 ≥60% 为可解析数字)"
|
|
7291
|
+
}]
|
|
7292
|
+
};
|
|
7293
|
+
const seriesList = numbers.slice(0, 8).map((c) => ({
|
|
7294
|
+
name: c.name,
|
|
7295
|
+
data: c.values
|
|
7296
|
+
}));
|
|
7297
|
+
if (numbers.length > 8) report.push({
|
|
7298
|
+
level: "warn",
|
|
7299
|
+
message: `数值列超过 8 个,仅取前 8 列(${seriesList.map((s) => s.name).join("、")})`
|
|
7300
|
+
});
|
|
7301
|
+
if (intent === "share") {
|
|
7302
|
+
if (seriesList.length === 1 && labels.length <= 8) {
|
|
7303
|
+
const pieData = labels.map((name, i) => ({
|
|
7304
|
+
name,
|
|
7305
|
+
value: seriesList[0].data[i]
|
|
7306
|
+
})).filter((d) => typeof d.value === "number" && d.value > 0);
|
|
7307
|
+
spec.type = "pie";
|
|
7308
|
+
spec.pieData = pieData;
|
|
7309
|
+
report.push({
|
|
7310
|
+
level: "info",
|
|
7311
|
+
message: `饼图:${seriesList[0].name} 按类目占比(${pieData.length} 项)`
|
|
7312
|
+
});
|
|
7313
|
+
return {
|
|
7314
|
+
spec,
|
|
7315
|
+
report
|
|
7316
|
+
};
|
|
7317
|
+
}
|
|
7318
|
+
if (seriesList.length >= 2 && seriesList.length <= 8) {
|
|
7319
|
+
const sums = seriesList.map((s) => s.data.reduce((acc, v) => typeof v === "number" && Number.isFinite(v) ? acc + v : acc, 0));
|
|
7320
|
+
if (sums.every((v) => v > 0)) {
|
|
7321
|
+
spec.type = "pie";
|
|
7322
|
+
spec.pieData = seriesList.map((s, i) => ({
|
|
7323
|
+
name: s.name,
|
|
7324
|
+
value: sums[i]
|
|
7325
|
+
}));
|
|
7326
|
+
report.push({
|
|
7327
|
+
level: "info",
|
|
7328
|
+
message: `饼图:按度量合计占比(${sums.length} 项)`
|
|
7329
|
+
});
|
|
7330
|
+
return {
|
|
7331
|
+
spec,
|
|
7332
|
+
report
|
|
7333
|
+
};
|
|
7334
|
+
}
|
|
7335
|
+
}
|
|
7336
|
+
}
|
|
7337
|
+
const labelWidth = labels.reduce((m, l) => Math.max(m, l.length), 0);
|
|
7338
|
+
const wantRank = intent === "rank" || intent === "compare" && seriesList.length === 1 && labels.length >= 9;
|
|
7339
|
+
if (seriesList.length === 1 && (wantRank || labels.length >= 13 || labelWidth >= 5 && labels.length >= 7)) {
|
|
7340
|
+
spec.type = "horizontal-bar";
|
|
7341
|
+
spec.labels = labels;
|
|
7342
|
+
spec.series = seriesList;
|
|
7343
|
+
report.push({
|
|
7344
|
+
level: "info",
|
|
7345
|
+
message: "横向条形图:单度量分类对比,类目名走纵列更易读"
|
|
7346
|
+
});
|
|
7347
|
+
finishCartesian(spec, hint, labels, seriesList, report);
|
|
7348
|
+
return {
|
|
7349
|
+
spec,
|
|
7350
|
+
report
|
|
7351
|
+
};
|
|
7352
|
+
}
|
|
7353
|
+
if (intent === "stack" && seriesList.length >= 2) {
|
|
7354
|
+
spec.type = "stacked-bar";
|
|
7355
|
+
report.push({
|
|
7356
|
+
level: "info",
|
|
7357
|
+
message: "堆叠柱状图:多度量叠加看构成"
|
|
7358
|
+
});
|
|
7359
|
+
} else if ((intent === "compare" || intent === "rank") && seriesList.length <= 3 && labels.length <= 12) {
|
|
7360
|
+
spec.type = "bar";
|
|
7361
|
+
report.push({
|
|
7362
|
+
level: "info",
|
|
7363
|
+
message: "柱状图:类目间量值对比"
|
|
7364
|
+
});
|
|
7365
|
+
} else {
|
|
7366
|
+
spec.type = "line";
|
|
7367
|
+
report.push({
|
|
7368
|
+
level: "info",
|
|
7369
|
+
message: "折线图:维度有序(时间/类目),看走势与多系列对比"
|
|
7370
|
+
});
|
|
7371
|
+
}
|
|
7372
|
+
spec.labels = labels;
|
|
7373
|
+
spec.series = seriesList;
|
|
7374
|
+
if (seriesList.length > 1) spec.legend = { show: true };
|
|
7375
|
+
finishCartesian(spec, hint, labels, seriesList, report);
|
|
7376
|
+
return {
|
|
7377
|
+
spec,
|
|
7378
|
+
report
|
|
7379
|
+
};
|
|
7380
|
+
}
|
|
7381
|
+
function finishCartesian(spec, hint, labels, seriesList, report) {
|
|
7382
|
+
if (hint.narrative) {
|
|
7383
|
+
buildNarrative(spec, labels, seriesList);
|
|
7384
|
+
if (spec.annotations) report.push({
|
|
7385
|
+
level: "info",
|
|
7386
|
+
message: `叙述注解 ${spec.annotations.length} 处(峰值 callout / 末点环比 delta)`
|
|
7387
|
+
});
|
|
7388
|
+
}
|
|
7389
|
+
}
|
|
7390
|
+
function defaultIntent({ times, numbers, cats, labels }) {
|
|
7391
|
+
if (times > 0 && numbers > 0) return "trend";
|
|
7392
|
+
if (numbers >= 2 && cats === 0) return "relation";
|
|
7393
|
+
if (numbers === 1 && (cats > 0 || labels > 0)) return "compare";
|
|
7394
|
+
return "trend";
|
|
7395
|
+
}
|
|
7396
|
+
var SPEC_RULES = [
|
|
7397
|
+
"type 必填,取值必须是 schema enum 中的图表类型",
|
|
7398
|
+
"series[].data 长度必须与 labels 一致,缺失值用 null",
|
|
7399
|
+
"叙述注解 annotations 单图不超过 3 处,emphasis 焦点最多 1 个系列",
|
|
7400
|
+
"不使用 schema 之外的字段",
|
|
7401
|
+
`支持的图表类型:${chartOptionsSchema.properties.type.enum.join(" / ")}`
|
|
7402
|
+
];
|
|
7403
|
+
//#endregion
|
|
7404
|
+
//#region src/ai/examples.js
|
|
7405
|
+
var SPEC_EXAMPLES = [
|
|
7406
|
+
{
|
|
7407
|
+
requirement: "看上半年各渠道销售额走势",
|
|
7408
|
+
spec: {
|
|
7409
|
+
type: "line",
|
|
7410
|
+
title: "渠道月度销售额",
|
|
7411
|
+
labels: [
|
|
7412
|
+
"1月",
|
|
7413
|
+
"2月",
|
|
7414
|
+
"3月",
|
|
7415
|
+
"4月",
|
|
7416
|
+
"5月",
|
|
7417
|
+
"6月"
|
|
7418
|
+
],
|
|
7419
|
+
series: [{
|
|
7420
|
+
name: "线上商城",
|
|
7421
|
+
data: [
|
|
7422
|
+
320,
|
|
7423
|
+
356,
|
|
7424
|
+
401,
|
|
7425
|
+
388,
|
|
7426
|
+
520,
|
|
7427
|
+
560
|
|
7428
|
+
]
|
|
7429
|
+
}, {
|
|
7430
|
+
name: "线下门店",
|
|
7431
|
+
data: [
|
|
7432
|
+
280,
|
|
7433
|
+
302,
|
|
7434
|
+
346,
|
|
7435
|
+
331,
|
|
7436
|
+
352,
|
|
7437
|
+
428
|
|
7438
|
+
]
|
|
7439
|
+
}],
|
|
7440
|
+
legend: { show: true }
|
|
7441
|
+
}
|
|
7442
|
+
},
|
|
7443
|
+
{
|
|
7444
|
+
requirement: "各渠道销售额占比",
|
|
7445
|
+
spec: {
|
|
7446
|
+
type: "pie",
|
|
7447
|
+
title: "渠道销售额占比",
|
|
7448
|
+
pieData: [
|
|
7449
|
+
{
|
|
7450
|
+
name: "线上商城",
|
|
7451
|
+
value: 2545
|
|
7452
|
+
},
|
|
7453
|
+
{
|
|
7454
|
+
name: "线下门店",
|
|
7455
|
+
value: 2039
|
|
7456
|
+
},
|
|
7457
|
+
{
|
|
7458
|
+
name: "社群团购",
|
|
7459
|
+
value: 844
|
|
7460
|
+
}
|
|
7461
|
+
]
|
|
7462
|
+
}
|
|
7463
|
+
},
|
|
7464
|
+
{
|
|
7465
|
+
requirement: "各门店销量排行",
|
|
7466
|
+
spec: {
|
|
7467
|
+
type: "horizontal-bar",
|
|
7468
|
+
title: "门店销量排行",
|
|
7469
|
+
labels: [
|
|
7470
|
+
"线下体验店",
|
|
7471
|
+
"线上旗舰店",
|
|
7472
|
+
"社群团购点",
|
|
7473
|
+
"仓储会员店",
|
|
7474
|
+
"社区便利店"
|
|
7475
|
+
],
|
|
7476
|
+
series: [{
|
|
7477
|
+
name: "销量",
|
|
7478
|
+
data: [
|
|
7479
|
+
120,
|
|
7480
|
+
98,
|
|
7481
|
+
80,
|
|
7482
|
+
60,
|
|
7483
|
+
52
|
|
7484
|
+
]
|
|
7485
|
+
}]
|
|
7486
|
+
}
|
|
7487
|
+
},
|
|
7488
|
+
{
|
|
7489
|
+
requirement: "广告投放与成交金额的关系",
|
|
7490
|
+
spec: {
|
|
7491
|
+
type: "scatter",
|
|
7492
|
+
title: "投放-成交关系",
|
|
7493
|
+
scatterData: [
|
|
7494
|
+
{
|
|
7495
|
+
x: 100,
|
|
7496
|
+
y: 12,
|
|
7497
|
+
label: "华东"
|
|
7498
|
+
},
|
|
7499
|
+
{
|
|
7500
|
+
x: 200,
|
|
7501
|
+
y: 26,
|
|
7502
|
+
label: "华南"
|
|
7503
|
+
},
|
|
7504
|
+
{
|
|
7505
|
+
x: 300,
|
|
7506
|
+
y: 31,
|
|
7507
|
+
label: "华北"
|
|
7508
|
+
}
|
|
7509
|
+
]
|
|
7510
|
+
}
|
|
7511
|
+
}
|
|
7512
|
+
];
|
|
7513
|
+
function formatExamples(maxCount = SPEC_EXAMPLES.length) {
|
|
7514
|
+
return SPEC_EXAMPLES.slice(0, maxCount).map((e) => `需求:${e.requirement}\nSpec:${JSON.stringify(e.spec)}`).join("\n\n");
|
|
7515
|
+
}
|
|
7516
|
+
//#endregion
|
|
7517
|
+
//#region src/ai/prompt.js
|
|
7518
|
+
var PREVIEW_ROWS = 8;
|
|
7519
|
+
function buildChartPrompt({ data, requirement, extraRules = [], examples = true } = {}) {
|
|
7520
|
+
const table = parseDataTable(data);
|
|
7521
|
+
let digest;
|
|
7522
|
+
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} 行` : ""}`;
|
|
7523
|
+
else if (data !== void 0 && data !== null) digest = typeof data === "string" ? data : JSON.stringify(data);
|
|
7524
|
+
else digest = "(未提供数据,请基于需求虚构合理示例数据)";
|
|
7525
|
+
const rules = [...SPEC_RULES, ...extraRules];
|
|
7526
|
+
const sections = [
|
|
7527
|
+
"你是图表配置生成器。根据下面的数据与需求,产出 EvChart 的 options JSON(下称 Spec),它将被 <ev-chart :options> 直接渲染。",
|
|
7528
|
+
`## Options Schema\n${JSON.stringify(chartOptionsSchema)}`,
|
|
7529
|
+
`## 数据预览\n${digest}`
|
|
7530
|
+
];
|
|
7531
|
+
if (examples) sections.push(`## 示例\n${formatExamples()}`);
|
|
7532
|
+
sections.push(requirement ? `## 需求\n${requirement}` : "## 需求\n根据数据形状自行选择最合适的图表类型,并拟一个不超过 12 字的标题。");
|
|
7533
|
+
sections.push(`## 硬性规则\n${rules.map((r, i) => `${i + 1}. ${r}`).join("\n")}`);
|
|
7534
|
+
return sections.join("\n\n");
|
|
7535
|
+
}
|
|
7536
|
+
//#endregion
|
|
7537
|
+
//#region src/ai/lint.js
|
|
7538
|
+
var DEFAULT_SIZE = {
|
|
7539
|
+
width: 800,
|
|
7540
|
+
height: 450
|
|
7541
|
+
};
|
|
7542
|
+
function checkTextOverflow(svg, width, height) {
|
|
7543
|
+
const issues = [];
|
|
7544
|
+
let count = 0;
|
|
7545
|
+
for (const m of svg.matchAll(/<text x="([-\d.]+)" y="([-\d.]+)"/g)) {
|
|
7546
|
+
const x = parseFloat(m[1]);
|
|
7547
|
+
const y = parseFloat(m[2]);
|
|
7548
|
+
if (x < -1 || x > width + 1 || y < 0 || y > height + 1) count++;
|
|
7549
|
+
}
|
|
7550
|
+
if (count > 0) issues.push({
|
|
7551
|
+
level: "warn",
|
|
7552
|
+
message: `${count} 处文本超出画布边界(可能被裁剪)`,
|
|
7553
|
+
rule: "text-overflow"
|
|
7554
|
+
});
|
|
7555
|
+
return issues;
|
|
7556
|
+
}
|
|
7557
|
+
function checkTextOverlap(svg) {
|
|
7558
|
+
const issues = [];
|
|
7559
|
+
const texts = [];
|
|
7560
|
+
for (const m of svg.matchAll(/<text x="([-\d.]+)" y="([-\d.]+)" font-size="([\d.]+)"([^>]*)>([^<]*)<\/text>/g)) {
|
|
7561
|
+
const x = parseFloat(m[1]);
|
|
7562
|
+
const y = parseFloat(m[2]);
|
|
7563
|
+
const fs = parseFloat(m[3]);
|
|
7564
|
+
const attrs = m[4];
|
|
7565
|
+
const content = m[5].trim();
|
|
7566
|
+
if (!content) continue;
|
|
7567
|
+
const w = estimateTextWidth(content, fs);
|
|
7568
|
+
let left = x;
|
|
7569
|
+
if (/text-anchor="middle"/.test(attrs)) left = x - w / 2;
|
|
7570
|
+
else if (/text-anchor="end"/.test(attrs)) left = x - w;
|
|
7571
|
+
texts.push({
|
|
7572
|
+
left,
|
|
7573
|
+
right: left + w,
|
|
7574
|
+
y,
|
|
7575
|
+
fs
|
|
7576
|
+
});
|
|
7577
|
+
}
|
|
7578
|
+
let count = 0;
|
|
7579
|
+
for (let i = 0; i < texts.length; i++) for (let j = i + 1; j < texts.length; j++) {
|
|
7580
|
+
const a = texts[i];
|
|
7581
|
+
const b = texts[j];
|
|
7582
|
+
if (Math.abs(a.y - b.y) >= Math.max(a.fs, b.fs) * .8) continue;
|
|
7583
|
+
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++;
|
|
7584
|
+
}
|
|
7585
|
+
if (count > 0) issues.push({
|
|
7586
|
+
level: "warn",
|
|
7587
|
+
message: `${count} 处文本相互重叠(标签或注解可能互相遮挡)`,
|
|
7588
|
+
rule: "text-overlap"
|
|
7589
|
+
});
|
|
7590
|
+
return issues;
|
|
7591
|
+
}
|
|
7592
|
+
function luminance(color) {
|
|
7593
|
+
let s = String(color).trim();
|
|
7594
|
+
if (/^#[0-9a-fA-F]{6}$/.test(s)) s = s.slice(1);
|
|
7595
|
+
else if (/^#[0-9a-fA-F]{3}$/.test(s)) s = s.slice(1).split("").map((c) => c + c).join("");
|
|
7596
|
+
else return null;
|
|
7597
|
+
const n = parseInt(s, 16);
|
|
7598
|
+
const chan = [
|
|
7599
|
+
n >> 16 & 255,
|
|
7600
|
+
n >> 8 & 255,
|
|
7601
|
+
n & 255
|
|
7602
|
+
].map((v) => {
|
|
7603
|
+
const c = v / 255;
|
|
7604
|
+
return c <= .03928 ? c / 12.92 : Math.pow((c + .055) / 1.055, 2.4);
|
|
7605
|
+
});
|
|
7606
|
+
return .2126 * chan[0] + .7152 * chan[1] + .0722 * chan[2];
|
|
7607
|
+
}
|
|
7608
|
+
function contrastRatio(a, b) {
|
|
7609
|
+
const la = luminance(a);
|
|
7610
|
+
const lb = luminance(b);
|
|
7611
|
+
if (la === null || lb === null) return null;
|
|
7612
|
+
return (Math.max(la, lb) + .05) / (Math.min(la, lb) + .05);
|
|
7613
|
+
}
|
|
7614
|
+
function checkPaletteContrast(spec) {
|
|
7615
|
+
const issues = [];
|
|
7616
|
+
if (!spec.theme || !Array.isArray(spec.theme.colors)) return issues;
|
|
7617
|
+
[false, true].forEach((dark) => {
|
|
7618
|
+
const theme = getTheme(dark, spec.theme);
|
|
7619
|
+
spec.theme.colors.forEach((c) => {
|
|
7620
|
+
const ratio = contrastRatio(c, theme.backgroundColor);
|
|
7621
|
+
if (ratio !== null && ratio < 3) issues.push({
|
|
7622
|
+
level: "warn",
|
|
7623
|
+
message: `自定义色板${dark ? "暗色" : "浅色"}模式对比度不足 3:1:${c}(${ratio.toFixed(1)})`,
|
|
7624
|
+
rule: "low-contrast"
|
|
7625
|
+
});
|
|
7626
|
+
});
|
|
7627
|
+
});
|
|
7628
|
+
return issues;
|
|
7629
|
+
}
|
|
7630
|
+
function lintChartSpec(spec, opts = {}) {
|
|
7631
|
+
const issues = [];
|
|
7632
|
+
const fixed = { ...spec };
|
|
7633
|
+
if (!fixed || typeof fixed !== "object") return {
|
|
7634
|
+
issues: [{
|
|
7635
|
+
level: "error",
|
|
7636
|
+
message: "Spec 必须是对象"
|
|
7637
|
+
}],
|
|
7638
|
+
spec: fixed
|
|
7639
|
+
};
|
|
7640
|
+
const { warnings } = validateOptions(fixed);
|
|
7641
|
+
warnings.forEach((w) => issues.push({
|
|
7642
|
+
level: "error",
|
|
7643
|
+
message: `${w.path} ${w.message}`,
|
|
7644
|
+
path: w.path
|
|
7645
|
+
}));
|
|
7646
|
+
if (issues.some((i) => i.level === "error" && i.path === "options.type")) return {
|
|
7647
|
+
issues,
|
|
7648
|
+
spec: fixed
|
|
7649
|
+
};
|
|
7650
|
+
if (Array.isArray(fixed.annotations) && fixed.annotations.length > 3) {
|
|
7651
|
+
fixed.annotations = fixed.annotations.slice(0, 3);
|
|
7652
|
+
issues.push({
|
|
7653
|
+
level: "info",
|
|
7654
|
+
message: "注解超过 3 处,已按焦点预算裁剪",
|
|
7655
|
+
rule: "annotation-budget"
|
|
7656
|
+
});
|
|
7657
|
+
}
|
|
7658
|
+
if ((fixed.type === "pie" || fixed.type === "doughnut") && Array.isArray(fixed.pieData) && fixed.pieData.length > 8) issues.push({
|
|
7659
|
+
level: "warn",
|
|
7660
|
+
message: `饼图 ${fixed.pieData.length} 个扇区过多(>8),建议改用横向条形图`,
|
|
7661
|
+
rule: "pie-slices"
|
|
7662
|
+
});
|
|
7663
|
+
issues.push(...checkPaletteContrast(fixed));
|
|
7664
|
+
if (fixed.type === "bar" && Array.isArray(fixed.series) && fixed.series.length === 1 && Array.isArray(fixed.labels)) {
|
|
7665
|
+
const width = opts.width ?? DEFAULT_SIZE.width;
|
|
7666
|
+
const labels = fixed.labels.map(String);
|
|
7667
|
+
const maxLabelWidth = Math.max(...labels.map((l) => estimateTextWidth(l, 12)), 0);
|
|
7668
|
+
const slots = Math.max(1, (width - 80) / Math.max(1, labels.length));
|
|
7669
|
+
if (labels.length >= 7 && maxLabelWidth > slots * .9) {
|
|
7670
|
+
fixed.type = "horizontal-bar";
|
|
7671
|
+
issues.push({
|
|
7672
|
+
level: "info",
|
|
7673
|
+
message: "类目名过宽拥挤,已自动改为横向条形图",
|
|
7674
|
+
rule: "bar-to-horizontal"
|
|
7675
|
+
});
|
|
7676
|
+
}
|
|
7677
|
+
}
|
|
7678
|
+
if (typeof document !== "undefined" && fixed.type) try {
|
|
7679
|
+
const width = opts.width ?? DEFAULT_SIZE.width;
|
|
7680
|
+
const height = opts.height ?? DEFAULT_SIZE.height;
|
|
7681
|
+
const canvas = document.createElement("canvas");
|
|
7682
|
+
canvas.width = width;
|
|
7683
|
+
canvas.height = height;
|
|
7684
|
+
const real = canvas.getContext("2d");
|
|
7685
|
+
if (real) {
|
|
7686
|
+
const recorder = createSvgRecorder(real);
|
|
7687
|
+
renderChart(canvas, {
|
|
7688
|
+
options: fixed,
|
|
7689
|
+
ctx: recorder.ctx,
|
|
7690
|
+
dpr: 1,
|
|
7691
|
+
progress: 1,
|
|
7692
|
+
hoverIndex: -1,
|
|
7693
|
+
mouseX: -1,
|
|
7694
|
+
mouseY: -1,
|
|
7695
|
+
showCrosshair: false
|
|
7696
|
+
});
|
|
7697
|
+
issues.push(...checkTextOverflow(recorder.toSvg(width, height, "#ffffff"), width, height));
|
|
7698
|
+
issues.push(...checkTextOverlap(recorder.toSvg(width, height, "#ffffff")));
|
|
7699
|
+
}
|
|
7700
|
+
} catch {
|
|
7701
|
+
issues.push({
|
|
7702
|
+
level: "info",
|
|
7703
|
+
message: "无头渲染自检不可用,已跳过几何检查",
|
|
7704
|
+
rule: "headless-skipped"
|
|
7705
|
+
});
|
|
7706
|
+
}
|
|
7707
|
+
return {
|
|
7708
|
+
issues,
|
|
7709
|
+
spec: fixed
|
|
7710
|
+
};
|
|
7711
|
+
}
|
|
7712
|
+
//#endregion
|
|
6187
7713
|
//#region src/index.js
|
|
6188
7714
|
var components = { EvChart: chart_default };
|
|
6189
7715
|
function install(app, _options = {}) {
|
|
@@ -6192,4 +7718,4 @@ function install(app, _options = {}) {
|
|
|
6192
7718
|
var EvokeCharts = { install };
|
|
6193
7719
|
var src_default = { install };
|
|
6194
7720
|
//#endregion
|
|
6195
|
-
export { chart_default as EvChart, EvokeCharts, applySeriesPalette, clearSeriesPalette, components, src_default as default, injectChartContext, install, provideChartContext, useChart };
|
|
7721
|
+
export { chart_default as EvChart, EvokeCharts, applySeriesPalette, buildChartPrompt, chartOptionsSchema, clearSeriesPalette, components, src_default as default, generateChartSpec, injectChartContext, install, lintChartSpec, parseDataTable, provideChartContext, useChart, validateOptions };
|