sankeyLayout.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. var layout = require("../../util/layout");
  20. var zrUtil = require("zrender/lib/core/util");
  21. var _model = require("../../util/model");
  22. var groupData = _model.groupData;
  23. /*
  24. * Licensed to the Apache Software Foundation (ASF) under one
  25. * or more contributor license agreements. See the NOTICE file
  26. * distributed with this work for additional information
  27. * regarding copyright ownership. The ASF licenses this file
  28. * to you under the Apache License, Version 2.0 (the
  29. * "License"); you may not use this file except in compliance
  30. * with the License. You may obtain a copy of the License at
  31. *
  32. * http://www.apache.org/licenses/LICENSE-2.0
  33. *
  34. * Unless required by applicable law or agreed to in writing,
  35. * software distributed under the License is distributed on an
  36. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  37. * KIND, either express or implied. See the License for the
  38. * specific language governing permissions and limitations
  39. * under the License.
  40. */
  41. function _default(ecModel, api, payload) {
  42. ecModel.eachSeriesByType('sankey', function (seriesModel) {
  43. var nodeWidth = seriesModel.get('nodeWidth');
  44. var nodeGap = seriesModel.get('nodeGap');
  45. var layoutInfo = getViewRect(seriesModel, api);
  46. seriesModel.layoutInfo = layoutInfo;
  47. var width = layoutInfo.width;
  48. var height = layoutInfo.height;
  49. var graph = seriesModel.getGraph();
  50. var nodes = graph.nodes;
  51. var edges = graph.edges;
  52. computeNodeValues(nodes);
  53. var filteredNodes = zrUtil.filter(nodes, function (node) {
  54. return node.getLayout().value === 0;
  55. });
  56. var iterations = filteredNodes.length !== 0 ? 0 : seriesModel.get('layoutIterations');
  57. var orient = seriesModel.get('orient');
  58. var nodeAlign = seriesModel.get('nodeAlign');
  59. layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations, orient, nodeAlign);
  60. });
  61. }
  62. /**
  63. * Get the layout position of the whole view
  64. *
  65. * @param {module:echarts/model/Series} seriesModel the model object of sankey series
  66. * @param {module:echarts/ExtensionAPI} api provide the API list that the developer can call
  67. * @return {module:zrender/core/BoundingRect} size of rect to draw the sankey view
  68. */
  69. function getViewRect(seriesModel, api) {
  70. return layout.getLayoutRect(seriesModel.getBoxLayoutParams(), {
  71. width: api.getWidth(),
  72. height: api.getHeight()
  73. });
  74. }
  75. function layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations, orient, nodeAlign) {
  76. computeNodeBreadths(nodes, edges, nodeWidth, width, height, orient, nodeAlign);
  77. computeNodeDepths(nodes, edges, height, width, nodeGap, iterations, orient);
  78. computeEdgeDepths(nodes, orient);
  79. }
  80. /**
  81. * Compute the value of each node by summing the associated edge's value
  82. *
  83. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  84. */
  85. function computeNodeValues(nodes) {
  86. zrUtil.each(nodes, function (node) {
  87. var value1 = sum(node.outEdges, getEdgeValue);
  88. var value2 = sum(node.inEdges, getEdgeValue);
  89. var nodeRawValue = node.getValue() || 0;
  90. var value = Math.max(value1, value2, nodeRawValue);
  91. node.setLayout({
  92. value: value
  93. }, true);
  94. });
  95. }
  96. /**
  97. * Compute the x-position for each node.
  98. *
  99. * Here we use Kahn algorithm to detect cycle when we traverse
  100. * the node to computer the initial x position.
  101. *
  102. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  103. * @param {number} nodeWidth the dx of the node
  104. * @param {number} width the whole width of the area to draw the view
  105. */
  106. function computeNodeBreadths(nodes, edges, nodeWidth, width, height, orient, nodeAlign) {
  107. // Used to mark whether the edge is deleted. if it is deleted,
  108. // the value is 0, otherwise it is 1.
  109. var remainEdges = []; // Storage each node's indegree.
  110. var indegreeArr = []; //Used to storage the node with indegree is equal to 0.
  111. var zeroIndegrees = [];
  112. var nextTargetNode = [];
  113. var x = 0;
  114. var kx = 0;
  115. for (var i = 0; i < edges.length; i++) {
  116. remainEdges[i] = 1;
  117. }
  118. for (i = 0; i < nodes.length; i++) {
  119. indegreeArr[i] = nodes[i].inEdges.length;
  120. if (indegreeArr[i] === 0) {
  121. zeroIndegrees.push(nodes[i]);
  122. }
  123. }
  124. var maxNodeDepth = -1; // Traversing nodes using topological sorting to calculate the
  125. // horizontal(if orient === 'horizontal') or vertical(if orient === 'vertical')
  126. // position of the nodes.
  127. while (zeroIndegrees.length) {
  128. for (var idx = 0; idx < zeroIndegrees.length; idx++) {
  129. var node = zeroIndegrees[idx];
  130. var item = node.hostGraph.data.getRawDataItem(node.dataIndex);
  131. var isItemDepth = item.depth != null && item.depth >= 0;
  132. if (isItemDepth && item.depth > maxNodeDepth) {
  133. maxNodeDepth = item.depth;
  134. }
  135. node.setLayout({
  136. depth: isItemDepth ? item.depth : x
  137. }, true);
  138. orient === 'vertical' ? node.setLayout({
  139. dy: nodeWidth
  140. }, true) : node.setLayout({
  141. dx: nodeWidth
  142. }, true);
  143. for (var edgeIdx = 0; edgeIdx < node.outEdges.length; edgeIdx++) {
  144. var edge = node.outEdges[edgeIdx];
  145. var indexEdge = edges.indexOf(edge);
  146. remainEdges[indexEdge] = 0;
  147. var targetNode = edge.node2;
  148. var nodeIndex = nodes.indexOf(targetNode);
  149. if (--indegreeArr[nodeIndex] === 0 && nextTargetNode.indexOf(targetNode) < 0) {
  150. nextTargetNode.push(targetNode);
  151. }
  152. }
  153. }
  154. ++x;
  155. zeroIndegrees = nextTargetNode;
  156. nextTargetNode = [];
  157. }
  158. for (i = 0; i < remainEdges.length; i++) {
  159. if (remainEdges[i] === 1) {
  160. throw new Error('Sankey is a DAG, the original data has cycle!');
  161. }
  162. }
  163. var maxDepth = maxNodeDepth > x - 1 ? maxNodeDepth : x - 1;
  164. if (nodeAlign && nodeAlign !== 'left') {
  165. adjustNodeWithNodeAlign(nodes, nodeAlign, orient, maxDepth);
  166. }
  167. var kx = orient === 'vertical' ? (height - nodeWidth) / maxDepth : (width - nodeWidth) / maxDepth;
  168. scaleNodeBreadths(nodes, kx, orient);
  169. }
  170. function isNodeDepth(node) {
  171. var item = node.hostGraph.data.getRawDataItem(node.dataIndex);
  172. return item.depth != null && item.depth >= 0;
  173. }
  174. function adjustNodeWithNodeAlign(nodes, nodeAlign, orient, maxDepth) {
  175. if (nodeAlign === 'right') {
  176. var nextSourceNode = [];
  177. var remainNodes = nodes;
  178. var nodeHeight = 0;
  179. while (remainNodes.length) {
  180. for (var i = 0; i < remainNodes.length; i++) {
  181. var node = remainNodes[i];
  182. node.setLayout({
  183. skNodeHeight: nodeHeight
  184. }, true);
  185. for (var j = 0; j < node.inEdges.length; j++) {
  186. var edge = node.inEdges[j];
  187. if (nextSourceNode.indexOf(edge.node1) < 0) {
  188. nextSourceNode.push(edge.node1);
  189. }
  190. }
  191. }
  192. remainNodes = nextSourceNode;
  193. nextSourceNode = [];
  194. ++nodeHeight;
  195. }
  196. zrUtil.each(nodes, function (node) {
  197. if (!isNodeDepth(node)) {
  198. node.setLayout({
  199. depth: Math.max(0, maxDepth - node.getLayout().skNodeHeight)
  200. }, true);
  201. }
  202. });
  203. } else if (nodeAlign === 'justify') {
  204. moveSinksRight(nodes, maxDepth);
  205. }
  206. }
  207. /**
  208. * All the node without outEgdes are assigned maximum x-position and
  209. * be aligned in the last column.
  210. *
  211. * @param {module:echarts/data/Graph~Node} nodes. node of sankey view.
  212. * @param {number} maxDepth. use to assign to node without outEdges as x-position.
  213. */
  214. function moveSinksRight(nodes, maxDepth) {
  215. zrUtil.each(nodes, function (node) {
  216. if (!isNodeDepth(node) && !node.outEdges.length) {
  217. node.setLayout({
  218. depth: maxDepth
  219. }, true);
  220. }
  221. });
  222. }
  223. /**
  224. * Scale node x-position to the width
  225. *
  226. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  227. * @param {number} kx multiple used to scale nodes
  228. */
  229. function scaleNodeBreadths(nodes, kx, orient) {
  230. zrUtil.each(nodes, function (node) {
  231. var nodeDepth = node.getLayout().depth * kx;
  232. orient === 'vertical' ? node.setLayout({
  233. y: nodeDepth
  234. }, true) : node.setLayout({
  235. x: nodeDepth
  236. }, true);
  237. });
  238. }
  239. /**
  240. * Using Gauss-Seidel iterations method to compute the node depth(y-position)
  241. *
  242. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  243. * @param {module:echarts/data/Graph~Edge} edges edge of sankey view
  244. * @param {number} height the whole height of the area to draw the view
  245. * @param {number} nodeGap the vertical distance between two nodes
  246. * in the same column.
  247. * @param {number} iterations the number of iterations for the algorithm
  248. */
  249. function computeNodeDepths(nodes, edges, height, width, nodeGap, iterations, orient) {
  250. var nodesByBreadth = prepareNodesByBreadth(nodes, orient);
  251. initializeNodeDepth(nodesByBreadth, edges, height, width, nodeGap, orient);
  252. resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
  253. for (var alpha = 1; iterations > 0; iterations--) {
  254. // 0.99 is a experience parameter, ensure that each iterations of
  255. // changes as small as possible.
  256. alpha *= 0.99;
  257. relaxRightToLeft(nodesByBreadth, alpha, orient);
  258. resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
  259. relaxLeftToRight(nodesByBreadth, alpha, orient);
  260. resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
  261. }
  262. }
  263. function prepareNodesByBreadth(nodes, orient) {
  264. var nodesByBreadth = [];
  265. var keyAttr = orient === 'vertical' ? 'y' : 'x';
  266. var groupResult = groupData(nodes, function (node) {
  267. return node.getLayout()[keyAttr];
  268. });
  269. groupResult.keys.sort(function (a, b) {
  270. return a - b;
  271. });
  272. zrUtil.each(groupResult.keys, function (key) {
  273. nodesByBreadth.push(groupResult.buckets.get(key));
  274. });
  275. return nodesByBreadth;
  276. }
  277. /**
  278. * Compute the original y-position for each node
  279. *
  280. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  281. * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
  282. * group by the array of all sankey nodes based on the nodes x-position.
  283. * @param {module:echarts/data/Graph~Edge} edges edge of sankey view
  284. * @param {number} height the whole height of the area to draw the view
  285. * @param {number} nodeGap the vertical distance between two nodes
  286. */
  287. function initializeNodeDepth(nodesByBreadth, edges, height, width, nodeGap, orient) {
  288. var minKy = Infinity;
  289. zrUtil.each(nodesByBreadth, function (nodes) {
  290. var n = nodes.length;
  291. var sum = 0;
  292. zrUtil.each(nodes, function (node) {
  293. sum += node.getLayout().value;
  294. });
  295. var ky = orient === 'vertical' ? (width - (n - 1) * nodeGap) / sum : (height - (n - 1) * nodeGap) / sum;
  296. if (ky < minKy) {
  297. minKy = ky;
  298. }
  299. });
  300. zrUtil.each(nodesByBreadth, function (nodes) {
  301. zrUtil.each(nodes, function (node, i) {
  302. var nodeDy = node.getLayout().value * minKy;
  303. if (orient === 'vertical') {
  304. node.setLayout({
  305. x: i
  306. }, true);
  307. node.setLayout({
  308. dx: nodeDy
  309. }, true);
  310. } else {
  311. node.setLayout({
  312. y: i
  313. }, true);
  314. node.setLayout({
  315. dy: nodeDy
  316. }, true);
  317. }
  318. });
  319. });
  320. zrUtil.each(edges, function (edge) {
  321. var edgeDy = +edge.getValue() * minKy;
  322. edge.setLayout({
  323. dy: edgeDy
  324. }, true);
  325. });
  326. }
  327. /**
  328. * Resolve the collision of initialized depth (y-position)
  329. *
  330. * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
  331. * group by the array of all sankey nodes based on the nodes x-position.
  332. * @param {number} nodeGap the vertical distance between two nodes
  333. * @param {number} height the whole height of the area to draw the view
  334. */
  335. function resolveCollisions(nodesByBreadth, nodeGap, height, width, orient) {
  336. var keyAttr = orient === 'vertical' ? 'x' : 'y';
  337. zrUtil.each(nodesByBreadth, function (nodes) {
  338. nodes.sort(function (a, b) {
  339. return a.getLayout()[keyAttr] - b.getLayout()[keyAttr];
  340. });
  341. var nodeX;
  342. var node;
  343. var dy;
  344. var y0 = 0;
  345. var n = nodes.length;
  346. var nodeDyAttr = orient === 'vertical' ? 'dx' : 'dy';
  347. for (var i = 0; i < n; i++) {
  348. node = nodes[i];
  349. dy = y0 - node.getLayout()[keyAttr];
  350. if (dy > 0) {
  351. nodeX = node.getLayout()[keyAttr] + dy;
  352. orient === 'vertical' ? node.setLayout({
  353. x: nodeX
  354. }, true) : node.setLayout({
  355. y: nodeX
  356. }, true);
  357. }
  358. y0 = node.getLayout()[keyAttr] + node.getLayout()[nodeDyAttr] + nodeGap;
  359. }
  360. var viewWidth = orient === 'vertical' ? width : height; // If the bottommost node goes outside the bounds, push it back up
  361. dy = y0 - nodeGap - viewWidth;
  362. if (dy > 0) {
  363. nodeX = node.getLayout()[keyAttr] - dy;
  364. orient === 'vertical' ? node.setLayout({
  365. x: nodeX
  366. }, true) : node.setLayout({
  367. y: nodeX
  368. }, true);
  369. y0 = nodeX;
  370. for (i = n - 2; i >= 0; --i) {
  371. node = nodes[i];
  372. dy = node.getLayout()[keyAttr] + node.getLayout()[nodeDyAttr] + nodeGap - y0;
  373. if (dy > 0) {
  374. nodeX = node.getLayout()[keyAttr] - dy;
  375. orient === 'vertical' ? node.setLayout({
  376. x: nodeX
  377. }, true) : node.setLayout({
  378. y: nodeX
  379. }, true);
  380. }
  381. y0 = node.getLayout()[keyAttr];
  382. }
  383. }
  384. });
  385. }
  386. /**
  387. * Change the y-position of the nodes, except most the right side nodes
  388. *
  389. * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
  390. * group by the array of all sankey nodes based on the node x-position.
  391. * @param {number} alpha parameter used to adjust the nodes y-position
  392. */
  393. function relaxRightToLeft(nodesByBreadth, alpha, orient) {
  394. zrUtil.each(nodesByBreadth.slice().reverse(), function (nodes) {
  395. zrUtil.each(nodes, function (node) {
  396. if (node.outEdges.length) {
  397. var y = sum(node.outEdges, weightedTarget, orient) / sum(node.outEdges, getEdgeValue, orient);
  398. if (isNaN(y)) {
  399. var len = node.outEdges.length;
  400. y = len ? sum(node.outEdges, centerTarget, orient) / len : 0;
  401. }
  402. if (orient === 'vertical') {
  403. var nodeX = node.getLayout().x + (y - center(node, orient)) * alpha;
  404. node.setLayout({
  405. x: nodeX
  406. }, true);
  407. } else {
  408. var nodeY = node.getLayout().y + (y - center(node, orient)) * alpha;
  409. node.setLayout({
  410. y: nodeY
  411. }, true);
  412. }
  413. }
  414. });
  415. });
  416. }
  417. function weightedTarget(edge, orient) {
  418. return center(edge.node2, orient) * edge.getValue();
  419. }
  420. function centerTarget(edge, orient) {
  421. return center(edge.node2, orient);
  422. }
  423. function weightedSource(edge, orient) {
  424. return center(edge.node1, orient) * edge.getValue();
  425. }
  426. function centerSource(edge, orient) {
  427. return center(edge.node1, orient);
  428. }
  429. function center(node, orient) {
  430. return orient === 'vertical' ? node.getLayout().x + node.getLayout().dx / 2 : node.getLayout().y + node.getLayout().dy / 2;
  431. }
  432. function getEdgeValue(edge) {
  433. return edge.getValue();
  434. }
  435. function sum(array, cb, orient) {
  436. var sum = 0;
  437. var len = array.length;
  438. var i = -1;
  439. while (++i < len) {
  440. var value = +cb.call(array, array[i], orient);
  441. if (!isNaN(value)) {
  442. sum += value;
  443. }
  444. }
  445. return sum;
  446. }
  447. /**
  448. * Change the y-position of the nodes, except most the left side nodes
  449. *
  450. * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
  451. * group by the array of all sankey nodes based on the node x-position.
  452. * @param {number} alpha parameter used to adjust the nodes y-position
  453. */
  454. function relaxLeftToRight(nodesByBreadth, alpha, orient) {
  455. zrUtil.each(nodesByBreadth, function (nodes) {
  456. zrUtil.each(nodes, function (node) {
  457. if (node.inEdges.length) {
  458. var y = sum(node.inEdges, weightedSource, orient) / sum(node.inEdges, getEdgeValue, orient);
  459. if (isNaN(y)) {
  460. var len = node.inEdges.length;
  461. y = len ? sum(node.inEdges, centerSource, orient) / len : 0;
  462. }
  463. if (orient === 'vertical') {
  464. var nodeX = node.getLayout().x + (y - center(node, orient)) * alpha;
  465. node.setLayout({
  466. x: nodeX
  467. }, true);
  468. } else {
  469. var nodeY = node.getLayout().y + (y - center(node, orient)) * alpha;
  470. node.setLayout({
  471. y: nodeY
  472. }, true);
  473. }
  474. }
  475. });
  476. });
  477. }
  478. /**
  479. * Compute the depth(y-position) of each edge
  480. *
  481. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  482. */
  483. function computeEdgeDepths(nodes, orient) {
  484. var keyAttr = orient === 'vertical' ? 'x' : 'y';
  485. zrUtil.each(nodes, function (node) {
  486. node.outEdges.sort(function (a, b) {
  487. return a.node2.getLayout()[keyAttr] - b.node2.getLayout()[keyAttr];
  488. });
  489. node.inEdges.sort(function (a, b) {
  490. return a.node1.getLayout()[keyAttr] - b.node1.getLayout()[keyAttr];
  491. });
  492. });
  493. zrUtil.each(nodes, function (node) {
  494. var sy = 0;
  495. var ty = 0;
  496. zrUtil.each(node.outEdges, function (edge) {
  497. edge.setLayout({
  498. sy: sy
  499. }, true);
  500. sy += edge.getLayout().dy;
  501. });
  502. zrUtil.each(node.inEdges, function (edge) {
  503. edge.setLayout({
  504. ty: ty
  505. }, true);
  506. ty += edge.getLayout().dy;
  507. });
  508. });
  509. }
  510. module.exports = _default;