|  | /* | 
|  | * Licensed to the Apache Software Foundation (ASF) under one | 
|  | * or more contributor license agreements.  See the NOTICE file | 
|  | * distributed with this work for additional information | 
|  | * regarding copyright ownership.  The ASF licenses this file | 
|  | * to you under the Apache License, Version 2.0 (the | 
|  | * "License"); you may not use this file except in compliance | 
|  | * with the License.  You may obtain a copy of the License at | 
|  | * | 
|  | *   http://www.apache.org/licenses/LICENSE-2.0 | 
|  | * | 
|  | * Unless required by applicable law or agreed to in writing, | 
|  | * software distributed under the License is distributed on an | 
|  | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | 
|  | * KIND, either express or implied.  See the License for the | 
|  | * specific language governing permissions and limitations | 
|  | * under the License. | 
|  | */ | 
|  |  | 
|  | // TODO: move labels out of viewport. | 
|  |  | 
|  | import { | 
|  | Text as ZRText, | 
|  | BoundingRect, | 
|  | Polyline, | 
|  | updateProps, | 
|  | initProps, | 
|  | isElementRemoved | 
|  | } from '../util/graphic'; | 
|  | import { getECData } from '../util/innerStore'; | 
|  | import ExtensionAPI from '../core/ExtensionAPI'; | 
|  | import { | 
|  | ZRTextAlign, | 
|  | ZRTextVerticalAlign, | 
|  | LabelLayoutOption, | 
|  | LabelLayoutOptionCallback, | 
|  | LabelLayoutOptionCallbackParams, | 
|  | LabelLineOption, | 
|  | Dictionary, | 
|  | ECElement, | 
|  | SeriesDataType | 
|  | } from '../util/types'; | 
|  | import { parsePercent } from '../util/number'; | 
|  | import ChartView from '../view/Chart'; | 
|  | import Element, { ElementTextConfig } from 'zrender/src/Element'; | 
|  | import { RectLike } from 'zrender/src/core/BoundingRect'; | 
|  | import Transformable from 'zrender/src/core/Transformable'; | 
|  | import { updateLabelLinePoints, setLabelLineStyle, getLabelLineStatesModels } from './labelGuideHelper'; | 
|  | import SeriesModel from '../model/Series'; | 
|  | import { makeInner } from '../util/model'; | 
|  | import { retrieve2, each, keys, isFunction, filter, indexOf } from 'zrender/src/core/util'; | 
|  | import { PathStyleProps } from 'zrender/src/graphic/Path'; | 
|  | import Model from '../model/Model'; | 
|  | import { prepareLayoutList, hideOverlap, shiftLayoutOnX, shiftLayoutOnY } from './labelLayoutHelper'; | 
|  | import { labelInner, animateLabelValue } from './labelStyle'; | 
|  |  | 
|  | interface LabelDesc { | 
|  | label: ZRText | 
|  | labelLine: Polyline | 
|  |  | 
|  | seriesModel: SeriesModel | 
|  | // Can be null if label doesn't represent any data. | 
|  | dataIndex?: number | 
|  | // Can be null if label doesn't represent any data. | 
|  | dataType?: SeriesDataType | 
|  |  | 
|  | layoutOption: LabelLayoutOptionCallback | LabelLayoutOption | 
|  | computedLayoutOption: LabelLayoutOption | 
|  |  | 
|  | hostRect: RectLike | 
|  | rect: RectLike | 
|  |  | 
|  | priority: number | 
|  |  | 
|  | defaultAttr: SavedLabelAttr | 
|  | } | 
|  |  | 
|  | interface SavedLabelAttr { | 
|  | ignore: boolean | 
|  | labelGuideIgnore: boolean | 
|  |  | 
|  | x: number | 
|  | y: number | 
|  | scaleX: number | 
|  | scaleY: number | 
|  | rotation: number | 
|  |  | 
|  | style: { | 
|  | align: ZRTextAlign | 
|  | verticalAlign: ZRTextVerticalAlign | 
|  | width: number | 
|  | height: number | 
|  | fontSize: number | string | 
|  |  | 
|  | x: number | 
|  | y: number | 
|  | } | 
|  |  | 
|  | cursor: string | 
|  |  | 
|  | // Configuration in attached element | 
|  | attachedPos: ElementTextConfig['position'] | 
|  | attachedRot: ElementTextConfig['rotation'] | 
|  |  | 
|  | } | 
|  |  | 
|  | 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 label = labelItem.label; | 
|  | const labelLine = hostEl && hostEl.getTextGuideLine(); | 
|  | return { | 
|  | dataIndex: labelItem.dataIndex, | 
|  | dataType: labelItem.dataType, | 
|  | seriesIndex: labelItem.seriesModel.seriesIndex, | 
|  | text: labelItem.label.style.text, | 
|  | rect: labelItem.hostRect, | 
|  | labelRect: labelItem.rect, | 
|  | // x: labelAttr.x, | 
|  | // y: labelAttr.y, | 
|  | align: label.style.align, | 
|  | verticalAlign: label.style.verticalAlign, | 
|  | labelLinePoints: cloneArr(labelLine && labelLine.shape.points) | 
|  | }; | 
|  | } | 
|  |  | 
|  | const LABEL_OPTION_TO_STYLE_KEYS = ['align', 'verticalAlign', 'width', 'height', 'fontSize'] as const; | 
|  |  | 
|  | const dummyTransformable = new Transformable(); | 
|  |  | 
|  | const labelLayoutInnerStore = makeInner<{ | 
|  | oldLayout: { | 
|  | x: number, | 
|  | y: number, | 
|  | rotation: number | 
|  | }, | 
|  | oldLayoutSelect?: { | 
|  | x?: number, | 
|  | y?: number, | 
|  | rotation?: number | 
|  | }, | 
|  | oldLayoutEmphasis?: { | 
|  | x?: number, | 
|  | y?: number, | 
|  | rotation?: number | 
|  | }, | 
|  |  | 
|  | needsUpdateLabelLine?: boolean | 
|  | }, ZRText>(); | 
|  |  | 
|  | const labelLineAnimationStore = makeInner<{ | 
|  | oldLayout: { | 
|  | points: number[][] | 
|  | } | 
|  | }, Polyline>(); | 
|  |  | 
|  | type LabelLineOptionMixin = { | 
|  | labelLine: LabelLineOption, | 
|  | emphasis: { labelLine: LabelLineOption } | 
|  | }; | 
|  |  | 
|  | function extendWithKeys(target: Dictionary<any>, source: Dictionary<any>, keys: string[]) { | 
|  | for (let i = 0; i < keys.length; i++) { | 
|  | const key = keys[i]; | 
|  | if (source[key] != null) { | 
|  | target[key] = source[key]; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | const LABEL_LAYOUT_PROPS = ['x', 'y', 'rotation']; | 
|  |  | 
|  | class LabelManager { | 
|  |  | 
|  | private _labelList: LabelDesc[] = []; | 
|  | private _chartViewList: ChartView[] = []; | 
|  |  | 
|  | constructor() {} | 
|  |  | 
|  | clearLabels() { | 
|  | this._labelList = []; | 
|  | this._chartViewList = []; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Add label to manager | 
|  | */ | 
|  | private _addLabel( | 
|  | dataIndex: number | null | undefined, | 
|  | dataType: SeriesDataType | null | undefined, | 
|  | seriesModel: SeriesModel, | 
|  | label: ZRText, | 
|  | layoutOption: LabelDesc['layoutOption'] | 
|  | ) { | 
|  | const labelStyle = label.style; | 
|  | const hostEl = label.__hostTarget; | 
|  | const textConfig = hostEl.textConfig || {}; | 
|  |  | 
|  | // TODO: If label is in other state. | 
|  | const labelTransform = label.getComputedTransform(); | 
|  | const labelRect = label.getBoundingRect().plain(); | 
|  | BoundingRect.applyTransform(labelRect, labelRect, labelTransform); | 
|  |  | 
|  | if (labelTransform) { | 
|  | dummyTransformable.setLocalTransform(labelTransform); | 
|  | } | 
|  | else { | 
|  | // Identity transform. | 
|  | dummyTransformable.x = dummyTransformable.y = dummyTransformable.rotation = | 
|  | dummyTransformable.originX = dummyTransformable.originY = 0; | 
|  | dummyTransformable.scaleX = dummyTransformable.scaleY = 1; | 
|  | } | 
|  |  | 
|  | const host = label.__hostTarget; | 
|  | let hostRect; | 
|  | if (host) { | 
|  | hostRect = host.getBoundingRect().plain(); | 
|  | const transform = host.getComputedTransform(); | 
|  | BoundingRect.applyTransform(hostRect, hostRect, transform); | 
|  | } | 
|  |  | 
|  | const labelGuide = hostRect && host.getTextGuideLine(); | 
|  |  | 
|  | this._labelList.push({ | 
|  | label, | 
|  | labelLine: labelGuide, | 
|  |  | 
|  | seriesModel, | 
|  | dataIndex, | 
|  | dataType, | 
|  |  | 
|  | layoutOption, | 
|  | computedLayoutOption: null, | 
|  |  | 
|  | rect: labelRect, | 
|  |  | 
|  | hostRect, | 
|  |  | 
|  | // Label with lower priority will be hidden when overlapped | 
|  | // Use rect size as default priority | 
|  | priority: hostRect ? hostRect.width * hostRect.height : 0, | 
|  |  | 
|  | // Save default label attributes. | 
|  | // For restore if developers want get back to default value in callback. | 
|  | defaultAttr: { | 
|  | ignore: label.ignore, | 
|  | labelGuideIgnore: labelGuide && labelGuide.ignore, | 
|  |  | 
|  | x: dummyTransformable.x, | 
|  | y: dummyTransformable.y, | 
|  | scaleX: dummyTransformable.scaleX, | 
|  | scaleY: dummyTransformable.scaleY, | 
|  | rotation: dummyTransformable.rotation, | 
|  |  | 
|  | style: { | 
|  | x: labelStyle.x, | 
|  | y: labelStyle.y, | 
|  |  | 
|  | align: labelStyle.align, | 
|  | verticalAlign: labelStyle.verticalAlign, | 
|  | width: labelStyle.width, | 
|  | height: labelStyle.height, | 
|  |  | 
|  | fontSize: labelStyle.fontSize | 
|  | }, | 
|  |  | 
|  | cursor: label.cursor, | 
|  |  | 
|  | attachedPos: textConfig.position, | 
|  | attachedRot: textConfig.rotation | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | addLabelsOfSeries(chartView: ChartView) { | 
|  | this._chartViewList.push(chartView); | 
|  |  | 
|  | const seriesModel = chartView.__model; | 
|  |  | 
|  | const layoutOption = seriesModel.get('labelLayout'); | 
|  |  | 
|  | /** | 
|  | * Ignore layouting if it's not specified anything. | 
|  | */ | 
|  | if (!(isFunction(layoutOption) || keys(layoutOption).length)) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | chartView.group.traverse((child) => { | 
|  | if (child.ignore) { | 
|  | return true;    // Stop traverse descendants. | 
|  | } | 
|  |  | 
|  | // Only support label being hosted on graphic elements. | 
|  | const textEl = child.getTextContent(); | 
|  | const ecData = getECData(child); | 
|  | // Can only attach the text on the element with dataIndex | 
|  | if (textEl && !(textEl as ECElement).disableLabelLayout) { | 
|  | this._addLabel(ecData.dataIndex, ecData.dataType, seriesModel, textEl, layoutOption); | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | updateLayoutConfig(api: ExtensionAPI) { | 
|  | const width = api.getWidth(); | 
|  | const height = api.getHeight(); | 
|  |  | 
|  | function createDragHandler(el: Element, labelLineModel: Model) { | 
|  | return function () { | 
|  | updateLabelLinePoints(el, labelLineModel); | 
|  | }; | 
|  | } | 
|  | for (let i = 0; i < this._labelList.length; i++) { | 
|  | const labelItem = this._labelList[i]; | 
|  | const label = labelItem.label; | 
|  | const hostEl = label.__hostTarget; | 
|  | const defaultLabelAttr = labelItem.defaultAttr; | 
|  | let layoutOption; | 
|  | // TODO A global layout option? | 
|  | if (isFunction(labelItem.layoutOption)) { | 
|  | layoutOption = labelItem.layoutOption( | 
|  | prepareLayoutCallbackParams(labelItem, hostEl) | 
|  | ); | 
|  | } | 
|  | else { | 
|  | layoutOption = labelItem.layoutOption; | 
|  | } | 
|  |  | 
|  | layoutOption = layoutOption || {}; | 
|  | labelItem.computedLayoutOption = layoutOption; | 
|  |  | 
|  | const degreeToRadian = Math.PI / 180; | 
|  | // TODO hostEl should always exists. | 
|  | // Or label should not have parent because the x, y is all in global space. | 
|  | if (hostEl) { | 
|  | hostEl.setTextConfig({ | 
|  | // Force to set local false. | 
|  | local: false, | 
|  | // Ignore position and rotation config on the host el if x or y is changed. | 
|  | position: (layoutOption.x != null || layoutOption.y != null) | 
|  | ? null : defaultLabelAttr.attachedPos, | 
|  | // Ignore rotation config on the host el if rotation is changed. | 
|  | rotation: layoutOption.rotate != null | 
|  | ? layoutOption.rotate * degreeToRadian : defaultLabelAttr.attachedRot, | 
|  | offset: [layoutOption.dx || 0, layoutOption.dy || 0] | 
|  | }); | 
|  | } | 
|  | let needsUpdateLabelLine = false; | 
|  | if (layoutOption.x != null) { | 
|  | // TODO width of chart view. | 
|  | label.x = parsePercent(layoutOption.x, width); | 
|  | label.setStyle('x', 0);  // Ignore movement in style. TODO: origin. | 
|  | needsUpdateLabelLine = true; | 
|  | } | 
|  | else { | 
|  | label.x = defaultLabelAttr.x; | 
|  | label.setStyle('x', defaultLabelAttr.style.x); | 
|  | } | 
|  |  | 
|  | if (layoutOption.y != null) { | 
|  | // TODO height of chart view. | 
|  | label.y = parsePercent(layoutOption.y, height); | 
|  | label.setStyle('y', 0);  // Ignore movement in style. | 
|  | needsUpdateLabelLine = true; | 
|  | } | 
|  | else { | 
|  | label.y = defaultLabelAttr.y; | 
|  | label.setStyle('y', defaultLabelAttr.style.y); | 
|  | } | 
|  |  | 
|  | if (layoutOption.labelLinePoints) { | 
|  | const guideLine = hostEl.getTextGuideLine(); | 
|  | if (guideLine) { | 
|  | guideLine.setShape({ points: layoutOption.labelLinePoints }); | 
|  | // Not update | 
|  | needsUpdateLabelLine = false; | 
|  | } | 
|  | } | 
|  |  | 
|  | const labelLayoutStore = labelLayoutInnerStore(label); | 
|  | labelLayoutStore.needsUpdateLabelLine = needsUpdateLabelLine; | 
|  |  | 
|  | label.rotation = layoutOption.rotate != null | 
|  | ? layoutOption.rotate * degreeToRadian : defaultLabelAttr.rotation; | 
|  |  | 
|  | label.scaleX = defaultLabelAttr.scaleX; | 
|  | label.scaleY = defaultLabelAttr.scaleY; | 
|  |  | 
|  | for (let k = 0; k < LABEL_OPTION_TO_STYLE_KEYS.length; k++) { | 
|  | const key = LABEL_OPTION_TO_STYLE_KEYS[k]; | 
|  | label.setStyle(key, layoutOption[key] != null ? layoutOption[key] : defaultLabelAttr.style[key]); | 
|  | } | 
|  |  | 
|  |  | 
|  | if (layoutOption.draggable) { | 
|  | label.draggable = true; | 
|  | label.cursor = 'move'; | 
|  | if (hostEl) { | 
|  | let hostModel: Model<LabelLineOptionMixin> = | 
|  | labelItem.seriesModel as SeriesModel<LabelLineOptionMixin>; | 
|  | if (labelItem.dataIndex != null) { | 
|  | const data = labelItem.seriesModel.getData(labelItem.dataType); | 
|  | hostModel = data.getItemModel<LabelLineOptionMixin>(labelItem.dataIndex); | 
|  | } | 
|  | label.on('drag', createDragHandler(hostEl, hostModel.getModel('labelLine'))); | 
|  | } | 
|  | } | 
|  | else { | 
|  | // TODO Other drag functions? | 
|  | label.off('drag'); | 
|  | label.cursor = defaultLabelAttr.cursor; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | layout(api: ExtensionAPI) { | 
|  | const width = api.getWidth(); | 
|  | const height = api.getHeight(); | 
|  |  | 
|  | const labelList = prepareLayoutList(this._labelList); | 
|  | const labelsNeedsAdjustOnX = filter(labelList, function (item) { | 
|  | return item.layoutOption.moveOverlap === 'shiftX'; | 
|  | }); | 
|  | const labelsNeedsAdjustOnY = filter(labelList, function (item) { | 
|  | return item.layoutOption.moveOverlap === 'shiftY'; | 
|  | }); | 
|  |  | 
|  | shiftLayoutOnX(labelsNeedsAdjustOnX, 0, width); | 
|  | shiftLayoutOnY(labelsNeedsAdjustOnY, 0, height); | 
|  |  | 
|  | const labelsNeedsHideOverlap = filter(labelList, function (item) { | 
|  | return item.layoutOption.hideOverlap; | 
|  | }); | 
|  |  | 
|  | hideOverlap(labelsNeedsHideOverlap); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Process all labels. Not only labels with layoutOption. | 
|  | */ | 
|  | processLabelsOverall() { | 
|  | each(this._chartViewList, (chartView) => { | 
|  | const seriesModel = chartView.__model; | 
|  | const ignoreLabelLineUpdate = chartView.ignoreLabelLineUpdate; | 
|  | const animationEnabled = seriesModel.isAnimationEnabled(); | 
|  |  | 
|  | chartView.group.traverse((child) => { | 
|  | if (child.ignore && !(child as ECElement).forceLabelAnimation) { | 
|  | return true;    // Stop traverse descendants. | 
|  | } | 
|  |  | 
|  | let needsUpdateLabelLine = !ignoreLabelLineUpdate; | 
|  | const label = child.getTextContent(); | 
|  | if (!needsUpdateLabelLine && label) { | 
|  | needsUpdateLabelLine = labelLayoutInnerStore(label).needsUpdateLabelLine; | 
|  | } | 
|  | if (needsUpdateLabelLine) { | 
|  | this._updateLabelLine(child, seriesModel); | 
|  | } | 
|  |  | 
|  | if (animationEnabled) { | 
|  | this._animateLabels(child, seriesModel); | 
|  | } | 
|  | }); | 
|  | }); | 
|  | } | 
|  |  | 
|  | private _updateLabelLine(el: Element, seriesModel: SeriesModel) { | 
|  | // Only support label being hosted on graphic elements. | 
|  | const textEl = el.getTextContent(); | 
|  | // Update label line style. | 
|  | const ecData = getECData(el); | 
|  | const dataIndex = ecData.dataIndex; | 
|  |  | 
|  | // Only support labelLine on the labels represent data. | 
|  | if (textEl && dataIndex != null) { | 
|  | const data = seriesModel.getData(ecData.dataType); | 
|  | const itemModel = data.getItemModel<LabelLineOptionMixin>(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, getLabelLineStatesModels(itemModel), defaultStyle); | 
|  |  | 
|  | updateLabelLinePoints(el, labelLineModel); | 
|  | } | 
|  | } | 
|  |  | 
|  | private _animateLabels(el: Element, seriesModel: SeriesModel) { | 
|  | const textEl = el.getTextContent(); | 
|  | const guideLine = el.getTextGuideLine(); | 
|  | // Animate | 
|  | if (textEl | 
|  | // `forceLabelAnimation` has the highest priority | 
|  | && ((el as ECElement).forceLabelAnimation | 
|  | || !textEl.ignore | 
|  | && !textEl.invisible | 
|  | && !(el as ECElement).disableLabelAnimation | 
|  | && !isElementRemoved(el) | 
|  | ) | 
|  | ) { | 
|  | const layoutStore = labelLayoutInnerStore(textEl); | 
|  | const oldLayout = layoutStore.oldLayout; | 
|  | const ecData = getECData(el); | 
|  | const dataIndex = ecData.dataIndex; | 
|  | const newProps = { | 
|  | x: textEl.x, | 
|  | y: textEl.y, | 
|  | rotation: textEl.rotation | 
|  | }; | 
|  | const data = seriesModel.getData(ecData.dataType); | 
|  |  | 
|  | if (!oldLayout) { | 
|  | textEl.attr(newProps); | 
|  | // Disable fade in animation if value animation is enabled. | 
|  | if (!labelInner(textEl).valueAnimation) { | 
|  | const oldOpacity = retrieve2(textEl.style.opacity, 1); | 
|  | // Fade in animation | 
|  | textEl.style.opacity = 0; | 
|  | initProps(textEl, { | 
|  | style: { opacity: oldOpacity } | 
|  | }, seriesModel, dataIndex); | 
|  | } | 
|  | } | 
|  | 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, dataIndex); | 
|  | } | 
|  | 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); | 
|  | } | 
|  |  | 
|  | animateLabelValue(textEl, dataIndex, data, seriesModel, 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; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  |  | 
|  | export default LabelManager; |