|  | /* | 
|  | * 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 * as zrUtil from 'zrender/src/core/util'; | 
|  | import * as graphic from '../../util/graphic'; | 
|  | import { toggleHoverEmphasis } from '../../util/states'; | 
|  | import {createSymbol, normalizeSymbolOffset} from '../../util/symbol'; | 
|  | import {parsePercent, isNumeric} from '../../util/number'; | 
|  | import ChartView from '../../view/Chart'; | 
|  | import PictorialBarSeriesModel, {PictorialBarDataItemOption} from './PictorialBarSeries'; | 
|  | import ExtensionAPI from '../../core/ExtensionAPI'; | 
|  | import SeriesData from '../../data/SeriesData'; | 
|  | import GlobalModel from '../../model/Global'; | 
|  | import Model from '../../model/Model'; | 
|  | import { ColorString, AnimationOptionMixin, ECElement } from '../../util/types'; | 
|  | import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; | 
|  | import type Displayable from 'zrender/src/graphic/Displayable'; | 
|  | import type Axis2D from '../../coord/cartesian/Axis2D'; | 
|  | import type Element from 'zrender/src/Element'; | 
|  | import { getDefaultLabel } from '../helper/labelHelper'; | 
|  | import { PathProps, PathStyleProps } from 'zrender/src/graphic/Path'; | 
|  | import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; | 
|  | import ZRImage from 'zrender/src/graphic/Image'; | 
|  | import { getECData } from '../../util/innerStore'; | 
|  |  | 
|  | const BAR_BORDER_WIDTH_QUERY = ['itemStyle', 'borderWidth'] as const; | 
|  |  | 
|  | // index: +isHorizontal | 
|  | const LAYOUT_ATTRS = [ | 
|  | {xy: 'x', wh: 'width', index: 0, posDesc: ['left', 'right']}, | 
|  | {xy: 'y', wh: 'height', index: 1, posDesc: ['top', 'bottom']} | 
|  | ] as const; | 
|  |  | 
|  | const pathForLineWidth = new graphic.Circle(); | 
|  |  | 
|  | type ItemModel = Model<PictorialBarDataItemOption> & { | 
|  | getAnimationDelayParams(path: any): {index: number, count: number} | 
|  | isAnimationEnabled(): boolean | 
|  | }; | 
|  | type RectShape = graphic.Rect['shape']; | 
|  | type RectLayout = RectShape; | 
|  |  | 
|  | type PictorialSymbol = ReturnType<typeof createSymbol> & { | 
|  | __pictorialAnimationIndex: number | 
|  | __pictorialRepeatTimes: number | 
|  | }; | 
|  |  | 
|  | interface SymbolMeta { | 
|  | dataIndex: number | 
|  |  | 
|  | symbolPatternSize: number | 
|  | symbolType: string | 
|  | symbolMargin: number | 
|  | symbolSize: number[] | 
|  | symbolScale: number[] | 
|  | symbolRepeat: PictorialBarDataItemOption['symbolRepeat'] | 
|  | symbolClip: PictorialBarDataItemOption['symbolClip'] | 
|  | symbolRepeatDirection: PictorialBarDataItemOption['symbolRepeatDirection'] | 
|  |  | 
|  | layout: RectLayout | 
|  |  | 
|  | repeatTimes: number | 
|  |  | 
|  | rotation: number | 
|  |  | 
|  | pathPosition: number[] | 
|  | bundlePosition: number[] | 
|  |  | 
|  | pxSign: number | 
|  |  | 
|  | barRectShape: RectShape | 
|  | clipShape: RectShape | 
|  |  | 
|  | boundingLength: number | 
|  | repeatCutLength: number | 
|  |  | 
|  | valueLineWidth: number | 
|  |  | 
|  | opacity: number | 
|  | style: PathStyleProps | 
|  | z2: number | 
|  |  | 
|  | itemModel: ItemModel | 
|  |  | 
|  | animationModel?: ItemModel | 
|  |  | 
|  | hoverScale: boolean | 
|  | } | 
|  |  | 
|  | interface CreateOpts { | 
|  | ecSize: { width: number, height: number } | 
|  | seriesModel: PictorialBarSeriesModel | 
|  | coordSys: Cartesian2D | 
|  | coordSysExtent: number[][] | 
|  | isHorizontal: boolean | 
|  | valueDim: typeof LAYOUT_ATTRS[number] | 
|  | categoryDim: typeof LAYOUT_ATTRS[number] | 
|  | } | 
|  |  | 
|  | interface PictorialBarElement extends graphic.Group { | 
|  | __pictorialBundle: graphic.Group | 
|  | __pictorialShapeStr: string | 
|  | __pictorialSymbolMeta: SymbolMeta | 
|  |  | 
|  | __pictorialMainPath: PictorialSymbol | 
|  |  | 
|  | __pictorialBarRect: graphic.Rect | 
|  |  | 
|  | __pictorialClipPath: graphic.Rect | 
|  | } | 
|  |  | 
|  | class PictorialBarView extends ChartView { | 
|  | static type = 'pictorialBar'; | 
|  | readonly type = PictorialBarView.type; | 
|  |  | 
|  | private _data: SeriesData; | 
|  |  | 
|  | render( | 
|  | seriesModel: PictorialBarSeriesModel, | 
|  | ecModel: GlobalModel, | 
|  | api: ExtensionAPI | 
|  | ) { | 
|  | const group = this.group; | 
|  | const data = seriesModel.getData(); | 
|  | const oldData = this._data; | 
|  |  | 
|  | const cartesian = seriesModel.coordinateSystem; | 
|  | const baseAxis = cartesian.getBaseAxis(); | 
|  | const isHorizontal = baseAxis.isHorizontal(); | 
|  | const coordSysRect = cartesian.master.getRect(); | 
|  |  | 
|  | const opt: CreateOpts = { | 
|  | ecSize: {width: api.getWidth(), height: api.getHeight()}, | 
|  | seriesModel: seriesModel, | 
|  | coordSys: cartesian, | 
|  | coordSysExtent: [ | 
|  | [coordSysRect.x, coordSysRect.x + coordSysRect.width], | 
|  | [coordSysRect.y, coordSysRect.y + coordSysRect.height] | 
|  | ], | 
|  | isHorizontal: isHorizontal, | 
|  | valueDim: LAYOUT_ATTRS[+isHorizontal], | 
|  | categoryDim: LAYOUT_ATTRS[1 - (+isHorizontal)] | 
|  | }; | 
|  |  | 
|  | data.diff(oldData) | 
|  | .add(function (dataIndex) { | 
|  | if (!data.hasValue(dataIndex)) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | const itemModel = getItemModel(data, dataIndex); | 
|  | const symbolMeta = getSymbolMeta(data, dataIndex, itemModel, opt); | 
|  |  | 
|  | const bar = createBar(data, opt, symbolMeta); | 
|  |  | 
|  | data.setItemGraphicEl(dataIndex, bar); | 
|  | group.add(bar); | 
|  |  | 
|  | updateCommon(bar, opt, symbolMeta); | 
|  | }) | 
|  | .update(function (newIndex, oldIndex) { | 
|  | let bar = oldData.getItemGraphicEl(oldIndex) as PictorialBarElement; | 
|  |  | 
|  | if (!data.hasValue(newIndex)) { | 
|  | group.remove(bar); | 
|  | return; | 
|  | } | 
|  |  | 
|  | const itemModel = getItemModel(data, newIndex); | 
|  | const symbolMeta = getSymbolMeta(data, newIndex, itemModel, opt); | 
|  |  | 
|  | const pictorialShapeStr = getShapeStr(data, symbolMeta); | 
|  | if (bar && pictorialShapeStr !== bar.__pictorialShapeStr) { | 
|  | group.remove(bar); | 
|  | data.setItemGraphicEl(newIndex, null); | 
|  | bar = null; | 
|  | } | 
|  |  | 
|  | if (bar) { | 
|  | updateBar(bar, opt, symbolMeta); | 
|  | } | 
|  | else { | 
|  | bar = createBar(data, opt, symbolMeta, true); | 
|  | } | 
|  |  | 
|  | data.setItemGraphicEl(newIndex, bar); | 
|  | bar.__pictorialSymbolMeta = symbolMeta; | 
|  | // Add back | 
|  | group.add(bar); | 
|  |  | 
|  | updateCommon(bar, opt, symbolMeta); | 
|  | }) | 
|  | .remove(function (dataIndex) { | 
|  | const bar = oldData.getItemGraphicEl(dataIndex) as PictorialBarElement; | 
|  | bar && removeBar( | 
|  | oldData, dataIndex, bar.__pictorialSymbolMeta.animationModel, bar | 
|  | ); | 
|  | }) | 
|  | .execute(); | 
|  |  | 
|  | this._data = data; | 
|  |  | 
|  | return this.group; | 
|  | } | 
|  |  | 
|  | remove(ecModel: GlobalModel, api: ExtensionAPI) { | 
|  | const group = this.group; | 
|  | const data = this._data; | 
|  | if (ecModel.get('animation')) { | 
|  | if (data) { | 
|  | data.eachItemGraphicEl(function (bar: PictorialBarElement) { | 
|  | removeBar(data, getECData(bar).dataIndex, ecModel as Model<AnimationOptionMixin>, bar); | 
|  | }); | 
|  | } | 
|  | } | 
|  | else { | 
|  | group.removeAll(); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // Set or calculate default value about symbol, and calculate layout info. | 
|  | function getSymbolMeta( | 
|  | data: SeriesData, | 
|  | dataIndex: number, | 
|  | itemModel: ItemModel, | 
|  | opt: CreateOpts | 
|  | ): SymbolMeta { | 
|  | const layout = data.getItemLayout(dataIndex) as RectLayout; | 
|  | const symbolRepeat = itemModel.get('symbolRepeat'); | 
|  | const symbolClip = itemModel.get('symbolClip'); | 
|  | const symbolPosition = itemModel.get('symbolPosition') || 'start'; | 
|  | const symbolRotate = itemModel.get('symbolRotate'); | 
|  | const rotation = (symbolRotate || 0) * Math.PI / 180 || 0; | 
|  | const symbolPatternSize = itemModel.get('symbolPatternSize') || 2; | 
|  | const isAnimationEnabled = itemModel.isAnimationEnabled(); | 
|  |  | 
|  | const symbolMeta: SymbolMeta = { | 
|  | dataIndex: dataIndex, | 
|  | layout: layout, | 
|  | itemModel: itemModel, | 
|  | symbolType: data.getItemVisual(dataIndex, 'symbol') || 'circle', | 
|  | style: data.getItemVisual(dataIndex, 'style'), | 
|  | symbolClip: symbolClip, | 
|  | symbolRepeat: symbolRepeat, | 
|  | symbolRepeatDirection: itemModel.get('symbolRepeatDirection'), | 
|  | symbolPatternSize: symbolPatternSize, | 
|  | rotation: rotation, | 
|  | animationModel: isAnimationEnabled ? itemModel : null, | 
|  | hoverScale: isAnimationEnabled && itemModel.get(['emphasis', 'scale']), | 
|  | z2: itemModel.getShallow('z', true) || 0 | 
|  | } as SymbolMeta; | 
|  |  | 
|  | prepareBarLength(itemModel, symbolRepeat, layout, opt, symbolMeta); | 
|  |  | 
|  | prepareSymbolSize( | 
|  | data, dataIndex, layout, symbolRepeat, symbolClip, symbolMeta.boundingLength, | 
|  | symbolMeta.pxSign, symbolPatternSize, opt, symbolMeta | 
|  | ); | 
|  |  | 
|  | prepareLineWidth(itemModel, symbolMeta.symbolScale, rotation, opt, symbolMeta); | 
|  |  | 
|  | const symbolSize = symbolMeta.symbolSize; | 
|  | const symbolOffset = normalizeSymbolOffset(itemModel.get('symbolOffset'), symbolSize); | 
|  |  | 
|  | prepareLayoutInfo( | 
|  | itemModel, symbolSize, layout, symbolRepeat, symbolClip, symbolOffset as number[], | 
|  | symbolPosition, symbolMeta.valueLineWidth, symbolMeta.boundingLength, symbolMeta.repeatCutLength, | 
|  | opt, symbolMeta | 
|  | ); | 
|  |  | 
|  | return symbolMeta; | 
|  | } | 
|  |  | 
|  | // bar length can be negative. | 
|  | function prepareBarLength( | 
|  | itemModel: ItemModel, | 
|  | symbolRepeat: PictorialBarDataItemOption['symbolRepeat'], | 
|  | layout: RectLayout, | 
|  | opt: CreateOpts, | 
|  | outputSymbolMeta: SymbolMeta | 
|  | ) { | 
|  | const valueDim = opt.valueDim; | 
|  | const symbolBoundingData = itemModel.get('symbolBoundingData'); | 
|  | const valueAxis = opt.coordSys.getOtherAxis(opt.coordSys.getBaseAxis()); | 
|  | const zeroPx = valueAxis.toGlobalCoord(valueAxis.dataToCoord(0)); | 
|  | const pxSignIdx = 1 - +(layout[valueDim.wh] <= 0); | 
|  | let boundingLength; | 
|  |  | 
|  | if (zrUtil.isArray(symbolBoundingData)) { | 
|  | const symbolBoundingExtent = [ | 
|  | convertToCoordOnAxis(valueAxis, symbolBoundingData[0]) - zeroPx, | 
|  | convertToCoordOnAxis(valueAxis, symbolBoundingData[1]) - zeroPx | 
|  | ]; | 
|  | symbolBoundingExtent[1] < symbolBoundingExtent[0] && (symbolBoundingExtent.reverse()); | 
|  | boundingLength = symbolBoundingExtent[pxSignIdx]; | 
|  | } | 
|  | else if (symbolBoundingData != null) { | 
|  | boundingLength = convertToCoordOnAxis(valueAxis, symbolBoundingData) - zeroPx; | 
|  | } | 
|  | else if (symbolRepeat) { | 
|  | boundingLength = opt.coordSysExtent[valueDim.index][pxSignIdx] - zeroPx; | 
|  | } | 
|  | else { | 
|  | boundingLength = layout[valueDim.wh]; | 
|  | } | 
|  |  | 
|  | outputSymbolMeta.boundingLength = boundingLength; | 
|  |  | 
|  | if (symbolRepeat) { | 
|  | outputSymbolMeta.repeatCutLength = layout[valueDim.wh]; | 
|  | } | 
|  |  | 
|  | // if 'pxSign' means sign of pixel,  it can't be zero, or symbolScale will be zero | 
|  | // and when borderWidth be settled, the actual linewidth will be NaN | 
|  | outputSymbolMeta.pxSign = boundingLength > 0 ? 1 : -1; | 
|  | } | 
|  |  | 
|  | function convertToCoordOnAxis(axis: Axis2D, value: number) { | 
|  | return axis.toGlobalCoord(axis.dataToCoord(axis.scale.parse(value))); | 
|  | } | 
|  |  | 
|  | // Support ['100%', '100%'] | 
|  | function prepareSymbolSize( | 
|  | data: SeriesData, | 
|  | dataIndex: number, | 
|  | layout: RectLayout, | 
|  | symbolRepeat: PictorialBarDataItemOption['symbolRepeat'], | 
|  | symbolClip: unknown, | 
|  | boundingLength: number, | 
|  | pxSign: number, | 
|  | symbolPatternSize: number, | 
|  | opt: CreateOpts, | 
|  | outputSymbolMeta: SymbolMeta | 
|  | ) { | 
|  | const valueDim = opt.valueDim; | 
|  | const categoryDim = opt.categoryDim; | 
|  | const categorySize = Math.abs(layout[categoryDim.wh]); | 
|  |  | 
|  | const symbolSize = data.getItemVisual(dataIndex, 'symbolSize'); | 
|  | let parsedSymbolSize: number[]; | 
|  | if (zrUtil.isArray(symbolSize)) { | 
|  | parsedSymbolSize = symbolSize.slice(); | 
|  | } | 
|  | else { | 
|  | if (symbolSize == null) { | 
|  | // will parse to number below | 
|  | parsedSymbolSize = ['100%', '100%'] as unknown as number[]; | 
|  | } | 
|  | else { | 
|  | parsedSymbolSize = [symbolSize, symbolSize]; | 
|  | } | 
|  | } | 
|  |  | 
|  | // Note: percentage symbolSize (like '100%') do not consider lineWidth, because it is | 
|  | // to complicated to calculate real percent value if considering scaled lineWidth. | 
|  | // So the actual size will bigger than layout size if lineWidth is bigger than zero, | 
|  | // which can be tolerated in pictorial chart. | 
|  |  | 
|  | parsedSymbolSize[categoryDim.index] = parsePercent( | 
|  | parsedSymbolSize[categoryDim.index], | 
|  | categorySize | 
|  | ); | 
|  | parsedSymbolSize[valueDim.index] = parsePercent( | 
|  | parsedSymbolSize[valueDim.index], | 
|  | symbolRepeat ? categorySize : Math.abs(boundingLength) | 
|  | ); | 
|  |  | 
|  | outputSymbolMeta.symbolSize = parsedSymbolSize; | 
|  |  | 
|  | // If x or y is less than zero, show reversed shape. | 
|  | const symbolScale = outputSymbolMeta.symbolScale = [ | 
|  | parsedSymbolSize[0] / symbolPatternSize, | 
|  | parsedSymbolSize[1] / symbolPatternSize | 
|  | ]; | 
|  | // Follow convention, 'right' and 'top' is the normal scale. | 
|  | symbolScale[valueDim.index] *= (opt.isHorizontal ? -1 : 1) * pxSign; | 
|  | } | 
|  |  | 
|  | function prepareLineWidth( | 
|  | itemModel: ItemModel, | 
|  | symbolScale: number[], | 
|  | rotation: number, | 
|  | opt: CreateOpts, | 
|  | outputSymbolMeta: SymbolMeta | 
|  | ) { | 
|  | // In symbols are drawn with scale, so do not need to care about the case that width | 
|  | // or height are too small. But symbol use strokeNoScale, where acture lineWidth should | 
|  | // be calculated. | 
|  | let valueLineWidth = itemModel.get(BAR_BORDER_WIDTH_QUERY) || 0; | 
|  |  | 
|  | if (valueLineWidth) { | 
|  | pathForLineWidth.attr({ | 
|  | scaleX: symbolScale[0], | 
|  | scaleY: symbolScale[1], | 
|  | rotation: rotation | 
|  | }); | 
|  | pathForLineWidth.updateTransform(); | 
|  | valueLineWidth /= pathForLineWidth.getLineScale(); | 
|  | valueLineWidth *= symbolScale[opt.valueDim.index]; | 
|  | } | 
|  |  | 
|  | outputSymbolMeta.valueLineWidth = valueLineWidth || 0; | 
|  | } | 
|  |  | 
|  | function prepareLayoutInfo( | 
|  | itemModel: ItemModel, | 
|  | symbolSize: number[], | 
|  | layout: RectLayout, | 
|  | symbolRepeat: PictorialBarDataItemOption['symbolRepeat'], | 
|  | symbolClip: PictorialBarDataItemOption['symbolClip'], | 
|  | symbolOffset: number[], | 
|  | symbolPosition: PictorialBarDataItemOption['symbolPosition'], | 
|  | valueLineWidth: number, | 
|  | boundingLength: number, | 
|  | repeatCutLength: number, | 
|  | opt: CreateOpts, | 
|  | outputSymbolMeta: SymbolMeta | 
|  | ) { | 
|  | const categoryDim = opt.categoryDim; | 
|  | const valueDim = opt.valueDim; | 
|  | const pxSign = outputSymbolMeta.pxSign; | 
|  |  | 
|  | const unitLength = Math.max(symbolSize[valueDim.index] + valueLineWidth, 0); | 
|  | let pathLen = unitLength; | 
|  |  | 
|  | // Note: rotation will not effect the layout of symbols, because user may | 
|  | // want symbols to rotate on its center, which should not be translated | 
|  | // when rotating. | 
|  |  | 
|  | if (symbolRepeat) { | 
|  | const absBoundingLength = Math.abs(boundingLength); | 
|  |  | 
|  | let symbolMargin = zrUtil.retrieve(itemModel.get('symbolMargin'), '15%') + ''; | 
|  | let hasEndGap = false; | 
|  | if (symbolMargin.lastIndexOf('!') === symbolMargin.length - 1) { | 
|  | hasEndGap = true; | 
|  | symbolMargin = symbolMargin.slice(0, symbolMargin.length - 1); | 
|  | } | 
|  | let symbolMarginNumeric = parsePercent(symbolMargin, symbolSize[valueDim.index]); | 
|  |  | 
|  | let uLenWithMargin = Math.max(unitLength + symbolMarginNumeric * 2, 0); | 
|  |  | 
|  | // When symbol margin is less than 0, margin at both ends will be subtracted | 
|  | // to ensure that all of the symbols will not be overflow the given area. | 
|  | let endFix = hasEndGap ? 0 : symbolMarginNumeric * 2; | 
|  |  | 
|  | // Both final repeatTimes and final symbolMarginNumeric area calculated based on | 
|  | // boundingLength. | 
|  | const repeatSpecified = isNumeric(symbolRepeat); | 
|  | let repeatTimes = repeatSpecified | 
|  | ? symbolRepeat as number | 
|  | : toIntTimes((absBoundingLength + endFix) / uLenWithMargin); | 
|  |  | 
|  | // Adjust calculate margin, to ensure each symbol is displayed | 
|  | // entirely in the given layout area. | 
|  | const mDiff = absBoundingLength - repeatTimes * unitLength; | 
|  | symbolMarginNumeric = mDiff / 2 / (hasEndGap ? repeatTimes : Math.max(repeatTimes - 1, 1)); | 
|  | uLenWithMargin = unitLength + symbolMarginNumeric * 2; | 
|  | endFix = hasEndGap ? 0 : symbolMarginNumeric * 2; | 
|  |  | 
|  | // Update repeatTimes when not all symbol will be shown. | 
|  | if (!repeatSpecified && symbolRepeat !== 'fixed') { | 
|  | repeatTimes = repeatCutLength | 
|  | ? toIntTimes((Math.abs(repeatCutLength) + endFix) / uLenWithMargin) | 
|  | : 0; | 
|  | } | 
|  |  | 
|  | pathLen = repeatTimes * uLenWithMargin - endFix; | 
|  | outputSymbolMeta.repeatTimes = repeatTimes; | 
|  | outputSymbolMeta.symbolMargin = symbolMarginNumeric; | 
|  | } | 
|  |  | 
|  | const sizeFix = pxSign * (pathLen / 2); | 
|  | const pathPosition = outputSymbolMeta.pathPosition = [] as number[]; | 
|  | pathPosition[categoryDim.index] = layout[categoryDim.wh] / 2; | 
|  | pathPosition[valueDim.index] = symbolPosition === 'start' | 
|  | ? sizeFix | 
|  | : symbolPosition === 'end' | 
|  | ? boundingLength - sizeFix | 
|  | : boundingLength / 2; // 'center' | 
|  | if (symbolOffset) { | 
|  | pathPosition[0] += symbolOffset[0]; | 
|  | pathPosition[1] += symbolOffset[1]; | 
|  | } | 
|  |  | 
|  | const bundlePosition = outputSymbolMeta.bundlePosition = [] as number[]; | 
|  | bundlePosition[categoryDim.index] = layout[categoryDim.xy]; | 
|  | bundlePosition[valueDim.index] = layout[valueDim.xy]; | 
|  |  | 
|  | const barRectShape = outputSymbolMeta.barRectShape = zrUtil.extend({}, layout); | 
|  | barRectShape[valueDim.wh] = pxSign * Math.max( | 
|  | Math.abs(layout[valueDim.wh]), Math.abs(pathPosition[valueDim.index] + sizeFix) | 
|  | ); | 
|  | barRectShape[categoryDim.wh] = layout[categoryDim.wh]; | 
|  |  | 
|  | const clipShape = outputSymbolMeta.clipShape = {} as RectShape; | 
|  | // Consider that symbol may be overflow layout rect. | 
|  | clipShape[categoryDim.xy] = -layout[categoryDim.xy]; | 
|  | clipShape[categoryDim.wh] = opt.ecSize[categoryDim.wh]; | 
|  | clipShape[valueDim.xy] = 0; | 
|  | clipShape[valueDim.wh] = layout[valueDim.wh]; | 
|  | } | 
|  |  | 
|  | function createPath(symbolMeta: SymbolMeta) { | 
|  | const symbolPatternSize = symbolMeta.symbolPatternSize; | 
|  | const path = createSymbol( | 
|  | // Consider texture img, make a big size. | 
|  | symbolMeta.symbolType, | 
|  | -symbolPatternSize / 2, | 
|  | -symbolPatternSize / 2, | 
|  | symbolPatternSize, | 
|  | symbolPatternSize | 
|  | ); | 
|  | (path as Displayable).attr({ | 
|  | culling: true | 
|  | }); | 
|  | path.type !== 'image' && path.setStyle({ | 
|  | strokeNoScale: true | 
|  | }); | 
|  |  | 
|  | return path as PictorialSymbol; | 
|  | } | 
|  |  | 
|  | function createOrUpdateRepeatSymbols( | 
|  | bar: PictorialBarElement, opt: CreateOpts, symbolMeta: SymbolMeta, isUpdate?: boolean | 
|  | ) { | 
|  | const bundle = bar.__pictorialBundle; | 
|  | const symbolSize = symbolMeta.symbolSize; | 
|  | const valueLineWidth = symbolMeta.valueLineWidth; | 
|  | const pathPosition = symbolMeta.pathPosition; | 
|  | const valueDim = opt.valueDim; | 
|  | const repeatTimes = symbolMeta.repeatTimes || 0; | 
|  |  | 
|  | let index = 0; | 
|  | const unit = symbolSize[opt.valueDim.index] + valueLineWidth + symbolMeta.symbolMargin * 2; | 
|  |  | 
|  | eachPath(bar, function (path) { | 
|  | path.__pictorialAnimationIndex = index; | 
|  | path.__pictorialRepeatTimes = repeatTimes; | 
|  | if (index < repeatTimes) { | 
|  | updateAttr(path, null, makeTarget(index), symbolMeta, isUpdate); | 
|  | } | 
|  | else { | 
|  | updateAttr(path, null, { scaleX: 0, scaleY: 0 }, symbolMeta, isUpdate, function () { | 
|  | bundle.remove(path); | 
|  | }); | 
|  | } | 
|  |  | 
|  | // updateHoverAnimation(path, symbolMeta); | 
|  |  | 
|  | index++; | 
|  | }); | 
|  |  | 
|  | for (; index < repeatTimes; index++) { | 
|  | const path = createPath(symbolMeta); | 
|  | path.__pictorialAnimationIndex = index; | 
|  | path.__pictorialRepeatTimes = repeatTimes; | 
|  | bundle.add(path); | 
|  |  | 
|  | const target = makeTarget(index); | 
|  |  | 
|  | updateAttr( | 
|  | path, | 
|  | { | 
|  | x: target.x, | 
|  | y: target.y, | 
|  | scaleX: 0, | 
|  | scaleY: 0 | 
|  | }, | 
|  | { | 
|  | scaleX: target.scaleX, | 
|  | scaleY: target.scaleY, | 
|  | rotation: target.rotation | 
|  | }, | 
|  | symbolMeta, | 
|  | isUpdate | 
|  | ); | 
|  | } | 
|  |  | 
|  | function makeTarget(index: number) { | 
|  | const position = pathPosition.slice(); | 
|  | // (start && pxSign > 0) || (end && pxSign < 0): i = repeatTimes - index | 
|  | // Otherwise: i = index; | 
|  | const pxSign = symbolMeta.pxSign; | 
|  | let i = index; | 
|  | if (symbolMeta.symbolRepeatDirection === 'start' ? pxSign > 0 : pxSign < 0) { | 
|  | i = repeatTimes - 1 - index; | 
|  | } | 
|  | position[valueDim.index] = unit * (i - repeatTimes / 2 + 0.5) + pathPosition[valueDim.index]; | 
|  |  | 
|  | return { | 
|  | x: position[0], | 
|  | y: position[1], | 
|  | scaleX: symbolMeta.symbolScale[0], | 
|  | scaleY: symbolMeta.symbolScale[1], | 
|  | rotation: symbolMeta.rotation | 
|  | }; | 
|  | } | 
|  | } | 
|  |  | 
|  | function createOrUpdateSingleSymbol( | 
|  | bar: PictorialBarElement, | 
|  | opt: CreateOpts, | 
|  | symbolMeta: SymbolMeta, | 
|  | isUpdate?: boolean | 
|  | ) { | 
|  | const bundle = bar.__pictorialBundle; | 
|  | let mainPath = bar.__pictorialMainPath; | 
|  |  | 
|  | if (!mainPath) { | 
|  | mainPath = bar.__pictorialMainPath = createPath(symbolMeta); | 
|  | bundle.add(mainPath); | 
|  |  | 
|  | updateAttr( | 
|  | mainPath, | 
|  | { | 
|  | x: symbolMeta.pathPosition[0], | 
|  | y: symbolMeta.pathPosition[1], | 
|  | scaleX: 0, | 
|  | scaleY: 0, | 
|  | rotation: symbolMeta.rotation | 
|  | }, | 
|  | { | 
|  | scaleX: symbolMeta.symbolScale[0], | 
|  | scaleY: symbolMeta.symbolScale[1] | 
|  | }, | 
|  | symbolMeta, | 
|  | isUpdate | 
|  | ); | 
|  | } | 
|  | else { | 
|  | updateAttr( | 
|  | mainPath, | 
|  | null, | 
|  | { | 
|  | x: symbolMeta.pathPosition[0], | 
|  | y: symbolMeta.pathPosition[1], | 
|  | scaleX: symbolMeta.symbolScale[0], | 
|  | scaleY: symbolMeta.symbolScale[1], | 
|  | rotation: symbolMeta.rotation | 
|  | }, | 
|  | symbolMeta, | 
|  | isUpdate | 
|  | ); | 
|  | } | 
|  | } | 
|  |  | 
|  | // bar rect is used for label. | 
|  | function createOrUpdateBarRect( | 
|  | bar: PictorialBarElement, | 
|  | symbolMeta: SymbolMeta, | 
|  | isUpdate?: boolean | 
|  | ) { | 
|  | const rectShape = zrUtil.extend({}, symbolMeta.barRectShape); | 
|  |  | 
|  | let barRect = bar.__pictorialBarRect; | 
|  | if (!barRect) { | 
|  | barRect = bar.__pictorialBarRect = new graphic.Rect({ | 
|  | z2: 2, | 
|  | shape: rectShape, | 
|  | silent: true, | 
|  | style: { | 
|  | stroke: 'transparent', | 
|  | fill: 'transparent', | 
|  | lineWidth: 0 | 
|  | } | 
|  | }); | 
|  | (barRect as ECElement).disableMorphing = true; | 
|  |  | 
|  | bar.add(barRect); | 
|  | } | 
|  | else { | 
|  | updateAttr(barRect, null, {shape: rectShape}, symbolMeta, isUpdate); | 
|  | } | 
|  | } | 
|  |  | 
|  | function createOrUpdateClip( | 
|  | bar: PictorialBarElement, | 
|  | opt: CreateOpts, | 
|  | symbolMeta: SymbolMeta, | 
|  | isUpdate?: boolean | 
|  | ) { | 
|  | // If not clip, symbol will be remove and rebuilt. | 
|  | if (symbolMeta.symbolClip) { | 
|  | let clipPath = bar.__pictorialClipPath; | 
|  | const clipShape = zrUtil.extend({}, symbolMeta.clipShape); | 
|  | const valueDim = opt.valueDim; | 
|  | const animationModel = symbolMeta.animationModel; | 
|  | const dataIndex = symbolMeta.dataIndex; | 
|  |  | 
|  | if (clipPath) { | 
|  | graphic.updateProps( | 
|  | clipPath, {shape: clipShape}, animationModel, dataIndex | 
|  | ); | 
|  | } | 
|  | else { | 
|  | clipShape[valueDim.wh] = 0; | 
|  | clipPath = new graphic.Rect({shape: clipShape}); | 
|  | bar.__pictorialBundle.setClipPath(clipPath); | 
|  | bar.__pictorialClipPath = clipPath; | 
|  |  | 
|  | const target = {} as RectShape; | 
|  | target[valueDim.wh] = symbolMeta.clipShape[valueDim.wh]; | 
|  |  | 
|  | graphic[isUpdate ? 'updateProps' : 'initProps']( | 
|  | clipPath, {shape: target}, animationModel, dataIndex | 
|  | ); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | function getItemModel(data: SeriesData, dataIndex: number) { | 
|  | const itemModel = data.getItemModel(dataIndex) as ItemModel; | 
|  | itemModel.getAnimationDelayParams = getAnimationDelayParams; | 
|  | itemModel.isAnimationEnabled = isAnimationEnabled; | 
|  | return itemModel; | 
|  | } | 
|  |  | 
|  | function getAnimationDelayParams(this: ItemModel, path: PictorialSymbol) { | 
|  | // The order is the same as the z-order, see `symbolRepeatDiretion`. | 
|  | return { | 
|  | index: path.__pictorialAnimationIndex, | 
|  | count: path.__pictorialRepeatTimes | 
|  | }; | 
|  | } | 
|  |  | 
|  | function isAnimationEnabled(this: ItemModel) { | 
|  | // `animation` prop can be set on itemModel in pictorial bar chart. | 
|  | return this.parentModel.isAnimationEnabled() && !!this.getShallow('animation'); | 
|  | } | 
|  |  | 
|  | function createBar(data: SeriesData, opt: CreateOpts, symbolMeta: SymbolMeta, isUpdate?: boolean) { | 
|  | // bar is the main element for each data. | 
|  | const bar = new graphic.Group() as PictorialBarElement; | 
|  | // bundle is used for location and clip. | 
|  | const bundle = new graphic.Group(); | 
|  | bar.add(bundle); | 
|  | bar.__pictorialBundle = bundle; | 
|  |  | 
|  | bundle.x = symbolMeta.bundlePosition[0]; | 
|  | bundle.y = symbolMeta.bundlePosition[1]; | 
|  |  | 
|  | if (symbolMeta.symbolRepeat) { | 
|  | createOrUpdateRepeatSymbols(bar, opt, symbolMeta); | 
|  | } | 
|  | else { | 
|  | createOrUpdateSingleSymbol(bar, opt, symbolMeta); | 
|  | } | 
|  |  | 
|  | createOrUpdateBarRect(bar, symbolMeta, isUpdate); | 
|  |  | 
|  | createOrUpdateClip(bar, opt, symbolMeta, isUpdate); | 
|  |  | 
|  | bar.__pictorialShapeStr = getShapeStr(data, symbolMeta); | 
|  | bar.__pictorialSymbolMeta = symbolMeta; | 
|  | return bar; | 
|  | } | 
|  |  | 
|  | function updateBar(bar: PictorialBarElement, opt: CreateOpts, symbolMeta: SymbolMeta) { | 
|  | const animationModel = symbolMeta.animationModel; | 
|  | const dataIndex = symbolMeta.dataIndex; | 
|  | const bundle = bar.__pictorialBundle; | 
|  |  | 
|  | graphic.updateProps( | 
|  | bundle, { | 
|  | x: symbolMeta.bundlePosition[0], | 
|  | y: symbolMeta.bundlePosition[1] | 
|  | }, animationModel, dataIndex | 
|  | ); | 
|  |  | 
|  | if (symbolMeta.symbolRepeat) { | 
|  | createOrUpdateRepeatSymbols(bar, opt, symbolMeta, true); | 
|  | } | 
|  | else { | 
|  | createOrUpdateSingleSymbol(bar, opt, symbolMeta, true); | 
|  | } | 
|  |  | 
|  | createOrUpdateBarRect(bar, symbolMeta, true); | 
|  |  | 
|  | createOrUpdateClip(bar, opt, symbolMeta, true); | 
|  | } | 
|  |  | 
|  | function removeBar( | 
|  | data: SeriesData, dataIndex: number, animationModel: Model<AnimationOptionMixin>, bar: PictorialBarElement | 
|  | ) { | 
|  | // Not show text when animating | 
|  | const labelRect = bar.__pictorialBarRect; | 
|  | labelRect && (labelRect.removeTextContent()); | 
|  |  | 
|  | const paths = []; | 
|  | eachPath(bar, function (path) { | 
|  | paths.push(path); | 
|  | }); | 
|  | bar.__pictorialMainPath && paths.push(bar.__pictorialMainPath); | 
|  |  | 
|  | // I do not find proper remove animation for clip yet. | 
|  | bar.__pictorialClipPath && (animationModel = null); | 
|  |  | 
|  | zrUtil.each(paths, function (path) { | 
|  | graphic.removeElement( | 
|  | path, { scaleX: 0, scaleY: 0 }, animationModel, dataIndex, | 
|  | function () { | 
|  | bar.parent && bar.parent.remove(bar); | 
|  | } | 
|  | ); | 
|  | }); | 
|  |  | 
|  | data.setItemGraphicEl(dataIndex, null); | 
|  | } | 
|  |  | 
|  | function getShapeStr(data: SeriesData, symbolMeta: SymbolMeta) { | 
|  | return [ | 
|  | data.getItemVisual(symbolMeta.dataIndex, 'symbol') || 'none', | 
|  | !!symbolMeta.symbolRepeat, | 
|  | !!symbolMeta.symbolClip | 
|  | ].join(':'); | 
|  | } | 
|  |  | 
|  | function eachPath<Ctx>( | 
|  | bar: PictorialBarElement, | 
|  | cb: (this: Ctx, el: PictorialSymbol) => void, | 
|  | context?: Ctx | 
|  | ) { | 
|  | // Do not use Group#eachChild, because it do not support remove. | 
|  | zrUtil.each(bar.__pictorialBundle.children(), function (el) { | 
|  | el !== bar.__pictorialBarRect && cb.call(context, el); | 
|  | }); | 
|  | } | 
|  |  | 
|  | function updateAttr<T extends Element>( | 
|  | el: T, | 
|  | immediateAttrs: PathProps, | 
|  | animationAttrs: PathProps, | 
|  | symbolMeta: SymbolMeta, | 
|  | isUpdate?: boolean, | 
|  | cb?: () => void | 
|  | ) { | 
|  | immediateAttrs && el.attr(immediateAttrs); | 
|  | // when symbolCip used, only clip path has init animation, otherwise it would be weird effect. | 
|  | if (symbolMeta.symbolClip && !isUpdate) { | 
|  | animationAttrs && el.attr(animationAttrs); | 
|  | } | 
|  | else { | 
|  | animationAttrs && graphic[isUpdate ? 'updateProps' : 'initProps']( | 
|  | el, animationAttrs, symbolMeta.animationModel, symbolMeta.dataIndex, cb | 
|  | ); | 
|  | } | 
|  | } | 
|  |  | 
|  | function updateCommon( | 
|  | bar: PictorialBarElement, | 
|  | opt: CreateOpts, | 
|  | symbolMeta: SymbolMeta | 
|  | ) { | 
|  | const dataIndex = symbolMeta.dataIndex; | 
|  | const itemModel = symbolMeta.itemModel; | 
|  | // Color must be excluded. | 
|  | // Because symbol provide setColor individually to set fill and stroke | 
|  | const emphasisModel = itemModel.getModel('emphasis'); | 
|  | const emphasisStyle = emphasisModel.getModel('itemStyle').getItemStyle(); | 
|  | const blurStyle = itemModel.getModel(['blur', 'itemStyle']).getItemStyle(); | 
|  | const selectStyle = itemModel.getModel(['select', 'itemStyle']).getItemStyle(); | 
|  | const cursorStyle = itemModel.getShallow('cursor'); | 
|  |  | 
|  | const focus = emphasisModel.get('focus'); | 
|  | const blurScope = emphasisModel.get('blurScope'); | 
|  | const hoverScale = emphasisModel.get('scale'); | 
|  |  | 
|  | eachPath(bar, function (path) { | 
|  | if (path instanceof ZRImage) { | 
|  | const pathStyle = path.style; | 
|  | path.useStyle(zrUtil.extend({ | 
|  | // TODO other properties like dx, dy ? | 
|  | image: pathStyle.image, | 
|  | x: pathStyle.x, y: pathStyle.y, | 
|  | width: pathStyle.width, height: pathStyle.height | 
|  | }, symbolMeta.style)); | 
|  | } | 
|  | else { | 
|  | path.useStyle(symbolMeta.style); | 
|  | } | 
|  |  | 
|  | const emphasisState = path.ensureState('emphasis'); | 
|  | emphasisState.style = emphasisStyle; | 
|  |  | 
|  | if (hoverScale) { | 
|  | // NOTE: Must after scale is set after updateAttr | 
|  | emphasisState.scaleX = path.scaleX * 1.1; | 
|  | emphasisState.scaleY = path.scaleY * 1.1; | 
|  | } | 
|  |  | 
|  | path.ensureState('blur').style = blurStyle; | 
|  | path.ensureState('select').style = selectStyle; | 
|  |  | 
|  | cursorStyle && (path.cursor = cursorStyle); | 
|  | path.z2 = symbolMeta.z2; | 
|  | }); | 
|  |  | 
|  | const barPositionOutside = opt.valueDim.posDesc[+(symbolMeta.boundingLength > 0)]; | 
|  | const barRect = bar.__pictorialBarRect; | 
|  |  | 
|  | setLabelStyle( | 
|  | barRect, getLabelStatesModels(itemModel), | 
|  | { | 
|  | labelFetcher: opt.seriesModel, | 
|  | labelDataIndex: dataIndex, | 
|  | defaultText: getDefaultLabel(opt.seriesModel.getData(), dataIndex), | 
|  | inheritColor: symbolMeta.style.fill as ColorString, | 
|  | defaultOpacity: symbolMeta.style.opacity, | 
|  | defaultOutsidePosition: barPositionOutside | 
|  | } | 
|  | ); | 
|  |  | 
|  | toggleHoverEmphasis(bar, focus, blurScope, emphasisModel.get('disabled')); | 
|  | } | 
|  |  | 
|  | function toIntTimes(times: number) { | 
|  | const roundedTimes = Math.round(times); | 
|  | // Escapse accurate error | 
|  | return Math.abs(times - roundedTimes) < 1e-4 | 
|  | ? roundedTimes | 
|  | : Math.ceil(times); | 
|  | } | 
|  |  | 
|  | export default PictorialBarView; |