| /* | |
| * 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 graphic from '../../util/graphic'; | |
| import SymbolClz from './Symbol'; | |
| import { isObject } from 'zrender/src/core/util'; | |
| import SeriesData from '../../data/SeriesData'; | |
| import type Displayable from 'zrender/src/graphic/Displayable'; | |
| import { | |
| StageHandlerProgressParams, | |
| LabelOption, | |
| SymbolOptionMixin, | |
| ItemStyleOption, | |
| ZRColor, | |
| AnimationOptionMixin, | |
| ZRStyleProps, | |
| StatesOptionMixin, | |
| BlurScope, | |
| DisplayState, | |
| DefaultEmphasisFocus | |
| } from '../../util/types'; | |
| import { CoordinateSystemClipArea } from '../../coord/CoordinateSystem'; | |
| import Model from '../../model/Model'; | |
| import { ScatterSeriesOption } from '../scatter/ScatterSeries'; | |
| import { getLabelStatesModels } from '../../label/labelStyle'; | |
| import Element from 'zrender/src/Element'; | |
| import SeriesModel from '../../model/Series'; | |
| interface UpdateOpt { | |
| isIgnore?(idx: number): boolean | |
| clipShape?: CoordinateSystemClipArea, | |
| getSymbolPoint?(idx: number): number[] | |
| disableAnimation?: boolean | |
| } | |
| interface SymbolLike extends graphic.Group { | |
| updateData(data: SeriesData, idx: number, scope?: SymbolDrawSeriesScope, opt?: UpdateOpt): void | |
| fadeOut?(cb: () => void, seriesModel: SeriesModel): void | |
| } | |
| interface SymbolLikeCtor { | |
| new(data: SeriesData, idx: number, scope?: SymbolDrawSeriesScope, opt?: UpdateOpt): SymbolLike | |
| } | |
| function symbolNeedsDraw(data: SeriesData, point: number[], idx: number, opt: UpdateOpt) { | |
| return point && !isNaN(point[0]) && !isNaN(point[1]) | |
| && !(opt.isIgnore && opt.isIgnore(idx)) | |
| // We do not set clipShape on group, because it will cut part of | |
| // the symbol element shape. We use the same clip shape here as | |
| // the line clip. | |
| && !(opt.clipShape && !opt.clipShape.contain(point[0], point[1])) | |
| && data.getItemVisual(idx, 'symbol') !== 'none'; | |
| } | |
| function normalizeUpdateOpt(opt: UpdateOpt) { | |
| if (opt != null && !isObject(opt)) { | |
| opt = {isIgnore: opt}; | |
| } | |
| return opt || {}; | |
| } | |
| interface RippleEffectOption { | |
| period?: number | |
| /** | |
| * Scale of ripple | |
| */ | |
| scale?: number | |
| brushType?: 'fill' | 'stroke' | |
| color?: ZRColor, | |
| /** | |
| * ripple number | |
| */ | |
| number?: number | |
| } | |
| interface SymbolDrawStateOption { | |
| itemStyle?: ItemStyleOption | |
| label?: LabelOption | |
| } | |
| // TODO Separate series and item? | |
| export interface SymbolDrawItemModelOption extends SymbolOptionMixin<object>, | |
| StatesOptionMixin<SymbolDrawStateOption, { | |
| emphasis?: { | |
| focus?: DefaultEmphasisFocus | |
| scale?: boolean | number | |
| } | |
| }>, | |
| SymbolDrawStateOption { | |
| cursor?: string | |
| // If has ripple effect | |
| rippleEffect?: RippleEffectOption | |
| } | |
| export interface SymbolDrawSeriesScope { | |
| emphasisItemStyle?: ZRStyleProps | |
| blurItemStyle?: ZRStyleProps | |
| selectItemStyle?: ZRStyleProps | |
| focus?: DefaultEmphasisFocus | |
| blurScope?: BlurScope | |
| emphasisDisabled?: boolean | |
| labelStatesModels: Record<DisplayState, Model<LabelOption>> | |
| itemModel?: Model<SymbolDrawItemModelOption> | |
| hoverScale?: boolean | number | |
| cursorStyle?: string | |
| fadeIn?: boolean | |
| } | |
| function makeSeriesScope(data: SeriesData): SymbolDrawSeriesScope { | |
| const seriesModel = data.hostModel as Model<ScatterSeriesOption>; | |
| const emphasisModel = seriesModel.getModel('emphasis'); | |
| return { | |
| emphasisItemStyle: emphasisModel.getModel('itemStyle').getItemStyle(), | |
| blurItemStyle: seriesModel.getModel(['blur', 'itemStyle']).getItemStyle(), | |
| selectItemStyle: seriesModel.getModel(['select', 'itemStyle']).getItemStyle(), | |
| focus: emphasisModel.get('focus'), | |
| blurScope: emphasisModel.get('blurScope'), | |
| emphasisDisabled: emphasisModel.get('disabled'), | |
| hoverScale: emphasisModel.get('scale'), | |
| labelStatesModels: getLabelStatesModels(seriesModel), | |
| cursorStyle: seriesModel.get('cursor') | |
| }; | |
| } | |
| export type ListForSymbolDraw = SeriesData<Model<SymbolDrawItemModelOption & AnimationOptionMixin>>; | |
| class SymbolDraw { | |
| group = new graphic.Group(); | |
| private _data: ListForSymbolDraw; | |
| private _SymbolCtor: SymbolLikeCtor; | |
| private _seriesScope: SymbolDrawSeriesScope; | |
| private _getSymbolPoint: UpdateOpt['getSymbolPoint']; | |
| private _progressiveEls: SymbolLike[]; | |
| constructor(SymbolCtor?: SymbolLikeCtor) { | |
| this._SymbolCtor = SymbolCtor || SymbolClz as SymbolLikeCtor; | |
| } | |
| /** | |
| * Update symbols draw by new data | |
| */ | |
| updateData(data: ListForSymbolDraw, opt?: UpdateOpt) { | |
| // Remove progressive els. | |
| this._progressiveEls = null; | |
| opt = normalizeUpdateOpt(opt); | |
| const group = this.group; | |
| const seriesModel = data.hostModel; | |
| const oldData = this._data; | |
| const SymbolCtor = this._SymbolCtor; | |
| const disableAnimation = opt.disableAnimation; | |
| const seriesScope = makeSeriesScope(data); | |
| const symbolUpdateOpt = { disableAnimation }; | |
| const getSymbolPoint = opt.getSymbolPoint || function (idx: number) { | |
| return data.getItemLayout(idx); | |
| }; | |
| // There is no oldLineData only when first rendering or switching from | |
| // stream mode to normal mode, where previous elements should be removed. | |
| if (!oldData) { | |
| group.removeAll(); | |
| } | |
| data.diff(oldData) | |
| .add(function (newIdx) { | |
| const point = getSymbolPoint(newIdx); | |
| if (symbolNeedsDraw(data, point, newIdx, opt)) { | |
| const symbolEl = new SymbolCtor(data, newIdx, seriesScope, symbolUpdateOpt); | |
| symbolEl.setPosition(point); | |
| data.setItemGraphicEl(newIdx, symbolEl); | |
| group.add(symbolEl); | |
| } | |
| }) | |
| .update(function (newIdx, oldIdx) { | |
| let symbolEl = oldData.getItemGraphicEl(oldIdx) as SymbolLike; | |
| const point = getSymbolPoint(newIdx) as number[]; | |
| if (!symbolNeedsDraw(data, point, newIdx, opt)) { | |
| group.remove(symbolEl); | |
| return; | |
| } | |
| const newSymbolType = data.getItemVisual(newIdx, 'symbol') || 'circle'; | |
| const oldSymbolType = symbolEl | |
| && (symbolEl as SymbolClz).getSymbolType | |
| && (symbolEl as SymbolClz).getSymbolType(); | |
| if (!symbolEl | |
| // Create a new if symbol type changed. | |
| || (oldSymbolType && oldSymbolType !== newSymbolType) | |
| ) { | |
| group.remove(symbolEl); | |
| symbolEl = new SymbolCtor(data, newIdx, seriesScope, symbolUpdateOpt); | |
| symbolEl.setPosition(point); | |
| } | |
| else { | |
| symbolEl.updateData(data, newIdx, seriesScope, symbolUpdateOpt); | |
| const target = { | |
| x: point[0], | |
| y: point[1] | |
| }; | |
| disableAnimation | |
| ? symbolEl.attr(target) | |
| : graphic.updateProps(symbolEl, target, seriesModel); | |
| } | |
| // Add back | |
| group.add(symbolEl); | |
| data.setItemGraphicEl(newIdx, symbolEl); | |
| }) | |
| .remove(function (oldIdx) { | |
| const el = oldData.getItemGraphicEl(oldIdx) as SymbolLike; | |
| el && el.fadeOut(function () { | |
| group.remove(el); | |
| }, seriesModel as SeriesModel); | |
| }) | |
| .execute(); | |
| this._getSymbolPoint = getSymbolPoint; | |
| this._data = data; | |
| }; | |
| updateLayout() { | |
| const data = this._data; | |
| if (data) { | |
| // Not use animation | |
| data.eachItemGraphicEl((el, idx) => { | |
| const point = this._getSymbolPoint(idx); | |
| el.setPosition(point); | |
| el.markRedraw(); | |
| }); | |
| } | |
| }; | |
| incrementalPrepareUpdate(data: ListForSymbolDraw) { | |
| this._seriesScope = makeSeriesScope(data); | |
| this._data = null; | |
| this.group.removeAll(); | |
| }; | |
| /** | |
| * Update symbols draw by new data | |
| */ | |
| incrementalUpdate(taskParams: StageHandlerProgressParams, data: ListForSymbolDraw, opt?: UpdateOpt) { | |
| // Clear | |
| this._progressiveEls = []; | |
| opt = normalizeUpdateOpt(opt); | |
| function updateIncrementalAndHover(el: Displayable) { | |
| if (!el.isGroup) { | |
| el.incremental = true; | |
| el.ensureState('emphasis').hoverLayer = true; | |
| } | |
| } | |
| for (let idx = taskParams.start; idx < taskParams.end; idx++) { | |
| const point = data.getItemLayout(idx) as number[]; | |
| if (symbolNeedsDraw(data, point, idx, opt)) { | |
| const el = new this._SymbolCtor(data, idx, this._seriesScope); | |
| el.traverse(updateIncrementalAndHover); | |
| el.setPosition(point); | |
| this.group.add(el); | |
| data.setItemGraphicEl(idx, el); | |
| this._progressiveEls.push(el); | |
| } | |
| } | |
| }; | |
| eachRendered(cb: (el: Element) => boolean | void) { | |
| graphic.traverseElements(this._progressiveEls || this.group, cb); | |
| } | |
| remove(enableAnimation?: boolean) { | |
| const group = this.group; | |
| const data = this._data; | |
| // Incremental model do not have this._data. | |
| if (data && enableAnimation) { | |
| data.eachItemGraphicEl(function (el: SymbolLike) { | |
| el.fadeOut(function () { | |
| group.remove(el); | |
| }, data.hostModel as SeriesModel); | |
| }); | |
| } | |
| else { | |
| group.removeAll(); | |
| } | |
| }; | |
| } | |
| export default SymbolDraw; |