Basketball Stats Through D3 & React
Tags: d3, react, javascript, data
Published: 2019-06-25 06:01:30

---
title: Basketball Stats Through D3 & React
published: true
description: Tips and tricks for creating data visualizations with D3 and React.
tags: d3,react,javascript,data
---

![](https://thepracticaldev.s3.amazonaws.com/i/cof58zsskyfyif4ub93k.png)

Make a _Donut Chart_ to visualize the scoring totals of the 2018-19 **Los Angeles Lakers** basketball team.

# Data

The first thing we need to create our data visualization is, not coincidentally, data. [This well written article](https://benbernardblog.com/web-scraping-and-crawling-are-perfectly-legal-right/) explains some of the legal and ethical ramifications of web scraping. [This repository](https://github.com/awesomedata/awesome-public-datasets) offers links to free public data. **Dev.to** itself has many articles on data, web scrapers, and visualizations. My two cents is that for simple data visualizations projects, good old **Chrome Devtools** on its own is more than enough to gather and shape data. Check out this over-simplified example.

|      Name       | Age |
| :-------------: | :-: |
|  LeBron James   | 34  |
| Zion Williamson | 18  |
| Micheal Jordan  | 56  |

Given the above table here are the steps to massage the data:

1. Open **Chrome Devtools**
2. Isolate all table rows
3. Convert results from a _NodeList_ to an _Array_ and ditch the title row
4. Extract text from each table data cell and map the results to a new array of objects
5. Type `c` (the variable name) and press `Enter` and your new array will be displayed in the console
6. Right click the array and chose `Store as Global Variable`. You will see `temp1` appear in the console.
7. Use the built in `copy` function to copy the temporary variable to the clipboard - `copy(temp1)`
8. Paste your data into a **JavaScript** or **JSON** file.
9. 🤯

```js
var a = document.querySelectorAll('tr') // 2
var b = Array.from(a).slice(1) // 3
var c = b.map(el => {
  // 4
  var name = el.children[0].innerText
  var age = el.children[1].innerText
  return { name, age }
})

c // 5
// right click array
copy(temp1) // 7
```

Note that every scenario is different and this example is simplified to help explain the process. Also, all of the logic above can be put into a single function to streamline the process. Remember you can create multi-line functions in the console by using `Shift+Enter` to create new lines. With this method we have what amounts to manual web scraping with **JavaScript** 101. Be sure to read a website's _Terms of Service_ before going `willy-nilly` and harvesting data where you aren't supposed to.

![](https://thepracticaldev.s3.amazonaws.com/i/vtojf81pxsrfj6q30wtj.png)

# Create a Donut Chart

Getting **D3** and **React** to work together is not really that complicated. Generally, all that is needed is an entry point to the DOM and some logic that initializes the visualization when the page loads. To get started with our example project we want to have `create-react-app` installed. The first step is to create a new project. The first thing I like to do is clear out the `src` directory, leaving only `App.js` and `index.js`. Don't forget to remove any old `import` statements. Before we write any code we need to snag a couple dependencies.

1- Download **D3** and **Styled Components**.

```
npm i d3 styled-components
```

2- Create a new file `whatever-you-want.js`, or even `data.js` in the `src` directory. The data used in the example is available [in this gist](https://gist.github.com/benjaminadk/45f2349ab415a334be4374b5fffd4884).

3- Create some basic boilerplate that can be used for a variety of projects with this configuration - aka **D3** + **React** + **Styled Components**. I encourage you to tweak whatever you see fit as like most developers I have my own quircks and patterns. Case in point, I am bothered by `#000000` black so I use `#333333`, I like the font `Raleway`, etc. If you haven't used _Hooks_ before, the `useEffect` hook with an empty `[]` dependency array is similar to `componentDidMount` in a **React** class component. The numbered comments correspond to upcoming steps and are the place to insert the code from those steps.

```jsx
import React, { useRef, useEffect, useState } from 'react'
import * as d3 from 'd3'
import styled, { createGlobalStyle } from 'styled-components'
import data from './data'

const width = 1000
const height = 600
const black = '#333333'
const title = 'My Data Visualization'

// 4

// 7

export const GlobalStyle = createGlobalStyle`
@import url('https://fonts.googleapis.com/css?family=Raleway:400,600&display=swap');

body {
  font-family: 'Raleway', Arial, Helvetica, sans-serif;
  color: ${black};
  padding: 0;
  margin: 0;
}
`

export const Container = styled.div`
  display: grid;
  grid-template-rows: 30px 1fr;
  align-items: center;
  .title {
    font-size: 25px;
    font-weight: 600;
    padding-left: 20px;
  }
`

export const Visualization = styled.div`
  justify-self: center;
  width: ${width}px;
  height: ${height}px;
// 6
`

export default () => {
  const visualization = useRef(null)

  useEffect(() => {
    var svg = d3
      .select(visualization.current)
      .append('svg')
      .attr('width', width)
      .attr('height', height)
// 5

// 8
  }, [])

  return (
    <>
      <GlobalStyle/>
      <Container>
        <div className='title'>{title}</div>
        <Visualization ref={visualization} />
        {/*10*/}
      </Container>
    <>
  )
}
```

4- We need to establish a color scheme and some dimensions for our _Donut Chart_.

The radius of our pastry.

```js
const radius = Math.min(width, height) / 2
```

It only makes sense to use a **Lakers** color theme.

```js
var lakersColors = d3
  .scaleLinear()
  .domain([0, 1, 2, 3])
  .range(['#7E1DAF', '#C08BDA', '#FEEBBD', '#FDBB21'])
```

The **D3** `pie` function will map our data into pie slices. It does this by adding fields such as `startAngle` and `endAngle` behind the scenes. We are using an optional `sort` function just to shuffle the order of the slices. Play around with this, pass it `null` or even leave it out to get different arrangements. Finally, we use the `value` function to tell **D3** to use the `points` property to divide up the pie. Log the `pie` variable to the console to help conceptualize what the **D3** pie function did to our data.

```js
var pie = d3
  .pie()
  .sort((a, b) => {
    return a.name.length - b.name.length
  })
  .value(d => d.points)(data)
```

Now we need to create circular layouts using the `arc` function. The variable `arc` is for our _Donut Chart_ and the `outerArc` will be used as a guide for labels later. `getMidAngle` is a helper function to be used at a later time also.

```js
var arc = d3
  .arc()
  .outerRadius(radius * 0.7)
  .innerRadius(radius * 0.4)

var outerArc = d3
  .arc()
  .outerRadius(radius * 0.9)
  .innerRadius(radius * 0.9)

function getMidAngle(d) {
  return d.startAngle + (d.endAngle - d.startAngle) / 2
}
```

5- With a structure in place are almost to the point of seeing something on the screen.

Chain the following to our original `svg` variable declaration.

```js
   .append('g')
   .attr('transform', `translate(${width / 2}, ${height / 2})`)
```

Now the magic happens when we feed our `pie` back to **D3**.

```js
svg
  .selectAll('slices')
  .data(pie)
  .enter()
  .append('path')
  .attr('d', arc)
  .attr('fill', (d, i) => lakersColors(i % 4))
  .attr('stroke', black)
  .attr('stroke-width', 1)
```

Next we need to draw lines from each slice that will eventually point to a label. The well named `centroid` function returns an array with `[x,y]` coordinates to the center point of the `pie` slice (in this case `d`) within the `arc`. In the end we are returning an array of three coordinate arrays that correspond to the origin point, bend point, and termination point of each line the now appears on the screen. The `midAngle` helps determine which direction to point the tail of our line.

```js
svg
  .selectAll('lines')
  .data(pie)
  .enter()
  .append('polyline')
  .attr('stroke', black)
  .attr('stroke-width', 1)
  .style('fill', 'none')
  .attr('points', d => {
    var posA = arc.centroid(d)
    var posB = outerArc.centroid(d)
    var posC = outerArc.centroid(d)
    var midAngle = getMidAngle(d)
    posC[0] = radius * 0.95 * (midAngle < Math.PI ? 1 : -1)
    return [posA, posB, posC]
  })
```

Now our lines are ready for labels. The label seems to look better by adding some symmetry by flip-flopping the order of `name` and `points` based on which side of the chart it appears on. Notice that the `pie` function moved our original `data` into a key named `data`. The top level keys of `pie` objects contain the angle measurements used in the `getMidAngle` function.

```js
svg
  .selectAll('labels')
  .data(pie)
  .enter()
  .append('text')
  .text(d => {
    var midAngle = getMidAngle(d)
    return midAngle < Math.PI
      ? `${d.data.name} - ${d.data.points}`
      : `${d.data.points} - ${d.data.name}`
  })
  .attr('class', 'label')
  .attr('transform', d => {
    var pos = outerArc.centroid(d)
    var midAngle = getMidAngle(d)
    pos[0] = radius * 0.99 * (midAngle < Math.PI ? 1 : -1)
    return `translate(${pos})`
  })
  .style('text-anchor', d => {
    var midAngle = getMidAngle(d)
    return midAngle < Math.PI ? 'start' : 'end'
  })
```

6- To polish off our labels with some style we just need to add a couple lines of code to the `Visualization` styled component. Having used **D3** to add a `class` attribute inside a **React** `useEffect` hook and then defining that class using **Styled Components** seems to check the boxes on integrating the libraries.

```css
.label {
  font-size: 12px;
  font-weight: 600;
}
```

7- We are looking good but why not add a little more flavor to give the user an interactive feel. We can quickly grab the total amount of points scored using the `sum` function from **D3**.

```js
var total = d3.sum(data, d => d.points)
```

8- The `showTotal` function will simply tack on a `text` node displaying our total. The `text-anchor` style property of `middle` should center the text within our *Donut* hole. The `hideTotal` function will come into play in a bit. Notice we are calling the `showTotal` function to make sure the text is showing when the page loads.

```js
function showTotal() {
  svg
    .append('text')
    .text(`Total: ${total}`)
    .attr('class', 'total')
    .style('text-anchor', 'middle')
}

function hideTotal() {
  svg.selectAll('.total').remove()
}

showTotal()
```

We should tack on another class for `total` right next to our `label` class from step 6.

```css
.total {
  font-size: 20px;
  font-weight: 600;
}
```

9- The numbered comment system is a getting a little gnarly at this point, but if you have made it this far you are smart enough to follow along. These next functions can go below `hideTotal`. These are listeners we will apply to each slice.

```js
function onMouseOver(d, i) {
  hideTotal()
  setPlayer(d.data)
  d3.select(this)
    .attr('fill', d3.rgb(lakersColors(i % 4)).brighter(0.5))
    .attr('stroke-width', 2)
    .attr('transform', 'scale(1.1)')
}

function onMouseOut(d, i) {
  setPlayer(null)
  showTotal()
  d3.select(this)
    .attr('fill', lakersColors(i % 4))
    .attr('stroke-width', 1)
    .attr('transform', 'scale(1)')
}
```

When a slice is hovered the stroke and fill will be emphasized and a slight scale up will add a cool effect. The total points text will also be toggled so we can stick a tooltip with a little more information smack dab in the hole. First we need to create a piece of `state`, what would a **React** app be without it.

```js
const [player, setPlayer] = useState(null)
```

A keen observer may have noticed the reference to `this` and wondered what was happening. The following listeners need to be tacked on to the end of the `slices` **D3** chain.

```js
   .attr('class', 'slice')
   .on('mouseover', onMouseOver)
   .on('mouseout', onMouseOut)
```

Since we are using a `transform` on the `slice` class let's control it through another couple lines in the `Visualization` styled component.

```css
.slice {
  transition: transform 0.5s ease-in;
}
```

10- We can now create the tooltip to display the `player` state that changes as individual slices are moused over.

```jsx
{
  player ? (
    <Tooltip>
      <div>
        <span className='label'>Name: </span>
        <span>{player.name}</span>
        <br />
        <span className='label'>Points: </span>
        <span>{player.points}</span>
        <br />
        <span className='label'>Percent: </span>
        <span>{Math.round((player.points / total) * 1000) / 10}%</span>
      </div>
    </Tooltip>
  ) : null
}
```

In terms of new information the user is only getting the percentage of the team's points the current player scored. However, with the centralized position combined with the movement a nice effect and a nice feeling of interactivity is created. A similar pattern could be used more effectively if there was more information to show or I was smarter. It seems the last thing needed is the `Tooltip` component, which goes with the other styled components.

```jsx
export const Tooltip = styled.div`
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: ${radius * 0.7}px;
  height: ${radius * 0.7}px;
  display: grid;
  align-items: center;
  justify-items: center;
  border-radius: 50%;
  margin-top: 10px;
  font-size: 12px;
  background: #ffffff;
  .label {
    font-weight: 600;
  }
`
```

Alas, our final code should look something like the following.

```jsx
import React, { useRef, useEffect, useState } from 'react'
import * as d3 from 'd3'
import data from './data'
import styled, { createGlobalStyle } from 'styled-components'

/**
 * Constants
 */
const width = 1000
const height = 600
const radius = Math.min(width, height) / 2
const black = '#333333'
const title = 'Los Angeles Lakers Scoring 2018-19'

/**
 * D3 Helpers
 */

// total points
var total = d3.sum(data, d => d.points)

// lakers colors
var lakersColors = d3
  .scaleLinear()
  .domain([0, 1, 2, 3])
  .range(['#7E1DAF', '#C08BDA', '#FEEBBD', '#FDBB21'])

// pie transformation
var pie = d3
  .pie()
  .sort((a, b) => {
    return a.name.length - b.name.length
  })
  .value(d => d.points)(data)

// inner arc used for pie chart
var arc = d3
  .arc()
  .outerRadius(radius * 0.7)
  .innerRadius(radius * 0.4)

// outer arc used for labels
var outerArc = d3
  .arc()
  .outerRadius(radius * 0.9)
  .innerRadius(radius * 0.9)

// midAngle helper function
function getMidAngle(d) {
  return d.startAngle + (d.endAngle - d.startAngle) / 2
}
/**
 * Global Style Sheet
 */
export const GlobalStyle = createGlobalStyle`
@import url('https://fonts.googleapis.com/css?family=Raleway:400,600&display=swap');

body {
  font-family: 'Raleway', Arial, Helvetica, sans-serif;
  color: ${black};
  padding: 0;
  margin: 0;
}
`

/**
 * Styled Components
 */
export const Container = styled.div`
  display: grid;
  grid-template-rows: 30px 1fr;
  align-items: center;
  user-select: none;
  .title {
    font-size: 25px;
    font-weight: 600;
    padding-left: 20px;
  }
`

export const Visualization = styled.div`
  justify-self: center;
  width: ${width}px;
  height: ${height}px;
  .slice {
    transition: transform 0.5s ease-in;
  }
  .label {
    font-size: 12px;
    font-weight: 600;
  }
  .total {
    font-size: 20px;
    font-weight: 600;
  }
`

export const Tooltip = styled.div`
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: ${radius * 0.7}px;
  height: ${radius * 0.7}px;
  display: grid;
  align-items: center;
  justify-items: center;
  border-radius: 50%;
  margin-top: 10px;
  font-size: 12px;
  background: #ffffff;
  .label {
    font-weight: 600;
  }
`

export default () => {
  const [player, setPlayer] = useState(null)

  const visualization = useRef(null)

  useEffect(() => {
    var svg = d3
      .select(visualization.current)
      .append('svg')
      .attr('width', width)
      .attr('height', height)
      .append('g')
      .attr('transform', `translate(${width / 2}, ${height / 2})`)

    svg
      .selectAll('slices')
      .data(pie)
      .enter()
      .append('path')
      .attr('d', arc)
      .attr('fill', (d, i) => lakersColors(i % 4))
      .attr('stroke', black)
      .attr('stroke-width', 1)
      .attr('class', 'slice')
      .on('mouseover', onMouseOver)
      .on('mouseout', onMouseOut)

    svg
      .selectAll('lines')
      .data(pie)
      .enter()
      .append('polyline')
      .attr('stroke', black)
      .attr('stroke-width', 1)
      .style('fill', 'none')
      .attr('points', d => {
        var posA = arc.centroid(d)
        var posB = outerArc.centroid(d)
        var posC = outerArc.centroid(d)
        var midAngle = getMidAngle(d)
        posC[0] = radius * 0.95 * (midAngle < Math.PI ? 1 : -1)
        return [posA, posB, posC]
      })

    svg
      .selectAll('labels')
      .data(pie)
      .enter()
      .append('text')
      .text(d => {
        var midAngle = getMidAngle(d)
        return midAngle < Math.PI
          ? `${d.data.name} - ${d.data.points}`
          : `${d.data.points} - ${d.data.name}`
      })
      .attr('class', 'label')
      .attr('transform', d => {
        var pos = outerArc.centroid(d)
        var midAngle = getMidAngle(d)
        pos[0] = radius * 0.99 * (midAngle < Math.PI ? 1 : -1)
        return `translate(${pos})`
      })
      .style('text-anchor', d => {
        var midAngle = getMidAngle(d)
        return midAngle < Math.PI ? 'start' : 'end'
      })

    function showTotal() {
      svg
        .append('text')
        .text(`Total: ${total}`)
        .attr('class', 'total')
        .style('text-anchor', 'middle')
    }

    function hideTotal() {
      svg.selectAll('.total').remove()
    }

    function onMouseOver(d, i) {
      hideTotal()
      setPlayer(d.data)
      d3.select(this)
        .attr('fill', d3.rgb(lakersColors(i % 4)).brighter(0.5))
        .attr('stroke-width', 2)
        .attr('transform', 'scale(1.1)')
    }

    function onMouseOut(d, i) {
      setPlayer(null)
      showTotal()
      d3.select(this)
        .attr('fill', lakersColors(i % 4))
        .attr('stroke-width', 1)
        .attr('transform', 'scale(1)')
    }

    showTotal()
  }, [])

  return (
    <>
      <GlobalStyle />
      <Container>
        <div className='title'>{title}</div>
        <Visualization ref={visualization} />
        {player ? (
          <Tooltip>
            <div>
              <span className='label'>Name: </span>
              <span>{player.name}</span>
              <br />
              <span className='label'>Points: </span>
              <span>{player.points}</span>
              <br />
              <span className='label'>Percent: </span>
              <span>{Math.round((player.points / total) * 1000) / 10}%</span>
            </div>
          </Tooltip>
        ) : null}
      </Container>
    </>
  )
}
```

[**NBA Player Salaries & Performance 2018-19 (Bubble Chart)**](https://benjaminadk.github.io/d3-nba-salaries/)

[**Inspiration for example Donut Chart**](https://www.d3-graph-gallery.com/graph/donut_label.html)