treemapLayout.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. var zrUtil = require("zrender/lib/core/util");
  2. var BoundingRect = require("zrender/lib/core/BoundingRect");
  3. var _number = require("../../util/number");
  4. var parsePercent = _number.parsePercent;
  5. var MAX_SAFE_INTEGER = _number.MAX_SAFE_INTEGER;
  6. var layout = require("../../util/layout");
  7. var helper = require("./helper");
  8. var mathMax = Math.max;
  9. var mathMin = Math.min;
  10. var retrieveValue = zrUtil.retrieve;
  11. var each = zrUtil.each;
  12. var PATH_BORDER_WIDTH = ['itemStyle', 'normal', 'borderWidth'];
  13. var PATH_GAP_WIDTH = ['itemStyle', 'normal', 'gapWidth'];
  14. var PATH_UPPER_LABEL_SHOW = ['upperLabel', 'normal', 'show'];
  15. var PATH_UPPER_LABEL_HEIGHT = ['upperLabel', 'normal', 'height'];
  16. /**
  17. * @public
  18. */
  19. function _default(ecModel, api, payload) {
  20. // Layout result in each node:
  21. // {x, y, width, height, area, borderWidth}
  22. var condition = {
  23. mainType: 'series',
  24. subType: 'treemap',
  25. query: payload
  26. };
  27. ecModel.eachComponent(condition, function (seriesModel) {
  28. var ecWidth = api.getWidth();
  29. var ecHeight = api.getHeight();
  30. var seriesOption = seriesModel.option;
  31. var layoutInfo = layout.getLayoutRect(seriesModel.getBoxLayoutParams(), {
  32. width: api.getWidth(),
  33. height: api.getHeight()
  34. });
  35. var size = seriesOption.size || []; // Compatible with ec2.
  36. var containerWidth = parsePercent(retrieveValue(layoutInfo.width, size[0]), ecWidth);
  37. var containerHeight = parsePercent(retrieveValue(layoutInfo.height, size[1]), ecHeight); // Fetch payload info.
  38. var payloadType = payload && payload.type;
  39. var targetInfo = helper.retrieveTargetInfo(payload, seriesModel);
  40. var rootRect = payloadType === 'treemapRender' || payloadType === 'treemapMove' ? payload.rootRect : null;
  41. var viewRoot = seriesModel.getViewRoot();
  42. var viewAbovePath = helper.getPathToRoot(viewRoot);
  43. if (payloadType !== 'treemapMove') {
  44. var rootSize = payloadType === 'treemapZoomToNode' ? estimateRootSize(seriesModel, targetInfo, viewRoot, containerWidth, containerHeight) : rootRect ? [rootRect.width, rootRect.height] : [containerWidth, containerHeight];
  45. var sort = seriesOption.sort;
  46. if (sort && sort !== 'asc' && sort !== 'desc') {
  47. sort = 'desc';
  48. }
  49. var options = {
  50. squareRatio: seriesOption.squareRatio,
  51. sort: sort,
  52. leafDepth: seriesOption.leafDepth
  53. }; // layout should be cleared because using updateView but not update.
  54. viewRoot.hostTree.clearLayouts(); // TODO
  55. // optimize: if out of view clip, do not layout.
  56. // But take care that if do not render node out of view clip,
  57. // how to calculate start po
  58. var viewRootLayout = {
  59. x: 0,
  60. y: 0,
  61. width: rootSize[0],
  62. height: rootSize[1],
  63. area: rootSize[0] * rootSize[1]
  64. };
  65. viewRoot.setLayout(viewRootLayout);
  66. squarify(viewRoot, options, false, 0); // Supplement layout.
  67. var viewRootLayout = viewRoot.getLayout();
  68. each(viewAbovePath, function (node, index) {
  69. var childValue = (viewAbovePath[index + 1] || viewRoot).getValue();
  70. node.setLayout(zrUtil.extend({
  71. dataExtent: [childValue, childValue],
  72. borderWidth: 0,
  73. upperHeight: 0
  74. }, viewRootLayout));
  75. });
  76. }
  77. var treeRoot = seriesModel.getData().tree.root;
  78. treeRoot.setLayout(calculateRootPosition(layoutInfo, rootRect, targetInfo), true);
  79. seriesModel.setLayoutInfo(layoutInfo); // FIXME
  80. // 现在没有clip功能,暂时取ec高宽。
  81. prunning(treeRoot, // Transform to base element coordinate system.
  82. new BoundingRect(-layoutInfo.x, -layoutInfo.y, ecWidth, ecHeight), viewAbovePath, viewRoot, 0);
  83. });
  84. }
  85. /**
  86. * Layout treemap with squarify algorithm.
  87. * @see https://graphics.ethz.ch/teaching/scivis_common/Literature/squarifiedTreeMaps.pdf
  88. * @see https://github.com/mbostock/d3/blob/master/src/layout/treemap.js
  89. *
  90. * @protected
  91. * @param {module:echarts/data/Tree~TreeNode} node
  92. * @param {Object} options
  93. * @param {string} options.sort 'asc' or 'desc'
  94. * @param {number} options.squareRatio
  95. * @param {boolean} hideChildren
  96. * @param {number} depth
  97. */
  98. function squarify(node, options, hideChildren, depth) {
  99. var width;
  100. var height;
  101. if (node.isRemoved()) {
  102. return;
  103. }
  104. var thisLayout = node.getLayout();
  105. width = thisLayout.width;
  106. height = thisLayout.height; // Considering border and gap
  107. var nodeModel = node.getModel();
  108. var borderWidth = nodeModel.get(PATH_BORDER_WIDTH);
  109. var halfGapWidth = nodeModel.get(PATH_GAP_WIDTH) / 2;
  110. var upperLabelHeight = getUpperLabelHeight(nodeModel);
  111. var upperHeight = Math.max(borderWidth, upperLabelHeight);
  112. var layoutOffset = borderWidth - halfGapWidth;
  113. var layoutOffsetUpper = upperHeight - halfGapWidth;
  114. var nodeModel = node.getModel();
  115. node.setLayout({
  116. borderWidth: borderWidth,
  117. upperHeight: upperHeight,
  118. upperLabelHeight: upperLabelHeight
  119. }, true);
  120. width = mathMax(width - 2 * layoutOffset, 0);
  121. height = mathMax(height - layoutOffset - layoutOffsetUpper, 0);
  122. var totalArea = width * height;
  123. var viewChildren = initChildren(node, nodeModel, totalArea, options, hideChildren, depth);
  124. if (!viewChildren.length) {
  125. return;
  126. }
  127. var rect = {
  128. x: layoutOffset,
  129. y: layoutOffsetUpper,
  130. width: width,
  131. height: height
  132. };
  133. var rowFixedLength = mathMin(width, height);
  134. var best = Infinity; // the best row score so far
  135. var row = [];
  136. row.area = 0;
  137. for (var i = 0, len = viewChildren.length; i < len;) {
  138. var child = viewChildren[i];
  139. row.push(child);
  140. row.area += child.getLayout().area;
  141. var score = worst(row, rowFixedLength, options.squareRatio); // continue with this orientation
  142. if (score <= best) {
  143. i++;
  144. best = score;
  145. } // abort, and try a different orientation
  146. else {
  147. row.area -= row.pop().getLayout().area;
  148. position(row, rowFixedLength, rect, halfGapWidth, false);
  149. rowFixedLength = mathMin(rect.width, rect.height);
  150. row.length = row.area = 0;
  151. best = Infinity;
  152. }
  153. }
  154. if (row.length) {
  155. position(row, rowFixedLength, rect, halfGapWidth, true);
  156. }
  157. if (!hideChildren) {
  158. var childrenVisibleMin = nodeModel.get('childrenVisibleMin');
  159. if (childrenVisibleMin != null && totalArea < childrenVisibleMin) {
  160. hideChildren = true;
  161. }
  162. }
  163. for (var i = 0, len = viewChildren.length; i < len; i++) {
  164. squarify(viewChildren[i], options, hideChildren, depth + 1);
  165. }
  166. }
  167. /**
  168. * Set area to each child, and calculate data extent for visual coding.
  169. */
  170. function initChildren(node, nodeModel, totalArea, options, hideChildren, depth) {
  171. var viewChildren = node.children || [];
  172. var orderBy = options.sort;
  173. orderBy !== 'asc' && orderBy !== 'desc' && (orderBy = null);
  174. var overLeafDepth = options.leafDepth != null && options.leafDepth <= depth; // leafDepth has higher priority.
  175. if (hideChildren && !overLeafDepth) {
  176. return node.viewChildren = [];
  177. } // Sort children, order by desc.
  178. viewChildren = zrUtil.filter(viewChildren, function (child) {
  179. return !child.isRemoved();
  180. });
  181. sort(viewChildren, orderBy);
  182. var info = statistic(nodeModel, viewChildren, orderBy);
  183. if (info.sum === 0) {
  184. return node.viewChildren = [];
  185. }
  186. info.sum = filterByThreshold(nodeModel, totalArea, info.sum, orderBy, viewChildren);
  187. if (info.sum === 0) {
  188. return node.viewChildren = [];
  189. } // Set area to each child.
  190. for (var i = 0, len = viewChildren.length; i < len; i++) {
  191. var area = viewChildren[i].getValue() / info.sum * totalArea; // Do not use setLayout({...}, true), because it is needed to clear last layout.
  192. viewChildren[i].setLayout({
  193. area: area
  194. });
  195. }
  196. if (overLeafDepth) {
  197. viewChildren.length && node.setLayout({
  198. isLeafRoot: true
  199. }, true);
  200. viewChildren.length = 0;
  201. }
  202. node.viewChildren = viewChildren;
  203. node.setLayout({
  204. dataExtent: info.dataExtent
  205. }, true);
  206. return viewChildren;
  207. }
  208. /**
  209. * Consider 'visibleMin'. Modify viewChildren and get new sum.
  210. */
  211. function filterByThreshold(nodeModel, totalArea, sum, orderBy, orderedChildren) {
  212. // visibleMin is not supported yet when no option.sort.
  213. if (!orderBy) {
  214. return sum;
  215. }
  216. var visibleMin = nodeModel.get('visibleMin');
  217. var len = orderedChildren.length;
  218. var deletePoint = len; // Always travel from little value to big value.
  219. for (var i = len - 1; i >= 0; i--) {
  220. var value = orderedChildren[orderBy === 'asc' ? len - i - 1 : i].getValue();
  221. if (value / sum * totalArea < visibleMin) {
  222. deletePoint = i;
  223. sum -= value;
  224. }
  225. }
  226. orderBy === 'asc' ? orderedChildren.splice(0, len - deletePoint) : orderedChildren.splice(deletePoint, len - deletePoint);
  227. return sum;
  228. }
  229. /**
  230. * Sort
  231. */
  232. function sort(viewChildren, orderBy) {
  233. if (orderBy) {
  234. viewChildren.sort(function (a, b) {
  235. var diff = orderBy === 'asc' ? a.getValue() - b.getValue() : b.getValue() - a.getValue();
  236. return diff === 0 ? orderBy === 'asc' ? a.dataIndex - b.dataIndex : b.dataIndex - a.dataIndex : diff;
  237. });
  238. }
  239. return viewChildren;
  240. }
  241. /**
  242. * Statistic
  243. */
  244. function statistic(nodeModel, children, orderBy) {
  245. // Calculate sum.
  246. var sum = 0;
  247. for (var i = 0, len = children.length; i < len; i++) {
  248. sum += children[i].getValue();
  249. } // Statistic data extent for latter visual coding.
  250. // Notice: data extent should be calculate based on raw children
  251. // but not filtered view children, otherwise visual mapping will not
  252. // be stable when zoom (where children is filtered by visibleMin).
  253. var dimension = nodeModel.get('visualDimension');
  254. var dataExtent; // The same as area dimension.
  255. if (!children || !children.length) {
  256. dataExtent = [NaN, NaN];
  257. } else if (dimension === 'value' && orderBy) {
  258. dataExtent = [children[children.length - 1].getValue(), children[0].getValue()];
  259. orderBy === 'asc' && dataExtent.reverse();
  260. } // Other dimension.
  261. else {
  262. var dataExtent = [Infinity, -Infinity];
  263. each(children, function (child) {
  264. var value = child.getValue(dimension);
  265. value < dataExtent[0] && (dataExtent[0] = value);
  266. value > dataExtent[1] && (dataExtent[1] = value);
  267. });
  268. }
  269. return {
  270. sum: sum,
  271. dataExtent: dataExtent
  272. };
  273. }
  274. /**
  275. * Computes the score for the specified row,
  276. * as the worst aspect ratio.
  277. */
  278. function worst(row, rowFixedLength, ratio) {
  279. var areaMax = 0;
  280. var areaMin = Infinity;
  281. for (var i = 0, area, len = row.length; i < len; i++) {
  282. area = row[i].getLayout().area;
  283. if (area) {
  284. area < areaMin && (areaMin = area);
  285. area > areaMax && (areaMax = area);
  286. }
  287. }
  288. var squareArea = row.area * row.area;
  289. var f = rowFixedLength * rowFixedLength * ratio;
  290. return squareArea ? mathMax(f * areaMax / squareArea, squareArea / (f * areaMin)) : Infinity;
  291. }
  292. /**
  293. * Positions the specified row of nodes. Modifies `rect`.
  294. */
  295. function position(row, rowFixedLength, rect, halfGapWidth, flush) {
  296. // When rowFixedLength === rect.width,
  297. // it is horizontal subdivision,
  298. // rowFixedLength is the width of the subdivision,
  299. // rowOtherLength is the height of the subdivision,
  300. // and nodes will be positioned from left to right.
  301. // wh[idx0WhenH] means: when horizontal,
  302. // wh[idx0WhenH] => wh[0] => 'width'.
  303. // xy[idx1WhenH] => xy[1] => 'y'.
  304. var idx0WhenH = rowFixedLength === rect.width ? 0 : 1;
  305. var idx1WhenH = 1 - idx0WhenH;
  306. var xy = ['x', 'y'];
  307. var wh = ['width', 'height'];
  308. var last = rect[xy[idx0WhenH]];
  309. var rowOtherLength = rowFixedLength ? row.area / rowFixedLength : 0;
  310. if (flush || rowOtherLength > rect[wh[idx1WhenH]]) {
  311. rowOtherLength = rect[wh[idx1WhenH]]; // over+underflow
  312. }
  313. for (var i = 0, rowLen = row.length; i < rowLen; i++) {
  314. var node = row[i];
  315. var nodeLayout = {};
  316. var step = rowOtherLength ? node.getLayout().area / rowOtherLength : 0;
  317. var wh1 = nodeLayout[wh[idx1WhenH]] = mathMax(rowOtherLength - 2 * halfGapWidth, 0); // We use Math.max/min to avoid negative width/height when considering gap width.
  318. var remain = rect[xy[idx0WhenH]] + rect[wh[idx0WhenH]] - last;
  319. var modWH = i === rowLen - 1 || remain < step ? remain : step;
  320. var wh0 = nodeLayout[wh[idx0WhenH]] = mathMax(modWH - 2 * halfGapWidth, 0);
  321. nodeLayout[xy[idx1WhenH]] = rect[xy[idx1WhenH]] + mathMin(halfGapWidth, wh1 / 2);
  322. nodeLayout[xy[idx0WhenH]] = last + mathMin(halfGapWidth, wh0 / 2);
  323. last += modWH;
  324. node.setLayout(nodeLayout, true);
  325. }
  326. rect[xy[idx1WhenH]] += rowOtherLength;
  327. rect[wh[idx1WhenH]] -= rowOtherLength;
  328. } // Return [containerWidth, containerHeight] as defualt.
  329. function estimateRootSize(seriesModel, targetInfo, viewRoot, containerWidth, containerHeight) {
  330. // If targetInfo.node exists, we zoom to the node,
  331. // so estimate whold width and heigth by target node.
  332. var currNode = (targetInfo || {}).node;
  333. var defaultSize = [containerWidth, containerHeight];
  334. if (!currNode || currNode === viewRoot) {
  335. return defaultSize;
  336. }
  337. var parent;
  338. var viewArea = containerWidth * containerHeight;
  339. var area = viewArea * seriesModel.option.zoomToNodeRatio;
  340. while (parent = currNode.parentNode) {
  341. // jshint ignore:line
  342. var sum = 0;
  343. var siblings = parent.children;
  344. for (var i = 0, len = siblings.length; i < len; i++) {
  345. sum += siblings[i].getValue();
  346. }
  347. var currNodeValue = currNode.getValue();
  348. if (currNodeValue === 0) {
  349. return defaultSize;
  350. }
  351. area *= sum / currNodeValue; // Considering border, suppose aspect ratio is 1.
  352. var parentModel = parent.getModel();
  353. var borderWidth = parentModel.get(PATH_BORDER_WIDTH);
  354. var upperHeight = Math.max(borderWidth, getUpperLabelHeight(parentModel, borderWidth));
  355. area += 4 * borderWidth * borderWidth + (3 * borderWidth + upperHeight) * Math.pow(area, 0.5);
  356. area > MAX_SAFE_INTEGER && (area = MAX_SAFE_INTEGER);
  357. currNode = parent;
  358. }
  359. area < viewArea && (area = viewArea);
  360. var scale = Math.pow(area / viewArea, 0.5);
  361. return [containerWidth * scale, containerHeight * scale];
  362. } // Root postion base on coord of containerGroup
  363. function calculateRootPosition(layoutInfo, rootRect, targetInfo) {
  364. if (rootRect) {
  365. return {
  366. x: rootRect.x,
  367. y: rootRect.y
  368. };
  369. }
  370. var defaultPosition = {
  371. x: 0,
  372. y: 0
  373. };
  374. if (!targetInfo) {
  375. return defaultPosition;
  376. } // If targetInfo is fetched by 'retrieveTargetInfo',
  377. // old tree and new tree are the same tree,
  378. // so the node still exists and we can visit it.
  379. var targetNode = targetInfo.node;
  380. var layout = targetNode.getLayout();
  381. if (!layout) {
  382. return defaultPosition;
  383. } // Transform coord from local to container.
  384. var targetCenter = [layout.width / 2, layout.height / 2];
  385. var node = targetNode;
  386. while (node) {
  387. var nodeLayout = node.getLayout();
  388. targetCenter[0] += nodeLayout.x;
  389. targetCenter[1] += nodeLayout.y;
  390. node = node.parentNode;
  391. }
  392. return {
  393. x: layoutInfo.width / 2 - targetCenter[0],
  394. y: layoutInfo.height / 2 - targetCenter[1]
  395. };
  396. } // Mark nodes visible for prunning when visual coding and rendering.
  397. // Prunning depends on layout and root position, so we have to do it after layout.
  398. function prunning(node, clipRect, viewAbovePath, viewRoot, depth) {
  399. var nodeLayout = node.getLayout();
  400. var nodeInViewAbovePath = viewAbovePath[depth];
  401. var isAboveViewRoot = nodeInViewAbovePath && nodeInViewAbovePath === node;
  402. if (nodeInViewAbovePath && !isAboveViewRoot || depth === viewAbovePath.length && node !== viewRoot) {
  403. return;
  404. }
  405. node.setLayout({
  406. // isInView means: viewRoot sub tree + viewAbovePath
  407. isInView: true,
  408. // invisible only means: outside view clip so that the node can not
  409. // see but still layout for animation preparation but not render.
  410. invisible: !isAboveViewRoot && !clipRect.intersect(nodeLayout),
  411. isAboveViewRoot: isAboveViewRoot
  412. }, true); // Transform to child coordinate.
  413. var childClipRect = new BoundingRect(clipRect.x - nodeLayout.x, clipRect.y - nodeLayout.y, clipRect.width, clipRect.height);
  414. each(node.viewChildren || [], function (child) {
  415. prunning(child, childClipRect, viewAbovePath, viewRoot, depth + 1);
  416. });
  417. }
  418. function getUpperLabelHeight(model) {
  419. return model.get(PATH_UPPER_LABEL_SHOW) ? model.get(PATH_UPPER_LABEL_HEIGHT) : 0;
  420. }
  421. module.exports = _default;