├── README.md ├── includes └── export-csv.js ├── highcharts-accessible.js └── example.html /README.md: -------------------------------------------------------------------------------- 1 | # highcharts-accessible 2 | This plugin improves accessibility of the graphs rendered by Highcharts. 3 | Full documenation at http://seth-myers.github.io/highcharts-accessible/ 4 | Live example at http://seth-myers.com/highcharts-accessible/ 5 | 6 | ## About 7 | **This only works with bar, column, line and pie charts so far. Will be adding more chart types later.** 8 | 9 | It works by automatically applying three best practices for accessible SVGs. 10 | 11 | 1. Automatically creates and inserts intelligent title and description tags. 12 | 2. Inserts ARIA attributes and language definition in the SVG to further optimize the chart for screen readers 13 | 3. Automatically inserts a link to a downloadable CSV of the data. 14 | 15 | Highchart SVG rendered **without** the plugin: 16 | ![alt text](https://github.com/seth-myers/highcharts-accessible/blob/gh-pages/images/without.png "Highchart SVG rendered without the plugin") 17 | 18 | Highchart SVG rendered **with** the plugin: 19 | ![alt text](https://github.com/seth-myers/highcharts-accessible/blob/gh-pages/images/with.png "https://github.com/seth-myers/highcharts-accessible/blob/gh-pages/images/with.png") 20 | 21 | ## Dependency 22 | Include the [export-csv.js](http://www.highcharts.com/plugin-registry/single/7/Export%20Data "export-csv.js download page") plugin to enable the CSV functionality. 23 | 24 | ## Installation 25 | Import this plugin after `highcharts.js` and `export-csv.js`, like this: 26 | ``` 27 | 28 | 29 | 30 | ``` 31 | ## Usage 32 | All parameters are optional, but `accessibleSettings: {}` must be called in order to run the plugin. 33 | ```javascript 34 | $("#container").highcharts({ 35 | accessibleSettings: { 36 | ``` 37 | 38 | > **autoAccessible: Boolean** 39 | > Allow a summary of the chart to be written automatically. This is the primary purpose of the plugin. If set to false, a user-defined title and description must be provided. Defaults to `true`. 40 | 41 | > **csvLinkText: String** 42 | > Text used in link to download a CSV file. If you have more than one highchart SVG on a page, it is important to add text that helps users associate the link with the chart. Defaults to “Download chart data.”. 43 | 44 | > **langAttribute: String** 45 | > Change the language attribute for the SVG. [More information](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xml:lang "Mozilla SVG lang") on lang attribute. Defaults to “en” for English. 46 | 47 | > **timeSeries:Boolean** 48 | > Indicate if the dataset has a time element. This affects how the autoAccessible feature process the text. Defaults to `true`. 49 | 50 | > **userDesc: String** 51 | > Text to be used in the SVG’s description tag. This will override the autoAccessible feature. I recommend using this option and writing your own summary of what the chart shows. It will likely be more readable than what the autoAccessible script can generate. Defaults to `null`. 52 | 53 | > **userTitle: String** 54 | > Text to be used in the SVG’s title tag. If this is tag is not used, access will use the text in highchart’s title.text in the SVG title tag. If that is empty, it defaults to "This is a data visualization. Please see the following description tag for a synopsis of what it shows.". 55 | 56 | > **xUnitPrefix: String** 57 | > Text to prepend to the x value in the description tag. Defaults to empty string. 58 | 59 | > **xUnitSuffix: String** 60 | > Text to append to the x value in the description tag. Defaults to empty string. 61 | 62 | > **yUnitPrefix: String** 63 | > Text to prepend to the y value in the description tag. Defaults to empty string. 64 | 65 | > **yUnitSuffix: String** 66 | > Text to append to the y value in the description tag. Defaults to empty string. 67 | 68 | ```javascript 69 | } 70 | }); 71 | ``` 72 | 73 | *"Inclusive design is good design."* 74 | -------------------------------------------------------------------------------- /includes/export-csv.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A Highcharts plugin for exporting data from a rendered chart as CSV, XLS or HTML table 3 | * 4 | * Author: Torstein Honsi 5 | * Licence: MIT 6 | * Version: 1.4.1 7 | */ 8 | /*global Highcharts, window, document, Blob */ 9 | (function (factory) { 10 | if (typeof module === 'object' && module.exports) { 11 | module.exports = factory; 12 | } else { 13 | factory(Highcharts); 14 | } 15 | })(function (Highcharts) { 16 | 17 | 'use strict'; 18 | 19 | var each = Highcharts.each, 20 | pick = Highcharts.pick, 21 | downloadAttrSupported = document.createElement('a').download !== undefined; 22 | 23 | Highcharts.setOptions({ 24 | lang: { 25 | downloadCSV: 'Download CSV', 26 | downloadXLS: 'Download XLS', 27 | viewData: 'View data table' 28 | } 29 | }); 30 | 31 | 32 | /** 33 | * Get the data rows as a two dimensional array 34 | */ 35 | Highcharts.Chart.prototype.getDataRows = function () { 36 | var options = (this.options.exporting || {}).csv || {}, 37 | xAxis = this.xAxis[0], 38 | rows = {}, 39 | rowArr = [], 40 | dataRows, 41 | names = [], 42 | i, 43 | x, 44 | xTitle = xAxis.options.title && xAxis.options.title.text, 45 | 46 | // Options 47 | dateFormat = options.dateFormat || '%Y-%m-%d %H:%M:%S', 48 | columnHeaderFormatter = options.columnHeaderFormatter || function (series, key, keyLength) { 49 | return series.name + (keyLength > 1 ? ' ('+ key + ')' : ''); 50 | }; 51 | 52 | // Loop the series and index values 53 | i = 0; 54 | each(this.series, function (series) { 55 | var keys = series.options.keys, 56 | pointArrayMap = keys || series.pointArrayMap || ['y'], 57 | valueCount = pointArrayMap.length, 58 | requireSorting = series.requireSorting, 59 | categoryMap = {}, 60 | j; 61 | 62 | // Map the categories for value axes 63 | each(pointArrayMap, function (prop) { 64 | categoryMap[prop] = (series[prop + 'Axis'] && series[prop + 'Axis'].categories) || []; 65 | }); 66 | 67 | if (series.options.includeInCSVExport !== false && series.visible !== false) { // #55 68 | j = 0; 69 | while (j < valueCount) { 70 | names.push(columnHeaderFormatter(series, pointArrayMap[j], pointArrayMap.length)); 71 | j = j + 1; 72 | } 73 | 74 | each(series.points, function (point, pIdx) { 75 | var key = requireSorting ? point.x : pIdx, 76 | prop, 77 | val; 78 | 79 | j = 0; 80 | 81 | if (!rows[key]) { 82 | rows[key] = []; 83 | } 84 | rows[key].x = point.x; 85 | 86 | // Pies, funnels etc. use point name in X row 87 | if (!series.xAxis) { 88 | rows[key].name = point.name; 89 | } 90 | 91 | while (j < valueCount) { 92 | prop = pointArrayMap[j]; // y, z etc 93 | val = point[prop]; 94 | rows[key][i + j] = pick(categoryMap[prop][val], val); // Pick a Y axis category if present 95 | j = j + 1; 96 | } 97 | 98 | }); 99 | i = i + j; 100 | } 101 | }); 102 | 103 | // Make a sortable array 104 | for (x in rows) { 105 | if (rows.hasOwnProperty(x)) { 106 | rowArr.push(rows[x]); 107 | } 108 | } 109 | // Sort it by X values 110 | rowArr.sort(function (a, b) { 111 | return a.x - b.x; 112 | }); 113 | 114 | // Add header row 115 | if (!xTitle) { 116 | xTitle = xAxis.isDatetimeAxis ? 'DateTime' : 'Category'; 117 | } 118 | dataRows = [[xTitle].concat(names)]; 119 | 120 | // Transform the rows to CSV 121 | each(rowArr, function (row) { 122 | 123 | var category = row.name; 124 | if (!category) { 125 | if (xAxis.isDatetimeAxis) { 126 | category = Highcharts.dateFormat(dateFormat, row.x); 127 | } else if (xAxis.categories) { 128 | category = pick(xAxis.names[row.x], xAxis.categories[row.x], row.x) 129 | } else { 130 | category = row.x; 131 | } 132 | } 133 | 134 | // Add the X/date/category 135 | row.unshift(category); 136 | dataRows.push(row); 137 | }); 138 | 139 | return dataRows; 140 | }; 141 | 142 | /** 143 | * Get a CSV string 144 | */ 145 | Highcharts.Chart.prototype.getCSV = function (useLocalDecimalPoint) { 146 | var csv = '', 147 | rows = this.getDataRows(), 148 | options = (this.options.exporting || {}).csv || {}, 149 | itemDelimiter = options.itemDelimiter || ',', // use ';' for direct import to Excel 150 | lineDelimiter = options.lineDelimiter || '\n'; // '\n' isn't working with the js csv data extraction 151 | 152 | // Transform the rows to CSV 153 | each(rows, function (row, i) { 154 | var val = '', 155 | j = row.length, 156 | n = useLocalDecimalPoint ? (1.1).toLocaleString()[1] : '.'; 157 | while (j--) { 158 | val = row[j]; 159 | if (typeof val === "string") { 160 | val = '"' + val + '"'; 161 | } 162 | if (typeof val === 'number') { 163 | if (n === ',') { 164 | val = val.toString().replace(".", ","); 165 | } 166 | } 167 | row[j] = val; 168 | } 169 | // Add the values 170 | csv += row.join(itemDelimiter); 171 | 172 | // Add the line delimiter 173 | if (i < rows.length - 1) { 174 | csv += lineDelimiter; 175 | } 176 | }); 177 | return csv; 178 | }; 179 | 180 | /** 181 | * Build a HTML table with the data 182 | */ 183 | Highcharts.Chart.prototype.getTable = function (useLocalDecimalPoint) { 184 | var html = '', 185 | rows = this.getDataRows(); 186 | 187 | // Transform the rows to HTML 188 | each(rows, function (row, i) { 189 | var tag = i ? 'td' : 'th', 190 | val, 191 | j, 192 | n = useLocalDecimalPoint ? (1.1).toLocaleString()[1] : '.'; 193 | 194 | html += ''; 195 | for (j = 0; j < row.length; j = j + 1) { 196 | val = row[j]; 197 | // Add the cell 198 | if (typeof val === 'number') { 199 | val = val.toString(); 200 | if (n === ',') { 201 | val = val.replace('.', n); 202 | } 203 | html += '<' + tag + ' class="number">' + val + ''; 204 | 205 | } else { 206 | html += '<' + tag + '>' + (val === undefined ? '' : val) + ''; 207 | } 208 | } 209 | 210 | html += ''; 211 | }); 212 | html += '
'; 213 | return html; 214 | }; 215 | 216 | function getContent(chart, href, extension, content, MIME) { 217 | var a, 218 | blobObject, 219 | name, 220 | options = (chart.options.exporting || {}).csv || {}, 221 | url = options.url || 'http://www.highcharts.com/studies/csv-export/download.php'; 222 | 223 | if (chart.options.exporting.filename) { 224 | name = chart.options.exporting.filename; 225 | } else if (chart.title) { 226 | name = chart.title.textStr.replace(/ /g, '-').toLowerCase(); 227 | } else { 228 | name = 'chart'; 229 | } 230 | 231 | // MS specific. Check this first because of bug with Edge (#76) 232 | if (window.Blob && window.navigator.msSaveOrOpenBlob) { 233 | // Falls to msSaveOrOpenBlob if download attribute is not supported 234 | blobObject = new Blob([content]); 235 | window.navigator.msSaveOrOpenBlob(blobObject, name + '.' + extension); 236 | 237 | // Download attribute supported 238 | } else if (downloadAttrSupported) { 239 | a = document.createElement('a'); 240 | a.href = href; 241 | a.target = '_blank'; 242 | a.download = name + '.' + extension; 243 | document.body.appendChild(a); 244 | a.click(); 245 | a.remove(); 246 | 247 | } else { 248 | // Fall back to server side handling 249 | Highcharts.post(url, { 250 | data: content, 251 | type: MIME, 252 | extension: extension 253 | }); 254 | } 255 | } 256 | 257 | /** 258 | * Call this on click of 'Download CSV' button 259 | */ 260 | Highcharts.Chart.prototype.downloadCSV = function () { 261 | var csv = this.getCSV(true); 262 | getContent( 263 | this, 264 | 'data:text/csv,' + csv.replace(/\n/g, '%0A'), 265 | 'csv', 266 | csv, 267 | 'text/csv' 268 | ); 269 | }; 270 | 271 | /** 272 | * Call this on click of 'Download XLS' button 273 | */ 274 | Highcharts.Chart.prototype.downloadXLS = function () { 275 | var uri = 'data:application/vnd.ms-excel;base64,', 276 | template = '' + 277 | '' + 280 | '' + 281 | '' + 282 | '' + 283 | this.getTable(true) + 284 | '', 285 | base64 = function (s) { 286 | return window.btoa(unescape(encodeURIComponent(s))); // #50 287 | }; 288 | getContent( 289 | this, 290 | uri + base64(template), 291 | 'xls', 292 | template, 293 | 'application/vnd.ms-excel' 294 | ); 295 | }; 296 | 297 | /** 298 | * View the data in a table below the chart 299 | */ 300 | Highcharts.Chart.prototype.viewData = function () { 301 | if (!this.insertedTable) { 302 | var div = document.createElement('div'); 303 | div.className = 'highcharts-data-table'; 304 | // Insert after the chart container 305 | this.renderTo.parentNode.insertBefore(div, this.renderTo.nextSibling); 306 | div.innerHTML = this.getTable(); 307 | this.insertedTable = true; 308 | } 309 | }; 310 | 311 | 312 | // Add "Download CSV" to the exporting menu. Use download attribute if supported, else 313 | // run a simple PHP script that returns a file. The source code for the PHP script can be viewed at 314 | // https://raw.github.com/highslide-software/highcharts.com/master/studies/csv-export/csv.php 315 | if (Highcharts.getOptions().exporting) { 316 | Highcharts.getOptions().exporting.buttons.contextButton.menuItems.push({ 317 | textKey: 'downloadCSV', 318 | onclick: function () { this.downloadCSV(); } 319 | }, { 320 | textKey: 'downloadXLS', 321 | onclick: function () { this.downloadXLS(); } 322 | }, { 323 | textKey: 'viewData', 324 | onclick: function () { this.viewData(); } 325 | }); 326 | } 327 | 328 | }); 329 | -------------------------------------------------------------------------------- /highcharts-accessible.js: -------------------------------------------------------------------------------- 1 | // Accessible SVG resources 2 | // http://www.w3.org/TR/SVG/access.html#SVGAccessibilityGuidelines 3 | // http://www.idpf.org/accessibility/guidelines/ 4 | 5 | // TODO 6 | // add ONKEYPRESS handler if ONCLICK hanlder was added by highcharts (Not a normal function of highcharts, but it can be done.) 7 | // add autoAccessible functinality for remaining charts 8 | (function(H) { 9 | 10 | H.wrap(H.Chart.prototype, 'init', function(proceed) { 11 | 12 | // Do not run accesssibility if it wasn't setup by user 13 | if (!arguments[1].accessibleSettings) { 14 | proceed.apply(this, Array.prototype.slice.call(arguments, 1)); 15 | return; 16 | } 17 | 18 | // set options 19 | var accOptions = { 20 | approvedTypes: ['bar', 'column', 'line', 'pie'], 21 | autoAccessible: (arguments[1].accessibleSettings.autoAccessible === undefined) ? true : arguments[1].accessibleSettings.autoAccessible, // default to auto-accessible feature on 22 | chartType: findChartTypes(arguments[1]), 23 | csvLinkText: arguments[1].accessibleSettings.csvLinkText || "Download chart data.", 24 | langAttribute: arguments[1].accessibleSettings.langAttribute || "en", // default to english, 25 | timeSeries: (arguments[1].accessibleSettings.timeSeries === undefined) ? true : arguments[1].accessibleSettings.timeSeries, // default to time series 26 | userDesc: arguments[1].accessibleSettings.desc || null, 27 | userTitle: arguments[1].accessibleSettings.title || null, 28 | xUnitPrefix: arguments[1].accessibleSettings.xUnitPrefix || "", 29 | xUnitSuffix: arguments[1].accessibleSettings.xUnitSuffix || "", 30 | yUnitPrefix: arguments[1].accessibleSettings.yUnitPrefix || "", 31 | yUnitSuffix: arguments[1].accessibleSettings.yUnitSuffix || "" 32 | }; 33 | 34 | 35 | 36 | // Ensure title and desc text are entered if the autoAccessible feature is off. 37 | if (!accOptions.autoAccessible && !accOptions.userTitle || !accOptions.autoAccessible && !accOptions.userDesc) { 38 | throw "highcharts-accessible plugin error: A description and title must be provided if the autoAccessible feature is set to false."; 39 | } 40 | 41 | // Let highcharts make the chart. 42 | proceed.apply(this, Array.prototype.slice.call(arguments, 1)); 43 | 44 | // Add manually-entered title and desc tags if autoAccessible feature is off. 45 | if (!accOptions.autoAccessible) { 46 | setTags(accOptions.userTitle, accOptions.userDesc, this, accOptions); 47 | return; 48 | } 49 | 50 | var titleToPost = processTitle(accOptions, this); 51 | var descToPost = processDesc(accOptions, this); 52 | 53 | // add the new tags and ARIA attributes 54 | setTags(titleToPost, descToPost, this, accOptions); 55 | 56 | // Add CSV download, if dependency not available suggest user adds export-csv.js to automatically allow for CSV download 57 | if (typeof this.getCSV !== 'function') { 58 | console.warn('highcharts-accessible plugin warning: please load the export-csv.js plugin to enable CSV download functionality: http://www.highcharts.com/plugin-registry/single/7/Export%20Data '); 59 | } else { 60 | csvDownload(this.getCSV(), this.renderTo.id, accOptions); 61 | }; 62 | 63 | }); 64 | 65 | // return string for the description tag 66 | // in highcharts, data is generally y-vals and dates/labels are x-vals 67 | function processDesc(o, d) { 68 | 69 | // return user-supplied desc to access if available 70 | if (o.userDesc) { 71 | return o.userDesc; 72 | } 73 | 74 | var theReturn = ""; 75 | var series = d.series; 76 | 77 | // number of data points combined. 78 | // This is used to keep the descriptions from getting too long. 79 | var combinedSeriesLen = series[0].data.length * series.length; 80 | 81 | for (var i = 0; i < series.length; i++) { 82 | var s = series[i]; 83 | var seriesType = o.chartType[i]; 84 | var xVals = s.processedXData; 85 | var xValsLen = xVals.length; 86 | var xLabels = get(s, 'xAxis.categories') 87 | var yVals = s.processedYData; 88 | var yValsLen = yVals.length; 89 | var yValsHighLow = getHighLow(yVals); // use this to build the bar time series chart, say which date is highest/lowest for each data series 90 | var sName = s.name; 91 | 92 | // Warn if this chart type isn't currently supported. 93 | if (o.approvedTypes.indexOf(seriesType) < 0) { 94 | console.log("highcharts-accessible plugin error: This chart type is still in development. Access can only process " + o.approvedTypes.toString() + " charts. This data series will not be added to the description."); 95 | continue; 96 | } 97 | 98 | // These build the string to place into the desc tag 99 | // format text for pie chart, describe each series 100 | if (seriesType == 'pie') { 101 | var total = yVals.reduce(function(a, b) { 102 | return a + b; 103 | }); 104 | for (var ii = 0; ii < xValsLen; ii++) { 105 | theReturn += s.data[ii].name + " " + o.yUnitPrefix + yVals[ii] + o.yUnitSuffix + ", " + round((yVals[ii] / total * 100), 1) + "% of the total. "; 106 | } 107 | } 108 | 109 | if (o.timeSeries) { 110 | // format text for time series 111 | if (seriesType == 'line' || seriesType == 'bar' || seriesType == 'column') { 112 | theReturn += sName + " " + getDirection(yVals[0], yVals[yValsLen - 1]) + " to " + o.yUnitPrefix + yVals[yValsLen - 1] + o.yUnitSuffix + " in " + o.xUnitPrefix + xVals[xValsLen - 1] + o.xUnitSuffix + " from " + o.yUnitPrefix + yVals[0] + o.yUnitSuffix + " in " + o.xUnitPrefix + xVals[0] + o.xUnitSuffix + ". " + sName + " peaked at " + o.yUnitPrefix + yValsHighLow.high[0] + o.yUnitSuffix + " in " + o.xUnitPrefix + xVals[yValsHighLow.high[1]] + o.xUnitSuffix + ", and dipped lowest to " + o.yUnitPrefix + yValsHighLow.low[0] + o.yUnitSuffix + " in " + o.xUnitPrefix + xVals[yValsHighLow.low[1]] + ". "; 113 | } 114 | } else { 115 | // format text for nominal series 116 | // list all values if there are less than 15 data points in the whole chart 117 | if (combinedSeriesLen < 15) { 118 | if (seriesType == 'line' || seriesType == 'bar' || seriesType == 'column') { 119 | for (var ii = 0; ii < xValsLen; ii++) { 120 | theReturn += o.xUnitPrefix + yVals[ii] + o.xUnitSuffix + ", " + xLabels[ii] + ", " + sName + ". "; 121 | } 122 | } 123 | // otherwise just say the high, low for each series 124 | } else { 125 | if (seriesType == 'line' || seriesType == 'bar' || seriesType == 'column') { 126 | theReturn += sName + ", highest value of " + o.yUnitPrefix + yValsHighLow.high[0] + o.yUnitSuffix + " for " + xLabels[yValsHighLow.high[1]] + ". " + sName + ", lowest value of " + o.yUnitPrefix + yValsHighLow.low[0] + o.yUnitSuffix + " for " + xLabels[yValsHighLow.low[1]] + ". "; 127 | } 128 | } 129 | } 130 | } 131 | 132 | if (theReturn == "") { 133 | return "This chart format is not yet supported by the Accessible Highcharts plugin. Please see the link below to download the data."; 134 | } else { 135 | return theReturn; 136 | } 137 | } 138 | 139 | function processTitle(o, d) { 140 | 141 | // return user-supplied title to access if available 142 | if (o.userTitle) { 143 | return o.userTitle; 144 | } 145 | 146 | // if not, try to use the title given to highcharts.js 147 | var hgTitle = get(d, 'userOptions.title.text'); 148 | if (hgTitle && hgTitle != "") { 149 | return hgTitle; 150 | } 151 | 152 | // otherwise say its a chart and direct folks to the description tag 153 | return "This is a data visualization. Please see the following description tag for a synopsis of what it shows."; 154 | } 155 | 156 | // Not sure if download attribute or opening new window is better 157 | // Both approaches seem to have the same result 158 | function csvDownload(d, chartID, userOptions) { 159 | 160 | // denote when the CSV file ends per 508-coordinator 161 | d += '\nEnd of File'; 162 | 163 | var theSVG = document.getElementById(chartID); 164 | var newlinkNode = document.createElement("a"); 165 | var newlinkContent = document.createTextNode(userOptions.csvLinkText); 166 | newlinkNode.appendChild(newlinkContent); 167 | newlinkNode.setAttribute("href", "data:text/csv;charset=utf-8," + escape(d)); 168 | newlinkNode.setAttribute("download", "chart-data.csv"); 169 | theSVG.appendChild(newlinkNode); 170 | 171 | // newlinkNode.onclick = function() { 172 | // //window.open( "data:text/csv;charset=utf-8," + escape(d) ); 173 | // }; 174 | } 175 | 176 | // get chart type for each series item 177 | // return array of chart types 178 | function findChartTypes(d) { 179 | 180 | var theReturn = []; 181 | var chartSeries = get(d, 'chart.type'); 182 | 183 | for (var i = 0; i < d.series.length; i++) { 184 | 185 | var seriesString = 'd.series[' + i + '].type'; 186 | 187 | if (get(d, seriesString)) { 188 | theReturn.push(d.series[i].type) 189 | } else if (chartSeries) { 190 | theReturn.push(chartSeries) 191 | } else { 192 | theReturn.push('line'); 193 | } 194 | 195 | } 196 | return theReturn; 197 | } 198 | 199 | // return object of arrays high/low value and index position 200 | // { high: [val, index], low: [val, index] } 201 | function getHighLow(d) { 202 | var theReturn = { 203 | high: [d[0], 0], 204 | low: [d[0], 0] 205 | }; 206 | for (var i = 0; i < d.length; i++) { 207 | 208 | if (d[i] > theReturn.high[0]) { 209 | theReturn.high = [d[i], i]; 210 | } 211 | if (d[i] < theReturn.low[0]) { 212 | theReturn.low = [d[i], i]; 213 | } 214 | } 215 | return theReturn; 216 | } 217 | 218 | function getDirection(s, e) { 219 | var equal; 220 | var text; 221 | if (s == e) { 222 | return 'did not change'; 223 | } else { 224 | return s < e ? 'increased' : 'decreased'; 225 | } 226 | } 227 | 228 | // http://stackoverflow.com/questions/23808928/javascript-elegant-way-to-check-nested-object-properties-for-null-undefined 229 | function get(obj, key) { 230 | return key.split(".").reduce(function(o, x) { 231 | return (typeof o == "undefined" || o === null) ? o : o[x]; 232 | }, obj); 233 | } 234 | 235 | function round(num, places) { 236 | var multiplier = Math.pow(10, places); 237 | return Math.round(num * multiplier) / multiplier; 238 | } 239 | 240 | // add desc and title tags with ARIA attributres, add lang attibute to SVG 241 | function setTags(tTag, dTag, chartData, userOptions) { 242 | 243 | //console.log(userOptions); 244 | 245 | var chartID = chartData.renderTo.id; 246 | var theSVG = document.getElementById(chartID).firstChild.firstChild; 247 | var descNode = theSVG.firstChild; 248 | 249 | // add lang attribute to SVG tag 250 | theSVG.setAttribute("xml:lang", userOptions.langAttribute); 251 | 252 | // add ID to desc node for ARIA 253 | descNode.setAttribute("id", chartID + "-accessible-desc"); 254 | 255 | descText = descNode.firstChild.nodeValue = dTag; 256 | 257 | var newTitleNode = document.createElement("title"); 258 | var newTitleContent = document.createTextNode(tTag); 259 | newTitleNode.appendChild(newTitleContent); 260 | 261 | // add ID to title node for Aria 262 | newTitleNode.setAttribute("id", chartID + "-accessible-title"); 263 | 264 | // insert the titgle tag at the beginning of the svg 265 | theSVG.insertBefore(newTitleNode, descNode); 266 | 267 | // set ARIA attributes on SVG tag 268 | theSVG.setAttribute("aria-labelledby", chartID + "-accessible-title"); 269 | theSVG.setAttribute("aria-describedby", chartID + "-accessible-desc"); 270 | } 271 | 272 | }(Highcharts)); 273 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Accessibility Test for Highcharts Plugin 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 |
21 |
22 | 23 |

Accessibility Test for Highcharts Plugin

24 |

This plugin automatically adds intelligent title and description tags to SVG charts rendered by Highcharts. It also adds ARIA attributes to bolster accessibility, as well as dynamically creates a CSV file for download.

25 | 26 | 27 |
28 |

Line Chart Test

29 |
30 |
31 | 32 |
33 | 34 |
35 |

Bar Chart Test

36 |
37 |
38 | 39 |
40 | 41 |
42 |

Column Chart Test

43 |
44 |
45 | 46 |
47 | 48 |
49 |

Pie Chart Test

50 |
51 |
52 | 53 |
54 | 55 |
56 |

Nominal Series Test

57 |
58 |
59 | 60 | 61 |
62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 436 | 437 | --------------------------------------------------------------------------------