/* * 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 | 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(); 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(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(); 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, fromZero: boolean ) { if (fromZero || map.hasKey(name)) { let i = 0; while (map.hasKey(name + i)) { i++; } name += i; } map.set(name, true); return name; }