NGToolsCSharp/NGTools/Scripts/Highcharts-7.1.1/code/es-modules/modules/accessibility/components/SeriesComponent.js

1154 lines
34 KiB
JavaScript
Raw Normal View History

2024-09-13 08:44:13 +00:00
/* *
*
* (c) 2009-2019 Øystein Moseng
*
* Accessibility component for series and points.
*
* License: www.highcharts.com/license
*
* */
'use strict';
import H from '../../../parts/Globals.js';
import AccessibilityComponent from '../AccessibilityComponent.js';
import KeyboardNavigationHandler from '../KeyboardNavigationHandler.js';
var merge = H.merge,
pick = H.pick;
/*
* Set for which series types it makes sense to move to the closest point with
* up/down arrows, and which series types should just move to next series.
*/
H.Series.prototype.keyboardMoveVertical = true;
['column', 'pie'].forEach(function (type) {
if (H.seriesTypes[type]) {
H.seriesTypes[type].prototype.keyboardMoveVertical = false;
}
});
/**
* Get the index of a point in a series. This is needed when using e.g. data
* grouping.
*
* @private
* @function getPointIndex
*
* @param {Highcharts.Point} point
* The point to find index of.
*
* @return {number}
* The index in the series.points array of the point.
*/
function getPointIndex(point) {
var index = point.index,
points = point.series.points,
i = points.length;
if (points[index] !== point) {
while (i--) {
if (points[i] === point) {
return i;
}
}
} else {
return index;
}
}
/**
* Determine if a series should be skipped
*
* @private
* @function isSkipSeries
*
* @param {Highcharts.Series} series
*
* @return {boolean}
*/
function isSkipSeries(series) {
var a11yOptions = series.chart.options.accessibility,
seriesA11yOptions = series.options.accessibility || {},
seriesKbdNavOptions = seriesA11yOptions.keyboardNavigation;
return seriesKbdNavOptions && seriesKbdNavOptions.enabled === false ||
seriesA11yOptions.enabled === false ||
series.options.enableMouseTracking === false || // #8440
!series.visible ||
// Skip all points in a series where pointDescriptionThreshold is
// reached
(a11yOptions.pointDescriptionThreshold &&
a11yOptions.pointDescriptionThreshold <= series.points.length);
}
/**
* Determine if a point should be skipped
*
* @private
* @function isSkipPoint
*
* @param {Highcharts.Point} point
*
* @return {boolean}
*/
function isSkipPoint(point) {
var a11yOptions = point.series.chart.options.accessibility;
return point.isNull && a11yOptions.keyboardNavigation.skipNullPoints ||
point.visible === false ||
isSkipSeries(point.series);
}
/**
* Get the point in a series that is closest (in distance) to a reference point.
* Optionally supply weight factors for x and y directions.
*
* @private
* @function getClosestPoint
*
* @param {Highcharts.Point} point
* @param {Highcharts.Series} series
* @param {number} [xWeight]
* @param {number} [yWeight]
*
* @return {Highcharts.Point|undefined}
*/
function getClosestPoint(point, series, xWeight, yWeight) {
var minDistance = Infinity,
dPoint,
minIx,
distance,
i = series.points.length;
if (point.plotX === undefined || point.plotY === undefined) {
return;
}
while (i--) {
dPoint = series.points[i];
if (dPoint.plotX === undefined || dPoint.plotY === undefined) {
continue;
}
distance = (point.plotX - dPoint.plotX) *
(point.plotX - dPoint.plotX) * (xWeight || 1) +
(point.plotY - dPoint.plotY) *
(point.plotY - dPoint.plotY) * (yWeight || 1);
if (distance < minDistance) {
minDistance = distance;
minIx = i;
}
}
return minIx !== undefined && series.points[minIx];
}
/**
* Highlights a point (show tooltip and display hover state).
*
* @private
* @function Highcharts.Point#highlight
*
* @return {Highcharts.Point}
* This highlighted point.
*/
H.Point.prototype.highlight = function () {
var chart = this.series.chart;
if (!this.isNull) {
this.onMouseOver(); // Show the hover marker and tooltip
} else {
if (chart.tooltip) {
chart.tooltip.hide(0);
}
// Don't call blur on the element, as it messes up the chart div's focus
}
// We focus only after calling onMouseOver because the state change can
// change z-index and mess up the element.
if (this.graphic) {
chart.setFocusToElement(this.graphic);
}
chart.highlightedPoint = this;
return this;
};
/**
* Function to highlight next/previous point in chart.
*
* @private
* @function Highcharts.Chart#highlightAdjacentPoint
*
* @param {boolean} next
* Flag for the direction.
*
* @return {Highcharts.Point|boolean}
* Returns highlighted point on success, false on failure (no adjacent
* point to highlight in chosen direction).
*/
H.Chart.prototype.highlightAdjacentPoint = function (next) {
var chart = this,
series = chart.series,
curPoint = chart.highlightedPoint,
curPointIndex = curPoint && getPointIndex(curPoint) || 0,
curPoints = curPoint && curPoint.series.points,
lastSeries = chart.series && chart.series[chart.series.length - 1],
lastPoint = lastSeries && lastSeries.points &&
lastSeries.points[lastSeries.points.length - 1],
newSeries,
newPoint;
// If no points, return false
if (!series[0] || !series[0].points) {
return false;
}
if (!curPoint) {
// No point is highlighted yet. Try first/last point depending on move
// direction
newPoint = next ? series[0].points[0] : lastPoint;
} else {
// We have a highlighted point.
// Grab next/prev point & series
newSeries = series[curPoint.series.index + (next ? 1 : -1)];
newPoint = curPoints[curPointIndex + (next ? 1 : -1)];
if (!newPoint && newSeries) {
// Done with this series, try next one
newPoint = newSeries.points[next ? 0 : newSeries.points.length - 1];
}
// If there is no adjacent point, we return false
if (!newPoint) {
return false;
}
}
// Recursively skip points
if (isSkipPoint(newPoint)) {
// If we skip this whole series, move to the end of the series before we
// recurse, just to optimize
newSeries = newPoint.series;
if (isSkipSeries(newSeries)) {
chart.highlightedPoint = next ?
newSeries.points[newSeries.points.length - 1] :
newSeries.points[0];
} else {
// Otherwise, just move one point
chart.highlightedPoint = newPoint;
}
// Retry
return chart.highlightAdjacentPoint(next);
}
// There is an adjacent point, highlight it
return newPoint.highlight();
};
/**
* Highlight first valid point in a series. Returns the point if successfully
* highlighted, otherwise false. If there is a highlighted point in the series,
* use that as starting point.
*
* @private
* @function Highcharts.Series#highlightFirstValidPoint
*
* @return {Highcharts.Point|boolean}
*/
H.Series.prototype.highlightFirstValidPoint = function () {
var curPoint = this.chart.highlightedPoint,
start = (curPoint && curPoint.series) === this ?
getPointIndex(curPoint) :
0,
points = this.points,
len = points.length;
if (points && len) {
for (var i = start; i < len; ++i) {
if (!isSkipPoint(points[i])) {
return points[i].highlight();
}
}
for (var j = start; j >= 0; --j) {
if (!isSkipPoint(points[j])) {
return points[j].highlight();
}
}
}
return false;
};
/**
* Highlight next/previous series in chart. Returns false if no adjacent series
* in the direction, otherwise returns new highlighted point.
*
* @private
* @function Highcharts.Chart#highlightAdjacentSeries
*
* @param {boolean} down
*
* @return {Highcharts.Point|boolean}
*/
H.Chart.prototype.highlightAdjacentSeries = function (down) {
var chart = this,
newSeries,
newPoint,
adjacentNewPoint,
curPoint = chart.highlightedPoint,
lastSeries = chart.series && chart.series[chart.series.length - 1],
lastPoint = lastSeries && lastSeries.points &&
lastSeries.points[lastSeries.points.length - 1];
// If no point is highlighted, highlight the first/last point
if (!chart.highlightedPoint) {
newSeries = down ? (chart.series && chart.series[0]) : lastSeries;
newPoint = down ?
(newSeries && newSeries.points && newSeries.points[0]) : lastPoint;
return newPoint ? newPoint.highlight() : false;
}
newSeries = chart.series[curPoint.series.index + (down ? -1 : 1)];
if (!newSeries) {
return false;
}
// We have a new series in this direction, find the right point
// Weigh xDistance as counting much higher than Y distance
newPoint = getClosestPoint(curPoint, newSeries, 4);
if (!newPoint) {
return false;
}
// New series and point exists, but we might want to skip it
if (isSkipSeries(newSeries)) {
// Skip the series
newPoint.highlight();
adjacentNewPoint = chart.highlightAdjacentSeries(down); // Try recurse
if (!adjacentNewPoint) {
// Recurse failed
curPoint.highlight();
return false;
}
// Recurse succeeded
return adjacentNewPoint;
}
// Highlight the new point or any first valid point back or forwards from it
newPoint.highlight();
return newPoint.series.highlightFirstValidPoint();
};
/**
* Highlight the closest point vertically.
*
* @private
* @function Highcharts.Chart#highlightAdjacentPointVertical
*
* @param {boolean} down
*
* @return {Highcharts.Point|boolean}
*/
H.Chart.prototype.highlightAdjacentPointVertical = function (down) {
var curPoint = this.highlightedPoint,
minDistance = Infinity,
bestPoint;
if (curPoint.plotX === undefined || curPoint.plotY === undefined) {
return false;
}
this.series.forEach(function (series) {
if (series === curPoint.series || isSkipSeries(series)) {
return;
}
series.points.forEach(function (point) {
if (point.plotY === undefined || point.plotX === undefined ||
point === curPoint) {
return;
}
var yDistance = point.plotY - curPoint.plotY,
width = Math.abs(point.plotX - curPoint.plotX),
distance = Math.abs(yDistance) * Math.abs(yDistance) +
width * width * 4; // Weigh horizontal distance highly
// Reverse distance number if axis is reversed
if (series.yAxis.reversed) {
yDistance *= -1;
}
if (
yDistance <= 0 && down || yDistance >= 0 && !down || // Chk dir
distance < 5 || // Points in same spot => infinite loop
isSkipPoint(point)
) {
return;
}
if (distance < minDistance) {
minDistance = distance;
bestPoint = point;
}
});
});
return bestPoint ? bestPoint.highlight() : false;
};
/**
* Get accessible time description for a point on a datetime axis.
*
* @private
* @function Highcharts.Point#getTimeDescription
*
* @return {string}
* The description as string.
*/
H.Point.prototype.getA11yTimeDescription = function () {
var point = this,
series = point.series,
chart = series.chart,
a11yOptions = chart.options.accessibility;
if (series.xAxis && series.xAxis.isDatetimeAxis) {
return chart.time.dateFormat(
a11yOptions.pointDateFormatter &&
a11yOptions.pointDateFormatter(point) ||
a11yOptions.pointDateFormat ||
H.Tooltip.prototype.getXDateFormat.call(
{
getDateFormat: H.Tooltip.prototype.getDateFormat,
chart: chart
},
point,
chart.options.tooltip,
series.xAxis
),
point.x
);
}
};
/**
* The SeriesComponent class
*
* @private
* @class
* @name Highcharts.SeriesComponent
* @param {Highcharts.Chart} chart
* Chart object
*/
var SeriesComponent = function (chart) {
this.initBase(chart);
this.init();
};
SeriesComponent.prototype = new AccessibilityComponent();
H.extend(SeriesComponent.prototype, /** @lends Highcharts.SeriesComponent */ {
/**
* Init the component.
*/
init: function () {
var component = this;
// On destroy, we need to clean up the focus border and the state.
this.addEvent(H.Series, 'destroy', function () {
var chart = this.chart;
if (
chart === component.chart &&
chart.highlightedPoint &&
chart.highlightedPoint.series === this
) {
delete chart.highlightedPoint;
if (chart.focusElement) {
chart.focusElement.removeFocusBorder();
}
}
});
// Hide tooltip from screen readers when it is shown
this.addEvent(H.Tooltip, 'refresh', function () {
if (
this.chart === component.chart &&
this.label &&
this.label.element
) {
this.label.element.setAttribute('aria-hidden', true);
}
});
// Hide series labels
this.addEvent(this.chart, 'afterDrawSeriesLabels', function () {
this.series.forEach(function (series) {
if (series.labelBySeries) {
series.labelBySeries.attr('aria-hidden', true);
}
});
});
// Set up announcing of new data
this.initAnnouncer();
},
/**
* Called on first render/updates to the chart, including options changes.
*/
onChartUpdate: function () {
var component = this,
chart = this.chart;
chart.series.forEach(function (series) {
component[
(series.options.accessibility &&
series.options.accessibility.enabled) !== false ?
'addSeriesDescription' : 'hideSeriesFromScreenReader'
](series);
});
},
/**
* Get keyboard navigation handler for this component.
* @return {Highcharts.KeyboardNavigationHandler}
*/
getKeyboardNavigation: function () {
var keys = this.keyCodes,
chart = this.chart,
a11yOptions = chart.options.accessibility,
// Function that attempts to highlight next/prev point, returns
// the response number. Handles wrap around.
attemptNextPoint = function (directionIsNext) {
if (!chart.highlightAdjacentPoint(directionIsNext)) {
// Failed to highlight next, wrap to last/first if we
// have wrapAround
if (a11yOptions.keyboardNavigation.wrapAround) {
return this.init(directionIsNext ? 1 : -1);
}
return this.response[directionIsNext ? 'next' : 'prev'];
}
return this.response.success;
};
return new KeyboardNavigationHandler(chart, {
keyCodeMap: [
// Arrow sideways
[[
keys.left, keys.right
], function (keyCode) {
return attemptNextPoint.call(this, keyCode === keys.right);
}],
// Arrow vertical
[[
keys.up, keys.down
], function (keyCode) {
var down = keyCode === keys.down,
navOptions = a11yOptions.keyboardNavigation;
// Handle serialized mode, act like left/right
if (navOptions.mode && navOptions.mode === 'serialize') {
return attemptNextPoint.call(
this, keyCode === keys.down
);
}
// Normal mode, move between series
var highlightMethod = chart.highlightedPoint &&
chart.highlightedPoint.series.keyboardMoveVertical ?
'highlightAdjacentPointVertical' :
'highlightAdjacentSeries';
chart[highlightMethod](down);
return this.response.success;
}],
// Enter/Spacebar
[[
keys.enter, keys.space
], function () {
if (chart.highlightedPoint) {
chart.highlightedPoint.firePointEvent('click');
}
}]
],
// Always start highlighting from scratch when entering this module
init: function (dir) {
var numSeries = chart.series.length,
i = dir > 0 ? 0 : numSeries,
res;
if (dir > 0) {
delete chart.highlightedPoint;
// Find first valid point to highlight
while (i < numSeries) {
res = chart.series[i].highlightFirstValidPoint();
if (res) {
break;
}
++i;
}
} else {
// Find last valid point to highlight
while (i--) {
chart.highlightedPoint = chart.series[i].points[
chart.series[i].points.length - 1
];
// Highlight first valid point in the series will also
// look backwards. It always starts from currently
// highlighted point.
res = chart.series[i].highlightFirstValidPoint();
if (res) {
break;
}
}
}
// Nothing to highlight
return this.response.success;
},
// If leaving points, don't show tooltip anymore
terminate: function () {
if (chart.tooltip) {
chart.tooltip.hide(0);
}
delete chart.highlightedPoint;
}
});
},
/**
* Returns true if a point should be clickable.
* @private
* @param {Highcharts.Point} point The point to test.
* @return {boolean} True if the point can be clicked.
*/
isPointClickable: function (point) {
var seriesOpts = point.series.options || {},
seriesPointEvents = seriesOpts.point && seriesOpts.point.events;
return point && point.graphic && point.graphic.element &&
(
point.hcEvents && point.hcEvents.click ||
seriesPointEvents && seriesPointEvents.click ||
(
point.options &&
point.options.events &&
point.options.events.click
)
);
},
/**
* Initialize the new data announcer.
* @private
*/
initAnnouncer: function () {
var chart = this.chart,
a11yOptions = chart.options.accessibility,
component = this;
this.lastAnnouncementTime = 0;
this.dirty = {
allSeries: {}
};
// Add the live region
this.announceRegion = this.createElement('div');
this.announceRegion.setAttribute('aria-hidden', false);
this.announceRegion.setAttribute(
'aria-live', a11yOptions.announceNewData.interruptUser ?
'assertive' : 'polite'
);
merge(true, this.announceRegion.style, this.hiddenStyle);
chart.renderTo.insertBefore(
this.announceRegion, chart.renderTo.firstChild
);
// After drilldown, make sure we reset time counter, and also that we
// highlight the first series.
this.addEvent(this.chart, 'afterDrilldown', function () {
chart.highlightedPoint = null;
if (chart.options.accessibility.announceNewData.enabled) {
if (this.series && this.series.length) {
var el = component.getSeriesElement(this.series[0]);
if (el.focus && el.getAttribute('aria-label')) {
el.focus();
} else {
this.series[0].highlightFirstValidPoint();
}
}
component.lastAnnouncementTime = 0;
if (chart.focusElement) {
chart.focusElement.removeFocusBorder();
}
}
});
// On new data in the series, make sure we add it to the dirty list
this.addEvent(H.Series, 'updatedData', function () {
if (
this.chart === chart &&
this.chart.options.accessibility.announceNewData.enabled
) {
component.dirty.hasDirty = true;
component.dirty.allSeries[this.name + this.index] = this;
}
});
// New series
this.addEvent(chart, 'afterAddSeries', function (e) {
if (this.options.accessibility.announceNewData.enabled) {
var series = e.series;
component.dirty.hasDirty = true;
component.dirty.allSeries[series.name + series.index] = series;
// Add it to newSeries storage unless we already have one
component.dirty.newSeries = component.dirty.newSeries ===
undefined ? series : null;
}
});
// New point
this.addEvent(H.Series, 'addPoint', function (e) {
if (this.chart === chart &&
this.chart.options.accessibility.announceNewData.enabled) {
// Add it to newPoint storage unless we already have one
component.dirty.newPoint = component.dirty.newPoint ===
undefined ? e.point : null;
}
});
// On redraw: compile what we know about new data, and build
// announcement
this.addEvent(chart, 'redraw', function () {
if (
this.options.accessibility.announceNewData &&
component.dirty.hasDirty
) {
var newPoint = component.dirty.newPoint,
newPoints;
// 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) {
newPoints = newPoint.series.data.filter(function (point) {
return point.x === newPoint.x && point.y === newPoint.y;
});
// We have list of points with the same x and y values. If
// this list is one point long, we have our new point.
newPoint = newPoints.length === 1 ? newPoints[0] : newPoint;
}
// Queue the announcement
component.announceNewData(
Object.keys(component.dirty.allSeries).map(function (ix) {
return component.dirty.allSeries[ix];
}),
component.dirty.newSeries,
newPoint
);
// Reset
component.dirty = {
allSeries: {}
};
}
});
},
/**
* Handle announcement 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.
*/
announceNewData: function (dirtySeries, newSeries, newPoint) {
var chart = this.chart,
annOptions = chart.options.accessibility.announceNewData;
if (annOptions.enabled) {
var component = this,
now = +new Date(),
dTime = now - this.lastAnnouncementTime,
time = Math.max(0, annOptions.minAnnounceInterval - dTime),
allSeries;
// Add affected series from existing queued announcement
if (this.queuedAnnouncement) {
var uniqueSeries = (this.queuedAnnouncement.series || [])
.concat(dirtySeries)
.reduce(function (acc, cur) {
acc[cur.name + cur.index] = cur;
return acc;
}, {});
allSeries = Object.keys(uniqueSeries).map(function (ix) {
return uniqueSeries[ix];
});
} else {
allSeries = [].concat(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
component.queuedAnnouncementTimer = setTimeout(function () {
if (component && component.announceRegion) {
component.lastAnnouncementTime = +new Date();
component.announceRegion.innerHTML = component
.queuedAnnouncement.message;
// Delete contents after a second to avoid user
// finding the live region in the DOM.
if (component.clearAnnouncementContainerTimer) {
clearTimeout(
component.clearAnnouncementContainerTimer
);
}
component.clearAnnouncementContainerTimer = setTimeout(
function () {
component.announceRegion.innerHTML = '';
delete
component.clearAnnouncementContainerTimer;
}, 1000
);
delete component.queuedAnnouncement;
delete component.queuedAnnouncementTimer;
}
}, time);
}
}
},
/**
* Handle announcement 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.
*
* @return {string} 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';
return chart.langFormat(
'accessibility.announceNewData.' + langKey, {
chartTitle: this.stripTags(
chart.options.title.text || chart.langFormat(
'accessibility.defaultChartTitle', { chart: chart }
)
),
seriesDesc: newSeries ?
this.defaultSeriesDescriptionFormatter(newSeries) : null,
pointDesc: newPoint ?
this.defaultPointDescriptionFormatter(newPoint) : null,
point: newPoint,
series: newSeries
}
);
},
/**
* Utility function. Reverses child nodes of a DOM element.
* @private
* @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} node
*/
reverseChildNodes: function (node) {
var i = node.childNodes.length;
while (i--) {
node.appendChild(node.childNodes[i]);
}
},
/**
* Get the DOM element for the first point in the series.
* @private
* @param {Highcharts.Series} series The series to get element for.
* @return {Highcharts.SVGDOMElement} The DOM element for the point.
*/
getSeriesFirstPointElement: function (series) {
return (
series.points &&
series.points.length &&
series.points[0].graphic &&
series.points[0].graphic.element
);
},
/**
* Get the DOM element for the series that we put accessibility info on.
* @private
* @param {Highcharts.Series} series The series to get element for.
* @return {Highcharts.SVGDOMElement} The DOM element for the series
*/
getSeriesElement: function (series) {
var firstPointEl = this.getSeriesFirstPointElement(series);
return (
firstPointEl &&
firstPointEl.parentNode || series.graph &&
series.graph.element || series.group &&
series.group.element
); // Could be tracker series depending on series type
},
/**
* Hide series from screen readers.
* @private
* @param {Highcharts.Series} series The series to hide
*/
hideSeriesFromScreenReader: function (series) {
var seriesEl = this.getSeriesElement(series);
if (seriesEl) {
seriesEl.setAttribute('aria-hidden', true);
}
},
/**
* Put accessible info on series and points of a series.
* @private
* @param {Highcharts.Series} series The series to add info on.
*/
addSeriesDescription: function (series) {
var component = this,
chart = series.chart,
a11yOptions = chart.options.accessibility,
seriesA11yOptions = series.options.accessibility || {},
firstPointEl = component.getSeriesFirstPointElement(series),
seriesEl = component.getSeriesElement(series);
if (seriesEl) {
// Unhide series
this.unhideElementFromScreenReaders(seriesEl);
// For some series types the order of elements do not match the
// order of points in series. In that case we have to reverse them
// in order for AT to read them out in an understandable order
if (seriesEl.lastChild === firstPointEl) {
component.reverseChildNodes(seriesEl);
}
// Make individual point elements accessible if possible. Note: If
// markers are disabled there might not be any elements there to
// make accessible.
if (
series.points && (
series.points.length <
a11yOptions.pointDescriptionThreshold ||
a11yOptions.pointDescriptionThreshold === false
) &&
!seriesA11yOptions.exposeAsGroupOnly
) {
series.points.forEach(function (point) {
var pointEl = point.graphic && point.graphic.element;
if (pointEl) {
pointEl.setAttribute('role', 'img');
pointEl.setAttribute('tabindex', '-1');
pointEl.setAttribute('aria-label',
component.stripTags(
seriesA11yOptions.pointDescriptionFormatter &&
seriesA11yOptions
.pointDescriptionFormatter(point) ||
a11yOptions.pointDescriptionFormatter &&
a11yOptions.pointDescriptionFormatter(point) ||
component
.defaultPointDescriptionFormatter(point)
));
}
});
}
// Make series element accessible
if (chart.series.length > 1 || a11yOptions.describeSingleSeries) {
// Handle role attribute
if (seriesA11yOptions.exposeAsGroupOnly) {
seriesEl.setAttribute('role', 'img');
} else if (a11yOptions.landmarkVerbosity === 'all') {
seriesEl.setAttribute('role', 'region');
} /* else do not add role */
seriesEl.setAttribute('tabindex', '-1');
seriesEl.setAttribute(
'aria-label',
component.stripTags(
a11yOptions.seriesDescriptionFormatter &&
a11yOptions.seriesDescriptionFormatter(series) ||
component.defaultSeriesDescriptionFormatter(series)
)
);
}
}
},
/**
* Return string with information about series.
* @private
* @return {string}
*/
defaultSeriesDescriptionFormatter: function (series) {
var chart = series.chart,
seriesA11yOptions = series.options.accessibility || {},
desc = seriesA11yOptions.description,
description = desc && chart.langFormat(
'accessibility.series.description', {
description: desc,
series: series
}
),
xAxisInfo = chart.langFormat(
'accessibility.series.xAxisDescription',
{
name: series.xAxis && series.xAxis.getDescription(),
series: series
}
),
yAxisInfo = chart.langFormat(
'accessibility.series.yAxisDescription',
{
name: series.yAxis && series.yAxis.getDescription(),
series: series
}
),
summaryContext = {
name: series.name || '',
ix: series.index + 1,
numSeries: chart.series && chart.series.length,
numPoints: series.points && series.points.length,
series: series
},
combination = chart.types && chart.types.length > 1 ?
'Combination' : '',
summary = chart.langFormat(
'accessibility.series.summary.' + series.type + combination,
summaryContext
) || chart.langFormat(
'accessibility.series.summary.default' + combination,
summaryContext
);
return summary + (description ? ' ' + description : '') + (
chart.yAxis && chart.yAxis.length > 1 && this.yAxis ?
' ' + yAxisInfo : ''
) + (
chart.xAxis && chart.xAxis.length > 1 && this.xAxis ?
' ' + xAxisInfo : ''
);
},
/**
* Return string with information about point.
* @private
* @return {string}
*/
defaultPointDescriptionFormatter: function (point) {
var series = point.series,
chart = series.chart,
a11yOptions = chart.options.accessibility,
tooltipOptions = point.series.tooltipOptions || {},
valuePrefix = a11yOptions.pointValuePrefix ||
tooltipOptions.valuePrefix || '',
valueSuffix = a11yOptions.pointValueSuffix ||
tooltipOptions.valueSuffix || '',
description = point.options && point.options.accessibility &&
point.options.accessibility.description,
timeDesc = point.getA11yTimeDescription(),
numberFormat = function (value) {
if (H.isNumber(value)) {
var lang = H.defaultOptions.lang;
return H.numberFormat(
value,
a11yOptions.pointValueDecimals ||
tooltipOptions.valueDecimals || -1,
lang.decimalPoint,
lang.accessibility.thousandsSep ||
lang.thousandsSep
);
}
return value;
},
showXDescription = pick(
series.xAxis &&
series.xAxis.options.accessibility &&
series.xAxis.options.accessibility.enabled,
!chart.angular
),
pointCategory = series.xAxis && series.xAxis.categories &&
point.category !== undefined && '' + point.category;
// Pick and choose properties for a succint label
var xDesc = point.name || timeDesc ||
pointCategory && pointCategory.replace('<br/>', ' ') || (
point.id && point.id.indexOf('highcharts-') < 0 ?
point.id : ('x, ' + point.x)
),
valueDesc = point.series.pointArrayMap ?
point.series.pointArrayMap.reduce(function (desc, key) {
return desc + (desc.length ? ', ' : '') + key + ': ' +
valuePrefix + numberFormat(
pick(point[key], point.options[key])
) + valueSuffix;
}, '') :
(
point.value !== undefined ?
valuePrefix + numberFormat(point.value) + valueSuffix :
valuePrefix + numberFormat(point.y) + valueSuffix
);
return (point.index !== undefined ? (point.index + 1) + '. ' : '') +
(showXDescription ? xDesc + ', ' : '') + valueDesc + '.' +
(description ? ' ' + description : '') +
(chart.series.length > 1 && series.name ? ' ' + series.name : '');
}
});
export default SeriesComponent;