UNPKG

echarts

Version:

Apache ECharts is a powerful, interactive charting and data visualization library for browser

458 lines (454 loc) 17.2 kB
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * AUTO-GENERATED FILE. DO NOT MODIFY. */ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { OrientedBoundingRect, WH, XY, ensureCopyRect, ensureCopyTransform, expandOrShrinkRect, isBoundingRectAxisAligned } from '../util/graphic.js'; import { LabelMarginType } from './labelStyle.js'; var LABEL_LAYOUT_BASE_PROPS = ['label', 'labelLine', 'layoutOption', 'priority', 'defaultAttr', 'marginForce', 'minMarginForce', 'marginDefault', 'suggestIgnore']; var LABEL_LAYOUT_DIRTY_BIT_OTHERS = 1; var LABEL_LAYOUT_DIRTY_BIT_OBB = 2; var LABEL_LAYOUT_DIRTY_ALL = LABEL_LAYOUT_DIRTY_BIT_OTHERS | LABEL_LAYOUT_DIRTY_BIT_OBB; export function setLabelLayoutDirty(labelGeometry, dirtyOrClear, dirtyBits) { dirtyBits = dirtyBits || LABEL_LAYOUT_DIRTY_ALL; dirtyOrClear ? labelGeometry.dirty |= dirtyBits : labelGeometry.dirty &= ~dirtyBits; } function isLabelLayoutDirty(labelGeometry, dirtyBits) { dirtyBits = dirtyBits || LABEL_LAYOUT_DIRTY_ALL; return labelGeometry.dirty == null || !!(labelGeometry.dirty & dirtyBits); } /** * [CAUTION] * - No auto dirty propagation mechanism yet. If the transform of the raw label or any of its ancestors is * changed, must sync the changes to the props of `LabelGeometry` by: * either explicitly call: * `setLabelLayoutDirty(labelLayout, true); ensureLabelLayoutWithGeometry(labelLayout);` * or call (if only translation is performed): * `labelLayoutApplyTranslation(labelLayout);` * - `label.ignore` is not necessarily falsy, and not considered in computing `LabelGeometry`, * since it might be modified by some overlap resolving handling. * - To duplicate or make a variation: * use `newLabelLayoutWithGeometry`. * * The result can also be the input of this method. * @return `NullUndefined` if and only if `labelLayout` is `NullUndefined`. */ export function ensureLabelLayoutWithGeometry(labelLayout) { if (!labelLayout) { return; } if (isLabelLayoutDirty(labelLayout)) { computeLabelGeometry(labelLayout, labelLayout.label, labelLayout); } return labelLayout; } /** * The props in `out` will be filled if existing, or created. */ export function computeLabelGeometry(out, label, opt) { // [CAUTION] These props may be modified directly for performance consideration, // therefore, do not output the internal data structure of zrender Element. var rawTransform = label.getComputedTransform(); out.transform = ensureCopyTransform(out.transform, rawTransform); // NOTE: should call `getBoundingRect` after `getComputedTransform`, or may get an inaccurate bounding rect. // The reason is that `getComputedTransform` calls `__host.updateInnerText()` internally, which updates the label // by `textConfig` mounted on the host. // PENDING: add a dirty bit for that in zrender? var outLocalRect = out.localRect = ensureCopyRect(out.localRect, label.getBoundingRect()); var labelStyleExt = label.style; var margin = labelStyleExt.margin; var marginForce = opt && opt.marginForce; var minMarginForce = opt && opt.minMarginForce; var marginDefault = opt && opt.marginDefault; var marginType = labelStyleExt.__marginType; if (marginType == null && marginDefault) { margin = marginDefault; marginType = LabelMarginType.textMargin; } // `textMargin` and `minMargin` can not exist both. for (var i = 0; i < 4; i++) { _tmpLabelMargin[i] = marginType === LabelMarginType.minMargin && minMarginForce && minMarginForce[i] != null ? minMarginForce[i] : marginForce && marginForce[i] != null ? marginForce[i] : margin ? margin[i] : 0; } if (marginType === LabelMarginType.textMargin) { expandOrShrinkRect(outLocalRect, _tmpLabelMargin, false, false); } var outGlobalRect = out.rect = ensureCopyRect(out.rect, outLocalRect); if (rawTransform) { outGlobalRect.applyTransform(rawTransform); } // Notice: label.style.margin is actually `minMargin / 2`, handled by `setTextStyleCommon`. if (marginType === LabelMarginType.minMargin) { expandOrShrinkRect(outGlobalRect, _tmpLabelMargin, false, false); } out.axisAligned = isBoundingRectAxisAligned(rawTransform); (out.label = out.label || {}).ignore = label.ignore; setLabelLayoutDirty(out, false); setLabelLayoutDirty(out, true, LABEL_LAYOUT_DIRTY_BIT_OBB); // Do not remove `obb` (if existing) for reuse, just reset the dirty bit. return out; } var _tmpLabelMargin = [0, 0, 0, 0]; /** * The props in `out` will be filled if existing, or created. */ export function computeLabelGeometry2(out, rawLocalRect, rawTransform) { out.transform = ensureCopyTransform(out.transform, rawTransform); out.localRect = ensureCopyRect(out.localRect, rawLocalRect); out.rect = ensureCopyRect(out.rect, rawLocalRect); if (rawTransform) { out.rect.applyTransform(rawTransform); } out.axisAligned = isBoundingRectAxisAligned(rawTransform); out.obb = undefined; // Reset to undefined, will be created by `ensureOBB` when using. (out.label = out.label || {}).ignore = false; return out; } /** * This is a shortcut of * ```js * labelLayout.label.x = newX; * labelLayout.label.y = newY; * setLabelLayoutDirty(labelLayout, true); * ensureLabelLayoutWithGeometry(labelLayout); * ``` * and provide better performance in this common case. */ export function labelLayoutApplyTranslation(labelLayout, offset) { if (!labelLayout) { return; } labelLayout.label.x += offset.x; labelLayout.label.y += offset.y; labelLayout.label.markRedraw(); var transform = labelLayout.transform; if (transform) { transform[4] += offset.x; transform[5] += offset.y; } var globalRect = labelLayout.rect; if (globalRect) { globalRect.x += offset.x; globalRect.y += offset.y; } var obb = labelLayout.obb; if (obb) { obb.fromBoundingRect(labelLayout.localRect, transform); } } /** * To duplicate or make a variation of a label layout. * Copy the only relevant properties to avoid the conflict or wrongly reuse of the props of `LabelLayoutWithGeometry`. */ export function newLabelLayoutWithGeometry(newBaseWithDefaults, source) { for (var i = 0; i < LABEL_LAYOUT_BASE_PROPS.length; i++) { var prop = LABEL_LAYOUT_BASE_PROPS[i]; if (newBaseWithDefaults[prop] == null) { newBaseWithDefaults[prop] = source[prop]; } } return ensureLabelLayoutWithGeometry(newBaseWithDefaults); } /** * Create obb if no one, can cache it. */ function ensureOBB(labelGeometry) { var obb = labelGeometry.obb; if (!obb || isLabelLayoutDirty(labelGeometry, LABEL_LAYOUT_DIRTY_BIT_OBB)) { labelGeometry.obb = obb = obb || new OrientedBoundingRect(); obb.fromBoundingRect(labelGeometry.localRect, labelGeometry.transform); setLabelLayoutDirty(labelGeometry, false, LABEL_LAYOUT_DIRTY_BIT_OBB); } return obb; } /** * Adjust labels on x/y direction to avoid overlap. * * PENDING: the current implementation is based on the global bounding rect rather than the local rect, * which may be not preferable in some edge cases when the label has rotation, but works for most cases, * since rotation is unnecessary when there is sufficient space, while squeezing is applied regardless * of overlapping when there is no enough space. * * NOTICE: * - The input `list` and its content will be modified (sort, label.x/y, rect). * - The caller should sync the modifications to the other parts by * `setLabelLayoutDirty` and `ensureLabelLayoutWithGeometry` if needed. * * @return adjusted */ export function shiftLayoutOnXY(list, xyDimIdx, // 0 for x, 1 for y minBound, // for x, leftBound; for y, topBound maxBound, // for x, rightBound; for y, bottomBound // If average the shifts on all labels and add them to 0 // TODO: Not sure if should enable it. // Pros: The angle of lines will distribute more equally // Cons: In some layout. It may not what user wanted. like in pie. the label of last sector is usually changed unexpectedly. balanceShift) { var len = list.length; var xyDim = XY[xyDimIdx]; var sizeDim = WH[xyDimIdx]; if (len < 2) { return false; } list.sort(function (a, b) { return a.rect[xyDim] - b.rect[xyDim]; }); var lastPos = 0; var delta; var adjusted = false; // const shifts = []; var totalShifts = 0; for (var i = 0; i < len; i++) { var item = list[i]; var rect = item.rect; delta = rect[xyDim] - lastPos; if (delta < 0) { // shiftForward(i, len, -delta); rect[xyDim] -= delta; item.label[xyDim] -= delta; adjusted = true; } var shift = Math.max(-delta, 0); // shifts.push(shift); totalShifts += shift; lastPos = rect[xyDim] + rect[sizeDim]; } if (totalShifts > 0 && balanceShift) { // Shift back to make the distribution more equally. shiftList(-totalShifts / len, 0, len); } // TODO bleedMargin? var first = list[0]; var last = list[len - 1]; var minGap; var maxGap; updateMinMaxGap(); // If ends exceed two bounds, squeeze at most 80%, then take the gap of two bounds. minGap < 0 && squeezeGaps(-minGap, 0.8); maxGap < 0 && squeezeGaps(maxGap, 0.8); updateMinMaxGap(); takeBoundsGap(minGap, maxGap, 1); takeBoundsGap(maxGap, minGap, -1); // Handle bailout when there is not enough space. updateMinMaxGap(); if (minGap < 0) { squeezeWhenBailout(-minGap); } if (maxGap < 0) { squeezeWhenBailout(maxGap); } function updateMinMaxGap() { minGap = first.rect[xyDim] - minBound; maxGap = maxBound - last.rect[xyDim] - last.rect[sizeDim]; } function takeBoundsGap(gapThisBound, gapOtherBound, moveDir) { if (gapThisBound < 0) { // Move from other gap if can. var moveFromMaxGap = Math.min(gapOtherBound, -gapThisBound); if (moveFromMaxGap > 0) { shiftList(moveFromMaxGap * moveDir, 0, len); var remained = moveFromMaxGap + gapThisBound; if (remained < 0) { squeezeGaps(-remained * moveDir, 1); } } else { squeezeGaps(-gapThisBound * moveDir, 1); } } } function shiftList(delta, start, end) { if (delta !== 0) { adjusted = true; } for (var i = start; i < end; i++) { var item = list[i]; var rect = item.rect; rect[xyDim] += delta; item.label[xyDim] += delta; } } // Squeeze gaps if the labels exceed margin. function squeezeGaps(delta, maxSqeezePercent) { var gaps = []; var totalGaps = 0; for (var i = 1; i < len; i++) { var prevItemRect = list[i - 1].rect; var gap = Math.max(list[i].rect[xyDim] - prevItemRect[xyDim] - prevItemRect[sizeDim], 0); gaps.push(gap); totalGaps += gap; } if (!totalGaps) { return; } var squeezePercent = Math.min(Math.abs(delta) / totalGaps, maxSqeezePercent); if (delta > 0) { for (var i = 0; i < len - 1; i++) { // Distribute the shift delta to all gaps. var movement = gaps[i] * squeezePercent; // Forward shiftList(movement, 0, i + 1); } } else { // Backward for (var i = len - 1; i > 0; i--) { // Distribute the shift delta to all gaps. var movement = gaps[i - 1] * squeezePercent; shiftList(-movement, i, len); } } } /** * Squeeze to allow overlap if there is no more space available. * Let other overlapping strategy like hideOverlap do the job instead of keep exceeding the bounds. */ function squeezeWhenBailout(delta) { var dir = delta < 0 ? -1 : 1; delta = Math.abs(delta); var moveForEachLabel = Math.ceil(delta / (len - 1)); for (var i = 0; i < len - 1; i++) { if (dir > 0) { // Forward shiftList(moveForEachLabel, 0, i + 1); } else { // Backward shiftList(-moveForEachLabel, len - i - 1, len); } delta -= moveForEachLabel; if (delta <= 0) { return; } } } return adjusted; } /** * @see `SavedLabelAttr` in `LabelManager.ts` * @see `hideOverlap` */ export function restoreIgnore(labelList) { for (var i = 0; i < labelList.length; i++) { var labelItem = labelList[i]; var defaultAttr = labelItem.defaultAttr; var labelLine = labelItem.labelLine; labelItem.label.attr('ignore', defaultAttr.ignore); labelLine && labelLine.attr('ignore', defaultAttr.labelGuideIgnore); } } /** * [NOTICE - restore]: * 'series:layoutlabels' may be triggered during some shortcut passes, such as zooming in series.graph/geo * (`updateLabelLayout`), where the modified `Element` props should be restorable from `defaultAttr`. * @see `SavedLabelAttr` in `LabelManager.ts` * `restoreIgnore` can be called to perform the restore, if needed. * * [NOTICE - state]: * Regarding Element's states, this method is only designed for the normal state. * PENDING: although currently this method is effectively called in other states in `updateLabelLayout` case, * the bad case is not noticeable in the zooming scenario. */ export function hideOverlap(labelList) { var displayedLabels = []; // TODO, render overflow visible first, put in the displayedLabels. labelList.sort(function (a, b) { return (b.suggestIgnore ? 1 : 0) - (a.suggestIgnore ? 1 : 0) || b.priority - a.priority; }); function hideEl(el) { if (!el.ignore) { // Show on emphasis. var emphasisState = el.ensureState('emphasis'); if (emphasisState.ignore == null) { emphasisState.ignore = false; } } el.ignore = true; } for (var i = 0; i < labelList.length; i++) { var labelItem = ensureLabelLayoutWithGeometry(labelList[i]); // The current `el.ignore` is involved, since some previous overlap // resolving strategies may have set `el.ignore` to true. if (labelItem.label.ignore) { continue; } var label = labelItem.label; var labelLine = labelItem.labelLine; // NOTICE: even when the with/height of globalRect of a label is 0, the label line should // still be displayed, since we should follow the concept of "truncation", meaning that // something exists even if it cannot be fully displayed. A visible label line is necessary // to allow users to get a tooltip with label info on hover. var overlapped = false; for (var j = 0; j < displayedLabels.length; j++) { if (labelIntersect(labelItem, displayedLabels[j], null, { touchThreshold: 0.05 })) { overlapped = true; break; } } // TODO Callback to determine if this overlap should be handled? if (overlapped) { hideEl(label); labelLine && hideEl(labelLine); } else { displayedLabels.push(labelItem); } } } /** * Enable fast check for performance; use obb if inevitable. * If `mtv` is used, `targetLayoutInfo` can be moved based on the values filled into `mtv`. * * This method is based only on the current `Element` states (regardless of other states). * Typically this method (and the entire layout process) is performed in normal state. */ export function labelIntersect(baseLayoutInfo, targetLayoutInfo, mtv, intersectOpt) { if (!baseLayoutInfo || !targetLayoutInfo) { return false; } if (baseLayoutInfo.label && baseLayoutInfo.label.ignore || targetLayoutInfo.label && targetLayoutInfo.label.ignore) { return false; } // Fast rejection. if (!baseLayoutInfo.rect.intersect(targetLayoutInfo.rect, mtv, intersectOpt)) { return false; } if (baseLayoutInfo.axisAligned && targetLayoutInfo.axisAligned) { return true; // obb is the same as the normal bounding rect. } return ensureOBB(baseLayoutInfo).intersect(ensureOBB(targetLayoutInfo), mtv, intersectOpt); }