Spaces:
Running
Running
| /* | |
| * 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. | |
| */ | |
| // FIXME emphasis label position is not same with normal label position | |
| import {parsePercent} from '../../util/number'; | |
| import PieSeriesModel, { PieSeriesOption, PieDataItemOption } from './PieSeries'; | |
| import { VectorArray } from 'zrender/src/core/vector'; | |
| import { HorizontalAlign, ZRTextAlign } from '../../util/types'; | |
| import { Sector, Polyline, Point } from '../../util/graphic'; | |
| import ZRText from 'zrender/src/graphic/Text'; | |
| import BoundingRect, {RectLike} from 'zrender/src/core/BoundingRect'; | |
| import { each, isNumber } from 'zrender/src/core/util'; | |
| import { limitTurnAngle, limitSurfaceAngle } from '../../label/labelGuideHelper'; | |
| import { shiftLayoutOnY } from '../../label/labelLayoutHelper'; | |
| const RADIAN = Math.PI / 180; | |
| interface LabelLayout { | |
| label: ZRText | |
| labelLine: Polyline | |
| position: PieSeriesOption['label']['position'] | |
| len: number | |
| len2: number | |
| minTurnAngle: number | |
| maxSurfaceAngle: number | |
| surfaceNormal: Point | |
| linePoints: VectorArray[] | |
| textAlign: HorizontalAlign | |
| labelDistance: number | |
| labelAlignTo: PieSeriesOption['label']['alignTo'] | |
| edgeDistance: number | |
| bleedMargin: PieSeriesOption['label']['bleedMargin'] | |
| rect: BoundingRect | |
| /** | |
| * user-set style.width. | |
| * This is useful because label.style.width might be changed | |
| * by constrainTextWidth. | |
| */ | |
| labelStyleWidth: number | |
| unconstrainedWidth: number | |
| targetTextWidth?: number | |
| } | |
| function adjustSingleSide( | |
| list: LabelLayout[], | |
| cx: number, | |
| cy: number, | |
| r: number, | |
| dir: -1 | 1, | |
| viewWidth: number, | |
| viewHeight: number, | |
| viewLeft: number, | |
| viewTop: number, | |
| farthestX: number | |
| ) { | |
| if (list.length < 2) { | |
| return; | |
| } | |
| interface SemiInfo { | |
| list: LabelLayout[] | |
| rB: number | |
| maxY: number | |
| }; | |
| function recalculateXOnSemiToAlignOnEllipseCurve(semi: SemiInfo) { | |
| const rB = semi.rB; | |
| const rB2 = rB * rB; | |
| for (let i = 0; i < semi.list.length; i++) { | |
| const item = semi.list[i]; | |
| const dy = Math.abs(item.label.y - cy); | |
| // horizontal r is always same with original r because x is not changed. | |
| const rA = r + item.len; | |
| const rA2 = rA * rA; | |
| // Use ellipse implicit function to calculate x | |
| const dx = Math.sqrt((1 - Math.abs(dy * dy / rB2)) * rA2); | |
| const newX = cx + (dx + item.len2) * dir; | |
| const deltaX = newX - item.label.x; | |
| const newTargetWidth = item.targetTextWidth - deltaX * dir; | |
| // text x is changed, so need to recalculate width. | |
| constrainTextWidth(item, newTargetWidth, true); | |
| item.label.x = newX; | |
| } | |
| } | |
| // Adjust X based on the shifted y. Make tight labels aligned on an ellipse curve. | |
| function recalculateX(items: LabelLayout[]) { | |
| // Extremes of | |
| const topSemi = { list: [], maxY: 0} as SemiInfo; | |
| const bottomSemi = { list: [], maxY: 0 } as SemiInfo; | |
| for (let i = 0; i < items.length; i++) { | |
| if (items[i].labelAlignTo !== 'none') { | |
| continue; | |
| } | |
| const item = items[i]; | |
| const semi = item.label.y > cy ? bottomSemi : topSemi; | |
| const dy = Math.abs(item.label.y - cy); | |
| if (dy >= semi.maxY) { | |
| const dx = item.label.x - cx - item.len2 * dir; | |
| // horizontal r is always same with original r because x is not changed. | |
| const rA = r + item.len; | |
| // Canculate rB based on the topest / bottemest label. | |
| const rB = Math.abs(dx) < rA | |
| ? Math.sqrt(dy * dy / (1 - dx * dx / rA / rA)) | |
| : rA; | |
| semi.rB = rB; | |
| semi.maxY = dy; | |
| } | |
| semi.list.push(item); | |
| } | |
| recalculateXOnSemiToAlignOnEllipseCurve(topSemi); | |
| recalculateXOnSemiToAlignOnEllipseCurve(bottomSemi); | |
| } | |
| const len = list.length; | |
| for (let i = 0; i < len; i++) { | |
| if (list[i].position === 'outer' && list[i].labelAlignTo === 'labelLine') { | |
| const dx = list[i].label.x - farthestX; | |
| list[i].linePoints[1][0] += dx; | |
| list[i].label.x = farthestX; | |
| } | |
| } | |
| if (shiftLayoutOnY(list, viewTop, viewTop + viewHeight)) { | |
| recalculateX(list); | |
| } | |
| } | |
| function avoidOverlap( | |
| labelLayoutList: LabelLayout[], | |
| cx: number, | |
| cy: number, | |
| r: number, | |
| viewWidth: number, | |
| viewHeight: number, | |
| viewLeft: number, | |
| viewTop: number | |
| ) { | |
| const leftList = []; | |
| const rightList = []; | |
| let leftmostX = Number.MAX_VALUE; | |
| let rightmostX = -Number.MAX_VALUE; | |
| for (let i = 0; i < labelLayoutList.length; i++) { | |
| const label = labelLayoutList[i].label; | |
| if (isPositionCenter(labelLayoutList[i])) { | |
| continue; | |
| } | |
| if (label.x < cx) { | |
| leftmostX = Math.min(leftmostX, label.x); | |
| leftList.push(labelLayoutList[i]); | |
| } | |
| else { | |
| rightmostX = Math.max(rightmostX, label.x); | |
| rightList.push(labelLayoutList[i]); | |
| } | |
| } | |
| for (let i = 0; i < labelLayoutList.length; i++) { | |
| const layout = labelLayoutList[i]; | |
| if (!isPositionCenter(layout) && layout.linePoints) { | |
| if (layout.labelStyleWidth != null) { | |
| continue; | |
| } | |
| const label = layout.label; | |
| const linePoints = layout.linePoints; | |
| let targetTextWidth; | |
| if (layout.labelAlignTo === 'edge') { | |
| if (label.x < cx) { | |
| targetTextWidth = linePoints[2][0] - layout.labelDistance | |
| - viewLeft - layout.edgeDistance; | |
| } | |
| else { | |
| targetTextWidth = viewLeft + viewWidth - layout.edgeDistance | |
| - linePoints[2][0] - layout.labelDistance; | |
| } | |
| } | |
| else if (layout.labelAlignTo === 'labelLine') { | |
| if (label.x < cx) { | |
| targetTextWidth = leftmostX - viewLeft - layout.bleedMargin; | |
| } | |
| else { | |
| targetTextWidth = viewLeft + viewWidth - rightmostX - layout.bleedMargin; | |
| } | |
| } | |
| else { | |
| if (label.x < cx) { | |
| targetTextWidth = label.x - viewLeft - layout.bleedMargin; | |
| } | |
| else { | |
| targetTextWidth = viewLeft + viewWidth - label.x - layout.bleedMargin; | |
| } | |
| } | |
| layout.targetTextWidth = targetTextWidth; | |
| constrainTextWidth(layout, targetTextWidth); | |
| } | |
| } | |
| adjustSingleSide(rightList, cx, cy, r, 1, viewWidth, viewHeight, viewLeft, viewTop, rightmostX); | |
| adjustSingleSide(leftList, cx, cy, r, -1, viewWidth, viewHeight, viewLeft, viewTop, leftmostX); | |
| for (let i = 0; i < labelLayoutList.length; i++) { | |
| const layout = labelLayoutList[i]; | |
| if (!isPositionCenter(layout) && layout.linePoints) { | |
| const label = layout.label; | |
| const linePoints = layout.linePoints; | |
| const isAlignToEdge = layout.labelAlignTo === 'edge'; | |
| const padding = label.style.padding as number[]; | |
| const paddingH = padding ? padding[1] + padding[3] : 0; | |
| // textRect.width already contains paddingH if bgColor is set | |
| const extraPaddingH = label.style.backgroundColor ? 0 : paddingH; | |
| const realTextWidth = layout.rect.width + extraPaddingH; | |
| const dist = linePoints[1][0] - linePoints[2][0]; | |
| if (isAlignToEdge) { | |
| if (label.x < cx) { | |
| linePoints[2][0] = viewLeft + layout.edgeDistance + realTextWidth + layout.labelDistance; | |
| } | |
| else { | |
| linePoints[2][0] = viewLeft + viewWidth - layout.edgeDistance | |
| - realTextWidth - layout.labelDistance; | |
| } | |
| } | |
| else { | |
| if (label.x < cx) { | |
| linePoints[2][0] = label.x + layout.labelDistance; | |
| } | |
| else { | |
| linePoints[2][0] = label.x - layout.labelDistance; | |
| } | |
| linePoints[1][0] = linePoints[2][0] + dist; | |
| } | |
| linePoints[1][1] = linePoints[2][1] = label.y; | |
| } | |
| } | |
| } | |
| /** | |
| * Set max width of each label, and then wrap each label to the max width. | |
| * | |
| * @param layout label layout | |
| * @param availableWidth max width for the label to display | |
| * @param forceRecalculate recaculate the text layout even if the current width | |
| * is smaller than `availableWidth`. This is useful when the text was previously | |
| * wrapped by calling `constrainTextWidth` but now `availableWidth` changed, in | |
| * which case, previous wrapping should be redo. | |
| */ | |
| function constrainTextWidth( | |
| layout: LabelLayout, | |
| availableWidth: number, | |
| forceRecalculate: boolean = false | |
| ) { | |
| if (layout.labelStyleWidth != null) { | |
| // User-defined style.width has the highest priority. | |
| return; | |
| } | |
| const label = layout.label; | |
| const style = label.style; | |
| const textRect = layout.rect; | |
| const bgColor = style.backgroundColor; | |
| const padding = style.padding as number[]; | |
| const paddingH = padding ? padding[1] + padding[3] : 0; | |
| const overflow = style.overflow; | |
| // textRect.width already contains paddingH if bgColor is set | |
| const oldOuterWidth = textRect.width + (bgColor ? 0 : paddingH); | |
| if (availableWidth < oldOuterWidth || forceRecalculate) { | |
| const oldHeight = textRect.height; | |
| if (overflow && overflow.match('break')) { | |
| // Temporarily set background to be null to calculate | |
| // the bounding box without background. | |
| label.setStyle('backgroundColor', null); | |
| // Set constraining width | |
| label.setStyle('width', availableWidth - paddingH); | |
| // This is the real bounding box of the text without padding. | |
| const innerRect = label.getBoundingRect(); | |
| label.setStyle('width', Math.ceil(innerRect.width)); | |
| label.setStyle('backgroundColor', bgColor); | |
| } | |
| else { | |
| const availableInnerWidth = availableWidth - paddingH; | |
| const newWidth = availableWidth < oldOuterWidth | |
| // Current text is too wide, use `availableWidth` as max width. | |
| ? availableInnerWidth | |
| : ( | |
| // Current available width is enough, but the text may have | |
| // already been wrapped with a smaller available width. | |
| forceRecalculate | |
| ? (availableInnerWidth > layout.unconstrainedWidth | |
| // Current available is larger than text width, | |
| // so don't constrain width (otherwise it may have | |
| // empty space in the background). | |
| ? null | |
| // Current available is smaller than text width, so | |
| // use the current available width as constraining | |
| // width. | |
| : availableInnerWidth | |
| ) | |
| // Current available width is enough, so no need to | |
| // constrain. | |
| : null | |
| ); | |
| label.setStyle('width', newWidth); | |
| } | |
| const newRect = label.getBoundingRect(); | |
| textRect.width = newRect.width; | |
| const margin = (label.style.margin || 0) + 2.1; | |
| textRect.height = newRect.height + margin; | |
| textRect.y -= (textRect.height - oldHeight) / 2; | |
| } | |
| } | |
| function isPositionCenter(sectorShape: LabelLayout) { | |
| // Not change x for center label | |
| return sectorShape.position === 'center'; | |
| } | |
| export default function pieLabelLayout( | |
| seriesModel: PieSeriesModel | |
| ) { | |
| const data = seriesModel.getData(); | |
| const labelLayoutList: LabelLayout[] = []; | |
| let cx; | |
| let cy; | |
| let hasLabelRotate = false; | |
| const minShowLabelRadian = (seriesModel.get('minShowLabelAngle') || 0) * RADIAN; | |
| const viewRect = data.getLayout('viewRect') as RectLike; | |
| const r = data.getLayout('r') as number; | |
| const viewWidth = viewRect.width; | |
| const viewLeft = viewRect.x; | |
| const viewTop = viewRect.y; | |
| const viewHeight = viewRect.height; | |
| function setNotShow(el: {ignore: boolean}) { | |
| el.ignore = true; | |
| } | |
| function isLabelShown(label: ZRText) { | |
| if (!label.ignore) { | |
| return true; | |
| } | |
| for (const key in label.states) { | |
| if (label.states[key].ignore === false) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| data.each(function (idx) { | |
| const sector = data.getItemGraphicEl(idx) as Sector; | |
| const sectorShape = sector.shape; | |
| const label = sector.getTextContent(); | |
| const labelLine = sector.getTextGuideLine(); | |
| const itemModel = data.getItemModel<PieDataItemOption>(idx); | |
| const labelModel = itemModel.getModel('label'); | |
| // Use position in normal or emphasis | |
| const labelPosition = labelModel.get('position') || itemModel.get(['emphasis', 'label', 'position']); | |
| const labelDistance = labelModel.get('distanceToLabelLine'); | |
| const labelAlignTo = labelModel.get('alignTo'); | |
| const edgeDistance = parsePercent(labelModel.get('edgeDistance'), viewWidth); | |
| const bleedMargin = labelModel.get('bleedMargin'); | |
| const labelLineModel = itemModel.getModel('labelLine'); | |
| let labelLineLen = labelLineModel.get('length'); | |
| labelLineLen = parsePercent(labelLineLen, viewWidth); | |
| let labelLineLen2 = labelLineModel.get('length2'); | |
| labelLineLen2 = parsePercent(labelLineLen2, viewWidth); | |
| if (Math.abs(sectorShape.endAngle - sectorShape.startAngle) < minShowLabelRadian) { | |
| each(label.states, setNotShow); | |
| label.ignore = true; | |
| if (labelLine) { | |
| each(labelLine.states, setNotShow); | |
| labelLine.ignore = true; | |
| } | |
| return; | |
| } | |
| if (!isLabelShown(label)) { | |
| return; | |
| } | |
| const midAngle = (sectorShape.startAngle + sectorShape.endAngle) / 2; | |
| const nx = Math.cos(midAngle); | |
| const ny = Math.sin(midAngle); | |
| let textX; | |
| let textY; | |
| let linePoints; | |
| let textAlign: ZRTextAlign; | |
| cx = sectorShape.cx; | |
| cy = sectorShape.cy; | |
| const isLabelInside = labelPosition === 'inside' || labelPosition === 'inner'; | |
| if (labelPosition === 'center') { | |
| textX = sectorShape.cx; | |
| textY = sectorShape.cy; | |
| textAlign = 'center'; | |
| } | |
| else { | |
| const x1 = (isLabelInside ? (sectorShape.r + sectorShape.r0) / 2 * nx : sectorShape.r * nx) + cx; | |
| const y1 = (isLabelInside ? (sectorShape.r + sectorShape.r0) / 2 * ny : sectorShape.r * ny) + cy; | |
| textX = x1 + nx * 3; | |
| textY = y1 + ny * 3; | |
| if (!isLabelInside) { | |
| // For roseType | |
| const x2 = x1 + nx * (labelLineLen + r - sectorShape.r); | |
| const y2 = y1 + ny * (labelLineLen + r - sectorShape.r); | |
| const x3 = x2 + ((nx < 0 ? -1 : 1) * labelLineLen2); | |
| const y3 = y2; | |
| if (labelAlignTo === 'edge') { | |
| // Adjust textX because text align of edge is opposite | |
| textX = nx < 0 | |
| ? viewLeft + edgeDistance | |
| : viewLeft + viewWidth - edgeDistance; | |
| } | |
| else { | |
| textX = x3 + (nx < 0 ? -labelDistance : labelDistance); | |
| } | |
| textY = y3; | |
| linePoints = [[x1, y1], [x2, y2], [x3, y3]]; | |
| } | |
| textAlign = isLabelInside | |
| ? 'center' | |
| : (labelAlignTo === 'edge' | |
| ? (nx > 0 ? 'right' : 'left') | |
| : (nx > 0 ? 'left' : 'right')); | |
| } | |
| const PI = Math.PI; | |
| let labelRotate = 0; | |
| const rotate = labelModel.get('rotate'); | |
| if (isNumber(rotate)) { | |
| labelRotate = rotate * (PI / 180); | |
| } | |
| else if (labelPosition === 'center') { | |
| labelRotate = 0; | |
| } | |
| else if (rotate === 'radial' || rotate === true) { | |
| const radialAngle = nx < 0 ? -midAngle + PI : -midAngle; | |
| labelRotate = radialAngle; | |
| } | |
| else if (rotate === 'tangential' | |
| && labelPosition !== 'outside' && labelPosition !== 'outer' | |
| ) { | |
| let rad = Math.atan2(nx, ny); | |
| if (rad < 0) { | |
| rad = PI * 2 + rad; | |
| } | |
| const isDown = ny > 0; | |
| if (isDown) { | |
| rad = PI + rad; | |
| } | |
| labelRotate = rad - PI; | |
| } | |
| hasLabelRotate = !!labelRotate; | |
| label.x = textX; | |
| label.y = textY; | |
| label.rotation = labelRotate; | |
| label.setStyle({ | |
| verticalAlign: 'middle' | |
| }); | |
| // Not sectorShape the inside label | |
| if (!isLabelInside) { | |
| const textRect = label.getBoundingRect().clone(); | |
| textRect.applyTransform(label.getComputedTransform()); | |
| // Text has a default 1px stroke. Exclude this. | |
| const margin = (label.style.margin || 0) + 2.1; | |
| textRect.y -= margin / 2; | |
| textRect.height += margin; | |
| labelLayoutList.push({ | |
| label, | |
| labelLine, | |
| position: labelPosition, | |
| len: labelLineLen, | |
| len2: labelLineLen2, | |
| minTurnAngle: labelLineModel.get('minTurnAngle'), | |
| maxSurfaceAngle: labelLineModel.get('maxSurfaceAngle'), | |
| surfaceNormal: new Point(nx, ny), | |
| linePoints: linePoints, | |
| textAlign: textAlign, | |
| labelDistance: labelDistance, | |
| labelAlignTo: labelAlignTo, | |
| edgeDistance: edgeDistance, | |
| bleedMargin: bleedMargin, | |
| rect: textRect, | |
| unconstrainedWidth: textRect.width, | |
| labelStyleWidth: label.style.width | |
| }); | |
| } | |
| else { | |
| label.setStyle({ | |
| align: textAlign | |
| }); | |
| const selectState = label.states.select; | |
| if (selectState) { | |
| selectState.x += label.x; | |
| selectState.y += label.y; | |
| } | |
| } | |
| sector.setTextConfig({ | |
| inside: isLabelInside | |
| }); | |
| }); | |
| if (!hasLabelRotate && seriesModel.get('avoidLabelOverlap')) { | |
| avoidOverlap(labelLayoutList, cx, cy, r, viewWidth, viewHeight, viewLeft, viewTop); | |
| } | |
| for (let i = 0; i < labelLayoutList.length; i++) { | |
| const layout = labelLayoutList[i]; | |
| const label = layout.label; | |
| const labelLine = layout.labelLine; | |
| const notShowLabel = isNaN(label.x) || isNaN(label.y); | |
| if (label) { | |
| label.setStyle({ | |
| align: layout.textAlign | |
| }); | |
| if (notShowLabel) { | |
| each(label.states, setNotShow); | |
| label.ignore = true; | |
| } | |
| const selectState = label.states.select; | |
| if (selectState) { | |
| selectState.x += label.x; | |
| selectState.y += label.y; | |
| } | |
| } | |
| if (labelLine) { | |
| const linePoints = layout.linePoints; | |
| if (notShowLabel || !linePoints) { | |
| each(labelLine.states, setNotShow); | |
| labelLine.ignore = true; | |
| } | |
| else { | |
| limitTurnAngle(linePoints, layout.minTurnAngle); | |
| limitSurfaceAngle(linePoints, layout.surfaceNormal, layout.maxSurfaceAngle); | |
| labelLine.setShape({ points: linePoints }); | |
| // Set the anchor to the midpoint of sector | |
| label.__hostTarget.textGuideLineConfig = { | |
| anchor: new Point(linePoints[0][0], linePoints[0][1]) | |
| }; | |
| } | |
| } | |
| } | |
| } | |