Data Visualization for Design Systems
Case study: Creating a responsive, accessible foundation for React charts within a design system
Case study: Creating a responsive, accessible foundation for React charts within a design system
Data visualization is a powerful way to help your users get the information they need quickly, and to make an app more engaging.
At Globality, we have a new feature planned to allow our clients to visually compare costs and timelines for submitted proposals from service providers in order to improve the award selection process. While we’ve got many talented software engineers who could build visualizations like these with any of the various chart generation tools available, one of Globality’s key goals is to deliver a consistent, world-class user experience, and one of the ways we do that is by designing and building our user interface within a design system. So rather than just building some charts, how do you build a foundation for creating consistent visualizations across your entire codebase?
Globality’s Proposal Insights prototypes
There are hundreds of libraries within the JavaScript ecosystem that make it easier to implement data visualization features. But choosing the wrong library can lead to years of headaches for your team. There are many pitfalls to avoid:
When integrating a new library, it’s worthwhile to carefully survey what’s available.
With hundreds or even thousands of potential libraries to investigate, there is always more to research, but we surveyed ten data visualization libraries in depth, paying special attention to:
That led to our big comparison table:
Our table comparing 10 data visualization libraries
One of our first challenges was to understand…how do you do data visualization in a React-y way? Thankfully we came across Amelia Wattenberger’s guide, Using React with D3.js.
Wattenberger presents an approach you might describe as “D3 a la carte” — take advantage of D3’s mature geometric tools while using React to handle rendering with composable, nested chart components. In fact, there are many libraries which use this approach: Visx, Reaviz, React-vis, and Recharts. This approach lets us use a control structure which is very familiar to React developers, where there’s close to a 1:1 relationship between the SVG elements that end up on the page and React components:
<BarChart>
<BarChart.GridRows />
{proposals.map((proposal) => (
<BarChart.Bar id={proposal.id} />
))}
<BarChart.AxisLeft />
<BarChart.AxisBottom />
</BarChart>
We looked at the release cadence, and the ratio of open to closed issues on Github for each of these for the last 6 months. Of all of these composable, React-first libraries, Visx seemed the best maintained:
Visx has been making releases and closing issues
Universal design is one of the top priorities for the Design System Team here at Globality. We want every user to be able to access the data they need, without impairment. We’ll see in Part 3 how this influenced our design decisions, but even at this early stage of choosing a library we found reasons to consider accessibility. The main differentiator here is keyboard navigation, and the standout was Highcharts:
Highcharts best-in-class keyboard navigation
Where every other library treats a chart more or less as a static image with “alt text”, Highcharts allows you to tab into a chart and navigate throughout the dataset with the keyboard. However, for our purposes at Globality, we decided that for keyboard users the best user experience would be to provide a separate table, so in the end keyboard navigability was not a factor in our decision.
We did three small spikes attempting to customize the look of the same chart in Chart.js, Highcharts, and Visx. Because Chart.js is written in Canvas rather than SVG, we found it a bit more difficult to integrate our existing tooltips and other React components. Visx felt extremely customizable due to its modular nature.
Lastly, bundle size wasn’t much of a differentiator, as the remaining contenders were all within the 50-100Kb range:
Highcharts eventually fell off of our list because we didn’t want to lock ourselves into a paid plan if there were viable open-source alternatives. Highcharts would be a great value for the money for a company that wants to save effort and use something off the shelf. But for our use case, where our design team wants to carefully customize the look and feel of our charts to fit within our design system, that ROI makes less sense.
Based on all of these considerations, we chose Visx for its price, customizability, architecture, and active maintainership. So far, this decision has worked great for us. Our initial batch of charts was implemented without a hitch, and Visx never got in our way.
One of the core building blocks of a design system is “design tokens.” These are static stylistic elements: font sizes, colors, line styles, etc. that can be shared between multiple components. By tokenizing your styles, we force conversations to happen between product managers, engineers, and designers, who often don’t realize when they create a never-before-seen stylistic element. Tokenization keeps both designs and code simpler over the long term, one of the benefits of the design system approach.
For visualizations, one of the key stylistic elements is color series. There are two main types of color series: categorical colors and sequential colors.
Categorical (or qualitative) color palettes are used to distinguish discrete categories of data. They are designed to be visually distinct from one another. For categorical colors, we have a hand-curated sequence of colors drawn from our design system palette. We made sure each step in the sequence was visually distinct from the previous:
Sequential colors for a quantitative question scale
Curating the sequential color palette was more difficult, because we wanted to be able to support an arbitrary number of gradations for any of the categorical colors specified above. Our algorithm for generating gradations required a bit of refinement to ensure that the saturation, lightness, and contrast were perceptually consistent between the different color series: Caption: Sequential color tokens for different numbers of series
While visualizations are largely… visual, there are still some typographic elements involved. One of our challenges was to understand whether we should reuse existing semantic tokens or create new ones. In general, most design system tokens will exist on two levels:
In our case, we needed a handful of new semantic tokens for chart ticks, tooltips, etc., but we were able to reuse existing concrete tokens from elsewhere in our design system.
Data visualization can require quite a lot of complicated code. When you add interactivity and then try to reuse code across multiple visualizations, it can become “spaghetti code” quite quickly. One of the goals for a good design system is to maintain components with clear responsibilities so that you can maximize consistency and minimize complexity. So as we implemented our foundational components, we refactored our APIs several times in order to achieve a clean separation of concerns.
Here’s the separation we arrived at:
Application Code | Individual Charts | Chart Container | Chart utils | |
---|---|---|---|---|
Example | Cost Breakdown Chart | Grouped Bar Chart | n/a | getTicks |
What it is | In the application layer, we expect to have many different visualizations, each with unique configuration | Each visualization will have a unique type of axes, data, and components (lines, bars, etc) | All “chart”-type visualizations, with an x- and y-axis share a common<ChartContainer> component |
Helper functions for use either by individual charts, or by application code |
Responsibilities |
|
|
|
|
One of the pitfalls we wanted to avoid was having a giant “mega-component” which centralized hundreds of features into one place. Part of why we chose Visx is that it is designed from the start to be composable: each individual SVG element — bars, axes, grid lines, etc. — is its own React component. And those are composed together in a JSX fragment. We then carried this over into our own component architecture when creating our design system components. Below is an example of the application code for a grouped bar chart:
Chart bars, axes, legends, etc mapped to their representation in JSX
We felt it was worth it to accept a little bit more boilerplate in the application layer in order to get simpler components. It also provides a good sized surface for targeted customization in the future. One of the challenges of creating design system components is to allow for future unknown customization. This “boilerplate-y” API surface provides many component boundaries where we can configure variation in the future.
Repeating the same code for each series in a bar group
We also started off integrating the data series deep into the GroupedBarChart
component. But again at the cost of increased boilerplate, we opted to allow the application code to own the data series. The application code needs to iterate over the series anyway.
Perhaps most importantly, since the series legends are built with HTML and not SVG, separating our concerns this way allows our chart components to be 100% focused on SVG. Visx provides a module for grouping data series, but it was simpler for us to just provide our own useSeries
hook which keeps track of:
<BarChart>
const {
getSeriesColor,
seriesOrder,
selectedSeriesIds,
setSelectedSeriesIds,
} = useSeries(
['providerFees', 'additionalExpenses', 'adjustments'],
{ canToggle: true }
);
As is typical when creating a new set of shared components, we started with just a single example and then added variations:
We started with a simple bar chart, then a timeline, and finally grouped bar
It became clear that there was a set of concerns that were common to all of these charts:
As we worked, we split out as a shared <ChartContainer>
component which creates a React Context that can provide all of those measurements wherever we are rendering SVG children:
<ChartContainer
ref={ref}
measurements={measurements]
width={layout.width]
height={layout.height}
doesOverflow={doesoverflow}
>
<BarChartContext.Provider value={barChartContext}>
<BarContext.Provider value={barChartContext}>
{childrent
</BarContext.Provider>
</BarChartContext.Provider>
</ChartContainer>
Lastly, each individual chart component — BarChart
, TimelineChart
, and GroupedBarChart
— of course has quite a bit of unique concerns. Notably, the shape of the data involved in each chart will be slightly different:
So each chart component has its own React Context which provides unique value getters, as well as the @visx/scale objects that tell the various bars, axes, and lines where to position themselves:
const barChartContext = useMemo (() => {
const rowsById = keyBy (data, 'id');
const tickValues = getTicks(data.map (getValue));
const min = tickValues[0];
const max = tickValues[tickValues.length - 1];
const xScale = scaleBand({
domain: data.map(({ id }) => id),
range: [0, measurements.dataWidth],
paddingInner,
paddingOuter,
});
const yScale = scaleLinear({
domain: [Math.min(min, 0), max],
round: true,
range: [measurements.dataHeight, O],
});
return {
xScale,
yScale,
getValue(id: string) {
const row = rowsById[id];
if (!row) {
throw new Error(`No row with id ${id}`);
}
return getValue(row);
},
rowsById,
tickValues
}
}, [data, measurements, getValue, paddingInner, paddingOuter]);
Once we had our core architecture in place, we needed several smaller services to help solve some of the trickier bits of chart drawing.
We can’t know in advance exactly what the data will look like that goes into these visualizations. A given project brief on Globality might receive dozens of bids or just one or two. So to be prepared for all possible situations, we first worked with design to identify minimum and maximum values for bars and the padding between them:
Minimum and maximum sizes for chart bars
From that starting point, and taking into account the available screen width, we can calculate the ratios that Visx uses to space out the axes. This allows us to have a nice, flexible framework for any kind of data to fit itself into any available space:
Bar size and spacing responds to available space & data needs
The bar width calculations allow us to handle an arbitrary number of data rows, we also have continuous axes that need to show the right ticks in the right places. Our design team had several requirements for tick placement:
In addition, we had the interesting case of the timeline chart, where in some cases projects could span just a few weeks, or in other cases many years!
Round number axis ticks adjusting to different data ranges
Lastly, while Visx does provide a @visx/tooltip package for positioning SVG content over a Visx element, because we are working within a design system, we wanted to use our existing HTML-based tooltip. That keeps the visualizations more consistent with the rest of the application. Since the tooltip library we’re using doesn’t support positioning over SVG elements, we gave the ChartContainer
component the responsibility of positioning an invisible HTML element for positioning tooltips:
This work sets a solid foundation for Globality to continue providing our customers with consistent, usable data visualizations that help them quickly understand their options throughout the sourcing pipeline.
Banner image “The 3D visualization of overwhelming data ;-)” by Elif Ayiter/Alpha Auer/…./ is licensed under CC BY-NC-ND 2.0.
Erik is a full stack developer, focused on Design Systems at Globality. He has a Masters in Human-Computer Interaction Design from Indiana University, and has worked at many small to medium startups. He loves the web, JavaScript, and great software development process.