GridAxis.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959
  1. /* *
  2. * (c) 2016 Highsoft AS
  3. * Authors: Lars A. V. Cabrera
  4. *
  5. * License: www.highcharts.com/license
  6. */
  7. 'use strict';
  8. import H from '../parts/Globals.js';
  9. var addEvent = H.addEvent,
  10. argsToArray = function (args) {
  11. return Array.prototype.slice.call(args, 1);
  12. },
  13. dateFormat = H.dateFormat,
  14. defined = H.defined,
  15. isArray = H.isArray,
  16. isNumber = H.isNumber,
  17. isObject = function (x) {
  18. // Always use strict mode
  19. return H.isObject(x, true);
  20. },
  21. merge = H.merge,
  22. pick = H.pick,
  23. wrap = H.wrap,
  24. Axis = H.Axis,
  25. Tick = H.Tick;
  26. /**
  27. * Set grid options for the axis labels. Requires Highcharts Gantt.
  28. *
  29. * @since 6.2.0
  30. * @product gantt
  31. * @apioption xAxis.grid
  32. */
  33. /**
  34. * Enable grid on the axis labels. Defaults to true for Gantt charts.
  35. *
  36. * @type {boolean}
  37. * @default true
  38. * @since 6.2.0
  39. * @product gantt
  40. * @apioption xAxis.grid.enabled
  41. */
  42. /**
  43. * Set specific options for each column (or row for horizontal axes) in the
  44. * grid. Each extra column/row is its own axis, and the axis options can be set
  45. * here.
  46. *
  47. * @sample gantt/demo/left-axis-table
  48. * Left axis as a table
  49. *
  50. * @type {Array<Highcharts.XAxisOptions>}
  51. * @apioption xAxis.grid.columns
  52. */
  53. /**
  54. * Set border color for the label grid lines.
  55. *
  56. * @type {Highcharts.ColorString}
  57. * @apioption xAxis.grid.borderColor
  58. */
  59. /**
  60. * Set border width of the label grid lines.
  61. *
  62. * @type {number}
  63. * @default 1
  64. * @apioption xAxis.grid.borderWidth
  65. */
  66. /**
  67. * Set cell height for grid axis labels. By default this is calculated from font
  68. * size.
  69. *
  70. * @type {number}
  71. * @apioption xAxis.grid.cellHeight
  72. */
  73. // Enum for which side the axis is on.
  74. // Maps to axis.side
  75. var axisSide = {
  76. top: 0,
  77. right: 1,
  78. bottom: 2,
  79. left: 3,
  80. 0: 'top',
  81. 1: 'right',
  82. 2: 'bottom',
  83. 3: 'left'
  84. };
  85. /**
  86. * Checks if an axis is a navigator axis.
  87. *
  88. * @private
  89. * @function Highcharts.Axis#isNavigatorAxis
  90. *
  91. * @return {boolean}
  92. * true if axis is found in axis.chart.navigator
  93. */
  94. Axis.prototype.isNavigatorAxis = function () {
  95. return /highcharts-navigator-[xy]axis/.test(this.options.className);
  96. };
  97. /**
  98. * Checks if an axis is the outer axis in its dimension. Since
  99. * axes are placed outwards in order, the axis with the highest
  100. * index is the outermost axis.
  101. *
  102. * Example: If there are multiple x-axes at the top of the chart,
  103. * this function returns true if the axis supplied is the last
  104. * of the x-axes.
  105. *
  106. * @private
  107. * @function Highcharts.Axis#isOuterAxis
  108. *
  109. * @return {boolean}
  110. * true if the axis is the outermost axis in its dimension; false if not
  111. */
  112. Axis.prototype.isOuterAxis = function () {
  113. var axis = this,
  114. chart = axis.chart,
  115. thisIndex = -1,
  116. isOuter = true;
  117. chart.axes.forEach(function (otherAxis, index) {
  118. if (otherAxis.side === axis.side && !otherAxis.isNavigatorAxis()) {
  119. if (otherAxis === axis) {
  120. // Get the index of the axis in question
  121. thisIndex = index;
  122. // Check thisIndex >= 0 in case thisIndex has
  123. // not been found yet
  124. } else if (thisIndex >= 0 && index > thisIndex) {
  125. // There was an axis on the same side with a
  126. // higher index.
  127. isOuter = false;
  128. }
  129. }
  130. });
  131. // There were either no other axes on the same side,
  132. // or the other axes were not farther from the chart
  133. return isOuter;
  134. };
  135. /**
  136. * Get the largest label width and height.
  137. *
  138. * @private
  139. * @function Highcharts.Axis#getMaxLabelDimensions
  140. *
  141. * @param {Highcharts.Dictionary<Highcharts.Tick>} ticks
  142. * All the ticks on one axis.
  143. *
  144. * @param {Array<number|string>} tickPositions
  145. * All the tick positions on one axis.
  146. *
  147. * @return {object}
  148. * object containing the properties height and width.
  149. */
  150. Axis.prototype.getMaxLabelDimensions = function (ticks, tickPositions) {
  151. var dimensions = {
  152. width: 0,
  153. height: 0
  154. };
  155. tickPositions.forEach(function (pos) {
  156. var tick = ticks[pos],
  157. tickHeight = 0,
  158. tickWidth = 0,
  159. label;
  160. if (isObject(tick)) {
  161. label = isObject(tick.label) ? tick.label : {};
  162. // Find width and height of tick
  163. tickHeight = label.getBBox ? label.getBBox().height : 0;
  164. tickWidth = isNumber(label.textPxLength) ? label.textPxLength : 0;
  165. // Update the result if width and/or height are larger
  166. dimensions.height = Math.max(tickHeight, dimensions.height);
  167. dimensions.width = Math.max(tickWidth, dimensions.width);
  168. }
  169. });
  170. return dimensions;
  171. };
  172. // Add custom date formats
  173. H.dateFormats = {
  174. // Week number
  175. W: function (timestamp) {
  176. var d = new Date(timestamp),
  177. yearStart,
  178. weekNo;
  179. d.setHours(0, 0, 0, 0);
  180. d.setDate(d.getDate() - (d.getDay() || 7));
  181. yearStart = new Date(d.getFullYear(), 0, 1);
  182. weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
  183. return weekNo;
  184. },
  185. // First letter of the day of the week, e.g. 'M' for 'Monday'.
  186. E: function (timestamp) {
  187. return dateFormat('%a', timestamp, true).charAt(0);
  188. }
  189. };
  190. addEvent(
  191. Tick,
  192. 'afterGetLabelPosition',
  193. /**
  194. * Center tick labels in cells.
  195. *
  196. * @private
  197. */
  198. function (e) {
  199. var tick = this,
  200. label = tick.label,
  201. axis = tick.axis,
  202. reversed = axis.reversed,
  203. chart = axis.chart,
  204. options = axis.options,
  205. gridOptions = (
  206. (options && isObject(options.grid)) ? options.grid : {}
  207. ),
  208. labelOpts = axis.options.labels,
  209. align = labelOpts.align,
  210. // verticalAlign is currently not supported for axis.labels.
  211. verticalAlign = 'middle', // labelOpts.verticalAlign,
  212. side = axisSide[axis.side],
  213. tickmarkOffset = e.tickmarkOffset,
  214. tickPositions = axis.tickPositions,
  215. tickPos = tick.pos - tickmarkOffset,
  216. nextTickPos = (
  217. isNumber(tickPositions[e.index + 1]) ?
  218. tickPositions[e.index + 1] - tickmarkOffset :
  219. axis.max + tickmarkOffset
  220. ),
  221. tickSize = axis.tickSize('tick', true),
  222. tickWidth = isArray(tickSize) ? tickSize[0] : 0,
  223. crispCorr = tickSize && tickSize[1] / 2,
  224. labelHeight,
  225. lblMetrics,
  226. lines,
  227. bottom,
  228. top,
  229. left,
  230. right;
  231. // Only center tick labels in grid axes
  232. if (gridOptions.enabled === true) {
  233. // Calculate top and bottom positions of the cell.
  234. if (side === 'top') {
  235. bottom = axis.top + axis.offset;
  236. top = bottom - tickWidth;
  237. } else if (side === 'bottom') {
  238. top = chart.chartHeight - axis.bottom + axis.offset;
  239. bottom = top + tickWidth;
  240. } else {
  241. bottom = axis.top + axis.len - axis.translate(
  242. reversed ? nextTickPos : tickPos
  243. );
  244. top = axis.top + axis.len - axis.translate(
  245. reversed ? tickPos : nextTickPos
  246. );
  247. }
  248. // Calculate left and right positions of the cell.
  249. if (side === 'right') {
  250. left = chart.chartWidth - axis.right + axis.offset;
  251. right = left + tickWidth;
  252. } else if (side === 'left') {
  253. right = axis.left + axis.offset;
  254. left = right - tickWidth;
  255. } else {
  256. left = Math.round(axis.left + axis.translate(
  257. reversed ? nextTickPos : tickPos
  258. )) - crispCorr;
  259. right = Math.round(axis.left + axis.translate(
  260. reversed ? tickPos : nextTickPos
  261. )) - crispCorr;
  262. }
  263. tick.slotWidth = right - left;
  264. // Calculate the positioning of the label based on alignment.
  265. e.pos.x = (
  266. align === 'left' ?
  267. left :
  268. align === 'right' ?
  269. right :
  270. left + ((right - left) / 2) // default to center
  271. );
  272. e.pos.y = (
  273. verticalAlign === 'top' ?
  274. top :
  275. verticalAlign === 'bottom' ?
  276. bottom :
  277. top + ((bottom - top) / 2) // default to middle
  278. );
  279. lblMetrics = chart.renderer.fontMetrics(
  280. labelOpts.style.fontSize,
  281. label.element
  282. );
  283. labelHeight = label.getBBox().height;
  284. // Adjustment to y position to align the label correctly.
  285. // Would be better to have a setter or similar for this.
  286. if (!labelOpts.useHTML) {
  287. lines = Math.round(labelHeight / lblMetrics.h);
  288. e.pos.y += (
  289. // Center the label
  290. // TODO: why does this actually center the label?
  291. ((lblMetrics.b - (lblMetrics.h - lblMetrics.f)) / 2) +
  292. // Adjust for height of additional lines.
  293. -(((lines - 1) * lblMetrics.h) / 2)
  294. );
  295. } else {
  296. e.pos.y += (
  297. // Readjust yCorr in htmlUpdateTransform
  298. lblMetrics.b +
  299. // Adjust for height of html label
  300. -(labelHeight / 2)
  301. );
  302. }
  303. e.pos.x += (axis.horiz && labelOpts.x || 0);
  304. }
  305. }
  306. );
  307. // Draw vertical axis ticks extra long to create cell floors and roofs.
  308. // Overrides the tickLength for vertical axes.
  309. addEvent(Axis, 'afterTickSize', function (e) {
  310. var axis = this,
  311. dimensions = axis.maxLabelDimensions,
  312. options = axis.options,
  313. gridOptions = (options && isObject(options.grid)) ? options.grid : {},
  314. labelPadding,
  315. distance;
  316. if (gridOptions.enabled === true) {
  317. labelPadding = (Math.abs(axis.defaultLeftAxisOptions.labels.x) * 2);
  318. distance = labelPadding +
  319. (axis.horiz ? dimensions.height : dimensions.width);
  320. if (isArray(e.tickSize)) {
  321. e.tickSize[0] = distance;
  322. } else {
  323. e.tickSize = [distance];
  324. }
  325. }
  326. });
  327. addEvent(Axis, 'afterGetTitlePosition', function (e) {
  328. var axis = this,
  329. options = axis.options,
  330. gridOptions = (options && isObject(options.grid)) ? options.grid : {};
  331. if (gridOptions.enabled === true) {
  332. // compute anchor points for each of the title align options
  333. var title = axis.axisTitle,
  334. titleWidth = title && title.getBBox().width,
  335. horiz = axis.horiz,
  336. axisLeft = axis.left,
  337. axisTop = axis.top,
  338. axisWidth = axis.width,
  339. axisHeight = axis.height,
  340. axisTitleOptions = options.title,
  341. opposite = axis.opposite,
  342. offset = axis.offset,
  343. tickSize = axis.tickSize() || [0],
  344. xOption = axisTitleOptions.x || 0,
  345. yOption = axisTitleOptions.y || 0,
  346. titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10),
  347. titleFontSize = axis.chart.renderer.fontMetrics(
  348. axisTitleOptions.style && axisTitleOptions.style.fontSize,
  349. title
  350. ).f,
  351. // TODO account for alignment
  352. // the position in the perpendicular direction of the axis
  353. offAxis = (horiz ? axisTop + axisHeight : axisLeft) +
  354. (horiz ? 1 : -1) * // horizontal axis reverses the margin
  355. (opposite ? -1 : 1) * // so does opposite axes
  356. (tickSize[0] / 2) +
  357. (axis.side === axisSide.bottom ? titleFontSize : 0);
  358. e.titlePosition.x = horiz ?
  359. axisLeft - titleWidth / 2 - titleMargin + xOption :
  360. offAxis + (opposite ? axisWidth : 0) + offset + xOption;
  361. e.titlePosition.y = horiz ?
  362. (
  363. offAxis -
  364. (opposite ? axisHeight : 0) +
  365. (opposite ? titleFontSize : -titleFontSize) / 2 +
  366. offset +
  367. yOption
  368. ) :
  369. axisTop - titleMargin + yOption;
  370. }
  371. });
  372. // Avoid altering tickInterval when reserving space.
  373. wrap(Axis.prototype, 'unsquish', function (proceed) {
  374. var axis = this,
  375. options = axis.options,
  376. gridOptions = (options && isObject(options.grid)) ? options.grid : {};
  377. if (gridOptions.enabled === true && this.categories) {
  378. return this.tickInterval;
  379. }
  380. return proceed.apply(this, argsToArray(arguments));
  381. });
  382. addEvent(
  383. Axis,
  384. 'afterSetOptions',
  385. /**
  386. * Creates a left and right wall on horizontal axes:
  387. *
  388. * - Places leftmost tick at the start of the axis, to create a left wall
  389. *
  390. * - Ensures that the rightmost tick is at the end of the axis, to create a
  391. * right wall.
  392. *
  393. * @private
  394. * @function
  395. */
  396. function (e) {
  397. var options = this.options,
  398. userOptions = e.userOptions,
  399. gridAxisOptions,
  400. gridOptions = (
  401. (options && isObject(options.grid)) ? options.grid : {}
  402. );
  403. if (gridOptions.enabled === true) {
  404. // Merge the user options into default grid axis options so that
  405. // when a user option is set, it takes presedence.
  406. gridAxisOptions = merge(true, {
  407. className: (
  408. 'highcharts-grid-axis ' + (userOptions.className || '')
  409. ),
  410. dateTimeLabelFormats: {
  411. hour: {
  412. list: ['%H:%M', '%H']
  413. },
  414. day: {
  415. list: ['%A, %e. %B', '%a, %e. %b', '%E']
  416. },
  417. week: {
  418. list: ['Week %W', 'W%W']
  419. },
  420. month: {
  421. list: ['%B', '%b', '%o']
  422. }
  423. },
  424. grid: {
  425. borderWidth: 1
  426. },
  427. labels: {
  428. padding: 2,
  429. style: {
  430. fontSize: '13px'
  431. }
  432. },
  433. title: {
  434. text: null,
  435. reserveSpace: false,
  436. rotation: 0
  437. },
  438. // In a grid axis, only allow one unit of certain types, for
  439. // example we shouln't have one grid cell spanning two days.
  440. units: [[
  441. 'millisecond', // unit name
  442. [1, 10, 100]
  443. ], [
  444. 'second',
  445. [1, 10]
  446. ], [
  447. 'minute',
  448. [1, 5, 15]
  449. ], [
  450. 'hour',
  451. [1, 6]
  452. ], [
  453. 'day',
  454. [1]
  455. ], [
  456. 'week',
  457. [1]
  458. ], [
  459. 'month',
  460. [1]
  461. ], [
  462. 'year',
  463. null
  464. ]]
  465. }, userOptions);
  466. // X-axis specific options
  467. if (this.coll === 'xAxis') {
  468. // For linked axes, tickPixelInterval is used only if the
  469. // tickPositioner below doesn't run or returns undefined (like
  470. // multiple years)
  471. if (
  472. defined(userOptions.linkedTo) &&
  473. !defined(userOptions.tickPixelInterval)
  474. ) {
  475. gridAxisOptions.tickPixelInterval = 350;
  476. }
  477. // For the secondary grid axis, use the primary axis' tick
  478. // intervals and return ticks one level higher.
  479. if (
  480. // Check for tick pixel interval in options
  481. !defined(userOptions.tickPixelInterval) &&
  482. // Only for linked axes
  483. defined(userOptions.linkedTo) &&
  484. !defined(userOptions.tickPositioner) &&
  485. !defined(userOptions.tickInterval)
  486. ) {
  487. gridAxisOptions.tickPositioner = function (min, max) {
  488. var parentInfo = (
  489. this.linkedParent &&
  490. this.linkedParent.tickPositions &&
  491. this.linkedParent.tickPositions.info
  492. );
  493. if (parentInfo) {
  494. var unitIdx,
  495. count,
  496. unitName,
  497. i,
  498. units = gridAxisOptions.units,
  499. unitRange;
  500. for (i = 0; i < units.length; i++) {
  501. if (units[i][0] === parentInfo.unitName) {
  502. unitIdx = i;
  503. break;
  504. }
  505. }
  506. // Spanning multiple years, go default
  507. if (!units[unitIdx][1]) {
  508. return;
  509. }
  510. // Get the first allowed count on the next unit.
  511. if (units[unitIdx + 1]) {
  512. unitName = units[unitIdx + 1][0];
  513. count = (units[unitIdx + 1][1] || [1])[0];
  514. }
  515. unitRange = H.timeUnits[unitName];
  516. this.tickInterval = unitRange * count;
  517. return this.getTimeTicks(
  518. {
  519. unitRange: unitRange,
  520. count: count,
  521. unitName: unitName
  522. },
  523. min,
  524. max,
  525. this.options.startOfWeek
  526. );
  527. }
  528. };
  529. }
  530. }
  531. // Now merge the combined options into the axis options
  532. merge(true, this.options, gridAxisOptions);
  533. if (this.horiz) {
  534. /* _________________________
  535. Make this: ___|_____|_____|_____|__|
  536. ^ ^
  537. _________________________
  538. Into this: |_____|_____|_____|_____|
  539. ^ ^ */
  540. options.minPadding = pick(userOptions.minPadding, 0);
  541. options.maxPadding = pick(userOptions.maxPadding, 0);
  542. }
  543. // If borderWidth is set, then use its value for tick and line
  544. // width.
  545. if (isNumber(options.grid.borderWidth)) {
  546. options.tickWidth = options.lineWidth = gridOptions.borderWidth;
  547. }
  548. }
  549. }
  550. );
  551. addEvent(
  552. Axis,
  553. 'afterSetAxisTranslation',
  554. function () {
  555. var axis = this,
  556. options = axis.options,
  557. gridOptions = (
  558. (options && isObject(options.grid)) ? options.grid : {}
  559. ),
  560. tickInfo = this.tickPositions && this.tickPositions.info,
  561. userLabels = this.userOptions.labels || {};
  562. if (this.horiz) {
  563. if (gridOptions.enabled === true) {
  564. axis.series.forEach(function (series) {
  565. series.options.pointRange = 0;
  566. });
  567. }
  568. // Lower level time ticks, like hours or minutes, represent points
  569. // in time and not ranges. These should be aligned left in the grid
  570. // cell by default. The same applies to years of higher order.
  571. if (
  572. tickInfo &&
  573. (
  574. options.dateTimeLabelFormats[tickInfo.unitName]
  575. .range === false ||
  576. tickInfo.count > 1 // years
  577. ) &&
  578. !defined(userLabels.align)
  579. ) {
  580. options.labels.align = 'left';
  581. if (!defined(userLabels.x)) {
  582. options.labels.x = 3;
  583. }
  584. }
  585. }
  586. }
  587. );
  588. // @todo Does this function do what the drawing says? Seems to affect ticks and
  589. // not the labels directly?
  590. addEvent(
  591. Axis,
  592. 'trimTicks',
  593. /**
  594. * Makes tick labels which are usually ignored in a linked axis displayed if
  595. * they are within range of linkedParent.min.
  596. * ```
  597. * _____________________________
  598. * | | | | |
  599. * Make this: | | 2 | 3 | 4 |
  600. * |___|_______|_______|_______|
  601. * ^
  602. * _____________________________
  603. * | | | | |
  604. * Into this: | 1 | 2 | 3 | 4 |
  605. * |___|_______|_______|_______|
  606. * ^
  607. * ```
  608. *
  609. * @private
  610. */
  611. function () {
  612. var axis = this,
  613. options = axis.options,
  614. gridOptions = (
  615. (options && isObject(options.grid)) ? options.grid : {}
  616. ),
  617. categoryAxis = axis.categories,
  618. tickPositions = axis.tickPositions,
  619. firstPos = tickPositions[0],
  620. lastPos = tickPositions[tickPositions.length - 1],
  621. linkedMin = axis.linkedParent && axis.linkedParent.min,
  622. linkedMax = axis.linkedParent && axis.linkedParent.max,
  623. min = linkedMin || axis.min,
  624. max = linkedMax || axis.max,
  625. tickInterval = axis.tickInterval,
  626. moreThanMin = firstPos > min,
  627. lessThanMax = lastPos < max,
  628. endMoreThanMin = firstPos < min && firstPos + tickInterval > min,
  629. startLessThanMax = lastPos > max && lastPos - tickInterval < max;
  630. if (
  631. gridOptions.enabled === true &&
  632. !categoryAxis &&
  633. (axis.horiz || axis.isLinked)
  634. ) {
  635. if ((moreThanMin || endMoreThanMin) && !options.startOnTick) {
  636. tickPositions[0] = min;
  637. }
  638. if ((lessThanMax || startLessThanMax) && !options.endOnTick) {
  639. tickPositions[tickPositions.length - 1] = max;
  640. }
  641. }
  642. }
  643. );
  644. addEvent(
  645. Axis,
  646. 'afterRender',
  647. /**
  648. * Draw an extra line on the far side of the outermost axis,
  649. * creating floor/roof/wall of a grid. And some padding.
  650. * ```
  651. * Make this:
  652. * (axis.min) __________________________ (axis.max)
  653. * | | | | |
  654. * Into this:
  655. * (axis.min) __________________________ (axis.max)
  656. * ___|____|____|____|____|__
  657. * ```
  658. *
  659. * @private
  660. * @function
  661. *
  662. * @param {Function} proceed
  663. * the original function
  664. */
  665. function () {
  666. var axis = this,
  667. options = axis.options,
  668. gridOptions = ((
  669. options && isObject(options.grid)) ? options.grid : {}
  670. ),
  671. labelPadding,
  672. distance,
  673. lineWidth,
  674. linePath,
  675. yStartIndex,
  676. yEndIndex,
  677. xStartIndex,
  678. xEndIndex,
  679. renderer = axis.chart.renderer,
  680. horiz = axis.horiz,
  681. axisGroupBox;
  682. if (gridOptions.enabled === true) {
  683. // @todo acutual label padding (top, bottom, left, right)
  684. // Label padding is needed to figure out where to draw the outer
  685. // line.
  686. labelPadding = (Math.abs(axis.defaultLeftAxisOptions.labels.x) * 2);
  687. axis.maxLabelDimensions = axis.getMaxLabelDimensions(
  688. axis.ticks,
  689. axis.tickPositions
  690. );
  691. distance = axis.maxLabelDimensions.width + labelPadding;
  692. lineWidth = options.lineWidth;
  693. // Remove right wall before rendering if updating
  694. if (axis.rightWall) {
  695. axis.rightWall.destroy();
  696. }
  697. axisGroupBox = axis.axisGroup.getBBox();
  698. /*
  699. Draw an extra axis line on outer axes
  700. >
  701. Make this: |______|______|______|___
  702. > _________________________
  703. Into this: |______|______|______|__|
  704. */
  705. if (axis.isOuterAxis() && axis.axisLine) {
  706. if (horiz) {
  707. // -1 to avoid adding distance each time the chart updates
  708. distance = axisGroupBox.height - 1;
  709. }
  710. if (lineWidth) {
  711. linePath = axis.getLinePath(lineWidth);
  712. xStartIndex = linePath.indexOf('M') + 1;
  713. xEndIndex = linePath.indexOf('L') + 1;
  714. yStartIndex = linePath.indexOf('M') + 2;
  715. yEndIndex = linePath.indexOf('L') + 2;
  716. // Negate distance if top or left axis
  717. if (axis.side === axisSide.top ||
  718. axis.side === axisSide.left
  719. ) {
  720. distance = -distance;
  721. }
  722. // If axis is horizontal, reposition line path vertically
  723. if (horiz) {
  724. linePath[yStartIndex] = (
  725. linePath[yStartIndex] + distance
  726. );
  727. linePath[yEndIndex] = linePath[yEndIndex] + distance;
  728. } else {
  729. // If axis is vertical, reposition line path
  730. // horizontally
  731. linePath[xStartIndex] = (
  732. linePath[xStartIndex] + distance
  733. );
  734. linePath[xEndIndex] = linePath[xEndIndex] + distance;
  735. }
  736. if (!axis.axisLineExtra) {
  737. axis.axisLineExtra = renderer.path(linePath)
  738. .attr({
  739. stroke: options.lineColor,
  740. 'stroke-width': lineWidth,
  741. zIndex: 7
  742. })
  743. .addClass('highcharts-axis-line')
  744. .add(axis.axisGroup);
  745. } else {
  746. axis.axisLineExtra.animate({
  747. d: linePath
  748. });
  749. }
  750. // show or hide the line depending on options.showEmpty
  751. axis.axisLine[axis.showAxis ? 'show' : 'hide'](true);
  752. }
  753. }
  754. }
  755. }
  756. );
  757. // Wraps axis init to draw cell walls on vertical axes.
  758. addEvent(Axis, 'init', function (e) {
  759. var axis = this,
  760. chart = axis.chart,
  761. userOptions = e.userOptions,
  762. gridOptions = (
  763. (userOptions && isObject(userOptions.grid)) ?
  764. userOptions.grid :
  765. {}
  766. ),
  767. columnOptions,
  768. column,
  769. columnIndex,
  770. i;
  771. function applyGridOptions() {
  772. var options = axis.options,
  773. // TODO: Consider using cell margins defined in % of font size?
  774. // 25 is optimal height for default fontSize (11px)
  775. // 25 / 11 ≈ 2.28
  776. fontSizeToCellHeightRatio = 25 / 11,
  777. fontSize = options.labels.style.fontSize,
  778. fontMetrics = axis.chart.renderer.fontMetrics(fontSize);
  779. // Center-align by default
  780. if (!options.labels) {
  781. options.labels = {};
  782. }
  783. options.labels.align = pick(options.labels.align, 'center');
  784. // @todo: Check against tickLabelPlacement between/on etc
  785. /* Prevents adding the last tick label if the axis is not a category
  786. axis.
  787. Since numeric labels are normally placed at starts and ends of a
  788. range of value, and this module makes the label point at the value,
  789. an "extra" label would appear. */
  790. if (!axis.categories) {
  791. options.showLastLabel = false;
  792. }
  793. // Make tick marks taller, creating cell walls of a grid. Use cellHeight
  794. // axis option if set
  795. if (axis.horiz) {
  796. options.tickLength = gridOptions.cellHeight ||
  797. fontMetrics.h * fontSizeToCellHeightRatio;
  798. }
  799. // Prevents rotation of labels when squished, as rotating them would not
  800. // help.
  801. axis.labelRotation = 0;
  802. options.labels.rotation = 0;
  803. }
  804. if (gridOptions.enabled) {
  805. if (defined(gridOptions.borderColor)) {
  806. userOptions.tickColor =
  807. userOptions.lineColor = gridOptions.borderColor;
  808. }
  809. // Handle columns, each column is a grid axis
  810. if (isArray(gridOptions.columns)) {
  811. columnIndex = 0;
  812. i = gridOptions.columns.length;
  813. while (i--) {
  814. columnOptions = merge(
  815. userOptions,
  816. gridOptions.columns[i],
  817. {
  818. // Force to behave like category axis
  819. type: 'category'
  820. }
  821. );
  822. delete columnOptions.grid.columns; // Prevent recursion
  823. column = new Axis(axis.chart, columnOptions);
  824. column.isColumn = true;
  825. column.columnIndex = columnIndex;
  826. wrap(column, 'labelFormatter', function (proceed) {
  827. var axis = this.axis,
  828. tickPos = axis.tickPositions,
  829. value = this.value,
  830. series = axis.series[0],
  831. isFirst = value === tickPos[0],
  832. isLast = value === tickPos[tickPos.length - 1],
  833. point = H.find(series.options.data, function (p) {
  834. return p[axis.isXAxis ? 'x' : 'y'] === value;
  835. });
  836. // Make additional properties available for the formatter
  837. this.isFirst = isFirst;
  838. this.isLast = isLast;
  839. this.point = point;
  840. // Call original labelFormatter
  841. return proceed.call(this);
  842. });
  843. columnIndex++;
  844. }
  845. // This axis should not be shown, instead the column axes take over
  846. addEvent(this, 'afterInit', function () {
  847. H.erase(chart.axes, this);
  848. H.erase(chart[axis.coll], this);
  849. });
  850. } else {
  851. addEvent(this, 'afterInit', applyGridOptions);
  852. }
  853. }
  854. });