Spaces:
Running
Running
| /* | |
| * 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 { DatasetModel } from '../../component/dataset/install'; | |
| import SeriesModel from '../../model/Series'; | |
| import { | |
| setAsPrimitive, map, isTypedArray, assert, each, retrieve2 | |
| } from 'zrender/src/core/util'; | |
| import { SourceMetaRawOption, Source, createSource, cloneSourceShallow } from '../Source'; | |
| import { | |
| SeriesEncodableModel, OptionSourceData, | |
| SOURCE_FORMAT_TYPED_ARRAY, SOURCE_FORMAT_ORIGINAL, | |
| SourceFormat, SeriesLayoutBy, OptionSourceHeader, | |
| DimensionDefinitionLoose, Dictionary | |
| } from '../../util/types'; | |
| import { | |
| querySeriesUpstreamDatasetModel, queryDatasetUpstreamDatasetModels | |
| } from './sourceHelper'; | |
| import { applyDataTransform } from './transform'; | |
| import DataStore, { DataStoreDimensionDefine } from '../DataStore'; | |
| import { DefaultDataProvider } from './dataProvider'; | |
| import { SeriesDataSchema } from './SeriesDataSchema'; | |
| type DataStoreMap = Dictionary<DataStore>; | |
| /** | |
| * [REQUIREMENT_MEMO]: | |
| * (0) `metaRawOption` means `dimensions`/`sourceHeader`/`seriesLayoutBy` in raw option. | |
| * (1) Keep support the feature: `metaRawOption` can be specified both on `series` and | |
| * `root-dataset`. Them on `series` has higher priority. | |
| * (2) Do not support to set `metaRawOption` on a `non-root-dataset`, because it might | |
| * confuse users: whether those props indicate how to visit the upstream source or visit | |
| * the transform result source, and some transforms has nothing to do with these props, | |
| * and some transforms might have multiple upstream. | |
| * (3) Transforms should specify `metaRawOption` in each output, just like they can be | |
| * declared in `root-dataset`. | |
| * (4) At present only support visit source in `SERIES_LAYOUT_BY_COLUMN` in transforms. | |
| * That is for reducing complexity in transforms. | |
| * PENDING: Whether to provide transposition transform? | |
| * | |
| * [IMPLEMENTAION_MEMO]: | |
| * "sourceVisitConfig" are calculated from `metaRawOption` and `data`. | |
| * They will not be calculated until `source` is about to be visited (to prevent from | |
| * duplicate calcuation). `source` is visited only in series and input to transforms. | |
| * | |
| * [DIMENSION_INHERIT_RULE]: | |
| * By default the dimensions are inherited from ancestors, unless a transform return | |
| * a new dimensions definition. | |
| * Consider the case: | |
| * ```js | |
| * dataset: [{ | |
| * source: [ ['Product', 'Sales', 'Prise'], ['Cookies', 321, 44.21], ...] | |
| * }, { | |
| * transform: { type: 'filter', ... } | |
| * }] | |
| * dataset: [{ | |
| * dimension: ['Product', 'Sales', 'Prise'], | |
| * source: [ ['Cookies', 321, 44.21], ...] | |
| * }, { | |
| * transform: { type: 'filter', ... } | |
| * }] | |
| * ``` | |
| * The two types of option should have the same behavior after transform. | |
| * | |
| * | |
| * [SCENARIO]: | |
| * (1) Provide source data directly: | |
| * ```js | |
| * series: { | |
| * encode: {...}, | |
| * dimensions: [...] | |
| * seriesLayoutBy: 'row', | |
| * data: [[...]] | |
| * } | |
| * ``` | |
| * (2) Series refer to dataset. | |
| * ```js | |
| * series: [{ | |
| * encode: {...} | |
| * // Ignore datasetIndex means `datasetIndex: 0` | |
| * // and the dimensions defination in dataset is used | |
| * }, { | |
| * encode: {...}, | |
| * seriesLayoutBy: 'column', | |
| * datasetIndex: 1 | |
| * }] | |
| * ``` | |
| * (3) dataset transform | |
| * ```js | |
| * dataset: [{ | |
| * source: [...] | |
| * }, { | |
| * source: [...] | |
| * }, { | |
| * // By default from 0. | |
| * transform: { type: 'filter', config: {...} } | |
| * }, { | |
| * // Piped. | |
| * transform: [ | |
| * { type: 'filter', config: {...} }, | |
| * { type: 'sort', config: {...} } | |
| * ] | |
| * }, { | |
| * id: 'regressionData', | |
| * fromDatasetIndex: 1, | |
| * // Third-party transform | |
| * transform: { type: 'ecStat:regression', config: {...} } | |
| * }, { | |
| * // retrieve the extra result. | |
| * id: 'regressionFormula', | |
| * fromDatasetId: 'regressionData', | |
| * fromTransformResult: 1 | |
| * }] | |
| * ``` | |
| */ | |
| export class SourceManager { | |
| // Currently only datasetModel can host `transform` | |
| private _sourceHost: DatasetModel | SeriesModel; | |
| // Cached source. Do not repeat calculating if not dirty. | |
| private _sourceList: Source[] = []; | |
| private _storeList: DataStoreMap[] = []; | |
| // version sign of each upstream source manager. | |
| private _upstreamSignList: string[] = []; | |
| private _versionSignBase = 0; | |
| private _dirty = true; | |
| constructor(sourceHost: DatasetModel | SeriesModel) { | |
| this._sourceHost = sourceHost; | |
| } | |
| /** | |
| * Mark dirty. | |
| */ | |
| dirty() { | |
| this._setLocalSource([], []); | |
| this._storeList = []; | |
| this._dirty = true; | |
| } | |
| private _setLocalSource( | |
| sourceList: Source[], | |
| upstreamSignList: string[] | |
| ): void { | |
| this._sourceList = sourceList; | |
| this._upstreamSignList = upstreamSignList; | |
| this._versionSignBase++; | |
| if (this._versionSignBase > 9e10) { | |
| this._versionSignBase = 0; | |
| } | |
| } | |
| /** | |
| * For detecting whether the upstream source is dirty, so that | |
| * the local cached source (in `_sourceList`) should be discarded. | |
| */ | |
| private _getVersionSign(): string { | |
| return this._sourceHost.uid + '_' + this._versionSignBase; | |
| } | |
| /** | |
| * Always return a source instance. Otherwise throw error. | |
| */ | |
| prepareSource(): void { | |
| // For the case that call `setOption` multiple time but no data changed, | |
| // cache the result source to prevent from repeating transform. | |
| if (this._isDirty()) { | |
| this._createSource(); | |
| this._dirty = false; | |
| } | |
| } | |
| private _createSource(): void { | |
| this._setLocalSource([], []); | |
| const sourceHost = this._sourceHost; | |
| const upSourceMgrList = this._getUpstreamSourceManagers(); | |
| const hasUpstream = !!upSourceMgrList.length; | |
| let resultSourceList: Source[]; | |
| let upstreamSignList: string[]; | |
| if (isSeries(sourceHost)) { | |
| const seriesModel = sourceHost as SeriesEncodableModel; | |
| let data; | |
| let sourceFormat: SourceFormat; | |
| let upSource: Source; | |
| // Has upstream dataset | |
| if (hasUpstream) { | |
| const upSourceMgr = upSourceMgrList[0]; | |
| upSourceMgr.prepareSource(); | |
| upSource = upSourceMgr.getSource(); | |
| data = upSource.data; | |
| sourceFormat = upSource.sourceFormat; | |
| upstreamSignList = [upSourceMgr._getVersionSign()]; | |
| } | |
| // Series data is from own. | |
| else { | |
| data = seriesModel.get('data', true) as OptionSourceData; | |
| sourceFormat = isTypedArray(data) | |
| ? SOURCE_FORMAT_TYPED_ARRAY : SOURCE_FORMAT_ORIGINAL; | |
| upstreamSignList = []; | |
| } | |
| // See [REQUIREMENT_MEMO], merge settings on series and parent dataset if it is root. | |
| const newMetaRawOption = this._getSourceMetaRawOption() || {} as SourceMetaRawOption; | |
| const upMetaRawOption = upSource && upSource.metaRawOption || {} as SourceMetaRawOption; | |
| const seriesLayoutBy = retrieve2(newMetaRawOption.seriesLayoutBy, upMetaRawOption.seriesLayoutBy) || null; | |
| const sourceHeader = retrieve2(newMetaRawOption.sourceHeader, upMetaRawOption.sourceHeader); | |
| // Note here we should not use `upSource.dimensionsDefine`. Consider the case: | |
| // `upSource.dimensionsDefine` is detected by `seriesLayoutBy: 'column'`, | |
| // but series need `seriesLayoutBy: 'row'`. | |
| const dimensions = retrieve2(newMetaRawOption.dimensions, upMetaRawOption.dimensions); | |
| // We share source with dataset as much as possible | |
| // to avoid extra memory cost of high dimensional data. | |
| const needsCreateSource = seriesLayoutBy !== upMetaRawOption.seriesLayoutBy | |
| || !!sourceHeader !== !!upMetaRawOption.sourceHeader | |
| || dimensions; | |
| resultSourceList = needsCreateSource ? [createSource( | |
| data, | |
| { seriesLayoutBy, sourceHeader, dimensions }, | |
| sourceFormat | |
| )] : []; | |
| } | |
| else { | |
| const datasetModel = sourceHost as DatasetModel; | |
| // Has upstream dataset. | |
| if (hasUpstream) { | |
| const result = this._applyTransform(upSourceMgrList); | |
| resultSourceList = result.sourceList; | |
| upstreamSignList = result.upstreamSignList; | |
| } | |
| // Is root dataset. | |
| else { | |
| const sourceData = datasetModel.get('source', true); | |
| resultSourceList = [createSource( | |
| sourceData, | |
| this._getSourceMetaRawOption(), | |
| null | |
| )]; | |
| upstreamSignList = []; | |
| } | |
| } | |
| if (__DEV__) { | |
| assert(resultSourceList && upstreamSignList); | |
| } | |
| this._setLocalSource(resultSourceList, upstreamSignList); | |
| } | |
| private _applyTransform( | |
| upMgrList: SourceManager[] | |
| ): { | |
| sourceList: Source[], | |
| upstreamSignList: string[] | |
| } { | |
| const datasetModel = this._sourceHost as DatasetModel; | |
| const transformOption = datasetModel.get('transform', true); | |
| const fromTransformResult = datasetModel.get('fromTransformResult', true); | |
| if (__DEV__) { | |
| assert(fromTransformResult != null || transformOption != null); | |
| } | |
| if (fromTransformResult != null) { | |
| let errMsg = ''; | |
| if (upMgrList.length !== 1) { | |
| if (__DEV__) { | |
| errMsg = 'When using `fromTransformResult`, there should be only one upstream dataset'; | |
| } | |
| doThrow(errMsg); | |
| } | |
| } | |
| let sourceList: Source[]; | |
| const upSourceList: Source[] = []; | |
| const upstreamSignList: string[] = []; | |
| each(upMgrList, upMgr => { | |
| upMgr.prepareSource(); | |
| const upSource = upMgr.getSource(fromTransformResult || 0); | |
| let errMsg = ''; | |
| if (fromTransformResult != null && !upSource) { | |
| if (__DEV__) { | |
| errMsg = 'Can not retrieve result by `fromTransformResult`: ' + fromTransformResult; | |
| } | |
| doThrow(errMsg); | |
| } | |
| upSourceList.push(upSource); | |
| upstreamSignList.push(upMgr._getVersionSign()); | |
| }); | |
| if (transformOption) { | |
| sourceList = applyDataTransform( | |
| transformOption, | |
| upSourceList, | |
| { datasetIndex: datasetModel.componentIndex } | |
| ); | |
| } | |
| else if (fromTransformResult != null) { | |
| sourceList = [cloneSourceShallow(upSourceList[0])]; | |
| } | |
| return { sourceList, upstreamSignList }; | |
| } | |
| private _isDirty(): boolean { | |
| if (this._dirty) { | |
| return true; | |
| } | |
| // All sourceList is from the some upstream. | |
| const upSourceMgrList = this._getUpstreamSourceManagers(); | |
| for (let i = 0; i < upSourceMgrList.length; i++) { | |
| const upSrcMgr = upSourceMgrList[i]; | |
| if ( | |
| // Consider the case that there is ancestor diry, call it recursively. | |
| // The performance is probably not an issue because usually the chain is not long. | |
| upSrcMgr._isDirty() | |
| || this._upstreamSignList[i] !== upSrcMgr._getVersionSign() | |
| ) { | |
| return true; | |
| } | |
| } | |
| } | |
| /** | |
| * @param sourceIndex By default 0, means "main source". | |
| * In most cases there is only one source. | |
| */ | |
| getSource(sourceIndex?: number): Source { | |
| sourceIndex = sourceIndex || 0; | |
| const source = this._sourceList[sourceIndex]; | |
| if (!source) { | |
| // Series may share source instance with dataset. | |
| const upSourceMgrList = this._getUpstreamSourceManagers(); | |
| return upSourceMgrList[0] | |
| && upSourceMgrList[0].getSource(sourceIndex); | |
| } | |
| return source; | |
| } | |
| /** | |
| * | |
| * Get a data store which can be shared across series. | |
| * Only available for series. | |
| * | |
| * @param seriesDimRequest Dimensions that are generated in series. | |
| * Should have been sorted by `storeDimIndex` asc. | |
| */ | |
| getSharedDataStore(seriesDimRequest: SeriesDataSchema): DataStore { | |
| if (__DEV__) { | |
| assert(isSeries(this._sourceHost), 'Can only call getDataStore on series source manager.'); | |
| } | |
| const schema = seriesDimRequest.makeStoreSchema(); | |
| return this._innerGetDataStore( | |
| schema.dimensions, seriesDimRequest.source, schema.hash | |
| ); | |
| } | |
| private _innerGetDataStore( | |
| storeDims: DataStoreDimensionDefine[], | |
| seriesSource: Source, | |
| sourceReadKey: string | |
| ): DataStore | undefined { | |
| // TODO Can use other sourceIndex? | |
| const sourceIndex = 0; | |
| const storeList = this._storeList; | |
| let cachedStoreMap = storeList[sourceIndex]; | |
| if (!cachedStoreMap) { | |
| cachedStoreMap = storeList[sourceIndex] = {}; | |
| } | |
| let cachedStore = cachedStoreMap[sourceReadKey]; | |
| if (!cachedStore) { | |
| const upSourceMgr = this._getUpstreamSourceManagers()[0]; | |
| if (isSeries(this._sourceHost) && upSourceMgr) { | |
| cachedStore = upSourceMgr._innerGetDataStore( | |
| storeDims, seriesSource, sourceReadKey | |
| ); | |
| } | |
| else { | |
| cachedStore = new DataStore(); | |
| // Always create store from source of series. | |
| cachedStore.initData( | |
| new DefaultDataProvider(seriesSource, storeDims.length), | |
| storeDims | |
| ); | |
| } | |
| cachedStoreMap[sourceReadKey] = cachedStore; | |
| } | |
| return cachedStore; | |
| } | |
| /** | |
| * PENDING: Is it fast enough? | |
| * If no upstream, return empty array. | |
| */ | |
| private _getUpstreamSourceManagers(): SourceManager[] { | |
| // Always get the relationship from the raw option. | |
| // Do not cache the link of the dependency graph, so that | |
| // there is no need to update them when change happens. | |
| const sourceHost = this._sourceHost; | |
| if (isSeries(sourceHost)) { | |
| const datasetModel = querySeriesUpstreamDatasetModel(sourceHost); | |
| return !datasetModel ? [] : [datasetModel.getSourceManager()]; | |
| } | |
| else { | |
| return map( | |
| queryDatasetUpstreamDatasetModels(sourceHost as DatasetModel), | |
| datasetModel => datasetModel.getSourceManager() | |
| ); | |
| } | |
| } | |
| private _getSourceMetaRawOption(): SourceMetaRawOption { | |
| const sourceHost = this._sourceHost; | |
| let seriesLayoutBy: SeriesLayoutBy; | |
| let sourceHeader: OptionSourceHeader; | |
| let dimensions: DimensionDefinitionLoose[]; | |
| if (isSeries(sourceHost)) { | |
| seriesLayoutBy = sourceHost.get('seriesLayoutBy', true); | |
| sourceHeader = sourceHost.get('sourceHeader', true); | |
| dimensions = sourceHost.get('dimensions', true); | |
| } | |
| // See [REQUIREMENT_MEMO], `non-root-dataset` do not support them. | |
| else if (!this._getUpstreamSourceManagers().length) { | |
| const model = sourceHost as DatasetModel; | |
| seriesLayoutBy = model.get('seriesLayoutBy', true); | |
| sourceHeader = model.get('sourceHeader', true); | |
| dimensions = model.get('dimensions', true); | |
| } | |
| return { seriesLayoutBy, sourceHeader, dimensions }; | |
| } | |
| } | |
| // Call this method after `super.init` and `super.mergeOption` to | |
| // disable the transform merge, but do not disable transform clone from rawOption. | |
| export function disableTransformOptionMerge(datasetModel: DatasetModel): void { | |
| const transformOption = datasetModel.option.transform; | |
| transformOption && setAsPrimitive(datasetModel.option.transform); | |
| } | |
| function isSeries(sourceHost: SourceManager['_sourceHost']): sourceHost is SeriesEncodableModel { | |
| // Avoid circular dependency with Series.ts | |
| return (sourceHost as SeriesModel).mainType === 'series'; | |
| } | |
| function doThrow(errMsg: string): void { | |
| throw new Error(errMsg); | |
| } | |