AxisProxy.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  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. /**
  20. * AUTO-GENERATED FILE. DO NOT MODIFY.
  21. */
  22. /*
  23. * Licensed to the Apache Software Foundation (ASF) under one
  24. * or more contributor license agreements. See the NOTICE file
  25. * distributed with this work for additional information
  26. * regarding copyright ownership. The ASF licenses this file
  27. * to you under the Apache License, Version 2.0 (the
  28. * "License"); you may not use this file except in compliance
  29. * with the License. You may obtain a copy of the License at
  30. *
  31. * http://www.apache.org/licenses/LICENSE-2.0
  32. *
  33. * Unless required by applicable law or agreed to in writing,
  34. * software distributed under the License is distributed on an
  35. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  36. * KIND, either express or implied. See the License for the
  37. * specific language governing permissions and limitations
  38. * under the License.
  39. */
  40. import { clone, defaults, each, map } from 'zrender/lib/core/util.js';
  41. import { asc, getAcceptableTickPrecision, linearMap, mathAbs, mathCeil, mathFloor, mathMax, mathMin, round } from '../../util/number.js';
  42. import sliderMove from '../helper/sliderMove.js';
  43. import { getAxisMainType, isCoordSupported } from './helper.js';
  44. import { ensureExtentAscSimply, SINGLE_REFERRING } from '../../util/model.js';
  45. import { isOrdinalScale, isTimeScale } from '../../scale/helper.js';
  46. import { AXIS_EXTENT_INFO_BUILD_FROM_DATA_ZOOM, scaleRawExtentInfoCreate } from '../../coord/scaleRawExtentInfo.js';
  47. /**
  48. * Operate single axis.
  49. * One axis can only operated by one axis operator.
  50. * Different dataZoomModels may be defined to operate the same axis.
  51. * (i.e. 'inside' data zoom and 'slider' data zoom components)
  52. * So dataZoomModels share one axisProxy in that case.
  53. */
  54. var AxisProxy = /** @class */function () {
  55. function AxisProxy(dimName, axisIndex, dataZoomModel, ecModel) {
  56. this._dimName = dimName;
  57. this._axisIndex = axisIndex;
  58. this.ecModel = ecModel;
  59. this._dataZoomModel = dataZoomModel;
  60. // /**
  61. // * @readOnly
  62. // * @private
  63. // */
  64. // this.hasSeriesStacked;
  65. }
  66. /**
  67. * Whether the axisProxy is hosted by dataZoomModel.
  68. */
  69. AxisProxy.prototype.hostedBy = function (dataZoomModel) {
  70. return this._dataZoomModel === dataZoomModel;
  71. };
  72. /**
  73. * @return `getWindow().value` can only have NaN or finite value.
  74. */
  75. AxisProxy.prototype.getWindow = function () {
  76. return clone(this._window);
  77. };
  78. AxisProxy.prototype.getTargetSeriesModels = function () {
  79. var seriesModels = [];
  80. this.ecModel.eachSeries(function (seriesModel) {
  81. if (isCoordSupported(seriesModel)) {
  82. var axisMainType = getAxisMainType(this._dimName);
  83. var axisModel = seriesModel.getReferringComponents(axisMainType, SINGLE_REFERRING).models[0];
  84. if (axisModel && this._axisIndex === axisModel.componentIndex) {
  85. seriesModels.push(seriesModel);
  86. }
  87. }
  88. }, this);
  89. return seriesModels;
  90. };
  91. AxisProxy.prototype.getAxisModel = function () {
  92. return this.ecModel.getComponent(this._dimName + 'Axis', this._axisIndex);
  93. };
  94. AxisProxy.prototype.getMinMaxSpan = function () {
  95. return clone(this._minMaxSpan);
  96. };
  97. /**
  98. * [CAVEAT] Keep this method pure, so that it can be called multiple times.
  99. */
  100. AxisProxy.prototype.calculateDataWindow = function (opt) {
  101. var dataExtent = this._extent;
  102. var axis = this.getAxisModel().axis;
  103. var scale = axis.scale;
  104. var dataZoomModel = this._dataZoomModel;
  105. var rangePropMode = dataZoomModel.getRangePropMode();
  106. var percentExtent = [0, 100];
  107. var percentWindow = [];
  108. var valueWindow = [];
  109. var hasPropModeValue;
  110. var needRound = [false, false];
  111. // NOTE:
  112. // The current percentage base calculation strategy:
  113. // - If the window boundary is NOT at 0% or 100%, boundary values are derived from the raw extent
  114. // (series data + axis.min/max; see `ScaleRawExtentInfo['makeForZoom']`). Any subsequent "nice"
  115. // expansion are excluded.
  116. // - If the window boundary is at 0% or 100%, the "nice"-expanded portion is included.
  117. // Pros:
  118. // - The effect may be preferable when users intend to quickly narrow down to data details,
  119. // especially when "nice strategy" excessively expands the extent.
  120. // - It simplifies the logic, otherwise, "nice strategy" would need to be applied twice (full window
  121. // + current window).
  122. // Cons:
  123. // - This strategy causes jitter when switching dataZoom to/from 0%/100% (though generally acceptable).
  124. each(['start', 'end'], function (prop, idx) {
  125. var boundPercent = opt[prop];
  126. var boundValue = opt[prop + 'Value'];
  127. // NOTE: dataZoom is based either on `percentProp` ('start', 'end') or
  128. // on `valueProp` ('startValue', 'endValue').
  129. // The former one is suitable for cases that a dataZoom component controls multiple
  130. // axes with different unit or extent, and the latter one is suitable for accurate
  131. // zoom by pixel (e.g., in dataZoomSelect).
  132. // we use `getRangePropMode()` to mark which prop is used. `rangePropMode` is updated
  133. // only when setOption or dispatchAction, otherwise it remains its original value.
  134. // (Why not only record `percentProp` and always map to `valueProp`? Because
  135. // the map `valueProp` -> `percentProp` -> `valueProp` probably not the original
  136. // `valueProp`. consider two axes constrolled by one dataZoom. They have different
  137. // data extent. All of values that are overflow the `dataExtent` will be calculated
  138. // to percent '100%').
  139. if (rangePropMode[idx] === 'percent') {
  140. boundPercent == null && (boundPercent = percentExtent[idx]);
  141. boundValue = linearMap(boundPercent, percentExtent, dataExtent);
  142. needRound[idx] = true;
  143. } else {
  144. hasPropModeValue = true;
  145. // NOTE: `scale.parse` can also round input for 'time' or 'ordinal' scale.
  146. if (boundValue == null) {
  147. boundValue = dataExtent[idx];
  148. } else {
  149. // Need to parse user inputs from ec option or action param.
  150. boundValue = scale.parse(boundValue);
  151. if (scale.sanitize) {
  152. boundValue = scale.sanitize(boundValue, dataExtent);
  153. }
  154. }
  155. // Calculating `percent` from `value` may be not accurate, because
  156. // This calculation can not be inverted, because all of values that
  157. // are overflow the `dataExtent` will be calculated to percent '100%'
  158. boundPercent = linearMap(boundValue, dataExtent, percentExtent);
  159. }
  160. // fallback to extent start/end when parsed value or percent is invalid
  161. valueWindow[idx] = boundValue == null || isNaN(boundValue) ? dataExtent[idx] : boundValue;
  162. percentWindow[idx] = boundPercent == null || isNaN(boundPercent) ? percentExtent[idx] : boundPercent;
  163. });
  164. // Historical behavior - enable switching.
  165. asc(valueWindow);
  166. asc(percentWindow);
  167. // The windows specified from `dispatchAction` or `setOption` may:
  168. // (1) be out of the extent, or
  169. // (2) do not comply with `minSpan/maxSpan`, `minValueSpan/maxValueSpan`.
  170. // So we clamp them here.
  171. // But we don't restrict window by `zoomLock` here, because we see `zoomLock` just as a
  172. // interaction constraint, where API is able to initialize/modify the window size even
  173. // though `zoomLock` specified.
  174. // PENDING: For historical reason, the option design is partially incompatible:
  175. // If `option.start` and `option.endValue` are specified, and when we choose whether
  176. // `min/maxValueSpan` or `minSpan/maxSpan` is applied, neither one is intuitive.
  177. // (Currently using `minValueSpan/maxValueSpan`.)
  178. var spans = this._minMaxSpan;
  179. hasPropModeValue ? restrictSet(valueWindow, percentWindow, dataExtent, percentExtent, false) : restrictSet(percentWindow, valueWindow, percentExtent, dataExtent, true);
  180. function restrictSet(fromWindow, toWindow, fromExtent, toExtent, toValue) {
  181. var suffix = toValue ? 'Span' : 'ValueSpan';
  182. sliderMove(0, fromWindow, fromExtent, 'all', spans['min' + suffix], spans['max' + suffix]);
  183. for (var i = 0; i < 2; i++) {
  184. toWindow[i] = linearMap(fromWindow[i], fromExtent, toExtent, true);
  185. if (toValue) {
  186. toWindow[i] = toWindow[i];
  187. needRound[i] = true;
  188. }
  189. }
  190. ensureExtentAscSimply(toWindow);
  191. }
  192. // - In 'time' and 'ordinal' scale, rounding by 0 is required.
  193. // - In 'interval' and 'log' scale, we round values for acceptable display with acceptable accuracy loose.
  194. // "Values" can be rounded only if they are generated from `percent`, since user-specified "value"
  195. // should be respected, and `DataZoomSelect` already performs its own rounding.
  196. // - Currently we only round "value" but not "percent", since there is no need so far.
  197. // - MEMO: See also #3228 and commit a89fd0d7f1833ecf08a4a5b7ecf651b4a0d8da41
  198. // - PENDING: The rounding result may slightly overflow the restriction from `min/maxSpan`,
  199. // but it is acceptable so far.
  200. var isScaleOrdinalOrTime = isOrdinalScale(scale) || isTimeScale(scale);
  201. // Typically pxExtent has been ready in coordSys create. (See `create` of `Grid.ts`)
  202. var pxExtent = axis.getExtent();
  203. // NOTICE: this pxSpan may be not accurate yet due to "outerBounds" logic, but acceptable.
  204. var pxSpan = mathAbs(pxExtent[1] - pxExtent[0]);
  205. var precision = isScaleOrdinalOrTime ? 0
  206. // NOTICE: We deliberately do not allow specifying this precision by users, until real requirements
  207. // occur. Otherwise, unnecessary complexity and bad case may be introduced. A small precision may
  208. // cause the rounded ends overflow the expected min/max significantly. And this precision effectively
  209. // determines the size of a roaming step, and a big step would likely constantly cut through series
  210. // shapes in an unexpected place and cause visual artifacts (e.g., for bar series). Although
  211. // theroetically that defect can be resolved by introducing extra spaces between axis min/max tick
  212. // and axis boundary (see `SCALE_EXTENT_KIND_MAPPING`), it's complicated and unnecessary.
  213. : getAcceptableTickPrecision(valueWindow, pxSpan, 0.5);
  214. each([[0, mathCeil], [1, mathFloor]], function (_a) {
  215. var idx = _a[0],
  216. ceilOrFloor = _a[1];
  217. if (!needRound[idx] || !isFinite(precision)) {
  218. return;
  219. }
  220. valueWindow[idx] = round(valueWindow[idx], precision);
  221. valueWindow[idx] = mathMin(dataExtent[1], mathMax(dataExtent[0], valueWindow[idx])); // Clamp.
  222. if (percentWindow[idx] === percentExtent[idx]) {
  223. // When `percent` is 0 or 100, `value` must be `dataExtent[0]` or `dataExtent[1]`
  224. // regardless of the calculated precision.
  225. // NOTE: `percentWindow` is never over [0, 100] at this moment.
  226. valueWindow[idx] = dataExtent[idx];
  227. if (isScaleOrdinalOrTime) {
  228. // In case that dataExtent[idx] is not an integer (may occur since it comes from user input)
  229. valueWindow[idx] = ceilOrFloor(valueWindow[idx]);
  230. }
  231. }
  232. });
  233. ensureExtentAscSimply(valueWindow);
  234. var percentInvertedWindow = [linearMap(valueWindow[0], dataExtent, percentExtent, true), linearMap(valueWindow[1], dataExtent, percentExtent, true)];
  235. ensureExtentAscSimply(percentInvertedWindow);
  236. return {
  237. value: valueWindow,
  238. percent: percentWindow,
  239. percentInverted: percentInvertedWindow,
  240. valuePrecision: precision
  241. };
  242. };
  243. /**
  244. * Notice: reset should not be called before series.restoreData() is called,
  245. * so it is recommended to be called in "process stage" but not "model init
  246. * stage".
  247. */
  248. AxisProxy.prototype.reset = function (dataZoomModel, alignToPercentInverted) {
  249. if (!this.hostedBy(dataZoomModel)) {
  250. return;
  251. }
  252. // It is important to get "consistent" extent when more then one axes is
  253. // controlled by a `dataZoom`, otherwise those axes will not be synchronized
  254. // when zooming. But it is difficult to know what is "consistent", considering
  255. // axes have different type or even different meanings (For example, two
  256. // time axes are used to compare data of the same date in different years).
  257. // So basically dataZoom just obtains extent by series.data (in category axis
  258. // extent can be obtained from axis.data).
  259. // Nevertheless, user can set min/max/scale on axes to make extent of axes
  260. // consistent.
  261. var axis = this.getAxisModel().axis;
  262. scaleRawExtentInfoCreate(axis, AXIS_EXTENT_INFO_BUILD_FROM_DATA_ZOOM);
  263. var rawExtentInfo = axis.scale.rawExtentInfo;
  264. this._extent = rawExtentInfo.makeNoZoom();
  265. // `calculateDataWindow` uses min/maxSpan.
  266. this._updateMinMaxSpan();
  267. var opt = dataZoomModel.settledOption;
  268. if (alignToPercentInverted) {
  269. opt = defaults({
  270. start: alignToPercentInverted[0],
  271. end: alignToPercentInverted[1]
  272. }, opt);
  273. }
  274. var _a = this._window = this.calculateDataWindow(opt),
  275. percent = _a.percent,
  276. value = _a.value;
  277. if (percent[0] !== 0) {
  278. rawExtentInfo.setZoomMM(0, value[0]);
  279. }
  280. if (percent[1] !== 100) {
  281. rawExtentInfo.setZoomMM(1, value[1]);
  282. }
  283. };
  284. AxisProxy.prototype.filterData = function (dataZoomModel, api) {
  285. if (!this.hostedBy(dataZoomModel)) {
  286. return;
  287. }
  288. var axisDim = this._dimName;
  289. var seriesModels = this.getTargetSeriesModels();
  290. var filterMode = dataZoomModel.get('filterMode');
  291. var valueWindow = this._window.value;
  292. if (filterMode === 'none') {
  293. return;
  294. }
  295. // FIXME
  296. // Toolbox may has dataZoom injected. And if there are stacked bar chart
  297. // with NaN data, NaN will be filtered and stack will be wrong.
  298. // So we need to force the mode to be set empty.
  299. // In fact, it is not a big deal that do not support filterMode-'filter'
  300. // when using toolbox#dataZoom, util tooltip#dataZoom support "single axis
  301. // selection" some day, which might need "adapt to data extent on the
  302. // otherAxis", which is disabled by filterMode-'empty'.
  303. // But currently, stack has been fixed to based on value but not index,
  304. // so this is not an issue any more.
  305. // let otherAxisModel = this.getOtherAxisModel();
  306. // if (dataZoomModel.get('$fromToolbox')
  307. // && otherAxisModel
  308. // && otherAxisModel.hasSeriesStacked
  309. // ) {
  310. // filterMode = 'empty';
  311. // }
  312. // TODO
  313. // filterMode 'weakFilter' and 'empty' is not optimized for huge data yet.
  314. each(seriesModels, function (seriesModel) {
  315. var seriesData = seriesModel.getData();
  316. var dataDims = seriesData.mapDimensionsAll(axisDim);
  317. if (!dataDims.length) {
  318. return;
  319. }
  320. if (filterMode === 'weakFilter') {
  321. var store_1 = seriesData.getStore();
  322. var dataDimIndices_1 = map(dataDims, function (dim) {
  323. return seriesData.getDimensionIndex(dim);
  324. }, seriesData);
  325. seriesData.filterSelf(function (dataIndex) {
  326. var leftOut;
  327. var rightOut;
  328. var hasValue;
  329. for (var i = 0; i < dataDims.length; i++) {
  330. var value = store_1.get(dataDimIndices_1[i], dataIndex);
  331. var thisHasValue = !isNaN(value);
  332. var thisLeftOut = value < valueWindow[0];
  333. var thisRightOut = value > valueWindow[1];
  334. if (thisHasValue && !thisLeftOut && !thisRightOut) {
  335. return true;
  336. }
  337. thisHasValue && (hasValue = true);
  338. thisLeftOut && (leftOut = true);
  339. thisRightOut && (rightOut = true);
  340. }
  341. // If both left out and right out, do not filter.
  342. return hasValue && leftOut && rightOut;
  343. });
  344. } else {
  345. each(dataDims, function (dim) {
  346. if (filterMode === 'empty') {
  347. seriesModel.setData(seriesData = seriesData.map(dim, function (value) {
  348. return !isInWindow(value) ? NaN : value;
  349. }));
  350. } else {
  351. var range = {};
  352. range[dim] = valueWindow;
  353. // console.time('AxisProxy_selectRange');
  354. seriesData.selectRange(range);
  355. // console.timeEnd('AxisProxy_selectRange');
  356. }
  357. });
  358. }
  359. each(dataDims, function (dim) {
  360. seriesData.setApproximateExtent(valueWindow, dim);
  361. });
  362. });
  363. function isInWindow(value) {
  364. return value >= valueWindow[0] && value <= valueWindow[1];
  365. }
  366. };
  367. AxisProxy.prototype._updateMinMaxSpan = function () {
  368. var minMaxSpan = this._minMaxSpan = {};
  369. var dataZoomModel = this._dataZoomModel;
  370. var dataExtent = this._extent;
  371. each(['min', 'max'], function (minMax) {
  372. var percentSpan = dataZoomModel.get(minMax + 'Span');
  373. var valueSpan = dataZoomModel.get(minMax + 'ValueSpan');
  374. valueSpan != null && (valueSpan = this.getAxisModel().axis.scale.parse(valueSpan));
  375. // minValueSpan and maxValueSpan has higher priority than minSpan and maxSpan
  376. if (valueSpan != null) {
  377. percentSpan = linearMap(dataExtent[0] + valueSpan, dataExtent, [0, 100], true);
  378. } else if (percentSpan != null) {
  379. valueSpan = linearMap(percentSpan, [0, 100], dataExtent, true) - dataExtent[0];
  380. }
  381. minMaxSpan[minMax + 'Span'] = percentSpan;
  382. minMaxSpan[minMax + 'ValueSpan'] = valueSpan;
  383. }, this);
  384. };
  385. return AxisProxy;
  386. }();
  387. export default AxisProxy;