OrdinalAxis.js 33 KB

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