AxisProxy.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. var zrUtil = require("zrender/lib/core/util");
  2. var numberUtil = require("../../util/number");
  3. var helper = require("./helper");
  4. var each = zrUtil.each;
  5. var asc = numberUtil.asc;
  6. /**
  7. * Operate single axis.
  8. * One axis can only operated by one axis operator.
  9. * Different dataZoomModels may be defined to operate the same axis.
  10. * (i.e. 'inside' data zoom and 'slider' data zoom components)
  11. * So dataZoomModels share one axisProxy in that case.
  12. *
  13. * @class
  14. */
  15. var AxisProxy = function (dimName, axisIndex, dataZoomModel, ecModel) {
  16. /**
  17. * @private
  18. * @type {string}
  19. */
  20. this._dimName = dimName;
  21. /**
  22. * @private
  23. */
  24. this._axisIndex = axisIndex;
  25. /**
  26. * @private
  27. * @type {Array.<number>}
  28. */
  29. this._valueWindow;
  30. /**
  31. * @private
  32. * @type {Array.<number>}
  33. */
  34. this._percentWindow;
  35. /**
  36. * @private
  37. * @type {Array.<number>}
  38. */
  39. this._dataExtent;
  40. /**
  41. * {minSpan, maxSpan, minValueSpan, maxValueSpan}
  42. * @private
  43. * @type {Object}
  44. */
  45. this._minMaxSpan;
  46. /**
  47. * @readOnly
  48. * @type {module: echarts/model/Global}
  49. */
  50. this.ecModel = ecModel;
  51. /**
  52. * @private
  53. * @type {module: echarts/component/dataZoom/DataZoomModel}
  54. */
  55. this._dataZoomModel = dataZoomModel;
  56. };
  57. AxisProxy.prototype = {
  58. constructor: AxisProxy,
  59. /**
  60. * Whether the axisProxy is hosted by dataZoomModel.
  61. *
  62. * @public
  63. * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel
  64. * @return {boolean}
  65. */
  66. hostedBy: function (dataZoomModel) {
  67. return this._dataZoomModel === dataZoomModel;
  68. },
  69. /**
  70. * @return {Array.<number>} Value can only be NaN or finite value.
  71. */
  72. getDataValueWindow: function () {
  73. return this._valueWindow.slice();
  74. },
  75. /**
  76. * @return {Array.<number>}
  77. */
  78. getDataPercentWindow: function () {
  79. return this._percentWindow.slice();
  80. },
  81. /**
  82. * @public
  83. * @param {number} axisIndex
  84. * @return {Array} seriesModels
  85. */
  86. getTargetSeriesModels: function () {
  87. var seriesModels = [];
  88. var ecModel = this.ecModel;
  89. ecModel.eachSeries(function (seriesModel) {
  90. if (helper.isCoordSupported(seriesModel.get('coordinateSystem'))) {
  91. var dimName = this._dimName;
  92. var axisModel = ecModel.queryComponents({
  93. mainType: dimName + 'Axis',
  94. index: seriesModel.get(dimName + 'AxisIndex'),
  95. id: seriesModel.get(dimName + 'AxisId')
  96. })[0];
  97. if (this._axisIndex === (axisModel && axisModel.componentIndex)) {
  98. seriesModels.push(seriesModel);
  99. }
  100. }
  101. }, this);
  102. return seriesModels;
  103. },
  104. getAxisModel: function () {
  105. return this.ecModel.getComponent(this._dimName + 'Axis', this._axisIndex);
  106. },
  107. getOtherAxisModel: function () {
  108. var axisDim = this._dimName;
  109. var ecModel = this.ecModel;
  110. var axisModel = this.getAxisModel();
  111. var isCartesian = axisDim === 'x' || axisDim === 'y';
  112. var otherAxisDim;
  113. var coordSysIndexName;
  114. if (isCartesian) {
  115. coordSysIndexName = 'gridIndex';
  116. otherAxisDim = axisDim === 'x' ? 'y' : 'x';
  117. } else {
  118. coordSysIndexName = 'polarIndex';
  119. otherAxisDim = axisDim === 'angle' ? 'radius' : 'angle';
  120. }
  121. var foundOtherAxisModel;
  122. ecModel.eachComponent(otherAxisDim + 'Axis', function (otherAxisModel) {
  123. if ((otherAxisModel.get(coordSysIndexName) || 0) === (axisModel.get(coordSysIndexName) || 0)) {
  124. foundOtherAxisModel = otherAxisModel;
  125. }
  126. });
  127. return foundOtherAxisModel;
  128. },
  129. getMinMaxSpan: function () {
  130. return zrUtil.clone(this._minMaxSpan);
  131. },
  132. /**
  133. * Only calculate by given range and this._dataExtent, do not change anything.
  134. *
  135. * @param {Object} opt
  136. * @param {number} [opt.start]
  137. * @param {number} [opt.end]
  138. * @param {number} [opt.startValue]
  139. * @param {number} [opt.endValue]
  140. */
  141. calculateDataWindow: function (opt) {
  142. var dataExtent = this._dataExtent;
  143. var axisModel = this.getAxisModel();
  144. var scale = axisModel.axis.scale;
  145. var rangePropMode = this._dataZoomModel.getRangePropMode();
  146. var percentExtent = [0, 100];
  147. var percentWindow = [opt.start, opt.end];
  148. var valueWindow = [];
  149. each(['startValue', 'endValue'], function (prop) {
  150. valueWindow.push(opt[prop] != null ? scale.parse(opt[prop]) : null);
  151. }); // Normalize bound.
  152. each([0, 1], function (idx) {
  153. var boundValue = valueWindow[idx];
  154. var boundPercent = percentWindow[idx]; // Notice: dataZoom is based either on `percentProp` ('start', 'end') or
  155. // on `valueProp` ('startValue', 'endValue'). The former one is suitable
  156. // for cases that a dataZoom component controls multiple axes with different
  157. // unit or extent, and the latter one is suitable for accurate zoom by pixel
  158. // (e.g., in dataZoomSelect). `valueProp` can be calculated from `percentProp`,
  159. // but it is awkward that `percentProp` can not be obtained from `valueProp`
  160. // accurately (because all of values that are overflow the `dataExtent` will
  161. // be calculated to percent '100%'). So we have to use
  162. // `dataZoom.getRangePropMode()` to mark which prop is used.
  163. // `rangePropMode` is updated only when setOption or dispatchAction, otherwise
  164. // it remains its original value.
  165. if (rangePropMode[idx] === 'percent') {
  166. if (boundPercent == null) {
  167. boundPercent = percentExtent[idx];
  168. } // Use scale.parse to math round for category or time axis.
  169. boundValue = scale.parse(numberUtil.linearMap(boundPercent, percentExtent, dataExtent, true));
  170. } else {
  171. // Calculating `percent` from `value` may be not accurate, because
  172. // This calculation can not be inversed, because all of values that
  173. // are overflow the `dataExtent` will be calculated to percent '100%'
  174. boundPercent = numberUtil.linearMap(boundValue, dataExtent, percentExtent, true);
  175. } // valueWindow[idx] = round(boundValue);
  176. // percentWindow[idx] = round(boundPercent);
  177. valueWindow[idx] = boundValue;
  178. percentWindow[idx] = boundPercent;
  179. });
  180. return {
  181. valueWindow: asc(valueWindow),
  182. percentWindow: asc(percentWindow)
  183. };
  184. },
  185. /**
  186. * Notice: reset should not be called before series.restoreData() called,
  187. * so it is recommanded to be called in "process stage" but not "model init
  188. * stage".
  189. *
  190. * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel
  191. */
  192. reset: function (dataZoomModel) {
  193. if (dataZoomModel !== this._dataZoomModel) {
  194. return;
  195. } // Culculate data window and data extent, and record them.
  196. this._dataExtent = calculateDataExtent(this, this._dimName, this.getTargetSeriesModels());
  197. var dataWindow = this.calculateDataWindow(dataZoomModel.option);
  198. this._valueWindow = dataWindow.valueWindow;
  199. this._percentWindow = dataWindow.percentWindow;
  200. setMinMaxSpan(this); // Update axis setting then.
  201. setAxisModel(this);
  202. },
  203. /**
  204. * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel
  205. */
  206. restore: function (dataZoomModel) {
  207. if (dataZoomModel !== this._dataZoomModel) {
  208. return;
  209. }
  210. this._valueWindow = this._percentWindow = null;
  211. setAxisModel(this, true);
  212. },
  213. /**
  214. * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel
  215. */
  216. filterData: function (dataZoomModel) {
  217. if (dataZoomModel !== this._dataZoomModel) {
  218. return;
  219. }
  220. var axisDim = this._dimName;
  221. var seriesModels = this.getTargetSeriesModels();
  222. var filterMode = dataZoomModel.get('filterMode');
  223. var valueWindow = this._valueWindow;
  224. if (filterMode === 'none') {
  225. return;
  226. } // FIXME
  227. // Toolbox may has dataZoom injected. And if there are stacked bar chart
  228. // with NaN data, NaN will be filtered and stack will be wrong.
  229. // So we need to force the mode to be set empty.
  230. // In fect, it is not a big deal that do not support filterMode-'filter'
  231. // when using toolbox#dataZoom, utill tooltip#dataZoom support "single axis
  232. // selection" some day, which might need "adapt to data extent on the
  233. // otherAxis", which is disabled by filterMode-'empty'.
  234. var otherAxisModel = this.getOtherAxisModel();
  235. if (dataZoomModel.get('$fromToolbox') && otherAxisModel && otherAxisModel.get('type') === 'category') {
  236. filterMode = 'empty';
  237. } // Process series data
  238. each(seriesModels, function (seriesModel) {
  239. var seriesData = seriesModel.getData();
  240. var dataDims = seriesModel.coordDimToDataDim(axisDim);
  241. if (filterMode === 'weakFilter') {
  242. seriesData && seriesData.filterSelf(function (dataIndex) {
  243. var leftOut;
  244. var rightOut;
  245. var hasValue;
  246. for (var i = 0; i < dataDims.length; i++) {
  247. var value = seriesData.get(dataDims[i], dataIndex);
  248. var thisHasValue = !isNaN(value);
  249. var thisLeftOut = value < valueWindow[0];
  250. var thisRightOut = value > valueWindow[1];
  251. if (thisHasValue && !thisLeftOut && !thisRightOut) {
  252. return true;
  253. }
  254. thisHasValue && (hasValue = true);
  255. thisLeftOut && (leftOut = true);
  256. thisRightOut && (rightOut = true);
  257. } // If both left out and right out, do not filter.
  258. return hasValue && leftOut && rightOut;
  259. });
  260. } else {
  261. seriesData && each(dataDims, function (dim) {
  262. if (filterMode === 'empty') {
  263. seriesModel.setData(seriesData.map(dim, function (value) {
  264. return !isInWindow(value) ? NaN : value;
  265. }));
  266. } else {
  267. seriesData.filterSelf(dim, isInWindow);
  268. }
  269. });
  270. }
  271. });
  272. function isInWindow(value) {
  273. return value >= valueWindow[0] && value <= valueWindow[1];
  274. }
  275. }
  276. };
  277. function calculateDataExtent(axisProxy, axisDim, seriesModels) {
  278. var dataExtent = [Infinity, -Infinity];
  279. each(seriesModels, function (seriesModel) {
  280. var seriesData = seriesModel.getData();
  281. if (seriesData) {
  282. each(seriesModel.coordDimToDataDim(axisDim), function (dim) {
  283. var seriesExtent = seriesData.getDataExtent(dim);
  284. seriesExtent[0] < dataExtent[0] && (dataExtent[0] = seriesExtent[0]);
  285. seriesExtent[1] > dataExtent[1] && (dataExtent[1] = seriesExtent[1]);
  286. });
  287. }
  288. });
  289. if (dataExtent[1] < dataExtent[0]) {
  290. dataExtent = [NaN, NaN];
  291. } // It is important to get "consistent" extent when more then one axes is
  292. // controlled by a `dataZoom`, otherwise those axes will not be synchronized
  293. // when zooming. But it is difficult to know what is "consistent", considering
  294. // axes have different type or even different meanings (For example, two
  295. // time axes are used to compare data of the same date in different years).
  296. // So basically dataZoom just obtains extent by series.data (in category axis
  297. // extent can be obtained from axis.data).
  298. // Nevertheless, user can set min/max/scale on axes to make extent of axes
  299. // consistent.
  300. fixExtentByAxis(axisProxy, dataExtent);
  301. return dataExtent;
  302. }
  303. function fixExtentByAxis(axisProxy, dataExtent) {
  304. var axisModel = axisProxy.getAxisModel();
  305. var min = axisModel.getMin(true); // For category axis, if min/max/scale are not set, extent is determined
  306. // by axis.data by default.
  307. var isCategoryAxis = axisModel.get('type') === 'category';
  308. var axisDataLen = isCategoryAxis && (axisModel.get('data') || []).length;
  309. if (min != null && min !== 'dataMin' && typeof min !== 'function') {
  310. dataExtent[0] = min;
  311. } else if (isCategoryAxis) {
  312. dataExtent[0] = axisDataLen > 0 ? 0 : NaN;
  313. }
  314. var max = axisModel.getMax(true);
  315. if (max != null && max !== 'dataMax' && typeof max !== 'function') {
  316. dataExtent[1] = max;
  317. } else if (isCategoryAxis) {
  318. dataExtent[1] = axisDataLen > 0 ? axisDataLen - 1 : NaN;
  319. }
  320. if (!axisModel.get('scale', true)) {
  321. dataExtent[0] > 0 && (dataExtent[0] = 0);
  322. dataExtent[1] < 0 && (dataExtent[1] = 0);
  323. } // For value axis, if min/max/scale are not set, we just use the extent obtained
  324. // by series data, which may be a little different from the extent calculated by
  325. // `axisHelper.getScaleExtent`. But the different just affects the experience a
  326. // little when zooming. So it will not be fixed until some users require it strongly.
  327. return dataExtent;
  328. }
  329. function setAxisModel(axisProxy, isRestore) {
  330. var axisModel = axisProxy.getAxisModel();
  331. var percentWindow = axisProxy._percentWindow;
  332. var valueWindow = axisProxy._valueWindow;
  333. if (!percentWindow) {
  334. return;
  335. } // [0, 500]: arbitrary value, guess axis extent.
  336. var precision = numberUtil.getPixelPrecision(valueWindow, [0, 500]);
  337. precision = Math.min(precision, 20); // isRestore or isFull
  338. var useOrigin = isRestore || percentWindow[0] === 0 && percentWindow[1] === 100;
  339. axisModel.setRange(useOrigin ? null : +valueWindow[0].toFixed(precision), useOrigin ? null : +valueWindow[1].toFixed(precision));
  340. }
  341. function setMinMaxSpan(axisProxy) {
  342. var minMaxSpan = axisProxy._minMaxSpan = {};
  343. var dataZoomModel = axisProxy._dataZoomModel;
  344. each(['min', 'max'], function (minMax) {
  345. minMaxSpan[minMax + 'Span'] = dataZoomModel.get(minMax + 'Span'); // minValueSpan and maxValueSpan has higher priority than minSpan and maxSpan
  346. var valueSpan = dataZoomModel.get(minMax + 'ValueSpan');
  347. if (valueSpan != null) {
  348. minMaxSpan[minMax + 'ValueSpan'] = valueSpan;
  349. valueSpan = axisProxy.getAxisModel().axis.scale.parse(valueSpan);
  350. if (valueSpan != null) {
  351. var dataExtent = axisProxy._dataExtent;
  352. minMaxSpan[minMax + 'Span'] = numberUtil.linearMap(dataExtent[0] + valueSpan, dataExtent, [0, 100], true);
  353. }
  354. }
  355. });
  356. }
  357. var _default = AxisProxy;
  358. module.exports = _default;