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