sankeyLayout.js 16 KB

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