axisAlignTicks.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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 { getAcceptableTickPrecision, isNullableNumberFinite, mathAbs, mathCeil, mathFloor, mathMax, mathRound, nice, NICE_MODE_MIN, quantity, round } from '../util/number.js';
  41. import IntervalScale from '../scale/Interval.js';
  42. import LogScale from '../scale/Log.js';
  43. import { updateIntervalOrLogScaleForNiceOrAligned } from './axisHelper.js';
  44. import { warn } from '../util/log.js';
  45. import { increaseInterval, isLogScale, getIntervalPrecision, intervalScaleEnsureValidExtent } from '../scale/helper.js';
  46. import { assert } from 'zrender/lib/core/util.js';
  47. import { adoptScaleRawExtentInfoAndPrepare } from './scaleRawExtentInfo.js';
  48. import { hasBreaks } from '../scale/break.js';
  49. /**
  50. * NOTE: See the summary of the process of extent determination in the comment of `scaleMapper.setExtent`.
  51. *
  52. * @see SCALE_EXTENT_CONSTRUCTION for the full processing flow.
  53. */
  54. export function scaleCalcAlign(targetAxis, alignToScale) {
  55. var targetScale = targetAxis.scale;
  56. var targetAxisModel = targetAxis.model;
  57. if (process.env.NODE_ENV !== 'production') {
  58. assert(targetScale && targetAxisModel && (targetScale instanceof IntervalScale || targetScale instanceof LogScale) && (alignToScale instanceof IntervalScale || alignToScale instanceof LogScale));
  59. }
  60. var targetExtentInfo = adoptScaleRawExtentInfoAndPrepare(targetScale, targetAxisModel, targetAxisModel.ecModel, targetAxis, null);
  61. // FIXME:
  62. // (1) Axis inverse is not considered yet.
  63. // (2) `SCALE_EXTENT_KIND_MAPPING` is not considered yet.
  64. var isTargetLogScale = isLogScale(targetScale);
  65. var alignToScaleLinear = isLogScale(alignToScale) ? alignToScale.intervalStub : alignToScale;
  66. var targetIntervalStub = isTargetLogScale ? targetScale.intervalStub : targetScale;
  67. var targetLogScaleBase = targetScale.base;
  68. var alignToTicks = alignToScaleLinear.getTicks();
  69. var alignToExpNiceTicks = alignToScaleLinear.getTicks({
  70. expandToNicedExtent: true
  71. });
  72. var alignToSegCount = alignToTicks.length - 1;
  73. if (process.env.NODE_ENV !== 'production') {
  74. // This is guards for future changes of `Interval#getTicks`.
  75. assert(!hasBreaks(alignToScale) && !hasBreaks(targetScale));
  76. assert(alignToSegCount > 0); // Ticks length >= 2 even on a blank scale.
  77. assert(alignToExpNiceTicks.length === alignToTicks.length);
  78. assert(alignToTicks[0].value <= alignToTicks[alignToSegCount].value);
  79. assert(alignToExpNiceTicks[0].value <= alignToTicks[0].value && alignToTicks[alignToSegCount].value <= alignToExpNiceTicks[alignToSegCount].value);
  80. if (alignToSegCount >= 2) {
  81. assert(alignToExpNiceTicks[1].value === alignToTicks[1].value);
  82. assert(alignToExpNiceTicks[alignToSegCount - 1].value === alignToTicks[alignToSegCount - 1].value);
  83. }
  84. }
  85. // The Current strategy: Find a proper interval and an extent for the target scale to derive ticks
  86. // matching exactly to ticks of `alignTo` scale.
  87. // Adjust min, max based on the extent of alignTo. When min or max is set in alignTo scale
  88. var t0; // diff ratio on min not-nice segment. 0 <= t0 < 1
  89. var t1; // diff ratio on max not-nice segment. 0 <= t1 < 1
  90. var alignToNiceSegCount; // >= 1
  91. // Consider ticks of `alignTo`, only these cases below may occur:
  92. if (alignToSegCount === 1) {
  93. // `alignToTicks` is like:
  94. // |--|
  95. // In this case, we make the corresponding 2 target ticks "nice".
  96. t0 = t1 = 0;
  97. alignToNiceSegCount = 1;
  98. } else if (alignToSegCount === 2) {
  99. // `alignToTicks` is like:
  100. // |-|-----| or
  101. // |-----|-| or
  102. // |-----|-----|
  103. // Notices that nice ticks do not necessarily exist in this case.
  104. // In this case, we choose the larger segment as the "nice segment" and
  105. // the corresponding target ticks are made "nice".
  106. var interval0 = mathAbs(alignToTicks[0].value - alignToTicks[1].value);
  107. var interval1 = mathAbs(alignToTicks[1].value - alignToTicks[2].value);
  108. t0 = t1 = 0;
  109. if (interval0 === interval1) {
  110. alignToNiceSegCount = 2;
  111. } else {
  112. alignToNiceSegCount = 1;
  113. if (interval0 < interval1) {
  114. t0 = interval0 / interval1;
  115. } else {
  116. t1 = interval1 / interval0;
  117. }
  118. }
  119. } else {
  120. // alignToSegCount >= 3
  121. // `alignToTicks` is like:
  122. // |-|-----|-----|-| or
  123. // |-----|-----|-| or
  124. // |-|-----|-----| or ...
  125. // At least one nice segment is present, and not-nice segments are only present on
  126. // the start and/or the end.
  127. // In this case, ticks corresponding to nice segments are made "nice".
  128. var alignToInterval = alignToScaleLinear.getConfig().interval;
  129. t0 = (1 - (alignToTicks[0].value - alignToExpNiceTicks[0].value) / alignToInterval) % 1;
  130. t1 = (1 - (alignToExpNiceTicks[alignToSegCount].value - alignToTicks[alignToSegCount].value) / alignToInterval) % 1;
  131. alignToNiceSegCount = alignToSegCount - (t0 ? 1 : 0) - (t1 ? 1 : 0);
  132. }
  133. if (process.env.NODE_ENV !== 'production') {
  134. assert(alignToNiceSegCount >= 1);
  135. }
  136. // NOTE:
  137. // Consider a case:
  138. // dataZoom controls all Y axes;
  139. // dataZoom end is 90% (maxFixed: true, dataZoomFixMinMax[0]: true);
  140. // but dataZoom start is 0% (minFixed: false, dataZoomFixMinMax[1]: false);
  141. // In this case,
  142. // - `Interval#calcNiceTicks` only uses `targetExtentInfo.max` as the upper bound, but expand the
  143. // lower bound to a "nice" tick and can get an acceptable result.
  144. // - `scaleCalcAlign` has to use both `targetExtentInfo.min/max` as the bounds without any expansion,
  145. // otherwise the lower bound may become negative unexpectedly, especially for all positive series data.
  146. var dataZoomFixMinMax = targetExtentInfo.zoomFixMM;
  147. var hasDataZoomFixMinMax = dataZoomFixMinMax[0] || dataZoomFixMinMax[1];
  148. var targetMinMaxFixed = [targetExtentInfo.fixMM[0] || hasDataZoomFixMinMax, targetExtentInfo.fixMM[1] || hasDataZoomFixMinMax];
  149. // MEMO: When only `xxxAxis.min` or `xxxAxis.max` is fixed,
  150. // - Even a "nice" interval can be calculated, ticks accumulated based on `min`/`max` can be "nice" only if
  151. // `min` or `max` is a "nice" number.
  152. // - Generating a "nice" interval may cause the extent have both positive and negative ticks, which may be
  153. // not preferable for all positive (very common) or all negative series data. But it can be simply resolved
  154. // by specifying `xxxAxis.min: 0`/`xxxAxis.max: 0`, so we do not specially handle this case here.
  155. // Therefore, we prioritize generating "nice" interval over preventing from crossing zero.
  156. // e.g., if series data are all positive and the max data is `11739`,
  157. // If setting `yAxis.max: 'dataMax'`, ticks may be like:
  158. // `11739, 8739, 5739, 2739, -1739` (not "nice" enough)
  159. // If setting `yAxis.max: 'dataMax', yAxis.min: 0`, ticks may be like:
  160. // `11739, 8805, 5870, 2935, 0` (not "nice" enough but may be acceptable)
  161. // If setting `yAxis.max: 12000, yAxis.min: 0`, ticks may be like:
  162. // `12000, 9000, 6000, 3000, 0` ("nice")
  163. var targetOldOutermostExtent = targetScale.getExtent();
  164. var targetOldIntervalExtent = targetIntervalStub.getExtent();
  165. var targetExtent = intervalScaleEnsureValidExtent(targetOldIntervalExtent, targetMinMaxFixed);
  166. var min;
  167. var max;
  168. var interval;
  169. var intervalPrecision;
  170. var maxNice;
  171. var minNice;
  172. function loopIncreaseInterval(cb) {
  173. // Typically this loop runs less than 5 times. But we still
  174. // use a fail-safe for future changes.
  175. var LOOP_MAX = 50;
  176. var loopGuard = 0;
  177. for (; loopGuard < LOOP_MAX; loopGuard++) {
  178. if (cb()) {
  179. break;
  180. }
  181. interval = isTargetLogScale
  182. // TODO: `mathMax(base, 2)` is a guardcode to avoid infinite loop,
  183. // but probably it should be guranteed by `LogScale` itself.
  184. ? interval * mathMax(targetLogScaleBase, 2) : increaseInterval(interval);
  185. intervalPrecision = getIntervalPrecision(interval);
  186. }
  187. if (process.env.NODE_ENV !== 'production') {
  188. if (loopGuard >= LOOP_MAX) {
  189. warn('incorrect impl in `scaleCalcAlign`.');
  190. }
  191. }
  192. }
  193. function updateMinFromMinNice() {
  194. min = round(minNice - interval * t0, intervalPrecision);
  195. }
  196. function updateMaxFromMaxNice() {
  197. max = round(maxNice + interval * t1, intervalPrecision);
  198. }
  199. function updateMinNiceFromMinT0Interval() {
  200. minNice = t0 ? round(min + interval * t0, intervalPrecision) : min;
  201. }
  202. function updateMaxNiceFromMaxT1Interval() {
  203. maxNice = t1 ? round(max - interval * t1, intervalPrecision) : max;
  204. }
  205. // NOTE: The new calculated `min`/`max` must NOT shrink the original extent; otherwise some series
  206. // data may be outside of the extent. They can expand the original extent slightly to align with
  207. // ticks of `alignTo`. In this case, more blank space is added but visually fine.
  208. if (targetMinMaxFixed[0] && targetMinMaxFixed[1]) {
  209. // Both `min` and `max` are specified (via dataZoom or ec option; consider both Cartesian, radar and
  210. // other possible axes). In this case, "nice" ticks can hardly be calculated, but reasonable ticks should
  211. // still be calculated whenever possible, especially `intervalPrecision` should be tuned for better
  212. // appearance and lower cumulative error.
  213. min = targetExtent[0];
  214. max = targetExtent[1];
  215. interval = (max - min) / (alignToNiceSegCount + t0 + t1);
  216. // Typically axis pixel extent is ready here. See `create` in `Grid.ts`.
  217. var axisPxExtent = targetAxis.getExtent();
  218. // NOTICE: this pxSpan may be not accurate yet due to "outerBounds" logic, but acceptable so far.
  219. var pxSpan = mathAbs(axisPxExtent[1] - axisPxExtent[0]);
  220. // We imperically choose `pxDiffAcceptable` as `0.5 / alignToNiceSegCount` for reduce cumulative
  221. // error, otherwise a discernible misalign (> 1px) may occur.
  222. // PENDING: We do not find a acceptable precision for LogScale here.
  223. // Theoretically it can be addressed but introduce more complexity. Is it necessary?
  224. intervalPrecision = getAcceptableTickPrecision([max, min], pxSpan, 0.5 / alignToNiceSegCount);
  225. updateMinNiceFromMinT0Interval();
  226. updateMaxNiceFromMaxT1Interval();
  227. if (isNullableNumberFinite(intervalPrecision)) {
  228. interval = round(interval, intervalPrecision);
  229. }
  230. } else {
  231. // Make a minimal enough `interval`, increase it later.
  232. // It is a similar logic as `IntervalScale#calcNiceTicks` and `LogScale#calcNiceTicks`.
  233. // Axis break is not supported, which is guranteed by the caller of this function.
  234. var targetSpan = targetExtent[1] - targetExtent[0];
  235. interval = isTargetLogScale ? mathMax(quantity(targetSpan), 1) : nice(targetSpan / alignToNiceSegCount, NICE_MODE_MIN);
  236. intervalPrecision = getIntervalPrecision(interval);
  237. if (targetMinMaxFixed[0]) {
  238. min = targetExtent[0];
  239. loopIncreaseInterval(function () {
  240. updateMinNiceFromMinT0Interval();
  241. maxNice = round(minNice + interval * alignToNiceSegCount, intervalPrecision);
  242. updateMaxFromMaxNice();
  243. if (max >= targetExtent[1]) {
  244. return true;
  245. }
  246. });
  247. } else if (targetMinMaxFixed[1]) {
  248. max = targetExtent[1];
  249. loopIncreaseInterval(function () {
  250. updateMaxNiceFromMaxT1Interval();
  251. minNice = round(maxNice - interval * alignToNiceSegCount, intervalPrecision);
  252. updateMinFromMinNice();
  253. if (min <= targetExtent[0]) {
  254. return true;
  255. }
  256. });
  257. } else {
  258. loopIncreaseInterval(function () {
  259. minNice = round(mathCeil(targetExtent[0] / interval) * interval, intervalPrecision);
  260. maxNice = round(mathFloor(targetExtent[1] / interval) * interval, intervalPrecision);
  261. // NOTE:
  262. // - `maxNice - minNice >= -interval` here.
  263. // - While `interval` increases, `currIntervalCount` decreases, minimum `-1`.
  264. var currIntervalCount = mathRound((maxNice - minNice) / interval);
  265. if (currIntervalCount <= alignToNiceSegCount) {
  266. var moreCount = alignToNiceSegCount - currIntervalCount;
  267. // Consider cases that negative tick do not make sense (or vice versa), users can simply
  268. // specify `xxxAxis.min/max: 0` to avoid negative. But we still automatically handle it
  269. // for some common cases whenever possible:
  270. // - When ec option is `xxxAxis.scale: false` (the default), it is usually unexpected if
  271. // negative (or positive) ticks are introduced.
  272. // - In LogScale, series data are usually either all > 1 or all < 1, rather than both,
  273. // that is, logarithm result is typically either all positive or all negative.
  274. var moreCountPair = void 0;
  275. var mayEnhanceZero = targetExtentInfo.incl0 || isTargetLogScale;
  276. // `bounds < 0` or `bounds > 0` may require more complex handling, so we only auto handle
  277. // `bounds === 0`.
  278. if (mayEnhanceZero && targetExtent[0] === 0) {
  279. // 0 has been included in extent and all positive.
  280. moreCountPair = [0, moreCount];
  281. } else if (mayEnhanceZero && targetExtent[1] === 0) {
  282. // 0 has been included in extent and all negative.
  283. moreCountPair = [moreCount, 0];
  284. } else {
  285. // Try to center ticks in axis space whenever possible, which is especially preferable
  286. // in `LogScale`.
  287. var lessHalfCount = mathFloor(moreCount / 2);
  288. moreCountPair = moreCount % 2 === 0 ? [lessHalfCount, lessHalfCount] : min + max < targetExtent[0] + targetExtent[1] ? [lessHalfCount, lessHalfCount + 1] : [lessHalfCount + 1, lessHalfCount];
  289. }
  290. minNice = round(minNice - interval * moreCountPair[0], intervalPrecision);
  291. maxNice = round(maxNice + interval * moreCountPair[1], intervalPrecision);
  292. updateMinFromMinNice();
  293. updateMaxFromMaxNice();
  294. if (min <= targetExtent[0] && max >= targetExtent[1]) {
  295. return true;
  296. }
  297. }
  298. });
  299. }
  300. }
  301. updateIntervalOrLogScaleForNiceOrAligned(targetScale, targetMinMaxFixed, targetOldIntervalExtent, [min, max], targetOldOutermostExtent, {
  302. // NOTE: Even in LogScale, `interval` should not be in log space.
  303. interval: interval,
  304. // Force ticks count, otherwise cumulative error may cause more unexpected ticks to be generated.
  305. // Though the overlapping tick labels may be auto-ignored, but probably unexpected, e.g., the min
  306. // tick label is ignored but the secondary min tick label is shown, which is unexpected when
  307. // `axis.min` is user-specified or dataZoom-specified.
  308. intervalCount: alignToNiceSegCount,
  309. intervalPrecision: intervalPrecision,
  310. niceExtent: [minNice, maxNice]
  311. });
  312. if (process.env.NODE_ENV !== 'production') {
  313. targetScale.freeze();
  314. }
  315. }