AxisProxy.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. var zrUtil = require("zrender/lib/core/util");
  20. var numberUtil = require("../../util/number");
  21. var helper = require("./helper");
  22. var sliderMove = require("../helper/sliderMove");
  23. /*
  24. * Licensed to the Apache Software Foundation (ASF) under one
  25. * or more contributor license agreements. See the NOTICE file
  26. * distributed with this work for additional information
  27. * regarding copyright ownership. The ASF licenses this file
  28. * to you under the Apache License, Version 2.0 (the
  29. * "License"); you may not use this file except in compliance
  30. * with the License. You may obtain a copy of the License at
  31. *
  32. * http://www.apache.org/licenses/LICENSE-2.0
  33. *
  34. * Unless required by applicable law or agreed to in writing,
  35. * software distributed under the License is distributed on an
  36. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  37. * KIND, either express or implied. See the License for the
  38. * specific language governing permissions and limitations
  39. * under the License.
  40. */
  41. var each = zrUtil.each;
  42. var asc = numberUtil.asc;
  43. /**
  44. * Operate single axis.
  45. * One axis can only operated by one axis operator.
  46. * Different dataZoomModels may be defined to operate the same axis.
  47. * (i.e. 'inside' data zoom and 'slider' data zoom components)
  48. * So dataZoomModels share one axisProxy in that case.
  49. *
  50. * @class
  51. */
  52. var AxisProxy = function (dimName, axisIndex, dataZoomModel, ecModel) {
  53. /**
  54. * @private
  55. * @type {string}
  56. */
  57. this._dimName = dimName;
  58. /**
  59. * @private
  60. */
  61. this._axisIndex = axisIndex;
  62. /**
  63. * @private
  64. * @type {Array.<number>}
  65. */
  66. this._valueWindow;
  67. /**
  68. * @private
  69. * @type {Array.<number>}
  70. */
  71. this._percentWindow;
  72. /**
  73. * @private
  74. * @type {Array.<number>}
  75. */
  76. this._dataExtent;
  77. /**
  78. * {minSpan, maxSpan, minValueSpan, maxValueSpan}
  79. * @private
  80. * @type {Object}
  81. */
  82. this._minMaxSpan;
  83. /**
  84. * @readOnly
  85. * @type {module: echarts/model/Global}
  86. */
  87. this.ecModel = ecModel;
  88. /**
  89. * @private
  90. * @type {module: echarts/component/dataZoom/DataZoomModel}
  91. */
  92. this._dataZoomModel = dataZoomModel; // /**
  93. // * @readOnly
  94. // * @private
  95. // */
  96. // this.hasSeriesStacked;
  97. };
  98. AxisProxy.prototype = {
  99. constructor: AxisProxy,
  100. /**
  101. * Whether the axisProxy is hosted by dataZoomModel.
  102. *
  103. * @public
  104. * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel
  105. * @return {boolean}
  106. */
  107. hostedBy: function (dataZoomModel) {
  108. return this._dataZoomModel === dataZoomModel;
  109. },
  110. /**
  111. * @return {Array.<number>} Value can only be NaN or finite value.
  112. */
  113. getDataValueWindow: function () {
  114. return this._valueWindow.slice();
  115. },
  116. /**
  117. * @return {Array.<number>}
  118. */
  119. getDataPercentWindow: function () {
  120. return this._percentWindow.slice();
  121. },
  122. /**
  123. * @public
  124. * @param {number} axisIndex
  125. * @return {Array} seriesModels
  126. */
  127. getTargetSeriesModels: function () {
  128. var seriesModels = [];
  129. var ecModel = this.ecModel;
  130. ecModel.eachSeries(function (seriesModel) {
  131. if (helper.isCoordSupported(seriesModel.get('coordinateSystem'))) {
  132. var dimName = this._dimName;
  133. var axisModel = ecModel.queryComponents({
  134. mainType: dimName + 'Axis',
  135. index: seriesModel.get(dimName + 'AxisIndex'),
  136. id: seriesModel.get(dimName + 'AxisId')
  137. })[0];
  138. if (this._axisIndex === (axisModel && axisModel.componentIndex)) {
  139. seriesModels.push(seriesModel);
  140. }
  141. }
  142. }, this);
  143. return seriesModels;
  144. },
  145. getAxisModel: function () {
  146. return this.ecModel.getComponent(this._dimName + 'Axis', this._axisIndex);
  147. },
  148. getOtherAxisModel: function () {
  149. var axisDim = this._dimName;
  150. var ecModel = this.ecModel;
  151. var axisModel = this.getAxisModel();
  152. var isCartesian = axisDim === 'x' || axisDim === 'y';
  153. var otherAxisDim;
  154. var coordSysIndexName;
  155. if (isCartesian) {
  156. coordSysIndexName = 'gridIndex';
  157. otherAxisDim = axisDim === 'x' ? 'y' : 'x';
  158. } else {
  159. coordSysIndexName = 'polarIndex';
  160. otherAxisDim = axisDim === 'angle' ? 'radius' : 'angle';
  161. }
  162. var foundOtherAxisModel;
  163. ecModel.eachComponent(otherAxisDim + 'Axis', function (otherAxisModel) {
  164. if ((otherAxisModel.get(coordSysIndexName) || 0) === (axisModel.get(coordSysIndexName) || 0)) {
  165. foundOtherAxisModel = otherAxisModel;
  166. }
  167. });
  168. return foundOtherAxisModel;
  169. },
  170. getMinMaxSpan: function () {
  171. return zrUtil.clone(this._minMaxSpan);
  172. },
  173. /**
  174. * Only calculate by given range and this._dataExtent, do not change anything.
  175. *
  176. * @param {Object} opt
  177. * @param {number} [opt.start]
  178. * @param {number} [opt.end]
  179. * @param {number} [opt.startValue]
  180. * @param {number} [opt.endValue]
  181. */
  182. calculateDataWindow: function (opt) {
  183. var dataExtent = this._dataExtent;
  184. var axisModel = this.getAxisModel();
  185. var scale = axisModel.axis.scale;
  186. var rangePropMode = this._dataZoomModel.getRangePropMode();
  187. var percentExtent = [0, 100];
  188. var percentWindow = [];
  189. var valueWindow = [];
  190. var hasPropModeValue;
  191. each(['start', 'end'], function (prop, idx) {
  192. var boundPercent = opt[prop];
  193. var boundValue = opt[prop + 'Value']; // Notice: dataZoom is based either on `percentProp` ('start', 'end') or
  194. // on `valueProp` ('startValue', 'endValue'). (They are based on the data extent
  195. // but not min/max of axis, which will be calculated by data window then).
  196. // The former one is suitable for cases that a dataZoom component controls multiple
  197. // axes with different unit or extent, and the latter one is suitable for accurate
  198. // zoom by pixel (e.g., in dataZoomSelect).
  199. // we use `getRangePropMode()` to mark which prop is used. `rangePropMode` is updated
  200. // only when setOption or dispatchAction, otherwise it remains its original value.
  201. // (Why not only record `percentProp` and always map to `valueProp`? Because
  202. // the map `valueProp` -> `percentProp` -> `valueProp` probably not the original
  203. // `valueProp`. consider two axes constrolled by one dataZoom. They have different
  204. // data extent. All of values that are overflow the `dataExtent` will be calculated
  205. // to percent '100%').
  206. if (rangePropMode[idx] === 'percent') {
  207. boundPercent == null && (boundPercent = percentExtent[idx]); // Use scale.parse to math round for category or time axis.
  208. boundValue = scale.parse(numberUtil.linearMap(boundPercent, percentExtent, dataExtent));
  209. } else {
  210. hasPropModeValue = true;
  211. boundValue = boundValue == null ? dataExtent[idx] : scale.parse(boundValue); // Calculating `percent` from `value` may be not accurate, because
  212. // This calculation can not be inversed, because all of values that
  213. // are overflow the `dataExtent` will be calculated to percent '100%'
  214. boundPercent = numberUtil.linearMap(boundValue, dataExtent, percentExtent);
  215. } // valueWindow[idx] = round(boundValue);
  216. // percentWindow[idx] = round(boundPercent);
  217. valueWindow[idx] = boundValue;
  218. percentWindow[idx] = boundPercent;
  219. });
  220. asc(valueWindow);
  221. asc(percentWindow); // The windows from user calling of `dispatchAction` might be out of the extent,
  222. // or do not obey the `min/maxSpan`, `min/maxValueSpan`. But we dont restrict window
  223. // by `zoomLock` here, because we see `zoomLock` just as a interaction constraint,
  224. // where API is able to initialize/modify the window size even though `zoomLock`
  225. // specified.
  226. var spans = this._minMaxSpan;
  227. hasPropModeValue ? restrictSet(valueWindow, percentWindow, dataExtent, percentExtent, false) : restrictSet(percentWindow, valueWindow, percentExtent, dataExtent, true);
  228. function restrictSet(fromWindow, toWindow, fromExtent, toExtent, toValue) {
  229. var suffix = toValue ? 'Span' : 'ValueSpan';
  230. sliderMove(0, fromWindow, fromExtent, 'all', spans['min' + suffix], spans['max' + suffix]);
  231. for (var i = 0; i < 2; i++) {
  232. toWindow[i] = numberUtil.linearMap(fromWindow[i], fromExtent, toExtent, true);
  233. toValue && (toWindow[i] = scale.parse(toWindow[i]));
  234. }
  235. }
  236. return {
  237. valueWindow: valueWindow,
  238. percentWindow: percentWindow
  239. };
  240. },
  241. /**
  242. * Notice: reset should not be called before series.restoreData() called,
  243. * so it is recommanded to be called in "process stage" but not "model init
  244. * stage".
  245. *
  246. * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel
  247. */
  248. reset: function (dataZoomModel) {
  249. if (dataZoomModel !== this._dataZoomModel) {
  250. return;
  251. }
  252. var targetSeries = this.getTargetSeriesModels(); // Culculate data window and data extent, and record them.
  253. this._dataExtent = calculateDataExtent(this, this._dimName, targetSeries); // this.hasSeriesStacked = false;
  254. // each(targetSeries, function (series) {
  255. // var data = series.getData();
  256. // var dataDim = data.mapDimension(this._dimName);
  257. // var stackedDimension = data.getCalculationInfo('stackedDimension');
  258. // if (stackedDimension && stackedDimension === dataDim) {
  259. // this.hasSeriesStacked = true;
  260. // }
  261. // }, this);
  262. // `calculateDataWindow` uses min/maxSpan.
  263. setMinMaxSpan(this);
  264. var dataWindow = this.calculateDataWindow(dataZoomModel.settledOption);
  265. this._valueWindow = dataWindow.valueWindow;
  266. this._percentWindow = dataWindow.percentWindow; // Update axis setting then.
  267. setAxisModel(this);
  268. },
  269. /**
  270. * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel
  271. */
  272. restore: function (dataZoomModel) {
  273. if (dataZoomModel !== this._dataZoomModel) {
  274. return;
  275. }
  276. this._valueWindow = this._percentWindow = null;
  277. setAxisModel(this, true);
  278. },
  279. /**
  280. * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel
  281. */
  282. filterData: function (dataZoomModel, api) {
  283. if (dataZoomModel !== this._dataZoomModel) {
  284. return;
  285. }
  286. var axisDim = this._dimName;
  287. var seriesModels = this.getTargetSeriesModels();
  288. var filterMode = dataZoomModel.get('filterMode');
  289. var valueWindow = this._valueWindow;
  290. if (filterMode === 'none') {
  291. return;
  292. } // FIXME
  293. // Toolbox may has dataZoom injected. And if there are stacked bar chart
  294. // with NaN data, NaN will be filtered and stack will be wrong.
  295. // So we need to force the mode to be set empty.
  296. // In fect, it is not a big deal that do not support filterMode-'filter'
  297. // when using toolbox#dataZoom, utill tooltip#dataZoom support "single axis
  298. // selection" some day, which might need "adapt to data extent on the
  299. // otherAxis", which is disabled by filterMode-'empty'.
  300. // But currently, stack has been fixed to based on value but not index,
  301. // so this is not an issue any more.
  302. // var otherAxisModel = this.getOtherAxisModel();
  303. // if (dataZoomModel.get('$fromToolbox')
  304. // && otherAxisModel
  305. // && otherAxisModel.hasSeriesStacked
  306. // ) {
  307. // filterMode = 'empty';
  308. // }
  309. // TODO
  310. // filterMode 'weakFilter' and 'empty' is not optimized for huge data yet.
  311. each(seriesModels, function (seriesModel) {
  312. var seriesData = seriesModel.getData();
  313. var dataDims = seriesData.mapDimension(axisDim, true);
  314. if (!dataDims.length) {
  315. return;
  316. }
  317. if (filterMode === 'weakFilter') {
  318. seriesData.filterSelf(function (dataIndex) {
  319. var leftOut;
  320. var rightOut;
  321. var hasValue;
  322. for (var i = 0; i < dataDims.length; i++) {
  323. var value = seriesData.get(dataDims[i], dataIndex);
  324. var thisHasValue = !isNaN(value);
  325. var thisLeftOut = value < valueWindow[0];
  326. var thisRightOut = value > valueWindow[1];
  327. if (thisHasValue && !thisLeftOut && !thisRightOut) {
  328. return true;
  329. }
  330. thisHasValue && (hasValue = true);
  331. thisLeftOut && (leftOut = true);
  332. thisRightOut && (rightOut = true);
  333. } // If both left out and right out, do not filter.
  334. return hasValue && leftOut && rightOut;
  335. });
  336. } else {
  337. each(dataDims, function (dim) {
  338. if (filterMode === 'empty') {
  339. seriesModel.setData(seriesData = seriesData.map(dim, function (value) {
  340. return !isInWindow(value) ? NaN : value;
  341. }));
  342. } else {
  343. var range = {};
  344. range[dim] = valueWindow; // console.time('select');
  345. seriesData.selectRange(range); // console.timeEnd('select');
  346. }
  347. });
  348. }
  349. each(dataDims, function (dim) {
  350. seriesData.setApproximateExtent(valueWindow, dim);
  351. });
  352. });
  353. function isInWindow(value) {
  354. return value >= valueWindow[0] && value <= valueWindow[1];
  355. }
  356. }
  357. };
  358. function calculateDataExtent(axisProxy, axisDim, seriesModels) {
  359. var dataExtent = [Infinity, -Infinity];
  360. each(seriesModels, function (seriesModel) {
  361. var seriesData = seriesModel.getData();
  362. if (seriesData) {
  363. each(seriesData.mapDimension(axisDim, true), function (dim) {
  364. var seriesExtent = seriesData.getApproximateExtent(dim);
  365. seriesExtent[0] < dataExtent[0] && (dataExtent[0] = seriesExtent[0]);
  366. seriesExtent[1] > dataExtent[1] && (dataExtent[1] = seriesExtent[1]);
  367. });
  368. }
  369. });
  370. if (dataExtent[1] < dataExtent[0]) {
  371. dataExtent = [NaN, NaN];
  372. } // It is important to get "consistent" extent when more then one axes is
  373. // controlled by a `dataZoom`, otherwise those axes will not be synchronized
  374. // when zooming. But it is difficult to know what is "consistent", considering
  375. // axes have different type or even different meanings (For example, two
  376. // time axes are used to compare data of the same date in different years).
  377. // So basically dataZoom just obtains extent by series.data (in category axis
  378. // extent can be obtained from axis.data).
  379. // Nevertheless, user can set min/max/scale on axes to make extent of axes
  380. // consistent.
  381. fixExtentByAxis(axisProxy, dataExtent);
  382. return dataExtent;
  383. }
  384. function fixExtentByAxis(axisProxy, dataExtent) {
  385. var axisModel = axisProxy.getAxisModel();
  386. var min = axisModel.getMin(true); // For category axis, if min/max/scale are not set, extent is determined
  387. // by axis.data by default.
  388. var isCategoryAxis = axisModel.get('type') === 'category';
  389. var axisDataLen = isCategoryAxis && axisModel.getCategories().length;
  390. if (min != null && min !== 'dataMin' && typeof min !== 'function') {
  391. dataExtent[0] = min;
  392. } else if (isCategoryAxis) {
  393. dataExtent[0] = axisDataLen > 0 ? 0 : NaN;
  394. }
  395. var max = axisModel.getMax(true);
  396. if (max != null && max !== 'dataMax' && typeof max !== 'function') {
  397. dataExtent[1] = max;
  398. } else if (isCategoryAxis) {
  399. dataExtent[1] = axisDataLen > 0 ? axisDataLen - 1 : NaN;
  400. }
  401. if (!axisModel.get('scale', true)) {
  402. dataExtent[0] > 0 && (dataExtent[0] = 0);
  403. dataExtent[1] < 0 && (dataExtent[1] = 0);
  404. } // For value axis, if min/max/scale are not set, we just use the extent obtained
  405. // by series data, which may be a little different from the extent calculated by
  406. // `axisHelper.getScaleExtent`. But the different just affects the experience a
  407. // little when zooming. So it will not be fixed until some users require it strongly.
  408. return dataExtent;
  409. }
  410. function setAxisModel(axisProxy, isRestore) {
  411. var axisModel = axisProxy.getAxisModel();
  412. var percentWindow = axisProxy._percentWindow;
  413. var valueWindow = axisProxy._valueWindow;
  414. if (!percentWindow) {
  415. return;
  416. } // [0, 500]: arbitrary value, guess axis extent.
  417. var precision = numberUtil.getPixelPrecision(valueWindow, [0, 500]);
  418. precision = Math.min(precision, 20); // isRestore or isFull
  419. var useOrigin = isRestore || percentWindow[0] === 0 && percentWindow[1] === 100;
  420. axisModel.setRange(useOrigin ? null : +valueWindow[0].toFixed(precision), useOrigin ? null : +valueWindow[1].toFixed(precision));
  421. }
  422. function setMinMaxSpan(axisProxy) {
  423. var minMaxSpan = axisProxy._minMaxSpan = {};
  424. var dataZoomModel = axisProxy._dataZoomModel;
  425. var dataExtent = axisProxy._dataExtent;
  426. each(['min', 'max'], function (minMax) {
  427. var percentSpan = dataZoomModel.get(minMax + 'Span');
  428. var valueSpan = dataZoomModel.get(minMax + 'ValueSpan');
  429. valueSpan != null && (valueSpan = axisProxy.getAxisModel().axis.scale.parse(valueSpan)); // minValueSpan and maxValueSpan has higher priority than minSpan and maxSpan
  430. if (valueSpan != null) {
  431. percentSpan = numberUtil.linearMap(dataExtent[0] + valueSpan, dataExtent, [0, 100], true);
  432. } else if (percentSpan != null) {
  433. valueSpan = numberUtil.linearMap(percentSpan, [0, 100], dataExtent, true) - dataExtent[0];
  434. }
  435. minMaxSpan[minMax + 'Span'] = percentSpan;
  436. minMaxSpan[minMax + 'ValueSpan'] = valueSpan;
  437. });
  438. }
  439. var _default = AxisProxy;
  440. module.exports = _default;