SeriesDescriber.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. /* *
  2. *
  3. * (c) 2009-2021 Øystein Moseng
  4. *
  5. * Place desriptions on a series and its points.
  6. *
  7. * License: www.highcharts.com/license
  8. *
  9. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  10. *
  11. * */
  12. 'use strict';
  13. import AnnotationsA11y from '../AnnotationsA11y.js';
  14. var getPointAnnotationTexts = AnnotationsA11y.getPointAnnotationTexts;
  15. import ChartUtilities from '../../Utils/ChartUtilities.js';
  16. var getAxisDescription = ChartUtilities.getAxisDescription, getSeriesFirstPointElement = ChartUtilities.getSeriesFirstPointElement, getSeriesA11yElement = ChartUtilities.getSeriesA11yElement, unhideChartElementFromAT = ChartUtilities.unhideChartElementFromAT;
  17. import F from '../../../Core/FormatUtilities.js';
  18. var format = F.format, numberFormat = F.numberFormat;
  19. import HTMLUtilities from '../../Utils/HTMLUtilities.js';
  20. var reverseChildNodes = HTMLUtilities.reverseChildNodes, stripHTMLTags = HTMLUtilities.stripHTMLTagsFromString;
  21. import Tooltip from '../../../Core/Tooltip.js';
  22. import U from '../../../Core/Utilities.js';
  23. var find = U.find, isNumber = U.isNumber, pick = U.pick, defined = U.defined;
  24. /* eslint-disable valid-jsdoc */
  25. /**
  26. * @private
  27. */
  28. function findFirstPointWithGraphic(point) {
  29. var sourcePointIndex = point.index;
  30. if (!point.series || !point.series.data || !defined(sourcePointIndex)) {
  31. return null;
  32. }
  33. return find(point.series.data, function (p) {
  34. return !!(p &&
  35. typeof p.index !== 'undefined' &&
  36. p.index > sourcePointIndex &&
  37. p.graphic &&
  38. p.graphic.element);
  39. }) || null;
  40. }
  41. /**
  42. * @private
  43. */
  44. function shouldAddDummyPoint(point) {
  45. // Note: Sunburst series use isNull for hidden points on drilldown.
  46. // Ignore these.
  47. var isSunburst = point.series && point.series.is('sunburst'), isNull = point.isNull;
  48. return isNull && !isSunburst;
  49. }
  50. /**
  51. * @private
  52. */
  53. function makeDummyElement(point, pos) {
  54. var renderer = point.series.chart.renderer, dummy = renderer.rect(pos.x, pos.y, 1, 1);
  55. dummy.attr({
  56. 'class': 'highcharts-a11y-dummy-point',
  57. fill: 'none',
  58. opacity: 0,
  59. 'fill-opacity': 0,
  60. 'stroke-opacity': 0
  61. });
  62. return dummy;
  63. }
  64. /**
  65. * @private
  66. * @param {Highcharts.Point} point
  67. * @return {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement|undefined}
  68. */
  69. function addDummyPointElement(point) {
  70. var series = point.series, firstPointWithGraphic = findFirstPointWithGraphic(point), firstGraphic = firstPointWithGraphic && firstPointWithGraphic.graphic, parentGroup = firstGraphic ?
  71. firstGraphic.parentGroup :
  72. series.graph || series.group, dummyPos = firstPointWithGraphic ? {
  73. x: pick(point.plotX, firstPointWithGraphic.plotX, 0),
  74. y: pick(point.plotY, firstPointWithGraphic.plotY, 0)
  75. } : {
  76. x: pick(point.plotX, 0),
  77. y: pick(point.plotY, 0)
  78. }, dummyElement = makeDummyElement(point, dummyPos);
  79. if (parentGroup && parentGroup.element) {
  80. point.graphic = dummyElement;
  81. point.hasDummyGraphic = true;
  82. dummyElement.add(parentGroup);
  83. // Move to correct pos in DOM
  84. parentGroup.element.insertBefore(dummyElement.element, firstGraphic ? firstGraphic.element : null);
  85. return dummyElement.element;
  86. }
  87. }
  88. /**
  89. * @private
  90. * @param {Highcharts.Series} series
  91. * @return {boolean}
  92. */
  93. function hasMorePointsThanDescriptionThreshold(series) {
  94. var chartA11yOptions = series.chart.options.accessibility, threshold = (chartA11yOptions.series.pointDescriptionEnabledThreshold);
  95. return !!(threshold !== false &&
  96. series.points &&
  97. series.points.length >= threshold);
  98. }
  99. /**
  100. * @private
  101. * @param {Highcharts.Series} series
  102. * @return {boolean}
  103. */
  104. function shouldSetScreenReaderPropsOnPoints(series) {
  105. var seriesA11yOptions = series.options.accessibility || {};
  106. return !hasMorePointsThanDescriptionThreshold(series) &&
  107. !seriesA11yOptions.exposeAsGroupOnly;
  108. }
  109. /**
  110. * @private
  111. * @param {Highcharts.Series} series
  112. * @return {boolean}
  113. */
  114. function shouldSetKeyboardNavPropsOnPoints(series) {
  115. var chartA11yOptions = series.chart.options.accessibility, seriesNavOptions = chartA11yOptions.keyboardNavigation.seriesNavigation;
  116. return !!(series.points && (series.points.length <
  117. seriesNavOptions.pointNavigationEnabledThreshold ||
  118. seriesNavOptions.pointNavigationEnabledThreshold === false));
  119. }
  120. /**
  121. * @private
  122. * @param {Highcharts.Series} series
  123. * @return {boolean}
  124. */
  125. function shouldDescribeSeriesElement(series) {
  126. var chart = series.chart, chartOptions = chart.options.chart, chartHas3d = chartOptions.options3d && chartOptions.options3d.enabled, hasMultipleSeries = chart.series.length > 1, describeSingleSeriesOption = chart.options.accessibility.series.describeSingleSeries, exposeAsGroupOnlyOption = (series.options.accessibility || {}).exposeAsGroupOnly, noDescribe3D = chartHas3d && hasMultipleSeries;
  127. return !noDescribe3D && (hasMultipleSeries || describeSingleSeriesOption ||
  128. exposeAsGroupOnlyOption || hasMorePointsThanDescriptionThreshold(series));
  129. }
  130. /**
  131. * @private
  132. * @param {Highcharts.Point} point
  133. * @param {number} value
  134. * @return {string}
  135. */
  136. function pointNumberToString(point, value) {
  137. var chart = point.series.chart, a11yPointOptions = chart.options.accessibility.point || {}, tooltipOptions = point.series.tooltipOptions || {}, lang = chart.options.lang;
  138. if (isNumber(value)) {
  139. return numberFormat(value, a11yPointOptions.valueDecimals ||
  140. tooltipOptions.valueDecimals ||
  141. -1, lang.decimalPoint, lang.accessibility.thousandsSep || lang.thousandsSep);
  142. }
  143. return value;
  144. }
  145. /**
  146. * @private
  147. * @param {Highcharts.Series} series
  148. * @return {string}
  149. */
  150. function getSeriesDescriptionText(series) {
  151. var seriesA11yOptions = series.options.accessibility || {}, descOpt = seriesA11yOptions.description;
  152. return descOpt && series.chart.langFormat('accessibility.series.description', {
  153. description: descOpt,
  154. series: series
  155. }) || '';
  156. }
  157. /**
  158. * @private
  159. * @param {Highcharts.series} series
  160. * @param {string} axisCollection
  161. * @return {string}
  162. */
  163. function getSeriesAxisDescriptionText(series, axisCollection) {
  164. var axis = series[axisCollection];
  165. return series.chart.langFormat('accessibility.series.' + axisCollection + 'Description', {
  166. name: getAxisDescription(axis),
  167. series: series
  168. });
  169. }
  170. /**
  171. * Get accessible time description for a point on a datetime axis.
  172. *
  173. * @private
  174. * @function Highcharts.Point#getTimeDescription
  175. * @param {Highcharts.Point} point
  176. * @return {string|undefined}
  177. * The description as string.
  178. */
  179. function getPointA11yTimeDescription(point) {
  180. var series = point.series, chart = series.chart, a11yOptions = chart.options.accessibility.point || {}, hasDateXAxis = series.xAxis && series.xAxis.dateTime;
  181. if (hasDateXAxis) {
  182. var tooltipDateFormat = Tooltip.prototype.getXDateFormat.call({
  183. getDateFormat: Tooltip.prototype.getDateFormat,
  184. chart: chart
  185. }, point, chart.options.tooltip, series.xAxis), dateFormat = a11yOptions.dateFormatter &&
  186. a11yOptions.dateFormatter(point) ||
  187. a11yOptions.dateFormat ||
  188. tooltipDateFormat;
  189. return chart.time.dateFormat(dateFormat, point.x, void 0);
  190. }
  191. }
  192. /**
  193. * @private
  194. * @param {Highcharts.Point} point
  195. * @return {string}
  196. */
  197. function getPointXDescription(point) {
  198. var timeDesc = getPointA11yTimeDescription(point), xAxis = point.series.xAxis || {}, pointCategory = xAxis.categories && defined(point.category) &&
  199. ('' + point.category).replace('<br/>', ' '), canUseId = point.id && point.id.indexOf('highcharts-') < 0, fallback = 'x, ' + point.x;
  200. return point.name || timeDesc || pointCategory ||
  201. (canUseId ? point.id : fallback);
  202. }
  203. /**
  204. * @private
  205. * @param {Highcharts.Point} point
  206. * @param {string} prefix
  207. * @param {string} suffix
  208. * @return {string}
  209. */
  210. function getPointArrayMapValueDescription(point, prefix, suffix) {
  211. var pre = prefix || '', suf = suffix || '', keyToValStr = function (key) {
  212. var num = pointNumberToString(point, pick(point[key], point.options[key]));
  213. return key + ': ' + pre + num + suf;
  214. }, pointArrayMap = point.series.pointArrayMap;
  215. return pointArrayMap.reduce(function (desc, key) {
  216. return desc + (desc.length ? ', ' : '') + keyToValStr(key);
  217. }, '');
  218. }
  219. /**
  220. * @private
  221. * @param {Highcharts.Point} point
  222. * @return {string}
  223. */
  224. function getPointValue(point) {
  225. var series = point.series, a11yPointOpts = series.chart.options.accessibility.point || {}, tooltipOptions = series.tooltipOptions || {}, valuePrefix = a11yPointOpts.valuePrefix ||
  226. tooltipOptions.valuePrefix || '', valueSuffix = a11yPointOpts.valueSuffix ||
  227. tooltipOptions.valueSuffix || '', fallbackKey = (typeof point.value !==
  228. 'undefined' ?
  229. 'value' : 'y'), fallbackDesc = pointNumberToString(point, point[fallbackKey]);
  230. if (point.isNull) {
  231. return series.chart.langFormat('accessibility.series.nullPointValue', {
  232. point: point
  233. });
  234. }
  235. if (series.pointArrayMap) {
  236. return getPointArrayMapValueDescription(point, valuePrefix, valueSuffix);
  237. }
  238. return valuePrefix + fallbackDesc + valueSuffix;
  239. }
  240. /**
  241. * Return the description for the annotation(s) connected to a point, or empty
  242. * string if none.
  243. *
  244. * @private
  245. * @param {Highcharts.Point} point The data point to get the annotation info from.
  246. * @return {string} Annotation description
  247. */
  248. function getPointAnnotationDescription(point) {
  249. var chart = point.series.chart;
  250. var langKey = 'accessibility.series.pointAnnotationsDescription';
  251. var annotations = getPointAnnotationTexts(point);
  252. var context = { point: point, annotations: annotations };
  253. return annotations.length ? chart.langFormat(langKey, context) : '';
  254. }
  255. /**
  256. * Return string with information about point.
  257. * @private
  258. * @return {string}
  259. */
  260. function getPointValueDescription(point) {
  261. var series = point.series, chart = series.chart, pointValueDescriptionFormat = chart.options.accessibility
  262. .point.valueDescriptionFormat, showXDescription = pick(series.xAxis &&
  263. series.xAxis.options.accessibility &&
  264. series.xAxis.options.accessibility.enabled, !chart.angular), xDesc = showXDescription ? getPointXDescription(point) : '', context = {
  265. point: point,
  266. index: defined(point.index) ? (point.index + 1) : '',
  267. xDescription: xDesc,
  268. value: getPointValue(point),
  269. separator: showXDescription ? ', ' : ''
  270. };
  271. return format(pointValueDescriptionFormat, context, chart);
  272. }
  273. /**
  274. * Return string with information about point.
  275. * @private
  276. * @return {string}
  277. */
  278. function defaultPointDescriptionFormatter(point) {
  279. var series = point.series, chart = series.chart, valText = getPointValueDescription(point), description = point.options && point.options.accessibility &&
  280. point.options.accessibility.description, userDescText = description ? ' ' + description : '', seriesNameText = chart.series.length > 1 && series.name ?
  281. ' ' + series.name + '.' : '', annotationsDesc = getPointAnnotationDescription(point), pointAnnotationsText = annotationsDesc ? ' ' + annotationsDesc : '';
  282. point.accessibility = point.accessibility || {};
  283. point.accessibility.valueDescription = valText;
  284. return valText + userDescText + seriesNameText + pointAnnotationsText;
  285. }
  286. /**
  287. * Set a11y props on a point element
  288. * @private
  289. * @param {Highcharts.Point} point
  290. * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} pointElement
  291. */
  292. function setPointScreenReaderAttribs(point, pointElement) {
  293. var series = point.series, a11yPointOptions = series.chart.options.accessibility.point || {}, seriesA11yOptions = series.options.accessibility || {}, label = stripHTMLTags(seriesA11yOptions.pointDescriptionFormatter &&
  294. seriesA11yOptions.pointDescriptionFormatter(point) ||
  295. a11yPointOptions.descriptionFormatter &&
  296. a11yPointOptions.descriptionFormatter(point) ||
  297. defaultPointDescriptionFormatter(point));
  298. pointElement.setAttribute('role', 'img');
  299. pointElement.setAttribute('aria-label', label);
  300. }
  301. /**
  302. * Add accessible info to individual point elements of a series
  303. * @private
  304. * @param {Highcharts.Series} series
  305. */
  306. function describePointsInSeries(series) {
  307. var setScreenReaderProps = shouldSetScreenReaderPropsOnPoints(series), setKeyboardProps = shouldSetKeyboardNavPropsOnPoints(series);
  308. if (setScreenReaderProps || setKeyboardProps) {
  309. series.points.forEach(function (point) {
  310. var pointEl = point.graphic && point.graphic.element ||
  311. shouldAddDummyPoint(point) && addDummyPointElement(point);
  312. var pointA11yDisabled = (point.options &&
  313. point.options.accessibility &&
  314. point.options.accessibility.enabled === false);
  315. if (pointEl) {
  316. // We always set tabindex, as long as we are setting props.
  317. // When setting tabindex, also remove default outline to
  318. // avoid ugly border on click.
  319. pointEl.setAttribute('tabindex', '-1');
  320. pointEl.style.outline = '0';
  321. if (setScreenReaderProps && !pointA11yDisabled) {
  322. setPointScreenReaderAttribs(point, pointEl);
  323. }
  324. else {
  325. pointEl.setAttribute('aria-hidden', true);
  326. }
  327. }
  328. });
  329. }
  330. }
  331. /**
  332. * Return string with information about series.
  333. * @private
  334. * @return {string}
  335. */
  336. function defaultSeriesDescriptionFormatter(series) {
  337. var chart = series.chart, chartTypes = chart.types || [], description = getSeriesDescriptionText(series), shouldDescribeAxis = function (coll) {
  338. return chart[coll] && chart[coll].length > 1 && series[coll];
  339. }, xAxisInfo = getSeriesAxisDescriptionText(series, 'xAxis'), yAxisInfo = getSeriesAxisDescriptionText(series, 'yAxis'), summaryContext = {
  340. name: series.name || '',
  341. ix: series.index + 1,
  342. numSeries: chart.series && chart.series.length,
  343. numPoints: series.points && series.points.length,
  344. series: series
  345. }, combinationSuffix = chartTypes.length > 1 ? 'Combination' : '', summary = chart.langFormat('accessibility.series.summary.' + series.type + combinationSuffix, summaryContext) || chart.langFormat('accessibility.series.summary.default' + combinationSuffix, summaryContext);
  346. return summary + (description ? ' ' + description : '') + (shouldDescribeAxis('yAxis') ? ' ' + yAxisInfo : '') + (shouldDescribeAxis('xAxis') ? ' ' + xAxisInfo : '');
  347. }
  348. /**
  349. * Set a11y props on a series element
  350. * @private
  351. * @param {Highcharts.Series} series
  352. * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} seriesElement
  353. */
  354. function describeSeriesElement(series, seriesElement) {
  355. var seriesA11yOptions = series.options.accessibility || {}, a11yOptions = series.chart.options.accessibility, landmarkVerbosity = a11yOptions.landmarkVerbosity;
  356. // Handle role attribute
  357. if (seriesA11yOptions.exposeAsGroupOnly) {
  358. seriesElement.setAttribute('role', 'img');
  359. }
  360. else if (landmarkVerbosity === 'all') {
  361. seriesElement.setAttribute('role', 'region');
  362. } /* else do not add role */
  363. seriesElement.setAttribute('tabindex', '-1');
  364. seriesElement.style.outline = '0'; // Don't show browser outline on click, despite tabindex
  365. seriesElement.setAttribute('aria-label', stripHTMLTags(a11yOptions.series.descriptionFormatter &&
  366. a11yOptions.series.descriptionFormatter(series) ||
  367. defaultSeriesDescriptionFormatter(series)));
  368. }
  369. /**
  370. * Put accessible info on series and points of a series.
  371. * @param {Highcharts.Series} series The series to add info on.
  372. */
  373. function describeSeries(series) {
  374. var chart = series.chart, firstPointEl = getSeriesFirstPointElement(series), seriesEl = getSeriesA11yElement(series), is3d = chart.is3d && chart.is3d();
  375. if (seriesEl) {
  376. // For some series types the order of elements do not match the
  377. // order of points in series. In that case we have to reverse them
  378. // in order for AT to read them out in an understandable order.
  379. // Due to z-index issues we can not do this for 3D charts.
  380. if (seriesEl.lastChild === firstPointEl && !is3d) {
  381. reverseChildNodes(seriesEl);
  382. }
  383. describePointsInSeries(series);
  384. unhideChartElementFromAT(chart, seriesEl);
  385. if (shouldDescribeSeriesElement(series)) {
  386. describeSeriesElement(series, seriesEl);
  387. }
  388. else {
  389. seriesEl.setAttribute('aria-label', '');
  390. }
  391. }
  392. }
  393. var SeriesDescriber = {
  394. describeSeries: describeSeries,
  395. defaultPointDescriptionFormatter: defaultPointDescriptionFormatter,
  396. defaultSeriesDescriptionFormatter: defaultSeriesDescriptionFormatter,
  397. getPointA11yTimeDescription: getPointA11yTimeDescription,
  398. getPointXDescription: getPointXDescription,
  399. getPointValue: getPointValue,
  400. getPointValueDescription: getPointValueDescription
  401. };
  402. export default SeriesDescriber;