From 580972089a8d73394247c1f82fe7c6c6056d849f Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 22 Apr 2020 22:52:16 +0800 Subject: [PATCH 01/82] feat: add label manager for each series to layout label. --- src/ExtensionAPI.ts | 45 ++--- src/chart/funnel/FunnelView.ts | 10 +- src/chart/graph/GraphView.ts | 2 + src/chart/sunburst/SunburstPiece.ts | 4 +- src/chart/sunburst/SunburstSeries.ts | 5 +- src/chart/tree/TreeView.ts | 2 + src/echarts.ts | 32 ++- src/model/Model.ts | 64 ++++-- src/model/mixin/dataFormat.ts | 30 +-- src/stream/Scheduler.ts | 16 +- src/util/LabelManager.ts | 286 +++++++++++++++++++++++++++ src/util/graphic.ts | 107 ++++++++-- src/util/types.ts | 49 ++++- test/animation-additive.html | 162 +++++++++++++++ test/bar-stack.html | 31 +-- test/graph-label-rotate.html | 14 +- test/label-overlap.html | 207 +++++++++++++++++++ 17 files changed, 936 insertions(+), 130 deletions(-) create mode 100644 src/util/LabelManager.ts create mode 100644 test/animation-additive.html create mode 100644 test/label-overlap.html 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/funnel/FunnelView.ts b/src/chart/funnel/FunnelView.ts index 47e12091c8..2cb92970cb 100644 --- a/src/chart/funnel/FunnelView.ts +++ b/src/chart/funnel/FunnelView.ts @@ -23,7 +23,8 @@ 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'; const opacityAccessPath = ['itemStyle', 'opacity'] as const; @@ -109,7 +110,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, @@ -151,10 +153,6 @@ 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'); diff --git a/src/chart/graph/GraphView.ts b/src/chart/graph/GraphView.ts index 7d604a360a..8f83896053 100644 --- a/src/chart/graph/GraphView.ts +++ b/src/chart/graph/GraphView.ts @@ -437,6 +437,8 @@ class GraphView extends ChartView { this._updateNodeAndLinkScale(); adjustEdge(seriesModel.getGraph(), getNodeGlobalScale(seriesModel)); this._lineDraw.updateLayout(); + // Only update label layout on zoom + api.updateLabelLayout(); }); } diff --git a/src/chart/sunburst/SunburstPiece.ts b/src/chart/sunburst/SunburstPiece.ts index 86bf38abc6..9fbb3fd2d5 100644 --- a/src/chart/sunburst/SunburstPiece.ts +++ b/src/chart/sunburst/SunburstPiece.ts @@ -218,9 +218,7 @@ class SunburstPiece extends graphic.Group { const labelHoverModel = itemModel.getModel(['emphasis', 'label']); let text = zrUtil.retrieve( - seriesModel.getFormattedLabel( - this.node.dataIndex, state, null, null, 'label' - ), + seriesModel.getFormattedLabel(this.node.dataIndex, state), this.node.name ); if (getLabelAttr('show') === false) { diff --git a/src/chart/sunburst/SunburstSeries.ts b/src/chart/sunburst/SunburstSeries.ts index be7b077a9f..327a1ab766 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 { diff --git a/src/chart/tree/TreeView.ts b/src/chart/tree/TreeView.ts index 2d16448b2c..1484889261 100644 --- a/src/chart/tree/TreeView.ts +++ b/src/chart/tree/TreeView.ts @@ -351,6 +351,8 @@ class TreeView extends ChartView { originY: e.originY }); this._updateNodeAndLinkScale(seriesModel); + // Only update label layout on zoom + api.updateLabelLayout(); }); } diff --git a/src/echarts.ts b/src/echarts.ts index 9711a772f4..ecaa352343 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 './util/LabelManager'; declare let global: any; type ModelFinder = modelUtil.ModelFinder; @@ -223,6 +224,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; @@ -287,6 +290,8 @@ class ECharts extends Eventful { this._messageCenter = new MessageCenter(); + this._labelManager = new LabelManager(); + // Init mouse events this._initEvents(); @@ -331,7 +336,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(); @@ -1050,6 +1055,12 @@ class ECharts extends Eventful { triggerUpdatedEvent.call(this, silent); } + updateLabelLayout() { + const labelManager = this._labelManager; + labelManager.updateLayoutConfig(this._api); + labelManager.layout(); + } + appendData(params: { seriesIndex: number, data: any @@ -1077,7 +1088,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; } @@ -1637,7 +1648,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; ecModel.eachSeries(function (seriesModel) { const chartView = ecIns._chartsMap[seriesModel.__viewId]; chartView.__alive = true; @@ -1649,7 +1664,7 @@ class ECharts extends Eventful { renderTask.dirty(); } - unfinished |= +renderTask.perform(scheduler.getPerformArgs(renderTask)); + unfinished = renderTask.perform(scheduler.getPerformArgs(renderTask)) || unfinished; chartView.group.silent = !!seriesModel.get('silent'); @@ -1658,8 +1673,15 @@ class ECharts extends Eventful { updateBlend(seriesModel, chartView); updateHoverEmphasisHandler(chartView); + + // Add albels. + labelManager.addLabelsOfSeries(chartView); }); - scheduler.unfinished |= unfinished; + + scheduler.unfinished = unfinished || scheduler.unfinished; + + labelManager.updateLayoutConfig(api); + labelManager.layout(); // If use hover layer updateHoverLayerStatus(ecIns, ecModel); diff --git a/src/model/Model.ts b/src/model/Model.ts index bd3e28292f..5660029342 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'; @@ -52,20 +50,11 @@ class Model { // TODO: TYPE use unkown // subclass overrided filed will be overwritten by this // class. That is, they should not be initialized here. - /** - * @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; @@ -89,7 +78,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, @@ -168,6 +157,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 */ @@ -180,7 +210,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 { @@ -204,7 +234,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/mixin/dataFormat.ts b/src/model/mixin/dataFormat.ts index eca285f0e6..5d1279526a 100644 --- a/src/model/mixin/dataFormat.ts +++ b/src/model/mixin/dataFormat.ts @@ -86,36 +86,38 @@ 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 + labelDimIndex?: number, + formatter?: string | ((params: object) => string) ): string { status = status || 'normal'; const data = this.getData(dataType); - const itemModel = data.getItemModel(dataIndex); const params = this.getDataParams(dataIndex, dataType); - 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/stream/Scheduler.ts b/src/stream/Scheduler.ts index b5759b4698..ff156b7d00 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; const scheduler = this; each(stageHandlers, function (stageHandler, idx) { @@ -332,7 +332,7 @@ class Scheduler { agentStubMap.each(function (stub) { stub.perform(performArgs); }); - unfinished |= overallTask.perform(performArgs) as any; + unfinished = unfinished || overallTask.perform(performArgs); } else if (seriesTaskMap) { seriesTaskMap.each(function (task, pipelineId) { @@ -351,7 +351,7 @@ class Scheduler { performArgs.skip = !stageHandler.performRawSeries && ecModel.isSeriesFiltered(task.context.model); scheduler.updatePayload(task, payload); - unfinished |= task.perform(performArgs) as any; + unfinished = unfinished || task.perform(performArgs); }); } }); @@ -360,18 +360,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/util/LabelManager.ts b/src/util/LabelManager.ts new file mode 100644 index 0000000000..b8b90552c8 --- /dev/null +++ b/src/util/LabelManager.ts @@ -0,0 +1,286 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import { OrientedBoundingRect, Text as ZRText, Point, BoundingRect, getECData } from './graphic'; +import { MatrixArray } from 'zrender/src/core/matrix'; +import ExtensionAPI from '../ExtensionAPI'; +import { + ZRTextAlign, + ZRTextVerticalAlign, + LabelLayoutOption, + LabelLayoutOptionCallback, + LabelLayoutOptionCallbackParams +} from './types'; +import { parsePercent } from './number'; +import SeriesModel from '../model/Series'; +import ChartView from '../view/Chart'; + +interface DisplayedLabelItem { + label: ZRText + rect: BoundingRect + localRect: BoundingRect + obb?: OrientedBoundingRect + axisAligned: boolean + transform: MatrixArray +} + +interface LabelItem { + label: ZRText + seriesIndex: number + dataIndex: number + layoutOption: LabelLayoutOptionCallback | LabelLayoutOption +} + +interface LabelLayoutInnerConfig { + overlap: LabelLayoutOption['overlap'] + overlapMargin: LabelLayoutOption['overlapMargin'] +} + +interface SavedLabelAttr { + x?: number + y?: number + rotation?: number + align?: ZRTextAlign + verticalAlign?: ZRTextVerticalAlign + width?: number + height?: number +} + +function prepareLayoutCallbackParams( + label: ZRText, + dataIndex: number, + seriesIndex: number +): LabelLayoutOptionCallbackParams { + const host = label.__hostTarget; + const labelTransform = label.getComputedTransform(); + const labelRect = label.getBoundingRect().plain(); + BoundingRect.applyTransform(labelRect, labelRect, labelTransform); + let x = 0; + let y = 0; + if (labelTransform) { + x = labelTransform[4]; + y = labelTransform[5]; + } + + let hostRect; + if (host) { + hostRect = host.getBoundingRect().plain(); + const transform = host.getComputedTransform(); + BoundingRect.applyTransform(hostRect, hostRect, transform); + } + + return { + dataIndex, + seriesIndex, + text: label.style.text, + rect: hostRect, + labelRect: labelRect, + x, y, + align: label.style.align, + verticalAlign: label.style.verticalAlign + }; +} + +const LABEL_OPTION_TO_STYLE_KEYS = ['align', 'verticalAlign', 'width', 'height'] as const; + +class LabelManager { + + private _labelList: LabelItem[] = []; + private _labelLayoutConfig: LabelLayoutInnerConfig[] = []; + + // Save default label attributes. + // For restore if developers want get back to default value in callback. + private _defaultLabelAttr: SavedLabelAttr[] = []; + + constructor() {} + + clearLabels() { + this._labelList = []; + this._labelLayoutConfig = []; + } + + /** + * Add label to manager + * @param dataIndex + * @param seriesIndex + * @param label + * @param layoutOption + */ + addLabel(dataIndex: number, seriesIndex: number, label: ZRText, layoutOption: LabelItem['layoutOption']) { + this._labelList.push({ + seriesIndex, + dataIndex, + label, + layoutOption + }); + // Push an empty config. Will be updated in updateLayoutConfig + this._labelLayoutConfig.push({} as LabelLayoutInnerConfig); + + const labelStyle = label.style; + this._defaultLabelAttr.push({ + x: label.x, + y: label.y, + rotation: label.rotation, + align: labelStyle.align, + verticalAlign: labelStyle.verticalAlign, + width: labelStyle.width, + height: labelStyle.height + }); + } + + addLabelsOfSeries(chartView: ChartView) { + const seriesModel = chartView.__model; + const layoutOption = seriesModel.get('labelLayout'); + 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 dataIndex = getECData(child).dataIndex; + if (textEl && dataIndex != null) { + this.addLabel(dataIndex, seriesModel.seriesIndex, textEl, layoutOption); + } + }); + } + + updateLayoutConfig(api: ExtensionAPI) { + const width = api.getWidth(); + const height = api.getHeight(); + for (let i = 0; i < this._labelList.length; i++) { + const labelItem = this._labelList[i]; + const label = labelItem.label; + const hostEl = label.__hostTarget; + const layoutConfig = this._labelLayoutConfig[i]; + const defaultLabelAttr = this._defaultLabelAttr[i]; + let layoutOption; + if (typeof labelItem.layoutOption === 'function') { + layoutOption = labelItem.layoutOption( + prepareLayoutCallbackParams(label, labelItem.dataIndex, labelItem.seriesIndex) + ); + } + else { + layoutOption = labelItem.layoutOption; + } + + layoutOption = layoutOption || {}; + // if (hostEl) { + // // Ignore position and rotation config on the host el. + // hostEl.setTextConfig({ + // position: null, + // rotation: null + // }); + // } + // label.x = layoutOption.x != null + // ? parsePercent(layoutOption.x, width) + // // Restore to default value if developers don't given a value. + // : defaultLabelAttr.x; + + // label.y = layoutOption.y != null + // ? parsePercent(layoutOption.y, height) + // : defaultLabelAttr.y; + + // label.rotation = layoutOption.rotation != null + // ? layoutOption.rotation : defaultLabelAttr.rotation; + + // label.x += layoutOption.dx || 0; + // label.y += layoutOption.dy || 0; + + // 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[key]); + // } + + layoutConfig.overlap = layoutOption.overlap; + layoutConfig.overlapMargin = layoutOption.overlapMargin; + } + } + + layout() { + // TODO: sort by priority + const labelList = this._labelList; + + const displayedLabels: DisplayedLabelItem[] = []; + const mvt = new Point(); + + for (let i = 0; i < labelList.length; i++) { + const labelItem = labelList[i]; + const layoutConfig = this._labelLayoutConfig[i]; + const label = labelItem.label; + const transform = label.getComputedTransform(); + // NOTE: Get bounding rect after getComputedTransform, or label may not been updated by the host el. + const localRect = label.getBoundingRect(); + const isAxisAligned = !transform || (transform[1] < 1e-5 && transform[2] < 1e-5); + + const globalRect = localRect.clone(); + globalRect.applyTransform(transform); + + let obb = isAxisAligned ? new OrientedBoundingRect(localRect, transform) : null; + let overlapped = false; + const overlapMargin = layoutConfig.overlapMargin || 0; + const marginSqr = overlapMargin * overlapMargin; + for (let j = 0; j < displayedLabels.length; j++) { + const existsTextCfg = displayedLabels[j]; + // Fast rejection. + if (!globalRect.intersect(existsTextCfg.rect, mvt) && mvt.lenSquare() > marginSqr) { + continue; + } + + if (isAxisAligned && existsTextCfg.axisAligned) { // Is overlapped + overlapped = true; + break; + } + + if (!existsTextCfg.obb) { // If self is not axis aligned. But other is. + existsTextCfg.obb = new OrientedBoundingRect(existsTextCfg.localRect, existsTextCfg.transform); + } + + if (!obb) { // If self is axis aligned. But other is not. + obb = new OrientedBoundingRect(localRect, transform); + } + + if (obb.intersect(existsTextCfg.obb, mvt) || mvt.lenSquare() < marginSqr) { + overlapped = true; + break; + } + } + + if (overlapped) { + label.hide(); + } + else { + label.show(); + displayedLabels.push({ + label, + rect: globalRect, + localRect, + obb, + axisAligned: isAxisAligned, + transform + }); + } + } + } +} + + + + +export default LabelManager; \ No newline at end of file diff --git a/src/util/graphic.ts b/src/util/graphic.ts index 9a35173106..8020892c68 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -39,6 +39,8 @@ 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'; @@ -605,19 +607,61 @@ interface SetLabelStyleOpt extends TextCommonParams { ), // Fetch text by `opt.labelFetcher.getFormattedLabel(opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex)` labelFetcher?: { - getFormattedLabel?: ( + getFormattedLabel: ( // In MapDraw case it can be string (region name) labelDataIndex: LDI, - state: DisplayState, - dataType: string, - labelDimIndex: number + status: DisplayState, + dataType?: string, + labelDimIndex?: number, + formatter?: string | ((params: object) => string) ) => string + // getDataParams: (labelDataIndex: LDI, dataType?: string) => object }, labelDataIndex?: LDI, labelDimIndex?: number } +// function handleSquashCallback( +// func: Function, +// labelDataIndex: LDI, +// labelFetcher: SetLabelStyleOpt['labelFetcher'], +// rect: RectLike, +// status: DisplayState +// ) { +// let params: { +// status?: DisplayState +// rect?: RectLike +// }; +// if (labelFetcher && labelFetcher.getDataParams) { +// params = labelFetcher.getDataParams(labelDataIndex); +// } +// else { +// params = {}; +// } +// params.status = status; +// params.rect = rect; +// return func(params); +// } + +// function getGlobalBoundingRect(el: Element) { +// const rect = el.getBoundingRect().clone(); +// const transform = el.getComputedTransform(); +// if (transform) { +// rect.applyTransform(transform); +// } +// return rect; +// } + +type LabelModel = Model string) +}>; +type LabelModelForText = Model & { + formatter?: string | ((params: any) => string) +}>; /** * Set normal styles and emphasis styles about text on target element * If target is a ZRText. It will create a new style object. @@ -627,10 +671,14 @@ interface SetLabelStyleOpt extends TextCommonParams { * 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. */ -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 @@ -639,6 +687,31 @@ export function setLabelStyle( opt = opt || EMPTY_OBJ; const isSetOnText = targetEl instanceof ZRText; + const labelFetcher = opt.labelFetcher; + const labelDataIndex = opt.labelDataIndex; + const labelDimIndex = opt.labelDimIndex; + + // TODO Performance optimization + // normalModel.squash(false, function (func: Function) { + // return handleSquashCallback( + // func, + // labelDataIndex, + // labelFetcher, + // isSetOnText ? null : getGlobalBoundingRect(targetEl), + // 'normal' + // ); + // }); + + // emphasisModel.squash(false, function (func: Function) { + // return handleSquashCallback( + // func, + // labelDataIndex, + // labelFetcher, + // isSetOnText ? null : getGlobalBoundingRect(targetEl), + // 'emphasis' + // ); + // }); + const showNormal = normalModel.getShallow('show'); const showEmphasis = emphasisModel.getShallow('show'); @@ -647,13 +720,12 @@ export function setLabelStyle( // label should be displayed, where text is fetched by `normal.formatter` or `opt.defaultText`. let richText = isSetOnText ? targetEl as ZRText : null; if (showNormal || showEmphasis) { - const labelFetcher = opt.labelFetcher; - const labelDataIndex = opt.labelDataIndex; - const labelDimIndex = opt.labelDimIndex; - let baseText; if (labelFetcher) { - baseText = labelFetcher.getFormattedLabel(labelDataIndex, 'normal', null, labelDimIndex); + baseText = labelFetcher.getFormattedLabel( + labelDataIndex, 'normal', null, labelDimIndex, + normalModel.get('formatter') + ); } if (baseText == null) { baseText = isFunction(opt.defaultText) ? opt.defaultText(labelDataIndex, opt) : opt.defaultText; @@ -661,7 +733,10 @@ export function setLabelStyle( const normalStyleText = baseText; const emphasisStyleText = retrieve2( labelFetcher - ? labelFetcher.getFormattedLabel(labelDataIndex, 'emphasis', null, labelDimIndex) + ? labelFetcher.getFormattedLabel( + labelDataIndex, 'emphasis', null, labelDimIndex, + emphasisModel.get('formatter') + ) : null, baseText ); @@ -738,6 +813,7 @@ export function setLabelStyle( targetEl.dirty(); } +export {setLabelStyle}; /** * Set basic textStyle properties. */ @@ -802,7 +878,7 @@ export function createTextConfig( } if (!textStyle.stroke) { textConfig.insideStroke = 'auto'; - // textConfig.outsideStroke = 'auto'; + textConfig.outsideStroke = 'auto'; } else if (opt.autoColor) { // TODO: stroke set to autoColor. if label is inside? @@ -1080,6 +1156,7 @@ function animateOrSetProps( delay: animationDelay || 0, easing: animationEasing, done: cb, + setToFinal: true, force: !!cb }) : (el.stopAnimation(), el.attr(props), cb && cb()); @@ -1440,5 +1517,7 @@ export { LinearGradient, RadialGradient, BoundingRect, + OrientedBoundingRect, + Point, Path }; \ No newline at end of file diff --git a/src/util/types.ts b/src/util/types.ts index 0503ec892c..47e0277ac1 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -798,6 +798,48 @@ export interface LabelGuideLineOption { lineStyle?: LineStyleOption } + +export interface LabelLayoutOptionCallbackParams { + dataIndex: number, + seriesIndex: number, + text: string + align: ZRTextAlign + verticalAlign: ZRTextVerticalAlign + rect: RectLike + labelRect: RectLike + x: number + y: number +}; + +export interface LabelLayoutOption { + overlap?: 'visible' | 'hidden' | 'blur' + /** + * Minimal margin between two labels which will be considered as overlapped. + */ + overlapMargin?: number + /** + * Can be absolute px number or percent string. + */ + x?: number | string + y?: number | string + /** + * offset on x based on the original position. + */ + dx?: number + /** + * offset on y based on the original position. + */ + dy?: number + rotation?: number + align?: ZRTextAlign + verticalAlign?: ZRTextVerticalAlign + width?: number + height?: number +} + +export type LabelLayoutOptionCallback = (params: LabelLayoutOptionCallbackParams) => LabelLayoutOption; + + interface TooltipFormatterCallback { /** * For sync callback @@ -871,7 +913,7 @@ export interface CommonTooltipOption { * * Support to be a callback */ - position?: number[] | string[] | TooltipBuiltinPosition | PositionCallback | TooltipBoxLayoutOption + position?: (number | string)[] | TooltipBuiltinPosition | PositionCallback | TooltipBoxLayoutOption confine?: boolean @@ -1075,6 +1117,11 @@ export interface SeriesOption extends * @default 'column' */ seriesLayoutBy?: 'column' | 'row' + + /** + * Global label layout option in label layout stage. + */ + labelLayout?: LabelLayoutOption | LabelLayoutOptionCallback } export interface SeriesOnCartesianOptionMixin { 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/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-overlap.html b/test/label-overlap.html new file mode 100644 index 0000000000..75de7c86b3 --- /dev/null +++ b/test/label-overlap.html @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ + + + + + + + + + + + From 2b8da727e2edd9e61d65c3029cb13d17c9cecf8e Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 23 Apr 2020 17:00:17 +0800 Subject: [PATCH 02/82] feat: improve label layouting. displayed by priority(default to be size of area). --- src/util/LabelManager.ts | 225 ++++++++++++++++++++++----------------- 1 file changed, 128 insertions(+), 97 deletions(-) diff --git a/src/util/LabelManager.ts b/src/util/LabelManager.ts index b8b90552c8..0cdf2f30c5 100644 --- a/src/util/LabelManager.ts +++ b/src/util/LabelManager.ts @@ -28,8 +28,10 @@ import { LabelLayoutOptionCallbackParams } from './types'; import { parsePercent } from './number'; -import SeriesModel from '../model/Series'; import ChartView from '../view/Chart'; +import { ElementTextConfig } from 'zrender/src/Element'; +import { RectLike } from 'zrender/src/core/BoundingRect'; +import Transformable from 'zrender/src/core/Transformable'; interface DisplayedLabelItem { label: ZRText @@ -40,58 +42,49 @@ interface DisplayedLabelItem { transform: MatrixArray } -interface LabelItem { +interface LabelLayoutDesc { label: ZRText seriesIndex: number dataIndex: number layoutOption: LabelLayoutOptionCallback | LabelLayoutOption -} -interface LabelLayoutInnerConfig { overlap: LabelLayoutOption['overlap'] overlapMargin: LabelLayoutOption['overlapMargin'] + + hostRect: RectLike + priority: number + + defaultAttr: SavedLabelAttr } interface SavedLabelAttr { - x?: number - y?: number - rotation?: number - align?: ZRTextAlign - verticalAlign?: ZRTextVerticalAlign - width?: number - height?: number -} + x: number + y: number + rotation: number -function prepareLayoutCallbackParams( - label: ZRText, - dataIndex: number, - seriesIndex: number -): LabelLayoutOptionCallbackParams { - const host = label.__hostTarget; - const labelTransform = label.getComputedTransform(); - const labelRect = label.getBoundingRect().plain(); - BoundingRect.applyTransform(labelRect, labelRect, labelTransform); - let x = 0; - let y = 0; - if (labelTransform) { - x = labelTransform[4]; - y = labelTransform[5]; - } + align: ZRTextAlign + verticalAlign: ZRTextVerticalAlign + width: number + height: number - let hostRect; - if (host) { - hostRect = host.getBoundingRect().plain(); - const transform = host.getComputedTransform(); - BoundingRect.applyTransform(hostRect, hostRect, transform); - } + // Configuration in attached element + attachedPos: ElementTextConfig['position'] + attachedRot: ElementTextConfig['rotation'] + rect: RectLike +} + +function prepareLayoutCallbackParams(labelItem: LabelLayoutDesc): LabelLayoutOptionCallbackParams { + const labelAttr = labelItem.defaultAttr; + const label = labelItem.label; return { - dataIndex, - seriesIndex, - text: label.style.text, - rect: hostRect, - labelRect: labelRect, - x, y, + dataIndex: labelItem.dataIndex, + seriesIndex: labelItem.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 }; @@ -99,20 +92,16 @@ function prepareLayoutCallbackParams( const LABEL_OPTION_TO_STYLE_KEYS = ['align', 'verticalAlign', 'width', 'height'] as const; -class LabelManager { +const dummyTransformable = new Transformable(); - private _labelList: LabelItem[] = []; - private _labelLayoutConfig: LabelLayoutInnerConfig[] = []; +class LabelManager { - // Save default label attributes. - // For restore if developers want get back to default value in callback. - private _defaultLabelAttr: SavedLabelAttr[] = []; + private _labelList: LabelLayoutDesc[] = []; constructor() {} clearLabels() { this._labelList = []; - this._labelLayoutConfig = []; } /** @@ -122,26 +111,59 @@ class LabelManager { * @param label * @param layoutOption */ - addLabel(dataIndex: number, seriesIndex: number, label: ZRText, layoutOption: LabelItem['layoutOption']) { + addLabel(dataIndex: number, seriesIndex: number, label: ZRText, layoutOption: LabelLayoutDesc['layoutOption']) { + const labelStyle = label.style; + const hostEl = label.__hostTarget; + const textConfig = hostEl.textConfig || {}; + + const labelTransform = label.getComputedTransform(); + const labelRect = label.getBoundingRect().plain(); + BoundingRect.applyTransform(labelRect, labelRect, labelTransform); + + dummyTransformable.setLocalTransform(labelTransform); + + const host = label.__hostTarget; + let hostRect; + if (host) { + hostRect = host.getBoundingRect().plain(); + const transform = host.getComputedTransform(); + BoundingRect.applyTransform(hostRect, hostRect, transform); + } + this._labelList.push({ seriesIndex, dataIndex, label, - layoutOption - }); - // Push an empty config. Will be updated in updateLayoutConfig - this._labelLayoutConfig.push({} as LabelLayoutInnerConfig); + layoutOption, - const labelStyle = label.style; - this._defaultLabelAttr.push({ - x: label.x, - y: label.y, - rotation: label.rotation, - align: labelStyle.align, - verticalAlign: labelStyle.verticalAlign, - width: labelStyle.width, - height: labelStyle.height + hostRect, + + overlap: 'hidden', + overlapMargin: 0, + + // 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: { + x: dummyTransformable.x, + y: dummyTransformable.y, + rotation: dummyTransformable.rotation, + + rect: labelRect, + + align: labelStyle.align, + verticalAlign: labelStyle.verticalAlign, + width: labelStyle.width, + height: labelStyle.height, + + attachedPos: textConfig.position, + attachedRot: textConfig.rotation + } }); + } addLabelsOfSeries(chartView: ChartView) { @@ -168,12 +190,11 @@ class LabelManager { const labelItem = this._labelList[i]; const label = labelItem.label; const hostEl = label.__hostTarget; - const layoutConfig = this._labelLayoutConfig[i]; - const defaultLabelAttr = this._defaultLabelAttr[i]; + const defaultLabelAttr = labelItem.defaultAttr; let layoutOption; if (typeof labelItem.layoutOption === 'function') { layoutOption = labelItem.layoutOption( - prepareLayoutCallbackParams(label, labelItem.dataIndex, labelItem.seriesIndex) + prepareLayoutCallbackParams(labelItem) ); } else { @@ -181,48 +202,51 @@ class LabelManager { } layoutOption = layoutOption || {}; - // if (hostEl) { - // // Ignore position and rotation config on the host el. - // hostEl.setTextConfig({ - // position: null, - // rotation: null - // }); - // } - // label.x = layoutOption.x != null - // ? parsePercent(layoutOption.x, width) - // // Restore to default value if developers don't given a value. - // : defaultLabelAttr.x; - - // label.y = layoutOption.y != null - // ? parsePercent(layoutOption.y, height) - // : defaultLabelAttr.y; - - // label.rotation = layoutOption.rotation != null - // ? layoutOption.rotation : defaultLabelAttr.rotation; - - // label.x += layoutOption.dx || 0; - // label.y += layoutOption.dy || 0; - - // 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[key]); - // } - - layoutConfig.overlap = layoutOption.overlap; - layoutConfig.overlapMargin = layoutOption.overlapMargin; + if (hostEl) { + hostEl.setTextConfig({ + // 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.rotation != null ? null : defaultLabelAttr.attachedRot, + offset: [layoutOption.dx || 0, layoutOption.dy || 0] + }); + } + label.x = layoutOption.x != null + ? parsePercent(layoutOption.x, width) + // Restore to default value if developers don't given a value. + : defaultLabelAttr.x; + + label.y = layoutOption.y != null + ? parsePercent(layoutOption.y, height) + : defaultLabelAttr.y; + + label.rotation = layoutOption.rotation != null + ? layoutOption.rotation : 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[key]); + } + + labelItem.overlap = layoutOption.overlap; + labelItem.overlapMargin = layoutOption.overlapMargin; } } layout() { - // TODO: sort by priority + // TODO: sort by priority(area) const labelList = this._labelList; const displayedLabels: DisplayedLabelItem[] = []; const mvt = new Point(); + labelList.sort(function (a, b) { + return b.priority - a.priority; + }); + for (let i = 0; i < labelList.length; i++) { const labelItem = labelList[i]; - const layoutConfig = this._labelLayoutConfig[i]; const label = labelItem.label; const transform = label.getComputedTransform(); // NOTE: Get bounding rect after getComputedTransform, or label may not been updated by the host el. @@ -234,7 +258,7 @@ class LabelManager { let obb = isAxisAligned ? new OrientedBoundingRect(localRect, transform) : null; let overlapped = false; - const overlapMargin = layoutConfig.overlapMargin || 0; + const overlapMargin = labelItem.overlapMargin || 0; const marginSqr = overlapMargin * overlapMargin; for (let j = 0; j < displayedLabels.length; j++) { const existsTextCfg = displayedLabels[j]; @@ -262,11 +286,18 @@ class LabelManager { } } + // TODO Callback to determine if this overlap should be handled? if (overlapped) { - label.hide(); + // label.setStyle({ opacity: 0.1 }); + // label.z = 0; + // Use invisible instead of ignore because ignored label won't be updated in the host. + label.attr('invisible', true); } else { - label.show(); + // TODO Restore z + // label.setStyle({ opacity: 1 }); + label.attr('invisible', false); + displayedLabels.push({ label, rect: globalRect, From 9718d12b5097aa9a28b262dd93af63b406becce1 Mon Sep 17 00:00:00 2001 From: pissang Date: Fri, 24 Apr 2020 14:26:05 +0800 Subject: [PATCH 03/82] fix(label): respect the original ignore value when handling overlap --- src/util/LabelManager.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/util/LabelManager.ts b/src/util/LabelManager.ts index 0cdf2f30c5..a91dacac44 100644 --- a/src/util/LabelManager.ts +++ b/src/util/LabelManager.ts @@ -58,6 +58,8 @@ interface LabelLayoutDesc { } interface SavedLabelAttr { + ignore: boolean + x: number y: number rotation: number @@ -111,7 +113,12 @@ class LabelManager { * @param label * @param layoutOption */ - addLabel(dataIndex: number, seriesIndex: number, label: ZRText, layoutOption: LabelLayoutDesc['layoutOption']) { + addLabel( + dataIndex: number, + seriesIndex: number, + label: ZRText, + layoutOption: LabelLayoutDesc['layoutOption'] + ) { const labelStyle = label.style; const hostEl = label.__hostTarget; const textConfig = hostEl.textConfig || {}; @@ -148,6 +155,8 @@ class LabelManager { // Save default label attributes. // For restore if developers want get back to default value in callback. defaultAttr: { + ignore: label.ignore, + x: dummyTransformable.x, y: dummyTransformable.y, rotation: dummyTransformable.rotation, @@ -290,13 +299,12 @@ class LabelManager { if (overlapped) { // label.setStyle({ opacity: 0.1 }); // label.z = 0; - // Use invisible instead of ignore because ignored label won't be updated in the host. - label.attr('invisible', true); + label.hide(); } else { // TODO Restore z // label.setStyle({ opacity: 1 }); - label.attr('invisible', false); + label.attr('ignore', labelItem.defaultAttr.ignore); displayedLabels.push({ label, From db8297bf131fb7dc808e756865ede394e703f2e2 Mon Sep 17 00:00:00 2001 From: pissang Date: Fri, 24 Apr 2020 21:42:39 +0800 Subject: [PATCH 04/82] feat(label): ignore style.x/y when x/y is given in labelLayout --- src/util/LabelManager.ts | 66 ++++++--- src/util/types.ts | 4 +- .../{label-overlap.html => label-layout.html} | 137 ++++++++++++++++-- 3 files changed, 178 insertions(+), 29 deletions(-) rename test/{label-overlap.html => label-layout.html} (59%) diff --git a/src/util/LabelManager.ts b/src/util/LabelManager.ts index a91dacac44..23e09b7ed6 100644 --- a/src/util/LabelManager.ts +++ b/src/util/LabelManager.ts @@ -64,10 +64,15 @@ interface SavedLabelAttr { y: number rotation: number - align: ZRTextAlign - verticalAlign: ZRTextVerticalAlign - width: number - height: number + style: { + align: ZRTextAlign + verticalAlign: ZRTextVerticalAlign + width: number + height: number + + x: number + y: number + } // Configuration in attached element attachedPos: ElementTextConfig['position'] @@ -85,8 +90,8 @@ function prepareLayoutCallbackParams(labelItem: LabelLayoutDesc): LabelLayoutOpt text: labelItem.label.style.text, rect: labelItem.hostRect, labelRect: labelAttr.rect, - x: labelAttr.x, - y: labelAttr.y, + // x: labelAttr.x, + // y: labelAttr.y, align: label.style.align, verticalAlign: label.style.verticalAlign }; @@ -163,10 +168,15 @@ class LabelManager { rect: labelRect, - align: labelStyle.align, - verticalAlign: labelStyle.verticalAlign, - width: labelStyle.width, - height: labelStyle.height, + 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 @@ -201,6 +211,7 @@ class LabelManager { 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) @@ -221,21 +232,35 @@ class LabelManager { offset: [layoutOption.dx || 0, layoutOption.dy || 0] }); } - label.x = layoutOption.x != null - ? parsePercent(layoutOption.x, width) - // Restore to default value if developers don't given a value. - : defaultLabelAttr.x; + if (layoutOption.x != null) { + // TODO width of chart view. + label.x = parsePercent(layoutOption.x, width); + label.setStyle('x', 0); // Ignore movement in style. + } + else { + label.x = defaultLabelAttr.x; + label.setStyle('x', defaultLabelAttr.style.x); + } - label.y = layoutOption.y != null - ? parsePercent(layoutOption.y, height) - : defaultLabelAttr.y; + if (layoutOption.y != null) { + // TODO height of chart view. + label.y = parsePercent(layoutOption.y, height); + label.setStyle('y', 0); // Ignore movement in style. + } + else { + label.y = defaultLabelAttr.y; + label.setStyle('y', defaultLabelAttr.style.y); + } label.rotation = layoutOption.rotation != null ? layoutOption.rotation : 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[key]); + label.setStyle( + key, + layoutOption[key] != null ? layoutOption[key] : defaultLabelAttr.style[key] + ); } labelItem.overlap = layoutOption.overlap; @@ -250,12 +275,17 @@ class LabelManager { const displayedLabels: DisplayedLabelItem[] = []; const mvt = new Point(); + // TODO, render overflow visible first, put in the displayedLabels. labelList.sort(function (a, b) { return b.priority - a.priority; }); for (let i = 0; i < labelList.length; i++) { const labelItem = labelList[i]; + if (labelItem.defaultAttr.ignore) { + continue; + } + const label = labelItem.label; const transform = label.getComputedTransform(); // NOTE: Get bounding rect after getComputedTransform, or label may not been updated by the host el. diff --git a/src/util/types.ts b/src/util/types.ts index 47e0277ac1..ea3c3b84bd 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -807,8 +807,8 @@ export interface LabelLayoutOptionCallbackParams { verticalAlign: ZRTextVerticalAlign rect: RectLike labelRect: RectLike - x: number - y: number + // x: number + // y: number }; export interface LabelLayoutOption { diff --git a/test/label-overlap.html b/test/label-layout.html similarity index 59% rename from test/label-overlap.html rename to test/label-layout.html index 75de7c86b3..ac49bb1158 100644 --- a/test/label-overlap.html +++ b/test/label-layout.html @@ -39,10 +39,13 @@
+
-
+
+
+
@@ -126,6 +129,69 @@ + + + + + + + + + + From 447c0a625477cc148214a474943b174c1df5f315 Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 30 Apr 2020 13:07:20 +0800 Subject: [PATCH 05/82] fix(progressive): fix task may not been executed in progressive rendering. --- src/component/geo.ts | 1 - src/component/legend/LegendView.ts | 1 + src/component/parallel.ts | 1 - src/component/title.ts | 1 + src/echarts.ts | 10 ++++------ src/stream/Scheduler.ts | 11 ++++++++--- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/component/geo.ts b/src/component/geo.ts index 44db79ec18..4f8f8ea57c 100644 --- a/src/component/geo.ts +++ b/src/component/geo.ts @@ -21,7 +21,6 @@ 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'; 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/parallel.ts b/src/component/parallel.ts index f273a20c0b..6b691f58d3 100644 --- a/src/component/parallel.ts +++ b/src/component/parallel.ts @@ -23,7 +23,6 @@ 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'; import ParallelModel, { ParallelCoordinateSystemOption } from '../coord/parallel/ParallelModel'; diff --git a/src/component/title.ts b/src/component/title.ts index c58d6eefb9..e4e508c937 100644 --- a/src/component/title.ts +++ b/src/component/title.ts @@ -243,6 +243,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/echarts.ts b/src/echarts.ts index 3121e9f015..4b29d76707 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1631,8 +1631,6 @@ class ECharts extends Eventful { const componentModel = componentView.__model; componentView.render(componentModel, ecModel, api, payload); - componentView.group.markRedraw(); - updateZ(componentModel, componentView); updateHoverEmphasisHandler(componentView); }); @@ -1654,7 +1652,7 @@ class ECharts extends Eventful { labelManager.clearLabels(); - let unfinished: boolean; + let unfinished: boolean = false; ecModel.eachSeries(function (seriesModel) { const chartView = ecIns._chartsMap[seriesModel.__viewId]; chartView.__alive = true; @@ -1665,11 +1663,11 @@ class ECharts extends Eventful { if (dirtyMap && dirtyMap.get(seriesModel.uid)) { renderTask.dirty(); } - - unfinished = renderTask.perform(scheduler.getPerformArgs(renderTask)) || unfinished; + if (renderTask.perform(scheduler.getPerformArgs(renderTask))) { + unfinished = true; + } chartView.group.silent = !!seriesModel.get('silent'); - chartView.group.markRedraw(); updateZ(seriesModel, chartView); diff --git a/src/stream/Scheduler.ts b/src/stream/Scheduler.ts index ff156b7d00..6bcaa1532c 100644 --- a/src/stream/Scheduler.ts +++ b/src/stream/Scheduler.ts @@ -301,7 +301,7 @@ class Scheduler { opt?: PerformStageTaskOpt ): void { opt = opt || {}; - let unfinished: boolean; + 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 = unfinished || overallTask.perform(performArgs); + 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 = unfinished || task.perform(performArgs); + + if (task.perform(performArgs)) { + unfinished = true; + } }); } }); From 7e84356bd4708a9853548bc816c52a75218e9399 Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 30 Apr 2020 13:12:27 +0800 Subject: [PATCH 06/82] fix: avoid unexpected treeshaking. --- src/component/geo.ts | 3 +++ src/component/parallel.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/component/geo.ts b/src/component/geo.ts index 4f8f8ea57c..21e499c154 100644 --- a/src/component/geo.ts +++ b/src/component/geo.ts @@ -25,6 +25,9 @@ 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/parallel.ts b/src/component/parallel.ts index 6b691f58d3..0c9f50e534 100644 --- a/src/component/parallel.ts +++ b/src/component/parallel.ts @@ -25,6 +25,9 @@ import parallelPreprocessor from '../coord/parallel/parallelPreprocessor'; import '../coord/parallel/parallelCreator'; 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'; From 5239e3cf3b732255fdfc793d393c76262c0462b0 Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 30 Apr 2020 17:32:13 +0800 Subject: [PATCH 07/82] feat: using state for all user interactions. --- src/chart/helper/Symbol.ts | 57 ++++----------- src/chart/pie/PieSeries.ts | 2 +- src/chart/pie/PieView.ts | 142 ++++++++++++++----------------------- src/util/LabelManager.ts | 17 ++++- src/util/graphic.ts | 69 ++++++++++++------ src/util/layout.ts | 1 + 6 files changed, 130 insertions(+), 158 deletions(-) diff --git a/src/chart/helper/Symbol.ts b/src/chart/helper/Symbol.ts index 33eab5da68..7bb3cd5e12 100644 --- a/src/chart/helper/Symbol.ts +++ b/src/chart/helper/Symbol.ts @@ -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 */ @@ -291,11 +278,20 @@ class Symbol extends graphic.Group { this._scaleX = symbolSize[0] / 2; this._scaleY = symbolSize[1] / 2; - symbolPath.onStateChange = ( - hoverAnimation && seriesModel.isAnimationEnabled() - ) ? onStateChange : null; - graphic.enableHoverEmphasis(symbolPath, hoverItemStyle); + symbolPath.ensureState('emphasis').style = hoverItemStyle; + + if (hoverAnimation && seriesModel.isAnimationEnabled()) { + const scaleEmphasisState = this.ensureState('emphasis'); + const scale = Math.max(1.1, 3 / this._scaleY + 1); + scaleEmphasisState.scaleX = scale; + scaleEmphasisState.scaleY = scale; + } + else { + this.states.emphasis = null; + } + + graphic.enableHoverEmphasis(this); } fadeOut(cb: () => void, opt?: { @@ -330,33 +326,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/pie/PieSeries.ts b/src/chart/pie/PieSeries.ts index f1bc70d413..4e0306c6d1 100644 --- a/src/chart/pie/PieSeries.ts +++ b/src/chart/pie/PieSeries.ts @@ -222,7 +222,7 @@ class PieSeriesModel extends SeriesModel { // 选中时扇区偏移量 selectedOffset: 10, // 高亮扇区偏移量 - hoverOffset: 10, + hoverOffset: 5, // If use strategy to avoid label overlapping avoidLabelOverlap: true, diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index 4383b25d97..8e1a9cb5b4 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -24,11 +24,10 @@ 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 } 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 { ElementAnimateConfig } from 'zrender/src/Element'; function updateDataSelected( this: PiePiece, @@ -40,7 +39,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', @@ -49,67 +47,38 @@ function updateDataSelected( seriesId: seriesModel.id }); + const animationCfg: ElementAnimateConfig = { + duration: seriesModel.get('animation') ? 200 : 0, + easing: 'cubicOut' + }; data.each(function (idx) { - toggleItemSelected( - data.getItemGraphicEl(idx), - data.getItemLayout(idx), - seriesModel.isSelected(data.getName(idx)), - selectedOffset, - hasAnimation - ); + const el = data.getItemGraphicEl(idx); + el.toggleState('select', seriesModel.isSelected(data.getName(idx)), animationCfg); }); } -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) { 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); } updateData(data: List, idx: number, firstCreate?: boolean): void { - const sector = this.childAt(0) as graphic.Sector; + const sector = this; const seriesModel = data.hostModel as PieSeriesModel; const itemModel = data.getItemModel(idx); @@ -161,53 +130,50 @@ class PiePiece extends graphic.Group { 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; + + + sector.toggleState('select', seriesModel.isSelected(data.getName(idx)), { + duration: seriesModel.get('animation') ? 200 : 0, + easing: 'cubicOut' + }); + 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') - ); - // 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 = (itemModel.get('hoverAnimation') && seriesModel.isAnimationEnabled()) - ? function (fromState: DisplayState, toState: DisplayState): void { - 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(true); - sector.animateTo({ - shape: { - r: layout.r + seriesModel.get('hoverOffset') - } - }, { duration: 300, easing: 'elasticOut' }); - } - else { - sector.stopAnimation(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(); + + const labelLineSelectState = labelLine.ensureState('select'); + const labelTextSelectState = labelText.ensureState('select'); + labelLineSelectState.x = dx; + labelLineSelectState.y = dy; + labelTextSelectState.x = dx; + labelTextSelectState.y = dy; graphic.enableHoverEmphasis(this); } 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; + const sector = this; + const labelLine = sector.getTextGuideLine(); + const labelText = sector.getTextContent(); const seriesModel = data.hostModel; const itemModel = data.getItemModel(idx); @@ -358,11 +324,9 @@ class PieView extends ChartView { .add(function (idx) { const piePiece = new PiePiece(data, idx); // Default expansion animation - if (isFirstRender && animationType !== 'scale') { - piePiece.eachChild(function (child) { - child.stopAnimation(true); - }); - } + // if (isFirstRender && animationType !== 'scale') { + // piePiece.stopAnimation(true); + // } selectedMode && piePiece.on('click', onSectorClick); @@ -375,11 +339,9 @@ class PieView extends ChartView { graphic.clearStates(piePiece); - if (!isFirstRender && animationTypeUpdate !== 'transition') { - piePiece.eachChild(function (child) { - child.stopAnimation(true); - }); - } + // if (!isFirstRender && animationTypeUpdate !== 'transition') { + // piePiece.stopAnimation(true); + // } piePiece.updateData(data, newIdx); diff --git a/src/util/LabelManager.ts b/src/util/LabelManager.ts index 23e09b7ed6..48c69888c2 100644 --- a/src/util/LabelManager.ts +++ b/src/util/LabelManager.ts @@ -128,11 +128,20 @@ class LabelManager { 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); - dummyTransformable.setLocalTransform(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; @@ -224,6 +233,8 @@ class LabelManager { layoutOption = layoutOption || {}; 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, @@ -347,6 +358,10 @@ class LabelManager { } } } + + updateLabelGuidLine() { + + } } diff --git a/src/util/graphic.ts b/src/util/graphic.ts index 69775ebf1f..c3e79c945f 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -45,7 +45,7 @@ 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 } from 'zrender/src/Element'; @@ -377,23 +377,12 @@ function singleEnterEmphasis(el: Element) { 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); - } - disp.z2 += Z2_EMPHASIS_LIFT; - } + el.useState('emphasis', true, { + duration: 300, + // TODO Configuration + easing: 'cubicOut' + }); const textContent = el.getTextContent(); if (textContent) { @@ -403,8 +392,10 @@ function singleEnterEmphasis(el: Element) { } -function singleEnterNormal(el: Element) { - el.clearStates(); +function singleLeaveEmphasis(el: Element) { + el.removeState('emphasis', { + duration: 300 + }); (el as ExtendedElement).__highlighted = false; } @@ -452,6 +443,38 @@ export function clearStates(el: Element) { } } +function elementStateProxy(this: Displayable, stateName: string): DisplayableState { + let state = this.states[stateName]; + if (stateName === 'emphasis' && this.style) { + const currentFill = this.style.fill; + const currentStroke = this.style.stroke; + if (currentFill || currentStroke) { + 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); + emphasisStyle.fill = liftColor(currentFill); + } + if (!hasFillOrStroke(emphasisStyle.stroke)) { + if (!cloned) { + state = extend({}, state); + emphasisStyle = extend({}, emphasisStyle); + } + emphasisStyle.stroke = liftColor(currentStroke); + } + + state.style = emphasisStyle; + } + } + + return state; +} + /** * Set hover style (namely "emphasis style") of element. * @param el Should not be `zrender/graphic/Group`. @@ -462,6 +485,8 @@ export function enableElementHoverEmphasis(el: Displayable, hoverStl?: ZRStylePr emphasisState.style = hoverStl; } + el.stateProxy = elementStateProxy; + // 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: @@ -469,7 +494,7 @@ export function enableElementHoverEmphasis(el: Displayable, hoverStl?: ZRStylePr // again by dataZoom. // (2) call `setOption` and replace elements totally when they are highlighted. if ((el as ExtendedDisplayable).__highlighted) { - singleEnterNormal(el); + // singleLeaveEmphasis(el); singleEnterEmphasis(el); } } @@ -485,7 +510,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) { @@ -495,7 +520,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) { 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) From 75a69b19e74627820aa5d12689040399065b00af Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 30 Apr 2020 17:44:34 +0800 Subject: [PATCH 08/82] fix(pie): fix pie hover state --- src/chart/pie/PieView.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index 8e1a9cb5b4..8c88b700c6 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -153,8 +153,8 @@ class PiePiece extends graphic.Sector { const emphasisState = sector.ensureState('emphasis'); emphasisState.shape = { - r: layout.r + itemModel.get('hoverAnimation') // TODO: Change a name. - ? seriesModel.get('hoverOffset') : 0 + r: layout.r + (itemModel.get('hoverAnimation') // TODO: Change a name. + ? seriesModel.get('hoverOffset') : 0) }; const labelLine = sector.getTextGuideLine(); From b631038efd7b55ffc3351d36e1f439cb48ccc8ce Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 30 Apr 2020 19:28:11 +0800 Subject: [PATCH 09/82] fix(label): fix emphasis position. --- src/util/graphic.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/util/graphic.ts b/src/util/graphic.ts index c3e79c945f..a1a84e8406 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -811,7 +811,8 @@ function setLabelStyle( targetElEmphasisState.textConfig = createTextConfig( emphasisState.style, emphasisModel, - opt + opt, + true ); } From 8bf2072baaa7c943210f5049333860caf435adf0 Mon Sep 17 00:00:00 2001 From: pissang Date: Sun, 3 May 2020 21:13:27 +0800 Subject: [PATCH 10/82] enhance(pie): improve pie animation. #12553 --- src/chart/pie/PieSeries.ts | 6 +- src/chart/pie/PieView.ts | 124 +++++++++++++------------------------ 2 files changed, 47 insertions(+), 83 deletions(-) diff --git a/src/chart/pie/PieSeries.ts b/src/chart/pie/PieSeries.ts index 4e0306c6d1..8e86ed93c0 100644 --- a/src/chart/pie/PieSeries.ts +++ b/src/chart/pie/PieSeries.ts @@ -285,10 +285,14 @@ class PieSeriesModel extends SeriesModel { // 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 8c88b700c6..4409db1be5 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -62,7 +62,7 @@ function updateDataSelected( */ class PiePiece extends graphic.Sector { - constructor(data: List, idx: number) { + constructor(data: List, idx: number, startAngle: number) { super(); this.z2 = 2; @@ -74,10 +74,10 @@ class PiePiece extends graphic.Sector { this.setTextContent(text); - this.updateData(data, idx, true); + this.updateData(data, idx, startAngle, true); } - updateData(data: List, idx: number, firstCreate?: boolean): void { + updateData(data: List, idx: number, startAngle?: number, firstCreate?: boolean): void { const sector = this; const seriesModel = data.hostModel as PieSeriesModel; @@ -104,26 +104,31 @@ class PiePiece extends graphic.Sector { } // 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')); @@ -311,22 +316,28 @@ class PieView extends ChartView { 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) { + let shape = data.getItemLayout(0) as graphic.Sector['shape']; + for (let s = 1; isNaN(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.stopAnimation(true); - // } + const piePiece = new PiePiece(data, idx, startAngle); selectedMode && piePiece.on('click', onSectorClick); @@ -339,11 +350,7 @@ class PieView extends ChartView { graphic.clearStates(piePiece); - // if (!isFirstRender && animationTypeUpdate !== 'transition') { - // piePiece.stopAnimation(true); - // } - - piePiece.updateData(data, newIdx); + piePiece.updateData(data, newIdx, startAngle); piePiece.off('click'); selectedMode && piePiece.on('click', onSectorClick); @@ -356,61 +363,14 @@ 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); - } - - 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 - )); - } - else { - // clipPath is used in first-time animation, so remove it when otherwise. See: #8994 - group.removeClipPath(); + // Always use initial animation. + if (seriesModel.get('animationTypeUpdate') !== 'expansion') { + this._data = data; } - - 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); From c30f1370939545abea1b275e81f57d82d6fafb3d Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 7 May 2020 21:58:52 +0800 Subject: [PATCH 11/82] feat: add auto calculated label guide line. --- src/chart/pie/PieView.ts | 12 +- src/echarts.ts | 2 +- src/{util => label}/LabelManager.ts | 46 +++- src/label/labelGuideHelper.ts | 366 ++++++++++++++++++++++++++++ 4 files changed, 406 insertions(+), 20 deletions(-) rename src/{util => label}/LabelManager.ts (92%) create mode 100644 src/label/labelGuideHelper.ts diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index 4409db1be5..b36be3cf83 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -235,7 +235,7 @@ class PiePiece extends graphic.Sector { outsideFill: visualColor }); - const targetTextStyle = { + const targetTextPos = { x: labelLayout.x, y: labelLayout.y }; @@ -244,9 +244,7 @@ class PiePiece extends graphic.Sector { shape: targetLineShape }, seriesModel, idx); - graphic.updateProps(labelText, { - style: targetTextStyle - }, seriesModel, idx); + graphic.updateProps(labelText, targetTextPos, seriesModel, idx); } else { labelLine.attr({ @@ -254,15 +252,11 @@ class PiePiece extends graphic.Sector { }); // Make sure update style on labelText after setLabelStyle. // Because setLabelStyle will replace a new style on it. - labelText.attr({ - style: targetTextStyle - }); + labelText.attr(targetTextPos); } labelText.attr({ rotation: labelLayout.rotation, - originX: labelLayout.x, - originY: labelLayout.y, z2: 10 }); diff --git a/src/echarts.ts b/src/echarts.ts index 4b29d76707..b92fcd7beb 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -71,7 +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 './util/LabelManager'; +import LabelManager from './label/LabelManager'; declare let global: any; type ModelFinder = modelUtil.ModelFinder; diff --git a/src/util/LabelManager.ts b/src/label/LabelManager.ts similarity index 92% rename from src/util/LabelManager.ts rename to src/label/LabelManager.ts index 48c69888c2..276ac1b5f4 100644 --- a/src/util/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -17,7 +17,16 @@ * under the License. */ -import { OrientedBoundingRect, Text as ZRText, Point, BoundingRect, getECData } from './graphic'; +// TODO: move labels out of viewport. + +import { + OrientedBoundingRect, + Text as ZRText, + Point, + BoundingRect, + getECData, + Polyline +} from '../util/graphic'; import { MatrixArray } from 'zrender/src/core/matrix'; import ExtensionAPI from '../ExtensionAPI'; import { @@ -26,12 +35,13 @@ import { LabelLayoutOption, LabelLayoutOptionCallback, LabelLayoutOptionCallbackParams -} from './types'; -import { parsePercent } from './number'; +} from '../util/types'; +import { parsePercent } from '../util/number'; import ChartView from '../view/Chart'; -import { ElementTextConfig } from 'zrender/src/Element'; +import { ElementTextConfig, ElementTextGuideLineConfig } from 'zrender/src/Element'; import { RectLike } from 'zrender/src/core/BoundingRect'; import Transformable from 'zrender/src/core/Transformable'; +import { updateLabelGuideLine } from './labelGuideHelper'; interface DisplayedLabelItem { label: ZRText @@ -44,8 +54,11 @@ interface DisplayedLabelItem { interface LabelLayoutDesc { label: ZRText + labelGuide: Polyline + seriesIndex: number dataIndex: number + layoutOption: LabelLayoutOptionCallback | LabelLayoutOption overlap: LabelLayoutOption['overlap'] @@ -59,6 +72,7 @@ interface LabelLayoutDesc { interface SavedLabelAttr { ignore: boolean + labelGuideIgnore: boolean x: number y: number @@ -151,10 +165,15 @@ class LabelManager { BoundingRect.applyTransform(hostRect, hostRect, transform); } + const labelGuide = hostRect && host.getTextGuideLine(); + this._labelList.push({ + label, + labelGuide: labelGuide, + seriesIndex, dataIndex, - label, + layoutOption, hostRect, @@ -170,6 +189,7 @@ class LabelManager { // 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, @@ -246,7 +266,7 @@ class LabelManager { if (layoutOption.x != null) { // TODO width of chart view. label.x = parsePercent(layoutOption.x, width); - label.setStyle('x', 0); // Ignore movement in style. + label.setStyle('x', 0); // Ignore movement in style. TODO: origin. } else { label.x = defaultLabelAttr.x; @@ -336,16 +356,19 @@ class LabelManager { } } + const labelGuide = labelItem.labelGuide; // TODO Callback to determine if this overlap should be handled? if (overlapped) { // label.setStyle({ opacity: 0.1 }); // label.z = 0; label.hide(); + labelGuide && labelGuide.hide(); } else { // TODO Restore z // label.setStyle({ opacity: 1 }); label.attr('ignore', labelItem.defaultAttr.ignore); + labelGuide && labelGuide.attr('ignore', labelItem.defaultAttr.labelGuideIgnore); displayedLabels.push({ label, @@ -356,11 +379,14 @@ class LabelManager { transform }); } - } - } - - updateLabelGuidLine() { + updateLabelGuideLine( + label, + globalRect, + label.__hostTarget, + labelItem.hostRect + ); + } } } diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts new file mode 100644 index 0000000000..b20511aa7d --- /dev/null +++ b/src/label/labelGuideHelper.ts @@ -0,0 +1,366 @@ +/* +* 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 { + Text as ZRText, + Point, + Path +} 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'; + +const PI2 = Math.PI * 2; +const CMD = PathProxy.CMD; + +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[]) { + 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; + const t = Math.min(Math.max(projectedLen / lineLen, 0), 1); + 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); + 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); + + xi = x0; + yi = y0; + break; + } + + if (d < minDist) { + minDist = d; + out.set(tmpPt[0], tmpPt[1]); + } + } + + return minDist; +} + +const pt0 = new Point(); +const pt1 = new Point(); +const pt2 = new Point(); +const dir = new Point(); +export function updateLabelGuideLine( + label: ZRText, + labelRect: RectLike, + target: Element, + targetRect: RectLike +) { + if (!target) { + return; + } + + const labelLine = target.getTextGuideLine(); + // Needs to create text guide in each charts. + if (!labelLine) { + return; + } + + const labelGuideConfig = target.textGuideLineConfig || {}; + if (!labelGuideConfig.autoCalculate) { + return; + } + + const points = [[0, 0], [0, 0], [0, 0]]; + + const searchSpace = labelGuideConfig.candidates || DEFAULT_SEARCH_SPACE; + + let minDist = Infinity; + const anchorPoint = labelGuideConfig && labelGuideConfig.anchor; + 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, labelGuideConfig.len); + + const dist = anchorPoint ? anchorPoint.distance(pt1) + : (target instanceof Path + ? nearestPointOnPath(pt1, target.path, pt2) + : nearestPointOnRect(pt1, targetRect, pt2)); + + // TODO pt2 is in the path + if (dist < minDist) { + minDist = dist; + pt0.toArray(points[0]); + pt1.toArray(points[1]); + pt2.toArray(points[2]); + } + } + + labelLine.setShape({ points }); +} \ No newline at end of file From e201982d38158d6c7f380f676153705ce31cfefa Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 14 May 2020 19:03:53 +0800 Subject: [PATCH 12/82] fix(state): fix some state issues on pie. --- src/chart/funnel/FunnelSeries.ts | 3 +- src/chart/pie/PieView.ts | 28 +++++++++------- src/util/graphic.ts | 55 ++++++++++++++++---------------- 3 files changed, 45 insertions(+), 41 deletions(-) diff --git a/src/chart/funnel/FunnelSeries.ts b/src/chart/funnel/FunnelSeries.ts index a67978813a..a54d967123 100644 --- a/src/chart/funnel/FunnelSeries.ts +++ b/src/chart/funnel/FunnelSeries.ts @@ -172,8 +172,7 @@ class FunnelSeriesModel extends SeriesModel { length: 20, lineStyle: { // color: 各异, - width: 1, - type: 'solid' + width: 1 } }, itemStyle: { diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index b36be3cf83..56d1ac619c 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -28,6 +28,7 @@ import { Payload, ColorString } from '../../util/types'; import List from '../../data/List'; import PieSeriesModel, {PieDataItemOption} from './PieSeries'; import { ElementAnimateConfig } from 'zrender/src/Element'; +import labelLayout from './labelLayout'; function updateDataSelected( this: PiePiece, @@ -143,12 +144,6 @@ class PiePiece extends graphic.Sector { sectorSelectState.x = dx; sectorSelectState.y = dy; - - sector.toggleState('select', seriesModel.isSelected(data.getName(idx)), { - duration: seriesModel.get('animation') ? 200 : 0, - easing: 'cubicOut' - }); - const cursorStyle = itemModel.getShallow('cursor'); cursorStyle && sector.attr('cursor', cursorStyle); @@ -165,14 +160,23 @@ class PiePiece extends graphic.Sector { const labelLine = sector.getTextGuideLine(); const labelText = sector.getTextContent(); - const labelLineSelectState = labelLine.ensureState('select'); - const labelTextSelectState = labelText.ensureState('select'); - labelLineSelectState.x = dx; - labelLineSelectState.y = dy; - labelTextSelectState.x = dx; - labelTextSelectState.y = dy; + labelLine.states.select = { + x: dx, y: dy + }; + if (layout.label) { + labelText.states.select = { + x: layout.label.x + dx, + y: layout.label.y + dy + }; + } graphic.enableHoverEmphasis(this); + + // Switch after `select` state updated. + sector.toggleState('select', seriesModel.isSelected(data.getName(idx)), { + duration: seriesModel.get('animation') ? 200 : 0, + easing: 'cubicOut' + }); } private _updateLabel(data: List, idx: number, withAnimation: boolean): void { diff --git a/src/util/graphic.ts b/src/util/graphic.ts index a1a84e8406..ea75df77eb 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -72,7 +72,8 @@ import { trim, isArrayLike, map, - defaults + defaults, + indexOf } from 'zrender/src/core/util'; @@ -383,11 +384,6 @@ function singleEnterEmphasis(el: Element) { // TODO Configuration easing: 'cubicOut' }); - - const textContent = el.getTextContent(); - if (textContent) { - textContent.z2 += Z2_EMPHASIS_LIFT; - } // TODO hover layer } @@ -446,36 +442,41 @@ export function clearStates(el: Element) { function elementStateProxy(this: Displayable, stateName: string): DisplayableState { let state = this.states[stateName]; if (stateName === 'emphasis' && this.style) { - const currentFill = this.style.fill; - const currentStroke = this.style.stroke; - if (currentFill || currentStroke) { - 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); - emphasisStyle.fill = liftColor(currentFill); - } - if (!hasFillOrStroke(emphasisStyle.stroke)) { - if (!cloned) { + const hasEmphasis = indexOf(this.currentStates, stateName) >= 0; + if (!(this instanceof ZRText)) { + const currentFill = this.style.fill; + const currentStroke = this.style.stroke; + if (currentFill || currentStroke) { + 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(currentFill); + } + if (!hasFillOrStroke(emphasisStyle.stroke)) { + if (!cloned) { + state = extend({}, state); + emphasisStyle = extend({}, emphasisStyle); + } + emphasisStyle.stroke = hasEmphasis ? currentStroke : liftColor(currentStroke); } - emphasisStyle.stroke = liftColor(currentStroke); - } - state.style = emphasisStyle; + state.style = emphasisStyle; + } } + state.z2 = this.z2 + Z2_EMPHASIS_LIFT; } return state; } -/** +/**FI * Set hover style (namely "emphasis style") of element. * @param el Should not be `zrender/graphic/Group`. */ @@ -694,7 +695,7 @@ type LabelModelForText = Model(targetEl: ZRText, normalModel: LabelModelForText, emphasisModel: LabelModelForText, opt?: SetLabelStyleOpt, normalSpecified?: TextStyleProps, emphasisSpecified?: TextStyleProps): void; From 17eb9c0bbd95c0135bacaad8f55f97cf23a74bdf Mon Sep 17 00:00:00 2001 From: pissang Date: Fri, 15 May 2020 16:25:51 +0800 Subject: [PATCH 13/82] feat(state): add stateAnimation in all series. use state in sunburst. --- src/chart/gauge/GaugeView.ts | 4 +- src/chart/pie/PieView.ts | 13 +- src/chart/sunburst/SunburstPiece.ts | 296 ++++++++++++--------------- src/chart/sunburst/SunburstSeries.ts | 5 +- src/chart/sunburst/SunburstView.ts | 6 +- src/echarts.ts | 25 ++- src/model/Model.ts | 62 +++--- src/model/Series.ts | 7 +- src/model/globalDefault.ts | 8 +- src/util/graphic.ts | 145 ++++--------- src/util/types.ts | 10 + src/visual/style.ts | 2 +- 12 files changed, 259 insertions(+), 324 deletions(-) diff --git a/src/chart/gauge/GaugeView.ts b/src/chart/gauge/GaugeView.ts index 77e3c8c24f..db84a9bc48 100644 --- a/src/chart/gauge/GaugeView.ts +++ b/src/chart/gauge/GaugeView.ts @@ -424,7 +424,7 @@ class GaugeView extends ChartView { text: data.getName(0), align: 'center', verticalAlign: 'middle' - }, {autoColor: autoColor, forceRich: true}) + }, {autoColor: autoColor}) })); } } @@ -464,7 +464,7 @@ class GaugeView extends ChartView { height: isNaN(height) ? null : height, align: 'center', verticalAlign: 'middle' - }, {autoColor: autoColor, forceRich: true}) + }, {autoColor: autoColor}) })); } } diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index 56d1ac619c..aa1b0d352b 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -27,8 +27,6 @@ import ExtensionAPI from '../../ExtensionAPI'; import { Payload, ColorString } from '../../util/types'; import List from '../../data/List'; import PieSeriesModel, {PieDataItemOption} from './PieSeries'; -import { ElementAnimateConfig } from 'zrender/src/Element'; -import labelLayout from './labelLayout'; function updateDataSelected( this: PiePiece, @@ -48,13 +46,9 @@ function updateDataSelected( seriesId: seriesModel.id }); - const animationCfg: ElementAnimateConfig = { - duration: seriesModel.get('animation') ? 200 : 0, - easing: 'cubicOut' - }; data.each(function (idx) { const el = data.getItemGraphicEl(idx); - el.toggleState('select', seriesModel.isSelected(data.getName(idx)), animationCfg); + el.toggleState('select', seriesModel.isSelected(data.getName(idx))); }); } @@ -173,10 +167,7 @@ class PiePiece extends graphic.Sector { graphic.enableHoverEmphasis(this); // Switch after `select` state updated. - sector.toggleState('select', seriesModel.isSelected(data.getName(idx)), { - duration: seriesModel.get('animation') ? 200 : 0, - easing: 'cubicOut' - }); + sector.toggleState('select', seriesModel.isSelected(data.getName(idx))); } private _updateLabel(data: List, idx: number, withAnimation: boolean): void { diff --git a/src/chart/sunburst/SunburstPiece.ts b/src/chart/sunburst/SunburstPiece.ts index aa44e40df7..0afe37a64f 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,16 @@ 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 - ) { - // 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); + // Disable animation for gradient since no interpolation method + // is supported for gradient + graphic.updateProps(sector, { + shape: sectorShape + }, seriesModel); + sector.useStyle(normalStyle); + + this._updateLabel(seriesModel); const cursorStyle = itemModel.getShallow('cursor'); cursorStyle && sector.attr('cursor', cursorStyle); @@ -177,13 +157,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); } } }); @@ -192,141 +172,139 @@ 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(); + // n.piece.updateData(false, n, 'normal'); } }); } onHighlight() { - this.updateData(false, this.node, 'highlight'); + this.removeState('downplay'); + this.useState('highlight', true); } onDownplay() { - this.updateData(false, this.node, 'downplay'); + this.removeState('highlight'); + this.useState('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), - 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'; + 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 === 'left') { - r = layout.r0 + labelPadding; - if (midAngle > Math.PI / 2) { - textAlign = 'right'; - } + + state.style = graphic.createTextStyle(labelStateModel, { + }, null, stateName !== 'normal', true); + if (text) { + state.style.text = text; } - else if (textAlign === 'right') { - r = layout.r - labelPadding; - if (midAngle > Math.PI / 2) { - textAlign = 'left'; - } + + // 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 = { + inside: labelPosition !== 'outside' + }; + if (labelColor) { + sectorState.textConfig.insideStroke = sectorState.textConfig.outsideFill = labelColor; } - } - graphic.setLabelStyle( - label, normalModel, labelHoverModel, - { - defaultText: labelModel.getShallow('show') ? text : null + 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( @@ -351,15 +329,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 327a1ab766..0b4fdd69d6 100644 --- a/src/chart/sunburst/SunburstSeries.ts +++ b/src/chart/sunburst/SunburstSeries.ts @@ -242,7 +242,10 @@ class SunburstSeriesModel extends SeriesModel { }, downplay: { itemStyle: { - opacity: 0.9 + opacity: 0.5 + }, + label: { + opacity: 0.6 } }, diff --git a/src/chart/sunburst/SunburstView.ts b/src/chart/sunburst/SunburstView.ts index bef20f911c..6a3e4f8a8b 100644 --- a/src/chart/sunburst/SunburstView.ts +++ b/src/chart/sunburst/SunburstView.ts @@ -130,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); @@ -172,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 @@ -208,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/echarts.ts b/src/echarts.ts index b92fcd7beb..ddca382414 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1673,6 +1673,8 @@ class ECharts extends Eventful { updateBlend(seriesModel, chartView); + updateStates(seriesModel, chartView); + updateHoverEmphasisHandler(chartView); // Add albels. @@ -1681,8 +1683,8 @@ class ECharts extends Eventful { scheduler.unfinished = unfinished || scheduler.unfinished; - labelManager.updateLayoutConfig(api); - labelManager.layout(); + // labelManager.updateLayoutConfig(api); + // labelManager.layout(); // If use hover layer updateHoverLayerStatus(ecIns, ecModel); @@ -1753,7 +1755,7 @@ 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); @@ -1769,6 +1771,22 @@ class ECharts extends Eventful { }); }; + updateStates = function (seriesModel: SeriesModel, view: ChartView): void { + const stateAnimationModel = seriesModel.getModel('stateAnimation'); + const enableAnimation = seriesModel.isAnimationEnabled(); + view.group.traverse(function (el: Displayable) { + if (el.states && el.states.emphasis) { + if (enableAnimation) { + // TODO textContent? + graphic.setStateTransition(el, stateAnimationModel); + } + else if (el.stateTransition) { + el.stateTransition = null; + } + } + }); + }; + function getHighDownDispatcher(target: Element) { while (target && !graphic.isHighDownDispatcher(target)) { target = target.parent; @@ -1888,6 +1906,7 @@ let renderSeries: ( let performPostUpdateFuncs: (ecModel: GlobalModel, api: ExtensionAPI) => void; let updateHoverLayerStatus: (ecIns: ECharts, ecModel: GlobalModel) => void; let updateBlend: (seriesModel: SeriesModel, chartView: ChartView) => void; +let updateStates: (model: SeriesModel, chartView: ChartView) => void; let updateZ: (model: ComponentModel, view: ComponentView | ChartView) => void; let updateHoverEmphasisHandler: (view: ComponentView | ChartView) => void; let createExtensionAPI: (ecIns: ECharts) => ExtensionAPI; diff --git a/src/model/Model.ts b/src/model/Model.ts index 3f42f723d3..a2f588fad2 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -169,37 +169,37 @@ class Model { // TODO: TYPE use unkown * * @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; - } + // 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 diff --git a/src/model/Series.ts b/src/model/Series.ts index fbceb6f202..bce6b8237c 100644 --- a/src/model/Series.ts +++ b/src/model/Series.ts @@ -534,10 +534,7 @@ class SeriesModel extends ComponentMode }; } - /** - * @return {boolean} - */ - isAnimationEnabled() { + isAnimationEnabled(): boolean { if (env.node) { return false; } @@ -547,7 +544,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..fe4feb3592 100644 --- a/src/model/globalDefault.ts +++ b/src/model/globalDefault.ts @@ -59,13 +59,19 @@ export default { // Default is source-over blendMode: null, + stateAnimation: { + duration: 300, + easing: 'cubicOut' + }, + animation: 'auto', animationDuration: 1000, animationDurationUpdate: 300, - animationEasing: 'exponentialOut', + animationEasing: 'cubicOut', animationEasingUpdate: 'cubicOut', animationThreshold: 2000, + // Configuration for progressive/incremental rendering progressiveThreshold: 3000, progressive: 400, diff --git a/src/util/graphic.ts b/src/util/graphic.ts index ea75df77eb..182b69cb78 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -60,7 +60,8 @@ import { ColorString, DataModel, ECEventData, - ZRStyleProps + ZRStyleProps, + AnimationOption } from './types'; import GlobalModel from '../model/Global'; import { makeInner } from './model'; @@ -127,8 +128,6 @@ type TextCommonParams = { */ autoColor?: ColorString - forceRich?: boolean - getTextPosition?: (textStyleModel: Model, isEmphasis?: boolean) => string | string[] | number[] defaultOutsidePosition?: LabelOption['position'] @@ -379,19 +378,13 @@ function singleEnterEmphasis(el: Element) { return; } - el.useState('emphasis', true, { - duration: 300, - // TODO Configuration - easing: 'cubicOut' - }); + el.useState('emphasis', true); // TODO hover layer } function singleLeaveEmphasis(el: Element) { - el.removeState('emphasis', { - duration: 300 - }); + el.removeState('emphasis'); (el as ExtendedElement).__highlighted = false; } @@ -548,6 +541,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. @@ -648,37 +658,6 @@ interface SetLabelStyleOpt extends TextCommonParams { } -// function handleSquashCallback( -// func: Function, -// labelDataIndex: LDI, -// labelFetcher: SetLabelStyleOpt['labelFetcher'], -// rect: RectLike, -// status: DisplayState -// ) { -// let params: { -// status?: DisplayState -// rect?: RectLike -// }; -// if (labelFetcher && labelFetcher.getDataParams) { -// params = labelFetcher.getDataParams(labelDataIndex); -// } -// else { -// params = {}; -// } -// params.status = status; -// params.rect = rect; -// return func(params); -// } - -// function getGlobalBoundingRect(el: Element) { -// const rect = el.getBoundingRect().clone(); -// const transform = el.getComputedTransform(); -// if (transform) { -// rect.applyTransform(transform); -// } -// return rect; -// } - type LabelModel = Model string) }>; @@ -717,33 +696,9 @@ function setLabelStyle( const labelDataIndex = opt.labelDataIndex; const labelDimIndex = opt.labelDimIndex; - // TODO Performance optimization - // normalModel.squash(false, function (func: Function) { - // return handleSquashCallback( - // func, - // labelDataIndex, - // labelFetcher, - // isSetOnText ? null : getGlobalBoundingRect(targetEl), - // 'normal' - // ); - // }); - - // emphasisModel.squash(false, function (func: Function) { - // return handleSquashCallback( - // func, - // labelDataIndex, - // labelFetcher, - // isSetOnText ? null : getGlobalBoundingRect(targetEl), - // 'emphasis' - // ); - // }); - 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) { let baseText; @@ -780,12 +735,6 @@ 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, @@ -848,12 +797,12 @@ export {setLabelStyle}; 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); @@ -863,25 +812,25 @@ export function createTextStyle( export function createTextConfig( textStyle: TextStyleProps, textStyleModel: Model, - opt?: TextCommonParams, - isEmphasis?: boolean + opt?: Pick, + isNotNormal?: boolean ) { 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'); if (opt.getTextPosition) { - labelPosition = opt.getTextPosition(textStyleModel, isEmphasis); + labelPosition = opt.getTextPosition(textStyleModel, isNotNormal); } else { 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. + // in bar series, and magic type should be considered. labelPosition === 'outside' && (labelPosition = opt.defaultOutsidePosition || 'top'); } @@ -905,17 +854,6 @@ export function createTextConfig( // Set default stroke, which is useful when label is over other // messy graphics (like lines) in background. textConfig.outsideStroke = 'rgba(255, 255, 255, 0.9)'; - // 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; - // } return textConfig; } @@ -933,8 +871,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. @@ -970,7 +908,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); } } } @@ -983,12 +921,7 @@ function setTextStyleCommon( textStyle.overflow = overflow; } - setTokenTextStyle(textStyle, textStyleModel, globalTextStyle, opt, isEmphasis, isAttached, true); - - // TODO - if (opt.forceRich && !opt.textStyle) { - opt.textStyle = {}; - } + setTokenTextStyle(textStyle, textStyleModel, globalTextStyle, opt, isNotNormal, isAttached, true); } // Consider case: @@ -1025,7 +958,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; @@ -1043,13 +976,13 @@ 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; let fillColor = textStyleModel.getShallow('color'); @@ -1078,7 +1011,7 @@ function setTokenTextStyle( } // TODO - if (!isEmphasis && !isAttached) { + if (!isNotNormal && !isAttached) { // Set default finally. if (textStyle.fill == null && opt.autoColor) { textStyle.fill = opt.autoColor; diff --git a/src/util/types.ts b/src/util/types.ts index ea3c3b84bd..d9bbd1befa 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -546,6 +546,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. */ @@ -1122,6 +1127,11 @@ export interface SeriesOption extends * Global label layout option in label layout stage. */ labelLayout?: LabelLayoutOption | LabelLayoutOptionCallback + + /** + * Animation config for state transition. + */ + stateAnimation?: AnimationOption } export interface SeriesOnCartesianOptionMixin { 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]; From 4ae89c2fd85b403bbe31f61ad77401e270d2ec3e Mon Sep 17 00:00:00 2001 From: pissang Date: Sun, 17 May 2020 16:14:50 +0800 Subject: [PATCH 14/82] enhance(state): clearStates and restoreState in the renderSeries overall. --- src/chart/bar/BarView.ts | 4 +- src/chart/bar/PictorialBarView.ts | 1 - src/chart/candlestick/CandlestickView.ts | 1 - src/chart/funnel/FunnelView.ts | 1 - src/chart/gauge/GaugeView.ts | 1 - src/chart/helper/LineDraw.ts | 1 - src/chart/helper/SymbolDraw.ts | 2 - src/chart/parallel/ParallelView.ts | 1 - src/chart/pie/PieView.ts | 2 - src/chart/radar/RadarView.ts | 2 - src/chart/sunburst/SunburstPiece.ts | 7 +- src/chart/sunburst/SunburstSeries.ts | 2 +- src/chart/sunburst/SunburstView.ts | 3 +- src/chart/sunburst/sunburstAction.ts | 2 +- src/chart/tree/TreeView.ts | 3 - src/component/marker/MarkAreaView.ts | 1 - src/echarts.ts | 91 +++++++++++++++++------- src/label/LabelManager.ts | 2 +- src/util/graphic.ts | 11 --- 19 files changed, 71 insertions(+), 67 deletions(-) diff --git a/src/chart/bar/BarView.ts b/src/chart/bar/BarView.ts index 4565a2bcbd..96db5fa78c 100644 --- a/src/chart/bar/BarView.ts +++ b/src/chart/bar/BarView.ts @@ -26,8 +26,7 @@ import { updateProps, initProps, enableHoverEmphasis, - setLabelStyle, - clearStates + setLabelStyle } from '../../util/graphic'; import Path, { PathProps } from 'zrender/src/graphic/Path'; import Group from 'zrender/src/graphic/Group'; @@ -236,7 +235,6 @@ class BarView extends ChartView { } if (el) { - clearStates(el); updateProps(el as Path, { shape: layout }, animationModel, newIndex); diff --git a/src/chart/bar/PictorialBarView.ts b/src/chart/bar/PictorialBarView.ts index 518630c1cd..e7d93daf5e 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 { 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/funnel/FunnelView.ts b/src/chart/funnel/FunnelView.ts index 0f515ac24c..9f6ab671e4 100644 --- a/src/chart/funnel/FunnelView.ts +++ b/src/chart/funnel/FunnelView.ts @@ -190,7 +190,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 db84a9bc48..0540baca77 100644 --- a/src/chart/gauge/GaugeView.ts +++ b/src/chart/gauge/GaugeView.ts @@ -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: { diff --git a/src/chart/helper/LineDraw.ts b/src/chart/helper/LineDraw.ts index 0acf339140..1422b465bd 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/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/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/PieView.ts b/src/chart/pie/PieView.ts index aa1b0d352b..f194434f61 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -337,8 +337,6 @@ class PieView extends ChartView { .update(function (newIdx, oldIdx) { const piePiece = oldData.getItemGraphicEl(oldIdx) as PiePiece; - graphic.clearStates(piePiece); - piePiece.updateData(data, newIdx, startAngle); piePiece.off('click'); diff --git a/src/chart/radar/RadarView.ts b/src/chart/radar/RadarView.ts index 98698b6ede..397b67f14a 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; diff --git a/src/chart/sunburst/SunburstPiece.ts b/src/chart/sunburst/SunburstPiece.ts index 0afe37a64f..0334111ee7 100644 --- a/src/chart/sunburst/SunburstPiece.ts +++ b/src/chart/sunburst/SunburstPiece.ts @@ -173,19 +173,16 @@ class SunburstPiece extends graphic.Sector { this.node.hostTree.root.eachNode(function (n: DrawTreeNode) { if (n.piece) { n.piece.clearStates(); - // n.piece.updateData(false, n, 'normal'); } }); } onHighlight() { - this.removeState('downplay'); - this.useState('highlight', true); + this.replaceState('downplay', 'highlight', true); } onDownplay() { - this.removeState('highlight'); - this.useState('downplay', true); + this.replaceState('highlight', 'downplay', true); } _updateLabel( diff --git a/src/chart/sunburst/SunburstSeries.ts b/src/chart/sunburst/SunburstSeries.ts index 0b4fdd69d6..75a58d0290 100644 --- a/src/chart/sunburst/SunburstSeries.ts +++ b/src/chart/sunburst/SunburstSeries.ts @@ -245,7 +245,7 @@ class SunburstSeriesModel extends SeriesModel { opacity: 0.5 }, label: { - opacity: 0.6 + opacity: 0.5 } }, diff --git a/src/chart/sunburst/SunburstView.ts b/src/chart/sunburst/SunburstView.ts index 6a3e4f8a8b..625543b4a1 100644 --- a/src/chart/sunburst/SunburstView.ts +++ b/src/chart/sunburst/SunburstView.ts @@ -25,8 +25,7 @@ import SunburstSeriesModel, { SunburstSeriesNodeItemOption } from './SunburstSer import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../ExtensionAPI'; import { TreeNode } from '../../data/Tree'; - -const ROOT_TO_NODE_ACTION = 'sunburstRootToNode'; +import { ROOT_TO_NODE_ACTION } from './sunburstAction'; interface DrawTreeNode extends TreeNode { parentNode: DrawTreeNode 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/tree/TreeView.ts b/src/chart/tree/TreeView.ts index 1484889261..5ec59adc11 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); }) diff --git a/src/component/marker/MarkAreaView.ts b/src/component/marker/MarkAreaView.ts index 3be84b959e..7398abb692 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) diff --git a/src/echarts.ts b/src/echarts.ts index ddca382414..c590c587fb 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1660,6 +1660,9 @@ 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(); } @@ -1673,21 +1676,25 @@ class ECharts extends Eventful { updateBlend(seriesModel, chartView); - updateStates(seriesModel, chartView); - updateHoverEmphasisHandler(chartView); - // Add albels. + // Add labels. labelManager.addLabelsOfSeries(chartView); + + // NOTE: Update states after label is added. + // Because in LabelManager#addLabel. It will cache the properties(transform, textConfig) of label. + // We need to cache the normal state. Not other states. + updateStates(seriesModel, chartView); }); scheduler.unfinished = unfinished || scheduler.unfinished; - // labelManager.updateLayoutConfig(api); - // labelManager.layout(); + labelManager.updateLayoutConfig(api); + labelManager.layout(); // If use hover layer - updateHoverLayerStatus(ecIns, ecModel); + // TODO + // updateHoverLayerStatus(ecIns, ecModel); // Add aria aria(ecIns._zr.dom, ecModel); @@ -1699,7 +1706,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; @@ -1717,7 +1724,7 @@ class ECharts extends Eventful { if (chartView.__alive) { chartView.group.traverse(function (el: ECElement) { // Don't switch back. - // el.useHoverLayer = true; + el.useHoverLayer = true; }); } }); @@ -1727,7 +1734,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') { @@ -1737,20 +1744,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 { const z = model.get('z'); const zlevel = model.get('zlevel'); // Set z and zlevel @@ -1771,17 +1776,54 @@ class ECharts extends Eventful { }); }; - updateStates = function (seriesModel: SeriesModel, view: ChartView): void { + interface DisplayableWithStatesHistory extends Displayable { + __prevStates: string[] + }; + // TODO States on component. + function clearStates(seriesModel: SeriesModel, view: ChartView): void { + view.group.traverse(function (el: Displayable) { + // TODO If el is incremental. + if (el.hasState()) { + (el as DisplayableWithStatesHistory).__prevStates = el.currentStates; + const textContent = el.getTextContent(); + const textGuide = el.getTextGuideLine(); + // Not use animation when clearStates and restore states in `updateStates` + if (el.stateTransition) { + el.stateTransition = null; + } + if (textContent && textContent.stateTransition) { + textContent.stateTransition = null; + } + if (textGuide && textGuide.stateTransition) { + textGuide.stateTransition = null; + } + el.clearStates(); + } + }); + } + + function updateStates(seriesModel: SeriesModel, view: ChartView): void { const stateAnimationModel = seriesModel.getModel('stateAnimation'); const enableAnimation = seriesModel.isAnimationEnabled(); 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. + if (el.__dirty && el.states && el.states.emphasis) { + const prevStates = (el as DisplayableWithStatesHistory).__prevStates; + if (prevStates) { + el.useStates(prevStates); + } + // Update state transition and enable animation again. if (enableAnimation) { - // TODO textContent? graphic.setStateTransition(el, stateAnimationModel); - } - else if (el.stateTransition) { - el.stateTransition = null; + const textContent = el.getTextContent(); + const textGuide = el.getTextGuideLine(); + // TODO Is it necessary to animate label? + if (textContent) { + graphic.setStateTransition(textContent, stateAnimationModel); + } + if (textGuide) { + graphic.setStateTransition(textGuide, stateAnimationModel); + } } } }); @@ -1805,7 +1847,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); }; @@ -1904,11 +1946,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 updateStates: (model: 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; diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index 276ac1b5f4..07e1835321 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -259,7 +259,7 @@ class LabelManager { position: (layoutOption.x != null || layoutOption.y != null) ? null : defaultLabelAttr.attachedPos, // Ignore rotation config on the host el if rotation is changed. - rotation: layoutOption.rotation != null ? null : defaultLabelAttr.attachedRot, + rotation: layoutOption.rotation != null ? layoutOption.rotation : defaultLabelAttr.attachedRot, offset: [layoutOption.dx || 0, layoutOption.dy || 0] }); } diff --git a/src/util/graphic.ts b/src/util/graphic.ts index 182b69cb78..e31a7828ad 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -480,17 +480,6 @@ export function enableElementHoverEmphasis(el: Displayable, hoverStl?: ZRStylePr } el.stateProxy = elementStateProxy; - - // 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) { - // singleLeaveEmphasis(el); - singleEnterEmphasis(el); - } } export function enterEmphasisWhenMouseOver(el: Element, e: ElementEvent) { From c94b74512cec5c7c3b424ad3ee10d85334ffbebe Mon Sep 17 00:00:00 2001 From: pissang Date: Mon, 18 May 2020 13:41:45 +0800 Subject: [PATCH 15/82] fix: fix rotation origin in graph and tree. fix itemStyle in and level of sunbrst. --- src/chart/graph/GraphView.ts | 4 ++-- src/chart/sunburst.ts | 2 ++ src/chart/sunburst/sunburstVisual.ts | 4 +--- src/chart/tree/TreeView.ts | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/chart/graph/GraphView.ts b/src/chart/graph/GraphView.ts index 8f83896053..e9e2559d4f 100644 --- a/src/chart/graph/GraphView.ts +++ b/src/chart/graph/GraphView.ts @@ -272,8 +272,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 = {}), { 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/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 5ec59adc11..dc01b8bb7a 100644 --- a/src/chart/tree/TreeView.ts +++ b/src/chart/tree/TreeView.ts @@ -518,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'); } From 0d467b1e0eb86e881829640fa90e5df9319a0511 Mon Sep 17 00:00:00 2001 From: pissang Date: Mon, 18 May 2020 13:45:30 +0800 Subject: [PATCH 16/82] fix: fix textContent z on hover. --- src/util/graphic.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/util/graphic.ts b/src/util/graphic.ts index e31a7828ad..412608591b 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -480,6 +480,14 @@ export function enableElementHoverEmphasis(el: Displayable, hoverStl?: ZRStylePr } el.stateProxy = elementStateProxy; + const textContent = el.getTextContent(); + const textGuide = el.getTextGuideLine(); + if (textContent) { + textContent.stateProxy = elementStateProxy; + } + if (textGuide) { + textGuide.stateProxy = elementStateProxy; + } } export function enterEmphasisWhenMouseOver(el: Element, e: ElementEvent) { From e4c130826810597ffff8c1b031897a9128c4f722 Mon Sep 17 00:00:00 2001 From: pissang Date: Mon, 18 May 2020 14:13:08 +0800 Subject: [PATCH 17/82] fix: still compat barBorderRadius, barBorderColor --- src/preprocessor/backwardCompat.ts | 39 ++++++++++++++++++++++++-- src/preprocessor/helper/compatStyle.ts | 2 +- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/preprocessor/backwardCompat.ts b/src/preprocessor/backwardCompat.ts index 9de1bf67c4..b83c529744 100644 --- a/src/preprocessor/backwardCompat.ts +++ b/src/preprocessor/backwardCompat.ts @@ -19,11 +19,12 @@ // 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'; function get(opt: Dictionary, path: string): any { const pathArr = path.split(','); @@ -70,6 +71,24 @@ const COMPATITABLE_COMPONENTS = [ 'grid', 'geo', 'parallel', 'legend', 'toolbox', 'title', 'visualMap', 'dataZoom', 'timeline' ]; +function compatBarItemStyle(option: Dictionary) { + const itemStyle = option && option.itemStyle; + if (itemStyle) { + if (itemStyle.barBorderRadius != null) { + itemStyle.barderRadius = itemStyle.barBorderRadius; + if (__DEV__) { + deprecateLog('barBorderRadius has been changed to borderRadius.'); + } + } + if (itemStyle.barBorderColor != null) { + itemStyle.borderColor = itemStyle.barBorderColor; + if (__DEV__) { + deprecateLog('barBorderColor has been changed to borderColor.'); + } + } + } +} + export default function (option: ECUnitOption, isTheme?: boolean) { compatStyle(option, isTheme); @@ -88,6 +107,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 +115,7 @@ export default function (option: ECUnitOption, isTheme?: boolean) { if (seriesOpt.clockWise != null) { // @ts-ignore seriesOpt.clockwise = seriesOpt.clockWise; + deprecateLog('clockWise has been changed to clockwise.'); } } else if (seriesType === 'gauge') { @@ -102,6 +123,20 @@ export default function (option: ECUnitOption, isTheme?: boolean) { pointerColor != null && set(seriesOpt, 'itemStyle.color', pointerColor); } + else if (seriesType === 'bar') { + compatBarItemStyle(seriesOpt); + // @ts-ignore + compatBarItemStyle(seriesOpt.emphasis); + const data = seriesOpt.data; + if (data && !isTypedArray(data)) { + for (let i = 0; i < data.length; i++) { + if (data[i]) { + 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; } From 6b86fff73f57f0e177b90ab749b0d1569883f0c8 Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 19 May 2020 14:19:32 +0800 Subject: [PATCH 18/82] fix: use highlighted and selected flag to determine if apply state. --- src/chart/pie/PieView.ts | 19 ++++++++++--------- src/echarts.ts | 15 ++++++++++++++- src/util/graphic.ts | 9 ++++----- src/util/types.ts | 3 +++ 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index f194434f61..0f2865a52c 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -24,7 +24,7 @@ import * as graphic from '../../util/graphic'; import ChartView from '../../view/Chart'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../ExtensionAPI'; -import { Payload, ColorString } from '../../util/types'; +import { Payload, ColorString, ECElement } from '../../util/types'; import List from '../../data/List'; import PieSeriesModel, {PieDataItemOption} from './PieSeries'; @@ -45,11 +45,6 @@ function updateDataSelected( name: name, seriesId: seriesModel.id }); - - data.each(function (idx) { - const el = data.getItemGraphicEl(idx); - el.toggleState('select', seriesModel.isSelected(data.getName(idx))); - }); } /** @@ -166,8 +161,8 @@ class PiePiece extends graphic.Sector { graphic.enableHoverEmphasis(this); - // Switch after `select` state updated. - sector.toggleState('select', seriesModel.isSelected(data.getName(idx))); + // 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 { @@ -296,11 +291,17 @@ 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; diff --git a/src/echarts.ts b/src/echarts.ts index c590c587fb..7420e3affd 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1779,6 +1779,7 @@ class ECharts extends Eventful { interface DisplayableWithStatesHistory extends Displayable { __prevStates: string[] }; + // Clear states without animation. // TODO States on component. function clearStates(seriesModel: SeriesModel, view: ChartView): void { view.group.traverse(function (el: Displayable) { @@ -1787,7 +1788,6 @@ class ECharts extends Eventful { (el as DisplayableWithStatesHistory).__prevStates = el.currentStates; const textContent = el.getTextContent(); const textGuide = el.getTextGuideLine(); - // Not use animation when clearStates and restore states in `updateStates` if (el.stateTransition) { el.stateTransition = null; } @@ -1809,6 +1809,7 @@ class ECharts extends Eventful { // Only updated on changed element. In case element is incremental and don't wan't to rerender. if (el.__dirty && el.states && el.states.emphasis) { const prevStates = (el as DisplayableWithStatesHistory).__prevStates; + // Restore states without animation if (prevStates) { el.useStates(prevStates); } @@ -1825,6 +1826,18 @@ class ECharts extends Eventful { graphic.setStateTransition(textGuide, stateAnimationModel); } } + + // The use higlighted and selected flag to toggle states. + const states = []; + if ((el as ECElement).selected) { + states.push('select'); + } + if ((el as ECElement).highlighted) { + states.push('emphasis'); + } + el.useStates(states); + // el.toggleState('select', (el as ECElement).selected); + // el.toggleState('emphasis', (el as ECElement).highlighted); } }); }; diff --git a/src/util/graphic.ts b/src/util/graphic.ts index 412608591b..308c6e26dd 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -106,7 +106,6 @@ type ExtendShapeReturn = ReturnType; type ExtendedProps = { - __highlighted?: boolean | 'layer' | 'plain' __highByOuter: number __highDownSilentOnTouch: boolean @@ -371,7 +370,7 @@ 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) { @@ -385,7 +384,7 @@ function singleEnterEmphasis(el: Element) { function singleLeaveEmphasis(el: Element) { el.removeState('emphasis'); - (el as ExtendedElement).__highlighted = false; + (el as ECElement).highlighted = false; } function updateElementState( @@ -398,9 +397,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); } diff --git a/src/util/types.ts b/src/util/types.ts index d9bbd1befa..88868b1e63 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; } export interface DataHost { From 0bb61d024309602b02314d636d835c443ae3d3e6 Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 19 May 2020 14:20:59 +0800 Subject: [PATCH 19/82] fix: exclude default stroke when intersecting labels. --- src/label/LabelManager.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index 07e1835321..8d203e42c6 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -325,6 +325,9 @@ class LabelManager { const globalRect = localRect.clone(); globalRect.applyTransform(transform); + // Text has a default 1px stroke. Exclude this. + globalRect.width -= 3; + globalRect.height -= 3; let obb = isAxisAligned ? new OrientedBoundingRect(localRect, transform) : null; let overlapped = false; From 601259d75d48eaa7e8c2164f7d1207c86116dac8 Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 26 May 2020 15:15:42 +0800 Subject: [PATCH 20/82] fix: fix hover scale in graph and tree --- src/chart/graph/GraphView.ts | 4 ++-- src/chart/helper/Symbol.ts | 29 +++++++++++++++++++---------- src/chart/tree/TreeView.ts | 4 ++-- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/chart/graph/GraphView.ts b/src/chart/graph/GraphView.ts index e9e2559d4f..f1dd3b1d70 100644 --- a/src/chart/graph/GraphView.ts +++ b/src/chart/graph/GraphView.ts @@ -448,8 +448,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/Symbol.ts b/src/chart/helper/Symbol.ts index 7bb3cd5e12..b67240a56a 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; @@ -174,8 +174,8 @@ class Symbol extends graphic.Group { const fadeIn = true; const target: PathProps = { - scaleX: this._scaleX, - scaleY: this._scaleY + scaleX: this._sizeX, + scaleY: this._sizeY }; fadeIn && (target.style = { opacity: symbolPath.style.opacity @@ -276,16 +276,14 @@ class Symbol extends graphic.Group { return useNameLabel ? data.getName(idx) : getDefaultLabel(data, idx); } - this._scaleX = symbolSize[0] / 2; - this._scaleY = symbolSize[1] / 2; + this._sizeX = symbolSize[0] / 2; + this._sizeY = symbolSize[1] / 2; symbolPath.ensureState('emphasis').style = hoverItemStyle; if (hoverAnimation && seriesModel.isAnimationEnabled()) { - const scaleEmphasisState = this.ensureState('emphasis'); - const scale = Math.max(1.1, 3 / this._scaleY + 1); - scaleEmphasisState.scaleX = scale; - scaleEmphasisState.scaleY = scale; + this.ensureState('emphasis'); + this.setSymbolScale(1); } else { this.states.emphasis = null; @@ -294,6 +292,17 @@ class Symbol extends graphic.Group { 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?: { keepLabel: boolean }) { diff --git a/src/chart/tree/TreeView.ts b/src/chart/tree/TreeView.ts index dc01b8bb7a..0910809a07 100644 --- a/src/chart/tree/TreeView.ts +++ b/src/chart/tree/TreeView.ts @@ -358,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); }); } From ea1978b4d0b6dee41a46d48cdcb1f9866a633050 Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 26 May 2020 15:16:05 +0800 Subject: [PATCH 21/82] fix: use fill instead of textFIll for text --- src/chart/helper/Line.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chart/helper/Line.ts b/src/chart/helper/Line.ts index 0ec7ce5085..d67694d48f 100644 --- a/src/chart/helper/Line.ts +++ b/src/chart/helper/Line.ts @@ -277,7 +277,7 @@ class Line extends graphic.Group { // Only these properties supported in this emphasis style here. emphasisState.style = { text: emphasisText as string, - textFill: hoverLabelModel.getTextColor(true), + fill: hoverLabelModel.getTextColor(true), // For merging hover style to normal style, do not use // `hoverLabelModel.getFont()` here. fontStyle: hoverLabelModel.getShallow('fontStyle'), From bce74350da4b1930fe49ceaa40421455b46add01 Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 27 May 2020 10:29:56 +0800 Subject: [PATCH 22/82] fix(bar): compat barBorderWidth --- src/preprocessor/backwardCompat.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/preprocessor/backwardCompat.ts b/src/preprocessor/backwardCompat.ts index b83c529744..c30362483f 100644 --- a/src/preprocessor/backwardCompat.ts +++ b/src/preprocessor/backwardCompat.ts @@ -71,19 +71,23 @@ 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) { - if (itemStyle.barBorderRadius != null) { - itemStyle.barderRadius = itemStyle.barBorderRadius; - if (__DEV__) { - deprecateLog('barBorderRadius has been changed to borderRadius.'); - } - } - if (itemStyle.barBorderColor != null) { - itemStyle.borderColor = itemStyle.barBorderColor; - if (__DEV__) { - deprecateLog('barBorderColor has been changed to borderColor.'); + 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}.`); + } } } } From 6fa462fcdb712585164a41e3a0034cb4002ca73b Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 27 May 2020 14:57:55 +0800 Subject: [PATCH 23/82] fix: compat barBorderXXX in backgroundStyle. rename barBorderRadius to borderRadius --- src/chart/bar/BarSeries.ts | 2 +- src/chart/bar/BarView.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chart/bar/BarSeries.ts b/src/chart/bar/BarSeries.ts index 7d3c9b9424..c21e0d5ec5 100644 --- a/src/chart/bar/BarSeries.ts +++ b/src/chart/bar/BarSeries.ts @@ -32,7 +32,7 @@ export interface BarItemStyleOption extends ItemStyleOption { /** * Border radius is not supported for bar on polar */ - barBorderRadius?: number | number[] + borderRadius?: number | number[] } export interface BarDataItemOption { name?: string diff --git a/src/chart/bar/BarView.ts b/src/chart/bar/BarView.ts index dd88cef513..90b377749c 100644 --- a/src/chart/bar/BarView.ts +++ b/src/chart/bar/BarView.ts @@ -543,7 +543,7 @@ function updateStyle( const hoverStyle = itemModel.getModel(['emphasis', 'itemStyle']).getItemStyle(); if (!isPolar) { - (el as Rect).setShape('r', itemModel.get(['itemStyle', 'barBorderRadius']) || 0); + (el as Rect).setShape('r', itemModel.get(['itemStyle', 'borderRadius']) || 0); } el.useStyle(style); From 34e4cb0f6a282b493abef79426cc09083780e8f7 Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 28 May 2020 13:39:34 +0800 Subject: [PATCH 24/82] feat: label and labelLine animation --- src/chart/pie/PieView.ts | 31 +++------ src/chart/pie/labelLayout.ts | 5 ++ src/echarts.ts | 15 +++-- src/label/LabelManager.ts | 118 +++++++++++++++++++++++++++------- src/label/labelGuideHelper.ts | 2 +- 5 files changed, 120 insertions(+), 51 deletions(-) diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index 0f2865a52c..8187c6a389 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -136,9 +136,7 @@ class PiePiece extends graphic.Sector { const cursorStyle = itemModel.getShallow('cursor'); cursorStyle && sector.attr('cursor', cursorStyle); - // 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._updateLabel(data, idx); const emphasisState = sector.ensureState('emphasis'); emphasisState.shape = { @@ -165,12 +163,11 @@ class PiePiece extends graphic.Sector { (sector as ECElement).selected = seriesModel.isSelected(data.getName(idx)); } - private _updateLabel(data: List, idx: number, withAnimation: boolean): void { + private _updateLabel(data: List, idx: number): void { const sector = this; const labelLine = sector.getTextGuideLine(); const labelText = sector.getTextContent(); - const seriesModel = data.hostModel; const itemModel = data.getItemModel(idx); const layout = data.getItemLayout(idx); const labelLayout = layout.label; @@ -225,25 +222,15 @@ class PiePiece extends graphic.Sector { outsideFill: visualColor }); - const targetTextPos = { + labelLine.attr({ + shape: targetLineShape + }); + // Make sure update style on labelText after setLabelStyle. + // Because setLabelStyle will replace a new style on it. + labelText.attr({ x: labelLayout.x, y: labelLayout.y - }; - if (withAnimation) { - graphic.updateProps(labelLine, { - shape: targetLineShape - }, seriesModel, idx); - - graphic.updateProps(labelText, targetTextPos, 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(targetTextPos); - } + }); labelText.attr({ rotation: labelLayout.rotation, diff --git a/src/chart/pie/labelLayout.ts b/src/chart/pie/labelLayout.ts index 04542b508a..d1da84df2a 100644 --- a/src/chart/pie/labelLayout.ts +++ b/src/chart/pie/labelLayout.ts @@ -323,6 +323,11 @@ export default function ( const text = seriesModel.getFormattedLabel(idx, 'normal') || data.getName(idx); const textRect = textContain.getBoundingRect(text, font, textAlign, 'top'); + // Text has a default 1px stroke. Exclude this. + textRect.x -= 1; + textRect.y -= 1; + textRect.width += 2.1; + textRect.height += 2.1; const isLabelInside = labelPosition === 'inside' || labelPosition === 'inner'; if (labelPosition === 'center') { diff --git a/src/echarts.ts b/src/echarts.ts index 6723c9dbb3..3a68ce7def 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1097,6 +1097,7 @@ class ECharts extends Eventful { const labelManager = this._labelManager; labelManager.updateLayoutConfig(this._api); labelManager.layout(); + labelManager.animateLabels(); } appendData(params: { @@ -1722,16 +1723,20 @@ class ECharts extends Eventful { // Add labels. labelManager.addLabelsOfSeries(chartView); - // NOTE: Update states after label is added. - // Because in LabelManager#addLabel. It will cache the properties(transform, textConfig) of label. - // We need to cache the normal state. Not other states. - updateStates(seriesModel, chartView); }); scheduler.unfinished = unfinished || scheduler.unfinished; labelManager.updateLayoutConfig(api); labelManager.layout(); + labelManager.animateLabels(); + + 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 // TODO @@ -1877,8 +1882,6 @@ class ECharts extends Eventful { states.push('emphasis'); } el.useStates(states); - // el.toggleState('select', (el as ECElement).selected); - // el.toggleState('emphasis', (el as ECElement).highlighted); } }); }; diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index 8d203e42c6..3cbdef573f 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -25,7 +25,9 @@ import { Point, BoundingRect, getECData, - Polyline + Polyline, + updateProps, + initProps } from '../util/graphic'; import { MatrixArray } from 'zrender/src/core/matrix'; import ExtensionAPI from '../ExtensionAPI'; @@ -38,10 +40,13 @@ import { } from '../util/types'; import { parsePercent } from '../util/number'; import ChartView from '../view/Chart'; -import { ElementTextConfig, ElementTextGuideLineConfig } from 'zrender/src/Element'; +import { ElementTextConfig } from 'zrender/src/Element'; import { RectLike } from 'zrender/src/core/BoundingRect'; import Transformable from 'zrender/src/core/Transformable'; import { updateLabelGuideLine } from './labelGuideHelper'; +import SeriesModel from '../model/Series'; +import { makeInner } from '../util/model'; +import { retrieve2, guid, each } from 'zrender/src/core/util'; interface DisplayedLabelItem { label: ZRText @@ -56,7 +61,7 @@ interface LabelLayoutDesc { label: ZRText labelGuide: Polyline - seriesIndex: number + seriesModel: SeriesModel dataIndex: number layoutOption: LabelLayoutOptionCallback | LabelLayoutOption @@ -100,7 +105,7 @@ function prepareLayoutCallbackParams(labelItem: LabelLayoutDesc): LabelLayoutOpt const label = labelItem.label; return { dataIndex: labelItem.dataIndex, - seriesIndex: labelItem.seriesIndex, + seriesIndex: labelItem.seriesModel.seriesIndex, text: labelItem.label.style.text, rect: labelItem.hostRect, labelRect: labelAttr.rect, @@ -115,26 +120,38 @@ const LABEL_OPTION_TO_STYLE_KEYS = ['align', 'verticalAlign', 'width', 'height'] const dummyTransformable = new Transformable(); +const labelAnimationStore = makeInner<{ + oldLayout: { + x: number, + y: number, + rotation: number + } +}, ZRText>(); + +const labelLineAnimationStore = makeInner<{ + oldLayout: { + points: number[][] + } +}, Polyline>(); + class LabelManager { private _labelList: LabelLayoutDesc[] = []; + private _chartViewList: ChartView[] = []; constructor() {} clearLabels() { this._labelList = []; + this._chartViewList = []; } /** * Add label to manager - * @param dataIndex - * @param seriesIndex - * @param label - * @param layoutOption */ addLabel( dataIndex: number, - seriesIndex: number, + seriesModel: SeriesModel, label: ZRText, layoutOption: LabelLayoutDesc['layoutOption'] ) { @@ -171,7 +188,7 @@ class LabelManager { label, labelGuide: labelGuide, - seriesIndex, + seriesModel, dataIndex, layoutOption, @@ -215,6 +232,8 @@ class LabelManager { } addLabelsOfSeries(chartView: ChartView) { + this._chartViewList.push(chartView); + const seriesModel = chartView.__model; const layoutOption = seriesModel.get('labelLayout'); chartView.group.traverse((child) => { @@ -226,7 +245,7 @@ class LabelManager { const textEl = child.getTextContent(); const dataIndex = getECData(child).dataIndex; if (textEl && dataIndex != null) { - this.addLabel(dataIndex, seriesModel.seriesIndex, textEl, layoutOption); + this.addLabel(dataIndex, seriesModel, textEl, layoutOption); } }); } @@ -251,6 +270,7 @@ class LabelManager { } layoutOption = layoutOption || {}; + if (hostEl) { hostEl.setTextConfig({ // Force to set local false. @@ -288,10 +308,7 @@ class LabelManager { 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] - ); + label.setStyle(key, layoutOption[key] != null ? layoutOption[key] : defaultLabelAttr.style[key]); } labelItem.overlap = layoutOption.overlap; @@ -325,9 +342,6 @@ class LabelManager { const globalRect = localRect.clone(); globalRect.applyTransform(transform); - // Text has a default 1px stroke. Exclude this. - globalRect.width -= 3; - globalRect.height -= 3; let obb = isAxisAligned ? new OrientedBoundingRect(localRect, transform) : null; let overlapped = false; @@ -362,14 +376,10 @@ class LabelManager { const labelGuide = labelItem.labelGuide; // TODO Callback to determine if this overlap should be handled? if (overlapped) { - // label.setStyle({ opacity: 0.1 }); - // label.z = 0; label.hide(); labelGuide && labelGuide.hide(); } else { - // TODO Restore z - // label.setStyle({ opacity: 1 }); label.attr('ignore', labelItem.defaultAttr.ignore); labelGuide && labelGuide.attr('ignore', labelItem.defaultAttr.labelGuideIgnore); @@ -391,6 +401,70 @@ class LabelManager { ); } } + + animateLabels() { + each(this._chartViewList, function (chartView) { + const seriesModel = chartView.__model; + if (!seriesModel.isAnimationEnabled()) { + 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 guideLine = child.getTextGuideLine(); + + if (textEl && !textEl.ignore && !textEl.invisible) { + const layoutStore = labelAnimationStore(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); + updateProps(textEl, newProps, seriesModel); + } + layoutStore.oldLayout = newProps; + } + + 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; + } + }); + }); + } } diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts index b20511aa7d..da4848b18d 100644 --- a/src/label/labelGuideHelper.ts +++ b/src/label/labelGuideHelper.ts @@ -346,7 +346,7 @@ export function updateLabelGuideLine( for (let i = 0; i < searchSpace.length; i++) { const candidate = searchSpace[i]; getCandidateAnchor(candidate, 0, labelRect, pt0, dir); - Point.scaleAndAdd(pt1, pt0, dir, labelGuideConfig.len); + Point.scaleAndAdd(pt1, pt0, dir, labelGuideConfig.len == null ? 15 : labelGuideConfig.len); const dist = anchorPoint ? anchorPoint.distance(pt1) : (target instanceof Path From 906e36324fa8677bfebfca574050d171b5496db3 Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 28 May 2020 13:40:24 +0800 Subject: [PATCH 25/82] fix: compat barBorderXXX in backgroundStyle --- src/preprocessor/backwardCompat.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/preprocessor/backwardCompat.ts b/src/preprocessor/backwardCompat.ts index c30362483f..13c22a00bc 100644 --- a/src/preprocessor/backwardCompat.ts +++ b/src/preprocessor/backwardCompat.ts @@ -25,6 +25,7 @@ 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'; function get(opt: Dictionary, path: string): any { const pathArr = path.split(','); @@ -129,6 +130,7 @@ export default function (option: ECUnitOption, isTheme?: boolean) { } else if (seriesType === 'bar') { compatBarItemStyle(seriesOpt); + compatBarItemStyle((seriesOpt as BarSeriesOption).backgroundStyle); // @ts-ignore compatBarItemStyle(seriesOpt.emphasis); const data = seriesOpt.data; From 678b42d5797c3b975cdacef834e0aa6fffed078d Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 28 May 2020 13:41:56 +0800 Subject: [PATCH 26/82] fix: fix class error in ES6 built file --- src/util/clazz.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/util/clazz.ts b/src/util/clazz.ts index ddf3549ded..7507bdfad9 100644 --- a/src/util/clazz.ts +++ b/src/util/clazz.ts @@ -99,17 +99,25 @@ export function enableClassExtend(rootClz: ExtendableConstructor, mandatoryMetho // constructor. // If this constructor/$constructor is declared, it is responsible for // calling the super constructor. - const ExtendedClass = (class { - constructor() { - if (!proto.$constructor) { - superClass.apply(this, arguments); + function ExtendedClass(this: any, ...args: any[]) { + if (!proto.$constructor) { + try { + // Will throw error if superClass is a es6 native class. + superClass.apply(this, args); } - else { - proto.$constructor.apply(this, arguments); + catch (e) { + const ins = zrUtil.createObject( + // @ts-ignore + ExtendedClass.prototype, new superClass(...args) + ); + return ins; } } - static [IS_EXTENDED_CLASS] = true; - }) as ExtendableConstructor; + else { + proto.$constructor.apply(this, arguments); + } + }; + ExtendedClass[IS_EXTENDED_CLASS] = true; zrUtil.extend(ExtendedClass.prototype, proto); @@ -119,7 +127,7 @@ export function enableClassExtend(rootClz: ExtendableConstructor, mandatoryMetho zrUtil.inherits(ExtendedClass, this); ExtendedClass.superClass = superClass; - return ExtendedClass as ExtendableConstructor; + return ExtendedClass as unknown as ExtendableConstructor; }; } From fba40c7a5ef29381ccb6daabdd7ce1510b04698d Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 28 May 2020 13:43:05 +0800 Subject: [PATCH 27/82] fix: fix not existed emphasis state caused error. --- src/util/graphic.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/util/graphic.ts b/src/util/graphic.ts index 72bf70e45f..36d7fd3e34 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -462,7 +462,9 @@ function elementStateProxy(this: Displayable, stateName: string): DisplayableSta state.style = emphasisStyle; } } - state.z2 = this.z2 + Z2_EMPHASIS_LIFT; + if (state) { + state.z2 = this.z2 + Z2_EMPHASIS_LIFT; + } } return state; From c11498d89e53949a1f62b0fc7d0ae0697e4d0146 Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 28 May 2020 19:06:23 +0800 Subject: [PATCH 28/82] refact: layout label after render in pie. --- src/chart/pie/PieView.ts | 53 +++--------- src/chart/pie/labelLayout.ts | 160 ++++++++++++++++++++++------------- src/chart/pie/pieLayout.ts | 11 +-- 3 files changed, 117 insertions(+), 107 deletions(-) diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index 8187c6a389..f52c6f75c5 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -27,6 +27,7 @@ import ExtensionAPI from '../../ExtensionAPI'; import { Payload, ColorString, ECElement } from '../../util/types'; import List from '../../data/List'; import PieSeriesModel, {PieDataItemOption} from './PieSeries'; +import labelLayout from './labelLayout'; function updateDataSelected( this: PiePiece, @@ -74,11 +75,6 @@ class PiePiece extends graphic.Sector { 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); @@ -136,7 +132,7 @@ class PiePiece extends graphic.Sector { const cursorStyle = itemModel.getShallow('cursor'); cursorStyle && sector.attr('cursor', cursorStyle); - this._updateLabel(data, idx); + this._updateLabel(seriesModel, data, idx); const emphasisState = sector.ensureState('emphasis'); emphasisState.shape = { @@ -150,12 +146,11 @@ class PiePiece extends graphic.Sector { labelLine.states.select = { x: dx, y: dy }; - if (layout.label) { - labelText.states.select = { - x: layout.label.x + dx, - y: layout.label.y + dy - }; - } + // TODO: needs dx, dy in zrender? + labelText.states.select = { + x: dx, + y: dy + }; graphic.enableHoverEmphasis(this); @@ -163,32 +158,16 @@ class PiePiece extends graphic.Sector { (sector as ECElement).selected = seriesModel.isSelected(data.getName(idx)); } - private _updateLabel(data: List, idx: number): void { + private _updateLabel(seriesModel: PieSeriesModel, data: List, idx: number): void { const sector = this; const labelLine = sector.getTextGuideLine(); const labelText = sector.getTextContent(); 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'); @@ -204,11 +183,10 @@ class PiePiece extends graphic.Sector { { labelFetcher: data.hostModel as PieSeriesModel, labelDataIndex: idx, - defaultText: labelLayout.text + defaultText: seriesModel.getFormattedLabel(idx, 'normal') + || data.getName(idx) }, { - align: labelLayout.textAlign, - verticalAlign: labelLayout.verticalAlign, opacity: style && style.opacity } ); @@ -216,24 +194,15 @@ class PiePiece extends graphic.Sector { // Set textConfig on sector. sector.setTextConfig({ local: true, - inside: !!labelLayout.inside, insideStroke: visualColor, // insideFill: 'auto', outsideFill: visualColor }); - labelLine.attr({ - shape: targetLineShape - }); // Make sure update style on labelText after setLabelStyle. // Because setLabelStyle will replace a new style on it. - labelText.attr({ - x: labelLayout.x, - y: labelLayout.y - }); labelText.attr({ - rotation: labelLayout.rotation, z2: 10 }); @@ -338,6 +307,8 @@ class PieView extends ChartView { }) .execute(); + labelLayout(seriesModel); + // Always use initial animation. if (seriesModel.get('animationTypeUpdate') !== 'expansion') { this._data = data; diff --git a/src/chart/pie/labelLayout.ts b/src/chart/pie/labelLayout.ts index d1da84df2a..90c53e5afe 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, VerticalAlign, ZRRectLike, ZRTextAlign } from '../../util/types'; +import { Sector, Polyline } from '../../util/graphic'; +import ZRText from 'zrender/src/graphic/Text'; +import { RectLike } from 'zrender/src/core/BoundingRect'; +import Displayable from 'zrender/src/graphic/Displayable'; +import { each } from 'zrender/src/core/util'; 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 linePoints: VectorArray[] textAlign: HorizontalAlign verticalAlign: VerticalAlign, rotation: number, - inside: boolean, labelDistance: number, labelAlignTo: PieSeriesOption['label']['alignTo'], labelMargin: number, bleedMargin: PieSeriesOption['label']['bleedMargin'], - textRect: ZRRectLike, - text: string, - font: string + textRect: ZRRectLike } function adjustSingleSide( @@ -73,7 +74,7 @@ function adjustSingleSide( list[j].y += delta; if (j > start && j + 1 < end - && list[j + 1].y > list[j].y + list[j].height + && list[j + 1].y > list[j].y + list[j].textRect.height ) { shiftUp(j, delta / 2); return; @@ -91,7 +92,7 @@ function adjustSingleSide( list[j].y -= delta; if (j > 0 - && list[j].y > list[j - 1].y + list[j - 1].height + && list[j].y > list[j - 1].y + list[j - 1].textRect.height ) { break; } @@ -155,7 +156,7 @@ function adjustSingleSide( if (delta < 0) { shiftDown(i, len, -delta, dir); } - lastY = list[i].y + list[i].height; + lastY = list[i].y + list[i].textRect.height; } if (viewHeight - lastY < 0) { shiftUp(len - 1, lastY - viewHeight); @@ -236,8 +237,11 @@ function avoidOverlap( if (targetTextWidth < layout.textRect.width) { // TODOTODO // layout.text = textContain.truncateText(layout.text, targetTextWidth, layout.font); + layout.label.style.width = targetTextWidth; + layout.label.style.overflow = 'truncate'; if (layout.labelAlignTo === 'edge') { - realTextWidth = textContain.getWidth(layout.text, layout.font); + realTextWidth = targetTextWidth; + // realTextWidth = textContain.getWidth(layout.text, layout.font); } } @@ -265,18 +269,13 @@ function avoidOverlap( } } -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 +284,18 @@ 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; + 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'); @@ -296,7 +305,6 @@ export default function ( const labelAlignTo = labelModel.get('alignTo'); const labelMargin = parsePercent(labelModel.get('margin'), viewWidth); const bleedMargin = labelModel.get('bleedMargin'); - const font = labelModel.getFont(); const labelLineModel = itemModel.getModel('labelLine'); let labelLineLen = labelLineModel.get('length'); @@ -304,25 +312,23 @@ export default function ( let labelLineLen2 = labelLineModel.get('length2'); labelLineLen2 = parsePercent(labelLineLen2, viewWidth); - if (layout.angle < minShowLabelRadian) { + if (Math.abs(sectorShape.endAngle - sectorShape.startAngle) < minShowLabelRadian) { return; } - const midAngle = (layout.startAngle + layout.endAngle) / 2; + const midAngle = (sectorShape.startAngle + sectorShape.endAngle) / 2; const dx = Math.cos(midAngle); const dy = 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 textRect = label.getBoundingRect().clone(); // Text has a default 1px stroke. Exclude this. textRect.x -= 1; textRect.y -= 1; @@ -331,21 +337,21 @@ export default function ( 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 * dx : sectorShape.r * dx) + cx; + const y1 = (isLabelInside ? (sectorShape.r + sectorShape.r0) / 2 * dy : sectorShape.r * dy) + cy; textX = x1 + dx * 3; textY = y1 + dy * 3; if (!isLabelInside) { // For roseType - const x2 = x1 + dx * (labelLineLen + r - layout.r); - const y2 = y1 + dy * (labelLineLen + r - layout.r); + const x2 = x1 + dx * (labelLineLen + r - sectorShape.r); + const y2 = y1 + dy * (labelLineLen + r - sectorShape.r); const x3 = x2 + ((dx < 0 ? -1 : 1) * labelLineLen2); const y3 = y2; @@ -381,33 +387,69 @@ export default function ( } 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 + + // Not sectorShape the inside label if (!isLabelInside) { - labelLayoutList.push(layout.label); + labelLayoutList.push({ + label, + labelLine, + x: textX, + y: textY, + position: labelPosition, + len: labelLineLen, + len2: labelLineLen2, + linePoints: linePoints, + textAlign: textAlign, + verticalAlign: 'middle', + rotation: labelRotate, + labelDistance: labelDistance, + labelAlignTo: labelAlignTo, + labelMargin: labelMargin, + bleedMargin: bleedMargin, + textRect: textRect + }); } + sector.setTextConfig({ + inside: isLabelInside + }); }); + if (!hasLabelRotate && seriesModel.get('avoidLabelOverlap')) { avoidOverlap(labelLayoutList, cx, cy, r, viewWidth, viewHeight, viewLeft, viewTop); } + + function setNotShow(el: {ignore: boolean}) { + el.ignore = true; + } + + for (let i = 0; i < labelLayoutList.length; i++) { + const layout = labelLayoutList[i]; + const label = layout.label; + const labelLine = layout.labelLine; + const notShowLabel = isNaN(layout.x) || isNaN(layout.y); + if (label) { + label.x = layout.x; + label.y = layout.y; + label.setStyle({ + align: layout.textAlign, + verticalAlign: layout.verticalAlign + }); + if (notShowLabel) { + each(label.states, setNotShow); + label.ignore = true; + } + const selectState = label.states.select; + if (selectState) { + selectState.x += layout.x; + selectState.y += layout.y; + } + } + if (labelLine) { + labelLine.setShape({ points: layout.linePoints }); + if (notShowLabel) { + each(labelLine.states, setNotShow); + labelLine.ignore = true; + } + } + } } \ 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 From 4062b9f55960447eb66053dfceaf3e7c7307bf4d Mon Sep 17 00:00:00 2001 From: pissang Date: Fri, 29 May 2020 09:45:34 +0800 Subject: [PATCH 29/82] feat: support state transition in geo component --- src/coord/geo/GeoModel.ts | 5 ++++- src/echarts.ts | 13 +++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/coord/geo/GeoModel.ts b/src/coord/geo/GeoModel.ts index 6c8dee38f4..5cd4827e89 100644 --- a/src/coord/geo/GeoModel.ts +++ b/src/coord/geo/GeoModel.ts @@ -32,7 +32,8 @@ import { ZRColor, LabelOption, DisplayState, - RoamOptionMixin + RoamOptionMixin, + AnimationOption } from '../../util/types'; import { NameMap } from './geoTypes'; import GlobalModel from '../../model/Global'; @@ -98,6 +99,8 @@ export interface GeoOption extends }; regions: RegoinOption[]; + + stateAnimation?: AnimationOption } const LABEL_FORMATTER_NORMAL = ['label', 'formatter'] as const; diff --git a/src/echarts.ts b/src/echarts.ts index 3a68ce7def..485e058e3d 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1670,9 +1670,14 @@ class ECharts extends Eventful { const componentModel = componentView.__model; componentView.render(componentModel, ecModel, api, payload); + clearStates(componentModel, componentView); + updateZ(componentModel, componentView); updateHoverEmphasisHandler(componentView); + + updateStates(componentModel, componentView); }); + }; /** @@ -1827,7 +1832,7 @@ class ECharts extends Eventful { }; // Clear states without animation. // TODO States on component. - function clearStates(seriesModel: SeriesModel, view: ChartView): void { + function clearStates(model: ComponentModel, view: ComponentView | ChartView): void { view.group.traverse(function (el: Displayable) { // TODO If el is incremental. if (el.hasState()) { @@ -1848,9 +1853,9 @@ class ECharts extends Eventful { }); } - function updateStates(seriesModel: SeriesModel, view: ChartView): void { - const stateAnimationModel = seriesModel.getModel('stateAnimation'); - const enableAnimation = seriesModel.isAnimationEnabled(); + function updateStates(model: ComponentModel, view: ComponentView | ChartView): void { + const stateAnimationModel = (model as SeriesModel).getModel('stateAnimation'); + const enableAnimation = model.isAnimationEnabled(); view.group.traverse(function (el: Displayable) { // Only updated on changed element. In case element is incremental and don't wan't to rerender. if (el.__dirty && el.states && el.states.emphasis) { From 5accd257d0e77f069c37e826927be7870e0f96f3 Mon Sep 17 00:00:00 2001 From: pissang Date: Mon, 1 Jun 2020 13:17:51 +0800 Subject: [PATCH 30/82] feat(pie): improve the shape of label layout --- src/chart/pie/labelLayout.ts | 130 ++++++++++++++++++++--------------- 1 file changed, 74 insertions(+), 56 deletions(-) diff --git a/src/chart/pie/labelLayout.ts b/src/chart/pie/labelLayout.ts index 90c53e5afe..ec057ec489 100644 --- a/src/chart/pie/labelLayout.ts +++ b/src/chart/pie/labelLayout.ts @@ -25,7 +25,6 @@ import { HorizontalAlign, VerticalAlign, ZRRectLike, ZRTextAlign } from '../../u import { Sector, Polyline } from '../../util/graphic'; import ZRText from 'zrender/src/graphic/Text'; import { RectLike } from 'zrender/src/core/BoundingRect'; -import Displayable from 'zrender/src/graphic/Displayable'; import { each } from 'zrender/src/core/util'; const RADIAN = Math.PI / 180; @@ -40,7 +39,6 @@ interface LabelLayout { len2: number linePoints: VectorArray[] textAlign: HorizontalAlign - verticalAlign: VerticalAlign, rotation: number, labelDistance: number, labelAlignTo: PieSeriesOption['label']['alignTo'], @@ -65,13 +63,17 @@ function adjustSingleSide( return a.y - b.y; }); + let adjusted = false; + function shiftDown(start: number, end: number, delta: number, dir: number) { for (let j = start; j < end; j++) { - if (list[j].y + delta > viewTop + viewHeight) { + if (list[j].y + delta + list[j].textRect.height / 2 > viewTop + viewHeight) { break; } list[j].y += delta; + adjusted = true; + if (j > start && j + 1 < end && list[j + 1].y > list[j].y + list[j].textRect.height @@ -86,11 +88,13 @@ function adjustSingleSide( function shiftUp(end: number, delta: number) { for (let j = end; j >= 0; j--) { - if (list[j].y - delta < viewTop) { + if (list[j].y - delta - list[j].textRect.height / 2 < viewTop) { break; } list[j].y -= delta; + adjusted = true; + if (j > 0 && list[j].y > list[j - 1].y + list[j - 1].textRect.height ) { @@ -99,52 +103,61 @@ function adjustSingleSide( } } - 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; - } + 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.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.x = cx + (dx + item.len2) * dir; + } + } - 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; + // Adjust X based on the shifted y. Make tight labels aligned on an ellipse curve. + function recalculateX(items: LabelLayout[]) { + // Extremes of + const topSemi = { list: [], maxY: 0} as SemiInfo; + const bottomSemi = { list: [], maxY: 0 } as SemiInfo; + + for (let i = 0; i < items.length; i++) { + if (items[i].labelAlignTo !== 'none') { + continue; } - if (!isDownList && deltaX <= lastDeltaX) { - // right-up, left-up - deltaX = lastDeltaX + 10; + const item = items[i]; + const semi = item.y > cy ? bottomSemi : topSemi; + const dy = Math.abs(item.y - cy); + if (dy > semi.maxY) { + const dx = item.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; @@ -161,16 +174,10 @@ function adjustSingleSide( 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 (adjusted) { + recalculateX(list); } - changeX(upList, false, cx, cy, r, dir); - changeX(downList, true, cx, cy, r, dir); } function avoidOverlap( @@ -291,6 +298,10 @@ export default function ( const viewTop = viewRect.y; const viewHeight = viewRect.height; + function setNotShow(el: {ignore: boolean}) { + el.ignore = true; + } + data.each(function (idx) { const sector = data.getItemGraphicEl(idx) as Sector; const sectorShape = sector.shape; @@ -313,6 +324,8 @@ export default function ( labelLineLen2 = parsePercent(labelLineLen2, viewWidth); if (Math.abs(sectorShape.endAngle - sectorShape.startAngle) < minShowLabelRadian) { + each(label.states, setNotShow); + label.ignore = true; return; } @@ -400,7 +413,6 @@ export default function ( len2: labelLineLen2, linePoints: linePoints, textAlign: textAlign, - verticalAlign: 'middle', rotation: labelRotate, labelDistance: labelDistance, labelAlignTo: labelAlignTo, @@ -409,6 +421,15 @@ export default function ( textRect: textRect }); } + else { + label.x = textX; + label.y = textY; + label.rotation = labelRotate; + label.setStyle({ + align: textAlign, + verticalAlign: 'middle' + }); + } sector.setTextConfig({ inside: isLabelInside }); @@ -418,10 +439,6 @@ export default function ( avoidOverlap(labelLayoutList, cx, cy, r, viewWidth, viewHeight, viewLeft, viewTop); } - function setNotShow(el: {ignore: boolean}) { - el.ignore = true; - } - for (let i = 0; i < labelLayoutList.length; i++) { const layout = labelLayoutList[i]; const label = layout.label; @@ -430,9 +447,10 @@ export default function ( if (label) { label.x = layout.x; label.y = layout.y; + label.rotation = layout.rotation; label.setStyle({ align: layout.textAlign, - verticalAlign: layout.verticalAlign + verticalAlign: 'middle' }); if (notShowLabel) { each(label.states, setNotShow); From d8b1f1cc5e27b89cfaed54dd88a93809ea989479 Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 2 Jun 2020 14:24:08 +0800 Subject: [PATCH 31/82] feat: add minTurnAngle in labelLine --- src/chart/pie/PieSeries.ts | 1 + src/chart/pie/PieView.ts | 2 +- src/chart/pie/labelLayout.ts | 10 +++- src/label/LabelManager.ts | 3 +- src/label/labelGuideHelper.ts | 91 ++++++++++++++++++++++++++++++++--- src/util/types.ts | 3 ++ 6 files changed, 99 insertions(+), 11 deletions(-) diff --git a/src/chart/pie/PieSeries.ts b/src/chart/pie/PieSeries.ts index 8e86ed93c0..e5c398bacb 100644 --- a/src/chart/pie/PieSeries.ts +++ b/src/chart/pie/PieSeries.ts @@ -272,6 +272,7 @@ class PieSeriesModel extends SeriesModel { // 引导线两段中的第二段长度 length2: 15, smooth: false, + minTurnAngle: 100, lineStyle: { // color: 各异, width: 1, diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index f52c6f75c5..6241d524a3 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -224,7 +224,7 @@ class PiePiece extends graphic.Sector { let smooth = labelLineModel.get('smooth'); if (smooth && smooth === true) { - smooth = 0.4; + smooth = 0.3; } labelLine.setShape({ smooth: smooth as number diff --git a/src/chart/pie/labelLayout.ts b/src/chart/pie/labelLayout.ts index ec057ec489..693008d44f 100644 --- a/src/chart/pie/labelLayout.ts +++ b/src/chart/pie/labelLayout.ts @@ -26,6 +26,7 @@ import { Sector, Polyline } from '../../util/graphic'; import ZRText from 'zrender/src/graphic/Text'; import { RectLike } from 'zrender/src/core/BoundingRect'; import { each } from 'zrender/src/core/util'; +import { limitTurnAngle } from '../../label/labelGuideHelper'; const RADIAN = Math.PI / 180; @@ -37,6 +38,7 @@ interface LabelLayout { position: PieSeriesOption['label']['position'], len: number len2: number + minTurnAngle: number linePoints: VectorArray[] textAlign: HorizontalAlign rotation: number, @@ -411,6 +413,7 @@ export default function ( position: labelPosition, len: labelLineLen, len2: labelLineLen2, + minTurnAngle: labelLineModel.get('minTurnAngle'), linePoints: linePoints, textAlign: textAlign, rotation: labelRotate, @@ -463,11 +466,14 @@ export default function ( } } if (labelLine) { - labelLine.setShape({ points: layout.linePoints }); - if (notShowLabel) { + if (notShowLabel || !layout.linePoints) { each(labelLine.states, setNotShow); labelLine.ignore = true; } + else { + limitTurnAngle(layout.linePoints, layout.minTurnAngle); + labelLine.setShape({ points: layout.linePoints }); + } } } } \ No newline at end of file diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index 3cbdef573f..d3cdfc5f6c 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -397,7 +397,8 @@ class LabelManager { label, globalRect, label.__hostTarget, - labelItem.hostRect + labelItem.hostRect, + labelItem.seriesModel.getModel(['labelLine']) ); } } diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts index da4848b18d..ddeba604cb 100644 --- a/src/label/labelGuideHelper.ts +++ b/src/label/labelGuideHelper.ts @@ -27,6 +27,8 @@ 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 { LabelGuideLineOption } from '../util/types'; +import Model from '../model/Model'; const PI2 = Math.PI * 2; const CMD = PathProxy.CMD; @@ -144,7 +146,9 @@ function projectPointToArc( } } -function projectPointToLine(x1: number, y1: number, x2: number, y2: number, x: number, y: number, out: number[]) { +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; @@ -157,7 +161,11 @@ function projectPointToLine(x1: number, y1: number, x2: number, y2: number, x: n // dot product const projectedLen = dx * dx1 + dy * dy1; - const t = Math.min(Math.max(projectedLen / lineLen, 0), 1); + 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; @@ -234,7 +242,7 @@ function nearestPointOnPath(pt: Point, path: PathProxy, out: Point) { yi = y0; break; case CMD.L: - d = projectPointToLine(xi, yi, data[i], data[i + 1], x, y, tmpPt); + d = projectPointToLine(xi, yi, data[i], data[i + 1], x, y, tmpPt, true); xi = data[i++]; yi = data[i++]; break; @@ -293,7 +301,7 @@ function nearestPointOnPath(pt: Point, path: PathProxy, out: Point) { d = projectPointToRect(x0, y0, width, height, x, y, tmpPt); break; case CMD.Z: - d = projectPointToLine(xi, yi, x0, y0, x, y, tmpPt); + d = projectPointToLine(xi, yi, x0, y0, x, y, tmpPt, true); xi = x0; yi = y0; @@ -309,15 +317,26 @@ function nearestPointOnPath(pt: Point, path: PathProxy, out: Point) { 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 updateLabelGuideLine( label: ZRText, labelRect: RectLike, target: Element, - targetRect: RectLike + targetRect: RectLike, + labelLineModel: Model ) { if (!target) { return; @@ -356,11 +375,69 @@ export function updateLabelGuideLine( // TODO pt2 is in the path if (dist < minDist) { minDist = dist; - pt0.toArray(points[0]); + pt2.toArray(points[0]); pt1.toArray(points[1]); - pt2.toArray(points[2]); + 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 (t < 0) { + Point.copy(tmpProjPoint, pt1); + } + else if (t > 1) { + Point.copy(tmpProjPoint, pt2); + } + + tmpProjPoint.toArray(linePoints[1]); + } } \ No newline at end of file diff --git a/src/util/types.ts b/src/util/types.ts index f0eaac1717..96a82adeaa 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -804,6 +804,7 @@ export interface LabelGuideLineOption { length?: number length2?: number smooth?: boolean | number + minTurnAngle?: number, lineStyle?: LineStyleOption } @@ -1132,6 +1133,8 @@ export interface SeriesOption extends */ labelLayout?: LabelLayoutOption | LabelLayoutOptionCallback + labelLine?: LabelGuideLineOption + /** * Animation config for state transition. */ From 86d6c2535774873b78c792c60a31954dac3b6b32 Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 2 Jun 2020 17:03:18 +0800 Subject: [PATCH 32/82] feat: ignore labelLayout if it's not specified in option --- src/chart/pie/PieSeries.ts | 10 ++++++++-- src/label/LabelManager.ts | 15 ++++++++++++--- src/util/types.ts | 8 ++++++-- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/chart/pie/PieSeries.ts b/src/chart/pie/PieSeries.ts index e5c398bacb..e1cfa912d6 100644 --- a/src/chart/pie/PieSeries.ts +++ b/src/chart/pie/PieSeries.ts @@ -224,8 +224,6 @@ class PieSeriesModel extends SeriesModel { // 高亮扇区偏移量 hoverOffset: 5, - // If use strategy to avoid label overlapping - avoidLabelOverlap: true, // 选择模式,默认关闭,可选single,multiple // selectedMode: false, // 南丁格尔玫瑰图模式,'radius'(半径) | 'area'(面积) @@ -283,6 +281,14 @@ class PieSeriesModel extends SeriesModel { borderWidth: 1 }, + labelLayout: { + // Hide the overlapped label. + overlap: 'hidden' + }, + + // If use strategy to avoid label overlapping + avoidLabelOverlap: true, + // Animation type. Valid values: expansion, scale animationType: 'expansion', diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index d3cdfc5f6c..29cf0de444 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -46,7 +46,7 @@ import Transformable from 'zrender/src/core/Transformable'; import { updateLabelGuideLine } from './labelGuideHelper'; import SeriesModel from '../model/Series'; import { makeInner } from '../util/model'; -import { retrieve2, guid, each } from 'zrender/src/core/util'; +import { retrieve2, guid, each, keys } from 'zrender/src/core/util'; interface DisplayedLabelItem { label: ZRText @@ -149,7 +149,7 @@ class LabelManager { /** * Add label to manager */ - addLabel( + private _addLabel( dataIndex: number, seriesModel: SeriesModel, label: ZRText, @@ -235,7 +235,16 @@ class LabelManager { this._chartViewList.push(chartView); const seriesModel = chartView.__model; + const layoutOption = seriesModel.get('labelLayout'); + + /** + * Ignore layouting if it's not specified anything. + */ + if (!layoutOption && !keys(layoutOption).length) { + return; + } + chartView.group.traverse((child) => { if (child.ignore) { return true; // Stop traverse descendants. @@ -245,7 +254,7 @@ class LabelManager { const textEl = child.getTextContent(); const dataIndex = getECData(child).dataIndex; if (textEl && dataIndex != null) { - this.addLabel(dataIndex, seriesModel, textEl, layoutOption); + this._addLabel(dataIndex, seriesModel, textEl, layoutOption); } }); } diff --git a/src/util/types.ts b/src/util/types.ts index 96a82adeaa..75ae6e3134 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -822,6 +822,10 @@ export interface LabelLayoutOptionCallbackParams { }; export interface LabelLayoutOption { + /** + * How to handle the element when it's overlapped + * @default 'visible' + */ overlap?: 'visible' | 'hidden' | 'blur' /** * Minimal margin between two labels which will be considered as overlapped. @@ -1128,13 +1132,13 @@ export interface SeriesOption extends */ seriesLayoutBy?: 'column' | 'row' + labelLine?: LabelGuideLineOption + /** * Global label layout option in label layout stage. */ labelLayout?: LabelLayoutOption | LabelLayoutOptionCallback - labelLine?: LabelGuideLineOption - /** * Animation config for state transition. */ From fead68830dbe728a43d3a49090ddf506c2c0a772 Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 2 Jun 2020 22:56:36 +0800 Subject: [PATCH 33/82] feat(label): support overlap configuration in labelLayout --- src/chart/pie/labelLayout.ts | 23 ++-- src/label/LabelManager.ts | 7 +- test/label-layout.html | 24 +++- test/pie-label.html | 219 +++++++++++++++++++++++++++++++++-- 4 files changed, 251 insertions(+), 22 deletions(-) diff --git a/src/chart/pie/labelLayout.ts b/src/chart/pie/labelLayout.ts index 693008d44f..9e30c26dee 100644 --- a/src/chart/pie/labelLayout.ts +++ b/src/chart/pie/labelLayout.ts @@ -21,7 +21,7 @@ import {parsePercent} from '../../util/number'; import PieSeriesModel, { PieSeriesOption, PieDataItemOption } from './PieSeries'; import { VectorArray } from 'zrender/src/core/vector'; -import { HorizontalAlign, VerticalAlign, ZRRectLike, ZRTextAlign } from '../../util/types'; +import { HorizontalAlign, ZRRectLike, ZRTextAlign } from '../../util/types'; import { Sector, Polyline } from '../../util/graphic'; import ZRText from 'zrender/src/graphic/Text'; import { RectLike } from 'zrender/src/core/BoundingRect'; @@ -69,13 +69,14 @@ function adjustSingleSide( function shiftDown(start: number, end: number, delta: number, dir: number) { for (let j = start; j < end; j++) { - if (list[j].y + delta + list[j].textRect.height / 2 > viewTop + viewHeight) { - break; - } - list[j].y += delta; adjusted = true; + // const textHeight = list[j].textRect.height; + // if (list[j].y + textHeight / 2 > viewTop + viewHeight) { + // list[j].y = viewTop + viewHeight - textHeight / 2; + // } + if (j > start && j + 1 < end && list[j + 1].y > list[j].y + list[j].textRect.height @@ -90,13 +91,14 @@ function adjustSingleSide( function shiftUp(end: number, delta: number) { for (let j = end; j >= 0; j--) { - if (list[j].y - delta - list[j].textRect.height / 2 < viewTop) { - break; - } - list[j].y -= delta; adjusted = true; + const textHeight = list[j].textRect.height; + if (list[j].y - textHeight / 2 < viewTop) { + list[j].y = viewTop + textHeight / 2; + } + if (j > 0 && list[j].y > list[j - 1].y + list[j - 1].textRect.height ) { @@ -173,6 +175,9 @@ function adjustSingleSide( } lastY = list[i].y + list[i].textRect.height; } + // PENDING: + // If data is sorted. Left top is usually the small data with a lower priority. + // So shift up and make sure the data on the bottom is always displayed well. if (viewHeight - lastY < 0) { shiftUp(len - 1, lastY - viewHeight); } diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index 29cf0de444..9c50a06bf6 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -241,7 +241,7 @@ class LabelManager { /** * Ignore layouting if it's not specified anything. */ - if (!layoutOption && !keys(layoutOption).length) { + if (!layoutOption || !keys(layoutOption).length) { return; } @@ -384,7 +384,10 @@ class LabelManager { const labelGuide = labelItem.labelGuide; // TODO Callback to determine if this overlap should be handled? - if (overlapped) { + if (overlapped + && labelItem.layoutOption + && (labelItem.layoutOption as LabelLayoutOption).overlap === 'hidden' + ) { label.hide(); labelGuide && labelGuide.hide(); } diff --git a/test/label-layout.html b/test/label-layout.html index ac49bb1158..5cc3f8b39e 100644 --- a/test/label-layout.html +++ b/test/label-layout.html @@ -78,6 +78,9 @@ label: { show: true }, + labelLayout: { + overlap: 'hidden' + }, data: [13244, 302, 301, 334, 390, 330, 320] }, { @@ -87,6 +90,9 @@ label: { show: true }, + labelLayout: { + overlap: 'hidden' + }, data: [120, 132, 101, 134, 90, 230, 210] }, { @@ -96,6 +102,9 @@ label: { show: true }, + labelLayout: { + overlap: 'hidden' + }, data: [220, 182, 191, 234, 290, 330, 310] }, { @@ -105,6 +114,9 @@ label: { show: true }, + labelLayout: { + overlap: 'hidden' + }, data: [150, 212, 201, 154, 190, 330, 410] }, { @@ -114,6 +126,9 @@ label: { show: true }, + labelLayout: { + overlap: 'hidden' + }, data: [820, 832, 901, 934, 1290, 1330, 1320] } ] @@ -165,6 +180,9 @@ show: true, position: 'top' }, + labelLayout: { + overlap: 'hidden' + }, itemStyle: { color: 'rgb(255, 70, 131)' }, @@ -233,6 +251,10 @@ position: 'right' }, + labelLayout: { + overlap: 'hidden' + }, + emphasis: { label: { show: true @@ -287,7 +309,7 @@ emphasis: { label: { show: true, - fontSize: '30', + fontSize: 30, fontWeight: 'bold' } }, diff --git a/test/pie-label.html b/test/pie-label.html index aa03b66c8f..6e0025b600 100644 --- a/test/pie-label.html +++ b/test/pie-label.html @@ -45,6 +45,8 @@
+
+
@@ -446,5 +454,196 @@ + + + + \ No newline at end of file From dae5a2b495ce5e8642f85fa175c40e5d059a1966 Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 3 Jun 2020 21:06:57 +0800 Subject: [PATCH 34/82] feat: add labelLine for all series --- src/chart/funnel/FunnelSeries.ts | 10 +- src/chart/funnel/FunnelView.ts | 41 ++++---- src/chart/pie/PieSeries.ts | 10 +- src/chart/pie/PieView.ts | 27 ++--- src/echarts.ts | 5 +- src/label/LabelManager.ts | 172 +++++++++++++++++++------------ src/label/labelGuideHelper.ts | 113 +++++++++++++++++--- src/util/types.ts | 4 +- src/view/Chart.ts | 6 ++ test/labelLine.html | 124 ++++++++++++++++++++++ test/pie-alignTo.html | 16 ++- 11 files changed, 383 insertions(+), 145 deletions(-) create mode 100644 test/labelLine.html diff --git a/src/chart/funnel/FunnelSeries.ts b/src/chart/funnel/FunnelSeries.ts index a54d967123..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 } diff --git a/src/chart/funnel/FunnelView.ts b/src/chart/funnel/FunnelView.ts index 9f6ab671e4..a93299e5c6 100644 --- a/src/chart/funnel/FunnelView.ts +++ b/src/chart/funnel/FunnelView.ts @@ -25,30 +25,30 @@ import ExtensionAPI from '../../ExtensionAPI'; import List from '../../data/List'; import { ColorString, LabelOption } from '../../util/types'; import Model from '../../model/Model'; +import { setLabelLineStyle } from '../../label/labelGuideHelper'; 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); @@ -92,8 +92,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; @@ -131,11 +131,9 @@ class FunnelPiece extends graphic.Group { outsideFill: visualColor }); - graphic.updateProps(labelLine, { - shape: { - points: labelLayout.linePoints || labelLayout.linePoints - } - }, seriesModel, idx); + labelLine.setShape({ + points: labelLayout.linePoints || labelLayout.linePoints + }); // Make sure update style on labelText after setLabelStyle. // Because setLabelStyle will replace a new style on it. @@ -153,18 +151,15 @@ class FunnelPiece extends graphic.Group { z2: 10 }); - 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 + }, { + autoCalculate: false }); - labelLine.setStyle(labelLineModel.getModel('lineStyle').getLineStyle()); - - const lineEmphasisState = labelLine.ensureState('emphasis'); - lineEmphasisState.style = labelLineHoverModel.getModel('lineStyle').getLineStyle(); } } @@ -174,6 +169,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; diff --git a/src/chart/pie/PieSeries.ts b/src/chart/pie/PieSeries.ts index e1cfa912d6..4b7fcc8237 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, @@ -57,12 +57,12 @@ export interface PieDataItemOption extends itemStyle?: ItemStyleOption label?: PieLabelOption - labelLine?: LabelGuideLineOption + labelLine?: LabelLineOption emphasis?: { itemStyle?: ItemStyleOption label?: PieLabelOption - labelLine?: LabelGuideLineOption + labelLine?: LabelLineOption } } export interface PieSeriesOption extends @@ -81,7 +81,7 @@ export interface PieSeriesOption extends // TODO: TYPE Color Callback itemStyle?: ItemStyleOption label?: PieLabelOption - labelLine?: LabelGuideLineOption + labelLine?: LabelLineOption clockwise?: boolean startAngle?: number @@ -99,7 +99,7 @@ export interface PieSeriesOption extends emphasis?: { itemStyle?: ItemStyleOption label?: PieLabelOption - labelLine?: LabelGuideLineOption + labelLine?: LabelLineOption } animationType?: 'expansion' | 'scale' diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index 6241d524a3..54b73ba2f4 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -28,6 +28,7 @@ import { Payload, ColorString, ECElement } from '../../util/types'; import List from '../../data/List'; import PieSeriesModel, {PieDataItemOption} from './PieSeries'; import labelLayout from './labelLayout'; +import { setLabelLineStyle } from '../../label/labelGuideHelper'; function updateDataSelected( this: PiePiece, @@ -160,13 +161,11 @@ class PiePiece extends graphic.Sector { private _updateLabel(seriesModel: PieSeriesModel, data: List, idx: number): void { const sector = this; - const labelLine = sector.getTextGuideLine(); const labelText = sector.getTextContent(); const itemModel = data.getItemModel(idx); const labelTextEmphasisState = labelText.ensureState('emphasis'); - const labelLineEmphasisState = labelLine.ensureState('emphasis'); const labelModel = itemModel.getModel('label'); const labelHoverModel = itemModel.getModel(['emphasis', 'label']); @@ -209,25 +208,15 @@ class PiePiece extends graphic.Sector { 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.3; - } - labelLine.setShape({ - smooth: smooth as number + }, { + autoCalculate: false }); } } @@ -238,6 +227,8 @@ class PieView extends ChartView { static type = 'pie'; + ignoreLabelLineUpdate = true; + private _sectorGroup: graphic.Group; private _data: List; diff --git a/src/echarts.ts b/src/echarts.ts index 485e058e3d..d4d32c87f6 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1097,7 +1097,7 @@ class ECharts extends Eventful { const labelManager = this._labelManager; labelManager.updateLayoutConfig(this._api); labelManager.layout(); - labelManager.animateLabels(); + labelManager.processLabelsOverall(); } appendData(params: { @@ -1727,14 +1727,13 @@ class ECharts extends Eventful { // Add labels. labelManager.addLabelsOfSeries(chartView); - }); scheduler.unfinished = unfinished || scheduler.unfinished; labelManager.updateLayoutConfig(api); labelManager.layout(); - labelManager.animateLabels(); + labelManager.processLabelsOverall(); ecModel.eachSeries(function (seriesModel) { const chartView = ecIns._chartsMap[seriesModel.__viewId]; diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index 9c50a06bf6..bb3246fcd7 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -36,17 +36,19 @@ import { ZRTextVerticalAlign, LabelLayoutOption, LabelLayoutOptionCallback, - LabelLayoutOptionCallbackParams + LabelLayoutOptionCallbackParams, + LabelLineOption } from '../util/types'; import { parsePercent } from '../util/number'; import ChartView from '../view/Chart'; -import { ElementTextConfig } from 'zrender/src/Element'; +import Element, { ElementTextConfig } from 'zrender/src/Element'; import { RectLike } from 'zrender/src/core/BoundingRect'; import Transformable from 'zrender/src/core/Transformable'; -import { updateLabelGuideLine } from './labelGuideHelper'; +import { updateLabelLinePoints, setLabelLineStyle } from './labelGuideHelper'; import SeriesModel from '../model/Series'; import { makeInner } from '../util/model'; -import { retrieve2, guid, each, keys } from 'zrender/src/core/util'; +import { retrieve2, each, keys } from 'zrender/src/core/util'; +import { PathStyleProps } from 'zrender/src/graphic/Path'; interface DisplayedLabelItem { label: ZRText @@ -59,7 +61,7 @@ interface DisplayedLabelItem { interface LabelLayoutDesc { label: ZRText - labelGuide: Polyline + labelLine: Polyline seriesModel: SeriesModel dataIndex: number @@ -186,7 +188,7 @@ class LabelManager { this._labelList.push({ label, - labelGuide: labelGuide, + labelLine: labelGuide, seriesModel, dataIndex, @@ -228,7 +230,6 @@ class LabelManager { attachedRot: textConfig.rotation } }); - } addLabelsOfSeries(chartView: ChartView) { @@ -253,6 +254,7 @@ class LabelManager { // Only support label being hosted on graphic elements. const textEl = child.getTextContent(); const dataIndex = getECData(child).dataIndex; + // Can only attach the text on the element with dataIndex if (textEl && dataIndex != null) { this._addLabel(dataIndex, seriesModel, textEl, layoutOption); } @@ -382,18 +384,18 @@ class LabelManager { } } - const labelGuide = labelItem.labelGuide; + const labelLine = labelItem.labelLine; // TODO Callback to determine if this overlap should be handled? if (overlapped && labelItem.layoutOption && (labelItem.layoutOption as LabelLayoutOption).overlap === 'hidden' ) { label.hide(); - labelGuide && labelGuide.hide(); + labelLine && labelLine.hide(); } else { label.attr('ignore', labelItem.defaultAttr.ignore); - labelGuide && labelGuide.attr('ignore', labelItem.defaultAttr.labelGuideIgnore); + labelLine && labelLine.attr('ignore', labelItem.defaultAttr.labelGuideIgnore); displayedLabels.push({ label, @@ -405,79 +407,115 @@ class LabelManager { }); } - updateLabelGuideLine( - label, - globalRect, - label.__hostTarget, - labelItem.hostRect, - labelItem.seriesModel.getModel(['labelLine']) - ); } } - animateLabels() { - each(this._chartViewList, function (chartView) { + /** + * Process all labels. Not only labels with layoutOption. + */ + processLabelsOverall() { + each(this._chartViewList, (chartView) => { const seriesModel = chartView.__model; - if (!seriesModel.isAnimationEnabled()) { - return; - } + const animationEnabled = seriesModel.isAnimationEnabled(); + const ignoreLabelLineUpdate = chartView.ignoreLabelLineUpdate; 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 guideLine = child.getTextGuideLine(); - - if (textEl && !textEl.ignore && !textEl.invisible) { - const layoutStore = labelAnimationStore(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); - updateProps(textEl, newProps, seriesModel); - } - layoutStore.oldLayout = newProps; + if (!ignoreLabelLineUpdate) { + this._updateLabelLine(child, seriesModel); } - 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; + 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<{ + labelLine: LabelLineOption, + emphasis: { labelLine: LabelLineOption } + }>(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 = labelAnimationStore(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); + updateProps(textEl, newProps, seriesModel); + } + layoutStore.oldLayout = newProps; + } + + 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; + } + } } diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts index ddeba604cb..c3cbc90fd7 100644 --- a/src/label/labelGuideHelper.ts +++ b/src/label/labelGuideHelper.ts @@ -20,19 +20,24 @@ import { Text as ZRText, Point, - Path + 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 { LabelGuideLineOption } from '../util/types'; +import { extend, defaults, retrieve2 } from 'zrender/src/core/util'; +import { LabelLineOption } from '../util/types'; import Model from '../model/Model'; +import { invert } from 'zrender/src/core/matrix'; 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]; @@ -331,50 +336,58 @@ const dir2 = new Point(); * @param target * @param targetRect */ -export function updateLabelGuideLine( - label: ZRText, - labelRect: RectLike, +export function updateLabelLinePoints( target: Element, - targetRect: RectLike, - labelLineModel: Model + labelLineModel: Model ) { if (!target) { return; } const labelLine = target.getTextGuideLine(); + const label = target.getTextContent(); // Needs to create text guide in each charts. - if (!labelLine) { + if (!(label && labelLine)) { return; } const labelGuideConfig = target.textGuideLineConfig || {}; - if (!labelGuideConfig.autoCalculate) { - return; - } 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 = 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, labelGuideConfig.len == null ? 15 : labelGuideConfig.len); + 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, targetRect, 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]); @@ -440,4 +453,76 @@ export function limitTurnAngle(linePoints: number[][], minTurnAngle: number) { tmpProjPoint.toArray(linePoints[1]); } -} \ No newline at end of file +} + + +type LabelLineModel = Model; +/** + * Create a label line if necessary and set it's style. + */ +export function setLabelLineStyle( + targetEl: Element, + statesModels: Record, + defaultStyle?: Polyline['style'], + defaultConfig?: Element['textGuideLineConfig'] +) { + 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 labelShowNormal = 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 + ? labelShowNormal + : retrieve2(label.states && label.states[stateName].ignore, labelShowNormal); + 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); + } + + const stateObj = isNormal ? labelLine : labelLine.ensureState(stateName); + // Make sure display. + stateObj.ignore = false; + // Set smooth + let smooth = stateModel.get('smooth'); + if (smooth && smooth === true) { + smooth = 0.4; + } + stateObj.shape = stateObj.shape || {}; + (stateObj.shape as Polyline['shape']).smooth = smooth as number; + + const styleObj = stateModel.getModel('lineStyle').getLineStyle(); + isNormal ? labelLine.useStyle(styleObj) : stateObj.style = styleObj; + } + } + + if (labelLine) { + defaults(labelLine.style, defaultStyle); + // Not fill. + labelLine.style.fill = null; + } +} diff --git a/src/util/types.ts b/src/util/types.ts index 75ae6e3134..994aab83f0 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -799,7 +799,7 @@ export interface LineLabelOption extends Omit + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + diff --git a/test/pie-alignTo.html b/test/pie-alignTo.html index 969fde27b0..db08612c2a 100644 --- a/test/pie-alignTo.html +++ b/test/pie-alignTo.html @@ -68,7 +68,7 @@ type: 'pie', radius: '50%', data: data, - animation: false, + labelLine: { length2: 15 }, @@ -84,7 +84,7 @@ type: 'pie', radius: '50%', data: data, - animation: false, + labelLine: { length2: 15 }, @@ -101,7 +101,7 @@ type: 'pie', radius: '50%', data: data, - animation: false, + labelLine: { length2: 15 }, @@ -119,7 +119,7 @@ radius: '25%', center: ['50%', '50%'], data: data, - animation: false, + labelLine: { length2: 15 }, @@ -136,7 +136,7 @@ radius: '25%', center: ['50%', '50%'], data: data, - animation: false, + labelLine: { length2: 15 }, @@ -154,7 +154,7 @@ radius: '25%', center: ['50%', '50%'], data: data, - animation: false, + labelLine: { length2: 15 }, @@ -171,7 +171,7 @@ radius: '25%', center: ['50%', '50%'], data: data, - animation: false, + labelLine: { length2: 15 }, @@ -251,8 +251,6 @@ }); }); - - From c344d13b654bde4281a5642bcbdcbd61f6bbd62f Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 3 Jun 2020 21:44:07 +0800 Subject: [PATCH 35/82] feat: add draggable in labelLayout --- src/label/LabelManager.ts | 43 ++++++++++++++++++++++++------ src/util/types.ts | 6 +++++ test/labelLine.html | 10 ++++--- test/pie-label.html | 56 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 12 deletions(-) diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index bb3246fcd7..523c48c8b3 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -49,6 +49,7 @@ import SeriesModel from '../model/Series'; import { makeInner } from '../util/model'; import { retrieve2, each, keys } from 'zrender/src/core/util'; import { PathStyleProps } from 'zrender/src/graphic/Path'; +import Model from '../model/Model'; interface DisplayedLabelItem { label: ZRText @@ -65,6 +66,7 @@ interface LabelLayoutDesc { seriesModel: SeriesModel dataIndex: number + dataType: string layoutOption: LabelLayoutOptionCallback | LabelLayoutOption @@ -107,6 +109,7 @@ function prepareLayoutCallbackParams(labelItem: LabelLayoutDesc): LabelLayoutOpt const label = labelItem.label; return { dataIndex: labelItem.dataIndex, + dataType: labelItem.dataType, seriesIndex: labelItem.seriesModel.seriesIndex, text: labelItem.label.style.text, rect: labelItem.hostRect, @@ -136,6 +139,11 @@ const labelLineAnimationStore = makeInner<{ } }, Polyline>(); +type LabelLineOptionMixin = { + labelLine: LabelLineOption, + emphasis: { labelLine: LabelLineOption } +}; + class LabelManager { private _labelList: LabelLayoutDesc[] = []; @@ -153,6 +161,7 @@ class LabelManager { */ private _addLabel( dataIndex: number, + dataType: string, seriesModel: SeriesModel, label: ZRText, layoutOption: LabelLayoutDesc['layoutOption'] @@ -192,6 +201,7 @@ class LabelManager { seriesModel, dataIndex, + dataType, layoutOption, @@ -253,10 +263,11 @@ class LabelManager { // Only support label being hosted on graphic elements. const textEl = child.getTextContent(); - const dataIndex = getECData(child).dataIndex; + 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, seriesModel, textEl, layoutOption); + this._addLabel(dataIndex, ecData.dataType, seriesModel, textEl, layoutOption); } }); } @@ -264,6 +275,12 @@ class LabelManager { 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; @@ -324,6 +341,21 @@ class LabelManager { labelItem.overlap = layoutOption.overlap; labelItem.overlapMargin = layoutOption.overlapMargin; + + 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'; + } } } @@ -406,7 +438,6 @@ class LabelManager { transform }); } - } } @@ -444,10 +475,7 @@ class LabelManager { if (textEl && dataIndex != null) { const data = seriesModel.getData(ecData.dataType); - const itemModel = data.getItemModel<{ - labelLine: LabelLineOption, - emphasis: { labelLine: LabelLineOption } - }>(dataIndex); + const itemModel = data.getItemModel(dataIndex); const defaultStyle: PathStyleProps = {}; const visualStyle = data.getItemVisual(dataIndex, 'style'); @@ -462,7 +490,6 @@ class LabelManager { emphasis: itemModel.getModel(['emphasis', 'labelLine']) }, defaultStyle); - updateLabelLinePoints(el, labelLineModel); } } diff --git a/src/util/types.ts b/src/util/types.ts index 994aab83f0..617fb94fa1 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -811,6 +811,7 @@ export interface LabelLineOption { export interface LabelLayoutOptionCallbackParams { dataIndex: number, + dataType: string, seriesIndex: number, text: string align: ZRTextAlign @@ -831,6 +832,11 @@ export interface LabelLayoutOption { * Minimal margin between two labels which will be considered as overlapped. */ overlapMargin?: number + + /** + * If label is draggable. + */ + draggable?: boolean /** * Can be absolute px number or percent string. */ diff --git a/test/labelLine.html b/test/labelLine.html index 38297a6837..6cdae40713 100644 --- a/test/labelLine.html +++ b/test/labelLine.html @@ -67,11 +67,13 @@ }, labelLayout: { y: 20, + draggable: true, align: 'center', overlap: 'hidden' }, labelLine: { - show: true + show: true, + length2: 10 }, label: { show: true, @@ -89,11 +91,13 @@ }, labelLayout: { y: 40, + draggable: true, align: 'center', overlap: 'hidden' }, labelLine: { - show: true + show: true, + length2: 10 }, label: { show: true, @@ -107,8 +111,6 @@ var chart = testHelper.create(echarts, 'main0', { title: [ - 'Test Case Description of main0', - '(Muliple lines and **emphasis** are supported in description)' ], option: option // height: 300, diff --git a/test/pie-label.html b/test/pie-label.html index 6e0025b600..ec6a077c47 100644 --- a/test/pie-label.html +++ b/test/pie-label.html @@ -47,6 +47,7 @@
+
+ \ No newline at end of file From 6f57d74131736e07011e77ea530b37b86f94e7b2 Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 4 Jun 2020 10:33:36 +0800 Subject: [PATCH 36/82] fix(label): fix logic issues in setLabelLineStyle --- src/label/labelGuideHelper.ts | 47 +++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts index c3cbc90fd7..3eee3bcbdc 100644 --- a/src/label/labelGuideHelper.ts +++ b/src/label/labelGuideHelper.ts @@ -457,6 +457,28 @@ export function limitTurnAngle(linePoints: number[][], minTurnAngle: number) { 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.4; + } + stateObj.shape = stateObj.shape || {}; + (stateObj.shape as Polyline['shape']).smooth = smooth as number; + + const styleObj = stateModel.getModel('lineStyle').getLineStyle(); + isNormal ? labelLine.useStyle(styleObj) : stateObj.style = styleObj; +} /** * Create a label line if necessary and set it's style. */ @@ -478,7 +500,7 @@ export function setLabelLineStyle( const normalModel = statesModels.normal; const showNormal = normalModel.get('show'); - const labelShowNormal = label.ignore; + const labelIgnoreNormal = label.ignore; for (let i = 0; i < STATES.length; i++) { const stateName = STATES[i]; @@ -487,8 +509,8 @@ export function setLabelLineStyle( if (stateModel) { const stateShow = stateModel.get('show'); const isLabelIgnored = isNormal - ? labelShowNormal - : retrieve2(label.states && label.states[stateName].ignore, labelShowNormal); + ? labelIgnoreNormal + : retrieve2(label.states && 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. ) { @@ -502,21 +524,14 @@ export function setLabelLineStyle( 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); + } } - const stateObj = isNormal ? labelLine : labelLine.ensureState(stateName); - // Make sure display. - stateObj.ignore = false; - // Set smooth - let smooth = stateModel.get('smooth'); - if (smooth && smooth === true) { - smooth = 0.4; - } - stateObj.shape = stateObj.shape || {}; - (stateObj.shape as Polyline['shape']).smooth = smooth as number; - - const styleObj = stateModel.getModel('lineStyle').getLineStyle(); - isNormal ? labelLine.useStyle(styleObj) : stateObj.style = styleObj; + setLabelLineState(labelLine, false, stateName, stateModel); } } From 65f4fc4570aae23ef76c20eb9c23cc6426786130 Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 4 Jun 2020 17:03:30 +0800 Subject: [PATCH 37/82] feat: change label overlap configuration for supporting overlap layout --- src/chart/funnel/FunnelView.ts | 2 -- src/chart/pie/PieSeries.ts | 2 +- src/chart/pie/PieView.ts | 2 -- src/label/LabelManager.ts | 26 +++++++------------- src/label/labelGuideHelper.ts | 3 +-- src/label/labelLayoutHelper.ts | 43 ++++++++++++++++++++++++++++++++++ src/util/types.ts | 14 +++++++---- test/label-layout.html | 14 +++++------ test/labelLine.html | 4 ++-- 9 files changed, 73 insertions(+), 37 deletions(-) create mode 100644 src/label/labelLayoutHelper.ts diff --git a/src/chart/funnel/FunnelView.ts b/src/chart/funnel/FunnelView.ts index a93299e5c6..e10efa0e5c 100644 --- a/src/chart/funnel/FunnelView.ts +++ b/src/chart/funnel/FunnelView.ts @@ -157,8 +157,6 @@ class FunnelPiece extends graphic.Polygon { }, { // Default use item visual color stroke: visualColor - }, { - autoCalculate: false }); } } diff --git a/src/chart/pie/PieSeries.ts b/src/chart/pie/PieSeries.ts index 4b7fcc8237..d2b3c181ba 100644 --- a/src/chart/pie/PieSeries.ts +++ b/src/chart/pie/PieSeries.ts @@ -283,7 +283,7 @@ class PieSeriesModel extends SeriesModel { labelLayout: { // Hide the overlapped label. - overlap: 'hidden' + hideOverlap: true }, // If use strategy to avoid label overlapping diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index 54b73ba2f4..2c6fe97486 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -215,8 +215,6 @@ class PiePiece extends graphic.Sector { }, { stroke: visualColor, opacity: style && style.opacity - }, { - autoCalculate: false }); } } diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index 523c48c8b3..194c22260c 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -47,7 +47,7 @@ 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 } from 'zrender/src/core/util'; +import { retrieve2, each, keys, isFunction } from 'zrender/src/core/util'; import { PathStyleProps } from 'zrender/src/graphic/Path'; import Model from '../model/Model'; @@ -69,9 +69,7 @@ interface LabelLayoutDesc { dataType: string layoutOption: LabelLayoutOptionCallback | LabelLayoutOption - - overlap: LabelLayoutOption['overlap'] - overlapMargin: LabelLayoutOption['overlapMargin'] + computedLayoutOption: LabelLayoutOption hostRect: RectLike priority: number @@ -204,12 +202,10 @@ class LabelManager { dataType, layoutOption, + computedLayoutOption: null, hostRect, - overlap: 'hidden', - overlapMargin: 0, - // Label with lower priority will be hidden when overlapped // Use rect size as default priority priority: hostRect ? hostRect.width * hostRect.height : 0, @@ -252,7 +248,7 @@ class LabelManager { /** * Ignore layouting if it's not specified anything. */ - if (!layoutOption || !keys(layoutOption).length) { + if (!(isFunction(layoutOption) || keys(layoutOption).length)) { return; } @@ -298,6 +294,7 @@ class LabelManager { } layoutOption = layoutOption || {}; + labelItem.computedLayoutOption = layoutOption; if (hostEl) { hostEl.setTextConfig({ @@ -339,9 +336,6 @@ class LabelManager { label.setStyle(key, layoutOption[key] != null ? layoutOption[key] : defaultLabelAttr.style[key]); } - labelItem.overlap = layoutOption.overlap; - labelItem.overlapMargin = layoutOption.overlapMargin; - if (layoutOption.draggable) { label.draggable = true; label.cursor = 'move'; @@ -377,6 +371,7 @@ class LabelManager { continue; } + const layoutOption = labelItem.computedLayoutOption; const label = labelItem.label; const transform = label.getComputedTransform(); // NOTE: Get bounding rect after getComputedTransform, or label may not been updated by the host el. @@ -388,8 +383,8 @@ class LabelManager { let obb = isAxisAligned ? new OrientedBoundingRect(localRect, transform) : null; let overlapped = false; - const overlapMargin = labelItem.overlapMargin || 0; - const marginSqr = overlapMargin * overlapMargin; + const minMargin = layoutOption.minMargin || 0; + const marginSqr = minMargin * minMargin; for (let j = 0; j < displayedLabels.length; j++) { const existsTextCfg = displayedLabels[j]; // Fast rejection. @@ -418,10 +413,7 @@ class LabelManager { const labelLine = labelItem.labelLine; // TODO Callback to determine if this overlap should be handled? - if (overlapped - && labelItem.layoutOption - && (labelItem.layoutOption as LabelLayoutOption).overlap === 'hidden' - ) { + if (overlapped && layoutOption.hideOverlap) { label.hide(); labelLine && labelLine.hide(); } diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts index 3eee3bcbdc..3ea413f7e3 100644 --- a/src/label/labelGuideHelper.ts +++ b/src/label/labelGuideHelper.ts @@ -18,7 +18,6 @@ */ import { - Text as ZRText, Point, Path, Polyline @@ -28,7 +27,7 @@ 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 { extend, defaults, retrieve2 } from 'zrender/src/core/util'; +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'; diff --git a/src/label/labelLayoutHelper.ts b/src/label/labelLayoutHelper.ts new file mode 100644 index 0000000000..c1df9b6f42 --- /dev/null +++ b/src/label/labelLayoutHelper.ts @@ -0,0 +1,43 @@ +/* +* 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'; + + +/** + * Adjust labels on x direction to avoid overlap. + */ +export function adjustLayoutOnX( + list: ZRText[], + leftBound: number, + rightBound: number +) { + +} + +/** + * Adjust labels on y direction to avoid overlap. + */ +export function adjustLayoutOnY( + list: ZRText[], + topBound: number, + bottomBound: number +) { + +} \ No newline at end of file diff --git a/src/util/types.ts b/src/util/types.ts index 617fb94fa1..fc20b967dd 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -824,14 +824,20 @@ export interface LabelLayoutOptionCallbackParams { export interface LabelLayoutOption { /** - * How to handle the element when it's overlapped - * @default 'visible' + * If move the overlapped label. If label is still overlapped after moved. + * It will determine if to hide this label with `hideOverlap` policy. */ - overlap?: 'visible' | 'hidden' | 'blur' + moveOverlap?: 'x' | 'y' | boolean + /** + * If hide the overlapped label. It will be handled after move. + * @default 'none' + */ + hideOverlap?: boolean + /** * Minimal margin between two labels which will be considered as overlapped. */ - overlapMargin?: number + minMargin?: number /** * If label is draggable. diff --git a/test/label-layout.html b/test/label-layout.html index 5cc3f8b39e..a3eaf35d3d 100644 --- a/test/label-layout.html +++ b/test/label-layout.html @@ -79,7 +79,7 @@ show: true }, labelLayout: { - overlap: 'hidden' + hideOverlap: true }, data: [13244, 302, 301, 334, 390, 330, 320] }, @@ -91,7 +91,7 @@ show: true }, labelLayout: { - overlap: 'hidden' + hideOverlap: true }, data: [120, 132, 101, 134, 90, 230, 210] }, @@ -103,7 +103,7 @@ show: true }, labelLayout: { - overlap: 'hidden' + hideOverlap: true }, data: [220, 182, 191, 234, 290, 330, 310] }, @@ -115,7 +115,7 @@ show: true }, labelLayout: { - overlap: 'hidden' + hideOverlap: true }, data: [150, 212, 201, 154, 190, 330, 410] }, @@ -127,7 +127,7 @@ show: true }, labelLayout: { - overlap: 'hidden' + hideOverlap: true }, data: [820, 832, 901, 934, 1290, 1330, 1320] } @@ -181,7 +181,7 @@ position: 'top' }, labelLayout: { - overlap: 'hidden' + hideOverlap: true }, itemStyle: { color: 'rgb(255, 70, 131)' @@ -252,7 +252,7 @@ }, labelLayout: { - overlap: 'hidden' + hideOverlap: true }, emphasis: { diff --git a/test/labelLine.html b/test/labelLine.html index 6cdae40713..d02d1eba98 100644 --- a/test/labelLine.html +++ b/test/labelLine.html @@ -69,7 +69,7 @@ y: 20, draggable: true, align: 'center', - overlap: 'hidden' + hideOverlap: true }, labelLine: { show: true, @@ -93,7 +93,7 @@ y: 40, draggable: true, align: 'center', - overlap: 'hidden' + hideOverlap: true }, labelLine: { show: true, From ca57e8034a8d3e8e69f657cbc8ca9f5be6733776 Mon Sep 17 00:00:00 2001 From: pissang Date: Fri, 5 Jun 2020 17:10:07 +0800 Subject: [PATCH 38/82] feat(label: add shift-x, shift-y option for moveOverlap in labelLayout --- src/chart/pie/labelLayout.ts | 9 +- src/echarts.ts | 4 +- src/label/LabelManager.ts | 111 ++++------------- src/label/labelLayoutHelper.ts | 210 ++++++++++++++++++++++++++++++++- src/util/types.ts | 8 +- test/label-layout.html | 122 ++++++++++++++++++- test/labelLine.html | 126 -------------------- 7 files changed, 358 insertions(+), 232 deletions(-) delete mode 100644 test/labelLine.html diff --git a/src/chart/pie/labelLayout.ts b/src/chart/pie/labelLayout.ts index 9e30c26dee..473cf5f66c 100644 --- a/src/chart/pie/labelLayout.ts +++ b/src/chart/pie/labelLayout.ts @@ -72,15 +72,10 @@ function adjustSingleSide( list[j].y += delta; adjusted = true; - // const textHeight = list[j].textRect.height; - // if (list[j].y + textHeight / 2 > viewTop + viewHeight) { - // list[j].y = viewTop + viewHeight - textHeight / 2; - // } - - if (j > start - && j + 1 < end + if (j > start && j + 1 < end && list[j + 1].y > list[j].y + list[j].textRect.height ) { + // Shift up so it can be more equaly distributed. shiftUp(j, delta / 2); return; } diff --git a/src/echarts.ts b/src/echarts.ts index d4d32c87f6..4ce37efb3a 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1096,7 +1096,7 @@ class ECharts extends Eventful { updateLabelLayout() { const labelManager = this._labelManager; labelManager.updateLayoutConfig(this._api); - labelManager.layout(); + labelManager.layout(this._api); labelManager.processLabelsOverall(); } @@ -1732,7 +1732,7 @@ class ECharts extends Eventful { scheduler.unfinished = unfinished || scheduler.unfinished; labelManager.updateLayoutConfig(api); - labelManager.layout(); + labelManager.layout(api); labelManager.processLabelsOverall(); ecModel.eachSeries(function (seriesModel) { diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index 194c22260c..5c5d3204f6 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -20,16 +20,13 @@ // TODO: move labels out of viewport. import { - OrientedBoundingRect, Text as ZRText, - Point, BoundingRect, getECData, Polyline, updateProps, initProps } from '../util/graphic'; -import { MatrixArray } from 'zrender/src/core/matrix'; import ExtensionAPI from '../ExtensionAPI'; import { ZRTextAlign, @@ -47,20 +44,12 @@ import Transformable from 'zrender/src/core/Transformable'; import { updateLabelLinePoints, setLabelLineStyle } from './labelGuideHelper'; import SeriesModel from '../model/Series'; import { makeInner } from '../util/model'; -import { retrieve2, each, keys, isFunction } from 'zrender/src/core/util'; +import { retrieve2, each, keys, isFunction, filter } from 'zrender/src/core/util'; import { PathStyleProps } from 'zrender/src/graphic/Path'; import Model from '../model/Model'; +import { LabelLayoutInfo, prepareLayoutList, hideOverlap, shiftLayoutOnX, shiftLayoutOnY } from './labelLayoutHelper'; -interface DisplayedLabelItem { - label: ZRText - rect: BoundingRect - localRect: BoundingRect - obb?: OrientedBoundingRect - axisAligned: boolean - transform: MatrixArray -} - -interface LabelLayoutDesc { +interface LabelDesc { label: ZRText labelLine: Polyline @@ -102,7 +91,7 @@ interface SavedLabelAttr { rect: RectLike } -function prepareLayoutCallbackParams(labelItem: LabelLayoutDesc): LabelLayoutOptionCallbackParams { +function prepareLayoutCallbackParams(labelItem: LabelDesc): LabelLayoutOptionCallbackParams { const labelAttr = labelItem.defaultAttr; const label = labelItem.label; return { @@ -144,7 +133,7 @@ type LabelLineOptionMixin = { class LabelManager { - private _labelList: LabelLayoutDesc[] = []; + private _labelList: LabelDesc[] = []; private _chartViewList: ChartView[] = []; constructor() {} @@ -162,7 +151,7 @@ class LabelManager { dataType: string, seriesModel: SeriesModel, label: ZRText, - layoutOption: LabelLayoutDesc['layoutOption'] + layoutOption: LabelDesc['layoutOption'] ) { const labelStyle = label.style; const hostEl = label.__hostTarget; @@ -353,84 +342,26 @@ class LabelManager { } } - layout() { - // TODO: sort by priority(area) - const labelList = this._labelList; - - const displayedLabels: DisplayedLabelItem[] = []; - const mvt = new Point(); + layout(api: ExtensionAPI) { + const width = api.getWidth(); + const height = api.getHeight(); - // TODO, render overflow visible first, put in the displayedLabels. - labelList.sort(function (a, b) { - return b.priority - a.priority; + const labelList = prepareLayoutList(this._labelList); + const labelsNeedsAdjustOnX = filter(labelList, function (item) { + return item.layoutOption.moveOverlap === 'shift-x'; + }); + const labelsNeedsAdjustOnY = filter(labelList, function (item) { + return item.layoutOption.moveOverlap === 'shift-y'; }); - for (let i = 0; i < labelList.length; i++) { - const labelItem = labelList[i]; - if (labelItem.defaultAttr.ignore) { - continue; - } - - const layoutOption = labelItem.computedLayoutOption; - const label = labelItem.label; - const transform = label.getComputedTransform(); - // NOTE: Get bounding rect after getComputedTransform, or label may not been updated by the host el. - const localRect = label.getBoundingRect(); - const isAxisAligned = !transform || (transform[1] < 1e-5 && transform[2] < 1e-5); - - const globalRect = localRect.clone(); - globalRect.applyTransform(transform); - - let obb = isAxisAligned ? new OrientedBoundingRect(localRect, transform) : null; - let overlapped = false; - const minMargin = layoutOption.minMargin || 0; - const marginSqr = minMargin * minMargin; - for (let j = 0; j < displayedLabels.length; j++) { - const existsTextCfg = displayedLabels[j]; - // Fast rejection. - if (!globalRect.intersect(existsTextCfg.rect, mvt) && mvt.lenSquare() > marginSqr) { - continue; - } - - if (isAxisAligned && existsTextCfg.axisAligned) { // Is overlapped - overlapped = true; - break; - } - - if (!existsTextCfg.obb) { // If self is not axis aligned. But other is. - existsTextCfg.obb = new OrientedBoundingRect(existsTextCfg.localRect, existsTextCfg.transform); - } - - if (!obb) { // If self is axis aligned. But other is not. - obb = new OrientedBoundingRect(localRect, transform); - } + shiftLayoutOnX(labelsNeedsAdjustOnX, 0, width); + shiftLayoutOnY(labelsNeedsAdjustOnY, 0, height); - if (obb.intersect(existsTextCfg.obb, mvt) || mvt.lenSquare() < marginSqr) { - overlapped = true; - break; - } - } + const labelsNeedsHideOverlap = filter(labelList, function (item) { + return item.layoutOption.hideOverlap; + }); - const labelLine = labelItem.labelLine; - // TODO Callback to determine if this overlap should be handled? - if (overlapped && layoutOption.hideOverlap) { - label.hide(); - labelLine && labelLine.hide(); - } - else { - label.attr('ignore', labelItem.defaultAttr.ignore); - labelLine && labelLine.attr('ignore', labelItem.defaultAttr.labelGuideIgnore); - - displayedLabels.push({ - label, - rect: globalRect, - localRect, - obb, - axisAligned: isAxisAligned, - transform - }); - } - } + hideOverlap(labelsNeedsHideOverlap); } /** diff --git a/src/label/labelLayoutHelper.ts b/src/label/labelLayoutHelper.ts index c1df9b6f42..861012df6e 100644 --- a/src/label/labelLayoutHelper.ts +++ b/src/label/labelLayoutHelper.ts @@ -18,26 +18,226 @@ */ import ZRText from 'zrender/src/graphic/Text'; +import { LabelLayoutOption } from '../util/types'; +import { BoundingRect, OrientedBoundingRect, Polyline } from '../util/graphic'; +interface LabelLayoutListPrepareInput { + label: ZRText + labelLine: Polyline + computedLayoutOption: LabelLayoutOption + priority: number + defaultAttr: { + ignore: boolean + labelGuideIgnore: boolean + } +} + +export interface LabelLayoutInfo { + label: ZRText + labelLine: Polyline + priority: number + rect: BoundingRect // Global rect + localRect: BoundingRect + obb?: OrientedBoundingRect // Only available when axisAligned is true + axisAligned: boolean + layoutOption: LabelLayoutOption + defaultAttr: { + ignore: boolean + labelGuideIgnore: boolean + } + transform: number[] +} + +export function prepareLayoutList(input: LabelLayoutListPrepareInput[]): LabelLayoutInfo[] { + const list: LabelLayoutInfo[] = []; + + for (let i = 0; i < input.length; i++) { + const rawItem = input[i]; + if (rawItem.defaultAttr.ignore) { + continue; + } + + const layoutOption = rawItem.computedLayoutOption; + const label = rawItem.label; + const transform = label.getComputedTransform(); + // NOTE: Get bounding rect after getComputedTransform, or label may not been updated by the host el. + const localRect = label.getBoundingRect(); + const isAxisAligned = !transform || (transform[1] < 1e-5 && transform[2] < 1e-5); + + // Text has a default 1px stroke. Exclude this. + const minMargin = (layoutOption.minMargin || 0) + 2.2; + const globalRect = localRect.clone(); + globalRect.applyTransform(transform); + globalRect.x -= minMargin / 2; + globalRect.y -= minMargin / 2; + globalRect.width += minMargin; + globalRect.height += minMargin; + + const obb = isAxisAligned ? new OrientedBoundingRect(localRect, transform) : null; + + list.push({ + label, + labelLine: rawItem.labelLine, + rect: globalRect, + localRect, + obb, + priority: rawItem.priority, + defaultAttr: rawItem.defaultAttr, + layoutOption: rawItem.computedLayoutOption, + axisAligned: isAxisAligned, + transform + }); + } + return list; +} + +function shiftLayout( + list: LabelLayoutInfo[], + xyDim: 'x' | 'y', + sizeDim: 'width' | 'height', + minBound: number, + maxBound: number +) { + if (!list.length) { + return; + } + + list.sort(function (a, b) { + return a.label[xyDim] - b.label[xyDim]; + }); + + function shiftForward(start: number, end: number, delta: number) { + for (let j = start; j < end; j++) { + list[j].label[xyDim] += delta; + + const rect = list[j].rect; + rect[xyDim] += delta; + + if (j > start && j + 1 < end + && list[j + 1].rect[xyDim] > rect[xyDim] + rect[sizeDim] + ) { + // Shift up so it can be more equaly distributed. + shiftBackward(j, delta / 2); + return; + } + } + + shiftBackward(end - 1, delta / 2); + } + + function shiftBackward(end: number, delta: number) { + for (let j = end; j >= 0; j--) { + list[j].label[xyDim] -= delta; + + const rect = list[j].rect; + rect[xyDim] -= delta; + + // const textSize = rect[sizeDim]; + const diffToMinBound = rect[xyDim] - minBound; + if (diffToMinBound < 0) { + rect[xyDim] -= diffToMinBound; + list[j].label[xyDim] -= diffToMinBound; + } + + if (j > 0 + && rect[xyDim] > list[j - 1].rect[xyDim] + list[j - 1].rect[sizeDim] + ) { + break; + } + } + } + let lastPos = 0; + let delta; + const len = list.length; + for (let i = 0; i < len; i++) { + delta = list[i].label[xyDim] - lastPos; + if (delta < 0) { + shiftForward(i, len, -delta); + } + lastPos = list[i].label[xyDim] + list[i].rect[sizeDim]; + } + if (maxBound - lastPos < 0) { + shiftBackward(len - 1, lastPos - maxBound); + } +} /** * Adjust labels on x direction to avoid overlap. */ -export function adjustLayoutOnX( - list: ZRText[], +export function shiftLayoutOnX( + list: LabelLayoutInfo[], leftBound: number, rightBound: number ) { - + shiftLayout(list, 'x', 'width', leftBound, rightBound); } /** * Adjust labels on y direction to avoid overlap. */ -export function adjustLayoutOnY( - list: ZRText[], +export function shiftLayoutOnY( + list: LabelLayoutInfo[], topBound: number, bottomBound: number ) { + shiftLayout(list, 'y', 'height', topBound, bottomBound); +} + +export function hideOverlap(labelList: LabelLayoutInfo[]) { + const displayedLabels: LabelLayoutInfo[] = []; + + // TODO, render overflow visible first, put in the displayedLabels. + labelList.sort(function (a, b) { + return b.priority - a.priority; + }); + + for (let i = 0; i < labelList.length; i++) { + const labelItem = labelList[i]; + const globalRect = labelItem.rect; + const isAxisAligned = labelItem.axisAligned; + const localRect = labelItem.localRect; + const transform = labelItem.transform; + const label = labelItem.label; + const labelLine = labelItem.labelLine; + + let obb = labelItem.obb; + let overlapped = false; + for (let j = 0; j < displayedLabels.length; j++) { + const existsTextCfg = displayedLabels[j]; + // Fast rejection. + if (!globalRect.intersect(existsTextCfg.rect)) { + continue; + } + + if (isAxisAligned && existsTextCfg.axisAligned) { // Is overlapped + overlapped = true; + break; + } + + if (!existsTextCfg.obb) { // If self is not axis aligned. But other is. + existsTextCfg.obb = new OrientedBoundingRect(existsTextCfg.localRect, existsTextCfg.transform); + } + + if (!obb) { // If self is axis aligned. But other is not. + obb = new OrientedBoundingRect(localRect, transform); + } + + if (obb.intersect(existsTextCfg.obb)) { + overlapped = true; + break; + } + } + + // TODO Callback to determine if this overlap should be handled? + if (overlapped) { + label.hide(); + labelLine && labelLine.hide(); + } + else { + label.attr('ignore', labelItem.defaultAttr.ignore); + labelLine && labelLine.attr('ignore', labelItem.defaultAttr.labelGuideIgnore); + displayedLabels.push(labelItem); + } + } } \ No newline at end of file diff --git a/src/util/types.ts b/src/util/types.ts index fc20b967dd..7c7228ab13 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -826,8 +826,14 @@ export interface LabelLayoutOption { /** * If move the overlapped label. If label is still overlapped after moved. * It will determine if to hide this label with `hideOverlap` policy. + * + * shift-x/y will keep the order on x/y + * shuffle-x/y will move the label around the original position randomly. */ - moveOverlap?: 'x' | 'y' | boolean + moveOverlap?: 'shift-x' + | 'shift-y' + | 'shuffle-x' + | 'shuffle-y' /** * If hide the overlapped label. It will be handled after move. * @default 'none' diff --git a/test/label-layout.html b/test/label-layout.html index a3eaf35d3d..f6fe7e616f 100644 --- a/test/label-layout.html +++ b/test/label-layout.html @@ -46,6 +46,8 @@
+
+
@@ -153,7 +155,7 @@ var data = [Math.round(Math.random() * 300)]; - for (var i = 1; i < 200; i++) { + for (var i = 1; i < 50; i++) { var now = new Date(base += oneDay); date.push([now.getFullYear(), now.getMonth() + 1, now.getDate()].join('/')); data.push(Math.round((Math.random() - 0.5) * 20 + data[i - 1])); @@ -200,6 +202,7 @@ ] }; var chart = testHelper.create(echarts, 'main1', { + width: 600, title: [ 'Overlap of line.' ], @@ -343,6 +346,123 @@ + + + + diff --git a/test/labelLine.html b/test/labelLine.html deleted file mode 100644 index d02d1eba98..0000000000 --- a/test/labelLine.html +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - From 0089d9b251b7c24bd9cd1687c6ae6ef44703bef0 Mon Sep 17 00:00:00 2001 From: pissang Date: Sun, 7 Jun 2020 21:50:20 +0800 Subject: [PATCH 39/82] feat(label): optimize label auto layout when aligned horizontally or vertically --- src/label/labelLayoutHelper.ts | 59 ++++++++++++++++++---------------- test/label-layout.html | 7 ++-- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/label/labelLayoutHelper.ts b/src/label/labelLayoutHelper.ts index 861012df6e..49e0c963c1 100644 --- a/src/label/labelLayoutHelper.ts +++ b/src/label/labelLayoutHelper.ts @@ -64,8 +64,7 @@ export function prepareLayoutList(input: LabelLayoutListPrepareInput[]): LabelLa const localRect = label.getBoundingRect(); const isAxisAligned = !transform || (transform[1] < 1e-5 && transform[2] < 1e-5); - // Text has a default 1px stroke. Exclude this. - const minMargin = (layoutOption.minMargin || 0) + 2.2; + const minMargin = layoutOption.minMargin || 0; const globalRect = localRect.clone(); globalRect.applyTransform(transform); globalRect.x -= minMargin / 2; @@ -98,14 +97,31 @@ function shiftLayout( minBound: number, maxBound: number ) { - if (!list.length) { + const len = list.length; + + if (!len) { return; } list.sort(function (a, b) { - return a.label[xyDim] - b.label[xyDim]; + return a.rect[xyDim] - b.rect[xyDim]; }); + let lastPos = 0; + let delta; + for (let i = 0; i < len; i++) { + delta = list[i].rect[xyDim] - lastPos; + if (delta < 0) { + shiftForward(i, len, -delta); + } + lastPos = list[i].rect[xyDim] + list[i].rect[sizeDim]; + } + // TODO bleedMargin? + if (maxBound < lastPos) { + shiftBackward(len - 1, lastPos - maxBound); + } + + function shiftForward(start: number, end: number, delta: number) { for (let j = start; j < end; j++) { list[j].label[xyDim] += delta; @@ -113,10 +129,8 @@ function shiftLayout( const rect = list[j].rect; rect[xyDim] += delta; - if (j > start && j + 1 < end - && list[j + 1].rect[xyDim] > rect[xyDim] + rect[sizeDim] - ) { - // Shift up so it can be more equaly distributed. + if (j > start && j + 1 < end && !list[j + 1].rect.intersect(rect)) { + // Shift the following so it can be more equaly distributed. shiftBackward(j, delta / 2); return; } @@ -127,9 +141,8 @@ function shiftLayout( function shiftBackward(end: number, delta: number) { for (let j = end; j >= 0; j--) { - list[j].label[xyDim] -= delta; - const rect = list[j].rect; + list[j].label[xyDim] -= delta; rect[xyDim] -= delta; // const textSize = rect[sizeDim]; @@ -139,26 +152,11 @@ function shiftLayout( list[j].label[xyDim] -= diffToMinBound; } - if (j > 0 - && rect[xyDim] > list[j - 1].rect[xyDim] + list[j - 1].rect[sizeDim] - ) { + if (j > 0 && !rect.intersect(list[j - 1].rect)) { break; } } } - let lastPos = 0; - let delta; - const len = list.length; - for (let i = 0; i < len; i++) { - delta = list[i].label[xyDim] - lastPos; - if (delta < 0) { - shiftForward(i, len, -delta); - } - lastPos = list[i].label[xyDim] + list[i].rect[sizeDim]; - } - if (maxBound - lastPos < 0) { - shiftBackward(len - 1, lastPos - maxBound); - } } /** @@ -191,14 +189,21 @@ export function hideOverlap(labelList: LabelLayoutInfo[]) { 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 globalRect = labelItem.rect; 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; diff --git a/test/label-layout.html b/test/label-layout.html index f6fe7e616f..72eaf1a7ee 100644 --- a/test/label-layout.html +++ b/test/label-layout.html @@ -413,11 +413,15 @@ ]; option = { - xAxis: {}, + xAxis: { + splitLine: { show: false } + }, yAxis: { + splitLine: { show: false }, scale: true }, grid: { + left: 100, width: 300 }, series: [{ @@ -430,7 +434,6 @@ labelLayout: { x: 500, draggable: true, - align: 'center', moveOverlap: 'shift-y', // hideOverlap: true, minMargin: 2 From 1c7a13053ddd6517c6616d0e0def2e2442c8f3ea Mon Sep 17 00:00:00 2001 From: pissang Date: Mon, 8 Jun 2020 00:27:08 +0800 Subject: [PATCH 40/82] feat(label): rewrite the label shift algorithm in layout --- src/chart/pie/labelLayout.ts | 4 ++ src/label/labelLayoutHelper.ts | 102 ++++++++++++++++++++++----------- 2 files changed, 71 insertions(+), 35 deletions(-) diff --git a/src/chart/pie/labelLayout.ts b/src/chart/pie/labelLayout.ts index 473cf5f66c..d2f7361675 100644 --- a/src/chart/pie/labelLayout.ts +++ b/src/chart/pie/labelLayout.ts @@ -61,6 +61,10 @@ function adjustSingleSide( viewTop: number, farthestX: number ) { + if (list.length < 2) { + return; + } + list.sort(function (a, b) { return a.y - b.y; }); diff --git a/src/label/labelLayoutHelper.ts b/src/label/labelLayoutHelper.ts index 49e0c963c1..fe7693dea2 100644 --- a/src/label/labelLayoutHelper.ts +++ b/src/label/labelLayoutHelper.ts @@ -99,7 +99,7 @@ function shiftLayout( ) { const len = list.length; - if (!len) { + if (len < 2) { return; } @@ -109,52 +109,84 @@ function shiftLayout( let lastPos = 0; let delta; + const shifts = []; + let totalShifts = 0; for (let i = 0; i < len; i++) { - delta = list[i].rect[xyDim] - lastPos; + const item = list[i]; + const rect = item.rect; + delta = rect[xyDim] - lastPos; if (delta < 0) { - shiftForward(i, len, -delta); + // shiftForward(i, len, -delta); + rect[xyDim] -= delta; + item.label[xyDim] -= delta; } - lastPos = list[i].rect[xyDim] + list[i].rect[sizeDim]; + const shift = Math.max(-delta, 0); + shifts.push(shift); + totalShifts += shift; + + lastPos = rect[xyDim] + rect[sizeDim]; } - // TODO bleedMargin? - if (maxBound < lastPos) { - shiftBackward(len - 1, lastPos - maxBound); + if (totalShifts > 0) { + // Shift back to make the distribution more equally. + shiftList(-totalShifts / len, 0, len); } - - function shiftForward(start: number, end: number, delta: number) { - for (let j = start; j < end; j++) { - list[j].label[xyDim] += delta; - - const rect = list[j].rect; - rect[xyDim] += delta; - - if (j > start && j + 1 < end && !list[j + 1].rect.intersect(rect)) { - // Shift the following so it can be more equaly distributed. - shiftBackward(j, delta / 2); - return; + // TODO bleedMargin? + const minGap = list[0].rect[xyDim] - minBound; + const last = list[len - 1]; + const maxGap = maxBound - last.rect[xyDim] - last.rect[sizeDim]; + + // If ends exceed two bounds + handleBoundsGap(minGap, maxGap, 1); + handleBoundsGap(maxGap, minGap, -1); + + function handleBoundsGap(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); + } + } + else { + squeezeGaps(-gapThisBound * moveDir); } } - - shiftBackward(end - 1, delta / 2); } - function shiftBackward(end: number, delta: number) { - for (let j = end; j >= 0; j--) { - const rect = list[j].rect; - list[j].label[xyDim] -= delta; - rect[xyDim] -= delta; + function shiftList(delta: number, start: number, end: number) { + for (let i = start; i < end; i++) { + const item = list[i]; + const rect = item.rect; + rect[xyDim] += delta; + item.label[xyDim] += delta; + } + } - // const textSize = rect[sizeDim]; - const diffToMinBound = rect[xyDim] - minBound; - if (diffToMinBound < 0) { - rect[xyDim] -= diffToMinBound; - list[j].label[xyDim] -= diffToMinBound; - } + // Squeeze gaps if the labels exceed margin. + function squeezeGaps(delta: 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; + } - if (j > 0 && !rect.intersect(list[j - 1].rect)) { - break; - } + for (let i = 0; i < len - 1; i++) { + // Distribute the shift delta to all gaps. + // NOTE: + // it may overlap if remained gap is not enough for the total movements. + // aka totalGaps / delta is < 1. In this situation the label may move too much and cause overlap again. + // This is by design. Let the hideOverlap do the job instead of keep exceeding the bounds. + shiftList(gaps[i] / totalGaps * delta, 0, i + 1); } } } From a7310d8458b6a8a02dde9960b5dfe8f90d86a253 Mon Sep 17 00:00:00 2001 From: pissang Date: Mon, 8 Jun 2020 14:52:26 +0800 Subject: [PATCH 41/82] feat(label): squeeze the labels then hide if there is no space. --- src/label/LabelManager.ts | 8 +++-- src/label/labelLayoutHelper.ts | 61 ++++++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index 5c5d3204f6..5601d2b0f5 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -285,6 +285,7 @@ class LabelManager { layoutOption = layoutOption || {}; labelItem.computedLayoutOption = layoutOption; + const degreeToRadian = Math.PI / 180; if (hostEl) { hostEl.setTextConfig({ // Force to set local false. @@ -293,7 +294,8 @@ class LabelManager { position: (layoutOption.x != null || layoutOption.y != null) ? null : defaultLabelAttr.attachedPos, // Ignore rotation config on the host el if rotation is changed. - rotation: layoutOption.rotation != null ? layoutOption.rotation : defaultLabelAttr.attachedRot, + rotation: layoutOption.rotate != null + ? layoutOption.rotate * degreeToRadian : defaultLabelAttr.attachedRot, offset: [layoutOption.dx || 0, layoutOption.dy || 0] }); } @@ -317,8 +319,8 @@ class LabelManager { label.setStyle('y', defaultLabelAttr.style.y); } - label.rotation = layoutOption.rotation != null - ? layoutOption.rotation : defaultLabelAttr.rotation; + 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]; diff --git a/src/label/labelLayoutHelper.ts b/src/label/labelLayoutHelper.ts index fe7693dea2..3c8a6e3412 100644 --- a/src/label/labelLayoutHelper.ts +++ b/src/label/labelLayoutHelper.ts @@ -132,14 +132,26 @@ function shiftLayout( } // TODO bleedMargin? - const minGap = list[0].rect[xyDim] - minBound; + const first = list[0]; const last = list[len - 1]; - const maxGap = maxBound - last.rect[xyDim] - last.rect[sizeDim]; + let minGap = first.rect[xyDim] - minBound; + let maxGap = maxBound - last.rect[xyDim] - last.rect[sizeDim]; // If ends exceed two bounds handleBoundsGap(minGap, maxGap, 1); handleBoundsGap(maxGap, minGap, -1); + // Handle bailout when there is not enough space. + minGap = first.rect[xyDim] - minBound; + maxGap = maxBound - last.rect[xyDim] - last.rect[sizeDim]; + + if (minGap < 0) { + squeezeWhenBailout(-minGap); + } + if (maxGap < 0) { + squeezeWhenBailout(maxGap); + } + function handleBoundsGap(gapThisBound: number, gapOtherBound: number, moveDir: 1 | -1) { if (gapThisBound < 0) { // Move from other gap if can. @@ -180,13 +192,48 @@ function shiftLayout( return; } + if (Math.abs(delta) > totalGaps) { + delta = totalGaps * (delta < 0 ? -1 : 1); + } + for (let i = 0; i < len - 1; i++) { // Distribute the shift delta to all gaps. - // NOTE: - // it may overlap if remained gap is not enough for the total movements. - // aka totalGaps / delta is < 1. In this situation the label may move too much and cause overlap again. - // This is by design. Let the hideOverlap do the job instead of keep exceeding the bounds. - shiftList(gaps[i] / totalGaps * delta, 0, i + 1); + const movement = gaps[i] / totalGaps * delta; + if (delta > 0) { + // Forward + shiftList(movement, 0, i + 1); + } + else { + // Backward + shiftList(movement, len - i - 1, 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; + } } } } From 73f0a6b6999ca6a49b7adbfdbca3a9d11acf7283 Mon Sep 17 00:00:00 2001 From: pissang Date: Mon, 8 Jun 2020 14:52:55 +0800 Subject: [PATCH 42/82] fix(type): prefer rotate over rotation in label configuration --- src/util/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/types.ts b/src/util/types.ts index 7c7228ab13..94cf6d404e 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -862,7 +862,7 @@ export interface LabelLayoutOption { * offset on y based on the original position. */ dy?: number - rotation?: number + rotate?: number align?: ZRTextAlign verticalAlign?: ZRTextVerticalAlign width?: number From eb94696f89e1d0779995f47614e6b5da688ec68c Mon Sep 17 00:00:00 2001 From: pissang Date: Mon, 8 Jun 2020 19:48:49 +0800 Subject: [PATCH 43/82] fix(state): fix emphasis toggle too frequently caused color lift accumulation --- src/util/graphic.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/util/graphic.ts b/src/util/graphic.ts index 36d7fd3e34..fa043c18ab 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -439,6 +439,17 @@ function elementStateProxy(this: Displayable, stateName: string): DisplayableSta const currentFill = this.style.fill; const currentStroke = this.style.stroke; if (currentFill || currentStroke) { + let fromState; + if (!hasEmphasis) { + fromState = {fill: currentFill, stroke: currentStroke}; + for (let i = 0; i < this.animators.length; i++) { + const animator = this.animators[i]; + if (animator.targetName === 'style') { + animator.saveFinalToTarget(fromState, ['fill', 'stroke']); + } + } + } + state = state || {}; // Apply default color lift let emphasisStyle = state.style || {}; @@ -449,14 +460,14 @@ function elementStateProxy(this: Displayable, stateName: string): DisplayableSta state = extend({}, state); emphasisStyle = extend({}, emphasisStyle); // Already being applied 'emphasis'. DON'T lift color multiple times. - emphasisStyle.fill = hasEmphasis ? currentFill : liftColor(currentFill); + emphasisStyle.fill = hasEmphasis ? currentFill : liftColor(fromState.fill); } if (!hasFillOrStroke(emphasisStyle.stroke)) { if (!cloned) { state = extend({}, state); emphasisStyle = extend({}, emphasisStyle); } - emphasisStyle.stroke = hasEmphasis ? currentStroke : liftColor(currentStroke); + emphasisStyle.stroke = hasEmphasis ? currentStroke : liftColor(fromState.stroke); } state.style = emphasisStyle; From 3a80a47a777f9852981477e07d4f6fd9ae67e1d1 Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 9 Jun 2020 13:10:08 +0800 Subject: [PATCH 44/82] fix(toolbox): fix icon emphasis status not keep when selected. --- src/component/toolbox/ToolboxView.ts | 62 ++++++++++++++++------------ src/echarts.ts | 4 +- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/component/toolbox/ToolboxView.ts b/src/component/toolbox/ToolboxView.ts index 4ecee543f6..f8750322ca 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/echarts.ts b/src/echarts.ts index 4ce37efb3a..393808cfa4 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1668,10 +1668,10 @@ 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.render(componentModel, ecModel, api, payload); + updateZ(componentModel, componentView); updateHoverEmphasisHandler(componentView); From 3f46e8a0c51156a7ea1cf31836a5d6d388de1b0b Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 9 Jun 2020 16:27:35 +0800 Subject: [PATCH 45/82] fix(state): fix wrong emphasis lift color when animation is enabled --- src/util/graphic.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/util/graphic.ts b/src/util/graphic.ts index fa043c18ab..10e1bbd6cd 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -439,12 +439,16 @@ function elementStateProxy(this: Displayable, stateName: string): DisplayableSta const currentFill = this.style.fill; const currentStroke = this.style.stroke; if (currentFill || currentStroke) { - let fromState; + 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.targetName === 'style') { + if (animator.__fromStateTransition + // Dont consider the animation to emphasis state. + && animator.__fromStateTransition.indexOf('emphasis') < 0 + && animator.targetName === 'style' + ) { animator.saveFinalToTarget(fromState, ['fill', 'stroke']); } } From a2645c1b4186e6d13bff7a81d6d2beaa6c292cb7 Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 9 Jun 2020 16:32:42 +0800 Subject: [PATCH 46/82] fix(state): forget to remove saved states in clearStates. --- src/echarts.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/echarts.ts b/src/echarts.ts index 393808cfa4..3efed0c382 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1836,19 +1836,23 @@ class ECharts extends Eventful { // TODO If el is incremental. if (el.hasState()) { (el as DisplayableWithStatesHistory).__prevStates = el.currentStates; - 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; - } el.clearStates(); } + else if ((el as DisplayableWithStatesHistory).__prevStates) { + (el as DisplayableWithStatesHistory).__prevStates = null; + } + + 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; + } }); } From 9919c522d856b280e9bf266ecb3d446bba21af4c Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 9 Jun 2020 16:41:43 +0800 Subject: [PATCH 47/82] fix(state): use flag to determine which states to restore instead of simply save and restore --- src/echarts.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/echarts.ts b/src/echarts.ts index 3efed0c382..0d16014ea6 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1826,21 +1826,14 @@ class ECharts extends Eventful { }); }; - interface DisplayableWithStatesHistory extends Displayable { - __prevStates: string[] - }; // Clear states without animation. // TODO States on component. function clearStates(model: ComponentModel, view: ComponentView | ChartView): void { view.group.traverse(function (el: Displayable) { // TODO If el is incremental. if (el.hasState()) { - (el as DisplayableWithStatesHistory).__prevStates = el.currentStates; el.clearStates(); } - else if ((el as DisplayableWithStatesHistory).__prevStates) { - (el as DisplayableWithStatesHistory).__prevStates = null; - } const textContent = el.getTextContent(); const textGuide = el.getTextGuideLine(); @@ -1862,11 +1855,6 @@ class ECharts extends Eventful { view.group.traverse(function (el: Displayable) { // Only updated on changed element. In case element is incremental and don't wan't to rerender. if (el.__dirty && el.states && el.states.emphasis) { - const prevStates = (el as DisplayableWithStatesHistory).__prevStates; - // Restore states without animation - if (prevStates) { - el.useStates(prevStates); - } // Update state transition and enable animation again. if (enableAnimation) { graphic.setStateTransition(el, stateAnimationModel); From dd37d60aaab640743fea49bba2d476ddd3fce3cb Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 9 Jun 2020 16:53:54 +0800 Subject: [PATCH 48/82] fix(state): fix last commit, still needs save, clear and restore states without animation. --- src/echarts.ts | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/echarts.ts b/src/echarts.ts index 0d16014ea6..13fc68cf18 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1826,15 +1826,13 @@ class ECharts extends Eventful { }); }; + interface DisplayableWithStatesHistory extends Displayable { + __prevStates: string[] + }; // Clear states without animation. // TODO States on component. function clearStates(model: ComponentModel, view: ComponentView | ChartView): void { view.group.traverse(function (el: Displayable) { - // TODO If el is incremental. - if (el.hasState()) { - el.clearStates(); - } - const textContent = el.getTextContent(); const textGuide = el.getTextGuideLine(); if (el.stateTransition) { @@ -1846,6 +1844,15 @@ class ECharts extends Eventful { if (textGuide && textGuide.stateTransition) { textGuide.stateTransition = null; } + + // TODO If el is incremental. + if (el.hasState()) { + (el as DisplayableWithStatesHistory).__prevStates = el.currentStates; + el.clearStates(); + } + else if ((el as DisplayableWithStatesHistory).__prevStates) { + (el as DisplayableWithStatesHistory).__prevStates = null; + } }); } @@ -1855,6 +1862,12 @@ class ECharts extends Eventful { view.group.traverse(function (el: Displayable) { // Only updated on changed element. In case element is incremental and don't wan't to rerender. if (el.__dirty && el.states && el.states.emphasis) { + const prevStates = (el as DisplayableWithStatesHistory).__prevStates; + // Restore states without animation + if (prevStates) { + el.useStates(prevStates); + } + // Update state transition and enable animation again. if (enableAnimation) { graphic.setStateTransition(el, stateAnimationModel); From e8605d0d55649db58f6f237da45830608a50a315 Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 9 Jun 2020 22:02:39 +0800 Subject: [PATCH 49/82] fix(label): fix label animation with selected states. --- src/echarts.ts | 14 +++---- src/label/LabelManager.ts | 84 ++++++++++++++++++++++++++++++++------- 2 files changed, 76 insertions(+), 22 deletions(-) diff --git a/src/echarts.ts b/src/echarts.ts index 13fc68cf18..31a9fb864d 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1098,6 +1098,7 @@ class ECharts extends Eventful { labelManager.updateLayoutConfig(this._api); labelManager.layout(this._api); labelManager.processLabelsOverall(); + labelManager.applyAnimation(); } appendData(params: { @@ -1734,6 +1735,7 @@ class ECharts extends Eventful { labelManager.updateLayoutConfig(api); labelManager.layout(api); labelManager.processLabelsOverall(); + labelManager.applyAnimation(); ecModel.eachSeries(function (seriesModel) { const chartView = ecIns._chartsMap[seriesModel.__viewId]; @@ -1742,6 +1744,7 @@ class ECharts extends Eventful { updateStates(seriesModel, chartView); }); + // If use hover layer // TODO // updateHoverLayerStatus(ecIns, ecModel); @@ -1826,9 +1829,6 @@ class ECharts extends Eventful { }); }; - interface DisplayableWithStatesHistory extends Displayable { - __prevStates: string[] - }; // Clear states without animation. // TODO States on component. function clearStates(model: ComponentModel, view: ComponentView | ChartView): void { @@ -1847,11 +1847,11 @@ class ECharts extends Eventful { // TODO If el is incremental. if (el.hasState()) { - (el as DisplayableWithStatesHistory).__prevStates = el.currentStates; + el.prevStates = el.currentStates; el.clearStates(); } - else if ((el as DisplayableWithStatesHistory).__prevStates) { - (el as DisplayableWithStatesHistory).__prevStates = null; + else if (el.prevStates) { + el.prevStates = null; } }); } @@ -1862,7 +1862,7 @@ class ECharts extends Eventful { view.group.traverse(function (el: Displayable) { // Only updated on changed element. In case element is incremental and don't wan't to rerender. if (el.__dirty && el.states && el.states.emphasis) { - const prevStates = (el as DisplayableWithStatesHistory).__prevStates; + const prevStates = el.prevStates; // Restore states without animation if (prevStates) { el.useStates(prevStates); diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index 5601d2b0f5..9f49fe7f67 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -34,7 +34,8 @@ import { LabelLayoutOption, LabelLayoutOptionCallback, LabelLayoutOptionCallbackParams, - LabelLineOption + LabelLineOption, + Dictionary } from '../util/types'; import { parsePercent } from '../util/number'; import ChartView from '../view/Chart'; @@ -44,10 +45,10 @@ 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 } from 'zrender/src/core/util'; +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 { LabelLayoutInfo, prepareLayoutList, hideOverlap, shiftLayoutOnX, shiftLayoutOnY } from './labelLayoutHelper'; +import { prepareLayoutList, hideOverlap, shiftLayoutOnX, shiftLayoutOnY } from './labelLayoutHelper'; interface LabelDesc { label: ZRText @@ -117,6 +118,16 @@ const labelAnimationStore = makeInner<{ x: number, y: number, rotation: number + }, + oldLayoutSelect?: { + x?: number, + y?: number, + rotation?: number + }, + oldLayoutEmphasis?: { + x?: number, + y?: number, + rotation?: number } }, ZRText>(); @@ -131,6 +142,17 @@ type LabelLineOptionMixin = { 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[] = []; @@ -372,22 +394,33 @@ class LabelManager { processLabelsOverall() { each(this._chartViewList, (chartView) => { const seriesModel = chartView.__model; - const animationEnabled = seriesModel.isAnimationEnabled(); const ignoreLabelLineUpdate = chartView.ignoreLabelLineUpdate; - chartView.group.traverse((child) => { - if (child.ignore) { - return true; // Stop traverse descendants. - } + if (!ignoreLabelLineUpdate) { + chartView.group.traverse((child) => { + if (child.ignore) { + return true; // Stop traverse descendants. + } - if (!ignoreLabelLineUpdate) { this._updateLabelLine(child, seriesModel); - } + }); + } + }); + } - if (animationEnabled) { + applyAnimation() { + each(this._chartViewList, (chartView) => { + const seriesModel = chartView.__model; + const animationEnabled = seriesModel.isAnimationEnabled(); + + if (animationEnabled) { + chartView.group.traverse((child) => { + if (child.ignore) { + return true; // Stop traverse descendants. + } this._animateLabels(child, seriesModel); - } - }); + }); + } }); } @@ -442,9 +475,32 @@ class LabelManager { } 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) { @@ -471,6 +527,4 @@ class LabelManager { } - - export default LabelManager; \ No newline at end of file From 1dc916d0fea606f3b6b2029f94e24366c2e52dc7 Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 10 Jun 2020 11:32:14 +0800 Subject: [PATCH 50/82] ts: update roamHelper --- src/component/helper/roamHelper.ts | 35 +++++++++++++----------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/component/helper/roamHelper.ts b/src/component/helper/roamHelper.ts index df06db01b2..810138fcd9 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(); } From 918599159ff7e5e5ce6d6e9d86cba9e0f875c44f Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 10 Jun 2020 18:49:48 +0800 Subject: [PATCH 51/82] fix(sunburst): fix sunburst animation --- src/chart/sunburst/SunburstPiece.ts | 12 +++++++----- src/component/helper/roamHelper.ts | 4 ++-- test/pie-label.html | 1 - 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/chart/sunburst/SunburstPiece.ts b/src/chart/sunburst/SunburstPiece.ts index 0334111ee7..b2c0a14a34 100644 --- a/src/chart/sunburst/SunburstPiece.ts +++ b/src/chart/sunburst/SunburstPiece.ts @@ -130,12 +130,14 @@ class SunburstPiece extends graphic.Sector { node.dataIndex ); } + else { + // Disable animation for gradient since no interpolation method + // is supported for gradient + graphic.updateProps(sector, { + shape: sectorShape + }, seriesModel); + } - // Disable animation for gradient since no interpolation method - // is supported for gradient - graphic.updateProps(sector, { - shape: sectorShape - }, seriesModel); sector.useStyle(normalStyle); this._updateLabel(seriesModel); diff --git a/src/component/helper/roamHelper.ts b/src/component/helper/roamHelper.ts index 810138fcd9..27b2332c91 100644 --- a/src/component/helper/roamHelper.ts +++ b/src/component/helper/roamHelper.ts @@ -21,8 +21,8 @@ import Element from 'zrender/src/Element'; interface ControllerHost { target: Element, - zoom: number - zoomLimit: {min: number, max: number} + zoom?: number + zoomLimit?: {min: number, max: number} } /** diff --git a/test/pie-label.html b/test/pie-label.html index ec6a077c47..0e8fa7896b 100644 --- a/test/pie-label.html +++ b/test/pie-label.html @@ -627,7 +627,6 @@ }, series: [{ type: 'pie', - radius: '50%', data: data, roseType: 'radius', radius: ['30%', '70%'], From fdcc88d1c9b524812c54b6e5132d838a415aeb10 Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 10 Jun 2020 19:04:08 +0800 Subject: [PATCH 52/82] fix(treemap): fix treemap label displayed text --- src/chart/treemap/TreemapSeries.ts | 1 + src/chart/treemap/TreemapView.ts | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/chart/treemap/TreemapSeries.ts b/src/chart/treemap/TreemapSeries.ts index 2f6d8606f8..532fd3def0 100644 --- a/src/chart/treemap/TreemapSeries.ts +++ b/src/chart/treemap/TreemapSeries.ts @@ -46,6 +46,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 595d51a254..b490cb5bfb 100644 --- a/src/chart/treemap/TreemapView.ts +++ b/src/chart/treemap/TreemapView.ts @@ -52,9 +52,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; @@ -912,9 +912,16 @@ function renderNode( height: number, upperLabelRect?: RectLike ) { + const normalLabelModel = nodeModel.getModel( + upperLabelRect ? PATH_UPPERLABEL_NORMAL : PATH_LABEL_NOAMAL + ); + const emphasisLabelModel = nodeModel.getModel( + upperLabelRect ? PATH_UPPERLABEL_EMPHASIS : PATH_LABEL_EMPHASIS + ); + let text = retrieve( seriesModel.getFormattedLabel( - thisNode.dataIndex, 'normal', null, null, upperLabelRect ? 'upperLabel' : 'label' + thisNode.dataIndex, 'normal', null, null, normalLabelModel.get('formatter') ), nodeModel.get('name') ); @@ -923,13 +930,6 @@ function renderNode( text = iconChar ? iconChar + ' ' + text : text; } - const normalLabelModel = nodeModel.getModel( - upperLabelRect ? PATH_UPPERLABEL_NORMAL : PATH_LABEL_NOAMAL - ); - const emphasisLabelModel = nodeModel.getModel( - upperLabelRect ? PATH_UPPERLABEL_EMPHASIS : PATH_LABEL_EMPHASIS - ); - const isShow = normalLabelModel.getShallow('show'); graphic.setLabelStyle( From 678a33b9ddfaca2a86cdf549f4ab9b98fab796b6 Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 10 Jun 2020 19:08:12 +0800 Subject: [PATCH 53/82] fix(visualMap): fix error when target series not exists. --- src/component/visualMap/VisualMapModel.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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); } From f3ed3fab45c69e566e2aea00c173900325b80fde Mon Sep 17 00:00:00 2001 From: pissang Date: Sat, 20 Jun 2020 22:49:29 +0800 Subject: [PATCH 54/82] feat(label): use the new layout algorithm in pie --- src/chart/pie/labelLayout.ts | 145 ++++++++++----------------------- src/label/labelLayoutHelper.ts | 22 +++-- 2 files changed, 59 insertions(+), 108 deletions(-) diff --git a/src/chart/pie/labelLayout.ts b/src/chart/pie/labelLayout.ts index d2f7361675..470119a4d9 100644 --- a/src/chart/pie/labelLayout.ts +++ b/src/chart/pie/labelLayout.ts @@ -21,18 +21,17 @@ import {parsePercent} from '../../util/number'; import PieSeriesModel, { PieSeriesOption, PieDataItemOption } from './PieSeries'; import { VectorArray } from 'zrender/src/core/vector'; -import { HorizontalAlign, ZRRectLike, ZRTextAlign } from '../../util/types'; +import { HorizontalAlign, ZRTextAlign } from '../../util/types'; import { Sector, Polyline } from '../../util/graphic'; import ZRText from 'zrender/src/graphic/Text'; -import { RectLike } from 'zrender/src/core/BoundingRect'; +import BoundingRect, {RectLike} from 'zrender/src/core/BoundingRect'; import { each } from 'zrender/src/core/util'; import { limitTurnAngle } 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'], @@ -41,12 +40,11 @@ interface LabelLayout { minTurnAngle: number linePoints: VectorArray[] textAlign: HorizontalAlign - rotation: number, labelDistance: number, labelAlignTo: PieSeriesOption['label']['alignTo'], labelMargin: number, bleedMargin: PieSeriesOption['label']['bleedMargin'], - textRect: ZRRectLike + rect: BoundingRect } function adjustSingleSide( @@ -65,46 +63,6 @@ function adjustSingleSide( return; } - list.sort(function (a, b) { - return a.y - b.y; - }); - - let adjusted = false; - - function shiftDown(start: number, end: number, delta: number, dir: number) { - for (let j = start; j < end; j++) { - list[j].y += delta; - adjusted = true; - - if (j > start && j + 1 < end - && list[j + 1].y > list[j].y + list[j].textRect.height - ) { - // Shift up so it can be more equaly distributed. - shiftUp(j, delta / 2); - return; - } - } - - shiftUp(end - 1, delta / 2); - } - - function shiftUp(end: number, delta: number) { - for (let j = end; j >= 0; j--) { - list[j].y -= delta; - adjusted = true; - - const textHeight = list[j].textRect.height; - if (list[j].y - textHeight / 2 < viewTop) { - list[j].y = viewTop + textHeight / 2; - } - - if (j > 0 - && list[j].y > list[j - 1].y + list[j - 1].textRect.height - ) { - break; - } - } - } interface SemiInfo { list: LabelLayout[] @@ -117,13 +75,13 @@ function adjustSingleSide( const rB2 = rB * rB; for (let i = 0; i < semi.list.length; i++) { const item = semi.list[i]; - const dy = Math.abs(item.y - cy); + 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.x = cx + (dx + item.len2) * dir; + item.label.x = cx + (dx + item.len2) * dir; } } @@ -138,10 +96,10 @@ function adjustSingleSide( continue; } const item = items[i]; - const semi = item.y > cy ? bottomSemi : topSemi; - const dy = Math.abs(item.y - cy); + const semi = item.label.y > cy ? bottomSemi : topSemi; + const dy = Math.abs(item.label.y - cy); if (dy > semi.maxY) { - const dx = item.x - cx - item.len2 * dir; + 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. @@ -158,30 +116,16 @@ function adjustSingleSide( recalculateXOnSemiToAlignOnEllipseCurve(bottomSemi); } - let lastY = 0; - let delta; const len = list.length; 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].textRect.height; - } - // PENDING: - // If data is sorted. Left top is usually the small data with a lower priority. - // So shift up and make sure the data on the bottom is always displayed well. - if (viewHeight - lastY < 0) { - shiftUp(len - 1, lastY - viewHeight); } - if (adjusted) { + if (shiftLayoutOnY(list, viewTop, viewTop + viewHeight)) { recalculateX(list); } } @@ -201,15 +145,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]); } } @@ -219,6 +164,7 @@ function avoidOverlap( for (let i = 0; i < labelLayoutList.length; i++) { const layout = labelLayoutList[i]; + const label = layout.label; if (isPositionCenter(layout)) { continue; } @@ -227,10 +173,10 @@ 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; } @@ -240,14 +186,14 @@ function avoidOverlap( } } 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; @@ -260,7 +206,7 @@ function avoidOverlap( const dist = linePoints[1][0] - linePoints[2][0]; if (isAlignToEdge) { - if (layout.x < cx) { + if (label.x < cx) { linePoints[2][0] = viewLeft + layout.labelMargin + realTextWidth + layout.labelDistance; } else { @@ -269,15 +215,15 @@ function avoidOverlap( } } 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; } } } @@ -347,12 +293,6 @@ export default function ( cx = sectorShape.cx; cy = sectorShape.cy; - const textRect = label.getBoundingRect().clone(); - // Text has a default 1px stroke. Exclude this. - textRect.x -= 1; - textRect.y -= 1; - textRect.width += 2.1; - textRect.height += 2.1; const isLabelInside = labelPosition === 'inside' || labelPosition === 'inner'; if (labelPosition === 'center') { @@ -407,31 +347,37 @@ export default function ( hasLabelRotate = !!labelRotate; + label.x = textX; + label.y = textY; + label.rotation = labelRotate; + // Not sectorShape the inside label if (!isLabelInside) { + const textRect = label.getBoundingRect().clone(); + textRect.applyTransform(label.getComputedTransform()); + // Text has a default 1px stroke. Exclude this. + textRect.x -= 1; + textRect.y -= 1; + textRect.width += 2.1; + textRect.height += 2.1; + labelLayoutList.push({ label, labelLine, - x: textX, - y: textY, position: labelPosition, len: labelLineLen, len2: labelLineLen2, minTurnAngle: labelLineModel.get('minTurnAngle'), linePoints: linePoints, textAlign: textAlign, - rotation: labelRotate, labelDistance: labelDistance, labelAlignTo: labelAlignTo, labelMargin: labelMargin, bleedMargin: bleedMargin, - textRect: textRect + rect: textRect }); } else { - label.x = textX; - label.y = textY; - label.rotation = labelRotate; label.setStyle({ align: textAlign, verticalAlign: 'middle' @@ -450,11 +396,8 @@ export default function ( const layout = labelLayoutList[i]; const label = layout.label; const labelLine = layout.labelLine; - const notShowLabel = isNaN(layout.x) || isNaN(layout.y); + const notShowLabel = isNaN(label.x) || isNaN(label.y); if (label) { - label.x = layout.x; - label.y = layout.y; - label.rotation = layout.rotation; label.setStyle({ align: layout.textAlign, verticalAlign: 'middle' @@ -465,8 +408,8 @@ export default function ( } const selectState = label.states.select; if (selectState) { - selectState.x += layout.x; - selectState.y += layout.y; + selectState.x += label.x; + selectState.y += label.y; } } if (labelLine) { diff --git a/src/label/labelLayoutHelper.ts b/src/label/labelLayoutHelper.ts index 3c8a6e3412..644091eaa0 100644 --- a/src/label/labelLayoutHelper.ts +++ b/src/label/labelLayoutHelper.ts @@ -91,7 +91,7 @@ export function prepareLayoutList(input: LabelLayoutListPrepareInput[]): LabelLa } function shiftLayout( - list: LabelLayoutInfo[], + list: Pick[], xyDim: 'x' | 'y', sizeDim: 'width' | 'height', minBound: number, @@ -109,6 +109,8 @@ function shiftLayout( let lastPos = 0; let delta; + let adjusted = false; + const shifts = []; let totalShifts = 0; for (let i = 0; i < len; i++) { @@ -119,6 +121,7 @@ function shiftLayout( // shiftForward(i, len, -delta); rect[xyDim] -= delta; item.label[xyDim] -= delta; + adjusted = true; } const shift = Math.max(-delta, 0); shifts.push(shift); @@ -170,6 +173,9 @@ function shiftLayout( } 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; @@ -236,28 +242,30 @@ function shiftLayout( } } } + + return adjusted; } /** * Adjust labels on x direction to avoid overlap. */ export function shiftLayoutOnX( - list: LabelLayoutInfo[], + list: Pick[], leftBound: number, rightBound: number -) { - shiftLayout(list, 'x', 'width', leftBound, rightBound); +): boolean { + return shiftLayout(list, 'x', 'width', leftBound, rightBound); } /** * Adjust labels on y direction to avoid overlap. */ export function shiftLayoutOnY( - list: LabelLayoutInfo[], + list: Pick[], topBound: number, bottomBound: number -) { - shiftLayout(list, 'y', 'height', topBound, bottomBound); +): boolean { + return shiftLayout(list, 'y', 'height', topBound, bottomBound); } export function hideOverlap(labelList: LabelLayoutInfo[]) { From 27f20445a0311f8cf09168b09221d1a625659ef4 Mon Sep 17 00:00:00 2001 From: pissang Date: Sun, 21 Jun 2020 18:06:43 +0800 Subject: [PATCH 55/82] fix(label): fix wrong squeeze logic in label layout --- src/label/labelLayoutHelper.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/label/labelLayoutHelper.ts b/src/label/labelLayoutHelper.ts index 644091eaa0..cb2bf9a2d7 100644 --- a/src/label/labelLayoutHelper.ts +++ b/src/label/labelLayoutHelper.ts @@ -202,16 +202,20 @@ function shiftLayout( delta = totalGaps * (delta < 0 ? -1 : 1); } - for (let i = 0; i < len - 1; i++) { - // Distribute the shift delta to all gaps. - const movement = gaps[i] / totalGaps * delta; - if (delta > 0) { + if (delta > 0) { + for (let i = 0; i < len - 1; i++) { + // Distribute the shift delta to all gaps. + const movement = gaps[i] / totalGaps * delta; // Forward shiftList(movement, 0, i + 1); } - else { - // Backward - shiftList(movement, len - i - 1, len); + } + else { + // Backward + for (let i = len - 1; i > 0; i--) { + // Distribute the shift delta to all gaps. + const movement = gaps[i - 1] / totalGaps * delta; + shiftList(movement, i, len); } } } From f1dd46d531afc1475de4c3b2f42f30d80b32a2a9 Mon Sep 17 00:00:00 2001 From: pissang Date: Mon, 22 Jun 2020 14:44:55 +0800 Subject: [PATCH 56/82] enhance(labelLine): optimize smooth in labelLine --- src/chart/helper/Symbol.ts | 1 - src/chart/pie/labelLayout.ts | 14 +- src/label/labelGuideHelper.ts | 35 +- src/util/types.ts | 2 +- test/pie-label-extreme.html | 721 ++++++++++++++++++++++++++++++++++ 5 files changed, 766 insertions(+), 7 deletions(-) create mode 100644 test/pie-label-extreme.html diff --git a/src/chart/helper/Symbol.ts b/src/chart/helper/Symbol.ts index b67240a56a..8689d63795 100644 --- a/src/chart/helper/Symbol.ts +++ b/src/chart/helper/Symbol.ts @@ -231,7 +231,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) { diff --git a/src/chart/pie/labelLayout.ts b/src/chart/pie/labelLayout.ts index 470119a4d9..3458873cf6 100644 --- a/src/chart/pie/labelLayout.ts +++ b/src/chart/pie/labelLayout.ts @@ -22,7 +22,7 @@ import {parsePercent} from '../../util/number'; import PieSeriesModel, { PieSeriesOption, PieDataItemOption } from './PieSeries'; import { VectorArray } from 'zrender/src/core/vector'; import { HorizontalAlign, ZRTextAlign } from '../../util/types'; -import { Sector, Polyline } from '../../util/graphic'; +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'; @@ -413,13 +413,19 @@ export default function ( } } if (labelLine) { - if (notShowLabel || !layout.linePoints) { + const linePoints = layout.linePoints; + if (notShowLabel || !linePoints) { each(labelLine.states, setNotShow); labelLine.ignore = true; } else { - limitTurnAngle(layout.linePoints, layout.minTurnAngle); - labelLine.setShape({ points: layout.linePoints }); + limitTurnAngle(linePoints, layout.minTurnAngle); + labelLine.setShape({ points: linePoints }); + + // Set the anchor to the midpoint of sector + label.__hostTarget.textGuideLineConfig = { + anchor: new Point(linePoints[0][0], linePoints[0][1]) + }; } } } diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts index 3ea413f7e3..0d872e0263 100644 --- a/src/label/labelGuideHelper.ts +++ b/src/label/labelGuideHelper.ts @@ -31,6 +31,7 @@ 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; @@ -470,7 +471,7 @@ function setLabelLineState( // Set smooth let smooth = stateModel.get('smooth'); if (smooth && smooth === true) { - smooth = 0.4; + smooth = 0.3; } stateObj.shape = stateObj.shape || {}; (stateObj.shape as Polyline['shape']).smooth = smooth as number; @@ -478,6 +479,35 @@ function setLabelLineState( 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; + path.moveTo(points[0][0], points[0][1]); + if (smooth > 0) { + 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 { + path.lineTo(points[1][0], points[1][1]); + path.lineTo(points[2][0], points[2][1]); + } +} + /** * Create a label line if necessary and set it's style. */ @@ -538,5 +568,8 @@ export function setLabelLineStyle( defaults(labelLine.style, defaultStyle); // Not fill. labelLine.style.fill = null; + + // Custom the buildPath. + labelLine.buildPath = buildLabelLinePath; } } diff --git a/src/util/types.ts b/src/util/types.ts index 94cf6d404e..e4f508e16e 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -1153,7 +1153,7 @@ export interface SeriesOption extends labelLine?: LabelLineOption /** - * Global label layout option in label layout stage. + * Overall label layout option in label layout stage. */ labelLayout?: LabelLayoutOption | LabelLayoutOptionCallback diff --git a/test/pie-label-extreme.html b/test/pie-label-extreme.html new file mode 100644 index 0000000000..1fcee550e8 --- /dev/null +++ b/test/pie-label-extreme.html @@ -0,0 +1,721 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + From 306779e26c3793147d75c95384067adea7565916 Mon Sep 17 00:00:00 2001 From: pissang Date: Mon, 22 Jun 2020 15:18:35 +0800 Subject: [PATCH 57/82] fix(labelLine): only set smooth when it's changed. --- src/label/labelGuideHelper.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts index 0d872e0263..bc14b449f9 100644 --- a/src/label/labelGuideHelper.ts +++ b/src/label/labelGuideHelper.ts @@ -474,7 +474,9 @@ function setLabelLineState( smooth = 0.3; } stateObj.shape = stateObj.shape || {}; - (stateObj.shape as Polyline['shape']).smooth = smooth as number; + 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; From 804a99c668e8a178761060135cfcf8379bd34f25 Mon Sep 17 00:00:00 2001 From: pissang Date: Mon, 22 Jun 2020 16:23:22 +0800 Subject: [PATCH 58/82] feat(pie): support overflow wrap --- src/chart/pie/PieSeries.ts | 1 + src/chart/pie/labelLayout.ts | 1 - src/label/labelGuideHelper.ts | 2 +- test/pie-label-extreme.html | 8 ++++++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/chart/pie/PieSeries.ts b/src/chart/pie/PieSeries.ts index d2b3c181ba..5ed642b076 100644 --- a/src/chart/pie/PieSeries.ts +++ b/src/chart/pie/PieSeries.ts @@ -247,6 +247,7 @@ class PieSeriesModel extends SeriesModel { // If rotate around circle rotate: 0, show: true, + overflow: 'truncate', // 'outer', 'inside', 'center' position: 'outer', // 'none', 'labelLine', 'edge'. Works only when position is 'outer' diff --git a/src/chart/pie/labelLayout.ts b/src/chart/pie/labelLayout.ts index 3458873cf6..14b8c3b1f2 100644 --- a/src/chart/pie/labelLayout.ts +++ b/src/chart/pie/labelLayout.ts @@ -197,7 +197,6 @@ function avoidOverlap( // TODOTODO // layout.text = textContain.truncateText(layout.text, targetTextWidth, layout.font); layout.label.style.width = targetTextWidth; - layout.label.style.overflow = 'truncate'; if (layout.labelAlignTo === 'edge') { realTextWidth = targetTextWidth; // realTextWidth = textContain.getWidth(layout.text, layout.font); diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts index bc14b449f9..1614b398f3 100644 --- a/src/label/labelGuideHelper.ts +++ b/src/label/labelGuideHelper.ts @@ -408,7 +408,7 @@ const tmpProjPoint = new Point(); * @param minTurnAngle Radian of minimum turn angle. 0 - 180 */ export function limitTurnAngle(linePoints: number[][], minTurnAngle: number) { - if (!(minTurnAngle < 180 && minTurnAngle > 0)) { + if (!(minTurnAngle <= 180 && minTurnAngle > 0)) { return; } minTurnAngle = minTurnAngle / 180 * Math.PI; diff --git a/test/pie-label-extreme.html b/test/pie-label-extreme.html index 1fcee550e8..e919d8a765 100644 --- a/test/pie-label-extreme.html +++ b/test/pie-label-extreme.html @@ -681,12 +681,14 @@ labelLine: { length: 15, length2: 15, - smooth: 0.3 + smooth: 0.3, + minTurnAngle: 110 }, label: { margin: 25, bleedMargin: 10, - alignTo: 'none' + alignTo: 'none', + overflow: 'truncate' } }; @@ -706,10 +708,12 @@ labelFolder.open(); labelLineFolder.open(); labelFolder.add(config.label, 'alignTo', ['none', 'edge', 'labelLine']).onChange(update); + labelFolder.add(config.label, 'overflow', ['truncate', 'wrap']).onChange(update); labelFolder.add(config.label, 'margin', 0, 50).onChange(update); labelFolder.add(config.label, 'bleedMargin', 0, 500).onChange(update); labelLineFolder.add(config.labelLine, 'length', 0, 500).onChange(update); labelLineFolder.add(config.labelLine, 'length2', 0, 500).onChange(update); + labelLineFolder.add(config.labelLine, 'minTurnAngle', 0, 180).onChange(update); labelLineFolder.add(config.labelLine, 'smooth', 0, 1).onChange(update); }); From 7db0a9c01d9eb817e2d8adf39754dc8c314f5a55 Mon Sep 17 00:00:00 2001 From: pissang Date: Mon, 22 Jun 2020 23:59:17 +0800 Subject: [PATCH 59/82] feat(label): enable darkMode --- src/echarts.ts | 6 ++++++ src/model/globalDefault.ts | 2 ++ src/theme/dark.ts | 2 ++ 3 files changed, 10 insertions(+) diff --git a/src/echarts.ts b/src/echarts.ts index 31a9fb864d..ba7f00eace 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1327,6 +1327,7 @@ class ECharts extends Eventful { // Set background let backgroundColor = ecModel.get('backgroundColor') || 'transparent'; + const darkMode = ecModel.get('darkMode'); // In IE8 if (!env.canvasSupported) { @@ -1338,6 +1339,11 @@ class ECharts extends Eventful { } else { zr.setBackgroundColor(backgroundColor); + + // Force set dark mode. + if (darkMode != null && darkMode !== 'auto') { + zr.setDarkMode(darkMode); + } } performPostUpdateFuncs(ecModel, api); diff --git a/src/model/globalDefault.ts b/src/model/globalDefault.ts index fe4feb3592..c47f865388 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 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: { From f40e0dd120edfcda5b1fb63d057627d9639c9701 Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 23 Jun 2020 00:00:36 +0800 Subject: [PATCH 60/82] =?UTF-8?q?feat(label):=20change=20label=20default?= =?UTF-8?q?=20color=20to=20black/white,=20'inherit'=20to=20set=20color?= =?UTF-8?q?=C2=A0same=20with=20element?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chart/bar/BarView.ts | 2 +- src/chart/bar/PictorialBarView.ts | 2 +- src/chart/custom.ts | 2 +- src/chart/gauge/GaugeView.ts | 6 +-- src/chart/helper/Line.ts | 2 +- src/chart/helper/Symbol.ts | 2 +- src/chart/pie/PieSeries.ts | 1 + src/chart/pie/PieView.ts | 5 +-- src/chart/radar/RadarView.ts | 2 +- src/chart/treemap/TreemapView.ts | 2 +- src/component/marker/MarkAreaView.ts | 2 +- src/util/graphic.ts | 61 ++++++++++++++-------------- 12 files changed, 45 insertions(+), 44 deletions(-) diff --git a/src/chart/bar/BarView.ts b/src/chart/bar/BarView.ts index 90b377749c..b9f86e20d4 100644 --- a/src/chart/bar/BarView.ts +++ b/src/chart/bar/BarView.ts @@ -566,7 +566,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 e7d93daf5e..e4a7429996 100644 --- a/src/chart/bar/PictorialBarView.ts +++ b/src/chart/bar/PictorialBarView.ts @@ -952,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/custom.ts b/src/chart/custom.ts index 3b801ff588..28de10d7f0 100644 --- a/src/chart/custom.ts +++ b/src/chart/custom.ts @@ -482,7 +482,7 @@ function makeRenderItem(customSeries, data, ecModel, api) { : currLabelNormalModel; const textStyle = graphicUtil.createTextStyle(labelModel, null, { - autoColor: currVisualColor, + inheritColor: currVisualColor, isRectText: true }); diff --git a/src/chart/gauge/GaugeView.ts b/src/chart/gauge/GaugeView.ts index 0540baca77..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 })); } @@ -423,7 +423,7 @@ class GaugeView extends ChartView { text: data.getName(0), align: 'center', verticalAlign: 'middle' - }, {autoColor: autoColor}) + }, {inheritColor: autoColor}) })); } } @@ -463,7 +463,7 @@ class GaugeView extends ChartView { height: isNaN(height) ? null : height, align: 'center', verticalAlign: 'middle' - }, {autoColor: autoColor}) + }, {inheritColor: autoColor}) })); } } diff --git a/src/chart/helper/Line.ts b/src/chart/helper/Line.ts index d67694d48f..72268aca1c 100644 --- a/src/chart/helper/Line.ts +++ b/src/chart/helper/Line.ts @@ -256,7 +256,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/Symbol.ts b/src/chart/helper/Symbol.ts index 8689d63795..2dd123d52d 100644 --- a/src/chart/helper/Symbol.ts +++ b/src/chart/helper/Symbol.ts @@ -266,7 +266,7 @@ class Symbol extends graphic.Group { labelFetcher: seriesModel, labelDataIndex: idx, defaultText: getLabelDefaultText, - autoColor: visualColor as ColorString + inheritColor: visualColor as ColorString } ); diff --git a/src/chart/pie/PieSeries.ts b/src/chart/pie/PieSeries.ts index 5ed642b076..01a828c210 100644 --- a/src/chart/pie/PieSeries.ts +++ b/src/chart/pie/PieSeries.ts @@ -244,6 +244,7 @@ class PieSeriesModel extends SeriesModel { height: null, label: { + color: 'inherit', // If rotate around circle rotate: 0, show: true, diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index 2c6fe97486..1598a95f76 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -182,6 +182,7 @@ class PiePiece extends graphic.Sector { { labelFetcher: data.hostModel as PieSeriesModel, labelDataIndex: idx, + inheritColor: visualColor, defaultText: seriesModel.getFormattedLabel(idx, 'normal') || data.getName(idx) }, @@ -193,9 +194,7 @@ class PiePiece extends graphic.Sector { // Set textConfig on sector. sector.setTextConfig({ local: true, - insideStroke: visualColor, - // insideFill: 'auto', - outsideFill: visualColor + outsideFill: labelModel.get('color') === 'inherit' ? visualColor : 'auto' }); // Make sure update style on labelText after setLabelStyle. diff --git a/src/chart/radar/RadarView.ts b/src/chart/radar/RadarView.ts index 397b67f14a..29bd6fb86f 100644 --- a/src/chart/radar/RadarView.ts +++ b/src/chart/radar/RadarView.ts @@ -237,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/treemap/TreemapView.ts b/src/chart/treemap/TreemapView.ts index b490cb5bfb..db0631758d 100644 --- a/src/chart/treemap/TreemapView.ts +++ b/src/chart/treemap/TreemapView.ts @@ -936,7 +936,7 @@ function renderNode( rectEl, normalLabelModel, emphasisLabelModel, { defaultText: isShow ? text : null, - autoColor: visualColor + inheritColor: visualColor } ); diff --git a/src/component/marker/MarkAreaView.ts b/src/component/marker/MarkAreaView.ts index 7398abb692..574fc550d9 100644 --- a/src/component/marker/MarkAreaView.ts +++ b/src/component/marker/MarkAreaView.ts @@ -300,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/util/graphic.ts b/src/util/graphic.ts index 10e1bbd6cd..bf863bac21 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -122,10 +122,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 + inheritColor?: ColorString getTextPosition?: (textStyleModel: Model, isEmphasis?: boolean) => string | string[] | number[] @@ -816,7 +816,7 @@ export {setLabelStyle}; export function createTextStyle( textStyleModel: Model, specifiedTextStyle?: TextStyleProps, // Can be overrided by settings in model. - opt?: Pick, + opt?: Pick, isNotNormal?: boolean, isAttached?: boolean // If text is attached on an element. If so, auto color will handling in zrender. ) { @@ -831,7 +831,7 @@ export function createTextStyle( export function createTextConfig( textStyle: TextStyleProps, textStyleModel: Model, - opt?: Pick, + opt?: Pick, isNotNormal?: boolean ) { const textConfig: ElementTextConfig = {}; @@ -868,19 +868,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; } @@ -898,7 +889,7 @@ export function createTextConfig( function setTextStyleCommon( textStyle: TextStyleProps, textStyleModel: Model, - opt?: Pick, + opt?: Pick, isNotNormal?: boolean, isAttached?: boolean ) { @@ -1003,7 +994,7 @@ function setTokenTextStyle( textStyle: TextStyleProps['rich'][string], textStyleModel: Model, globalTextStyle: LabelOption, - opt?: Pick, + opt?: Pick, isNotNormal?: boolean, isAttached?: boolean, isBlock?: boolean @@ -1011,14 +1002,24 @@ function setTokenTextStyle( // In merge mode, default value should not be given. 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; @@ -1040,8 +1041,8 @@ function setTokenTextStyle( // TODO 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; } } @@ -1073,11 +1074,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++) { From 27c3cce86ed76771b381e7920a89ebe6534e6b77 Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 23 Jun 2020 00:00:55 +0800 Subject: [PATCH 61/82] fix(labelLine): fix error when labelLine don't have points --- src/label/labelGuideHelper.ts | 10 +++-- test/pie-alignTo.html | 70 +++++++++++++++++------------------ test/pie-label-extreme.html | 5 ++- 3 files changed, 46 insertions(+), 39 deletions(-) diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts index 1614b398f3..5de50926fa 100644 --- a/src/label/labelGuideHelper.ts +++ b/src/label/labelGuideHelper.ts @@ -485,8 +485,11 @@ function setLabelLineState( 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) { + 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) { @@ -505,8 +508,9 @@ function buildLabelLinePath(path: CanvasRenderingContext2D, shape: Polyline['sha path.bezierCurveTo(midPoint2[0], midPoint2[1], midPoint2[0], midPoint2[1], points[2][0], points[2][1]); } else { - path.lineTo(points[1][0], points[1][1]); - path.lineTo(points[2][0], points[2][1]); + for (let i = 1; i < points.length; i++) { + path.lineTo(points[i][0], points[i][1]); + } } } diff --git a/test/pie-alignTo.html b/test/pie-alignTo.html index db08612c2a..ab95d50a00 100644 --- a/test/pie-alignTo.html +++ b/test/pie-alignTo.html @@ -211,45 +211,45 @@ }); var config = { length2: 15, - margin: 20 + margin: 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: { + margin: config.margin, + overflow: config.overflow } - chart2.setOption(option2); - } - }); - - 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; + }] + } + chart0.setOption(newOpt); + chart1.setOption(newOpt); + chartNone.setOption(newOpt); + + const newOpt2 = { series: [] }; + for (var i = 0; i < 4; ++i) { + newOpt2.series.push({ + labelLine: { + length2: config.length2, + }, + label: { + margin: config.margin, + overflow: config.overflow } - chart2.setOption(option2); - } - }); - }); + }) + } + chart2.setOption(newOpt2); + } + + gui.add(config, 'length2', 0, 300).onChange(update); + gui.add(config, 'margin', 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 index e919d8a765..fb9a2780e8 100644 --- a/test/pie-label-extreme.html +++ b/test/pie-label-extreme.html @@ -74,6 +74,9 @@ "fontSize": 10 }, "labelLine": { + "lineStyle": { + "color": '#ccc' + }, "smooth": true }, "data": [{ @@ -708,7 +711,7 @@ labelFolder.open(); labelLineFolder.open(); labelFolder.add(config.label, 'alignTo', ['none', 'edge', 'labelLine']).onChange(update); - labelFolder.add(config.label, 'overflow', ['truncate', 'wrap']).onChange(update); + labelFolder.add(config.label, 'overflow', ['truncate', 'break', 'breakAll']).onChange(update); labelFolder.add(config.label, 'margin', 0, 50).onChange(update); labelFolder.add(config.label, 'bleedMargin', 0, 500).onChange(update); labelLineFolder.add(config.labelLine, 'length', 0, 500).onChange(update); From 83df54fa3997ccd1b91b63ee397a0579ce0aba12 Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 23 Jun 2020 12:46:38 +0800 Subject: [PATCH 62/82] fix(label): label color is dark by default in pie and sunburst. --- src/chart/pie/PieSeries.ts | 2 +- src/chart/pie/PieView.ts | 14 ++++++++------ src/chart/sunburst/SunburstPiece.ts | 4 +--- test/aria-pie.html | 1 + test/label-layout.html | 2 +- test/sunburst-drink.html | 3 +++ 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/chart/pie/PieSeries.ts b/src/chart/pie/PieSeries.ts index 01a828c210..439a5bd9ce 100644 --- a/src/chart/pie/PieSeries.ts +++ b/src/chart/pie/PieSeries.ts @@ -244,7 +244,7 @@ class PieSeriesModel extends SeriesModel { height: null, label: { - color: 'inherit', + // color: 'inherit', // If rotate around circle rotate: 0, show: true, diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index 1598a95f76..613172753b 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -26,9 +26,10 @@ import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../ExtensionAPI'; import { Payload, ColorString, ECElement } from '../../util/types'; import List from '../../data/List'; -import PieSeriesModel, {PieDataItemOption} from './PieSeries'; +import PieSeriesModel, {PieDataItemOption, PieSeriesOption} from './PieSeries'; import labelLayout from './labelLayout'; import { setLabelLineStyle } from '../../label/labelGuideHelper'; +import Model from '../../model/Model'; function updateDataSelected( this: PiePiece, @@ -176,9 +177,9 @@ class PiePiece extends graphic.Sector { 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, @@ -193,8 +194,9 @@ class PiePiece extends graphic.Sector { // Set textConfig on sector. sector.setTextConfig({ - local: true, - outsideFill: labelModel.get('color') === 'inherit' ? visualColor : 'auto' + // reset position, rotation + position: null, + rotation: null }); // Make sure update style on labelText after setLabelStyle. diff --git a/src/chart/sunburst/SunburstPiece.ts b/src/chart/sunburst/SunburstPiece.ts index b2c0a14a34..00daa35b90 100644 --- a/src/chart/sunburst/SunburstPiece.ts +++ b/src/chart/sunburst/SunburstPiece.ts @@ -231,11 +231,9 @@ class SunburstPiece extends graphic.Sector { 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' }; - if (labelColor) { - sectorState.textConfig.insideStroke = sectorState.textConfig.outsideFill = labelColor; - } let r; const labelPadding = getLabelAttr(labelStateModel, 'distance') || 0; 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/label-layout.html b/test/label-layout.html index 72eaf1a7ee..941d3778ce 100644 --- a/test/label-layout.html +++ b/test/label-layout.html @@ -307,6 +307,7 @@ type: 'pie', radius: ['50%', '70%'], label: { + color: 'inherit', show: false }, emphasis: { @@ -450,7 +451,6 @@ formatter: function (param) { return param.data[3]; }, - color: '#333', textBorderColor: '#fff', textBorderWidth: 1, position: 'top' 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%', From 895cc0017ea3f1630508db43ef35d2e5b5f1f93e Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 23 Jun 2020 14:08:51 +0800 Subject: [PATCH 63/82] change(bar): change to fade out animation when series is removed. #12543 --- src/chart/bar/BarView.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/chart/bar/BarView.ts b/src/chart/bar/BarView.ts index 2b2e7f2a8c..617a554203 100644 --- a/src/chart/bar/BarView.ts +++ b/src/chart/bar/BarView.ts @@ -704,8 +704,8 @@ function removeRect( // Not show text when animating el.removeTextContent(); updateProps(el, { - shape: { - width: 0 + style: { + opacity: 0 } }, animationModel, dataIndex, function () { el.parent && el.parent.remove(el); @@ -720,8 +720,8 @@ function removeSector( // Not show text when animating el.removeTextContent(); updateProps(el, { - shape: { - r: el.shape.r0 + style: { + opacity: 0 } }, animationModel, dataIndex, function () { el.parent && el.parent.remove(el); From f6c5bef95b29781dcc78e4d949cd83be405f409f Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 23 Jun 2020 22:31:03 +0800 Subject: [PATCH 64/82] fix(label): recalculate labelLine if label position is changed by users in layout stage --- src/chart/funnel/FunnelView.ts | 9 +- src/echarts.ts | 2 - src/label/LabelManager.ts | 50 +++--- src/label/labelGuideHelper.ts | 2 +- test/label-layout.html | 283 +++++++++++++++++++++++++-------- test/pie-label.html | 5 +- 6 files changed, 250 insertions(+), 101 deletions(-) diff --git a/src/chart/funnel/FunnelView.ts b/src/chart/funnel/FunnelView.ts index e10efa0e5c..334510d003 100644 --- a/src/chart/funnel/FunnelView.ts +++ b/src/chart/funnel/FunnelView.ts @@ -26,6 +26,7 @@ import List from '../../data/List'; 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; @@ -131,10 +132,16 @@ class FunnelPiece extends graphic.Polygon { outsideFill: visualColor }); + const linePoints = labelLayout.linePoints; + labelLine.setShape({ - points: labelLayout.linePoints || labelLayout.linePoints + 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. graphic.updateProps(labelText, { diff --git a/src/echarts.ts b/src/echarts.ts index ed58220b68..68e6331604 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1127,7 +1127,6 @@ class ECharts extends Eventful { labelManager.updateLayoutConfig(this._api); labelManager.layout(this._api); labelManager.processLabelsOverall(); - labelManager.applyAnimation(); } appendData(params: { @@ -1770,7 +1769,6 @@ class ECharts extends Eventful { labelManager.updateLayoutConfig(api); labelManager.layout(api); labelManager.processLabelsOverall(); - labelManager.applyAnimation(); ecModel.eachSeries(function (seriesModel) { const chartView = ecIns._chartsMap[seriesModel.__viewId]; diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index 9f49fe7f67..3a84d2f782 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -113,7 +113,7 @@ const LABEL_OPTION_TO_STYLE_KEYS = ['align', 'verticalAlign', 'width', 'height'] const dummyTransformable = new Transformable(); -const labelAnimationStore = makeInner<{ +const labelLayoutInnerStore = makeInner<{ oldLayout: { x: number, y: number, @@ -128,7 +128,9 @@ const labelAnimationStore = makeInner<{ x?: number, y?: number, rotation?: number - } + }, + + changedByUser?: boolean }, ZRText>(); const labelLineAnimationStore = makeInner<{ @@ -321,10 +323,12 @@ class LabelManager { offset: [layoutOption.dx || 0, layoutOption.dy || 0] }); } + let changedByUser = 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. + changedByUser = changedByUser || true; } else { label.x = defaultLabelAttr.x; @@ -335,11 +339,15 @@ class LabelManager { // TODO height of chart view. label.y = parsePercent(layoutOption.y, height); label.setStyle('y', 0); // Ignore movement in style. + changedByUser = changedByUser || true; } else { label.y = defaultLabelAttr.y; label.setStyle('y', defaultLabelAttr.style.y); } + if (changedByUser) { + labelLayoutInnerStore(label).changedByUser = true; + } label.rotation = layoutOption.rotate != null ? layoutOption.rotate * degreeToRadian : defaultLabelAttr.rotation; @@ -395,32 +403,26 @@ class LabelManager { each(this._chartViewList, (chartView) => { const seriesModel = chartView.__model; const ignoreLabelLineUpdate = chartView.ignoreLabelLineUpdate; + const animationEnabled = seriesModel.isAnimationEnabled(); - if (!ignoreLabelLineUpdate) { - chartView.group.traverse((child) => { - if (child.ignore) { - return true; // Stop traverse descendants. - } + 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).changedByUser; + } + if (needsUpdateLabelLine) { this._updateLabelLine(child, seriesModel); - }); - } - }); - } - - applyAnimation() { - each(this._chartViewList, (chartView) => { - const seriesModel = chartView.__model; - const animationEnabled = seriesModel.isAnimationEnabled(); + } - if (animationEnabled) { - chartView.group.traverse((child) => { - if (child.ignore) { - return true; // Stop traverse descendants. - } + if (animationEnabled) { this._animateLabels(child, seriesModel); - }); - } + } + }); }); } @@ -457,7 +459,7 @@ class LabelManager { const guideLine = el.getTextGuideLine(); // Animate if (textEl && !textEl.ignore && !textEl.invisible) { - const layoutStore = labelAnimationStore(textEl); + const layoutStore = labelLayoutInnerStore(textEl); const oldLayout = layoutStore.oldLayout; const newProps = { x: textEl.x, diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts index 5de50926fa..aea74d4fd6 100644 --- a/src/label/labelGuideHelper.ts +++ b/src/label/labelGuideHelper.ts @@ -362,7 +362,7 @@ export function updateLabelLinePoints( let minDist = Infinity; const anchorPoint = labelGuideConfig && labelGuideConfig.anchor; const targetTransform = target.getComputedTransform(); - const targetInversedTransform = invert([], targetTransform); + const targetInversedTransform = targetTransform && invert([], targetTransform); const len = labelLineModel.get('length2') || 0; if (anchorPoint) { diff --git a/test/label-layout.html b/test/label-layout.html index 941d3778ce..a78077527d 100644 --- a/test/label-layout.html +++ b/test/label-layout.html @@ -48,6 +48,7 @@
+
@@ -214,81 +215,148 @@ - - - + + + + + + diff --git a/test/pie-label.html b/test/pie-label.html index 0e8fa7896b..69acccf3d4 100644 --- a/test/pie-label.html +++ b/test/pie-label.html @@ -629,10 +629,7 @@ type: 'pie', data: data, roseType: 'radius', - radius: ['30%', '70%'], - label: { - textBorderColor: 'none' - } + radius: ['30%', '70%'] }] }; From 97e15ec43f1fb5cf311efa5eac103bf3e6f21fbf Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 24 Jun 2020 17:54:40 +0800 Subject: [PATCH 65/82] fix(animation): force stop remove animation before init again. --- src/chart/bar/BarView.ts | 7 ++-- src/chart/bar/PictorialBarView.ts | 2 +- src/chart/helper/Symbol.ts | 16 ++++---- src/chart/line/LineSeries.ts | 1 + src/chart/line/LineView.ts | 2 +- src/component/timeline/SliderTimelineView.ts | 2 +- src/echarts.ts | 2 +- src/util/graphic.ts | 40 ++++++++++++++++---- 8 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/chart/bar/BarView.ts b/src/chart/bar/BarView.ts index 617a554203..d7dc1e14af 100644 --- a/src/chart/bar/BarView.ts +++ b/src/chart/bar/BarView.ts @@ -28,7 +28,8 @@ import { enableHoverEmphasis, setLabelStyle, updateLabel, - initLabel + initLabel, + removeElement } from '../../util/graphic'; import Path, { PathProps } from 'zrender/src/graphic/Path'; import Group from 'zrender/src/graphic/Group'; @@ -703,7 +704,7 @@ function removeRect( ) { // Not show text when animating el.removeTextContent(); - updateProps(el, { + removeElement(el, { style: { opacity: 0 } @@ -719,7 +720,7 @@ function removeSector( ) { // Not show text when animating el.removeTextContent(); - updateProps(el, { + removeElement(el, { style: { opacity: 0 } diff --git a/src/chart/bar/PictorialBarView.ts b/src/chart/bar/PictorialBarView.ts index e4a7429996..48a7774d3d 100644 --- a/src/chart/bar/PictorialBarView.ts +++ b/src/chart/bar/PictorialBarView.ts @@ -869,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); diff --git a/src/chart/helper/Symbol.ts b/src/chart/helper/Symbol.ts index 37be176dc0..e7bed16699 100644 --- a/src/chart/helper/Symbol.ts +++ b/src/chart/helper/Symbol.ts @@ -169,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._sizeX, - scaleY: this._sizeY + 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); } @@ -311,7 +309,7 @@ class Symbol extends graphic.Group { // Not show text when animating !(opt && opt.keepLabel) && (symbolPath.removeTextContent()); - graphic.updateProps( + graphic.removeElement( symbolPath, { style: { diff --git a/src/chart/line/LineSeries.ts b/src/chart/line/LineSeries.ts index 1fcb7485cb..42e0ce3f11 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, diff --git a/src/chart/line/LineView.ts b/src/chart/line/LineView.ts index 22dc22a0a0..8c31b3383b 100644 --- a/src/chart/line/LineView.ts +++ b/src/chart/line/LineView.ts @@ -501,7 +501,7 @@ class LineView extends ChartView { // Stop symbol animation and sync with line points // FIXME performance? data.eachItemGraphicEl(function (el) { - el.stopAnimation(true); + el.stopAnimation('', true); }); // In the case data zoom triggerred refreshing frequently diff --git a/src/component/timeline/SliderTimelineView.ts b/src/component/timeline/SliderTimelineView.ts index e8dc40d536..2e77374207 100644 --- a/src/component/timeline/SliderTimelineView.ts +++ b/src/component/timeline/SliderTimelineView.ts @@ -795,7 +795,7 @@ function pointerMoveTo( pointer.y = 0; } else { - pointer.stopAnimation(true); + pointer.stopAnimation('', true); pointer.animateTo({ x: toCoord, y: 0 diff --git a/src/echarts.ts b/src/echarts.ts index 68e6331604..4f76c11c32 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -554,7 +554,7 @@ class ECharts extends Eventful { const list = zr.storage.getDisplayList(); // Stop animations zrUtil.each(list, function (el: Element) { - el.stopAnimation(true); + el.stopAnimation('', true); }); return (zr.painter as SVGPainter).toDataURL(); diff --git a/src/util/graphic.ts b/src/util/graphic.ts index 182a2b74fd..58753971fe 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -688,7 +688,7 @@ type LabelModelForText = Model; function getLabelText( - opt?: SetLabelStyleOpt, + opt: SetLabelStyleOpt, normalModel: LabelModel, emphasisModel: LabelModel, interpolateValues?: ParsedValue | ParsedValue[] @@ -1136,7 +1136,7 @@ type AnimateOrSetPropsOption = { }; function animateOrSetProps( - isUpdate: boolean, + animationType: 'init' | 'update' | 'remove', el: Element, props: Props, animatableModel?: Model & { @@ -1158,19 +1158,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') { @@ -1185,6 +1188,11 @@ function animateOrSetProps( duration = duration(dataIndex as number); } + if (!isRemove) { + // Must stop the remove animation. + el.stopAnimation('remove'); + } + duration > 0 ? ( isFrom @@ -1194,6 +1202,7 @@ function animateOrSetProps( easing: animationEasing, done: cb, force: !!cb || !!during, + scope: animationType, during: during }) : el.animateTo(props, { @@ -1203,6 +1212,7 @@ function animateOrSetProps( done: cb, force: !!cb || !!during, setToFinal: true, + scope: animationType, during: during }) ) @@ -1240,7 +1250,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}; @@ -1261,7 +1271,21 @@ 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( @@ -1278,7 +1302,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); From abdb83190fbb5cd9ba143299fcf5f037f9ef6ae2 Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 24 Jun 2020 19:24:23 +0800 Subject: [PATCH 66/82] fix(line): optimize area animation --- src/chart/line/LineSeries.ts | 6 +++++- src/chart/line/LineView.ts | 1 - test/label-layout.html | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/chart/line/LineSeries.ts b/src/chart/line/LineSeries.ts index 42e0ce3f11..d4ad0d57c7 100644 --- a/src/chart/line/LineSeries.ts +++ b/src/chart/line/LineSeries.ts @@ -79,6 +79,11 @@ export interface LineSeriesOption extends SeriesOption, origin?: 'auto' | 'start' | 'end' } + emphasis?: { + label?: LabelOption + itemStyle?: ItemStyleOption + } + step?: false | 'start' | 'end' | 'middle' smooth?: boolean @@ -87,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 8c31b3383b..f49a1c881e 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/test/label-layout.html b/test/label-layout.html index a78077527d..011c444bec 100644 --- a/test/label-layout.html +++ b/test/label-layout.html @@ -340,10 +340,10 @@ const isLeft = params.labelRect.x < cx; return isLeft ? { x: cx - 200, - textAlign: 'right', + align: 'right', } : { x: cx + 100, - textAlign: 'left' + align: 'left' }; } }] From 2e2a76e8bafad1968a49588e207085b6e78108c0 Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 24 Jun 2020 19:43:54 +0800 Subject: [PATCH 67/82] refact(graph): use blur state instead of change opacity by manual --- src/chart/graph/GraphView.ts | 127 +++++++++++------------------ src/chart/helper/Line.ts | 4 + src/chart/helper/Symbol.ts | 2 +- src/component/helper/roamHelper.ts | 2 +- 4 files changed, 52 insertions(+), 83 deletions(-) diff --git a/src/chart/graph/GraphView.ts b/src/chart/graph/GraphView.ts index f1dd3b1d70..85758685e0 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,24 @@ 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 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; +function fadeInItem(nodeOrEdge: GraphNode | GraphEdge) { + const el = nodeOrEdge.getGraphicEl() as Symbol | Line; + if ((el as Symbol).getSymbolPath) { + (el as Symbol).getSymbolPath().removeState('blur'); + } + else { + (el as Line).getLinePath().removeState('blur'); } - - 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 as Symbol).getSymbolPath) { + (el as Symbol).getSymbolPath().useState('blur'); + } + else { + (el as Line).getLinePath().useState('blur'); + } } class GraphView extends ChartView { @@ -120,7 +87,6 @@ class GraphView extends ChartView { private _model: GraphSeriesModel; private _layoutTimeout: number; - private _unfocusDelayTimer: number; private _layouting: boolean; @@ -214,8 +180,13 @@ 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 = { + opacity: symbolPath.style.opacity * 0.1 + }; + el.on('mouseover', (el as any)[FOCUS_ADJACENCY] = function () { - graphView._clearTimer(); api.dispatchAction({ type: 'focusNodeAdjacency', seriesId: seriesModel.id, @@ -230,14 +201,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: linePath.style.opacity * 0.1 + }; + el.on('mouseover', (el as any)[FOCUS_ADJACENCY] = function () { - graphView._clearTimer(); api.dispatchAction({ type: 'focusNodeAdjacency', seriesId: seriesModel.id, @@ -293,27 +270,15 @@ 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( @@ -335,27 +300,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 +330,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); }); } diff --git a/src/chart/helper/Line.ts b/src/chart/helper/Line.ts index 72268aca1c..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; diff --git a/src/chart/helper/Symbol.ts b/src/chart/helper/Symbol.ts index e7bed16699..85f7553f97 100644 --- a/src/chart/helper/Symbol.ts +++ b/src/chart/helper/Symbol.ts @@ -95,7 +95,7 @@ class Symbol extends graphic.Group { * @param {boolean} toLastFrame */ stopSymbolAnimation(toLastFrame: boolean) { - this.childAt(0).stopAnimation(toLastFrame); + this.childAt(0).stopAnimation('', toLastFrame); } /** diff --git a/src/component/helper/roamHelper.ts b/src/component/helper/roamHelper.ts index 27b2332c91..07d5839607 100644 --- a/src/component/helper/roamHelper.ts +++ b/src/component/helper/roamHelper.ts @@ -22,7 +22,7 @@ import Element from 'zrender/src/Element'; interface ControllerHost { target: Element, zoom?: number - zoomLimit?: {min: number, max: number} + zoomLimit?: {min?: number, max?: number} } /** From 52c0f4d6ffa47f52db4e7436c7554f50c33d9c77 Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 24 Jun 2020 19:52:34 +0800 Subject: [PATCH 68/82] refact(sankey): use blur state. --- src/chart/graph/GraphView.ts | 30 ++++---- src/chart/sankey/SankeyView.ts | 124 +++++++++++---------------------- 2 files changed, 57 insertions(+), 97 deletions(-) diff --git a/src/chart/graph/GraphView.ts b/src/chart/graph/GraphView.ts index 85758685e0..33539f1083 100644 --- a/src/chart/graph/GraphView.ts +++ b/src/chart/graph/GraphView.ts @@ -53,21 +53,26 @@ function isViewCoordSys(coordSys: CoordinateSystem): coordSys is View { function fadeInItem(nodeOrEdge: GraphNode | GraphEdge) { const el = nodeOrEdge.getGraphicEl() as Symbol | Line; - if ((el as Symbol).getSymbolPath) { - (el as Symbol).getSymbolPath().removeState('blur'); - } - else { - (el as Line).getLinePath().removeState('blur'); + if (el) { + if ((el as Symbol).getSymbolPath) { + (el as Symbol).getSymbolPath().removeState('blur'); + } + else { + (el as Line).getLinePath().removeState('blur'); + } + } } function fadeOutItem(nodeOrEdge: GraphNode | GraphEdge) { const el = nodeOrEdge.getGraphicEl() as Symbol | Line; - if ((el as Symbol).getSymbolPath) { - (el as Symbol).getSymbolPath().useState('blur'); - } - else { - (el as Line).getLinePath().useState('blur'); + if (el) { + if ((el as Symbol).getSymbolPath) { + (el as Symbol).getSymbolPath().useState('blur'); + } + else { + (el as Line).getLinePath().useState('blur'); + } } } @@ -183,7 +188,8 @@ class GraphView extends ChartView { const symbolPath = el.getSymbolPath(); const blurState = symbolPath.ensureState('blur'); blurState.style = { - opacity: symbolPath.style.opacity * 0.1 + // TODO Based on the original opacity. + opacity: 0.1 }; el.on('mouseover', (el as any)[FOCUS_ADJACENCY] = function () { @@ -211,7 +217,7 @@ class GraphView extends ChartView { const linePath = el.getLinePath(); const blurState = linePath.ensureState('blur'); blurState.style = { - opacity: linePath.style.opacity * 0.1 + opacity: 0.1 }; el.on('mouseover', (el as any)[FOCUS_ADJACENCY] = function () { diff --git a/src/chart/sankey/SankeyView.ts b/src/chart/sankey/SankeyView.ts index 6c03f7bb85..bf9ae1755f 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(); @@ -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.1 + }; + 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); }); } } From 195ba7d305d887b64e297bc6ba40b12da00edc8b Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 2 Jul 2020 16:02:28 +0800 Subject: [PATCH 69/82] fix(label): fix interpolate value affect the original value. --- src/chart/sankey/SankeyView.ts | 4 ++-- src/util/graphic.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/chart/sankey/SankeyView.ts b/src/chart/sankey/SankeyView.ts index bf9ae1755f..9c31ab485d 100644 --- a/src/chart/sankey/SankeyView.ts +++ b/src/chart/sankey/SankeyView.ts @@ -218,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': @@ -347,7 +347,7 @@ class SankeyView extends ChartView { if (edgeModel.get('focusNodeAdjacency')) { const blurState = el.ensureState('blur'); blurState.style = { - opacity: 0.1 + opacity: 0.02 }; el.on('mouseover', el.focusNodeAdjHandler = function () { diff --git a/src/util/graphic.ts b/src/util/graphic.ts index 58753971fe..b39bf52efa 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -705,9 +705,9 @@ function getLabelText( null, labelDimIndex, normalModel && normalModel.get('formatter'), - { + interpolateValues != null ? { value: interpolateValues - } + } : null ); } if (baseText == null) { From b26b5d52814dfce3e42006a66aeecca1025e52e7 Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 2 Jul 2020 18:26:22 +0800 Subject: [PATCH 70/82] feat(label): can change labelLine points in labelLayout --- src/label/LabelManager.ts | 44 ++++++++--- src/util/types.ts | 5 ++ test/pie-label-mobile.html | 156 +++++++++++++++++++++++++++++++++++++ 3 files changed, 194 insertions(+), 11 deletions(-) create mode 100644 test/pie-label-mobile.html diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index 3a84d2f782..90ff6ada95 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -45,7 +45,7 @@ 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 { retrieve2, each, keys, isFunction, filter, indexOf, map, guid } from 'zrender/src/core/util'; import { PathStyleProps } from 'zrender/src/graphic/Path'; import Model from '../model/Model'; import { prepareLayoutList, hideOverlap, shiftLayoutOnX, shiftLayoutOnY } from './labelLayoutHelper'; @@ -92,9 +92,20 @@ interface SavedLabelAttr { rect: RectLike } -function prepareLayoutCallbackParams(labelItem: LabelDesc): LabelLayoutOptionCallbackParams { +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, @@ -105,7 +116,8 @@ function prepareLayoutCallbackParams(labelItem: LabelDesc): LabelLayoutOptionCal // x: labelAttr.x, // y: labelAttr.y, align: label.style.align, - verticalAlign: label.style.verticalAlign + verticalAlign: label.style.verticalAlign, + labelLinePoints: cloneArr(labelLine && labelLine.shape.points) }; } @@ -130,7 +142,7 @@ const labelLayoutInnerStore = makeInner<{ rotation?: number }, - changedByUser?: boolean + needsUpdateLabelLine?: boolean }, ZRText>(); const labelLineAnimationStore = makeInner<{ @@ -299,7 +311,7 @@ class LabelManager { // TODO A global layout option? if (typeof labelItem.layoutOption === 'function') { layoutOption = labelItem.layoutOption( - prepareLayoutCallbackParams(labelItem) + prepareLayoutCallbackParams(labelItem, hostEl) ); } else { @@ -323,12 +335,12 @@ class LabelManager { offset: [layoutOption.dx || 0, layoutOption.dy || 0] }); } - let changedByUser = false; + 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. - changedByUser = changedByUser || true; + needsUpdateLabelLine = true; } else { label.x = defaultLabelAttr.x; @@ -339,16 +351,25 @@ class LabelManager { // TODO height of chart view. label.y = parsePercent(layoutOption.y, height); label.setStyle('y', 0); // Ignore movement in style. - changedByUser = changedByUser || true; + needsUpdateLabelLine = true; } else { label.y = defaultLabelAttr.y; label.setStyle('y', defaultLabelAttr.style.y); } - if (changedByUser) { - labelLayoutInnerStore(label).changedByUser = true; + + 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; @@ -357,6 +378,7 @@ class LabelManager { label.setStyle(key, layoutOption[key] != null ? layoutOption[key] : defaultLabelAttr.style[key]); } + if (layoutOption.draggable) { label.draggable = true; label.cursor = 'move'; @@ -413,7 +435,7 @@ class LabelManager { let needsUpdateLabelLine = !ignoreLabelLineUpdate; const label = child.getTextContent(); if (!needsUpdateLabelLine && label) { - needsUpdateLabelLine = labelLayoutInnerStore(label).changedByUser; + needsUpdateLabelLine = labelLayoutInnerStore(label).needsUpdateLabelLine; } if (needsUpdateLabelLine) { this._updateLabelLine(child, seriesModel); diff --git a/src/util/types.ts b/src/util/types.ts index a739352521..d6f3b7962b 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -828,6 +828,8 @@ export interface LabelLayoutOptionCallbackParams { verticalAlign: ZRTextVerticalAlign rect: RectLike labelRect: RectLike + // Points of label line in pie/funnel + labelLinePoints?: number[][] // x: number // y: number }; @@ -877,6 +879,9 @@ export interface LabelLayoutOption { verticalAlign?: ZRTextVerticalAlign width?: number height?: number + + // Points of label line in pie/funnel + labelLinePoints?: number[][] } export type LabelLayoutOptionCallback = (params: LabelLayoutOptionCallbackParams) => LabelLayoutOption; diff --git a/test/pie-label-mobile.html b/test/pie-label-mobile.html new file mode 100644 index 0000000000..205bf539c8 --- /dev/null +++ b/test/pie-label-mobile.html @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + From 62563461213f21d3f0dd7a76791f60b4d612da1d Mon Sep 17 00:00:00 2001 From: pissang Date: Thu, 2 Jul 2020 19:51:53 +0800 Subject: [PATCH 71/82] fix(label): optimize layout on pie. --- src/label/labelLayoutHelper.ts | 60 +++++++++++++++++++++------------- test/pie-label-mobile.html | 10 +++--- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/label/labelLayoutHelper.ts b/src/label/labelLayoutHelper.ts index cb2bf9a2d7..0a7f227bd9 100644 --- a/src/label/labelLayoutHelper.ts +++ b/src/label/labelLayoutHelper.ts @@ -95,7 +95,8 @@ function shiftLayout( xyDim: 'x' | 'y', sizeDim: 'width' | 'height', minBound: number, - maxBound: number + maxBound: number, + balanceShift: boolean ) { const len = list.length; @@ -129,7 +130,7 @@ function shiftLayout( lastPos = rect[xyDim] + rect[sizeDim]; } - if (totalShifts > 0) { + if (totalShifts > 0 && balanceShift) { // Shift back to make the distribution more equally. shiftList(-totalShifts / len, 0, len); } @@ -137,16 +138,19 @@ function shiftLayout( // TODO bleedMargin? const first = list[0]; const last = list[len - 1]; - let minGap = first.rect[xyDim] - minBound; - let maxGap = maxBound - last.rect[xyDim] - last.rect[sizeDim]; + let minGap: number; + let maxGap: number; + updateMinMaxGap(); - // If ends exceed two bounds - handleBoundsGap(minGap, maxGap, 1); - handleBoundsGap(maxGap, minGap, -1); + // 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. - minGap = first.rect[xyDim] - minBound; - maxGap = maxBound - last.rect[xyDim] - last.rect[sizeDim]; + updateMinMaxGap(); if (minGap < 0) { squeezeWhenBailout(-minGap); @@ -155,7 +159,12 @@ function shiftLayout( squeezeWhenBailout(maxGap); } - function handleBoundsGap(gapThisBound: number, gapOtherBound: number, moveDir: 1 | -1) { + 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); @@ -163,11 +172,11 @@ function shiftLayout( shiftList(moveFromMaxGap * moveDir, 0, len); const remained = moveFromMaxGap + gapThisBound; if (remained < 0) { - squeezeGaps(-remained * moveDir); + squeezeGaps(-remained * moveDir, 1); } } else { - squeezeGaps(-gapThisBound * moveDir); + squeezeGaps(-gapThisBound * moveDir, 1); } } } @@ -185,7 +194,7 @@ function shiftLayout( } // Squeeze gaps if the labels exceed margin. - function squeezeGaps(delta: number) { + function squeezeGaps(delta: number, maxSqeezePercent: number) { const gaps: number[] = []; let totalGaps = 0; for (let i = 1; i < len; i++) { @@ -198,14 +207,12 @@ function shiftLayout( return; } - if (Math.abs(delta) > totalGaps) { - delta = totalGaps * (delta < 0 ? -1 : 1); - } + 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] / totalGaps * delta; + const movement = gaps[i] * squeezePercent; // Forward shiftList(movement, 0, i + 1); } @@ -214,8 +221,8 @@ function shiftLayout( // Backward for (let i = len - 1; i > 0; i--) { // Distribute the shift delta to all gaps. - const movement = gaps[i - 1] / totalGaps * delta; - shiftList(movement, i, len); + const movement = gaps[i - 1] * squeezePercent; + shiftList(-movement, i, len); } } } @@ -256,9 +263,14 @@ function shiftLayout( export function shiftLayoutOnX( list: Pick[], leftBound: number, - rightBound: 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); + return shiftLayout(list, 'x', 'width', leftBound, rightBound, balanceShift); } /** @@ -267,9 +279,11 @@ export function shiftLayoutOnX( export function shiftLayoutOnY( list: Pick[], topBound: number, - bottomBound: 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); + return shiftLayout(list, 'y', 'height', topBound, bottomBound, balanceShift); } export function hideOverlap(labelList: LabelLayoutInfo[]) { diff --git a/test/pie-label-mobile.html b/test/pie-label-mobile.html index 205bf539c8..15b512655a 100644 --- a/test/pie-label-mobile.html +++ b/test/pie-label-mobile.html @@ -133,12 +133,10 @@ const isLeft = params.labelRect.x < chart.getWidth() / 2; const points = params.labelLinePoints; // Update the end point. - if (isLeft) { - points[2][0] = params.labelRect.x; - } - else { - points[2][0] = params.labelRect.x + params.labelRect.width; - } + points[2][0] = isLeft + ? params.labelRect.x + : params.labelRect.x + params.labelRect.width; + return { labelLinePoints: points } From e629132e568e182b3a9cb602f61ef405bbeaf1db Mon Sep 17 00:00:00 2001 From: pissang Date: Fri, 3 Jul 2020 10:16:43 +0800 Subject: [PATCH 72/82] fix(label): labelLine has same z, zlevel with element --- src/echarts.ts | 16 +++++++++++----- src/label/labelGuideHelper.ts | 3 +-- test/pie-label-mobile.html | 10 ++++++++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/echarts.ts b/src/echarts.ts index 4f76c11c32..9595ada48a 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1854,13 +1854,19 @@ class ECharts extends Eventful { 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; } } }); diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts index aea74d4fd6..f089d30974 100644 --- a/src/label/labelGuideHelper.ts +++ b/src/label/labelGuideHelper.ts @@ -520,8 +520,7 @@ function buildLabelLinePath(path: CanvasRenderingContext2D, shape: Polyline['sha export function setLabelLineStyle( targetEl: Element, statesModels: Record, - defaultStyle?: Polyline['style'], - defaultConfig?: Element['textGuideLineConfig'] + defaultStyle?: Polyline['style'] ) { let labelLine = targetEl.getTextGuideLine(); const label = targetEl.getTextContent(); diff --git a/test/pie-label-mobile.html b/test/pie-label-mobile.html index 15b512655a..674e7b0228 100644 --- a/test/pie-label-mobile.html +++ b/test/pie-label-mobile.html @@ -114,11 +114,16 @@ series: [{ type: 'pie', radius: [20, 60], + itemStyle: { + borderColor: '#fff', + borderWidth: 1 + }, label: { alignTo: 'edge', - formatter: '{name|{b}}\n{time|{c}小时}', + formatter: '{name|{b}}\n{time|{c} 小时}', margin: 10, lineHeight: 15, + // color: 'inherit', rich: { time: { fontSize: 10, @@ -127,7 +132,8 @@ } }, labelLine: { - length: 5 + length: 15, + length2: 0 }, labelLayout: function (params) { const isLeft = params.labelRect.x < chart.getWidth() / 2; From bf6c1441bc31ff17ff7443e36ea8bacb14072edc Mon Sep 17 00:00:00 2001 From: pissang Date: Fri, 3 Jul 2020 15:04:03 +0800 Subject: [PATCH 73/82] refact(label): add minMargin for layouting. the original margin is renamed to edgeDistance The new add config is minMargin instead of margin is for not breaking the previous code using margin. --- src/chart/pie/PieSeries.ts | 4 ++-- src/chart/pie/labelLayout.ts | 27 ++++++++++++++------------- src/label/LabelManager.ts | 2 +- src/label/labelLayoutHelper.ts | 3 +-- src/preprocessor/backwardCompat.ts | 22 +++++++++++++++++++++- src/util/graphic.ts | 4 ++++ src/util/types.ts | 12 ++++++------ test/label-layout.html | 10 +++++----- test/pie-alignTo.html | 18 +++++++++--------- test/pie-label-mobile.html | 4 ++-- 10 files changed, 65 insertions(+), 41 deletions(-) diff --git a/src/chart/pie/PieSeries.ts b/src/chart/pie/PieSeries.ts index 439a5bd9ce..e3e877ccd3 100644 --- a/src/chart/pie/PieSeries.ts +++ b/src/chart/pie/PieSeries.ts @@ -44,7 +44,7 @@ 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 @@ -255,7 +255,7 @@ class PieSeriesModel extends SeriesModel { 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. diff --git a/src/chart/pie/labelLayout.ts b/src/chart/pie/labelLayout.ts index 14b8c3b1f2..ed77887850 100644 --- a/src/chart/pie/labelLayout.ts +++ b/src/chart/pie/labelLayout.ts @@ -42,7 +42,7 @@ interface LabelLayout { textAlign: HorizontalAlign labelDistance: number, labelAlignTo: PieSeriesOption['label']['alignTo'], - labelMargin: number, + edgeDistance: number, bleedMargin: PieSeriesOption['label']['bleedMargin'], rect: BoundingRect } @@ -178,10 +178,10 @@ function avoidOverlap( if (isAlignToEdge) { 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; } } @@ -206,10 +206,10 @@ function avoidOverlap( const dist = linePoints[1][0] - linePoints[2][0]; if (isAlignToEdge) { if (label.x < cx) { - linePoints[2][0] = viewLeft + layout.labelMargin + realTextWidth + layout.labelDistance; + 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; } } @@ -265,7 +265,7 @@ 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 labelLineModel = itemModel.getModel('labelLine'); @@ -316,8 +316,8 @@ export default function ( if (labelAlignTo === 'edge') { // Adjust textX because text align of edge is opposite textX = dx < 0 - ? viewLeft + labelMargin - : viewLeft + viewWidth - labelMargin; + ? viewLeft + edgeDistance + : viewLeft + viewWidth - edgeDistance; } else { textX = x3 + (dx < 0 ? -labelDistance : labelDistance); @@ -355,10 +355,11 @@ export default function ( const textRect = label.getBoundingRect().clone(); textRect.applyTransform(label.getComputedTransform()); // Text has a default 1px stroke. Exclude this. - textRect.x -= 1; - textRect.y -= 1; - textRect.width += 2.1; - textRect.height += 2.1; + 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, @@ -371,7 +372,7 @@ export default function ( textAlign: textAlign, labelDistance: labelDistance, labelAlignTo: labelAlignTo, - labelMargin: labelMargin, + edgeDistance: edgeDistance, bleedMargin: bleedMargin, rect: textRect }); diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index 90ff6ada95..7cac2740ba 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -45,7 +45,7 @@ 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, map, guid } from 'zrender/src/core/util'; +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'; diff --git a/src/label/labelLayoutHelper.ts b/src/label/labelLayoutHelper.ts index 0a7f227bd9..738b6b6da3 100644 --- a/src/label/labelLayoutHelper.ts +++ b/src/label/labelLayoutHelper.ts @@ -57,14 +57,13 @@ export function prepareLayoutList(input: LabelLayoutListPrepareInput[]): LabelLa continue; } - const layoutOption = rawItem.computedLayoutOption; const label = rawItem.label; const transform = label.getComputedTransform(); // NOTE: Get bounding rect after getComputedTransform, or label may not been updated by the host el. const localRect = label.getBoundingRect(); const isAxisAligned = !transform || (transform[1] < 1e-5 && transform[2] < 1e-5); - const minMargin = layoutOption.minMargin || 0; + const minMargin = label.style.margin || 0; const globalRect = localRect.clone(); globalRect.applyTransform(transform); globalRect.x -= minMargin / 2; diff --git a/src/preprocessor/backwardCompat.ts b/src/preprocessor/backwardCompat.ts index 13c22a00bc..f710dcc43e 100644 --- a/src/preprocessor/backwardCompat.ts +++ b/src/preprocessor/backwardCompat.ts @@ -26,6 +26,7 @@ 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(','); @@ -94,6 +95,18 @@ function compatBarItemStyle(option: Dictionary) { } } +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); @@ -122,6 +135,13 @@ export default function (option: ECUnitOption, isTheme?: boolean) { 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') { const pointerColor = get(seriesOpt, 'pointer.color'); @@ -136,7 +156,7 @@ export default function (option: ECUnitOption, isTheme?: boolean) { const data = seriesOpt.data; if (data && !isTypedArray(data)) { for (let i = 0; i < data.length; i++) { - if (data[i]) { + if (typeof data[i] === 'object') { compatBarItemStyle(data[i]); compatBarItemStyle(data[i] && data[i].emphasis); } diff --git a/src/util/graphic.ts b/src/util/graphic.ts index b39bf52efa..41a8e9d1e9 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -961,6 +961,10 @@ function setTextStyleCommon( if (overflow) { textStyle.overflow = overflow; } + const margin = textStyleModel.get('minMargin'); + if (margin != null) { + textStyle.margin = margin; + } setTokenTextStyle(textStyle, textStyleModel, globalTextStyle, opt, isNotNormal, isAttached, true); } diff --git a/src/util/types.ts b/src/util/types.ts index d6f3b7962b..9184fa45d3 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -774,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' @@ -851,12 +857,6 @@ export interface LabelLayoutOption { * @default 'none' */ hideOverlap?: boolean - - /** - * Minimal margin between two labels which will be considered as overlapped. - */ - minMargin?: number - /** * If label is draggable. */ diff --git a/test/label-layout.html b/test/label-layout.html index 011c444bec..bd17c2fa94 100644 --- a/test/label-layout.html +++ b/test/label-layout.html @@ -440,8 +440,7 @@ draggable: true, align: 'center', moveOverlap: 'shift-x', - hideOverlap: true, - minMargin: 10 + hideOverlap: true }, labelLine: { show: true, @@ -455,6 +454,7 @@ formatter: function (param) { return param.data[3]; }, + minMargin: 10, color: '#333', textBorderColor: '#fff', textBorderWidth: 1, @@ -504,8 +504,7 @@ x: 500, draggable: true, moveOverlap: 'shift-y', - // hideOverlap: true, - minMargin: 2 + // hideOverlap: true }, labelLine: { show: true, @@ -521,7 +520,8 @@ }, textBorderColor: '#fff', textBorderWidth: 1, - position: 'top' + position: 'top', + minMargin: 2 } }] }; diff --git a/test/pie-alignTo.html b/test/pie-alignTo.html index ab95d50a00..78c0e274f0 100644 --- a/test/pie-alignTo.html +++ b/test/pie-alignTo.html @@ -73,7 +73,7 @@ length2: 15 }, label: { - margin: 20, + edgeDistance: 20, position: 'outer' } }] @@ -89,7 +89,7 @@ length2: 15 }, label: { - margin: 20, + edgeDistance: 20, position: 'outer', alignTo: 'labelLine' } @@ -106,7 +106,7 @@ length2: 15 }, label: { - margin: 20, + edgeDistance: 20, position: 'outer', alignTo: 'edge' } @@ -141,7 +141,7 @@ length2: 15 }, label: { - margin: 20, + edgeDistance: 20, position: 'outer', alignTo: 'labelLine' }, @@ -176,7 +176,7 @@ length2: 15 }, label: { - margin: 20, + edgeDistance: 20, position: 'outer', alignTo: 'labelLine' }, @@ -211,7 +211,7 @@ }); var config = { length2: 15, - margin: 20, + edgeDistance: 20, overflow: 'truncate' }; @@ -222,7 +222,7 @@ length2: config.length2 }, label: { - margin: config.margin, + edgeDistance: config.edgeDistance, overflow: config.overflow } }] @@ -238,7 +238,7 @@ length2: config.length2, }, label: { - margin: config.margin, + edgeDistance: config.edgeDistance, overflow: config.overflow } }) @@ -247,7 +247,7 @@ } gui.add(config, 'length2', 0, 300).onChange(update); - gui.add(config, 'margin', 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-mobile.html b/test/pie-label-mobile.html index 674e7b0228..352dbbe883 100644 --- a/test/pie-label-mobile.html +++ b/test/pie-label-mobile.html @@ -121,9 +121,9 @@ label: { alignTo: 'edge', formatter: '{name|{b}}\n{time|{c} 小时}', - margin: 10, + minMargin: 5, + edgeDistance: 10, lineHeight: 15, - // color: 'inherit', rich: { time: { fontSize: 10, From c003487f6c0ef96be90d6f24955a2d2d5641ee94 Mon Sep 17 00:00:00 2001 From: pissang Date: Fri, 3 Jul 2020 23:29:03 +0800 Subject: [PATCH 74/82] feat(label): add maxSurfaceAngle limit in pie --- src/chart/pie/PieSeries.ts | 19 +++++++--- src/chart/pie/labelLayout.ts | 36 ++++++++++-------- src/label/labelGuideHelper.ts | 69 +++++++++++++++++++++++++++++++++++ test/pie-label-extreme.html | 6 +-- test/pie-label-mobile.html | 45 ++++++++++++++++++++++- 5 files changed, 150 insertions(+), 25 deletions(-) diff --git a/src/chart/pie/PieSeries.ts b/src/chart/pie/PieSeries.ts index e3e877ccd3..de34310934 100644 --- a/src/chart/pie/PieSeries.ts +++ b/src/chart/pie/PieSeries.ts @@ -51,18 +51,26 @@ interface PieLabelOption extends Omit { 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?: LabelLineOption + labelLine?: PieLabelLineOption emphasis?: { itemStyle?: ItemStyleOption label?: PieLabelOption - labelLine?: LabelLineOption + labelLine?: PieLabelLineOption } } export interface PieSeriesOption extends @@ -81,7 +89,7 @@ export interface PieSeriesOption extends // TODO: TYPE Color Callback itemStyle?: ItemStyleOption label?: PieLabelOption - labelLine?: LabelLineOption + labelLine?: PieLabelLineOption clockwise?: boolean startAngle?: number @@ -99,7 +107,7 @@ export interface PieSeriesOption extends emphasis?: { itemStyle?: ItemStyleOption label?: PieLabelOption - labelLine?: LabelLineOption + labelLine?: PieLabelLineOption } animationType?: 'expansion' | 'scale' @@ -272,7 +280,8 @@ class PieSeriesModel extends SeriesModel { // 引导线两段中的第二段长度 length2: 15, smooth: false, - minTurnAngle: 100, + minTurnAngle: 90, + maxSurfaceAngle: 90, lineStyle: { // color: 各异, width: 1, diff --git a/src/chart/pie/labelLayout.ts b/src/chart/pie/labelLayout.ts index ed77887850..d47f18fa3a 100644 --- a/src/chart/pie/labelLayout.ts +++ b/src/chart/pie/labelLayout.ts @@ -26,7 +26,7 @@ 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 } from '../../label/labelGuideHelper'; +import { limitTurnAngle, limitSurfaceAngle } from '../../label/labelGuideHelper'; import { shiftLayoutOnY } from '../../label/labelLayoutHelper'; const RADIAN = Math.PI / 180; @@ -38,6 +38,8 @@ interface LabelLayout { len: number len2: number minTurnAngle: number + maxSurfaceAngle: number + surfaceNormal: Point linePoints: VectorArray[] textAlign: HorizontalAlign labelDistance: number, @@ -281,8 +283,8 @@ export default function ( } const midAngle = (sectorShape.startAngle + sectorShape.endAngle) / 2; - const dx = Math.cos(midAngle); - const dy = Math.sin(midAngle); + const nx = Math.cos(midAngle); + const ny = Math.sin(midAngle); let textX; let textY; @@ -300,27 +302,27 @@ export default function ( textAlign = 'center'; } else { - const x1 = (isLabelInside ? (sectorShape.r + sectorShape.r0) / 2 * dx : sectorShape.r * dx) + cx; - const y1 = (isLabelInside ? (sectorShape.r + sectorShape.r0) / 2 * dy : sectorShape.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 - sectorShape.r); - const y2 = y1 + dy * (labelLineLen + r - sectorShape.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 + 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]]; @@ -329,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; @@ -340,7 +342,7 @@ export default function ( } else { labelRotate = rotate - ? (dx < 0 ? -midAngle + Math.PI : -midAngle) + ? (nx < 0 ? -midAngle + Math.PI : -midAngle) : 0; } @@ -368,6 +370,8 @@ export default function ( len: labelLineLen, len2: labelLineLen2, minTurnAngle: labelLineModel.get('minTurnAngle'), + maxSurfaceAngle: labelLineModel.get('maxSurfaceAngle'), + surfaceNormal: new Point(nx, ny), linePoints: linePoints, textAlign: textAlign, labelDistance: labelDistance, @@ -420,6 +424,8 @@ export default function ( } else { limitTurnAngle(linePoints, layout.minTurnAngle); + limitSurfaceAngle(linePoints, layout.surfaceNormal, layout.maxSurfaceAngle); + labelLine.setShape({ points: linePoints }); // Set the anchor to the midpoint of sector diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts index f089d30974..a501d1c3d8 100644 --- a/src/label/labelGuideHelper.ts +++ b/src/label/labelGuideHelper.ts @@ -444,6 +444,10 @@ export function limitTurnAngle(linePoints: number[][], minTurnAngle: number) { 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); } @@ -455,6 +459,71 @@ export function limitTurnAngle(linePoints: number[][], minTurnAngle: number) { } } +/** + * 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; diff --git a/test/pie-label-extreme.html b/test/pie-label-extreme.html index fb9a2780e8..7ce7d45ca8 100644 --- a/test/pie-label-extreme.html +++ b/test/pie-label-extreme.html @@ -688,7 +688,7 @@ minTurnAngle: 110 }, label: { - margin: 25, + edgeDistance: 25, bleedMargin: 10, alignTo: 'none', overflow: 'truncate' @@ -699,7 +699,7 @@ chart.setOption({ series: { label: Object.assign({}, config.label, { - margin: config.label.margin + '%' + edgeDistance: config.label.edgeDistance + '%' }), labelLine: config.labelLine } @@ -712,7 +712,7 @@ labelLineFolder.open(); labelFolder.add(config.label, 'alignTo', ['none', 'edge', 'labelLine']).onChange(update); labelFolder.add(config.label, 'overflow', ['truncate', 'break', 'breakAll']).onChange(update); - labelFolder.add(config.label, 'margin', 0, 50).onChange(update); + labelFolder.add(config.label, 'edgeDistance', 0, 50).onChange(update); labelFolder.add(config.label, 'bleedMargin', 0, 500).onChange(update); labelLineFolder.add(config.labelLine, 'length', 0, 500).onChange(update); labelLineFolder.add(config.labelLine, 'length2', 0, 500).onChange(update); diff --git a/test/pie-label-mobile.html b/test/pie-label-mobile.html index 352dbbe883..aef18c6b80 100644 --- a/test/pie-label-mobile.html +++ b/test/pie-label-mobile.html @@ -28,6 +28,7 @@ + @@ -46,6 +47,9 @@ background-color: #fff; text-align: left; } + .dg { + text-align: left; + } @@ -73,7 +77,7 @@ { name: '其它', value: 3.8 } ], - //////////////////////////////////////// + // //////////////////////////////////////// [ { name: '银河帝国5:迈向基地', value: 3.8 }, { name: '俞军产品方法论', value: 2.3 }, @@ -94,6 +98,8 @@ ] ]; + const charts = []; + datas.forEach(function (data) { const dom = document.createElement('div'); dom.classList.add('chart'); @@ -133,7 +139,8 @@ }, labelLine: { length: 15, - length2: 0 + length2: 0, + maxSurfaceAngle: 80 }, labelLayout: function (params) { const isLeft = params.labelRect.x < chart.getWidth() / 2; @@ -150,7 +157,41 @@ data: data }] }); + + charts.push(chart); }); + + const gui = new dat.GUI(); + const config = { + labelLine: { + smooth: 0, + maxSurfaceAngle: 80 + }, + label: { + edgeDistance: 10 + } + }; + + function update() { + charts.forEach(function (chart) { + chart.setOption({ + series: { + label: Object.assign({}, config.label, { + edgeDistance: config.label.edgeDistance + }), + labelLine: config.labelLine + } + }); + }); + } + + const labelFolder = gui.addFolder('label'); + const labelLineFolder = gui.addFolder('labelLine'); + labelFolder.open(); + labelLineFolder.open(); + labelFolder.add(config.label, 'edgeDistance', 0, 50).onChange(update); + labelLineFolder.add(config.labelLine, 'maxSurfaceAngle', 0, 90).onChange(update); + labelLineFolder.add(config.labelLine, 'smooth', 0, 1).onChange(update); }); From cac1f9b908c30de239756537e85fee3a7ef2e23b Mon Sep 17 00:00:00 2001 From: pissang Date: Sat, 4 Jul 2020 10:14:09 +0800 Subject: [PATCH 75/82] ts: fix types --- src/chart/custom.ts | 2 +- src/chart/map/MapView.ts | 2 +- src/component/geo/GeoView.ts | 2 +- src/component/helper/MapDraw.ts | 26 +++++++++++--------------- src/coord/geo/GeoModel.ts | 2 +- src/echarts.ts | 2 +- src/util/graphic.ts | 8 ++++---- 7 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/chart/custom.ts b/src/chart/custom.ts index 6fdb2e1578..684d16814f 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 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/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..d95c515074 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()); @@ -370,7 +368,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 +430,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/coord/geo/GeoModel.ts b/src/coord/geo/GeoModel.ts index 0158272040..ba8f524abb 100644 --- a/src/coord/geo/GeoModel.ts +++ b/src/coord/geo/GeoModel.ts @@ -102,7 +102,7 @@ export interface GeoOption extends regions: RegoinOption[]; - stateAnimation?: AnimationOption + stateAnimation?: AnimationOptionMixin } const LABEL_FORMATTER_NORMAL = ['label', 'formatter'] as const; diff --git a/src/echarts.ts b/src/echarts.ts index 9595ada48a..235fd09226 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -854,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 diff --git a/src/util/graphic.ts b/src/util/graphic.ts index 41a8e9d1e9..cf2fb941f7 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -1293,7 +1293,7 @@ export function removeElement( } function animateOrSetLabel( - isUpdate: boolean, + animationType: 'init' | 'update' | 'remove', el: Element, data: List, dataIndex: number, @@ -1361,7 +1361,7 @@ function animateOrSetLabel( }; const props: ElementProps = {}; - animateOrSetProps(isUpdate, el, props, animatableModel, dataIndex, null, during); + animateOrSetProps(animationType, el, props, animatableModel, dataIndex, null, during); } } @@ -1374,7 +1374,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( @@ -1386,7 +1386,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); } /** From 4ec8de7c4656927729d3f535d4252f91e5d45328 Mon Sep 17 00:00:00 2001 From: pissang Date: Sat, 4 Jul 2020 10:14:25 +0800 Subject: [PATCH 76/82] fix(dataZoom): fix position not update after resized --- src/component/dataZoom/SliderZoomView.ts | 1 + 1 file changed, 1 insertion(+) 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(); } /** From 9df0be955685f11b1c29d3df046a333054d80e45 Mon Sep 17 00:00:00 2001 From: pissang Date: Sat, 4 Jul 2020 10:25:38 +0800 Subject: [PATCH 77/82] fix(state): fix state transition may not being applied bug. --- src/echarts.ts | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/src/echarts.ts b/src/echarts.ts index 235fd09226..a9ea9ce6d2 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1902,38 +1902,49 @@ class ECharts extends Eventful { 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) { - // Only updated on changed element. In case element is incremental and don't wan't to rerender. - if (el.__dirty && el.states && el.states.emphasis) { - const prevStates = el.prevStates; - // Restore states without animation - if (prevStates) { - el.useStates(prevStates); + 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) { - graphic.setStateTransition(el, stateAnimationModel); + el.stateTransition = stateTransition; const textContent = el.getTextContent(); const textGuide = el.getTextGuideLine(); // TODO Is it necessary to animate label? if (textContent) { - graphic.setStateTransition(textContent, stateAnimationModel); + textContent.stateTransition = stateTransition; } if (textGuide) { - graphic.setStateTransition(textGuide, stateAnimationModel); + textGuide.stateTransition = stateTransition; } } // The use higlighted and selected flag to toggle states. - const states = []; - if ((el as ECElement).selected) { - states.push('select'); - } - if ((el as ECElement).highlighted) { - states.push('emphasis'); + if (el.__dirty) { + const states = []; + if ((el as ECElement).selected) { + states.push('select'); + } + if ((el as ECElement).highlighted) { + states.push('emphasis'); + } + el.useStates(states); } - el.useStates(states); } }); }; From 1cfa45d623c6bdc653b8b0f96d8b090ea417937e Mon Sep 17 00:00:00 2001 From: pissang Date: Sat, 4 Jul 2020 10:55:27 +0800 Subject: [PATCH 78/82] fix(state): fix z2 lift on emphasis --- src/util/graphic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/graphic.ts b/src/util/graphic.ts index cf2fb941f7..9d55b9306b 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -480,7 +480,7 @@ function elementStateProxy(this: Displayable, stateName: string): DisplayableSta if (state) { const z2EmphasisLift = (this as ECElement).z2EmphasisLift; // TODO Share with textContent? - state.z2 = this.z2 + z2EmphasisLift != null ? z2EmphasisLift : Z2_EMPHASIS_LIFT; + state.z2 = this.z2 + (z2EmphasisLift != null ? z2EmphasisLift : Z2_EMPHASIS_LIFT); } } From e43a58c935fad8779bf9003687a5cc3776578504 Mon Sep 17 00:00:00 2001 From: pissang Date: Sun, 5 Jul 2020 15:56:43 +0800 Subject: [PATCH 79/82] fix(pie): fix error when there is no data. --- src/chart/pie/PieView.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index 613172753b..c4c73afd75 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -261,9 +261,9 @@ class PieView extends ChartView { let startAngle: number; // First render - if (!oldData) { + if (!oldData && data.count() > 0) { let shape = data.getItemLayout(0) as graphic.Sector['shape']; - for (let s = 1; isNaN(shape.startAngle) && s < data.count(); ++s) { + for (let s = 1; isNaN(shape && shape.startAngle) && s < data.count(); ++s) { shape = data.getItemLayout(s); } if (shape) { From e63a8fef25777b649c2cbcafb5f11937e5e995df Mon Sep 17 00:00:00 2001 From: pissang Date: Sun, 5 Jul 2020 16:31:22 +0800 Subject: [PATCH 80/82] fix(label): fix map label layout. --- src/component/helper/MapDraw.ts | 2 + src/label/labelGuideHelper.ts | 2 +- test/label-layout.html | 87 +++++++++++++++++++++++++++++++-- 3 files changed, 86 insertions(+), 5 deletions(-) diff --git a/src/component/helper/MapDraw.ts b/src/component/helper/MapDraw.ts index d95c515074..0d50756fdb 100644 --- a/src/component/helper/MapDraw.ts +++ b/src/component/helper/MapDraw.ts @@ -317,6 +317,8 @@ class MapDraw { } ); + compoundPath.setTextContent(textEl); + if (!isFirstDraw) { // Text animation graphic.updateProps(textEl, { diff --git a/src/label/labelGuideHelper.ts b/src/label/labelGuideHelper.ts index a501d1c3d8..dfc6f7c8b4 100644 --- a/src/label/labelGuideHelper.ts +++ b/src/label/labelGuideHelper.ts @@ -613,7 +613,7 @@ export function setLabelLineStyle( const stateShow = stateModel.get('show'); const isLabelIgnored = isNormal ? labelIgnoreNormal - : retrieve2(label.states && label.states[stateName].ignore, 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. ) { diff --git a/test/label-layout.html b/test/label-layout.html index bd17c2fa94..621916f6d4 100644 --- a/test/label-layout.html +++ b/test/label-layout.html @@ -49,6 +49,7 @@
+
@@ -184,7 +185,7 @@ position: 'top' }, labelLayout: { - hideOverlap: true + hideOverlap: false }, itemStyle: { color: 'rgb(255, 70, 131)' @@ -437,7 +438,6 @@ }, labelLayout: { y: 20, - draggable: true, align: 'center', moveOverlap: 'shift-x', hideOverlap: true @@ -502,7 +502,6 @@ }, labelLayout: { x: 500, - draggable: true, moveOverlap: 'shift-y', // hideOverlap: true }, @@ -536,6 +535,86 @@ +