highcharts-rails 4.2.7 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.markdown +34 -0
  3. data/Gemfile +4 -0
  4. data/Rakefile +53 -32
  5. data/app/assets/javascripts/highcharts.js +18775 -17176
  6. data/app/assets/javascripts/highcharts/highcharts-3d.js +1849 -1563
  7. data/app/assets/javascripts/highcharts/highcharts-more.js +2162 -1988
  8. data/app/assets/javascripts/highcharts/modules/accessibility.js +1005 -0
  9. data/app/assets/javascripts/highcharts/modules/annotations.js +408 -401
  10. data/app/assets/javascripts/highcharts/modules/boost.js +561 -546
  11. data/app/assets/javascripts/highcharts/modules/broken-axis.js +330 -324
  12. data/app/assets/javascripts/highcharts/modules/data.js +973 -965
  13. data/app/assets/javascripts/highcharts/modules/drilldown.js +783 -723
  14. data/app/assets/javascripts/highcharts/modules/exporting.js +864 -785
  15. data/app/assets/javascripts/highcharts/modules/funnel.js +290 -306
  16. data/app/assets/javascripts/highcharts/modules/heatmap.js +701 -645
  17. data/app/assets/javascripts/highcharts/modules/no-data-to-display.js +150 -132
  18. data/app/assets/javascripts/highcharts/modules/offline-exporting.js +414 -355
  19. data/app/assets/javascripts/highcharts/modules/overlapping-datalabels.js +164 -0
  20. data/app/assets/javascripts/highcharts/modules/series-label.js +473 -448
  21. data/app/assets/javascripts/highcharts/modules/solid-gauge.js +279 -271
  22. data/app/assets/javascripts/highcharts/modules/treemap.js +921 -886
  23. data/app/assets/javascripts/highcharts/themes/dark-blue.js +307 -244
  24. data/app/assets/javascripts/highcharts/themes/dark-green.js +303 -244
  25. data/app/assets/javascripts/highcharts/themes/dark-unica.js +231 -201
  26. data/app/assets/javascripts/highcharts/themes/gray.js +314 -245
  27. data/app/assets/javascripts/highcharts/themes/grid-light.js +91 -66
  28. data/app/assets/javascripts/highcharts/themes/grid.js +124 -96
  29. data/app/assets/javascripts/highcharts/themes/sand-signika.js +119 -94
  30. data/app/assets/javascripts/highcharts/themes/skies.js +108 -85
  31. data/lib/highcharts/version.rb +1 -1
  32. metadata +13 -14
  33. data/app/assets/javascripts/highcharts/adapters/standalone-framework.js +0 -1
  34. data/app/assets/javascripts/highcharts/modules/canvas-tools.js +0 -3115
  35. data/app/assets/javascripts/highcharts/modules/map.js +0 -2117
