sankey.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. d3.sankey = function() {
  2. var sankey = {},
  3. nodeWidth = 24,
  4. nodePadding = 8,
  5. size = [1, 1],
  6. nodes = [],
  7. links = [],
  8. components = [];
  9. sankey.nodeWidth = function(_) {
  10. if (!arguments.length) return nodeWidth;
  11. nodeWidth = +_;
  12. return sankey;
  13. };
  14. sankey.nodePadding = function(_) {
  15. if (!arguments.length) return nodePadding;
  16. nodePadding = +_;
  17. return sankey;
  18. };
  19. sankey.nodes = function(_) {
  20. if (!arguments.length) return nodes;
  21. nodes = _;
  22. return sankey;
  23. };
  24. sankey.links = function(_) {
  25. if (!arguments.length) return links;
  26. links = _;
  27. return sankey;
  28. };
  29. sankey.size = function(_) {
  30. if (!arguments.length) return size;
  31. size = _;
  32. return sankey;
  33. };
  34. sankey.layout = function(iterations) {
  35. computeNodeLinks();
  36. computeNodeValues();
  37. computeNodeStructure();
  38. computeNodeBreadths();
  39. computeNodeDepths(iterations);
  40. computeLinkDepths();
  41. return sankey;
  42. };
  43. sankey.relayout = function() {
  44. computeLinkDepths();
  45. return sankey;
  46. };
  47. sankey.reversibleLink = function() {
  48. var curvature = .5;
  49. // Used when source is behind target, the first and last paths are simple
  50. // lines at the start and end node while the second path is the spline
  51. function forwardLink(part, d) {
  52. var x0 = d.source.x + d.source.dx,
  53. x1 = d.target.x,
  54. xi = d3.interpolateNumber(x0, x1),
  55. x2 = xi(curvature),
  56. x3 = xi(1 - curvature),
  57. y0 = d.source.y + d.sy,
  58. y1 = d.target.y + d.ty,
  59. y2 = d.source.y + d.sy + d.dy,
  60. y3 = d.target.y + d.ty + d.dy;
  61. switch (part) {
  62. case 0:
  63. return "M" + x0 + "," + y0 + "L" + x0 + "," + (y0 + d.dy);
  64. case 1:
  65. return "M" + x0 + "," + y0
  66. + "C" + x2 + "," + y0 + " " + x3 + "," + y1 + " " + x1 + "," + y1
  67. + "L" + x1 + "," + y3
  68. + "C" + x3 + "," + y3 + " " + x2 + "," + y2 + " " + x0 + "," + y2
  69. + "Z";
  70. case 2:
  71. return "M" + x1 + "," + y1 + "L" + x1 + "," + (y1 + d.dy);
  72. }
  73. }
  74. // Used for self loops and when the source is actually in front of the
  75. // target; the first element is a turning path from the source to the
  76. // destination, the second element connects the two twists and the last
  77. // twists into the target element.
  78. //
  79. //
  80. // /--Target
  81. // \----------------------\
  82. // Source--/
  83. //
  84. function backwardLink(part, d) {
  85. var curveExtension = 30;
  86. var curveDepth = 15;
  87. function getDir(d) {
  88. return d.source.y + d.sy > d.target.y + d.ty ? -1 : 1;
  89. }
  90. function p(x, y) {
  91. return x + "," + y + " ";
  92. }
  93. var dt = getDir(d) * curveDepth,
  94. x0 = d.source.x + d.source.dx,
  95. y0 = d.source.y + d.sy,
  96. x1 = d.target.x,
  97. y1 = d.target.y + d.ty;
  98. switch (part) {
  99. case 0:
  100. return "M" + p(x0, y0) +
  101. "C" + p(x0, y0) +
  102. p(x0 + curveExtension, y0) +
  103. p(x0 + curveExtension, y0 + dt) +
  104. "L" + p(x0 + curveExtension, y0 + dt + d.dy) +
  105. "C" + p(x0 + curveExtension, y0 + d.dy) +
  106. p(x0, y0 + d.dy) +
  107. p(x0, y0 + d.dy) +
  108. "Z";
  109. case 1:
  110. return "M" + p(x0 + curveExtension, y0 + dt) +
  111. "C" + p(x0 + curveExtension, y0 + 3 * dt) +
  112. p(x1 - curveExtension, y1 - 3 * dt) +
  113. p(x1 - curveExtension, y1 - dt) +
  114. "L" + p(x1 - curveExtension, y1 - dt + d.dy) +
  115. "C" + p(x1 - curveExtension, y1 - 3 * dt + d.dy) +
  116. p(x0 + curveExtension, y0 + 3 * dt + d.dy) +
  117. p(x0 + curveExtension, y0 + dt + d.dy) +
  118. "Z";
  119. case 2:
  120. return "M" + p(x1 - curveExtension, y1 - dt) +
  121. "C" + p(x1 - curveExtension, y1) +
  122. p(x1, y1) +
  123. p(x1, y1) +
  124. "L" + p(x1, y1 + d.dy) +
  125. "C" + p(x1, y1 + d.dy) +
  126. p(x1 - curveExtension, y1 + d.dy) +
  127. p(x1 - curveExtension, y1 + d.dy - dt) +
  128. "Z";
  129. }
  130. }
  131. return function(part) {
  132. return function(d) {
  133. if (d.source.x < d.target.x) {
  134. return forwardLink(part, d);
  135. } else {
  136. return backwardLink(part, d);
  137. }
  138. }
  139. }
  140. };
  141. // The standard link path using a constant width spline that needs a
  142. // single path element.
  143. sankey.link = function() {
  144. var curvature = .5;
  145. function link(d) {
  146. var x0 = d.source.x + d.source.dx,
  147. x1 = d.target.x,
  148. xi = d3.interpolateNumber(x0, x1),
  149. x2 = xi(curvature),
  150. x3 = xi(1 - curvature),
  151. y0 = d.source.y + d.sy + d.dy / 2,
  152. y1 = d.target.y + d.ty + d.dy / 2;
  153. return "M" + x0 + "," + y0
  154. + "C" + x2 + "," + y0
  155. + " " + x3 + "," + y1
  156. + " " + x1 + "," + y1;
  157. }
  158. link.curvature = function(_) {
  159. if (!arguments.length) return curvature;
  160. curvature = +_;
  161. return link;
  162. };
  163. return link;
  164. };
  165. // Populate the sourceLinks and targetLinks for each node.
  166. // Also, if the source and target are not objects, assume they are indices.
  167. function computeNodeLinks() {
  168. nodes.forEach(function(node) {
  169. node.sourceLinks = [];
  170. node.targetLinks = [];
  171. });
  172. links.forEach(function(link) {
  173. var source = link.source,
  174. target = link.target;
  175. source = (typeof source === "number") ? nodes[source] : nodes.find(node => node.name === source);
  176. target = (typeof target === "number") ? nodes[target] : nodes.find(node => node.name === target);
  177. link.source = source
  178. link.target = target
  179. source.sourceLinks.push(link);
  180. target.targetLinks.push(link);
  181. });
  182. }
  183. // Compute the value (size) of each node by summing the associated links.
  184. function computeNodeValues() {
  185. nodes.forEach(function(node) {
  186. if (!(node.value)) //if not already given
  187. node.value = Math.max(
  188. d3.sum(node.sourceLinks, value),
  189. d3.sum(node.targetLinks, value)
  190. );
  191. });
  192. }
  193. // Take the list of nodes and create a DAG of supervertices, each consisting
  194. // of a strongly connected component of the graph
  195. //
  196. // Based off:
  197. // http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
  198. function computeNodeStructure() {
  199. var nodeStack = [],
  200. index = 0;
  201. nodes.forEach(function(node) {
  202. if (!node.index) {
  203. connect(node);
  204. }
  205. });
  206. function connect(node) {
  207. node.index = index++;
  208. node.lowIndex = node.index;
  209. node.onStack = true;
  210. nodeStack.push(node);
  211. if (node.sourceLinks) {
  212. node.sourceLinks.forEach(function(sourceLink){
  213. var target = sourceLink.target;
  214. if (!target.hasOwnProperty('index')) {
  215. connect(target);
  216. node.lowIndex = Math.min(node.lowIndex, target.lowIndex);
  217. } else if (target.onStack) {
  218. node.lowIndex = Math.min(node.lowIndex, target.index);
  219. }
  220. });
  221. if (node.lowIndex === node.index) {
  222. var component = [], currentNode;
  223. do {
  224. currentNode = nodeStack.pop()
  225. currentNode.onStack = false;
  226. component.push(currentNode);
  227. } while (currentNode != node);
  228. components.push({
  229. root: node,
  230. scc: component
  231. });
  232. }
  233. }
  234. }
  235. components.forEach(function(component, i){
  236. component.index = i;
  237. component.scc.forEach(function(node) {
  238. node.component = i;
  239. });
  240. });
  241. }
  242. // Assign the breadth (x-position) for each strongly connected component,
  243. // followed by assigning breadth within the component.
  244. function computeNodeBreadths() {
  245. layerComponents();
  246. components.forEach(function(component, i){
  247. bfs(component.root, function(node){
  248. var result = node.sourceLinks
  249. .filter(function(sourceLink){
  250. return sourceLink.target.component == i;
  251. })
  252. .map(function(sourceLink){
  253. return sourceLink.target;
  254. });
  255. return result;
  256. });
  257. });
  258. var max = 0;
  259. var componentsByBreadth = d3.nest()
  260. .key(function(d) { return d.x; })
  261. .sortKeys(d3.ascending)
  262. .entries(components)
  263. .map(function(d) { return d.values; });
  264. var max = -1, nextMax = -1;
  265. componentsByBreadth.forEach(function(c){
  266. c.forEach(function(component){
  267. component.x = max + 1;
  268. component.scc.forEach(function(node){
  269. if (node.layer) node.x=node.layer;
  270. else node.x = component.x + node.x;
  271. nextMax = Math.max(nextMax, node.x);
  272. });
  273. });
  274. max = nextMax;
  275. });
  276. nodes
  277. .filter(function(node) {
  278. var outLinks = node.sourceLinks.filter(function(link){ return link.source.name != link.target.name; });
  279. return (outLinks.length == 0);
  280. })
  281. .forEach(function(node) { node.x = (node.layer)?node.x:max; })
  282. scaleNodeBreadths((size[0] - nodeWidth) / Math.max(max, 1));
  283. function flatten(a) {
  284. return [].concat.apply([], a);
  285. }
  286. function layerComponents() {
  287. var remainingComponents = components,
  288. nextComponents,
  289. visitedIndex,
  290. x = 0;
  291. while (remainingComponents.length) {
  292. nextComponents = [];
  293. visitedIndex = {};
  294. remainingComponents.forEach(function(component) {
  295. component.x = x;
  296. component.scc.forEach(function(n) {
  297. n.sourceLinks.forEach(function(l) {
  298. if (!visitedIndex.hasOwnProperty(l.target.component) &&
  299. l.target.component != component.index) {
  300. nextComponents.push(components[l.target.component]);
  301. visitedIndex[l.target.component] = true;
  302. }
  303. })
  304. });
  305. });
  306. remainingComponents = nextComponents;
  307. ++x;
  308. }
  309. }
  310. function bfs(node, extractTargets) {
  311. var queue = [node], currentCount = 1, nextCount = 0;
  312. var x = 0;
  313. while(currentCount > 0) {
  314. var currentNode = queue.shift();
  315. currentCount--;
  316. if (!currentNode.hasOwnProperty('x')) {
  317. currentNode.x = x;
  318. currentNode.dx = nodeWidth;
  319. var targets = extractTargets(currentNode);
  320. queue = queue.concat(targets);
  321. nextCount += targets.length;
  322. }
  323. if (currentCount == 0) { // level change
  324. x++;
  325. currentCount = nextCount;
  326. nextCount = 0;
  327. }
  328. }
  329. }
  330. //extra code for fixed layout - x part
  331. if (fixedlayout.length>0) {
  332. sankey.nodes().forEach(function(d,i){
  333. d.x=fixedlayout[i][0];
  334. })
  335. }
  336. }
  337. function moveSourcesRight() {
  338. nodes.forEach(function(node) {
  339. if (!node.targetLinks.length) {
  340. node.x = d3.min(node.sourceLinks, function(d) { return d.target.x; }) - 1;
  341. }
  342. });
  343. }
  344. function moveSinksRight(x) {
  345. nodes.forEach(function(node) {
  346. if (!node.sourceLinks.length) {
  347. node.x = x - 1;
  348. }
  349. });
  350. }
  351. function scaleNodeBreadths(kx) {
  352. nodes.forEach(function(node) {
  353. node.x *= kx;
  354. });
  355. }
  356. function computeNodeDepths(iterations) {
  357. var nodesByBreadth = d3.nest()
  358. .key(function(d) { return d.x; })
  359. .sortKeys(d3.ascending)
  360. .entries(nodes)
  361. .map(function(d) { return d.values; });
  362. initializeNodeDepth();
  363. resolveCollisions();
  364. for (var alpha = 1; iterations > 0; --iterations) {
  365. relaxRightToLeft(alpha *= .99);
  366. resolveCollisions();
  367. relaxLeftToRight(alpha);
  368. resolveCollisions();
  369. }
  370. function initializeNodeDepth() {
  371. var ky = d3.min(nodesByBreadth, function(nodes) {
  372. return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value);
  373. });
  374. nodesByBreadth.forEach(function(nodes) {
  375. nodes.forEach(function(node, i) {
  376. node.y = i;
  377. node.dy = node.value * ky;
  378. });
  379. });
  380. links.forEach(function(link) {
  381. link.dy = link.value * ky;
  382. });
  383. }
  384. function relaxLeftToRight(alpha) {
  385. nodesByBreadth.forEach(function(nodes, breadth) {
  386. nodes.forEach(function(node) {
  387. if (node.targetLinks.length) {
  388. var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
  389. node.y += (y - center(node)) * alpha;
  390. }
  391. });
  392. });
  393. function weightedSource(link) {
  394. return center(link.source) * link.value;
  395. }
  396. }
  397. function relaxRightToLeft(alpha) {
  398. nodesByBreadth.slice().reverse().forEach(function(nodes) {
  399. nodes.forEach(function(node) {
  400. if (node.sourceLinks.length) {
  401. var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
  402. node.y += (y - center(node)) * alpha;
  403. }
  404. });
  405. });
  406. function weightedTarget(link) {
  407. return center(link.target) * link.value;
  408. }
  409. }
  410. function resolveCollisions() {
  411. nodesByBreadth.forEach(function(nodes) {
  412. var node,
  413. dy,
  414. y0 = 0,
  415. n = nodes.length,
  416. i;
  417. // Push any overlapping nodes down.
  418. nodes.sort(ascendingDepth);
  419. for (i = 0; i < n; ++i) {
  420. node = nodes[i];
  421. dy = y0 - node.y;
  422. if (dy > 0) node.y += dy;
  423. y0 = node.y + node.dy + nodePadding;
  424. }
  425. // If the bottommost node goes outside the bounds, push it back up.
  426. dy = y0 - nodePadding - size[1];
  427. if (dy > 0) {
  428. y0 = node.y -= dy;
  429. // Push any overlapping nodes back up.
  430. for (i = n - 2; i >= 0; --i) {
  431. node = nodes[i];
  432. dy = node.y + node.dy + nodePadding - y0;
  433. if (dy > 0) node.y -= dy;
  434. y0 = node.y;
  435. }
  436. }
  437. });
  438. }
  439. function ascendingDepth(a, b) {
  440. return a.y - b.y;
  441. }
  442. //extra code for fixed layout - y part
  443. if (fixedlayout.length>0) {
  444. sankey.nodes().forEach(function(d,i){
  445. d.y=fixedlayout[i][1];
  446. })
  447. }
  448. }
  449. function computeLinkDepths() {
  450. nodes.forEach(function(node) {
  451. if (parallelrendering) {}
  452. else node.sourceLinks.sort(ascendingTargetDepth);
  453. node.targetLinks.sort(ascendingSourceDepth);
  454. });
  455. nodes.forEach(function(node) {
  456. var sy = 0, ty = 0;
  457. node.sourceLinks.forEach(function(link) {
  458. link.sy = sy;
  459. sy += link.dy;
  460. });
  461. node.targetLinks.forEach(function(link) {
  462. link.ty = ty;
  463. ty += link.dy;
  464. });
  465. });
  466. function ascendingSourceDepth(a, b) {
  467. return a.source.y - b.source.y;
  468. }
  469. function ascendingTargetDepth(a, b) {
  470. return a.target.y - b.target.y;
  471. }
  472. }
  473. function center(node) {
  474. return node.y + node.dy / 2;
  475. }
  476. function value(link) {
  477. return link.value;
  478. }
  479. return sankey;
  480. };