blob: 09aa10562c43b0c5f49887b947982e021a8ba50d [file] [log] [blame]
/*
* 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 * as zrUtil from 'zrender/src/core/util';
import * as numberUtil from '../../util/number';
import * as helper from './helper';
import sliderMove from '../helper/sliderMove';
var each = zrUtil.each;
var asc = numberUtil.asc;
/**
* Operate single axis.
* One axis can only operated by one axis operator.
* Different dataZoomModels may be defined to operate the same axis.
* (i.e. 'inside' data zoom and 'slider' data zoom components)
* So dataZoomModels share one axisProxy in that case.
*
* @class
*/
var AxisProxy = function (dimName, axisIndex, dataZoomModel, ecModel) {
/**
* @private
* @type {string}
*/
this._dimName = dimName;
/**
* @private
*/
this._axisIndex = axisIndex;
/**
* @private
* @type {Array.<number>}
*/
this._valueWindow;
/**
* @private
* @type {Array.<number>}
*/
this._percentWindow;
/**
* @private
* @type {Array.<number>}
*/
this._dataExtent;
/**
* {minSpan, maxSpan, minValueSpan, maxValueSpan}
* @private
* @type {Object}
*/
this._minMaxSpan;
/**
* @readOnly
* @type {module: echarts/model/Global}
*/
this.ecModel = ecModel;
/**
* @private
* @type {module: echarts/component/dataZoom/DataZoomModel}
*/
this._dataZoomModel = dataZoomModel; // /**
// * @readOnly
// * @private
// */
// this.hasSeriesStacked;
};
AxisProxy.prototype = {
constructor: AxisProxy,
/**
* Whether the axisProxy is hosted by dataZoomModel.
*
* @public
* @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel
* @return {boolean}
*/
hostedBy: function (dataZoomModel) {
return this._dataZoomModel === dataZoomModel;
},
/**
* @return {Array.<number>} Value can only be NaN or finite value.
*/
getDataValueWindow: function () {
return this._valueWindow.slice();
},
/**
* @return {Array.<number>}
*/
getDataPercentWindow: function () {
return this._percentWindow.slice();
},
/**
* @public
* @param {number} axisIndex
* @return {Array} seriesModels
*/
getTargetSeriesModels: function () {
var seriesModels = [];
var ecModel = this.ecModel;
ecModel.eachSeries(function (seriesModel) {
if (helper.isCoordSupported(seriesModel.get('coordinateSystem'))) {
var dimName = this._dimName;
var axisModel = ecModel.queryComponents({
mainType: dimName + 'Axis',
index: seriesModel.get(dimName + 'AxisIndex'),
id: seriesModel.get(dimName + 'AxisId')
})[0];
if (this._axisIndex === (axisModel && axisModel.componentIndex)) {
seriesModels.push(seriesModel);
}
}
}, this);
return seriesModels;
},
getAxisModel: function () {
return this.ecModel.getComponent(this._dimName + 'Axis', this._axisIndex);
},
getOtherAxisModel: function () {
var axisDim = this._dimName;
var ecModel = this.ecModel;
var axisModel = this.getAxisModel();
var isCartesian = axisDim === 'x' || axisDim === 'y';
var otherAxisDim;
var coordSysIndexName;
if (isCartesian) {
coordSysIndexName = 'gridIndex';
otherAxisDim = axisDim === 'x' ? 'y' : 'x';
} else {
coordSysIndexName = 'polarIndex';
otherAxisDim = axisDim === 'angle' ? 'radius' : 'angle';
}
var foundOtherAxisModel;
ecModel.eachComponent(otherAxisDim + 'Axis', function (otherAxisModel) {
if ((otherAxisModel.get(coordSysIndexName) || 0) === (axisModel.get(coordSysIndexName) || 0)) {
foundOtherAxisModel = otherAxisModel;
}
});
return foundOtherAxisModel;
},
getMinMaxSpan: function () {
return zrUtil.clone(this._minMaxSpan);
},
/**
* Only calculate by given range and this._dataExtent, do not change anything.
*
* @param {Object} opt
* @param {number} [opt.start]
* @param {number} [opt.end]
* @param {number} [opt.startValue]
* @param {number} [opt.endValue]
*/
calculateDataWindow: function (opt) {
var dataExtent = this._dataExtent;
var axisModel = this.getAxisModel();
var scale = axisModel.axis.scale;
var rangePropMode = this._dataZoomModel.getRangePropMode();
var percentExtent = [0, 100];
var percentWindow = [];
var valueWindow = [];
var hasPropModeValue;
each(['start', 'end'], function (prop, idx) {
var boundPercent = opt[prop];
var boundValue = opt[prop + 'Value']; // Notice: dataZoom is based either on `percentProp` ('start', 'end') or
// on `valueProp` ('startValue', 'endValue'). (They are based on the data extent
// but not min/max of axis, which will be calculated by data window then).
// The former one is suitable for cases that a dataZoom component controls multiple
// axes with different unit or extent, and the latter one is suitable for accurate
// zoom by pixel (e.g., in dataZoomSelect).
// we use `getRangePropMode()` to mark which prop is used. `rangePropMode` is updated
// only when setOption or dispatchAction, otherwise it remains its original value.
// (Why not only record `percentProp` and always map to `valueProp`? Because
// the map `valueProp` -> `percentProp` -> `valueProp` probably not the original
// `valueProp`. consider two axes constrolled by one dataZoom. They have different
// data extent. All of values that are overflow the `dataExtent` will be calculated
// to percent '100%').
if (rangePropMode[idx] === 'percent') {
boundPercent == null && (boundPercent = percentExtent[idx]); // Use scale.parse to math round for category or time axis.
boundValue = scale.parse(numberUtil.linearMap(boundPercent, percentExtent, dataExtent));
} else {
hasPropModeValue = true;
boundValue = boundValue == null ? dataExtent[idx] : scale.parse(boundValue); // Calculating `percent` from `value` may be not accurate, because
// This calculation can not be inversed, because all of values that
// are overflow the `dataExtent` will be calculated to percent '100%'
boundPercent = numberUtil.linearMap(boundValue, dataExtent, percentExtent);
} // valueWindow[idx] = round(boundValue);
// percentWindow[idx] = round(boundPercent);
valueWindow[idx] = boundValue;
percentWindow[idx] = boundPercent;
});
asc(valueWindow);
asc(percentWindow); // The windows from user calling of `dispatchAction` might be out of the extent,
// or do not obey the `min/maxSpan`, `min/maxValueSpan`. But we dont restrict window
// by `zoomLock` here, because we see `zoomLock` just as a interaction constraint,
// where API is able to initialize/modify the window size even though `zoomLock`
// specified.
var spans = this._minMaxSpan;
hasPropModeValue ? restrictSet(valueWindow, percentWindow, dataExtent, percentExtent, false) : restrictSet(percentWindow, valueWindow, percentExtent, dataExtent, true);
function restrictSet(fromWindow, toWindow, fromExtent, toExtent, toValue) {
var suffix = toValue ? 'Span' : 'ValueSpan';
sliderMove(0, fromWindow, fromExtent, 'all', spans['min' + suffix], spans['max' + suffix]);
for (var i = 0; i < 2; i++) {
toWindow[i] = numberUtil.linearMap(fromWindow[i], fromExtent, toExtent, true);
toValue && (toWindow[i] = scale.parse(toWindow[i]));
}
}
return {
valueWindow: valueWindow,
percentWindow: percentWindow
};
},
/**
* Notice: reset should not be called before series.restoreData() called,
* so it is recommanded to be called in "process stage" but not "model init
* stage".
*
* @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel
*/
reset: function (dataZoomModel) {
if (dataZoomModel !== this._dataZoomModel) {
return;
}
var targetSeries = this.getTargetSeriesModels(); // Culculate data window and data extent, and record them.
this._dataExtent = calculateDataExtent(this, this._dimName, targetSeries); // this.hasSeriesStacked = false;
// each(targetSeries, function (series) {
// var data = series.getData();
// var dataDim = data.mapDimension(this._dimName);
// var stackedDimension = data.getCalculationInfo('stackedDimension');
// if (stackedDimension && stackedDimension === dataDim) {
// this.hasSeriesStacked = true;
// }
// }, this);
// `calculateDataWindow` uses min/maxSpan.
setMinMaxSpan(this);
var dataWindow = this.calculateDataWindow(dataZoomModel.option);
this._valueWindow = dataWindow.valueWindow;
this._percentWindow = dataWindow.percentWindow; // Update axis setting then.
setAxisModel(this);
},
/**
* @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel
*/
restore: function (dataZoomModel) {
if (dataZoomModel !== this._dataZoomModel) {
return;
}
this._valueWindow = this._percentWindow = null;
setAxisModel(this, true);
},
/**
* @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel
*/
filterData: function (dataZoomModel, api) {
if (dataZoomModel !== this._dataZoomModel) {
return;
}
var axisDim = this._dimName;
var seriesModels = this.getTargetSeriesModels();
var filterMode = dataZoomModel.get('filterMode');
var valueWindow = this._valueWindow;
if (filterMode === 'none') {
return;
} // FIXME
// Toolbox may has dataZoom injected. And if there are stacked bar chart
// with NaN data, NaN will be filtered and stack will be wrong.
// So we need to force the mode to be set empty.
// In fect, it is not a big deal that do not support filterMode-'filter'
// when using toolbox#dataZoom, utill tooltip#dataZoom support "single axis
// selection" some day, which might need "adapt to data extent on the
// otherAxis", which is disabled by filterMode-'empty'.
// But currently, stack has been fixed to based on value but not index,
// so this is not an issue any more.
// var otherAxisModel = this.getOtherAxisModel();
// if (dataZoomModel.get('$fromToolbox')
// && otherAxisModel
// && otherAxisModel.hasSeriesStacked
// ) {
// filterMode = 'empty';
// }
// TODO
// filterMode 'weakFilter' and 'empty' is not optimized for huge data yet.
each(seriesModels, function (seriesModel) {
var seriesData = seriesModel.getData();
var dataDims = seriesData.mapDimension(axisDim, true);
if (!dataDims.length) {
return;
}
if (filterMode === 'weakFilter') {
seriesData.filterSelf(function (dataIndex) {
var leftOut;
var rightOut;
var hasValue;
for (var i = 0; i < dataDims.length; i++) {
var value = seriesData.get(dataDims[i], dataIndex);
var thisHasValue = !isNaN(value);
var thisLeftOut = value < valueWindow[0];
var thisRightOut = value > valueWindow[1];
if (thisHasValue && !thisLeftOut && !thisRightOut) {
return true;
}
thisHasValue && (hasValue = true);
thisLeftOut && (leftOut = true);
thisRightOut && (rightOut = true);
} // If both left out and right out, do not filter.
return hasValue && leftOut && rightOut;
});
} else {
each(dataDims, function (dim) {
if (filterMode === 'empty') {
seriesModel.setData(seriesData = seriesData.map(dim, function (value) {
return !isInWindow(value) ? NaN : value;
}));
} else {
var range = {};
range[dim] = valueWindow; // console.time('select');
seriesData.selectRange(range); // console.timeEnd('select');
}
});
}
each(dataDims, function (dim) {
seriesData.setApproximateExtent(valueWindow, dim);
});
});
function isInWindow(value) {
return value >= valueWindow[0] && value <= valueWindow[1];
}
}
};
function calculateDataExtent(axisProxy, axisDim, seriesModels) {
var dataExtent = [Infinity, -Infinity];
each(seriesModels, function (seriesModel) {
var seriesData = seriesModel.getData();
if (seriesData) {
each(seriesData.mapDimension(axisDim, true), function (dim) {
var seriesExtent = seriesData.getApproximateExtent(dim);
seriesExtent[0] < dataExtent[0] && (dataExtent[0] = seriesExtent[0]);
seriesExtent[1] > dataExtent[1] && (dataExtent[1] = seriesExtent[1]);
});
}
});
if (dataExtent[1] < dataExtent[0]) {
dataExtent = [NaN, NaN];
} // It is important to get "consistent" extent when more then one axes is
// controlled by a `dataZoom`, otherwise those axes will not be synchronized
// when zooming. But it is difficult to know what is "consistent", considering
// axes have different type or even different meanings (For example, two
// time axes are used to compare data of the same date in different years).
// So basically dataZoom just obtains extent by series.data (in category axis
// extent can be obtained from axis.data).
// Nevertheless, user can set min/max/scale on axes to make extent of axes
// consistent.
fixExtentByAxis(axisProxy, dataExtent);
return dataExtent;
}
function fixExtentByAxis(axisProxy, dataExtent) {
var axisModel = axisProxy.getAxisModel();
var min = axisModel.getMin(true); // For category axis, if min/max/scale are not set, extent is determined
// by axis.data by default.
var isCategoryAxis = axisModel.get('type') === 'category';
var axisDataLen = isCategoryAxis && axisModel.getCategories().length;
if (min != null && min !== 'dataMin' && typeof min !== 'function') {
dataExtent[0] = min;
} else if (isCategoryAxis) {
dataExtent[0] = axisDataLen > 0 ? 0 : NaN;
}
var max = axisModel.getMax(true);
if (max != null && max !== 'dataMax' && typeof max !== 'function') {
dataExtent[1] = max;
} else if (isCategoryAxis) {
dataExtent[1] = axisDataLen > 0 ? axisDataLen - 1 : NaN;
}
if (!axisModel.get('scale', true)) {
dataExtent[0] > 0 && (dataExtent[0] = 0);
dataExtent[1] < 0 && (dataExtent[1] = 0);
} // For value axis, if min/max/scale are not set, we just use the extent obtained
// by series data, which may be a little different from the extent calculated by
// `axisHelper.getScaleExtent`. But the different just affects the experience a
// little when zooming. So it will not be fixed until some users require it strongly.
return dataExtent;
}
function setAxisModel(axisProxy, isRestore) {
var axisModel = axisProxy.getAxisModel();
var percentWindow = axisProxy._percentWindow;
var valueWindow = axisProxy._valueWindow;
if (!percentWindow) {
return;
} // [0, 500]: arbitrary value, guess axis extent.
var precision = numberUtil.getPixelPrecision(valueWindow, [0, 500]);
precision = Math.min(precision, 20); // isRestore or isFull
var useOrigin = isRestore || percentWindow[0] === 0 && percentWindow[1] === 100;
axisModel.setRange(useOrigin ? null : +valueWindow[0].toFixed(precision), useOrigin ? null : +valueWindow[1].toFixed(precision));
}
function setMinMaxSpan(axisProxy) {
var minMaxSpan = axisProxy._minMaxSpan = {};
var dataZoomModel = axisProxy._dataZoomModel;
var dataExtent = axisProxy._dataExtent;
each(['min', 'max'], function (minMax) {
var percentSpan = dataZoomModel.get(minMax + 'Span');
var valueSpan = dataZoomModel.get(minMax + 'ValueSpan');
valueSpan != null && (valueSpan = axisProxy.getAxisModel().axis.scale.parse(valueSpan)); // minValueSpan and maxValueSpan has higher priority than minSpan and maxSpan
if (valueSpan != null) {
percentSpan = numberUtil.linearMap(dataExtent[0] + valueSpan, dataExtent, [0, 100], true);
} else if (percentSpan != null) {
valueSpan = numberUtil.linearMap(percentSpan, [0, 100], dataExtent, true) - dataExtent[0];
}
minMaxSpan[minMax + 'Span'] = percentSpan;
minMaxSpan[minMax + 'ValueSpan'] = valueSpan;
});
}
export default AxisProxy;