/* * 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. */ var _config = require("../config"); var __DEV__ = _config.__DEV__; var zrUtil = require("zrender/lib/core/util"); var env = require("zrender/lib/core/env"); var _format = require("../util/format"); var formatTime = _format.formatTime; var encodeHTML = _format.encodeHTML; var addCommas = _format.addCommas; var getTooltipMarker = _format.getTooltipMarker; var modelUtil = require("../util/model"); var ComponentModel = require("./Component"); var colorPaletteMixin = require("./mixin/colorPalette"); var dataFormatMixin = require("../model/mixin/dataFormat"); var _layout = require("../util/layout"); var getLayoutParams = _layout.getLayoutParams; var mergeLayoutParam = _layout.mergeLayoutParam; var _task = require("../stream/task"); var createTask = _task.createTask; var _sourceHelper = require("../data/helper/sourceHelper"); var prepareSource = _sourceHelper.prepareSource; var getSource = _sourceHelper.getSource; var _dataProvider = require("../data/helper/dataProvider"); var retrieveRawValue = _dataProvider.retrieveRawValue; /* * 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. */ 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' ? '
' : '\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' : '
' : ''; 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} dim * @param {Array.} value * @param {module:echarts/coord/single/SingleAxis} baseAxis * @return {Object} {dataIndices, nestestValue}. */ getAxisTooltipData: null, /** * See tooltip. * @abstract * @param {number} dataIndex * @return {Array.} 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; } } var _default = SeriesModel; module.exports = _default;