treemapLayout.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  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. /*
  41. * A third-party license is embedded for some of the code in this file:
  42. * The treemap layout implementation was originally copied from
  43. * "d3.js" with some modifications made for this project.
  44. * (See more details in the comment of the method "squarify" below.)
  45. * The use of the source code of this file is also subject to the terms
  46. * and consitions of the license of "d3.js" (BSD-3Clause, see
  47. * </licenses/LICENSE-d3>).
  48. */
  49. import * as zrUtil from 'zrender/lib/core/util.js';
  50. import BoundingRect from 'zrender/lib/core/BoundingRect.js';
  51. import { MAX_SAFE_INTEGER } from '../../util/number.js';
  52. import * as layout from '../../util/layout.js';
  53. import * as helper from '../helper/treeHelper.js';
  54. import { initExtentForUnion } from '../../util/model.js';
  55. import { clampByZoomLimit } from '../../coord/View.js';
  56. var mathMax = Math.max;
  57. var mathMin = Math.min;
  58. var each = zrUtil.each;
  59. var PATH_BORDER_WIDTH = ['itemStyle', 'borderWidth'];
  60. var PATH_GAP_WIDTH = ['itemStyle', 'gapWidth'];
  61. var PATH_UPPER_LABEL_SHOW = ['upperLabel', 'show'];
  62. var PATH_UPPER_LABEL_HEIGHT = ['upperLabel', 'height'];
  63. ;
  64. /**
  65. * @public
  66. */
  67. export default {
  68. seriesType: 'treemap',
  69. reset: function (seriesModel, ecModel, api, payload) {
  70. // Layout result in each node:
  71. // {x, y, width, height, area, borderWidth}
  72. var seriesOption = seriesModel.option;
  73. var refContainer = layout.createBoxLayoutReference(seriesModel, api).refContainer;
  74. var layoutInfo = layout.getLayoutRect(seriesModel.getBoxLayoutParams(), refContainer);
  75. // Fetch payload info.
  76. var payloadType = payload && payload.type;
  77. var types = ['treemapZoomToNode', 'treemapRootToNode'];
  78. var targetInfo = helper.retrieveTargetInfo(payload, types, seriesModel);
  79. var rootRect = payloadType === 'treemapRender' || payloadType === 'treemapMove' ? payload.rootRect : null;
  80. var viewRoot = seriesModel.getViewRoot();
  81. var viewAbovePath = helper.getPathToRoot(viewRoot);
  82. if (payloadType !== 'treemapMove') {
  83. var needClampZoom = false;
  84. var rootSize = payloadType === 'treemapZoomToNode' ? (needClampZoom = true, estimateRootSize(seriesModel, targetInfo, viewRoot, layoutInfo)) : rootRect ? (needClampZoom = true, zrUtil.extend({}, rootRect)) : zrUtil.extend({}, layoutInfo);
  85. if (needClampZoom) {
  86. var zoom = calculateCurrentZoom(layoutInfo, rootSize);
  87. zoom = treemapClampZoom(zoom, seriesModel);
  88. rootSize.width = layoutInfo.width * zoom;
  89. rootSize.height = layoutInfo.height * zoom;
  90. }
  91. var sort_1 = seriesOption.sort;
  92. if (sort_1 && sort_1 !== 'asc' && sort_1 !== 'desc') {
  93. // Default to be desc order.
  94. sort_1 = 'desc';
  95. }
  96. var options = {
  97. squareRatio: seriesOption.squareRatio,
  98. sort: sort_1,
  99. leafDepth: seriesOption.leafDepth
  100. };
  101. // layout should be cleared because using updateView but not update.
  102. viewRoot.hostTree.clearLayouts();
  103. // TODO
  104. // optimize: if out of view clip, do not layout.
  105. // But take care that if do not render node out of view clip,
  106. // how to calculate start po
  107. var viewRootLayout_1 = {
  108. x: 0,
  109. y: 0,
  110. width: rootSize.width,
  111. height: rootSize.height,
  112. area: rootSize.width * rootSize.height
  113. };
  114. viewRoot.setLayout(viewRootLayout_1);
  115. squarify(viewRoot, options, false, 0);
  116. // Supplement layout.
  117. viewRootLayout_1 = viewRoot.getLayout();
  118. each(viewAbovePath, function (node, index) {
  119. var childValue = (viewAbovePath[index + 1] || viewRoot).getValue();
  120. node.setLayout(zrUtil.extend({
  121. dataExtent: [childValue, childValue],
  122. borderWidth: 0,
  123. upperHeight: 0
  124. }, viewRootLayout_1));
  125. });
  126. }
  127. var treeRoot = seriesModel.getData().tree.root;
  128. treeRoot.setLayout(calculateRootPosition(layoutInfo, rootRect, targetInfo), true);
  129. seriesModel.setLayoutInfo(layoutInfo);
  130. // FIXME: narrow down pruning boungding rect.
  131. // Currently ec width/height is used becuases clip is not supported.
  132. prunning(treeRoot,
  133. // Transform to base element coordinate system.
  134. new BoundingRect(-layoutInfo.x, -layoutInfo.y, api.getWidth(), api.getHeight()), viewAbovePath, viewRoot, 0);
  135. }
  136. };
  137. /**
  138. * Layout treemap with squarify algorithm.
  139. * The original presentation of this algorithm
  140. * was made by Mark Bruls, Kees Huizing, and Jarke J. van Wijk
  141. * <https://graphics.ethz.ch/teaching/scivis_common/Literature/squarifiedTreeMaps.pdf>.
  142. * The implementation of this algorithm was originally copied from "d3.js"
  143. * <https://github.com/d3/d3/blob/9cc9a875e636a1dcf36cc1e07bdf77e1ad6e2c74/src/layout/treemap.js>
  144. * with some modifications made for this program.
  145. * See the license statement at the head of this file.
  146. */
  147. function squarify(node, options, hideChildren, depth) {
  148. var width;
  149. var height;
  150. if (node.isRemoved()) {
  151. return;
  152. }
  153. var thisLayout = node.getLayout();
  154. width = thisLayout.width;
  155. height = thisLayout.height;
  156. // Considering border and gap
  157. var nodeModel = node.getModel();
  158. var borderWidth = nodeModel.get(PATH_BORDER_WIDTH);
  159. var halfGapWidth = nodeModel.get(PATH_GAP_WIDTH) / 2;
  160. var upperLabelHeight = getUpperLabelHeight(nodeModel);
  161. var upperHeight = Math.max(borderWidth, upperLabelHeight);
  162. var layoutOffset = borderWidth - halfGapWidth;
  163. var layoutOffsetUpper = upperHeight - halfGapWidth;
  164. node.setLayout({
  165. borderWidth: borderWidth,
  166. upperHeight: upperHeight,
  167. upperLabelHeight: upperLabelHeight
  168. }, true);
  169. width = mathMax(width - 2 * layoutOffset, 0);
  170. height = mathMax(height - layoutOffset - layoutOffsetUpper, 0);
  171. var totalArea = width * height;
  172. var viewChildren = initChildren(node, nodeModel, totalArea, options, hideChildren, depth);
  173. if (!viewChildren.length) {
  174. return;
  175. }
  176. var rect = {
  177. x: layoutOffset,
  178. y: layoutOffsetUpper,
  179. width: width,
  180. height: height
  181. };
  182. var rowFixedLength = mathMin(width, height);
  183. var best = Infinity; // the best row score so far
  184. var row = [];
  185. row.area = 0;
  186. for (var i = 0, len = viewChildren.length; i < len;) {
  187. var child = viewChildren[i];
  188. row.push(child);
  189. row.area += child.getLayout().area;
  190. var score = worst(row, rowFixedLength, options.squareRatio);
  191. // continue with this orientation
  192. if (score <= best) {
  193. i++;
  194. best = score;
  195. }
  196. // abort, and try a different orientation
  197. else {
  198. row.area -= row.pop().getLayout().area;
  199. position(row, rowFixedLength, rect, halfGapWidth, false);
  200. rowFixedLength = mathMin(rect.width, rect.height);
  201. row.length = row.area = 0;
  202. best = Infinity;
  203. }
  204. }
  205. if (row.length) {
  206. position(row, rowFixedLength, rect, halfGapWidth, true);
  207. }
  208. if (!hideChildren) {
  209. var childrenVisibleMin = nodeModel.get('childrenVisibleMin');
  210. if (childrenVisibleMin != null && totalArea < childrenVisibleMin) {
  211. hideChildren = true;
  212. }
  213. }
  214. for (var i = 0, len = viewChildren.length; i < len; i++) {
  215. squarify(viewChildren[i], options, hideChildren, depth + 1);
  216. }
  217. }
  218. /**
  219. * Set area to each child, and calculate data extent for visual coding.
  220. */
  221. function initChildren(node, nodeModel, totalArea, options, hideChildren, depth) {
  222. var viewChildren = node.children || [];
  223. var orderBy = options.sort;
  224. orderBy !== 'asc' && orderBy !== 'desc' && (orderBy = null);
  225. var overLeafDepth = options.leafDepth != null && options.leafDepth <= depth;
  226. // leafDepth has higher priority.
  227. if (hideChildren && !overLeafDepth) {
  228. return node.viewChildren = [];
  229. }
  230. // Sort children, order by desc.
  231. viewChildren = zrUtil.filter(viewChildren, function (child) {
  232. return !child.isRemoved();
  233. });
  234. sort(viewChildren, orderBy);
  235. var info = statistic(nodeModel, viewChildren, orderBy);
  236. if (info.sum === 0) {
  237. return node.viewChildren = [];
  238. }
  239. info.sum = filterByThreshold(nodeModel, totalArea, info.sum, orderBy, viewChildren);
  240. if (info.sum === 0) {
  241. return node.viewChildren = [];
  242. }
  243. // Set area to each child.
  244. for (var i = 0, len = viewChildren.length; i < len; i++) {
  245. var area = viewChildren[i].getValue() / info.sum * totalArea;
  246. // Do not use setLayout({...}, true), because it is needed to clear last layout.
  247. viewChildren[i].setLayout({
  248. area: area
  249. });
  250. }
  251. if (overLeafDepth) {
  252. viewChildren.length && node.setLayout({
  253. isLeafRoot: true
  254. }, true);
  255. viewChildren.length = 0;
  256. }
  257. node.viewChildren = viewChildren;
  258. node.setLayout({
  259. dataExtent: info.dataExtent
  260. }, true);
  261. return viewChildren;
  262. }
  263. /**
  264. * Consider 'visibleMin'. Modify viewChildren and get new sum.
  265. */
  266. function filterByThreshold(nodeModel, totalArea, sum, orderBy, orderedChildren) {
  267. // visibleMin is not supported yet when no option.sort.
  268. if (!orderBy) {
  269. return sum;
  270. }
  271. var visibleMin = nodeModel.get('visibleMin');
  272. var len = orderedChildren.length;
  273. var deletePoint = len;
  274. // Always travel from little value to big value.
  275. for (var i = len - 1; i >= 0; i--) {
  276. var value = orderedChildren[orderBy === 'asc' ? len - i - 1 : i].getValue();
  277. if (value / sum * totalArea < visibleMin) {
  278. deletePoint = i;
  279. sum -= value;
  280. }
  281. }
  282. orderBy === 'asc' ? orderedChildren.splice(0, len - deletePoint) : orderedChildren.splice(deletePoint, len - deletePoint);
  283. return sum;
  284. }
  285. /**
  286. * Sort
  287. */
  288. function sort(viewChildren, orderBy) {
  289. if (orderBy) {
  290. viewChildren.sort(function (a, b) {
  291. var diff = orderBy === 'asc' ? a.getValue() - b.getValue() : b.getValue() - a.getValue();
  292. return diff === 0 ? orderBy === 'asc' ? a.dataIndex - b.dataIndex : b.dataIndex - a.dataIndex : diff;
  293. });
  294. }
  295. return viewChildren;
  296. }
  297. /**
  298. * Statistic
  299. */
  300. function statistic(nodeModel, children, orderBy) {
  301. // Calculate sum.
  302. var sum = 0;
  303. for (var i = 0, len = children.length; i < len; i++) {
  304. sum += children[i].getValue();
  305. }
  306. // Statistic data extent for latter visual coding.
  307. // Notice: data extent should be calculate based on raw children
  308. // but not filtered view children, otherwise visual mapping will not
  309. // be stable when zoom (where children is filtered by visibleMin).
  310. var dimension = nodeModel.get('visualDimension');
  311. var dataExtent;
  312. // The same as area dimension.
  313. if (!children || !children.length) {
  314. dataExtent = [NaN, NaN];
  315. } else if (dimension === 'value' && orderBy) {
  316. dataExtent = [children[children.length - 1].getValue(), children[0].getValue()];
  317. orderBy === 'asc' && dataExtent.reverse();
  318. }
  319. // Other dimension.
  320. else {
  321. dataExtent = initExtentForUnion();
  322. each(children, function (child) {
  323. var value = child.getValue(dimension);
  324. value < dataExtent[0] && (dataExtent[0] = value);
  325. value > dataExtent[1] && (dataExtent[1] = value);
  326. });
  327. }
  328. return {
  329. sum: sum,
  330. dataExtent: dataExtent
  331. };
  332. }
  333. /**
  334. * Computes the score for the specified row,
  335. * as the worst aspect ratio.
  336. */
  337. function worst(row, rowFixedLength, ratio) {
  338. var areaMax = 0;
  339. var areaMin = Infinity;
  340. for (var i = 0, area = void 0, len = row.length; i < len; i++) {
  341. area = row[i].getLayout().area;
  342. if (area) {
  343. area < areaMin && (areaMin = area);
  344. area > areaMax && (areaMax = area);
  345. }
  346. }
  347. var squareArea = row.area * row.area;
  348. var f = rowFixedLength * rowFixedLength * ratio;
  349. return squareArea ? mathMax(f * areaMax / squareArea, squareArea / (f * areaMin)) : Infinity;
  350. }
  351. /**
  352. * Positions the specified row of nodes. Modifies `rect`.
  353. */
  354. function position(row, rowFixedLength, rect, halfGapWidth, flush) {
  355. // When rowFixedLength === rect.width,
  356. // it is horizontal subdivision,
  357. // rowFixedLength is the width of the subdivision,
  358. // rowOtherLength is the height of the subdivision,
  359. // and nodes will be positioned from left to right.
  360. // wh[idx0WhenH] means: when horizontal,
  361. // wh[idx0WhenH] => wh[0] => 'width'.
  362. // xy[idx1WhenH] => xy[1] => 'y'.
  363. var idx0WhenH = rowFixedLength === rect.width ? 0 : 1;
  364. var idx1WhenH = 1 - idx0WhenH;
  365. var xy = ['x', 'y'];
  366. var wh = ['width', 'height'];
  367. var last = rect[xy[idx0WhenH]];
  368. var rowOtherLength = rowFixedLength ? row.area / rowFixedLength : 0;
  369. if (flush || rowOtherLength > rect[wh[idx1WhenH]]) {
  370. rowOtherLength = rect[wh[idx1WhenH]]; // over+underflow
  371. }
  372. for (var i = 0, rowLen = row.length; i < rowLen; i++) {
  373. var node = row[i];
  374. var nodeLayout = {};
  375. var step = rowOtherLength ? node.getLayout().area / rowOtherLength : 0;
  376. var wh1 = nodeLayout[wh[idx1WhenH]] = mathMax(rowOtherLength - 2 * halfGapWidth, 0);
  377. // We use Math.max/min to avoid negative width/height when considering gap width.
  378. var remain = rect[xy[idx0WhenH]] + rect[wh[idx0WhenH]] - last;
  379. var modWH = i === rowLen - 1 || remain < step ? remain : step;
  380. var wh0 = nodeLayout[wh[idx0WhenH]] = mathMax(modWH - 2 * halfGapWidth, 0);
  381. nodeLayout[xy[idx1WhenH]] = rect[xy[idx1WhenH]] + mathMin(halfGapWidth, wh1 / 2);
  382. nodeLayout[xy[idx0WhenH]] = last + mathMin(halfGapWidth, wh0 / 2);
  383. last += modWH;
  384. node.setLayout(nodeLayout, true);
  385. }
  386. rect[xy[idx1WhenH]] += rowOtherLength;
  387. rect[wh[idx1WhenH]] -= rowOtherLength;
  388. }
  389. // Return containerSize as default.
  390. function estimateRootSize(seriesModel, targetInfo, viewRoot, containerSize) {
  391. // If targetInfo.node exists, we zoom to the node,
  392. // so estimate whole width and height by target node.
  393. var currNode = (targetInfo || {}).node;
  394. var containerWidth = containerSize.width;
  395. var containerHeight = containerSize.height;
  396. var defaultSize = zrUtil.extend({}, containerSize);
  397. if (!currNode || currNode === viewRoot) {
  398. return defaultSize;
  399. }
  400. var parent;
  401. var viewArea = containerWidth * containerHeight;
  402. var area = viewArea * seriesModel.option.zoomToNodeRatio;
  403. while (parent = currNode.parentNode) {
  404. // jshint ignore:line
  405. var sum = 0;
  406. var siblings = parent.children;
  407. for (var i = 0, len = siblings.length; i < len; i++) {
  408. sum += siblings[i].getValue();
  409. }
  410. var currNodeValue = currNode.getValue();
  411. if (currNodeValue === 0) {
  412. return defaultSize;
  413. }
  414. area *= sum / currNodeValue;
  415. // Considering border, suppose aspect ratio is 1.
  416. var parentModel = parent.getModel();
  417. var borderWidth = parentModel.get(PATH_BORDER_WIDTH);
  418. var upperHeight = Math.max(borderWidth, getUpperLabelHeight(parentModel));
  419. area += 4 * borderWidth * borderWidth + (3 * borderWidth + upperHeight) * Math.pow(area, 0.5);
  420. area > MAX_SAFE_INTEGER && (area = MAX_SAFE_INTEGER);
  421. currNode = parent;
  422. }
  423. area < viewArea && (area = viewArea);
  424. var scale = Math.pow(area / viewArea, 0.5);
  425. return {
  426. width: containerWidth * scale,
  427. height: containerHeight * scale
  428. };
  429. }
  430. // Root position based on coord of containerGroup
  431. function calculateRootPosition(layoutInfo, rootRect, targetInfo) {
  432. if (rootRect) {
  433. return {
  434. x: rootRect.x,
  435. y: rootRect.y
  436. };
  437. }
  438. var defaultPosition = {
  439. x: 0,
  440. y: 0
  441. };
  442. if (!targetInfo) {
  443. return defaultPosition;
  444. }
  445. // If targetInfo is fetched by 'retrieveTargetInfo',
  446. // old tree and new tree are the same tree,
  447. // so the node still exists and we can visit it.
  448. var targetNode = targetInfo.node;
  449. var layout = targetNode.getLayout();
  450. if (!layout) {
  451. return defaultPosition;
  452. }
  453. // Transform coord from local to container.
  454. var targetCenter = [layout.width / 2, layout.height / 2];
  455. var node = targetNode;
  456. while (node) {
  457. var nodeLayout = node.getLayout();
  458. targetCenter[0] += nodeLayout.x;
  459. targetCenter[1] += nodeLayout.y;
  460. node = node.parentNode;
  461. }
  462. return {
  463. x: layoutInfo.width / 2 - targetCenter[0],
  464. y: layoutInfo.height / 2 - targetCenter[1]
  465. };
  466. }
  467. // Mark nodes visible for prunning when visual coding and rendering.
  468. // Prunning depends on layout and root position, so we have to do it after layout.
  469. function prunning(node, clipRect, viewAbovePath, viewRoot, depth) {
  470. var nodeLayout = node.getLayout();
  471. var nodeInViewAbovePath = viewAbovePath[depth];
  472. var isAboveViewRoot = nodeInViewAbovePath && nodeInViewAbovePath === node;
  473. if (nodeInViewAbovePath && !isAboveViewRoot || depth === viewAbovePath.length && node !== viewRoot) {
  474. return;
  475. }
  476. node.setLayout({
  477. // isInView means: viewRoot sub tree + viewAbovePath
  478. isInView: true,
  479. // invisible only means: outside view clip so that the node can not
  480. // see but still layout for animation preparation but not render.
  481. invisible: !isAboveViewRoot && !clipRect.intersect(nodeLayout),
  482. isAboveViewRoot: isAboveViewRoot
  483. }, true);
  484. // Transform to child coordinate.
  485. var childClipRect = new BoundingRect(clipRect.x - nodeLayout.x, clipRect.y - nodeLayout.y, clipRect.width, clipRect.height);
  486. each(node.viewChildren || [], function (child) {
  487. prunning(child, childClipRect, viewAbovePath, viewRoot, depth + 1);
  488. });
  489. }
  490. function getUpperLabelHeight(model) {
  491. return model.get(PATH_UPPER_LABEL_SHOW) ? model.get(PATH_UPPER_LABEL_HEIGHT) : 0;
  492. }
  493. export function calculateCurrentZoom(baseSize, currSize) {
  494. // width ratio and height ratio are suppposed to be the same.
  495. return currSize.width / baseSize.width || currSize.height / baseSize.height || 1;
  496. }
  497. export function treemapClampZoom(zoom, seriesModel) {
  498. return clampByZoomLimit(zoom, seriesModel.get('scaleLimit', true));
  499. }