NewDataAnnouncer.js 9.2 KB

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