sankeyLayout.js 16 KB

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