@@ -0,0 +1,1005 @@
1
+ /**
2
+ * @license Highcharts JS v5.0.0 (2016-09-29)
3
+ * Accessibility module
4
+ *
5
+ * (c) 2010-2016 Highsoft AS
6
+ * Author: Oystein Moseng
7
+ *
8
+ * License: www.highcharts.com/license
9
+ */
10
+ (function(factory) {
11
+ if (typeof module === 'object' && module.exports) {
12
+ module.exports = factory;
13
+ } else {
14
+ factory(Highcharts);
15
+ }
16
+ }(function(Highcharts) {
17
+ (function(H) {
18
+ /**
19
+ * Accessibility module
20
+ *
21
+ * (c) 2010-2016 Highsoft AS
22
+ * Author: Oystein Moseng
23
+ *
24
+ * License: www.highcharts.com/license
25
+ */
26
+ 'use strict';
27
+
28
+ var win = H.win,
29
+ doc = win.document,
30
+ each = H.each,
31
+ erase = H.erase,
32
+ addEvent = H.addEvent,
33
+ removeEvent = H.removeEvent,
34
+ fireEvent = H.fireEvent,
35
+ dateFormat = H.dateFormat,
36
+ merge = H.merge,
37
+ // Human readable description of series and each point in singular and plural
38
+ typeToSeriesMap = {
39
+ 'default': ['series', 'data point', 'data points'],
40
+ 'line': ['line', 'data point', 'data points'],
41
+ 'spline': ['line', 'data point', 'data points'],
42
+ 'area': ['line', 'data point', 'data points'],
43
+ 'areaspline': ['line', 'data point', 'data points'],
44
+ 'pie': ['pie', 'slice', 'slices'],
45
+ 'column': ['column series', 'column', 'columns'],
46
+ 'bar': ['bar series', 'bar', 'bars'],
47
+ 'scatter': ['scatter series', 'data point', 'data points'],
48
+ 'boxplot': ['boxplot series', 'box', 'boxes'],
49
+ 'arearange': ['arearange series', 'data point', 'data points'],
50
+ 'areasplinerange': ['areasplinerange series', 'data point', 'data points'],
51
+ 'bubble': ['bubble series', 'bubble', 'bubbles'],
52
+ 'columnrange': ['columnrange series', 'column', 'columns'],
53
+ 'errorbar': ['errorbar series', 'errorbar', 'errorbars'],
54
+ 'funnel': ['funnel', 'data point', 'data points'],
55
+ 'pyramid': ['pyramid', 'data point', 'data points'],
56
+ 'waterfall': ['waterfall series', 'column', 'columns'],
57
+ 'map': ['map', 'area', 'areas'],
58
+ 'mapline': ['line', 'data point', 'data points'],
59
+ 'mappoint': ['point series', 'data point', 'data points'],
60
+ 'mapbubble': ['bubble series', 'bubble', 'bubbles']
61
+ },
62
+ // Descriptions for exotic chart types
63
+ typeDescriptionMap = {
64
+ boxplot: ' Box plot charts are typically used to display groups of statistical data. ' +
65
+ 'Each data point in the chart can have up to 5 values: minimum, lower quartile, median, upper quartile and maximum. ',
66
+ arearange: ' Arearange charts are line charts displaying a range between a lower and higher value for each point. ',
67
+ areasplinerange: ' These charts are line charts displaying a range between a lower and higher value for each point. ',
68
+ bubble: ' Bubble charts are scatter charts where each data point also has a size value. ',
69
+ columnrange: ' Columnrange charts are column charts displaying a range between a lower and higher value for each point. ',
70
+ errorbar: ' Errorbar series are used to display the variability of the data. ',
71
+ funnel: ' Funnel charts are used to display reduction of data in stages. ',
72
+ pyramid: ' Pyramid charts consist of a single pyramid with item heights corresponding to each point value. ',
73
+ waterfall: ' A waterfall chart is a column chart where each column contributes towards a total end value. '
74
+ },
75
+ commonKeys = ['name', 'id', 'category', 'x', 'value', 'y'],
76
+ specialKeys = ['z', 'open', 'high', 'q3', 'median', 'q1', 'low', 'close']; // Tell user about all properties if points have one of these defined
77
+
78
+ // Default a11y options
79
+ H.setOptions({
80
+ accessibility: {
81
+ enabled: true,
82
+ pointDescriptionThreshold: 30, // set to false to disable
83
+ keyboardNavigation: {
84
+ enabled: true
85
+ // skipNullPoints: false
86
+ }
87
+ // describeSingleSeries: false
88
+ }
89
+ });
90
+
91
+ // Utility function. Reverses child nodes of a DOM element
92
+ function reverseChildNodes(node) {
93
+ var i = node.childNodes.length;
94
+ while (i--) {
95
+ node.appendChild(node.childNodes[i]);
96
+ }
97
+ }
98
+
99
+ // Utility function to attempt to fake a click event on an element
100
+ function fakeClickEvent(element) {
101
+ var fakeEvent;
102
+ if (element && element.onclick) {
103
+ fakeEvent = doc.createEvent('Events');
104
+ fakeEvent.initEvent('click', true, false);
105
+ element.onclick(fakeEvent);
106
+ }
107
+ }
108
+
109
+ // Whenever drawing series, put info on DOM elements
110
+ H.wrap(H.Series.prototype, 'render', function(proceed) {
111
+ proceed.apply(this, Array.prototype.slice.call(arguments, 1));
112
+ if (this.chart.options.accessibility.enabled) {
113
+ this.setA11yDescription();
114
+ }
115
+ });
116
+
117
+ // Put accessible info on series and points of a series
118
+ H.Series.prototype.setA11yDescription = function() {
119
+ var a11yOptions = this.chart.options.accessibility,
120
+ firstPointEl = this.points && this.points[0].graphic && this.points[0].graphic.element,
121
+ seriesEl = firstPointEl && firstPointEl.parentNode || this.graph && this.graph.element || this.group && this.group.element; // Could be tracker series depending on series type
122
+
123
+ if (seriesEl) {
124
+ // For some series types the order of elements do not match the order of points in series
125
+ // In that case we have to reverse them in order for AT to read them out in an understandable order
126
+ if (seriesEl.lastChild === firstPointEl) {
127
+ reverseChildNodes(seriesEl);
128
+ }
129
+ // Make individual point elements accessible if possible. Note: If markers are disabled there might not be any elements there to make accessible.
130
+ if (this.points && (this.points.length < a11yOptions.pointDescriptionThreshold || a11yOptions.pointDescriptionThreshold === false)) {
131
+ each(this.points, function(point) {
132
+ if (point.graphic) {
133
+ point.graphic.element.setAttribute('role', 'img');
134
+ point.graphic.element.setAttribute('tabindex', '-1');
135
+ point.graphic.element.setAttribute('aria-label', a11yOptions.pointDescriptionFormatter && a11yOptions.pointDescriptionFormatter(point) ||
136
+ point.buildPointInfoString());
137
+ }
138
+ });
139
+ }
140
+ // Make series element accessible
141
+ if (this.chart.series.length > 1 || a11yOptions.describeSingleSeries) {
142
+ seriesEl.setAttribute('role', 'region');
143
+ seriesEl.setAttribute('tabindex', '-1');
144
+ seriesEl.setAttribute('aria-label', a11yOptions.seriesDescriptionFormatter && a11yOptions.seriesDescriptionFormatter(this) ||
145
+ this.buildSeriesInfoString());
146
+ }
147
+ }
148
+ };
149
+
150
+ // Return string with information about series
151
+ H.Series.prototype.buildSeriesInfoString = function() {
152
+ var typeInfo = typeToSeriesMap[this.type] || typeToSeriesMap.default,
153
+ description = this.description || this.options.description;
154
+ return (this.name ? this.name + ', ' : '') +
155
+ (this.chart.types.length === 1 ? typeInfo[0] : 'series') + ' ' + (this.index + 1) + ' of ' + (this.chart.series.length) +
156
+ (this.chart.types.length === 1 ? ' with ' : '. ' + typeInfo[0] + ' with ') +
157
+ (this.points.length + ' ' + (this.points.length === 1 ? typeInfo[1] : typeInfo[2])) +
158
+ (description ? '. ' + description : '') +
159
+ (this.chart.yAxis.length > 1 && this.yAxis ? '. Y axis, ' + this.yAxis.getDescription() : '') +
160
+ (this.chart.xAxis.length > 1 && this.xAxis ? '. X axis, ' + this.xAxis.getDescription() : '');
161
+ };
162
+
163
+ // Return string with information about point
164
+ H.Point.prototype.buildPointInfoString = function() {
165
+ var point = this,
166
+ series = point.series,
167
+ a11yOptions = series.chart.options.accessibility,
168
+ infoString = '',
169
+ hasSpecialKey = false,
170
+ dateTimePoint = series.xAxis && series.xAxis.isDatetimeAxis,
171
+ timeDesc = dateTimePoint && dateFormat(a11yOptions.pointDateFormatter && a11yOptions.pointDateFormatter(point) || a11yOptions.pointDateFormat ||
172
+ H.Tooltip.prototype.getXDateFormat(point, series.chart.options.tooltip, series.xAxis), point.x);
173
+
174
+ each(specialKeys, function(key) {
175
+ if (point[key] !== undefined) {
176
+ hasSpecialKey = true;
177
+ }
178
+ });
179
+
180
+ // If the point has one of the less common properties defined, display all that are defined
181
+ if (hasSpecialKey) {
182
+ if (dateTimePoint) {
183
+ infoString = timeDesc;
184
+ }
185
+ each(commonKeys.concat(specialKeys), function(key) {
186
+ if (point[key] !== undefined && !(dateTimePoint && key === 'x')) {
187
+ infoString += (infoString ? '. ' : '') + key + ', ' + this[key];
188
+ }
189
+ });
190
+ } else {
191
+ // Pick and choose properties for a succint label
192
+ infoString = (this.name || timeDesc || this.category || this.id || 'x, ' + this.x) + ', ' +
193
+ (this.value !== undefined ? this.value : this.y);
194
+ }
195
+
196
+ return (this.index + 1) + '. ' + infoString + '.' + (this.description ? ' ' + this.description : '');
197
+ };
198
+
199
+ // Get descriptive label for axis
200
+ H.Axis.prototype.getDescription = function() {
201
+ return this.userOptions && this.userOptions.description || this.axisTitle && this.axisTitle.textStr ||
202
+ this.options.id || this.categories && 'categories' || 'values';
203
+ };
204
+
205
+ // Pan along axis in a direction (1 or -1), optionally with a defined granularity (number of steps it takes to walk across current view)
206
+ H.Axis.prototype.panStep = function(direction, granularity) {
207
+ var gran = granularity || 3,
208
+ extremes = this.getExtremes(),
209
+ step = (extremes.max - extremes.min) / gran * direction,
210
+ newMax = extremes.max + step,
211
+ newMin = extremes.min + step,
212
+ size = newMax - newMin;
213
+ if (direction < 0 && newMin < extremes.dataMin) {
214
+ newMin = extremes.dataMin;
215
+ newMax = newMin + size;
216
+ } else if (direction > 0 && newMax > extremes.dataMax) {
217
+ newMax = extremes.dataMax;
218
+ newMin = newMax - size;
219
+ }
220
+ this.setExtremes(newMin, newMax);
221
+ };
222
+
223
+ // Whenever adding or removing series, keep track of types present in chart
224
+ H.wrap(H.Series.prototype, 'init', function(proceed) {
225
+ proceed.apply(this, Array.prototype.slice.call(arguments, 1));
226
+ var chart = this.chart;
227
+ if (chart.options.accessibility.enabled) {
228
+ chart.types = chart.types || [];
229
+
230
+ // Add type to list if does not exist
231
+ if (chart.types.indexOf(this.type) < 0) {
232
+ chart.types.push(this.type);
233
+ }
234
+
235
+ addEvent(this, 'remove', function() {
236
+ var removedSeries = this,
237
+ hasType = false;
238
+
239
+ // Check if any of the other series have the same type as this one. Otherwise remove it from the list.
240
+ each(chart.series, function(s) {
241
+ if (s !== removedSeries && chart.types.indexOf(removedSeries.type) < 0) {
242
+ hasType = true;
243
+ }
244
+ });
245
+ if (!hasType) {
246
+ erase(chart.types, removedSeries.type);
247
+ }
248
+ });
249
+ }
250
+ });
251
+
252
+ // Return simplified description of chart type. Some types will not be familiar to most screen reader users, but we try.
253
+ H.Chart.prototype.getTypeDescription = function() {
254
+ var firstType = this.types && this.types[0],
255
+ mapTitle = this.series[0] && this.series[0].mapTitle;
256
+ if (!firstType) {
257
+ return 'Empty chart.';
258
+ } else if (firstType === 'map') {
259
+ return mapTitle ? 'Map of ' + mapTitle : 'Map of unspecified region.';
260
+ } else if (this.types.length > 1) {
261
+ return 'Combination chart.';
262
+ } else if (['spline', 'area', 'areaspline'].indexOf(firstType) > -1) {
263
+ return 'Line chart.';
264
+ }
265
+ return firstType + ' chart.' + (typeDescriptionMap[firstType] || '');
266
+ };
267
+
268
+ // Return object with text description of each of the chart's axes
269
+ H.Chart.prototype.getAxesDescription = function() {
270
+ var numXAxes = this.xAxis.length,
271
+ numYAxes = this.yAxis.length,
272
+ desc = {},
273
+ i;
274
+
275
+ if (numXAxes) {
276
+ desc.xAxis = 'The chart has ' + numXAxes + (numXAxes > 1 ? ' X axes' : ' X axis') + ' displaying ';
277
+ if (numXAxes < 2) {
278
+ desc.xAxis += this.xAxis[0].getDescription() + '.';
279
+ } else {
280
+ for (i = 0; i < numXAxes - 1; ++i) {
281
+ desc.xAxis += (i ? ', ' : '') + this.xAxis[i].getDescription();
282
+ }
283
+ desc.xAxis += ' and ' + this.xAxis[i].getDescription() + '.';
284
+ }
285
+ }
286
+
287
+ if (numYAxes) {
288
+ desc.yAxis = 'The chart has ' + numYAxes + (numYAxes > 1 ? ' Y axes' : ' Y axis') + ' displaying ';
289
+ if (numYAxes < 2) {
290
+ desc.yAxis += this.yAxis[0].getDescription() + '.';
291
+ } else {
292
+ for (i = 0; i < numYAxes - 1; ++i) {
293
+ desc.yAxis += (i ? ', ' : '') + this.yAxis[i].getDescription();
294
+ }
295
+ desc.yAxis += ' and ' + this.yAxis[i].getDescription() + '.';
296
+ }
297
+ }
298
+
299
+ return desc;
300
+ };
301
+
302
+ // Set a11y attribs on exporting menu
303
+ H.Chart.prototype.addAccessibleContextMenuAttribs = function() {
304
+ var exportList = this.exportDivElements;
305
+ if (exportList) {
306
+ // Set tabindex on the menu items to allow focusing by script
307
+ // Set role to give screen readers a chance to pick up the contents
308
+ each(exportList, function(item) {
309
+ if (item.tagName === 'DIV' &&
310
+ !(item.children && item.children.length)) {
311
+ item.setAttribute('role', 'menuitem');
312
+ item.setAttribute('tabindex', -1);
313
+ }
314
+ });
315
+ // Set accessibility properties on parent div
316
+ exportList[0].parentNode.setAttribute('role', 'menu');
317
+ exportList[0].parentNode.setAttribute('aria-label', 'Chart export');
318
+ }
319
+ };
320
+
321
+ // Highlight a point (show tooltip and display hover state). Returns the highlighted point.
322
+ H.Point.prototype.highlight = function() {
323
+ var chart = this.series.chart;
324
+ if (this.graphic && this.graphic.element.focus) {
325
+ this.graphic.element.focus();
326
+ }
327
+ if (!this.isNull) {
328
+ this.onMouseOver(); // Show the hover marker
329
+ chart.tooltip.refresh(chart.tooltip.shared ? [this] : this); // Show the tooltip
330
+ } else {
331
+ chart.tooltip.hide(0);
332
+ // Don't call blur on the element, as it messes up the chart div's focus
333
+ }
334
+ chart.highlightedPoint = this;
335
+ return this;
336
+ };
337
+
338
+ // Function to highlight next/previous point in chart
339
+ // Returns highlighted point on success, false on failure (no adjacent point to highlight in chosen direction)
340
+ H.Chart.prototype.highlightAdjacentPoint = function(next) {
341
+ var series = this.series,
342
+ curPoint = this.highlightedPoint,
343
+ newSeries,
344
+ newPoint;
345
+
346
+ // If no points, return false
347
+ if (!series[0] || !series[0].points) {
348
+ return false;
349
+ }
350
+
351
+ // Use first point if none already highlighted
352
+ if (!curPoint) {
353
+ return series[0].points[0].highlight();
354
+ }
355
+
356
+ newSeries = series[curPoint.series.index + (next ? 1 : -1)];
357
+ newPoint = next ?
358
+ // Try to grab next point
359
+ curPoint.series.points[curPoint.index + 1] || newSeries && newSeries.points[0] :
360
+ // Try to grab previous point
361
+ curPoint.series.points[curPoint.index - 1] ||
362
+ newSeries && newSeries.points[newSeries.points.length - 1];
363
+
364
+ // If there is no adjacent point, we return false
365
+ if (newPoint === undefined) {
366
+ return false;
367
+ }
368
+
369
+ // Recursively skip null points
370
+ if (newPoint.isNull && this.options.accessibility.keyboardNavigation &&
371
+ this.options.accessibility.keyboardNavigation.skipNullPoints) {
372
+ this.highlightedPoint = newPoint;
373
+ return this.highlightAdjacentPoint(next);
374
+ }
375
+
376
+ // There is an adjacent point, highlight it
377
+ return newPoint.highlight();
378
+ };
379
+
380
+ // Show the export menu and focus the first item (if exists)
381
+ H.Chart.prototype.showExportMenu = function() {
382
+ if (this.exportSVGElements && this.exportSVGElements[0]) {
383
+ this.exportSVGElements[0].element.onclick();
384
+ this.highlightExportItem(0);
385
+ }
386
+ };
387
+
388
+ // Highlight export menu item by index
389
+ H.Chart.prototype.highlightExportItem = function(ix) {
390
+ var listItem = this.exportDivElements && this.exportDivElements[ix],
391
+ curHighlighted = this.exportDivElements && this.exportDivElements[this.highlightedExportItem];
392
+
393
+ if (listItem && listItem.tagName === 'DIV' && !(listItem.children && listItem.children.length)) {
394
+ if (listItem.focus) {
395
+ listItem.focus();
396
+ }
397
+ if (curHighlighted && curHighlighted.onmouseout) {
398
+ curHighlighted.onmouseout();
399
+ }
400
+ if (listItem.onmouseover) {
401
+ listItem.onmouseover();
402
+ }
403
+ this.highlightedExportItem = ix;
404
+ return true;
405
+ }
406
+ };
407
+
408
+ // Highlight range selector button by index
409
+ H.Chart.prototype.highlightRangeSelectorButton = function(ix) {
410
+ var buttons = this.rangeSelector.buttons;
411
+ // Deselect old
412
+ if (buttons[this.highlightedRangeSelectorItemIx]) {
413
+ buttons[this.highlightedRangeSelectorItemIx].setState(this.oldRangeSelectorItemState || 0);
414
+ }
415
+ // Select new
416
+ this.highlightedRangeSelectorItemIx = ix;
417
+ if (buttons[ix]) {
418
+ if (buttons[ix].element.focus) {
419
+ buttons[ix].element.focus();
420
+ }
421
+ this.oldRangeSelectorItemState = buttons[ix].state;
422
+ buttons[ix].setState(2);
423
+ return true;
424
+ }
425
+ return false;
426
+ };
427
+
428
+ // Hide export menu
429
+ H.Chart.prototype.hideExportMenu = function() {
430
+ var exportList = this.exportDivElements;
431
+ if (exportList) {
432
+ each(exportList, function(el) {
433
+ fireEvent(el, 'mouseleave');
434
+ });
435
+ if (exportList[this.highlightedExportItem] && exportList[this.highlightedExportItem].onmouseout) {
436
+ exportList[this.highlightedExportItem].onmouseout();
437
+ }
438
+ this.highlightedExportItem = 0;
439
+ this.renderTo.focus();
440
+ }
441
+ };
442
+
443
+ // Add keyboard navigation handling to chart
444
+ H.Chart.prototype.addKeyboardNavEvents = function() {
445
+ var chart = this;
446
+
447
+ // Abstraction layer for keyboard navigation. Keep a map of keyCodes to handler functions, and a next/prev move handler for tab order.
448
+ // The module's keyCode handlers determine when to move to another module.
449
+ // Validate holds a function to determine if there are prerequisites for this module to run that are not met.
450
+ // Init holds a function to run once before any keyCodes are interpreted.
451
+ // transformTabs determines whether to transform tabs to left/right events or not. Defaults to true.
452
+ function KeyboardNavigationModule(options) {
453
+ this.keyCodeMap = options.keyCodeMap;
454
+ this.move = options.move;
455
+ this.validate = options.validate;
456
+ this.init = options.init;
457
+ this.transformTabs = options.transformTabs !== false;
458
+ }
459
+ KeyboardNavigationModule.prototype = {
460
+ // Find handler function(s) for key code in the keyCodeMap and run it.
461
+ run: function(e) {
462
+ var navModule = this,
463
+ keyCode = e.which || e.keyCode,
464
+ handled = false;
465
+ keyCode = this.transformTabs && keyCode === 9 ? (e.shiftKey ? 37 : 39) : keyCode; // Transform tabs
466
+ each(this.keyCodeMap, function(codeSet) {
467
+ if (codeSet[0].indexOf(keyCode) > -1) {
468
+ handled = codeSet[1].call(navModule, keyCode, e) === false ? false : true; // If explicitly returning false, we haven't handled it
469
+ }
470
+ });
471
+ return handled;
472
+ }
473
+ };
474
+ // Maintain abstraction between KeyboardNavigationModule and Highcharts
475
+ // The chart object keeps track of a list of KeyboardNavigationModules that we move through
476
+ function navModuleFactory(keyMap, options) {
477
+ return new KeyboardNavigationModule(merge({
478
+ keyCodeMap: keyMap,
479
+ // Move to next/prev valid module, or undefined if none, and init it.
480
+ // Returns true on success and false if there is no valid module to move to.
481
+ move: function(direction) {
482
+ chart.keyboardNavigationModuleIndex += direction;
483
+ var newModule = chart.keyboardNavigationModules[chart.keyboardNavigationModuleIndex];
484
+ if (newModule) {
485
+ if (newModule.validate && !newModule.validate()) {
486
+ return this.move(direction); // Invalid module
487
+ }
488
+ if (newModule.init) {
489
+ newModule.init(direction); // Valid module, init it
490
+ return true;
491
+ }
492
+ }
493
+ // No module
494
+ chart.keyboardNavigationModuleIndex = 0; // Reset counter
495
+ chart.slipNextTab = true; // Allow next tab to slip, as we will have focus on chart now
496
+ return false;
497
+ }
498
+ }, options));
499
+ }
500
+
501
+ // Route keydown events
502
+ function keydownHandler(ev) {
503
+ var e = ev || win.event,
504
+ keyCode = e.which || e.keyCode,
505
+ curNavModule = chart.keyboardNavigationModules[chart.keyboardNavigationModuleIndex];
506
+
507
+ // Handle tabbing
508
+ if (keyCode === 9) {
509
+ // If we reached end of chart, we need to let this tab slip through to allow users to tab further
510
+ if (chart.slipNextTab) {
511
+ chart.slipNextTab = false;
512
+ return;
513
+ }
514
+ }
515
+ // If key was not tab, don't slip the next tab
516
+ chart.slipNextTab = false;
517
+
518
+ // If there is a navigation module for the current index, run it. Otherwise, we are outside of the chart in some direction.
519
+ if (curNavModule) {
520
+ if (curNavModule.run(e)) {
521
+ e.preventDefault(); // If successfully handled, stop the event here.
522
+ }
523
+ }
524
+ }
525
+
526
+ // List of the different keyboard handling modes we use depending on where we are in the chart.
527
+ // Each mode has a set of handling functions mapped to key codes.
528
+ // Each mode determines when to move to the next/prev mode.
529
+ chart.keyboardNavigationModules = [
530
+ // Points
531
+ navModuleFactory([
532
+ // Left/Right
533
+ [
534
+ [37, 39],
535
+ function(keyCode) {
536
+ if (!chart.highlightAdjacentPoint(keyCode === 39)) { // Try to highlight adjacent point
537
+ return this.move(keyCode === 39 ? 1 : -1); // Failed. Move to next/prev module
538
+ }
539
+ }
540
+ ],
541
+ // Up/Down
542
+ [
543
+ [38, 40],
544
+ function(keyCode) {
545
+ var newSeries;
546
+ if (chart.highlightedPoint) {
547
+ newSeries = chart.series[chart.highlightedPoint.series.index + (keyCode === 38 ? -1 : 1)]; // Find prev/next series
548
+ if (newSeries && newSeries.points[0]) { // If series exists and has data, go for it
549
+ newSeries.points[0].highlight();
550
+ } else {
551
+ return this.move(keyCode === 40 ? 1 : -1); // Otherwise, attempt to move to next/prev module
552
+ }
553
+ }
554
+ }
555
+ ],
556
+ // Enter/Spacebar
557
+ [
558
+ [13, 32],
559
+ function() {
560
+ if (chart.highlightedPoint) {
561
+ chart.highlightedPoint.firePointEvent('click');
562
+ }
563
+ }
564
+ ]
565
+ ], {
566
+ // If coming back to points from other module, highlight last point
567
+ init: function(direction) {
568
+ var lastSeries = chart.series && chart.series[chart.series.length - 1],
569
+ lastPoint = lastSeries && lastSeries.points && lastSeries.points[lastSeries.points.length - 1];
570
+ if (direction < 0 && lastPoint) {
571
+ lastPoint.highlight();
572
+ }
573
+ }
574
+ }),
575
+
576
+ // Exporting
577
+ navModuleFactory([
578
+ // Left/Up
579
+ [
580
+ [37, 38],
581
+ function() {
582
+ var i = chart.highlightedExportItem || 0,
583
+ reachedEnd = true,
584
+ series = chart.series,
585
+ newSeries;
586
+ // Try to highlight prev item in list. Highlighting e.g. separators will fail.
587
+ while (i--) {
588
+ if (chart.highlightExportItem(i)) {
589
+ reachedEnd = false;
590
+ break;
591
+ }
592
+ }
593
+ if (reachedEnd) {
594
+ chart.hideExportMenu();
595
+ // Wrap to last point
596
+ if (series && series.length) {
597
+ newSeries = series[series.length - 1];
598
+ if (newSeries.points.length) {
599
+ newSeries.points[newSeries.points.length - 1].highlight();
600
+ }
601
+ }
602
+ // Try to move to prev module (should be points, since we wrapped to last point)
603
+ return this.move(-1);
604
+ }
605
+ }
606
+ ],
607
+ // Right/Down
608
+ [
609
+ [39, 40],
610
+ function() {
611
+ var highlightedExportItem = chart.highlightedExportItem || 0,
612
+ reachedEnd = true;
613
+ // Try to highlight next item in list. Highlighting e.g. separators will fail.
614
+ for (var i = highlightedExportItem + 1; i < chart.exportDivElements.length; ++i) {
615
+ if (chart.highlightExportItem(i)) {
616
+ reachedEnd = false;
617
+ break;
618
+ }
619
+ }
620
+ if (reachedEnd) {
621
+ chart.hideExportMenu();
622
+ return this.move(1); // Next module
623
+ }
624
+ }
625
+ ],
626
+ // Enter/Spacebar
627
+ [
628
+ [13, 32],
629
+ function() {
630
+ fakeClickEvent(chart.exportDivElements[chart.highlightedExportItem]);
631
+ }
632
+ ]
633
+ ], {
634
+ // Only run exporting navigation if exporting support exists and is enabled on chart
635
+ validate: function() {
636
+ return chart.exportChart && !(chart.options.exporting && chart.options.exporting.enabled === false);
637
+ },
638
+ // Show export menu
639
+ init: function(direction) {
640
+ chart.highlightedPoint = null;
641
+ chart.showExportMenu();
642
+ // If coming back to export menu from other module, try to highlight last item in menu
643
+ if (direction < 0 && chart.exportDivElements) {
644
+ for (var i = chart.exportDivElements.length; i > -1; --i) {
645
+ if (chart.highlightExportItem(i)) {
646
+ break;
647
+ }
648
+ }
649
+ }
650
+ }
651
+ }),
652
+
653
+ // Map zoom
654
+ navModuleFactory([
655
+ // Up/down/left/right
656
+ [
657
+ [38, 40, 37, 39],
658
+ function(keyCode) {
659
+ chart[keyCode === 38 || keyCode === 40 ? 'yAxis' : 'xAxis'][0].panStep(keyCode < 39 ? -1 : 1);
660
+ }
661
+ ],
662
+
663
+ // Tabs
664
+ [
665
+ [9],
666
+ function(keyCode, e) {
667
+ var button;
668
+ chart.mapNavButtons[chart.focusedMapNavButtonIx].setState(0); // Deselect old
669
+ if (e.shiftKey && !chart.focusedMapNavButtonIx || !e.shiftKey && chart.focusedMapNavButtonIx) { // trying to go somewhere we can't?
670
+ chart.mapZoom(); // Reset zoom
671
+ return this.move(e.shiftKey ? -1 : 1); // Nowhere to go, go to prev/next module
672
+ }
673
+ chart.focusedMapNavButtonIx += e.shiftKey ? -1 : 1;
674
+ button = chart.mapNavButtons[chart.focusedMapNavButtonIx];
675
+ if (button.element.focus) {
676
+ button.element.focus();
677
+ }
678
+ button.setState(2);
679
+ }
680
+ ],
681
+
682
+ // Enter/Spacebar
683
+ [
684
+ [13, 32],
685
+ function() {
686
+ fakeClickEvent(chart.mapNavButtons[chart.focusedMapNavButtonIx].element);
687
+ }
688
+ ]
689
+ ], {
690
+ // Only run this module if we have map zoom on the chart
691
+ validate: function() {
692
+ return chart.mapZoom && chart.mapNavButtons && chart.mapNavButtons.length === 2;
693
+ },
694
+
695
+ // Handle tabs separately
696
+ transformTabs: false,
697
+
698
+ // Make zoom buttons do their magic
699
+ init: function(direction) {
700
+ var zoomIn = chart.mapNavButtons[0],
701
+ zoomOut = chart.mapNavButtons[1],
702
+ initialButton = direction > 0 ? zoomIn : zoomOut;
703
+
704
+ each(chart.mapNavButtons, function(button, i) {
705
+ button.element.setAttribute('tabindex', -1);
706
+ button.element.setAttribute('role', 'button');
707
+ button.element.setAttribute('aria-label', 'Zoom ' + (i ? 'out' : '') + 'chart');
708
+ });
709
+
710
+ if (initialButton.element.focus) {
711
+ initialButton.element.focus();
712
+ }
713
+ initialButton.setState(2);
714
+ chart.focusedMapNavButtonIx = direction > 0 ? 0 : 1;
715
+ }
716
+ }),
717
+
718
+ // Highstock range selector (minus input boxes)
719
+ navModuleFactory([
720
+ // Left/Right/Up/Down
721
+ [
722
+ [37, 39, 38, 40],
723
+ function(keyCode) {
724
+ var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1;
725
+ // Try to highlight next/prev button
726
+ if (!chart.highlightRangeSelectorButton(chart.highlightedRangeSelectorItemIx + direction)) {
727
+ return this.move(direction);
728
+ }
729
+ }
730
+ ],
731
+ // Enter/Spacebar
732
+ [
733
+ [13, 32],
734
+ function() {
735
+ if (chart.oldRangeSelectorItemState !== 3) { // Don't allow click if button used to be disabled
736
+ fakeClickEvent(chart.rangeSelector.buttons[chart.highlightedRangeSelectorItemIx].element);
737
+ }
738
+ }
739
+ ]
740
+ ], {
741
+ // Only run this module if we have range selector
742
+ validate: function() {
743
+ return chart.rangeSelector && chart.rangeSelector.buttons && chart.rangeSelector.buttons.length;
744
+ },
745
+
746
+ // Make elements focusable and accessible
747
+ init: function(direction) {
748
+ each(chart.rangeSelector.buttons, function(button) {
749
+ button.element.setAttribute('tabindex', '-1');
750
+ button.element.setAttribute('role', 'button');
751
+ button.element.setAttribute('aria-label', 'Select range ' + (button.text && button.text.textStr));
752
+ });
753
+ // Focus first/last button
754
+ chart.highlightRangeSelectorButton(direction > 0 ? 0 : chart.rangeSelector.buttons.length - 1);
755
+ }
756
+ }),
757
+
758
+ // Highstock range selector, input boxes
759
+ navModuleFactory([
760
+ // Tab/Up/Down
761
+ [
762
+ [9, 38, 40],
763
+ function(keyCode, e) {
764
+ var direction = (keyCode === 9 && e.shiftKey || keyCode === 38) ? -1 : 1,
765
+ newIx = chart.highlightedInputRangeIx = chart.highlightedInputRangeIx + direction;
766
+ // Try to highlight next/prev item in list.
767
+ if (newIx > 1 || newIx < 0) { // Out of range
768
+ return this.move(direction);
769
+ }
770
+ chart.rangeSelector[newIx ? 'maxInput' : 'minInput'].focus(); // Input boxes are HTML, and should have focus support in all browsers
771
+ }
772
+ ]
773
+ ], {
774
+ // Only run if we have range selector with input boxes
775
+ validate: function() {
776
+ return chart.rangeSelector && chart.options.rangeSelector.inputEnabled !== false && chart.rangeSelector.minInput && chart.rangeSelector.maxInput;
777
+ },
778
+
779
+ // Handle tabs different from left/right (because we don't want to catch left/right in a text area)
780
+ transformTabs: false,
781
+
782
+ // Make boxes focusable by script, and accessible
783
+ init: function(direction) {
784
+ each(['minInput', 'maxInput'], function(key, i) {
785
+ if (chart.rangeSelector[key]) {
786
+ chart.rangeSelector[key].setAttribute('tabindex', '-1');
787
+ chart.rangeSelector[key].setAttribute('role', 'textbox');
788
+ chart.rangeSelector[key].setAttribute('aria-label', 'Select ' + (i ? 'end' : 'start') + ' date.');
789
+ }
790
+ });
791
+ // Highlight first/last input box
792
+ chart.highlightedInputRangeIx = direction > 0 ? 0 : 1;
793
+ chart.rangeSelector[chart.highlightedInputRangeIx ? 'maxInput' : 'minInput'].focus();
794
+ }
795
+ })
796
+ ];
797
+
798
+ // Init nav module index. We start at the first module, and as the user navigates through the chart the index will increase to use different handler modules.
799
+ chart.keyboardNavigationModuleIndex = 0;
800
+
801
+ // Make chart reachable by tab
802
+ if (!chart.renderTo.tabIndex) {
803
+ chart.renderTo.setAttribute('tabindex', '0');
804
+ }
805
+
806
+ // Handle keyboard events
807
+ addEvent(chart.renderTo, 'keydown', keydownHandler);
808
+ addEvent(chart, 'destroy', function() {
809
+ removeEvent(chart.renderTo, 'keydown', keydownHandler);
810
+ });
811
+ };
812
+
813
+ // Add screen reader region to chart.
814
+ // tableId is the HTML id of the table to focus when clicking the table anchor in the screen reader region.
815
+ H.Chart.prototype.addScreenReaderRegion = function(tableId) {
816
+ var chart = this,
817
+ series = chart.series,
818
+ options = chart.options,
819
+ a11yOptions = options.accessibility,
820
+ hiddenSection = chart.screenReaderRegion = doc.createElement('div'),
821
+ tableShortcut = doc.createElement('h3'),
822
+ tableShortcutAnchor = doc.createElement('a'),
823
+ chartHeading = doc.createElement('h3'),
824
+ hiddenStyle = { // CSS style to hide element from visual users while still exposing it to screen readers
825
+ position: 'absolute',
826
+ left: '-9999px',
827
+ top: 'auto',
828
+ width: '1px',
829
+ height: '1px',
830
+ overflow: 'hidden'
831
+ },
832
+ chartTypes = chart.types || [],
833
+ // Build axis info - but not for pies and maps. Consider not adding for certain other types as well (funnel, pyramid?)
834
+ axesDesc = (chartTypes.length === 1 && chartTypes[0] === 'pie' || chartTypes[0] === 'map') && {} || chart.getAxesDescription(),
835
+ chartTypeInfo = series[0] && typeToSeriesMap[series[0].type] || typeToSeriesMap.default;
836
+
837
+ hiddenSection.setAttribute('role', 'region');
838
+ hiddenSection.setAttribute('aria-label', 'Chart screen reader information.');
839
+
840
+ hiddenSection.innerHTML = a11yOptions.screenReaderSectionFormatter && a11yOptions.screenReaderSectionFormatter(chart) ||
841
+ '<div tabindex="0">Use regions/landmarks to skip ahead to chart' +
842
+ (series.length > 1 ? ' and navigate between data series' : '') + '.</div><h3>Summary.</h3><div>' + (options.title.text || 'Chart') +
843
+ (options.subtitle && options.subtitle.text ? '. ' + options.subtitle.text : '') +
844
+ '</div><h3>Long description.</h3><div>' + (options.chart.description || 'No description available.') +
845
+ '</div><h3>Structure.</h3><div>Chart type: ' + (options.chart.typeDescription || chart.getTypeDescription()) + '</div>' +
846
+ (series.length === 1 ? '<div>' + chartTypeInfo[0] + ' with ' + series[0].points.length + ' ' +
847
+ (series[0].points.length === 1 ? chartTypeInfo[1] : chartTypeInfo[2]) + '.</div>' : '') +
848
+ (axesDesc.xAxis ? ('<div>' + axesDesc.xAxis + '</div>') : '') +
849
+ (axesDesc.yAxis ? ('<div>' + axesDesc.yAxis + '</div>') : '');
850
+
851
+ // Add shortcut to data table if export-csv is loaded
852
+ if (chart.getCSV) {
853
+ tableShortcutAnchor.innerHTML = 'View as data table.';
854
+ tableShortcutAnchor.href = '#' + tableId;
855
+ tableShortcutAnchor.setAttribute('tabindex', '-1'); // Make this unreachable by user tabbing
856
+ tableShortcutAnchor.onclick = a11yOptions.onTableAnchorClick || function() {
857
+ chart.viewData();
858
+ doc.getElementById(tableId).focus();
859
+ };
860
+ tableShortcut.appendChild(tableShortcutAnchor);
861
+
862
+ hiddenSection.appendChild(tableShortcut);
863
+ }
864
+
865
+ chartHeading.innerHTML = 'Chart graphic.';
866
+ chart.renderTo.insertBefore(chartHeading, chart.renderTo.firstChild);
867
+ chart.renderTo.insertBefore(hiddenSection, chart.renderTo.firstChild);
868
+
869
+ // Hide the section and the chart heading
870
+ merge(true, chartHeading.style, hiddenStyle);
871
+ merge(true, hiddenSection.style, hiddenStyle);
872
+ };
873
+
874
+
875
+ // Make chart container accessible, and wrap table functionality
876
+ H.Chart.prototype.callbacks.push(function(chart) {
877
+ var options = chart.options,
878
+ a11yOptions = options.accessibility;
879
+
880
+ if (!a11yOptions.enabled) {
881
+ return;
882
+ }
883
+
884
+ var titleElement = doc.createElementNS('http://www.w3.org/2000/svg', 'title'),
885
+ exportGroupElement = doc.createElementNS('http://www.w3.org/2000/svg', 'g'),
886
+ descElement = chart.container.getElementsByTagName('desc')[0],
887
+ textElements = chart.container.getElementsByTagName('text'),
888
+ titleId = 'highcharts-title-' + chart.index,
889
+ tableId = 'highcharts-data-table-' + chart.index,
890
+ chartTitle = options.title.text || 'Chart',
891
+ oldColumnHeaderFormatter = options.exporting && options.exporting.csv && options.exporting.csv.columnHeaderFormatter,
892
+ topLevelColumns = [];
893
+
894
+ // Add SVG title/desc tags
895
+ titleElement.textContent = chartTitle;
896
+ titleElement.id = titleId;
897
+ descElement.parentNode.insertBefore(titleElement, descElement);
898
+ chart.renderTo.setAttribute('role', 'region');
899
+ chart.renderTo.setAttribute('aria-label', chartTitle + '. Use up and down arrows to navigate.');
900
+
901
+ // Set screen reader properties on export menu
902
+ if (chart.exportSVGElements && chart.exportSVGElements[0] && chart.exportSVGElements[0].element) {
903
+ var oldExportCallback = chart.exportSVGElements[0].element.onclick,
904
+ parent = chart.exportSVGElements[0].element.parentNode;
905
+ chart.exportSVGElements[0].element.onclick = function() {
906
+ oldExportCallback.apply(this, Array.prototype.slice.call(arguments));
907
+ chart.addAccessibleContextMenuAttribs();
908
+ chart.highlightExportItem(0);
909
+ };
910
+ chart.exportSVGElements[0].element.setAttribute('role', 'button');
911
+ chart.exportSVGElements[0].element.setAttribute('aria-label', 'View export menu');
912
+ exportGroupElement.appendChild(chart.exportSVGElements[0].element);
913
+ exportGroupElement.setAttribute('role', 'region');
914
+ exportGroupElement.setAttribute('aria-label', 'Chart export menu');
915
+ parent.appendChild(exportGroupElement);
916
+ }
917
+
918
+ // Hide text elements from screen readers
919
+ each(textElements, function(el) {
920
+ el.setAttribute('aria-hidden', 'true');
921
+ });
922
+
923
+ // Add top-secret screen reader region
924
+ chart.addScreenReaderRegion(tableId);
925
+
926
+ // Enable keyboard navigation
927
+ if (a11yOptions.keyboardNavigation) {
928
+ chart.addKeyboardNavEvents();
929
+ }
930
+
931
+ /* Wrap table functionality from export-csv */
932
+
933
+ // Keep track of columns
934
+ merge(true, options.exporting, {
935
+ csv: {
936
+ columnHeaderFormatter: function(series, key, keyLength) {
937
+ var prevCol = topLevelColumns[topLevelColumns.length - 1];
938
+ if (keyLength > 1) {
939
+ // We need multiple levels of column headers
940
+ // Populate a list of column headers to add in addition to the ones added by export-csv
941
+ if ((prevCol && prevCol.text) !== series.name) {
942
+ topLevelColumns.push({
943
+ text: series.name,
944
+ span: keyLength
945
+ });
946
+ }
947
+ }
948
+ if (oldColumnHeaderFormatter) {
949
+ return oldColumnHeaderFormatter.call(this, series, key, keyLength);
950
+ }
951
+ return keyLength > 1 ? key : series.name;
952
+ }
953
+ }
954
+ });
955
+
956
+ // Add ID and title/caption to table HTML
957
+ H.wrap(chart, 'getTable', function(proceed) {
958
+ return proceed.apply(this, Array.prototype.slice.call(arguments, 1))
959
+ .replace('<table>', '<table id="' + tableId + '" summary="Table representation of chart"><caption>' + chartTitle + '</caption>');
960
+ });
961
+
962
+ // Add accessibility attributes and top level columns
963
+ H.wrap(chart, 'viewData', function(proceed) {
964
+ if (!this.insertedTable) {
965
+ proceed.apply(this, Array.prototype.slice.call(arguments, 1));
966
+
967
+ var table = doc.getElementById(tableId),
968
+ body = table.getElementsByTagName('tbody')[0],
969
+ firstRow = body.firstChild.children,
970
+ columnHeaderRow = '<tr><td></td>',
971
+ cell,
972
+ newCell;
973
+
974
+ // Make table focusable by script
975
+ table.setAttribute('tabindex', '-1');
976
+
977
+ // Create row headers
978
+ each(body.children, function(el) {
979
+ cell = el.firstChild;
980
+ newCell = doc.createElement('th');
981
+ newCell.setAttribute('scope', 'row');
982
+ newCell.innerHTML = cell.innerHTML;
983
+ cell.parentNode.replaceChild(newCell, cell);
984
+ });
985
+
986
+ // Set scope for column headers
987
+ each(firstRow, function(el) {
988
+ if (el.tagName === 'TH') {
989
+ el.setAttribute('scope', 'col');
990
+ }
991
+ });
992
+
993
+ // Add top level columns
994
+ if (topLevelColumns.length) {
995
+ each(topLevelColumns, function(col) {
996
+ columnHeaderRow += '<th scope="col" colspan="' + col.span + '">' + col.text + '</th>';
997
+ });
998
+ body.insertAdjacentHTML('afterbegin', columnHeaderRow);
999
+ }
1000
+ }
1001
+ });
1002
+ });
1003
+
1004
+ }(Highcharts));
1005
+ }));