{"record":{"id":"4603382c22226ea7","repo":"plotly/plotly.js","slug":"node-nodeindex-is-already-part-of-a-group","errorCode":null,"errorMessage":"Node <nodeIndex> is already part of a group.","messagePattern":"Node <nodeIndex> is already part of a group\\.","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/traces/sankey/calc.js","lineNumber":50,"sourceCode":"    for(i = 0; i < linkSpec.value.length; i++) {\n        if(linkSpec.source[i] > maxNodeId) maxNodeId = linkSpec.source[i];\n        if(linkSpec.target[i] > maxNodeId) maxNodeId = linkSpec.target[i];\n    }\n    var nodeCount = maxNodeId + 1;\n    trace.node._count = nodeCount;\n\n    // Group nodes\n    var j;\n    var groups = trace.node.groups;\n    var groupLookup = {};\n    for(i = 0; i < groups.length; i++) {\n        var group = groups[i];\n        // Build a lookup table to quickly find in which group a node is\n        for(j = 0; j < group.length; j++) {\n            var nodeIndex = group[j];\n            var groupIndex = nodeCount + i;\n            if(groupLookup.hasOwnProperty(nodeIndex)) {\n                Lib.warn('Node ' + nodeIndex + ' is already part of a group.');\n            } else {\n                groupLookup[nodeIndex] = groupIndex;\n            }\n        }\n    }\n\n    // Process links\n    var groupedLinks = {\n        source: [],\n        target: []\n    };\n    for(i = 0; i < linkSpec.value.length; i++) {\n        var val = linkSpec.value[i];\n        // remove negative values, but keep zeros with special treatment\n        var source = linkSpec.source[i];\n        var target = linkSpec.target[i];\n        if(!(val > 0 && isIndex(source, nodeCount) && isIndex(target, nodeCount))) {\n            continue;","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/plotly/plotly.js/blob/1d090e0b5ffb8d0fdf6e0e4ff51d3f3cf67f1314/src/traces/sankey/calc.js#L32-L68","documentation":"Plotly emits this warning during sankey preprocessing in convertToD3Sankey (src/traces/sankey/calc.js:50) when the trace's `arrangement.snap`/grouping attribute (`node.groups`) lists the same node index in more than one group. Only the first group containing the node is honored; later duplicates are ignored, so the grouping silently differs from what was specified.","triggerScenarios":"Setting layout/trace sankey `node.groups` (array of arrays of node indices) where the same node index appears in two or more sub-arrays, e.g. groups: [[0,1],[1,2]].","commonSituations":"Hand-built group arrays assembled programmatically or by merging user selections where overlap was not deduplicated; sankey configs generated by tools that assume groups are disjoint.","solutions":["Remove the duplicate node index from all but one entry of node.groups so each node belongs to at most one group.","Deduplicate programmatically before assigning: keep the first group that contains a node and drop it from the rest.","If the overlap is intentional grouping semantics, restructure the links/nodes so one group suffices."],"exampleFix":"// before\nnode: { groups: [[0, 1, 2], [2, 3, 4]] } // node 2 duplicated\n// after\nnode: { groups: [[0, 1, 2], [3, 4]] }","handlingStrategy":"validation","validationCode":"const seen = new Set();\nfor (const g of nodeGroups) {\n  for (const n of g) {\n    if (seen.has(n)) throw new Error('Node ' + n + ' appears in multiple groups');\n    seen.add(n);\n  }\n}","typeGuard":"function groupsAreDisjoint(groups) {\n  const seen = new Set();\n  return groups.every(g => g.every(n => !seen.has(n) && seen.add(n)));\n}","tryCatchPattern":null,"preventionTips":["Deduplicate node indices across groups before assigning node.groups.","Generate groups from a partition helper rather than hand-writing arrays.","Watch the browser console for plotly Lib.warn output in CI screenshot tests."],"tags":["sankey","configuration","duplicate-node","data-validation"],"backgroundTag":"duplicate-node-grouping","analyzedSha":"1d090e0b5ffb8d0fdf6e0e4ff51d3f3cf67f1314","analyzedAt":"2026-09-02T22:03:39.906Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}