| /* |
| * 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; |