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