blob: e2d909a26652ca602d79247c566c113d8db6d321 [file] [log] [blame]
/*
* 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 List from '../../data/List';
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';
interface UpdateOpt {
isIgnore?(idx: number): boolean
clipShape?: CoordinateSystemClipArea,
getSymbolPoint?(idx: number): number[]
disableAnimation?: boolean
}
interface SymbolLike extends graphic.Group {
updateData(data: List, idx: number, scope?: SymbolDrawSeriesScope, opt?: UpdateOpt): void
fadeOut?(cb: () => void): void
}
interface SymbolLikeCtor {
new(data: List, idx: number, scope?: SymbolDrawSeriesScope, opt?: UpdateOpt): SymbolLike
}
function symbolNeedsDraw(data: List, 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
}
}>,
SymbolDrawStateOption {
cursor?: string
// If has ripple effect
rippleEffect?: RippleEffectOption
}
export interface SymbolDrawSeriesScope {
emphasisItemStyle?: ZRStyleProps
blurItemStyle?: ZRStyleProps
selectItemStyle?: ZRStyleProps
focus?: DefaultEmphasisFocus
blurScope?: BlurScope
labelStatesModels: Record<DisplayState, Model<LabelOption>>
itemModel?: Model<SymbolDrawItemModelOption>
hoverScale?: boolean
cursorStyle?: string
fadeIn?: boolean
}
function makeSeriesScope(data: List): 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'),
hoverScale: emphasisModel.get('scale'),
labelStatesModels: getLabelStatesModels(seriesModel),
cursorStyle: seriesModel.get('cursor')
};
}
export type ListForSymbolDraw = List<Model<SymbolDrawItemModelOption & AnimationOptionMixin>>;
class SymbolDraw {
group = new graphic.Group();
private _data: ListForSymbolDraw;
private _SymbolCtor: SymbolLikeCtor;
private _seriesScope: SymbolDrawSeriesScope;
private _getSymbolPoint: UpdateOpt['getSymbolPoint'];
constructor(SymbolCtor?: SymbolLikeCtor) {
this._SymbolCtor = SymbolCtor || SymbolClz as SymbolLikeCtor;
}
/**
* Update symbols draw by new data
*/
updateData(data: ListForSymbolDraw, opt?: UpdateOpt) {
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);
});
})
.execute();
this._getSymbolPoint = getSymbolPoint;
this._data = data;
};
isPersistent() {
return true;
};
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) {
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);
}
}
};
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);
});
});
}
else {
group.removeAll();
}
};
}
export default SymbolDraw;