echarts-nightly
Version:
Apache ECharts is a powerful, interactive charting and data visualization library for browser
410 lines (406 loc) • 14 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 { LabelMarginType } from '../util/types.js';
import { OrientedBoundingRect, expandOrShrinkRect, isBoundingRectAxisAligned } from '../util/graphic.js';
import { each, extend, retrieve2 } from 'zrender/lib/core/util.js';
import { normalizeCssArray } from '../util/format.js';
// Also prevent duck typing.
export var LABEL_LAYOUT_INFO_KIND_RAW = 1;
export var LABEL_LAYOUT_INFO_KIND_COMPUTED = 2;
export function createLabelLayoutList(rawList) {
var resultList = [];
each(rawList, function (raw) {
// PENDING: necessary?
raw = extend({}, raw);
var layoutInfo = prepareLabelLayoutInfo(raw);
if (layoutInfo) {
resultList.push(layoutInfo);
}
});
return resultList;
}
/**
* If `defaultAttr.ignore: true`, return `NullUndefined`.
* (the caller is reponsible for ensuring the label is always `ignore: true`.)
* Otherwise the `layoutInfo` will be modified and returned.
* `label.ignore` is not necessarily falsy, since it might be modified by some overlap resolving handling.
*
* The result can also be the input of this method.
*
* @see ensureLabelLayoutInfoComputed
*/
function prepareLabelLayoutInfo(layoutInfo) {
if (!layoutInfo || layoutInfo.defaultAttr.ignore) {
return;
}
var label = layoutInfo.label;
var ignoreMargin = layoutInfo.ignoreMargin;
var transform = label.getComputedTransform();
// NOTE: Get bounding rect after getComputedTransform, or label may not been updated by the host el.
var localRect = label.getBoundingRect();
var axisAligned = isBoundingRectAxisAligned(transform);
if (!ignoreMargin) {
localRect = applyTextMarginToLocalRect(label, localRect);
}
var globalRect = localRect.clone();
globalRect.applyTransform(transform);
if (!ignoreMargin && label.style.__marginType === LabelMarginType.minMargin) {
// `minMargin` only support number value.
var halfMinMargin = (label.style.margin || 0) / 2;
expandOrShrinkRect(globalRect, halfMinMargin, false, false);
}
var computed = layoutInfo;
computed.kind = LABEL_LAYOUT_INFO_KIND_COMPUTED,
// --- computed properties ---
computed.rect = globalRect;
computed.localRect = localRect;
computed.obb = null,
// will be created by `ensureOBB` when using.
computed.axisAligned = axisAligned;
computed.transform = transform;
return computed;
}
/**
* The reverse operation of `ensureLabelLayoutInfoComputedv`.
*/
export function rollbackToLabelLayoutInfoRaw(labelLayoutInfo) {
if (labelLayoutInfo == null) {
return;
}
var raw = labelLayoutInfo;
raw.kind = LABEL_LAYOUT_INFO_KIND_RAW;
return raw;
}
/**
* This method supports that the label layout info is not computed until needed,
* for performance consideration.
*
* [CAUTION]
* - If the raw label is changed, must call
* `ensureLabelLayoutInfoComputed(rollbackToLabelLayoutInfoRaw(layoutInfo))`
* to recreate the layout info.
* - Null checking is needed for the result. @see prepareLabelLayoutInfo
*
* Usage:
* To make a copy of labelLayoutInfo, simply:
* const layoutInfoCopy = rollbackToLabelLayoutInfoRaw(extends({}, someLabelLayoutInfo));
*/
export function ensureLabelLayoutInfoComputed(labelLayoutInfo) {
if (!labelLayoutInfo) {
return;
}
if (labelLayoutInfo.kind !== LABEL_LAYOUT_INFO_KIND_COMPUTED) {
labelLayoutInfo = prepareLabelLayoutInfo(labelLayoutInfo);
}
return labelLayoutInfo;
}
export function prepareIntersectionCheckInfo(localRect, transform) {
var globalRect = localRect.clone();
globalRect.applyTransform(transform);
return {
obb: null,
rect: globalRect,
localRect: localRect,
axisAligned: isBoundingRectAxisAligned(transform),
transform: transform
};
}
export function createSingleLayoutInfoComputed(el) {
return ensureLabelLayoutInfoComputed({
kind: LABEL_LAYOUT_INFO_KIND_RAW,
label: el,
priority: el.z2,
defaultAttr: {
ignore: el.ignore
}
});
}
/**
* Create obb if no one, can cache it.
*/
function ensureOBB(layoutInfo) {
return layoutInfo.obb || (layoutInfo.obb = new OrientedBoundingRect(layoutInfo.localRect, layoutInfo.transform));
}
/**
* The input localRect is never modified.
* The returned localRect maybe the input localRect.
*/
export function applyTextMarginToLocalRect(label, localRect) {
if (label.style.__marginType !== LabelMarginType.textMargin) {
return localRect;
}
var textMargin = normalizeCssArray(retrieve2(label.style.margin, [0, 0]));
localRect = localRect.clone();
expandOrShrinkRect(localRect, textMargin, false, false);
return localRect;
}
function shiftLayout(list, xyDim, sizeDim, minBound, maxBound, balanceShift) {
var len = list.length;
if (len < 2) {
return;
}
list.sort(function (a, b) {
return a.rect[xyDim] - b.rect[xyDim];
});
var lastPos = 0;
var delta;
var adjusted = false;
var 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;
}
/**
* Adjust labels on x direction to avoid overlap.
*/
export function shiftLayoutOnX(list, leftBound, rightBound,
// 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) {
return shiftLayout(list, 'x', 'width', leftBound, rightBound, balanceShift);
}
/**
* Adjust labels on y direction to avoid overlap.
*/
export function shiftLayoutOnY(list, topBound, bottomBound,
// If average the shifts on all labels and add them to 0
balanceShift) {
return shiftLayout(list, 'y', 'height', topBound, bottomBound, balanceShift);
}
/**
* [CAUTION]: the `label.ignore` in the input is not necessarily falsy.
* this method checks intersection regardless of current `ignore`,
* if no intersection, restore the `ignore` to `defaultAttr.ignore`.
* And `labelList` will be modified.
* Therefore, if some other overlap resolving strategy has ignored some elements,
* do not input them to this method.
* PENDING: review the diff between the requirements from LabelManager and AxisBuilder,
* and uniform the behavior?
*/
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 = ensureLabelLayoutInfoComputed(labelList[i]);
if (!labelItem || 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 {
label.attr('ignore', labelItem.defaultAttr.ignore);
labelLine && labelLine.attr('ignore', labelItem.defaultAttr.labelGuideIgnore);
displayedLabels.push(labelItem);
}
}
}
/**
* [NOTICE]:
* - `label.ignore` is not considered - no requirement so far.
* - `baseLayoutInfo` and `targetLayoutInfo` may be modified - obb may be created and saved.
*
* Enable fast check for performance; use obb if inevitable.
* If `mtv` is used, `targetLayoutInfo` can be moved based on the values filled into `mtv`.
*/
export function labelIntersect(baseLayoutInfo, targetLayoutInfo, mtv, intersectOpt) {
if (!baseLayoutInfo || !targetLayoutInfo) {
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);
}