sankeyLayout.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. var layout = require("../../util/layout");
  2. var nest = require("../../util/array/nest");
  3. var zrUtil = require("zrender/lib/core/util");
  4. /**
  5. * @file The layout algorithm of sankey view
  6. * @author Deqing Li(annong035@gmail.com)
  7. */
  8. function _default(ecModel, api, payload) {
  9. ecModel.eachSeriesByType('sankey', function (seriesModel) {
  10. var nodeWidth = seriesModel.get('nodeWidth');
  11. var nodeGap = seriesModel.get('nodeGap');
  12. var layoutInfo = getViewRect(seriesModel, api);
  13. seriesModel.layoutInfo = layoutInfo;
  14. var width = layoutInfo.width;
  15. var height = layoutInfo.height;
  16. var graph = seriesModel.getGraph();
  17. var nodes = graph.nodes;
  18. var edges = graph.edges;
  19. computeNodeValues(nodes);
  20. var filteredNodes = zrUtil.filter(nodes, function (node) {
  21. return node.getLayout().value === 0;
  22. });
  23. var iterations = filteredNodes.length !== 0 ? 0 : seriesModel.get('layoutIterations');
  24. layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations);
  25. });
  26. }
  27. /**
  28. * Get the layout position of the whole view
  29. *
  30. * @param {module:echarts/model/Series} seriesModel the model object of sankey series
  31. * @param {module:echarts/ExtensionAPI} api provide the API list that the developer can call
  32. * @return {module:zrender/core/BoundingRect} size of rect to draw the sankey view
  33. */
  34. function getViewRect(seriesModel, api) {
  35. return layout.getLayoutRect(seriesModel.getBoxLayoutParams(), {
  36. width: api.getWidth(),
  37. height: api.getHeight()
  38. });
  39. }
  40. function layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations) {
  41. computeNodeBreadths(nodes, nodeWidth, width);
  42. computeNodeDepths(nodes, edges, height, nodeGap, iterations);
  43. computeEdgeDepths(nodes);
  44. }
  45. /**
  46. * Compute the value of each node by summing the associated edge's value
  47. *
  48. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  49. */
  50. function computeNodeValues(nodes) {
  51. zrUtil.each(nodes, function (node) {
  52. var value1 = sum(node.outEdges, getEdgeValue);
  53. var value2 = sum(node.inEdges, getEdgeValue);
  54. var value = Math.max(value1, value2);
  55. node.setLayout({
  56. value: value
  57. }, true);
  58. });
  59. }
  60. /**
  61. * Compute the x-position for each node
  62. *
  63. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  64. * @param {number} nodeWidth the dx of the node
  65. * @param {number} width the whole width of the area to draw the view
  66. */
  67. function computeNodeBreadths(nodes, nodeWidth, width) {
  68. var remainNodes = nodes;
  69. var nextNode = null;
  70. var x = 0;
  71. var kx = 0;
  72. while (remainNodes.length) {
  73. nextNode = [];
  74. for (var i = 0, len = remainNodes.length; i < len; i++) {
  75. var node = remainNodes[i];
  76. node.setLayout({
  77. x: x
  78. }, true);
  79. node.setLayout({
  80. dx: nodeWidth
  81. }, true);
  82. for (var j = 0, lenj = node.outEdges.length; j < lenj; j++) {
  83. nextNode.push(node.outEdges[j].node2);
  84. }
  85. }
  86. remainNodes = nextNode;
  87. ++x;
  88. }
  89. moveSinksRight(nodes, x);
  90. kx = (width - nodeWidth) / (x - 1);
  91. scaleNodeBreadths(nodes, kx);
  92. }
  93. /**
  94. * All the node without outEgdes are assigned maximum x-position and
  95. * be aligned in the last column.
  96. *
  97. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  98. * @param {number} x value (x-1) use to assign to node without outEdges
  99. * as x-position
  100. */
  101. function moveSinksRight(nodes, x) {
  102. zrUtil.each(nodes, function (node) {
  103. if (!node.outEdges.length) {
  104. node.setLayout({
  105. x: x - 1
  106. }, true);
  107. }
  108. });
  109. }
  110. /**
  111. * Scale node x-position to the width
  112. *
  113. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  114. * @param {number} kx multiple used to scale nodes
  115. */
  116. function scaleNodeBreadths(nodes, kx) {
  117. zrUtil.each(nodes, function (node) {
  118. var nodeX = node.getLayout().x * kx;
  119. node.setLayout({
  120. x: nodeX
  121. }, true);
  122. });
  123. }
  124. /**
  125. * Using Gauss-Seidel iterations method to compute the node depth(y-position)
  126. *
  127. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  128. * @param {module:echarts/data/Graph~Edge} edges edge of sankey view
  129. * @param {number} height the whole height of the area to draw the view
  130. * @param {number} nodeGap the vertical distance between two nodes
  131. * in the same column.
  132. * @param {number} iterations the number of iterations for the algorithm
  133. */
  134. function computeNodeDepths(nodes, edges, height, nodeGap, iterations) {
  135. var nodesByBreadth = nest().key(function (d) {
  136. return d.getLayout().x;
  137. }).sortKeys(ascending).entries(nodes).map(function (d) {
  138. return d.values;
  139. });
  140. initializeNodeDepth(nodes, nodesByBreadth, edges, height, nodeGap);
  141. resolveCollisions(nodesByBreadth, nodeGap, height);
  142. for (var alpha = 1; iterations > 0; iterations--) {
  143. // 0.99 is a experience parameter, ensure that each iterations of
  144. // changes as small as possible.
  145. alpha *= 0.99;
  146. relaxRightToLeft(nodesByBreadth, alpha);
  147. resolveCollisions(nodesByBreadth, nodeGap, height);
  148. relaxLeftToRight(nodesByBreadth, alpha);
  149. resolveCollisions(nodesByBreadth, nodeGap, height);
  150. }
  151. }
  152. /**
  153. * Compute the original y-position for each node
  154. *
  155. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  156. * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
  157. * group by the array of all sankey nodes based on the nodes x-position.
  158. * @param {module:echarts/data/Graph~Edge} edges edge of sankey view
  159. * @param {number} height the whole height of the area to draw the view
  160. * @param {number} nodeGap the vertical distance between two nodes
  161. */
  162. function initializeNodeDepth(nodes, nodesByBreadth, edges, height, nodeGap) {
  163. var kyArray = [];
  164. zrUtil.each(nodesByBreadth, function (nodes) {
  165. var n = nodes.length;
  166. var sum = 0;
  167. zrUtil.each(nodes, function (node) {
  168. sum += node.getLayout().value;
  169. });
  170. var ky = (height - (n - 1) * nodeGap) / sum;
  171. kyArray.push(ky);
  172. });
  173. kyArray.sort(function (a, b) {
  174. return a - b;
  175. });
  176. var ky0 = kyArray[0];
  177. zrUtil.each(nodesByBreadth, function (nodes) {
  178. zrUtil.each(nodes, function (node, i) {
  179. node.setLayout({
  180. y: i
  181. }, true);
  182. var nodeDy = node.getLayout().value * ky0;
  183. node.setLayout({
  184. dy: nodeDy
  185. }, true);
  186. });
  187. });
  188. zrUtil.each(edges, function (edge) {
  189. var edgeDy = +edge.getValue() * ky0;
  190. edge.setLayout({
  191. dy: edgeDy
  192. }, true);
  193. });
  194. }
  195. /**
  196. * Resolve the collision of initialized depth (y-position)
  197. *
  198. * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
  199. * group by the array of all sankey nodes based on the nodes x-position.
  200. * @param {number} nodeGap the vertical distance between two nodes
  201. * @param {number} height the whole height of the area to draw the view
  202. */
  203. function resolveCollisions(nodesByBreadth, nodeGap, height) {
  204. zrUtil.each(nodesByBreadth, function (nodes) {
  205. var node;
  206. var dy;
  207. var y0 = 0;
  208. var n = nodes.length;
  209. var i;
  210. nodes.sort(ascendingDepth);
  211. for (i = 0; i < n; i++) {
  212. node = nodes[i];
  213. dy = y0 - node.getLayout().y;
  214. if (dy > 0) {
  215. var nodeY = node.getLayout().y + dy;
  216. node.setLayout({
  217. y: nodeY
  218. }, true);
  219. }
  220. y0 = node.getLayout().y + node.getLayout().dy + nodeGap;
  221. } // if the bottommost node goes outside the bounds, push it back up
  222. dy = y0 - nodeGap - height;
  223. if (dy > 0) {
  224. var nodeY = node.getLayout().y - dy;
  225. node.setLayout({
  226. y: nodeY
  227. }, true);
  228. y0 = node.getLayout().y;
  229. for (i = n - 2; i >= 0; --i) {
  230. node = nodes[i];
  231. dy = node.getLayout().y + node.getLayout().dy + nodeGap - y0;
  232. if (dy > 0) {
  233. nodeY = node.getLayout().y - dy;
  234. node.setLayout({
  235. y: nodeY
  236. }, true);
  237. }
  238. y0 = node.getLayout().y;
  239. }
  240. }
  241. });
  242. }
  243. /**
  244. * Change the y-position of the nodes, except most the right side nodes
  245. *
  246. * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
  247. * group by the array of all sankey nodes based on the node x-position.
  248. * @param {number} alpha parameter used to adjust the nodes y-position
  249. */
  250. function relaxRightToLeft(nodesByBreadth, alpha) {
  251. zrUtil.each(nodesByBreadth.slice().reverse(), function (nodes) {
  252. zrUtil.each(nodes, function (node) {
  253. if (node.outEdges.length) {
  254. var y = sum(node.outEdges, weightedTarget) / sum(node.outEdges, getEdgeValue);
  255. var nodeY = node.getLayout().y + (y - center(node)) * alpha;
  256. node.setLayout({
  257. y: nodeY
  258. }, true);
  259. }
  260. });
  261. });
  262. }
  263. function weightedTarget(edge) {
  264. return center(edge.node2) * edge.getValue();
  265. }
  266. /**
  267. * Change the y-position of the nodes, except most the left side nodes
  268. *
  269. * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
  270. * group by the array of all sankey nodes based on the node x-position.
  271. * @param {number} alpha parameter used to adjust the nodes y-position
  272. */
  273. function relaxLeftToRight(nodesByBreadth, alpha) {
  274. zrUtil.each(nodesByBreadth, function (nodes) {
  275. zrUtil.each(nodes, function (node) {
  276. if (node.inEdges.length) {
  277. var y = sum(node.inEdges, weightedSource) / sum(node.inEdges, getEdgeValue);
  278. var nodeY = node.getLayout().y + (y - center(node)) * alpha;
  279. node.setLayout({
  280. y: nodeY
  281. }, true);
  282. }
  283. });
  284. });
  285. }
  286. function weightedSource(edge) {
  287. return center(edge.node1) * edge.getValue();
  288. }
  289. /**
  290. * Compute the depth(y-position) of each edge
  291. *
  292. * @param {module:echarts/data/Graph~Node} nodes node of sankey view
  293. */
  294. function computeEdgeDepths(nodes) {
  295. zrUtil.each(nodes, function (node) {
  296. node.outEdges.sort(ascendingTargetDepth);
  297. node.inEdges.sort(ascendingSourceDepth);
  298. });
  299. zrUtil.each(nodes, function (node) {
  300. var sy = 0;
  301. var ty = 0;
  302. zrUtil.each(node.outEdges, function (edge) {
  303. edge.setLayout({
  304. sy: sy
  305. }, true);
  306. sy += edge.getLayout().dy;
  307. });
  308. zrUtil.each(node.inEdges, function (edge) {
  309. edge.setLayout({
  310. ty: ty
  311. }, true);
  312. ty += edge.getLayout().dy;
  313. });
  314. });
  315. }
  316. function ascendingTargetDepth(a, b) {
  317. return a.node2.getLayout().y - b.node2.getLayout().y;
  318. }
  319. function ascendingSourceDepth(a, b) {
  320. return a.node1.getLayout().y - b.node1.getLayout().y;
  321. }
  322. function sum(array, f) {
  323. var sum = 0;
  324. var len = array.length;
  325. var i = -1;
  326. while (++i < len) {
  327. var value = +f.call(array, array[i], i);
  328. if (!isNaN(value)) {
  329. sum += value;
  330. }
  331. }
  332. return sum;
  333. }
  334. function center(node) {
  335. return node.getLayout().y + node.getLayout().dy / 2;
  336. }
  337. function ascendingDepth(a, b) {
  338. return a.getLayout().y - b.getLayout().y;
  339. }
  340. function ascending(a, b) {
  341. return a < b ? -1 : a > b ? 1 : a === b ? 0 : NaN;
  342. }
  343. function getEdgeValue(edge) {
  344. return edge.getValue();
  345. }
  346. module.exports = _default;