Spaces:
Running
Running
| /* | |
| * 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 default 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; | |
| } | |