blob: 94a60fb871369c153e87b55711a1dc921c69df66 [file] [log] [blame]
/*
* 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,
DimensionIndexLoose, OptionDataItem, OptionDataValue, SourceFormat, SeriesLayoutBy, ParsedValue
} from '../../util/types';
import List from '../List';
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<number>): 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
) => OptionDataItem;
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
): OptionDataValue[] {
return (rawData as OptionDataValue[][])[idx + startIndex];
},
[SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_ROW]: function (
rawData, startIndex, dimsDef, idx
): OptionDataValue[] {
idx += startIndex;
const item = [];
const data = rawData as OptionDataValue[][];
for (let i = 0; i < data.length; i++) {
const row = data[i];
item.push(row ? row[idx] : null);
}
return item;
},
[SOURCE_FORMAT_OBJECT_ROWS]: getItemSimply,
[SOURCE_FORMAT_KEYED_COLUMNS]: function (
rawData, startIndex, dimsDef, idx
): OptionDataValue[] {
const item = [];
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.push(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 suppport count on "' + sourceFormat + '", "' + seriesLayoutBy + '".');
}
return method;
}
// TODO
// merge it to dataProvider?
type RawSourceValueGetter = (
dataItem: OptionDataItem,
dimIndex: DimensionIndex,
dimName: DimensionName
// If dimIndex is null/undefined, return OptionDataItem.
// Otherwise, return OptionDataValue.
) => OptionDataValue | OptionDataItem;
const getRawValueSimply = function (
dataItem: ArrayLike<OptionDataValue>, dimIndex: number, dimName: string
): OptionDataValue | ArrayLike<OptionDataValue> {
return dimIndex != null ? dataItem[dimIndex] : dataItem;
};
const rawSourceValueGetterMap: {[sourceFormat: string]: RawSourceValueGetter} = {
[SOURCE_FORMAT_ARRAY_ROWS]: getRawValueSimply,
[SOURCE_FORMAT_OBJECT_ROWS]: function (
dataItem: Dictionary<OptionDataValue>, dimIndex: number, dimName: string
): OptionDataValue | Dictionary<OptionDataValue> {
return dimIndex != null ? dataItem[dimName] : dataItem;
},
[SOURCE_FORMAT_KEYED_COLUMNS]: getRawValueSimply,
[SOURCE_FORMAT_ORIGINAL]: function (
dataItem: OptionDataItem, dimIndex: number, dimName: string
): OptionDataValue | OptionDataItem {
// FIXME: In some case (markpoint in geo (geo-map.html)),
// dataItem is {coord: [...]}
const value = getDataItemValue(dataItem);
return (dimIndex == null || !(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 suppport 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: List, dataIndex: number, dim?: DimensionName | DimensionIndexLoose
// If dimIndex is null/undefined, return OptionDataItem.
// Otherwise, return OptionDataValue.
): OptionDataValue | OptionDataItem {
if (!data) {
return;
}
// Consider data may be not persistent.
const dataItem = data.getRawDataItem(dataIndex);
if (dataItem == null) {
return;
}
const sourceFormat = data.getProvider().getSource().sourceFormat;
let dimName;
let dimIndex;
const dimInfo = data.getDimensionInfo(dim);
if (dimInfo) {
dimName = dimInfo.name;
dimIndex = dimInfo.index;
}
return getRawSourceValueGetter(sourceFormat)(dataItem, dimIndex, dimName);
}
/**
* 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: List, dataIndex: number, attr: string): any {
if (!data) {
return;
}
const sourceFormat = data.getProvider().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];
}
}