| /* |
| * 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 { __DEV__ } from '../../config'; |
| import { makeInner, getDataItemValue } from '../../util/model'; |
| import { getCoordSysDefineBySeries } from '../../model/referHelper'; |
| import { createHashMap, each, map, isArray, isString, isObject, isTypedArray, isArrayLike, extend, assert } from 'zrender/src/core/util'; |
| import Source from '../Source'; |
| import { SOURCE_FORMAT_ORIGINAL, SOURCE_FORMAT_ARRAY_ROWS, SOURCE_FORMAT_OBJECT_ROWS, SOURCE_FORMAT_KEYED_COLUMNS, SOURCE_FORMAT_UNKNOWN, SOURCE_FORMAT_TYPED_ARRAY, SERIES_LAYOUT_BY_ROW } from './sourceType'; |
| var inner = makeInner(); |
| /** |
| * @see {module:echarts/data/Source} |
| * @param {module:echarts/component/dataset/DatasetModel} datasetModel |
| * @return {string} sourceFormat |
| */ |
| |
| export function detectSourceFormat(datasetModel) { |
| var data = datasetModel.option.source; |
| var sourceFormat = SOURCE_FORMAT_UNKNOWN; |
| |
| if (isTypedArray(data)) { |
| sourceFormat = SOURCE_FORMAT_TYPED_ARRAY; |
| } else if (isArray(data)) { |
| // FIXME Whether tolerate null in top level array? |
| if (data.length === 0) { |
| sourceFormat = SOURCE_FORMAT_ARRAY_ROWS; |
| } |
| |
| for (var i = 0, len = data.length; i < len; i++) { |
| var item = data[i]; |
| |
| if (item == null) { |
| continue; |
| } else if (isArray(item)) { |
| sourceFormat = SOURCE_FORMAT_ARRAY_ROWS; |
| break; |
| } else if (isObject(item)) { |
| sourceFormat = SOURCE_FORMAT_OBJECT_ROWS; |
| break; |
| } |
| } |
| } else if (isObject(data)) { |
| for (var key in data) { |
| if (data.hasOwnProperty(key) && isArrayLike(data[key])) { |
| sourceFormat = SOURCE_FORMAT_KEYED_COLUMNS; |
| break; |
| } |
| } |
| } else if (data != null) { |
| throw new Error('Invalid data'); |
| } |
| |
| inner(datasetModel).sourceFormat = sourceFormat; |
| } |
| /** |
| * [Scenarios]: |
| * (1) Provide source data directly: |
| * series: { |
| * encode: {...}, |
| * dimensions: [...] |
| * seriesLayoutBy: 'row', |
| * data: [[...]] |
| * } |
| * (2) Refer to datasetModel. |
| * series: [{ |
| * encode: {...} |
| * // Ignore datasetIndex means `datasetIndex: 0` |
| * // and the dimensions defination in dataset is used |
| * }, { |
| * encode: {...}, |
| * seriesLayoutBy: 'column', |
| * datasetIndex: 1 |
| * }] |
| * |
| * Get data from series itself or datset. |
| * @return {module:echarts/data/Source} source |
| */ |
| |
| export function getSource(seriesModel) { |
| return inner(seriesModel).source; |
| } |
| /** |
| * MUST be called before mergeOption of all series. |
| * @param {module:echarts/model/Global} ecModel |
| */ |
| |
| export function resetSourceDefaulter(ecModel) { |
| // `datasetMap` is used to make default encode. |
| inner(ecModel).datasetMap = createHashMap(); |
| } |
| /** |
| * [Caution]: |
| * MUST be called after series option merged and |
| * before "series.getInitailData()" called. |
| * |
| * [The rule of making default encode]: |
| * Category axis (if exists) alway map to the first dimension. |
| * Each other axis occupies a subsequent dimension. |
| * |
| * [Why make default encode]: |
| * Simplify the typing of encode in option, avoiding the case like that: |
| * series: [{encode: {x: 0, y: 1}}, {encode: {x: 0, y: 2}}, {encode: {x: 0, y: 3}}], |
| * where the "y" have to be manually typed as "1, 2, 3, ...". |
| * |
| * @param {module:echarts/model/Series} seriesModel |
| */ |
| |
| export function prepareSource(seriesModel) { |
| var seriesOption = seriesModel.option; |
| var data = seriesOption.data; |
| var sourceFormat = isTypedArray(data) ? SOURCE_FORMAT_TYPED_ARRAY : SOURCE_FORMAT_ORIGINAL; |
| var fromDataset = false; |
| var seriesLayoutBy = seriesOption.seriesLayoutBy; |
| var sourceHeader = seriesOption.sourceHeader; |
| var dimensionsDefine = seriesOption.dimensions; |
| var datasetModel = getDatasetModel(seriesModel); |
| |
| if (datasetModel) { |
| var datasetOption = datasetModel.option; |
| data = datasetOption.source; |
| sourceFormat = inner(datasetModel).sourceFormat; |
| fromDataset = true; // These settings from series has higher priority. |
| |
| seriesLayoutBy = seriesLayoutBy || datasetOption.seriesLayoutBy; |
| sourceHeader == null && (sourceHeader = datasetOption.sourceHeader); |
| dimensionsDefine = dimensionsDefine || datasetOption.dimensions; |
| } |
| |
| var completeResult = completeBySourceData(data, sourceFormat, seriesLayoutBy, sourceHeader, dimensionsDefine); // Note: dataset option does not have `encode`. |
| |
| var encodeDefine = seriesOption.encode; |
| |
| if (!encodeDefine && datasetModel) { |
| encodeDefine = makeDefaultEncode(seriesModel, datasetModel, data, sourceFormat, seriesLayoutBy, completeResult); |
| } |
| |
| inner(seriesModel).source = new Source({ |
| data: data, |
| fromDataset: fromDataset, |
| seriesLayoutBy: seriesLayoutBy, |
| sourceFormat: sourceFormat, |
| dimensionsDefine: completeResult.dimensionsDefine, |
| startIndex: completeResult.startIndex, |
| dimensionsDetectCount: completeResult.dimensionsDetectCount, |
| encodeDefine: encodeDefine |
| }); |
| } // return {startIndex, dimensionsDefine, dimensionsCount} |
| |
| function completeBySourceData(data, sourceFormat, seriesLayoutBy, sourceHeader, dimensionsDefine) { |
| if (!data) { |
| return { |
| dimensionsDefine: normalizeDimensionsDefine(dimensionsDefine) |
| }; |
| } |
| |
| var dimensionsDetectCount; |
| var startIndex; |
| var findPotentialName; |
| |
| if (sourceFormat === SOURCE_FORMAT_ARRAY_ROWS) { |
| // Rule: Most of the first line are string: it is header. |
| // Caution: consider a line with 5 string and 1 number, |
| // it still can not be sure it is a head, because the |
| // 5 string may be 5 values of category columns. |
| if (sourceHeader === 'auto' || sourceHeader == null) { |
| arrayRowsTravelFirst(function (val) { |
| // '-' is regarded as null/undefined. |
| if (val != null && val !== '-') { |
| if (isString(val)) { |
| startIndex == null && (startIndex = 1); |
| } else { |
| startIndex = 0; |
| } |
| } // 10 is an experience number, avoid long loop. |
| |
| }, seriesLayoutBy, data, 10); |
| } else { |
| startIndex = sourceHeader ? 1 : 0; |
| } |
| |
| if (!dimensionsDefine && startIndex === 1) { |
| dimensionsDefine = []; |
| arrayRowsTravelFirst(function (val, index) { |
| dimensionsDefine[index] = val != null ? val : ''; |
| }, seriesLayoutBy, data); |
| } |
| |
| dimensionsDetectCount = dimensionsDefine ? dimensionsDefine.length : seriesLayoutBy === SERIES_LAYOUT_BY_ROW ? data.length : data[0] ? data[0].length : null; |
| } else if (sourceFormat === SOURCE_FORMAT_OBJECT_ROWS) { |
| if (!dimensionsDefine) { |
| dimensionsDefine = objectRowsCollectDimensions(data); |
| findPotentialName = true; |
| } |
| } else if (sourceFormat === SOURCE_FORMAT_KEYED_COLUMNS) { |
| if (!dimensionsDefine) { |
| dimensionsDefine = []; |
| findPotentialName = true; |
| each(data, function (colArr, key) { |
| dimensionsDefine.push(key); |
| }); |
| } |
| } else if (sourceFormat === SOURCE_FORMAT_ORIGINAL) { |
| var value0 = getDataItemValue(data[0]); |
| dimensionsDetectCount = isArray(value0) && value0.length || 1; |
| } else if (sourceFormat === SOURCE_FORMAT_TYPED_ARRAY) {} |
| |
| var potentialNameDimIndex; |
| |
| if (findPotentialName) { |
| each(dimensionsDefine, function (dim, idx) { |
| if ((isObject(dim) ? dim.name : dim) === 'name') { |
| potentialNameDimIndex = idx; |
| } |
| }); |
| } |
| |
| return { |
| startIndex: startIndex, |
| dimensionsDefine: normalizeDimensionsDefine(dimensionsDefine), |
| dimensionsDetectCount: dimensionsDetectCount, |
| potentialNameDimIndex: potentialNameDimIndex // TODO: potentialIdDimIdx |
| |
| }; |
| } // Consider dimensions defined like ['A', 'price', 'B', 'price', 'C', 'price'], |
| // which is reasonable. But dimension name is duplicated. |
| // Returns undefined or an array contains only object without null/undefiend or string. |
| |
| |
| function normalizeDimensionsDefine(dimensionsDefine) { |
| if (!dimensionsDefine) { |
| // The meaning of null/undefined is different from empty array. |
| return; |
| } |
| |
| var nameMap = createHashMap(); |
| return map(dimensionsDefine, function (item, index) { |
| item = extend({}, isObject(item) ? item : { |
| name: item |
| }); // User can set null in dimensions. |
| // We dont auto specify name, othewise a given name may |
| // cause it be refered unexpectedly. |
| |
| if (item.name == null) { |
| return item; |
| } // Also consider number form like 2012. |
| |
| |
| item.name += ''; // User may also specify displayName. |
| // displayName will always exists except user not |
| // specified or dim name is not specified or detected. |
| // (A auto generated dim name will not be used as |
| // displayName). |
| |
| if (item.displayName == null) { |
| item.displayName = item.name; |
| } |
| |
| var exist = nameMap.get(item.name); |
| |
| if (!exist) { |
| nameMap.set(item.name, { |
| count: 1 |
| }); |
| } else { |
| item.name += '-' + exist.count++; |
| } |
| |
| return item; |
| }); |
| } |
| |
| function arrayRowsTravelFirst(cb, seriesLayoutBy, data, maxLoop) { |
| maxLoop == null && (maxLoop = Infinity); |
| |
| if (seriesLayoutBy === SERIES_LAYOUT_BY_ROW) { |
| for (var i = 0; i < data.length && i < maxLoop; i++) { |
| cb(data[i] ? data[i][0] : null, i); |
| } |
| } else { |
| var value0 = data[0] || []; |
| |
| for (var i = 0; i < value0.length && i < maxLoop; i++) { |
| cb(value0[i], i); |
| } |
| } |
| } |
| |
| function objectRowsCollectDimensions(data) { |
| var firstIndex = 0; |
| var obj; |
| |
| while (firstIndex < data.length && !(obj = data[firstIndex++])) {} // jshint ignore: line |
| |
| |
| if (obj) { |
| var dimensions = []; |
| each(obj, function (value, key) { |
| dimensions.push(key); |
| }); |
| return dimensions; |
| } |
| } // ??? TODO merge to completedimensions, where also has |
| // default encode making logic. And the default rule |
| // should depends on series? consider 'map'. |
| |
| |
| function makeDefaultEncode(seriesModel, datasetModel, data, sourceFormat, seriesLayoutBy, completeResult) { |
| var coordSysDefine = getCoordSysDefineBySeries(seriesModel); |
| var encode = {}; // var encodeTooltip = []; |
| // var encodeLabel = []; |
| |
| var encodeItemName = []; |
| var encodeSeriesName = []; |
| var seriesType = seriesModel.subType; // ??? TODO refactor: provide by series itself. |
| // Consider the case: 'map' series is based on geo coordSys, |
| // 'graph', 'heatmap' can be based on cartesian. But can not |
| // give default rule simply here. |
| |
| var nSeriesMap = createHashMap(['pie', 'map', 'funnel']); |
| var cSeriesMap = createHashMap(['line', 'bar', 'pictorialBar', 'scatter', 'effectScatter', 'candlestick', 'boxplot']); // Usually in this case series will use the first data |
| // dimension as the "value" dimension, or other default |
| // processes respectively. |
| |
| if (coordSysDefine && cSeriesMap.get(seriesType) != null) { |
| var ecModel = seriesModel.ecModel; |
| var datasetMap = inner(ecModel).datasetMap; |
| var key = datasetModel.uid + '_' + seriesLayoutBy; |
| var datasetRecord = datasetMap.get(key) || datasetMap.set(key, { |
| categoryWayDim: 1, |
| valueWayDim: 0 |
| }); // TODO |
| // Auto detect first time axis and do arrangement. |
| |
| each(coordSysDefine.coordSysDims, function (coordDim) { |
| // In value way. |
| if (coordSysDefine.firstCategoryDimIndex == null) { |
| var dataDim = datasetRecord.valueWayDim++; |
| encode[coordDim] = dataDim; // ??? 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. |
| |
| encodeSeriesName.push(dataDim); // encodeTooltip.push(dataDim); |
| // encodeLabel.push(dataDim); |
| } // In category way, category axis. |
| else if (coordSysDefine.categoryAxisMap.get(coordDim)) { |
| encode[coordDim] = 0; |
| encodeItemName.push(0); |
| } // In category way, non-category axis. |
| else { |
| var dataDim = datasetRecord.categoryWayDim++; |
| encode[coordDim] = dataDim; // encodeTooltip.push(dataDim); |
| // encodeLabel.push(dataDim); |
| |
| encodeSeriesName.push(dataDim); |
| } |
| }); |
| } // Do not make a complex rule! Hard to code maintain and not necessary. |
| // ??? TODO refactor: provide by series itself. |
| // [{name: ..., value: ...}, ...] like: |
| else if (nSeriesMap.get(seriesType) != null) { |
| // Find the first not ordinal. (5 is an experience value) |
| var firstNotOrdinal; |
| |
| for (var i = 0; i < 5 && firstNotOrdinal == null; i++) { |
| if (!doGuessOrdinal(data, sourceFormat, seriesLayoutBy, completeResult.dimensionsDefine, completeResult.startIndex, i)) { |
| firstNotOrdinal = i; |
| } |
| } |
| |
| if (firstNotOrdinal != null) { |
| encode.value = firstNotOrdinal; |
| var nameDimIndex = completeResult.potentialNameDimIndex || Math.max(firstNotOrdinal - 1, 0); // By default, label use itemName in charts. |
| // So we dont set encodeLabel here. |
| |
| encodeSeriesName.push(nameDimIndex); |
| encodeItemName.push(nameDimIndex); // encodeTooltip.push(firstNotOrdinal); |
| } |
| } // encodeTooltip.length && (encode.tooltip = encodeTooltip); |
| // encodeLabel.length && (encode.label = encodeLabel); |
| |
| |
| encodeItemName.length && (encode.itemName = encodeItemName); |
| encodeSeriesName.length && (encode.seriesName = encodeSeriesName); |
| return encode; |
| } |
| /** |
| * If return null/undefined, indicate that should not use datasetModel. |
| */ |
| |
| |
| function getDatasetModel(seriesModel) { |
| var option = seriesModel.option; // 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. |
| |
| var thisData = option.data; |
| |
| if (!thisData) { |
| return seriesModel.ecModel.getComponent('dataset', option.datasetIndex || 0); |
| } |
| } |
| /** |
| * 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? |
| * |
| * @param {module:echars/data/Source} source |
| * @param {number} dimIndex |
| * @return {boolean} Whether ordinal. |
| */ |
| |
| |
| export function guessOrdinal(source, dimIndex) { |
| return doGuessOrdinal(source.data, source.sourceFormat, source.seriesLayoutBy, source.dimensionsDefine, source.startIndex, dimIndex); |
| } // dimIndex may be overflow source data. |
| |
| function doGuessOrdinal(data, sourceFormat, seriesLayoutBy, dimensionsDefine, startIndex, dimIndex) { |
| var result; // Experience value. |
| |
| var maxLoop = 5; |
| |
| if (isTypedArray(data)) { |
| return false; |
| } // When sourceType is 'objectRows' or 'keyedColumns', dimensionsDefine |
| // always exists in source. |
| |
| |
| var dimName; |
| |
| if (dimensionsDefine) { |
| dimName = dimensionsDefine[dimIndex]; |
| dimName = isObject(dimName) ? dimName.name : dimName; |
| } |
| |
| if (sourceFormat === SOURCE_FORMAT_ARRAY_ROWS) { |
| if (seriesLayoutBy === SERIES_LAYOUT_BY_ROW) { |
| var sample = data[dimIndex]; |
| |
| for (var i = 0; i < (sample || []).length && i < maxLoop; i++) { |
| if ((result = detectValue(sample[startIndex + i])) != null) { |
| return result; |
| } |
| } |
| } else { |
| for (var i = 0; i < data.length && i < maxLoop; i++) { |
| var row = data[startIndex + i]; |
| |
| if (row && (result = detectValue(row[dimIndex])) != null) { |
| return result; |
| } |
| } |
| } |
| } else if (sourceFormat === SOURCE_FORMAT_OBJECT_ROWS) { |
| if (!dimName) { |
| return; |
| } |
| |
| for (var i = 0; i < data.length && i < maxLoop; i++) { |
| var item = data[i]; |
| |
| if (item && (result = detectValue(item[dimName])) != null) { |
| return result; |
| } |
| } |
| } else if (sourceFormat === SOURCE_FORMAT_KEYED_COLUMNS) { |
| if (!dimName) { |
| return; |
| } |
| |
| var sample = data[dimName]; |
| |
| if (!sample || isTypedArray(sample)) { |
| return false; |
| } |
| |
| for (var i = 0; i < sample.length && i < maxLoop; i++) { |
| if ((result = detectValue(sample[i])) != null) { |
| return result; |
| } |
| } |
| } else if (sourceFormat === SOURCE_FORMAT_ORIGINAL) { |
| for (var i = 0; i < data.length && i < maxLoop; i++) { |
| var item = data[i]; |
| var val = getDataItemValue(item); |
| |
| if (!isArray(val)) { |
| return false; |
| } |
| |
| if ((result = detectValue(val[dimIndex])) != null) { |
| return result; |
| } |
| } |
| } |
| |
| function detectValue(val) { |
| // Consider usage convenience, '1', '2' will be treated as "number". |
| // `isFinit('')` get `true`. |
| if (val != null && isFinite(val) && val !== '') { |
| return false; |
| } else if (isString(val) && val !== '-') { |
| return true; |
| } |
| } |
| |
| return false; |
| } |