OrdinalAxis.js 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777
  1. /* *
  2. *
  3. * (c) 2010-2020 Torstein Honsi
  4. *
  5. * License: www.highcharts.com/license
  6. *
  7. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  8. *
  9. * */
  10. 'use strict';
  11. import Axis from './Axis.js';
  12. import H from '../Globals.js';
  13. import U from '../Utilities.js';
  14. var addEvent = U.addEvent, css = U.css, defined = U.defined, pick = U.pick, timeUnits = U.timeUnits;
  15. import '../Chart/Chart.js';
  16. // Has a dependency on Navigator due to the use of Axis.toFixedRange
  17. import '../Navigator.js';
  18. import '../Series/Series.js';
  19. var Chart = H.Chart, Series = H.Series;
  20. /**
  21. * Extends the axis with ordinal support.
  22. * @private
  23. */
  24. var OrdinalAxis;
  25. (function (OrdinalAxis) {
  26. /* *
  27. *
  28. * Classes
  29. *
  30. * */
  31. /**
  32. * @private
  33. */
  34. var Composition = /** @class */ (function () {
  35. /* *
  36. *
  37. * Constructors
  38. *
  39. * */
  40. /**
  41. * @private
  42. */
  43. function Composition(axis) {
  44. this.index = {};
  45. this.axis = axis;
  46. }
  47. /* *
  48. *
  49. * Functions
  50. *
  51. * */
  52. /**
  53. * Calculate the ordinal positions before tick positions are calculated.
  54. *
  55. * @private
  56. */
  57. Composition.prototype.beforeSetTickPositions = function () {
  58. var axis = this.axis, ordinal = axis.ordinal, len, ordinalPositions = [], uniqueOrdinalPositions, useOrdinal = false, dist, extremes = axis.getExtremes(), min = extremes.min, max = extremes.max, minIndex, maxIndex, slope, hasBreaks = axis.isXAxis && !!axis.options.breaks, isOrdinal = axis.options.ordinal, overscrollPointsRange = Number.MAX_VALUE, ignoreHiddenSeries = axis.chart.options.chart.ignoreHiddenSeries, i, hasBoostedSeries;
  59. // Apply the ordinal logic
  60. if (isOrdinal || hasBreaks) { // #4167 YAxis is never ordinal ?
  61. axis.series.forEach(function (series, i) {
  62. uniqueOrdinalPositions = [];
  63. if ((!ignoreHiddenSeries || series.visible !== false) &&
  64. (series.takeOrdinalPosition !== false || hasBreaks)) {
  65. // concatenate the processed X data into the existing
  66. // positions, or the empty array
  67. ordinalPositions = ordinalPositions.concat(series.processedXData);
  68. len = ordinalPositions.length;
  69. // remove duplicates (#1588)
  70. ordinalPositions.sort(function (a, b) {
  71. // without a custom function it is sorted as strings
  72. return a - b;
  73. });
  74. overscrollPointsRange = Math.min(overscrollPointsRange, pick(
  75. // Check for a single-point series:
  76. series.closestPointRange, overscrollPointsRange));
  77. if (len) {
  78. i = 0;
  79. while (i < len - 1) {
  80. if (ordinalPositions[i] !== ordinalPositions[i + 1]) {
  81. uniqueOrdinalPositions.push(ordinalPositions[i + 1]);
  82. }
  83. i++;
  84. }
  85. // Check first item:
  86. if (uniqueOrdinalPositions[0] !== ordinalPositions[0]) {
  87. uniqueOrdinalPositions.unshift(ordinalPositions[0]);
  88. }
  89. ordinalPositions = uniqueOrdinalPositions;
  90. }
  91. }
  92. if (series.isSeriesBoosting) {
  93. hasBoostedSeries = true;
  94. }
  95. });
  96. if (hasBoostedSeries) {
  97. ordinalPositions.length = 0;
  98. }
  99. // cache the length
  100. len = ordinalPositions.length;
  101. // Check if we really need the overhead of mapping axis data
  102. // against the ordinal positions. If the series consist of
  103. // evenly spaced data any way, we don't need any ordinal logic.
  104. if (len > 2) { // two points have equal distance by default
  105. dist = ordinalPositions[1] - ordinalPositions[0];
  106. i = len - 1;
  107. while (i-- && !useOrdinal) {
  108. if (ordinalPositions[i + 1] - ordinalPositions[i] !== dist) {
  109. useOrdinal = true;
  110. }
  111. }
  112. // When zooming in on a week, prevent axis padding for
  113. // weekends even though the data within the week is evenly
  114. // spaced.
  115. if (!axis.options.keepOrdinalPadding &&
  116. (ordinalPositions[0] - min > dist ||
  117. max - ordinalPositions[ordinalPositions.length - 1] >
  118. dist)) {
  119. useOrdinal = true;
  120. }
  121. }
  122. else if (axis.options.overscroll) {
  123. if (len === 2) {
  124. // Exactly two points, distance for overscroll is fixed:
  125. overscrollPointsRange =
  126. ordinalPositions[1] - ordinalPositions[0];
  127. }
  128. else if (len === 1) {
  129. // We have just one point, closest distance is unknown.
  130. // Assume then it is last point and overscrolled range:
  131. overscrollPointsRange = axis.options.overscroll;
  132. ordinalPositions = [
  133. ordinalPositions[0],
  134. ordinalPositions[0] + overscrollPointsRange
  135. ];
  136. }
  137. else {
  138. // In case of zooming in on overscrolled range, stick to
  139. // the old range:
  140. overscrollPointsRange = ordinal.overscrollPointsRange;
  141. }
  142. }
  143. // Record the slope and offset to compute the linear values from
  144. // the array index. Since the ordinal positions may exceed the
  145. // current range, get the start and end positions within it
  146. // (#719, #665b)
  147. if (useOrdinal) {
  148. if (axis.options.overscroll) {
  149. ordinal.overscrollPointsRange = overscrollPointsRange;
  150. ordinalPositions = ordinalPositions.concat(ordinal.getOverscrollPositions());
  151. }
  152. // Register
  153. ordinal.positions = ordinalPositions;
  154. // This relies on the ordinalPositions being set. Use
  155. // Math.max and Math.min to prevent padding on either sides
  156. // of the data.
  157. minIndex = axis.ordinal2lin(// #5979
  158. Math.max(min, ordinalPositions[0]), true);
  159. maxIndex = Math.max(axis.ordinal2lin(Math.min(max, ordinalPositions[ordinalPositions.length - 1]), true), 1); // #3339
  160. // Set the slope and offset of the values compared to the
  161. // indices in the ordinal positions
  162. ordinal.slope = slope = (max - min) / (maxIndex - minIndex);
  163. ordinal.offset = min - (minIndex * slope);
  164. }
  165. else {
  166. ordinal.overscrollPointsRange = pick(axis.closestPointRange, ordinal.overscrollPointsRange);
  167. ordinal.positions = axis.ordinal.slope = ordinal.offset =
  168. void 0;
  169. }
  170. }
  171. axis.isOrdinal = isOrdinal && useOrdinal; // #3818, #4196, #4926
  172. ordinal.groupIntervalFactor = null; // reset for next run
  173. };
  174. /**
  175. * Get the ordinal positions for the entire data set. This is necessary
  176. * in chart panning because we need to find out what points or data
  177. * groups are available outside the visible range. When a panning
  178. * operation starts, if an index for the given grouping does not exists,
  179. * it is created and cached. This index is deleted on updated data, so
  180. * it will be regenerated the next time a panning operation starts.
  181. *
  182. * @private
  183. */
  184. Composition.prototype.getExtendedPositions = function () {
  185. var ordinal = this, axis = ordinal.axis, axisProto = axis.constructor.prototype, chart = axis.chart, grouping = axis.series[0].currentDataGrouping, ordinalIndex = ordinal.index, key = grouping ?
  186. grouping.count + grouping.unitName :
  187. 'raw', overscroll = axis.options.overscroll, extremes = axis.getExtremes(), fakeAxis, fakeSeries;
  188. // If this is the first time, or the ordinal index is deleted by
  189. // updatedData,
  190. // create it.
  191. if (!ordinalIndex) {
  192. ordinalIndex = ordinal.index = {};
  193. }
  194. if (!ordinalIndex[key]) {
  195. // Create a fake axis object where the extended ordinal
  196. // positions are emulated
  197. fakeAxis = {
  198. series: [],
  199. chart: chart,
  200. getExtremes: function () {
  201. return {
  202. min: extremes.dataMin,
  203. max: extremes.dataMax + overscroll
  204. };
  205. },
  206. options: {
  207. ordinal: true
  208. },
  209. ordinal: {},
  210. ordinal2lin: axisProto.ordinal2lin,
  211. val2lin: axisProto.val2lin // #2590
  212. };
  213. fakeAxis.ordinal.axis = fakeAxis;
  214. // Add the fake series to hold the full data, then apply
  215. // processData to it
  216. axis.series.forEach(function (series) {
  217. fakeSeries = {
  218. xAxis: fakeAxis,
  219. xData: series.xData.slice(),
  220. chart: chart,
  221. destroyGroupedData: H.noop,
  222. getProcessedData: H.Series.prototype.getProcessedData
  223. };
  224. fakeSeries.xData = fakeSeries.xData.concat(ordinal.getOverscrollPositions());
  225. fakeSeries.options = {
  226. dataGrouping: grouping ? {
  227. enabled: true,
  228. forced: true,
  229. // doesn't matter which, use the fastest
  230. approximation: 'open',
  231. units: [[
  232. grouping.unitName,
  233. [grouping.count]
  234. ]]
  235. } : {
  236. enabled: false
  237. }
  238. };
  239. series.processData.apply(fakeSeries);
  240. fakeAxis.series.push(fakeSeries);
  241. });
  242. // Run beforeSetTickPositions to compute the ordinalPositions
  243. axis.ordinal.beforeSetTickPositions.apply({ axis: fakeAxis });
  244. // Cache it
  245. ordinalIndex[key] = fakeAxis.ordinal.positions;
  246. }
  247. return ordinalIndex[key];
  248. };
  249. /**
  250. * Find the factor to estimate how wide the plot area would have been if
  251. * ordinal gaps were included. This value is used to compute an imagined
  252. * plot width in order to establish the data grouping interval.
  253. *
  254. * A real world case is the intraday-candlestick example. Without this
  255. * logic, it would show the correct data grouping when viewing a range
  256. * within each day, but once moving the range to include the gap between
  257. * two days, the interval would include the cut-away night hours and the
  258. * data grouping would be wrong. So the below method tries to compensate
  259. * by identifying the most common point interval, in this case days.
  260. *
  261. * An opposite case is presented in issue #718. We have a long array of
  262. * daily data, then one point is appended one hour after the last point.
  263. * We expect the data grouping not to change.
  264. *
  265. * In the future, if we find cases where this estimation doesn't work
  266. * optimally, we might need to add a second pass to the data grouping
  267. * logic, where we do another run with a greater interval if the number
  268. * of data groups is more than a certain fraction of the desired group
  269. * count.
  270. *
  271. * @private
  272. */
  273. Composition.prototype.getGroupIntervalFactor = function (xMin, xMax, series) {
  274. var ordinal = this, axis = ordinal.axis, i, processedXData = series.processedXData, len = processedXData.length, distances = [], median, groupIntervalFactor = ordinal.groupIntervalFactor;
  275. // Only do this computation for the first series, let the other
  276. // inherit it (#2416)
  277. if (!groupIntervalFactor) {
  278. // Register all the distances in an array
  279. for (i = 0; i < len - 1; i++) {
  280. distances[i] =
  281. processedXData[i + 1] - processedXData[i];
  282. }
  283. // Sort them and find the median
  284. distances.sort(function (a, b) {
  285. return a - b;
  286. });
  287. median = distances[Math.floor(len / 2)];
  288. // Compensate for series that don't extend through the entire
  289. // axis extent. #1675.
  290. xMin = Math.max(xMin, processedXData[0]);
  291. xMax = Math.min(xMax, processedXData[len - 1]);
  292. ordinal.groupIntervalFactor = groupIntervalFactor =
  293. (len * median) / (xMax - xMin);
  294. }
  295. // Return the factor needed for data grouping
  296. return groupIntervalFactor;
  297. };
  298. /**
  299. * Get ticks for an ordinal axis within a range where points don't
  300. * exist. It is required when overscroll is enabled. We can't base on
  301. * points, because we may not have any, so we use approximated
  302. * pointRange and generate these ticks between Axis.dataMax,
  303. * Axis.dataMax + Axis.overscroll evenly spaced. Used in panning and
  304. * navigator scrolling.
  305. *
  306. * @private
  307. */
  308. Composition.prototype.getOverscrollPositions = function () {
  309. var ordinal = this, axis = ordinal.axis, extraRange = axis.options.overscroll, distance = ordinal.overscrollPointsRange, positions = [], max = axis.dataMax;
  310. if (defined(distance)) {
  311. // Max + pointRange because we need to scroll to the last
  312. positions.push(max);
  313. while (max <= axis.dataMax + extraRange) {
  314. max += distance;
  315. positions.push(max);
  316. }
  317. }
  318. return positions;
  319. };
  320. /**
  321. * Make the tick intervals closer because the ordinal gaps make the
  322. * ticks spread out or cluster.
  323. *
  324. * @private
  325. */
  326. Composition.prototype.postProcessTickInterval = function (tickInterval) {
  327. // Problem: https://jsfiddle.net/highcharts/FQm4E/1/
  328. // This is a case where this algorithm doesn't work optimally. In
  329. // this case, the tick labels are spread out per week, but all the
  330. // gaps reside within weeks. So we have a situation where the labels
  331. // are courser than the ordinal gaps, and thus the tick interval
  332. // should not be altered.
  333. var ordinal = this, axis = ordinal.axis, ordinalSlope = ordinal.slope, ret;
  334. if (ordinalSlope) {
  335. if (!axis.options.breaks) {
  336. ret = tickInterval / (ordinalSlope / axis.closestPointRange);
  337. }
  338. else {
  339. ret = axis.closestPointRange || tickInterval; // #7275
  340. }
  341. }
  342. else {
  343. ret = tickInterval;
  344. }
  345. return ret;
  346. };
  347. return Composition;
  348. }());
  349. OrdinalAxis.Composition = Composition;
  350. /* *
  351. *
  352. * Functions
  353. *
  354. * */
  355. /**
  356. * Extends the axis with ordinal support.
  357. *
  358. * @private
  359. *
  360. * @param AxisClass
  361. * Axis class to extend.
  362. *
  363. * @param ChartClass
  364. * Chart class to use.
  365. *
  366. * @param SeriesClass
  367. * Series class to use.
  368. */
  369. function compose(AxisClass, ChartClass, SeriesClass) {
  370. AxisClass.keepProps.push('ordinal');
  371. var axisProto = AxisClass.prototype;
  372. /**
  373. * In an ordinal axis, there might be areas with dense consentrations of
  374. * points, then large gaps between some. Creating equally distributed
  375. * ticks over this entire range may lead to a huge number of ticks that
  376. * will later be removed. So instead, break the positions up in
  377. * segments, find the tick positions for each segment then concatenize
  378. * them. This method is used from both data grouping logic and X axis
  379. * tick position logic.
  380. *
  381. * @private
  382. */
  383. AxisClass.prototype.getTimeTicks = function (normalizedInterval, min, max, startOfWeek, positions, closestDistance, findHigherRanks) {
  384. if (positions === void 0) { positions = []; }
  385. if (closestDistance === void 0) { closestDistance = 0; }
  386. var start = 0, end, segmentPositions, higherRanks = {}, hasCrossedHigherRank, info, posLength, outsideMax, groupPositions = [], lastGroupPosition = -Number.MAX_VALUE, tickPixelIntervalOption = this.options.tickPixelInterval, time = this.chart.time,
  387. // Record all the start positions of a segment, to use when
  388. // deciding what's a gap in the data.
  389. segmentStarts = [];
  390. // The positions are not always defined, for example for ordinal
  391. // positions when data has regular interval (#1557, #2090)
  392. if ((!this.options.ordinal && !this.options.breaks) ||
  393. !positions ||
  394. positions.length < 3 ||
  395. typeof min === 'undefined') {
  396. return time.getTimeTicks.apply(time, arguments);
  397. }
  398. // Analyze the positions array to split it into segments on gaps
  399. // larger than 5 times the closest distance. The closest distance is
  400. // already found at this point, so we reuse that instead of
  401. // computing it again.
  402. posLength = positions.length;
  403. for (end = 0; end < posLength; end++) {
  404. outsideMax = end && positions[end - 1] > max;
  405. if (positions[end] < min) { // Set the last position before min
  406. start = end;
  407. }
  408. if (end === posLength - 1 ||
  409. positions[end + 1] - positions[end] > closestDistance * 5 ||
  410. outsideMax) {
  411. // For each segment, calculate the tick positions from the
  412. // getTimeTicks utility function. The interval will be the
  413. // same regardless of how long the segment is.
  414. if (positions[end] > lastGroupPosition) { // #1475
  415. segmentPositions = time.getTimeTicks(normalizedInterval, positions[start], positions[end], startOfWeek);
  416. // Prevent duplicate groups, for example for multiple
  417. // segments within one larger time frame (#1475)
  418. while (segmentPositions.length &&
  419. segmentPositions[0] <= lastGroupPosition) {
  420. segmentPositions.shift();
  421. }
  422. if (segmentPositions.length) {
  423. lastGroupPosition =
  424. segmentPositions[segmentPositions.length - 1];
  425. }
  426. segmentStarts.push(groupPositions.length);
  427. groupPositions = groupPositions.concat(segmentPositions);
  428. }
  429. // Set start of next segment
  430. start = end + 1;
  431. }
  432. if (outsideMax) {
  433. break;
  434. }
  435. }
  436. // Get the grouping info from the last of the segments. The info is
  437. // the same for all segments.
  438. info = segmentPositions.info;
  439. // Optionally identify ticks with higher rank, for example when the
  440. // ticks have crossed midnight.
  441. if (findHigherRanks && info.unitRange <= timeUnits.hour) {
  442. end = groupPositions.length - 1;
  443. // Compare points two by two
  444. for (start = 1; start < end; start++) {
  445. if (time.dateFormat('%d', groupPositions[start]) !==
  446. time.dateFormat('%d', groupPositions[start - 1])) {
  447. higherRanks[groupPositions[start]] = 'day';
  448. hasCrossedHigherRank = true;
  449. }
  450. }
  451. // If the complete array has crossed midnight, we want to mark
  452. // the first positions also as higher rank
  453. if (hasCrossedHigherRank) {
  454. higherRanks[groupPositions[0]] = 'day';
  455. }
  456. info.higherRanks = higherRanks;
  457. }
  458. // Save the info
  459. info.segmentStarts = segmentStarts;
  460. groupPositions.info = info;
  461. // Don't show ticks within a gap in the ordinal axis, where the
  462. // space between two points is greater than a portion of the tick
  463. // pixel interval
  464. if (findHigherRanks && defined(tickPixelIntervalOption)) {
  465. var length = groupPositions.length, i = length, itemToRemove, translated, translatedArr = [], lastTranslated, medianDistance, distance, distances = [];
  466. // Find median pixel distance in order to keep a reasonably even
  467. // distance between ticks (#748)
  468. while (i--) {
  469. translated = this.translate(groupPositions[i]);
  470. if (lastTranslated) {
  471. distances[i] = lastTranslated - translated;
  472. }
  473. translatedArr[i] = lastTranslated = translated;
  474. }
  475. distances.sort();
  476. medianDistance = distances[Math.floor(distances.length / 2)];
  477. if (medianDistance < tickPixelIntervalOption * 0.6) {
  478. medianDistance = null;
  479. }
  480. // Now loop over again and remove ticks where needed
  481. i = groupPositions[length - 1] > max ? length - 1 : length; // #817
  482. lastTranslated = void 0;
  483. while (i--) {
  484. translated = translatedArr[i];
  485. distance = Math.abs(lastTranslated - translated);
  486. // #4175 - when axis is reversed, the distance, is negative
  487. // but tickPixelIntervalOption positive, so we need to
  488. // compare the same values
  489. // Remove ticks that are closer than 0.6 times the pixel
  490. // interval from the one to the right, but not if it is
  491. // close to the median distance (#748).
  492. if (lastTranslated &&
  493. distance < tickPixelIntervalOption * 0.8 &&
  494. (medianDistance === null || distance < medianDistance * 0.8)) {
  495. // Is this a higher ranked position with a normal
  496. // position to the right?
  497. if (higherRanks[groupPositions[i]] &&
  498. !higherRanks[groupPositions[i + 1]]) {
  499. // Yes: remove the lower ranked neighbour to the
  500. // right
  501. itemToRemove = i + 1;
  502. lastTranslated = translated; // #709
  503. }
  504. else {
  505. // No: remove this one
  506. itemToRemove = i;
  507. }
  508. groupPositions.splice(itemToRemove, 1);
  509. }
  510. else {
  511. lastTranslated = translated;
  512. }
  513. }
  514. }
  515. return groupPositions;
  516. };
  517. /**
  518. * Translate from linear (internal) to axis value.
  519. *
  520. * @private
  521. * @function Highcharts.Axis#lin2val
  522. *
  523. * @param {number} val
  524. * The linear abstracted value.
  525. *
  526. * @param {boolean} [fromIndex]
  527. * Translate from an index in the ordinal positions rather than a
  528. * value.
  529. *
  530. * @return {number}
  531. */
  532. axisProto.lin2val = function (val, fromIndex) {
  533. var axis = this, ordinal = axis.ordinal, ordinalPositions = ordinal.positions, ret;
  534. // the visible range contains only equally spaced values
  535. if (!ordinalPositions) {
  536. ret = val;
  537. }
  538. else {
  539. var ordinalSlope = ordinal.slope, ordinalOffset = ordinal.offset, i = ordinalPositions.length - 1, linearEquivalentLeft, linearEquivalentRight, distance;
  540. // Handle the case where we translate from the index directly,
  541. // used only when panning an ordinal axis
  542. if (fromIndex) {
  543. if (val < 0) { // out of range, in effect panning to the left
  544. val = ordinalPositions[0];
  545. }
  546. else if (val > i) { // out of range, panning to the right
  547. val = ordinalPositions[i];
  548. }
  549. else { // split it up
  550. i = Math.floor(val);
  551. distance = val - i; // the decimal
  552. }
  553. // Loop down along the ordinal positions. When the linear
  554. // equivalent of i matches an ordinal position, interpolate
  555. // between the left and right values.
  556. }
  557. else {
  558. while (i--) {
  559. linearEquivalentLeft =
  560. (ordinalSlope * i) + ordinalOffset;
  561. if (val >= linearEquivalentLeft) {
  562. linearEquivalentRight =
  563. (ordinalSlope *
  564. (i + 1)) +
  565. ordinalOffset;
  566. // something between 0 and 1
  567. distance = (val - linearEquivalentLeft) /
  568. (linearEquivalentRight - linearEquivalentLeft);
  569. break;
  570. }
  571. }
  572. }
  573. // If the index is within the range of the ordinal positions,
  574. // return the associated or interpolated value. If not, just
  575. // return the value.
  576. return (typeof distance !== 'undefined' &&
  577. typeof ordinalPositions[i] !== 'undefined' ?
  578. ordinalPositions[i] + (distance ?
  579. distance *
  580. (ordinalPositions[i + 1] - ordinalPositions[i]) :
  581. 0) :
  582. val);
  583. }
  584. return ret;
  585. };
  586. /**
  587. * Translate from a linear axis value to the corresponding ordinal axis
  588. * position. If there are no gaps in the ordinal axis this will be the
  589. * same. The translated value is the value that the point would have if
  590. * the axis were linear, using the same min and max.
  591. *
  592. * @private
  593. * @function Highcharts.Axis#val2lin
  594. *
  595. * @param {number} val
  596. * The axis value.
  597. *
  598. * @param {boolean} [toIndex]
  599. * Whether to return the index in the ordinalPositions or the new value.
  600. *
  601. * @return {number}
  602. */
  603. axisProto.val2lin = function (val, toIndex) {
  604. var axis = this, ordinal = axis.ordinal, ordinalPositions = ordinal.positions, ret;
  605. if (!ordinalPositions) {
  606. ret = val;
  607. }
  608. else {
  609. var ordinalLength = ordinalPositions.length, i, distance, ordinalIndex;
  610. // first look for an exact match in the ordinalpositions array
  611. i = ordinalLength;
  612. while (i--) {
  613. if (ordinalPositions[i] === val) {
  614. ordinalIndex = i;
  615. break;
  616. }
  617. }
  618. // if that failed, find the intermediate position between the
  619. // two nearest values
  620. i = ordinalLength - 1;
  621. while (i--) {
  622. if (val > ordinalPositions[i] || i === 0) { // interpolate
  623. // something between 0 and 1
  624. distance = (val - ordinalPositions[i]) /
  625. (ordinalPositions[i + 1] - ordinalPositions[i]);
  626. ordinalIndex = i + distance;
  627. break;
  628. }
  629. }
  630. ret = toIndex ?
  631. ordinalIndex :
  632. ordinal.slope *
  633. (ordinalIndex || 0) +
  634. ordinal.offset;
  635. }
  636. return ret;
  637. };
  638. // Record this to prevent overwriting by broken-axis module (#5979)
  639. axisProto.ordinal2lin = axisProto.val2lin;
  640. /* eslint-disable no-invalid-this */
  641. addEvent(AxisClass, 'afterInit', function () {
  642. var axis = this;
  643. if (!axis.ordinal) {
  644. axis.ordinal = new OrdinalAxis.Composition(axis);
  645. }
  646. });
  647. addEvent(AxisClass, 'foundExtremes', function () {
  648. var axis = this;
  649. if (axis.isXAxis &&
  650. defined(axis.options.overscroll) &&
  651. axis.max === axis.dataMax &&
  652. (
  653. // Panning is an execption. We don't want to apply
  654. // overscroll when panning over the dataMax
  655. !axis.chart.mouseIsDown ||
  656. axis.isInternal) && (
  657. // Scrollbar buttons are the other execption:
  658. !axis.eventArgs ||
  659. axis.eventArgs && axis.eventArgs.trigger !== 'navigator')) {
  660. axis.max += axis.options.overscroll;
  661. // Live data and buttons require translation for the min:
  662. if (!axis.isInternal && defined(axis.userMin)) {
  663. axis.min += axis.options.overscroll;
  664. }
  665. }
  666. });
  667. // For ordinal axis, that loads data async, redraw axis after data is
  668. // loaded. If we don't do that, axis will have the same extremes as
  669. // previously, but ordinal positions won't be calculated. See #10290
  670. addEvent(AxisClass, 'afterSetScale', function () {
  671. var axis = this;
  672. if (axis.horiz && !axis.isDirty) {
  673. axis.isDirty = axis.isOrdinal &&
  674. axis.chart.navigator &&
  675. !axis.chart.navigator.adaptToUpdatedData;
  676. }
  677. });
  678. addEvent(AxisClass, 'initialAxisTranslation', function () {
  679. var axis = this;
  680. if (axis.ordinal) {
  681. axis.ordinal.beforeSetTickPositions();
  682. axis.tickInterval = axis.ordinal.postProcessTickInterval(axis.tickInterval);
  683. }
  684. });
  685. // Extending the Chart.pan method for ordinal axes
  686. addEvent(ChartClass, 'pan', function (e) {
  687. var chart = this, xAxis = chart.xAxis[0], overscroll = xAxis.options.overscroll, chartX = e.originalEvent.chartX, panning = chart.options.chart &&
  688. chart.options.chart.panning, runBase = false;
  689. if (panning &&
  690. panning.type !== 'y' &&
  691. xAxis.options.ordinal &&
  692. xAxis.series.length) {
  693. var mouseDownX = chart.mouseDownX, extremes = xAxis.getExtremes(), dataMax = extremes.dataMax, min = extremes.min, max = extremes.max, trimmedRange, hoverPoints = chart.hoverPoints, closestPointRange = (xAxis.closestPointRange ||
  694. (xAxis.ordinal && xAxis.ordinal.overscrollPointsRange)), pointPixelWidth = (xAxis.translationSlope *
  695. (xAxis.ordinal.slope || closestPointRange)),
  696. // how many ordinal units did we move?
  697. movedUnits = (mouseDownX - chartX) / pointPixelWidth,
  698. // get index of all the chart's points
  699. extendedAxis = { ordinal: { positions: xAxis.ordinal.getExtendedPositions() } }, ordinalPositions, searchAxisLeft, lin2val = xAxis.lin2val, val2lin = xAxis.val2lin, searchAxisRight;
  700. // we have an ordinal axis, but the data is equally spaced
  701. if (!extendedAxis.ordinal.positions) {
  702. runBase = true;
  703. }
  704. else if (Math.abs(movedUnits) > 1) {
  705. // Remove active points for shared tooltip
  706. if (hoverPoints) {
  707. hoverPoints.forEach(function (point) {
  708. point.setState();
  709. });
  710. }
  711. if (movedUnits < 0) {
  712. searchAxisLeft = extendedAxis;
  713. searchAxisRight = xAxis.ordinal.positions ? xAxis : extendedAxis;
  714. }
  715. else {
  716. searchAxisLeft = xAxis.ordinal.positions ? xAxis : extendedAxis;
  717. searchAxisRight = extendedAxis;
  718. }
  719. // In grouped data series, the last ordinal position
  720. // represents the grouped data, which is to the left of the
  721. // real data max. If we don't compensate for this, we will
  722. // be allowed to pan grouped data series passed the right of
  723. // the plot area.
  724. ordinalPositions = searchAxisRight.ordinal.positions;
  725. if (dataMax >
  726. ordinalPositions[ordinalPositions.length - 1]) {
  727. ordinalPositions.push(dataMax);
  728. }
  729. // Get the new min and max values by getting the ordinal
  730. // index for the current extreme, then add the moved units
  731. // and translate back to values. This happens on the
  732. // extended ordinal positions if the new position is out of
  733. // range, else it happens on the current x axis which is
  734. // smaller and faster.
  735. chart.fixedRange = max - min;
  736. trimmedRange = xAxis.navigatorAxis.toFixedRange(null, null, lin2val.apply(searchAxisLeft, [
  737. val2lin.apply(searchAxisLeft, [min, true]) + movedUnits,
  738. true // translate from index
  739. ]), lin2val.apply(searchAxisRight, [
  740. val2lin.apply(searchAxisRight, [max, true]) + movedUnits,
  741. true // translate from index
  742. ]));
  743. // Apply it if it is within the available data range
  744. if (trimmedRange.min >= Math.min(extremes.dataMin, min) &&
  745. trimmedRange.max <= Math.max(dataMax, max) + overscroll) {
  746. xAxis.setExtremes(trimmedRange.min, trimmedRange.max, true, false, { trigger: 'pan' });
  747. }
  748. chart.mouseDownX = chartX; // set new reference for next run
  749. css(chart.container, { cursor: 'move' });
  750. }
  751. }
  752. else {
  753. runBase = true;
  754. }
  755. // revert to the linear chart.pan version
  756. if (runBase || (panning && /y/.test(panning.type))) {
  757. if (overscroll) {
  758. xAxis.max = xAxis.dataMax + overscroll;
  759. }
  760. }
  761. else {
  762. e.preventDefault();
  763. }
  764. });
  765. addEvent(SeriesClass, 'updatedData', function () {
  766. var xAxis = this.xAxis;
  767. // Destroy the extended ordinal index on updated data
  768. if (xAxis && xAxis.options.ordinal) {
  769. delete xAxis.ordinal.index;
  770. }
  771. });
  772. /* eslint-enable no-invalid-this */
  773. }
  774. OrdinalAxis.compose = compose;
  775. })(OrdinalAxis || (OrdinalAxis = {}));
  776. OrdinalAxis.compose(Axis, Chart, Series); // @todo move to StockChart, remove from master
  777. export default OrdinalAxis;