| /* | 
 | * 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 {makeInner, getDataItemValue, queryReferringComponents, SINGLE_REFERRING} from '../../util/model'; | 
 | import { | 
 |     createHashMap, | 
 |     each, | 
 |     isArray, | 
 |     isString, | 
 |     isObject, | 
 |     isTypedArray, | 
 |     HashMap | 
 | } from 'zrender/src/core/util'; | 
 | import { Source } from '../Source'; | 
 |  | 
 | import { | 
 |     SOURCE_FORMAT_ORIGINAL, | 
 |     SOURCE_FORMAT_ARRAY_ROWS, | 
 |     SOURCE_FORMAT_OBJECT_ROWS, | 
 |     SERIES_LAYOUT_BY_ROW, | 
 |     SOURCE_FORMAT_KEYED_COLUMNS, | 
 |     DimensionName, | 
 |     OptionSourceDataArrayRows, | 
 |     OptionDataValue, | 
 |     OptionSourceDataKeyedColumns, | 
 |     OptionSourceDataOriginal, | 
 |     OptionSourceDataObjectRows, | 
 |     OptionEncode, | 
 |     DimensionIndex, | 
 |     SeriesEncodableModel | 
 | } from '../../util/types'; | 
 | import { DatasetModel } from '../../component/dataset/install'; | 
 | import SeriesModel from '../../model/Series'; | 
 | import GlobalModel from '../../model/Global'; | 
 | import { CoordDimensionDefinition } from './createDimensions'; | 
 |  | 
 | // The result of `guessOrdinal`. | 
 | export const BE_ORDINAL = { | 
 |     Must: 1, // Encounter string but not '-' and not number-like. | 
 |     Might: 2, // Encounter string but number-like. | 
 |     Not: 3 // Other cases | 
 | }; | 
 | type BeOrdinalValue = (typeof BE_ORDINAL)[keyof typeof BE_ORDINAL]; | 
 |  | 
 | const innerGlobalModel = makeInner<{ | 
 |     datasetMap: HashMap<DatasetRecord, string> | 
 | }, GlobalModel>(); | 
 |  | 
 |  | 
 | interface DatasetRecord { | 
 |     categoryWayDim: number; | 
 |     valueWayDim: number; | 
 | } | 
 |  | 
 | type SeriesEncodeInternal = { | 
 |     [key in keyof OptionEncode]: DimensionIndex[]; | 
 | }; | 
 |  | 
 | /** | 
 |  * MUST be called before mergeOption of all series. | 
 |  */ | 
 | export function resetSourceDefaulter(ecModel: GlobalModel): void { | 
 |     // `datasetMap` is used to make default encode. | 
 |     innerGlobalModel(ecModel).datasetMap = createHashMap(); | 
 | } | 
 |  | 
 | /** | 
 |  * [The strategy of the arrengment of data dimensions for dataset]: | 
 |  * "value way": all axes are non-category axes. So series one by one take | 
 |  *     several (the number is coordSysDims.length) dimensions from dataset. | 
 |  *     The result of data arrengment of data dimensions like: | 
 |  *     | ser0_x | ser0_y | ser1_x | ser1_y | ser2_x | ser2_y | | 
 |  * "category way": at least one axis is category axis. So the the first data | 
 |  *     dimension is always mapped to the first category axis and shared by | 
 |  *     all of the series. The other data dimensions are taken by series like | 
 |  *     "value way" does. | 
 |  *     The result of data arrengment of data dimensions like: | 
 |  *     | ser_shared_x | ser0_y | ser1_y | ser2_y | | 
 |  * | 
 |  * @return encode Never be `null/undefined`. | 
 |  */ | 
 | export function makeSeriesEncodeForAxisCoordSys( | 
 |     coordDimensions: (DimensionName | CoordDimensionDefinition)[], | 
 |     seriesModel: SeriesModel, | 
 |     source: Source | 
 | ): SeriesEncodeInternal { | 
 |     const encode: SeriesEncodeInternal = {}; | 
 |  | 
 |     const datasetModel = querySeriesUpstreamDatasetModel(seriesModel); | 
 |     // Currently only make default when using dataset, util more reqirements occur. | 
 |     if (!datasetModel || !coordDimensions) { | 
 |         return encode; | 
 |     } | 
 |  | 
 |     const encodeItemName: DimensionIndex[] = []; | 
 |     const encodeSeriesName: DimensionIndex[] = []; | 
 |  | 
 |     const ecModel = seriesModel.ecModel; | 
 |     const datasetMap = innerGlobalModel(ecModel).datasetMap; | 
 |     const key = datasetModel.uid + '_' + source.seriesLayoutBy; | 
 |  | 
 |     let baseCategoryDimIndex: number; | 
 |     let categoryWayValueDimStart; | 
 |     coordDimensions = coordDimensions.slice(); | 
 |     each(coordDimensions, function (coordDimInfoLoose, coordDimIdx) { | 
 |         const coordDimInfo: CoordDimensionDefinition = isObject(coordDimInfoLoose) | 
 |             ? coordDimInfoLoose | 
 |             : (coordDimensions[coordDimIdx] = { name: coordDimInfoLoose as DimensionName }); | 
 |         if (coordDimInfo.type === 'ordinal' && baseCategoryDimIndex == null) { | 
 |             baseCategoryDimIndex = coordDimIdx; | 
 |             categoryWayValueDimStart = getDataDimCountOnCoordDim(coordDimInfo); | 
 |         } | 
 |         encode[coordDimInfo.name] = []; | 
 |     }); | 
 |  | 
 |     const datasetRecord = datasetMap.get(key) | 
 |         || datasetMap.set(key, {categoryWayDim: categoryWayValueDimStart, valueWayDim: 0}); | 
 |  | 
 |     // TODO | 
 |     // Auto detect first time axis and do arrangement. | 
 |     each(coordDimensions, function (coordDimInfo: CoordDimensionDefinition, coordDimIdx) { | 
 |         const coordDimName = coordDimInfo.name; | 
 |         const count = getDataDimCountOnCoordDim(coordDimInfo); | 
 |  | 
 |         // In value way. | 
 |         if (baseCategoryDimIndex == null) { | 
 |             const start = datasetRecord.valueWayDim; | 
 |             pushDim(encode[coordDimName], start, count); | 
 |             pushDim(encodeSeriesName, start, count); | 
 |             datasetRecord.valueWayDim += count; | 
 |  | 
 |             // ??? TODO give a better default series name rule? | 
 |             // especially when encode x y specified. | 
 |             // consider: when multiple series share one dimension | 
 |             // category axis, series name should better use | 
 |             // the other dimension name. On the other hand, use | 
 |             // both dimensions name. | 
 |         } | 
 |         // In category way, the first category axis. | 
 |         else if (baseCategoryDimIndex === coordDimIdx) { | 
 |             pushDim(encode[coordDimName], 0, count); | 
 |             pushDim(encodeItemName, 0, count); | 
 |         } | 
 |         // In category way, the other axis. | 
 |         else { | 
 |             const start = datasetRecord.categoryWayDim; | 
 |             pushDim(encode[coordDimName], start, count); | 
 |             pushDim(encodeSeriesName, start, count); | 
 |             datasetRecord.categoryWayDim += count; | 
 |         } | 
 |     }); | 
 |  | 
 |     function pushDim(dimIdxArr: DimensionIndex[], idxFrom: number, idxCount: number) { | 
 |         for (let i = 0; i < idxCount; i++) { | 
 |             dimIdxArr.push(idxFrom + i); | 
 |         } | 
 |     } | 
 |  | 
 |     function getDataDimCountOnCoordDim(coordDimInfo: CoordDimensionDefinition) { | 
 |         const dimsDef = coordDimInfo.dimsDef; | 
 |         return dimsDef ? dimsDef.length : 1; | 
 |     } | 
 |  | 
 |     encodeItemName.length && (encode.itemName = encodeItemName); | 
 |     encodeSeriesName.length && (encode.seriesName = encodeSeriesName); | 
 |  | 
 |     return encode; | 
 | } | 
 |  | 
 | /** | 
 |  * Work for data like [{name: ..., value: ...}, ...]. | 
 |  * | 
 |  * @return encode Never be `null/undefined`. | 
 |  */ | 
 | export function makeSeriesEncodeForNameBased( | 
 |     seriesModel: SeriesModel, | 
 |     source: Source, | 
 |     dimCount: number | 
 | ): SeriesEncodeInternal { | 
 |     const encode: SeriesEncodeInternal = {}; | 
 |  | 
 |     const datasetModel = querySeriesUpstreamDatasetModel(seriesModel); | 
 |     // Currently only make default when using dataset, util more reqirements occur. | 
 |     if (!datasetModel) { | 
 |         return encode; | 
 |     } | 
 |  | 
 |     const sourceFormat = source.sourceFormat; | 
 |     const dimensionsDefine = source.dimensionsDefine; | 
 |  | 
 |     let potentialNameDimIndex; | 
 |     if (sourceFormat === SOURCE_FORMAT_OBJECT_ROWS || sourceFormat === SOURCE_FORMAT_KEYED_COLUMNS) { | 
 |         each(dimensionsDefine, function (dim, idx) { | 
 |             if ((isObject(dim) ? dim.name : dim) === 'name') { | 
 |                 potentialNameDimIndex = idx; | 
 |             } | 
 |         }); | 
 |     } | 
 |  | 
 |     type IdxResult = { v: number, n: number }; | 
 |  | 
 |     const idxResult = (function () { | 
 |  | 
 |         const idxRes0 = {} as IdxResult; | 
 |         const idxRes1 = {} as IdxResult; | 
 |         const guessRecords = []; | 
 |  | 
 |         // 5 is an experience value. | 
 |         for (let i = 0, len = Math.min(5, dimCount); i < len; i++) { | 
 |             const guessResult = doGuessOrdinal( | 
 |                 source.data, sourceFormat, source.seriesLayoutBy, | 
 |                 dimensionsDefine, source.startIndex, i | 
 |             ); | 
 |             guessRecords.push(guessResult); | 
 |             const isPureNumber = guessResult === BE_ORDINAL.Not; | 
 |  | 
 |             // [Strategy of idxRes0]: find the first BE_ORDINAL.Not as the value dim, | 
 |             // and then find a name dim with the priority: | 
 |             // "BE_ORDINAL.Might|BE_ORDINAL.Must" > "other dim" > "the value dim itself". | 
 |             if (isPureNumber && idxRes0.v == null && i !== potentialNameDimIndex) { | 
 |                 idxRes0.v = i; | 
 |             } | 
 |             if (idxRes0.n == null | 
 |                 || (idxRes0.n === idxRes0.v) | 
 |                 || (!isPureNumber && guessRecords[idxRes0.n] === BE_ORDINAL.Not) | 
 |             ) { | 
 |                 idxRes0.n = i; | 
 |             } | 
 |             if (fulfilled(idxRes0) && guessRecords[idxRes0.n] !== BE_ORDINAL.Not) { | 
 |                 return idxRes0; | 
 |             } | 
 |  | 
 |             // [Strategy of idxRes1]: if idxRes0 not satisfied (that is, no BE_ORDINAL.Not), | 
 |             // find the first BE_ORDINAL.Might as the value dim, | 
 |             // and then find a name dim with the priority: | 
 |             // "other dim" > "the value dim itself". | 
 |             // That is for backward compat: number-like (e.g., `'3'`, `'55'`) can be | 
 |             // treated as number. | 
 |             if (!isPureNumber) { | 
 |                 if (guessResult === BE_ORDINAL.Might && idxRes1.v == null && i !== potentialNameDimIndex) { | 
 |                     idxRes1.v = i; | 
 |                 } | 
 |                 if (idxRes1.n == null || (idxRes1.n === idxRes1.v)) { | 
 |                     idxRes1.n = i; | 
 |                 } | 
 |             } | 
 |         } | 
 |  | 
 |         function fulfilled(idxResult: IdxResult) { | 
 |             return idxResult.v != null && idxResult.n != null; | 
 |         } | 
 |  | 
 |         return fulfilled(idxRes0) ? idxRes0 : fulfilled(idxRes1) ? idxRes1 : null; | 
 |     })(); | 
 |  | 
 |     if (idxResult) { | 
 |         encode.value = [idxResult.v]; | 
 |         // `potentialNameDimIndex` has highest priority. | 
 |         const nameDimIndex = potentialNameDimIndex != null ? potentialNameDimIndex : idxResult.n; | 
 |         // By default, label uses itemName in charts. | 
 |         // So we don't set encodeLabel here. | 
 |         encode.itemName = [nameDimIndex]; | 
 |         encode.seriesName = [nameDimIndex]; | 
 |     } | 
 |  | 
 |     return encode; | 
 | } | 
 |  | 
 | /** | 
 |  * @return If return null/undefined, indicate that should not use datasetModel. | 
 |  */ | 
 | export function querySeriesUpstreamDatasetModel( | 
 |     seriesModel: SeriesEncodableModel | 
 | ): DatasetModel { | 
 |     // Caution: consider the scenario: | 
 |     // A dataset is declared and a series is not expected to use the dataset, | 
 |     // and at the beginning `setOption({series: { noData })` (just prepare other | 
 |     // option but no data), then `setOption({series: {data: [...]}); In this case, | 
 |     // the user should set an empty array to avoid that dataset is used by default. | 
 |     const thisData = seriesModel.get('data', true); | 
 |     if (!thisData) { | 
 |         return queryReferringComponents( | 
 |             seriesModel.ecModel, | 
 |             'dataset', | 
 |             { | 
 |                 index: seriesModel.get('datasetIndex', true), | 
 |                 id: seriesModel.get('datasetId', true) | 
 |             }, | 
 |             SINGLE_REFERRING | 
 |         ).models[0] as DatasetModel; | 
 |     } | 
 | } | 
 |  | 
 | /** | 
 |  * @return Always return an array event empty. | 
 |  */ | 
 | export function queryDatasetUpstreamDatasetModels( | 
 |     datasetModel: DatasetModel | 
 | ): DatasetModel[] { | 
 |     // Only these attributes declared, we by defualt reference to `datasetIndex: 0`. | 
 |     // Otherwise, no reference. | 
 |     if (!datasetModel.get('transform', true) | 
 |         && !datasetModel.get('fromTransformResult', true) | 
 |     ) { | 
 |         return []; | 
 |     } | 
 |  | 
 |     return queryReferringComponents( | 
 |         datasetModel.ecModel, | 
 |         'dataset', | 
 |         { | 
 |             index: datasetModel.get('fromDatasetIndex', true), | 
 |             id: datasetModel.get('fromDatasetId', true) | 
 |         }, | 
 |         SINGLE_REFERRING | 
 |     ).models as DatasetModel[]; | 
 | } | 
 |  | 
 | /** | 
 |  * The rule should not be complex, otherwise user might not | 
 |  * be able to known where the data is wrong. | 
 |  * The code is ugly, but how to make it neat? | 
 |  */ | 
 | export function guessOrdinal(source: Source, dimIndex: DimensionIndex): BeOrdinalValue { | 
 |     return doGuessOrdinal( | 
 |         source.data, | 
 |         source.sourceFormat, | 
 |         source.seriesLayoutBy, | 
 |         source.dimensionsDefine, | 
 |         source.startIndex, | 
 |         dimIndex | 
 |     ); | 
 | } | 
 |  | 
 | // dimIndex may be overflow source data. | 
 | // return {BE_ORDINAL} | 
 | function doGuessOrdinal( | 
 |     data: Source['data'], | 
 |     sourceFormat: Source['sourceFormat'], | 
 |     seriesLayoutBy: Source['seriesLayoutBy'], | 
 |     dimensionsDefine: Source['dimensionsDefine'], | 
 |     startIndex: Source['startIndex'], | 
 |     dimIndex: DimensionIndex | 
 | ): BeOrdinalValue { | 
 |     let result; | 
 |     // Experience value. | 
 |     const maxLoop = 5; | 
 |  | 
 |     if (isTypedArray(data)) { | 
 |         return BE_ORDINAL.Not; | 
 |     } | 
 |  | 
 |     // When sourceType is 'objectRows' or 'keyedColumns', dimensionsDefine | 
 |     // always exists in source. | 
 |     let dimName; | 
 |     let dimType; | 
 |     if (dimensionsDefine) { | 
 |         const dimDefItem = dimensionsDefine[dimIndex]; | 
 |         if (isObject(dimDefItem)) { | 
 |             dimName = dimDefItem.name; | 
 |             dimType = dimDefItem.type; | 
 |         } | 
 |         else if (isString(dimDefItem)) { | 
 |             dimName = dimDefItem; | 
 |         } | 
 |     } | 
 |  | 
 |     if (dimType != null) { | 
 |         return dimType === 'ordinal' ? BE_ORDINAL.Must : BE_ORDINAL.Not; | 
 |     } | 
 |  | 
 |     if (sourceFormat === SOURCE_FORMAT_ARRAY_ROWS) { | 
 |         const dataArrayRows = data as OptionSourceDataArrayRows; | 
 |         if (seriesLayoutBy === SERIES_LAYOUT_BY_ROW) { | 
 |             const sample = dataArrayRows[dimIndex]; | 
 |             for (let i = 0; i < (sample || []).length && i < maxLoop; i++) { | 
 |                 if ((result = detectValue(sample[startIndex + i])) != null) { | 
 |                     return result; | 
 |                 } | 
 |             } | 
 |         } | 
 |         else { | 
 |             for (let i = 0; i < dataArrayRows.length && i < maxLoop; i++) { | 
 |                 const row = dataArrayRows[startIndex + i]; | 
 |                 if (row && (result = detectValue(row[dimIndex])) != null) { | 
 |                     return result; | 
 |                 } | 
 |             } | 
 |         } | 
 |     } | 
 |     else if (sourceFormat === SOURCE_FORMAT_OBJECT_ROWS) { | 
 |         const dataObjectRows = data as OptionSourceDataObjectRows; | 
 |         if (!dimName) { | 
 |             return BE_ORDINAL.Not; | 
 |         } | 
 |         for (let i = 0; i < dataObjectRows.length && i < maxLoop; i++) { | 
 |             const item = dataObjectRows[i]; | 
 |             if (item && (result = detectValue(item[dimName])) != null) { | 
 |                 return result; | 
 |             } | 
 |         } | 
 |     } | 
 |     else if (sourceFormat === SOURCE_FORMAT_KEYED_COLUMNS) { | 
 |         const dataKeyedColumns = data as OptionSourceDataKeyedColumns; | 
 |         if (!dimName) { | 
 |             return BE_ORDINAL.Not; | 
 |         } | 
 |         const sample = dataKeyedColumns[dimName]; | 
 |         if (!sample || isTypedArray(sample)) { | 
 |             return BE_ORDINAL.Not; | 
 |         } | 
 |         for (let i = 0; i < sample.length && i < maxLoop; i++) { | 
 |             if ((result = detectValue(sample[i])) != null) { | 
 |                 return result; | 
 |             } | 
 |         } | 
 |     } | 
 |     else if (sourceFormat === SOURCE_FORMAT_ORIGINAL) { | 
 |         const dataOriginal = data as OptionSourceDataOriginal; | 
 |         for (let i = 0; i < dataOriginal.length && i < maxLoop; i++) { | 
 |             const item = dataOriginal[i]; | 
 |             const val = getDataItemValue(item); | 
 |             if (!isArray(val)) { | 
 |                 return BE_ORDINAL.Not; | 
 |             } | 
 |             if ((result = detectValue(val[dimIndex])) != null) { | 
 |                 return result; | 
 |             } | 
 |         } | 
 |     } | 
 |  | 
 |     function detectValue(val: OptionDataValue): BeOrdinalValue { | 
 |         const beStr = isString(val); | 
 |         // Consider usage convenience, '1', '2' will be treated as "number". | 
 |         // `isFinit('')` get `true`. | 
 |         if (val != null && isFinite(val as number) && val !== '') { | 
 |             return beStr ? BE_ORDINAL.Might : BE_ORDINAL.Not; | 
 |         } | 
 |         else if (beStr && val !== '-') { | 
 |             return BE_ORDINAL.Must; | 
 |         } | 
 |     } | 
 |  | 
 |     return BE_ORDINAL.Not; | 
 | } |