labelLayoutHelper.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  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 { OrientedBoundingRect, WH, XY, ensureCopyRect, ensureCopyTransform, expandOrShrinkRect, isBoundingRectAxisAligned } from '../util/graphic.js';
  41. import { LabelMarginType } from './labelStyle.js';
  42. var LABEL_LAYOUT_BASE_PROPS = ['label', 'labelLine', 'layoutOption', 'priority', 'defaultAttr', 'marginForce', 'minMarginForce', 'marginDefault', 'suggestIgnore'];
  43. var LABEL_LAYOUT_DIRTY_BIT_OTHERS = 1;
  44. var LABEL_LAYOUT_DIRTY_BIT_OBB = 2;
  45. var LABEL_LAYOUT_DIRTY_ALL = LABEL_LAYOUT_DIRTY_BIT_OTHERS | LABEL_LAYOUT_DIRTY_BIT_OBB;
  46. export function setLabelLayoutDirty(labelGeometry, dirtyOrClear, dirtyBits) {
  47. dirtyBits = dirtyBits || LABEL_LAYOUT_DIRTY_ALL;
  48. dirtyOrClear ? labelGeometry.dirty |= dirtyBits : labelGeometry.dirty &= ~dirtyBits;
  49. }
  50. function isLabelLayoutDirty(labelGeometry, dirtyBits) {
  51. dirtyBits = dirtyBits || LABEL_LAYOUT_DIRTY_ALL;
  52. return labelGeometry.dirty == null || !!(labelGeometry.dirty & dirtyBits);
  53. }
  54. /**
  55. * [CAUTION]
  56. * - No auto dirty propagation mechanism yet. If the transform of the raw label or any of its ancestors is
  57. * changed, must sync the changes to the props of `LabelGeometry` by:
  58. * either explicitly call:
  59. * `setLabelLayoutDirty(labelLayout, true); ensureLabelLayoutWithGeometry(labelLayout);`
  60. * or call (if only translation is performed):
  61. * `labelLayoutApplyTranslation(labelLayout);`
  62. * - `label.ignore` is not necessarily falsy, and not considered in computing `LabelGeometry`,
  63. * since it might be modified by some overlap resolving handling.
  64. * - To duplicate or make a variation:
  65. * use `newLabelLayoutWithGeometry`.
  66. *
  67. * The result can also be the input of this method.
  68. * @return `NullUndefined` if and only if `labelLayout` is `NullUndefined`.
  69. */
  70. export function ensureLabelLayoutWithGeometry(labelLayout) {
  71. if (!labelLayout) {
  72. return;
  73. }
  74. if (isLabelLayoutDirty(labelLayout)) {
  75. computeLabelGeometry(labelLayout, labelLayout.label, labelLayout);
  76. }
  77. return labelLayout;
  78. }
  79. /**
  80. * The props in `out` will be filled if existing, or created.
  81. */
  82. export function computeLabelGeometry(out, label, opt) {
  83. // [CAUTION] These props may be modified directly for performance consideration,
  84. // therefore, do not output the internal data structure of zrender Element.
  85. var rawTransform = label.getComputedTransform();
  86. out.transform = ensureCopyTransform(out.transform, rawTransform);
  87. // NOTE: should call `getBoundingRect` after `getComputedTransform`, or may get an inaccurate bounding rect.
  88. // The reason is that `getComputedTransform` calls `__host.updateInnerText()` internally, which updates the label
  89. // by `textConfig` mounted on the host.
  90. // PENDING: add a dirty bit for that in zrender?
  91. var outLocalRect = out.localRect = ensureCopyRect(out.localRect, label.getBoundingRect());
  92. var labelStyleExt = label.style;
  93. var margin = labelStyleExt.margin;
  94. var marginForce = opt && opt.marginForce;
  95. var minMarginForce = opt && opt.minMarginForce;
  96. var marginDefault = opt && opt.marginDefault;
  97. var marginType = labelStyleExt.__marginType;
  98. if (marginType == null && marginDefault) {
  99. margin = marginDefault;
  100. marginType = LabelMarginType.textMargin;
  101. }
  102. // `textMargin` and `minMargin` can not exist both.
  103. for (var i = 0; i < 4; i++) {
  104. _tmpLabelMargin[i] = marginType === LabelMarginType.minMargin && minMarginForce && minMarginForce[i] != null ? minMarginForce[i] : marginForce && marginForce[i] != null ? marginForce[i] : margin ? margin[i] : 0;
  105. }
  106. if (marginType === LabelMarginType.textMargin) {
  107. expandOrShrinkRect(outLocalRect, _tmpLabelMargin, false, false);
  108. }
  109. var outGlobalRect = out.rect = ensureCopyRect(out.rect, outLocalRect);
  110. if (rawTransform) {
  111. outGlobalRect.applyTransform(rawTransform);
  112. }
  113. // Notice: label.style.margin is actually `minMargin / 2`, handled by `setTextStyleCommon`.
  114. if (marginType === LabelMarginType.minMargin) {
  115. expandOrShrinkRect(outGlobalRect, _tmpLabelMargin, false, false);
  116. }
  117. out.axisAligned = isBoundingRectAxisAligned(rawTransform);
  118. (out.label = out.label || {}).ignore = label.ignore;
  119. setLabelLayoutDirty(out, false);
  120. setLabelLayoutDirty(out, true, LABEL_LAYOUT_DIRTY_BIT_OBB);
  121. // Do not remove `obb` (if existing) for reuse, just reset the dirty bit.
  122. return out;
  123. }
  124. var _tmpLabelMargin = [0, 0, 0, 0];
  125. /**
  126. * The props in `out` will be filled if existing, or created.
  127. */
  128. export function computeLabelGeometry2(out, rawLocalRect, rawTransform) {
  129. out.transform = ensureCopyTransform(out.transform, rawTransform);
  130. out.localRect = ensureCopyRect(out.localRect, rawLocalRect);
  131. out.rect = ensureCopyRect(out.rect, rawLocalRect);
  132. if (rawTransform) {
  133. out.rect.applyTransform(rawTransform);
  134. }
  135. out.axisAligned = isBoundingRectAxisAligned(rawTransform);
  136. out.obb = undefined; // Reset to undefined, will be created by `ensureOBB` when using.
  137. (out.label = out.label || {}).ignore = false;
  138. return out;
  139. }
  140. /**
  141. * This is a shortcut of
  142. * ```js
  143. * labelLayout.label.x = newX;
  144. * labelLayout.label.y = newY;
  145. * setLabelLayoutDirty(labelLayout, true);
  146. * ensureLabelLayoutWithGeometry(labelLayout);
  147. * ```
  148. * and provide better performance in this common case.
  149. */
  150. export function labelLayoutApplyTranslation(labelLayout, offset) {
  151. if (!labelLayout) {
  152. return;
  153. }
  154. labelLayout.label.x += offset.x;
  155. labelLayout.label.y += offset.y;
  156. labelLayout.label.markRedraw();
  157. var transform = labelLayout.transform;
  158. if (transform) {
  159. transform[4] += offset.x;
  160. transform[5] += offset.y;
  161. }
  162. var globalRect = labelLayout.rect;
  163. if (globalRect) {
  164. globalRect.x += offset.x;
  165. globalRect.y += offset.y;
  166. }
  167. var obb = labelLayout.obb;
  168. if (obb) {
  169. obb.fromBoundingRect(labelLayout.localRect, transform);
  170. }
  171. }
  172. /**
  173. * To duplicate or make a variation of a label layout.
  174. * Copy the only relevant properties to avoid the conflict or wrongly reuse of the props of `LabelLayoutWithGeometry`.
  175. */
  176. export function newLabelLayoutWithGeometry(newBaseWithDefaults, source) {
  177. for (var i = 0; i < LABEL_LAYOUT_BASE_PROPS.length; i++) {
  178. var prop = LABEL_LAYOUT_BASE_PROPS[i];
  179. if (newBaseWithDefaults[prop] == null) {
  180. newBaseWithDefaults[prop] = source[prop];
  181. }
  182. }
  183. return ensureLabelLayoutWithGeometry(newBaseWithDefaults);
  184. }
  185. /**
  186. * Create obb if no one, can cache it.
  187. */
  188. function ensureOBB(labelGeometry) {
  189. var obb = labelGeometry.obb;
  190. if (!obb || isLabelLayoutDirty(labelGeometry, LABEL_LAYOUT_DIRTY_BIT_OBB)) {
  191. labelGeometry.obb = obb = obb || new OrientedBoundingRect();
  192. obb.fromBoundingRect(labelGeometry.localRect, labelGeometry.transform);
  193. setLabelLayoutDirty(labelGeometry, false, LABEL_LAYOUT_DIRTY_BIT_OBB);
  194. }
  195. return obb;
  196. }
  197. /**
  198. * Adjust labels on x/y direction to avoid overlap.
  199. *
  200. * PENDING: the current implementation is based on the global bounding rect rather than the local rect,
  201. * which may be not preferable in some edge cases when the label has rotation, but works for most cases,
  202. * since rotation is unnecessary when there is sufficient space, while squeezing is applied regardless
  203. * of overlapping when there is no enough space.
  204. *
  205. * NOTICE:
  206. * - The input `list` and its content will be modified (sort, label.x/y, rect).
  207. * - The caller should sync the modifications to the other parts by
  208. * `setLabelLayoutDirty` and `ensureLabelLayoutWithGeometry` if needed.
  209. *
  210. * @return adjusted
  211. */
  212. export function shiftLayoutOnXY(list, xyDimIdx,
  213. // 0 for x, 1 for y
  214. minBound,
  215. // for x, leftBound; for y, topBound
  216. maxBound,
  217. // for x, rightBound; for y, bottomBound
  218. // If average the shifts on all labels and add them to 0
  219. // TODO: Not sure if should enable it.
  220. // Pros: The angle of lines will distribute more equally
  221. // Cons: In some layout. It may not what user wanted. like in pie. the label of last sector is usually changed unexpectedly.
  222. balanceShift) {
  223. var len = list.length;
  224. var xyDim = XY[xyDimIdx];
  225. var sizeDim = WH[xyDimIdx];
  226. if (len < 2) {
  227. return false;
  228. }
  229. list.sort(function (a, b) {
  230. return a.rect[xyDim] - b.rect[xyDim];
  231. });
  232. var lastPos = 0;
  233. var delta;
  234. var adjusted = false;
  235. // const shifts = [];
  236. var totalShifts = 0;
  237. for (var i = 0; i < len; i++) {
  238. var item = list[i];
  239. var rect = item.rect;
  240. delta = rect[xyDim] - lastPos;
  241. if (delta < 0) {
  242. // shiftForward(i, len, -delta);
  243. rect[xyDim] -= delta;
  244. item.label[xyDim] -= delta;
  245. adjusted = true;
  246. }
  247. var shift = Math.max(-delta, 0);
  248. // shifts.push(shift);
  249. totalShifts += shift;
  250. lastPos = rect[xyDim] + rect[sizeDim];
  251. }
  252. if (totalShifts > 0 && balanceShift) {
  253. // Shift back to make the distribution more equally.
  254. shiftList(-totalShifts / len, 0, len);
  255. }
  256. // TODO bleedMargin?
  257. var first = list[0];
  258. var last = list[len - 1];
  259. var minGap;
  260. var maxGap;
  261. updateMinMaxGap();
  262. // If ends exceed two bounds, squeeze at most 80%, then take the gap of two bounds.
  263. minGap < 0 && squeezeGaps(-minGap, 0.8);
  264. maxGap < 0 && squeezeGaps(maxGap, 0.8);
  265. updateMinMaxGap();
  266. takeBoundsGap(minGap, maxGap, 1);
  267. takeBoundsGap(maxGap, minGap, -1);
  268. // Handle bailout when there is not enough space.
  269. updateMinMaxGap();
  270. if (minGap < 0) {
  271. squeezeWhenBailout(-minGap);
  272. }
  273. if (maxGap < 0) {
  274. squeezeWhenBailout(maxGap);
  275. }
  276. function updateMinMaxGap() {
  277. minGap = first.rect[xyDim] - minBound;
  278. maxGap = maxBound - last.rect[xyDim] - last.rect[sizeDim];
  279. }
  280. function takeBoundsGap(gapThisBound, gapOtherBound, moveDir) {
  281. if (gapThisBound < 0) {
  282. // Move from other gap if can.
  283. var moveFromMaxGap = Math.min(gapOtherBound, -gapThisBound);
  284. if (moveFromMaxGap > 0) {
  285. shiftList(moveFromMaxGap * moveDir, 0, len);
  286. var remained = moveFromMaxGap + gapThisBound;
  287. if (remained < 0) {
  288. squeezeGaps(-remained * moveDir, 1);
  289. }
  290. } else {
  291. squeezeGaps(-gapThisBound * moveDir, 1);
  292. }
  293. }
  294. }
  295. function shiftList(delta, start, end) {
  296. if (delta !== 0) {
  297. adjusted = true;
  298. }
  299. for (var i = start; i < end; i++) {
  300. var item = list[i];
  301. var rect = item.rect;
  302. rect[xyDim] += delta;
  303. item.label[xyDim] += delta;
  304. }
  305. }
  306. // Squeeze gaps if the labels exceed margin.
  307. function squeezeGaps(delta, maxSqeezePercent) {
  308. var gaps = [];
  309. var totalGaps = 0;
  310. for (var i = 1; i < len; i++) {
  311. var prevItemRect = list[i - 1].rect;
  312. var gap = Math.max(list[i].rect[xyDim] - prevItemRect[xyDim] - prevItemRect[sizeDim], 0);
  313. gaps.push(gap);
  314. totalGaps += gap;
  315. }
  316. if (!totalGaps) {
  317. return;
  318. }
  319. var squeezePercent = Math.min(Math.abs(delta) / totalGaps, maxSqeezePercent);
  320. if (delta > 0) {
  321. for (var i = 0; i < len - 1; i++) {
  322. // Distribute the shift delta to all gaps.
  323. var movement = gaps[i] * squeezePercent;
  324. // Forward
  325. shiftList(movement, 0, i + 1);
  326. }
  327. } else {
  328. // Backward
  329. for (var i = len - 1; i > 0; i--) {
  330. // Distribute the shift delta to all gaps.
  331. var movement = gaps[i - 1] * squeezePercent;
  332. shiftList(-movement, i, len);
  333. }
  334. }
  335. }
  336. /**
  337. * Squeeze to allow overlap if there is no more space available.
  338. * Let other overlapping strategy like hideOverlap do the job instead of keep exceeding the bounds.
  339. */
  340. function squeezeWhenBailout(delta) {
  341. var dir = delta < 0 ? -1 : 1;
  342. delta = Math.abs(delta);
  343. var moveForEachLabel = Math.ceil(delta / (len - 1));
  344. for (var i = 0; i < len - 1; i++) {
  345. if (dir > 0) {
  346. // Forward
  347. shiftList(moveForEachLabel, 0, i + 1);
  348. } else {
  349. // Backward
  350. shiftList(-moveForEachLabel, len - i - 1, len);
  351. }
  352. delta -= moveForEachLabel;
  353. if (delta <= 0) {
  354. return;
  355. }
  356. }
  357. }
  358. return adjusted;
  359. }
  360. /**
  361. * @see `SavedLabelAttr` in `LabelManager.ts`
  362. * @see `hideOverlap`
  363. */
  364. export function restoreIgnore(labelList) {
  365. for (var i = 0; i < labelList.length; i++) {
  366. var labelItem = labelList[i];
  367. var defaultAttr = labelItem.defaultAttr;
  368. var labelLine = labelItem.labelLine;
  369. labelItem.label.attr('ignore', defaultAttr.ignore);
  370. labelLine && labelLine.attr('ignore', defaultAttr.labelGuideIgnore);
  371. }
  372. }
  373. /**
  374. * [NOTICE - restore]:
  375. * 'series:layoutlabels' may be triggered during some shortcut passes, such as zooming in series.graph/geo
  376. * (`updateLabelLayout`), where the modified `Element` props should be restorable from `defaultAttr`.
  377. * @see `SavedLabelAttr` in `LabelManager.ts`
  378. * `restoreIgnore` can be called to perform the restore, if needed.
  379. *
  380. * [NOTICE - state]:
  381. * Regarding Element's states, this method is only designed for the normal state.
  382. * PENDING: although currently this method is effectively called in other states in `updateLabelLayout` case,
  383. * the bad case is not noticeable in the zooming scenario.
  384. */
  385. export function hideOverlap(labelList) {
  386. var displayedLabels = [];
  387. // TODO, render overflow visible first, put in the displayedLabels.
  388. labelList.sort(function (a, b) {
  389. return (b.suggestIgnore ? 1 : 0) - (a.suggestIgnore ? 1 : 0) || b.priority - a.priority;
  390. });
  391. function hideEl(el) {
  392. if (!el.ignore) {
  393. // Show on emphasis.
  394. var emphasisState = el.ensureState('emphasis');
  395. if (emphasisState.ignore == null) {
  396. emphasisState.ignore = false;
  397. }
  398. }
  399. el.ignore = true;
  400. }
  401. for (var i = 0; i < labelList.length; i++) {
  402. var labelItem = ensureLabelLayoutWithGeometry(labelList[i]);
  403. // The current `el.ignore` is involved, since some previous overlap
  404. // resolving strategies may have set `el.ignore` to true.
  405. if (labelItem.label.ignore) {
  406. continue;
  407. }
  408. var label = labelItem.label;
  409. var labelLine = labelItem.labelLine;
  410. // NOTICE: even when the with/height of globalRect of a label is 0, the label line should
  411. // still be displayed, since we should follow the concept of "truncation", meaning that
  412. // something exists even if it cannot be fully displayed. A visible label line is necessary
  413. // to allow users to get a tooltip with label info on hover.
  414. var overlapped = false;
  415. for (var j = 0; j < displayedLabels.length; j++) {
  416. if (labelIntersect(labelItem, displayedLabels[j], null, {
  417. touchThreshold: 0.05
  418. })) {
  419. overlapped = true;
  420. break;
  421. }
  422. }
  423. // TODO Callback to determine if this overlap should be handled?
  424. if (overlapped) {
  425. hideEl(label);
  426. labelLine && hideEl(labelLine);
  427. } else {
  428. displayedLabels.push(labelItem);
  429. }
  430. }
  431. }
  432. /**
  433. * Enable fast check for performance; use obb if inevitable.
  434. * If `mtv` is used, `targetLayoutInfo` can be moved based on the values filled into `mtv`.
  435. *
  436. * This method is based only on the current `Element` states (regardless of other states).
  437. * Typically this method (and the entire layout process) is performed in normal state.
  438. */
  439. export function labelIntersect(baseLayoutInfo, targetLayoutInfo, mtv, intersectOpt) {
  440. if (!baseLayoutInfo || !targetLayoutInfo) {
  441. return false;
  442. }
  443. if (baseLayoutInfo.label && baseLayoutInfo.label.ignore || targetLayoutInfo.label && targetLayoutInfo.label.ignore) {
  444. return false;
  445. }
  446. // Fast rejection.
  447. if (!baseLayoutInfo.rect.intersect(targetLayoutInfo.rect, mtv, intersectOpt)) {
  448. return false;
  449. }
  450. if (baseLayoutInfo.axisAligned && targetLayoutInfo.axisAligned) {
  451. return true; // obb is the same as the normal bounding rect.
  452. }
  453. return ensureOBB(baseLayoutInfo).intersect(ensureOBB(targetLayoutInfo), mtv, intersectOpt);
  454. }