sankeyLayout.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  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 value = Math.max(value1, value2);
  90. node.setLayout({
  91. value: value
  92. }, true);
  93. });
  94. }
  95. /**
  96. * Compute the x-position for each node.
  97. *
  98. * Here we use Kahn algorithm to detect cycle when we traverse
  99. * the node to computer the initial x position.
  100. *
  101. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  102. * @param {number} nodeWidth the dx of the node
  103. * @param {number} width the whole width of the area to draw the view
  104. */
  105. function computeNodeBreadths(nodes, edges, nodeWidth, width, height, orient, nodeAlign) {
  106. // Used to mark whether the edge is deleted. if it is deleted,
  107. // the value is 0, otherwise it is 1.
  108. var remainEdges = []; // Storage each node's indegree.
  109. var indegreeArr = []; //Used to storage the node with indegree is equal to 0.
  110. var zeroIndegrees = [];
  111. var nextTargetNode = [];
  112. var x = 0;
  113. var kx = 0;
  114. for (var i = 0; i < edges.length; i++) {
  115. remainEdges[i] = 1;
  116. }
  117. for (i = 0; i < nodes.length; i++) {
  118. indegreeArr[i] = nodes[i].inEdges.length;
  119. if (indegreeArr[i] === 0) {
  120. zeroIndegrees.push(nodes[i]);
  121. }
  122. }
  123. var maxNodeDepth = -1; // Traversing nodes using topological sorting to calculate the
  124. // horizontal(if orient === 'horizontal') or vertical(if orient === 'vertical')
  125. // position of the nodes.
  126. while (zeroIndegrees.length) {
  127. for (var idx = 0; idx < zeroIndegrees.length; idx++) {
  128. var node = zeroIndegrees[idx];
  129. var item = node.hostGraph.data.getRawDataItem(node.dataIndex);
  130. var isItemDepth = item.depth != null && item.depth >= 0;
  131. if (isItemDepth && item.depth > maxNodeDepth) {
  132. maxNodeDepth = item.depth;
  133. }
  134. node.setLayout({
  135. depth: isItemDepth ? item.depth : x
  136. }, true);
  137. orient === 'vertical' ? node.setLayout({
  138. dy: nodeWidth
  139. }, true) : node.setLayout({
  140. dx: nodeWidth
  141. }, true);
  142. for (var edgeIdx = 0; edgeIdx < node.outEdges.length; edgeIdx++) {
  143. var edge = node.outEdges[edgeIdx];
  144. var indexEdge = edges.indexOf(edge);
  145. remainEdges[indexEdge] = 0;
  146. var targetNode = edge.node2;
  147. var nodeIndex = nodes.indexOf(targetNode);
  148. if (--indegreeArr[nodeIndex] === 0 && nextTargetNode.indexOf(targetNode) < 0) {
  149. nextTargetNode.push(targetNode);
  150. }
  151. }
  152. }
  153. ++x;
  154. zeroIndegrees = nextTargetNode;
  155. nextTargetNode = [];
  156. }
  157. for (i = 0; i < remainEdges.length; i++) {
  158. if (remainEdges[i] === 1) {
  159. throw new Error('Sankey is a DAG, the original data has cycle!');
  160. }
  161. }
  162. var maxDepth = maxNodeDepth > x - 1 ? maxNodeDepth : x - 1;
  163. if (nodeAlign && nodeAlign !== 'left') {
  164. adjustNodeWithNodeAlign(nodes, nodeAlign, orient, maxDepth);
  165. }
  166. var kx = orient === 'vertical' ? (height - nodeWidth) / maxDepth : (width - nodeWidth) / maxDepth;
  167. scaleNodeBreadths(nodes, kx, orient);
  168. }
  169. function isNodeDepth(node) {
  170. var item = node.hostGraph.data.getRawDataItem(node.dataIndex);
  171. return item.depth != null && item.depth >= 0;
  172. }
  173. function adjustNodeWithNodeAlign(nodes, nodeAlign, orient, maxDepth) {
  174. if (nodeAlign === 'right') {
  175. var nextSourceNode = [];
  176. var remainNodes = nodes;
  177. var nodeHeight = 0;
  178. while (remainNodes.length) {
  179. for (var i = 0; i < remainNodes.length; i++) {
  180. var node = remainNodes[i];
  181. node.setLayout({
  182. skNodeHeight: nodeHeight
  183. }, true);
  184. for (var j = 0; j < node.inEdges.length; j++) {
  185. var edge = node.inEdges[j];
  186. if (nextSourceNode.indexOf(edge.node1) < 0) {
  187. nextSourceNode.push(edge.node1);
  188. }
  189. }
  190. }
  191. remainNodes = nextSourceNode;
  192. nextSourceNode = [];
  193. ++nodeHeight;
  194. }
  195. zrUtil.each(nodes, function (node) {
  196. if (!isNodeDepth(node)) {
  197. node.setLayout({
  198. depth: Math.max(0, maxDepth - node.getLayout().skNodeHeight)
  199. }, true);
  200. }
  201. });
  202. } else if (nodeAlign === 'justify') {
  203. moveSinksRight(nodes, maxDepth);
  204. }
  205. }
  206. /**
  207. * All the node without outEgdes are assigned maximum x-position and
  208. * be aligned in the last column.
  209. *
  210. * @param {module:echarts/data/Graph~Node} nodes. node of sankey view.
  211. * @param {number} maxDepth. use to assign to node without outEdges as x-position.
  212. */
  213. function moveSinksRight(nodes, maxDepth) {
  214. zrUtil.each(nodes, function (node) {
  215. if (!isNodeDepth(node) && !node.outEdges.length) {
  216. node.setLayout({
  217. depth: maxDepth
  218. }, true);
  219. }
  220. });
  221. }
  222. /**
  223. * Scale node x-position to the width
  224. *
  225. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  226. * @param {number} kx multiple used to scale nodes
  227. */
  228. function scaleNodeBreadths(nodes, kx, orient) {
  229. zrUtil.each(nodes, function (node) {
  230. var nodeDepth = node.getLayout().depth * kx;
  231. orient === 'vertical' ? node.setLayout({
  232. y: nodeDepth
  233. }, true) : node.setLayout({
  234. x: nodeDepth
  235. }, true);
  236. });
  237. }
  238. /**
  239. * Using Gauss-Seidel iterations method to compute the node depth(y-position)
  240. *
  241. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  242. * @param {module:echarts/data/Graph~Edge} edges edge of sankey view
  243. * @param {number} height the whole height of the area to draw the view
  244. * @param {number} nodeGap the vertical distance between two nodes
  245. * in the same column.
  246. * @param {number} iterations the number of iterations for the algorithm
  247. */
  248. function computeNodeDepths(nodes, edges, height, width, nodeGap, iterations, orient) {
  249. var nodesByBreadth = prepareNodesByBreadth(nodes, orient);
  250. initializeNodeDepth(nodesByBreadth, edges, height, width, nodeGap, orient);
  251. resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
  252. for (var alpha = 1; iterations > 0; iterations--) {
  253. // 0.99 is a experience parameter, ensure that each iterations of
  254. // changes as small as possible.
  255. alpha *= 0.99;
  256. relaxRightToLeft(nodesByBreadth, alpha, orient);
  257. resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
  258. relaxLeftToRight(nodesByBreadth, alpha, orient);
  259. resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
  260. }
  261. }
  262. function prepareNodesByBreadth(nodes, orient) {
  263. var nodesByBreadth = [];
  264. var keyAttr = orient === 'vertical' ? 'y' : 'x';
  265. var groupResult = groupData(nodes, function (node) {
  266. return node.getLayout()[keyAttr];
  267. });
  268. groupResult.keys.sort(function (a, b) {
  269. return a - b;
  270. });
  271. zrUtil.each(groupResult.keys, function (key) {
  272. nodesByBreadth.push(groupResult.buckets.get(key));
  273. });
  274. return nodesByBreadth;
  275. }
  276. /**
  277. * Compute the original y-position for each node
  278. *
  279. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  280. * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
  281. * group by the array of all sankey nodes based on the nodes x-position.
  282. * @param {module:echarts/data/Graph~Edge} edges edge of sankey view
  283. * @param {number} height the whole height of the area to draw the view
  284. * @param {number} nodeGap the vertical distance between two nodes
  285. */
  286. function initializeNodeDepth(nodesByBreadth, edges, height, width, nodeGap, orient) {
  287. var minKy = Infinity;
  288. zrUtil.each(nodesByBreadth, function (nodes) {
  289. var n = nodes.length;
  290. var sum = 0;
  291. zrUtil.each(nodes, function (node) {
  292. sum += node.getLayout().value;
  293. });
  294. var ky = orient === 'vertical' ? (width - (n - 1) * nodeGap) / sum : (height - (n - 1) * nodeGap) / sum;
  295. if (ky < minKy) {
  296. minKy = ky;
  297. }
  298. });
  299. zrUtil.each(nodesByBreadth, function (nodes) {
  300. zrUtil.each(nodes, function (node, i) {
  301. var nodeDy = node.getLayout().value * minKy;
  302. if (orient === 'vertical') {
  303. node.setLayout({
  304. x: i
  305. }, true);
  306. node.setLayout({
  307. dx: nodeDy
  308. }, true);
  309. } else {
  310. node.setLayout({
  311. y: i
  312. }, true);
  313. node.setLayout({
  314. dy: nodeDy
  315. }, true);
  316. }
  317. });
  318. });
  319. zrUtil.each(edges, function (edge) {
  320. var edgeDy = +edge.getValue() * minKy;
  321. edge.setLayout({
  322. dy: edgeDy
  323. }, true);
  324. });
  325. }
  326. /**
  327. * Resolve the collision of initialized depth (y-position)
  328. *
  329. * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
  330. * group by the array of all sankey nodes based on the nodes x-position.
  331. * @param {number} nodeGap the vertical distance between two nodes
  332. * @param {number} height the whole height of the area to draw the view
  333. */
  334. function resolveCollisions(nodesByBreadth, nodeGap, height, width, orient) {
  335. var keyAttr = orient === 'vertical' ? 'x' : 'y';
  336. zrUtil.each(nodesByBreadth, function (nodes) {
  337. nodes.sort(function (a, b) {
  338. return a.getLayout()[keyAttr] - b.getLayout()[keyAttr];
  339. });
  340. var nodeX;
  341. var node;
  342. var dy;
  343. var y0 = 0;
  344. var n = nodes.length;
  345. var nodeDyAttr = orient === 'vertical' ? 'dx' : 'dy';
  346. for (var i = 0; i < n; i++) {
  347. node = nodes[i];
  348. dy = y0 - node.getLayout()[keyAttr];
  349. if (dy > 0) {
  350. nodeX = node.getLayout()[keyAttr] + dy;
  351. orient === 'vertical' ? node.setLayout({
  352. x: nodeX
  353. }, true) : node.setLayout({
  354. y: nodeX
  355. }, true);
  356. }
  357. y0 = node.getLayout()[keyAttr] + node.getLayout()[nodeDyAttr] + nodeGap;
  358. }
  359. var viewWidth = orient === 'vertical' ? width : height; // If the bottommost node goes outside the bounds, push it back up
  360. dy = y0 - nodeGap - viewWidth;
  361. if (dy > 0) {
  362. nodeX = node.getLayout()[keyAttr] - dy;
  363. orient === 'vertical' ? node.setLayout({
  364. x: nodeX
  365. }, true) : node.setLayout({
  366. y: nodeX
  367. }, true);
  368. y0 = nodeX;
  369. for (i = n - 2; i >= 0; --i) {
  370. node = nodes[i];
  371. dy = node.getLayout()[keyAttr] + node.getLayout()[nodeDyAttr] + nodeGap - y0;
  372. if (dy > 0) {
  373. nodeX = node.getLayout()[keyAttr] - dy;
  374. orient === 'vertical' ? node.setLayout({
  375. x: nodeX
  376. }, true) : node.setLayout({
  377. y: nodeX
  378. }, true);
  379. }
  380. y0 = node.getLayout()[keyAttr];
  381. }
  382. }
  383. });
  384. }
  385. /**
  386. * Change the y-position of the nodes, except most the right side nodes
  387. *
  388. * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
  389. * group by the array of all sankey nodes based on the node x-position.
  390. * @param {number} alpha parameter used to adjust the nodes y-position
  391. */
  392. function relaxRightToLeft(nodesByBreadth, alpha, orient) {
  393. zrUtil.each(nodesByBreadth.slice().reverse(), function (nodes) {
  394. zrUtil.each(nodes, function (node) {
  395. if (node.outEdges.length) {
  396. var y = sum(node.outEdges, weightedTarget, orient) / sum(node.outEdges, getEdgeValue, orient);
  397. if (orient === 'vertical') {
  398. var nodeX = node.getLayout().x + (y - center(node, orient)) * alpha;
  399. node.setLayout({
  400. x: nodeX
  401. }, true);
  402. } else {
  403. var nodeY = node.getLayout().y + (y - center(node, orient)) * alpha;
  404. node.setLayout({
  405. y: nodeY
  406. }, true);
  407. }
  408. }
  409. });
  410. });
  411. }
  412. function weightedTarget(edge, orient) {
  413. return center(edge.node2, orient) * edge.getValue();
  414. }
  415. function weightedSource(edge, orient) {
  416. return center(edge.node1, orient) * edge.getValue();
  417. }
  418. function center(node, orient) {
  419. return orient === 'vertical' ? node.getLayout().x + node.getLayout().dx / 2 : node.getLayout().y + node.getLayout().dy / 2;
  420. }
  421. function getEdgeValue(edge) {
  422. return edge.getValue();
  423. }
  424. function sum(array, f, orient) {
  425. var sum = 0;
  426. var len = array.length;
  427. var i = -1;
  428. while (++i < len) {
  429. var value = +f.call(array, array[i], orient);
  430. if (!isNaN(value)) {
  431. sum += value;
  432. }
  433. }
  434. return sum;
  435. }
  436. /**
  437. * Change the y-position of the nodes, except most the left side nodes
  438. *
  439. * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
  440. * group by the array of all sankey nodes based on the node x-position.
  441. * @param {number} alpha parameter used to adjust the nodes y-position
  442. */
  443. function relaxLeftToRight(nodesByBreadth, alpha, orient) {
  444. zrUtil.each(nodesByBreadth, function (nodes) {
  445. zrUtil.each(nodes, function (node) {
  446. if (node.inEdges.length) {
  447. var y = sum(node.inEdges, weightedSource, orient) / sum(node.inEdges, getEdgeValue, orient);
  448. if (orient === 'vertical') {
  449. var nodeX = node.getLayout().x + (y - center(node, orient)) * alpha;
  450. node.setLayout({
  451. x: nodeX
  452. }, true);
  453. } else {
  454. var nodeY = node.getLayout().y + (y - center(node, orient)) * alpha;
  455. node.setLayout({
  456. y: nodeY
  457. }, true);
  458. }
  459. }
  460. });
  461. });
  462. }
  463. /**
  464. * Compute the depth(y-position) of each edge
  465. *
  466. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  467. */
  468. function computeEdgeDepths(nodes, orient) {
  469. var keyAttr = orient === 'vertical' ? 'x' : 'y';
  470. zrUtil.each(nodes, function (node) {
  471. node.outEdges.sort(function (a, b) {
  472. return a.node2.getLayout()[keyAttr] - b.node2.getLayout()[keyAttr];
  473. });
  474. node.inEdges.sort(function (a, b) {
  475. return a.node1.getLayout()[keyAttr] - b.node1.getLayout()[keyAttr];
  476. });
  477. });
  478. zrUtil.each(nodes, function (node) {
  479. var sy = 0;
  480. var ty = 0;
  481. zrUtil.each(node.outEdges, function (edge) {
  482. edge.setLayout({
  483. sy: sy
  484. }, true);
  485. sy += edge.getLayout().dy;
  486. });
  487. zrUtil.each(node.inEdges, function (edge) {
  488. edge.setLayout({
  489. ty: ty
  490. }, true);
  491. ty += edge.getLayout().dy;
  492. });
  493. });
  494. }
  495. module.exports = _default;