diff --git a/src/ExtensionAPI.ts b/src/ExtensionAPI.ts index dae64dd2cc..cb72d65372 100644 --- a/src/ExtensionAPI.ts +++ b/src/ExtensionAPI.ts @@ -23,32 +23,33 @@ import {CoordinateSystemMaster} from './coord/CoordinateSystem'; import Element from 'zrender/src/Element'; import ComponentModel from './model/Component'; -const availableMethods = { - getDom: 1, - getZr: 1, - getWidth: 1, - getHeight: 1, - getDevicePixelRatio: 1, - dispatchAction: 1, - isDisposed: 1, - on: 1, - off: 1, - getDataURL: 1, - getConnectedDataURL: 1, - getModel: 1, - getOption: 1, - getViewOfComponentModel: 1, - getViewOfSeriesModel: 1, - getId: 1 -}; - -interface ExtensionAPI extends Pick {} +const availableMethods: (keyof EChartsType)[] = [ + 'getDom', + 'getZr', + 'getWidth', + 'getHeight', + 'getDevicePixelRatio', + 'dispatchAction', + 'isDisposed', + 'on', + 'off', + 'getDataURL', + 'getConnectedDataURL', + 'getModel', + 'getOption', + 'getViewOfComponentModel', + 'getViewOfSeriesModel', + 'getId', + 'updateLabelLayout' +]; + +interface ExtensionAPI extends Pick {} abstract class ExtensionAPI { constructor(ecInstance: EChartsType) { - zrUtil.each(availableMethods, function (v, name: string) { - (this as any)[name] = zrUtil.bind((ecInstance as any)[name], ecInstance); + zrUtil.each(availableMethods, function (methodName: string) { + (this as any)[methodName] = zrUtil.bind((ecInstance as any)[methodName], ecInstance); }, this); } diff --git a/src/chart/bar/BarView.ts b/src/chart/bar/BarView.ts index 336c2562cd..d7dc1e14af 100644 --- a/src/chart/bar/BarView.ts +++ b/src/chart/bar/BarView.ts @@ -27,12 +27,11 @@ import { initProps, enableHoverEmphasis, setLabelStyle, - clearStates, updateLabel, - initLabel + initLabel, + removeElement } from '../../util/graphic'; import Path, { PathProps } from 'zrender/src/graphic/Path'; -import * as numberUtil from '../../util/number'; import Group from 'zrender/src/graphic/Group'; import {throttle} from '../../util/throttle'; import {createClipPath} from '../helper/createClipPathFromCoordSys'; @@ -41,7 +40,15 @@ import ChartView from '../../view/Chart'; import List, {DefaultDataVisual} from '../../data/List'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../ExtensionAPI'; -import { StageHandlerProgressParams, ZRElementEvent, ColorString, OrdinalSortInfo, Payload, OrdinalNumber, OrdinalRawValue, DisplayState, ParsedValue } from '../../util/types'; +import { + StageHandlerProgressParams, + ZRElementEvent, + ColorString, + OrdinalSortInfo, + Payload, + OrdinalNumber, + ParsedValue +} from '../../util/types'; import BarSeriesModel, { BarSeriesOption, BarDataItemOption } from './BarSeries'; import type Axis2D from '../../coord/cartesian/Axis2D'; import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; @@ -272,7 +279,15 @@ class BarView extends ChartView { } const el = elementCreator[coord.type]( - seriesModel, data, dataIndex, layout, isHorizontalOrRadial, animationModel, false, getDuring(), roundCap + seriesModel, + data, + dataIndex, + layout, + isHorizontalOrRadial, + animationModel, + false, + getDuring(), + roundCap ); data.setItemGraphicEl(dataIndex, el); group.add(el); @@ -317,13 +332,12 @@ class BarView extends ChartView { } if (el) { - clearStates(el); - if (coord.type === 'cartesian2d' && baseAxis.type === 'category' && (baseAxis as Axis2D).model.get('sort') ) { const rect = layout as RectShape; - let seriesShape, axisShape; + let seriesShape; + let axisShape; if (baseAxis.dim === 'x') { axisShape = { x: rect.x, @@ -346,7 +360,14 @@ class BarView extends ChartView { } if (!isReorder) { - updateProps(el as Path, { shape: seriesShape }, animationModel, newIndex, null, getDuring()); + updateProps( + el as Path, + { shape: seriesShape }, + animationModel, + newIndex, + null, + getDuring() + ); } updateProps(el as Path, { shape: axisShape }, axisAnimationModel, newIndex, null); } @@ -363,7 +384,15 @@ class BarView extends ChartView { } else { el = elementCreator[coord.type]( - seriesModel, data, newIndex, layout, isHorizontalOrRadial, animationModel, true, getDuring(), roundCap + seriesModel, + data, + newIndex, + layout, + isHorizontalOrRadial, + animationModel, + true, + getDuring(), + roundCap ); } @@ -624,7 +653,9 @@ const elementCreator: { }; const labelModel = seriesModel.getModel('label'); - (isUpdate ? updateLabel : initLabel)(rect, data, newIndex, labelModel, seriesModel, animationModel, defaultTextGetter); + (isUpdate ? updateLabel : initLabel)( + rect, data, newIndex, labelModel, seriesModel, animationModel, defaultTextGetter + ); } return rect; @@ -657,7 +688,7 @@ const elementCreator: { sectorShape[animateProperty] = isRadial ? 0 : layout.startAngle; animateTarget[animateProperty] = layout[animateProperty]; (isUpdate ? updateProps : initProps)(sector, { - shape: animateTarget, + shape: animateTarget // __value: typeof dataValue === 'string' ? parseInt(dataValue, 10) : dataValue }, animationModel); } @@ -673,9 +704,9 @@ function removeRect( ) { // Not show text when animating el.removeTextContent(); - updateProps(el, { - shape: { - width: 0 + removeElement(el, { + style: { + opacity: 0 } }, animationModel, dataIndex, function () { el.parent && el.parent.remove(el); @@ -689,9 +720,9 @@ function removeSector( ) { // Not show text when animating el.removeTextContent(); - updateProps(el, { - shape: { - r: el.shape.r0 + removeElement(el, { + style: { + opacity: 0 } }, animationModel, dataIndex, function () { el.parent && el.parent.remove(el); @@ -776,7 +807,7 @@ function updateStyle( labelFetcher: seriesModel, labelDataIndex: dataIndex, defaultText: getDefaultLabel(seriesModel.getData(), dataIndex), - autoColor: style.fill as ColorString, + inheritColor: style.fill as ColorString, defaultOutsidePosition: labelPositionOutside } ); diff --git a/src/chart/bar/PictorialBarView.ts b/src/chart/bar/PictorialBarView.ts index 518630c1cd..48a7774d3d 100644 --- a/src/chart/bar/PictorialBarView.ts +++ b/src/chart/bar/PictorialBarView.ts @@ -190,7 +190,6 @@ class PictorialBarView extends ChartView { } if (bar) { - bar.clearStates(); updateBar(bar, opt, symbolMeta); } else { @@ -870,7 +869,7 @@ function removeBar( bar.__pictorialClipPath && (animationModel = null); zrUtil.each(pathes, function (path) { - graphic.updateProps( + graphic.removeElement( path, { scaleX: 0, scaleY: 0 }, animationModel, dataIndex, function () { bar.parent && bar.parent.remove(bar); @@ -953,7 +952,7 @@ function updateCommon( labelFetcher: opt.seriesModel, labelDataIndex: dataIndex, defaultText: getDefaultLabel(opt.seriesModel.getData(), dataIndex), - autoColor: symbolMeta.style.fill as ColorString, + inheritColor: symbolMeta.style.fill as ColorString, defaultOutsidePosition: barPositionOutside } ); diff --git a/src/chart/candlestick/CandlestickView.ts b/src/chart/candlestick/CandlestickView.ts index 6b9909d72c..636a0af05d 100644 --- a/src/chart/candlestick/CandlestickView.ts +++ b/src/chart/candlestick/CandlestickView.ts @@ -132,7 +132,6 @@ class CandlestickView extends ChartView { el = createNormalBox(itemLayout, newIdx); } else { - graphic.clearStates(el); graphic.updateProps(el, { shape: { points: itemLayout.ends diff --git a/src/chart/custom.ts b/src/chart/custom.ts index 1a06308f7b..6d8ba0106d 100644 --- a/src/chart/custom.ts +++ b/src/chart/custom.ts @@ -17,7 +17,7 @@ * under the License. */ - +// @ts-nocheck import {__DEV__} from '../config'; import { hasOwn, assert, isString, retrieve2, retrieve3, defaults, each, keys, isArrayLike, bind @@ -1377,7 +1377,7 @@ function makeRenderItem( visualColor != null && (itemStyle.fill = visualColor); opacity != null && (itemStyle.opacity = opacity); - const opt = {autoColor: isString(visualColor) ? visualColor : '#000'}; + const opt = {inheritColor: isString(visualColor) ? visualColor : '#000'}; const labelModel = getLabelModel(dataIndexInside, NORMAL); // Now that the feture of "auto adjust text fill/stroke" has been migrated to zrender // since ec5, we should set `isAttached` as `false` here and make compat in diff --git a/src/chart/funnel/FunnelSeries.ts b/src/chart/funnel/FunnelSeries.ts index a67978813a..3e3c98f721 100644 --- a/src/chart/funnel/FunnelSeries.ts +++ b/src/chart/funnel/FunnelSeries.ts @@ -28,7 +28,7 @@ import { BoxLayoutOptionMixin, HorizontalAlign, LabelOption, - LabelGuideLineOption, + LabelLineOption, ItemStyleOption, OptionDataValueNumeric } from '../../util/types'; @@ -51,12 +51,12 @@ export interface FunnelDataItemOption { height?: number | string } label?: FunnelLabelOption - labelLine?: LabelGuideLineOption + labelLine?: LabelLineOption emphasis?: { itemStyle?: ItemStyleOption label?: FunnelLabelOption - labelLine?: LabelGuideLineOption + labelLine?: LabelLineOption } } @@ -80,12 +80,12 @@ export interface FunnelSeriesOption funnelAlign?: HorizontalAlign label?: FunnelLabelOption - labelLine?: LabelGuideLineOption + labelLine?: LabelLineOption itemStyle?: ItemStyleOption emphasis?: { label?: FunnelLabelOption - labelLine?: LabelGuideLineOption + labelLine?: LabelLineOption itemStyle?: ItemStyleOption } @@ -172,8 +172,7 @@ class FunnelSeriesModel extends SeriesModel { length: 20, lineStyle: { // color: 各异, - width: 1, - type: 'solid' + width: 1 } }, itemStyle: { diff --git a/src/chart/funnel/FunnelView.ts b/src/chart/funnel/FunnelView.ts index cfed271740..334510d003 100644 --- a/src/chart/funnel/FunnelView.ts +++ b/src/chart/funnel/FunnelView.ts @@ -23,31 +23,33 @@ import FunnelSeriesModel, {FunnelDataItemOption} from './FunnelSeries'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../ExtensionAPI'; import List from '../../data/List'; -import { ColorString } from '../../util/types'; +import { ColorString, LabelOption } from '../../util/types'; +import Model from '../../model/Model'; +import { setLabelLineStyle } from '../../label/labelGuideHelper'; +import points from '../../layout/points'; const opacityAccessPath = ['itemStyle', 'opacity'] as const; /** * Piece of pie including Sector, Label, LabelLine */ -class FunnelPiece extends graphic.Group { +class FunnelPiece extends graphic.Polygon { constructor(data: List, idx: number) { super(); - const polygon = new graphic.Polygon(); + const polygon = this; const labelLine = new graphic.Polyline(); const text = new graphic.Text(); - this.add(polygon); - this.add(labelLine); polygon.setTextContent(text); + this.setTextGuideLine(labelLine); this.updateData(data, idx, true); } updateData(data: List, idx: number, firstCreate?: boolean) { - const polygon = this.childAt(0) as graphic.Polygon; + const polygon = this; const seriesModel = data.hostModel; const itemModel = data.getItemModel(idx); @@ -91,8 +93,8 @@ class FunnelPiece extends graphic.Group { } _updateLabel(data: List, idx: number) { - const polygon = this.childAt(0); - const labelLine = this.childAt(1) as graphic.Polyline; + const polygon = this; + const labelLine = this.getTextGuideLine(); const labelText = polygon.getTextContent(); const seriesModel = data.hostModel; @@ -109,7 +111,8 @@ class FunnelPiece extends graphic.Group { const visualColor = data.getItemVisual(idx, 'style').fill as ColorString; graphic.setLabelStyle( - labelText, labelModel, labelHoverModel, + // position will not be used in setLabelStyle + labelText, labelModel as Model, labelHoverModel as Model, { labelFetcher: data.hostModel as FunnelSeriesModel, labelDataIndex: idx, @@ -129,11 +132,15 @@ class FunnelPiece extends graphic.Group { outsideFill: visualColor }); - graphic.updateProps(labelLine, { - shape: { - points: labelLayout.linePoints || labelLayout.linePoints - } - }, seriesModel, idx); + const linePoints = labelLayout.linePoints; + + labelLine.setShape({ + points: linePoints + }); + + polygon.textGuideLineConfig = { + anchor: linePoints ? new graphic.Point(linePoints[0][0], linePoints[0][1]) : null + }; // Make sure update style on labelText after setLabelStyle. // Because setLabelStyle will replace a new style on it. @@ -151,22 +158,13 @@ class FunnelPiece extends graphic.Group { z2: 10 }); - labelText.ignore = !labelModel.get('show'); - const labelTextEmphasisState = labelText.ensureState('emphasis'); - labelTextEmphasisState.ignore = !labelHoverModel.get('show'); - - labelLine.ignore = !labelLineModel.get('show'); - const labelLineEmphasisState = labelLine.ensureState('emphasis'); - labelLineEmphasisState.ignore = !labelLineHoverModel.get('show'); - - // Default use item visual color - labelLine.setStyle({ + setLabelLineStyle(polygon, { + normal: labelLineModel, + emphasis: labelLineHoverModel + }, { + // Default use item visual color stroke: visualColor }); - labelLine.setStyle(labelLineModel.getModel('lineStyle').getLineStyle()); - - const lineEmphasisState = labelLine.ensureState('emphasis'); - lineEmphasisState.style = labelLineHoverModel.getModel('lineStyle').getLineStyle(); } } @@ -176,6 +174,8 @@ class FunnelView extends ChartView { private _data: List; + ignoreLabelLineUpdate = true; + render(seriesModel: FunnelSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) { const data = seriesModel.getData(); const oldData = this._data; @@ -192,7 +192,6 @@ class FunnelView extends ChartView { }) .update(function (newIdx, oldIdx) { const piece = oldData.getItemGraphicEl(oldIdx) as FunnelPiece; - graphic.clearStates(piece); piece.updateData(data, newIdx); diff --git a/src/chart/gauge/GaugeView.ts b/src/chart/gauge/GaugeView.ts index 77e3c8c24f..cd766d0de8 100644 --- a/src/chart/gauge/GaugeView.ts +++ b/src/chart/gauge/GaugeView.ts @@ -262,7 +262,7 @@ class GaugeView extends ChartView { y: unitY * (r - splitLineLen - distance) + cy, verticalAlign: unitY < -0.4 ? 'top' : (unitY > 0.4 ? 'bottom' : 'middle'), align: unitX < -0.4 ? 'left' : (unitX > 0.4 ? 'right' : 'center') - }, {autoColor: autoColor}), + }, {inheritColor: autoColor}), silent: true })); } @@ -347,7 +347,6 @@ class GaugeView extends ChartView { }) .update(function (newIdx, oldIdx) { const pointer = oldData.getItemGraphicEl(oldIdx) as PointerPath; - graphic.clearStates(pointer); graphic.updateProps(pointer, { shape: { @@ -424,7 +423,7 @@ class GaugeView extends ChartView { text: data.getName(0), align: 'center', verticalAlign: 'middle' - }, {autoColor: autoColor, forceRich: true}) + }, {inheritColor: autoColor}) })); } } @@ -464,7 +463,7 @@ class GaugeView extends ChartView { height: isNaN(height) ? null : height, align: 'center', verticalAlign: 'middle' - }, {autoColor: autoColor, forceRich: true}) + }, {inheritColor: autoColor}) })); } } diff --git a/src/chart/graph/GraphView.ts b/src/chart/graph/GraphView.ts index 7d604a360a..33539f1083 100644 --- a/src/chart/graph/GraphView.ts +++ b/src/chart/graph/GraphView.ts @@ -32,20 +32,16 @@ import ExtensionAPI from '../../ExtensionAPI'; import GraphSeriesModel, { GraphNodeItemOption, GraphEdgeItemOption } from './GraphSeries'; import { CoordinateSystem } from '../../coord/CoordinateSystem'; import View from '../../coord/View'; -import { GraphNode, GraphEdge } from '../../data/Graph'; -import Displayable from 'zrender/src/graphic/Displayable'; import Symbol from '../helper/Symbol'; import Model from '../../model/Model'; import { Payload } from '../../util/types'; -import { LineLabel } from '../helper/Line'; import List from '../../data/List'; +import Line from '../helper/Line'; +import { GraphNode, GraphEdge } from '../../data/Graph'; const FOCUS_ADJACENCY = '__focusNodeAdjacency'; const UNFOCUS_ADJACENCY = '__unfocusNodeAdjacency'; -const nodeOpacityPath = ['itemStyle', 'opacity'] as const; -const lineOpacityPath = ['lineStyle', 'opacity'] as const; - interface FocusNodePayload extends Payload { dataIndex: number edgeDataIndex: number @@ -55,53 +51,29 @@ function isViewCoordSys(coordSys: CoordinateSystem): coordSys is View { return coordSys.type === 'view'; } -function getItemOpacity( - item: GraphNode | GraphEdge, - opacityPath: typeof nodeOpacityPath | typeof lineOpacityPath -): number { - const opacity = item.getVisual('opacity'); - return opacity != null - ? opacity : item.getModel().get(opacityPath); -} +function fadeInItem(nodeOrEdge: GraphNode | GraphEdge) { + const el = nodeOrEdge.getGraphicEl() as Symbol | Line; + if (el) { + if ((el as Symbol).getSymbolPath) { + (el as Symbol).getSymbolPath().removeState('blur'); + } + else { + (el as Line).getLinePath().removeState('blur'); + } -function fadeOutItem( - item: GraphNode | GraphEdge, - opacityPath: typeof nodeOpacityPath | typeof lineOpacityPath, - opacityRatio?: number -) { - const el = item.getGraphicEl() as Symbol; // TODO Symbol? - let opacity = getItemOpacity(item, opacityPath); - - if (opacityRatio != null) { - opacity == null && (opacity = 1); - opacity *= opacityRatio; } - - el.downplay && el.downplay(); - el.traverse(function (child: LineLabel) { - if (!child.isGroup) { - let opct = child.lineLabelOriginalOpacity; - if (opct == null || opacityRatio != null) { - opct = opacity; - } - child.setStyle('opacity', opct); - } - }); } -function fadeInItem( - item: GraphNode | GraphEdge, - opacityPath: typeof nodeOpacityPath | typeof lineOpacityPath -) { - const opacity = getItemOpacity(item, opacityPath); - const el = item.getGraphicEl() as Symbol; - // Should go back to normal opacity first, consider hoverLayer, - // where current state is copied to elMirror, and support - // emphasis opacity here. - el.traverse(function (child: Displayable) { - !child.isGroup && child.setStyle('opacity', opacity); - }); - el.highlight && el.highlight(); +function fadeOutItem(nodeOrEdge: GraphNode | GraphEdge) { + const el = nodeOrEdge.getGraphicEl() as Symbol | Line; + if (el) { + if ((el as Symbol).getSymbolPath) { + (el as Symbol).getSymbolPath().useState('blur'); + } + else { + (el as Line).getLinePath().useState('blur'); + } + } } class GraphView extends ChartView { @@ -120,7 +92,6 @@ class GraphView extends ChartView { private _model: GraphSeriesModel; private _layoutTimeout: number; - private _unfocusDelayTimer: number; private _layouting: boolean; @@ -214,8 +185,14 @@ class GraphView extends ChartView { (el as any)[UNFOCUS_ADJACENCY] && el.off('mouseout', (el as any)[UNFOCUS_ADJACENCY]); if (itemModel.get('focusNodeAdjacency')) { + const symbolPath = el.getSymbolPath(); + const blurState = symbolPath.ensureState('blur'); + blurState.style = { + // TODO Based on the original opacity. + opacity: 0.1 + }; + el.on('mouseover', (el as any)[FOCUS_ADJACENCY] = function () { - graphView._clearTimer(); api.dispatchAction({ type: 'focusNodeAdjacency', seriesId: seriesModel.id, @@ -230,14 +207,20 @@ class GraphView extends ChartView { }); data.graph.eachEdge(function (edge) { - const el = edge.getGraphicEl(); + const el = edge.getGraphicEl() as Line; (el as any)[FOCUS_ADJACENCY] && el.off('mouseover', (el as any)[FOCUS_ADJACENCY]); (el as any)[UNFOCUS_ADJACENCY] && el.off('mouseout', (el as any)[UNFOCUS_ADJACENCY]); if (edge.getModel().get('focusNodeAdjacency')) { + + const linePath = el.getLinePath(); + const blurState = linePath.ensureState('blur'); + blurState.style = { + opacity: 0.1 + }; + el.on('mouseover', (el as any)[FOCUS_ADJACENCY] = function () { - graphView._clearTimer(); api.dispatchAction({ type: 'focusNodeAdjacency', seriesId: seriesModel.id, @@ -272,8 +255,8 @@ class GraphView extends ChartView { symbolPath.setTextConfig({ rotation: -rad, - position: textPosition - // textOrigin: 'center' + position: textPosition, + origin: 'center' }); const emphasisState = symbolPath.ensureState('emphasis'); zrUtil.extend(emphasisState.textConfig || (emphasisState.textConfig = {}), { @@ -293,29 +276,17 @@ class GraphView extends ChartView { dispose() { this._controller && this._controller.dispose(); this._controllerHost = null; - this._clearTimer(); } _dispatchUnfocus(api: ExtensionAPI) { const self = this; - this._clearTimer(); - this._unfocusDelayTimer = setTimeout(function () { - self._unfocusDelayTimer = null; - api.dispatchAction({ - type: 'unfocusNodeAdjacency', - seriesId: self._model.id - }); - }, 500) as any; + api.dispatchAction({ + type: 'unfocusNodeAdjacency', + seriesId: self._model.id + }); } - _clearTimer() { - if (this._unfocusDelayTimer) { - clearTimeout(this._unfocusDelayTimer); - this._unfocusDelayTimer = null; - } - } - focusNodeAdjacency( seriesModel: GraphSeriesModel, ecModel: GlobalModel, @@ -335,27 +306,27 @@ class GraphView extends ChartView { } graph.eachNode(function (node) { - fadeOutItem(node, nodeOpacityPath, 0.1); + fadeOutItem(node); }); graph.eachEdge(function (edge) { - fadeOutItem(edge, lineOpacityPath, 0.1); + fadeOutItem(edge); }); if (node) { - fadeInItem(node, nodeOpacityPath); + fadeInItem(node); zrUtil.each(node.edges, function (adjacentEdge) { if (adjacentEdge.dataIndex < 0) { return; } - fadeInItem(adjacentEdge, lineOpacityPath); - fadeInItem(adjacentEdge.node1, nodeOpacityPath); - fadeInItem(adjacentEdge.node2, nodeOpacityPath); + fadeInItem(adjacentEdge); + fadeInItem(adjacentEdge.node1); + fadeInItem(adjacentEdge.node2); }); } if (edge) { - fadeInItem(edge, lineOpacityPath); - fadeInItem(edge.node1, nodeOpacityPath); - fadeInItem(edge.node2, nodeOpacityPath); + fadeInItem(edge); + fadeInItem(edge.node1); + fadeInItem(edge.node2); } } @@ -365,10 +336,10 @@ class GraphView extends ChartView { const graph = seriesModel.getData().graph; graph.eachNode(function (node) { - fadeOutItem(node, nodeOpacityPath); + fadeInItem(node); }); graph.eachEdge(function (edge) { - fadeOutItem(edge, lineOpacityPath); + fadeInItem(edge); }); } @@ -437,6 +408,8 @@ class GraphView extends ChartView { this._updateNodeAndLinkScale(); adjustEdge(seriesModel.getGraph(), getNodeGlobalScale(seriesModel)); this._lineDraw.updateLayout(); + // Only update label layout on zoom + api.updateLabelLayout(); }); } @@ -446,8 +419,8 @@ class GraphView extends ChartView { const nodeScale = getNodeGlobalScale(seriesModel); - data.eachItemGraphicEl(function (el, idx) { - el.scaleX = el.scaleY = nodeScale; + data.eachItemGraphicEl(function (el: Symbol, idx) { + el.setSymbolScale(nodeScale); }); } diff --git a/src/chart/helper/Line.ts b/src/chart/helper/Line.ts index d67694d48f..7ae5b8666c 100644 --- a/src/chart/helper/Line.ts +++ b/src/chart/helper/Line.ts @@ -182,6 +182,10 @@ class Line extends graphic.Group { this._updateCommonStl(lineData, idx, seriesScope); }; + getLinePath() { + return this.childAt(0) as graphic.Line; + } + _updateCommonStl(lineData: List, idx: number, seriesScope?: LineDrawSeriesScope) { const seriesModel = lineData.hostModel as SeriesModel; @@ -256,7 +260,7 @@ class Line extends graphic.Group { label.useStyle(graphic.createTextStyle(labelModel, { text: normalText as string }, { - autoColor: defaultLabelColor + inheritColor: defaultLabelColor })); label.__align = label.style.align; diff --git a/src/chart/helper/LineDraw.ts b/src/chart/helper/LineDraw.ts index ea68bd9b0b..d5c5da14a9 100644 --- a/src/chart/helper/LineDraw.ts +++ b/src/chart/helper/LineDraw.ts @@ -204,7 +204,6 @@ class LineDraw { itemEl = new this._LineCtor(newLineData, newIdx, seriesScope); } else { - graphic.clearStates(itemEl); itemEl.updateData(newLineData, newIdx, seriesScope); } diff --git a/src/chart/helper/Symbol.ts b/src/chart/helper/Symbol.ts index bdc7e9936e..e67e654e5a 100644 --- a/src/chart/helper/Symbol.ts +++ b/src/chart/helper/Symbol.ts @@ -46,8 +46,8 @@ class Symbol extends graphic.Group { /** * Original scale */ - private _scaleX: number; - private _scaleY: number; + private _sizeX: number; + private _sizeY: number; private _z2: number; @@ -111,19 +111,6 @@ class Symbol extends graphic.Group { return this.childAt(0) as ECSymbol; } - /** - * Get scale(aka, current symbol size). - * Including the change caused by animation - */ - getScale() { - const symbolPath = this.childAt(0); - return [symbolPath.scaleX, symbolPath.scaleY]; - } - - getOriginalScale() { - return [this._scaleX, this._scaleY]; - } - /** * Highlight symbol */ @@ -182,20 +169,18 @@ class Symbol extends graphic.Group { if (isInit) { const symbolPath = this.childAt(0) as ECSymbol; - // Always fadeIn. Because it has fadeOut animation when symbol is removed.. - // const fadeIn = seriesScope && seriesScope.fadeIn; - const fadeIn = true; const target: PathProps = { - scaleX: this._scaleX, - scaleY: this._scaleY + scaleX: this._sizeX, + scaleY: this._sizeY, + style: { + // Always fadeIn. Because it has fadeOut animation when symbol is removed.. + opacity: symbolPath.style.opacity + } }; - fadeIn && (target.style = { - opacity: symbolPath.style.opacity - }); symbolPath.scaleX = symbolPath.scaleY = 0; - fadeIn && (symbolPath.style.opacity = 0); + symbolPath.style.opacity = 0; graphic.initProps(symbolPath, target, seriesModel, idx); } @@ -244,7 +229,6 @@ class Symbol extends graphic.Group { cursorStyle && symbolPath.attr('cursor', cursorStyle); - // PENDING setColor before setStyle!!! const symbolStyle = data.getItemVisual(idx, 'style'); const visualColor = symbolStyle.fill; if (symbolPath.__isEmptyBrush) { @@ -280,7 +264,7 @@ class Symbol extends graphic.Group { labelFetcher: seriesModel, labelDataIndex: idx, defaultText: getLabelDefaultText, - autoColor: visualColor as ColorString + inheritColor: visualColor as ColorString } ); @@ -289,13 +273,31 @@ class Symbol extends graphic.Group { return useNameLabel ? data.getName(idx) : getDefaultLabel(data, idx); } - this._scaleX = symbolSize[0] / 2; - this._scaleY = symbolSize[1] / 2; - symbolPath.onStateChange = ( - hoverAnimation && seriesModel.isAnimationEnabled() - ) ? onStateChange : null; + this._sizeX = symbolSize[0] / 2; + this._sizeY = symbolSize[1] / 2; + + symbolPath.ensureState('emphasis').style = hoverItemStyle; - graphic.enableHoverEmphasis(symbolPath, hoverItemStyle); + if (hoverAnimation && seriesModel.isAnimationEnabled()) { + this.ensureState('emphasis'); + this.setSymbolScale(1); + } + else { + this.states.emphasis = null; + } + + graphic.enableHoverEmphasis(this); + } + + setSymbolScale(scale: number) { + const emphasisState = this.states.emphasis; + if (emphasisState) { + const hoverScale = Math.max(scale * 1.1, 3 / this._sizeY + scale); + emphasisState.scaleX = hoverScale; + emphasisState.scaleY = hoverScale; + } + + this.scaleX = this.scaleY = scale; } fadeOut(cb: () => void, opt?: { @@ -307,7 +309,7 @@ class Symbol extends graphic.Group { // Not show text when animating !(opt && opt.keepLabel) && (symbolPath.removeTextContent()); - graphic.updateProps( + graphic.removeElement( symbolPath, { style: { @@ -330,33 +332,6 @@ class Symbol extends graphic.Group { } } -function onStateChange(this: ECSymbol, fromState: DisplayState, toState: DisplayState) { - // Do not support this hover animation util some scenario required. - // Animation can only be supported in hover layer when using `el.incremetal`. - if (this.incremental || this.useHoverLayer) { - return; - } - - const scale = (this.parent as Symbol).getOriginalScale(); - if (toState === 'emphasis') { - const ratio = scale[1] / scale[0]; - const emphasisOpt = { - scaleX: Math.max(scale[0] * 1.1, scale[0] + 3), - scaleY: Math.max(scale[1] * 1.1, scale[1] + 3 * ratio) - }; - // FIXME - // modify it after support stop specified animation. - // toState === fromState - // ? (this.stopAnimation(), this.attr(emphasisOpt)) - this.animateTo(emphasisOpt, { duration: 400, easing: 'elasticOut' }); - } - else if (toState === 'normal') { - this.animateTo({ - scaleX: scale[0], - scaleY: scale[1] - }, { duration: 400, easing: 'elasticOut' }); - } -} function driftSymbol(this: ECSymbol, dx: number, dy: number) { this.parent.drift(dx, dy); diff --git a/src/chart/helper/SymbolDraw.ts b/src/chart/helper/SymbolDraw.ts index 938a4d8c8c..37da3a7d27 100644 --- a/src/chart/helper/SymbolDraw.ts +++ b/src/chart/helper/SymbolDraw.ts @@ -177,8 +177,6 @@ class SymbolDraw { symbolEl.setPosition(point); } else { - graphic.clearStates(symbolEl); - symbolEl.updateData(data, newIdx, seriesScope); graphic.updateProps(symbolEl, { x: point[0], diff --git a/src/chart/line/LineSeries.ts b/src/chart/line/LineSeries.ts index 1fcb7485cb..d4ad0d57c7 100644 --- a/src/chart/line/LineSeries.ts +++ b/src/chart/line/LineSeries.ts @@ -53,6 +53,7 @@ export interface LineDataItemOption extends SymbolOptionMixin { } } + export interface LineSeriesOption extends SeriesOption, SeriesOnCartesianOptionMixin, SeriesOnPolarOptionMixin, @@ -78,6 +79,11 @@ export interface LineSeriesOption extends SeriesOption, origin?: 'auto' | 'start' | 'end' } + emphasis?: { + label?: LabelOption + itemStyle?: ItemStyleOption + } + step?: false | 'start' | 'end' | 'middle' smooth?: boolean @@ -86,7 +92,6 @@ export interface LineSeriesOption extends SeriesOption, connectNulls?: boolean - showSymbol?: boolean // false | 'auto': follow the label interval strategy. // true: show all symbols. diff --git a/src/chart/line/LineView.ts b/src/chart/line/LineView.ts index e0342f31cf..8fb1f801c2 100644 --- a/src/chart/line/LineView.ts +++ b/src/chart/line/LineView.ts @@ -786,7 +786,6 @@ class LineView extends ChartView { polygon.stopAnimation(); graphic.updateProps(polygon, { shape: { - points: next, stackedOnPoints: stackedOnNext } }, seriesModel); diff --git a/src/chart/map/MapView.ts b/src/chart/map/MapView.ts index 6e0bd06736..d5ff91172f 100644 --- a/src/chart/map/MapView.ts +++ b/src/chart/map/MapView.ts @@ -83,7 +83,7 @@ class MapView extends ChartView { ) ) { if (mapModel.needsDrawMap) { - const mapDraw = this._mapDraw || new MapDraw(api, true); + const mapDraw = this._mapDraw || new MapDraw(api); group.add(mapDraw.group); mapDraw.draw(mapModel, ecModel, api, this, payload); diff --git a/src/chart/parallel/ParallelView.ts b/src/chart/parallel/ParallelView.ts index fe874d1ab2..e3d50833db 100644 --- a/src/chart/parallel/ParallelView.ts +++ b/src/chart/parallel/ParallelView.ts @@ -77,7 +77,6 @@ class ParallelView extends ChartView { function update(newDataIndex: number, oldDataIndex: number) { const line = oldData.getItemGraphicEl(oldDataIndex) as graphic.Polyline; - graphic.clearStates(line); const points = createLinePoints(data, newDataIndex, dimensions, coordSys); data.setItemGraphicEl(newDataIndex, line); diff --git a/src/chart/pie/PieSeries.ts b/src/chart/pie/PieSeries.ts index f1bc70d413..de34310934 100644 --- a/src/chart/pie/PieSeries.ts +++ b/src/chart/pie/PieSeries.ts @@ -30,7 +30,7 @@ import { SeriesOption, CallbackDataParams, CircleLayoutOptionMixin, - LabelGuideLineOption, + LabelLineOption, ItemStyleOption, LabelOption, BoxLayoutOptionMixin, @@ -44,25 +44,33 @@ import List from '../../data/List'; interface PieLabelOption extends Omit { rotate?: number alignTo?: 'none' | 'labelLine' | 'edge' - margin?: string | number + edgeDistance?: string | number bleedMargin?: number distanceToLabelLine?: number position?: LabelOption['position'] | 'outer' | 'inner' | 'center' } +interface PieLabelLineOption extends LabelLineOption { + /** + * Max angle between labelLine and surface normal. + * 0 - 180 + */ + maxSurfaceAngle?: number +} + export interface PieDataItemOption extends OptionDataItemObject, SelectableTarget { itemStyle?: ItemStyleOption label?: PieLabelOption - labelLine?: LabelGuideLineOption + labelLine?: PieLabelLineOption emphasis?: { itemStyle?: ItemStyleOption label?: PieLabelOption - labelLine?: LabelGuideLineOption + labelLine?: PieLabelLineOption } } export interface PieSeriesOption extends @@ -81,7 +89,7 @@ export interface PieSeriesOption extends // TODO: TYPE Color Callback itemStyle?: ItemStyleOption label?: PieLabelOption - labelLine?: LabelGuideLineOption + labelLine?: PieLabelLineOption clockwise?: boolean startAngle?: number @@ -99,7 +107,7 @@ export interface PieSeriesOption extends emphasis?: { itemStyle?: ItemStyleOption label?: PieLabelOption - labelLine?: LabelGuideLineOption + labelLine?: PieLabelLineOption } animationType?: 'expansion' | 'scale' @@ -222,10 +230,8 @@ class PieSeriesModel extends SeriesModel { // 选中时扇区偏移量 selectedOffset: 10, // 高亮扇区偏移量 - hoverOffset: 10, + hoverOffset: 5, - // If use strategy to avoid label overlapping - avoidLabelOverlap: true, // 选择模式,默认关闭,可选single,multiple // selectedMode: false, // 南丁格尔玫瑰图模式,'radius'(半径) | 'area'(面积) @@ -246,16 +252,18 @@ class PieSeriesModel extends SeriesModel { height: null, label: { + // color: 'inherit', // If rotate around circle rotate: 0, show: true, + overflow: 'truncate', // 'outer', 'inside', 'center' position: 'outer', // 'none', 'labelLine', 'edge'. Works only when position is 'outer' alignTo: 'none', // Closest distance between label and chart edge. // Works only position is 'outer' and alignTo is 'edge'. - margin: '25%', + edgeDistance: '25%', // Works only position is 'outer' and alignTo is not 'edge'. bleedMargin: 10, // Distance between text and label line. @@ -272,6 +280,8 @@ class PieSeriesModel extends SeriesModel { // 引导线两段中的第二段长度 length2: 15, smooth: false, + minTurnAngle: 90, + maxSurfaceAngle: 90, lineStyle: { // color: 各异, width: 1, @@ -282,13 +292,25 @@ class PieSeriesModel extends SeriesModel { borderWidth: 1 }, + labelLayout: { + // Hide the overlapped label. + hideOverlap: true + }, + + // If use strategy to avoid label overlapping + avoidLabelOverlap: true, + // Animation type. Valid values: expansion, scale animationType: 'expansion', + animationDuration: 1000, + // Animation type when update. Valid values: transition, expansion animationTypeUpdate: 'transition', - animationEasing: 'cubicOut' + animationEasingUpdate: 'cubicInOut', + animationDurationUpdate: 500, + animationEasing: 'cubicInOut' }; } diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index 2c1d6153c3..c4c73afd75 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -24,11 +24,12 @@ import * as graphic from '../../util/graphic'; import ChartView from '../../view/Chart'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../ExtensionAPI'; -import { Payload, DisplayState, ECElement, ColorString } from '../../util/types'; +import { Payload, ColorString, ECElement } from '../../util/types'; import List from '../../data/List'; -import PieSeriesModel, {PieDataItemOption} from './PieSeries'; -import { Dictionary } from 'zrender/src/core/types'; -import Element from 'zrender/src/Element'; +import PieSeriesModel, {PieDataItemOption, PieSeriesOption} from './PieSeries'; +import labelLayout from './labelLayout'; +import { setLabelLineStyle } from '../../label/labelGuideHelper'; +import Model from '../../model/Model'; function updateDataSelected( this: PiePiece, @@ -40,7 +41,6 @@ function updateDataSelected( const data = seriesModel.getData(); const dataIndex = graphic.getECData(this).dataIndex; const name = data.getName(dataIndex); - const selectedOffset = seriesModel.get('selectedOffset'); api.dispatchAction({ type: 'pieToggleSelect', @@ -48,78 +48,35 @@ function updateDataSelected( name: name, seriesId: seriesModel.id }); - - data.each(function (idx) { - toggleItemSelected( - data.getItemGraphicEl(idx), - data.getItemLayout(idx), - seriesModel.isSelected(data.getName(idx)), - selectedOffset, - hasAnimation - ); - }); -} - -function toggleItemSelected( - el: Element, - layout: Dictionary, // FIXME:TS make a type. - isSelected: boolean, - selectedOffset: number, - hasAnimation: boolean -): void { - const midAngle = (layout.startAngle + layout.endAngle) / 2; - - const dx = Math.cos(midAngle); - const dy = Math.sin(midAngle); - - const offset = isSelected ? selectedOffset : 0; - const obj = { - x: dx * offset, - y: dy * offset - }; - - hasAnimation - // animateTo will stop revious animation like update transition - ? el.animate() - .when(200, obj) - .start('bounceOut') - : el.attr(obj); } /** * Piece of pie including Sector, Label, LabelLine */ -class PiePiece extends graphic.Group { +class PiePiece extends graphic.Sector { - constructor(data: List, idx: number) { + constructor(data: List, idx: number, startAngle: number) { super(); - const sector = new graphic.Sector({ - z2: 2 - }); + this.z2 = 2; const polyline = new graphic.Polyline(); const text = new graphic.Text(); - this.add(sector); - this.add(polyline); - sector.setTextContent(text); + this.setTextGuideLine(polyline); + + this.setTextContent(text); - this.updateData(data, idx, true); + this.updateData(data, idx, startAngle, true); } - updateData(data: List, idx: number, firstCreate?: boolean): void { - const sector = this.childAt(0) as graphic.Sector; + updateData(data: List, idx: number, startAngle?: number, firstCreate?: boolean): void { + const sector = this; const seriesModel = data.hostModel as PieSeriesModel; const itemModel = data.getItemModel(idx); const layout = data.getItemLayout(idx); const sectorShape = zrUtil.extend({}, layout); - // Not animate label - sectorShape.label = null; - sectorShape.viewRect = null; - - const animationTypeUpdate = seriesModel.getShallow('animationTypeUpdate'); if (firstCreate) { sector.setShape(sectorShape); @@ -135,103 +92,82 @@ class PiePiece extends graphic.Group { } // Expansion else { - sector.shape.endAngle = layout.startAngle; - graphic.updateProps(sector, { - shape: { - endAngle: layout.endAngle - } - }, seriesModel, idx); + if (startAngle != null) { + sector.setShape({ startAngle, endAngle: startAngle }); + graphic.initProps(sector, { + shape: { + startAngle: layout.startAngle, + endAngle: layout.endAngle + } + }, seriesModel, idx); + } + else { + sector.shape.endAngle = layout.startAngle; + graphic.updateProps(sector, { + shape: { + endAngle: layout.endAngle + } + }, seriesModel, idx); + } } } else { - if (animationTypeUpdate === 'expansion') { - // Sectors are set to be target shape and an overlaying clipPath is used for animation - sector.setShape(sectorShape); - } - else { - // Transition animation from the old shape - graphic.updateProps(sector, { - shape: sectorShape - }, seriesModel, idx); - } + // Transition animation from the old shape + graphic.updateProps(sector, { + shape: sectorShape + }, seriesModel, idx); } sector.useStyle(data.getItemVisual(idx, 'style')); const sectorEmphasisState = sector.ensureState('emphasis'); sectorEmphasisState.style = itemModel.getModel(['emphasis', 'itemStyle']).getItemStyle(); + const sectorSelectState = sector.ensureState('select'); + const midAngle = (layout.startAngle + layout.endAngle) / 2; + const offset = seriesModel.get('selectedOffset'); + const dx = Math.cos(midAngle) * offset; + const dy = Math.sin(midAngle) * offset; + sectorSelectState.x = dx; + sectorSelectState.y = dy; + const cursorStyle = itemModel.getShallow('cursor'); cursorStyle && sector.attr('cursor', cursorStyle); - // Toggle selected - toggleItemSelected( - this, - data.getItemLayout(idx), - seriesModel.isSelected(data.getName(idx)), - seriesModel.get('selectedOffset'), - seriesModel.get('animation') - ); + this._updateLabel(seriesModel, data, idx); - // Label and text animation should be applied only for transition type animation when update - const withAnimation = !firstCreate && animationTypeUpdate === 'transition'; - this._updateLabel(data, idx, withAnimation); - - (this as ECElement).onStateChange = !seriesModel.get('silent') - ? function (fromState: DisplayState, toState: DisplayState): void { - if (seriesModel.isAnimationEnabled() && itemModel.get('hoverAnimation')) { - if (toState === 'emphasis') { - // Sector may has animation of updating data. Force to move to the last frame - // Or it may stopped on the wrong shape - sector.stopAnimation(null, true); - sector.animateTo({ - shape: { - r: layout.r + seriesModel.get('hoverOffset') - } - }, { duration: 300, easing: 'elasticOut' }); - } - else { - sector.stopAnimation(null, true); - sector.animateTo({ - shape: { - r: layout.r - } - }, { duration: 300, easing: 'elasticOut' }); - } - } - } - : null; + const emphasisState = sector.ensureState('emphasis'); + emphasisState.shape = { + r: layout.r + (itemModel.get('hoverAnimation') // TODO: Change a name. + ? seriesModel.get('hoverOffset') : 0) + }; + + const labelLine = sector.getTextGuideLine(); + const labelText = sector.getTextContent(); + + labelLine.states.select = { + x: dx, y: dy + }; + // TODO: needs dx, dy in zrender? + labelText.states.select = { + x: dx, + y: dy + }; graphic.enableHoverEmphasis(this); + + // State will be set after all rendered in the pipeline. + (sector as ECElement).selected = seriesModel.isSelected(data.getName(idx)); } - private _updateLabel(data: List, idx: number, withAnimation: boolean): void { - const sector = this.childAt(0); - const labelLine = this.childAt(1) as graphic.Polyline; - const labelText = sector.getTextContent() as graphic.Text; + private _updateLabel(seriesModel: PieSeriesModel, data: List, idx: number): void { + const sector = this; + const labelText = sector.getTextContent(); - const seriesModel = data.hostModel; const itemModel = data.getItemModel(idx); - const layout = data.getItemLayout(idx); - const labelLayout = layout.label; - // let visualColor = data.getItemVisual(idx, 'color'); const labelTextEmphasisState = labelText.ensureState('emphasis'); - const labelLineEmphasisState = labelLine.ensureState('emphasis'); - - if (!labelLayout || isNaN(labelLayout.x) || isNaN(labelLayout.y)) { - labelText.ignore = labelTextEmphasisState.ignore = true; - labelLine.ignore = labelLineEmphasisState.ignore = true; - return; - } - const targetLineShape: { - points: number[][] - } = { - points: labelLayout.linePoints || [ - [labelLayout.x, labelLayout.y], [labelLayout.x, labelLayout.y], [labelLayout.x, labelLayout.y] - ] - }; const labelModel = itemModel.getModel('label'); const labelHoverModel = itemModel.getModel(['emphasis', 'label']); const labelLineModel = itemModel.getModel('labelLine'); @@ -241,84 +177,46 @@ class PiePiece extends graphic.Group { const visualColor = style && style.fill as ColorString; graphic.setLabelStyle( - labelText, - labelModel, - labelHoverModel, + sector, + labelModel as Model>, // position / rotate won't be used. + labelHoverModel as Model>, { labelFetcher: data.hostModel as PieSeriesModel, labelDataIndex: idx, - defaultText: labelLayout.text + inheritColor: visualColor, + defaultText: seriesModel.getFormattedLabel(idx, 'normal') + || data.getName(idx) }, { - align: labelLayout.textAlign, - verticalAlign: labelLayout.verticalAlign, opacity: style && style.opacity } ); // Set textConfig on sector. sector.setTextConfig({ - local: true, - inside: !!labelLayout.inside, - insideStroke: visualColor, - // insideFill: 'auto', - outsideFill: visualColor + // reset position, rotation + position: null, + rotation: null }); - const targetTextStyle = { - x: labelLayout.x, - y: labelLayout.y - }; - if (withAnimation) { - graphic.updateProps(labelLine, { - shape: targetLineShape - }, seriesModel, idx); - - graphic.updateProps(labelText, { - style: targetTextStyle - }, seriesModel, idx); - } - else { - labelLine.attr({ - shape: targetLineShape - }); - // Make sure update style on labelText after setLabelStyle. - // Because setLabelStyle will replace a new style on it. - labelText.attr({ - style: targetTextStyle - }); - } + // Make sure update style on labelText after setLabelStyle. + // Because setLabelStyle will replace a new style on it. labelText.attr({ - rotation: labelLayout.rotation, - originX: labelLayout.x, - originY: labelLayout.y, z2: 10 }); labelText.ignore = !labelModel.get('show'); labelTextEmphasisState.ignore = !labelHoverModel.get('show'); - labelLine.ignore = !labelLineModel.get('show'); - labelLineEmphasisState.ignore = !labelLineHoverModel.get('show'); - // Default use item visual color - labelLine.setStyle({ + setLabelLineStyle(this, { + normal: labelLineModel, + emphasis: labelLineHoverModel + }, { stroke: visualColor, opacity: style && style.opacity }); - labelLine.setStyle(labelLineModel.getModel('lineStyle').getLineStyle()); - - const lineEmphasisState = labelLine.ensureState('emphasis'); - lineEmphasisState.style = labelLineHoverModel.getModel('lineStyle').getLineStyle(); - - let smooth = labelLineModel.get('smooth'); - if (smooth && smooth === true) { - smooth = 0.4; - } - labelLine.setShape({ - smooth: smooth as number - }); } } @@ -328,6 +226,8 @@ class PieView extends ChartView { static type = 'pie'; + ignoreLabelLineUpdate = true; + private _sectorGroup: graphic.Group; private _data: List; @@ -337,33 +237,43 @@ class PieView extends ChartView { } render(seriesModel: PieSeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void { + const data = seriesModel.getData(); if (payload && (payload.from === this.uid)) { + // update selected status + data.each(function (idx) { + const el = data.getItemGraphicEl(idx); + (el as ECElement).selected = seriesModel.isSelected(data.getName(idx)); + }); + return; } - const data = seriesModel.getData(); const oldData = this._data; const group = this.group; const hasAnimation = ecModel.get('animation'); - const isFirstRender = !oldData; - const animationType = seriesModel.get('animationType'); - const animationTypeUpdate = seriesModel.get('animationTypeUpdate'); const onSectorClick = zrUtil.curry( updateDataSelected, this.uid, seriesModel, hasAnimation, api ); const selectedMode = seriesModel.get('selectedMode'); + + let startAngle: number; + // First render + if (!oldData && data.count() > 0) { + let shape = data.getItemLayout(0) as graphic.Sector['shape']; + for (let s = 1; isNaN(shape && shape.startAngle) && s < data.count(); ++s) { + shape = data.getItemLayout(s); + } + if (shape) { + startAngle = shape.startAngle; + } + } + data.diff(oldData) .add(function (idx) { - const piePiece = new PiePiece(data, idx); - // Default expansion animation - if (isFirstRender && animationType !== 'scale') { - piePiece.eachChild(function (child) { - child.stopAnimation(null, true); - }); - } + const piePiece = new PiePiece(data, idx, startAngle); selectedMode && piePiece.on('click', onSectorClick); @@ -374,15 +284,7 @@ class PieView extends ChartView { .update(function (newIdx, oldIdx) { const piePiece = oldData.getItemGraphicEl(oldIdx) as PiePiece; - graphic.clearStates(piePiece); - - if (!isFirstRender && animationTypeUpdate !== 'transition') { - piePiece.eachChild(function (child) { - child.stopAnimation(null, true); - }); - } - - piePiece.updateData(data, newIdx); + piePiece.updateData(data, newIdx, startAngle); piePiece.off('click'); selectedMode && piePiece.on('click', onSectorClick); @@ -395,61 +297,16 @@ class PieView extends ChartView { }) .execute(); - if ( - hasAnimation && data.count() > 0 - && (isFirstRender ? animationType !== 'scale' : animationTypeUpdate !== 'transition') - ) { - let shape = data.getItemLayout(0); - for (let s = 1; isNaN(shape.startAngle) && s < data.count(); ++s) { - shape = data.getItemLayout(s); - } + labelLayout(seriesModel); - const r = Math.max(api.getWidth(), api.getHeight()) / 2; - - const removeClipPath = zrUtil.bind(group.removeClipPath, group); - group.setClipPath(this._createClipPath( - shape.cx, shape.cy, r, shape.startAngle, shape.clockwise, removeClipPath, seriesModel, isFirstRender - )); + // Always use initial animation. + if (seriesModel.get('animationTypeUpdate') !== 'expansion') { + this._data = data; } - else { - // clipPath is used in first-time animation, so remove it when otherwise. See: #8994 - group.removeClipPath(); - } - - this._data = data; } dispose() {} - _createClipPath( - cx: number, cy: number, r: number, - startAngle: number, clockwise: boolean, - // @ts-ignore FIXME:TS make type in util.grpahic - cb, - seriesModel: PieSeriesModel, isFirstRender: boolean - ): graphic.Sector { - const clipPath = new graphic.Sector({ - shape: { - cx: cx, - cy: cy, - r0: 0, - r: r, - startAngle: startAngle, - endAngle: startAngle, - clockwise: clockwise - } - }); - - const initOrUpdate = isFirstRender ? graphic.initProps : graphic.updateProps; - initOrUpdate(clipPath, { - shape: { - endAngle: startAngle + (clockwise ? 1 : -1) * Math.PI * 2 - } - }, seriesModel, cb); - - return clipPath; - } - containPoint(point: number[], seriesModel: PieSeriesModel): boolean { const data = seriesModel.getData(); const itemLayout = data.getItemLayout(0); diff --git a/src/chart/pie/labelLayout.ts b/src/chart/pie/labelLayout.ts index 04542b508a..d47f18fa3a 100644 --- a/src/chart/pie/labelLayout.ts +++ b/src/chart/pie/labelLayout.ts @@ -18,34 +18,35 @@ */ // FIXME emphasis label position is not same with normal label position - -import * as textContain from 'zrender/src/contain/text'; import {parsePercent} from '../../util/number'; import PieSeriesModel, { PieSeriesOption, PieDataItemOption } from './PieSeries'; import { VectorArray } from 'zrender/src/core/vector'; -import { HorizontalAlign, VerticalAlign, ZRRectLike } from '../../util/types'; +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 } from 'zrender/src/core/util'; +import { limitTurnAngle, limitSurfaceAngle } from '../../label/labelGuideHelper'; +import { shiftLayoutOnY } from '../../label/labelLayoutHelper'; const RADIAN = Math.PI / 180; interface LabelLayout { - x: number - y: number + label: ZRText, + labelLine: Polyline, position: PieSeriesOption['label']['position'], - height: number len: number len2: number + minTurnAngle: number + maxSurfaceAngle: number + surfaceNormal: Point linePoints: VectorArray[] textAlign: HorizontalAlign - verticalAlign: VerticalAlign, - rotation: number, - inside: boolean, labelDistance: number, labelAlignTo: PieSeriesOption['label']['alignTo'], - labelMargin: number, + edgeDistance: number, bleedMargin: PieSeriesOption['label']['bleedMargin'], - textRect: ZRRectLike, - text: string, - font: string + rect: BoundingRect } function adjustSingleSide( @@ -60,116 +61,75 @@ function adjustSingleSide( viewTop: number, farthestX: number ) { - list.sort(function (a, b) { - return a.y - b.y; - }); - - function shiftDown(start: number, end: number, delta: number, dir: number) { - for (let j = start; j < end; j++) { - if (list[j].y + delta > viewTop + viewHeight) { - break; - } - - list[j].y += delta; - if (j > start - && j + 1 < end - && list[j + 1].y > list[j].y + list[j].height - ) { - shiftUp(j, delta / 2); - return; - } - } - - shiftUp(end - 1, delta / 2); + if (list.length < 2) { + return; } - function shiftUp(end: number, delta: number) { - for (let j = end; j >= 0; j--) { - if (list[j].y - delta < viewTop) { - break; - } - list[j].y -= delta; - if (j > 0 - && list[j].y > list[j - 1].y + list[j - 1].height - ) { - break; - } + 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); + item.label.x = cx + (dx + item.len2) * dir; } } - function changeX( - list: LabelLayout[], isDownList: boolean, - cx: number, cy: number, r: number, - dir: 1 | -1 - ) { - let lastDeltaX = dir > 0 - ? isDownList // right-side - ? Number.MAX_VALUE // down - : 0 // up - : isDownList // left-side - ? Number.MAX_VALUE // down - : 0; // up - - for (let i = 0, l = list.length; i < l; i++) { - if (list[i].labelAlignTo !== 'none') { - continue; - } + // 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; - const deltaY = Math.abs(list[i].y - cy); - const length = list[i].len; - const length2 = list[i].len2; - let deltaX = (deltaY < r + length) - ? Math.sqrt( - (r + length + length2) * (r + length + length2) - - deltaY * deltaY - ) - : Math.abs(list[i].x - cx); - if (isDownList && deltaX >= lastDeltaX) { - // right-down, left-down - deltaX = lastDeltaX - 10; + for (let i = 0; i < items.length; i++) { + if (items[i].labelAlignTo !== 'none') { + continue; } - if (!isDownList && deltaX <= lastDeltaX) { - // right-up, left-up - deltaX = lastDeltaX + 10; + 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 = dx < rA + ? Math.sqrt(dy * dy / (1 - dx * dx / rA / rA)) + : rA; + semi.rB = rB; + semi.maxY = dy; } - - list[i].x = cx + deltaX * dir; - lastDeltaX = deltaX; + semi.list.push(item); } + + recalculateXOnSemiToAlignOnEllipseCurve(topSemi); + recalculateXOnSemiToAlignOnEllipseCurve(bottomSemi); } - let lastY = 0; - let delta; const len = list.length; - const upList = []; - const downList = []; for (let i = 0; i < len; i++) { if (list[i].position === 'outer' && list[i].labelAlignTo === 'labelLine') { - const dx = list[i].x - farthestX; + const dx = list[i].label.x - farthestX; list[i].linePoints[1][0] += dx; - list[i].x = farthestX; - } - - delta = list[i].y - lastY; - if (delta < 0) { - shiftDown(i, len, -delta, dir); + list[i].label.x = farthestX; } - lastY = list[i].y + list[i].height; - } - if (viewHeight - lastY < 0) { - shiftUp(len - 1, lastY - viewHeight); } - for (let i = 0; i < len; i++) { - if (list[i].y >= cy) { - downList.push(list[i]); - } - else { - upList.push(list[i]); - } + + if (shiftLayoutOnY(list, viewTop, viewTop + viewHeight)) { + recalculateX(list); } - changeX(upList, false, cx, cy, r, dir); - changeX(downList, true, cx, cy, r, dir); } function avoidOverlap( @@ -187,15 +147,16 @@ function avoidOverlap( 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 (labelLayoutList[i].x < cx) { - leftmostX = Math.min(leftmostX, labelLayoutList[i].x); + if (label.x < cx) { + leftmostX = Math.min(leftmostX, label.x); leftList.push(labelLayoutList[i]); } else { - rightmostX = Math.max(rightmostX, labelLayoutList[i].x); + rightmostX = Math.max(rightmostX, label.x); rightList.push(labelLayoutList[i]); } } @@ -205,6 +166,7 @@ function avoidOverlap( for (let i = 0; i < labelLayoutList.length; i++) { const layout = labelLayoutList[i]; + const label = layout.label; if (isPositionCenter(layout)) { continue; } @@ -213,70 +175,67 @@ function avoidOverlap( if (linePoints) { const isAlignToEdge = layout.labelAlignTo === 'edge'; - let realTextWidth = layout.textRect.width; + let realTextWidth = layout.rect.width; let targetTextWidth; if (isAlignToEdge) { - if (layout.x < cx) { + if (label.x < cx) { targetTextWidth = linePoints[2][0] - layout.labelDistance - - viewLeft - layout.labelMargin; + - viewLeft - layout.edgeDistance; } else { - targetTextWidth = viewLeft + viewWidth - layout.labelMargin + targetTextWidth = viewLeft + viewWidth - layout.edgeDistance - linePoints[2][0] - layout.labelDistance; } } else { - if (layout.x < cx) { - targetTextWidth = layout.x - viewLeft - layout.bleedMargin; + if (label.x < cx) { + targetTextWidth = label.x - viewLeft - layout.bleedMargin; } else { - targetTextWidth = viewLeft + viewWidth - layout.x - layout.bleedMargin; + targetTextWidth = viewLeft + viewWidth - label.x - layout.bleedMargin; } } - if (targetTextWidth < layout.textRect.width) { + if (targetTextWidth < layout.rect.width) { // TODOTODO // layout.text = textContain.truncateText(layout.text, targetTextWidth, layout.font); + layout.label.style.width = targetTextWidth; if (layout.labelAlignTo === 'edge') { - realTextWidth = textContain.getWidth(layout.text, layout.font); + realTextWidth = targetTextWidth; + // realTextWidth = textContain.getWidth(layout.text, layout.font); } } const dist = linePoints[1][0] - linePoints[2][0]; if (isAlignToEdge) { - if (layout.x < cx) { - linePoints[2][0] = viewLeft + layout.labelMargin + realTextWidth + layout.labelDistance; + if (label.x < cx) { + linePoints[2][0] = viewLeft + layout.edgeDistance + realTextWidth + layout.labelDistance; } else { - linePoints[2][0] = viewLeft + viewWidth - layout.labelMargin + linePoints[2][0] = viewLeft + viewWidth - layout.edgeDistance - realTextWidth - layout.labelDistance; } } else { - if (layout.x < cx) { - linePoints[2][0] = layout.x + layout.labelDistance; + if (label.x < cx) { + linePoints[2][0] = label.x + layout.labelDistance; } else { - linePoints[2][0] = layout.x - layout.labelDistance; + linePoints[2][0] = label.x - layout.labelDistance; } linePoints[1][0] = linePoints[2][0] + dist; } - linePoints[1][1] = linePoints[2][1] = layout.y; + linePoints[1][1] = linePoints[2][1] = label.y; } } } -function isPositionCenter(layout: LabelLayout) { +function isPositionCenter(sectorShape: LabelLayout) { // Not change x for center label - return layout.position === 'center'; + return sectorShape.position === 'center'; } export default function ( - seriesModel: PieSeriesModel, - r: number, - viewWidth: number, - viewHeight: number, - viewLeft: number, - viewTop: number + seriesModel: PieSeriesModel ) { const data = seriesModel.getData(); const labelLayoutList: LabelLayout[] = []; @@ -285,8 +244,22 @@ export default function ( 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; + } + data.each(function (idx) { - const layout = data.getItemLayout(idx); + const sector = data.getItemGraphicEl(idx) as Sector; + const sectorShape = sector.shape; + const label = sector.getTextContent(); + const labelLine = sector.getTextGuideLine(); const itemModel = data.getItemModel(idx); const labelModel = itemModel.getModel('label'); @@ -294,9 +267,8 @@ export default function ( const labelPosition = labelModel.get('position') || itemModel.get(['emphasis', 'label', 'position']); const labelDistance = labelModel.get('distanceToLabelLine'); const labelAlignTo = labelModel.get('alignTo'); - const labelMargin = parsePercent(labelModel.get('margin'), viewWidth); + const edgeDistance = parsePercent(labelModel.get('edgeDistance'), viewWidth); const bleedMargin = labelModel.get('bleedMargin'); - const font = labelModel.getFont(); const labelLineModel = itemModel.getModel('labelLine'); let labelLineLen = labelLineModel.get('length'); @@ -304,54 +276,53 @@ export default function ( let labelLineLen2 = labelLineModel.get('length2'); labelLineLen2 = parsePercent(labelLineLen2, viewWidth); - if (layout.angle < minShowLabelRadian) { + if (Math.abs(sectorShape.endAngle - sectorShape.startAngle) < minShowLabelRadian) { + each(label.states, setNotShow); + label.ignore = true; return; } - const midAngle = (layout.startAngle + layout.endAngle) / 2; - const dx = Math.cos(midAngle); - const dy = Math.sin(midAngle); + 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; + let textAlign: ZRTextAlign; - cx = layout.cx; - cy = layout.cy; + cx = sectorShape.cx; + cy = sectorShape.cy; - const text = seriesModel.getFormattedLabel(idx, 'normal') - || data.getName(idx); - const textRect = textContain.getBoundingRect(text, font, textAlign, 'top'); const isLabelInside = labelPosition === 'inside' || labelPosition === 'inner'; if (labelPosition === 'center') { - textX = layout.cx; - textY = layout.cy; + textX = sectorShape.cx; + textY = sectorShape.cy; textAlign = 'center'; } else { - const x1 = (isLabelInside ? (layout.r + layout.r0) / 2 * dx : layout.r * dx) + cx; - const y1 = (isLabelInside ? (layout.r + layout.r0) / 2 * dy : layout.r * dy) + cy; + 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 + dx * 3; - textY = y1 + dy * 3; + textX = x1 + nx * 3; + textY = y1 + ny * 3; if (!isLabelInside) { // For roseType - const x2 = x1 + dx * (labelLineLen + r - layout.r); - const y2 = y1 + dy * (labelLineLen + r - layout.r); - const x3 = x2 + ((dx < 0 ? -1 : 1) * labelLineLen2); + 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 = dx < 0 - ? viewLeft + labelMargin - : viewLeft + viewWidth - labelMargin; + textX = nx < 0 + ? viewLeft + edgeDistance + : viewLeft + viewWidth - edgeDistance; } else { - textX = x3 + (dx < 0 ? -labelDistance : labelDistance); + textX = x3 + (nx < 0 ? -labelDistance : labelDistance); } textY = y3; linePoints = [[x1, y1], [x2, y2], [x3, y3]]; @@ -360,8 +331,8 @@ export default function ( textAlign = isLabelInside ? 'center' : (labelAlignTo === 'edge' - ? (dx > 0 ? 'right' : 'left') - : (dx > 0 ? 'left' : 'right')); + ? (nx > 0 ? 'right' : 'left') + : (nx > 0 ? 'left' : 'right')); } let labelRotate; @@ -371,38 +342,97 @@ export default function ( } else { labelRotate = rotate - ? (dx < 0 ? -midAngle + Math.PI : -midAngle) + ? (nx < 0 ? -midAngle + Math.PI : -midAngle) : 0; } hasLabelRotate = !!labelRotate; - layout.label = { - x: textX, - y: textY, - position: labelPosition, - height: textRect.height, - len: labelLineLen, - len2: labelLineLen2, - linePoints: linePoints, - textAlign: textAlign, - verticalAlign: 'middle', - rotation: labelRotate, - inside: isLabelInside, - labelDistance: labelDistance, - labelAlignTo: labelAlignTo, - labelMargin: labelMargin, - bleedMargin: bleedMargin, - textRect: textRect, - text: text, - font: font - }; - - // Not layout the inside label + + label.x = textX; + label.y = textY; + label.rotation = labelRotate; + + // Not sectorShape the inside label if (!isLabelInside) { - labelLayoutList.push(layout.label); + 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.x -= margin / 2; + textRect.y -= margin / 2; + textRect.width += margin; + 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 + }); + } + else { + label.setStyle({ + align: textAlign, + verticalAlign: 'middle' + }); } + 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, + verticalAlign: 'middle' + }); + 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]) + }; + } + } + } } \ No newline at end of file diff --git a/src/chart/pie/pieLayout.ts b/src/chart/pie/pieLayout.ts index 5925c17951..5a83f808f9 100644 --- a/src/chart/pie/pieLayout.ts +++ b/src/chart/pie/pieLayout.ts @@ -19,7 +19,6 @@ import {parsePercent, linearMap} from '../../util/number'; import * as layout from '../../util/layout'; -import labelLayout from './labelLayout'; import * as zrUtil from 'zrender/src/core/util'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../ExtensionAPI'; @@ -94,6 +93,8 @@ export default function ( let currentAngle = startAngle; const dir = clockwise ? 1 : -1; + data.setLayout({ viewRect, r }); + data.each(valueDim, function (value: number, idx: number) { let angle; if (isNaN(value)) { @@ -107,8 +108,7 @@ export default function ( r0: r0, r: roseType ? NaN - : r, - viewRect: viewRect + : r }); return; } @@ -141,8 +141,7 @@ export default function ( r0: r0, r: roseType ? linearMap(value, extent, [r0, r]) - : r, - viewRect: viewRect + : r }); currentAngle = endAngle; @@ -179,7 +178,5 @@ export default function ( }); } } - - labelLayout(seriesModel, r, viewRect.width, viewRect.height, viewRect.x, viewRect.y); }); } \ No newline at end of file diff --git a/src/chart/radar/RadarView.ts b/src/chart/radar/RadarView.ts index 98698b6ede..29bd6fb86f 100644 --- a/src/chart/radar/RadarView.ts +++ b/src/chart/radar/RadarView.ts @@ -144,8 +144,6 @@ class RadarView extends ChartView { .update(function (newIdx, oldIdx) { const itemGroup = oldData.getItemGraphicEl(oldIdx) as graphic.Group; - graphic.clearStates(itemGroup); - const polyline = itemGroup.childAt(0) as graphic.Polyline; const polygon = itemGroup.childAt(1) as graphic.Polygon; const symbolGroup = itemGroup.childAt(2) as graphic.Group; @@ -239,7 +237,7 @@ class RadarView extends ChartView { labelDataIndex: idx, labelDimIndex: symbolPath.__dimIdx, defaultText: defaultText as string, - autoColor: color as ColorString + inheritColor: color as ColorString } ); }); diff --git a/src/chart/sankey/SankeyView.ts b/src/chart/sankey/SankeyView.ts index 6c03f7bb85..9c31ab485d 100644 --- a/src/chart/sankey/SankeyView.ts +++ b/src/chart/sankey/SankeyView.ts @@ -26,15 +26,10 @@ import ChartView from '../../view/Chart'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../ExtensionAPI'; import { GraphNode, GraphEdge } from '../../data/Graph'; -import { GraphNodeItemOption, GraphEdgeItemOption } from '../graph/GraphSeries'; +import { GraphEdgeItemOption } from '../graph/GraphSeries'; import List from '../../data/List'; import { RectLike } from 'zrender/src/core/BoundingRect'; -const nodeOpacityPath = ['itemStyle', 'opacity'] as const; -const hoverNodeOpacityPath = ['emphasis', 'itemStyle', 'opacity'] as const; -const lineOpacityPath = ['lineStyle', 'opacity'] as const; -const hoverLineOpacityPath = ['emphasis', 'lineStyle', 'opacity'] as const; - interface FocusNodeAdjacencyPayload extends Payload { dataIndex?: number edgeDataIndex?: number @@ -48,54 +43,19 @@ interface SankeyEl extends graphic.Path { unfocusNodeAdjHandler(): void } -function getItemOpacity( - item: GraphNode | GraphEdge, - opacityPath: readonly string[] -): number { - return item.getVisual('opacity') - // TODO: TYPE - || item.getModel().get(opacityPath as typeof nodeOpacityPath); -} - -function fadeOutItem( - item: GraphNode | GraphEdge, - opacityPath: readonly string[], - opacityRatio?: number -) { - const el = item.getGraphicEl() as SankeyEl; - let opacity = getItemOpacity(item, opacityPath); - - if (opacityRatio != null) { - opacity == null && (opacity = 1); - opacity *= opacityRatio; +function fadeInItem(nodeOrEdge: GraphNode | GraphEdge) { + const el = nodeOrEdge.getGraphicEl(); + if (el) { + el.removeState('blur'); } - - el.downplay && el.downplay(); - - el.traverse(function (child) { - if (child.type !== 'group') { - child.setStyle('opacity', opacity); - } - }); } -function fadeInItem( - item: GraphNode | GraphEdge, - opacityPath: readonly string[] -) { - const opacity = getItemOpacity(item, opacityPath); - const el = item.getGraphicEl() as SankeyEl; - - // Support emphasis here. - el.highlight && el.highlight(); - - el.traverse(function (child) { - if (child.type !== 'group') { - child.setStyle('opacity', opacity); - } - }); +function fadeOutItem(nodeOrEdge: GraphNode | GraphEdge) { + const el = nodeOrEdge.getGraphicEl(); + if (el) { + el.useState('blur'); + } } - class SankeyPathShape { x1 = 0; y1 = 0; @@ -175,8 +135,6 @@ class SankeyView extends ChartView { private _data: List; - private _unfocusDelayTimer: number; - render(seriesModel: SankeySeriesModel, ecModel: GlobalModel, api: ExtensionAPI) { const sankeyView = this; const graph = seriesModel.getGraph(); @@ -260,7 +218,7 @@ class SankeyView extends ChartView { cpy2: cpy2 }); - curve.setStyle(lineStyleModel.getItemStyle()); + curve.useStyle(lineStyleModel.getItemStyle()); // Special color, use source node color or target node color switch (curve.style.fill) { case 'source': @@ -357,9 +315,13 @@ class SankeyView extends ChartView { el.unfocusNodeAdjHandler && el.off('mouseout', el.unfocusNodeAdjHandler); if (itemModel.get('focusNodeAdjacency')) { + const blurState = el.ensureState('blur'); + blurState.style = { + opacity: 0.1 + }; + el.on('mouseover', el.focusNodeAdjHandler = function () { if (!sankeyView._focusAdjacencyDisabled) { - sankeyView._clearTimer(); api.dispatchAction({ type: 'focusNodeAdjacency', seriesId: seriesModel.id, @@ -383,9 +345,13 @@ class SankeyView extends ChartView { el.unfocusNodeAdjHandler && el.off('mouseout', el.unfocusNodeAdjHandler); if (edgeModel.get('focusNodeAdjacency')) { + const blurState = el.ensureState('blur'); + blurState.style = { + opacity: 0.02 + }; + el.on('mouseover', el.focusNodeAdjHandler = function () { if (!sankeyView._focusAdjacencyDisabled) { - sankeyView._clearTimer(); api.dispatchAction({ type: 'focusNodeAdjacency', seriesId: seriesModel.id, @@ -412,26 +378,14 @@ class SankeyView extends ChartView { } dispose() { - this._clearTimer(); } _dispatchUnfocus(api: ExtensionAPI) { const self = this; - this._clearTimer(); - this._unfocusDelayTimer = setTimeout(function () { - self._unfocusDelayTimer = null; - api.dispatchAction({ - type: 'unfocusNodeAdjacency', - seriesId: self._model.id - }); - }, 500) as any; - } - - _clearTimer() { - if (this._unfocusDelayTimer) { - clearTimeout(this._unfocusDelayTimer); - this._unfocusDelayTimer = null; - } + api.dispatchAction({ + type: 'unfocusNodeAdjacency', + seriesId: self._model.id + }); } focusNodeAdjacency( @@ -452,23 +406,23 @@ class SankeyView extends ChartView { const edge = graph.getEdgeByIndex(edgeDataIndex); graph.eachNode(function (node) { - fadeOutItem(node, nodeOpacityPath, 0.1); + fadeOutItem(node); }); graph.eachEdge(function (edge) { - fadeOutItem(edge, lineOpacityPath, 0.1); + fadeOutItem(edge); }); if (node) { const itemModel = data.getItemModel(dataIndex); - fadeInItem(node, hoverNodeOpacityPath); + fadeInItem(node); const focusNodeAdj = itemModel.get('focusNodeAdjacency'); if (focusNodeAdj === 'outEdges') { zrUtil.each(node.outEdges, function (edge) { if (edge.dataIndex < 0) { return; } - fadeInItem(edge, hoverLineOpacityPath); - fadeInItem(edge.node2, hoverNodeOpacityPath); + fadeInItem(edge); + fadeInItem(edge.node2); }); } else if (focusNodeAdj === 'inEdges') { @@ -476,8 +430,8 @@ class SankeyView extends ChartView { if (edge.dataIndex < 0) { return; } - fadeInItem(edge, hoverLineOpacityPath); - fadeInItem(edge.node1, hoverNodeOpacityPath); + fadeInItem(edge); + fadeInItem(edge.node1); }); } else if (focusNodeAdj === 'allEdges') { @@ -485,16 +439,16 @@ class SankeyView extends ChartView { if (edge.dataIndex < 0) { return; } - fadeInItem(edge, hoverLineOpacityPath); - (edge.node1 !== node) && fadeInItem(edge.node1, hoverNodeOpacityPath); - (edge.node2 !== node) && fadeInItem(edge.node2, hoverNodeOpacityPath); + fadeInItem(edge); + (edge.node1 !== node) && fadeInItem(edge.node1); + (edge.node2 !== node) && fadeInItem(edge.node2); }); } } if (edge) { - fadeInItem(edge, hoverLineOpacityPath); - fadeInItem(edge.node1, hoverNodeOpacityPath); - fadeInItem(edge.node2, hoverNodeOpacityPath); + fadeInItem(edge); + fadeInItem(edge.node1); + fadeInItem(edge.node2); } } @@ -504,10 +458,10 @@ class SankeyView extends ChartView { const graph = seriesModel.getGraph(); graph.eachNode(function (node) { - fadeOutItem(node, nodeOpacityPath); + fadeInItem(node); }); graph.eachEdge(function (edge) { - fadeOutItem(edge, lineOpacityPath); + fadeInItem(edge); }); } } diff --git a/src/chart/sunburst.ts b/src/chart/sunburst.ts index 8922feb967..3d47543f6d 100644 --- a/src/chart/sunburst.ts +++ b/src/chart/sunburst.ts @@ -25,7 +25,9 @@ import './sunburst/SunburstView'; import './sunburst/sunburstAction'; import sunburstLayout from './sunburst/sunburstLayout'; +import sunburstVisual from './sunburst/sunburstVisual'; import dataFilter from '../processor/dataFilter'; echarts.registerLayout(zrUtil.curry(sunburstLayout, 'sunburst')); echarts.registerProcessor(zrUtil.curry(dataFilter, 'sunburst')); +echarts.registerVisual(sunburstVisual); \ No newline at end of file diff --git a/src/chart/sunburst/SunburstPiece.ts b/src/chart/sunburst/SunburstPiece.ts index 67dc105461..efb3125c7d 100644 --- a/src/chart/sunburst/SunburstPiece.ts +++ b/src/chart/sunburst/SunburstPiece.ts @@ -19,11 +19,13 @@ import * as zrUtil from 'zrender/src/core/util'; import * as graphic from '../../util/graphic'; -import { ColorString } from '../../util/types'; import { TreeNode } from '../../data/Tree'; import SunburstSeriesModel, { SunburstSeriesNodeItemOption, SunburstSeriesOption } from './SunburstSeries'; import GlobalModel from '../../model/Global'; import { AllPropTypes } from 'zrender/src/core/types'; +import { PathStyleProps } from 'zrender/src/graphic/Path'; +import { ColorString } from '../../util/types'; +import Model from '../../model/Model'; const NodeHighlightPolicy = { NONE: 'none', // not downplay others @@ -41,7 +43,7 @@ interface DrawTreeNode extends TreeNode { /** * Sunburstce of Sunburst including Sector, Label, LabelLine */ -class SunburstPiece extends graphic.Group { +class SunburstPiece extends graphic.Sector { node: TreeNode; @@ -51,22 +53,20 @@ class SunburstPiece extends graphic.Group { constructor(node: TreeNode, seriesModel: SunburstSeriesModel, ecModel: GlobalModel) { super(); - const sector = new graphic.Sector({ - z2: DEFAULT_SECTOR_Z, - textConfig: { - inside: true - } - }); - this.add(sector); - graphic.getECData(sector).seriesIndex = seriesModel.seriesIndex; + this.z2 = DEFAULT_SECTOR_Z; + this.textConfig = { + inside: true + }; + + graphic.getECData(this).seriesIndex = seriesModel.seriesIndex; const text = new graphic.Text({ z2: DEFAULT_TEXT_Z, silent: node.getModel().get(['label', 'silent']) }); - sector.setTextContent(text); + this.setTextContent(text); - this.updateData(true, node, 'normal', seriesModel, ecModel); + this.updateData(true, node, seriesModel, ecModel); // Hover to change label and labelLine // FIXME @@ -85,7 +85,7 @@ class SunburstPiece extends graphic.Group { updateData( firstCreate: boolean, node: TreeNode, - state: 'emphasis' | 'normal' | 'highlight' | 'downplay', + // state: 'emphasis' | 'normal' | 'highlight' | 'downplay', seriesModel?: SunburstSeriesModel, ecModel?: GlobalModel ) { @@ -95,7 +95,7 @@ class SunburstPiece extends graphic.Group { seriesModel = seriesModel || this._seriesModel; ecModel = ecModel || this._ecModel; - const sector = this.childAt(0) as graphic.Sector; + const sector = this; graphic.getECData(sector).dataIndex = node.dataIndex; const itemModel = node.getModel(); @@ -108,23 +108,13 @@ class SunburstPiece extends graphic.Group { // const visualColor = getNodeColor(node, seriesModel, ecModel); // fillDefaultColor(node, seriesModel, visualColor); - const normalStyle = node.getVisual('style'); - let style; - if (state === 'normal') { - style = normalStyle; - } - else { - const stateStyle = itemModel.getModel([state, 'itemStyle']) - .getItemStyle(); - style = zrUtil.merge(stateStyle, normalStyle); - } - // style = zrUtil.defaults( - // { - // lineJoin: 'bevel', - // fill: style.fill || visualColor - // }, - // style - // ); + const normalStyle = node.getVisual('style') as PathStyleProps; + normalStyle.lineJoin = 'bevel'; + + zrUtil.each(['emphasis', 'highlight', 'downplay'] as const, function (stateName) { + const state = sector.ensureState(stateName); + state.style = itemModel.getModel([stateName, 'itemStyle']).getItemStyle(); + }); if (firstCreate) { sector.setShape(sectorShape); @@ -139,26 +129,18 @@ class SunburstPiece extends graphic.Group { seriesModel, node.dataIndex ); - sector.useStyle(style); } - else if (typeof style.fill === 'object' && style.fill.type - || typeof sector.style.fill === 'object' && sector.style.fill.type - ) { + else { // Disable animation for gradient since no interpolation method // is supported for gradient graphic.updateProps(sector, { shape: sectorShape }, seriesModel); - sector.useStyle(style); - } - else { - graphic.updateProps(sector, { - shape: sectorShape, - style: style - }, seriesModel); } - this._updateLabel(seriesModel, style.fill, state); + sector.useStyle(normalStyle); + + this._updateLabel(seriesModel); const cursorStyle = itemModel.getShallow('cursor'); cursorStyle && sector.attr('cursor', cursorStyle); @@ -179,13 +161,13 @@ class SunburstPiece extends graphic.Group { this.node.hostTree.root.eachNode(function (n: DrawTreeNode) { if (n.piece) { if (that.node === n) { - n.piece.updateData(false, n, 'emphasis'); + n.piece.useState('emphasis', true); } else if (isNodeHighlighted(n, that.node, highlightPolicy)) { - n.piece.childAt(0).trigger('highlight'); + n.piece.useState('highlight', true); } else if (highlightPolicy !== NodeHighlightPolicy.NONE) { - n.piece.childAt(0).trigger('downplay'); + n.piece.useState('downplay', true); } } }); @@ -194,143 +176,134 @@ class SunburstPiece extends graphic.Group { onNormal() { this.node.hostTree.root.eachNode(function (n: DrawTreeNode) { if (n.piece) { - n.piece.updateData(false, n, 'normal'); + n.piece.clearStates(); } }); } onHighlight() { - this.updateData(false, this.node, 'highlight'); + this.replaceState('downplay', 'highlight', true); } onDownplay() { - this.updateData(false, this.node, 'downplay'); + this.replaceState('highlight', 'downplay', true); } _updateLabel( - seriesModel: SunburstSeriesModel, - visualColor: ColorString, - state: 'emphasis' | 'normal' | 'highlight' | 'downplay' + seriesModel: SunburstSeriesModel ) { const itemModel = this.node.getModel(); - const normalModel = itemModel.getModel('label'); - const labelModel = state === 'normal' || state === 'emphasis' - ? normalModel - : itemModel.getModel([state, 'label']); - const labelHoverModel = itemModel.getModel(['emphasis', 'label']); - - let text = zrUtil.retrieve( - seriesModel.getFormattedLabel( - this.node.dataIndex, state, null, null, 'label' - ), - this.node.name - ); - if (getLabelAttr('show') === false) { - text = ''; - } + const normalLabelModel = itemModel.getModel('label'); const layout = this.node.getLayout(); - let labelMinAngle = labelModel.get('minAngle'); - if (labelMinAngle == null) { - labelMinAngle = normalModel.get('minAngle'); - } - labelMinAngle = labelMinAngle / 180 * Math.PI; const angle = layout.endAngle - layout.startAngle; - if (labelMinAngle != null && Math.abs(angle) < labelMinAngle) { - // Not displaying text when angle is too small - text = ''; - } - - const sector = this.childAt(0); - const label = sector.getTextContent(); - const midAngle = (layout.startAngle + layout.endAngle) / 2; const dx = Math.cos(midAngle); const dy = Math.sin(midAngle); - let r; - const labelPosition = getLabelAttr('position'); - const labelPadding = getLabelAttr('distance') || 0; - let textAlign = getLabelAttr('align'); - if (labelPosition === 'outside') { - r = layout.r + labelPadding; - textAlign = midAngle > Math.PI / 2 ? 'right' : 'left'; - } - else { - if (!textAlign || textAlign === 'center') { - r = (layout.r + layout.r0) / 2; - textAlign = 'center'; - } - else if (textAlign === 'left') { - r = layout.r0 + labelPadding; - if (midAngle > Math.PI / 2) { - textAlign = 'right'; - } + const sector = this; + const label = sector.getTextContent(); + const dataIndex = this.node.dataIndex; + + zrUtil.each(['normal', 'emphasis', 'highlight', 'downplay'] as const, (stateName) => { + + const labelStateModel = stateName === 'normal' ? itemModel.getModel('label') + : itemModel.getModel([stateName, 'label']); + const labelMinAngle = labelStateModel.get('minAngle') / 180 * Math.PI; + const isNormal = stateName === 'normal'; + + const state = isNormal ? label : label.ensureState(stateName); + let text = seriesModel.getFormattedLabel(dataIndex, stateName); + if (isNormal) { + text = text || this.node.name; } - else if (textAlign === 'right') { - r = layout.r - labelPadding; - if (midAngle > Math.PI / 2) { - textAlign = 'left'; - } + + state.style = graphic.createTextStyle(labelStateModel, { + }, null, stateName !== 'normal', true); + if (text) { + state.style.text = text; } - } - graphic.setLabelStyle( - label, normalModel, labelHoverModel, - { - defaultText: labelModel.getShallow('show') ? text : null + // Not displaying text when angle is too small + state.ignore = labelMinAngle != null && Math.abs(angle) < labelMinAngle; + + const labelPosition = getLabelAttr(labelStateModel, 'position'); + + const sectorState = isNormal ? sector : sector.states[stateName]; + const labelColor = sectorState.style.fill as ColorString; + sectorState.textConfig = { + outsideFill: labelStateModel.get('color') === 'inherit' ? labelColor : null, + inside: labelPosition !== 'outside' + }; + + let r; + const labelPadding = getLabelAttr(labelStateModel, 'distance') || 0; + let textAlign = getLabelAttr(labelStateModel, 'align'); + if (labelPosition === 'outside') { + r = layout.r + labelPadding; + textAlign = midAngle > Math.PI / 2 ? 'right' : 'left'; + } + else { + if (!textAlign || textAlign === 'center') { + r = (layout.r + layout.r0) / 2; + textAlign = 'center'; + } + else if (textAlign === 'left') { + r = layout.r0 + labelPadding; + if (midAngle > Math.PI / 2) { + textAlign = 'right'; + } + } + else if (textAlign === 'right') { + r = layout.r - labelPadding; + if (midAngle > Math.PI / 2) { + textAlign = 'left'; + } + } } - ); - sector.setTextConfig({ - inside: labelPosition !== 'outside', - insideStroke: visualColor, - // insideFill: 'auto', - outsideFill: visualColor - }); - label.attr('style', { - text: text, - align: textAlign, - verticalAlign: getLabelAttr('verticalAlign') || 'middle', - opacity: getLabelAttr('opacity') - }); + state.style.align = textAlign; + state.style.verticalAlign = getLabelAttr(labelStateModel, 'verticalAlign') || 'middle'; - label.x = r * dx + layout.cx; - label.y = r * dy + layout.cy; + state.x = r * dx + layout.cx; + state.y = r * dy + layout.cy; - const rotateType = getLabelAttr('rotate'); - let rotate = 0; - if (rotateType === 'radial') { - rotate = -midAngle; - if (rotate < -Math.PI / 2) { - rotate += Math.PI; + const rotateType = getLabelAttr(labelStateModel, 'rotate'); + let rotate = 0; + if (rotateType === 'radial') { + rotate = -midAngle; + if (rotate < -Math.PI / 2) { + rotate += Math.PI; + } } - } - else if (rotateType === 'tangential') { - rotate = Math.PI / 2 - midAngle; - if (rotate > Math.PI / 2) { - rotate -= Math.PI; + else if (rotateType === 'tangential') { + rotate = Math.PI / 2 - midAngle; + if (rotate > Math.PI / 2) { + rotate -= Math.PI; + } + else if (rotate < -Math.PI / 2) { + rotate += Math.PI; + } } - else if (rotate < -Math.PI / 2) { - rotate += Math.PI; + else if (typeof rotateType === 'number') { + rotate = rotateType * Math.PI / 180; } - } - else if (typeof rotateType === 'number') { - rotate = rotateType * Math.PI / 180; - } - label.attr('rotation', rotate); - type LabelOption = SunburstSeriesNodeItemOption['label']; - function getLabelAttr(name: T): LabelOption[T] { - const stateAttr = labelModel.get(name); + state.rotation = rotate; + }); + + + type LabelOpt = SunburstSeriesOption['label']; + function getLabelAttr(model: Model, name: T): LabelOpt[T] { + const stateAttr = model.get(name); if (stateAttr == null) { - return normalModel.get(name); - } - else { - return stateAttr; + return normalLabelModel.get(name) as LabelOpt[T]; } + return stateAttr; } + + label.dirtyStyle(); } _initEvents( @@ -355,15 +328,13 @@ class SunburstPiece extends graphic.Group { that.onHighlight(); }; - if (seriesModel.isAnimationEnabled()) { - sector - .on('mouseover', onEmphasis) - .on('mouseout', onNormal) - .on('emphasis', onEmphasis) - .on('normal', onNormal) - .on('downplay', onDownplay) - .on('highlight', onHighlight); - } + sector + .on('mouseover', onEmphasis) + .on('mouseout', onNormal) + .on('emphasis', onEmphasis) + .on('normal', onNormal) + .on('downplay', onDownplay) + .on('highlight', onHighlight); } } diff --git a/src/chart/sunburst/SunburstSeries.ts b/src/chart/sunburst/SunburstSeries.ts index 0d37644d76..bc679c91bd 100644 --- a/src/chart/sunburst/SunburstSeries.ts +++ b/src/chart/sunburst/SunburstSeries.ts @@ -139,10 +139,7 @@ export interface SunburstSeriesOption extends SeriesOption, CircleLayoutOptionMi interface SunburstSeriesModel { getFormattedLabel( dataIndex: number, - state?: 'emphasis' | 'normal' | 'highlight' | 'downplay', - dataType?: string, - dimIndex?: number, - labelProp?: string + state?: 'emphasis' | 'normal' | 'highlight' | 'downplay' ): string } class SunburstSeriesModel extends SeriesModel { @@ -248,7 +245,7 @@ class SunburstSeriesModel extends SeriesModel { opacity: 0.5 }, label: { - opacity: 0.6 + opacity: 0.5 } }, @@ -256,7 +253,6 @@ class SunburstSeriesModel extends SeriesModel { animationType: 'expansion', animationDuration: 1000, animationDurationUpdate: 500, - animationEasing: 'cubicOut', data: [], diff --git a/src/chart/sunburst/SunburstView.ts b/src/chart/sunburst/SunburstView.ts index 6de104fd89..afdd28dd71 100644 --- a/src/chart/sunburst/SunburstView.ts +++ b/src/chart/sunburst/SunburstView.ts @@ -25,10 +25,8 @@ import SunburstSeriesModel, { SunburstSeriesNodeItemOption } from './SunburstSer import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../ExtensionAPI'; import { TreeNode } from '../../data/Tree'; -import {windowOpen} from '../../util/format'; - - -const ROOT_TO_NODE_ACTION = 'sunburstRootToNode'; +import { ROOT_TO_NODE_ACTION } from './sunburstAction'; +import { windowOpen } from '../../util/format'; interface DrawTreeNode extends TreeNode { parentNode: DrawTreeNode @@ -132,7 +130,7 @@ class SunburstView extends ChartView { if (newNode) { // Update oldNode.piece.updateData( - false, newNode, 'normal', seriesModel, ecModel); + false, newNode, seriesModel, ecModel); // For tooltip data.setItemGraphicEl(newNode.dataIndex, oldNode.piece); @@ -174,7 +172,7 @@ class SunburstView extends ChartView { if (self.virtualPiece) { // Update self.virtualPiece.updateData( - false, virtualRoot, 'normal', seriesModel, ecModel); + false, virtualRoot, seriesModel, ecModel); } else { // Add @@ -210,7 +208,7 @@ class SunburstView extends ChartView { const viewRoot = this.seriesModel.getViewRoot(); viewRoot.eachNode((node: DrawTreeNode) => { if (!targetFound - && node.piece && node.piece.childAt(0) === e.target + && node.piece && node.piece === e.target ) { const nodeClick = node.getModel().get('nodeClick'); if (nodeClick === 'rootToNode') { diff --git a/src/chart/sunburst/sunburstAction.ts b/src/chart/sunburst/sunburstAction.ts index 85df1d6d6b..9416462bc4 100644 --- a/src/chart/sunburst/sunburstAction.ts +++ b/src/chart/sunburst/sunburstAction.ts @@ -27,7 +27,7 @@ import SunburstSeriesModel from './SunburstSeries'; import { Payload } from '../../util/types'; import GlobalModel from '../../model/Global'; -const ROOT_TO_NODE_ACTION = 'sunburstRootToNode'; +export const ROOT_TO_NODE_ACTION = 'sunburstRootToNode'; interface SunburstRootToNodePayload extends Payload {} diff --git a/src/chart/sunburst/sunburstVisual.ts b/src/chart/sunburst/sunburstVisual.ts index 97eb785a78..bd3dc42191 100644 --- a/src/chart/sunburst/sunburstVisual.ts +++ b/src/chart/sunburst/sunburstVisual.ts @@ -22,13 +22,11 @@ import SunburstSeriesModel, { SunburstSeriesNodeItemOption } from './SunburstSer import { extend } from 'zrender/src/core/util'; export default function (ecModel: GlobalModel) { - - ecModel.eachSeriesByType('graph', function (seriesModel: SunburstSeriesModel) { + ecModel.eachSeriesByType('sunburst', function (seriesModel: SunburstSeriesModel) { const data = seriesModel.getData(); const tree = data.tree; tree.eachNode(function (node) { const model = node.getModel(); - // TODO Optimize const style = model.getModel('itemStyle').getItemStyle(); const existsStyle = data.ensureUniqueItemVisual(node.dataIndex, 'style'); extend(existsStyle, style); diff --git a/src/chart/tree/TreeView.ts b/src/chart/tree/TreeView.ts index 2d16448b2c..0910809a07 100644 --- a/src/chart/tree/TreeView.ts +++ b/src/chart/tree/TreeView.ts @@ -224,9 +224,6 @@ class TreeView extends ChartView { symbolEl && removeNode(oldData, oldIdx, symbolEl, group, seriesModel, seriesScope); return; } - if (symbolEl) { - graphic.clearStates(symbolEl); - } // Update node and edge updateNode(data, newIdx, symbolEl, group, seriesModel, seriesScope); }) @@ -351,6 +348,8 @@ class TreeView extends ChartView { originY: e.originY }); this._updateNodeAndLinkScale(seriesModel); + // Only update label layout on zoom + api.updateLabelLayout(); }); } @@ -359,8 +358,8 @@ class TreeView extends ChartView { const nodeScale = this._getNodeGlobalScale(seriesModel); - data.eachItemGraphicEl(function (el, idx) { - el.scaleX = el.scaleY = nodeScale; + data.eachItemGraphicEl(function (el: SymbolClz, idx) { + el.setSymbolScale(nodeScale); }); } @@ -519,8 +518,8 @@ function updateNode( if (textContent) { symbolPath.setTextConfig({ position: seriesScope.labelModel.get('position') || textPosition, - rotation: rotate == null ? -rad : labelRotateRadian - // textOrigin: 'center', + rotation: rotate == null ? -rad : labelRotateRadian, + origin: 'center' }); textContent.setStyle('verticalAlign', 'middle'); } diff --git a/src/chart/treemap/TreemapSeries.ts b/src/chart/treemap/TreemapSeries.ts index 18b036b9f1..c61d8d21df 100644 --- a/src/chart/treemap/TreemapSeries.ts +++ b/src/chart/treemap/TreemapSeries.ts @@ -47,6 +47,7 @@ interface BreadcrumbItemStyleOption extends ItemStyleOption { interface TreemapSeriesLabelOption extends LabelOption { ellipsis?: boolean + formatter?: string | ((params: CallbackDataParams) => string) } interface TreemapSeriesItemStyleOption extends ItemStyleOption { diff --git a/src/chart/treemap/TreemapView.ts b/src/chart/treemap/TreemapView.ts index 5b5d5d2585..cd5af6a087 100644 --- a/src/chart/treemap/TreemapView.ts +++ b/src/chart/treemap/TreemapView.ts @@ -54,9 +54,9 @@ const Group = graphic.Group; const Rect = graphic.Rect; const DRAG_THRESHOLD = 3; -const PATH_LABEL_NOAMAL = ['label'] as const; +const PATH_LABEL_NOAMAL = 'label'; const PATH_LABEL_EMPHASIS = ['emphasis', 'label'] as const; -const PATH_UPPERLABEL_NORMAL = ['upperLabel'] as const; +const PATH_UPPERLABEL_NORMAL = 'upperLabel'; const PATH_UPPERLABEL_EMPHASIS = ['emphasis', 'upperLabel'] as const; const Z_BASE = 10; // Should bigger than every z. const Z_BG = 1; @@ -914,8 +914,6 @@ function renderNode( height: number, upperLabelRect?: RectLike ) { - const defaultText = nodeModel.get('name'); - const normalLabelModel = nodeModel.getModel( upperLabelRect ? PATH_UPPERLABEL_NORMAL : PATH_LABEL_NOAMAL ); @@ -923,16 +921,26 @@ function renderNode( upperLabelRect ? PATH_UPPERLABEL_EMPHASIS : PATH_LABEL_EMPHASIS ); + let text = retrieve( + seriesModel.getFormattedLabel( + thisNode.dataIndex, 'normal', null, null, normalLabelModel.get('formatter') + ), + nodeModel.get('name') + ); + if (!upperLabelRect && thisLayout.isLeafRoot) { + const iconChar = seriesModel.get('drillDownIcon', true); + text = iconChar ? iconChar + ' ' + text : text; + } + const isShow = normalLabelModel.getShallow('show'); graphic.setLabelStyle( rectEl, normalLabelModel, emphasisLabelModel, { - defaultText: isShow ? defaultText : null, - autoColor: visualColor, + defaultText: isShow ? text : null, + inheritColor: visualColor, labelFetcher: seriesModel, - labelDataIndex: thisNode.dataIndex, - labelProp: upperLabelRect ? 'upperLabel' : 'label' + labelDataIndex: thisNode.dataIndex } ); diff --git a/src/component/dataZoom/SliderZoomView.ts b/src/component/dataZoom/SliderZoomView.ts index d49e9a8db4..bfd3d7fa73 100644 --- a/src/component/dataZoom/SliderZoomView.ts +++ b/src/component/dataZoom/SliderZoomView.ts @@ -251,6 +251,7 @@ class SliderZoomView extends DataZoomView { const rect = thisGroup.getBoundingRect([barGroup]); thisGroup.x = location.x - rect.x; thisGroup.y = location.y - rect.y; + thisGroup.markRedraw(); } /** diff --git a/src/component/geo.ts b/src/component/geo.ts index 44db79ec18..21e499c154 100644 --- a/src/component/geo.ts +++ b/src/component/geo.ts @@ -21,11 +21,13 @@ import * as echarts from '../echarts'; import * as zrUtil from 'zrender/src/core/util'; -import '../coord/geo/GeoModel'; import '../coord/geo/geoCreator'; import './geo/GeoView'; import '../action/geoRoam'; import { ActionInfo } from '../util/types'; + +// NOTE: DONT Remove this import, or GeoModel will be treeshaked. +import '../coord/geo/GeoModel'; import GeoModel from '../coord/geo/GeoModel'; function makeAction( diff --git a/src/component/geo/GeoView.ts b/src/component/geo/GeoView.ts index 8f184da1af..c4587c016b 100644 --- a/src/component/geo/GeoView.ts +++ b/src/component/geo/GeoView.ts @@ -33,7 +33,7 @@ class GeoView extends ComponentView { private _mapDraw: MapDraw; init(ecModel: GlobalModel, api: ExtensionAPI) { - const mapDraw = new MapDraw(api, true); + const mapDraw = new MapDraw(api); this._mapDraw = mapDraw; this.group.add(mapDraw.group); diff --git a/src/component/helper/MapDraw.ts b/src/component/helper/MapDraw.ts index 46a4d8546c..0d50756fdb 100644 --- a/src/component/helper/MapDraw.ts +++ b/src/component/helper/MapDraw.ts @@ -71,14 +71,13 @@ class MapDraw { private _controller: RoamController; private _controllerHost: { - target?: graphic.Group; + target: graphic.Group; zoom?: number; zoomLimit?: GeoCommonOptionMixin['scaleLimit']; }; readonly group: graphic.Group; - private _updateGroup: boolean; /** * This flag is used to make sure that only one among @@ -96,14 +95,13 @@ class MapDraw { private _backgroundGroup: graphic.Group; - constructor(api: ExtensionAPI, updateGroup: boolean) { + constructor(api: ExtensionAPI) { const group = new graphic.Group(); this.uid = getUID('ec_map_draw'); // @ts-ignore FIXME:TS this._controller = new RoamController(api.getZr()); - this._controllerHost = {target: updateGroup ? group : null}; + this._controllerHost = { target: group }; this.group = group; - this._updateGroup = updateGroup; group.add(this._regionsGroup = new graphic.Group() as RegionsGroup); group.add(this._backgroundGroup = new graphic.Group()); @@ -319,6 +317,8 @@ class MapDraw { } ); + compoundPath.setTextContent(textEl); + if (!isFirstDraw) { // Text animation graphic.updateProps(textEl, { @@ -370,7 +370,7 @@ class MapDraw { this._controller.dispose(); this._mapName && geoSourceManager.removeGraphic(this._mapName, this.uid); this._mapName = null; - this._controllerHost = {}; + this._controllerHost = null; } private _updateBackground(geo: Geo): void { @@ -432,15 +432,13 @@ class MapDraw { originY: e.originY })); - if (this._updateGroup) { - const group = this.group; - this._regionsGroup.traverse(function (el) { - if (el.type === 'text') { - el.scaleX = 1 / group.scaleX; - el.scaleY = 1 / group.scaleY; - } - }); - } + const group = this.group; + this._regionsGroup.traverse(function (el) { + if (el.type === 'text') { + el.scaleX = 1 / group.scaleX; + el.scaleY = 1 / group.scaleY; + } + }); }, this); controller.setPointerChecker(function (e, x, y) { diff --git a/src/component/helper/roamHelper.ts b/src/component/helper/roamHelper.ts index df06db01b2..07d5839607 100644 --- a/src/component/helper/roamHelper.ts +++ b/src/component/helper/roamHelper.ts @@ -17,35 +17,30 @@ * under the License. */ -// @ts-nocheck +import Element from 'zrender/src/Element'; + +interface ControllerHost { + target: Element, + zoom?: number + zoomLimit?: {min?: number, max?: number} +} /** * For geo and graph. - * - * @param {Object} controllerHost - * @param {module:zrender/Element} controllerHost.target */ -export function updateViewOnPan(controllerHost, dx, dy) { +export function updateViewOnPan(controllerHost: ControllerHost, dx: number, dy: number) { const target = controllerHost.target; - const pos = target.position; - pos[0] += dx; - pos[1] += dy; + target.x += dx; + target.y += dy; target.dirty(); } /** * For geo and graph. - * - * @param {Object} controllerHost - * @param {module:zrender/Element} controllerHost.target - * @param {number} controllerHost.zoom - * @param {number} controllerHost.zoomLimit like: {min: 1, max: 2} */ -export function updateViewOnZoom(controllerHost, zoomDelta, zoomX, zoomY) { +export function updateViewOnZoom(controllerHost: ControllerHost, zoomDelta: number, zoomX: number, zoomY: number) { const target = controllerHost.target; const zoomLimit = controllerHost.zoomLimit; - const pos = target.position; - const scale = target.scale; let newZoom = controllerHost.zoom = controllerHost.zoom || 1; newZoom *= zoomDelta; @@ -60,10 +55,10 @@ export function updateViewOnZoom(controllerHost, zoomDelta, zoomX, zoomY) { const zoomScale = newZoom / controllerHost.zoom; controllerHost.zoom = newZoom; // Keep the mouse center when scaling - pos[0] -= (zoomX - pos[0]) * (zoomScale - 1); - pos[1] -= (zoomY - pos[1]) * (zoomScale - 1); - scale[0] *= zoomScale; - scale[1] *= zoomScale; + target.x -= (zoomX - target.x) * (zoomScale - 1); + target.y -= (zoomY - target.y) * (zoomScale - 1); + target.scaleX *= zoomScale; + target.scaleY *= zoomScale; target.dirty(); } diff --git a/src/component/legend/LegendView.ts b/src/component/legend/LegendView.ts index dafa108d28..4a07bc0654 100644 --- a/src/component/legend/LegendView.ts +++ b/src/component/legend/LegendView.ts @@ -140,6 +140,7 @@ class LegendView extends ComponentView { ); this.group.x = layoutRect.x - mainRect.x; this.group.y = layoutRect.y - mainRect.y; + this.group.markRedraw(); // Render background after group is layout. this.group.add( diff --git a/src/component/marker/MarkAreaView.ts b/src/component/marker/MarkAreaView.ts index 3be84b959e..574fc550d9 100644 --- a/src/component/marker/MarkAreaView.ts +++ b/src/component/marker/MarkAreaView.ts @@ -273,7 +273,6 @@ class MarkAreaView extends MarkerView { }) .update(function (newIdx, oldIdx) { const polygon = inner(polygonGroup).data.getItemGraphicEl(oldIdx) as graphic.Polygon; - graphic.clearStates(polygon); graphic.updateProps(polygon, { shape: { points: areaData.getItemLayout(newIdx) @@ -301,7 +300,7 @@ class MarkAreaView extends MarkerView { labelFetcher: maModel, labelDataIndex: idx, defaultText: areaData.getName(idx) || '', - autoColor: typeof style.fill === 'string' + inheritColor: typeof style.fill === 'string' ? colorUtil.modifyAlpha(style.fill, 1) : '#000' } ); diff --git a/src/component/parallel.ts b/src/component/parallel.ts index f273a20c0b..0c9f50e534 100644 --- a/src/component/parallel.ts +++ b/src/component/parallel.ts @@ -23,9 +23,11 @@ import * as zrUtil from 'zrender/src/core/util'; import * as throttleUtil from '../util/throttle'; import parallelPreprocessor from '../coord/parallel/parallelPreprocessor'; import '../coord/parallel/parallelCreator'; -import '../coord/parallel/ParallelModel'; import './parallelAxis'; import GlobalModel from '../model/Global'; + +// NOTE: DONT Remove this import, or GeoModel will be treeshaked. +import '../coord/parallel/ParallelModel'; import ParallelModel, { ParallelCoordinateSystemOption } from '../coord/parallel/ParallelModel'; import ExtensionAPI from '../ExtensionAPI'; import ComponentView from '../view/Component'; diff --git a/src/component/title.ts b/src/component/title.ts index 3c098d12ac..d506b3ffd8 100644 --- a/src/component/title.ts +++ b/src/component/title.ts @@ -245,6 +245,7 @@ class TitleView extends ComponentView { group.x = layoutRect.x; group.y = layoutRect.y; + group.markRedraw(); const alignStyle = { align: textAlign, verticalAlign: textVerticalAlign diff --git a/src/component/toolbox/ToolboxView.ts b/src/component/toolbox/ToolboxView.ts index edc80bac3c..27e6829519 100644 --- a/src/component/toolbox/ToolboxView.ts +++ b/src/component/toolbox/ToolboxView.ts @@ -143,8 +143,9 @@ class ToolboxView extends ComponentView { const iconPaths = this.iconPaths; option.iconStatus = option.iconStatus || {}; option.iconStatus[iconName] = status; - // FIXME - iconPaths[iconName] && iconPaths[iconName].trigger(status); + if (iconPaths[iconName]) { + graphic[status === 'emphasis' ? 'enterEmphasis' : 'leaveEmphasis'](iconPaths[iconName]); + } }; if (feature instanceof ToolboxFeature) { @@ -240,31 +241,40 @@ class ToolboxView extends ComponentView { }, tooltipModel.option); } - graphic.enableHoverEmphasis(path); - - if (toolboxModel.get('showTitle')) { - (path as ExtendedPath).__title = titlesMap[iconName]; - (path as graphic.Path).on('mouseover', function () { - // Should not reuse above hoverStyle, which might be modified. - const hoverStyle = iconStyleEmphasisModel.getItemStyle(); - const defaultTextPosition = toolboxModel.get('orient') === 'vertical' - ? (toolboxModel.get('right') == null ? 'right' as const : 'left' as const) - : (toolboxModel.get('bottom') == null ? 'bottom' as const : 'top' as const); - textContent.setStyle({ - fill: (iconStyleEmphasisModel.get('textFill') - || hoverStyle.fill || hoverStyle.stroke || '#000') as string, - backgroundColor: iconStyleEmphasisModel.get('textBackgroundColor') - }); - path.setTextConfig({ - position: iconStyleEmphasisModel.get('textPosition') || defaultTextPosition - }); - textContent.ignore = false; - }) - .on('mouseout', function () { - textContent.ignore = true; + // graphic.enableHoverEmphasis(path); + + (path as ExtendedPath).__title = titlesMap[iconName]; + (path as graphic.Path).on('mouseover', function () { + // Should not reuse above hoverStyle, which might be modified. + const hoverStyle = iconStyleEmphasisModel.getItemStyle(); + const defaultTextPosition = toolboxModel.get('orient') === 'vertical' + ? (toolboxModel.get('right') == null ? 'right' as const : 'left' as const) + : (toolboxModel.get('bottom') == null ? 'bottom' as const : 'top' as const); + textContent.setStyle({ + fill: (iconStyleEmphasisModel.get('textFill') + || hoverStyle.fill || hoverStyle.stroke || '#000') as string, + backgroundColor: iconStyleEmphasisModel.get('textBackgroundColor') }); - } - path.trigger(featureModel.get(['iconStatus', iconName]) || 'normal'); + path.setTextConfig({ + position: iconStyleEmphasisModel.get('textPosition') || defaultTextPosition + }); + textContent.ignore = !toolboxModel.get('showTitle'); + + // Use enterEmphasis and leaveEmphasis provide by ec. + // There are flags managed by the echarts. + graphic.enterEmphasis(this); + }) + .on('mouseout', function () { + if (featureModel.get(['iconStatus', iconName]) !== 'emphasis') { + graphic.leaveEmphasis(this); + } + textContent.hide(); + }); + + graphic[ + featureModel.get(['iconStatus', iconName]) === 'emphasis' + ? 'enterEmphasis' : 'leaveEmphasis' + ](path); group.add(path); (path as graphic.Path).on('click', zrUtil.bind( diff --git a/src/component/visualMap/VisualMapModel.ts b/src/component/visualMap/VisualMapModel.ts index 605e8dd05c..3d93748c93 100644 --- a/src/component/visualMap/VisualMapModel.ts +++ b/src/component/visualMap/VisualMapModel.ts @@ -261,7 +261,10 @@ class VisualMapModel extends Com context?: Ctx ) { zrUtil.each(this.getTargetSeriesIndices(), function (seriesIndex) { - callback.call(context, this.ecModel.getSeriesByIndex(seriesIndex)); + const seriesModel = this.ecModel.getSeriesByIndex(seriesIndex); + if (seriesModel) { + callback.call(context, seriesModel); + } }, this); } diff --git a/src/coord/geo/GeoModel.ts b/src/coord/geo/GeoModel.ts index f5a1fe03d6..ba8f524abb 100644 --- a/src/coord/geo/GeoModel.ts +++ b/src/coord/geo/GeoModel.ts @@ -101,6 +101,8 @@ export interface GeoOption extends }; regions: RegoinOption[]; + + stateAnimation?: AnimationOptionMixin } const LABEL_FORMATTER_NORMAL = ['label', 'formatter'] as const; diff --git a/src/echarts.ts b/src/echarts.ts index 765ee6a160..e0d748ac88 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -71,6 +71,7 @@ import IncrementalDisplayable from 'zrender/src/graphic/IncrementalDisplayable'; import 'zrender/src/canvas/canvas'; import { seriesSymbolTask, dataSymbolTask } from './visual/symbol'; import { getVisualFromData, getItemVisualFromData } from './visual/helper'; +import LabelManager from './label/LabelManager'; declare let global: any; type ModelFinder = modelUtil.ModelFinder; @@ -215,10 +216,6 @@ let renderSeries: ( dirtyMap?: {[uid: string]: any} ) => void; let performPostUpdateFuncs: (ecModel: GlobalModel, api: ExtensionAPI) => void; -let updateHoverLayerStatus: (ecIns: ECharts, ecModel: GlobalModel) => void; -let updateBlend: (seriesModel: SeriesModel, chartView: ChartView) => void; -let updateZ: (model: ComponentModel, view: ComponentView | ChartView) => void; -let updateHoverEmphasisHandler: (view: ComponentView | ChartView) => void; let createExtensionAPI: (ecIns: ECharts) => ExtensionAPI; let enableConnect: (chart: ECharts) => void; @@ -270,6 +267,8 @@ class ECharts extends Eventful { private _loadingFX: LoadingEffect; + private _labelManager: LabelManager; + private [OPTION_UPDATED]: boolean | {silent: boolean}; private [IN_MAIN_PROCESS]: boolean; private [CONNECT_STATUS_KEY]: ConnectStatus; @@ -334,6 +333,8 @@ class ECharts extends Eventful { this._messageCenter = new MessageCenter(); + this._labelManager = new LabelManager(); + // Init mouse events this._initEvents(); @@ -378,7 +379,7 @@ class ECharts extends Eventful { let remainTime = TEST_FRAME_REMAIN_TIME; const ecModel = this._model; const api = this._api; - scheduler.unfinished = +false; + scheduler.unfinished = false; do { const startTime = +new Date(); @@ -853,7 +854,7 @@ class ECharts extends Eventful { else if (ecData && ecData.dataIndex != null) { const dataModel = ecData.dataModel || ecModel.getSeriesByIndex(ecData.seriesIndex); params = ( - dataModel && dataModel.getDataParams(ecData.dataIndex, ecData.dataType, el) || {} + dataModel && dataModel.getDataParams(ecData.dataIndex, ecData.dataType) || {} ) as ECEvent; } // If element has custom eventData of components @@ -1121,6 +1122,13 @@ class ECharts extends Eventful { triggerUpdatedEvent.call(this, silent); } + updateLabelLayout() { + const labelManager = this._labelManager; + labelManager.updateLayoutConfig(this._api); + labelManager.layout(this._api); + labelManager.processLabelsOverall(); + } + appendData(params: { seriesIndex: number, data: any @@ -1148,7 +1156,7 @@ class ECharts extends Eventful { // graphic elements have to be changed, which make the usage of // `appendData` meaningless. - this._scheduler.unfinished = +true; + this._scheduler.unfinished = true; } @@ -1347,6 +1355,7 @@ class ECharts extends Eventful { // Set background let backgroundColor = ecModel.get('backgroundColor') || 'transparent'; + const darkMode = ecModel.get('darkMode'); // In IE8 if (!env.canvasSupported) { @@ -1358,6 +1367,11 @@ class ECharts extends Eventful { } else { zr.setBackgroundColor(backgroundColor); + + // Force set dark mode. + if (darkMode != null && darkMode !== 'auto') { + zr.setDarkMode(darkMode); + } } performPostUpdateFuncs(ecModel, api); @@ -1689,13 +1703,16 @@ class ECharts extends Eventful { ): void { each(dirtyList || ecIns._componentsViews, function (componentView: ComponentView) { const componentModel = componentView.__model; - componentView.render(componentModel, ecModel, api, payload); + clearStates(componentModel, componentView); - componentView.group.markRedraw(); + componentView.render(componentModel, ecModel, api, payload); updateZ(componentModel, componentView); updateHoverEmphasisHandler(componentView); + + updateStates(componentModel, componentView); }); + }; /** @@ -1710,7 +1727,11 @@ class ECharts extends Eventful { ): void { // Render all charts const scheduler = ecIns._scheduler; - let unfinished: number; + const labelManager = ecIns._labelManager; + + labelManager.clearLabels(); + + let unfinished: boolean = false; ecModel.eachSeries(function (seriesModel) { const chartView = ecIns._chartsMap[seriesModel.__viewId]; chartView.__alive = true; @@ -1718,11 +1739,15 @@ class ECharts extends Eventful { const renderTask = chartView.renderTask; scheduler.updatePayload(renderTask, payload); + // TODO states on marker. + clearStates(seriesModel, chartView); + if (dirtyMap && dirtyMap.get(seriesModel.uid)) { renderTask.dirty(); } - - unfinished |= +renderTask.perform(scheduler.getPerformArgs(renderTask)); + if (renderTask.perform(scheduler.getPerformArgs(renderTask))) { + unfinished = true; + } chartView.group.silent = !!seriesModel.get('silent'); // Should not call markRedraw on group, because it will disable zrender @@ -1734,11 +1759,28 @@ class ECharts extends Eventful { updateBlend(seriesModel, chartView); updateHoverEmphasisHandler(chartView); + + // Add labels. + labelManager.addLabelsOfSeries(chartView); }); - scheduler.unfinished |= unfinished; + + scheduler.unfinished = unfinished || scheduler.unfinished; + + labelManager.updateLayoutConfig(api); + labelManager.layout(api); + labelManager.processLabelsOverall(); + + ecModel.eachSeries(function (seriesModel) { + const chartView = ecIns._chartsMap[seriesModel.__viewId]; + // NOTE: Update states after label is updated. + // label should be in normal status when layouting. + updateStates(seriesModel, chartView); + }); + // If use hover layer - updateHoverLayerStatus(ecIns, ecModel); + // TODO + // updateHoverLayerStatus(ecIns, ecModel); // Add aria aria(ecIns._zr.dom, ecModel); @@ -1750,7 +1792,7 @@ class ECharts extends Eventful { }); }; - updateHoverLayerStatus = function (ecIns: ECharts, ecModel: GlobalModel): void { + function updateHoverLayerStatus(ecIns: ECharts, ecModel: GlobalModel): void { const zr = ecIns._zr; const storage = zr.storage; let elCount = 0; @@ -1768,7 +1810,7 @@ class ECharts extends Eventful { if (chartView.__alive) { chartView.group.traverse(function (el: ECElement) { // Don't switch back. - // el.useHoverLayer = true; + el.useHoverLayer = true; }); } }); @@ -1778,7 +1820,7 @@ class ECharts extends Eventful { /** * Update chart progressive and blend. */ - updateBlend = function (seriesModel: SeriesModel, chartView: ChartView): void { + function updateBlend(seriesModel: SeriesModel, chartView: ChartView): void { const blendMode = seriesModel.get('blendMode') || null; if (__DEV__) { if (!env.canvasSupported && blendMode && blendMode !== 'source-over') { @@ -1788,20 +1830,18 @@ class ECharts extends Eventful { chartView.group.traverse(function (el: Displayable) { // FIXME marker and other components if (!el.isGroup) { - // Only set if blendMode is changed. In case element is incremental and don't wan't to rerender. - if (el.style.blend !== blendMode) { - el.setStyle('blend', blendMode); - } + // DONT mark the element dirty. In case element is incremental and don't wan't to rerender. + el.style.blend = blendMode; } if ((el as IncrementalDisplayable).eachPendingDisplayable) { (el as IncrementalDisplayable).eachPendingDisplayable(function (displayable) { - displayable.setStyle('blend', blendMode); + displayable.style.blend = blendMode; }); } }); }; - updateZ = function (model: ComponentModel, view: ComponentView | ChartView): void { + function updateZ(model: ComponentModel, view: ComponentView | ChartView): void { if (model.preventAutoZ) { return; } @@ -1809,18 +1849,101 @@ class ECharts extends Eventful { const zlevel = model.get('zlevel'); // Set z and zlevel view.group.traverse(function (el: Displayable) { - if (el.type !== 'group') { + if (!el.isGroup) { z != null && (el.z = z); zlevel != null && (el.zlevel = zlevel); // TODO if textContent is on group. - const textContent = el.getTextContent(); - if (textContent) { - textContent.z = el.z; - textContent.zlevel = el.zlevel; + const label = el.getTextContent(); + const labelLine = el.getTextGuideLine(); + if (label) { + label.z = el.z; + label.zlevel = el.zlevel; // lift z2 of text content // TODO if el.emphasis.z2 is spcefied, what about textContent. - textContent.z2 = el.z2 + 1; + label.z2 = el.z2 + 1; + } + if (labelLine) { + labelLine.z = el.z; + labelLine.zlevel = el.zlevel; + labelLine.z2 = el.z2 - 1; + } + } + }); + }; + + // Clear states without animation. + // TODO States on component. + function clearStates(model: ComponentModel, view: ComponentView | ChartView): void { + view.group.traverse(function (el: Displayable) { + const textContent = el.getTextContent(); + const textGuide = el.getTextGuideLine(); + if (el.stateTransition) { + el.stateTransition = null; + } + if (textContent && textContent.stateTransition) { + textContent.stateTransition = null; + } + if (textGuide && textGuide.stateTransition) { + textGuide.stateTransition = null; + } + + // TODO If el is incremental. + if (el.hasState()) { + el.prevStates = el.currentStates; + el.clearStates(); + } + else if (el.prevStates) { + el.prevStates = null; + } + }); + } + + function updateStates(model: ComponentModel, view: ComponentView | ChartView): void { + const stateAnimationModel = (model as SeriesModel).getModel('stateAnimation'); + const enableAnimation = model.isAnimationEnabled(); + const duration = stateAnimationModel.get('duration'); + const stateTransition = duration > 0 ? { + duration, + delay: stateAnimationModel.get('delay'), + easing: stateAnimationModel.get('easing') + } : null; + view.group.traverse(function (el: Displayable) { + if (el.states && el.states.emphasis) { + // Only updated on changed element. In case element is incremental and don't wan't to rerender. + // TODO, a more proper way? + if (el.__dirty) { + const prevStates = el.prevStates; + // Restore states without animation + if (prevStates) { + el.useStates(prevStates); + } + } + + // Update state transition and enable animation again. + if (enableAnimation) { + el.stateTransition = stateTransition; + const textContent = el.getTextContent(); + const textGuide = el.getTextGuideLine(); + // TODO Is it necessary to animate label? + if (textContent) { + textContent.stateTransition = stateTransition; + } + if (textGuide) { + textGuide.stateTransition = stateTransition; + } + } + + // The use higlighted and selected flag to toggle states. + if (el.__dirty) { + const states = []; + if ((el as ECElement).selected) { + states.push('select'); + } + if ((el as ECElement).highlighted) { + states.push('emphasis'); + } + el.useStates(states); } } }); @@ -1844,7 +1967,7 @@ class ECharts extends Eventful { graphic.leaveEmphasisWhenMouseOut(dispatcher, e); } } - updateHoverEmphasisHandler = function (view: ComponentView | ChartView): void { + function updateHoverEmphasisHandler(view: ComponentView | ChartView): void { view.group.on('mouseover', onMouseOver) .on('mouseout', onMouseOut); }; @@ -1907,7 +2030,6 @@ class ECharts extends Eventful { } - const echartsProto = ECharts.prototype; echartsProto.on = createRegisterEventWithLowercaseECharts('on'); echartsProto.off = createRegisterEventWithLowercaseECharts('off'); diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts new file mode 100644 index 0000000000..7cac2740ba --- /dev/null +++ b/src/label/LabelManager.ts @@ -0,0 +1,554 @@ +/* +* 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. +*/ + +// TODO: move labels out of viewport. + +import { + Text as ZRText, + BoundingRect, + getECData, + Polyline, + updateProps, + initProps +} from '../util/graphic'; +import ExtensionAPI from '../ExtensionAPI'; +import { + ZRTextAlign, + ZRTextVerticalAlign, + LabelLayoutOption, + LabelLayoutOptionCallback, + LabelLayoutOptionCallbackParams, + LabelLineOption, + Dictionary +} from '../util/types'; +import { parsePercent } from '../util/number'; +import ChartView from '../view/Chart'; +import Element, { ElementTextConfig } from 'zrender/src/Element'; +import { RectLike } from 'zrender/src/core/BoundingRect'; +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, filter, indexOf } from 'zrender/src/core/util'; +import { PathStyleProps } from 'zrender/src/graphic/Path'; +import Model from '../model/Model'; +import { prepareLayoutList, hideOverlap, shiftLayoutOnX, shiftLayoutOnY } from './labelLayoutHelper'; + +interface LabelDesc { + label: ZRText + labelLine: Polyline + + seriesModel: SeriesModel + dataIndex: number + dataType: string + + layoutOption: LabelLayoutOptionCallback | LabelLayoutOption + computedLayoutOption: LabelLayoutOption + + hostRect: RectLike + priority: number + + defaultAttr: SavedLabelAttr +} + +interface SavedLabelAttr { + ignore: boolean + labelGuideIgnore: boolean + + x: number + y: number + rotation: number + + style: { + align: ZRTextAlign + verticalAlign: ZRTextVerticalAlign + width: number + height: number + + x: number + y: number + } + + // Configuration in attached element + attachedPos: ElementTextConfig['position'] + attachedRot: ElementTextConfig['rotation'] + + rect: RectLike +} + +function cloneArr(points: number[][]) { + if (points) { + const newPoints = []; + for (let i = 0; i < points.length; i++) { + newPoints.push(points[i].slice()); + } + return newPoints; + } +} + +function prepareLayoutCallbackParams(labelItem: LabelDesc, hostEl?: Element): LabelLayoutOptionCallbackParams { + const labelAttr = labelItem.defaultAttr; + const label = labelItem.label; + const labelLine = hostEl && hostEl.getTextGuideLine(); + return { + dataIndex: labelItem.dataIndex, + dataType: labelItem.dataType, + seriesIndex: labelItem.seriesModel.seriesIndex, + text: labelItem.label.style.text, + rect: labelItem.hostRect, + labelRect: labelAttr.rect, + // x: labelAttr.x, + // y: labelAttr.y, + align: label.style.align, + verticalAlign: label.style.verticalAlign, + labelLinePoints: cloneArr(labelLine && labelLine.shape.points) + }; +} + +const LABEL_OPTION_TO_STYLE_KEYS = ['align', 'verticalAlign', 'width', 'height'] as const; + +const dummyTransformable = new Transformable(); + +const labelLayoutInnerStore = makeInner<{ + oldLayout: { + x: number, + y: number, + rotation: number + }, + oldLayoutSelect?: { + x?: number, + y?: number, + rotation?: number + }, + oldLayoutEmphasis?: { + x?: number, + y?: number, + rotation?: number + }, + + needsUpdateLabelLine?: boolean +}, ZRText>(); + +const labelLineAnimationStore = makeInner<{ + oldLayout: { + points: number[][] + } +}, Polyline>(); + +type LabelLineOptionMixin = { + labelLine: LabelLineOption, + emphasis: { labelLine: LabelLineOption } +}; + +function extendWithKeys(target: Dictionary, source: Dictionary, keys: string[]) { + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (source[key] != null) { + target[key] = source[key]; + } + } +} + +const LABEL_LAYOUT_PROPS = ['x', 'y', 'rotation']; + +class LabelManager { + + private _labelList: LabelDesc[] = []; + private _chartViewList: ChartView[] = []; + + constructor() {} + + clearLabels() { + this._labelList = []; + this._chartViewList = []; + } + + /** + * Add label to manager + */ + private _addLabel( + dataIndex: number, + dataType: string, + seriesModel: SeriesModel, + label: ZRText, + layoutOption: LabelDesc['layoutOption'] + ) { + const labelStyle = label.style; + const hostEl = label.__hostTarget; + const textConfig = hostEl.textConfig || {}; + + // TODO: If label is in other state. + const labelTransform = label.getComputedTransform(); + const labelRect = label.getBoundingRect().plain(); + BoundingRect.applyTransform(labelRect, labelRect, labelTransform); + + if (labelTransform) { + dummyTransformable.setLocalTransform(labelTransform); + } + else { + // Identity transform. + dummyTransformable.x = dummyTransformable.y = dummyTransformable.rotation = + dummyTransformable.originX = dummyTransformable.originY = 0; + dummyTransformable.scaleX = dummyTransformable.scaleY = 1; + } + + const host = label.__hostTarget; + let hostRect; + if (host) { + hostRect = host.getBoundingRect().plain(); + const transform = host.getComputedTransform(); + BoundingRect.applyTransform(hostRect, hostRect, transform); + } + + const labelGuide = hostRect && host.getTextGuideLine(); + + this._labelList.push({ + label, + labelLine: labelGuide, + + seriesModel, + dataIndex, + dataType, + + layoutOption, + computedLayoutOption: null, + + hostRect, + + // Label with lower priority will be hidden when overlapped + // Use rect size as default priority + priority: hostRect ? hostRect.width * hostRect.height : 0, + + // Save default label attributes. + // For restore if developers want get back to default value in callback. + defaultAttr: { + ignore: label.ignore, + labelGuideIgnore: labelGuide && labelGuide.ignore, + + x: dummyTransformable.x, + y: dummyTransformable.y, + rotation: dummyTransformable.rotation, + + rect: labelRect, + + style: { + x: labelStyle.x, + y: labelStyle.y, + + align: labelStyle.align, + verticalAlign: labelStyle.verticalAlign, + width: labelStyle.width, + height: labelStyle.height + }, + + attachedPos: textConfig.position, + attachedRot: textConfig.rotation + } + }); + } + + addLabelsOfSeries(chartView: ChartView) { + this._chartViewList.push(chartView); + + const seriesModel = chartView.__model; + + const layoutOption = seriesModel.get('labelLayout'); + + /** + * Ignore layouting if it's not specified anything. + */ + if (!(isFunction(layoutOption) || keys(layoutOption).length)) { + return; + } + + chartView.group.traverse((child) => { + if (child.ignore) { + return true; // Stop traverse descendants. + } + + // Only support label being hosted on graphic elements. + const textEl = child.getTextContent(); + const ecData = getECData(child); + const dataIndex = ecData.dataIndex; + // Can only attach the text on the element with dataIndex + if (textEl && dataIndex != null) { + this._addLabel(dataIndex, ecData.dataType, seriesModel, textEl, layoutOption); + } + }); + } + + updateLayoutConfig(api: ExtensionAPI) { + const width = api.getWidth(); + const height = api.getHeight(); + + function createDragHandler(el: Element, labelLineModel: Model) { + return function () { + updateLabelLinePoints(el, labelLineModel); + }; + } + for (let i = 0; i < this._labelList.length; i++) { + const labelItem = this._labelList[i]; + const label = labelItem.label; + const hostEl = label.__hostTarget; + const defaultLabelAttr = labelItem.defaultAttr; + let layoutOption; + // TODO A global layout option? + if (typeof labelItem.layoutOption === 'function') { + layoutOption = labelItem.layoutOption( + prepareLayoutCallbackParams(labelItem, hostEl) + ); + } + else { + layoutOption = labelItem.layoutOption; + } + + layoutOption = layoutOption || {}; + labelItem.computedLayoutOption = layoutOption; + + const degreeToRadian = Math.PI / 180; + if (hostEl) { + hostEl.setTextConfig({ + // Force to set local false. + local: false, + // Ignore position and rotation config on the host el if x or y is changed. + position: (layoutOption.x != null || layoutOption.y != null) + ? null : defaultLabelAttr.attachedPos, + // Ignore rotation config on the host el if rotation is changed. + rotation: layoutOption.rotate != null + ? layoutOption.rotate * degreeToRadian : defaultLabelAttr.attachedRot, + offset: [layoutOption.dx || 0, layoutOption.dy || 0] + }); + } + let needsUpdateLabelLine = false; + if (layoutOption.x != null) { + // TODO width of chart view. + label.x = parsePercent(layoutOption.x, width); + label.setStyle('x', 0); // Ignore movement in style. TODO: origin. + needsUpdateLabelLine = true; + } + else { + label.x = defaultLabelAttr.x; + label.setStyle('x', defaultLabelAttr.style.x); + } + + if (layoutOption.y != null) { + // TODO height of chart view. + label.y = parsePercent(layoutOption.y, height); + label.setStyle('y', 0); // Ignore movement in style. + needsUpdateLabelLine = true; + } + else { + label.y = defaultLabelAttr.y; + label.setStyle('y', defaultLabelAttr.style.y); + } + + if (layoutOption.labelLinePoints) { + const guideLine = hostEl.getTextGuideLine(); + if (guideLine) { + guideLine.setShape({ points: layoutOption.labelLinePoints }); + // Not update + needsUpdateLabelLine = false; + } + } + + const labelLayoutStore = labelLayoutInnerStore(label); + labelLayoutStore.needsUpdateLabelLine = needsUpdateLabelLine; + + label.rotation = layoutOption.rotate != null + ? layoutOption.rotate * degreeToRadian : defaultLabelAttr.rotation; + + for (let k = 0; k < LABEL_OPTION_TO_STYLE_KEYS.length; k++) { + const key = LABEL_OPTION_TO_STYLE_KEYS[k]; + label.setStyle(key, layoutOption[key] != null ? layoutOption[key] : defaultLabelAttr.style[key]); + } + + + if (layoutOption.draggable) { + label.draggable = true; + label.cursor = 'move'; + if (hostEl) { + const data = labelItem.seriesModel.getData(labelItem.dataType); + const itemModel = data.getItemModel(labelItem.dataIndex); + label.on('drag', createDragHandler(hostEl, itemModel.getModel('labelLine'))); + } + } + else { + // TODO Other drag functions? + label.off('drag'); + label.cursor = 'default'; + } + } + } + + layout(api: ExtensionAPI) { + const width = api.getWidth(); + const height = api.getHeight(); + + 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'; + }); + + shiftLayoutOnX(labelsNeedsAdjustOnX, 0, width); + shiftLayoutOnY(labelsNeedsAdjustOnY, 0, height); + + const labelsNeedsHideOverlap = filter(labelList, function (item) { + return item.layoutOption.hideOverlap; + }); + + hideOverlap(labelsNeedsHideOverlap); + } + + /** + * Process all labels. Not only labels with layoutOption. + */ + processLabelsOverall() { + each(this._chartViewList, (chartView) => { + const seriesModel = chartView.__model; + const ignoreLabelLineUpdate = chartView.ignoreLabelLineUpdate; + const animationEnabled = seriesModel.isAnimationEnabled(); + + chartView.group.traverse((child) => { + if (child.ignore) { + return true; // Stop traverse descendants. + } + + let needsUpdateLabelLine = !ignoreLabelLineUpdate; + const label = child.getTextContent(); + if (!needsUpdateLabelLine && label) { + needsUpdateLabelLine = labelLayoutInnerStore(label).needsUpdateLabelLine; + } + if (needsUpdateLabelLine) { + this._updateLabelLine(child, seriesModel); + } + + if (animationEnabled) { + this._animateLabels(child, seriesModel); + } + }); + }); + } + + private _updateLabelLine(el: Element, seriesModel: SeriesModel) { + // Only support label being hosted on graphic elements. + const textEl = el.getTextContent(); + // Update label line style. + const ecData = getECData(el); + const dataIndex = ecData.dataIndex; + + if (textEl && dataIndex != null) { + const data = seriesModel.getData(ecData.dataType); + const itemModel = data.getItemModel(dataIndex); + + const defaultStyle: PathStyleProps = {}; + const visualStyle = data.getItemVisual(dataIndex, 'style'); + const visualType = data.getVisual('drawType'); + // Default to be same with main color + defaultStyle.stroke = visualStyle[visualType]; + + const labelLineModel = itemModel.getModel('labelLine'); + + setLabelLineStyle(el, { + normal: labelLineModel, + emphasis: itemModel.getModel(['emphasis', 'labelLine']) + }, defaultStyle); + + updateLabelLinePoints(el, labelLineModel); + } + } + + private _animateLabels(el: Element, seriesModel: SeriesModel) { + const textEl = el.getTextContent(); + const guideLine = el.getTextGuideLine(); + // Animate + if (textEl && !textEl.ignore && !textEl.invisible) { + const layoutStore = labelLayoutInnerStore(textEl); + const oldLayout = layoutStore.oldLayout; + const newProps = { + x: textEl.x, + y: textEl.y, + rotation: textEl.rotation + }; + if (!oldLayout) { + textEl.attr(newProps); + const oldOpacity = retrieve2(textEl.style.opacity, 1); + // Fade in animation + textEl.style.opacity = 0; + initProps(textEl, { + style: { opacity: oldOpacity } + }, seriesModel); + } + else { + textEl.attr(oldLayout); + + // Make sure the animation from is in the right status. + const prevStates = el.prevStates; + if (prevStates) { + if (indexOf(prevStates, 'select') >= 0) { + textEl.attr(layoutStore.oldLayoutSelect); + } + if (indexOf(prevStates, 'emphasis') >= 0) { + textEl.attr(layoutStore.oldLayoutEmphasis); + } + } + updateProps(textEl, newProps, seriesModel); + } + layoutStore.oldLayout = newProps; + + if (textEl.states.select) { + const layoutSelect = layoutStore.oldLayoutSelect = {}; + extendWithKeys(layoutSelect, newProps, LABEL_LAYOUT_PROPS); + extendWithKeys(layoutSelect, textEl.states.select, LABEL_LAYOUT_PROPS); + } + + if (textEl.states.emphasis) { + const layoutEmphasis = layoutStore.oldLayoutEmphasis = {}; + extendWithKeys(layoutEmphasis, newProps, LABEL_LAYOUT_PROPS); + extendWithKeys(layoutEmphasis, textEl.states.emphasis, LABEL_LAYOUT_PROPS); + } + } + + if (guideLine && !guideLine.ignore && !guideLine.invisible) { + const layoutStore = labelLineAnimationStore(guideLine); + const oldLayout = layoutStore.oldLayout; + const newLayout = { points: guideLine.shape.points }; + if (!oldLayout) { + guideLine.setShape(newLayout); + guideLine.style.strokePercent = 0; + initProps(guideLine, { + style: { strokePercent: 1 } + }, seriesModel); + } + else { + guideLine.attr({ shape: oldLayout }); + updateProps(guideLine, { + shape: newLayout + }, seriesModel); + } + + layoutStore.oldLayout = newLayout; + } + } +} + + +export default LabelManager; \ No newline at end of file diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts new file mode 100644 index 0000000000..dfc6f7c8b4 --- /dev/null +++ b/src/label/labelGuideHelper.ts @@ -0,0 +1,649 @@ +/* +* 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 { + Point, + Path, + Polyline +} from '../util/graphic'; +import PathProxy from 'zrender/src/core/PathProxy'; +import { RectLike } from 'zrender/src/core/BoundingRect'; +import { normalizeRadian } from 'zrender/src/contain/util'; +import { cubicProjectPoint, quadraticProjectPoint } from 'zrender/src/core/curve'; +import Element from 'zrender/src/Element'; +import { defaults, retrieve2 } from 'zrender/src/core/util'; +import { LabelLineOption } from '../util/types'; +import Model from '../model/Model'; +import { invert } from 'zrender/src/core/matrix'; +import * as vector from 'zrender/src/core/vector'; + +const PI2 = Math.PI * 2; +const CMD = PathProxy.CMD; + +const STATES = ['normal', 'emphasis'] as const; + +const DEFAULT_SEARCH_SPACE = ['top', 'right', 'bottom', 'left'] as const; + +type CandidatePosition = typeof DEFAULT_SEARCH_SPACE[number]; + +function getCandidateAnchor( + pos: CandidatePosition, + distance: number, + rect: RectLike, + outPt: Point, + outDir: Point +) { + const width = rect.width; + const height = rect.height; + switch (pos) { + case 'top': + outPt.set( + rect.x + width / 2, + rect.y - distance + ); + outDir.set(0, -1); + break; + case 'bottom': + outPt.set( + rect.x + width / 2, + rect.y + height + distance + ); + outDir.set(0, 1); + break; + case 'left': + outPt.set( + rect.x - distance, + rect.y + height / 2 + ); + outDir.set(-1, 0); + break; + case 'right': + outPt.set( + rect.x + width + distance, + rect.y + height / 2 + ); + outDir.set(1, 0); + break; + } +} + + +function projectPointToArc( + cx: number, cy: number, r: number, startAngle: number, endAngle: number, anticlockwise: boolean, + x: number, y: number, out: number[] +): number { + x -= cx; + y -= cy; + const d = Math.sqrt(x * x + y * y); + x /= d; + y /= d; + + // Intersect point. + const ox = x * r + cx; + const oy = y * r + cy; + + if (Math.abs(startAngle - endAngle) % PI2 < 1e-4) { + // Is a circle + out[0] = ox; + out[1] = oy; + return d - r; + } + + if (anticlockwise) { + const tmp = startAngle; + startAngle = normalizeRadian(endAngle); + endAngle = normalizeRadian(tmp); + } + else { + startAngle = normalizeRadian(startAngle); + endAngle = normalizeRadian(endAngle); + } + if (startAngle > endAngle) { + endAngle += PI2; + } + + let angle = Math.atan2(y, x); + if (angle < 0) { + angle += PI2; + } + if ((angle >= startAngle && angle <= endAngle) + || (angle + PI2 >= startAngle && angle + PI2 <= endAngle)) { + // Project point is on the arc. + out[0] = ox; + out[1] = oy; + return d - r; + } + + const x1 = r * Math.cos(startAngle) + cx; + const y1 = r * Math.sin(startAngle) + cy; + + const x2 = r * Math.cos(endAngle) + cx; + const y2 = r * Math.sin(endAngle) + cy; + + const d1 = (x1 - x) * (x1 - x) + (y1 - y) * (y1 - y); + const d2 = (x2 - x) * (x2 - x) + (y2 - y) * (y2 - y); + + if (d1 < d2) { + out[0] = x1; + out[1] = y1; + return Math.sqrt(d1); + } + else { + out[0] = x2; + out[1] = y2; + return Math.sqrt(d2); + } +} + +function projectPointToLine( + x1: number, y1: number, x2: number, y2: number, x: number, y: number, out: number[], limitToEnds: boolean +) { + const dx = x - x1; + const dy = y - y1; + + let dx1 = x2 - x1; + let dy1 = y2 - y1; + + const lineLen = Math.sqrt(dx1 * dx1 + dy1 * dy1); + dx1 /= lineLen; + dy1 /= lineLen; + + // dot product + const projectedLen = dx * dx1 + dy * dy1; + let t = projectedLen / lineLen; + if (limitToEnds) { + t = Math.min(Math.max(t, 0), 1); + } + t *= lineLen; + const ox = out[0] = x1 + t * dx1; + const oy = out[1] = y1 + t * dy1; + + return Math.sqrt((ox - x) * (ox - x) + (oy - y) * (oy - y)); +} + +function projectPointToRect( + x1: number, y1: number, width: number, height: number, x: number, y: number, out: number[] +): number { + if (width < 0) { + x1 = x1 + width; + width = -width; + } + if (height < 0) { + y1 = y1 + height; + height = -height; + } + const x2 = x1 + width; + const y2 = y1 + height; + + const ox = out[0] = Math.min(Math.max(x, x1), x2); + const oy = out[1] = Math.min(Math.max(y, y1), y2); + + return Math.sqrt((ox - x) * (ox - x) + (oy - y) * (oy - y)); +} + +const tmpPt: number[] = []; + +function nearestPointOnRect(pt: Point, rect: RectLike, out: Point) { + const dist = projectPointToRect( + rect.x, rect.y, rect.width, rect.height, + pt.x, pt.y, tmpPt + ); + out.set(tmpPt[0], tmpPt[1]); + return dist; +} +/** + * Calculate min distance corresponding point. + * This method won't evaluate if point is in the path. + */ +function nearestPointOnPath(pt: Point, path: PathProxy, out: Point) { + let xi = 0; + let yi = 0; + let x0 = 0; + let y0 = 0; + let x1; + let y1; + + let minDist = Infinity; + + const data = path.data; + const x = pt.x; + const y = pt.y; + + for (let i = 0; i < data.length;) { + const cmd = data[i++]; + + if (i === 1) { + xi = data[i]; + yi = data[i + 1]; + x0 = xi; + y0 = yi; + } + + let d = minDist; + + switch (cmd) { + case CMD.M: + // moveTo 命令重新创建一个新的 subpath, 并且更新新的起点 + // 在 closePath 的时候使用 + x0 = data[i++]; + y0 = data[i++]; + xi = x0; + yi = y0; + break; + case CMD.L: + d = projectPointToLine(xi, yi, data[i], data[i + 1], x, y, tmpPt, true); + xi = data[i++]; + yi = data[i++]; + break; + case CMD.C: + d = cubicProjectPoint( + xi, yi, + data[i++], data[i++], data[i++], data[i++], data[i], data[i + 1], + x, y, tmpPt + ); + + xi = data[i++]; + yi = data[i++]; + break; + case CMD.Q: + d = quadraticProjectPoint( + xi, yi, + data[i++], data[i++], data[i], data[i + 1], + x, y, tmpPt + ); + xi = data[i++]; + yi = data[i++]; + break; + case CMD.A: + // TODO Arc 判断的开销比较大 + const cx = data[i++]; + const cy = data[i++]; + const rx = data[i++]; + const ry = data[i++]; + const theta = data[i++]; + const dTheta = data[i++]; + // TODO Arc 旋转 + i += 1; + const anticlockwise = !!(1 - data[i++]); + x1 = Math.cos(theta) * rx + cx; + y1 = Math.sin(theta) * ry + cy; + // 不是直接使用 arc 命令 + if (i <= 1) { + // 第一个命令起点还未定义 + x0 = x1; + y0 = y1; + } + // zr 使用scale来模拟椭圆, 这里也对x做一定的缩放 + const _x = (x - cx) * ry / rx + cx; + d = projectPointToArc( + cx, cy, ry, theta, theta + dTheta, anticlockwise, + _x, y, tmpPt + ); + xi = Math.cos(theta + dTheta) * rx + cx; + yi = Math.sin(theta + dTheta) * ry + cy; + break; + case CMD.R: + x0 = xi = data[i++]; + y0 = yi = data[i++]; + const width = data[i++]; + const height = data[i++]; + d = projectPointToRect(x0, y0, width, height, x, y, tmpPt); + break; + case CMD.Z: + d = projectPointToLine(xi, yi, x0, y0, x, y, tmpPt, true); + + xi = x0; + yi = y0; + break; + } + + if (d < minDist) { + minDist = d; + out.set(tmpPt[0], tmpPt[1]); + } + } + + return minDist; +} + +// Temporal varible for intermediate usage. +const pt0 = new Point(); +const pt1 = new Point(); +const pt2 = new Point(); +const dir = new Point(); +const dir2 = new Point(); + +/** + * Calculate a proper guide line based on the label position and graphic element definition + * @param label + * @param labelRect + * @param target + * @param targetRect + */ +export function updateLabelLinePoints( + target: Element, + labelLineModel: Model +) { + if (!target) { + return; + } + + const labelLine = target.getTextGuideLine(); + const label = target.getTextContent(); + // Needs to create text guide in each charts. + if (!(label && labelLine)) { + return; + } + + const labelGuideConfig = target.textGuideLineConfig || {}; + + const points = [[0, 0], [0, 0], [0, 0]]; + + const searchSpace = labelGuideConfig.candidates || DEFAULT_SEARCH_SPACE; + const labelRect = label.getBoundingRect().clone(); + labelRect.applyTransform(label.getComputedTransform()); + + let minDist = Infinity; + const anchorPoint = labelGuideConfig && labelGuideConfig.anchor; + const targetTransform = target.getComputedTransform(); + const targetInversedTransform = targetTransform && invert([], targetTransform); + const len = labelLineModel.get('length2') || 0; + + if (anchorPoint) { + pt2.copy(anchorPoint); + } + for (let i = 0; i < searchSpace.length; i++) { + const candidate = searchSpace[i]; + getCandidateAnchor(candidate, 0, labelRect, pt0, dir); + Point.scaleAndAdd(pt1, pt0, dir, len); + + // Transform to target coord space. + pt1.transform(targetInversedTransform); + + const dist = anchorPoint ? anchorPoint.distance(pt1) + : (target instanceof Path + ? nearestPointOnPath(pt1, target.path, pt2) + : nearestPointOnRect(pt1, target.getBoundingRect(), pt2)); + + // TODO pt2 is in the path + if (dist < minDist) { + minDist = dist; + // Transform back to global space. + pt1.transform(targetTransform); + pt2.transform(targetTransform); + + pt2.toArray(points[0]); + pt1.toArray(points[1]); + pt0.toArray(points[2]); + } + } + + limitTurnAngle(points, labelLineModel.get('minTurnAngle')); + + labelLine.setShape({ points }); +} + +// Temporal variable for the limitTurnAngle function +const tmpArr: number[] = []; +const tmpProjPoint = new Point(); +/** + * Reduce the line segment attached to the label to limit the turn angle between two segments. + * @param linePoints + * @param minTurnAngle Radian of minimum turn angle. 0 - 180 + */ +export function limitTurnAngle(linePoints: number[][], minTurnAngle: number) { + if (!(minTurnAngle <= 180 && minTurnAngle > 0)) { + return; + } + minTurnAngle = minTurnAngle / 180 * Math.PI; + // The line points can be + // /pt1----pt2 (label) + // / + // pt0/ + pt0.fromArray(linePoints[0]); + pt1.fromArray(linePoints[1]); + pt2.fromArray(linePoints[2]); + + Point.sub(dir, pt0, pt1); + Point.sub(dir2, pt2, pt1); + + const len1 = dir.len(); + const len2 = dir2.len(); + if (len1 < 1e-3 || len2 < 1e-3) { + return; + } + + dir.scale(1 / len1); + dir2.scale(1 / len2); + + const angleCos = dir.dot(dir2); + const minTurnAngleCos = Math.cos(minTurnAngle); + if (minTurnAngleCos < angleCos) { // Smaller than minTurnAngle + // Calculate project point of pt0 on pt1-pt2 + const d = projectPointToLine(pt1.x, pt1.y, pt2.x, pt2.y, pt0.x, pt0.y, tmpArr, false); + tmpProjPoint.fromArray(tmpArr); + // Calculate new projected length with limited minTurnAngle and get the new connect point + tmpProjPoint.scaleAndAdd(dir2, d / Math.tan(Math.PI - minTurnAngle)); + // Limit the new calculated connect point between pt1 and pt2. + const t = pt2.x !== pt1.x + ? (tmpProjPoint.x - pt1.x) / (pt2.x - pt1.x) + : (tmpProjPoint.y - pt1.y) / (pt2.y - pt1.y); + if (isNaN(t)) { + return; + } + + if (t < 0) { + Point.copy(tmpProjPoint, pt1); + } + else if (t > 1) { + Point.copy(tmpProjPoint, pt2); + } + + tmpProjPoint.toArray(linePoints[1]); + } +} + +/** + * Limit the angle of line and the surface + * @param maxSurfaceAngle Radian of minimum turn angle. 0 - 180. 0 is same direction to normal. 180 is opposite + */ +export function limitSurfaceAngle(linePoints: vector.VectorArray[], surfaceNormal: Point, maxSurfaceAngle: number) { + if (!(maxSurfaceAngle <= 180 && maxSurfaceAngle > 0)) { + return; + } + maxSurfaceAngle = maxSurfaceAngle / 180 * Math.PI; + + pt0.fromArray(linePoints[0]); + pt1.fromArray(linePoints[1]); + pt2.fromArray(linePoints[2]); + + Point.sub(dir, pt1, pt0); + Point.sub(dir2, pt2, pt1); + + const len1 = dir.len(); + const len2 = dir2.len(); + + if (len1 < 1e-3 || len2 < 1e-3) { + return; + } + + dir.scale(1 / len1); + dir2.scale(1 / len2); + + const angleCos = dir.dot(surfaceNormal); + const maxSurfaceAngleCos = Math.cos(maxSurfaceAngle); + + if (angleCos < maxSurfaceAngleCos) { + // Calculate project point of pt0 on pt1-pt2 + const d = projectPointToLine(pt1.x, pt1.y, pt2.x, pt2.y, pt0.x, pt0.y, tmpArr, false); + tmpProjPoint.fromArray(tmpArr); + + const HALF_PI = Math.PI / 2; + const angle2 = Math.acos(dir2.dot(surfaceNormal)); + const newAngle = HALF_PI + angle2 - maxSurfaceAngle; + if (newAngle >= HALF_PI) { + // parallel + Point.copy(tmpProjPoint, pt2); + } + else { + // Calculate new projected length with limited minTurnAngle and get the new connect point + tmpProjPoint.scaleAndAdd(dir2, d / Math.tan(Math.PI / 2 - newAngle)); + // Limit the new calculated connect point between pt1 and pt2. + const t = pt2.x !== pt1.x + ? (tmpProjPoint.x - pt1.x) / (pt2.x - pt1.x) + : (tmpProjPoint.y - pt1.y) / (pt2.y - pt1.y); + if (isNaN(t)) { + return; + } + + if (t < 0) { + Point.copy(tmpProjPoint, pt1); + } + else if (t > 1) { + Point.copy(tmpProjPoint, pt2); + } + } + + tmpProjPoint.toArray(linePoints[1]); + } +} + + +type LabelLineModel = Model; + +function setLabelLineState( + labelLine: Polyline, + ignore: boolean, + stateName: string, + stateModel: Model +) { + const isNormal = stateName === 'normal'; + const stateObj = isNormal ? labelLine : labelLine.ensureState(stateName); + // Make sure display. + stateObj.ignore = ignore; + // Set smooth + let smooth = stateModel.get('smooth'); + if (smooth && smooth === true) { + smooth = 0.3; + } + stateObj.shape = stateObj.shape || {}; + if (smooth > 0) { + (stateObj.shape as Polyline['shape']).smooth = smooth as number; + } + + const styleObj = stateModel.getModel('lineStyle').getLineStyle(); + isNormal ? labelLine.useStyle(styleObj) : stateObj.style = styleObj; +} + +function buildLabelLinePath(path: CanvasRenderingContext2D, shape: Polyline['shape']) { + const smooth = shape.smooth as number; + const points = shape.points; + if (!points) { + return; + } + path.moveTo(points[0][0], points[0][1]); + if (smooth > 0 && points.length >= 3) { + const len1 = vector.dist(points[0], points[1]); + const len2 = vector.dist(points[1], points[2]); + if (!len1 || !len2) { + path.lineTo(points[1][0], points[1][1]); + path.lineTo(points[2][0], points[2][1]); + return; + } + + const moveLen = Math.min(len1, len2) * smooth; + + const midPoint0 = vector.lerp([], points[1], points[0], moveLen / len1); + const midPoint2 = vector.lerp([], points[1], points[2], moveLen / len2); + + const midPoint1 = vector.lerp([], midPoint0, midPoint2, 0.5); + path.bezierCurveTo(midPoint0[0], midPoint0[1], midPoint0[0], midPoint0[1], midPoint1[0], midPoint1[1]); + path.bezierCurveTo(midPoint2[0], midPoint2[1], midPoint2[0], midPoint2[1], points[2][0], points[2][1]); + } + else { + for (let i = 1; i < points.length; i++) { + path.lineTo(points[i][0], points[i][1]); + } + } +} + +/** + * Create a label line if necessary and set it's style. + */ +export function setLabelLineStyle( + targetEl: Element, + statesModels: Record, + defaultStyle?: Polyline['style'] +) { + let labelLine = targetEl.getTextGuideLine(); + const label = targetEl.getTextContent(); + if (!label) { + // Not show label line if there is no label. + if (labelLine) { + targetEl.removeTextGuideLine(); + } + return; + } + + const normalModel = statesModels.normal; + const showNormal = normalModel.get('show'); + const labelIgnoreNormal = label.ignore; + + for (let i = 0; i < STATES.length; i++) { + const stateName = STATES[i]; + const stateModel = statesModels[stateName]; + const isNormal = stateName === 'normal'; + if (stateModel) { + const stateShow = stateModel.get('show'); + const isLabelIgnored = isNormal + ? labelIgnoreNormal + : retrieve2(label.states[stateName] && label.states[stateName].ignore, labelIgnoreNormal); + if (isLabelIgnored // Not show when label is not shown in this state. + || !retrieve2(stateShow, showNormal) // Use normal state by default if not set. + ) { + const stateObj = isNormal ? labelLine : (labelLine && labelLine.states.normal); + if (stateObj) { + stateObj.ignore = true; + } + continue; + } + // Create labelLine if not exists + if (!labelLine) { + labelLine = new Polyline(); + targetEl.setTextGuideLine(labelLine); + // Reset state of normal because it's new created. + // NOTE: NORMAL should always been the first! + if (!isNormal && (labelIgnoreNormal || !showNormal)) { + setLabelLineState(labelLine, true, 'normal', statesModels.normal); + } + } + + setLabelLineState(labelLine, false, stateName, stateModel); + } + } + + if (labelLine) { + defaults(labelLine.style, defaultStyle); + // Not fill. + labelLine.style.fill = null; + + // Custom the buildPath. + labelLine.buildPath = buildLabelLinePath; + } +} diff --git a/src/label/labelLayoutHelper.ts b/src/label/labelLayoutHelper.ts new file mode 100644 index 0000000000..738b6b6da3 --- /dev/null +++ b/src/label/labelLayoutHelper.ts @@ -0,0 +1,352 @@ +/* +* 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 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 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); + + const minMargin = label.style.margin || 0; + 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: Pick[], + xyDim: 'x' | 'y', + sizeDim: 'width' | 'height', + minBound: number, + maxBound: number, + balanceShift: boolean +) { + const len = list.length; + + if (len < 2) { + return; + } + + list.sort(function (a, b) { + return a.rect[xyDim] - b.rect[xyDim]; + }); + + let lastPos = 0; + let delta; + let adjusted = false; + + const shifts = []; + let totalShifts = 0; + for (let i = 0; i < len; i++) { + const item = list[i]; + const rect = item.rect; + delta = rect[xyDim] - lastPos; + if (delta < 0) { + // shiftForward(i, len, -delta); + rect[xyDim] -= delta; + item.label[xyDim] -= delta; + adjusted = true; + } + const 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? + const first = list[0]; + const last = list[len - 1]; + let minGap: number; + let maxGap: number; + 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: number, gapOtherBound: number, moveDir: 1 | -1) { + if (gapThisBound < 0) { + // Move from other gap if can. + const moveFromMaxGap = Math.min(gapOtherBound, -gapThisBound); + if (moveFromMaxGap > 0) { + shiftList(moveFromMaxGap * moveDir, 0, len); + const remained = moveFromMaxGap + gapThisBound; + if (remained < 0) { + squeezeGaps(-remained * moveDir, 1); + } + } + else { + squeezeGaps(-gapThisBound * moveDir, 1); + } + } + } + + function shiftList(delta: number, start: number, end: number) { + if (delta !== 0) { + adjusted = true; + } + for (let i = start; i < end; i++) { + const item = list[i]; + const rect = item.rect; + rect[xyDim] += delta; + item.label[xyDim] += delta; + } + } + + // Squeeze gaps if the labels exceed margin. + function squeezeGaps(delta: number, maxSqeezePercent: number) { + const gaps: number[] = []; + let totalGaps = 0; + for (let i = 1; i < len; i++) { + const prevItemRect = list[i - 1].rect; + const gap = Math.max(list[i].rect[xyDim] - prevItemRect[xyDim] - prevItemRect[sizeDim], 0); + gaps.push(gap); + totalGaps += gap; + } + if (!totalGaps) { + return; + } + + const squeezePercent = Math.min(Math.abs(delta) / totalGaps, maxSqeezePercent); + + if (delta > 0) { + for (let i = 0; i < len - 1; i++) { + // Distribute the shift delta to all gaps. + const movement = gaps[i] * squeezePercent; + // Forward + shiftList(movement, 0, i + 1); + } + } + else { + // Backward + for (let i = len - 1; i > 0; i--) { + // Distribute the shift delta to all gaps. + const 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: number) { + const dir = delta < 0 ? -1 : 1; + delta = Math.abs(delta); + const moveForEachLabel = Math.ceil(delta / (len - 1)); + + for (let 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: Pick[], + leftBound: number, + rightBound: number, + // 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?: boolean +): boolean { + return shiftLayout(list, 'x', 'width', leftBound, rightBound, balanceShift); +} + +/** + * Adjust labels on y direction to avoid overlap. + */ +export function shiftLayoutOnY( + list: Pick[], + topBound: number, + bottomBound: number, + // If average the shifts on all labels and add them to 0 + balanceShift?: boolean +): boolean { + return shiftLayout(list, 'y', 'height', topBound, bottomBound, balanceShift); +} + +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; + }); + + const globalRect = new BoundingRect(0, 0, 0, 0); + + for (let i = 0; i < labelList.length; i++) { + const labelItem = labelList[i]; + const isAxisAligned = labelItem.axisAligned; + const localRect = labelItem.localRect; + const transform = labelItem.transform; + const label = labelItem.label; + const labelLine = labelItem.labelLine; + globalRect.copy(labelItem.rect); + // Add a threshold because layout may be aligned precisely. + globalRect.width -= 0.1; + globalRect.height -= 0.1; + globalRect.x += 0.05; + globalRect.y += 0.05; + + 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 diff --git a/src/model/Model.ts b/src/model/Model.ts index e743be4989..e9cf064e29 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -17,7 +17,6 @@ * under the License. */ -import * as zrUtil from 'zrender/src/core/util'; import env from 'zrender/src/core/env'; import { enableClassExtend, @@ -33,8 +32,7 @@ import {ItemStyleMixin} from './mixin/itemStyle'; import GlobalModel from './Global'; import { ModelOption } from '../util/types'; import { Dictionary } from 'zrender/src/core/types'; - -const mixin = zrUtil.mixin; +import { mixin, clone, merge, extend, isFunction } from 'zrender/src/core/util'; // Since model.option can be not only `Dictionary` but also primary types, // we do this conditional type to avoid getting type 'never'; @@ -56,20 +54,11 @@ class Model { // TODO: TYPE use unkown // var c = new C(); // console.log(c.xxx); // expect 5 but always 1. - /** - * @readOnly - */ parentModel: Model; - /** - * @readOnly - */ - ecModel: GlobalModel;; + ecModel: GlobalModel; - /** - * @readOnly - */ - option: Opt; + option: Opt; // TODO Opt should only be object. constructor(option?: Opt, parentModel?: Model, ecModel?: GlobalModel) { this.parentModel = parentModel; @@ -93,7 +82,7 @@ class Model { // TODO: TYPE use unkown * Merge the input option to me. */ mergeOption(option: Opt, ecModel?: GlobalModel): void { - zrUtil.merge(this.option, option, true); + merge(this.option, option, true); } // FIXME:TS consider there is parentModel, @@ -175,6 +164,47 @@ class Model { // TODO: TYPE use unkown return new Model(obj, parentModel, this.ecModel); } + /** + * Squash option stack into one. + * parentModel will be removed after squashed. + * + * NOTE: resolveParentPath will not be applied here for simplicity. DON'T use this function + * if resolveParentPath is modified. + * + * @param deepMerge If do deep merge. Default to be false. + */ + // squash( + // deepMerge?: boolean, + // handleCallback?: (func: () => object) => object + // ) { + // const optionStack = []; + // let model: Model = this; + // while (model) { + // if (model.option) { + // optionStack.push(model.option); + // } + // model = model.parentModel; + // } + + // const newOption = {} as Opt; + // let option; + // while (option = optionStack.pop()) { // Top down merge + // if (isFunction(option) && handleCallback) { + // option = handleCallback(option); + // } + // if (deepMerge) { + // merge(newOption, option); + // } + // else { + // extend(newOption, option); + // } + // } + + // // Remove parentModel + // this.option = newOption; + // this.parentModel = null; + // } + /** * If model has option */ @@ -187,7 +217,7 @@ class Model { // TODO: TYPE use unkown // Pending clone(): Model { const Ctor = this.constructor; - return new (Ctor as any)(zrUtil.clone(this.option)); + return new (Ctor as any)(clone(this.option)); } // setReadOnly(properties): void { @@ -211,7 +241,7 @@ class Model { // TODO: TYPE use unkown // FIXME:TS check whether put this method here isAnimationEnabled(): boolean { - if (!env.node) { + if (!env.node && this.option) { if (this.option.animation != null) { return !!this.option.animation; } diff --git a/src/model/Series.ts b/src/model/Series.ts index 905652b676..619a0c2fc4 100644 --- a/src/model/Series.ts +++ b/src/model/Series.ts @@ -538,10 +538,7 @@ class SeriesModel extends ComponentMode }; } - /** - * @return {boolean} - */ - isAnimationEnabled() { + isAnimationEnabled(): boolean { if (env.node) { return false; } @@ -551,7 +548,7 @@ class SeriesModel extends ComponentMode animationEnabled = false; } } - return animationEnabled; + return !!animationEnabled; } restoreData() { diff --git a/src/model/globalDefault.ts b/src/model/globalDefault.ts index ad2199bfeb..4625525a50 100644 --- a/src/model/globalDefault.ts +++ b/src/model/globalDefault.ts @@ -25,6 +25,8 @@ if (typeof navigator !== 'undefined') { } export default { + + darkMode: 'auto', // backgroundColor: 'rgba(0,0,0,0)', // https://dribbble.com/shots/1065960-Infographic-Pie-chart-visualization @@ -59,13 +61,19 @@ export default { // Default is source-over blendMode: null, + stateAnimation: { + duration: 300, + easing: 'cubicOut' + }, + animation: 'auto', animationDuration: 1000, - animationDurationUpdate: 300, - animationEasing: 'exponentialOut', - animationEasingUpdate: 'cubicOut', + animationDurationUpdate: 500, + animationEasing: 'cubicInOut', + animationEasingUpdate: 'cubicInOut', animationThreshold: 2000, + // Configuration for progressive/incremental rendering progressiveThreshold: 3000, progressive: 400, diff --git a/src/model/mixin/dataFormat.ts b/src/model/mixin/dataFormat.ts index 45d205b546..eacee2349c 100644 --- a/src/model/mixin/dataFormat.ts +++ b/src/model/mixin/dataFormat.ts @@ -18,10 +18,17 @@ */ import * as zrUtil from 'zrender/src/core/util'; -import Element from 'zrender/src/Element'; import {retrieveRawValue} from '../../data/helper/dataProvider'; import {formatTpl} from '../../util/format'; -import { DataHost, DisplayState, TooltipRenderMode, CallbackDataParams, ColorString, ZRColor, OptionDataValue, ParsedValue } from '../../util/types'; +import { + DataHost, + DisplayState, + TooltipRenderMode, + CallbackDataParams, + ColorString, + ZRColor, + OptionDataValue +} from '../../util/types'; import GlobalModel from '../Global'; const DIMENSION_LABEL_REG = /\{@(.+?)\}/g; @@ -44,8 +51,7 @@ class DataFormatMixin { */ getDataParams( dataIndex: number, - dataType?: string, - el?: Element, // May be used in override. + dataType?: string ): CallbackDataParams { const data = this.getData(dataType); @@ -88,43 +94,44 @@ class DataFormatMixin { * @param dataIndex * @param status 'normal' by default * @param dataType - * @param dimIndex Only used in some chart that + * @param labelDimIndex Only used in some chart that * use formatter in different dimensions, like radar. - * @param labelProp 'label' by default - * @return If not formatter, return null/undefined + * @param formatter Formatter given outside. + * @return return null/undefined if no formatter */ getFormattedLabel( dataIndex: number, status?: DisplayState, dataType?: string, - dimIndex?: number, - labelProp?: string, - // interpolateValues?: ParsedValue | ParsedValue[] + labelDimIndex?: number, + formatter?: string | ((params: object) => string), extendParams?: Partial ): string { status = status || 'normal'; const data = this.getData(dataType); - const itemModel = data.getItemModel(dataIndex); - const params = this.getDataParams(dataIndex, dataType, null); + const params = this.getDataParams(dataIndex, dataType); if (extendParams) { zrUtil.extend(params, extendParams); } - if (dimIndex != null && (params.value instanceof Array)) { - params.value = params.value[dimIndex]; + if (labelDimIndex != null && (params.value instanceof Array)) { + params.value = params.value[labelDimIndex]; } - // @ts-ignore FIXME:TooltipModel - const formatter = itemModel.get(status === 'normal' - ? [(labelProp || 'label'), 'formatter'] - : [status, labelProp || 'label', 'formatter'] - ); + if (!formatter) { + const itemModel = data.getItemModel(dataIndex); + // @ts-ignore + formatter = itemModel.get(status === 'normal' + ? ['label', 'formatter'] + : [status, 'label', 'formatter'] + ); + } if (typeof formatter === 'function') { params.status = status; - params.dimensionIndex = dimIndex; + params.dimensionIndex = labelDimIndex; return formatter(params); } else if (typeof formatter === 'string') { diff --git a/src/preprocessor/backwardCompat.ts b/src/preprocessor/backwardCompat.ts index 9de1bf67c4..f710dcc43e 100644 --- a/src/preprocessor/backwardCompat.ts +++ b/src/preprocessor/backwardCompat.ts @@ -19,11 +19,14 @@ // Compatitable with 2.0 -import {each, isArray, isObject} from 'zrender/src/core/util'; -import compatStyle from './helper/compatStyle'; +import {each, isArray, isObject, isTypedArray} from 'zrender/src/core/util'; +import compatStyle, {deprecateLog} from './helper/compatStyle'; import {normalizeToArray} from '../util/model'; import { Dictionary } from 'zrender/src/core/types'; import { ECUnitOption, SeriesOption } from '../util/types'; +import { __DEV__ } from '../config'; +import type { BarSeriesOption } from '../chart/bar/BarSeries'; +import { PieSeriesOption } from '../chart/pie/PieSeries'; function get(opt: Dictionary, path: string): any { const pathArr = path.split(','); @@ -70,6 +73,40 @@ const COMPATITABLE_COMPONENTS = [ 'grid', 'geo', 'parallel', 'legend', 'toolbox', 'title', 'visualMap', 'dataZoom', 'timeline' ]; +const BAR_ITEM_STYLE_MAP = [ + ['borderRadius', 'barBorderRadius'], + ['borderColor', 'barBorderColor'], + ['borderWidth', 'barBorderWidth'] +]; + +function compatBarItemStyle(option: Dictionary) { + const itemStyle = option && option.itemStyle; + if (itemStyle) { + for (let i = 0; i < BAR_ITEM_STYLE_MAP.length; i++) { + const oldName = BAR_ITEM_STYLE_MAP[i][1]; + const newName = BAR_ITEM_STYLE_MAP[i][0]; + if (itemStyle[oldName] != null) { + itemStyle[newName] = itemStyle[oldName]; + if (__DEV__) { + deprecateLog(`${oldName} has been changed to ${newName}.`); + } + } + } + } +} + +function compatPieLabel(option: Dictionary) { + if (!option) { + return; + } + if (option.alignTo === 'edge' && option.margin != null && option.edgeDistance == null) { + if (__DEV__) { + deprecateLog('label.margin has been changed to label.edgeDistance in pie.'); + } + option.edgeDistance = option.margin; + } +} + export default function (option: ECUnitOption, isTheme?: boolean) { compatStyle(option, isTheme); @@ -88,6 +125,7 @@ export default function (option: ECUnitOption, isTheme?: boolean) { if (seriesOpt.clipOverflow != null) { // @ts-ignore seriesOpt.clip = seriesOpt.clipOverflow; + deprecateLog('clipOverflow has been changed to clip.'); } } else if (seriesType === 'pie' || seriesType === 'gauge') { @@ -95,6 +133,14 @@ export default function (option: ECUnitOption, isTheme?: boolean) { if (seriesOpt.clockWise != null) { // @ts-ignore seriesOpt.clockwise = seriesOpt.clockWise; + deprecateLog('clockWise has been changed to clockwise.'); + } + compatPieLabel((seriesOpt as PieSeriesOption).label); + const data = seriesOpt.data; + if (data && !isTypedArray(data)) { + for (let i = 0; i < data.length; i++) { + compatPieLabel(data[i]); + } } } else if (seriesType === 'gauge') { @@ -102,6 +148,21 @@ export default function (option: ECUnitOption, isTheme?: boolean) { pointerColor != null && set(seriesOpt, 'itemStyle.color', pointerColor); } + else if (seriesType === 'bar') { + compatBarItemStyle(seriesOpt); + compatBarItemStyle((seriesOpt as BarSeriesOption).backgroundStyle); + // @ts-ignore + compatBarItemStyle(seriesOpt.emphasis); + const data = seriesOpt.data; + if (data && !isTypedArray(data)) { + for (let i = 0; i < data.length; i++) { + if (typeof data[i] === 'object') { + compatBarItemStyle(data[i]); + compatBarItemStyle(data[i] && data[i].emphasis); + } + } + } + } compatLayoutProperties(seriesOpt); }); diff --git a/src/preprocessor/helper/compatStyle.ts b/src/preprocessor/helper/compatStyle.ts index f991fe16fb..bbf8214773 100644 --- a/src/preprocessor/helper/compatStyle.ts +++ b/src/preprocessor/helper/compatStyle.ts @@ -31,7 +31,7 @@ const POSSIBLE_STYLES = [ ]; const storedLogs: Dictionary = {}; -function deprecateLog(str: string) { +export function deprecateLog(str: string) { if (storedLogs[str]) { // Not display duplicate message. return; } diff --git a/src/stream/Scheduler.ts b/src/stream/Scheduler.ts index b5759b4698..6bcaa1532c 100644 --- a/src/stream/Scheduler.ts +++ b/src/stream/Scheduler.ts @@ -106,7 +106,7 @@ class Scheduler { // Shared with echarts.js, should only be modified by // this file and echarts.js - unfinished: number; + unfinished: boolean; private _dataProcessorHandlers: StageHandlerInternal[]; private _visualHandlers: StageHandlerInternal[]; @@ -301,7 +301,7 @@ class Scheduler { opt?: PerformStageTaskOpt ): void { opt = opt || {}; - let unfinished: number; + let unfinished: boolean = false; const scheduler = this; each(stageHandlers, function (stageHandler, idx) { @@ -332,7 +332,9 @@ class Scheduler { agentStubMap.each(function (stub) { stub.perform(performArgs); }); - unfinished |= overallTask.perform(performArgs) as any; + if (overallTask.perform(performArgs)) { + unfinished = true; + } } else if (seriesTaskMap) { seriesTaskMap.each(function (task, pipelineId) { @@ -351,7 +353,10 @@ class Scheduler { performArgs.skip = !stageHandler.performRawSeries && ecModel.isSeriesFiltered(task.context.model); scheduler.updatePayload(task, payload); - unfinished |= task.perform(performArgs) as any; + + if (task.perform(performArgs)) { + unfinished = true; + } }); } }); @@ -360,18 +365,18 @@ class Scheduler { return opt.setDirty && (!opt.dirtyMap || opt.dirtyMap.get(task.__pipeline.id)); } - this.unfinished |= unfinished; + this.unfinished = unfinished || this.unfinished; } performSeriesTasks(ecModel: GlobalModel): void { - let unfinished: number; + let unfinished: boolean; ecModel.eachSeries(function (seriesModel) { // Progress to the end for dataInit and dataRestore. - unfinished |= seriesModel.dataTask.perform() as any; + unfinished = seriesModel.dataTask.perform() || unfinished; }); - this.unfinished |= unfinished; + this.unfinished = unfinished || this.unfinished; } plan(): void { diff --git a/src/theme/dark.ts b/src/theme/dark.ts index 02439bd45a..9e4cc186ed 100644 --- a/src/theme/dark.ts +++ b/src/theme/dark.ts @@ -54,6 +54,8 @@ const colorPalette = [ '#eedd78', '#73a373', '#73b9bc', '#7289ab', '#91ca8c', '#f49f42' ]; const theme = { + darkMode: true, + color: colorPalette, backgroundColor: '#333', tooltip: { diff --git a/src/util/graphic.ts b/src/util/graphic.ts index 98063682ea..36c11605fb 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -40,11 +40,13 @@ import CompoundPath from 'zrender/src/graphic/CompoundPath'; import LinearGradient from 'zrender/src/graphic/LinearGradient'; import RadialGradient from 'zrender/src/graphic/RadialGradient'; import BoundingRect from 'zrender/src/core/BoundingRect'; +import OrientedBoundingRect from 'zrender/src/core/OrientedBoundingRect'; +import Point from 'zrender/src/core/Point'; import IncrementalDisplayable from 'zrender/src/graphic/IncrementalDisplayable'; import * as subPixelOptimizeUtil from 'zrender/src/graphic/helper/subPixelOptimize'; import { Dictionary } from 'zrender/src/core/types'; import LRU from 'zrender/src/core/LRU'; -import Displayable, { DisplayableProps } from 'zrender/src/graphic/Displayable'; +import Displayable, { DisplayableProps, DisplayableState } from 'zrender/src/graphic/Displayable'; import { PatternObject } from 'zrender/src/graphic/Pattern'; import { GradientObject } from 'zrender/src/graphic/Gradient'; import Element, { ElementEvent, ElementTextConfig, ElementProps } from 'zrender/src/Element'; @@ -60,6 +62,7 @@ import { DataModel, ECEventData, ZRStyleProps, + AnimationOption, TextCommonOption, SeriesOption, ParsedValue, @@ -76,6 +79,7 @@ import { isArrayLike, map, defaults, + indexOf, isObject } from 'zrender/src/core/util'; import * as numberUtil from './number'; @@ -105,7 +109,6 @@ type ExtendShapeReturn = ReturnType; type ExtendedProps = { - __highlighted?: boolean | 'layer' | 'plain' __highByOuter: number __highDownSilentOnTouch: boolean @@ -122,12 +125,10 @@ type TextCommonParams = { */ disableBox?: boolean /** - * Specify a color when color is 'auto', - * for textFill, textStroke, textBackgroundColor, and textBorderColor. If autoColor specified, it is used as default textFill. + * Specify a color when color is 'inherit', + * If inheritColor specified, it is used as default textFill. */ - autoColor?: ColorString - - forceRich?: boolean + inheritColor?: ColorString defaultOutsidePosition?: LabelOption['position'] @@ -370,43 +371,21 @@ function liftColor(color: string): string { function singleEnterEmphasis(el: Element) { - (el as ExtendedElement).__highlighted = true; + (el as ECElement).highlighted = true; // el may be an array. if (!el.states.emphasis) { return; } - const disp = el as Displayable; - - const emphasisStyle = disp.states.emphasis.style; - const currentFill = disp.style && disp.style.fill; - const currentStroke = disp.style && disp.style.stroke; - - el.useState('emphasis'); - if (emphasisStyle && (currentFill || currentStroke)) { - if (!hasFillOrStroke(emphasisStyle.fill)) { - disp.style.fill = liftColor(currentFill); - } - if (!hasFillOrStroke(emphasisStyle.stroke)) { - disp.style.stroke = liftColor(currentStroke); - } - const z2EmphasisLift = (disp as ECElement).z2EmphasisLift; - disp.z2 += z2EmphasisLift != null ? z2EmphasisLift : Z2_EMPHASIS_LIFT; - } - - const textContent = el.getTextContent(); - if (textContent) { - const z2EmphasisLift = (textContent as ECElement).z2EmphasisLift; - textContent.z2 += z2EmphasisLift != null ? z2EmphasisLift : Z2_EMPHASIS_LIFT; - } + el.useState('emphasis', true); // TODO hover layer } -function singleEnterNormal(el: Element) { - el.clearStates(); - (el as ExtendedElement).__highlighted = false; +function singleLeaveEmphasis(el: Element) { + el.removeState('emphasis'); + (el as ECElement).highlighted = false; } function updateElementState( @@ -419,9 +398,9 @@ function updateElementState( let toState: DisplayState = NORMAL; let trigger; // See the rule of `onStateChange` on `graphic.setAsHighDownDispatcher`. - el.__highlighted && (fromState = EMPHASIS, trigger = true); + (el as ECElement).highlighted && (fromState = EMPHASIS, trigger = true); updater(el, commonParam); - el.__highlighted && (toState = EMPHASIS, trigger = true); + (el as ECElement).highlighted && (toState = EMPHASIS, trigger = true); trigger && el.__onStateChange && el.__onStateChange(fromState, toState); } @@ -453,7 +432,63 @@ export function clearStates(el: Element) { } } -/** +function elementStateProxy(this: Displayable, stateName: string): DisplayableState { + let state = this.states[stateName]; + if (stateName === 'emphasis' && this.style) { + const hasEmphasis = indexOf(this.currentStates, stateName) >= 0; + if (!(this instanceof ZRText)) { + const currentFill = this.style.fill; + const currentStroke = this.style.stroke; + if (currentFill || currentStroke) { + let fromState: {fill: ColorString, stroke: ColorString}; + if (!hasEmphasis) { + fromState = {fill: currentFill, stroke: currentStroke}; + for (let i = 0; i < this.animators.length; i++) { + const animator = this.animators[i]; + if (animator.__fromStateTransition + // Dont consider the animation to emphasis state. + && animator.__fromStateTransition.indexOf('emphasis') < 0 + && animator.targetName === 'style' + ) { + animator.saveFinalToTarget(fromState, ['fill', 'stroke']); + } + } + } + + state = state || {}; + // Apply default color lift + let emphasisStyle = state.style || {}; + let cloned = false; + if (!hasFillOrStroke(emphasisStyle.fill)) { + cloned = true; + // Not modify the original value. + state = extend({}, state); + emphasisStyle = extend({}, emphasisStyle); + // Already being applied 'emphasis'. DON'T lift color multiple times. + emphasisStyle.fill = hasEmphasis ? currentFill : liftColor(fromState.fill); + } + if (!hasFillOrStroke(emphasisStyle.stroke)) { + if (!cloned) { + state = extend({}, state); + emphasisStyle = extend({}, emphasisStyle); + } + emphasisStyle.stroke = hasEmphasis ? currentStroke : liftColor(fromState.stroke); + } + + state.style = emphasisStyle; + } + } + if (state) { + const z2EmphasisLift = (this as ECElement).z2EmphasisLift; + // TODO Share with textContent? + state.z2 = this.z2 + (z2EmphasisLift != null ? z2EmphasisLift : Z2_EMPHASIS_LIFT); + } + } + + return state; +} + +/**FI * Set hover style (namely "emphasis style") of element. * @param el Should not be `zrender/graphic/Group`. */ @@ -463,15 +498,14 @@ export function enableElementHoverEmphasis(el: Displayable, hoverStl?: ZRStylePr emphasisState.style = hoverStl; } - // FIXME - // It is not completely right to save "normal"/"emphasis" flag on elements. - // It probably should be saved on `data` of series. Consider the cases: - // (1) A highlighted elements are moved out of the view port and re-enter - // again by dataZoom. - // (2) call `setOption` and replace elements totally when they are highlighted. - if ((el as ExtendedDisplayable).__highlighted) { - singleEnterNormal(el); - singleEnterEmphasis(el); + el.stateProxy = elementStateProxy; + const textContent = el.getTextContent(); + const textGuide = el.getTextGuideLine(); + if (textContent) { + textContent.stateProxy = elementStateProxy; + } + if (textGuide) { + textGuide.stateProxy = elementStateProxy; } } @@ -486,7 +520,7 @@ export function leaveEmphasisWhenMouseOut(el: Element, e: ElementEvent) { !shouldSilent(el, e) // "emphasis" event highlight has higher priority than mouse highlight. && !(el as ExtendedElement).__highByOuter - && traverseUpdateState((el as ExtendedElement), singleEnterNormal); + && traverseUpdateState((el as ExtendedElement), singleLeaveEmphasis); } export function enterEmphasis(el: Element, highlightDigit?: number) { @@ -496,7 +530,7 @@ export function enterEmphasis(el: Element, highlightDigit?: number) { export function leaveEmphasis(el: Element, highlightDigit?: number) { !((el as ExtendedElement).__highByOuter &= ~(1 << (highlightDigit || 0))) - && traverseUpdateState((el as ExtendedElement), singleEnterNormal); + && traverseUpdateState((el as ExtendedElement), singleLeaveEmphasis); } function shouldSilent(el: Element, e: ElementEvent) { @@ -523,6 +557,23 @@ export function enableHoverEmphasis(el: Element, hoverStyle?: ZRStyleProps) { traverseUpdateState(el as ExtendedElement, enableElementHoverEmphasis, hoverStyle); } +/** + * Set animation config on state transition. + */ +export function setStateTransition(el: Element, animatableModel: Model) { + const duration = animatableModel.get('duration'); + if (duration > 0) { + el.stateTransition = { + duration, + delay: animatableModel.get('delay'), + easing: animatableModel.get('easing') + }; + } + else if (el.stateTransition) { + el.stateTransition = null; + } +} + /** * @param {module:zrender/Element} el * @param {Function} [el.onStateChange] Called when state updated. @@ -611,40 +662,67 @@ interface SetLabelStyleOpt extends TextCommonParams { // opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex, opt.labelProp // ) labelFetcher?: { - getFormattedLabel?: ( + getFormattedLabel: ( // In MapDraw case it can be string (region name) labelDataIndex: LDI, - state: DisplayState, - dataType: string, - labelDimIndex: number, - labelProp: string, + status: DisplayState, + dataType?: string, + labelDimIndex?: number, + formatter?: string | ((params: object) => string), extendParams?: Partial ) => string + // getDataParams: (labelDataIndex: LDI, dataType?: string) => object }, labelDataIndex?: LDI, labelDimIndex?: number - labelProp?: string } -function getLabelText(opt?: SetLabelStyleOpt, interpolateValues?: ParsedValue | ParsedValue[]) { +type LabelModel = Model string) +}>; +type LabelModelForText = Model & { + formatter?: string | ((params: any) => string) +}>; + +function getLabelText( + opt: SetLabelStyleOpt, + normalModel: LabelModel, + emphasisModel: LabelModel, + interpolateValues?: ParsedValue | ParsedValue[] +) { const labelFetcher = opt.labelFetcher; const labelDataIndex = opt.labelDataIndex; const labelDimIndex = opt.labelDimIndex; - const labelProp = opt.labelProp; let baseText; if (labelFetcher) { - baseText = labelFetcher.getFormattedLabel(labelDataIndex, 'normal', null, labelDimIndex, labelProp, { - value: interpolateValues - }); + baseText = labelFetcher.getFormattedLabel( + labelDataIndex, + 'normal', + null, + labelDimIndex, + normalModel && normalModel.get('formatter'), + interpolateValues != null ? { + value: interpolateValues + } : null + ); } if (baseText == null) { baseText = isFunction(opt.defaultText) ? opt.defaultText(labelDataIndex, opt) : opt.defaultText; } const emphasisStyleText = retrieve2( labelFetcher - ? labelFetcher.getFormattedLabel(labelDataIndex, 'emphasis', null, labelDimIndex, labelProp) + ? labelFetcher.getFormattedLabel( + labelDataIndex, + 'emphasis', + null, + labelDimIndex, + emphasisModel && emphasisModel.get('formatter') + ) : null, baseText ); @@ -661,12 +739,16 @@ function getLabelText(opt?: SetLabelStyleOpt, interpolateValues?: Pars * And create a new style object. * * NOTICE: Because the style on ZRText will be replaced with new(only x, y are keeped). - * So please use the style on ZRText after use this method. + * So please update the style on ZRText after use this method. */ -export function setLabelStyle( +// eslint-disable-next-line +function setLabelStyle(targetEl: ZRText, normalModel: LabelModelForText, emphasisModel: LabelModelForText, opt?: SetLabelStyleOpt, normalSpecified?: TextStyleProps, emphasisSpecified?: TextStyleProps): void; +// eslint-disable-next-line +function setLabelStyle(targetEl: Element, normalModel: LabelModel, emphasisModel: LabelModel, opt?: SetLabelStyleOpt, normalSpecified?: TextStyleProps, emphasisSpecified?: TextStyleProps): void; +function setLabelStyle( targetEl: Element, - normalModel: Model, - emphasisModel: Model, + normalModel: LabelModel, + emphasisModel: LabelModel, opt?: SetLabelStyleOpt, normalSpecified?: TextStyleProps, emphasisSpecified?: TextStyleProps @@ -678,9 +760,6 @@ export function setLabelStyle( const showNormal = normalModel.getShallow('show'); const showEmphasis = emphasisModel.getShallow('show'); - // Consider performance, only fetch label when necessary. - // If `normal.show` is `false` and `emphasis.show` is `true` and `emphasis.formatter` is not set, - // label should be displayed, where text is fetched by `normal.formatter` or `opt.defaultText`. let richText = isSetOnText ? targetEl as ZRText : null; if (showNormal || showEmphasis) { if (!isSetOnText) { @@ -696,12 +775,6 @@ export function setLabelStyle( const emphasisState = richText.ensureState('emphasis'); emphasisState.ignore = !showEmphasis; - // Always set `textStyle` even if `normalStyle.text` is null, because default - // values have to be set on `normalStyle`. - // If we set default values on `emphasisStyle`, consider case: - // Firstly, `setOption(... label: {normal: {text: null}, emphasis: {show: true}} ...);` - // Secondly, `setOption(... label: {noraml: {show: true, text: 'abc', color: 'red'} ...);` - // Then the 'red' will not work on emphasis. const normalStyle = createTextStyle( normalModel, normalSpecified, @@ -739,7 +812,7 @@ export function setLabelStyle( // auto slient is those cases. richText.silent = !!normalModel.getShallow('silent'); - const labelText = getLabelText(opt); + const labelText = getLabelText(opt, normalModel, emphasisModel); normalStyle.text = labelText.normal; emphasisState.style.text = labelText.emphasis; @@ -764,18 +837,19 @@ export function setLabelStyle( targetEl.dirty(); } +export {setLabelStyle}; /** * Set basic textStyle properties. */ export function createTextStyle( textStyleModel: Model, specifiedTextStyle?: TextStyleProps, // Can be overrided by settings in model. - opt?: TextCommonParams, - isEmphasis?: boolean, + opt?: Pick, + isNotNormal?: boolean, isAttached?: boolean // If text is attached on an element. If so, auto color will handling in zrender. ) { const textStyle: TextStyleProps = {}; - setTextStyleCommon(textStyle, textStyleModel, opt, isEmphasis, isAttached); + setTextStyleCommon(textStyle, textStyleModel, opt, isNotNormal, isAttached); specifiedTextStyle && extend(textStyle, specifiedTextStyle); // textStyle.host && textStyle.host.dirty && textStyle.host.dirty(false); @@ -785,20 +859,20 @@ export function createTextStyle( export function createTextConfig( textStyle: TextStyleProps, textStyleModel: Model, - opt: TextCommonParams, - isEmphasis: boolean + opt?: Pick, + isNotNormal?: boolean ) { opt = opt || {}; const textConfig: ElementTextConfig = {}; let labelPosition; let labelRotate = textStyleModel.getShallow('rotate'); const labelDistance = retrieve2( - textStyleModel.getShallow('distance'), isEmphasis ? null : 5 + textStyleModel.getShallow('distance'), isNotNormal ? null : 5 ); const labelOffset = textStyleModel.getShallow('offset'); labelPosition = textStyleModel.getShallow('position') - || (isEmphasis ? null : 'inside'); + || (isNotNormal ? null : 'inside'); // 'outside' is not a valid zr textPostion value, but used // in bar series, and magric type should be considered. labelPosition === 'outside' && (labelPosition = opt.defaultOutsidePosition || 'top'); @@ -818,19 +892,10 @@ export function createTextConfig( } // fill and auto is determined by the color of path fill if it's not specified by developers. - textConfig.outsideFill = opt.autoColor || null; - // if (!textStyle.fill) { - // textConfig.insideFill = 'auto'; - // textConfig.outsideFill = opt.autoColor || null; - // } - // if (!textStyle.stroke) { - // textConfig.insideStroke = 'auto'; - // } - // else if (opt.autoColor) { - // // TODO: stroke set to autoColor. if label is inside? - // textConfig.insideStroke = opt.autoColor; - // } + textConfig.outsideFill = textStyleModel.get('color') === 'inherit' + ? (opt.inheritColor || null) + : 'auto'; return textConfig; } @@ -848,8 +913,8 @@ export function createTextConfig( function setTextStyleCommon( textStyle: TextStyleProps, textStyleModel: Model, - opt?: TextCommonParams, - isEmphasis?: boolean, + opt?: Pick, + isNotNormal?: boolean, isAttached?: boolean ) { // Consider there will be abnormal when merge hover style to normal style if given default value. @@ -885,7 +950,7 @@ function setTextStyleCommon( // the default color `'blue'` will not be adopted if no color declared in `rich`. // That might confuses users. So probably we should put `textStyleModel` as the // root ancestor of the `richTextStyle`. But that would be a break change. - setTokenTextStyle(richResult[name] = {}, richTextStyle, globalTextStyle, opt, isEmphasis, isAttached); + setTokenTextStyle(richResult[name] = {}, richTextStyle, globalTextStyle, opt, isNotNormal, isAttached); } } } @@ -897,13 +962,12 @@ function setTextStyleCommon( if (overflow) { textStyle.overflow = overflow; } - - setTokenTextStyle(textStyle, textStyleModel, globalTextStyle, opt, isEmphasis, isAttached, true); - - // TODO - if (opt.forceRich && !opt.textStyle) { - opt.textStyle = {}; + const margin = textStyleModel.get('minMargin'); + if (margin != null) { + textStyle.margin = margin; } + + setTokenTextStyle(textStyle, textStyleModel, globalTextStyle, opt, isNotNormal, isAttached, true); } // Consider case: @@ -940,7 +1004,7 @@ function getRichItemNames(textStyleModel: Model) { } const TEXT_PROPS_WITH_GLOBAL = [ - 'fontStyle', 'fontWeight', 'fontSize', 'fontFamily', + 'fontStyle', 'fontWeight', 'fontSize', 'fontFamily', 'opacity', 'textShadowColor', 'textShadowBlur', 'textShadowOffsetX', 'textShadowOffsetY' ] as const; @@ -958,22 +1022,32 @@ function setTokenTextStyle( textStyle: TextStyleProps['rich'][string], textStyleModel: Model, globalTextStyle: LabelOption, - opt?: TextCommonParams, - isEmphasis?: boolean, + opt?: Pick, + isNotNormal?: boolean, isAttached?: boolean, isBlock?: boolean ) { // In merge mode, default value should not be given. - globalTextStyle = !isEmphasis && globalTextStyle || EMPTY_OBJ; + globalTextStyle = !isNotNormal && globalTextStyle || EMPTY_OBJ; - const autoColor = opt && opt.autoColor; + const inheritColor = opt && opt.inheritColor; let fillColor = textStyleModel.getShallow('color'); let strokeColor = textStyleModel.getShallow('textBorderColor'); - if (fillColor === 'auto' && autoColor) { - fillColor = autoColor; + if (fillColor === 'inherit') { + if (inheritColor) { + fillColor = inheritColor; + } + else { + fillColor = null; + } } - if (strokeColor === 'auto' && autoColor) { - strokeColor = autoColor; + if (strokeColor === 'inherit' && inheritColor) { + if (inheritColor) { + strokeColor = inheritColor; + } + else { + strokeColor = inheritColor; + } } fillColor = fillColor || globalTextStyle.color; strokeColor = strokeColor || globalTextStyle.textBorderColor; @@ -993,10 +1067,10 @@ function setTokenTextStyle( } // TODO - if (!isEmphasis && !isAttached) { + if (!isNotNormal && !isAttached) { // Set default finally. - if (textStyle.fill == null && opt.autoColor) { - textStyle.fill = opt.autoColor; + if (textStyle.fill == null && opt.inheritColor) { + textStyle.fill = opt.inheritColor; } } @@ -1028,11 +1102,11 @@ function setTokenTextStyle( if (!isBlock || !opt.disableBox) { - if (textStyle.backgroundColor === 'auto' && autoColor) { - textStyle.backgroundColor = autoColor; + if (textStyle.backgroundColor === 'auto' && inheritColor) { + textStyle.backgroundColor = inheritColor; } - if (textStyle.borderColor === 'auto' && autoColor) { - textStyle.borderColor = autoColor; + if (textStyle.borderColor === 'auto' && inheritColor) { + textStyle.borderColor = inheritColor; } for (let i = 0; i < TEXT_PROPS_BOX.length; i++) { @@ -1067,7 +1141,7 @@ type AnimateOrSetPropsOption = { }; function animateOrSetProps( - isUpdate: boolean, + animationType: 'init' | 'update' | 'remove', el: Element, props: Props, animatableModel?: Model & { @@ -1089,19 +1163,22 @@ function animateOrSetProps( isFrom = dataIndex.isFrom; dataIndex = dataIndex.dataIndex; } + const isUpdate = animationType === 'update'; + const isRemove = animationType === 'remove'; // Do not check 'animation' property directly here. Consider this case: // animation model is an `itemModel`, whose does not have `isAnimationEnabled` // but its parent model (`seriesModel`) does. const animationEnabled = animatableModel && animatableModel.isAnimationEnabled(); if (animationEnabled) { - let duration = animatableModel.getShallow( + // TODO Configurable + let duration = isRemove ? 200 : animatableModel.getShallow( isUpdate ? 'animationDurationUpdate' : 'animationDuration' ); - const animationEasing = animatableModel.getShallow( + const animationEasing = isRemove ? 'cubicOut' : animatableModel.getShallow( isUpdate ? 'animationEasingUpdate' : 'animationEasing' ); - let animationDelay = animatableModel.getShallow( + let animationDelay = isRemove ? 0 : animatableModel.getShallow( isUpdate ? 'animationDelayUpdate' : 'animationDelay' ); if (typeof animationDelay === 'function') { @@ -1116,6 +1193,11 @@ function animateOrSetProps( duration = duration(dataIndex as number); } + if (!isRemove) { + // Must stop the remove animation. + el.stopAnimation('remove'); + } + duration > 0 ? ( isFrom @@ -1125,6 +1207,7 @@ function animateOrSetProps( easing: animationEasing, done: cb, force: !!cb || !!during, + scope: animationType, during: during }) : el.animateTo(props, { @@ -1133,6 +1216,8 @@ function animateOrSetProps( easing: animationEasing, done: cb, force: !!cb || !!during, + setToFinal: true, + scope: animationType, during: during }) ) @@ -1170,7 +1255,7 @@ function updateProps( cb?: AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption['during'], during?: AnimateOrSetPropsOption['during'] ) { - animateOrSetProps(true, el, props, animatableModel, dataIndex, cb, during); + animateOrSetProps('update', el, props, animatableModel, dataIndex, cb, during); } export {updateProps}; @@ -1191,11 +1276,25 @@ export function initProps( cb?: AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption['during'], during?: AnimateOrSetPropsOption['during'] ) { - animateOrSetProps(false, el, props, animatableModel, dataIndex, cb, during); + animateOrSetProps('init', el, props, animatableModel, dataIndex, cb, during); +} + +/** + * Remove graphic element + */ +export function removeElement( + el: Element, + props: Props, + animatableModel?: Model, + dataIndex?: AnimateOrSetPropsOption['dataIndex'] | AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption, + cb?: AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption['during'], + during?: AnimateOrSetPropsOption['during'] +) { + animateOrSetProps('remove', el, props, animatableModel, dataIndex, cb, during); } function animateOrSetLabel( - isUpdate: boolean, + animationType: 'init' | 'update' | 'remove', el: Element, data: List, dataIndex: number, @@ -1208,7 +1307,7 @@ function animateOrSetLabel( const valueAnimationEnabled = labelModel && labelModel.get('valueAnimation'); if (valueAnimationEnabled) { const precisionOption = labelModel.get('precision'); - let precision: number = precisionOption === 'auto' ? 0 : precisionOption; + const precision: number = precisionOption === 'auto' ? 0 : precisionOption; let interpolateValues: (number | string)[] | (number | string); const rawValues = seriesModel.getRawValue(dataIndex); @@ -1256,14 +1355,14 @@ function animateOrSetLabel( defaultText: defaultTextGetter ? defaultTextGetter(interpolated) : interpolated + '' - }, interpolated); + }, labelModel, null, interpolated); text.style.text = labelText.normal; text.dirty(); } }; const props: ElementProps = {}; - animateOrSetProps(isUpdate, el, props, animatableModel, dataIndex, null, during); + animateOrSetProps(animationType, el, props, animatableModel, dataIndex, null, during); } } @@ -1276,7 +1375,7 @@ export function updateLabel( animatableModel?: Model, defaultTextGetter?: (value: ParsedValue[] | ParsedValue) => string ) { - animateOrSetLabel(true, el, data, dataIndex, labelModel, seriesModel, animatableModel, defaultTextGetter); + animateOrSetLabel('update', el, data, dataIndex, labelModel, seriesModel, animatableModel, defaultTextGetter); } export function initLabel( @@ -1288,7 +1387,7 @@ export function initLabel( animatableModel?: Model, defaultTextGetter?: (value: ParsedValue[] | ParsedValue) => string ) { - animateOrSetLabel(false, el, data, dataIndex, labelModel, seriesModel, animatableModel, defaultTextGetter); + animateOrSetLabel('init', el, data, dataIndex, labelModel, seriesModel, animatableModel, defaultTextGetter); } /** @@ -1595,5 +1694,7 @@ export { LinearGradient, RadialGradient, BoundingRect, + OrientedBoundingRect, + Point, Path }; \ No newline at end of file diff --git a/src/util/layout.ts b/src/util/layout.ts index c18e990500..31e0fac062 100644 --- a/src/util/layout.ts +++ b/src/util/layout.ts @@ -116,6 +116,7 @@ function boxLayout( child.x = x; child.y = y; + child.markRedraw(); orient === 'horizontal' ? (x = nextX + gap) diff --git a/src/util/types.ts b/src/util/types.ts index 50b7f7ce72..bfd2006b00 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -106,6 +106,9 @@ export interface ECElement extends Element { }; highDownSilentOnTouch?: boolean; onStateChange?: (fromState: 'normal' | 'emphasis', toState: 'normal' | 'emphasis') => void; + + highlighted?: boolean; + selected?: boolean; z2EmphasisLift?: number; } @@ -552,6 +555,11 @@ export type AnimationDelayCallbackParam = { export type AnimationDurationCallback = (idx: number) => number; export type AnimationDelayCallback = (idx: number, params?: AnimationDelayCallbackParam) => number; +export interface AnimationOption { + duration?: number + easing?: AnimationEasing + delay?: number +} /** * Mixin of option set to control the animation of series. */ @@ -766,6 +774,12 @@ export interface LabelOption extends TextCommonOption { rotate?: number offset?: number[] + /** + * Min margin between labels. Used when label has layout. + */ + // It's minMargin instead of margin is for not breaking the previous code using margin. + minMargin?: number + overflow?: TextStyleProps['overflow'] silent?: boolean precision?: number | 'auto' @@ -801,14 +815,77 @@ export interface LineLabelOption extends Omit LabelLayoutOption; + + interface TooltipFormatterCallback { /** * For sync callback @@ -882,7 +959,7 @@ export interface CommonTooltipOption { * * Support to be a callback */ - position?: number[] | string[] | TooltipBuiltinPosition | PositionCallback | TooltipBoxLayoutOption + position?: (number | string)[] | TooltipBuiltinPosition | PositionCallback | TooltipBoxLayoutOption confine?: boolean @@ -1086,6 +1163,18 @@ export interface SeriesOption extends * @default 'column' */ seriesLayoutBy?: 'column' | 'row' + + labelLine?: LabelLineOption + + /** + * Overall label layout option in label layout stage. + */ + labelLayout?: LabelLayoutOption | LabelLayoutOptionCallback + + /** + * Animation config for state transition. + */ + stateAnimation?: AnimationOption } export interface SeriesOnCartesianOptionMixin { diff --git a/src/view/Chart.ts b/src/view/Chart.ts index 2b5e3d7144..529ed9486e 100644 --- a/src/view/Chart.ts +++ b/src/view/Chart.ts @@ -114,6 +114,12 @@ class ChartView { readonly renderTask: SeriesTask; + /** + * Ignore label line update in global stage. Will handle it in chart itself. + * Used in pie / funnel + */ + ignoreLabelLineUpdate: boolean; + // ---------------------- // Injectable properties // ---------------------- diff --git a/src/visual/style.ts b/src/visual/style.ts index 605edeea59..1d52fcf605 100644 --- a/src/visual/style.ts +++ b/src/visual/style.ts @@ -187,7 +187,7 @@ const dataColorPaletteTask: StageHandler = { idxMap[rawIdx] = idx; }); - // Iterate on dat before filtered. To make sure color from palette can be + // Iterate on data before filtered. To make sure color from palette can be // Consistent when toggling legend. dataAll.each(function (rawIdx) { const idx = idxMap[rawIdx]; diff --git a/test/animation-additive.html b/test/animation-additive.html new file mode 100644 index 0000000000..ad62f73a72 --- /dev/null +++ b/test/animation-additive.html @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + +
+
+ + + \ No newline at end of file diff --git a/test/aria-pie.html b/test/aria-pie.html index c324210401..c62310cee0 100644 --- a/test/aria-pie.html +++ b/test/aria-pie.html @@ -98,6 +98,7 @@ type: 'pie', radius : '55%', center: ['50%', '60%'], + selectedMode: 'single', data:[ {value:335, name:'直接访问'}, {value:310, name:'邮件营销'}, diff --git a/test/bar-stack.html b/test/bar-stack.html index 1176c30f91..2d339bccb5 100644 --- a/test/bar-stack.html +++ b/test/bar-stack.html @@ -46,33 +46,6 @@ ], function (echarts) { var option = { - "tooltip": { - "trigger": "axis", - "axisPointer": { - "type": "shadow" - } - }, - "toolbox": { - "show": true, - "feature": { - "dataZoom": { - "yAxisIndex": "none" - }, - "dataView": { - "readOnly": false - }, - "magicType": { - "type": [ - "line", - "bar", - "stack", - "tiled" - ] - }, - "restore": {}, - "saveAsImage": {} - } - }, "xAxis": { type: 'category' }, @@ -99,14 +72,14 @@ ], barMinHeight: 10, label: { - normal: {show: true} + show: true }, "name": "zly13" }, { "type": "bar", "stack": "all", label: { - normal: {show: true} + show: true }, "data": [ ["哪有那么多审批", 66], diff --git a/test/graph-label-rotate.html b/test/graph-label-rotate.html index dec5d010ba..5b89368daf 100644 --- a/test/graph-label-rotate.html +++ b/test/graph-label-rotate.html @@ -63,13 +63,13 @@ roam: true, label: { show: true, - rotate: 30, - fontWeight:5, - fontSize: 26, - color: "#000", - distance: 15, - position: 'inside', - verticalAlign: 'middle' + rotate: 30, + fontWeight:5, + fontSize: 26, + color: "#000", + distance: 15, + position: 'inside', + verticalAlign: 'middle' }, edgeSymbol: ['circle', 'arrow'], edgeSymbolSize: [4, 10], diff --git a/test/label-layout.html b/test/label-layout.html new file mode 100644 index 0000000000..621916f6d4 --- /dev/null +++ b/test/label-layout.html @@ -0,0 +1,695 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/pie-alignTo.html b/test/pie-alignTo.html index 969fde27b0..78c0e274f0 100644 --- a/test/pie-alignTo.html +++ b/test/pie-alignTo.html @@ -68,12 +68,12 @@ type: 'pie', radius: '50%', data: data, - animation: false, + labelLine: { length2: 15 }, label: { - margin: 20, + edgeDistance: 20, position: 'outer' } }] @@ -84,12 +84,12 @@ type: 'pie', radius: '50%', data: data, - animation: false, + labelLine: { length2: 15 }, label: { - margin: 20, + edgeDistance: 20, position: 'outer', alignTo: 'labelLine' } @@ -101,12 +101,12 @@ type: 'pie', radius: '50%', data: data, - animation: false, + labelLine: { length2: 15 }, label: { - margin: 20, + edgeDistance: 20, position: 'outer', alignTo: 'edge' } @@ -119,7 +119,7 @@ radius: '25%', center: ['50%', '50%'], data: data, - animation: false, + labelLine: { length2: 15 }, @@ -136,12 +136,12 @@ radius: '25%', center: ['50%', '50%'], data: data, - animation: false, + labelLine: { length2: 15 }, label: { - margin: 20, + edgeDistance: 20, position: 'outer', alignTo: 'labelLine' }, @@ -154,7 +154,7 @@ radius: '25%', center: ['50%', '50%'], data: data, - animation: false, + labelLine: { length2: 15 }, @@ -171,12 +171,12 @@ radius: '25%', center: ['50%', '50%'], data: data, - animation: false, + labelLine: { length2: 15 }, label: { - margin: 20, + edgeDistance: 20, position: 'outer', alignTo: 'labelLine' }, @@ -211,48 +211,46 @@ }); var config = { length2: 15, - margin: 20 + edgeDistance: 20, + overflow: 'truncate' }; - gui - .add(config, 'length2', 0, 300) - .onChange(function (value) { - if (chart0) { - option0.series[0].labelLine.length2 = value; - option1.series[0].labelLine.length2 = value; - optionNone.series[0].labelLine.length2 = value; - chart0.setOption(option0); - chart1.setOption(option1); - chartNone.setOption(optionNone); - - for (var i = 0; i < 4; ++i) { - option2.series[i].labelLine.length2 = value; + function update() { + const newOpt = { + series: [{ + labelLine: { + length2: config.length2 + }, + label: { + edgeDistance: config.edgeDistance, + overflow: config.overflow } - chart2.setOption(option2); - } - }); + }] + } + chart0.setOption(newOpt); + chart1.setOption(newOpt); + chartNone.setOption(newOpt); - gui - .add(config, 'margin', 0, 300) - .onChange(function (value) { - if (chart0) { - option0.series[0].label.margin = value; - option1.series[0].label.margin = value; - optionNone.series[0].label.margin = value; - chart0.setOption(option0); - chart1.setOption(option1); - chartNone.setOption(optionNone); - - for (var i = 0; i < 4; ++i) { - option2.series[i].label.margin = value; + const newOpt2 = { series: [] }; + for (var i = 0; i < 4; ++i) { + newOpt2.series.push({ + labelLine: { + length2: config.length2, + }, + label: { + edgeDistance: config.edgeDistance, + overflow: config.overflow } - chart2.setOption(option2); - } - }); - }); - - + }) + } + chart2.setOption(newOpt2); + } + gui.add(config, 'length2', 0, 300).onChange(update); + gui.add(config, 'edgeDistance', 0, 300).onChange(update); + gui.add(config, 'overflow', ['truncate', 'break', 'breakAll']).onChange(update); + }); + diff --git a/test/pie-label-extreme.html b/test/pie-label-extreme.html new file mode 100644 index 0000000000..7ce7d45ca8 --- /dev/null +++ b/test/pie-label-extreme.html @@ -0,0 +1,728 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + diff --git a/test/pie-label-mobile.html b/test/pie-label-mobile.html new file mode 100644 index 0000000000..aef18c6b80 --- /dev/null +++ b/test/pie-label-mobile.html @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + diff --git a/test/pie-label.html b/test/pie-label.html index aa03b66c8f..69acccf3d4 100644 --- a/test/pie-label.html +++ b/test/pie-label.html @@ -45,6 +45,9 @@
+
+
+
@@ -446,5 +455,247 @@ + + + + + \ No newline at end of file diff --git a/test/sunburst-canvas.html b/test/sunburst-canvas.html index c4d5dbc032..8636924289 100644 --- a/test/sunburst-canvas.html +++ b/test/sunburst-canvas.html @@ -229,8 +229,6 @@ data: data, label: { rotate: 0, - textBorderColor: '#444', - textBorderWidth: 2, fontSize: 14 }, levels: [{}, { diff --git a/test/sunburst-drink.html b/test/sunburst-drink.html index e86cbf76f3..b6b49e66a8 100644 --- a/test/sunburst-drink.html +++ b/test/sunburst-drink.html @@ -713,6 +713,9 @@ data: data, radius: [0, '95%'], sort: null, + label: { + color: 'inherit' + }, levels: [{}, { r0: '15%', r: '35%',