NewDataAnnouncer.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. /* *
  2. *
  3. * (c) 2009-2021 Øystein Moseng
  4. *
  5. * Handle announcing new data for a chart.
  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 H from '../../../Core/Globals.js';
  14. import Series from '../../../Core/Series/Series.js';
  15. import U from '../../../Core/Utilities.js';
  16. var extend = U.extend, defined = U.defined;
  17. import ChartUtilities from '../../Utils/ChartUtilities.js';
  18. var getChartTitle = ChartUtilities.getChartTitle;
  19. import SeriesDescriber from './SeriesDescriber.js';
  20. var defaultPointDescriptionFormatter = SeriesDescriber
  21. .defaultPointDescriptionFormatter, defaultSeriesDescriptionFormatter = SeriesDescriber
  22. .defaultSeriesDescriptionFormatter;
  23. import Announcer from '../../Utils/Announcer.js';
  24. import EventProvider from '../../Utils/EventProvider.js';
  25. /* eslint-disable no-invalid-this, valid-jsdoc */
  26. /**
  27. * @private
  28. */
  29. function chartHasAnnounceEnabled(chart) {
  30. return !!chart.options.accessibility.announceNewData.enabled;
  31. }
  32. /**
  33. * @private
  34. */
  35. function findPointInDataArray(point) {
  36. var candidates = point.series.data.filter(function (candidate) {
  37. return point.x === candidate.x && point.y === candidate.y;
  38. });
  39. return candidates.length === 1 ? candidates[0] : point;
  40. }
  41. /**
  42. * Get array of unique series from two arrays
  43. * @private
  44. */
  45. function getUniqueSeries(arrayA, arrayB) {
  46. var uniqueSeries = (arrayA || []).concat(arrayB || [])
  47. .reduce(function (acc, cur) {
  48. acc[cur.name + cur.index] = cur;
  49. return acc;
  50. }, {});
  51. return Object.keys(uniqueSeries).map(function (ix) {
  52. return uniqueSeries[ix];
  53. });
  54. }
  55. /**
  56. * @private
  57. * @class
  58. */
  59. var NewDataAnnouncer = function (chart) {
  60. this.chart = chart;
  61. };
  62. extend(NewDataAnnouncer.prototype, {
  63. /**
  64. * Initialize the new data announcer.
  65. * @private
  66. */
  67. init: function () {
  68. var chart = this.chart;
  69. var announceOptions = chart.options.accessibility.announceNewData;
  70. var announceType = announceOptions.interruptUser ? 'assertive' : 'polite';
  71. this.lastAnnouncementTime = 0;
  72. this.dirty = {
  73. allSeries: {}
  74. };
  75. this.eventProvider = new EventProvider();
  76. this.announcer = new Announcer(chart, announceType);
  77. this.addEventListeners();
  78. },
  79. /**
  80. * Remove traces of announcer.
  81. * @private
  82. */
  83. destroy: function () {
  84. this.eventProvider.removeAddedEvents();
  85. this.announcer.destroy();
  86. },
  87. /**
  88. * Add event listeners for the announcer
  89. * @private
  90. */
  91. addEventListeners: function () {
  92. var announcer = this, chart = this.chart, e = this.eventProvider;
  93. e.addEvent(chart, 'afterDrilldown', function () {
  94. announcer.lastAnnouncementTime = 0;
  95. });
  96. e.addEvent(Series, 'updatedData', function () {
  97. announcer.onSeriesUpdatedData(this);
  98. });
  99. e.addEvent(chart, 'afterAddSeries', function (e) {
  100. announcer.onSeriesAdded(e.series);
  101. });
  102. e.addEvent(Series, 'addPoint', function (e) {
  103. announcer.onPointAdded(e.point);
  104. });
  105. e.addEvent(chart, 'redraw', function () {
  106. announcer.announceDirtyData();
  107. });
  108. },
  109. /**
  110. * On new data in the series, make sure we add it to the dirty list.
  111. * @private
  112. * @param {Highcharts.Series} series
  113. */
  114. onSeriesUpdatedData: function (series) {
  115. var chart = this.chart;
  116. if (series.chart === chart && chartHasAnnounceEnabled(chart)) {
  117. this.dirty.hasDirty = true;
  118. this.dirty.allSeries[series.name + series.index] = series;
  119. }
  120. },
  121. /**
  122. * On new data series added, update dirty list.
  123. * @private
  124. * @param {Highcharts.Series} series
  125. */
  126. onSeriesAdded: function (series) {
  127. if (chartHasAnnounceEnabled(this.chart)) {
  128. this.dirty.hasDirty = true;
  129. this.dirty.allSeries[series.name + series.index] = series;
  130. // Add it to newSeries storage unless we already have one
  131. this.dirty.newSeries = defined(this.dirty.newSeries) ?
  132. void 0 : series;
  133. }
  134. },
  135. /**
  136. * On new point added, update dirty list.
  137. * @private
  138. * @param {Highcharts.Point} point
  139. */
  140. onPointAdded: function (point) {
  141. var chart = point.series.chart;
  142. if (this.chart === chart && chartHasAnnounceEnabled(chart)) {
  143. // Add it to newPoint storage unless we already have one
  144. this.dirty.newPoint = defined(this.dirty.newPoint) ?
  145. void 0 : point;
  146. }
  147. },
  148. /**
  149. * Gather what we know and announce the data to user.
  150. * @private
  151. */
  152. announceDirtyData: function () {
  153. var chart = this.chart, announcer = this;
  154. if (chart.options.accessibility.announceNewData &&
  155. this.dirty.hasDirty) {
  156. var newPoint = this.dirty.newPoint;
  157. // If we have a single new point, see if we can find it in the
  158. // data array. Otherwise we can only pass through options to
  159. // the description builder, and it is a bit sparse in info.
  160. if (newPoint) {
  161. newPoint = findPointInDataArray(newPoint);
  162. }
  163. this.queueAnnouncement(Object.keys(this.dirty.allSeries).map(function (ix) {
  164. return announcer.dirty.allSeries[ix];
  165. }), this.dirty.newSeries, newPoint);
  166. // Reset
  167. this.dirty = {
  168. allSeries: {}
  169. };
  170. }
  171. },
  172. /**
  173. * Announce to user that there is new data.
  174. * @private
  175. * @param {Array<Highcharts.Series>} dirtySeries
  176. * Array of series with new data.
  177. * @param {Highcharts.Series} [newSeries]
  178. * If a single new series was added, a reference to this series.
  179. * @param {Highcharts.Point} [newPoint]
  180. * If a single point was added, a reference to this point.
  181. */
  182. queueAnnouncement: function (dirtySeries, newSeries, newPoint) {
  183. var _this = this;
  184. var chart = this.chart;
  185. var annOptions = chart.options.accessibility.announceNewData;
  186. if (annOptions.enabled) {
  187. var now = +new Date();
  188. var dTime = now - this.lastAnnouncementTime;
  189. var time = Math.max(0, annOptions.minAnnounceInterval - dTime);
  190. // Add series from previously queued announcement.
  191. var allSeries = getUniqueSeries(this.queuedAnnouncement && this.queuedAnnouncement.series, dirtySeries);
  192. // Build message and announce
  193. var message = this.buildAnnouncementMessage(allSeries, newSeries, newPoint);
  194. if (message) {
  195. // Is there already one queued?
  196. if (this.queuedAnnouncement) {
  197. clearTimeout(this.queuedAnnouncementTimer);
  198. }
  199. // Build the announcement
  200. this.queuedAnnouncement = {
  201. time: now,
  202. message: message,
  203. series: allSeries
  204. };
  205. // Queue the announcement
  206. this.queuedAnnouncementTimer = setTimeout(function () {
  207. if (_this && _this.announcer) {
  208. _this.lastAnnouncementTime = +new Date();
  209. _this.announcer.announce(_this.queuedAnnouncement.message);
  210. delete _this.queuedAnnouncement;
  211. delete _this.queuedAnnouncementTimer;
  212. }
  213. }, time);
  214. }
  215. }
  216. },
  217. /**
  218. * Get announcement message for new data.
  219. * @private
  220. * @param {Array<Highcharts.Series>} dirtySeries
  221. * Array of series with new data.
  222. * @param {Highcharts.Series} [newSeries]
  223. * If a single new series was added, a reference to this series.
  224. * @param {Highcharts.Point} [newPoint]
  225. * If a single point was added, a reference to this point.
  226. *
  227. * @return {string|null}
  228. * The announcement message to give to user.
  229. */
  230. buildAnnouncementMessage: function (dirtySeries, newSeries, newPoint) {
  231. var chart = this.chart, annOptions = chart.options.accessibility.announceNewData;
  232. // User supplied formatter?
  233. if (annOptions.announcementFormatter) {
  234. var formatterRes = annOptions.announcementFormatter(dirtySeries, newSeries, newPoint);
  235. if (formatterRes !== false) {
  236. return formatterRes.length ? formatterRes : null;
  237. }
  238. }
  239. // Default formatter - use lang options
  240. var multiple = H.charts && H.charts.length > 1 ? 'Multiple' : 'Single', langKey = newSeries ? 'newSeriesAnnounce' + multiple :
  241. newPoint ? 'newPointAnnounce' + multiple : 'newDataAnnounce', chartTitle = getChartTitle(chart);
  242. return chart.langFormat('accessibility.announceNewData.' + langKey, {
  243. chartTitle: chartTitle,
  244. seriesDesc: newSeries ?
  245. defaultSeriesDescriptionFormatter(newSeries) :
  246. null,
  247. pointDesc: newPoint ?
  248. defaultPointDescriptionFormatter(newPoint) :
  249. null,
  250. point: newPoint,
  251. series: newSeries
  252. });
  253. }
  254. });
  255. export default NewDataAnnouncer;