sankeyLayout.js 16 KB

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