Accessible Rendering and Navigation
Counterpoint’s state management functionality helps you accommodate users with different preferences about the appearance, animation, and navigation style of your interfaces. Check out the Accessible Gapminder plot to see some examples.
Responding to Global User Preferences
It’s important to respect users’ accessibility preferences regarding graphics and animation. This can help users who have vestibular or photosensitive disabilities use your visualizations.
Ordinarily, these preferences are exposed to your application as CSS media queries. Counterpoint wraps
these media queries in a reactive type called the RenderContext.
To respond to RenderContext changes in your rendering, add it to your ticker:
const renderContext = getRenderContext();
let ticker = new Ticker([
// other elements...
renderContext
]);
Then, in your drawing function, you can use the following properties of the render context (which are exposed as simple JavaScript types, not Counterpoint Attributes):
- Reduced Motion (
renderContext.prefersReducedMotion, boolean): wraps the CSSprefers-reduced-motionmedia query - Reduced Transparency (
renderContext.prefersReducedTransparency, boolean): wraps the CSSprefers-reduced-transparencymedia query - Contrast (
renderContext.contrastPreference, enum): wraps the CSSprefers-contrastmedia query. Possible values areContrastPreference.none,more, orless. - Color Scheme (
renderContext.colorSchemePreference, enum): wraps the CSSprefers-color-schememedia query. Possible values areColorSchemePreference.lightordark.
Implementing Alternate Animation Styles with Events
As described in Event Listeners, render groups provide an event dispatching mechanism that allows you to separate the triggering of animation events with how marks execute them. This can allow you to implement alternative animation styles, such as versions with and without motion or transparency.
In the below example, you can click the button to change between motion animations and fade animations.
To implement this, let’s say we have a variable called reduceMotion
representing the value of the checkbox above (or we can get the value from RenderContext.prefersReducedMotion).
We configure the render group to listen for an event called ‘animate’ and perform
the appropriate animation depending on the reduceMotion setting. To implement
the fade animations, we simply create a clone of each mark, animate its alpha
using the stage manager, then remove the original.
renderGroup.configureStaging({
initialize: (mark) => mark.setAttr('alpha', 0.0),
enter: (mark) => mark.animateTo('alpha', 1.0).wait('alpha'),
exit: (mark) => mark.animateTo('alpha', 0.0).wait('alpha')
}).onEvent('animate', (mark, locationFn) => {
let newCoords = locationFn(mark);
if (reduceMotion) {
// fade animation
let clone = mark.copy(mark.id, { x: newCoords.x, y: newCoords.y });
markSet.addMark(clone);
markSet.deleteMark(mark);
} else {
// motion animation
mark.animateTo('x', newCoords.x, { duration: 1000 });
mark.animateTo('y', newCoords.y, { duration: 1000 });
}
});
Here, locationFn is a function specifying where each point should be moved to.
When the points need to be animated, we dispatch an ‘animate’ event and provide
that location function:
markSet.dispatch('animate', (mark) => ({
x: mark.attr('x') + Math.random() * 50 - 25,
y: mark.attr('y') + Math.random() * 50 - 25
}));
Navigation with Data Navigator
Data Navigator is a library to help make data visualizations navigable by interfacing with a variety of input formats, such as key presses, gestures, and voice commands. While Counterpoint is a state management library, Data Navigator is stateless. So the two libraries complement each other to help you develop performant animated visualizations that are also accessible to those who need alternate ways of navigating charts.
To see an example of how Data Navigator can be used with Counterpoint, see the running demo and source code of an accessible Gapminder chart.