123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518 |
- /*
- * 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 zrUtil = require("zrender/lib/core/util");
- var numberUtil = require("../../util/number");
- var helper = require("./helper");
- var sliderMove = require("../helper/sliderMove");
- /*
- * 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 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.settledOption);
- 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;
- });
- }
- var _default = AxisProxy;
- module.exports = _default;
|