/** * List for data storage * @module echarts/data/List */ define(function (require) { var UNDEFINED = 'undefined'; var globalObj = typeof window === 'undefined' ? global : window; var Float64Array = typeof globalObj.Float64Array === UNDEFINED ? Array : globalObj.Float64Array; var Int32Array = typeof globalObj.Int32Array === UNDEFINED ? Array : globalObj.Int32Array; var dataCtors = { 'float': Float64Array, 'int': Int32Array, // Ordinal data type can be string or int 'ordinal': Array, 'number': Array, 'time': Array, 'unknown': Array }; var Model = require('../model/Model'); var DataDiffer = require('./DataDiffer'); var zrUtil = require('zrender/core/util'); var modelUtil = require('../util/model'); var isObject = zrUtil.isObject; var IMMUTABLE_PROPERTIES = [ 'stackedOn', '_nameList', '_idList', '_rawData' ]; var transferImmuProperties = function (a, b) { zrUtil.each(IMMUTABLE_PROPERTIES.concat(b.__wrappedMethods || []), function (propName) { if (b.hasOwnProperty(propName)) { a[propName] = b[propName]; } }); a.__wrappedMethods = b.__wrappedMethods; }; /** * @constructor * @alias module:echarts/data/List * * @param {Array.} dimensions * Dimensions should be concrete names like x, y, z, lng, lat, angle, radius * @param {module:echarts/model/Model} hostModel */ var List = function (dimensions, hostModel) { dimensions = dimensions || ['x', 'y']; var dimensionInfos = {}; var dimensionNames = []; for (var i = 0; i < dimensions.length; i++) { var dimensionName; var dimensionInfo = {}; if (typeof dimensions[i] === 'string') { dimensionName = dimensions[i]; dimensionInfo = { name: dimensionName, stackable: false, // Type can be 'float', 'int', 'number' // Default is number, Precision of float may not enough type: 'number' }; } else { dimensionInfo = dimensions[i]; dimensionName = dimensionInfo.name; dimensionInfo.type = dimensionInfo.type || 'number'; } dimensionNames.push(dimensionName); dimensionInfos[dimensionName] = dimensionInfo; } /** * @readOnly * @type {Array.} */ this.dimensions = dimensionNames; /** * Infomation of each data dimension, like data type. * @type {Object} */ this._dimensionInfos = dimensionInfos; /** * @type {module:echarts/model/Model} */ this.hostModel = hostModel; /** * @type {module:echarts/model/Model} */ this.dataType; /** * Indices stores the indices of data subset after filtered. * This data subset will be used in chart. * @type {Array.} * @readOnly */ this.indices = []; /** * Data storage * @type {Object.} * @private */ this._storage = {}; /** * @type {Array.} */ this._nameList = []; /** * @type {Array.} */ this._idList = []; /** * Models of data option is stored sparse for optimizing memory cost * @type {Array.} * @private */ this._optionModels = []; /** * @param {module:echarts/data/List} */ this.stackedOn = null; /** * Global visual properties after visual coding * @type {Object} * @private */ this._visual = {}; /** * Globel layout properties. * @type {Object} * @private */ this._layout = {}; /** * Item visual properties after visual coding * @type {Array.} * @private */ this._itemVisuals = []; /** * Item layout properties after layout * @type {Array.} * @private */ this._itemLayouts = []; /** * Graphic elemnents * @type {Array.} * @private */ this._graphicEls = []; /** * @type {Array.} * @private */ this._rawData; /** * @type {Object} * @private */ this._extent; }; var listProto = List.prototype; listProto.type = 'list'; /** * Get dimension name * @param {string|number} dim * Dimension can be concrete names like x, y, z, lng, lat, angle, radius * Or a ordinal number. For example getDimensionInfo(0) will return 'x' or 'lng' or 'radius' * @return {string} Concrete dim name. */ listProto.getDimension = function (dim) { if (!isNaN(dim)) { dim = this.dimensions[dim] || dim; } return dim; }; /** * Get type and stackable info of particular dimension * @param {string|number} dim * Dimension can be concrete names like x, y, z, lng, lat, angle, radius * Or a ordinal number. For example getDimensionInfo(0) will return 'x' or 'lng' or 'radius' */ listProto.getDimensionInfo = function (dim) { return zrUtil.clone(this._dimensionInfos[this.getDimension(dim)]); }; /** * Initialize from data * @param {Array.} data * @param {Array.} [nameList] * @param {Function} [dimValueGetter] (dataItem, dimName, dataIndex, dimIndex) => number */ listProto.initData = function (data, nameList, dimValueGetter) { data = data || []; if (!zrUtil.isArray(data)) { throw new Error('Invalid data.'); } this._rawData = data; // Clear var storage = this._storage = {}; var indices = this.indices = []; var dimensions = this.dimensions; var size = data.length; var dimensionInfoMap = this._dimensionInfos; var idList = []; var nameRepeatCount = {}; nameList = nameList || []; // Init storage for (var i = 0; i < dimensions.length; i++) { var dimInfo = dimensionInfoMap[dimensions[i]]; var DataCtor = dataCtors[dimInfo.type]; storage[dimensions[i]] = new DataCtor(size); } // Default dim value getter dimValueGetter = dimValueGetter || function (dataItem, dimName, dataIndex, dimIndex) { var value = modelUtil.getDataItemValue(dataItem); return modelUtil.converDataValue( zrUtil.isArray(value) ? value[dimIndex] // If value is a single number or something else not array. : value, dimensionInfoMap[dimName] ); }; for (var idx = 0; idx < data.length; idx++) { var dataItem = data[idx]; // Each data item is value // [1, 2] // 2 // Bar chart, line chart which uses category axis // only gives the 'y' value. 'x' value is the indices of cateogry // Use a tempValue to normalize the value to be a (x, y) value // Store the data by dimensions for (var k = 0; k < dimensions.length; k++) { var dim = dimensions[k]; var dimStorage = storage[dim]; // PENDING NULL is empty or zero dimStorage[idx] = dimValueGetter(dataItem, dim, idx, k); } indices.push(idx); } // Use the name in option and create id for (var i = 0; i < data.length; i++) { var id = ''; if (!nameList[i]) { nameList[i] = data[i].name; // Try using the id in option id = data[i].id; } var name = nameList[i] || ''; if (!id && name) { // Use name as id and add counter to avoid same name nameRepeatCount[name] = nameRepeatCount[name] || 0; id = name; if (nameRepeatCount[name] > 0) { id += '__ec__' + nameRepeatCount[name]; } nameRepeatCount[name]++; } id && (idList[i] = id); } this._nameList = nameList; this._idList = idList; }; /** * @return {number} */ listProto.count = function () { return this.indices.length; }; /** * Get value. Return NaN if idx is out of range. * @param {string} dim Dim must be concrete name. * @param {number} idx * @param {boolean} stack * @return {number} */ listProto.get = function (dim, idx, stack) { var storage = this._storage; var dataIndex = this.indices[idx]; // If value not exists if (dataIndex == null) { return NaN; } var value = storage[dim] && storage[dim][dataIndex]; // FIXME ordinal data type is not stackable if (stack) { var dimensionInfo = this._dimensionInfos[dim]; if (dimensionInfo && dimensionInfo.stackable) { var stackedOn = this.stackedOn; while (stackedOn) { // Get no stacked data of stacked on var stackedValue = stackedOn.get(dim, idx); // Considering positive stack, negative stack and empty data if ((value >= 0 && stackedValue > 0) // Positive stack || (value <= 0 && stackedValue < 0) // Negative stack ) { value += stackedValue; } stackedOn = stackedOn.stackedOn; } } } return value; }; /** * Get value for multi dimensions. * @param {Array.} [dimensions] If ignored, using all dimensions. * @param {number} idx * @param {boolean} stack * @return {number} */ listProto.getValues = function (dimensions, idx, stack) { var values = []; if (!zrUtil.isArray(dimensions)) { stack = idx; idx = dimensions; dimensions = this.dimensions; } for (var i = 0, len = dimensions.length; i < len; i++) { values.push(this.get(dimensions[i], idx, stack)); } return values; }; /** * If value is NaN. Inlcuding '-' * @param {string} dim * @param {number} idx * @return {number} */ listProto.hasValue = function (idx) { var dimensions = this.dimensions; var dimensionInfos = this._dimensionInfos; for (var i = 0, len = dimensions.length; i < len; i++) { if ( // Ordinal type can be string or number dimensionInfos[dimensions[i]].type !== 'ordinal' && isNaN(this.get(dimensions[i], idx)) ) { return false; } } return true; }; /** * Get extent of data in one dimension * @param {string} dim * @param {boolean} stack */ listProto.getDataExtent = function (dim, stack) { var dimData = this._storage[dim]; var dimInfo = this.getDimensionInfo(dim); stack = (dimInfo && dimInfo.stackable) && stack; var dimExtent = (this._extent || (this._extent = {}))[dim + (!!stack)]; var value; if (dimExtent) { return dimExtent; } // var dimInfo = this._dimensionInfos[dim]; if (dimData) { var min = Infinity; var max = -Infinity; // var isOrdinal = dimInfo.type === 'ordinal'; for (var i = 0, len = this.count(); i < len; i++) { value = this.get(dim, i, stack); // FIXME // if (isOrdinal && typeof value === 'string') { // value = zrUtil.indexOf(dimData, value); // } value < min && (min = value); value > max && (max = value); } return (this._extent[dim + stack] = [min, max]); } else { return [Infinity, -Infinity]; } }; /** * Get sum of data in one dimension * @param {string} dim * @param {boolean} stack */ listProto.getSum = function (dim, stack) { var dimData = this._storage[dim]; var sum = 0; if (dimData) { for (var i = 0, len = this.count(); i < len; i++) { var value = this.get(dim, i, stack); if (!isNaN(value)) { sum += value; } } } return sum; }; /** * Retreive the index with given value * @param {number} idx * @param {number} value * @return {number} */ // FIXME Precision of float value listProto.indexOf = function (dim, value) { var storage = this._storage; var dimData = storage[dim]; var indices = this.indices; if (dimData) { for (var i = 0, len = indices.length; i < len; i++) { var rawIndex = indices[i]; if (dimData[rawIndex] === value) { return i; } } } return -1; }; /** * Retreive the index with given name * @param {number} idx * @param {number} name * @return {number} */ listProto.indexOfName = function (name) { var indices = this.indices; var nameList = this._nameList; for (var i = 0, len = indices.length; i < len; i++) { var rawIndex = indices[i]; if (nameList[rawIndex] === name) { return i; } } return -1; }; /** * Retreive the index of nearest value * @param {string} dim * @param {number} value * @param {boolean} stack If given value is after stacked * @return {number} */ listProto.indexOfNearest = function (dim, value, stack) { var storage = this._storage; var dimData = storage[dim]; if (dimData) { var minDist = Number.MAX_VALUE; var nearestIdx = -1; for (var i = 0, len = this.count(); i < len; i++) { var diff = value - this.get(dim, i, stack); var dist = Math.abs(diff); if (dist < minDist // For the case of two data are same on xAxis, which has sequence data. // Show the nearest index // https://github.com/ecomfe/echarts/issues/2869 || (dist === minDist && diff > 0) ) { minDist = dist; nearestIdx = i; } } return nearestIdx; } return -1; }; /** * Get raw data index * @param {number} idx * @return {number} */ listProto.getRawIndex = function (idx) { var rawIdx = this.indices[idx]; return rawIdx == null ? -1 : rawIdx; }; /** * Get raw data item * @param {number} idx * @return {number} */ listProto.getRawDataItem = function (idx) { return this._rawData[this.getRawIndex(idx)]; }; /** * @param {number} idx * @param {boolean} [notDefaultIdx=false] * @return {string} */ listProto.getName = function (idx) { return this._nameList[this.indices[idx]] || ''; }; /** * @param {number} idx * @param {boolean} [notDefaultIdx=false] * @return {string} */ listProto.getId = function (idx) { return this._idList[this.indices[idx]] || (this.getRawIndex(idx) + ''); }; function normalizeDimensions(dimensions) { if (!zrUtil.isArray(dimensions)) { dimensions = [dimensions]; } return dimensions; } /** * Data iteration * @param {string|Array.} * @param {Function} cb * @param {boolean} [stack=false] * @param {*} [context=this] * * @example * list.each('x', function (x, idx) {}); * list.each(['x', 'y'], function (x, y, idx) {}); * list.each(function (idx) {}) */ listProto.each = function (dimensions, cb, stack, context) { if (typeof dimensions === 'function') { context = stack; stack = cb; cb = dimensions; dimensions = []; } dimensions = zrUtil.map( normalizeDimensions(dimensions), this.getDimension, this ); var value = []; var dimSize = dimensions.length; var indices = this.indices; context = context || this; for (var i = 0; i < indices.length; i++) { if (dimSize === 0) { cb.call(context, i); } // Simple optimization else if (dimSize === 1) { cb.call(context, this.get(dimensions[0], i, stack), i); } else { for (var k = 0; k < dimSize; k++) { value[k] = this.get(dimensions[k], i, stack); } // Index value[k] = i; cb.apply(context, value); } } }; /** * Data filter * @param {string|Array.} * @param {Function} cb * @param {boolean} [stack=false] * @param {*} [context=this] */ listProto.filterSelf = function (dimensions, cb, stack, context) { if (typeof dimensions === 'function') { context = stack; stack = cb; cb = dimensions; dimensions = []; } dimensions = zrUtil.map( normalizeDimensions(dimensions), this.getDimension, this ); var newIndices = []; var value = []; var dimSize = dimensions.length; var indices = this.indices; context = context || this; for (var i = 0; i < indices.length; i++) { var keep; // Simple optimization if (dimSize === 1) { keep = cb.call( context, this.get(dimensions[0], i, stack), i ); } else { for (var k = 0; k < dimSize; k++) { value[k] = this.get(dimensions[k], i, stack); } value[k] = i; keep = cb.apply(context, value); } if (keep) { newIndices.push(indices[i]); } } this.indices = newIndices; // Reset data extent this._extent = {}; return this; }; /** * Data mapping to a plain array * @param {string|Array.} [dimensions] * @param {Function} cb * @param {boolean} [stack=false] * @param {*} [context=this] * @return {Array} */ listProto.mapArray = function (dimensions, cb, stack, context) { if (typeof dimensions === 'function') { context = stack; stack = cb; cb = dimensions; dimensions = []; } var result = []; this.each(dimensions, function () { result.push(cb && cb.apply(this, arguments)); }, stack, context); return result; }; function cloneListForMapAndSample(original, excludeDimensions) { var allDimensions = original.dimensions; var list = new List( zrUtil.map(allDimensions, original.getDimensionInfo, original), original.hostModel ); // FIXME If needs stackedOn, value may already been stacked transferImmuProperties(list, original); var storage = list._storage = {}; var originalStorage = original._storage; // Init storage for (var i = 0; i < allDimensions.length; i++) { var dim = allDimensions[i]; var dimStore = originalStorage[dim]; if (zrUtil.indexOf(excludeDimensions, dim) >= 0) { storage[dim] = new dimStore.constructor( originalStorage[dim].length ); } else { // Direct reference for other dimensions storage[dim] = originalStorage[dim]; } } return list; } /** * Data mapping to a new List with given dimensions * @param {string|Array.} dimensions * @param {Function} cb * @param {boolean} [stack=false] * @param {*} [context=this] * @return {Array} */ listProto.map = function (dimensions, cb, stack, context) { dimensions = zrUtil.map( normalizeDimensions(dimensions), this.getDimension, this ); var list = cloneListForMapAndSample(this, dimensions); // Following properties are all immutable. // So we can reference to the same value var indices = list.indices = this.indices; var storage = list._storage; var tmpRetValue = []; this.each(dimensions, function () { var idx = arguments[arguments.length - 1]; var retValue = cb && cb.apply(this, arguments); if (retValue != null) { // a number if (typeof retValue === 'number') { tmpRetValue[0] = retValue; retValue = tmpRetValue; } for (var i = 0; i < retValue.length; i++) { var dim = dimensions[i]; var dimStore = storage[dim]; var rawIdx = indices[idx]; if (dimStore) { dimStore[rawIdx] = retValue[i]; } } } }, stack, context); return list; }; /** * Large data down sampling on given dimension * @param {string} dimension * @param {number} rate * @param {Function} sampleValue * @param {Function} sampleIndex Sample index for name and id */ listProto.downSample = function (dimension, rate, sampleValue, sampleIndex) { var list = cloneListForMapAndSample(this, [dimension]); var storage = this._storage; var targetStorage = list._storage; var originalIndices = this.indices; var indices = list.indices = []; var frameValues = []; var frameIndices = []; var frameSize = Math.floor(1 / rate); var dimStore = targetStorage[dimension]; var len = this.count(); // Copy data from original data for (var i = 0; i < storage[dimension].length; i++) { targetStorage[dimension][i] = storage[dimension][i]; } for (var i = 0; i < len; i += frameSize) { // Last frame if (frameSize > len - i) { frameSize = len - i; frameValues.length = frameSize; } for (var k = 0; k < frameSize; k++) { var idx = originalIndices[i + k]; frameValues[k] = dimStore[idx]; frameIndices[k] = idx; } var value = sampleValue(frameValues); var idx = frameIndices[sampleIndex(frameValues, value) || 0]; // Only write value on the filtered data dimStore[idx] = value; indices.push(idx); } return list; }; /** * Get model of one data item. * * @param {number} idx */ // FIXME Model proxy ? listProto.getItemModel = function (idx) { var hostModel = this.hostModel; idx = this.indices[idx]; return new Model(this._rawData[idx], hostModel, hostModel && hostModel.ecModel); }; /** * Create a data differ * @param {module:echarts/data/List} otherList * @return {module:echarts/data/DataDiffer} */ listProto.diff = function (otherList) { var idList = this._idList; var otherIdList = otherList && otherList._idList; return new DataDiffer( otherList ? otherList.indices : [], this.indices, function (idx) { return otherIdList[idx] || (idx + ''); }, function (idx) { return idList[idx] || (idx + ''); } ); }; /** * Get visual property. * @param {string} key */ listProto.getVisual = function (key) { var visual = this._visual; return visual && visual[key]; }; /** * Set visual property * @param {string|Object} key * @param {*} [value] * * @example * setVisual('color', color); * setVisual({ * 'color': color * }); */ listProto.setVisual = function (key, val) { if (isObject(key)) { for (var name in key) { if (key.hasOwnProperty(name)) { this.setVisual(name, key[name]); } } return; } this._visual = this._visual || {}; this._visual[key] = val; }; /** * Set layout property. * @param {string} key * @param {*} [val] */ listProto.setLayout = function (key, val) { if (isObject(key)) { for (var name in key) { if (key.hasOwnProperty(name)) { this.setLayout(name, key[name]); } } return; } this._layout[key] = val; }; /** * Get layout property. * @param {string} key. * @return {*} */ listProto.getLayout = function (key) { return this._layout[key]; }; /** * Get layout of single data item * @param {number} idx */ listProto.getItemLayout = function (idx) { return this._itemLayouts[idx]; }; /** * Set layout of single data item * @param {number} idx * @param {Object} layout * @param {boolean=} [merge=false] */ listProto.setItemLayout = function (idx, layout, merge) { this._itemLayouts[idx] = merge ? zrUtil.extend(this._itemLayouts[idx] || {}, layout) : layout; }; /** * Clear all layout of single data item */ listProto.clearItemLayouts = function () { this._itemLayouts.length = 0; }; /** * Get visual property of single data item * @param {number} idx * @param {string} key * @param {boolean} ignoreParent */ listProto.getItemVisual = function (idx, key, ignoreParent) { var itemVisual = this._itemVisuals[idx]; var val = itemVisual && itemVisual[key]; if (val == null && !ignoreParent) { // Use global visual property return this.getVisual(key); } return val; }; /** * Set visual property of single data item * * @param {number} idx * @param {string|Object} key * @param {*} [value] * * @example * setItemVisual(0, 'color', color); * setItemVisual(0, { * 'color': color * }); */ listProto.setItemVisual = function (idx, key, value) { var itemVisual = this._itemVisuals[idx] || {}; this._itemVisuals[idx] = itemVisual; if (isObject(key)) { for (var name in key) { if (key.hasOwnProperty(name)) { itemVisual[name] = key[name]; } } return; } itemVisual[key] = value; }; var setItemDataAndSeriesIndex = function (child) { child.seriesIndex = this.seriesIndex; child.dataIndex = this.dataIndex; child.dataType = this.dataType; }; /** * Set graphic element relative to data. It can be set as null * @param {number} idx * @param {module:zrender/Element} [el] */ listProto.setItemGraphicEl = function (idx, el) { var hostModel = this.hostModel; if (el) { // Add data index and series index for indexing the data by element // Useful in tooltip el.dataIndex = idx; el.dataType = this.dataType; el.seriesIndex = hostModel && hostModel.seriesIndex; if (el.type === 'group') { el.traverse(setItemDataAndSeriesIndex, el); } } this._graphicEls[idx] = el; }; /** * @param {number} idx * @return {module:zrender/Element} */ listProto.getItemGraphicEl = function (idx) { return this._graphicEls[idx]; }; /** * @param {Function} cb * @param {*} context */ listProto.eachItemGraphicEl = function (cb, context) { zrUtil.each(this._graphicEls, function (el, idx) { if (el) { cb && cb.call(context, el, idx); } }); }; /** * Shallow clone a new list except visual and layout properties, and graph elements. * New list only change the indices. */ listProto.cloneShallow = function () { var dimensionInfoList = zrUtil.map(this.dimensions, this.getDimensionInfo, this); var list = new List(dimensionInfoList, this.hostModel); // FIXME list._storage = this._storage; transferImmuProperties(list, this); list.indices = this.indices.slice(); return list; }; /** * Wrap some method to add more feature * @param {string} methodName * @param {Function} injectFunction */ listProto.wrapMethod = function (methodName, injectFunction) { var originalMethod = this[methodName]; if (typeof originalMethod !== 'function') { return; } this.__wrappedMethods = this.__wrappedMethods || []; this.__wrappedMethods.push(methodName); this[methodName] = function () { var res = originalMethod.apply(this, arguments); return injectFunction.apply(this, [res].concat(zrUtil.slice(arguments))); }; }; // Methods that create a new list based on this list should be listed here. // Notice that those method should `RETURN` the new list. listProto.TRANSFERABLE_METHODS = ['cloneShallow', 'downSample', 'map']; // Methods that change indices of this list should be listed here. listProto.CHANGABLE_METHODS = ['filterSelf']; return List; });