255 lines
9.2 KiB
JavaScript
255 lines
9.2 KiB
JavaScript
|
/* *
|
||
|
*
|
||
|
* (c) 2009-2020 Øystein Moseng
|
||
|
*
|
||
|
* Handle announcing new data for a chart.
|
||
|
*
|
||
|
* License: www.highcharts.com/license
|
||
|
*
|
||
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||
|
*
|
||
|
* */
|
||
|
'use strict';
|
||
|
import H from '../../../../parts/Globals.js';
|
||
|
import U from '../../../../parts/Utilities.js';
|
||
|
var extend = U.extend, defined = U.defined;
|
||
|
import ChartUtilities from '../../utils/chartUtilities.js';
|
||
|
var getChartTitle = ChartUtilities.getChartTitle;
|
||
|
import SeriesDescriber from './SeriesDescriber.js';
|
||
|
var defaultPointDescriptionFormatter = SeriesDescriber
|
||
|
.defaultPointDescriptionFormatter, defaultSeriesDescriptionFormatter = SeriesDescriber
|
||
|
.defaultSeriesDescriptionFormatter;
|
||
|
import Announcer from '../../utils/Announcer.js';
|
||
|
import EventProvider from '../../utils/EventProvider.js';
|
||
|
/* eslint-disable no-invalid-this, valid-jsdoc */
|
||
|
/**
|
||
|
* @private
|
||
|
*/
|
||
|
function chartHasAnnounceEnabled(chart) {
|
||
|
return !!chart.options.accessibility.announceNewData.enabled;
|
||
|
}
|
||
|
/**
|
||
|
* @private
|
||
|
*/
|
||
|
function findPointInDataArray(point) {
|
||
|
var candidates = point.series.data.filter(function (candidate) {
|
||
|
return point.x === candidate.x && point.y === candidate.y;
|
||
|
});
|
||
|
return candidates.length === 1 ? candidates[0] : point;
|
||
|
}
|
||
|
/**
|
||
|
* Get array of unique series from two arrays
|
||
|
* @private
|
||
|
*/
|
||
|
function getUniqueSeries(arrayA, arrayB) {
|
||
|
var uniqueSeries = (arrayA || []).concat(arrayB || [])
|
||
|
.reduce(function (acc, cur) {
|
||
|
acc[cur.name + cur.index] = cur;
|
||
|
return acc;
|
||
|
}, {});
|
||
|
return Object.keys(uniqueSeries).map(function (ix) {
|
||
|
return uniqueSeries[ix];
|
||
|
});
|
||
|
}
|
||
|
/**
|
||
|
* @private
|
||
|
* @class
|
||
|
*/
|
||
|
var NewDataAnnouncer = function (chart) {
|
||
|
this.chart = chart;
|
||
|
};
|
||
|
extend(NewDataAnnouncer.prototype, {
|
||
|
/**
|
||
|
* Initialize the new data announcer.
|
||
|
* @private
|
||
|
*/
|
||
|
init: function () {
|
||
|
var chart = this.chart;
|
||
|
var announceOptions = chart.options.accessibility.announceNewData;
|
||
|
var announceType = announceOptions.interruptUser ? 'assertive' : 'polite';
|
||
|
this.lastAnnouncementTime = 0;
|
||
|
this.dirty = {
|
||
|
allSeries: {}
|
||
|
};
|
||
|
this.eventProvider = new EventProvider();
|
||
|
this.announcer = new Announcer(chart, announceType);
|
||
|
this.addEventListeners();
|
||
|
},
|
||
|
/**
|
||
|
* Remove traces of announcer.
|
||
|
* @private
|
||
|
*/
|
||
|
destroy: function () {
|
||
|
this.eventProvider.removeAddedEvents();
|
||
|
this.announcer.destroy();
|
||
|
},
|
||
|
/**
|
||
|
* Add event listeners for the announcer
|
||
|
* @private
|
||
|
*/
|
||
|
addEventListeners: function () {
|
||
|
var announcer = this, chart = this.chart, e = this.eventProvider;
|
||
|
e.addEvent(chart, 'afterDrilldown', function () {
|
||
|
announcer.lastAnnouncementTime = 0;
|
||
|
});
|
||
|
e.addEvent(H.Series, 'updatedData', function () {
|
||
|
announcer.onSeriesUpdatedData(this);
|
||
|
});
|
||
|
e.addEvent(chart, 'afterAddSeries', function (e) {
|
||
|
announcer.onSeriesAdded(e.series);
|
||
|
});
|
||
|
e.addEvent(H.Series, 'addPoint', function (e) {
|
||
|
announcer.onPointAdded(e.point);
|
||
|
});
|
||
|
e.addEvent(chart, 'redraw', function () {
|
||
|
announcer.announceDirtyData();
|
||
|
});
|
||
|
},
|
||
|
/**
|
||
|
* On new data in the series, make sure we add it to the dirty list.
|
||
|
* @private
|
||
|
* @param {Highcharts.Series} series
|
||
|
*/
|
||
|
onSeriesUpdatedData: function (series) {
|
||
|
var chart = this.chart;
|
||
|
if (series.chart === chart && chartHasAnnounceEnabled(chart)) {
|
||
|
this.dirty.hasDirty = true;
|
||
|
this.dirty.allSeries[series.name + series.index] = series;
|
||
|
}
|
||
|
},
|
||
|
/**
|
||
|
* On new data series added, update dirty list.
|
||
|
* @private
|
||
|
* @param {Highcharts.Series} series
|
||
|
*/
|
||
|
onSeriesAdded: function (series) {
|
||
|
if (chartHasAnnounceEnabled(this.chart)) {
|
||
|
this.dirty.hasDirty = true;
|
||
|
this.dirty.allSeries[series.name + series.index] = series;
|
||
|
// Add it to newSeries storage unless we already have one
|
||
|
this.dirty.newSeries = defined(this.dirty.newSeries) ?
|
||
|
void 0 : series;
|
||
|
}
|
||
|
},
|
||
|
/**
|
||
|
* On new point added, update dirty list.
|
||
|
* @private
|
||
|
* @param {Highcharts.Point} point
|
||
|
*/
|
||
|
onPointAdded: function (point) {
|
||
|
var chart = point.series.chart;
|
||
|
if (this.chart === chart && chartHasAnnounceEnabled(chart)) {
|
||
|
// Add it to newPoint storage unless we already have one
|
||
|
this.dirty.newPoint = defined(this.dirty.newPoint) ?
|
||
|
void 0 : point;
|
||
|
}
|
||
|
},
|
||
|
/**
|
||
|
* Gather what we know and announce the data to user.
|
||
|
* @private
|
||
|
*/
|
||
|
announceDirtyData: function () {
|
||
|
var chart = this.chart, announcer = this;
|
||
|
if (chart.options.accessibility.announceNewData &&
|
||
|
this.dirty.hasDirty) {
|
||
|
var newPoint = this.dirty.newPoint;
|
||
|
// If we have a single new point, see if we can find it in the
|
||
|
// data array. Otherwise we can only pass through options to
|
||
|
// the description builder, and it is a bit sparse in info.
|
||
|
if (newPoint) {
|
||
|
newPoint = findPointInDataArray(newPoint);
|
||
|
}
|
||
|
this.queueAnnouncement(Object.keys(this.dirty.allSeries).map(function (ix) {
|
||
|
return announcer.dirty.allSeries[ix];
|
||
|
}), this.dirty.newSeries, newPoint);
|
||
|
// Reset
|
||
|
this.dirty = {
|
||
|
allSeries: {}
|
||
|
};
|
||
|
}
|
||
|
},
|
||
|
/**
|
||
|
* Announce to user that there is new data.
|
||
|
* @private
|
||
|
* @param {Array<Highcharts.Series>} dirtySeries
|
||
|
* Array of series with new data.
|
||
|
* @param {Highcharts.Series} [newSeries]
|
||
|
* If a single new series was added, a reference to this series.
|
||
|
* @param {Highcharts.Point} [newPoint]
|
||
|
* If a single point was added, a reference to this point.
|
||
|
*/
|
||
|
queueAnnouncement: function (dirtySeries, newSeries, newPoint) {
|
||
|
var _this = this;
|
||
|
var chart = this.chart;
|
||
|
var annOptions = chart.options.accessibility.announceNewData;
|
||
|
if (annOptions.enabled) {
|
||
|
var now = +new Date();
|
||
|
var dTime = now - this.lastAnnouncementTime;
|
||
|
var time = Math.max(0, annOptions.minAnnounceInterval - dTime);
|
||
|
// Add series from previously queued announcement.
|
||
|
var allSeries = getUniqueSeries(this.queuedAnnouncement && this.queuedAnnouncement.series, dirtySeries);
|
||
|
// Build message and announce
|
||
|
var message = this.buildAnnouncementMessage(allSeries, newSeries, newPoint);
|
||
|
if (message) {
|
||
|
// Is there already one queued?
|
||
|
if (this.queuedAnnouncement) {
|
||
|
clearTimeout(this.queuedAnnouncementTimer);
|
||
|
}
|
||
|
// Build the announcement
|
||
|
this.queuedAnnouncement = {
|
||
|
time: now,
|
||
|
message: message,
|
||
|
series: allSeries
|
||
|
};
|
||
|
// Queue the announcement
|
||
|
this.queuedAnnouncementTimer = setTimeout(function () {
|
||
|
if (_this && _this.announcer) {
|
||
|
_this.lastAnnouncementTime = +new Date();
|
||
|
_this.announcer.announce(_this.queuedAnnouncement.message);
|
||
|
delete _this.queuedAnnouncement;
|
||
|
delete _this.queuedAnnouncementTimer;
|
||
|
}
|
||
|
}, time);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
/**
|
||
|
* Get announcement message for new data.
|
||
|
* @private
|
||
|
* @param {Array<Highcharts.Series>} dirtySeries
|
||
|
* Array of series with new data.
|
||
|
* @param {Highcharts.Series} [newSeries]
|
||
|
* If a single new series was added, a reference to this series.
|
||
|
* @param {Highcharts.Point} [newPoint]
|
||
|
* If a single point was added, a reference to this point.
|
||
|
*
|
||
|
* @return {string|null}
|
||
|
* The announcement message to give to user.
|
||
|
*/
|
||
|
buildAnnouncementMessage: function (dirtySeries, newSeries, newPoint) {
|
||
|
var chart = this.chart, annOptions = chart.options.accessibility.announceNewData;
|
||
|
// User supplied formatter?
|
||
|
if (annOptions.announcementFormatter) {
|
||
|
var formatterRes = annOptions.announcementFormatter(dirtySeries, newSeries, newPoint);
|
||
|
if (formatterRes !== false) {
|
||
|
return formatterRes.length ? formatterRes : null;
|
||
|
}
|
||
|
}
|
||
|
// Default formatter - use lang options
|
||
|
var multiple = H.charts && H.charts.length > 1 ? 'Multiple' : 'Single', langKey = newSeries ? 'newSeriesAnnounce' + multiple :
|
||
|
newPoint ? 'newPointAnnounce' + multiple : 'newDataAnnounce', chartTitle = getChartTitle(chart);
|
||
|
return chart.langFormat('accessibility.announceNewData.' + langKey, {
|
||
|
chartTitle: chartTitle,
|
||
|
seriesDesc: newSeries ?
|
||
|
defaultSeriesDescriptionFormatter(newSeries) :
|
||
|
null,
|
||
|
pointDesc: newPoint ?
|
||
|
defaultPointDescriptionFormatter(newPoint) :
|
||
|
null,
|
||
|
point: newPoint,
|
||
|
series: newSeries
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
export default NewDataAnnouncer;
|