提交 ca57e803 编写于 作者: P pissang

feat(label: add shift-x, shift-y option for moveOverlap in labelLayout

上级 65f4fc45
......@@ -72,15 +72,10 @@ function adjustSingleSide(
list[j].y += delta;
adjusted = true;
// const textHeight = list[j].textRect.height;
// if (list[j].y + textHeight / 2 > viewTop + viewHeight) {
// list[j].y = viewTop + viewHeight - textHeight / 2;
// }
if (j > start
&& j + 1 < end
if (j > start && j + 1 < end
&& list[j + 1].y > list[j].y + list[j].textRect.height
) {
// Shift up so it can be more equaly distributed.
shiftUp(j, delta / 2);
return;
}
......
......@@ -1096,7 +1096,7 @@ class ECharts extends Eventful {
updateLabelLayout() {
const labelManager = this._labelManager;
labelManager.updateLayoutConfig(this._api);
labelManager.layout();
labelManager.layout(this._api);
labelManager.processLabelsOverall();
}
......@@ -1732,7 +1732,7 @@ class ECharts extends Eventful {
scheduler.unfinished = unfinished || scheduler.unfinished;
labelManager.updateLayoutConfig(api);
labelManager.layout();
labelManager.layout(api);
labelManager.processLabelsOverall();
ecModel.eachSeries(function (seriesModel) {
......
......@@ -20,16 +20,13 @@
// TODO: move labels out of viewport.
import {
OrientedBoundingRect,
Text as ZRText,
Point,
BoundingRect,
getECData,
Polyline,
updateProps,
initProps
} from '../util/graphic';
import { MatrixArray } from 'zrender/src/core/matrix';
import ExtensionAPI from '../ExtensionAPI';
import {
ZRTextAlign,
......@@ -47,20 +44,12 @@ import Transformable from 'zrender/src/core/Transformable';
import { updateLabelLinePoints, setLabelLineStyle } from './labelGuideHelper';
import SeriesModel from '../model/Series';
import { makeInner } from '../util/model';
import { retrieve2, each, keys, isFunction } from 'zrender/src/core/util';
import { retrieve2, each, keys, isFunction, filter } from 'zrender/src/core/util';
import { PathStyleProps } from 'zrender/src/graphic/Path';
import Model from '../model/Model';
import { LabelLayoutInfo, prepareLayoutList, hideOverlap, shiftLayoutOnX, shiftLayoutOnY } from './labelLayoutHelper';
interface DisplayedLabelItem {
label: ZRText
rect: BoundingRect
localRect: BoundingRect
obb?: OrientedBoundingRect
axisAligned: boolean
transform: MatrixArray
}
interface LabelLayoutDesc {
interface LabelDesc {
label: ZRText
labelLine: Polyline
......@@ -102,7 +91,7 @@ interface SavedLabelAttr {
rect: RectLike
}
function prepareLayoutCallbackParams(labelItem: LabelLayoutDesc): LabelLayoutOptionCallbackParams {
function prepareLayoutCallbackParams(labelItem: LabelDesc): LabelLayoutOptionCallbackParams {
const labelAttr = labelItem.defaultAttr;
const label = labelItem.label;
return {
......@@ -144,7 +133,7 @@ type LabelLineOptionMixin = {
class LabelManager {
private _labelList: LabelLayoutDesc[] = [];
private _labelList: LabelDesc[] = [];
private _chartViewList: ChartView[] = [];
constructor() {}
......@@ -162,7 +151,7 @@ class LabelManager {
dataType: string,
seriesModel: SeriesModel,
label: ZRText,
layoutOption: LabelLayoutDesc['layoutOption']
layoutOption: LabelDesc['layoutOption']
) {
const labelStyle = label.style;
const hostEl = label.__hostTarget;
......@@ -353,84 +342,26 @@ class LabelManager {
}
}
layout() {
// TODO: sort by priority(area)
const labelList = this._labelList;
const displayedLabels: DisplayedLabelItem[] = [];
const mvt = new Point();
layout(api: ExtensionAPI) {
const width = api.getWidth();
const height = api.getHeight();
// TODO, render overflow visible first, put in the displayedLabels.
labelList.sort(function (a, b) {
return b.priority - a.priority;
const labelList = prepareLayoutList(this._labelList);
const labelsNeedsAdjustOnX = filter(labelList, function (item) {
return item.layoutOption.moveOverlap === 'shift-x';
});
const labelsNeedsAdjustOnY = filter(labelList, function (item) {
return item.layoutOption.moveOverlap === 'shift-y';
});
for (let i = 0; i < labelList.length; i++) {
const labelItem = labelList[i];
if (labelItem.defaultAttr.ignore) {
continue;
}
const layoutOption = labelItem.computedLayoutOption;
const label = labelItem.label;
const transform = label.getComputedTransform();
// NOTE: Get bounding rect after getComputedTransform, or label may not been updated by the host el.
const localRect = label.getBoundingRect();
const isAxisAligned = !transform || (transform[1] < 1e-5 && transform[2] < 1e-5);
const globalRect = localRect.clone();
globalRect.applyTransform(transform);
let obb = isAxisAligned ? new OrientedBoundingRect(localRect, transform) : null;
let overlapped = false;
const minMargin = layoutOption.minMargin || 0;
const marginSqr = minMargin * minMargin;
for (let j = 0; j < displayedLabels.length; j++) {
const existsTextCfg = displayedLabels[j];
// Fast rejection.
if (!globalRect.intersect(existsTextCfg.rect, mvt) && mvt.lenSquare() > marginSqr) {
continue;
}
if (isAxisAligned && existsTextCfg.axisAligned) { // Is overlapped
overlapped = true;
break;
}
if (!existsTextCfg.obb) { // If self is not axis aligned. But other is.
existsTextCfg.obb = new OrientedBoundingRect(existsTextCfg.localRect, existsTextCfg.transform);
}
if (!obb) { // If self is axis aligned. But other is not.
obb = new OrientedBoundingRect(localRect, transform);
}
shiftLayoutOnX(labelsNeedsAdjustOnX, 0, width);
shiftLayoutOnY(labelsNeedsAdjustOnY, 0, height);
if (obb.intersect(existsTextCfg.obb, mvt) || mvt.lenSquare() < marginSqr) {
overlapped = true;
break;
}
}
const labelsNeedsHideOverlap = filter(labelList, function (item) {
return item.layoutOption.hideOverlap;
});
const labelLine = labelItem.labelLine;
// TODO Callback to determine if this overlap should be handled?
if (overlapped && layoutOption.hideOverlap) {
label.hide();
labelLine && labelLine.hide();
}
else {
label.attr('ignore', labelItem.defaultAttr.ignore);
labelLine && labelLine.attr('ignore', labelItem.defaultAttr.labelGuideIgnore);
displayedLabels.push({
label,
rect: globalRect,
localRect,
obb,
axisAligned: isAxisAligned,
transform
});
}
}
hideOverlap(labelsNeedsHideOverlap);
}
/**
......
......@@ -18,26 +18,226 @@
*/
import ZRText from 'zrender/src/graphic/Text';
import { LabelLayoutOption } from '../util/types';
import { BoundingRect, OrientedBoundingRect, Polyline } from '../util/graphic';
interface LabelLayoutListPrepareInput {
label: ZRText
labelLine: Polyline
computedLayoutOption: LabelLayoutOption
priority: number
defaultAttr: {
ignore: boolean
labelGuideIgnore: boolean
}
}
export interface LabelLayoutInfo {
label: ZRText
labelLine: Polyline
priority: number
rect: BoundingRect // Global rect
localRect: BoundingRect
obb?: OrientedBoundingRect // Only available when axisAligned is true
axisAligned: boolean
layoutOption: LabelLayoutOption
defaultAttr: {
ignore: boolean
labelGuideIgnore: boolean
}
transform: number[]
}
export function prepareLayoutList(input: LabelLayoutListPrepareInput[]): LabelLayoutInfo[] {
const list: LabelLayoutInfo[] = [];
for (let i = 0; i < input.length; i++) {
const rawItem = input[i];
if (rawItem.defaultAttr.ignore) {
continue;
}
const layoutOption = rawItem.computedLayoutOption;
const label = rawItem.label;
const transform = label.getComputedTransform();
// NOTE: Get bounding rect after getComputedTransform, or label may not been updated by the host el.
const localRect = label.getBoundingRect();
const isAxisAligned = !transform || (transform[1] < 1e-5 && transform[2] < 1e-5);
// Text has a default 1px stroke. Exclude this.
const minMargin = (layoutOption.minMargin || 0) + 2.2;
const globalRect = localRect.clone();
globalRect.applyTransform(transform);
globalRect.x -= minMargin / 2;
globalRect.y -= minMargin / 2;
globalRect.width += minMargin;
globalRect.height += minMargin;
const obb = isAxisAligned ? new OrientedBoundingRect(localRect, transform) : null;
list.push({
label,
labelLine: rawItem.labelLine,
rect: globalRect,
localRect,
obb,
priority: rawItem.priority,
defaultAttr: rawItem.defaultAttr,
layoutOption: rawItem.computedLayoutOption,
axisAligned: isAxisAligned,
transform
});
}
return list;
}
function shiftLayout(
list: LabelLayoutInfo[],
xyDim: 'x' | 'y',
sizeDim: 'width' | 'height',
minBound: number,
maxBound: number
) {
if (!list.length) {
return;
}
list.sort(function (a, b) {
return a.label[xyDim] - b.label[xyDim];
});
function shiftForward(start: number, end: number, delta: number) {
for (let j = start; j < end; j++) {
list[j].label[xyDim] += delta;
const rect = list[j].rect;
rect[xyDim] += delta;
if (j > start && j + 1 < end
&& list[j + 1].rect[xyDim] > rect[xyDim] + rect[sizeDim]
) {
// Shift up so it can be more equaly distributed.
shiftBackward(j, delta / 2);
return;
}
}
shiftBackward(end - 1, delta / 2);
}
function shiftBackward(end: number, delta: number) {
for (let j = end; j >= 0; j--) {
list[j].label[xyDim] -= delta;
const rect = list[j].rect;
rect[xyDim] -= delta;
// const textSize = rect[sizeDim];
const diffToMinBound = rect[xyDim] - minBound;
if (diffToMinBound < 0) {
rect[xyDim] -= diffToMinBound;
list[j].label[xyDim] -= diffToMinBound;
}
if (j > 0
&& rect[xyDim] > list[j - 1].rect[xyDim] + list[j - 1].rect[sizeDim]
) {
break;
}
}
}
let lastPos = 0;
let delta;
const len = list.length;
for (let i = 0; i < len; i++) {
delta = list[i].label[xyDim] - lastPos;
if (delta < 0) {
shiftForward(i, len, -delta);
}
lastPos = list[i].label[xyDim] + list[i].rect[sizeDim];
}
if (maxBound - lastPos < 0) {
shiftBackward(len - 1, lastPos - maxBound);
}
}
/**
* Adjust labels on x direction to avoid overlap.
*/
export function adjustLayoutOnX(
list: ZRText[],
export function shiftLayoutOnX(
list: LabelLayoutInfo[],
leftBound: number,
rightBound: number
) {
shiftLayout(list, 'x', 'width', leftBound, rightBound);
}
/**
* Adjust labels on y direction to avoid overlap.
*/
export function adjustLayoutOnY(
list: ZRText[],
export function shiftLayoutOnY(
list: LabelLayoutInfo[],
topBound: number,
bottomBound: number
) {
shiftLayout(list, 'y', 'height', topBound, bottomBound);
}
export function hideOverlap(labelList: LabelLayoutInfo[]) {
const displayedLabels: LabelLayoutInfo[] = [];
// TODO, render overflow visible first, put in the displayedLabels.
labelList.sort(function (a, b) {
return b.priority - a.priority;
});
for (let i = 0; i < labelList.length; i++) {
const labelItem = labelList[i];
const globalRect = labelItem.rect;
const isAxisAligned = labelItem.axisAligned;
const localRect = labelItem.localRect;
const transform = labelItem.transform;
const label = labelItem.label;
const labelLine = labelItem.labelLine;
let obb = labelItem.obb;
let overlapped = false;
for (let j = 0; j < displayedLabels.length; j++) {
const existsTextCfg = displayedLabels[j];
// Fast rejection.
if (!globalRect.intersect(existsTextCfg.rect)) {
continue;
}
if (isAxisAligned && existsTextCfg.axisAligned) { // Is overlapped
overlapped = true;
break;
}
if (!existsTextCfg.obb) { // If self is not axis aligned. But other is.
existsTextCfg.obb = new OrientedBoundingRect(existsTextCfg.localRect, existsTextCfg.transform);
}
if (!obb) { // If self is axis aligned. But other is not.
obb = new OrientedBoundingRect(localRect, transform);
}
if (obb.intersect(existsTextCfg.obb)) {
overlapped = true;
break;
}
}
// TODO Callback to determine if this overlap should be handled?
if (overlapped) {
label.hide();
labelLine && labelLine.hide();
}
else {
label.attr('ignore', labelItem.defaultAttr.ignore);
labelLine && labelLine.attr('ignore', labelItem.defaultAttr.labelGuideIgnore);
displayedLabels.push(labelItem);
}
}
}
\ No newline at end of file
......@@ -826,8 +826,14 @@ export interface LabelLayoutOption {
/**
* If move the overlapped label. If label is still overlapped after moved.
* It will determine if to hide this label with `hideOverlap` policy.
*
* shift-x/y will keep the order on x/y
* shuffle-x/y will move the label around the original position randomly.
*/
moveOverlap?: 'x' | 'y' | boolean
moveOverlap?: 'shift-x'
| 'shift-y'
| 'shuffle-x'
| 'shuffle-y'
/**
* If hide the overlapped label. It will be handled after move.
* @default 'none'
......
......@@ -46,6 +46,8 @@ under the License.
<div id="main3"></div>
<div id="main4"></div>
<div id="main5"></div>
<div id="main6"></div>
<div id="main7"></div>
......@@ -153,7 +155,7 @@ under the License.
var data = [Math.round(Math.random() * 300)];
for (var i = 1; i < 200; i++) {
for (var i = 1; i < 50; i++) {
var now = new Date(base += oneDay);
date.push([now.getFullYear(), now.getMonth() + 1, now.getDate()].join('/'));
data.push(Math.round((Math.random() - 0.5) * 20 + data[i - 1]));
......@@ -200,6 +202,7 @@ under the License.
]
};
var chart = testHelper.create(echarts, 'main1', {
width: 600,
title: [
'Overlap of line.'
],
......@@ -343,6 +346,123 @@ under the License.
</script>
<script>
require(['echarts'/*, 'map/js/china' */], function (echarts) {
var option;
var data = [
[[28604,77,17096869,'Australia',1990],[31163,77.4,27662440,'Canada',1990],[1516,68,1154605773,'China',1990],[13670,74.7,10582082,'Cuba',1990],[28599,75,4986705,'Finland',1990],[29476,77.1,56943299,'France',1990],[31476,75.4,78958237,'Germany',1990],[28666,78.1,254830,'Iceland',1990],[1777,57.7,870601776,'India',1990],[29550,79.1,122249285,'Japan',1990],[2076,67.9,20194354,'North Korea',1990],[12087,72,42972254,'South Korea',1990],[24021,75.4,3397534,'New Zealand',1990],[43296,76.8,4240375,'Norway',1990],[10088,70.8,38195258,'Poland',1990],[19349,69.6,147568552,'Russia',1990],[10670,67.3,53994605,'Turkey',1990],[26424,75.7,57110117,'United Kingdom',1990],[37062,75.4,252847810,'United States',1990]],
[[44056,81.8,23968973,'Australia',2015],[43294,81.7,35939927,'Canada',2015],[13334,76.9,1376048943,'China',2015],[21291,78.5,11389562,'Cuba',2015],[38923,80.8,5503457,'Finland',2015],[37599,81.9,64395345,'France',2015],[44053,81.1,80688545,'Germany',2015],[42182,82.8,329425,'Iceland',2015],[5903,66.8,1311050527,'India',2015],[36162,83.5,126573481,'Japan',2015],[1390,71.4,25155317,'North Korea',2015],[34644,80.7,50293439,'South Korea',2015],[34186,80.6,4528526,'New Zealand',2015],[64304,81.6,5210967,'Norway',2015],[24787,77.3,38611794,'Poland',2015],[23038,73.13,143456918,'Russia',2015],[19360,76.5,78665830,'Turkey',2015],[38225,81.4,64715810,'United Kingdom',2015],[53354,79.1,321773631,'United States',2015]]
];
option = {
xAxis: {},
yAxis: {
scale: true
},
series: [{
name: '1990',
data: data[0],
type: 'scatter',
symbolSize: function (data) {
return Math.sqrt(data[2]) / 5e2;
},
labelLayout: {
y: 20,
draggable: true,
align: 'center',
moveOverlap: 'shift-x',
hideOverlap: true,
minMargin: 10
},
labelLine: {
show: true,
length2: 5,
lineStyle: {
color: '#bbb'
}
},
label: {
show: true,
formatter: function (param) {
return param.data[3];
},
color: '#333',
textBorderColor: '#fff',
textBorderWidth: 1,
position: 'top'
}
}]
};
var chart = testHelper.create(echarts, 'main6', {
title: [
'Overlap Shift X'
],
option: option
});
});
</script>
<script>
require(['echarts'/*, 'map/js/china' */], function (echarts) {
var option;
var data = [
[[28604,77,17096869,'Australia',1990],[31163,77.4,27662440,'Canada',1990],[1516,68,1154605773,'China',1990],[13670,74.7,10582082,'Cuba',1990],[28599,75,4986705,'Finland',1990],[29476,77.1,56943299,'France',1990],[31476,75.4,78958237,'Germany',1990],[28666,78.1,254830,'Iceland',1990],[1777,57.7,870601776,'India',1990],[29550,79.1,122249285,'Japan',1990],[2076,67.9,20194354,'North Korea',1990],[12087,72,42972254,'South Korea',1990],[24021,75.4,3397534,'New Zealand',1990],[43296,76.8,4240375,'Norway',1990],[10088,70.8,38195258,'Poland',1990],[19349,69.6,147568552,'Russia',1990],[10670,67.3,53994605,'Turkey',1990],[26424,75.7,57110117,'United Kingdom',1990],[37062,75.4,252847810,'United States',1990]],
[[44056,81.8,23968973,'Australia',2015],[43294,81.7,35939927,'Canada',2015],[13334,76.9,1376048943,'China',2015],[21291,78.5,11389562,'Cuba',2015],[38923,80.8,5503457,'Finland',2015],[37599,81.9,64395345,'France',2015],[44053,81.1,80688545,'Germany',2015],[42182,82.8,329425,'Iceland',2015],[5903,66.8,1311050527,'India',2015],[36162,83.5,126573481,'Japan',2015],[1390,71.4,25155317,'North Korea',2015],[34644,80.7,50293439,'South Korea',2015],[34186,80.6,4528526,'New Zealand',2015],[64304,81.6,5210967,'Norway',2015],[24787,77.3,38611794,'Poland',2015],[23038,73.13,143456918,'Russia',2015],[19360,76.5,78665830,'Turkey',2015],[38225,81.4,64715810,'United Kingdom',2015],[53354,79.1,321773631,'United States',2015]]
];
option = {
xAxis: {},
yAxis: {
scale: true
},
grid: {
width: 300
},
series: [{
name: '1990',
data: data[0],
type: 'scatter',
symbolSize: function (data) {
return Math.sqrt(data[2]) / 5e2;
},
labelLayout: {
x: 500,
draggable: true,
align: 'center',
moveOverlap: 'shift-y',
// hideOverlap: true,
minMargin: 2
},
labelLine: {
show: true,
length2: 5,
lineStyle: {
color: '#bbb'
}
},
label: {
show: true,
formatter: function (param) {
return param.data[3];
},
color: '#333',
textBorderColor: '#fff',
textBorderWidth: 1,
position: 'top'
}
}]
};
var chart = testHelper.create(echarts, 'main7', {
title: [
'Overlap Shift Y'
],
option: option
});
});
</script>
</body>
</html>
<!DOCTYPE html>
<!--
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.
-->
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="lib/esl.js"></script>
<script src="lib/config.js"></script>
<script src="lib/jquery.min.js"></script>
<script src="lib/facePrint.js"></script>
<script src="lib/testHelper.js"></script>
<!-- <script src="ut/lib/canteen.js"></script> -->
<link rel="stylesheet" href="lib/reset.css" />
</head>
<body>
<style>
</style>
<div id="main0"></div>
<script>
require(['echarts'/*, 'map/js/china' */], function (echarts) {
var option;
var data = [
[[28604,77,17096869,'Australia',1990],[31163,77.4,27662440,'Canada',1990],[1516,68,1154605773,'China',1990],[13670,74.7,10582082,'Cuba',1990],[28599,75,4986705,'Finland',1990],[29476,77.1,56943299,'France',1990],[31476,75.4,78958237,'Germany',1990],[28666,78.1,254830,'Iceland',1990],[1777,57.7,870601776,'India',1990],[29550,79.1,122249285,'Japan',1990],[2076,67.9,20194354,'North Korea',1990],[12087,72,42972254,'South Korea',1990],[24021,75.4,3397534,'New Zealand',1990],[43296,76.8,4240375,'Norway',1990],[10088,70.8,38195258,'Poland',1990],[19349,69.6,147568552,'Russia',1990],[10670,67.3,53994605,'Turkey',1990],[26424,75.7,57110117,'United Kingdom',1990],[37062,75.4,252847810,'United States',1990]],
[[44056,81.8,23968973,'Australia',2015],[43294,81.7,35939927,'Canada',2015],[13334,76.9,1376048943,'China',2015],[21291,78.5,11389562,'Cuba',2015],[38923,80.8,5503457,'Finland',2015],[37599,81.9,64395345,'France',2015],[44053,81.1,80688545,'Germany',2015],[42182,82.8,329425,'Iceland',2015],[5903,66.8,1311050527,'India',2015],[36162,83.5,126573481,'Japan',2015],[1390,71.4,25155317,'North Korea',2015],[34644,80.7,50293439,'South Korea',2015],[34186,80.6,4528526,'New Zealand',2015],[64304,81.6,5210967,'Norway',2015],[24787,77.3,38611794,'Poland',2015],[23038,73.13,143456918,'Russia',2015],[19360,76.5,78665830,'Turkey',2015],[38225,81.4,64715810,'United Kingdom',2015],[53354,79.1,321773631,'United States',2015]]
];
option = {
xAxis: {},
yAxis: {},
series: [{
name: '1990',
data: data[0],
type: 'scatter',
symbolSize: function (data) {
return Math.sqrt(data[2]) / 5e2;
},
labelLayout: {
y: 20,
draggable: true,
align: 'center',
hideOverlap: true
},
labelLine: {
show: true,
length2: 10
},
label: {
show: true,
formatter: function (param) {
return param.data[3];
},
position: 'top'
}
}, {
name: '2015',
data: data[1],
type: 'scatter',
symbolSize: function (data) {
return Math.sqrt(data[2]) / 5e2;
},
labelLayout: {
y: 40,
draggable: true,
align: 'center',
hideOverlap: true
},
labelLine: {
show: true,
length2: 10
},
label: {
show: true,
formatter: function (param) {
return param.data[3];
},
position: 'top'
}
}]
};
var chart = testHelper.create(echarts, 'main0', {
title: [
],
option: option
// height: 300,
// buttons: [{text: 'btn-txt', onclick: function () {}}],
// recordCanvas: true,
});
});
</script>
</body>
</html>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册