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