{"record":{"id":"9953e433ce7d476d","repo":"apache/echarts","slug":"sankey-is-a-dag-the-original-data-has-cycle","errorCode":null,"errorMessage":"Sankey is a DAG, the original data has cycle!","messagePattern":"Sankey is a DAG, the original data has cycle!","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/chart/sankey/sankeyLayout.ts","lineNumber":170,"sourceCode":"            for (let edgeIdx = 0; edgeIdx < node.outEdges.length; edgeIdx++) {\n                const edge = node.outEdges[edgeIdx];\n                const indexEdge = zrUtil.indexOf(edges, edge);\n                remainEdges[indexEdge] = 0;\n                const targetNode = edge.node2;\n                const nodeIndex = zrUtil.indexOf(nodes, targetNode);\n                if (--indegreeArr[nodeIndex] === 0 && zrUtil.indexOf(nextTargetNode, targetNode) < 0) {\n                    nextTargetNode.push(targetNode);\n                }\n            }\n        }\n        ++x;\n        zeroIndegrees = nextTargetNode;\n        nextTargetNode = [];\n    }\n\n    for (let i = 0; i < remainEdges.length; i++) {\n        if (remainEdges[i] === 1) {\n            throw new Error('Sankey is a DAG, the original data has cycle!');\n        }\n    }\n\n    const maxDepth = maxNodeDepth > x - 1 ? maxNodeDepth : x - 1;\n    if (nodeAlign && nodeAlign !== 'left') {\n        adjustNodeWithNodeAlign(nodes, nodeAlign, orient, maxDepth);\n    }\n    const kx = orient === 'vertical'\n        ? (height - nodeWidth) / maxDepth\n        : (width - nodeWidth) / maxDepth;\n\n    scaleNodeBreadths(nodes, kx, orient);\n}\n\nfunction isNodeDepth(node: GraphNode) {\n    const item = node.hostGraph.data.getRawDataItem(node.dataIndex) as SankeyNodeItemOption;\n    return item.depth != null && item.depth >= 0;\n}","sourceCodeStart":152,"sourceCodeEnd":188,"githubUrl":"https://github.com/apache/echarts/blob/30076aedcd7b7f65d8dd8e8d9ece46ce778133a3/src/chart/sankey/sankeyLayout.ts#L152-L188","documentation":"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.","triggerScenarios":"Sankey edges/links form a directed cycle (A->B->C->A), contain self-loops, or contain mutually reachable nodes through the directed edges.","commonSituations":"Auto-generated graph data with feedback loops; merging node/edge tables that introduce back-edges; edge source/target swapped inconsistently.","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"],"exampleFix":"// before\nlinks: [\n  { source: 'A', target: 'B', value: 5 },\n  { source: 'B', target: 'C', value: 3 },\n  { source: 'C', target: 'A', value: 2 } // closes a cycle\n]\n\n// after\nlinks: [\n  { source: 'A', target: 'B', value: 5 },\n  { source: 'B', target: 'C', value: 3 }\n]","handlingStrategy":"try-catch","validationCode":"function hasCycle(nodes: any[], links: any[]): boolean {\n  const adj = new Map<string, string[]>();\n  nodes.forEach(n => adj.set(n.id ?? n.name, []));\n  links.forEach(l => adj.get(l.source)?.push(l.target));\n  const WHITE = 0, GRAY = 1, BLACK = 2;\n  const color = new Map<string, number>();\n  nodes.forEach(n => color.set(n.id ?? n.name, WHITE));\n  let cycle = false;\n  const visit = (u: string) => {\n    if (cycle) return;\n    color.set(u, GRAY);\n    (adj.get(u) || []).forEach(v => {\n      if (color.get(v) === GRAY) cycle = true;\n      else if (color.get(v) === WHITE) visit(v);\n    });\n    color.set(u, BLACK);\n  };\n  nodes.forEach(n => { if (color.get(n.id ?? n.name) === WHITE) visit(n.id ?? n.name); });\n  return cycle;\n}\nif (hasCycle(nodes, links)) throw new Error('graph has a cycle; sankey needs a DAG');","typeGuard":null,"tryCatchPattern":"try {\n  chart.setOption({ series: [{ type: 'sankey', data: nodes, links }] });\n} catch (e) {\n  if (/DAG.*cycle/.test((e as Error).message)) {\n    reportCycleToUser(nodes, links);\n  } else {\n    throw e;\n  }\n}","preventionTips":["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"],"tags":["sankey","dag","cycle","data-validation","runtime"],"backgroundTag":null,"analyzedSha":"30076aedcd7b7f65d8dd8e8d9ece46ce778133a3","analyzedAt":"2026-08-12T12:42:28.134Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}