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 { | |
| DimensionDefinitionLoose, OptionEncode, OptionEncodeValue, | |
| EncodeDefaulter, | |
| OptionSourceData, | |
| DimensionName, | |
| DimensionDefinition, | |
| DataVisualDimensions, | |
| DimensionIndex, | |
| VISUAL_DIMENSIONS | |
| } from '../../util/types'; | |
| import SeriesDimensionDefine from '../SeriesDimensionDefine'; | |
| import { | |
| createHashMap, defaults, each, extend, HashMap, isObject, isString | |
| } from 'zrender/src/core/util'; | |
| import OrdinalMeta from '../OrdinalMeta'; | |
| import { createSourceFromSeriesDataOption, isSourceInstance, Source } from '../Source'; | |
| import { CtorInt32Array } from '../DataStore'; | |
| import { normalizeToArray } from '../../util/model'; | |
| import { BE_ORDINAL, guessOrdinal } from './sourceHelper'; | |
| import { | |
| createDimNameMap, ensureSourceDimNameMap, SeriesDataSchema, shouldOmitUnusedDimensions | |
| } from './SeriesDataSchema'; | |
| export interface CoordDimensionDefinition extends DimensionDefinition { | |
| dimsDef?: (DimensionName | { name: DimensionName, defaultTooltip?: boolean })[]; | |
| otherDims?: DataVisualDimensions; | |
| ordinalMeta?: OrdinalMeta; | |
| coordDim?: DimensionName; | |
| coordDimIndex?: DimensionIndex; | |
| } | |
| export type CoordDimensionDefinitionLoose = CoordDimensionDefinition['name'] | CoordDimensionDefinition; | |
| export type PrepareSeriesDataSchemaParams = { | |
| coordDimensions?: CoordDimensionDefinitionLoose[], | |
| /** | |
| * Will use `source.dimensionsDefine` if not given. | |
| */ | |
| dimensionsDefine?: DimensionDefinitionLoose[], | |
| /** | |
| * Will use `source.encodeDefine` if not given. | |
| */ | |
| encodeDefine?: HashMap<OptionEncodeValue, DimensionName> | OptionEncode, | |
| dimensionsCount?: number, | |
| /** | |
| * Make default encode if user not specified. | |
| */ | |
| encodeDefaulter?: EncodeDefaulter, | |
| generateCoord?: string, | |
| generateCoordCount?: number, | |
| /** | |
| * If be able to omit unused dimension | |
| * Used to improve the performance on high dimension data. | |
| */ | |
| canOmitUnusedDimensions?: boolean | |
| }; | |
| /** | |
| * For outside usage compat (like echarts-gl are using it). | |
| */ | |
| export function createDimensions( | |
| source: Source | OptionSourceData, | |
| opt?: PrepareSeriesDataSchemaParams | |
| ): SeriesDimensionDefine[] { | |
| return prepareSeriesDataSchema(source, opt).dimensions; | |
| } | |
| /** | |
| * This method builds the relationship between: | |
| * + "what the coord sys or series requires (see `coordDimensions`)", | |
| * + "what the user defines (in `encode` and `dimensions`, see `opt.dimensionsDefine` and `opt.encodeDefine`)" | |
| * + "what the data source provids (see `source`)". | |
| * | |
| * Some guess strategy will be adapted if user does not define something. | |
| * If no 'value' dimension specified, the first no-named dimension will be | |
| * named as 'value'. | |
| * | |
| * @return The results are always sorted by `storeDimIndex` asc. | |
| */ | |
| export default function prepareSeriesDataSchema( | |
| // TODO: TYPE completeDimensions type | |
| source: Source | OptionSourceData, | |
| opt?: PrepareSeriesDataSchemaParams | |
| ): SeriesDataSchema { | |
| if (!isSourceInstance(source)) { | |
| source = createSourceFromSeriesDataOption(source as OptionSourceData); | |
| } | |
| opt = opt || {}; | |
| const sysDims = opt.coordDimensions || []; | |
| const dimsDef = opt.dimensionsDefine || source.dimensionsDefine || []; | |
| const coordDimNameMap = createHashMap<true, DimensionName>(); | |
| const resultList: SeriesDimensionDefine[] = []; | |
| const dimCount = getDimCount(source, sysDims, dimsDef, opt.dimensionsCount); | |
| // Try to ignore unused dimensions if sharing a high dimension datastore | |
| // 30 is an experience value. | |
| const omitUnusedDimensions = opt.canOmitUnusedDimensions && shouldOmitUnusedDimensions(dimCount); | |
| const isUsingSourceDimensionsDef = dimsDef === source.dimensionsDefine; | |
| const dataDimNameMap = isUsingSourceDimensionsDef | |
| ? ensureSourceDimNameMap(source) : createDimNameMap(dimsDef); | |
| let encodeDef = opt.encodeDefine; | |
| if (!encodeDef && opt.encodeDefaulter) { | |
| encodeDef = opt.encodeDefaulter(source, dimCount); | |
| } | |
| const encodeDefMap = createHashMap<DimensionIndex[] | false, DimensionName>(encodeDef as any); | |
| const indicesMap = new CtorInt32Array(dimCount); | |
| for (let i = 0; i < indicesMap.length; i++) { | |
| indicesMap[i] = -1; | |
| } | |
| function getResultItem(dimIdx: number) { | |
| const idx = indicesMap[dimIdx]; | |
| if (idx < 0) { | |
| const dimDefItemRaw = dimsDef[dimIdx]; | |
| const dimDefItem = isObject(dimDefItemRaw) ? dimDefItemRaw : { name: dimDefItemRaw }; | |
| const resultItem = new SeriesDimensionDefine(); | |
| const userDimName = dimDefItem.name; | |
| if (userDimName != null && dataDimNameMap.get(userDimName) != null) { | |
| // Only if `series.dimensions` is defined in option | |
| // displayName, will be set, and dimension will be displayed vertically in | |
| // tooltip by default. | |
| resultItem.name = resultItem.displayName = userDimName; | |
| } | |
| dimDefItem.type != null && (resultItem.type = dimDefItem.type); | |
| dimDefItem.displayName != null && (resultItem.displayName = dimDefItem.displayName); | |
| const newIdx = resultList.length; | |
| indicesMap[dimIdx] = newIdx; | |
| resultItem.storeDimIndex = dimIdx; | |
| resultList.push(resultItem); | |
| return resultItem; | |
| } | |
| return resultList[idx]; | |
| } | |
| if (!omitUnusedDimensions) { | |
| for (let i = 0; i < dimCount; i++) { | |
| getResultItem(i); | |
| } | |
| } | |
| // Set `coordDim` and `coordDimIndex` by `encodeDefMap` and normalize `encodeDefMap`. | |
| encodeDefMap.each(function (dataDimsRaw, coordDim) { | |
| const dataDims = normalizeToArray(dataDimsRaw as []).slice(); | |
| // Note: It is allowed that `dataDims.length` is `0`, e.g., options is | |
| // `{encode: {x: -1, y: 1}}`. Should not filter anything in | |
| // this case. | |
| if (dataDims.length === 1 && !isString(dataDims[0]) && dataDims[0] < 0) { | |
| encodeDefMap.set(coordDim, false); | |
| return; | |
| } | |
| const validDataDims = encodeDefMap.set(coordDim, []) as DimensionIndex[]; | |
| each(dataDims, function (resultDimIdxOrName, idx) { | |
| // The input resultDimIdx can be dim name or index. | |
| const resultDimIdx = isString(resultDimIdxOrName) | |
| ? dataDimNameMap.get(resultDimIdxOrName) | |
| : resultDimIdxOrName; | |
| if (resultDimIdx != null && resultDimIdx < dimCount) { | |
| validDataDims[idx] = resultDimIdx; | |
| applyDim(getResultItem(resultDimIdx), coordDim, idx); | |
| } | |
| }); | |
| }); | |
| // Apply templates and default order from `sysDims`. | |
| let availDimIdx = 0; | |
| each(sysDims, function (sysDimItemRaw) { | |
| let coordDim: DimensionName; | |
| let sysDimItemDimsDef: CoordDimensionDefinition['dimsDef']; | |
| let sysDimItemOtherDims: CoordDimensionDefinition['otherDims']; | |
| let sysDimItem: CoordDimensionDefinition; | |
| if (isString(sysDimItemRaw)) { | |
| coordDim = sysDimItemRaw; | |
| sysDimItem = {} as CoordDimensionDefinition; | |
| } | |
| else { | |
| sysDimItem = sysDimItemRaw; | |
| coordDim = sysDimItem.name; | |
| const ordinalMeta = sysDimItem.ordinalMeta; | |
| sysDimItem.ordinalMeta = null; | |
| sysDimItem = extend({}, sysDimItem); | |
| sysDimItem.ordinalMeta = ordinalMeta; | |
| // `coordDimIndex` should not be set directly. | |
| sysDimItemDimsDef = sysDimItem.dimsDef; | |
| sysDimItemOtherDims = sysDimItem.otherDims; | |
| sysDimItem.name = sysDimItem.coordDim = sysDimItem.coordDimIndex = | |
| sysDimItem.dimsDef = sysDimItem.otherDims = null; | |
| } | |
| let dataDims = encodeDefMap.get(coordDim); | |
| // negative resultDimIdx means no need to mapping. | |
| if (dataDims === false) { | |
| return; | |
| } | |
| dataDims = normalizeToArray(dataDims); | |
| // dimensions provides default dim sequences. | |
| if (!dataDims.length) { | |
| for (let i = 0; i < (sysDimItemDimsDef && sysDimItemDimsDef.length || 1); i++) { | |
| while (availDimIdx < dimCount && getResultItem(availDimIdx).coordDim != null) { | |
| availDimIdx++; | |
| } | |
| availDimIdx < dimCount && dataDims.push(availDimIdx++); | |
| } | |
| } | |
| // Apply templates. | |
| each(dataDims, function (resultDimIdx, coordDimIndex) { | |
| const resultItem = getResultItem(resultDimIdx); | |
| // Coordinate system has a higher priority on dim type than source. | |
| if (isUsingSourceDimensionsDef && sysDimItem.type != null) { | |
| resultItem.type = sysDimItem.type; | |
| } | |
| applyDim(defaults(resultItem, sysDimItem), coordDim, coordDimIndex); | |
| if (resultItem.name == null && sysDimItemDimsDef) { | |
| let sysDimItemDimsDefItem = sysDimItemDimsDef[coordDimIndex]; | |
| !isObject(sysDimItemDimsDefItem) && (sysDimItemDimsDefItem = { | |
| name: sysDimItemDimsDefItem | |
| }); | |
| resultItem.name = resultItem.displayName = sysDimItemDimsDefItem.name; | |
| resultItem.defaultTooltip = sysDimItemDimsDefItem.defaultTooltip; | |
| } | |
| // FIXME refactor, currently only used in case: {otherDims: {tooltip: false}} | |
| sysDimItemOtherDims && defaults(resultItem.otherDims, sysDimItemOtherDims); | |
| }); | |
| }); | |
| function applyDim(resultItem: SeriesDimensionDefine, coordDim: DimensionName, coordDimIndex: DimensionIndex) { | |
| if (VISUAL_DIMENSIONS.get(coordDim as keyof DataVisualDimensions) != null) { | |
| resultItem.otherDims[coordDim as keyof DataVisualDimensions] = coordDimIndex; | |
| } | |
| else { | |
| resultItem.coordDim = coordDim; | |
| resultItem.coordDimIndex = coordDimIndex; | |
| coordDimNameMap.set(coordDim, true); | |
| } | |
| } | |
| // Make sure the first extra dim is 'value'. | |
| const generateCoord = opt.generateCoord; | |
| let generateCoordCount = opt.generateCoordCount; | |
| const fromZero = generateCoordCount != null; | |
| generateCoordCount = generateCoord ? (generateCoordCount || 1) : 0; | |
| const extra = generateCoord || 'value'; | |
| function ifNoNameFillWithCoordName(resultItem: SeriesDimensionDefine): void { | |
| if (resultItem.name == null) { | |
| // Duplication will be removed in the next step. | |
| resultItem.name = resultItem.coordDim; | |
| } | |
| } | |
| // Set dim `name` and other `coordDim` and other props. | |
| if (!omitUnusedDimensions) { | |
| for (let resultDimIdx = 0; resultDimIdx < dimCount; resultDimIdx++) { | |
| const resultItem = getResultItem(resultDimIdx); | |
| const coordDim = resultItem.coordDim; | |
| if (coordDim == null) { | |
| // TODO no need to generate coordDim for isExtraCoord? | |
| resultItem.coordDim = genCoordDimName( | |
| extra, coordDimNameMap, fromZero | |
| ); | |
| resultItem.coordDimIndex = 0; | |
| // Series specified generateCoord is using out. | |
| if (!generateCoord || generateCoordCount <= 0) { | |
| resultItem.isExtraCoord = true; | |
| } | |
| generateCoordCount--; | |
| } | |
| ifNoNameFillWithCoordName(resultItem); | |
| if (resultItem.type == null | |
| && ( | |
| guessOrdinal(source, resultDimIdx) === BE_ORDINAL.Must | |
| // Consider the case: | |
| // { | |
| // dataset: {source: [ | |
| // ['2001', 123], | |
| // ['2002', 456], | |
| // ... | |
| // ['The others', 987], | |
| // ]}, | |
| // series: {type: 'pie'} | |
| // } | |
| // The first column should better be treated as a "ordinal" although it | |
| // might not be detected as an "ordinal" by `guessOrdinal`. | |
| || (resultItem.isExtraCoord | |
| && (resultItem.otherDims.itemName != null | |
| || resultItem.otherDims.seriesName != null | |
| ) | |
| ) | |
| ) | |
| ) { | |
| resultItem.type = 'ordinal'; | |
| } | |
| } | |
| } | |
| else { | |
| each(resultList, resultItem => { | |
| // PENDING: guessOrdinal or let user specify type: 'ordinal' manually? | |
| ifNoNameFillWithCoordName(resultItem); | |
| }); | |
| // Sort dimensions: there are some rule that use the last dim as label, | |
| // and for some latter travel process easier. | |
| resultList.sort((item0, item1) => item0.storeDimIndex - item1.storeDimIndex); | |
| } | |
| removeDuplication(resultList); | |
| return new SeriesDataSchema({ | |
| source, | |
| dimensions: resultList, | |
| fullDimensionCount: dimCount, | |
| dimensionOmitted: omitUnusedDimensions | |
| }); | |
| } | |
| function removeDuplication(result: SeriesDimensionDefine[]) { | |
| const duplicationMap = createHashMap<number>(); | |
| for (let i = 0; i < result.length; i++) { | |
| const dim = result[i]; | |
| const dimOriginalName = dim.name; | |
| let count = duplicationMap.get(dimOriginalName) || 0; | |
| if (count > 0) { | |
| // Starts from 0. | |
| dim.name = dimOriginalName + (count - 1); | |
| } | |
| count++; | |
| duplicationMap.set(dimOriginalName, count); | |
| } | |
| } | |
| // ??? TODO | |
| // Originally detect dimCount by data[0]. Should we | |
| // optimize it to only by sysDims and dimensions and encode. | |
| // So only necessary dims will be initialized. | |
| // But | |
| // (1) custom series should be considered. where other dims | |
| // may be visited. | |
| // (2) sometimes user need to calculate bubble size or use visualMap | |
| // on other dimensions besides coordSys needed. | |
| // So, dims that is not used by system, should be shared in data store? | |
| function getDimCount( | |
| source: Source, | |
| sysDims: CoordDimensionDefinitionLoose[], | |
| dimsDef: DimensionDefinitionLoose[], | |
| optDimCount?: number | |
| ): number { | |
| // Note that the result dimCount should not small than columns count | |
| // of data, otherwise `dataDimNameMap` checking will be incorrect. | |
| let dimCount = Math.max( | |
| source.dimensionsDetectedCount || 1, | |
| sysDims.length, | |
| dimsDef.length, | |
| optDimCount || 0 | |
| ); | |
| each(sysDims, function (sysDimItem) { | |
| let sysDimItemDimsDef; | |
| if (isObject(sysDimItem) && (sysDimItemDimsDef = sysDimItem.dimsDef)) { | |
| dimCount = Math.max(dimCount, sysDimItemDimsDef.length); | |
| } | |
| }); | |
| return dimCount; | |
| } | |
| function genCoordDimName( | |
| name: DimensionName, | |
| map: HashMap<unknown, DimensionName>, | |
| fromZero: boolean | |
| ) { | |
| if (fromZero || map.hasKey(name)) { | |
| let i = 0; | |
| while (map.hasKey(name + i)) { | |
| i++; | |
| } | |
| name += i; | |
| } | |
| map.set(name, true); | |
| return name; | |
| } | |