/** * @license Highcharts JS v6.1.0 (2018-04-13) * Accessibility module * * (c) 2010-2017 Highsoft AS * Author: Oystein Moseng * * License: www.highcharts.com/license */ 'use strict'; (function (factory) { if (typeof module === 'object' && module.exports) { module.exports = factory; } else { factory(Highcharts); } }(function (Highcharts) { (function (H) { /** * Accessibility module - internationalization support * * (c) 2010-2018 Highsoft AS * Author: Øystein Moseng * * License: www.highcharts.com/license */ var each = H.each, pick = H.pick; /** * String trim that works for IE6-8 as well. * @param {string} str The input string * @return {string} The trimmed string */ function stringTrim(str) { return str.trim && str.trim() || str.replace(/^\s+|\s+$/g, ''); } /** * i18n utility function. Format a single array or plural statement in a format * string. If the statement is not an array or plural statement, returns the * statement within brackets. Invalid array statements return an empty string. */ function formatExtendedStatement(statement, ctx) { var eachStart = statement.indexOf('#each('), pluralStart = statement.indexOf('#plural('), indexStart = statement.indexOf('['), indexEnd = statement.indexOf(']'), arr, result; // Dealing with an each-function? if (eachStart > -1) { var eachEnd = statement.slice(eachStart).indexOf(')') + eachStart, preEach = statement.substring(0, eachStart), postEach = statement.substring(eachEnd + 1), eachStatement = statement.substring(eachStart + 6, eachEnd), eachArguments = eachStatement.split(','), lenArg = Number(eachArguments[1]), len; result = ''; arr = ctx[eachArguments[0]]; if (arr) { lenArg = isNaN(lenArg) ? arr.length : lenArg; len = lenArg < 0 ? arr.length + lenArg : Math.min(lenArg, arr.length); // Overshoot // Run through the array for the specified length for (var i = 0; i < len; ++i) { result += preEach + arr[i] + postEach; } } return result.length ? result : ''; } // Dealing with a plural-function? if (pluralStart > -1) { var pluralEnd = statement.slice(pluralStart).indexOf(')') + pluralStart, pluralStatement = statement.substring(pluralStart + 8, pluralEnd), pluralArguments = pluralStatement.split(','), num = Number(ctx[pluralArguments[0]]); switch (num) { case 0: result = pick(pluralArguments[4], pluralArguments[1]); break; case 1: result = pick(pluralArguments[2], pluralArguments[1]); break; case 2: result = pick(pluralArguments[3], pluralArguments[1]); break; default: result = pluralArguments[1]; } return result ? stringTrim(result) : ''; } // Array index if (indexStart > -1) { var arrayName = statement.substring(0, indexStart), ix = Number(statement.substring(indexStart + 1, indexEnd)), val; arr = ctx[arrayName]; if (!isNaN(ix) && arr) { if (ix < 0) { val = arr[arr.length + ix]; // Handle negative overshoot if (val === undefined) { val = arr[0]; } } else { val = arr[ix]; // Handle positive overshoot if (val === undefined) { val = arr[arr.length - 1]; } } } return val !== undefined ? val : ''; } // Standard substitution, delegate to H.format or similar return '{' + statement + '}'; } /** * i18n formatting function. Extends H.format() functionality by also handling * arrays and plural conditionals. Arrays can be indexed as follows: * * Format: 'This is the first index: {myArray[0]}. The last: {myArray[-1]}.' * Context: { myArray: [0, 1, 2, 3, 4, 5] } * Result: 'This is the first index: 0. The last: 5.' * * They can also be iterated using the #each() function. This will repeat the * contents of the bracket expression for each element. Example: * * Format: 'List contains: {#each(myArray)cm }' * Context: { myArray: [0, 1, 2] } * Result: 'List contains: 0cm 1cm 2cm ' * * The #each() function optionally takes a length parameter. If positive, this * parameter specifies the max number of elements to iterate through. If * negative, the function will subtract the number from the length of the array. * Use this to stop iterating before the array ends. Example: * * Format: 'List contains: {#each(myArray, -1) }and {myArray[-1]}.' * Context: { myArray: [0, 1, 2, 3] } * Result: 'List contains: 0, 1, 2, and 3.' * * Use the #plural() function to pick a string depending on whether or not a * context object is 1. Arguments are #plural(obj, plural, singular). Example: * * Format: 'Has {numPoints} {#plural(numPoints, points, point}.' * Context: { numPoints: 5 } * Result: 'Has 5 points.' * * Optionally there are additional parameters for dual and none: * #plural(obj,plural,singular,dual,none) * Example: * * Format: 'Has {#plural(numPoints, many points, one point, two points, none}.' * Context: { numPoints: 2 } * Result: 'Has two points.' * * The dual or none parameters will take precedence if they are supplied. * * @param {string} formatString The string to format. * @param {object} context Context to apply to the format string. * @param {Time} time A `Time` instance for date formatting, passed on to * H.format(). * @return {string} The formatted string. */ H.i18nFormat = function (formatString, context, time) { var getFirstBracketStatement = function (sourceStr, offset) { var str = sourceStr.slice(offset || 0), startBracket = str.indexOf('{'), endBracket = str.indexOf('}'); if (startBracket > -1 && endBracket > startBracket) { return { statement: str.substring(startBracket + 1, endBracket), begin: offset + startBracket + 1, end: offset + endBracket }; } }, tokens = [], bracketRes, constRes, cursor = 0; // Tokenize format string into bracket statements and constants do { bracketRes = getFirstBracketStatement(formatString, cursor); constRes = formatString.substring( cursor, bracketRes && bracketRes.begin - 1 ); // If we have constant content before this bracket statement, add it if (constRes.length) { tokens.push({ value: constRes, type: 'constant' }); } // Add the bracket statement if (bracketRes) { tokens.push({ value: bracketRes.statement, type: 'statement' }); } cursor = bracketRes && bracketRes.end + 1; } while (bracketRes); // Perform the formatting. The formatArrayStatement function returns the // statement in brackets if it is not an array statement, which means it // gets picked up by H.format below. each(tokens, function (token) { if (token.type === 'statement') { token.value = formatExtendedStatement(token.value, context); } }); // Join string back together and pass to H.format to pick up non-array // statements. return H.format(H.reduce(tokens, function (acc, cur) { return acc + cur.value; }, ''), context, time); }; /** * Apply context to a format string from lang options of the chart. * @param {string} langKey Key (using dot notation) into lang option structure * @param {object} context Context to apply to the format string * @return {string} The formatted string */ H.Chart.prototype.langFormat = function (langKey, context, time) { var keys = langKey.split('.'), formatString = this.options.lang, i = 0; for (; i < keys.length; ++i) { formatString = formatString && formatString[keys[i]]; } return typeof formatString === 'string' && H.i18nFormat( formatString, context, time ); }; H.setOptions({ lang: { /** * Configure the accessibility strings in the chart. Requires the * [accessibility module](//code.highcharts.com/modules/accessibility. * js) to be loaded. For a description of the module and information * on its features, see [Highcharts Accessibility](http://www.highcharts. * com/docs/chart-concepts/accessibility). * * For more dynamic control over the accessibility functionality, see * [accessibility.pointDescriptionFormatter]( * accessibility.pointDescriptionFormatter), * [accessibility.seriesDescriptionFormatter]( * accessibility.seriesDescriptionFormatter), and * [accessibility.screenReaderSectionFormatter]( * accessibility.screenReaderSectionFormatter). * * @since 6.0.6 * @type {Object} * @optionparent lang.accessibility */ accessibility: { /* eslint-disable max-len */ screenReaderRegionLabel: 'Chart screen reader information.', navigationHint: 'Use regions/landmarks to skip ahead to chart {#plural(numSeries, and navigate between data series,)}', defaultChartTitle: 'Chart', longDescriptionHeading: 'Long description.', noDescription: 'No description available.', structureHeading: 'Structure.', viewAsDataTable: 'View as data table.', chartHeading: 'Chart graphic.', chartContainerLabel: 'Interactive chart. {title}. Use up and down arrows to navigate with most screen readers.', rangeSelectorMinInput: 'Select start date.', rangeSelectorMaxInput: 'Select end date.', tableSummary: 'Table representation of chart.', mapZoomIn: 'Zoom chart', mapZoomOut: 'Zoom out chart', rangeSelectorButton: 'Select range {buttonText}', legendItem: 'Toggle visibility of series {itemName}', /** * Title element text for the chart SVG element. Leave this * empty to disable adding the title element. Browsers will display * this content when hovering over elements in the chart. Assistive * technology may use this element to label the chart. * * @since 6.0.8 */ svgContainerTitle: '{chartTitle}', /** * Descriptions of lesser known series types. The relevant * description is added to the screen reader information region * when these series types are used. * * @since 6.0.6 * @type {Object} * @optionparent lang.accessibility.seriesTypeDescriptions */ seriesTypeDescriptions: { boxplot: 'Box plot charts are typically used to display ' + 'groups of statistical data. Each data point in the ' + 'chart can have up to 5 values: minimum, lower quartile, ' + 'median, upper quartile, and maximum.', arearange: 'Arearange charts are line charts displaying a ' + 'range between a lower and higher value for each point.', areasplinerange: 'These charts are line charts displaying a ' + 'range between a lower and higher value for each point.', bubble: 'Bubble charts are scatter charts where each data ' + 'point also has a size value.', columnrange: 'Columnrange charts are column charts ' + 'displaying a range between a lower and higher value for ' + 'each point.', errorbar: 'Errorbar series are used to display the ' + 'variability of the data.', funnel: 'Funnel charts are used to display reduction of data ' + 'in stages.', pyramid: 'Pyramid charts consist of a single pyramid with ' + 'item heights corresponding to each point value.', waterfall: 'A waterfall chart is a column chart where each ' + 'column contributes towards a total end value.' }, /** * Chart type description strings. This is added to the chart * information region. * * If there is only a single series type used in the chart, we use * the format string for the series type, or default if missing. * There is one format string for cases where there is only a single * series in the chart, and one for multiple series of the same * type. * * @since 6.0.6 * @type {Object} * @optionparent lang.accessibility.chartTypes */ chartTypes: { emptyChart: 'Empty chart', mapTypeDescription: 'Map of {mapTitle} with {numSeries} data series.', unknownMap: 'Map of unspecified region with {numSeries} data series.', combinationChart: 'Combination chart with {numSeries} data series.', defaultSingle: 'Chart with {numPoints} data {#plural(numPoints, points, point)}.', defaultMultiple: 'Chart with {numSeries} data series.', splineSingle: 'Line chart with {numPoints} data {#plural(numPoints, points, point)}.', splineMultiple: 'Line chart with {numSeries} lines.', lineSingle: 'Line chart with {numPoints} data {#plural(numPoints, points, point)}.', lineMultiple: 'Line chart with {numSeries} lines.', columnSingle: 'Bar chart with {numPoints} {#plural(numPoints, bars, bar)}.', columnMultiple: 'Bar chart with {numSeries} data series.', barSingle: 'Bar chart with {numPoints} {#plural(numPoints, bars, bar)}.', barMultiple: 'Bar chart with {numSeries} data series.', pieSingle: 'Pie chart with {numPoints} {#plural(numPoints, slices, slice)}.', pieMultiple: 'Pie chart with {numSeries} pies.', scatterSingle: 'Scatter chart with {numPoints} {#plural(numPoints, points, point)}.', scatterMultiple: 'Scatter chart with {numSeries} data series.', boxplotSingle: 'Boxplot with {numPoints} {#plural(numPoints, boxes, box)}.', boxplotMultiple: 'Boxplot with {numSeries} data series.', bubbleSingle: 'Bubble chart with {numPoints} {#plural(numPoints, bubbles, bubble)}.', bubbleMultiple: 'Bubble chart with {numSeries} data series.' }, /** * Axis description format strings. * * @since 6.0.6 * @type {Object} * @optionparent lang.accessibility.axis */ axis: { xAxisDescriptionSingular: 'The chart has 1 X axis displaying {names[0]}.', xAxisDescriptionPlural: 'The chart has {numAxes} X axes displaying {#each(names, -1) }and {names[-1]}', yAxisDescriptionSingular: 'The chart has 1 Y axis displaying {names[0]}.', yAxisDescriptionPlural: 'The chart has {numAxes} Y axes displaying {#each(names, -1) }and {names[-1]}' }, /** * Exporting menu format strings for accessibility module. * * @since 6.0.6 * @type {Object} * @optionparent lang.accessibility.exporting */ exporting: { chartMenuLabel: 'Chart export', menuButtonLabel: 'View export menu', exportRegionLabel: 'Chart export menu' }, /** * Lang configuration for different series types. For more dynamic * control over the series element descriptions, see * [accessibility.seriesDescriptionFormatter]( * accessibility.seriesDescriptionFormatter). * * @since 6.0.6 * @type {Object} * @optionparent lang.accessibility.series */ series: { /** * Lang configuration for the series main summary. Each series * type has two modes: * 1. This series type is the only series type used in the * chart * 2. This is a combination chart with multiple series types * * If a definition does not exist for the specific series type * and mode, the 'default' lang definitions are used. * * @since 6.0.6 * @type {Object} * @optionparent lang.accessibility.series.summary */ summary: { default: '{name}, series {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.', defaultCombination: '{name}, series {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.', line: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.', lineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.', spline: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.', splineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.', column: '{name}, bar series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bars, bar)}.', columnCombination: '{name}, series {ix} of {numSeries}. Bar series with {numPoints} {#plural(numPoints, bars, bar)}.', bar: '{name}, bar series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bars, bar)}.', barCombination: '{name}, series {ix} of {numSeries}. Bar series with {numPoints} {#plural(numPoints, bars, bar)}.', pie: '{name}, pie {ix} of {numSeries} with {numPoints} {#plural(numPoints, slices, slice)}.', pieCombination: '{name}, series {ix} of {numSeries}. Pie with {numPoints} {#plural(numPoints, slices, slice)}.', scatter: '{name}, scatter plot {ix} of {numSeries} with {numPoints} {#plural(numPoints, points, point)}.', scatterCombination: '{name}, series {ix} of {numSeries}, scatter plot with {numPoints} {#plural(numPoints, points, point)}.', boxplot: '{name}, boxplot {ix} of {numSeries} with {numPoints} {#plural(numPoints, boxes, box)}.', boxplotCombination: '{name}, series {ix} of {numSeries}. Boxplot with {numPoints} {#plural(numPoints, boxes, box)}.', bubble: '{name}, bubble series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bubbles, bubble)}.', bubbleCombination: '{name}, series {ix} of {numSeries}. Bubble series with {numPoints} {#plural(numPoints, bubbles, bubble)}.', map: '{name}, map {ix} of {numSeries} with {numPoints} {#plural(numPoints, areas, area)}.', mapCombination: '{name}, series {ix} of {numSeries}. Map with {numPoints} {#plural(numPoints, areas, area)}.', mapline: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.', maplineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.', mapbubble: '{name}, bubble series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bubbles, bubble)}.', mapbubbleCombination: '{name}, series {ix} of {numSeries}. Bubble series with {numPoints} {#plural(numPoints, bubbles, bubble)}.' }, /* eslint-enable max-len */ /** * User supplied description text. This is added after the main * summary if present. * * @type {String} * @since 6.0.6 */ description: '{description}', /** * xAxis description for series if there are multiple xAxes in * the chart. * * @type {String} * @since 6.0.6 */ xAxisDescription: 'X axis, {name}', /** * yAxis description for series if there are multiple yAxes in * the chart. * * @type {String} * @since 6.0.6 */ yAxisDescription: 'Y axis, {name}' } } } }); }(Highcharts)); (function (H) { /** * Accessibility module - Screen Reader support * * (c) 2010-2017 Highsoft AS * Author: Oystein Moseng * * License: www.highcharts.com/license */ var win = H.win, doc = win.document, each = H.each, map = H.map, erase = H.erase, addEvent = H.addEvent, merge = H.merge, // CSS style to hide element from visual users while still exposing it to // screen readers hiddenStyle = { position: 'absolute', left: '-9999px', top: 'auto', width: '1px', height: '1px', overflow: 'hidden' }; // If a point has one of the special keys defined, we expose all keys to the // screen reader. H.Series.prototype.commonKeys = ['name', 'id', 'category', 'x', 'value', 'y']; H.Series.prototype.specialKeys = [ 'z', 'open', 'high', 'q3', 'median', 'q1', 'low', 'close' ]; if (H.seriesTypes.pie) { // A pie is always simple. Don't quote me on that. H.seriesTypes.pie.prototype.specialKeys = []; } /** * HTML encode some characters vulnerable for XSS. * @param {string} html The input string * @return {string} The excaped string */ function htmlencode(html) { return html .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') .replace(/\//g, '/'); } /** * Strip HTML tags away from a string. Used for aria-label attributes, painting * on a canvas will fail if the text contains tags. * @param {String} s The input string * @return {String} The filtered string */ function stripTags(s) { return typeof s === 'string' ? s.replace(/<\/?[^>]+(>|$)/g, '') : s; } /** * Accessibility options */ H.setOptions({ /** * Options for configuring accessibility for the chart. Requires the * [accessibility module](//code.highcharts.com/modules/accessibility. * js) to be loaded. For a description of the module and information * on its features, see [Highcharts Accessibility](http://www.highcharts. * com/docs/chart-concepts/accessibility). * * @since 5.0.0 * @type {Object} * @optionparent accessibility */ accessibility: { /** * Whether or not to add series descriptions to charts with a single * series. * * @type {Boolean} * @default false * @since 5.0.0 * @apioption accessibility.describeSingleSeries */ /** * Function to run upon clicking the "View as Data Table" link in the * screen reader region. * * By default Highcharts will insert and set focus to a data table * representation of the chart. * * @type {Function} * @since 5.0.0 * @apioption accessibility.onTableAnchorClick */ /** * Date format to use for points on datetime axes when describing them * to screen reader users. * * Defaults to the same format as in tooltip. * * For an overview of the replacement codes, see * [dateFormat](#Highcharts.dateFormat). * * @type {String} * @see [pointDateFormatter](#accessibility.pointDateFormatter) * @since 5.0.0 * @apioption accessibility.pointDateFormat */ /** * Formatter function to determine the date/time format used with * points on datetime axes when describing them to screen reader users. * Receives one argument, `point`, referring to the point to describe. * Should return a date format string compatible with * [dateFormat](#Highcharts.dateFormat). * * @type {Function} * @see [pointDateFormat](#accessibility.pointDateFormat) * @since 5.0.0 * @apioption accessibility.pointDateFormatter */ /** * Formatter function to use instead of the default for point * descriptions. * Receives one argument, `point`, referring to the point to describe. * Should return a String with the description of the point for a screen * reader user. * * @type {Function} * @see [point.description](#series.line.data.description) * @since 5.0.0 * @apioption accessibility.pointDescriptionFormatter */ /** * Formatter function to use instead of the default for series * descriptions. Receives one argument, `series`, referring to the * series to describe. Should return a String with the description of * the series for a screen reader user. * * @type {Function} * @see [series.description](#plotOptions.series.description) * @since 5.0.0 * @apioption accessibility.seriesDescriptionFormatter */ /** * Enable accessibility features for the chart. * * @type {Boolean} * @default true * @since 5.0.0 */ enabled: true, /** * When a series contains more points than this, we no longer expose * information about individual points to screen readers. * * Set to `false` to disable. * * @type {Number|Boolean} * @since 5.0.0 */ pointDescriptionThreshold: false, // set to false to disable /** * A formatter function to create the HTML contents of the hidden screen * reader information region. Receives one argument, `chart`, referring * to the chart object. Should return a String with the HTML content * of the region. * * The link to view the chart as a data table will be added * automatically after the custom HTML content. * * @type {Function} * @default undefined * @since 5.0.0 */ screenReaderSectionFormatter: function (chart) { var options = chart.options, chartTypes = chart.types || [], formatContext = { chart: chart, numSeries: chart.series && chart.series.length }, // Build axis info - but not for pies and maps. Consider not // adding for certain other types as well (funnel, pyramid?) axesDesc = ( chartTypes.length === 1 && chartTypes[0] === 'pie' || chartTypes[0] === 'map' ) && {} || chart.getAxesDescription(); return '
' + chart.langFormat( 'accessibility.navigationHint', formatContext ) + '

' + ( options.title.text ? htmlencode(options.title.text) : chart.langFormat( 'accessibility.defaultChartTitle', formatContext ) ) + ( options.subtitle && options.subtitle.text ? '. ' + htmlencode(options.subtitle.text) : '' ) + '

' + chart.langFormat( 'accessibility.longDescriptionHeading', formatContext ) + '

' + ( options.chart.description || chart.langFormat( 'accessibility.noDescription', formatContext ) ) + '

' + chart.langFormat( 'accessibility.structureHeading', formatContext ) + '

' + ( options.chart.typeDescription || chart.getTypeDescription() ) + '
' + (axesDesc.xAxis ? ( '
' + axesDesc.xAxis + '
' ) : '') + (axesDesc.yAxis ? ( '
' + axesDesc.yAxis + '
' ) : ''); } } }); /** * A text description of the chart. * * If the Accessibility module is loaded, this is included by default * as a long description of the chart and its contents in the hidden * screen reader information region. * * @type {String} * @see [typeDescription](#chart.typeDescription) * @default undefined * @since 5.0.0 * @apioption chart.description */ /** * A text description of the chart type. * * If the Accessibility module is loaded, this will be included in the * description of the chart in the screen reader information region. * * * Highcharts will by default attempt to guess the chart type, but for * more complex charts it is recommended to specify this property for * clarity. * * @type {String} * @default undefined * @since 5.0.0 * @apioption chart.typeDescription */ // Utility function. Reverses child nodes of a DOM element function reverseChildNodes(node) { var i = node.childNodes.length; while (i--) { node.appendChild(node.childNodes[i]); } } // Whenever drawing series, put info on DOM elements H.addEvent(H.Series, 'afterRender', function () { if (this.chart.options.accessibility.enabled) { this.setA11yDescription(); } }); // Put accessible info on series and points of a series H.Series.prototype.setA11yDescription = function () { var a11yOptions = this.chart.options.accessibility, firstPointEl = ( this.points && this.points.length && this.points[0].graphic && this.points[0].graphic.element ), seriesEl = ( firstPointEl && firstPointEl.parentNode || this.graph && this.graph.element || this.group && this.group.element ); // Could be tracker series depending on series type if (seriesEl) { // For some series types the order of elements do not match the order of // points in series. In that case we have to reverse them in order for // AT to read them out in an understandable order if (seriesEl.lastChild === firstPointEl) { reverseChildNodes(seriesEl); } // Make individual point elements accessible if possible. Note: If // markers are disabled there might not be any elements there to make // accessible. if ( this.points && ( this.points.length < a11yOptions.pointDescriptionThreshold || a11yOptions.pointDescriptionThreshold === false ) ) { each(this.points, function (point) { if (point.graphic) { point.graphic.element.setAttribute('role', 'img'); point.graphic.element.setAttribute('tabindex', '-1'); point.graphic.element.setAttribute('aria-label', stripTags( point.series.options.pointDescriptionFormatter && point.series.options.pointDescriptionFormatter(point) || a11yOptions.pointDescriptionFormatter && a11yOptions.pointDescriptionFormatter(point) || point.buildPointInfoString() )); } }); } // Make series element accessible if (this.chart.series.length > 1 || a11yOptions.describeSingleSeries) { seriesEl.setAttribute( 'role', this.options.exposeElementToA11y ? 'img' : 'region' ); seriesEl.setAttribute('tabindex', '-1'); seriesEl.setAttribute( 'aria-label', stripTags( a11yOptions.seriesDescriptionFormatter && a11yOptions.seriesDescriptionFormatter(this) || this.buildSeriesInfoString() ) ); } } }; // Return string with information about series H.Series.prototype.buildSeriesInfoString = function () { var chart = this.chart, desc = this.description || this.options.description, description = desc && chart.langFormat( 'accessibility.series.description', { description: desc, series: this } ), xAxisInfo = chart.langFormat( 'accessibility.series.xAxisDescription', { name: this.xAxis && this.xAxis.getDescription(), series: this } ), yAxisInfo = chart.langFormat( 'accessibility.series.yAxisDescription', { name: this.yAxis && this.yAxis.getDescription(), series: this } ), summaryContext = { name: this.name || '', ix: this.index + 1, numSeries: chart.series.length, numPoints: this.points.length, series: this }, combination = chart.types.length === 1 ? '' : 'Combination', summary = chart.langFormat( 'accessibility.series.summary.' + this.type + combination, summaryContext ) || chart.langFormat( 'accessibility.series.summary.default' + combination, summaryContext ); return summary + (description ? ' ' + description : '') + ( chart.yAxis.length > 1 && this.yAxis ? ' ' + yAxisInfo : '' ) + ( chart.xAxis.length > 1 && this.xAxis ? ' ' + xAxisInfo : '' ); }; // Return string with information about point H.Point.prototype.buildPointInfoString = function () { var point = this, series = point.series, a11yOptions = series.chart.options.accessibility, infoString = '', dateTimePoint = series.xAxis && series.xAxis.isDatetimeAxis, timeDesc = dateTimePoint && series.chart.time.dateFormat( a11yOptions.pointDateFormatter && a11yOptions.pointDateFormatter(point) || a11yOptions.pointDateFormat || H.Tooltip.prototype.getXDateFormat.call( { getDateFormat: H.Tooltip.prototype.getDateFormat, chart: series.chart }, point, series.chart.options.tooltip, series.xAxis ), point.x ), hasSpecialKey = H.find(series.specialKeys, function (key) { return point[key] !== undefined; }); // If the point has one of the less common properties defined, display all // that are defined if (hasSpecialKey) { if (dateTimePoint) { infoString = timeDesc; } each(series.commonKeys.concat(series.specialKeys), function (key) { if (point[key] !== undefined && !(dateTimePoint && key === 'x')) { infoString += (infoString ? '. ' : '') + key + ', ' + point[key]; } }); } else { // Pick and choose properties for a succint label infoString = ( this.name || timeDesc || this.category || this.id || 'x, ' + this.x ) + ', ' + (this.value !== undefined ? this.value : this.y); } return (this.index + 1) + '. ' + infoString + '.' + (this.description ? ' ' + this.description : ''); }; // Get descriptive label for axis H.Axis.prototype.getDescription = function () { return ( this.userOptions && this.userOptions.description || this.axisTitle && this.axisTitle.textStr || this.options.id || this.categories && 'categories' || this.isDatetimeAxis && 'Time' || 'values' ); }; // Whenever adding or removing series, keep track of types present in chart addEvent(H.Series, 'afterInit', function () { var chart = this.chart; if (chart.options.accessibility.enabled) { chart.types = chart.types || []; // Add type to list if does not exist if (chart.types.indexOf(this.type) < 0) { chart.types.push(this.type); } } }); addEvent(H.Series, 'remove', function () { var chart = this.chart, removedSeries = this, hasType = false; // Check if any of the other series have the same type as this one. // Otherwise remove it from the list. each(chart.series, function (s) { if ( s !== removedSeries && chart.types.indexOf(removedSeries.type) < 0 ) { hasType = true; } }); if (!hasType) { erase(chart.types, removedSeries.type); } }); // Return simplified description of chart type. Some types will not be familiar // to most screen reader users, but in those cases we try to add a description // of the type. H.Chart.prototype.getTypeDescription = function () { var firstType = this.types && this.types[0], firstSeries = this.series && this.series[0] || {}, mapTitle = firstSeries.mapTitle, typeDesc = this.langFormat( 'accessibility.seriesTypeDescriptions.' + firstType, { chart: this } ), formatContext = { numSeries: this.series.length, numPoints: firstSeries.points && firstSeries.points.length, chart: this, mapTitle: mapTitle }, multi = this.series && this.series.length === 1 ? 'Single' : 'Multiple'; if (!firstType) { return this.langFormat( 'accessibility.chartTypes.emptyChart', formatContext ); } else if (firstType === 'map') { return mapTitle ? this.langFormat( 'accessibility.chartTypes.mapTypeDescription', formatContext ) : this.langFormat( 'accessibility.chartTypes.unknownMap', formatContext ); } else if (this.types.length > 1) { return this.langFormat( 'accessibility.chartTypes.combinationChart', formatContext ); } return ( this.langFormat( 'accessibility.chartTypes.' + firstType + multi, formatContext ) || this.langFormat( 'accessibility.chartTypes.default' + multi, formatContext ) ) + (typeDesc ? ' ' + typeDesc : ''); }; // Return object with text description of each of the chart's axes H.Chart.prototype.getAxesDescription = function () { var numXAxes = this.xAxis.length, numYAxes = this.yAxis.length, desc = {}; if (numXAxes) { desc.xAxis = this.langFormat( 'accessibility.axis.xAxisDescription' + ( numXAxes > 1 ? 'Plural' : 'Singular' ), { chart: this, names: map(this.xAxis, function (axis) { return axis.getDescription(); }), numAxes: numXAxes } ); } if (numYAxes) { desc.yAxis = this.langFormat( 'accessibility.axis.yAxisDescription' + ( numYAxes > 1 ? 'Plural' : 'Singular' ), { chart: this, names: map(this.yAxis, function (axis) { return axis.getDescription(); }), numAxes: numYAxes } ); } return desc; }; // Set a11y attribs on exporting menu H.Chart.prototype.addAccessibleContextMenuAttribs = function () { var exportList = this.exportDivElements; if (exportList) { // Set tabindex on the menu items to allow focusing by script // Set role to give screen readers a chance to pick up the contents each(exportList, function (item) { if (item.tagName === 'DIV' && !(item.children && item.children.length)) { item.setAttribute('role', 'menuitem'); item.setAttribute('tabindex', -1); } }); // Set accessibility properties on parent div exportList[0].parentNode.setAttribute('role', 'menu'); exportList[0].parentNode.setAttribute('aria-label', this.langFormat( 'accessibility.exporting.chartMenuLabel', { chart: this } ) ); } }; // Add screen reader region to chart. // tableId is the HTML id of the table to focus when clicking the table anchor // in the screen reader region. H.Chart.prototype.addScreenReaderRegion = function (id, tableId) { var chart = this, hiddenSection = chart.screenReaderRegion = doc.createElement('div'), tableShortcut = doc.createElement('h4'), tableShortcutAnchor = doc.createElement('a'), chartHeading = doc.createElement('h4'); hiddenSection.setAttribute('id', id); hiddenSection.setAttribute('role', 'region'); hiddenSection.setAttribute( 'aria-label', chart.langFormat( 'accessibility.screenReaderRegionLabel', { chart: this } ) ); hiddenSection.innerHTML = chart.options.accessibility .screenReaderSectionFormatter(chart); // Add shortcut to data table if export-data is loaded if (chart.getCSV) { tableShortcutAnchor.innerHTML = chart.langFormat( 'accessibility.viewAsDataTable', { chart: chart } ); tableShortcutAnchor.href = '#' + tableId; // Make this unreachable by user tabbing tableShortcutAnchor.setAttribute('tabindex', '-1'); tableShortcutAnchor.onclick = chart.options.accessibility.onTableAnchorClick || function () { chart.viewData(); doc.getElementById(tableId).focus(); }; tableShortcut.appendChild(tableShortcutAnchor); hiddenSection.appendChild(tableShortcut); } // Note: JAWS seems to refuse to read aria-label on the container, so add an // h4 element as title for the chart. chartHeading.innerHTML = chart.langFormat( 'accessibility.chartHeading', { chart: chart } ); chart.renderTo.insertBefore(chartHeading, chart.renderTo.firstChild); chart.renderTo.insertBefore(hiddenSection, chart.renderTo.firstChild); // Hide the section and the chart heading merge(true, chartHeading.style, hiddenStyle); merge(true, hiddenSection.style, hiddenStyle); }; // Make chart container accessible, and wrap table functionality H.Chart.prototype.callbacks.push(function (chart) { var options = chart.options, a11yOptions = options.accessibility; if (!a11yOptions.enabled) { return; } var titleElement, exportGroupElement = doc.createElementNS( 'http://www.w3.org/2000/svg', 'g' ), descElement = chart.container.getElementsByTagName('desc')[0], textElements = chart.container.getElementsByTagName('text'), titleId = 'highcharts-title-' + chart.index, tableId = 'highcharts-data-table-' + chart.index, hiddenSectionId = 'highcharts-information-region-' + chart.index, chartTitle = options.title.text || chart.langFormat( 'accessibility.defaultChartTitle', { chart: chart } ), svgContainerTitle = stripTags(chart.langFormat( 'accessibility.svgContainerTitle', { chartTitle: chartTitle } )); // Add SVG title tag if it is set if (svgContainerTitle.length) { titleElement = doc.createElementNS( 'http://www.w3.org/2000/svg', 'title' ); titleElement.textContent = svgContainerTitle; titleElement.id = titleId; descElement.parentNode.insertBefore(titleElement, descElement); } chart.renderTo.setAttribute('role', 'region'); chart.renderTo.setAttribute( 'aria-label', chart.langFormat( 'accessibility.chartContainerLabel', { title: stripTags(chartTitle), chart: chart } ) ); // Set screen reader properties on export menu if ( chart.exportSVGElements && chart.exportSVGElements[0] && chart.exportSVGElements[0].element ) { var oldExportCallback = chart.exportSVGElements[0].element.onclick, parent = chart.exportSVGElements[0].element.parentNode; chart.exportSVGElements[0].element.onclick = function () { oldExportCallback.apply( this, Array.prototype.slice.call(arguments) ); chart.addAccessibleContextMenuAttribs(); chart.highlightExportItem(0); }; chart.exportSVGElements[0].element.setAttribute('role', 'button'); chart.exportSVGElements[0].element.setAttribute( 'aria-label', chart.langFormat( 'accessibility.exporting.menuButtonLabel', { chart: chart } ) ); exportGroupElement.appendChild(chart.exportSVGElements[0].element); exportGroupElement.setAttribute('role', 'region'); exportGroupElement.setAttribute('aria-label', chart.langFormat( 'accessibility.exporting.exportRegionLabel', { chart: chart } )); parent.appendChild(exportGroupElement); } // Set screen reader properties on input boxes for range selector. We need // to do this regardless of whether or not these are visible, as they are // by default part of the page's tabindex unless we set them to -1. if (chart.rangeSelector) { each(['minInput', 'maxInput'], function (key, i) { if (chart.rangeSelector[key]) { chart.rangeSelector[key].setAttribute('tabindex', '-1'); chart.rangeSelector[key].setAttribute('role', 'textbox'); chart.rangeSelector[key].setAttribute( 'aria-label', chart.langFormat( 'accessibility.rangeSelector' + (i ? 'MaxInput' : 'MinInput'), { chart: chart } ) ); } }); } // Hide text elements from screen readers each(textElements, function (el) { el.setAttribute('aria-hidden', 'true'); }); // Add top-secret screen reader region chart.addScreenReaderRegion(hiddenSectionId, tableId); // Add ID and summary attr to table HTML H.wrap(chart, 'getTable', function (proceed) { return proceed.apply(this, Array.prototype.slice.call(arguments, 1)) .replace( '', '
' ); }); }); }(Highcharts)); (function (H) { /** * Accessibility module - Keyboard navigation * * (c) 2010-2017 Highsoft AS * Author: Oystein Moseng * * License: www.highcharts.com/license */ var win = H.win, doc = win.document, each = H.each, addEvent = H.addEvent, fireEvent = H.fireEvent, merge = H.merge, pick = H.pick, hasSVGFocusSupport; // Add focus border functionality to SVGElements. // Draws a new rect on top of element around its bounding box. H.extend(H.SVGElement.prototype, { addFocusBorder: function (margin, style) { // Allow updating by just adding new border if (this.focusBorder) { this.removeFocusBorder(); } // Add the border rect var bb = this.getBBox(), pad = pick(margin, 3); this.focusBorder = this.renderer.rect( bb.x - pad, bb.y - pad, bb.width + 2 * pad, bb.height + 2 * pad, style && style.borderRadius ) .addClass('highcharts-focus-border') .attr({ stroke: style && style.stroke, 'stroke-width': style && style.strokeWidth }) .attr({ zIndex: 99 }) .add(this.parentGroup); }, removeFocusBorder: function () { if (this.focusBorder) { this.focusBorder.destroy(); delete this.focusBorder; } } }); // Set for which series types it makes sense to move to the closest point with // up/down arrows, and which series types should just move to next series. H.Series.prototype.keyboardMoveVertical = true; each(['column', 'pie'], function (type) { if (H.seriesTypes[type]) { H.seriesTypes[type].prototype.keyboardMoveVertical = false; } }); /** * Strip HTML tags away from a string. Used for aria-label attributes, painting * on a canvas will fail if the text contains tags. * @param {String} s The input string * @return {String} The filtered string */ function stripTags(s) { return typeof s === 'string' ? s.replace(/<\/?[^>]+(>|$)/g, '') : s; } /** * Set default keyboard navigation options */ H.setOptions({ accessibility: { /** * Options for keyboard navigation. * * @type {Object} * @since 5.0.0 * @apioption accessibility.keyboardNavigation */ keyboardNavigation: { /** * Enable keyboard navigation for the chart. * * @type {Boolean} * @default true * @since 5.0.0 * @apioption accessibility.keyboardNavigation.enabled */ enabled: true, /** * Options for the focus border drawn around elements while * navigating through them. * * @type {Object} * @sample highcharts/accessibility/custom-focus * Custom focus ring * @since 6.0.3 * @apioption accessibility.keyboardNavigation.focusBorder */ focusBorder: { /** * Enable/disable focus border for chart. * * @type {Boolean} * @default true * @since 6.0.3 * @apioption accessibility.keyboardNavigation.focusBorder.enabled */ enabled: true, /** * Hide the browser's default focus indicator. * * @type {Boolean} * @default true * @since 6.0.4 * @apioption accessibility.keyboardNavigation.focusBorder.hideBrowserFocusOutline */ hideBrowserFocusOutline: true, /** * Style options for the focus border drawn around elements * while navigating through them. Note that some browsers in * addition draw their own borders for focused elements. These * automatic borders can not be styled by Highcharts. * * In styled mode, the border is given the * `.highcharts-focus-border` class. * * @type {Object} * @since 6.0.3 * @apioption accessibility.keyboardNavigation.focusBorder.style */ style: { /** * Color of the focus border. * * @type {Color} * @default #000000 * @since 6.0.3 * @apioption accessibility.keyboardNavigation.focusBorder.style.color */ color: '#335cad', /** * Line width of the focus border. * * @type {Number} * @default 2 * @since 6.0.3 * @apioption accessibility.keyboardNavigation.focusBorder.style.lineWidth */ lineWidth: 2, /** * Border radius of the focus border. * * @type {Number} * @default 3 * @since 6.0.3 * @apioption accessibility.keyboardNavigation.focusBorder.style.borderRadius */ borderRadius: 3 }, /** * Focus border margin around the elements. * * @type {Number} * @default 2 * @since 6.0.3 * @apioption accessibility.keyboardNavigation.focusBorder.margin */ margin: 2 }, /** * Set the keyboard navigation mode for the chart. Can be "normal" * or "serialize". In normal mode, left/right arrow keys move * between points in a series, while up/down arrow keys move between * series. Up/down navigation acts intelligently to figure out which * series makes sense to move to from any given point. * * In "serialize" mode, points are instead navigated as a single * list. Left/right behaves as in "normal" mode. Up/down arrow keys * will behave like left/right. This is useful for unifying * navigation behavior with/without screen readers enabled. * * @type {String} * @default normal * @since 6.0.4 * @apioption accessibility.keyboardNavigation.mode */ /** * Skip null points when navigating through points with the * keyboard. * * @type {Boolean} * @default true * @since 5.0.0 * @apioption accessibility.keyboardNavigation.skipNullPoints */ skipNullPoints: true } } }); /** * Keyboard navigation for the legend. Requires the Accessibility module. * @since 5.0.14 * @apioption legend.keyboardNavigation */ /** * Enable/disable keyboard navigation for the legend. Requires the Accessibility * module. * * @type {Boolean} * @see [accessibility.keyboardNavigation]( * #accessibility.keyboardNavigation.enabled) * @default true * @since 5.0.13 * @apioption legend.keyboardNavigation.enabled */ // Abstraction layer for keyboard navigation. Keep a map of keyCodes to // handler functions, and a next/prev move handler for tab order. The // module's keyCode handlers determine when to move to another module. // Validate holds a function to determine if there are prerequisites for // this module to run that are not met. Init holds a function to run once // before any keyCodes are interpreted. Terminate holds a function to run // once before moving to next/prev module. // The chart object keeps track of a list of KeyboardNavigationModules. function KeyboardNavigationModule(chart, options) { this.chart = chart; this.id = options.id; this.keyCodeMap = options.keyCodeMap; this.validate = options.validate; this.init = options.init; this.terminate = options.terminate; } KeyboardNavigationModule.prototype = { // Find handler function(s) for key code in the keyCodeMap and run it. run: function (e) { var navModule = this, keyCode = e.which || e.keyCode, found = false, handled = false; each(this.keyCodeMap, function (codeSet) { if (codeSet[0].indexOf(keyCode) > -1) { found = true; handled = codeSet[1].call(navModule, keyCode, e) === false ? // If explicitly returning false, we haven't handled it false : true; } }); // Default tab handler, move to next/prev module if (!found && keyCode === 9) { handled = this.move(e.shiftKey ? -1 : 1); } return handled; }, // Move to next/prev valid module, or undefined if none, and init // it. Returns true on success and false if there is no valid module // to move to. move: function (direction) { var chart = this.chart; if (this.terminate) { this.terminate(direction); } chart.keyboardNavigationModuleIndex += direction; var newModule = chart.keyboardNavigationModules[ chart.keyboardNavigationModuleIndex ]; // Remove existing focus border if any if (chart.focusElement) { chart.focusElement.removeFocusBorder(); } // Verify new module if (newModule) { if (newModule.validate && !newModule.validate()) { return this.move(direction); // Invalid module, recurse } if (newModule.init) { newModule.init(direction); // Valid module, init it return true; } } // No module chart.keyboardNavigationModuleIndex = 0; // Reset counter // Set focus to chart or exit anchor depending on direction if (direction > 0) { this.chart.exiting = true; this.chart.tabExitAnchor.focus(); } else { this.chart.renderTo.focus(); } return false; } }; // Utility function to attempt to fake a click event on an element function fakeClickEvent(element) { var fakeEvent; if (element && element.onclick && doc.createEvent) { fakeEvent = doc.createEvent('Events'); fakeEvent.initEvent('click', true, false); element.onclick(fakeEvent); } } // Determine if a point should be skipped function isSkipPoint(point) { var a11yOptions = point.series.chart.options.accessibility; return point.isNull && a11yOptions.keyboardNavigation.skipNullPoints || point.series.options.skipKeyboardNavigation || !point.series.visible || point.visible === false || // Skip all points in a series where pointDescriptionThreshold is // reached (a11yOptions.pointDescriptionThreshold && a11yOptions.pointDescriptionThreshold <= point.series.points.length); } // Get the point in a series that is closest (in distance) to a reference point // Optionally supply weight factors for x and y directions function getClosestPoint(point, series, xWeight, yWeight) { var minDistance = Infinity, dPoint, minIx, distance, i = series.points.length; if (point.plotX === undefined || point.plotY === undefined) { return; } while (i--) { dPoint = series.points[i]; if (dPoint.plotX === undefined || dPoint.plotY === undefined) { continue; } distance = (point.plotX - dPoint.plotX) * (point.plotX - dPoint.plotX) * (xWeight || 1) + (point.plotY - dPoint.plotY) * (point.plotY - dPoint.plotY) * (yWeight || 1); if (distance < minDistance) { minDistance = distance; minIx = i; } } return minIx !== undefined && series.points[minIx]; } // Pan along axis in a direction (1 or -1), optionally with a defined // granularity (number of steps it takes to walk across current view) H.Axis.prototype.panStep = function (direction, granularity) { var gran = granularity || 3, extremes = this.getExtremes(), step = (extremes.max - extremes.min) / gran * direction, newMax = extremes.max + step, newMin = extremes.min + step, size = newMax - newMin; if (direction < 0 && newMin < extremes.dataMin) { newMin = extremes.dataMin; newMax = newMin + size; } else if (direction > 0 && newMax > extremes.dataMax) { newMax = extremes.dataMax; newMin = newMax - size; } this.setExtremes(newMin, newMax); }; // Set chart's focus to an SVGElement. Calls focus() on it, and draws the focus // border. If the focusElement argument is supplied, it draws the border around // svgElement and sets the focus to focusElement. H.Chart.prototype.setFocusToElement = function (svgElement, focusElement) { var focusBorderOptions = this.options.accessibility .keyboardNavigation.focusBorder, browserFocusElement = focusElement || svgElement; // Set browser focus if possible if ( browserFocusElement.element && browserFocusElement.element.focus ) { browserFocusElement.element.focus(); // Hide default focus ring if (focusBorderOptions.hideBrowserFocusOutline) { browserFocusElement.css({ outline: 'none' }); } } if (focusBorderOptions.enabled) { // Remove old focus border if (this.focusElement) { this.focusElement.removeFocusBorder(); } // Draw focus border (since some browsers don't do it automatically) svgElement.addFocusBorder(focusBorderOptions.margin, { stroke: focusBorderOptions.style.color, strokeWidth: focusBorderOptions.style.lineWidth, borderRadius: focusBorderOptions.style.borderRadius }); this.focusElement = svgElement; } }; // Highlight a point (show tooltip and display hover state). Returns the // highlighted point. H.Point.prototype.highlight = function () { var chart = this.series.chart; if (!this.isNull) { this.onMouseOver(); // Show the hover marker and tooltip } else { if (chart.tooltip) { chart.tooltip.hide(0); } // Don't call blur on the element, as it messes up the chart div's focus } // We focus only after calling onMouseOver because the state change can // change z-index and mess up the element. if (this.graphic) { chart.setFocusToElement(this.graphic); } chart.highlightedPoint = this; return this; }; // Function to highlight next/previous point in chart // Returns highlighted point on success, false on failure (no adjacent point to // highlight in chosen direction) H.Chart.prototype.highlightAdjacentPoint = function (next) { var chart = this, series = chart.series, curPoint = chart.highlightedPoint, curPointIndex = curPoint && curPoint.index || 0, curPoints = curPoint && curPoint.series.points, lastSeries = chart.series && chart.series[chart.series.length - 1], lastPoint = lastSeries && lastSeries.points && lastSeries.points[lastSeries.points.length - 1], newSeries, newPoint; // If no points, return false if (!series[0] || !series[0].points) { return false; } if (!curPoint) { // No point is highlighted yet. Try first/last point depending on move // direction newPoint = next ? series[0].points[0] : lastPoint; } else { // We have a highlighted point. // Find index of current point in series.points array. Necessary for // dataGrouping (and maybe zoom?) if (curPoints[curPointIndex] !== curPoint) { for (var i = 0; i < curPoints.length; ++i) { if (curPoints[i] === curPoint) { curPointIndex = i; break; } } } // Grab next/prev point & series newSeries = series[curPoint.series.index + (next ? 1 : -1)]; newPoint = curPoints[curPointIndex + (next ? 1 : -1)] || // Done with this series, try next one newSeries && newSeries.points[next ? 0 : newSeries.points.length - 1]; // If there is no adjacent point, we return false if (!newPoint) { return false; } } // Recursively skip null points or points in series that should be skipped if (isSkipPoint(newPoint)) { chart.highlightedPoint = newPoint; return chart.highlightAdjacentPoint(next); } // There is an adjacent point, highlight it return newPoint.highlight(); }; // Highlight first valid point in a series. Returns the point if successfully // highlighted, otherwise false. If there is a highlighted point in the series, // use that as starting point. H.Series.prototype.highlightFirstValidPoint = function () { var curPoint = this.chart.highlightedPoint, start = (curPoint && curPoint.series) === this ? curPoint.index : 0, points = this.points; if (points) { for (var i = start, len = points.length; i < len; ++i) { if (!isSkipPoint(points[i])) { return points[i].highlight(); } } for (var j = start; j >= 0; --j) { if (!isSkipPoint(points[j])) { return points[j].highlight(); } } } return false; }; // Highlight next/previous series in chart. Returns false if no adjacent series // in the direction, otherwise returns new highlighted point. H.Chart.prototype.highlightAdjacentSeries = function (down) { var chart = this, newSeries, newPoint, adjacentNewPoint, curPoint = chart.highlightedPoint, lastSeries = chart.series && chart.series[chart.series.length - 1], lastPoint = lastSeries && lastSeries.points && lastSeries.points[lastSeries.points.length - 1]; // If no point is highlighted, highlight the first/last point if (!chart.highlightedPoint) { newSeries = down ? (chart.series && chart.series[0]) : lastSeries; newPoint = down ? (newSeries && newSeries.points && newSeries.points[0]) : lastPoint; return newPoint ? newPoint.highlight() : false; } newSeries = chart.series[curPoint.series.index + (down ? -1 : 1)]; if (!newSeries) { return false; } // We have a new series in this direction, find the right point // Weigh xDistance as counting much higher than Y distance newPoint = getClosestPoint(curPoint, newSeries, 4); if (!newPoint) { return false; } // New series and point exists, but we might want to skip it if (!newSeries.visible) { // Skip the series newPoint.highlight(); adjacentNewPoint = chart.highlightAdjacentSeries(down); // Try recurse if (!adjacentNewPoint) { // Recurse failed curPoint.highlight(); return false; } // Recurse succeeded return adjacentNewPoint; } // Highlight the new point or any first valid point back or forwards from it newPoint.highlight(); return newPoint.series.highlightFirstValidPoint(); }; // Highlight the closest point vertically H.Chart.prototype.highlightAdjacentPointVertical = function (down) { var curPoint = this.highlightedPoint, minDistance = Infinity, bestPoint; if (curPoint.plotX === undefined || curPoint.plotY === undefined) { return false; } each(this.series, function (series) { each(series.points, function (point) { if (point.plotY === undefined || point.plotX === undefined || point === curPoint) { return; } var yDistance = point.plotY - curPoint.plotY, width = Math.abs(point.plotX - curPoint.plotX), distance = Math.abs(yDistance) * Math.abs(yDistance) + width * width * 4; // Weigh horizontal distance highly // Reverse distance number if axis is reversed if (series.yAxis.reversed) { yDistance *= -1; } if ( yDistance < 0 && down || yDistance > 0 && !down || // Wrong dir distance < 5 || // Points in same spot => infinite loop isSkipPoint(point) ) { return; } if (distance < minDistance) { minDistance = distance; bestPoint = point; } }); }); return bestPoint ? bestPoint.highlight() : false; }; // Show the export menu and focus the first item (if exists) H.Chart.prototype.showExportMenu = function () { if (this.exportSVGElements && this.exportSVGElements[0]) { this.exportSVGElements[0].element.onclick(); this.highlightExportItem(0); } }; // Hide export menu H.Chart.prototype.hideExportMenu = function () { var exportList = this.exportDivElements; if (exportList) { each(exportList, function (el) { fireEvent(el, 'mouseleave'); }); if ( exportList[this.highlightedExportItem] && exportList[this.highlightedExportItem].onmouseout ) { exportList[this.highlightedExportItem].onmouseout(); } this.highlightedExportItem = 0; if (hasSVGFocusSupport) { // Only focus if we can set focus back to the elements after // destroying the menu (#7422) this.renderTo.focus(); } } }; // Highlight export menu item by index H.Chart.prototype.highlightExportItem = function (ix) { var listItem = this.exportDivElements && this.exportDivElements[ix], curHighlighted = this.exportDivElements && this.exportDivElements[this.highlightedExportItem]; if ( listItem && listItem.tagName === 'DIV' && !(listItem.children && listItem.children.length) ) { if (listItem.focus && hasSVGFocusSupport) { // Only focus if we can set focus back to the elements after // destroying the menu (#7422) listItem.focus(); } if (curHighlighted && curHighlighted.onmouseout) { curHighlighted.onmouseout(); } if (listItem.onmouseover) { listItem.onmouseover(); } this.highlightedExportItem = ix; return true; } }; // Try to highlight the last valid export menu item H.Chart.prototype.highlightLastExportItem = function () { var chart = this, i; if (chart.exportDivElements) { i = chart.exportDivElements.length; while (i--) { if (chart.highlightExportItem(i)) { break; } } } }; // Highlight range selector button by index H.Chart.prototype.highlightRangeSelectorButton = function (ix) { var buttons = this.rangeSelector.buttons; // Deselect old if (buttons[this.highlightedRangeSelectorItemIx]) { buttons[this.highlightedRangeSelectorItemIx].setState( this.oldRangeSelectorItemState || 0 ); } // Select new this.highlightedRangeSelectorItemIx = ix; if (buttons[ix]) { this.setFocusToElement(buttons[ix].box, buttons[ix]); this.oldRangeSelectorItemState = buttons[ix].state; buttons[ix].setState(2); return true; } return false; }; // Highlight legend item by index H.Chart.prototype.highlightLegendItem = function (ix) { var items = this.legend.allItems, oldIx = this.highlightedLegendItemIx; if (items[ix]) { if (items[oldIx]) { fireEvent( items[oldIx].legendGroup.element, 'mouseout' ); } // Scroll if we have to if (items[ix].pageIx !== undefined && items[ix].pageIx + 1 !== this.legend.currentPage) { this.legend.scroll(1 + items[ix].pageIx - this.legend.currentPage); } // Focus this.highlightedLegendItemIx = ix; this.setFocusToElement(items[ix].legendItem, items[ix].legendGroup); fireEvent(items[ix].legendGroup.element, 'mouseover'); return true; } return false; }; // Add keyboard navigation handling modules to chart H.Chart.prototype.addKeyboardNavigationModules = function () { var chart = this; function navModuleFactory(id, keyMap, options) { return new KeyboardNavigationModule(chart, merge({ keyCodeMap: keyMap }, { id: id }, options)); } // List of the different keyboard handling modes we use depending on where // we are in the chart. Each mode has a set of handling functions mapped to // key codes. Each mode determines when to move to the next/prev mode. chart.keyboardNavigationModules = [ // Entry point catching the first tab, allowing users to tab into points // more intuitively. navModuleFactory('entry', []), // Points navModuleFactory('points', [ // Left/Right [[37, 39], function (keyCode) { var right = keyCode === 39; if (!chart.highlightAdjacentPoint(right)) { // Failed to highlight next, wrap to last/first return this.init(right ? 1 : -1); } return true; }], // Up/Down [[38, 40], function (keyCode) { var down = keyCode !== 38, navOptions = chart.options.accessibility.keyboardNavigation; if (navOptions.mode && navOptions.mode === 'serialize') { // Act like left/right if (!chart.highlightAdjacentPoint(down)) { return this.init(down ? 1 : -1); } return true; } // Normal mode, move between series var highlightMethod = chart.highlightedPoint && chart.highlightedPoint.series.keyboardMoveVertical ? 'highlightAdjacentPointVertical' : 'highlightAdjacentSeries'; chart[highlightMethod](down); return true; }], // Enter/Spacebar [[13, 32], function () { if (chart.highlightedPoint) { chart.highlightedPoint.firePointEvent('click'); } }] ], { // Always start highlighting from scratch when entering this module init: function (dir) { var numSeries = chart.series.length, i = dir > 0 ? 0 : numSeries, res; if (dir > 0) { delete chart.highlightedPoint; // Find first valid point to highlight while (i < numSeries) { res = chart.series[i].highlightFirstValidPoint(); if (res) { return res; } ++i; } } else { // Find last valid point to highlight while (i--) { chart.highlightedPoint = chart.series[i].points[ chart.series[i].points.length - 1 ]; // Highlight first valid point in the series will also // look backwards. It always starts from currently // highlighted point. res = chart.series[i].highlightFirstValidPoint(); if (res) { return res; } } } }, // If leaving points, don't show tooltip anymore terminate: function () { if (chart.tooltip) { chart.tooltip.hide(0); } delete chart.highlightedPoint; } }), // Exporting navModuleFactory('exporting', [ // Left/Up [[37, 38], function () { var i = chart.highlightedExportItem || 0, reachedEnd = true; // Try to highlight prev item in list. Highlighting e.g. // separators will fail. while (i--) { if (chart.highlightExportItem(i)) { reachedEnd = false; break; } } if (reachedEnd) { chart.highlightLastExportItem(); return true; } }], // Right/Down [[39, 40], function () { var highlightedExportItem = chart.highlightedExportItem || 0, reachedEnd = true; // Try to highlight next item in list. Highlighting e.g. // separators will fail. for ( var i = highlightedExportItem + 1; i < chart.exportDivElements.length; ++i ) { if (chart.highlightExportItem(i)) { reachedEnd = false; break; } } if (reachedEnd) { chart.highlightExportItem(0); return true; } }], // Enter/Spacebar [[13, 32], function () { fakeClickEvent( chart.exportDivElements[chart.highlightedExportItem] ); }] ], { // Only run exporting navigation if exporting support exists and is // enabled on chart validate: function () { return ( chart.exportChart && !( chart.options.exporting && chart.options.exporting.enabled === false ) ); }, // Show export menu init: function (direction) { chart.highlightedPoint = null; chart.showExportMenu(); // If coming back to export menu from other module, try to // highlight last item in menu if (direction < 0) { chart.highlightLastExportItem(); } }, // Hide the menu terminate: function () { chart.hideExportMenu(); } }), // Map zoom navModuleFactory('mapZoom', [ // Up/down/left/right [[38, 40, 37, 39], function (keyCode) { chart[keyCode === 38 || keyCode === 40 ? 'yAxis' : 'xAxis'][0] .panStep(keyCode < 39 ? -1 : 1); }], // Tabs [[9], function (keyCode, e) { var button; // Deselect old chart.mapNavButtons[chart.focusedMapNavButtonIx].setState(0); if ( e.shiftKey && !chart.focusedMapNavButtonIx || !e.shiftKey && chart.focusedMapNavButtonIx ) { // trying to go somewhere we can't? chart.mapZoom(); // Reset zoom // Nowhere to go, go to prev/next module return this.move(e.shiftKey ? -1 : 1); } chart.focusedMapNavButtonIx += e.shiftKey ? -1 : 1; button = chart.mapNavButtons[chart.focusedMapNavButtonIx]; chart.setFocusToElement(button.box, button); button.setState(2); }], // Enter/Spacebar [[13, 32], function () { fakeClickEvent( chart.mapNavButtons[chart.focusedMapNavButtonIx].element ); }] ], { // Only run this module if we have map zoom on the chart validate: function () { return ( chart.mapZoom && chart.mapNavButtons && chart.mapNavButtons.length === 2 ); }, // Make zoom buttons do their magic init: function (direction) { var zoomIn = chart.mapNavButtons[0], zoomOut = chart.mapNavButtons[1], initialButton = direction > 0 ? zoomIn : zoomOut; each(chart.mapNavButtons, function (button, i) { button.element.setAttribute('tabindex', -1); button.element.setAttribute('role', 'button'); button.element.setAttribute( 'aria-label', chart.langFormat( 'accessibility.mapZoom' + (i ? 'Out' : 'In'), { chart: chart } ) ); }); chart.setFocusToElement(initialButton.box, initialButton); initialButton.setState(2); chart.focusedMapNavButtonIx = direction > 0 ? 0 : 1; } }), // Highstock range selector (minus input boxes) navModuleFactory('rangeSelector', [ // Left/Right/Up/Down [[37, 39, 38, 40], function (keyCode) { var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1; // Try to highlight next/prev button if ( !chart.highlightRangeSelectorButton( chart.highlightedRangeSelectorItemIx + direction ) ) { return this.move(direction); } }], // Enter/Spacebar [[13, 32], function () { // Don't allow click if button used to be disabled if (chart.oldRangeSelectorItemState !== 3) { fakeClickEvent( chart.rangeSelector.buttons[ chart.highlightedRangeSelectorItemIx ].element ); } }] ], { // Only run this module if we have range selector validate: function () { return ( chart.rangeSelector && chart.rangeSelector.buttons && chart.rangeSelector.buttons.length ); }, // Make elements focusable and accessible init: function (direction) { each(chart.rangeSelector.buttons, function (button) { button.element.setAttribute('tabindex', '-1'); button.element.setAttribute('role', 'button'); button.element.setAttribute( 'aria-label', chart.langFormat( 'accessibility.rangeSelectorButton', { chart: chart, buttonText: button.text && button.text.textStr } ) ); }); // Focus first/last button chart.highlightRangeSelectorButton( direction > 0 ? 0 : chart.rangeSelector.buttons.length - 1 ); } }), // Highstock range selector, input boxes navModuleFactory('rangeSelectorInput', [ // Tab/Up/Down [[9, 38, 40], function (keyCode, e) { var direction = (keyCode === 9 && e.shiftKey || keyCode === 38) ? -1 : 1, newIx = chart.highlightedInputRangeIx = chart.highlightedInputRangeIx + direction; // Try to highlight next/prev item in list. if (newIx > 1 || newIx < 0) { // Out of range return this.move(direction); } chart.rangeSelector[newIx ? 'maxInput' : 'minInput'].focus(); }] ], { // Only run if we have range selector with input boxes validate: function () { var inputVisible = ( chart.rangeSelector && chart.rangeSelector.inputGroup && chart.rangeSelector.inputGroup.element .getAttribute('visibility') !== 'hidden' ); return ( inputVisible && chart.options.rangeSelector.inputEnabled !== false && chart.rangeSelector.minInput && chart.rangeSelector.maxInput ); }, // Highlight first/last input box init: function (direction) { chart.highlightedInputRangeIx = direction > 0 ? 0 : 1; chart.rangeSelector[ chart.highlightedInputRangeIx ? 'maxInput' : 'minInput' ].focus(); } }), // Legend navigation navModuleFactory('legend', [ // Left/Right/Up/Down [[37, 39, 38, 40], function (keyCode) { var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1; // Try to highlight next/prev legend item if (!chart.highlightLegendItem( chart.highlightedLegendItemIx + direction ) && chart.legend.allItems.length > 1) { // Wrap around if more than 1 item this.init(direction); } }], // Enter/Spacebar [[13, 32], function () { fakeClickEvent( chart.legend.allItems[ chart.highlightedLegendItemIx ].legendItem.element.parentNode ); }] ], { // Only run this module if we have at least one legend - wait for // it - item. Don't run if the legend is populated by a colorAxis. // Don't run if legend navigation is disabled. validate: function () { return chart.legend && chart.legend.allItems && chart.legend.display && !(chart.colorAxis && chart.colorAxis.length) && (chart.options.legend && chart.options.legend.keyboardNavigation && chart.options.legend.keyboardNavigation.enabled) !== false; }, // Make elements focusable and accessible init: function (direction) { each(chart.legend.allItems, function (item) { item.legendGroup.element.setAttribute('tabindex', '-1'); item.legendGroup.element.setAttribute('role', 'button'); item.legendGroup.element.setAttribute( 'aria-label', chart.langFormat( 'accessibility.legendItem', { chart: chart, itemName: stripTags(item.name) } ) ); }); // Focus first/last item chart.highlightLegendItem( direction > 0 ? 0 : chart.legend.allItems.length - 1 ); } }) ]; }; // Add exit anchor to the chart // We use this to move focus out of chart whenever we want, by setting focus // to this div and not preventing the default tab action. // We also use this when users come back into the chart by tabbing back, in // order to navigate from the end of the chart. // Function returns the unbind function for the exit anchor's event handler. H.Chart.prototype.addExitAnchor = function () { var chart = this; chart.tabExitAnchor = doc.createElement('div'); chart.tabExitAnchor.setAttribute('tabindex', '0'); // Hide exit anchor merge(true, chart.tabExitAnchor.style, { position: 'absolute', left: '-9999px', top: 'auto', width: '1px', height: '1px', overflow: 'hidden' }); chart.renderTo.appendChild(chart.tabExitAnchor); return addEvent(chart.tabExitAnchor, 'focus', function (ev) { var e = ev || win.event, curModule; // If focusing and we are exiting, do nothing once. if (!chart.exiting) { // Not exiting, means we are coming in backwards chart.renderTo.focus(); e.preventDefault(); // Move to last valid keyboard nav module // Note the we don't run it, just set the index chart.keyboardNavigationModuleIndex = chart.keyboardNavigationModules.length - 1; curModule = chart.keyboardNavigationModules[ chart.keyboardNavigationModuleIndex ]; // Validate the module if (curModule.validate && !curModule.validate()) { // Invalid. // Move inits next valid module in direction curModule.move(-1); } else { // We have a valid module, init it curModule.init(-1); } } else { // Don't skip the next focus, we only skip once. chart.exiting = false; } } ); }; // Clear the chart and reset the navigation state H.Chart.prototype.resetKeyboardNavigation = function () { var chart = this, curMod = ( chart.keyboardNavigationModules && chart.keyboardNavigationModules[ chart.keyboardNavigationModuleIndex || 0 ] ); if (curMod && curMod.terminate) { curMod.terminate(); } if (chart.focusElement) { chart.focusElement.removeFocusBorder(); } chart.keyboardNavigationModuleIndex = 0; chart.keyboardReset = true; }; /** * On destroy, we need to clean up the focus border and the state */ H.addEvent(H.Series, 'destroy', function () { var chart = this.chart; if (chart.highlightedPoint && chart.highlightedPoint.series === this) { delete chart.highlightedPoint; if (chart.focusElement) { chart.focusElement.removeFocusBorder(); } } }); // Add keyboard navigation events on chart load H.Chart.prototype.callbacks.push(function (chart) { var a11yOptions = chart.options.accessibility; if (a11yOptions.enabled && a11yOptions.keyboardNavigation.enabled) { // Test if we have focus support for SVG elements hasSVGFocusSupport = !!chart.renderTo .getElementsByTagName('g')[0].focus; // Init nav modules. We start at the first module, and as the user // navigates through the chart the index will increase to use different // handler modules. chart.addKeyboardNavigationModules(); chart.keyboardNavigationModuleIndex = 0; // Make chart container reachable by tab if ( chart.container.hasAttribute && !chart.container.hasAttribute('tabIndex') ) { chart.container.setAttribute('tabindex', '0'); } // Add tab exit anchor if (!chart.tabExitAnchor) { chart.unbindExitAnchorFocus = chart.addExitAnchor(); } // Handle keyboard events by routing them to active keyboard nav module chart.unbindKeydownHandler = addEvent(chart.renderTo, 'keydown', function (ev) { var e = ev || win.event, curNavModule = chart.keyboardNavigationModules[ chart.keyboardNavigationModuleIndex ]; chart.keyboardReset = false; // If there is a nav module for the current index, run it. // Otherwise, we are outside of the chart in some direction. if (curNavModule) { if (curNavModule.run(e)) { // Successfully handled this key event, stop default e.preventDefault(); } } }); // Reset chart navigation state if we click outside the chart and it's // not already reset chart.unbindBlurHandler = addEvent(doc, 'mouseup', function () { if ( !chart.keyboardReset && !(chart.pointer && chart.pointer.chartPosition) ) { chart.resetKeyboardNavigation(); } }); // Add cleanup handlers addEvent(chart, 'destroy', function () { chart.resetKeyboardNavigation(); if (chart.unbindExitAnchorFocus && chart.tabExitAnchor) { chart.unbindExitAnchorFocus(); } if (chart.unbindKeydownHandler && chart.renderTo) { chart.unbindKeydownHandler(); } if (chart.unbindBlurHandler) { chart.unbindBlurHandler(); } }); } }); }(Highcharts)); }));