| /* |
| * 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 * as zrUtil from 'zrender/src/core/util'; |
| import env from 'zrender/src/core/env'; |
| import { formatTime, encodeHTML, addCommas, getTooltipMarker } from '../util/format'; |
| import * as modelUtil from '../util/model'; |
| import ComponentModel from './Component'; |
| import colorPaletteMixin from './mixin/colorPalette'; |
| import dataFormatMixin from '../model/mixin/dataFormat'; |
| import { getLayoutParams, mergeLayoutParam } from '../util/layout'; |
| import { createTask } from '../stream/task'; |
| import { prepareSource, getSource } from '../data/helper/sourceHelper'; |
| import { retrieveRawValue } from '../data/helper/dataProvider'; |
| var inner = modelUtil.makeInner(); |
| var SeriesModel = ComponentModel.extend({ |
| type: 'series.__base__', |
| |
| /** |
| * @readOnly |
| */ |
| seriesIndex: 0, |
| // coodinateSystem will be injected in the echarts/CoordinateSystem |
| coordinateSystem: null, |
| |
| /** |
| * @type {Object} |
| * @protected |
| */ |
| defaultOption: null, |
| |
| /** |
| * Data provided for legend |
| * @type {Function} |
| */ |
| // PENDING |
| legendDataProvider: null, |
| |
| /** |
| * Access path of color for visual |
| */ |
| visualColorAccessPath: 'itemStyle.color', |
| |
| /** |
| * Support merge layout params. |
| * Only support 'box' now (left/right/top/bottom/width/height). |
| * @type {string|Object} Object can be {ignoreSize: true} |
| * @readOnly |
| */ |
| layoutMode: null, |
| init: function (option, parentModel, ecModel, extraOpt) { |
| /** |
| * @type {number} |
| * @readOnly |
| */ |
| this.seriesIndex = this.componentIndex; |
| this.dataTask = createTask({ |
| count: dataTaskCount, |
| reset: dataTaskReset |
| }); |
| this.dataTask.context = { |
| model: this |
| }; |
| this.mergeDefaultAndTheme(option, ecModel); |
| prepareSource(this); |
| var data = this.getInitialData(option, ecModel); |
| wrapData(data, this); |
| this.dataTask.context.data = data; |
| |
| /** |
| * @type {module:echarts/data/List|module:echarts/data/Tree|module:echarts/data/Graph} |
| * @private |
| */ |
| inner(this).dataBeforeProcessed = data; // If we reverse the order (make data firstly, and then make |
| // dataBeforeProcessed by cloneShallow), cloneShallow will |
| // cause data.graph.data !== data when using |
| // module:echarts/data/Graph or module:echarts/data/Tree. |
| // See module:echarts/data/helper/linkList |
| // Theoretically, it is unreasonable to call `seriesModel.getData()` in the model |
| // init or merge stage, because the data can be restored. So we do not `restoreData` |
| // and `setData` here, which forbids calling `seriesModel.getData()` in this stage. |
| // Call `seriesModel.getRawData()` instead. |
| // this.restoreData(); |
| |
| autoSeriesName(this); |
| }, |
| |
| /** |
| * Util for merge default and theme to option |
| * @param {Object} option |
| * @param {module:echarts/model/Global} ecModel |
| */ |
| mergeDefaultAndTheme: function (option, ecModel) { |
| var layoutMode = this.layoutMode; |
| var inputPositionParams = layoutMode ? getLayoutParams(option) : {}; // Backward compat: using subType on theme. |
| // But if name duplicate between series subType |
| // (for example: parallel) add component mainType, |
| // add suffix 'Series'. |
| |
| var themeSubType = this.subType; |
| |
| if (ComponentModel.hasClass(themeSubType)) { |
| themeSubType += 'Series'; |
| } |
| |
| zrUtil.merge(option, ecModel.getTheme().get(this.subType)); |
| zrUtil.merge(option, this.getDefaultOption()); // Default label emphasis `show` |
| |
| modelUtil.defaultEmphasis(option, 'label', ['show']); |
| this.fillDataTextStyle(option.data); |
| |
| if (layoutMode) { |
| mergeLayoutParam(option, inputPositionParams, layoutMode); |
| } |
| }, |
| mergeOption: function (newSeriesOption, ecModel) { |
| // this.settingTask.dirty(); |
| newSeriesOption = zrUtil.merge(this.option, newSeriesOption, true); |
| this.fillDataTextStyle(newSeriesOption.data); |
| var layoutMode = this.layoutMode; |
| |
| if (layoutMode) { |
| mergeLayoutParam(this.option, newSeriesOption, layoutMode); |
| } |
| |
| prepareSource(this); |
| var data = this.getInitialData(newSeriesOption, ecModel); |
| wrapData(data, this); |
| this.dataTask.dirty(); |
| this.dataTask.context.data = data; |
| inner(this).dataBeforeProcessed = data; |
| autoSeriesName(this); |
| }, |
| fillDataTextStyle: function (data) { |
| // Default data label emphasis `show` |
| // FIXME Tree structure data ? |
| // FIXME Performance ? |
| if (data && !zrUtil.isTypedArray(data)) { |
| var props = ['show']; |
| |
| for (var i = 0; i < data.length; i++) { |
| if (data[i] && data[i].label) { |
| modelUtil.defaultEmphasis(data[i], 'label', props); |
| } |
| } |
| } |
| }, |
| |
| /** |
| * Init a data structure from data related option in series |
| * Must be overwritten |
| */ |
| getInitialData: function () {}, |
| |
| /** |
| * Append data to list |
| * @param {Object} params |
| * @param {Array|TypedArray} params.data |
| */ |
| appendData: function (params) { |
| // FIXME ??? |
| // (1) If data from dataset, forbidden append. |
| // (2) support append data of dataset. |
| var data = this.getRawData(); |
| data.appendData(params.data); |
| }, |
| |
| /** |
| * Consider some method like `filter`, `map` need make new data, |
| * We should make sure that `seriesModel.getData()` get correct |
| * data in the stream procedure. So we fetch data from upstream |
| * each time `task.perform` called. |
| * @param {string} [dataType] |
| * @return {module:echarts/data/List} |
| */ |
| getData: function (dataType) { |
| var task = getCurrentTask(this); |
| |
| if (task) { |
| var data = task.context.data; |
| return dataType == null ? data : data.getLinkedData(dataType); |
| } else { |
| // When series is not alive (that may happen when click toolbox |
| // restore or setOption with not merge mode), series data may |
| // be still need to judge animation or something when graphic |
| // elements want to know whether fade out. |
| return inner(this).data; |
| } |
| }, |
| |
| /** |
| * @param {module:echarts/data/List} data |
| */ |
| setData: function (data) { |
| var task = getCurrentTask(this); |
| |
| if (task) { |
| var context = task.context; // Consider case: filter, data sample. |
| |
| if (context.data !== data && task.modifyOutputEnd) { |
| task.setOutputEnd(data.count()); |
| } |
| |
| context.outputData = data; // Caution: setData should update context.data, |
| // Because getData may be called multiply in a |
| // single stage and expect to get the data just |
| // set. (For example, AxisProxy, x y both call |
| // getData and setDate sequentially). |
| // So the context.data should be fetched from |
| // upstream each time when a stage starts to be |
| // performed. |
| |
| if (task !== this.dataTask) { |
| context.data = data; |
| } |
| } |
| |
| inner(this).data = data; |
| }, |
| |
| /** |
| * @see {module:echarts/data/helper/sourceHelper#getSource} |
| * @return {module:echarts/data/Source} source |
| */ |
| getSource: function () { |
| return getSource(this); |
| }, |
| |
| /** |
| * Get data before processed |
| * @return {module:echarts/data/List} |
| */ |
| getRawData: function () { |
| return inner(this).dataBeforeProcessed; |
| }, |
| |
| /** |
| * Get base axis if has coordinate system and has axis. |
| * By default use coordSys.getBaseAxis(); |
| * Can be overrided for some chart. |
| * @return {type} description |
| */ |
| getBaseAxis: function () { |
| var coordSys = this.coordinateSystem; |
| return coordSys && coordSys.getBaseAxis && coordSys.getBaseAxis(); |
| }, |
| // FIXME |
| |
| /** |
| * Default tooltip formatter |
| * |
| * @param {number} dataIndex |
| * @param {boolean} [multipleSeries=false] |
| * @param {number} [dataType] |
| * @param {string} [renderMode='html'] valid values: 'html' and 'richText'. |
| * 'html' is used for rendering tooltip in extra DOM form, and the result |
| * string is used as DOM HTML content. |
| * 'richText' is used for rendering tooltip in rich text form, for those where |
| * DOM operation is not supported. |
| * @return {Object} formatted tooltip with `html` and `markers` |
| */ |
| formatTooltip: function (dataIndex, multipleSeries, dataType, renderMode) { |
| var series = this; |
| renderMode = renderMode || 'html'; |
| var newLine = renderMode === 'html' ? '<br/>' : '\n'; |
| var isRichText = renderMode === 'richText'; |
| var markers = {}; |
| var markerId = 0; |
| |
| function formatArrayValue(value) { |
| // ??? TODO refactor these logic. |
| // check: category-no-encode-has-axis-data in dataset.html |
| var vertially = zrUtil.reduce(value, function (vertially, val, idx) { |
| var dimItem = data.getDimensionInfo(idx); |
| return vertially |= dimItem && dimItem.tooltip !== false && dimItem.displayName != null; |
| }, 0); |
| var result = []; |
| tooltipDims.length ? zrUtil.each(tooltipDims, function (dim) { |
| setEachItem(retrieveRawValue(data, dataIndex, dim), dim); |
| }) // By default, all dims is used on tooltip. |
| : zrUtil.each(value, setEachItem); |
| |
| function setEachItem(val, dim) { |
| var dimInfo = data.getDimensionInfo(dim); // If `dimInfo.tooltip` is not set, show tooltip. |
| |
| if (!dimInfo || dimInfo.otherDims.tooltip === false) { |
| return; |
| } |
| |
| var dimType = dimInfo.type; |
| var markName = 'sub' + series.seriesIndex + 'at' + markerId; |
| var dimHead = getTooltipMarker({ |
| color: color, |
| type: 'subItem', |
| renderMode: renderMode, |
| markerId: markName |
| }); |
| var dimHeadStr = typeof dimHead === 'string' ? dimHead : dimHead.content; |
| var valStr = (vertially ? dimHeadStr + encodeHTML(dimInfo.displayName || '-') + ': ' : '') + // FIXME should not format time for raw data? |
| encodeHTML(dimType === 'ordinal' ? val + '' : dimType === 'time' ? multipleSeries ? '' : formatTime('yyyy/MM/dd hh:mm:ss', val) : addCommas(val)); |
| valStr && result.push(valStr); |
| |
| if (isRichText) { |
| markers[markName] = color; |
| ++markerId; |
| } |
| } |
| |
| var newLine = vertially ? isRichText ? '\n' : '<br/>' : ''; |
| var content = newLine + result.join(newLine || ', '); |
| return { |
| renderMode: renderMode, |
| content: content, |
| style: markers |
| }; |
| } |
| |
| function formatSingleValue(val) { |
| // return encodeHTML(addCommas(val)); |
| return { |
| renderMode: renderMode, |
| content: encodeHTML(addCommas(val)), |
| style: markers |
| }; |
| } |
| |
| var data = this.getData(); |
| var tooltipDims = data.mapDimension('defaultedTooltip', true); |
| var tooltipDimLen = tooltipDims.length; |
| var value = this.getRawValue(dataIndex); |
| var isValueArr = zrUtil.isArray(value); |
| var color = data.getItemVisual(dataIndex, 'color'); |
| |
| if (zrUtil.isObject(color) && color.colorStops) { |
| color = (color.colorStops[0] || {}).color; |
| } |
| |
| color = color || 'transparent'; // Complicated rule for pretty tooltip. |
| |
| var formattedValue = tooltipDimLen > 1 || isValueArr && !tooltipDimLen ? formatArrayValue(value) : tooltipDimLen ? formatSingleValue(retrieveRawValue(data, dataIndex, tooltipDims[0])) : formatSingleValue(isValueArr ? value[0] : value); |
| var content = formattedValue.content; |
| var markName = series.seriesIndex + 'at' + markerId; |
| var colorEl = getTooltipMarker({ |
| color: color, |
| type: 'item', |
| renderMode: renderMode, |
| markerId: markName |
| }); |
| markers[markName] = color; |
| ++markerId; |
| var name = data.getName(dataIndex); |
| var seriesName = this.name; |
| |
| if (!modelUtil.isNameSpecified(this)) { |
| seriesName = ''; |
| } |
| |
| seriesName = seriesName ? encodeHTML(seriesName) + (!multipleSeries ? newLine : ': ') : ''; |
| var colorStr = typeof colorEl === 'string' ? colorEl : colorEl.content; |
| var html = !multipleSeries ? seriesName + colorStr + (name ? encodeHTML(name) + ': ' + content : content) : colorStr + seriesName + content; |
| return { |
| html: html, |
| markers: markers |
| }; |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| isAnimationEnabled: function () { |
| if (env.node) { |
| return false; |
| } |
| |
| var animationEnabled = this.getShallow('animation'); |
| |
| if (animationEnabled) { |
| if (this.getData().count() > this.getShallow('animationThreshold')) { |
| animationEnabled = false; |
| } |
| } |
| |
| return animationEnabled; |
| }, |
| restoreData: function () { |
| this.dataTask.dirty(); |
| }, |
| getColorFromPalette: function (name, scope, requestColorNum) { |
| var ecModel = this.ecModel; // PENDING |
| |
| var color = colorPaletteMixin.getColorFromPalette.call(this, name, scope, requestColorNum); |
| |
| if (!color) { |
| color = ecModel.getColorFromPalette(name, scope, requestColorNum); |
| } |
| |
| return color; |
| }, |
| |
| /** |
| * Use `data.mapDimension(coordDim, true)` instead. |
| * @deprecated |
| */ |
| coordDimToDataDim: function (coordDim) { |
| return this.getRawData().mapDimension(coordDim, true); |
| }, |
| |
| /** |
| * Get progressive rendering count each step |
| * @return {number} |
| */ |
| getProgressive: function () { |
| return this.get('progressive'); |
| }, |
| |
| /** |
| * Get progressive rendering count each step |
| * @return {number} |
| */ |
| getProgressiveThreshold: function () { |
| return this.get('progressiveThreshold'); |
| }, |
| |
| /** |
| * Get data indices for show tooltip content. See tooltip. |
| * @abstract |
| * @param {Array.<string>|string} dim |
| * @param {Array.<number>} value |
| * @param {module:echarts/coord/single/SingleAxis} baseAxis |
| * @return {Object} {dataIndices, nestestValue}. |
| */ |
| getAxisTooltipData: null, |
| |
| /** |
| * See tooltip. |
| * @abstract |
| * @param {number} dataIndex |
| * @return {Array.<number>} Point of tooltip. null/undefined can be returned. |
| */ |
| getTooltipPosition: null, |
| |
| /** |
| * @see {module:echarts/stream/Scheduler} |
| */ |
| pipeTask: null, |
| |
| /** |
| * Convinient for override in extended class. |
| * @protected |
| * @type {Function} |
| */ |
| preventIncremental: null, |
| |
| /** |
| * @public |
| * @readOnly |
| * @type {Object} |
| */ |
| pipelineContext: null |
| }); |
| zrUtil.mixin(SeriesModel, dataFormatMixin); |
| zrUtil.mixin(SeriesModel, colorPaletteMixin); |
| /** |
| * MUST be called after `prepareSource` called |
| * Here we need to make auto series, especially for auto legend. But we |
| * do not modify series.name in option to avoid side effects. |
| */ |
| |
| function autoSeriesName(seriesModel) { |
| // User specified name has higher priority, otherwise it may cause |
| // series can not be queried unexpectedly. |
| var name = seriesModel.name; |
| |
| if (!modelUtil.isNameSpecified(seriesModel)) { |
| seriesModel.name = getSeriesAutoName(seriesModel) || name; |
| } |
| } |
| |
| function getSeriesAutoName(seriesModel) { |
| var data = seriesModel.getRawData(); |
| var dataDims = data.mapDimension('seriesName', true); |
| var nameArr = []; |
| zrUtil.each(dataDims, function (dataDim) { |
| var dimInfo = data.getDimensionInfo(dataDim); |
| dimInfo.displayName && nameArr.push(dimInfo.displayName); |
| }); |
| return nameArr.join(' '); |
| } |
| |
| function dataTaskCount(context) { |
| return context.model.getRawData().count(); |
| } |
| |
| function dataTaskReset(context) { |
| var seriesModel = context.model; |
| seriesModel.setData(seriesModel.getRawData().cloneShallow()); |
| return dataTaskProgress; |
| } |
| |
| function dataTaskProgress(param, context) { |
| // Avoid repead cloneShallow when data just created in reset. |
| if (param.end > context.outputData.count()) { |
| context.model.getRawData().cloneShallow(context.outputData); |
| } |
| } // TODO refactor |
| |
| |
| function wrapData(data, seriesModel) { |
| zrUtil.each(data.CHANGABLE_METHODS, function (methodName) { |
| data.wrapMethod(methodName, zrUtil.curry(onDataSelfChange, seriesModel)); |
| }); |
| } |
| |
| function onDataSelfChange(seriesModel) { |
| var task = getCurrentTask(seriesModel); |
| |
| if (task) { |
| // Consider case: filter, selectRange |
| task.setOutputEnd(this.count()); |
| } |
| } |
| |
| function getCurrentTask(seriesModel) { |
| var scheduler = (seriesModel.ecModel || {}).scheduler; |
| var pipeline = scheduler && scheduler.getPipeline(seriesModel.uid); |
| |
| if (pipeline) { |
| // When pipline finished, the currrentTask keep the last |
| // task (renderTask). |
| var task = pipeline.currentTask; |
| |
| if (task) { |
| var agentStubMap = task.agentStubMap; |
| |
| if (agentStubMap) { |
| task = agentStubMap.get(seriesModel.uid); |
| } |
| } |
| |
| return task; |
| } |
| } |
| |
| export default SeriesModel; |