Skip to main content
Test Double company logo
Services
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say Hello
Test Double logo
Menu
Services
BackGrid of dots icon
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
Cycle icon
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say hello
Developers
Developers
Developers
Software tooling & tips

Building a well-factored pie graph with React and D3

Discover how to effectively integrate React and D3 to create a seamless, interactive pie graph. Learn the strategy behind separating state management and graph presentation for optimal results.
Sam Jones
|
January 4, 2017
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Our goal is to analyze two popular tools to find their strengths, then build a well-factored integration between the two tools.

As an example of this technique, we will create a well-factored pie graph. We will focus on separating the responsibilities of state management and graph presentation by using tools that shine for each purpose separately. We will use React to manage state and D3 for presentation.

The live example below was built in an example repository. I’m going to focus on overall strategy and critical analysis, rather than provide a comprehensive code listing. Feel free to follow along with the code example.

 

Incoming data

const data = [{
    name: 'Apple',
    percent: 30,
    color: '#E7BD2C'
  }, {
    name: 'Cherry',
    percent: 60,
    color: '#E73E2'
  }, {
    name: 'Pumpkin',
    percent: 10,
    color: '#E7932C'
  }]

We will provide data to fill our pie graph. Each piece of data will contain it’s name, color for the graph, and the percent of the graph it will represent.

Start with state

We will make our graph interactive by highlighting which slice we’ve clicked.

When considering this requirement, we will need to store state within our graph. Our state will be the data to build the graph, and also, a record of which slice has been selected. The ability to manage state in this way is the main strength of React. React provides 3 important features for this graph.

  1. We can store the incoming data as state within the component to eventually provide to the presentation piece.
  2. We can provide a function to update the selected slice and provide it as a click event to the presentation piece.
  3. We can test the state change in isolation, and verify the interaction with our presentation piece.

The last feature, test isolation, is important to our goal of creating a well factored interaction between components. If we separate the presentation piece and treat it as a black box, we can have a more focused test for our component. Our component will be smaller and easier to understand. It will have the single responsibility of managing the collaboration between our state and our presentation piece.

A collaborating component

Let’s begin examining how our React Component is managing the collaboration between our data and our presentation piece. We’ll take a look at how we store the incoming data, after first preparing it for our presentation piece.

import { calculateAngles } from './pie.d3'

export default class Pie extends React.Component {
  constructor(props) {
    // ...
    this.state = {
      data: calculateAngles(this.props.data).sort(d => d.selected)
    }
  }
  // ...
}

Separate the calculation

We must transform the percentages into angles between 0 and 2 PI to satisfy the presentation piece. However, you don’t see a hint of that algorithm here. The calculation could happen in-line, but let’s consider how that code should be factored.

Inlining the calculation would distract from the responsibility of the component. When we separate it from the component, then the component does not need to know the algorithm to prepare the data. It’s only responsibility remains to coordinate the incoming data and our presentation piece. Most importantly, the calculation can be tested in isolation. Our component only needs to verify the interaction with the calculation.

Presentation as a black box

When our component is mounted, and also when the state has changed, we are sending our current state as data to the presentation piece. At this point our component doesn’t even know that we’re using D3 to render the pie graph, just that it has a collaborator to do the hard work.

import { loadGraphic } from './pie.d3'

export default class Pie extends React.Component {
  // ...
  createGraphic() {
    loadGraphic({
      rootNode: this.refs.arc,
      data: this.state.data,
      onSliceClick: this.selectData.bind(this),
      height: this.props.height,
      width: this.props.width
    })
  }
  // ...
}

If we look at the function call to loadGraphic, which renders the pie graph, we’ve got a clean “black box” interaction.

  • We’re providing a state manipulation function as a click event.
  • We’re providing a rendered DOM node as the root of the pie graph.
  • We’re providing the current state of the component as the data for the pie graph.

This allows us to perform interaction based testing with the presentation piece. We can focus on one responsibility still, coordinating our state with our presentation piece.

Rendering a pie graph

React has the ability to build an SVG, since <svg> can easily be represented with JSX. However, D3 has a large set of mature libraries for building complex visualizations. For example, we will be using the d3-shape library to calculate the paths of a our slices within our pie graph. Defining the paths is a complex calculation, which D3 provides an easy to use API for.

React and D3 both have impressive strengths that apply directly to the problem being solved, but in different ways. It is best to find a way to integrate the two tools, rather than coerce one tool into doing a job that it is not ideally suited for. This integration could be complex, tightly coupled, and difficult to change in the future. However, by factoring them properly, we are creating a partnership that allows both tools to focus on what they are best at.

A clean entry point

When designing a loosely coupled component, as we are with our presentation piece, it’s important to define an easy to interact with entry point. You saw in the createGraphic function of our component that the public API was clean and easy to call.

Just as important, though, is having a small, simple, and easy to understand function body within that entry point. Let’s take a look at our loadGraphic function for an example.

