1 | /**
|
2 | * @license Highcharts JS v5.0.2 (2016-10-26)
|
3 | * GridAxis
|
4 | *
|
5 | * (c) 2016 Lars A. V. Cabrera
|
6 | *
|
7 | * --- WORK IN PROGRESS ---
|
8 | *
|
9 | * License: www.highcharts.com/license
|
10 | */
|
11 | (function(factory) {
|
12 | if (typeof module === 'object' && module.exports) {
|
13 | module.exports = factory;
|
14 | } else {
|
15 | factory(Highcharts);
|
16 | }
|
17 | }(function(Highcharts) {
|
18 | (function(H) {
|
19 | /**
|
20 | * (c) 2016 Highsoft AS
|
21 | * Authors: Lars A. V. Cabrera
|
22 | *
|
23 | * License: www.highcharts.com/license
|
24 | */
|
25 | ;
|
26 |
|
27 | var dateFormat = H.dateFormat,
|
28 | each = H.each,
|
29 | isObject = H.isObject,
|
30 | pick = H.pick,
|
31 | wrap = H.wrap,
|
32 | Axis = H.Axis,
|
33 | Chart = H.Chart,
|
34 | Tick = H.Tick;
|
35 |
|
36 |
|
37 | // Enum for which side the axis is on.
|
38 | // Maps to axis.side
|
39 | var axisSide = {
|
40 | top: 0,
|
41 | right: 1,
|
42 | bottom: 2,
|
43 | left: 3,
|
44 | 0: 'top',
|
45 | 1: 'right',
|
46 | 2: 'bottom',
|
47 | 3: 'left'
|
48 | };
|
49 |
|
50 | /**
|
51 | * Checks if an axis is the outer axis in its dimension. Since
|
52 | * axes are placed outwards in order, the axis with the highest
|
53 | * index is the outermost axis.
|
54 | *
|
55 | * Example: If there are multiple x-axes at the top of the chart,
|
56 | * this function returns true if the axis supplied is the last
|
57 | * of the x-axes.
|
58 | *
|
59 | * @return true if the axis is the outermost axis in its dimension;
|
60 | * false if not
|
61 | */
|
62 | Axis.prototype.isOuterAxis = function() {
|
63 | var axis = this,
|
64 | thisIndex = -1,
|
65 | isOuter = true;
|
66 |
|
67 | each(this.chart.axes, function(otherAxis, index) {
|
68 | if (otherAxis.side === axis.side) {
|
69 | if (otherAxis === axis) {
|
70 | // Get the index of the axis in question
|
71 | thisIndex = index;
|
72 |
|
73 | // Check thisIndex >= 0 in case thisIndex has
|
74 | // not been found yet
|
75 | } else if (thisIndex >= 0 && index > thisIndex) {
|
76 | // There was an axis on the same side with a
|
77 | // higher index. Exit the loop.
|
78 | isOuter = false;
|
79 | return;
|
80 | }
|
81 | }
|
82 | });
|
83 | // There were either no other axes on the same side,
|
84 | // or the other axes were not farther from the chart
|
85 | return isOuter;
|
86 | };
|
87 |
|
88 | /**
|
89 | * Shortcut function to Tick.label.getBBox().width.
|
90 | *
|
91 | * @return {number} width - the width of the tick label
|
92 | */
|
93 | Tick.prototype.getLabelWidth = function() {
|
94 | return this.label.getBBox().width;
|
95 | };
|
96 |
|
97 | /**
|
98 | * Get the maximum label length.
|
99 | * This function can be used in states where the axis.maxLabelLength has not
|
100 | * been set.
|
101 | *
|
102 | * @param {boolean} force - Optional parameter to force a new calculation, even
|
103 | * if a value has already been set
|
104 | * @return {number} maxLabelLength - the maximum label length of the axis
|
105 | */
|
106 | Axis.prototype.getMaxLabelLength = function(force) {
|
107 | var tickPositions = this.tickPositions,
|
108 | ticks = this.ticks,
|
109 | maxLabelLength = 0;
|
110 |
|
111 | if (!this.maxLabelLength || force) {
|
112 | each(tickPositions, function(tick) {
|
113 | tick = ticks[tick];
|
114 | if (tick && tick.labelLength > maxLabelLength) {
|
115 | maxLabelLength = tick.labelLength;
|
116 | }
|
117 | });
|
118 | this.maxLabelLength = maxLabelLength;
|
119 | }
|
120 | return this.maxLabelLength;
|
121 | };
|
122 |
|
123 | /**
|
124 | * Adds the axis defined in axis.options.title
|
125 | */
|
126 | Axis.prototype.addTitle = function() {
|
127 | var axis = this,
|
128 | renderer = axis.chart.renderer,
|
129 | axisParent = axis.axisParent,
|
130 | horiz = axis.horiz,
|
131 | opposite = axis.opposite,
|
132 | options = axis.options,
|
133 | axisTitleOptions = options.title,
|
134 | hasData,
|
135 | showAxis,
|
136 | textAlign;
|
137 |
|
138 | // For reuse in Axis.render
|
139 | hasData = axis.hasData();
|
140 | axis.showAxis = showAxis = hasData || pick(options.showEmpty, true);
|
141 |
|
142 | // Disregard title generation in original Axis.getOffset()
|
143 | options.title = '';
|
144 |
|
145 | if (!axis.axisTitle) {
|
146 | textAlign = axisTitleOptions.textAlign;
|
147 | if (!textAlign) {
|
148 | textAlign = (horiz ? {
|
149 | low: 'left',
|
150 | middle: 'center',
|
151 | high: 'right'
|
152 | } : {
|
153 | low: opposite ? 'right' : 'left',
|
154 | middle: 'center',
|
155 | high: opposite ? 'left' : 'right'
|
156 | })[axisTitleOptions.align];
|
157 | }
|
158 | axis.axisTitle = renderer.text(
|
159 | axisTitleOptions.text,
|
160 | 0,
|
161 | 0,
|
162 | axisTitleOptions.useHTML
|
163 | )
|
164 | .attr({
|
165 | zIndex: 7,
|
166 | rotation: axisTitleOptions.rotation || 0,
|
167 | align: textAlign
|
168 | })
|
169 | .addClass('highcharts-axis-title')
|
170 |
|
171 | .css(axisTitleOptions.style)
|
172 |
|
173 | // Add to axisParent instead of axisGroup, to ignore the space
|
174 | // it takes
|
175 | .add(axisParent);
|
176 | axis.axisTitle.isNew = true;
|
177 | }
|
178 |
|
179 |
|
180 | // hide or show the title depending on whether showEmpty is set
|
181 | axis.axisTitle[showAxis ? 'show' : 'hide'](true);
|
182 | };
|
183 |
|
184 | /**
|
185 | * Add custom date formats
|
186 | */
|
187 | H.dateFormats = {
|
188 | // Week number
|
189 | W: function(timestamp) {
|
190 | var date = new Date(timestamp),
|
191 | day = date.getUTCDay() === 0 ? 7 : date.getUTCDay(),
|
192 | time = date.getTime(),
|
193 | startOfYear = new Date(date.getUTCFullYear(), 0, 1, -6),
|
194 | dayNumber;
|
195 | date.setDate(date.getUTCDate() + 4 - day);
|
196 | dayNumber = Math.floor((time - startOfYear) / 86400000);
|
197 | return 1 + Math.floor(dayNumber / 7);
|
198 | },
|
199 | // First letter of the day of the week, e.g. 'M' for 'Monday'.
|
200 | E: function(timestamp) {
|
201 | return dateFormat('%a', timestamp, true).charAt(0);
|
202 | }
|
203 | };
|
204 |
|
205 | /**
|
206 | * Prevents adding the last tick label if the axis is not a category axis.
|
207 | *
|
208 | * Since numeric labels are normally placed at starts and ends of a range of
|
209 | * value, and this module makes the label point at the value, an "extra" label
|
210 | * would appear.
|
211 | *
|
212 | * @param {function} proceed - the original function
|
213 | */
|
214 | wrap(Tick.prototype, 'addLabel', function(proceed) {
|
215 | var axis = this.axis,
|
216 | isCategoryAxis = axis.options.categories !== undefined,
|
217 | tickPositions = axis.tickPositions,
|
218 | lastTick = tickPositions[tickPositions.length - 1],
|
219 | isLastTick = this.pos !== lastTick;
|
220 |
|
221 | if (!axis.options.grid || isCategoryAxis || isLastTick) {
|
222 | proceed.apply(this);
|
223 | }
|
224 | });
|
225 |
|
226 | /**
|
227 | * Center tick labels vertically and horizontally between ticks
|
228 | *
|
229 | * @param {function} proceed - the original function
|
230 | *
|
231 | * @return {object} object - an object containing x and y positions
|
232 | * for the tick
|
233 | */
|
234 | wrap(Tick.prototype, 'getLabelPosition', function(proceed, x, y, label) {
|
235 | var retVal = proceed.apply(this, Array.prototype.slice.call(arguments, 1)),
|
236 | axis = this.axis,
|
237 | options = axis.options,
|
238 | tickInterval = options.tickInterval || 1,
|
239 | newX,
|
240 | newPos,
|
241 | axisHeight,
|
242 | fontSize,
|
243 | labelMetrics,
|
244 | lblB,
|
245 | lblH,
|
246 | labelCenter;
|
247 |
|
248 | // Only center tick labels if axis has option grid: true
|
249 | if (options.grid) {
|
250 | fontSize = options.labels.style.fontSize;
|
251 | labelMetrics = axis.chart.renderer.fontMetrics(fontSize, label);
|
252 | lblB = labelMetrics.b;
|
253 | lblH = labelMetrics.h;
|
254 |
|
255 | if (axis.horiz && options.categories === undefined) {
|
256 | // Center x position
|
257 | axisHeight = axis.axisGroup.getBBox().height;
|
258 | newPos = this.pos + tickInterval / 2;
|
259 | retVal.x = axis.translate(newPos) + axis.left;
|
260 | labelCenter = (axisHeight / 2) + (lblH / 2) - Math.abs(lblH - lblB);
|
261 |
|
262 | // Center y position
|
263 | if (axis.side === axisSide.top) {
|
264 | retVal.y = y - labelCenter;
|
265 | } else {
|
266 | retVal.y = y + labelCenter;
|
267 | }
|
268 | } else {
|
269 | // Center y position
|
270 | if (options.categories === undefined) {
|
271 | newPos = this.pos + (tickInterval / 2);
|
272 | retVal.y = axis.translate(newPos) + axis.top + (lblB / 2);
|
273 | }
|
274 |
|
275 | // Center x position
|
276 | newX = (this.getLabelWidth() / 2) - (axis.maxLabelLength / 2);
|
277 | if (axis.side === axisSide.left) {
|
278 | retVal.x += newX;
|
279 | } else {
|
280 | retVal.x -= newX;
|
281 | }
|
282 | }
|
283 | }
|
284 | return retVal;
|
285 | });
|
286 |
|
287 |
|
288 | /**
|
289 | * Draw vertical ticks extra long to create cell floors and roofs.
|
290 | * Overrides the tickLength for vertical axes.
|
291 | *
|
292 | * @param {function} proceed - the original function
|
293 | * @returns {array} retVal -
|
294 | */
|
295 | wrap(Axis.prototype, 'tickSize', function(proceed) {
|
296 | var axis = this,
|
297 | retVal = proceed.apply(axis, Array.prototype.slice.call(arguments, 1)),
|
298 | labelPadding,
|
299 | distance;
|
300 |
|
301 | if (axis.options.grid && !axis.horiz) {
|
302 | labelPadding = (Math.abs(axis.defaultLeftAxisOptions.labels.x) * 2);
|
303 | if (!axis.maxLabelLength) {
|
304 | axis.maxLabelLength = axis.getMaxLabelLength();
|
305 | }
|
306 | distance = axis.maxLabelLength + labelPadding;
|
307 |
|
308 | retVal[0] = distance;
|
309 | }
|
310 | return retVal;
|
311 | });
|
312 |
|
313 | /**
|
314 | * Disregards space required by axisTitle, by adding axisTitle to axisParent
|
315 | * instead of axisGroup, and disregarding margins and offsets related to
|
316 | * axisTitle.
|
317 | *
|
318 | * @param {function} proceed - the original function
|
319 | */
|
320 | wrap(Axis.prototype, 'getOffset', function(proceed) {
|
321 | var axis = this,
|
322 | axisOffset = axis.chart.axisOffset,
|
323 | side = axis.side,
|
324 | axisHeight,
|
325 | tickSize,
|
326 | options = axis.options,
|
327 | axisTitleOptions = options.title,
|
328 | addTitle = axisTitleOptions &&
|
329 | axisTitleOptions.text &&
|
330 | axisTitleOptions.enabled !== false;
|
331 |
|
332 | if (axis.options.grid && isObject(axis.options.title)) {
|
333 |
|
334 | tickSize = axis.tickSize('tick')[0];
|
335 | if (axisOffset[side] && tickSize) {
|
336 | axisHeight = axisOffset[side] + tickSize;
|
337 | }
|
338 |
|
339 | if (addTitle) {
|
340 | // Use the custom addTitle() to add it, while preventing making room
|
341 | // for it
|
342 | axis.addTitle();
|
343 | }
|
344 |
|
345 | proceed.apply(axis, Array.prototype.slice.call(arguments, 1));
|
346 |
|
347 | axisOffset[side] = pick(axisHeight, axisOffset[side]);
|
348 |
|
349 |
|
350 | // Put axis options back after original Axis.getOffset() has been called
|
351 | options.title = axisTitleOptions;
|
352 |
|
353 | } else {
|
354 | proceed.apply(axis, Array.prototype.slice.call(arguments, 1));
|
355 | }
|
356 | });
|
357 |
|
358 | /**
|
359 | * Prevents rotation of labels when squished, as rotating them would not
|
360 | * help.
|
361 | *
|
362 | * @param {function} proceed - the original function
|
363 | */
|
364 | wrap(Axis.prototype, 'renderUnsquish', function(proceed) {
|
365 | if (this.options.grid) {
|
366 | this.labelRotation = 0;
|
367 | this.options.labels.rotation = 0;
|
368 | }
|
369 | proceed.apply(this);
|
370 | });
|
371 |
|
372 | /**
|
373 | * Places leftmost tick at the start of the axis, to create a left wall.
|
374 | *
|
375 | * @param {function} proceed - the original function
|
376 | */
|
377 | wrap(Axis.prototype, 'setOptions', function(proceed, userOptions) {
|
378 | var axis = this;
|
379 | if (userOptions.grid && axis.horiz) {
|
380 | userOptions.startOnTick = true;
|
381 | userOptions.minPadding = 0;
|
382 | userOptions.endOnTick = true;
|
383 | }
|
384 | proceed.apply(this, Array.prototype.slice.call(arguments, 1));
|
385 | });
|
386 |
|
387 | /**
|
388 | * Draw an extra line on the far side of the the axisLine,
|
389 | * creating cell roofs of a grid.
|
390 | *
|
391 | * @param {function} proceed - the original function
|
392 | */
|
393 | wrap(Axis.prototype, 'render', function(proceed) {
|
394 | var axis = this,
|
395 | options = axis.options,
|
396 | labelPadding,
|
397 | distance,
|
398 | lineWidth,
|
399 | linePath,
|
400 | yStartIndex,
|
401 | yEndIndex,
|
402 | xStartIndex,
|
403 | xEndIndex,
|
404 | renderer = axis.chart.renderer,
|
405 | axisGroupBox;
|
406 |
|
407 | if (options.grid) {
|
408 | labelPadding = (Math.abs(axis.defaultLeftAxisOptions.labels.x) * 2);
|
409 | distance = axis.maxLabelLength + labelPadding;
|
410 | lineWidth = options.lineWidth;
|
411 |
|
412 | // Remove right wall before rendering
|
413 | if (axis.rightWall) {
|
414 | axis.rightWall.destroy();
|
415 | }
|
416 |
|
417 | // Call original Axis.render() to obtain axis.axisLine and
|
418 | // axis.axisGroup
|
419 | proceed.apply(axis);
|
420 |
|
421 | axisGroupBox = axis.axisGroup.getBBox();
|
422 |
|
423 | // Add right wall on horizontal axes
|
424 | if (axis.horiz) {
|
425 | axis.rightWall = renderer.path([
|
426 | 'M',
|
427 | axisGroupBox.x + axis.width + 1, // account for left wall
|
428 | axisGroupBox.y,
|
429 | 'L',
|
430 | axisGroupBox.x + axis.width + 1, // account for left wall
|
431 | axisGroupBox.y + axisGroupBox.height
|
432 | ])
|
433 | .attr({
|
434 | stroke: options.tickColor || '#ccd6eb',
|
435 | 'stroke-width': options.tickWidth || 1,
|
436 | zIndex: 7,
|
437 | class: 'grid-wall'
|
438 | })
|
439 | .add(axis.axisGroup);
|
440 | }
|
441 |
|
442 | if (axis.isOuterAxis() && axis.axisLine) {
|
443 | if (axis.horiz) {
|
444 | // -1 to avoid adding distance each time the chart updates
|
445 | distance = axisGroupBox.height - 1;
|
446 | }
|
447 |
|
448 | if (lineWidth) {
|
449 | linePath = axis.getLinePath(lineWidth);
|
450 | xStartIndex = linePath.indexOf('M') + 1;
|
451 | xEndIndex = linePath.indexOf('L') + 1;
|
452 | yStartIndex = linePath.indexOf('M') + 2;
|
453 | yEndIndex = linePath.indexOf('L') + 2;
|
454 |
|
455 | // Negate distance if top or left axis
|
456 | if (axis.side === axisSide.top || axis.side === axisSide.left) {
|
457 | distance = -distance;
|
458 | }
|
459 |
|
460 | // If axis is horizontal, reposition line path vertically
|
461 | if (axis.horiz) {
|
462 | linePath[yStartIndex] = linePath[yStartIndex] + distance;
|
463 | linePath[yEndIndex] = linePath[yEndIndex] + distance;
|
464 | } else {
|
465 | // If axis is vertical, reposition line path horizontally
|
466 | linePath[xStartIndex] = linePath[xStartIndex] + distance;
|
467 | linePath[xEndIndex] = linePath[xEndIndex] + distance;
|
468 | }
|
469 |
|
470 | if (!axis.axisLineExtra) {
|
471 | axis.axisLineExtra = renderer.path(linePath)
|
472 | .attr({
|
473 | stroke: options.lineColor,
|
474 | 'stroke-width': lineWidth,
|
475 | zIndex: 7
|
476 | })
|
477 | .add(axis.axisGroup);
|
478 | } else {
|
479 | axis.axisLineExtra.animate({
|
480 | d: linePath
|
481 | });
|
482 | }
|
483 |
|
484 | // show or hide the line depending on options.showEmpty
|
485 | axis.axisLine[axis.showAxis ? 'show' : 'hide'](true);
|
486 | }
|
487 | }
|
488 | } else {
|
489 | proceed.apply(axis);
|
490 | }
|
491 | });
|
492 |
|
493 | /**
|
494 | * Wraps chart rendering with the following customizations:
|
495 | * 1. Prohibit timespans of multitudes of a time unit
|
496 | * 2. Draw cell walls on vertical axes
|
497 | *
|
498 | * @param {function} proceed - the original function
|
499 | */
|
500 | wrap(Chart.prototype, 'render', function(proceed) {
|
501 | // 25 is optimal height for default fontSize (11px)
|
502 | // 25 / 11 ≈ 2.28
|
503 | var fontSizeToCellHeightRatio = 25 / 11,
|
504 | fontMetrics,
|
505 | fontSize;
|
506 |
|
507 | each(this.axes, function(axis) {
|
508 | var options = axis.options;
|
509 | if (options.grid) {
|
510 | fontSize = options.labels.style.fontSize;
|
511 | fontMetrics = axis.chart.renderer.fontMetrics(fontSize);
|
512 |
|
513 | // Prohibit timespans of multitudes of a time unit,
|
514 | // e.g. two days, three weeks, etc.
|
515 | if (options.type === 'datetime') {
|
516 | options.units = [
|
517 | ['millisecond', [1]],
|
518 | ['second', [1]],
|
519 | ['minute', [1]],
|
520 | ['hour', [1]],
|
521 | ['day', [1]],
|
522 | ['week', [1]],
|
523 | ['month', [1]],
|
524 | ['year', null]
|
525 | ];
|
526 | }
|
527 |
|
528 | // Make tick marks taller, creating cell walls of a grid.
|
529 | // Use cellHeight axis option if set
|
530 | if (axis.horiz) {
|
531 | options.tickLength = options.cellHeight ||
|
532 | fontMetrics.h * fontSizeToCellHeightRatio;
|
533 | } else {
|
534 | options.tickWidth = 1;
|
535 | if (!options.lineWidth) {
|
536 | options.lineWidth = 1;
|
537 | }
|
538 | }
|
539 | }
|
540 | });
|
541 |
|
542 | // Call original Chart.render()
|
543 | proceed.apply(this);
|
544 | });
|
545 |
|
546 | }(Highcharts));
|
547 | }));
|