'use strict'; var tarjan = require('strongly-connected-components'); var Lib = require('../../lib'); var wrap = require('../../lib/gup').wrap; var isArrayOrTypedArray = Lib.isArrayOrTypedArray; var isIndex = Lib.isIndex; var Colorscale = require('../../components/colorscale'); function convertToD3Sankey(trace) { var nodeSpec = trace.node; var linkSpec = trace.link; var links = []; var hasLinkColorArray = isArrayOrTypedArray(linkSpec.color); var hasLinkHoverColorArray = isArrayOrTypedArray(linkSpec.hovercolor); var hasLinkCustomdataArray = isArrayOrTypedArray(linkSpec.customdata); var linkedNodes = {}; var components = {}; var componentCount = linkSpec.colorscales.length; var i; for(i = 0; i < componentCount; i++) { var cscale = linkSpec.colorscales[i]; var specs = Colorscale.extractScale(cscale, {cLetter: 'c'}); var scale = Colorscale.makeColorScaleFunc(specs); components[cscale.label] = scale; } var maxNodeId = 0; for(i = 0; i < linkSpec.value.length; i++) { if(linkSpec.source[i] > maxNodeId) maxNodeId = linkSpec.source[i]; if(linkSpec.target[i] > maxNodeId) maxNodeId = linkSpec.target[i]; } var nodeCount = maxNodeId + 1; trace.node._count = nodeCount; // Group nodes var j; var groups = trace.node.groups; var groupLookup = {}; for(i = 0; i < groups.length; i++) { var group = groups[i]; // Build a lookup table to quickly find in which group a node is for(j = 0; j < group.length; j++) { var nodeIndex = group[j]; var groupIndex = nodeCount + i; if(groupLookup.hasOwnProperty(nodeIndex)) { Lib.warn('Node ' + nodeIndex + ' is already part of a group.'); } else { groupLookup[nodeIndex] = groupIndex; } } } // Process links var groupedLinks = { source: [], target: [] }; for(i = 0; i < linkSpec.value.length; i++) { var val = linkSpec.value[i]; // remove negative values, but keep zeros with special treatment var source = linkSpec.source[i]; var target = linkSpec.target[i]; if(!(val > 0 && isIndex(source, nodeCount) && isIndex(target, nodeCount))) { continue; } // Remove links that are within the same group if(groupLookup.hasOwnProperty(source) && groupLookup.hasOwnProperty(target) && groupLookup[source] === groupLookup[target]) { continue; } // if link targets a node in the group, relink target to that group if(groupLookup.hasOwnProperty(target)) { target = groupLookup[target]; } // if link originates from a node in a group, relink source to that group if(groupLookup.hasOwnProperty(source)) { source = groupLookup[source]; } source = +source; target = +target; linkedNodes[source] = linkedNodes[target] = true; var label = ''; if(linkSpec.label && linkSpec.label[i]) label = linkSpec.label[i]; var concentrationscale = null; if(label && components.hasOwnProperty(label)) concentrationscale = components[label]; links.push({ pointNumber: i, label: label, color: hasLinkColorArray ? linkSpec.color[i] : linkSpec.color, hovercolor: hasLinkHoverColorArray ? linkSpec.hovercolor[i] : linkSpec.hovercolor, customdata: hasLinkCustomdataArray ? linkSpec.customdata[i] : linkSpec.customdata, concentrationscale: concentrationscale, source: source, target: target, value: +val }); groupedLinks.source.push(source); groupedLinks.target.push(target); } // Process nodes var totalCount = nodeCount + groups.length; var hasNodeColorArray = isArrayOrTypedArray(nodeSpec.color); var hasNodeCustomdataArray = isArrayOrTypedArray(nodeSpec.customdata); var nodes = []; for(i = 0; i < totalCount; i++) { if(!linkedNodes[i]) continue; var l = nodeSpec.label[i]; nodes.push({ group: (i > nodeCount - 1), childrenNodes: [], pointNumber: i, label: l, color: hasNodeColorArray ? nodeSpec.color[i] : nodeSpec.color, customdata: hasNodeCustomdataArray ? nodeSpec.customdata[i] : nodeSpec.customdata }); } // Check if we have circularity on the resulting graph var circular = false; if(circularityPresent(totalCount, groupedLinks.source, groupedLinks.target)) { circular = true; } return { circular: circular, links: links, nodes: nodes, // Data structure for groups groups: groups, groupLookup: groupLookup }; } function circularityPresent(nodeLen, sources, targets) { var nodes = Lib.init2dArray(nodeLen, 0); for(var i = 0; i < Math.min(sources.length, targets.length); i++) { if(Lib.isIndex(sources[i], nodeLen) && Lib.isIndex(targets[i], nodeLen)) { if(sources[i] === targets[i]) { return true; // self-link which is also a scc of one } nodes[sources[i]].push(targets[i]); } } var scc = tarjan(nodes); // Tarján's strongly connected components algorithm coded by Mikola Lysenko // returns at least one non-singular component if there's circularity in the graph return scc.components.some(function(c) { return c.length > 1; }); } module.exports = function calc(gd, trace) { var result = convertToD3Sankey(trace); return wrap({ circular: result.circular, _nodes: result.nodes, _links: result.links, // Data structure for grouping _groups: result.groups, _groupLookup: result.groupLookup, }); };