echarts
Version:
Apache ECharts is a powerful, interactive charting and data visualization library for browser
458 lines (454 loc) • 17.2 kB
JavaScript
/*
* 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);
}