Data Visualization for Design Systems - Globality Blog

Data Visualization for Design Systems

Case study: Creating a responsive, accessible foundation for React charts within a design system

Share:

featured post

Introduction

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 new Proposal Insights featureGlobality’s Proposal Insights prototypes

Part 1: Choosing foundation libraries

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:

  • Libraries can become unmaintained, such that bugs languish and PRs stop getting merged
  • A library’s architecture can make it difficult to extend and customize
  • A library that is too bulky can slow down your application start time
  • Missing or incomplete documentation can slow down developers trying to add features and fix bugs
  • The patterns chosen by a library may not fit well with the patterns you’ve chosen for your application

When integrating a new library, it’s worthwhile to carefully survey what’s available.

The Survey

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:

  • The way the API is structured
  • React and TypeScript support
  • Accessibility
  • How chart components can be customized
  • How active the project appeared to be

That led to our big comparison table:

Our table comparing 10 data visualization librariesOur table comparing 10 data visualization libraries

 

The differences that mattered to us

React-y-ness

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>

An Active Project

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 issuesVisx has been making releases and closing issues

Accessibility

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 navigationHighcharts 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.

Customization

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.

Bundle Size and Cost

Lastly, bundle size wasn’t much of a differentiator, as the remaining contenders were all within the 50-100Kb range:

Table of bundle sizes for different chart librariesBundle sizes

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.

Choosing a Library

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.

Visx logoVisx logo

 


Part 2: Design Tokens

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 Colors

Categorical colors in chart with seriesColors in chart with legend

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:

Categorical color tokensCategorical color tokens

Sequential Colors

Sequential colors for a quantitative question scaleSequential 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

Typography Tokens

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:

  • concrete tokens will have concrete names like “red” or “pencil” and will define the entire space of what’s allowed within the design system.
  • semantic tokens are usually aliases for concrete tokens, with semantic names like “warning” or “edit.” They refer not to what the token is but what it represents.

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.

Typographic tokensTypographic tokens

 


Part 3: Separation of concerns

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.

Overall Architecture

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
  • Maintain a tree of React components representing all of the sub-components of a chart
  • Render everything outside of the SVG
  • Enumerate data series
  • Assign colors and styles to chart elements
  • Rendering the visualization SVG using Visx
  • Maintain React Context for data rows
  • Maintain React Context for x- and y-scales
  • Maintain margins and spacing between axes, data area, and chart boundaries
  • Handle responsiveness and scrolling
  • useSeries maintains series visibility state, assigns colors
  • getTicks spaces out ticks
  • calculateBarPadding lays out bar widths

Application Code

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 JSXChart 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:

  • which series are toggled on/off
  • the series order
  • which color corresponds to each series
<BarChart>
const {
    getSeriesColor,
    seriesOrder,
    selectedSeriesIds,
    setSelectedSeriesIds,
} = useSeries(
    ['providerFees', 'additionalExpenses', 'adjustments'],
    { canToggle: true }
);

The ChartContainer Component

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 barWe 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:

  • They all needed to create margins for the axes
  • The application needed to be able to control the chart width and height
  • The charts need to be able to respond to the available screen width
  • When there is limited space, the chart container needs to scroll
  • They all need to position a tooltip over data

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>

Individual chart components

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:

  • Bar charts have just a single value for each row in your data table
  • A timeline chart has two values: start and end
  • The grouped bar chart has an arbitrary number of values: one for each series

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]);

Part 4: Building core systems

Once we had our core architecture in place, we needed several smaller services to help solve some of the trickier bits of chart drawing.

Chart Measurements and Responsive Bars

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 barsMinimum 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 needsBar size and spacing responds to available space & data needs

Tick Spacing

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:

  • Bars should never stick out above the largest tick value, or below the smallest
  • There should never be too many or too few ticks. 5-7 is ideal
  • Ticks should fall on nice round numbers (10,000, 250,000, etc)

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 rangesRound number axis ticks adjusting to different data ranges

Tooltip Service

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:

Bar tooltipsBar tooltips

The Future

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.

 

Author

  • 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.

Erik Pukinskis

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.