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

  1. Break the cycle in your edge data (remove the back-edge that closes the loop)
  2. If the feedback is semantically real, choose a chart type that supports cyclic graphs
  3. 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

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


AI-assisted analysis of apache/echarts@30076aedcd (2026-08-12). Data as JSON: /api/errors/9953e433ce7d476d. Report an issue: GitHub.