scaleRawExtentInfo.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  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 { assert, isArray, eqNaN, isFunction, each, createHashMap } from 'zrender/lib/core/util.js';
  41. import { parsePercent } from 'zrender/lib/contain/text.js';
  42. import { isIntervalScale, isLogScale, isOrdinalScale, isTimeScale } from '../scale/helper.js';
  43. import { makeInner, initExtentForUnion, unionExtentFromNumber, isValidNumberForExtent, extentHasValue, unionExtentFromExtent, unionExtentStartFromNumber, unionExtentEndFromNumber, ensureExtentAscSimply } from '../util/model.js';
  44. import { discourageOnAxisZero, getDataDimensionsOnAxis, isAxisOnBand } from './axisHelper.js';
  45. import { getCoordForCoordSysUsageKindBox } from '../core/CoordinateSystem.js';
  46. import { error } from '../util/log.js';
  47. import { isNullableNumberFinite, mathMax, mathMin } from '../util/number.js';
  48. import { SCALE_EXTENT_KIND_MAPPING } from '../scale/scaleMapper.js';
  49. import { eachKeyOnAxis, eachSeriesOnAxis } from './axisStatistics.js';
  50. /**
  51. * NOTICE: Can be only used in `ensureScaleStore(axisLike)`.
  52. *
  53. * In most cases the instances of `Axis` and `Scale` are one-to-one mapping and share the same lifecycle.
  54. * But in some external usage (such as echarts-gl), axis instance does not necessarily exist, and only
  55. * scale instance and axisModel are used. Therefore we store the internal info on scale instance directly.
  56. */
  57. var scaleInner = makeInner();
  58. export var AXIS_EXTENT_INFO_BUILD_FROM_COORD_SYS_UPDATE = 1;
  59. export var AXIS_EXTENT_INFO_BUILD_FROM_DATA_ZOOM = 2;
  60. var AXIS_EXTENT_INFO_BUILD_FROM_EMPTY = 3;
  61. var ScaleRawExtentInfo = /** @class */function () {
  62. function ScaleRawExtentInfo(scale, model,
  63. // Typically: data extent from all series on this axis.
  64. dataExtent, requireStartValue, requireContainShape) {
  65. var isOrdinal = isOrdinalScale(scale);
  66. var axisDataLen = isOrdinal
  67. // FIXME: there is a flaw here: if there is no "block" data processor like `dataZoom`,
  68. // and progressive rendering is using, here the category result might just only contain
  69. // the processed chunk rather than the entire result.
  70. ? model.getCategories().length : null;
  71. // [CATEGORY_AXIS_MODEL_DATA_IS_EMPTY_ARRAY]:
  72. // This is only for backward compatibility - `xxxAxis: {data: []}` can declare this axis as
  73. // a "category" axis and use `series.data` to determine its extent but the axis is blank - only
  74. // axis line is displayed. This is a conincidence, but it is used in some cases.
  75. var categoryAxisModelDataIsEmptyArray;
  76. if (isOrdinal) {
  77. var axisModelDataArray = model.getCategories(true);
  78. categoryAxisModelDataIsEmptyArray = axisModelDataArray && !axisModelDataArray.length;
  79. }
  80. // NOTE: also considered the input dataExtent may be still in the initialized state `[Infinity, -Infinity]`.
  81. var dataMM = dataExtent.slice();
  82. // custom dataMin/dataMax.
  83. // Also considered `modelDataMinMax[0] > modelDataMinMax[1]` may occur.
  84. if (isIntervalScale(scale) || isLogScale(scale) || isTimeScale(scale)) {
  85. unionExtentStartFromNumber(dataMM, parseAxisModelMinMax(scale, model.get('dataMin', true)));
  86. unionExtentEndFromNumber(dataMM, parseAxisModelMinMax(scale, model.get('dataMax', true)));
  87. }
  88. if (!extentHasValue(dataMM)) {
  89. // dataMM may be still `[Infinity, -Infinity]`, we use `NaN` on the subsequent calculations
  90. // to force the `noZoomEffMM` to be `[NaN, NaN]` if needed.
  91. dataMM[0] = dataMM[1] = NaN;
  92. }
  93. var noZoomEffMM = [];
  94. var fixMM = [false, false];
  95. // Notice: When min/max is not set (that is, when there are null/undefined,
  96. // which is the most common case), these cases should be ensured:
  97. // (1) For 'ordinal', show all axis.data.
  98. // (2) For others:
  99. // + `boundaryGap` is applied (if min/max set, boundaryGap is
  100. // disabled).
  101. // + If `needIncludeZero`, min/max should be zero, otherwise, min/max should
  102. // be the result that originalExtent enlarged by boundaryGap.
  103. // (3) If no data, it should be ensured that `scale.setBlank` is set.
  104. var modelMinRaw = model.get('min', true);
  105. if (modelMinRaw === 'dataMin') {
  106. noZoomEffMM[0] = dataMM[0];
  107. fixMM[0] = true;
  108. } else {
  109. noZoomEffMM[0] = parseAxisModelMinMax(scale, isFunction(modelMinRaw)
  110. // This callback always provides users the full data extent (before data is filtered).
  111. ? modelMinRaw({
  112. min: dataMM[0],
  113. max: dataMM[1]
  114. }) : modelMinRaw);
  115. // If `xxxAxis.min: null/undefined`, min should not be fixed.
  116. fixMM[0] = noZoomEffMM[0] != null;
  117. }
  118. var modelMaxRaw = model.get('max', true);
  119. if (modelMaxRaw === 'dataMax') {
  120. noZoomEffMM[1] = dataMM[1];
  121. fixMM[1] = true;
  122. } else {
  123. noZoomEffMM[1] = parseAxisModelMinMax(scale, isFunction(modelMaxRaw)
  124. // This callback always provides users the full data extent (before data is filtered).
  125. ? modelMaxRaw({
  126. min: dataMM[0],
  127. max: dataMM[1]
  128. }) : modelMaxRaw);
  129. // If `xxxAxis.max: null/undefined`, max should not be fixed.
  130. fixMM[1] = noZoomEffMM[1] != null;
  131. }
  132. var boundaryGap = parseBoundaryGapOption(scale, model);
  133. var span = !isOrdinal
  134. // PENDING: Historicall behavior but may not reasonable enough.
  135. ? dataMM[1] - dataMM[0] || Math.abs(dataMM[0]) : null;
  136. // NOTE: If a numeric axis min/max is specified as 'dataMin'/'dataMax',
  137. // `boundaryGap` will not be used.
  138. if (noZoomEffMM[0] == null) {
  139. noZoomEffMM[0] = isOrdinal ? categoryAxisModelDataIsEmptyArray ? dataMM[0] : axisDataLen ? 0 : NaN : dataMM[0] - boundaryGap[0] * span;
  140. }
  141. if (noZoomEffMM[1] == null) {
  142. noZoomEffMM[1] = isOrdinal ? categoryAxisModelDataIsEmptyArray ? dataMM[1] : axisDataLen ? axisDataLen - 1 : NaN : dataMM[1] + boundaryGap[1] * span;
  143. }
  144. // Normalize to `NaN` if invalid; e.g., this may occur when `dataMM` has Infinity.
  145. !isValidNumberForExtent(noZoomEffMM[0]) && (noZoomEffMM[0] = NaN);
  146. !isValidNumberForExtent(noZoomEffMM[1]) && (noZoomEffMM[1] = NaN);
  147. var isBlank = categoryAxisModelDataIsEmptyArray || eqNaN(noZoomEffMM[0]) || eqNaN(noZoomEffMM[1]) || isOrdinal && !axisDataLen;
  148. // NOTE: `needIncludeZero` is not applicable to LogScale, TimeScale, OrdinalScale.
  149. var needIncludeZeroApplicable = isIntervalScale(scale);
  150. var needIncludeZero = needIncludeZeroApplicable && model.needIncludeZero && model.needIncludeZero();
  151. if (needIncludeZero) {
  152. if (noZoomEffMM[0] > 0 && noZoomEffMM[1] > 0 && !fixMM[0]) {
  153. noZoomEffMM[0] = 0;
  154. // fixMM[0] = true;
  155. }
  156. if (noZoomEffMM[0] < 0 && noZoomEffMM[1] < 0 && !fixMM[1]) {
  157. noZoomEffMM[1] = 0;
  158. // fixMM[1] = true;
  159. }
  160. }
  161. var needToggleAxisInverse = false;
  162. if (noZoomEffMM[0] > noZoomEffMM[1]) {
  163. // Historically, if users set `xxxAxis.min > xxxAxis.max`, or `xxxAxis.max < dataExtent[0]`,
  164. // or `xxxAxis.min > dataExtent[1]` the behavior is sometimes like `xxxAxis.inverse = true`,
  165. // sometimes abnormal. We remain backward compatible with the former one, though this feature
  166. // may not be reasonable.
  167. // And handle it after "needIncludeZero" is also for backward compatibility.
  168. noZoomEffMM.reverse();
  169. needToggleAxisInverse = true;
  170. }
  171. var startValue = parseAxisModelMinMax(scale, model.get('startValue', true));
  172. var startValueSpecified = startValue != null;
  173. if (!isNullableNumberFinite(startValue) && requireStartValue) {
  174. startValue = scale.getDefaultStartValue ? scale.getDefaultStartValue() : 0;
  175. }
  176. if (isNullableNumberFinite(startValue)
  177. // Keep backward compatibility and enable `xxxAxis.scale: true` enabled on bar series:
  178. // if `xxxAxis.scale: true` and `startValue` is not specified, do not union the default `startValue`,
  179. && (startValueSpecified || !needIncludeZeroApplicable || needIncludeZero)) {
  180. if (startValue < noZoomEffMM[0] && !fixMM[0]) {
  181. noZoomEffMM[0] = startValue;
  182. fixMM[0] = true;
  183. } else if (startValue > noZoomEffMM[1] && !fixMM[1]) {
  184. noZoomEffMM[1] = startValue;
  185. fixMM[1] = true;
  186. }
  187. }
  188. var internal = this._i = {
  189. scale: scale,
  190. dataMM: dataMM,
  191. noZoomEffMM: noZoomEffMM,
  192. zoomMM: [],
  193. fixMM: fixMM,
  194. zoomFixMM: [false, false],
  195. startValue: startValue,
  196. isBlank: isBlank,
  197. incl0: needIncludeZero,
  198. tggAxInv: needToggleAxisInverse,
  199. ctnShp: requireContainShape
  200. };
  201. sanitizeExtent(internal, noZoomEffMM);
  202. }
  203. ScaleRawExtentInfo.prototype.makeNoZoom = function () {
  204. return this._i.noZoomEffMM.slice();
  205. };
  206. ScaleRawExtentInfo.prototype.makeFinal = function () {
  207. var internal = this._i;
  208. var zoomMM = internal.zoomMM;
  209. var noZoomEffMM = internal.noZoomEffMM;
  210. var zoomFixMM = internal.zoomFixMM;
  211. var fixMM = internal.fixMM;
  212. var result = {
  213. fixMM: fixMM,
  214. zoomFixMM: zoomFixMM,
  215. isBlank: internal.isBlank,
  216. incl0: internal.incl0,
  217. tggAxInv: internal.tggAxInv,
  218. ctnShp: internal.ctnShp,
  219. effMM: noZoomEffMM.slice()
  220. };
  221. var effMM = result.effMM;
  222. // NOTE: Switching `fixMM` probably leads to abrupt extent changes when draging a `dataZoom`
  223. // handle, since `fixMM` impact the "nice extent" and "nice ticks" calculation.
  224. // Consider a case:
  225. // dataZoom `start` is 2% but its `end` is 100%, (or vice versa), we currently only set `fixMM[0]`
  226. // as `true` but remain `fixMM[1]` as `false` for this case to avoid unnecessary abrupt change.
  227. // Incidentally, the effect is not unacceptable if we set both `fixMM[0]/[1]` as `true`.
  228. if (zoomMM[0] != null) {
  229. effMM[0] = zoomMM[0];
  230. fixMM[0] = zoomFixMM[0] = true;
  231. }
  232. if (zoomMM[1] != null) {
  233. effMM[1] = zoomMM[1];
  234. fixMM[1] = zoomFixMM[1] = true;
  235. }
  236. sanitizeExtent(internal, effMM);
  237. return result;
  238. };
  239. ScaleRawExtentInfo.prototype.makeRenderInfo = function () {
  240. return {
  241. startValue: this._i.startValue
  242. };
  243. };
  244. /**
  245. * NOTICE:
  246. * - Do not set them if the percent are 0% or 100%. (See `AxisProxy['reset']`.)
  247. * - The caller must ensure `start <= end` and the range is equal or less then `noZoomEffMM`.
  248. * (See `AxisProxy['calculateDataWindow']`.)
  249. * - The outcome `_zoomMM` may have both `NullUndefined` and a finite value, like `[undefined, 123]`.
  250. */
  251. ScaleRawExtentInfo.prototype.setZoomMM = function (idxMinMax, val) {
  252. this._i.zoomMM[idxMinMax] = val;
  253. };
  254. return ScaleRawExtentInfo;
  255. }();
  256. export { ScaleRawExtentInfo };
  257. /**
  258. * Should be called when a new extent is created or modified.
  259. */
  260. function sanitizeExtent(internal, mm) {
  261. var scale = internal.scale;
  262. var dataMM = internal.dataMM;
  263. if (scale.sanitize) {
  264. mm[0] = scale.sanitize(mm[0], dataMM);
  265. mm[1] = scale.sanitize(mm[1], dataMM);
  266. ensureExtentAscSimply(mm);
  267. }
  268. }
  269. function parseAxisModelMinMax(scale, minMax) {
  270. return minMax == null ? null // null/undefined means not specified and other default values can be applied.
  271. : eqNaN(minMax) ? NaN // NaN means a deliberate invalid number.
  272. : scale.parse(minMax);
  273. }
  274. function parseBoundaryGapOption(scale, model) {
  275. var boundaryGapOptionArr;
  276. if (isOrdinalScale(scale)) {
  277. boundaryGapOptionArr = [0, 0];
  278. } else {
  279. var boundaryGap = model.get('boundaryGap');
  280. if (typeof boundaryGap === 'boolean') {
  281. if (process.env.NODE_ENV !== 'production') {
  282. if (boundaryGap === true) {
  283. console.warn('Boolean type for boundaryGap is only ' + 'allowed for ordinal axis. Please use string in ' + 'percentage instead, e.g., "20%". Currently, ' + 'boundaryGap is set to 0.');
  284. }
  285. }
  286. boundaryGap = null;
  287. }
  288. boundaryGapOptionArr = isArray(boundaryGap) ? boundaryGap : [boundaryGap, boundaryGap];
  289. }
  290. return [parseBoundaryGapOptionItem(boundaryGapOptionArr[0]), parseBoundaryGapOptionItem(boundaryGapOptionArr[1])];
  291. }
  292. function parseBoundaryGapOptionItem(opt) {
  293. return parsePercent(typeof opt === 'boolean' ? 0 : opt, 1) || 0;
  294. }
  295. /**
  296. * NOTE: `associateSeriesWithAxis` is not necessarily called, e.g., when
  297. * an axis is not used by any series.
  298. */
  299. function ensureScaleStore(axisLike) {
  300. var store = scaleInner(axisLike.scale);
  301. if (!store.extent) {
  302. store.extent = initExtentForUnion();
  303. }
  304. return store;
  305. }
  306. /**
  307. * This supports union extent on case like: pie (or other similar series)
  308. * lays out on cartesian2d.
  309. * @see scaleRawExtentInfoCreate
  310. */
  311. export function scaleRawExtentInfoEnableBoxCoordSysUsage(axisLike, coordSysDimIdxMap) {
  312. ensureScaleStore(axisLike).dimIdxInCoord = coordSysDimIdxMap.get(axisLike.dim);
  313. }
  314. /**
  315. * @usage
  316. * class SomeCoordSys {
  317. * static create() {
  318. * ecModel.eachSeries(function (seriesModel) {
  319. * associateSeriesWithAxis(axis1, seriesModel, ...);
  320. * associateSeriesWithAxis(axis2, seriesModel, ...);
  321. * // ...
  322. * });
  323. * }
  324. * update() {
  325. * scaleRawExtentInfoCreate(axis1);
  326. * scaleRawExtentInfoCreate(axis2);
  327. * }
  328. * }
  329. * class AxisProxy {
  330. * reset() {
  331. * scaleRawExtentInfoCreate(axis1);
  332. * }
  333. * }
  334. *
  335. * NOTICE:
  336. * - `associateSeriesWithAxis`(in `axisStatistics.ts`) should be called in:
  337. * - Coord sys create method.
  338. * - `scaleRawExtentInfoCreate` should be typically called in:
  339. * - `dataZoom` processor. It requires processing like:
  340. * 1. Filter series data by dataZoom1;
  341. * 2. Union the filtered data and init the extent of the orthogonal axes, which is the 100% of dataZoom2;
  342. * 3. Filter series data by dataZoom2;
  343. * 4. ...
  344. * - Coord sys update method, for other axes that not covered by `dataZoom`.
  345. * NOTE: If a `dataZoom` covers this series, this data and its extent has been dataZoom-filtered.
  346. * Therefore this handling should not before `dataZoom`.
  347. * - The callback of `min`/`max` in ec option should NOT be called multiple times,
  348. * therefore, we initialize `ScaleRawExtentInfo` uniformly in `scaleRawExtentInfoCreate`.
  349. *
  350. * @see SCALE_EXTENT_CONSTRUCTION for the full processing flow.
  351. */
  352. export function scaleRawExtentInfoCreate(axis, from) {
  353. var scale = axis.scale;
  354. var model = axis.model;
  355. var axisDim = axis.dim;
  356. if (process.env.NODE_ENV !== 'production') {
  357. assert(scale && model && axisDim);
  358. }
  359. if (scale.rawExtentInfo) {
  360. if (process.env.NODE_ENV !== 'production') {
  361. // Check for incorrect impl - the duplicated calling of this method is only allowed in
  362. // these cases:
  363. // - First in `AxisProxy['reset']` (for dataZoom)
  364. // - Then in `CoordinateSystem['update']`.
  365. // - Then after `chart.appendData()` due to `dirtyOnOverallProgress: true`
  366. assert(scale.rawExtentInfo.from !== from || from === AXIS_EXTENT_INFO_BUILD_FROM_DATA_ZOOM);
  367. }
  368. return;
  369. }
  370. scaleRawExtentInfoCreateDeal(scale, axis, axisDim, model, from);
  371. }
  372. function scaleRawExtentInfoCreateDeal(scale, axis, axisDim, model, from) {
  373. var scaleStore = ensureScaleStore(axis);
  374. var extent = scaleStore.extent;
  375. var requireStartValue = false;
  376. eachSeriesOnAxis(axis, function (seriesModel) {
  377. if (seriesModel.boxCoordinateSystem) {
  378. // This supports union extent on case like: pie (or other similar series)
  379. // lays out on cartesian2d.
  380. var coord = getCoordForCoordSysUsageKindBox(seriesModel).coord;
  381. var dimIdx = scaleStore.dimIdxInCoord;
  382. if (!(dimIdx >= 0)) {
  383. if (process.env.NODE_ENV !== 'production') {
  384. // Require `scaleRawExtentInfoEnableBoxCoordSysUsage` have been called to support it.
  385. // But if users set it, give a error log but no exceptions.
  386. error("Property \"series.coord\" is not supported on axis " + seriesModel.boxCoordinateSystem.type + ".");
  387. }
  388. }
  389. // Only `[val1, val2]` case needs to be supported currently.
  390. else if (isArray(coord)) {
  391. var coordItem = coord[dimIdx];
  392. if (coordItem != null && !isArray(coordItem)) {
  393. unionExtentFromNumber(extent, scale.parse(coordItem));
  394. }
  395. }
  396. } else if (seriesModel.coordinateSystem) {
  397. // NOTE: This data may have been filtered by dataZoom on orthogonal axes.
  398. var data_1 = seriesModel.getData();
  399. if (data_1) {
  400. var filter_1 = scale.getFilter ? scale.getFilter() : null;
  401. each(getDataDimensionsOnAxis(data_1, axisDim), function (dim) {
  402. unionExtentFromExtent(extent, data_1.getApproximateExtent(dim, filter_1));
  403. });
  404. }
  405. if (seriesModel.__requireStartValue && seriesModel.__requireStartValue(axis)) {
  406. requireStartValue = true;
  407. }
  408. }
  409. });
  410. var requireContainShape = determineRequireContainShape(scale, axis, model);
  411. var rawExtentInfo = new ScaleRawExtentInfo(scale, model, extent, requireStartValue, requireContainShape);
  412. injectScaleRawExtentInfo(scale, rawExtentInfo, from);
  413. scaleStore.extent = null; // Clean up
  414. }
  415. /**
  416. * `rawExtentInfo` may not be created in some cases, such as no series declared or extra useless
  417. * axes declared in ec option. In this case we still create a default one for that empty axis.
  418. */
  419. function scaleRawExtentInfoBuildDefault(axisLike, dataExtent) {
  420. var scale = axisLike.scale;
  421. if (process.env.NODE_ENV !== 'production') {
  422. assert(!scale.rawExtentInfo);
  423. }
  424. injectScaleRawExtentInfo(scale, new ScaleRawExtentInfo(scale, axisLike.model, dataExtent, false, false), AXIS_EXTENT_INFO_BUILD_FROM_EMPTY);
  425. }
  426. function injectScaleRawExtentInfo(scale, scaleRawExtentInfo, from) {
  427. // @ts-ignore
  428. scale.rawExtentInfo = scaleRawExtentInfo;
  429. // @ts-ignore
  430. scaleRawExtentInfo.from = from;
  431. }
  432. /**
  433. * See `axisSnippets.ts` for some commonly used handlers.
  434. *
  435. * FIXME:
  436. * `boundaryGap: true` (i.e., `onBand: true` in code) has long been supported on category axis.
  437. * And it is implemented in different code and not merged to this implementation yet.
  438. */
  439. export function registerAxisContainShapeHandler(
  440. // `axisStatKey` is used to quickly omit irrelevant handlers,
  441. // since handlers need to be iterated per axis.
  442. axisStatKey, handler) {
  443. if (process.env.NODE_ENV !== 'production') {
  444. assert(!axisContainShapeHandlerMap.get(axisStatKey));
  445. }
  446. axisContainShapeHandlerMap.set(axisStatKey, handler);
  447. }
  448. var axisContainShapeHandlerMap = createHashMap();
  449. /**
  450. * Prepare axis scale extent before "nice".
  451. * Item of returned array can only be number (including Infinity and NaN).
  452. */
  453. export function adoptScaleRawExtentInfoAndPrepare(scale, model, ecModel, axis, externalDataExtent) {
  454. if (process.env.NODE_ENV !== 'production') {
  455. assert(!externalDataExtent || !scale.rawExtentInfo);
  456. }
  457. if (!scale.rawExtentInfo) {
  458. scaleRawExtentInfoBuildDefault({
  459. scale: scale,
  460. model: model
  461. }, externalDataExtent || initExtentForUnion());
  462. }
  463. var rawExtentResult = scale.rawExtentInfo.makeFinal();
  464. // NOTE: This `scale.setExtent()` is required by:
  465. // - `axisNiceTicks.ts` and `axisAlignTicks.ts`, where the internal `scaleMapper` may be required.
  466. var effectiveMinMax = rawExtentResult.effMM;
  467. scale.setExtent(effectiveMinMax[0], effectiveMinMax[1]);
  468. scale.setBlank(rawExtentResult.isBlank);
  469. if (axis && rawExtentResult.tggAxInv && ecModel && !ecModel.get('legacyMinMaxDontInverseAxis')) {
  470. axis.inverse = !axis.inverse;
  471. }
  472. return rawExtentResult;
  473. }
  474. function determineRequireContainShape(scale, axis, model) {
  475. var onBand = isAxisOnBand(scale, model);
  476. var modelContainShape = model.get('containShape', true);
  477. if (modelContainShape == null && !onBand) {
  478. modelContainShape = true;
  479. }
  480. if (!modelContainShape) {
  481. return false;
  482. }
  483. var requireContainShape = false;
  484. eachKeyOnAxis(axis, function (axisStatKey) {
  485. requireContainShape = !!axisContainShapeHandlerMap.get(axisStatKey) || requireContainShape;
  486. });
  487. return requireContainShape;
  488. }
  489. /**
  490. * This implements ec option `someAxis.containShape`. That is, expand scale extent slightly to
  491. * ensure shapes of specific series are fully contained in the axis extent without overflow.
  492. *
  493. * NOTICE:
  494. * Scale extent (data extent) and axis pixel extent (pixel extent) and are required as inputs.
  495. * - See BAND_WIDTH_USED_SCALE_LINEAR_SPAN.
  496. * - Axis pixel extent has been set outside, though it may be modified later (e.g., via `outerBounds`).
  497. *
  498. * @tutorial [AXIS_CONTAIN_SHAPE_PROCESSING_ORDER]
  499. * This is a trade-off between the following 2 approaches:
  500. * - Steps: (the current implementation)
  501. * 1. Process `dataZoom` based on a full window `noZoomEffMM`.
  502. * 2. Perform "nice" or "align" scale, where `intervalScaleEnsureValidExtent`-ish may be performed to
  503. * expand extent to avoid `extent[0] === extent[1]`.
  504. * 3. Calculate linear supplement of containShape based on the final result.
  505. * Cons:
  506. * - Abrupt changes occur when zooming away from 0% or 100%.
  507. * - Edge shapes are clipped in "dataZoom shadow".
  508. * - Steps: (discarded)
  509. * 1. Calculate linear supplement of containShape based on `noZoomEffMM` and
  510. * `intervalScaleEnsureValidExtent`-ish.
  511. * 2. Process `dataZoom` based on a full window `noZoomEffMM + linearSupplement`.
  512. * 3. Perform "nice"/"align" scale.
  513. * Cons:
  514. * - Input `startValue: 0` (in ec option or action) does not corresponds to `0%`, which is unacceptable.
  515. * - Not easy to perform `intervalScaleEnsureValidExtent`-ish before "nice"/"align" processing.
  516. *
  517. * @see SCALE_EXTENT_CONSTRUCTION for the full processing flow.
  518. */
  519. export function adoptScaleExtentKindMapping(axis, scale, rawExtentResult, ecModel) {
  520. if (!rawExtentResult.ctnShp) {
  521. return;
  522. }
  523. var linearSupplement;
  524. eachKeyOnAxis(axis, function (axisStatKey) {
  525. var handler = axisContainShapeHandlerMap.get(axisStatKey);
  526. if (handler) {
  527. // This feature can be implemented by either expanding axis extent or scale extent. The choice depends
  528. // on whether series shape sizes are defined in pixels or data space. For example, scatter series glyph
  529. // sizes is mainly defined in pixel, while bar series `bandWidth` is mainly determined by given percents
  530. // of data scale. Since currently scatter does not require this feature, we implement it only on the
  531. // data scale.
  532. var singleLinearSupplement = handler(axis, ecModel);
  533. if (singleLinearSupplement) {
  534. linearSupplement = linearSupplement || [0, 0];
  535. unionExtentStartFromNumber(linearSupplement, singleLinearSupplement[0]);
  536. unionExtentEndFromNumber(linearSupplement, singleLinearSupplement[1]);
  537. // Consider the consistency of `onZero` behavior (not varying by series data; otherwise error-prone
  538. // to users), if any `containShape` really performed on this axis, discourage `onZero`.
  539. discourageOnAxisZero(axis);
  540. }
  541. }
  542. });
  543. if (!linearSupplement) {
  544. return;
  545. }
  546. var scaleExtent = scale.getExtent();
  547. if (isOrdinalScale(scale)) {
  548. if (!axis.onBand) {
  549. // - Zooming on `OrdinalScale` auto "snaps" to integer ticks, which causes edge shapes to
  550. // always overlap and be clipped at the boundaries. Therefore we always supplement it with
  551. // half bandWith to avoid that overlapping.
  552. // - `linearSupplement` is typically [-0.5, 0.5] in this case. `linearSupplement` exists only
  553. // if any series call `registerAxisContainShapeHandler`.
  554. // - PENDING: For historical reason, `onBand: true` has another implementation to handle
  555. // this case. Merge them to this?
  556. scale.setExtent2(SCALE_EXTENT_KIND_MAPPING, mathMin(scaleExtent[0], scaleExtent[0] + linearSupplement[0]), mathMax(scaleExtent[1], scaleExtent[1] + linearSupplement[1]));
  557. }
  558. } else {
  559. // For other cases, `SCALE_EXTENT_KIND_MAPPING` is only used on the full window of `dataZoom`,
  560. // where the visual result is more intuitive when zooming: when dataZoom is applied and its ends
  561. // (i.e., `zoomMM`) do not reach 0% or 100%, the axis ends should exactly respect to the dataZoom
  562. // ends, and shapes are clipped if overflowing.
  563. var scaleExtentExpanded = scaleExtent.slice();
  564. if (!rawExtentResult.zoomFixMM[0]) {
  565. scaleExtentExpanded[0] = mathMin(scaleExtentExpanded[0], scale.transformOut(scale.transformIn(scaleExtentExpanded[0], null) + linearSupplement[0], null));
  566. }
  567. if (!rawExtentResult.zoomFixMM[1]) {
  568. scaleExtentExpanded[1] = mathMax(scaleExtentExpanded[1], scale.transformOut(scale.transformIn(scaleExtentExpanded[1], null) + linearSupplement[1], null));
  569. }
  570. if (scaleExtentExpanded[0] < scaleExtent[0] || scaleExtentExpanded[1] > scaleExtent[1]) {
  571. scale.setExtent2(SCALE_EXTENT_KIND_MAPPING, scaleExtentExpanded[0], scaleExtentExpanded[1]);
  572. }
  573. }
  574. // NOTE: since currently `SCALE_EXTENT_KIND_MAPPING` is never required to be displayed, we
  575. // do not need to find a proper precision for that. But if it is required in the future, We
  576. // can use `getAcceptableTickPrecision` to find a proper precision.
  577. }