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