import compose from 'lodash/fp/compose'

export function loadGraphic({rootNode, data, onSliceClick, height=600, width=600}) {
  compose(
    createLegend(data, height, width),
    createChart(data, height, width, onSliceClick),
    createShadow,
    setContext(height, width)
  )(rootNode)
}

We’re using a functional programming concept called compose to define what it means to load our graphic. Compose gives us the ability to glue together a series of steps into a larger piece of functionality. The important take-away here is that each step in building the graphic says WHAT is happening, and not HOW. It’s composed into a larger piece of functionality on rootNode.

The power of pie

To properly understand the value in choosing to use D3 for presentation, we will need to take a closer look at the API it provides for defining a pie graph.

import { arc as d3Arc, pie as d3Pie } from 'd3-shape'

function createChart(data, height, width, onSliceClick) {
  return function(rootNode) {
    // 1. Transforming the data within the chart
    const chart = rootNode.append('g')
      .selectAll('g')
      .data(pie(data), d => d.data.name)

    // 2. Adding our slices to the chart
    chart.enter().insert('path')
      .attr('d', slice(radius(height, width)))
      // ...

    chart.exit().remove()

    return rootNode
  }
}

function radius(height, width) {
  return width / 2
}

function pie(data) {
  return d3Pie().value(d => d.name)(data)
}

function arc(radius) {
  return d3Arc().outerRadius(radius * 0.4).innerRadius(radius * 0.2)
}

function slice(radius) {
  return arc(radius).startAngle(d => d.data.startAngle).endAngle(d => d.data.endAngle)
}

There two important things happening in the createChart function that are the crux of the value provided by D3.

1. Transforming the data within the chart

When we define the chart, we will create a container group (<g>) to hold our chart. We will assign the data for this group by using a function pie. This provides a declarative API to define the data being iterated over, and abstract away any nitty-gritty details about its structure. Noticing a pattern?

2. Adding our slices to the chart

For each piece of data defined by the pie transformation, we will define a path. This path needs daunting calculations to be performed. Luckily D3 has taken care of this task for us using the slice function! If you don’t believe this is valuable, take a look at the HTML source for the example above and try to understand the path being defined.

In summary

Well factored code allows us to respond to strengths and weaknesses in our tools. We can integrate our favorites together more easily, and with better success when we understand how to do a few simple things!

  • Define Separate Responsibilities
  • Retain Focus Inside Single Units
  • Define Clear Boundaries
  • Create Clean Public APIs

There is a repository available with a full code example for this post. It is under the MIT license, so please use it to learn, share, or expand upon for your own React+D3 pie graph. If you’ve got any questions, or ideas on how to improve the code, please open an issue!

Relevant links

  • React
  • D3
  • SVG
  • Example Repository

Related Insights

🔗
Creating advanced line graphs in React with MUI X Charts
🔗
The nine best recommendations in the new React docs
🔗
A guide to building a custom ESLint rule for React Testing Library

Explore our insights

See all insights
Leadership
Leadership
Leadership
Why we coach the system, not just the team

Slow delivery isn’t usually about your people—it’s about your system. Shifting focus to incremental improvements in the system helps change not just processes but behaviors for lasting change.

by
Doc Norton
Developers
Developers
Developers
Developer QA checklist for feature releases

Quality Assurance is a mindset integrated throughout development to catch issues early, build user trust, and reduce maintenance costs. These recommended procedures for dev teams without dedicated QA roles establish collective responsibility for ensuring feature stability, functionality, and usability before release.

by
Lee Quarella
Developers
Developers
Developers
From engineer to consultant: The powerful shift from inward to outward focus

What transforms a skilled software engineer into an exceptional consultant? Approach new codebases with respect rather than judgment, embrace constraints as creative boundaries, and prioritize client needs over personal preferences.

by
Dave Mosher
Letter art spelling out NEAT

Join the conversation

Technology is a means to an end: answers to very human questions. That’s why we created a community for developers and product managers.

Explore the community
Test Double Executive Leadership Team

Learn about our team

Like what we have to say about building great software and great teams?

Get to know us
No items found.
Test Double company logo
Improving the way the world builds software.
What we do
Services OverviewSoftware DeliveryProduct ManagementLegacy ModernizationDevOpsUpgrade RailsTechnical RecruitmentTechnical Assessments
Who WE ARE
About UsCulture & CareersGreat CausesEDIOur TeamContact UsNews & AwardsN.E.A.T.
Resources
Case StudiesAll InsightsLeadership InsightsDeveloper InsightsProduct InsightsPairing & Office Hours
NEWSLETTER
Sign up hear about our latest innovations.
Your email has been added!
Oops! Something went wrong while submitting the form.
Standard Ruby badge
614.349.4279hello@testdouble.com
Privacy Policy
© 2020 Test Double. All Rights Reserved.