feat: 扩展 ECharts 第二阶段图表支持

新增 tree、treemap、sunburst、graph、sankey、chord、funnel、gauge 和 pictorialBar 系列,并注册对应的 ECharts 按需模块。

完善层级与关系数据校验、节点和边数量限制、默认布局及错误占位规则,同时补充默认 Markdown 与 README 示例。

静态渲染全局关闭 ECharts 动画,修复 Treemap 标签冻结在透明动画帧的问题,并完成快速预览、精确 PDF、容器镜像和 147 项全项目测试验证。
This commit is contained in:
SkyJourney
2026-07-27 17:47:28 +08:00
parent f5a46f79f1
commit 92a5bfd016
13 changed files with 1162 additions and 21 deletions
@@ -11,6 +11,9 @@ export interface EChartsSecurityLimits {
maxArrayLength: number;
maxStringLength: number;
maxSeries: number;
maxHierarchyNodes: number;
maxGraphNodes: number;
maxGraphEdges: number;
}
export const defaultEChartsSecurityLimits: EChartsSecurityLimits = {
@@ -19,7 +22,10 @@ export const defaultEChartsSecurityLimits: EChartsSecurityLimits = {
maxObjectKeys: 1_000,
maxArrayLength: 20_000,
maxStringLength: 100_000,
maxSeries: 32
maxSeries: 32,
maxHierarchyNodes: 5_000,
maxGraphNodes: 5_000,
maxGraphEdges: 10_000
};
const forbiddenPropertyNames = new Set([
@@ -257,6 +263,86 @@ function assertSupportedSeries(
`option.series.${index}.type 暂不支持 ${String(type)}`
);
}
assertSeriesStructureLimits(item, index, type, limits);
}
}
function countHierarchyNodes(
value: JsonValue | undefined,
limit: number,
path: string
) {
if (!Array.isArray(value)) {
return;
}
let count = 0;
const pending = [...value];
while (pending.length > 0) {
const node = pending.pop();
count += 1;
if (count > limit) {
throw new EChartsFenceError(
"LIMIT_EXCEEDED",
`${path} 层级节点数量不能超过 ${limit}`
);
}
if (
node !== null &&
!Array.isArray(node) &&
typeof node === "object" &&
Array.isArray(node.children)
) {
pending.push(...node.children);
}
}
}
function arrayLength(value: JsonValue | undefined) {
return Array.isArray(value) ? value.length : 0;
}
function assertSeriesStructureLimits(
series: Record<string, JsonValue>,
seriesIndex: number,
type: string,
limits: EChartsSecurityLimits
) {
const path = `option.series.${seriesIndex}`;
if (
type === "tree" ||
type === "treemap" ||
type === "sunburst"
) {
countHierarchyNodes(
series.data,
limits.maxHierarchyNodes,
`${path}.data`
);
return;
}
if (
type !== "graph" &&
type !== "sankey" &&
type !== "chord"
) {
return;
}
const nodeCount =
arrayLength(series.data) + arrayLength(series.nodes);
if (nodeCount > limits.maxGraphNodes) {
throw new EChartsFenceError(
"LIMIT_EXCEEDED",
`${path} 节点数量不能超过 ${limits.maxGraphNodes}`
);
}
const edgeCount =
arrayLength(series.links) + arrayLength(series.edges);
if (edgeCount > limits.maxGraphEdges) {
throw new EChartsFenceError(
"LIMIT_EXCEEDED",
`${path} 边数量不能超过 ${limits.maxGraphEdges}`
);
}
}