apache/echarts · error
Sankey is a DAG, the original data has cycle!
Error message
Sankey is a DAG, the original data has cycle!
What it means
Runtime (NOT dev-only) check in sankeyLayout: Sankey requires a directed acyclic graph. The layout performs a topological sort via indegree counting; any edge still flagged in remainEdges after the sort means a directed cycle exists, which the layout rejects by throwing. Fires in production builds.
Source
Thrown at src/chart/sankey/sankeyLayout.ts:170
for (let edgeIdx = 0; edgeIdx < node.outEdges.length; edgeIdx++) {
const edge = node.outEdges[edgeIdx];
const indexEdge = zrUtil.indexOf(edges, edge);
remainEdges[indexEdge] = 0;
const targetNode = edge.node2;
const nodeIndex = zrUtil.indexOf(nodes, targetNode);
if (--indegreeArr[nodeIndex] === 0 && zrUtil.indexOf(nextTargetNode, targetNode) < 0) {
nextTargetNode.push(targetNode);
}
}
}
++x;
zeroIndegrees = nextTargetNode;
nextTargetNode = [];
}
for (let i = 0; i < remainEdges.length; i++) {
if (remainEdges[i] === 1) {
throw new Error('Sankey is a DAG, the original data has cycle!');
}
}
const maxDepth = maxNodeDepth > x - 1 ? maxNodeDepth : x - 1;
if (nodeAlign && nodeAlign !== 'left') {
adjustNodeWithNodeAlign(nodes, nodeAlign, orient, maxDepth);
}
const kx = orient === 'vertical'
? (height - nodeWidth) / maxDepth
: (width - nodeWidth) / maxDepth;
scaleNodeBreadths(nodes, kx, orient);
}
function isNodeDepth(node: GraphNode) {
const item = node.hostGraph.data.getRawDataItem(node.dataIndex) as SankeyNodeItemOption;
return item.depth != null && item.depth >= 0;
}View on GitHub (pinned to 30076aedcd)
Solutions
- Break the cycle in your edge data (remove the back-edge that closes the loop)
- If the feedback is semantically real, choose a chart type that supports cyclic graphs
- Direction-correct edges (consistent source->target) so the graph is acyclic
Example fix
// before
links: [
{ source: 'A', target: 'B', value: 5 },
{ source: 'B', target: 'C', value: 3 },
{ source: 'C', target: 'A', value: 2 } // closes a cycle
]
// after
links: [
{ source: 'A', target: 'B', value: 5 },
{ source: 'B', target: 'C', value: 3 }
] Defensive patterns
Strategy: try-catch
Validate before calling
function hasCycle(nodes: any[], links: any[]): boolean {
const adj = new Map<string, string[]>();
nodes.forEach(n => adj.set(n.id ?? n.name, []));
links.forEach(l => adj.get(l.source)?.push(l.target));
const WHITE = 0, GRAY = 1, BLACK = 2;
const color = new Map<string, number>();
nodes.forEach(n => color.set(n.id ?? n.name, WHITE));
let cycle = false;
const visit = (u: string) => {
if (cycle) return;
color.set(u, GRAY);
(adj.get(u) || []).forEach(v => {
if (color.get(v) === GRAY) cycle = true;
else if (color.get(v) === WHITE) visit(v);
});
color.set(u, BLACK);
};
nodes.forEach(n => { if (color.get(n.id ?? n.name) === WHITE) visit(n.id ?? n.name); });
return cycle;
}
if (hasCycle(nodes, links)) throw new Error('graph has a cycle; sankey needs a DAG'); Try / catch
try {
chart.setOption({ series: [{ type: 'sankey', data: nodes, links }] });
} catch (e) {
if (/DAG.*cycle/.test((e as Error).message)) {
reportCycleToUser(nodes, links);
} else {
throw e;
}
} Prevention
- Run a cycle-detection pass on graph data before setOption
- Validate that edges produce a directed acyclic graph
- Treat any back-edge as a data error and surface it to the user
When it happens
Trigger: Sankey edges/links form a directed cycle (A->B->C->A), contain self-loops, or contain mutually reachable nodes through the directed edges.
Common situations: Auto-generated graph data with feedback loops; merging node/edge tables that introduce back-edges; edge source/target swapped inconsistently.
Related errors
- Invalid geoJson format {}
- BMap api is not loaded
- Only one bmap component can exist
- levels[i].depth is mandatory and should be natural number
- Data view format error {}
AI-assisted analysis of apache/echarts@30076aedcd (2026-08-12).
Data as JSON: /api/errors/9953e433ce7d476d.
Report an issue: GitHub.