| /* |
| * 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 mutiple series share one dimension |
| // category axis, series name should better use |
| // the other dimsion 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 use itemName in charts. |
| // So we dont 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; |
| } |