| /* |
| * 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 |
| } 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, |
| OptionEncode, |
| 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'; |
| |
| /** |
| * [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[]; |
| |
| /** |
| * encode definition in option. |
| * can be null/undefined. |
| * Might be specified outside. |
| */ |
| readonly encodeDefine: HashMap<OptionEncodeValue, DimensionName>; |
| |
| /** |
| * 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; |
| |
| // readonly frozen: boolean; |
| |
| |
| 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.dimensionsDefine = fields.dimensionsDefine; |
| this.dimensionsDetectedCount = fields.dimensionsDetectedCount; |
| this.encodeDefine = fields.encodeDefine; |
| this.metaRawOption = fields.metaRawOption; |
| } |
| |
| // There is performance issue in some browser like Safari, |
| // an also slower than clone in Chrome. |
| // So DO NOT use `Object.freeze`. |
| /** |
| * When expose the source to thrid-party transform, it probably better to |
| * freeze to make sure immutability. |
| * If a third-party transform modify the raw upstream data structure, it might bring about |
| * "uncertain effect" when using multiple transforms with different combinations. |
| * |
| * [Caveat] |
| * `OptionManager.ts` have perform `clone` in `setOption`. |
| * The original user input object should better not be frozen in case they |
| * make other usages. |
| */ |
| // freeze() { |
| // assert(sourceFormatCanBeExposed(this)); |
| // const data = this.data as OptionSourceDataArrayRows; |
| // if (this.frozen || !data || !isFunction(Object.freeze)) { |
| // return; |
| // } |
| // // @ts-ignore |
| // this.frozen = true; |
| // // PENDING: |
| // // There is a flaw that there might be non-primitive values like `Date`. |
| // // Is it worth handling that? |
| // for (let i = 0; i < data.length; i++) { |
| // Object.freeze(data[i]); |
| // } |
| // Object.freeze(data); |
| // } |
| |
| } |
| |
| export function isSourceInstance(val: unknown): val is Source { |
| return val instanceof SourceImpl; |
| } |
| |
| export function createSource( |
| sourceData: OptionSourceData, |
| thisMetaRawOption: SourceMetaRawOption, |
| // can be null. If not provided, auto detect it from `sourceData`. |
| sourceFormat: SourceFormat, |
| encodeDefine: OptionEncode // can be null |
| ): 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, |
| encodeDefine: makeEncodeDefine(encodeDefine), |
| 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, |
| encodeDefine: makeEncodeDefine(source.encodeDefine) |
| }); |
| } |
| |
| function makeEncodeDefine( |
| encodeDefine: OptionEncode | HashMap<OptionEncodeValue, DimensionName> |
| ): HashMap<OptionEncodeValue, DimensionName> { |
| // null means user not specify `series.encode`. |
| return encodeDefine |
| ? createHashMap<OptionEncodeValue, DimensionName>(encodeDefine) |
| : null; |
| } |
| |
| /** |
| * 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)) { |
| 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; |
| |
| // PEDING: 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) { |
| const dimensions: DimensionDefinitionLoose[] = []; |
| each(obj, function (value, key) { |
| dimensions.push(key); |
| }); |
| return dimensions; |
| } |
| } |
| |
| // 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/undefiend 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 dont auto specify name, othewise a given name may |
| // cause it be refered 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); |
| } |
| } |
| } |