@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/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 ticks = axisConfig.ticks || 5;
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, ticks);
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
- if (leftRange && options.annotation) renderAnnotations(renderCtx, leftRange);
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 _hoisted_3 = { class: "ev-chart__empty-icon-wrap" };
4209
- var _hoisted_4 = { class: "ev-chart__empty-text" };
4210
- var _hoisted_5 = {
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 _hoisted_6 = { class: "ev-chart__loading-bars" };
4215
- var _hoisted_7 = {
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 _hoisted_8 = { class: "ev-chart__error-text" };
4220
- var _hoisted_9 = ["innerHTML"];
4221
- var _hoisted_10 = {
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
- if (!getDataZoomConfig(opt)) return opt;
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 opt;
4363
- const sliced = { ...opt };
4364
- const n = (opt.labels || []).length;
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 = (opt.labels || []).slice(startIdx, endIdx);
4368
- if (opt.series) sliced.series = opt.series.map((s) => ({
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 (opt.candleData) sliced.candleData = opt.candleData.slice(startIdx, endIdx);
5002
+ if (base.candleData) sliced.candleData = base.candleData.slice(startIdx, endIdx);
4373
5003
  sliced.__sliceStart = startIdx;
4374
- } else if (opt.scatterData && opt.scatterData.length > 0) {
4375
- const xs = opt.scatterData.map((d) => d.x);
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 = opt.scatterData.filter((d) => d.x >= lo && d.x <= hi);
4381
- sliced.scatterData = filtered.length > 0 ? filtered : opt.scatterData;
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 (spaceBelow >= th) top = relY + gap;
4431
- else if (spaceAbove >= th) top = relY - gap - th;
4432
- else top = spaceBelow >= spaceAbove ? containerRect.height - th - margin : legendBottom;
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 animEnabled = animate && props.options.animation?.enabled !== false;
4484
- animationState = createAnimation(props.options.animation);
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
- const nextFocus = props.options.legend?.hoverEmphasis !== false && legendName ? legendName : null;
5506
- if (nextFocus !== focusSeries.value) {
5507
- focusSeries.value = nextFocus;
5508
- redraw();
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", _hoisted_2, [createElementVNode("div", _hoisted_3, [(openBlock(), createBlock(resolveDynamicComponent(DataLine), { class: "ev-chart__empty-img" }))]), createElementVNode("span", _hoisted_4, toDisplayString(emptyTextI18n.value), 1)])) : createCommentVNode("", true)]),
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", _hoisted_5, [createElementVNode("div", _hoisted_6, [(openBlock(), createElementBlock(Fragment, null, renderList(6, (i) => {
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", _hoisted_7, [_cache[0] || (_cache[0] = createElementVNode("span", { class: "ev-chart__error-icon" }, [createElementVNode("svg", {
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", _hoisted_8, toDisplayString(internalError.value), 1)])) : createCommentVNode("", true)]),
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, _hoisted_9)) : createCommentVNode("", true)]),
6817
+ }, null, 14, _hoisted_10)) : createCommentVNode("", true)]),
6028
6818
  _: 1
6029
6819
  }),
6030
- createElementVNode("div", _hoisted_10, toDisplayString(ariaLiveText.value), 1)
6820
+ createElementVNode("div", _hoisted_11, toDisplayString(ariaLiveText.value), 1)
6031
6821
  ], 12, _hoisted_1);
6032
6822
  };
6033
6823
  }
6034
- }), [["__scopeId", "data-v-a07b68e8"]]);
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 };