accessibility.src.js 103 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046
  1. /**
  2. * @license Highcharts JS v7.0.2 (2019-01-17)
  3. * Accessibility module
  4. *
  5. * (c) 2010-2019 Highsoft AS
  6. * Author: Oystein Moseng
  7. *
  8. * License: www.highcharts.com/license
  9. */
  10. 'use strict';
  11. (function (factory) {
  12. if (typeof module === 'object' && module.exports) {
  13. factory['default'] = factory;
  14. module.exports = factory;
  15. } else if (typeof define === 'function' && define.amd) {
  16. define(function () {
  17. return factory;
  18. });
  19. } else {
  20. factory(typeof Highcharts !== 'undefined' ? Highcharts : undefined);
  21. }
  22. }(function (Highcharts) {
  23. (function (H) {
  24. /**
  25. * Accessibility module - internationalization support
  26. *
  27. * (c) 2010-2019 Highsoft AS
  28. * Author: Øystein Moseng
  29. *
  30. * License: www.highcharts.com/license
  31. */
  32. var pick = H.pick;
  33. /**
  34. * String trim that works for IE6-8 as well.
  35. *
  36. * @private
  37. * @function stringTrim
  38. *
  39. * @param {string} str
  40. * The input string
  41. *
  42. * @return {string}
  43. * The trimmed string
  44. */
  45. function stringTrim(str) {
  46. return str.trim && str.trim() || str.replace(/^\s+|\s+$/g, '');
  47. }
  48. /**
  49. * i18n utility function. Format a single array or plural statement in a format
  50. * string. If the statement is not an array or plural statement, returns the
  51. * statement within brackets. Invalid array statements return an empty string.
  52. *
  53. * @private
  54. * @function formatExtendedStatement
  55. *
  56. * @param {string} statement
  57. *
  58. * @param {Highcharts.Dictionary<*>} ctx
  59. * Context to apply to the format string.
  60. *
  61. * @return {string}
  62. */
  63. function formatExtendedStatement(statement, ctx) {
  64. var eachStart = statement.indexOf('#each('),
  65. pluralStart = statement.indexOf('#plural('),
  66. indexStart = statement.indexOf('['),
  67. indexEnd = statement.indexOf(']'),
  68. arr,
  69. result;
  70. // Dealing with an each-function?
  71. if (eachStart > -1) {
  72. var eachEnd = statement.slice(eachStart).indexOf(')') + eachStart,
  73. preEach = statement.substring(0, eachStart),
  74. postEach = statement.substring(eachEnd + 1),
  75. eachStatement = statement.substring(eachStart + 6, eachEnd),
  76. eachArguments = eachStatement.split(','),
  77. lenArg = Number(eachArguments[1]),
  78. len;
  79. result = '';
  80. arr = ctx[eachArguments[0]];
  81. if (arr) {
  82. lenArg = isNaN(lenArg) ? arr.length : lenArg;
  83. len = lenArg < 0 ?
  84. arr.length + lenArg :
  85. Math.min(lenArg, arr.length); // Overshoot
  86. // Run through the array for the specified length
  87. for (var i = 0; i < len; ++i) {
  88. result += preEach + arr[i] + postEach;
  89. }
  90. }
  91. return result.length ? result : '';
  92. }
  93. // Dealing with a plural-function?
  94. if (pluralStart > -1) {
  95. var pluralEnd = statement.slice(pluralStart).indexOf(')') + pluralStart,
  96. pluralStatement = statement.substring(pluralStart + 8, pluralEnd),
  97. pluralArguments = pluralStatement.split(','),
  98. num = Number(ctx[pluralArguments[0]]);
  99. switch (num) {
  100. case 0:
  101. result = pick(pluralArguments[4], pluralArguments[1]);
  102. break;
  103. case 1:
  104. result = pick(pluralArguments[2], pluralArguments[1]);
  105. break;
  106. case 2:
  107. result = pick(pluralArguments[3], pluralArguments[1]);
  108. break;
  109. default:
  110. result = pluralArguments[1];
  111. }
  112. return result ? stringTrim(result) : '';
  113. }
  114. // Array index
  115. if (indexStart > -1) {
  116. var arrayName = statement.substring(0, indexStart),
  117. ix = Number(statement.substring(indexStart + 1, indexEnd)),
  118. val;
  119. arr = ctx[arrayName];
  120. if (!isNaN(ix) && arr) {
  121. if (ix < 0) {
  122. val = arr[arr.length + ix];
  123. // Handle negative overshoot
  124. if (val === undefined) {
  125. val = arr[0];
  126. }
  127. } else {
  128. val = arr[ix];
  129. // Handle positive overshoot
  130. if (val === undefined) {
  131. val = arr[arr.length - 1];
  132. }
  133. }
  134. }
  135. return val !== undefined ? val : '';
  136. }
  137. // Standard substitution, delegate to H.format or similar
  138. return '{' + statement + '}';
  139. }
  140. /**
  141. * i18n formatting function. Extends Highcharts.format() functionality by also
  142. * handling arrays and plural conditionals. Arrays can be indexed as follows:
  143. *
  144. * - Format: 'This is the first index: {myArray[0]}. The last: {myArray[-1]}.'
  145. *
  146. * - Context: { myArray: [0, 1, 2, 3, 4, 5] }
  147. *
  148. * - Result: 'This is the first index: 0. The last: 5.'
  149. *
  150. *
  151. * They can also be iterated using the #each() function. This will repeat the
  152. * contents of the bracket expression for each element. Example:
  153. *
  154. * - Format: 'List contains: {#each(myArray)cm }'
  155. *
  156. * - Context: { myArray: [0, 1, 2] }
  157. *
  158. * - Result: 'List contains: 0cm 1cm 2cm '
  159. *
  160. *
  161. * The #each() function optionally takes a length parameter. If positive, this
  162. * parameter specifies the max number of elements to iterate through. If
  163. * negative, the function will subtract the number from the length of the array.
  164. * Use this to stop iterating before the array ends. Example:
  165. *
  166. * - Format: 'List contains: {#each(myArray, -1) }and {myArray[-1]}.'
  167. *
  168. * - Context: { myArray: [0, 1, 2, 3] }
  169. *
  170. * - Result: 'List contains: 0, 1, 2, and 3.'
  171. *
  172. *
  173. * Use the #plural() function to pick a string depending on whether or not a
  174. * context object is 1. Arguments are #plural(obj, plural, singular). Example:
  175. *
  176. * - Format: 'Has {numPoints} {#plural(numPoints, points, point}.'
  177. *
  178. * - Context: { numPoints: 5 }
  179. *
  180. * - Result: 'Has 5 points.'
  181. *
  182. *
  183. * Optionally there are additional parameters for dual and none: #plural(obj,
  184. * plural, singular, dual, none). Example:
  185. *
  186. * - Format: 'Has {#plural(numPoints, many points, one point, two points,
  187. * none}.'
  188. *
  189. * - Context: { numPoints: 2 }
  190. *
  191. * - Result: 'Has two points.'
  192. *
  193. *
  194. * The dual or none parameters will take precedence if they are supplied.
  195. *
  196. *
  197. * @function Highcharts.i18nFormat
  198. * @requires a11y-i18n
  199. *
  200. * @param {string} formatString
  201. * The string to format.
  202. *
  203. * @param {Highcharts.Dictionary<*>} context
  204. * Context to apply to the format string.
  205. *
  206. * @param {Highcharts.Time} time
  207. * A `Time` instance for date formatting, passed on to H.format().
  208. *
  209. * @return {string}
  210. * The formatted string.
  211. */
  212. H.i18nFormat = function (formatString, context, time) {
  213. var getFirstBracketStatement = function (sourceStr, offset) {
  214. var str = sourceStr.slice(offset || 0),
  215. startBracket = str.indexOf('{'),
  216. endBracket = str.indexOf('}');
  217. if (startBracket > -1 && endBracket > startBracket) {
  218. return {
  219. statement: str.substring(startBracket + 1, endBracket),
  220. begin: offset + startBracket + 1,
  221. end: offset + endBracket
  222. };
  223. }
  224. },
  225. tokens = [],
  226. bracketRes,
  227. constRes,
  228. cursor = 0;
  229. // Tokenize format string into bracket statements and constants
  230. do {
  231. bracketRes = getFirstBracketStatement(formatString, cursor);
  232. constRes = formatString.substring(
  233. cursor,
  234. bracketRes && bracketRes.begin - 1
  235. );
  236. // If we have constant content before this bracket statement, add it
  237. if (constRes.length) {
  238. tokens.push({
  239. value: constRes,
  240. type: 'constant'
  241. });
  242. }
  243. // Add the bracket statement
  244. if (bracketRes) {
  245. tokens.push({
  246. value: bracketRes.statement,
  247. type: 'statement'
  248. });
  249. }
  250. cursor = bracketRes && bracketRes.end + 1;
  251. } while (bracketRes);
  252. // Perform the formatting. The formatArrayStatement function returns the
  253. // statement in brackets if it is not an array statement, which means it
  254. // gets picked up by H.format below.
  255. tokens.forEach(function (token) {
  256. if (token.type === 'statement') {
  257. token.value = formatExtendedStatement(token.value, context);
  258. }
  259. });
  260. // Join string back together and pass to H.format to pick up non-array
  261. // statements.
  262. return H.format(tokens.reduce(function (acc, cur) {
  263. return acc + cur.value;
  264. }, ''), context, time);
  265. };
  266. /**
  267. * Apply context to a format string from lang options of the chart.
  268. *
  269. * @function Highcharts.Chart#langFormat
  270. * @requires a11y-i18n
  271. *
  272. * @param {string} langKey
  273. * Key (using dot notation) into lang option structure.
  274. *
  275. * @param {Highcharts.Dictionary<*>} context
  276. * Context to apply to the format string.
  277. *
  278. * @return {string}
  279. * The formatted string.
  280. */
  281. H.Chart.prototype.langFormat = function (langKey, context, time) {
  282. var keys = langKey.split('.'),
  283. formatString = this.options.lang,
  284. i = 0;
  285. for (; i < keys.length; ++i) {
  286. formatString = formatString && formatString[keys[i]];
  287. }
  288. return typeof formatString === 'string' && H.i18nFormat(
  289. formatString, context, time
  290. );
  291. };
  292. H.setOptions({
  293. lang: {
  294. /**
  295. * Configure the accessibility strings in the chart. Requires the
  296. * [accessibility module](//code.highcharts.com/modules/accessibility.js)
  297. * to be loaded. For a description of the module and information on its
  298. * features, see [Highcharts Accessibility](
  299. * http://www.highcharts.com/docs/chart-concepts/accessibility).
  300. *
  301. * For more dynamic control over the accessibility functionality, see
  302. * [accessibility.pointDescriptionFormatter](
  303. * accessibility.pointDescriptionFormatter),
  304. * [accessibility.seriesDescriptionFormatter](
  305. * accessibility.seriesDescriptionFormatter), and
  306. * [accessibility.screenReaderSectionFormatter](
  307. * accessibility.screenReaderSectionFormatter).
  308. *
  309. * @since 6.0.6
  310. * @optionparent lang.accessibility
  311. */
  312. accessibility: {
  313. /* eslint-disable max-len */
  314. screenReaderRegionLabel: 'Chart screen reader information.',
  315. navigationHint: 'Use regions/landmarks to skip ahead to chart {#plural(numSeries, and navigate between data series,)}',
  316. defaultChartTitle: 'Chart',
  317. longDescriptionHeading: 'Long description.',
  318. noDescription: 'No description available.',
  319. structureHeading: 'Structure.',
  320. viewAsDataTable: 'View as data table.',
  321. chartHeading: 'Chart graphic.',
  322. chartContainerLabel: 'Interactive chart. {title}. Use up and down arrows to navigate with most screen readers.',
  323. rangeSelectorMinInput: 'Select start date.',
  324. rangeSelectorMaxInput: 'Select end date.',
  325. tableSummary: 'Table representation of chart.',
  326. mapZoomIn: 'Zoom chart',
  327. mapZoomOut: 'Zoom out chart',
  328. rangeSelectorButton: 'Select range {buttonText}',
  329. legendItem: 'Toggle visibility of series {itemName}',
  330. /* eslint-enable max-len */
  331. /**
  332. * Title element text for the chart SVG element. Leave this
  333. * empty to disable adding the title element. Browsers will display
  334. * this content when hovering over elements in the chart. Assistive
  335. * technology may use this element to label the chart.
  336. *
  337. * @since 6.0.8
  338. */
  339. svgContainerTitle: '{chartTitle}',
  340. /**
  341. * Descriptions of lesser known series types. The relevant
  342. * description is added to the screen reader information region
  343. * when these series types are used.
  344. *
  345. * @since 6.0.6
  346. */
  347. seriesTypeDescriptions: {
  348. boxplot: 'Box plot charts are typically used to display ' +
  349. 'groups of statistical data. Each data point in the ' +
  350. 'chart can have up to 5 values: minimum, lower quartile, ' +
  351. 'median, upper quartile, and maximum.',
  352. arearange: 'Arearange charts are line charts displaying a ' +
  353. 'range between a lower and higher value for each point.',
  354. areasplinerange: 'These charts are line charts displaying a ' +
  355. 'range between a lower and higher value for each point.',
  356. bubble: 'Bubble charts are scatter charts where each data ' +
  357. 'point also has a size value.',
  358. columnrange: 'Columnrange charts are column charts ' +
  359. 'displaying a range between a lower and higher value for ' +
  360. 'each point.',
  361. errorbar: 'Errorbar series are used to display the ' +
  362. 'variability of the data.',
  363. funnel: 'Funnel charts are used to display reduction of data ' +
  364. 'in stages.',
  365. pyramid: 'Pyramid charts consist of a single pyramid with ' +
  366. 'item heights corresponding to each point value.',
  367. waterfall: 'A waterfall chart is a column chart where each ' +
  368. 'column contributes towards a total end value.'
  369. },
  370. /**
  371. * Chart type description strings. This is added to the chart
  372. * information region.
  373. *
  374. * If there is only a single series type used in the chart, we use
  375. * the format string for the series type, or default if missing.
  376. * There is one format string for cases where there is only a single
  377. * series in the chart, and one for multiple series of the same
  378. * type.
  379. *
  380. * @since 6.0.6
  381. */
  382. chartTypes: {
  383. /* eslint-disable max-len */
  384. emptyChart: 'Empty chart',
  385. mapTypeDescription: 'Map of {mapTitle} with {numSeries} data series.',
  386. unknownMap: 'Map of unspecified region with {numSeries} data series.',
  387. combinationChart: 'Combination chart with {numSeries} data series.',
  388. defaultSingle: 'Chart with {numPoints} data {#plural(numPoints, points, point)}.',
  389. defaultMultiple: 'Chart with {numSeries} data series.',
  390. splineSingle: 'Line chart with {numPoints} data {#plural(numPoints, points, point)}.',
  391. splineMultiple: 'Line chart with {numSeries} lines.',
  392. lineSingle: 'Line chart with {numPoints} data {#plural(numPoints, points, point)}.',
  393. lineMultiple: 'Line chart with {numSeries} lines.',
  394. columnSingle: 'Bar chart with {numPoints} {#plural(numPoints, bars, bar)}.',
  395. columnMultiple: 'Bar chart with {numSeries} data series.',
  396. barSingle: 'Bar chart with {numPoints} {#plural(numPoints, bars, bar)}.',
  397. barMultiple: 'Bar chart with {numSeries} data series.',
  398. pieSingle: 'Pie chart with {numPoints} {#plural(numPoints, slices, slice)}.',
  399. pieMultiple: 'Pie chart with {numSeries} pies.',
  400. scatterSingle: 'Scatter chart with {numPoints} {#plural(numPoints, points, point)}.',
  401. scatterMultiple: 'Scatter chart with {numSeries} data series.',
  402. boxplotSingle: 'Boxplot with {numPoints} {#plural(numPoints, boxes, box)}.',
  403. boxplotMultiple: 'Boxplot with {numSeries} data series.',
  404. bubbleSingle: 'Bubble chart with {numPoints} {#plural(numPoints, bubbles, bubble)}.',
  405. bubbleMultiple: 'Bubble chart with {numSeries} data series.'
  406. }, /* eslint-enable max-len */
  407. /**
  408. * Axis description format strings.
  409. *
  410. * @since 6.0.6
  411. */
  412. axis: {
  413. /* eslint-disable max-len */
  414. xAxisDescriptionSingular: 'The chart has 1 X axis displaying {names[0]}.',
  415. xAxisDescriptionPlural: 'The chart has {numAxes} X axes displaying {#names.forEach(-1) }and {names[-1]}',
  416. yAxisDescriptionSingular: 'The chart has 1 Y axis displaying {names[0]}.',
  417. yAxisDescriptionPlural: 'The chart has {numAxes} Y axes displaying {#names.forEach(-1) }and {names[-1]}'
  418. }, /* eslint-enable max-len */
  419. /**
  420. * Exporting menu format strings for accessibility module.
  421. *
  422. * @since 6.0.6
  423. */
  424. exporting: {
  425. chartMenuLabel: 'Chart export',
  426. menuButtonLabel: 'View export menu',
  427. exportRegionLabel: 'Chart export menu'
  428. },
  429. /**
  430. * Lang configuration for different series types. For more dynamic
  431. * control over the series element descriptions, see
  432. * [accessibility.seriesDescriptionFormatter](
  433. * accessibility.seriesDescriptionFormatter).
  434. *
  435. * @since 6.0.6
  436. */
  437. series: {
  438. /**
  439. * Lang configuration for the series main summary. Each series
  440. * type has two modes:
  441. *
  442. * 1. This series type is the only series type used in the
  443. * chart
  444. *
  445. * 2. This is a combination chart with multiple series types
  446. *
  447. * If a definition does not exist for the specific series type
  448. * and mode, the 'default' lang definitions are used.
  449. *
  450. * @since 6.0.6
  451. */
  452. summary: {
  453. /* eslint-disable max-len */
  454. 'default': '{name}, series {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.',
  455. defaultCombination: '{name}, series {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.',
  456. line: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.',
  457. lineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.',
  458. spline: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.',
  459. splineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.',
  460. column: '{name}, bar series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bars, bar)}.',
  461. columnCombination: '{name}, series {ix} of {numSeries}. Bar series with {numPoints} {#plural(numPoints, bars, bar)}.',
  462. bar: '{name}, bar series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bars, bar)}.',
  463. barCombination: '{name}, series {ix} of {numSeries}. Bar series with {numPoints} {#plural(numPoints, bars, bar)}.',
  464. pie: '{name}, pie {ix} of {numSeries} with {numPoints} {#plural(numPoints, slices, slice)}.',
  465. pieCombination: '{name}, series {ix} of {numSeries}. Pie with {numPoints} {#plural(numPoints, slices, slice)}.',
  466. scatter: '{name}, scatter plot {ix} of {numSeries} with {numPoints} {#plural(numPoints, points, point)}.',
  467. scatterCombination: '{name}, series {ix} of {numSeries}, scatter plot with {numPoints} {#plural(numPoints, points, point)}.',
  468. boxplot: '{name}, boxplot {ix} of {numSeries} with {numPoints} {#plural(numPoints, boxes, box)}.',
  469. boxplotCombination: '{name}, series {ix} of {numSeries}. Boxplot with {numPoints} {#plural(numPoints, boxes, box)}.',
  470. bubble: '{name}, bubble series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bubbles, bubble)}.',
  471. bubbleCombination: '{name}, series {ix} of {numSeries}. Bubble series with {numPoints} {#plural(numPoints, bubbles, bubble)}.',
  472. map: '{name}, map {ix} of {numSeries} with {numPoints} {#plural(numPoints, areas, area)}.',
  473. mapCombination: '{name}, series {ix} of {numSeries}. Map with {numPoints} {#plural(numPoints, areas, area)}.',
  474. mapline: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.',
  475. maplineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.',
  476. mapbubble: '{name}, bubble series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bubbles, bubble)}.',
  477. mapbubbleCombination: '{name}, series {ix} of {numSeries}. Bubble series with {numPoints} {#plural(numPoints, bubbles, bubble)}.'
  478. }, /* eslint-enable max-len */
  479. /**
  480. * User supplied description text. This is added after the main
  481. * summary if present.
  482. *
  483. * @since 6.0.6
  484. */
  485. description: '{description}',
  486. /**
  487. * xAxis description for series if there are multiple xAxes in
  488. * the chart.
  489. *
  490. * @since 6.0.6
  491. */
  492. xAxisDescription: 'X axis, {name}',
  493. /**
  494. * yAxis description for series if there are multiple yAxes in
  495. * the chart.
  496. *
  497. * @since 6.0.6
  498. */
  499. yAxisDescription: 'Y axis, {name}'
  500. }
  501. }
  502. }
  503. });
  504. }(Highcharts));
  505. (function (H) {
  506. /**
  507. * Accessibility module - Screen Reader support
  508. *
  509. * (c) 2010-2017 Highsoft AS
  510. * Author: Oystein Moseng
  511. *
  512. * License: www.highcharts.com/license
  513. */
  514. var win = H.win,
  515. doc = win.document,
  516. erase = H.erase,
  517. addEvent = H.addEvent,
  518. merge = H.merge,
  519. // CSS style to hide element from visual users while still exposing it to
  520. // screen readers
  521. hiddenStyle = {
  522. position: 'absolute',
  523. left: '-9999px',
  524. top: 'auto',
  525. width: '1px',
  526. height: '1px',
  527. overflow: 'hidden'
  528. };
  529. // If a point has one of the special keys defined, we expose all keys to the
  530. // screen reader.
  531. H.Series.prototype.commonKeys = ['name', 'id', 'category', 'x', 'value', 'y'];
  532. H.Series.prototype.specialKeys = [
  533. 'z', 'open', 'high', 'q3', 'median', 'q1', 'low', 'close'
  534. ];
  535. if (H.seriesTypes.pie) {
  536. // A pie is always simple. Don't quote me on that.
  537. H.seriesTypes.pie.prototype.specialKeys = [];
  538. }
  539. /**
  540. * HTML encode some characters vulnerable for XSS.
  541. *
  542. * @private
  543. * @function htmlencode
  544. *
  545. * @param {string} html
  546. * The input string.
  547. *
  548. * @return {string}
  549. * The excaped string.
  550. */
  551. function htmlencode(html) {
  552. return html
  553. .replace(/&/g, '&amp;')
  554. .replace(/</g, '&lt;')
  555. .replace(/>/g, '&gt;')
  556. .replace(/"/g, '&quot;')
  557. .replace(/'/g, '&#x27;')
  558. .replace(/\//g, '&#x2F;');
  559. }
  560. /**
  561. * Strip HTML tags away from a string. Used for aria-label attributes, painting
  562. * on a canvas will fail if the text contains tags.
  563. *
  564. * @private
  565. * @function stripTags
  566. *
  567. * @param {string} s
  568. * The input string.
  569. *
  570. * @return {string}
  571. * The filtered string.
  572. */
  573. function stripTags(s) {
  574. return typeof s === 'string' ? s.replace(/<\/?[^>]+(>|$)/g, '') : s;
  575. }
  576. // Accessibility options
  577. H.setOptions({
  578. /**
  579. * Options for configuring accessibility for the chart. Requires the
  580. * [accessibility module](https://code.highcharts.com/modules/accessibility.js)
  581. * to be loaded. For a description of the module and information
  582. * on its features, see
  583. * [Highcharts Accessibility](http://www.highcharts.com/docs/chart-concepts/accessibility).
  584. *
  585. * @since 5.0.0
  586. * @optionparent accessibility
  587. */
  588. accessibility: {
  589. /**
  590. * Whether or not to add series descriptions to charts with a single
  591. * series.
  592. *
  593. * @type {boolean}
  594. * @default false
  595. * @since 5.0.0
  596. * @apioption accessibility.describeSingleSeries
  597. */
  598. /**
  599. * Function to run upon clicking the "View as Data Table" link in the
  600. * screen reader region.
  601. *
  602. * By default Highcharts will insert and set focus to a data table
  603. * representation of the chart.
  604. *
  605. * @type {Function}
  606. * @since 5.0.0
  607. * @apioption accessibility.onTableAnchorClick
  608. */
  609. /**
  610. * Date format to use for points on datetime axes when describing them
  611. * to screen reader users.
  612. *
  613. * Defaults to the same format as in tooltip.
  614. *
  615. * For an overview of the replacement codes, see
  616. * [dateFormat](/class-reference/Highcharts#dateFormat).
  617. *
  618. * @see [pointDateFormatter](#accessibility.pointDateFormatter)
  619. *
  620. * @type {string}
  621. * @since 5.0.0
  622. * @apioption accessibility.pointDateFormat
  623. */
  624. /**
  625. * Formatter function to determine the date/time format used with
  626. * points on datetime axes when describing them to screen reader users.
  627. * Receives one argument, `point`, referring to the point to describe.
  628. * Should return a date format string compatible with
  629. * [dateFormat](/class-reference/Highcharts#dateFormat).
  630. *
  631. * @see [pointDateFormat](#accessibility.pointDateFormat)
  632. *
  633. * @type {Function}
  634. * @since 5.0.0
  635. * @apioption accessibility.pointDateFormatter
  636. */
  637. /**
  638. * Formatter function to use instead of the default for point
  639. * descriptions.
  640. * Receives one argument, `point`, referring to the point to describe.
  641. * Should return a String with the description of the point for a screen
  642. * reader user.
  643. *
  644. * @see [point.description](#series.line.data.description)
  645. *
  646. * @type {Function}
  647. * @since 5.0.0
  648. * @apioption accessibility.pointDescriptionFormatter
  649. */
  650. /**
  651. * Formatter function to use instead of the default for series
  652. * descriptions. Receives one argument, `series`, referring to the
  653. * series to describe. Should return a String with the description of
  654. * the series for a screen reader user.
  655. *
  656. * @see [series.description](#plotOptions.series.description)
  657. *
  658. * @type {Function}
  659. * @since 5.0.0
  660. * @apioption accessibility.seriesDescriptionFormatter
  661. */
  662. /**
  663. * Enable accessibility features for the chart.
  664. *
  665. * @since 5.0.0
  666. */
  667. enabled: true,
  668. /**
  669. * When a series contains more points than this, we no longer expose
  670. * information about individual points to screen readers.
  671. *
  672. * Set to `false` to disable.
  673. *
  674. * @type {false|number}
  675. * @since 5.0.0
  676. */
  677. pointDescriptionThreshold: false, // set to false to disable
  678. /**
  679. * A formatter function to create the HTML contents of the hidden screen
  680. * reader information region. Receives one argument, `chart`, referring
  681. * to the chart object. Should return a String with the HTML content
  682. * of the region.
  683. *
  684. * The link to view the chart as a data table will be added
  685. * automatically after the custom HTML content.
  686. *
  687. * @type {Function}
  688. * @default undefined
  689. * @since 5.0.0
  690. */
  691. screenReaderSectionFormatter: function (chart) {
  692. var options = chart.options,
  693. chartTypes = chart.types || [],
  694. formatContext = {
  695. chart: chart,
  696. numSeries: chart.series && chart.series.length
  697. },
  698. // Build axis info - but not for pies and maps. Consider not
  699. // adding for certain other types as well (funnel, pyramid?)
  700. axesDesc = (
  701. chartTypes.length === 1 && chartTypes[0] === 'pie' ||
  702. chartTypes[0] === 'map'
  703. ) && {} || chart.getAxesDescription();
  704. return '<div>' + chart.langFormat(
  705. 'accessibility.navigationHint', formatContext
  706. ) + '</div><h3>' +
  707. (
  708. options.title.text ?
  709. htmlencode(options.title.text) :
  710. chart.langFormat(
  711. 'accessibility.defaultChartTitle', formatContext
  712. )
  713. ) +
  714. (
  715. options.subtitle && options.subtitle.text ?
  716. '. ' + htmlencode(options.subtitle.text) :
  717. ''
  718. ) +
  719. '</h3><h4>' + chart.langFormat(
  720. 'accessibility.longDescriptionHeading', formatContext
  721. ) + '</h4><div>' +
  722. (
  723. options.chart.description || chart.langFormat(
  724. 'accessibility.noDescription', formatContext
  725. )
  726. ) +
  727. '</div><h4>' + chart.langFormat(
  728. 'accessibility.structureHeading', formatContext
  729. ) + '</h4><div>' +
  730. (
  731. options.chart.typeDescription ||
  732. chart.getTypeDescription()
  733. ) + '</div>' +
  734. (axesDesc.xAxis ? (
  735. '<div>' + axesDesc.xAxis + '</div>'
  736. ) : '') +
  737. (axesDesc.yAxis ? (
  738. '<div>' + axesDesc.yAxis + '</div>'
  739. ) : '');
  740. }
  741. }
  742. });
  743. /**
  744. * A text description of the chart.
  745. *
  746. * If the Accessibility module is loaded, this is included by default
  747. * as a long description of the chart and its contents in the hidden
  748. * screen reader information region.
  749. *
  750. * @see [typeDescription](#chart.typeDescription)
  751. *
  752. * @type {string}
  753. * @since 5.0.0
  754. * @apioption chart.description
  755. */
  756. /**
  757. * A text description of the chart type.
  758. *
  759. * If the Accessibility module is loaded, this will be included in the
  760. * description of the chart in the screen reader information region.
  761. *
  762. *
  763. * Highcharts will by default attempt to guess the chart type, but for
  764. * more complex charts it is recommended to specify this property for
  765. * clarity.
  766. *
  767. * @type {string}
  768. * @since 5.0.0
  769. * @apioption chart.typeDescription
  770. */
  771. /**
  772. * Utility function. Reverses child nodes of a DOM element.
  773. *
  774. * @private
  775. * @function reverseChildNodes
  776. *
  777. * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} node
  778. */
  779. function reverseChildNodes(node) {
  780. var i = node.childNodes.length;
  781. while (i--) {
  782. node.appendChild(node.childNodes[i]);
  783. }
  784. }
  785. // Whenever drawing series, put info on DOM elements
  786. H.addEvent(H.Series, 'afterRender', function () {
  787. if (this.chart.options.accessibility.enabled) {
  788. this.setA11yDescription();
  789. }
  790. });
  791. /**
  792. * Put accessible info on series and points of a series.
  793. *
  794. * @private
  795. * @function Highcharts.Series#setA11yDescription
  796. */
  797. H.Series.prototype.setA11yDescription = function () {
  798. var a11yOptions = this.chart.options.accessibility,
  799. firstPointEl = (
  800. this.points &&
  801. this.points.length &&
  802. this.points[0].graphic &&
  803. this.points[0].graphic.element
  804. ),
  805. seriesEl = (
  806. firstPointEl &&
  807. firstPointEl.parentNode || this.graph &&
  808. this.graph.element || this.group &&
  809. this.group.element
  810. ); // Could be tracker series depending on series type
  811. if (seriesEl) {
  812. // For some series types the order of elements do not match the order of
  813. // points in series. In that case we have to reverse them in order for
  814. // AT to read them out in an understandable order
  815. if (seriesEl.lastChild === firstPointEl) {
  816. reverseChildNodes(seriesEl);
  817. }
  818. // Make individual point elements accessible if possible. Note: If
  819. // markers are disabled there might not be any elements there to make
  820. // accessible.
  821. if (
  822. this.points && (
  823. this.points.length < a11yOptions.pointDescriptionThreshold ||
  824. a11yOptions.pointDescriptionThreshold === false
  825. )
  826. ) {
  827. this.points.forEach(function (point) {
  828. if (point.graphic) {
  829. point.graphic.element.setAttribute('role', 'img');
  830. point.graphic.element.setAttribute('tabindex', '-1');
  831. point.graphic.element.setAttribute('aria-label', stripTags(
  832. point.series.options.pointDescriptionFormatter &&
  833. point.series.options.pointDescriptionFormatter(point) ||
  834. a11yOptions.pointDescriptionFormatter &&
  835. a11yOptions.pointDescriptionFormatter(point) ||
  836. point.buildPointInfoString()
  837. ));
  838. }
  839. });
  840. }
  841. // Make series element accessible
  842. if (this.chart.series.length > 1 || a11yOptions.describeSingleSeries) {
  843. seriesEl.setAttribute(
  844. 'role',
  845. this.options.exposeElementToA11y ? 'img' : 'region'
  846. );
  847. seriesEl.setAttribute('tabindex', '-1');
  848. seriesEl.setAttribute(
  849. 'aria-label',
  850. stripTags(
  851. a11yOptions.seriesDescriptionFormatter &&
  852. a11yOptions.seriesDescriptionFormatter(this) ||
  853. this.buildSeriesInfoString()
  854. )
  855. );
  856. }
  857. }
  858. };
  859. /**
  860. * Return string with information about series.
  861. *
  862. * @private
  863. * @function Highcharts.Series#buildSeriesInfoString
  864. *
  865. * @return {string}
  866. */
  867. H.Series.prototype.buildSeriesInfoString = function () {
  868. var chart = this.chart,
  869. desc = this.description || this.options.description,
  870. description = desc && chart.langFormat(
  871. 'accessibility.series.description', {
  872. description: desc,
  873. series: this
  874. }
  875. ),
  876. xAxisInfo = chart.langFormat(
  877. 'accessibility.series.xAxisDescription',
  878. {
  879. name: this.xAxis && this.xAxis.getDescription(),
  880. series: this
  881. }
  882. ),
  883. yAxisInfo = chart.langFormat(
  884. 'accessibility.series.yAxisDescription',
  885. {
  886. name: this.yAxis && this.yAxis.getDescription(),
  887. series: this
  888. }
  889. ),
  890. summaryContext = {
  891. name: this.name || '',
  892. ix: this.index + 1,
  893. numSeries: chart.series.length,
  894. numPoints: this.points.length,
  895. series: this
  896. },
  897. combination = chart.types.length === 1 ? '' : 'Combination',
  898. summary = chart.langFormat(
  899. 'accessibility.series.summary.' + this.type + combination,
  900. summaryContext
  901. ) || chart.langFormat(
  902. 'accessibility.series.summary.default' + combination,
  903. summaryContext
  904. );
  905. return summary + (description ? ' ' + description : '') + (
  906. chart.yAxis.length > 1 && this.yAxis ?
  907. ' ' + yAxisInfo : ''
  908. ) + (
  909. chart.xAxis.length > 1 && this.xAxis ?
  910. ' ' + xAxisInfo : ''
  911. );
  912. };
  913. /**
  914. * Return string with information about point.
  915. *
  916. * @private
  917. * @function Highcharts.Point#buildPointInfoString
  918. *
  919. * @return {string}
  920. */
  921. H.Point.prototype.buildPointInfoString = function () {
  922. var point = this,
  923. series = point.series,
  924. a11yOptions = series.chart.options.accessibility,
  925. infoString = '',
  926. dateTimePoint = series.xAxis && series.xAxis.isDatetimeAxis,
  927. timeDesc =
  928. dateTimePoint &&
  929. series.chart.time.dateFormat(
  930. a11yOptions.pointDateFormatter &&
  931. a11yOptions.pointDateFormatter(point) ||
  932. a11yOptions.pointDateFormat ||
  933. H.Tooltip.prototype.getXDateFormat.call(
  934. {
  935. getDateFormat: H.Tooltip.prototype.getDateFormat,
  936. chart: series.chart
  937. },
  938. point,
  939. series.chart.options.tooltip,
  940. series.xAxis
  941. ),
  942. point.x
  943. ),
  944. hasSpecialKey = H.find(series.specialKeys, function (key) {
  945. return point[key] !== undefined;
  946. });
  947. // If the point has one of the less common properties defined, display all
  948. // that are defined
  949. if (hasSpecialKey) {
  950. if (dateTimePoint) {
  951. infoString = timeDesc;
  952. }
  953. series.commonKeys.concat(series.specialKeys).forEach(function (key) {
  954. if (point[key] !== undefined && !(dateTimePoint && key === 'x')) {
  955. infoString += (infoString ? '. ' : '') +
  956. key + ', ' +
  957. point[key];
  958. }
  959. });
  960. } else {
  961. // Pick and choose properties for a succint label
  962. infoString =
  963. (
  964. this.name ||
  965. timeDesc ||
  966. this.category ||
  967. this.id ||
  968. 'x, ' + this.x
  969. ) + ', ' +
  970. (this.value !== undefined ? this.value : this.y);
  971. }
  972. return (this.index + 1) + '. ' + infoString + '.' +
  973. (this.description ? ' ' + this.description : '');
  974. };
  975. /**
  976. * Get descriptive label for axis.
  977. *
  978. * @private
  979. * @function Highcharts.Axis#getDescription
  980. *
  981. * @return {string}
  982. */
  983. H.Axis.prototype.getDescription = function () {
  984. return (
  985. this.userOptions && this.userOptions.description ||
  986. this.axisTitle && this.axisTitle.textStr ||
  987. this.options.id ||
  988. this.categories && 'categories' ||
  989. this.isDatetimeAxis && 'Time' ||
  990. 'values'
  991. );
  992. };
  993. // Whenever adding or removing series, keep track of types present in chart
  994. addEvent(H.Series, 'afterInit', function () {
  995. var chart = this.chart;
  996. if (chart.options.accessibility.enabled) {
  997. chart.types = chart.types || [];
  998. // Add type to list if does not exist
  999. if (chart.types.indexOf(this.type) < 0) {
  1000. chart.types.push(this.type);
  1001. }
  1002. }
  1003. });
  1004. addEvent(H.Series, 'remove', function () {
  1005. var chart = this.chart,
  1006. removedSeries = this,
  1007. hasType = false;
  1008. // Check if any of the other series have the same type as this one.
  1009. // Otherwise remove it from the list.
  1010. chart.series.forEach(function (s) {
  1011. if (
  1012. s !== removedSeries &&
  1013. chart.types.indexOf(removedSeries.type) < 0
  1014. ) {
  1015. hasType = true;
  1016. }
  1017. });
  1018. if (!hasType) {
  1019. erase(chart.types, removedSeries.type);
  1020. }
  1021. });
  1022. /**
  1023. * Return simplified description of chart type. Some types will not be familiar
  1024. * to most screen reader users, but in those cases we try to add a description
  1025. * of the type.
  1026. *
  1027. * @private
  1028. * @function Highcharts.Chart#getTypeDescription
  1029. *
  1030. * @return {string}
  1031. */
  1032. H.Chart.prototype.getTypeDescription = function () {
  1033. var firstType = this.types && this.types[0],
  1034. firstSeries = this.series && this.series[0] || {},
  1035. mapTitle = firstSeries.mapTitle,
  1036. typeDesc = this.langFormat(
  1037. 'accessibility.seriesTypeDescriptions.' + firstType,
  1038. { chart: this }
  1039. ),
  1040. formatContext = {
  1041. numSeries: this.series.length,
  1042. numPoints: firstSeries.points && firstSeries.points.length,
  1043. chart: this,
  1044. mapTitle: mapTitle
  1045. },
  1046. multi = this.series && this.series.length === 1 ? 'Single' : 'Multiple';
  1047. if (!firstType) {
  1048. return this.langFormat(
  1049. 'accessibility.chartTypes.emptyChart', formatContext
  1050. );
  1051. }
  1052. if (firstType === 'map') {
  1053. return mapTitle ?
  1054. this.langFormat(
  1055. 'accessibility.chartTypes.mapTypeDescription',
  1056. formatContext
  1057. ) :
  1058. this.langFormat(
  1059. 'accessibility.chartTypes.unknownMap',
  1060. formatContext
  1061. );
  1062. }
  1063. if (this.types.length > 1) {
  1064. return this.langFormat(
  1065. 'accessibility.chartTypes.combinationChart', formatContext
  1066. );
  1067. }
  1068. return (
  1069. this.langFormat(
  1070. 'accessibility.chartTypes.' + firstType + multi,
  1071. formatContext
  1072. ) ||
  1073. this.langFormat(
  1074. 'accessibility.chartTypes.default' + multi,
  1075. formatContext
  1076. )
  1077. ) +
  1078. (typeDesc ? ' ' + typeDesc : '');
  1079. };
  1080. /**
  1081. * Return object with text description of each of the chart's axes.
  1082. *
  1083. * @private
  1084. * @function Highcharts.Chart#getAxesDescription
  1085. *
  1086. * @return {*}
  1087. */
  1088. H.Chart.prototype.getAxesDescription = function () {
  1089. var numXAxes = this.xAxis.length,
  1090. numYAxes = this.yAxis.length,
  1091. desc = {};
  1092. if (numXAxes) {
  1093. desc.xAxis = this.langFormat(
  1094. 'accessibility.axis.xAxisDescription' + (
  1095. numXAxes > 1 ? 'Plural' : 'Singular'
  1096. ),
  1097. {
  1098. chart: this,
  1099. names: this.xAxis.map(function (axis) {
  1100. return axis.getDescription();
  1101. }),
  1102. numAxes: numXAxes
  1103. }
  1104. );
  1105. }
  1106. if (numYAxes) {
  1107. desc.yAxis = this.langFormat(
  1108. 'accessibility.axis.yAxisDescription' + (
  1109. numYAxes > 1 ? 'Plural' : 'Singular'
  1110. ),
  1111. {
  1112. chart: this,
  1113. names: this.yAxis.map(function (axis) {
  1114. return axis.getDescription();
  1115. }),
  1116. numAxes: numYAxes
  1117. }
  1118. );
  1119. }
  1120. return desc;
  1121. };
  1122. /**
  1123. * Set a11y attribs on exporting menu.
  1124. *
  1125. * @private
  1126. * @function Highcharts.Chart#addAccessibleContextMenuAttribs
  1127. */
  1128. H.Chart.prototype.addAccessibleContextMenuAttribs = function () {
  1129. var exportList = this.exportDivElements;
  1130. if (exportList) {
  1131. // Set tabindex on the menu items to allow focusing by script
  1132. // Set role to give screen readers a chance to pick up the contents
  1133. exportList.forEach(function (item) {
  1134. if (item.tagName === 'DIV' &&
  1135. !(item.children && item.children.length)) {
  1136. item.setAttribute('role', 'menuitem');
  1137. item.setAttribute('tabindex', -1);
  1138. }
  1139. });
  1140. // Set accessibility properties on parent div
  1141. exportList[0].parentNode.setAttribute('role', 'menu');
  1142. exportList[0].parentNode.setAttribute(
  1143. 'aria-label',
  1144. this.langFormat(
  1145. 'accessibility.exporting.chartMenuLabel', { chart: this }
  1146. )
  1147. );
  1148. }
  1149. };
  1150. /**
  1151. * Add screen reader region to chart. tableId is the HTML id of the table to
  1152. * focus when clicking the table anchor in the screen reader region.
  1153. *
  1154. * @private
  1155. * @function Highcharts.Chart#addScreenReaderRegion
  1156. *
  1157. * @param {string} id
  1158. *
  1159. * @param {string} tableId
  1160. */
  1161. H.Chart.prototype.addScreenReaderRegion = function (id, tableId) {
  1162. var chart = this,
  1163. hiddenSection = chart.screenReaderRegion = doc.createElement('div'),
  1164. tableShortcut = doc.createElement('h4'),
  1165. tableShortcutAnchor = doc.createElement('a'),
  1166. chartHeading = doc.createElement('h4');
  1167. hiddenSection.setAttribute('id', id);
  1168. hiddenSection.setAttribute('role', 'region');
  1169. hiddenSection.setAttribute(
  1170. 'aria-label',
  1171. chart.langFormat(
  1172. 'accessibility.screenReaderRegionLabel', { chart: this }
  1173. )
  1174. );
  1175. hiddenSection.innerHTML = chart.options.accessibility
  1176. .screenReaderSectionFormatter(chart);
  1177. // Add shortcut to data table if export-data is loaded
  1178. if (chart.getCSV) {
  1179. tableShortcutAnchor.innerHTML = chart.langFormat(
  1180. 'accessibility.viewAsDataTable', { chart: chart }
  1181. );
  1182. tableShortcutAnchor.href = '#' + tableId;
  1183. // Make this unreachable by user tabbing
  1184. tableShortcutAnchor.setAttribute('tabindex', '-1');
  1185. tableShortcutAnchor.onclick =
  1186. chart.options.accessibility.onTableAnchorClick || function () {
  1187. chart.viewData();
  1188. doc.getElementById(tableId).focus();
  1189. };
  1190. tableShortcut.appendChild(tableShortcutAnchor);
  1191. hiddenSection.appendChild(tableShortcut);
  1192. }
  1193. // Note: JAWS seems to refuse to read aria-label on the container, so add an
  1194. // h4 element as title for the chart.
  1195. chartHeading.innerHTML = chart.langFormat(
  1196. 'accessibility.chartHeading', { chart: chart }
  1197. );
  1198. chart.renderTo.insertBefore(chartHeading, chart.renderTo.firstChild);
  1199. chart.renderTo.insertBefore(hiddenSection, chart.renderTo.firstChild);
  1200. // Hide the section and the chart heading
  1201. merge(true, chartHeading.style, hiddenStyle);
  1202. merge(true, hiddenSection.style, hiddenStyle);
  1203. };
  1204. // Make chart container accessible, and wrap table functionality.
  1205. H.Chart.prototype.callbacks.push(function (chart) {
  1206. var options = chart.options,
  1207. a11yOptions = options.accessibility;
  1208. if (!a11yOptions.enabled) {
  1209. return;
  1210. }
  1211. var titleElement,
  1212. descElement = chart.container.getElementsByTagName('desc')[0],
  1213. textElements = chart.container.getElementsByTagName('text'),
  1214. titleId = 'highcharts-title-' + chart.index,
  1215. tableId = 'highcharts-data-table-' + chart.index,
  1216. hiddenSectionId = 'highcharts-information-region-' + chart.index,
  1217. chartTitle = options.title.text || chart.langFormat(
  1218. 'accessibility.defaultChartTitle', { chart: chart }
  1219. ),
  1220. svgContainerTitle = stripTags(chart.langFormat(
  1221. 'accessibility.svgContainerTitle', {
  1222. chartTitle: chartTitle
  1223. }
  1224. ));
  1225. // Add SVG title tag if it is set
  1226. if (svgContainerTitle.length) {
  1227. titleElement = doc.createElementNS(
  1228. 'http://www.w3.org/2000/svg',
  1229. 'title'
  1230. );
  1231. titleElement.textContent = svgContainerTitle;
  1232. titleElement.id = titleId;
  1233. descElement.parentNode.insertBefore(titleElement, descElement);
  1234. }
  1235. chart.renderTo.setAttribute('role', 'region');
  1236. chart.renderTo.setAttribute(
  1237. 'aria-label',
  1238. chart.langFormat(
  1239. 'accessibility.chartContainerLabel',
  1240. {
  1241. title: stripTags(chartTitle),
  1242. chart: chart
  1243. }
  1244. )
  1245. );
  1246. // Set screen reader properties on export menu
  1247. if (
  1248. chart.exportSVGElements &&
  1249. chart.exportSVGElements[0] &&
  1250. chart.exportSVGElements[0].element
  1251. ) {
  1252. // Set event handler on button
  1253. var button = chart.exportSVGElements[0].element,
  1254. oldExportCallback = button.onclick;
  1255. button.onclick = function () {
  1256. oldExportCallback.apply(
  1257. this,
  1258. Array.prototype.slice.call(arguments)
  1259. );
  1260. chart.addAccessibleContextMenuAttribs();
  1261. chart.highlightExportItem(0);
  1262. };
  1263. // Set props on button
  1264. button.setAttribute('role', 'button');
  1265. button.setAttribute(
  1266. 'aria-label',
  1267. chart.langFormat(
  1268. 'accessibility.exporting.menuButtonLabel', { chart: chart }
  1269. )
  1270. );
  1271. // Set props on group
  1272. chart.exportingGroup.element.setAttribute('role', 'region');
  1273. chart.exportingGroup.element.setAttribute(
  1274. 'aria-label',
  1275. chart.langFormat(
  1276. 'accessibility.exporting.exportRegionLabel', { chart: chart }
  1277. )
  1278. );
  1279. }
  1280. // Set screen reader properties on input boxes for range selector. We need
  1281. // to do this regardless of whether or not these are visible, as they are
  1282. // by default part of the page's tabindex unless we set them to -1.
  1283. if (chart.rangeSelector) {
  1284. ['minInput', 'maxInput'].forEach(function (key, i) {
  1285. if (chart.rangeSelector[key]) {
  1286. chart.rangeSelector[key].setAttribute('tabindex', '-1');
  1287. chart.rangeSelector[key].setAttribute('role', 'textbox');
  1288. chart.rangeSelector[key].setAttribute(
  1289. 'aria-label',
  1290. chart.langFormat(
  1291. 'accessibility.rangeSelector' +
  1292. (i ? 'MaxInput' : 'MinInput'), { chart: chart }
  1293. )
  1294. );
  1295. }
  1296. });
  1297. }
  1298. // Hide text elements from screen readers
  1299. [].forEach.call(textElements, function (el) {
  1300. el.setAttribute('aria-hidden', 'true');
  1301. });
  1302. // Add top-secret screen reader region
  1303. chart.addScreenReaderRegion(hiddenSectionId, tableId);
  1304. // Add ID and summary attr to table HTML
  1305. addEvent(chart, 'afterGetTable', function (e) {
  1306. e.html = e.html
  1307. .replace(
  1308. '<table ',
  1309. '<table summary="' + chart.langFormat(
  1310. 'accessibility.tableSummary', { chart: chart }
  1311. ) + '"'
  1312. );
  1313. });
  1314. });
  1315. }(Highcharts));
  1316. (function (H) {
  1317. /**
  1318. * Accessibility module - Keyboard navigation
  1319. *
  1320. * (c) 2010-2017 Highsoft AS
  1321. * Author: Oystein Moseng
  1322. *
  1323. * License: www.highcharts.com/license
  1324. */
  1325. var win = H.win,
  1326. doc = win.document,
  1327. addEvent = H.addEvent,
  1328. fireEvent = H.fireEvent,
  1329. merge = H.merge,
  1330. pick = H.pick;
  1331. /*
  1332. * Add focus border functionality to SVGElements. Draws a new rect on top of
  1333. * element around its bounding box.
  1334. */
  1335. H.extend(H.SVGElement.prototype, {
  1336. /**
  1337. * @private
  1338. * @function Highcharts.SVGElement#addFocusBorder
  1339. *
  1340. * @param {number} margin
  1341. *
  1342. * @param {Higcharts.CSSObject} style
  1343. */
  1344. addFocusBorder: function (margin, style) {
  1345. // Allow updating by just adding new border
  1346. if (this.focusBorder) {
  1347. this.removeFocusBorder();
  1348. }
  1349. // Add the border rect
  1350. var bb = this.getBBox(),
  1351. pad = pick(margin, 3);
  1352. this.focusBorder = this.renderer.rect(
  1353. bb.x - pad,
  1354. bb.y - pad,
  1355. bb.width + 2 * pad,
  1356. bb.height + 2 * pad,
  1357. style && style.borderRadius
  1358. )
  1359. .addClass('highcharts-focus-border')
  1360. .attr({
  1361. zIndex: 99
  1362. })
  1363. .add(this.parentGroup);
  1364. if (!this.renderer.styledMode) {
  1365. this.focusBorder.attr({
  1366. stroke: style && style.stroke,
  1367. 'stroke-width': style && style.strokeWidth
  1368. });
  1369. }
  1370. },
  1371. /**
  1372. * @private
  1373. * @function Highcharts.SVGElement#removeFocusBorder
  1374. */
  1375. removeFocusBorder: function () {
  1376. if (this.focusBorder) {
  1377. this.focusBorder.destroy();
  1378. delete this.focusBorder;
  1379. }
  1380. }
  1381. });
  1382. /*
  1383. * Set for which series types it makes sense to move to the closest point with
  1384. * up/down arrows, and which series types should just move to next series.
  1385. */
  1386. H.Series.prototype.keyboardMoveVertical = true;
  1387. ['column', 'pie'].forEach(function (type) {
  1388. if (H.seriesTypes[type]) {
  1389. H.seriesTypes[type].prototype.keyboardMoveVertical = false;
  1390. }
  1391. });
  1392. /**
  1393. * Strip HTML tags away from a string. Used for aria-label attributes, painting
  1394. * on a canvas will fail if the text contains tags.
  1395. *
  1396. * @private
  1397. * @function stripTags
  1398. *
  1399. * @param {string} s
  1400. * The input string
  1401. *
  1402. * @return {string}
  1403. * The filtered string
  1404. */
  1405. function stripTags(s) {
  1406. return typeof s === 'string' ? s.replace(/<\/?[^>]+(>|$)/g, '') : s;
  1407. }
  1408. /**
  1409. * Get the index of a point in a series. This is needed when using e.g. data
  1410. * grouping.
  1411. *
  1412. * @private
  1413. * @function getPointIndex
  1414. *
  1415. * @param {Highcharts.Point} point
  1416. * The point to find index of.
  1417. *
  1418. * @return {number}
  1419. * The index in the series.points array of the point.
  1420. */
  1421. function getPointIndex(point) {
  1422. var index = point.index,
  1423. points = point.series.points,
  1424. i = points.length;
  1425. if (points[index] !== point) {
  1426. while (i--) {
  1427. if (points[i] === point) {
  1428. return i;
  1429. }
  1430. }
  1431. } else {
  1432. return index;
  1433. }
  1434. }
  1435. // Set default keyboard navigation options
  1436. H.setOptions({
  1437. /**
  1438. * @since 5.0.0
  1439. * @optionparent accessibility
  1440. */
  1441. accessibility: {
  1442. /**
  1443. * Options for keyboard navigation.
  1444. *
  1445. * @since 5.0.0
  1446. */
  1447. keyboardNavigation: {
  1448. /**
  1449. * Enable keyboard navigation for the chart.
  1450. *
  1451. * @since 5.0.0
  1452. */
  1453. enabled: true,
  1454. /**
  1455. * Options for the focus border drawn around elements while
  1456. * navigating through them.
  1457. *
  1458. * @sample highcharts/accessibility/custom-focus
  1459. * Custom focus ring
  1460. *
  1461. * @since 6.0.3
  1462. */
  1463. focusBorder: {
  1464. /**
  1465. * Enable/disable focus border for chart.
  1466. *
  1467. * @since 6.0.3
  1468. */
  1469. enabled: true,
  1470. /**
  1471. * Hide the browser's default focus indicator.
  1472. *
  1473. * @since 6.0.4
  1474. */
  1475. hideBrowserFocusOutline: true,
  1476. /**
  1477. * Style options for the focus border drawn around elements
  1478. * while navigating through them. Note that some browsers in
  1479. * addition draw their own borders for focused elements. These
  1480. * automatic borders can not be styled by Highcharts.
  1481. *
  1482. * In styled mode, the border is given the
  1483. * `.highcharts-focus-border` class.
  1484. *
  1485. * @type {Highcharts.CSSObject}
  1486. * @default {"color": "#335cad", "lineWidth": 2, "borderRadius": 3}
  1487. * @since 6.0.3
  1488. */
  1489. style: {
  1490. /** @ignore-option */
  1491. color: '#335cad',
  1492. /** @ignore-option */
  1493. lineWidth: 2,
  1494. /** @ignore-option */
  1495. borderRadius: 3
  1496. },
  1497. /**
  1498. * Focus border margin around the elements.
  1499. *
  1500. * @since 6.0.3
  1501. */
  1502. margin: 2
  1503. },
  1504. /**
  1505. * Set the keyboard navigation mode for the chart. Can be "normal"
  1506. * or "serialize". In normal mode, left/right arrow keys move
  1507. * between points in a series, while up/down arrow keys move between
  1508. * series. Up/down navigation acts intelligently to figure out which
  1509. * series makes sense to move to from any given point.
  1510. *
  1511. * In "serialize" mode, points are instead navigated as a single
  1512. * list. Left/right behaves as in "normal" mode. Up/down arrow keys
  1513. * will behave like left/right. This is useful for unifying
  1514. * navigation behavior with/without screen readers enabled.
  1515. *
  1516. * @type {string}
  1517. * @default normal
  1518. * @since 6.0.4
  1519. * @validvalue ["normal", "serialize"]
  1520. * @apioption accessibility.keyboardNavigation.mode
  1521. */
  1522. /**
  1523. * Skip null points when navigating through points with the
  1524. * keyboard.
  1525. *
  1526. * @since 5.0.0
  1527. */
  1528. skipNullPoints: true
  1529. }
  1530. }
  1531. });
  1532. /**
  1533. * Keyboard navigation for the legend. Requires the Accessibility module.
  1534. *
  1535. * @since 5.0.14
  1536. * @apioption legend.keyboardNavigation
  1537. */
  1538. /**
  1539. * Enable/disable keyboard navigation for the legend. Requires the Accessibility
  1540. * module.
  1541. *
  1542. * @see [accessibility.keyboardNavigation](
  1543. * #accessibility.keyboardNavigation.enabled)
  1544. *
  1545. * @type {boolean}
  1546. * @default true
  1547. * @since 5.0.13
  1548. * @apioption legend.keyboardNavigation.enabled
  1549. */
  1550. /**
  1551. * Abstraction layer for keyboard navigation. Keep a map of keyCodes to handler
  1552. * functions, and a next/prev move handler for tab order. The module's keyCode
  1553. * handlers determine when to move to another module. Validate holds a function
  1554. * to determine if there are prerequisites for this module to run that are not
  1555. * met. Init holds a function to run once before any keyCodes are interpreted.
  1556. * Terminate holds a function to run once before moving to next/prev module.
  1557. *
  1558. * @private
  1559. * @class
  1560. * @name KeyboardNavigationModule
  1561. *
  1562. * @param {Highcharts.Chart} chart
  1563. * The chart object keeps track of a list of KeyboardNavigationModules.
  1564. *
  1565. * @param {*} options
  1566. */
  1567. function KeyboardNavigationModule(chart, options) {
  1568. this.chart = chart;
  1569. this.id = options.id;
  1570. this.keyCodeMap = options.keyCodeMap;
  1571. this.validate = options.validate;
  1572. this.init = options.init;
  1573. this.terminate = options.terminate;
  1574. }
  1575. KeyboardNavigationModule.prototype = {
  1576. /**
  1577. * Find handler function(s) for key code in the keyCodeMap and run it.
  1578. *
  1579. * @private
  1580. * @function KeyboardNavigationModule#run
  1581. *
  1582. * @param {global.Event} e
  1583. *
  1584. * @return {boolean}
  1585. */
  1586. run: function (e) {
  1587. var navModule = this,
  1588. keyCode = e.which || e.keyCode,
  1589. found = false,
  1590. handled = false;
  1591. this.keyCodeMap.forEach(function (codeSet) {
  1592. if (codeSet[0].indexOf(keyCode) > -1) {
  1593. found = true;
  1594. handled = codeSet[1].call(navModule, keyCode, e) !== false;
  1595. }
  1596. });
  1597. // Default tab handler, move to next/prev module
  1598. if (!found && keyCode === 9) {
  1599. handled = this.move(e.shiftKey ? -1 : 1);
  1600. }
  1601. return handled;
  1602. },
  1603. /**
  1604. * Move to next/prev valid module, or undefined if none, and init it.
  1605. * Returns true on success and false if there is no valid module to move to.
  1606. *
  1607. * @private
  1608. * @function KeyboardNavigationModule#move
  1609. *
  1610. * @param {number} direction
  1611. *
  1612. * @return {boolean}
  1613. */
  1614. move: function (direction) {
  1615. var chart = this.chart;
  1616. if (this.terminate) {
  1617. this.terminate(direction);
  1618. }
  1619. chart.keyboardNavigationModuleIndex += direction;
  1620. var newModule = chart.keyboardNavigationModules[
  1621. chart.keyboardNavigationModuleIndex
  1622. ];
  1623. // Remove existing focus border if any
  1624. if (chart.focusElement) {
  1625. chart.focusElement.removeFocusBorder();
  1626. }
  1627. // Verify new module
  1628. if (newModule) {
  1629. if (newModule.validate && !newModule.validate()) {
  1630. return this.move(direction); // Invalid module, recurse
  1631. }
  1632. if (newModule.init) {
  1633. newModule.init(direction); // Valid module, init it
  1634. return true;
  1635. }
  1636. }
  1637. // No module
  1638. chart.keyboardNavigationModuleIndex = 0; // Reset counter
  1639. // Set focus to chart or exit anchor depending on direction
  1640. if (direction > 0) {
  1641. this.chart.exiting = true;
  1642. this.chart.tabExitAnchor.focus();
  1643. } else {
  1644. this.chart.renderTo.focus();
  1645. }
  1646. return false;
  1647. }
  1648. };
  1649. /**
  1650. * Utility function to attempt to fake a click event on an element.
  1651. *
  1652. * @private
  1653. * @function fakeClickEvent
  1654. *
  1655. * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement}
  1656. */
  1657. function fakeClickEvent(element) {
  1658. var fakeEvent;
  1659. if (element && element.onclick && doc.createEvent) {
  1660. fakeEvent = doc.createEvent('Events');
  1661. fakeEvent.initEvent('click', true, false);
  1662. element.onclick(fakeEvent);
  1663. }
  1664. }
  1665. /**
  1666. * Determine if a series should be skipped
  1667. *
  1668. * @private
  1669. * @function isSkipSeries
  1670. *
  1671. * @param {Highcharts.Series} series
  1672. *
  1673. * @return {boolean}
  1674. */
  1675. function isSkipSeries(series) {
  1676. var a11yOptions = series.chart.options.accessibility;
  1677. return series.options.skipKeyboardNavigation ||
  1678. series.options.enableMouseTracking === false || // #8440
  1679. !series.visible ||
  1680. // Skip all points in a series where pointDescriptionThreshold is
  1681. // reached
  1682. (a11yOptions.pointDescriptionThreshold &&
  1683. a11yOptions.pointDescriptionThreshold <= series.points.length);
  1684. }
  1685. /**
  1686. * Determine if a point should be skipped
  1687. *
  1688. * @private
  1689. * @function isSkipPoint
  1690. *
  1691. * @param {Highcharts.Point} point
  1692. *
  1693. * @return {boolean}
  1694. */
  1695. function isSkipPoint(point) {
  1696. var a11yOptions = point.series.chart.options.accessibility;
  1697. return point.isNull && a11yOptions.keyboardNavigation.skipNullPoints ||
  1698. point.visible === false ||
  1699. isSkipSeries(point.series);
  1700. }
  1701. /**
  1702. * Get the point in a series that is closest (in distance) to a reference point.
  1703. * Optionally supply weight factors for x and y directions.
  1704. *
  1705. * @private
  1706. * @function getClosestPoint
  1707. *
  1708. * @param {Highcharts.Point} point
  1709. *
  1710. * @param {Highcharts.Series} series
  1711. *
  1712. * @param {number} [xWeight]
  1713. *
  1714. * @param {number} [yWeight]
  1715. *
  1716. * @return {Highcharts.Point|undefined}
  1717. */
  1718. function getClosestPoint(point, series, xWeight, yWeight) {
  1719. var minDistance = Infinity,
  1720. dPoint,
  1721. minIx,
  1722. distance,
  1723. i = series.points.length;
  1724. if (point.plotX === undefined || point.plotY === undefined) {
  1725. return;
  1726. }
  1727. while (i--) {
  1728. dPoint = series.points[i];
  1729. if (dPoint.plotX === undefined || dPoint.plotY === undefined) {
  1730. continue;
  1731. }
  1732. distance = (point.plotX - dPoint.plotX) *
  1733. (point.plotX - dPoint.plotX) * (xWeight || 1) +
  1734. (point.plotY - dPoint.plotY) *
  1735. (point.plotY - dPoint.plotY) * (yWeight || 1);
  1736. if (distance < minDistance) {
  1737. minDistance = distance;
  1738. minIx = i;
  1739. }
  1740. }
  1741. return minIx !== undefined && series.points[minIx];
  1742. }
  1743. /**
  1744. * Pan along axis in a direction (1 or -1), optionally with a defined
  1745. * granularity (number of steps it takes to walk across current view)
  1746. *
  1747. * @private
  1748. * @function Highcharts.Axis#panStep
  1749. *
  1750. * @param {number} direction
  1751. *
  1752. * @param {number} [granularity]
  1753. */
  1754. H.Axis.prototype.panStep = function (direction, granularity) {
  1755. var gran = granularity || 3,
  1756. extremes = this.getExtremes(),
  1757. step = (extremes.max - extremes.min) / gran * direction,
  1758. newMax = extremes.max + step,
  1759. newMin = extremes.min + step,
  1760. size = newMax - newMin;
  1761. if (direction < 0 && newMin < extremes.dataMin) {
  1762. newMin = extremes.dataMin;
  1763. newMax = newMin + size;
  1764. } else if (direction > 0 && newMax > extremes.dataMax) {
  1765. newMax = extremes.dataMax;
  1766. newMin = newMax - size;
  1767. }
  1768. this.setExtremes(newMin, newMax);
  1769. };
  1770. /**
  1771. * Set chart's focus to an SVGElement. Calls focus() on it, and draws the focus
  1772. * border.
  1773. *
  1774. * @private
  1775. * @function Highcharts.Chart#setFocusToElement
  1776. *
  1777. * @param {Highcharts.SVGElement} svgElement
  1778. * Element to draw the border around.
  1779. *
  1780. * @param {Highcharts.SVGElement} [focusElement]
  1781. * If supplied, it draws the border around svgElement and sets the focus
  1782. * to focusElement.
  1783. */
  1784. H.Chart.prototype.setFocusToElement = function (svgElement, focusElement) {
  1785. var focusBorderOptions = this.options.accessibility
  1786. .keyboardNavigation.focusBorder,
  1787. browserFocusElement = focusElement || svgElement;
  1788. // Set browser focus if possible
  1789. if (
  1790. browserFocusElement.element &&
  1791. browserFocusElement.element.focus
  1792. ) {
  1793. browserFocusElement.element.focus();
  1794. // Hide default focus ring
  1795. if (focusBorderOptions.hideBrowserFocusOutline) {
  1796. browserFocusElement.css({ outline: 'none' });
  1797. }
  1798. }
  1799. if (focusBorderOptions.enabled) {
  1800. // Remove old focus border
  1801. if (this.focusElement) {
  1802. this.focusElement.removeFocusBorder();
  1803. }
  1804. // Draw focus border (since some browsers don't do it automatically)
  1805. svgElement.addFocusBorder(focusBorderOptions.margin, {
  1806. stroke: focusBorderOptions.style.color,
  1807. strokeWidth: focusBorderOptions.style.lineWidth,
  1808. borderRadius: focusBorderOptions.style.borderRadius
  1809. });
  1810. this.focusElement = svgElement;
  1811. }
  1812. };
  1813. /**
  1814. * Highlights a point (show tooltip and display hover state).
  1815. *
  1816. * @private
  1817. * @function Highcharts.Point#highlight
  1818. *
  1819. * @return {Highcharts.Point}
  1820. * This highlighted point.
  1821. */
  1822. H.Point.prototype.highlight = function () {
  1823. var chart = this.series.chart;
  1824. if (!this.isNull) {
  1825. this.onMouseOver(); // Show the hover marker and tooltip
  1826. } else {
  1827. if (chart.tooltip) {
  1828. chart.tooltip.hide(0);
  1829. }
  1830. // Don't call blur on the element, as it messes up the chart div's focus
  1831. }
  1832. // We focus only after calling onMouseOver because the state change can
  1833. // change z-index and mess up the element.
  1834. if (this.graphic) {
  1835. chart.setFocusToElement(this.graphic);
  1836. }
  1837. chart.highlightedPoint = this;
  1838. return this;
  1839. };
  1840. /**
  1841. * Function to highlight next/previous point in chart.
  1842. *
  1843. * @private
  1844. * @function Highcharts.Chart#highlightAdjacentPoint
  1845. *
  1846. * @param {boolean} next
  1847. * Flag for the direction.
  1848. *
  1849. * @return {Highcharts.Point|false}
  1850. * Returns highlighted point on success, false on failure (no adjacent
  1851. * point to highlight in chosen direction).
  1852. */
  1853. H.Chart.prototype.highlightAdjacentPoint = function (next) {
  1854. var chart = this,
  1855. series = chart.series,
  1856. curPoint = chart.highlightedPoint,
  1857. curPointIndex = curPoint && getPointIndex(curPoint) || 0,
  1858. curPoints = curPoint && curPoint.series.points,
  1859. lastSeries = chart.series && chart.series[chart.series.length - 1],
  1860. lastPoint = lastSeries && lastSeries.points &&
  1861. lastSeries.points[lastSeries.points.length - 1],
  1862. newSeries,
  1863. newPoint;
  1864. // If no points, return false
  1865. if (!series[0] || !series[0].points) {
  1866. return false;
  1867. }
  1868. if (!curPoint) {
  1869. // No point is highlighted yet. Try first/last point depending on move
  1870. // direction
  1871. newPoint = next ? series[0].points[0] : lastPoint;
  1872. } else {
  1873. // We have a highlighted point.
  1874. // Grab next/prev point & series
  1875. newSeries = series[curPoint.series.index + (next ? 1 : -1)];
  1876. newPoint = curPoints[curPointIndex + (next ? 1 : -1)];
  1877. if (!newPoint && newSeries) {
  1878. // Done with this series, try next one
  1879. newPoint = newSeries.points[next ? 0 : newSeries.points.length - 1];
  1880. }
  1881. // If there is no adjacent point, we return false
  1882. if (!newPoint) {
  1883. return false;
  1884. }
  1885. }
  1886. // Recursively skip points
  1887. if (isSkipPoint(newPoint)) {
  1888. // If we skip this whole series, move to the end of the series before we
  1889. // recurse, just to optimize
  1890. newSeries = newPoint.series;
  1891. if (isSkipSeries(newSeries)) {
  1892. chart.highlightedPoint = next ?
  1893. newSeries.points[newSeries.points.length - 1] :
  1894. newSeries.points[0];
  1895. } else {
  1896. // Otherwise, just move one point
  1897. chart.highlightedPoint = newPoint;
  1898. }
  1899. // Retry
  1900. return chart.highlightAdjacentPoint(next);
  1901. }
  1902. // There is an adjacent point, highlight it
  1903. return newPoint.highlight();
  1904. };
  1905. /**
  1906. * Highlight first valid point in a series. Returns the point if successfully
  1907. * highlighted, otherwise false. If there is a highlighted point in the series,
  1908. * use that as starting point.
  1909. *
  1910. * @private
  1911. * @function Highcharts.Series#highlightFirstValidPoint
  1912. *
  1913. * @return {Highcharts.Point|false}
  1914. */
  1915. H.Series.prototype.highlightFirstValidPoint = function () {
  1916. var curPoint = this.chart.highlightedPoint,
  1917. start = (curPoint && curPoint.series) === this ?
  1918. getPointIndex(curPoint) :
  1919. 0,
  1920. points = this.points;
  1921. if (points) {
  1922. for (var i = start, len = points.length; i < len; ++i) {
  1923. if (!isSkipPoint(points[i])) {
  1924. return points[i].highlight();
  1925. }
  1926. }
  1927. for (var j = start; j >= 0; --j) {
  1928. if (!isSkipPoint(points[j])) {
  1929. return points[j].highlight();
  1930. }
  1931. }
  1932. }
  1933. return false;
  1934. };
  1935. /**
  1936. * Highlight next/previous series in chart. Returns false if no adjacent series
  1937. * in the direction, otherwise returns new highlighted point.
  1938. *
  1939. * @private
  1940. * @function Highcharts.Chart#highlightAdjacentSeries
  1941. *
  1942. * @param {boolean} down
  1943. *
  1944. * @return {Highcharts.Point|false}
  1945. */
  1946. H.Chart.prototype.highlightAdjacentSeries = function (down) {
  1947. var chart = this,
  1948. newSeries,
  1949. newPoint,
  1950. adjacentNewPoint,
  1951. curPoint = chart.highlightedPoint,
  1952. lastSeries = chart.series && chart.series[chart.series.length - 1],
  1953. lastPoint = lastSeries && lastSeries.points &&
  1954. lastSeries.points[lastSeries.points.length - 1];
  1955. // If no point is highlighted, highlight the first/last point
  1956. if (!chart.highlightedPoint) {
  1957. newSeries = down ? (chart.series && chart.series[0]) : lastSeries;
  1958. newPoint = down ?
  1959. (newSeries && newSeries.points && newSeries.points[0]) : lastPoint;
  1960. return newPoint ? newPoint.highlight() : false;
  1961. }
  1962. newSeries = chart.series[curPoint.series.index + (down ? -1 : 1)];
  1963. if (!newSeries) {
  1964. return false;
  1965. }
  1966. // We have a new series in this direction, find the right point
  1967. // Weigh xDistance as counting much higher than Y distance
  1968. newPoint = getClosestPoint(curPoint, newSeries, 4);
  1969. if (!newPoint) {
  1970. return false;
  1971. }
  1972. // New series and point exists, but we might want to skip it
  1973. if (isSkipSeries(newSeries)) {
  1974. // Skip the series
  1975. newPoint.highlight();
  1976. adjacentNewPoint = chart.highlightAdjacentSeries(down); // Try recurse
  1977. if (!adjacentNewPoint) {
  1978. // Recurse failed
  1979. curPoint.highlight();
  1980. return false;
  1981. }
  1982. // Recurse succeeded
  1983. return adjacentNewPoint;
  1984. }
  1985. // Highlight the new point or any first valid point back or forwards from it
  1986. newPoint.highlight();
  1987. return newPoint.series.highlightFirstValidPoint();
  1988. };
  1989. /**
  1990. * Highlight the closest point vertically.
  1991. *
  1992. * @private
  1993. * @function Highcharts.Chart#highlightAdjacentPointVertical
  1994. *
  1995. * @param {boolean} down
  1996. *
  1997. * @return {Highcharts.Point|false}
  1998. */
  1999. H.Chart.prototype.highlightAdjacentPointVertical = function (down) {
  2000. var curPoint = this.highlightedPoint,
  2001. minDistance = Infinity,
  2002. bestPoint;
  2003. if (curPoint.plotX === undefined || curPoint.plotY === undefined) {
  2004. return false;
  2005. }
  2006. this.series.forEach(function (series) {
  2007. if (isSkipSeries(series)) {
  2008. return;
  2009. }
  2010. series.points.forEach(function (point) {
  2011. if (point.plotY === undefined || point.plotX === undefined ||
  2012. point === curPoint) {
  2013. return;
  2014. }
  2015. var yDistance = point.plotY - curPoint.plotY,
  2016. width = Math.abs(point.plotX - curPoint.plotX),
  2017. distance = Math.abs(yDistance) * Math.abs(yDistance) +
  2018. width * width * 4; // Weigh horizontal distance highly
  2019. // Reverse distance number if axis is reversed
  2020. if (series.yAxis.reversed) {
  2021. yDistance *= -1;
  2022. }
  2023. if (
  2024. yDistance < 0 && down || yDistance > 0 && !down || // Wrong dir
  2025. distance < 5 || // Points in same spot => infinite loop
  2026. isSkipPoint(point)
  2027. ) {
  2028. return;
  2029. }
  2030. if (distance < minDistance) {
  2031. minDistance = distance;
  2032. bestPoint = point;
  2033. }
  2034. });
  2035. });
  2036. return bestPoint ? bestPoint.highlight() : false;
  2037. };
  2038. /**
  2039. * Show the export menu and focus the first item (if exists).
  2040. *
  2041. * @private
  2042. * @function Highcharts.Chart#showExportMenu
  2043. */
  2044. H.Chart.prototype.showExportMenu = function () {
  2045. if (this.exportSVGElements && this.exportSVGElements[0]) {
  2046. this.exportSVGElements[0].element.onclick();
  2047. this.highlightExportItem(0);
  2048. }
  2049. };
  2050. /**
  2051. * Hide export menu.
  2052. *
  2053. * @private
  2054. * @function Highcharts.Chart#hideExportMenu
  2055. */
  2056. H.Chart.prototype.hideExportMenu = function () {
  2057. var chart = this,
  2058. exportList = chart.exportDivElements;
  2059. if (exportList && chart.exportContextMenu) {
  2060. // Reset hover states etc.
  2061. exportList.forEach(function (el) {
  2062. if (el.className === 'highcharts-menu-item' && el.onmouseout) {
  2063. el.onmouseout();
  2064. }
  2065. });
  2066. chart.highlightedExportItem = 0;
  2067. // Hide the menu div
  2068. chart.exportContextMenu.hideMenu();
  2069. // Make sure the chart has focus and can capture keyboard events
  2070. chart.container.focus();
  2071. }
  2072. };
  2073. /**
  2074. * Highlight export menu item by index.
  2075. *
  2076. * @private
  2077. * @function Highcharts.Chart#highlightExportItem
  2078. *
  2079. * @param {number} ix
  2080. *
  2081. * @return {true|undefined}
  2082. */
  2083. H.Chart.prototype.highlightExportItem = function (ix) {
  2084. var listItem = this.exportDivElements && this.exportDivElements[ix],
  2085. curHighlighted =
  2086. this.exportDivElements &&
  2087. this.exportDivElements[this.highlightedExportItem],
  2088. hasSVGFocusSupport;
  2089. if (
  2090. listItem &&
  2091. listItem.tagName === 'DIV' &&
  2092. !(listItem.children && listItem.children.length)
  2093. ) {
  2094. // Test if we have focus support for SVG elements
  2095. hasSVGFocusSupport = !!(
  2096. this.renderTo.getElementsByTagName('g')[0] || {}
  2097. ).focus;
  2098. // Only focus if we can set focus back to the elements after
  2099. // destroying the menu (#7422)
  2100. if (listItem.focus && hasSVGFocusSupport) {
  2101. listItem.focus();
  2102. }
  2103. if (curHighlighted && curHighlighted.onmouseout) {
  2104. curHighlighted.onmouseout();
  2105. }
  2106. if (listItem.onmouseover) {
  2107. listItem.onmouseover();
  2108. }
  2109. this.highlightedExportItem = ix;
  2110. return true;
  2111. }
  2112. };
  2113. /**
  2114. * Try to highlight the last valid export menu item.
  2115. *
  2116. * @private
  2117. * @function Highcharts.Chart#highlightLastExportItem
  2118. */
  2119. H.Chart.prototype.highlightLastExportItem = function () {
  2120. var chart = this,
  2121. i;
  2122. if (chart.exportDivElements) {
  2123. i = chart.exportDivElements.length;
  2124. while (i--) {
  2125. if (chart.highlightExportItem(i)) {
  2126. break;
  2127. }
  2128. }
  2129. }
  2130. };
  2131. /**
  2132. * Highlight range selector button by index.
  2133. *
  2134. * @private
  2135. * @function Highcharts.Chart#highlightRangeSelectorButton
  2136. *
  2137. * @param {number} ix
  2138. *
  2139. * @return {boolean}
  2140. */
  2141. H.Chart.prototype.highlightRangeSelectorButton = function (ix) {
  2142. var buttons = this.rangeSelector.buttons;
  2143. // Deselect old
  2144. if (buttons[this.highlightedRangeSelectorItemIx]) {
  2145. buttons[this.highlightedRangeSelectorItemIx].setState(
  2146. this.oldRangeSelectorItemState || 0
  2147. );
  2148. }
  2149. // Select new
  2150. this.highlightedRangeSelectorItemIx = ix;
  2151. if (buttons[ix]) {
  2152. this.setFocusToElement(buttons[ix].box, buttons[ix]);
  2153. this.oldRangeSelectorItemState = buttons[ix].state;
  2154. buttons[ix].setState(2);
  2155. return true;
  2156. }
  2157. return false;
  2158. };
  2159. /**
  2160. * Highlight legend item by index.
  2161. *
  2162. * @private
  2163. * @function Highcharts.Chart#highlightLegendItem
  2164. *
  2165. * @param {number} ix
  2166. *
  2167. * @return {boolean}
  2168. */
  2169. H.Chart.prototype.highlightLegendItem = function (ix) {
  2170. var items = this.legend.allItems,
  2171. oldIx = this.highlightedLegendItemIx;
  2172. if (items[ix]) {
  2173. if (items[oldIx]) {
  2174. fireEvent(
  2175. items[oldIx].legendGroup.element,
  2176. 'mouseout'
  2177. );
  2178. }
  2179. // Scroll if we have to
  2180. if (items[ix].pageIx !== undefined &&
  2181. items[ix].pageIx + 1 !== this.legend.currentPage) {
  2182. this.legend.scroll(1 + items[ix].pageIx - this.legend.currentPage);
  2183. }
  2184. // Focus
  2185. this.highlightedLegendItemIx = ix;
  2186. this.setFocusToElement(items[ix].legendItem, items[ix].legendGroup);
  2187. fireEvent(items[ix].legendGroup.element, 'mouseover');
  2188. return true;
  2189. }
  2190. return false;
  2191. };
  2192. /**
  2193. * Add keyboard navigation handling modules to chart.
  2194. *
  2195. * @private
  2196. * @function Highcharts.Chart#addKeyboardNavigationModules
  2197. */
  2198. H.Chart.prototype.addKeyboardNavigationModules = function () {
  2199. var chart = this;
  2200. /**
  2201. * @private
  2202. * @function navModuleFactory
  2203. *
  2204. * @param {string} id
  2205. *
  2206. * @param {Array<Array<number>,Function>} keyMap
  2207. *
  2208. * @param {Highcharts.Dictionary<Function>} options
  2209. *
  2210. * @return {KeyboardNavigationModule}
  2211. */
  2212. function navModuleFactory(id, keyMap, options) {
  2213. return new KeyboardNavigationModule(chart, merge({
  2214. keyCodeMap: keyMap
  2215. }, { id: id }, options));
  2216. }
  2217. /**
  2218. * List of the different keyboard handling modes we use depending on where
  2219. * we are in the chart. Each mode has a set of handling functions mapped to
  2220. * key codes. Each mode determines when to move to the next/prev mode.
  2221. *
  2222. * @private
  2223. * @name Highcharts.Chart#keyboardNavigationModules
  2224. * @type {Array<KeyboardNavigationModule>}
  2225. */
  2226. chart.keyboardNavigationModules = [
  2227. // Entry point catching the first tab, allowing users to tab into points
  2228. // more intuitively.
  2229. navModuleFactory('entry', []),
  2230. // Points
  2231. navModuleFactory('points', [
  2232. // Left/Right
  2233. [[37, 39], function (keyCode) {
  2234. var right = keyCode === 39;
  2235. if (!chart.highlightAdjacentPoint(right)) {
  2236. // Failed to highlight next, wrap to last/first
  2237. return this.init(right ? 1 : -1);
  2238. }
  2239. return true;
  2240. }],
  2241. // Up/Down
  2242. [[38, 40], function (keyCode) {
  2243. var down = keyCode !== 38,
  2244. navOptions = chart.options.accessibility.keyboardNavigation;
  2245. if (navOptions.mode && navOptions.mode === 'serialize') {
  2246. // Act like left/right
  2247. if (!chart.highlightAdjacentPoint(down)) {
  2248. return this.init(down ? 1 : -1);
  2249. }
  2250. return true;
  2251. }
  2252. // Normal mode, move between series
  2253. var highlightMethod = chart.highlightedPoint &&
  2254. chart.highlightedPoint.series.keyboardMoveVertical ?
  2255. 'highlightAdjacentPointVertical' :
  2256. 'highlightAdjacentSeries';
  2257. chart[highlightMethod](down);
  2258. return true;
  2259. }],
  2260. // Enter/Spacebar
  2261. [[13, 32], function () {
  2262. if (chart.highlightedPoint) {
  2263. chart.highlightedPoint.firePointEvent('click');
  2264. }
  2265. }]
  2266. ], {
  2267. // Always start highlighting from scratch when entering this module
  2268. init: function (dir) {
  2269. var numSeries = chart.series.length,
  2270. i = dir > 0 ? 0 : numSeries,
  2271. res;
  2272. if (dir > 0) {
  2273. delete chart.highlightedPoint;
  2274. // Find first valid point to highlight
  2275. while (i < numSeries) {
  2276. res = chart.series[i].highlightFirstValidPoint();
  2277. if (res) {
  2278. return res;
  2279. }
  2280. ++i;
  2281. }
  2282. } else {
  2283. // Find last valid point to highlight
  2284. while (i--) {
  2285. chart.highlightedPoint = chart.series[i].points[
  2286. chart.series[i].points.length - 1
  2287. ];
  2288. // Highlight first valid point in the series will also
  2289. // look backwards. It always starts from currently
  2290. // highlighted point.
  2291. res = chart.series[i].highlightFirstValidPoint();
  2292. if (res) {
  2293. return res;
  2294. }
  2295. }
  2296. }
  2297. },
  2298. // If leaving points, don't show tooltip anymore
  2299. terminate: function () {
  2300. if (chart.tooltip) {
  2301. chart.tooltip.hide(0);
  2302. }
  2303. delete chart.highlightedPoint;
  2304. }
  2305. }),
  2306. // Exporting
  2307. navModuleFactory('exporting', [
  2308. // Left/Up
  2309. [[37, 38], function () {
  2310. var i = chart.highlightedExportItem || 0,
  2311. reachedEnd = true;
  2312. // Try to highlight prev item in list. Highlighting e.g.
  2313. // separators will fail.
  2314. while (i--) {
  2315. if (chart.highlightExportItem(i)) {
  2316. reachedEnd = false;
  2317. break;
  2318. }
  2319. }
  2320. if (reachedEnd) {
  2321. chart.highlightLastExportItem();
  2322. return true;
  2323. }
  2324. }],
  2325. // Right/Down
  2326. [[39, 40], function () {
  2327. var highlightedExportItem = chart.highlightedExportItem || 0,
  2328. reachedEnd = true;
  2329. // Try to highlight next item in list. Highlighting e.g.
  2330. // separators will fail.
  2331. for (
  2332. var i = highlightedExportItem + 1;
  2333. i < chart.exportDivElements.length;
  2334. ++i
  2335. ) {
  2336. if (chart.highlightExportItem(i)) {
  2337. reachedEnd = false;
  2338. break;
  2339. }
  2340. }
  2341. if (reachedEnd) {
  2342. chart.highlightExportItem(0);
  2343. return true;
  2344. }
  2345. }],
  2346. // Enter/Spacebar
  2347. [[13, 32], function () {
  2348. fakeClickEvent(
  2349. chart.exportDivElements[chart.highlightedExportItem]
  2350. );
  2351. }]
  2352. ], {
  2353. // Only run exporting navigation if exporting support exists and is
  2354. // enabled on chart
  2355. validate: function () {
  2356. return (
  2357. chart.exportChart &&
  2358. !(
  2359. chart.options.exporting &&
  2360. chart.options.exporting.enabled === false
  2361. )
  2362. );
  2363. },
  2364. // Show export menu
  2365. init: function (direction) {
  2366. chart.highlightedPoint = null;
  2367. chart.showExportMenu();
  2368. // If coming back to export menu from other module, try to
  2369. // highlight last item in menu
  2370. if (direction < 0) {
  2371. chart.highlightLastExportItem();
  2372. }
  2373. },
  2374. // Hide the menu
  2375. terminate: function () {
  2376. chart.hideExportMenu();
  2377. }
  2378. }),
  2379. // Map zoom
  2380. navModuleFactory('mapZoom', [
  2381. // Up/down/left/right
  2382. [[38, 40, 37, 39], function (keyCode) {
  2383. chart[keyCode === 38 || keyCode === 40 ? 'yAxis' : 'xAxis'][0]
  2384. .panStep(keyCode < 39 ? -1 : 1);
  2385. }],
  2386. // Tabs
  2387. [[9], function (keyCode, e) {
  2388. var button;
  2389. // Deselect old
  2390. chart.mapNavButtons[chart.focusedMapNavButtonIx].setState(0);
  2391. if (
  2392. e.shiftKey && !chart.focusedMapNavButtonIx ||
  2393. !e.shiftKey && chart.focusedMapNavButtonIx
  2394. ) { // trying to go somewhere we can't?
  2395. chart.mapZoom(); // Reset zoom
  2396. // Nowhere to go, go to prev/next module
  2397. return this.move(e.shiftKey ? -1 : 1);
  2398. }
  2399. chart.focusedMapNavButtonIx += e.shiftKey ? -1 : 1;
  2400. button = chart.mapNavButtons[chart.focusedMapNavButtonIx];
  2401. chart.setFocusToElement(button.box, button);
  2402. button.setState(2);
  2403. }],
  2404. // Enter/Spacebar
  2405. [[13, 32], function () {
  2406. fakeClickEvent(
  2407. chart.mapNavButtons[chart.focusedMapNavButtonIx].element
  2408. );
  2409. }]
  2410. ], {
  2411. // Only run this module if we have map zoom on the chart
  2412. validate: function () {
  2413. return (
  2414. chart.mapZoom &&
  2415. chart.mapNavButtons &&
  2416. chart.mapNavButtons.length === 2
  2417. );
  2418. },
  2419. // Make zoom buttons do their magic
  2420. init: function (direction) {
  2421. var zoomIn = chart.mapNavButtons[0],
  2422. zoomOut = chart.mapNavButtons[1],
  2423. initialButton = direction > 0 ? zoomIn : zoomOut;
  2424. chart.mapNavButtons.forEach(function (button, i) {
  2425. button.element.setAttribute('tabindex', -1);
  2426. button.element.setAttribute('role', 'button');
  2427. button.element.setAttribute(
  2428. 'aria-label',
  2429. chart.langFormat(
  2430. 'accessibility.mapZoom' + (i ? 'Out' : 'In'),
  2431. { chart: chart }
  2432. )
  2433. );
  2434. });
  2435. chart.setFocusToElement(initialButton.box, initialButton);
  2436. initialButton.setState(2);
  2437. chart.focusedMapNavButtonIx = direction > 0 ? 0 : 1;
  2438. }
  2439. }),
  2440. // Highstock range selector (minus input boxes)
  2441. navModuleFactory('rangeSelector', [
  2442. // Left/Right/Up/Down
  2443. [[37, 39, 38, 40], function (keyCode) {
  2444. var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1;
  2445. // Try to highlight next/prev button
  2446. if (
  2447. !chart.highlightRangeSelectorButton(
  2448. chart.highlightedRangeSelectorItemIx + direction
  2449. )
  2450. ) {
  2451. return this.move(direction);
  2452. }
  2453. }],
  2454. // Enter/Spacebar
  2455. [[13, 32], function () {
  2456. // Don't allow click if button used to be disabled
  2457. if (chart.oldRangeSelectorItemState !== 3) {
  2458. fakeClickEvent(
  2459. chart.rangeSelector.buttons[
  2460. chart.highlightedRangeSelectorItemIx
  2461. ].element
  2462. );
  2463. }
  2464. }]
  2465. ], {
  2466. // Only run this module if we have range selector
  2467. validate: function () {
  2468. return (
  2469. chart.rangeSelector &&
  2470. chart.rangeSelector.buttons &&
  2471. chart.rangeSelector.buttons.length
  2472. );
  2473. },
  2474. // Make elements focusable and accessible
  2475. init: function (direction) {
  2476. chart.rangeSelector.buttons.forEach(function (button) {
  2477. button.element.setAttribute('tabindex', '-1');
  2478. button.element.setAttribute('role', 'button');
  2479. button.element.setAttribute(
  2480. 'aria-label',
  2481. chart.langFormat(
  2482. 'accessibility.rangeSelectorButton',
  2483. {
  2484. chart: chart,
  2485. buttonText: button.text && button.text.textStr
  2486. }
  2487. )
  2488. );
  2489. });
  2490. // Focus first/last button
  2491. chart.highlightRangeSelectorButton(
  2492. direction > 0 ? 0 : chart.rangeSelector.buttons.length - 1
  2493. );
  2494. }
  2495. }),
  2496. // Highstock range selector, input boxes
  2497. navModuleFactory('rangeSelectorInput', [
  2498. // Tab/Up/Down
  2499. [[9, 38, 40], function (keyCode, e) {
  2500. var direction =
  2501. (keyCode === 9 && e.shiftKey || keyCode === 38) ? -1 : 1,
  2502. newIx = chart.highlightedInputRangeIx =
  2503. chart.highlightedInputRangeIx + direction;
  2504. // Try to highlight next/prev item in list.
  2505. if (newIx > 1 || newIx < 0) { // Out of range
  2506. return this.move(direction);
  2507. }
  2508. chart.rangeSelector[newIx ? 'maxInput' : 'minInput'].focus();
  2509. }]
  2510. ], {
  2511. // Only run if we have range selector with input boxes
  2512. validate: function () {
  2513. var inputVisible = (
  2514. chart.rangeSelector &&
  2515. chart.rangeSelector.inputGroup &&
  2516. chart.rangeSelector.inputGroup.element
  2517. .getAttribute('visibility') !== 'hidden'
  2518. );
  2519. return (
  2520. inputVisible &&
  2521. chart.options.rangeSelector.inputEnabled !== false &&
  2522. chart.rangeSelector.minInput &&
  2523. chart.rangeSelector.maxInput
  2524. );
  2525. },
  2526. // Highlight first/last input box
  2527. init: function (direction) {
  2528. chart.highlightedInputRangeIx = direction > 0 ? 0 : 1;
  2529. chart.rangeSelector[
  2530. chart.highlightedInputRangeIx ? 'maxInput' : 'minInput'
  2531. ].focus();
  2532. }
  2533. }),
  2534. // Legend navigation
  2535. navModuleFactory('legend', [
  2536. // Left/Right/Up/Down
  2537. [[37, 39, 38, 40], function (keyCode) {
  2538. var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1;
  2539. // Try to highlight next/prev legend item
  2540. if (!chart.highlightLegendItem(
  2541. chart.highlightedLegendItemIx + direction
  2542. ) && chart.legend.allItems.length > 1) {
  2543. // Wrap around if more than 1 item
  2544. this.init(direction);
  2545. }
  2546. }],
  2547. // Enter/Spacebar
  2548. [[13, 32], function () {
  2549. var legendElement = chart.legend.allItems[
  2550. chart.highlightedLegendItemIx
  2551. ].legendItem.element;
  2552. fakeClickEvent(
  2553. !chart.legend.options.useHTML ? // #8561
  2554. legendElement.parentNode : legendElement
  2555. );
  2556. }]
  2557. ], {
  2558. // Only run this module if we have at least one legend - wait for
  2559. // it - item. Don't run if the legend is populated by a colorAxis.
  2560. // Don't run if legend navigation is disabled.
  2561. validate: function () {
  2562. return chart.legend && chart.legend.allItems &&
  2563. chart.legend.display &&
  2564. !(chart.colorAxis && chart.colorAxis.length) &&
  2565. (chart.options.legend &&
  2566. chart.options.legend.keyboardNavigation &&
  2567. chart.options.legend.keyboardNavigation.enabled) !== false;
  2568. },
  2569. // Make elements focusable and accessible
  2570. init: function (direction) {
  2571. chart.legend.allItems.forEach(function (item) {
  2572. item.legendGroup.element.setAttribute('tabindex', '-1');
  2573. item.legendGroup.element.setAttribute('role', 'button');
  2574. item.legendGroup.element.setAttribute(
  2575. 'aria-label',
  2576. chart.langFormat(
  2577. 'accessibility.legendItem',
  2578. {
  2579. chart: chart,
  2580. itemName: stripTags(item.name)
  2581. }
  2582. )
  2583. );
  2584. });
  2585. // Focus first/last item
  2586. chart.highlightLegendItem(
  2587. direction > 0 ? 0 : chart.legend.allItems.length - 1
  2588. );
  2589. }
  2590. })
  2591. ];
  2592. };
  2593. /**
  2594. * Add exit anchor to the chart. We use this to move focus out of chart whenever
  2595. * we want, by setting focus to this div and not preventing the default tab
  2596. * action. We also use this when users come back into the chart by tabbing back,
  2597. * in order to navigate from the end of the chart.
  2598. *
  2599. * @private
  2600. * @function Highcharts.Chart#addExitAnchor
  2601. *
  2602. * @return {Function}
  2603. * Returns the unbind function for the exit anchor's event handler.
  2604. */
  2605. H.Chart.prototype.addExitAnchor = function () {
  2606. var chart = this;
  2607. chart.tabExitAnchor = doc.createElement('div');
  2608. chart.tabExitAnchor.setAttribute('tabindex', '0');
  2609. // Hide exit anchor
  2610. merge(true, chart.tabExitAnchor.style, {
  2611. position: 'absolute',
  2612. left: '-9999px',
  2613. top: 'auto',
  2614. width: '1px',
  2615. height: '1px',
  2616. overflow: 'hidden'
  2617. });
  2618. chart.renderTo.appendChild(chart.tabExitAnchor);
  2619. return addEvent(
  2620. chart.tabExitAnchor,
  2621. 'focus',
  2622. function (ev) {
  2623. var e = ev || win.event,
  2624. curModule;
  2625. // If focusing and we are exiting, do nothing once.
  2626. if (!chart.exiting) {
  2627. // Not exiting, means we are coming in backwards
  2628. chart.renderTo.focus();
  2629. e.preventDefault();
  2630. // Move to last valid keyboard nav module
  2631. // Note the we don't run it, just set the index
  2632. chart.keyboardNavigationModuleIndex =
  2633. chart.keyboardNavigationModules.length - 1;
  2634. curModule = chart.keyboardNavigationModules[
  2635. chart.keyboardNavigationModuleIndex
  2636. ];
  2637. // Validate the module
  2638. if (curModule.validate && !curModule.validate()) {
  2639. // Invalid.
  2640. // Move inits next valid module in direction
  2641. curModule.move(-1);
  2642. } else {
  2643. // We have a valid module, init it
  2644. curModule.init(-1);
  2645. }
  2646. } else {
  2647. // Don't skip the next focus, we only skip once.
  2648. chart.exiting = false;
  2649. }
  2650. }
  2651. );
  2652. };
  2653. /**
  2654. * Clear the chart and reset the navigation state.
  2655. *
  2656. * @private
  2657. * @function Highcharts.Chart#resetKeyboardNavigation
  2658. */
  2659. H.Chart.prototype.resetKeyboardNavigation = function () {
  2660. var chart = this,
  2661. curMod = (
  2662. chart.keyboardNavigationModules &&
  2663. chart.keyboardNavigationModules[
  2664. chart.keyboardNavigationModuleIndex || 0
  2665. ]
  2666. );
  2667. if (curMod && curMod.terminate) {
  2668. curMod.terminate();
  2669. }
  2670. if (chart.focusElement) {
  2671. chart.focusElement.removeFocusBorder();
  2672. }
  2673. chart.keyboardNavigationModuleIndex = 0;
  2674. chart.keyboardReset = true;
  2675. };
  2676. // On destroy, we need to clean up the focus border and the state.
  2677. H.addEvent(H.Series, 'destroy', function () {
  2678. var chart = this.chart;
  2679. if (chart.highlightedPoint && chart.highlightedPoint.series === this) {
  2680. delete chart.highlightedPoint;
  2681. if (chart.focusElement) {
  2682. chart.focusElement.removeFocusBorder();
  2683. }
  2684. }
  2685. });
  2686. // Add keyboard navigation events on chart load.
  2687. H.Chart.prototype.callbacks.push(function (chart) {
  2688. var a11yOptions = chart.options.accessibility;
  2689. if (a11yOptions.enabled && a11yOptions.keyboardNavigation.enabled) {
  2690. // Init nav modules. We start at the first module, and as the user
  2691. // navigates through the chart the index will increase to use different
  2692. // handler modules.
  2693. chart.addKeyboardNavigationModules();
  2694. chart.keyboardNavigationModuleIndex = 0;
  2695. // Make chart container reachable by tab
  2696. if (
  2697. chart.container.hasAttribute &&
  2698. !chart.container.hasAttribute('tabIndex')
  2699. ) {
  2700. chart.container.setAttribute('tabindex', '0');
  2701. }
  2702. // Add tab exit anchor
  2703. if (!chart.tabExitAnchor) {
  2704. chart.unbindExitAnchorFocus = chart.addExitAnchor();
  2705. }
  2706. // Handle keyboard events by routing them to active keyboard nav module
  2707. chart.unbindKeydownHandler = addEvent(chart.renderTo, 'keydown',
  2708. function (ev) {
  2709. var e = ev || win.event,
  2710. curNavModule = chart.keyboardNavigationModules[
  2711. chart.keyboardNavigationModuleIndex
  2712. ];
  2713. chart.keyboardReset = false;
  2714. // If there is a nav module for the current index, run it.
  2715. // Otherwise, we are outside of the chart in some direction.
  2716. if (curNavModule) {
  2717. if (curNavModule.run(e)) {
  2718. // Successfully handled this key event, stop default
  2719. e.preventDefault();
  2720. }
  2721. }
  2722. });
  2723. // Reset chart navigation state if we click outside the chart and it's
  2724. // not already reset
  2725. chart.unbindBlurHandler = addEvent(doc, 'mouseup', function () {
  2726. if (
  2727. !chart.keyboardReset &&
  2728. !(chart.pointer && chart.pointer.chartPosition)
  2729. ) {
  2730. chart.resetKeyboardNavigation();
  2731. }
  2732. });
  2733. // Add cleanup handlers
  2734. addEvent(chart, 'destroy', function () {
  2735. chart.resetKeyboardNavigation();
  2736. if (chart.unbindExitAnchorFocus && chart.tabExitAnchor) {
  2737. chart.unbindExitAnchorFocus();
  2738. }
  2739. if (chart.unbindKeydownHandler && chart.renderTo) {
  2740. chart.unbindKeydownHandler();
  2741. }
  2742. if (chart.unbindBlurHandler) {
  2743. chart.unbindBlurHandler();
  2744. }
  2745. });
  2746. }
  2747. });
  2748. }(Highcharts));
  2749. return (function () {
  2750. }());
  2751. }));