/*
|
* 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 env from 'zrender/src/core/env';
|
import type {MorphDividingMethod} from 'zrender/src/tool/morphPath';
|
import {PathStyleProps} from 'zrender/src/graphic/Path';
|
import * as modelUtil from '../util/model';
|
import {
|
DataHost, DimensionName, StageHandlerProgressParams,
|
SeriesOption, ZRColor, BoxLayoutOptionMixin,
|
ScaleDataValue, Dictionary, OptionDataItemObject, SeriesDataType, DimensionLoose
|
} from '../util/types';
|
import ComponentModel, { ComponentModelConstructor } from './Component';
|
import {PaletteMixin} from './mixin/palette';
|
import { DataFormatMixin } from '../model/mixin/dataFormat';
|
import Model from '../model/Model';
|
import {
|
getLayoutParams,
|
mergeLayoutParam,
|
fetchLayoutMode
|
} from '../util/layout';
|
import {createTask} from '../core/task';
|
import GlobalModel from './Global';
|
import { CoordinateSystem } from '../coord/CoordinateSystem';
|
import { ExtendableConstructor, mountExtend, Constructor } from '../util/clazz';
|
import { PipelineContext, SeriesTaskContext, GeneralTask, OverallTask, SeriesTask } from '../core/Scheduler';
|
import LegendVisualProvider from '../visual/LegendVisualProvider';
|
import List from '../data/List';
|
import Axis from '../coord/Axis';
|
import type { BrushCommonSelectorsForSeries, BrushSelectableArea } from '../component/brush/selector';
|
import makeStyleMapper from './mixin/makeStyleMapper';
|
import { SourceManager } from '../data/helper/sourceManager';
|
import { Source } from '../data/Source';
|
import { defaultSeriesFormatTooltip } from '../component/tooltip/seriesFormatTooltip';
|
import {ECSymbol} from '../util/symbol';
|
import {Group} from '../util/graphic';
|
import {LegendSymbolParams} from '../component/legend/LegendModel';
|
|
const inner = modelUtil.makeInner<{
|
data: List
|
dataBeforeProcessed: List
|
sourceManager: SourceManager
|
}, SeriesModel>();
|
|
function getSelectionKey(data: List, dataIndex: number): string {
|
return data.getName(dataIndex) || data.getId(dataIndex);
|
}
|
|
interface SeriesModel {
|
/**
|
* Convinient for override in extended class.
|
* Implement it if needed.
|
*/
|
preventIncremental(): boolean;
|
/**
|
* See tooltip.
|
* Implement it if needed.
|
* @return Point of tooltip. null/undefined can be returned.
|
*/
|
getTooltipPosition(dataIndex: number): number[];
|
|
/**
|
* Get data indices for show tooltip content. See tooltip.
|
* Implement it if needed.
|
*/
|
getAxisTooltipData(
|
dim: DimensionName[],
|
value: ScaleDataValue,
|
baseAxis: Axis
|
): {
|
dataIndices: number[],
|
nestestValue: any
|
};
|
|
/**
|
* Get position for marker
|
*/
|
getMarkerPosition(value: ScaleDataValue[]): number[];
|
|
/**
|
* Get legend icon symbol according to each series type
|
*/
|
getLegendIcon(opt: LegendSymbolParams): ECSymbol | Group;
|
|
/**
|
* See `component/brush/selector.js`
|
* Defined the brush selector for this series.
|
*/
|
brushSelector(
|
dataIndex: number,
|
data: List,
|
selectors: BrushCommonSelectorsForSeries,
|
area: BrushSelectableArea
|
): boolean;
|
|
enableAriaDecal(): void;
|
}
|
|
class SeriesModel<Opt extends SeriesOption = SeriesOption> extends ComponentModel<Opt> {
|
|
// [Caution]: Becuase this class or desecendants can be used as `XXX.extend(subProto)`,
|
// the class members must not be initialized in constructor or declaration place.
|
// Otherwise there is bad case:
|
// class A {xxx = 1;}
|
// enableClassExtend(A);
|
// class B extends A {}
|
// var C = B.extend({xxx: 5});
|
// var c = new C();
|
// console.log(c.xxx); // expect 5 but always 1.
|
|
// @readonly
|
type: string;
|
|
// Should be implenented in subclass.
|
defaultOption: SeriesOption;
|
|
// @readonly
|
seriesIndex: number;
|
|
// coodinateSystem will be injected in the echarts/CoordinateSystem
|
coordinateSystem: CoordinateSystem;
|
|
// Injected outside
|
dataTask: SeriesTask;
|
// Injected outside
|
pipelineContext: PipelineContext;
|
|
// only avalible in `render()` caused by `setOption`.
|
__transientTransitionOpt: {
|
// [MEMO] Currently only support single "from". If intending to
|
// support multiple "from", if not hard to implement "merge morph",
|
// but correspondingly not easy to implement "split morph".
|
|
// Both from and to can be null/undefined, which meams no transform mapping.
|
from: DimensionLoose;
|
to: DimensionLoose;
|
dividingMethod: MorphDividingMethod;
|
};
|
|
// ---------------------------------------
|
// Props to tell visual/style.ts about how to do visual encoding.
|
// ---------------------------------------
|
// legend visual provider to the legend component
|
legendVisualProvider: LegendVisualProvider;
|
|
// Access path of style for visual
|
visualStyleAccessPath: string;
|
// Which property is treated as main color. Which can get from the palette.
|
visualDrawType: 'fill' | 'stroke';
|
// Style mapping rules.
|
visualStyleMapper: ReturnType<typeof makeStyleMapper>;
|
// If ignore style on data. It's only for global visual/style.ts
|
// Enabled when series it self will handle it.
|
ignoreStyleOnData: boolean;
|
// If use palette on each data.
|
useColorPaletteOnData: boolean;
|
// If do symbol visual encoding
|
hasSymbolVisual: boolean;
|
// Default symbol type.
|
defaultSymbol: string;
|
// Symbol provide to legend.
|
legendSymbol: string;
|
|
// ---------------------------------------
|
// Props about data selection
|
// ---------------------------------------
|
private _selectedDataIndicesMap: Dictionary<number> = {};
|
|
readonly preventUsingHoverLayer: boolean;
|
|
static protoInitialize = (function () {
|
const proto = SeriesModel.prototype;
|
proto.type = 'series.__base__';
|
proto.seriesIndex = 0;
|
proto.useColorPaletteOnData = false;
|
proto.ignoreStyleOnData = false;
|
proto.hasSymbolVisual = false;
|
proto.defaultSymbol = 'circle';
|
// Make sure the values can be accessed!
|
proto.visualStyleAccessPath = 'itemStyle';
|
proto.visualDrawType = 'fill';
|
})();
|
|
|
init(option: Opt, parentModel: Model, ecModel: GlobalModel) {
|
|
this.seriesIndex = this.componentIndex;
|
|
this.dataTask = createTask<SeriesTaskContext>({
|
count: dataTaskCount,
|
reset: dataTaskReset
|
});
|
this.dataTask.context = {model: this};
|
|
this.mergeDefaultAndTheme(option, ecModel);
|
|
const sourceManager = inner(this).sourceManager = new SourceManager(this);
|
sourceManager.prepareSource();
|
|
const data = this.getInitialData(option, ecModel);
|
wrapData(data, this);
|
this.dataTask.context.data = data;
|
|
if (__DEV__) {
|
zrUtil.assert(data, 'getInitialData returned invalid data.');
|
}
|
|
inner(this).dataBeforeProcessed = data;
|
|
// If we reverse the order (make data firstly, and then make
|
// dataBeforeProcessed by cloneShallow), cloneShallow will
|
// cause data.graph.data !== data when using
|
// module:echarts/data/Graph or module:echarts/data/Tree.
|
// See module:echarts/data/helper/linkList
|
|
// Theoretically, it is unreasonable to call `seriesModel.getData()` in the model
|
// init or merge stage, because the data can be restored. So we do not `restoreData`
|
// and `setData` here, which forbids calling `seriesModel.getData()` in this stage.
|
// Call `seriesModel.getRawData()` instead.
|
// this.restoreData();
|
|
autoSeriesName(this);
|
|
this._initSelectedMapFromData(data);
|
}
|
|
/**
|
* Util for merge default and theme to option
|
*/
|
mergeDefaultAndTheme(option: Opt, ecModel: GlobalModel): void {
|
const layoutMode = fetchLayoutMode(this);
|
const inputPositionParams = layoutMode
|
? getLayoutParams(option as BoxLayoutOptionMixin) : {};
|
|
// Backward compat: using subType on theme.
|
// But if name duplicate between series subType
|
// (for example: parallel) add component mainType,
|
// add suffix 'Series'.
|
let themeSubType = this.subType;
|
if ((ComponentModel as ComponentModelConstructor).hasClass(themeSubType)) {
|
themeSubType += 'Series';
|
}
|
zrUtil.merge(
|
option,
|
ecModel.getTheme().get(this.subType)
|
);
|
zrUtil.merge(option, this.getDefaultOption());
|
|
// Default label emphasis `show`
|
modelUtil.defaultEmphasis(option, 'label', ['show']);
|
|
this.fillDataTextStyle(option.data as ArrayLike<any>);
|
|
if (layoutMode) {
|
mergeLayoutParam(option as BoxLayoutOptionMixin, inputPositionParams, layoutMode);
|
}
|
}
|
|
mergeOption(newSeriesOption: Opt, ecModel: GlobalModel) {
|
// this.settingTask.dirty();
|
|
newSeriesOption = zrUtil.merge(this.option, newSeriesOption, true);
|
this.fillDataTextStyle(newSeriesOption.data as ArrayLike<any>);
|
|
const layoutMode = fetchLayoutMode(this);
|
if (layoutMode) {
|
mergeLayoutParam(
|
this.option as BoxLayoutOptionMixin,
|
newSeriesOption as BoxLayoutOptionMixin,
|
layoutMode
|
);
|
}
|
|
const sourceManager = inner(this).sourceManager;
|
sourceManager.dirty();
|
sourceManager.prepareSource();
|
|
const data = this.getInitialData(newSeriesOption, ecModel);
|
wrapData(data, this);
|
this.dataTask.dirty();
|
this.dataTask.context.data = data;
|
|
inner(this).dataBeforeProcessed = data;
|
|
autoSeriesName(this);
|
|
this._initSelectedMapFromData(data);
|
}
|
|
fillDataTextStyle(data: ArrayLike<any>): void {
|
// Default data label emphasis `show`
|
// FIXME Tree structure data ?
|
// FIXME Performance ?
|
if (data && !zrUtil.isTypedArray(data)) {
|
const props = ['show'];
|
for (let i = 0; i < data.length; i++) {
|
if (data[i] && data[i].label) {
|
modelUtil.defaultEmphasis(data[i], 'label', props);
|
}
|
}
|
}
|
}
|
|
/**
|
* Init a data structure from data related option in series
|
* Must be overriden.
|
*/
|
getInitialData(option: Opt, ecModel: GlobalModel): List {
|
return;
|
}
|
|
/**
|
* Append data to list
|
*/
|
appendData(params: {data: ArrayLike<any>}): void {
|
// FIXME ???
|
// (1) If data from dataset, forbidden append.
|
// (2) support append data of dataset.
|
const data = this.getRawData();
|
data.appendData(params.data);
|
}
|
|
/**
|
* Consider some method like `filter`, `map` need make new data,
|
* We should make sure that `seriesModel.getData()` get correct
|
* data in the stream procedure. So we fetch data from upstream
|
* each time `task.perform` called.
|
*/
|
getData(dataType?: SeriesDataType): List<this> {
|
const task = getCurrentTask(this);
|
if (task) {
|
const data = task.context.data;
|
return (dataType == null ? data : data.getLinkedData(dataType)) as List<this>;
|
}
|
else {
|
// When series is not alive (that may happen when click toolbox
|
// restore or setOption with not merge mode), series data may
|
// be still need to judge animation or something when graphic
|
// elements want to know whether fade out.
|
return inner(this).data as List<this>;
|
}
|
}
|
|
getAllData(): ({
|
data: List,
|
type?: SeriesDataType
|
})[] {
|
const mainData = this.getData();
|
return (mainData && mainData.getLinkedDataAll)
|
? mainData.getLinkedDataAll()
|
: [{ data: mainData }];
|
}
|
|
setData(data: List): void {
|
const task = getCurrentTask(this);
|
if (task) {
|
const context = task.context;
|
// Consider case: filter, data sample.
|
// FIXME:TS never used, so comment it
|
// if (context.data !== data && task.modifyOutputEnd) {
|
// task.setOutputEnd(data.count());
|
// }
|
context.outputData = data;
|
// Caution: setData should update context.data,
|
// Because getData may be called multiply in a
|
// single stage and expect to get the data just
|
// set. (For example, AxisProxy, x y both call
|
// getData and setDate sequentially).
|
// So the context.data should be fetched from
|
// upstream each time when a stage starts to be
|
// performed.
|
if (task !== this.dataTask) {
|
context.data = data;
|
}
|
}
|
inner(this).data = data;
|
}
|
|
getSource(): Source {
|
return inner(this).sourceManager.getSource();
|
}
|
|
/**
|
* Get data before processed
|
*/
|
getRawData(): List {
|
return inner(this).dataBeforeProcessed;
|
}
|
|
/**
|
* Get base axis if has coordinate system and has axis.
|
* By default use coordSys.getBaseAxis();
|
* Can be overrided for some chart.
|
* @return {type} description
|
*/
|
getBaseAxis(): Axis {
|
const coordSys = this.coordinateSystem;
|
// @ts-ignore
|
return coordSys && coordSys.getBaseAxis && coordSys.getBaseAxis();
|
}
|
|
/**
|
* Default tooltip formatter
|
*
|
* @param dataIndex
|
* @param multipleSeries
|
* @param dataType
|
* @param renderMode valid values: 'html'(by default) and 'richText'.
|
* 'html' is used for rendering tooltip in extra DOM form, and the result
|
* string is used as DOM HTML content.
|
* 'richText' is used for rendering tooltip in rich text form, for those where
|
* DOM operation is not supported.
|
* @return formatted tooltip with `html` and `markers`
|
* Notice: The override method can also return string
|
*/
|
formatTooltip(
|
dataIndex: number,
|
multipleSeries?: boolean,
|
dataType?: SeriesDataType
|
): ReturnType<DataFormatMixin['formatTooltip']> {
|
return defaultSeriesFormatTooltip({
|
series: this,
|
dataIndex: dataIndex,
|
multipleSeries: multipleSeries
|
});
|
}
|
|
isAnimationEnabled(): boolean {
|
if (env.node) {
|
return false;
|
}
|
let animationEnabled = this.getShallow('animation');
|
if (animationEnabled) {
|
if (this.getData().count() > this.getShallow('animationThreshold')) {
|
animationEnabled = false;
|
}
|
}
|
return !!animationEnabled;
|
}
|
|
restoreData() {
|
this.dataTask.dirty();
|
}
|
|
getColorFromPalette(name: string, scope: any, requestColorNum?: number): ZRColor {
|
const ecModel = this.ecModel;
|
// PENDING
|
let color = PaletteMixin.prototype.getColorFromPalette.call(this, name, scope, requestColorNum);
|
if (!color) {
|
color = ecModel.getColorFromPalette(name, scope, requestColorNum);
|
}
|
return color;
|
}
|
|
/**
|
* Use `data.mapDimensionsAll(coordDim)` instead.
|
* @deprecated
|
*/
|
coordDimToDataDim(coordDim: DimensionName): DimensionName[] {
|
return this.getRawData().mapDimensionsAll(coordDim);
|
}
|
|
/**
|
* Get progressive rendering count each step
|
*/
|
getProgressive(): number | false {
|
return this.get('progressive');
|
}
|
|
/**
|
* Get progressive rendering count each step
|
*/
|
getProgressiveThreshold(): number {
|
return this.get('progressiveThreshold');
|
}
|
|
// PENGING If selectedMode is null ?
|
select(innerDataIndices: number[], dataType?: SeriesDataType): void {
|
this._innerSelect(this.getData(dataType), innerDataIndices);
|
}
|
|
unselect(innerDataIndices: number[], dataType?: SeriesDataType): void {
|
const selectedMap = this.option.selectedMap;
|
if (!selectedMap) {
|
return;
|
}
|
const data = this.getData(dataType);
|
for (let i = 0; i < innerDataIndices.length; i++) {
|
const dataIndex = innerDataIndices[i];
|
const nameOrId = getSelectionKey(data, dataIndex);
|
selectedMap[nameOrId] = false;
|
this._selectedDataIndicesMap[nameOrId] = -1;
|
}
|
}
|
|
toggleSelect(innerDataIndices: number[], dataType?: SeriesDataType): void {
|
const tmpArr: number[] = [];
|
for (let i = 0; i < innerDataIndices.length; i++) {
|
tmpArr[0] = innerDataIndices[i];
|
this.isSelected(innerDataIndices[i], dataType)
|
? this.unselect(tmpArr, dataType)
|
: this.select(tmpArr, dataType);
|
}
|
}
|
|
getSelectedDataIndices(): number[] {
|
const selectedDataIndicesMap = this._selectedDataIndicesMap;
|
const nameOrIds = zrUtil.keys(selectedDataIndicesMap);
|
const dataIndices = [];
|
for (let i = 0; i < nameOrIds.length; i++) {
|
const dataIndex = selectedDataIndicesMap[nameOrIds[i]];
|
if (dataIndex >= 0) {
|
dataIndices.push(dataIndex);
|
}
|
}
|
return dataIndices;
|
}
|
|
isSelected(dataIndex: number, dataType?: SeriesDataType): boolean {
|
const selectedMap = this.option.selectedMap;
|
if (!selectedMap) {
|
return false;
|
}
|
|
const data = this.getData(dataType);
|
const nameOrId = getSelectionKey(data, dataIndex);
|
return selectedMap[nameOrId] || false;
|
}
|
|
private _innerSelect(data: List, innerDataIndices: number[]) {
|
const selectedMode = this.option.selectedMode;
|
const len = innerDataIndices.length;
|
if (!selectedMode || !len) {
|
return;
|
}
|
|
if (selectedMode === 'multiple') {
|
const selectedMap = this.option.selectedMap || (this.option.selectedMap = {});
|
for (let i = 0; i < len; i++) {
|
const dataIndex = innerDataIndices[i];
|
// TODO diffrent types of data share same object.
|
const nameOrId = getSelectionKey(data, dataIndex);
|
selectedMap[nameOrId] = true;
|
this._selectedDataIndicesMap[nameOrId] = data.getRawIndex(dataIndex);
|
}
|
}
|
else if (selectedMode === 'single' || selectedMode === true) {
|
const lastDataIndex = innerDataIndices[len - 1];
|
const nameOrId = getSelectionKey(data, lastDataIndex);
|
this.option.selectedMap = {
|
[nameOrId]: true
|
};
|
this._selectedDataIndicesMap = {
|
[nameOrId]: data.getRawIndex(lastDataIndex)
|
};
|
}
|
}
|
|
private _initSelectedMapFromData(data: List) {
|
// Ignore select info in data if selectedMap exists.
|
// NOTE It's only for legacy usage. edge data is not supported.
|
if (this.option.selectedMap) {
|
return;
|
}
|
|
const dataIndices: number[] = [];
|
if (data.hasItemOption) {
|
data.each(function (idx) {
|
const rawItem = data.getRawDataItem(idx);
|
if (rawItem && (rawItem as OptionDataItemObject<unknown>).selected) {
|
dataIndices.push(idx);
|
}
|
});
|
}
|
|
if (dataIndices.length > 0) {
|
this._innerSelect(data, dataIndices);
|
}
|
}
|
|
// /**
|
// * @see {module:echarts/stream/Scheduler}
|
// */
|
// abstract pipeTask: null
|
|
static registerClass(clz: Constructor): Constructor {
|
return ComponentModel.registerClass(clz);
|
}
|
}
|
|
interface SeriesModel<Opt extends SeriesOption = SeriesOption>
|
extends DataFormatMixin, PaletteMixin<Opt>, DataHost {
|
|
// methods that can be implemented optionally to provide to components
|
/**
|
* Get dimension to render shadow in dataZoom component
|
*/
|
getShadowDim?(): string
|
}
|
zrUtil.mixin(SeriesModel, DataFormatMixin);
|
zrUtil.mixin(SeriesModel, PaletteMixin);
|
|
export type SeriesModelConstructor = typeof SeriesModel & ExtendableConstructor;
|
mountExtend(SeriesModel, ComponentModel as SeriesModelConstructor);
|
|
|
/**
|
* MUST be called after `prepareSource` called
|
* Here we need to make auto series, especially for auto legend. But we
|
* do not modify series.name in option to avoid side effects.
|
*/
|
function autoSeriesName(seriesModel: SeriesModel): void {
|
// User specified name has higher priority, otherwise it may cause
|
// series can not be queried unexpectedly.
|
const name = seriesModel.name;
|
if (!modelUtil.isNameSpecified(seriesModel)) {
|
seriesModel.name = getSeriesAutoName(seriesModel) || name;
|
}
|
}
|
|
function getSeriesAutoName(seriesModel: SeriesModel): string {
|
const data = seriesModel.getRawData();
|
const dataDims = data.mapDimensionsAll('seriesName');
|
const nameArr: string[] = [];
|
zrUtil.each(dataDims, function (dataDim) {
|
const dimInfo = data.getDimensionInfo(dataDim);
|
dimInfo.displayName && nameArr.push(dimInfo.displayName);
|
});
|
return nameArr.join(' ');
|
}
|
|
function dataTaskCount(context: SeriesTaskContext): number {
|
return context.model.getRawData().count();
|
}
|
|
function dataTaskReset(context: SeriesTaskContext) {
|
const seriesModel = context.model;
|
seriesModel.setData(seriesModel.getRawData().cloneShallow());
|
return dataTaskProgress;
|
}
|
|
function dataTaskProgress(param: StageHandlerProgressParams, context: SeriesTaskContext): void {
|
// Avoid repead cloneShallow when data just created in reset.
|
if (context.outputData && param.end > context.outputData.count()) {
|
context.model.getRawData().cloneShallow(context.outputData);
|
}
|
}
|
|
// TODO refactor
|
function wrapData(data: List, seriesModel: SeriesModel): void {
|
zrUtil.each([...data.CHANGABLE_METHODS, ...data.DOWNSAMPLE_METHODS], function (methodName) {
|
data.wrapMethod(methodName as any, zrUtil.curry(onDataChange, seriesModel));
|
});
|
}
|
|
function onDataChange(this: List, seriesModel: SeriesModel, newList: List): List {
|
const task = getCurrentTask(seriesModel);
|
if (task) {
|
// Consider case: filter, selectRange
|
task.setOutputEnd((newList || this).count());
|
}
|
return newList;
|
}
|
|
function getCurrentTask(seriesModel: SeriesModel): GeneralTask {
|
const scheduler = (seriesModel.ecModel || {}).scheduler;
|
const pipeline = scheduler && scheduler.getPipeline(seriesModel.uid);
|
|
if (pipeline) {
|
// When pipline finished, the currrentTask keep the last
|
// task (renderTask).
|
let task = pipeline.currentTask;
|
if (task) {
|
const agentStubMap = (task as OverallTask).agentStubMap;
|
if (agentStubMap) {
|
task = agentStubMap.get(seriesModel.uid);
|
}
|
}
|
return task;
|
}
|
}
|
|
|
export default SeriesModel;
|