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-motion
media query - Reduced Transparency (
renderContext.prefersReducedTransparency
, boolean): wraps the CSSprefers-reduced-transparency
media query - Contrast (
renderContext.contrastPreference
, enum): wraps the CSSprefers-contrast
media query. Possible values areContrastPreference.none
,more
, orless
. - Color Scheme (
renderContext.colorSchemePreference
, enum): wraps the CSSprefers-color-scheme
media query. Possible values areColorSchemePreference.light
ordark
.
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.