sankeyLayout.js 16 KB

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