| /* | |
| * 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 {createSymbol, normalizeSymbolOffset, normalizeSymbolSize} from '../../util/symbol'; | |
| import * as graphic from '../../util/graphic'; | |
| import {getECData} from '../../util/innerStore'; | |
| import { enterEmphasis, leaveEmphasis, toggleHoverEmphasis } from '../../util/states'; | |
| import {getDefaultLabel} from './labelHelper'; | |
| import SeriesData from '../../data/SeriesData'; | |
| import { ColorString, BlurScope, AnimationOption, ZRColor, AnimationOptionMixin } from '../../util/types'; | |
| import SeriesModel from '../../model/Series'; | |
| import { PathProps } from 'zrender/src/graphic/Path'; | |
| import { SymbolDrawSeriesScope, SymbolDrawItemModelOption } from './SymbolDraw'; | |
| import { extend } from 'zrender/src/core/util'; | |
| import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; | |
| import ZRImage from 'zrender/src/graphic/Image'; | |
| import { saveOldStyle } from '../../animation/basicTransition'; | |
| import Model from '../../model/Model'; | |
| type ECSymbol = ReturnType<typeof createSymbol>; | |
| interface SymbolOpts { | |
| disableAnimation?: boolean | |
| useNameLabel?: boolean | |
| symbolInnerColor?: ZRColor | |
| } | |
| class Symbol extends graphic.Group { | |
| private _symbolType: string; | |
| /** | |
| * Original scale | |
| */ | |
| private _sizeX: number; | |
| private _sizeY: number; | |
| private _z2: number; | |
| constructor(data: SeriesData, idx: number, seriesScope?: SymbolDrawSeriesScope, opts?: SymbolOpts) { | |
| super(); | |
| this.updateData(data, idx, seriesScope, opts); | |
| } | |
| _createSymbol( | |
| symbolType: string, | |
| data: SeriesData, | |
| idx: number, | |
| symbolSize: number[], | |
| keepAspect: boolean | |
| ) { | |
| // Remove paths created before | |
| this.removeAll(); | |
| // let symbolPath = createSymbol( | |
| // symbolType, -0.5, -0.5, 1, 1, color | |
| // ); | |
| // If width/height are set too small (e.g., set to 1) on ios10 | |
| // and macOS Sierra, a circle stroke become a rect, no matter what | |
| // the scale is set. So we set width/height as 2. See #4150. | |
| const symbolPath = createSymbol( | |
| symbolType, -1, -1, 2, 2, null, keepAspect | |
| ); | |
| symbolPath.attr({ | |
| z2: 100, | |
| culling: true, | |
| scaleX: symbolSize[0] / 2, | |
| scaleY: symbolSize[1] / 2 | |
| }); | |
| // Rewrite drift method | |
| symbolPath.drift = driftSymbol; | |
| this._symbolType = symbolType; | |
| this.add(symbolPath); | |
| } | |
| /** | |
| * Stop animation | |
| * @param {boolean} toLastFrame | |
| */ | |
| stopSymbolAnimation(toLastFrame: boolean) { | |
| this.childAt(0).stopAnimation(null, toLastFrame); | |
| } | |
| getSymbolType() { | |
| return this._symbolType; | |
| } | |
| /** | |
| * FIXME: | |
| * Caution: This method breaks the encapsulation of this module, | |
| * but it indeed brings convenience. So do not use the method | |
| * unless you detailedly know all the implements of `Symbol`, | |
| * especially animation. | |
| * | |
| * Get symbol path element. | |
| */ | |
| getSymbolPath() { | |
| return this.childAt(0) as ECSymbol; | |
| } | |
| /** | |
| * Highlight symbol | |
| */ | |
| highlight() { | |
| enterEmphasis(this.childAt(0)); | |
| } | |
| /** | |
| * Downplay symbol | |
| */ | |
| downplay() { | |
| leaveEmphasis(this.childAt(0)); | |
| } | |
| /** | |
| * @param {number} zlevel | |
| * @param {number} z | |
| */ | |
| setZ(zlevel: number, z: number) { | |
| const symbolPath = this.childAt(0) as ECSymbol; | |
| symbolPath.zlevel = zlevel; | |
| symbolPath.z = z; | |
| } | |
| setDraggable(draggable: boolean, hasCursorOption?: boolean) { | |
| const symbolPath = this.childAt(0) as ECSymbol; | |
| symbolPath.draggable = draggable; | |
| symbolPath.cursor = !hasCursorOption && draggable ? 'move' : symbolPath.cursor; | |
| } | |
| /** | |
| * Update symbol properties | |
| */ | |
| updateData(data: SeriesData, idx: number, seriesScope?: SymbolDrawSeriesScope, opts?: SymbolOpts) { | |
| this.silent = false; | |
| const symbolType = data.getItemVisual(idx, 'symbol') || 'circle'; | |
| const seriesModel = data.hostModel as SeriesModel; | |
| const symbolSize = Symbol.getSymbolSize(data, idx); | |
| const isInit = symbolType !== this._symbolType; | |
| const disableAnimation = opts && opts.disableAnimation; | |
| if (isInit) { | |
| const keepAspect = data.getItemVisual(idx, 'symbolKeepAspect'); | |
| this._createSymbol(symbolType as string, data, idx, symbolSize, keepAspect); | |
| } | |
| else { | |
| const symbolPath = this.childAt(0) as ECSymbol; | |
| symbolPath.silent = false; | |
| const target = { | |
| scaleX: symbolSize[0] / 2, | |
| scaleY: symbolSize[1] / 2 | |
| }; | |
| disableAnimation ? symbolPath.attr(target) | |
| : graphic.updateProps(symbolPath, target, seriesModel, idx); | |
| saveOldStyle(symbolPath); | |
| } | |
| this._updateCommon(data, idx, symbolSize, seriesScope, opts); | |
| if (isInit) { | |
| const symbolPath = this.childAt(0) as ECSymbol; | |
| if (!disableAnimation) { | |
| const target: PathProps = { | |
| scaleX: this._sizeX, | |
| scaleY: this._sizeY, | |
| style: { | |
| // Always fadeIn. Because it has fadeOut animation when symbol is removed.. | |
| opacity: symbolPath.style.opacity | |
| } | |
| }; | |
| symbolPath.scaleX = symbolPath.scaleY = 0; | |
| symbolPath.style.opacity = 0; | |
| graphic.initProps(symbolPath, target, seriesModel, idx); | |
| } | |
| } | |
| if (disableAnimation) { | |
| // Must stop leave transition manually if don't call initProps or updateProps. | |
| this.childAt(0).stopAnimation('leave'); | |
| } | |
| } | |
| _updateCommon( | |
| data: SeriesData, | |
| idx: number, | |
| symbolSize: number[], | |
| seriesScope?: SymbolDrawSeriesScope, | |
| opts?: SymbolOpts | |
| ) { | |
| const symbolPath = this.childAt(0) as ECSymbol; | |
| const seriesModel = data.hostModel as SeriesModel; | |
| let emphasisItemStyle; | |
| let blurItemStyle; | |
| let selectItemStyle; | |
| let focus; | |
| let blurScope: BlurScope; | |
| let emphasisDisabled: boolean; | |
| let labelStatesModels; | |
| let hoverScale: SymbolDrawSeriesScope['hoverScale']; | |
| let cursorStyle: SymbolDrawSeriesScope['cursorStyle']; | |
| if (seriesScope) { | |
| emphasisItemStyle = seriesScope.emphasisItemStyle; | |
| blurItemStyle = seriesScope.blurItemStyle; | |
| selectItemStyle = seriesScope.selectItemStyle; | |
| focus = seriesScope.focus; | |
| blurScope = seriesScope.blurScope; | |
| labelStatesModels = seriesScope.labelStatesModels; | |
| hoverScale = seriesScope.hoverScale; | |
| cursorStyle = seriesScope.cursorStyle; | |
| emphasisDisabled = seriesScope.emphasisDisabled; | |
| } | |
| if (!seriesScope || data.hasItemOption) { | |
| const itemModel = (seriesScope && seriesScope.itemModel) | |
| ? seriesScope.itemModel : data.getItemModel<SymbolDrawItemModelOption>(idx); | |
| const emphasisModel = itemModel.getModel('emphasis'); | |
| emphasisItemStyle = emphasisModel.getModel('itemStyle').getItemStyle(); | |
| selectItemStyle = itemModel.getModel(['select', 'itemStyle']).getItemStyle(); | |
| blurItemStyle = itemModel.getModel(['blur', 'itemStyle']).getItemStyle(); | |
| focus = emphasisModel.get('focus'); | |
| blurScope = emphasisModel.get('blurScope'); | |
| emphasisDisabled = emphasisModel.get('disabled'); | |
| labelStatesModels = getLabelStatesModels(itemModel); | |
| hoverScale = emphasisModel.getShallow('scale'); | |
| cursorStyle = itemModel.getShallow('cursor'); | |
| } | |
| const symbolRotate = data.getItemVisual(idx, 'symbolRotate'); | |
| symbolPath.attr('rotation', (symbolRotate || 0) * Math.PI / 180 || 0); | |
| const symbolOffset = normalizeSymbolOffset(data.getItemVisual(idx, 'symbolOffset'), symbolSize); | |
| if (symbolOffset) { | |
| symbolPath.x = symbolOffset[0]; | |
| symbolPath.y = symbolOffset[1]; | |
| } | |
| cursorStyle && symbolPath.attr('cursor', cursorStyle); | |
| const symbolStyle = data.getItemVisual(idx, 'style'); | |
| const visualColor = symbolStyle.fill; | |
| if (symbolPath instanceof ZRImage) { | |
| const pathStyle = symbolPath.style; | |
| symbolPath.useStyle(extend({ | |
| // TODO other properties like x, y ? | |
| image: pathStyle.image, | |
| x: pathStyle.x, y: pathStyle.y, | |
| width: pathStyle.width, height: pathStyle.height | |
| }, symbolStyle)); | |
| } | |
| else { | |
| if (symbolPath.__isEmptyBrush) { | |
| // fill and stroke will be swapped if it's empty. | |
| // So we cloned a new style to avoid it affecting the original style in visual storage. | |
| // TODO Better implementation. No empty logic! | |
| symbolPath.useStyle(extend({}, symbolStyle)); | |
| } | |
| else { | |
| symbolPath.useStyle(symbolStyle); | |
| } | |
| // Disable decal because symbol scale will been applied on the decal. | |
| symbolPath.style.decal = null; | |
| symbolPath.setColor(visualColor, opts && opts.symbolInnerColor); | |
| symbolPath.style.strokeNoScale = true; | |
| } | |
| const liftZ = data.getItemVisual(idx, 'liftZ'); | |
| const z2Origin = this._z2; | |
| if (liftZ != null) { | |
| if (z2Origin == null) { | |
| this._z2 = symbolPath.z2; | |
| symbolPath.z2 += liftZ; | |
| } | |
| } | |
| else if (z2Origin != null) { | |
| symbolPath.z2 = z2Origin; | |
| this._z2 = null; | |
| } | |
| const useNameLabel = opts && opts.useNameLabel; | |
| setLabelStyle( | |
| symbolPath, labelStatesModels, | |
| { | |
| labelFetcher: seriesModel, | |
| labelDataIndex: idx, | |
| defaultText: getLabelDefaultText, | |
| inheritColor: visualColor as ColorString, | |
| defaultOpacity: symbolStyle.opacity | |
| } | |
| ); | |
| // Do not execute util needed. | |
| function getLabelDefaultText(idx: number) { | |
| return useNameLabel ? data.getName(idx) : getDefaultLabel(data, idx); | |
| } | |
| this._sizeX = symbolSize[0] / 2; | |
| this._sizeY = symbolSize[1] / 2; | |
| const emphasisState = symbolPath.ensureState('emphasis'); | |
| emphasisState.style = emphasisItemStyle; | |
| symbolPath.ensureState('select').style = selectItemStyle; | |
| symbolPath.ensureState('blur').style = blurItemStyle; | |
| // null / undefined / true means to use default strategy. | |
| // 0 / false / negative number / NaN / Infinity means no scale. | |
| const scaleRatio = | |
| hoverScale == null || hoverScale === true | |
| ? Math.max(1.1, 3 / this._sizeY) | |
| // PENDING: restrict hoverScale > 1? It seems unreasonable to scale down | |
| : isFinite(hoverScale as number) && hoverScale > 0 | |
| ? +hoverScale | |
| : 1; | |
| // always set scale to allow resetting | |
| emphasisState.scaleX = this._sizeX * scaleRatio; | |
| emphasisState.scaleY = this._sizeY * scaleRatio; | |
| this.setSymbolScale(1); | |
| toggleHoverEmphasis(this, focus, blurScope, emphasisDisabled); | |
| } | |
| setSymbolScale(scale: number) { | |
| this.scaleX = this.scaleY = scale; | |
| } | |
| fadeOut(cb: () => void, seriesModel: Model<AnimationOptionMixin>, opt?: { | |
| fadeLabel: boolean, | |
| animation?: AnimationOption | |
| }) { | |
| const symbolPath = this.childAt(0) as ECSymbol; | |
| const dataIndex = getECData(this).dataIndex; | |
| const animationOpt = opt && opt.animation; | |
| // Avoid mistaken hover when fading out | |
| this.silent = symbolPath.silent = true; | |
| // Not show text when animating | |
| if (opt && opt.fadeLabel) { | |
| const textContent = symbolPath.getTextContent(); | |
| if (textContent) { | |
| graphic.removeElement(textContent, { | |
| style: { | |
| opacity: 0 | |
| } | |
| }, seriesModel, { | |
| dataIndex, | |
| removeOpt: animationOpt, | |
| cb() { | |
| symbolPath.removeTextContent(); | |
| } | |
| }); | |
| } | |
| } | |
| else { | |
| symbolPath.removeTextContent(); | |
| } | |
| graphic.removeElement( | |
| symbolPath, | |
| { | |
| style: { | |
| opacity: 0 | |
| }, | |
| scaleX: 0, | |
| scaleY: 0 | |
| }, | |
| seriesModel, | |
| { dataIndex, cb, removeOpt: animationOpt} | |
| ); | |
| } | |
| static getSymbolSize(data: SeriesData, idx: number) { | |
| return normalizeSymbolSize(data.getItemVisual(idx, 'symbolSize')); | |
| } | |
| } | |
| function driftSymbol(this: ECSymbol, dx: number, dy: number) { | |
| this.parent.drift(dx, dy); | |
| } | |
| export default Symbol; |