/* * 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; /** * [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); }