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