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 { | |
isTypedArray, HashMap, clone, createHashMap, isArray, isObject, isArrayLike, | |
hasOwn, assert, each, map, isNumber, isString, keys | |
} from 'zrender/src/core/util'; | |
import { | |
SourceFormat, SeriesLayoutBy, DimensionDefinition, | |
OptionEncodeValue, OptionSourceData, | |
SOURCE_FORMAT_ORIGINAL, | |
SERIES_LAYOUT_BY_COLUMN, | |
SOURCE_FORMAT_UNKNOWN, | |
SOURCE_FORMAT_KEYED_COLUMNS, | |
SOURCE_FORMAT_TYPED_ARRAY, | |
DimensionName, | |
OptionSourceHeader, | |
DimensionDefinitionLoose, | |
SOURCE_FORMAT_ARRAY_ROWS, | |
SOURCE_FORMAT_OBJECT_ROWS, | |
Dictionary, | |
OptionSourceDataObjectRows, | |
OptionDataValue, | |
OptionSourceDataArrayRows, | |
SERIES_LAYOUT_BY_ROW, | |
OptionSourceDataOriginal, | |
OptionSourceDataKeyedColumns | |
} from '../util/types'; | |
import { DatasetOption } from '../component/dataset/install'; | |
import { getDataItemValue } from '../util/model'; | |
import { BE_ORDINAL, guessOrdinal } from './helper/sourceHelper'; | |
/** | |
* [sourceFormat] | |
* | |
* + "original": | |
* This format is only used in series.data, where | |
* itemStyle can be specified in data item. | |
* | |
* + "arrayRows": | |
* [ | |
* ['product', 'score', 'amount'], | |
* ['Matcha Latte', 89.3, 95.8], | |
* ['Milk Tea', 92.1, 89.4], | |
* ['Cheese Cocoa', 94.4, 91.2], | |
* ['Walnut Brownie', 85.4, 76.9] | |
* ] | |
* | |
* + "objectRows": | |
* [ | |
* {product: 'Matcha Latte', score: 89.3, amount: 95.8}, | |
* {product: 'Milk Tea', score: 92.1, amount: 89.4}, | |
* {product: 'Cheese Cocoa', score: 94.4, amount: 91.2}, | |
* {product: 'Walnut Brownie', score: 85.4, amount: 76.9} | |
* ] | |
* | |
* + "keyedColumns": | |
* { | |
* 'product': ['Matcha Latte', 'Milk Tea', 'Cheese Cocoa', 'Walnut Brownie'], | |
* 'count': [823, 235, 1042, 988], | |
* 'score': [95.8, 81.4, 91.2, 76.9] | |
* } | |
* | |
* + "typedArray" | |
* | |
* + "unknown" | |
*/ | |
export interface SourceMetaRawOption { | |
seriesLayoutBy: SeriesLayoutBy; | |
sourceHeader: OptionSourceHeader; | |
dimensions: DimensionDefinitionLoose[]; | |
} | |
// Prevent from `new Source()` external and circular reference. | |
export interface Source extends SourceImpl {}; | |
// @inner | |
class SourceImpl { | |
/** | |
* Not null/undefined. | |
*/ | |
readonly data: OptionSourceData; | |
/** | |
* See also "detectSourceFormat". | |
* Not null/undefined. | |
*/ | |
readonly sourceFormat: SourceFormat; | |
/** | |
* 'row' or 'column' | |
* Not null/undefined. | |
*/ | |
readonly seriesLayoutBy: SeriesLayoutBy; | |
/** | |
* dimensions definition from: | |
* (1) standalone defined in option prop `dimensions: [...]` | |
* (2) detected from option data. See `determineSourceDimensions`. | |
* If can not be detected (e.g., there is only pure data `[[11, 33], ...]` | |
* `dimensionsDefine` will be null/undefined. | |
*/ | |
readonly dimensionsDefine: DimensionDefinition[]; | |
/** | |
* Only make sense in `SOURCE_FORMAT_ARRAY_ROWS`. | |
* That is the same as `sourceHeader: number`, | |
* which means from which line the real data start. | |
* Not null/undefined, uint. | |
*/ | |
readonly startIndex: number; | |
/** | |
* Dimension count detected from data. Only works when `dimensionDefine` | |
* does not exists. | |
* Can be null/undefined (when unknown), uint. | |
*/ | |
readonly dimensionsDetectedCount: number; | |
/** | |
* Raw props from user option. | |
*/ | |
readonly metaRawOption: SourceMetaRawOption; | |
constructor(fields: { | |
data: OptionSourceData, | |
sourceFormat: SourceFormat, // default: SOURCE_FORMAT_UNKNOWN | |
// Visit config are optional: | |
seriesLayoutBy?: SeriesLayoutBy, // default: 'column' | |
dimensionsDefine?: DimensionDefinition[], | |
startIndex?: number, // default: 0 | |
dimensionsDetectedCount?: number, | |
metaRawOption?: SourceMetaRawOption, | |
// [Caveat] | |
// This is the raw user defined `encode` in `series`. | |
// If user not defined, DO NOT make a empty object or hashMap here. | |
// An empty object or hashMap will prevent from auto generating encode. | |
encodeDefine?: HashMap<OptionEncodeValue, DimensionName> | |
}) { | |
this.data = fields.data || ( | |
fields.sourceFormat === SOURCE_FORMAT_KEYED_COLUMNS ? {} : [] | |
); | |
this.sourceFormat = fields.sourceFormat || SOURCE_FORMAT_UNKNOWN; | |
// Visit config | |
this.seriesLayoutBy = fields.seriesLayoutBy || SERIES_LAYOUT_BY_COLUMN; | |
this.startIndex = fields.startIndex || 0; | |
this.dimensionsDetectedCount = fields.dimensionsDetectedCount; | |
this.metaRawOption = fields.metaRawOption; | |
const dimensionsDefine = this.dimensionsDefine = fields.dimensionsDefine; | |
if (dimensionsDefine) { | |
for (let i = 0; i < dimensionsDefine.length; i++) { | |
const dim = dimensionsDefine[i]; | |
if (dim.type == null) { | |
if (guessOrdinal(this, i) === BE_ORDINAL.Must) { | |
dim.type = 'ordinal'; | |
} | |
} | |
} | |
} | |
} | |
} | |
export function isSourceInstance(val: unknown): val is Source { | |
return val instanceof SourceImpl; | |
} | |
/** | |
* Create a source from option. | |
* NOTE: Created source is immutable. Don't change any properties in it. | |
*/ | |
export function createSource( | |
sourceData: OptionSourceData, | |
thisMetaRawOption: SourceMetaRawOption, | |
// can be null. If not provided, auto detect it from `sourceData`. | |
sourceFormat: SourceFormat | |
): Source { | |
sourceFormat = sourceFormat || detectSourceFormat(sourceData); | |
const seriesLayoutBy = thisMetaRawOption.seriesLayoutBy; | |
const determined = determineSourceDimensions( | |
sourceData, | |
sourceFormat, | |
seriesLayoutBy, | |
thisMetaRawOption.sourceHeader, | |
thisMetaRawOption.dimensions | |
); | |
const source = new SourceImpl({ | |
data: sourceData, | |
sourceFormat: sourceFormat, | |
seriesLayoutBy: seriesLayoutBy, | |
dimensionsDefine: determined.dimensionsDefine, | |
startIndex: determined.startIndex, | |
dimensionsDetectedCount: determined.dimensionsDetectedCount, | |
metaRawOption: clone(thisMetaRawOption) | |
}); | |
return source; | |
} | |
/** | |
* Wrap original series data for some compatibility cases. | |
*/ | |
export function createSourceFromSeriesDataOption(data: OptionSourceData): Source { | |
return new SourceImpl({ | |
data: data, | |
sourceFormat: isTypedArray(data) | |
? SOURCE_FORMAT_TYPED_ARRAY | |
: SOURCE_FORMAT_ORIGINAL | |
}); | |
} | |
/** | |
* Clone source but excludes source data. | |
*/ | |
export function cloneSourceShallow(source: Source): Source { | |
return new SourceImpl({ | |
data: source.data, | |
sourceFormat: source.sourceFormat, | |
seriesLayoutBy: source.seriesLayoutBy, | |
dimensionsDefine: clone(source.dimensionsDefine), | |
startIndex: source.startIndex, | |
dimensionsDetectedCount: source.dimensionsDetectedCount | |
}); | |
} | |
/** | |
* Note: An empty array will be detected as `SOURCE_FORMAT_ARRAY_ROWS`. | |
*/ | |
export function detectSourceFormat(data: DatasetOption['source']): SourceFormat { | |
let sourceFormat: SourceFormat = SOURCE_FORMAT_UNKNOWN; | |
if (isTypedArray(data)) { | |
sourceFormat = SOURCE_FORMAT_TYPED_ARRAY; | |
} | |
else if (isArray(data)) { | |
// FIXME Whether tolerate null in top level array? | |
if (data.length === 0) { | |
sourceFormat = SOURCE_FORMAT_ARRAY_ROWS; | |
} | |
for (let i = 0, len = data.length; i < len; i++) { | |
const item = data[i]; | |
if (item == null) { | |
continue; | |
} | |
else if (isArray(item) || isTypedArray(item)) { | |
sourceFormat = SOURCE_FORMAT_ARRAY_ROWS; | |
break; | |
} | |
else if (isObject(item)) { | |
sourceFormat = SOURCE_FORMAT_OBJECT_ROWS; | |
break; | |
} | |
} | |
} | |
else if (isObject(data)) { | |
for (const key in data) { | |
if (hasOwn(data, key) && isArrayLike((data as Dictionary<unknown>)[key])) { | |
sourceFormat = SOURCE_FORMAT_KEYED_COLUMNS; | |
break; | |
} | |
} | |
} | |
return sourceFormat; | |
} | |
/** | |
* Determine the source definitions from data standalone dimensions definitions | |
* are not specified. | |
*/ | |
function determineSourceDimensions( | |
data: OptionSourceData, | |
sourceFormat: SourceFormat, | |
seriesLayoutBy: SeriesLayoutBy, | |
sourceHeader: OptionSourceHeader, | |
// standalone raw dimensions definition, like: | |
// { | |
// dimensions: ['aa', 'bb', { name: 'cc', type: 'time' }] | |
// } | |
// in `dataset` or `series` | |
dimensionsDefine: DimensionDefinitionLoose[] | |
): { | |
// If the input `dimensionsDefine` is specified, return it. | |
// Else determine dimensions from the input `data`. | |
// If not determined, `dimensionsDefine` will be null/undefined. | |
dimensionsDefine: Source['dimensionsDefine']; | |
startIndex: Source['startIndex']; | |
dimensionsDetectedCount: Source['dimensionsDetectedCount']; | |
} { | |
let dimensionsDetectedCount; | |
let startIndex: number; | |
// PENDING: Could data be null/undefined here? | |
// currently, if `dataset.source` not specified, error thrown. | |
// if `series.data` not specified, nothing rendered without error thrown. | |
// Should test these cases. | |
if (!data) { | |
return { | |
dimensionsDefine: normalizeDimensionsOption(dimensionsDefine), | |
startIndex, | |
dimensionsDetectedCount | |
}; | |
} | |
if (sourceFormat === SOURCE_FORMAT_ARRAY_ROWS) { | |
const dataArrayRows = data as OptionSourceDataArrayRows; | |
// Rule: Most of the first line are string: it is header. | |
// Caution: consider a line with 5 string and 1 number, | |
// it still can not be sure it is a head, because the | |
// 5 string may be 5 values of category columns. | |
if (sourceHeader === 'auto' || sourceHeader == null) { | |
arrayRowsTravelFirst(function (val) { | |
// '-' is regarded as null/undefined. | |
if (val != null && val !== '-') { | |
if (isString(val)) { | |
startIndex == null && (startIndex = 1); | |
} | |
else { | |
startIndex = 0; | |
} | |
} | |
// 10 is an experience number, avoid long loop. | |
}, seriesLayoutBy, dataArrayRows, 10); | |
} | |
else { | |
startIndex = isNumber(sourceHeader) ? sourceHeader : sourceHeader ? 1 : 0; | |
} | |
if (!dimensionsDefine && startIndex === 1) { | |
dimensionsDefine = []; | |
arrayRowsTravelFirst(function (val, index) { | |
dimensionsDefine[index] = (val != null ? val + '' : '') as DimensionName; | |
}, seriesLayoutBy, dataArrayRows, Infinity); | |
} | |
dimensionsDetectedCount = dimensionsDefine | |
? dimensionsDefine.length | |
: seriesLayoutBy === SERIES_LAYOUT_BY_ROW | |
? dataArrayRows.length | |
: dataArrayRows[0] | |
? dataArrayRows[0].length | |
: null; | |
} | |
else if (sourceFormat === SOURCE_FORMAT_OBJECT_ROWS) { | |
if (!dimensionsDefine) { | |
dimensionsDefine = objectRowsCollectDimensions(data as OptionSourceDataObjectRows); | |
} | |
} | |
else if (sourceFormat === SOURCE_FORMAT_KEYED_COLUMNS) { | |
if (!dimensionsDefine) { | |
dimensionsDefine = []; | |
each(data as OptionSourceDataKeyedColumns, function (colArr, key) { | |
dimensionsDefine.push(key); | |
}); | |
} | |
} | |
else if (sourceFormat === SOURCE_FORMAT_ORIGINAL) { | |
const value0 = getDataItemValue((data as OptionSourceDataOriginal)[0]); | |
dimensionsDetectedCount = isArray(value0) && value0.length || 1; | |
} | |
else if (sourceFormat === SOURCE_FORMAT_TYPED_ARRAY) { | |
if (__DEV__) { | |
assert(!!dimensionsDefine, 'dimensions must be given if data is TypedArray.'); | |
} | |
} | |
return { | |
startIndex: startIndex, | |
dimensionsDefine: normalizeDimensionsOption(dimensionsDefine), | |
dimensionsDetectedCount: dimensionsDetectedCount | |
}; | |
} | |
function objectRowsCollectDimensions(data: OptionSourceDataObjectRows): DimensionDefinitionLoose[] { | |
let firstIndex = 0; | |
let obj; | |
while (firstIndex < data.length && !(obj = data[firstIndex++])) {} // jshint ignore: line | |
if (obj) { | |
return keys(obj); | |
} | |
} | |
// Consider dimensions defined like ['A', 'price', 'B', 'price', 'C', 'price'], | |
// which is reasonable. But dimension name is duplicated. | |
// Returns undefined or an array contains only object without null/undefined or string. | |
function normalizeDimensionsOption(dimensionsDefine: DimensionDefinitionLoose[]): DimensionDefinition[] { | |
if (!dimensionsDefine) { | |
// The meaning of null/undefined is different from empty array. | |
return; | |
} | |
const nameMap = createHashMap<{ count: number }, string>(); | |
return map(dimensionsDefine, function (rawItem, index) { | |
rawItem = isObject(rawItem) ? rawItem : { name: rawItem }; | |
// Other fields will be discarded. | |
const item: DimensionDefinition = { | |
name: rawItem.name, | |
displayName: rawItem.displayName, | |
type: rawItem.type | |
}; | |
// User can set null in dimensions. | |
// We don't auto specify name, otherwise a given name may | |
// cause it to be referred unexpectedly. | |
if (item.name == null) { | |
return item; | |
} | |
// Also consider number form like 2012. | |
item.name += ''; | |
// User may also specify displayName. | |
// displayName will always exists except user not | |
// specified or dim name is not specified or detected. | |
// (A auto generated dim name will not be used as | |
// displayName). | |
if (item.displayName == null) { | |
item.displayName = item.name; | |
} | |
const exist = nameMap.get(item.name); | |
if (!exist) { | |
nameMap.set(item.name, {count: 1}); | |
} | |
else { | |
item.name += '-' + exist.count++; | |
} | |
return item; | |
}); | |
} | |
function arrayRowsTravelFirst( | |
cb: (val: OptionDataValue, idx: number) => void, | |
seriesLayoutBy: SeriesLayoutBy, | |
data: OptionSourceDataArrayRows, | |
maxLoop: number | |
): void { | |
if (seriesLayoutBy === SERIES_LAYOUT_BY_ROW) { | |
for (let i = 0; i < data.length && i < maxLoop; i++) { | |
cb(data[i] ? data[i][0] : null, i); | |
} | |
} | |
else { | |
const value0 = data[0] || []; | |
for (let i = 0; i < value0.length && i < maxLoop; i++) { | |
cb(value0[i], i); | |
} | |
} | |
} | |
export function shouldRetrieveDataByName(source: Source): boolean { | |
const sourceFormat = source.sourceFormat; | |
return sourceFormat === SOURCE_FORMAT_OBJECT_ROWS || sourceFormat === SOURCE_FORMAT_KEYED_COLUMNS; | |
} | |