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. | |
*/ | |
// TODO | |
// ??? refactor? check the outer usage of data provider. | |
// merge with defaultDimValueGetter? | |
import {isTypedArray, extend, assert, each, isObject, bind} from 'zrender/src/core/util'; | |
import {getDataItemValue} from '../../util/model'; | |
import { createSourceFromSeriesDataOption, Source, isSourceInstance } from '../Source'; | |
import {ArrayLike, Dictionary} from 'zrender/src/core/types'; | |
import { | |
SOURCE_FORMAT_ORIGINAL, | |
SOURCE_FORMAT_OBJECT_ROWS, | |
SOURCE_FORMAT_KEYED_COLUMNS, | |
SOURCE_FORMAT_TYPED_ARRAY, | |
SOURCE_FORMAT_ARRAY_ROWS, | |
SERIES_LAYOUT_BY_COLUMN, | |
SERIES_LAYOUT_BY_ROW, | |
DimensionName, DimensionIndex, OptionSourceData, | |
OptionDataItem, OptionDataValue, SourceFormat, SeriesLayoutBy, ParsedValue, DimensionLoose, NullUndefined | |
} from '../../util/types'; | |
import SeriesData from '../SeriesData'; | |
export interface DataProvider { | |
/** | |
* true: all of the value are in primitive type (in type `OptionDataValue`). | |
* false: Not sure whether any of them is non primitive type (in type `OptionDataItemObject`). | |
* Like `data: [ { value: xx, itemStyle: {...} }, ...]` | |
* At present it only happen in `SOURCE_FORMAT_ORIGINAL`. | |
*/ | |
pure?: boolean; | |
/** | |
* If data is persistent and will not be released after use. | |
*/ | |
persistent?: boolean; | |
getSource(): Source; | |
count(): number; | |
getItem(idx: number, out?: OptionDataItem): OptionDataItem; | |
fillStorage?( | |
start: number, | |
end: number, | |
out: ArrayLike<ParsedValue>[], | |
extent: number[][] | |
): void | |
appendData?(newData: ArrayLike<OptionDataItem>): void; | |
clean?(): void; | |
} | |
let providerMethods: Dictionary<any>; | |
let mountMethods: (provider: DefaultDataProvider, data: OptionSourceData, source: Source) => void; | |
export interface DefaultDataProvider { | |
fillStorage?( | |
start: number, | |
end: number, | |
out: ArrayLike<ParsedValue>[], | |
extent: number[][] | |
): void | |
} | |
/** | |
* If normal array used, mutable chunk size is supported. | |
* If typed array used, chunk size must be fixed. | |
*/ | |
export class DefaultDataProvider implements DataProvider { | |
private _source: Source; | |
private _data: OptionSourceData; | |
private _offset: number; | |
private _dimSize: number; | |
pure: boolean; | |
persistent: boolean; | |
static protoInitialize = (function () { | |
// PENDING: To avoid potential incompat (e.g., prototype | |
// is visited somewhere), still init them on prototype. | |
const proto = DefaultDataProvider.prototype; | |
proto.pure = false; | |
proto.persistent = true; | |
})(); | |
constructor(sourceParam: Source | OptionSourceData, dimSize?: number) { | |
// let source: Source; | |
const source: Source = !isSourceInstance(sourceParam) | |
? createSourceFromSeriesDataOption(sourceParam as OptionSourceData) | |
: sourceParam as Source; | |
// declare source is Source; | |
this._source = source; | |
const data = this._data = source.data; | |
// Typed array. TODO IE10+? | |
if (source.sourceFormat === SOURCE_FORMAT_TYPED_ARRAY) { | |
if (__DEV__) { | |
if (dimSize == null) { | |
throw new Error('Typed array data must specify dimension size'); | |
} | |
} | |
this._offset = 0; | |
this._dimSize = dimSize; | |
this._data = data; | |
} | |
mountMethods(this, data, source); | |
} | |
getSource(): Source { | |
return this._source; | |
} | |
count(): number { | |
return 0; | |
} | |
getItem(idx: number, out?: ArrayLike<OptionDataValue>): OptionDataItem { | |
return; | |
} | |
appendData(newData: OptionSourceData): void { | |
} | |
clean(): void { | |
} | |
private static internalField = (function () { | |
mountMethods = function (provider, data, source) { | |
const sourceFormat = source.sourceFormat; | |
const seriesLayoutBy = source.seriesLayoutBy; | |
const startIndex = source.startIndex; | |
const dimsDef = source.dimensionsDefine; | |
const methods = providerMethods[getMethodMapKey(sourceFormat, seriesLayoutBy)]; | |
if (__DEV__) { | |
assert(methods, 'Invalide sourceFormat: ' + sourceFormat); | |
} | |
extend(provider, methods); | |
if (sourceFormat === SOURCE_FORMAT_TYPED_ARRAY) { | |
provider.getItem = getItemForTypedArray; | |
provider.count = countForTypedArray; | |
provider.fillStorage = fillStorageForTypedArray; | |
} | |
else { | |
const rawItemGetter = getRawSourceItemGetter(sourceFormat, seriesLayoutBy); | |
provider.getItem = bind(rawItemGetter, null, data, startIndex, dimsDef); | |
const rawCounter = getRawSourceDataCounter(sourceFormat, seriesLayoutBy); | |
provider.count = bind(rawCounter, null, data, startIndex, dimsDef); | |
} | |
}; | |
const getItemForTypedArray: DefaultDataProvider['getItem'] = function ( | |
this: DefaultDataProvider, idx: number, out: ArrayLike<number> | |
): ArrayLike<number> { | |
idx = idx - this._offset; | |
out = out || []; | |
const data = this._data; | |
const dimSize = this._dimSize; | |
const offset = dimSize * idx; | |
for (let i = 0; i < dimSize; i++) { | |
out[i] = (data as ArrayLike<number>)[offset + i]; | |
} | |
return out; | |
}; | |
const fillStorageForTypedArray: DefaultDataProvider['fillStorage'] = function ( | |
this: DefaultDataProvider, start: number, end: number, storage: ArrayLike<ParsedValue>[], extent: number[][] | |
) { | |
const data = this._data as ArrayLike<number>; | |
const dimSize = this._dimSize; | |
for (let dim = 0; dim < dimSize; dim++) { | |
const dimExtent = extent[dim]; | |
let min = dimExtent[0] == null ? Infinity : dimExtent[0]; | |
let max = dimExtent[1] == null ? -Infinity : dimExtent[1]; | |
const count = end - start; | |
const arr = storage[dim]; | |
for (let i = 0; i < count; i++) { | |
// appendData with TypedArray will always do replace in provider. | |
const val = data[i * dimSize + dim]; | |
arr[start + i] = val; | |
val < min && (min = val); | |
val > max && (max = val); | |
} | |
dimExtent[0] = min; | |
dimExtent[1] = max; | |
} | |
}; | |
const countForTypedArray: DefaultDataProvider['count'] = function ( | |
this: DefaultDataProvider | |
) { | |
return this._data ? ((this._data as ArrayLike<number>).length / this._dimSize) : 0; | |
}; | |
providerMethods = { | |
[SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_COLUMN]: { | |
pure: true, | |
appendData: appendDataSimply | |
}, | |
[SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_ROW]: { | |
pure: true, | |
appendData: function () { | |
throw new Error('Do not support appendData when set seriesLayoutBy: "row".'); | |
} | |
}, | |
[SOURCE_FORMAT_OBJECT_ROWS]: { | |
pure: true, | |
appendData: appendDataSimply | |
}, | |
[SOURCE_FORMAT_KEYED_COLUMNS]: { | |
pure: true, | |
appendData: function (this: DefaultDataProvider, newData: Dictionary<OptionDataValue[]>) { | |
const data = this._data as Dictionary<OptionDataValue[]>; | |
each(newData, function (newCol, key) { | |
const oldCol = data[key] || (data[key] = []); | |
for (let i = 0; i < (newCol || []).length; i++) { | |
oldCol.push(newCol[i]); | |
} | |
}); | |
} | |
}, | |
[SOURCE_FORMAT_ORIGINAL]: { | |
appendData: appendDataSimply | |
}, | |
[SOURCE_FORMAT_TYPED_ARRAY]: { | |
persistent: false, | |
pure: true, | |
appendData: function (this: DefaultDataProvider, newData: ArrayLike<number>): void { | |
if (__DEV__) { | |
assert( | |
isTypedArray(newData), | |
'Added data must be TypedArray if data in initialization is TypedArray' | |
); | |
} | |
this._data = newData; | |
}, | |
// Clean self if data is already used. | |
clean: function (this: DefaultDataProvider): void { | |
// PENDING | |
this._offset += this.count(); | |
this._data = null; | |
} | |
} | |
}; | |
function appendDataSimply(this: DefaultDataProvider, newData: ArrayLike<OptionDataItem>): void { | |
for (let i = 0; i < newData.length; i++) { | |
(this._data as any[]).push(newData[i]); | |
} | |
} | |
})(); | |
} | |
type RawSourceItemGetter = ( | |
rawData: OptionSourceData, | |
startIndex: number, | |
dimsDef: { name?: DimensionName }[], | |
idx: number, | |
// Only used in SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_ROW and SOURCE_FORMAT_KEYED_COLUMNS | |
// to avoid create a new [] if `out` is provided. | |
out?: ArrayLike<OptionDataValue> | |
) => OptionDataItem | ArrayLike<OptionDataValue>; | |
const getItemSimply: RawSourceItemGetter = function ( | |
rawData, startIndex, dimsDef, idx | |
): OptionDataItem { | |
return (rawData as [])[idx]; | |
}; | |
const rawSourceItemGetterMap: Dictionary<RawSourceItemGetter> = { | |
[SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_COLUMN]: function ( | |
rawData, startIndex, dimsDef, idx | |
) { | |
return (rawData as OptionDataValue[][])[idx + startIndex]; | |
}, | |
[SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_ROW]: function ( | |
rawData, startIndex, dimsDef, idx, out | |
) { | |
idx += startIndex; | |
const item = out || []; | |
const data = rawData as OptionDataValue[][]; | |
for (let i = 0; i < data.length; i++) { | |
const row = data[i]; | |
item[i] = row ? row[idx] : null; | |
} | |
return item; | |
}, | |
[SOURCE_FORMAT_OBJECT_ROWS]: getItemSimply, | |
[SOURCE_FORMAT_KEYED_COLUMNS]: function ( | |
rawData, startIndex, dimsDef, idx, out | |
) { | |
const item = out || []; | |
for (let i = 0; i < dimsDef.length; i++) { | |
const dimName = dimsDef[i].name; | |
if (__DEV__) { | |
if (dimName == null) { | |
throw new Error(); | |
} | |
} | |
const col = (rawData as Dictionary<OptionDataValue[]>)[dimName]; | |
item[i] = col ? col[idx] : null; | |
} | |
return item; | |
}, | |
[SOURCE_FORMAT_ORIGINAL]: getItemSimply | |
}; | |
export function getRawSourceItemGetter( | |
sourceFormat: SourceFormat, seriesLayoutBy: SeriesLayoutBy | |
): RawSourceItemGetter { | |
const method = rawSourceItemGetterMap[getMethodMapKey(sourceFormat, seriesLayoutBy)]; | |
if (__DEV__) { | |
assert(method, 'Do not support get item on "' + sourceFormat + '", "' + seriesLayoutBy + '".'); | |
} | |
return method; | |
} | |
type RawSourceDataCounter = ( | |
rawData: OptionSourceData, | |
startIndex: number, | |
dimsDef: { name?: DimensionName }[] | |
) => number; | |
const countSimply: RawSourceDataCounter = function ( | |
rawData, startIndex, dimsDef | |
) { | |
return (rawData as []).length; | |
}; | |
const rawSourceDataCounterMap: Dictionary<RawSourceDataCounter> = { | |
[SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_COLUMN]: function ( | |
rawData, startIndex, dimsDef | |
) { | |
return Math.max(0, (rawData as OptionDataItem[][]).length - startIndex); | |
}, | |
[SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_ROW]: function ( | |
rawData, startIndex, dimsDef | |
) { | |
const row = (rawData as OptionDataValue[][])[0]; | |
return row ? Math.max(0, row.length - startIndex) : 0; | |
}, | |
[SOURCE_FORMAT_OBJECT_ROWS]: countSimply, | |
[SOURCE_FORMAT_KEYED_COLUMNS]: function ( | |
rawData, startIndex, dimsDef | |
) { | |
const dimName = dimsDef[0].name; | |
if (__DEV__) { | |
if (dimName == null) { | |
throw new Error(); | |
} | |
} | |
const col = (rawData as Dictionary<OptionDataValue[]>)[dimName]; | |
return col ? col.length : 0; | |
}, | |
[SOURCE_FORMAT_ORIGINAL]: countSimply | |
}; | |
export function getRawSourceDataCounter( | |
sourceFormat: SourceFormat, seriesLayoutBy: SeriesLayoutBy | |
): RawSourceDataCounter { | |
const method = rawSourceDataCounterMap[getMethodMapKey(sourceFormat, seriesLayoutBy)]; | |
if (__DEV__) { | |
assert(method, 'Do not support count on "' + sourceFormat + '", "' + seriesLayoutBy + '".'); | |
} | |
return method; | |
} | |
type RawSourceValueGetter = ( | |
dataItem: OptionDataItem, | |
dimIndex: DimensionIndex, | |
property: DimensionName | |
) => OptionDataValue; | |
const getRawValueSimply = function ( | |
dataItem: ArrayLike<OptionDataValue>, dimIndex: number, property: string | |
): OptionDataValue { | |
return dataItem[dimIndex]; | |
}; | |
const rawSourceValueGetterMap: Partial<Record<SourceFormat, RawSourceValueGetter>> = { | |
[SOURCE_FORMAT_ARRAY_ROWS]: getRawValueSimply, | |
[SOURCE_FORMAT_OBJECT_ROWS]: function ( | |
dataItem: Dictionary<OptionDataValue>, dimIndex: number, property: string | |
): OptionDataValue { | |
return dataItem[property]; | |
}, | |
[SOURCE_FORMAT_KEYED_COLUMNS]: getRawValueSimply, | |
[SOURCE_FORMAT_ORIGINAL]: function ( | |
dataItem: OptionDataItem, dimIndex: number, property: string | |
): OptionDataValue { | |
// FIXME: In some case (markpoint in geo (geo-map.html)), | |
// dataItem is {coord: [...]} | |
const value = getDataItemValue(dataItem); | |
return !(value instanceof Array) | |
? value | |
: value[dimIndex]; | |
}, | |
[SOURCE_FORMAT_TYPED_ARRAY]: getRawValueSimply | |
}; | |
export function getRawSourceValueGetter(sourceFormat: SourceFormat): RawSourceValueGetter { | |
const method = rawSourceValueGetterMap[sourceFormat]; | |
if (__DEV__) { | |
assert(method, 'Do not support get value on "' + sourceFormat + '".'); | |
} | |
return method; | |
} | |
function getMethodMapKey(sourceFormat: SourceFormat, seriesLayoutBy: SeriesLayoutBy): string { | |
return sourceFormat === SOURCE_FORMAT_ARRAY_ROWS | |
? sourceFormat + '_' + seriesLayoutBy | |
: sourceFormat; | |
} | |
// ??? FIXME can these logic be more neat: getRawValue, getRawDataItem, | |
// Consider persistent. | |
// Caution: why use raw value to display on label or tooltip? | |
// A reason is to avoid format. For example time value we do not know | |
// how to format is expected. More over, if stack is used, calculated | |
// value may be 0.91000000001, which have brings trouble to display. | |
// TODO: consider how to treat null/undefined/NaN when display? | |
export function retrieveRawValue( | |
data: SeriesData, dataIndex: number, | |
// If dimIndex is null/undefined, return OptionDataItem. | |
// Otherwise, return OptionDataValue. | |
dim?: DimensionLoose | NullUndefined | |
): OptionDataValue | OptionDataItem { | |
if (!data) { | |
return; | |
} | |
// Consider data may be not persistent. | |
const dataItem = data.getRawDataItem(dataIndex); | |
if (dataItem == null) { | |
return; | |
} | |
const store = data.getStore(); | |
const sourceFormat = store.getSource().sourceFormat; | |
if (dim != null) { | |
const dimIndex = data.getDimensionIndex(dim); | |
const property = store.getDimensionProperty(dimIndex); | |
return getRawSourceValueGetter(sourceFormat)(dataItem, dimIndex, property); | |
} | |
else { | |
let result = dataItem; | |
if (sourceFormat === SOURCE_FORMAT_ORIGINAL) { | |
result = getDataItemValue(dataItem); | |
} | |
return result; | |
} | |
} | |
/** | |
* Compatible with some cases (in pie, map) like: | |
* data: [{name: 'xx', value: 5, selected: true}, ...] | |
* where only sourceFormat is 'original' and 'objectRows' supported. | |
* | |
* // TODO | |
* Supported detail options in data item when using 'arrayRows'. | |
* | |
* @param data | |
* @param dataIndex | |
* @param attr like 'selected' | |
*/ | |
export function retrieveRawAttr(data: SeriesData, dataIndex: number, attr: string): any { | |
if (!data) { | |
return; | |
} | |
const sourceFormat = data.getStore().getSource().sourceFormat; | |
if (sourceFormat !== SOURCE_FORMAT_ORIGINAL | |
&& sourceFormat !== SOURCE_FORMAT_OBJECT_ROWS | |
) { | |
return; | |
} | |
let dataItem = data.getRawDataItem(dataIndex); | |
if (sourceFormat === SOURCE_FORMAT_ORIGINAL && !isObject(dataItem)) { | |
dataItem = null; | |
} | |
if (dataItem) { | |
return (dataItem as Dictionary<OptionDataValue>)[attr]; | |
} | |
} | |