How to build a type-safe pie chart in SolidJs with d3 and tailwind
Tags: solidjs, d3, typescript, tailwindcss
Published: 2023-05-22 15:03:56


#Introduction
You might have a lot of data in your SolidJs app that needs a good chart to make it easier to read. If so, you're in the right place! In this article we will go from zero to a fully type-safe and customizable pie chart powered by [d3](https://d3js.org/), so open your terminal and let's start!

#Project setup

First, we need a new Solid project with typescript, tailwind and d3. Following the Solid docs, we'll just grab the [typescript + tailwindcss template](https://github.com/solidjs/templates) and then install d3 and the d3 types.

In your terminal: 
```
$ npx degit solidjs/templates/ts-tailwindcss awesome-charts
$ cd awesome-charts
$ yarn add d3
$ yarn add -D @types/d3
```
Now that we have a blank project with all the dependencies we need, start the development server and open your favorite text editor.
```
$ yarn dev
```
You should see a `Hello tailwind!` on your browser, now let's go to the `src/App.tsx` file and add a little bit of markup.
```tsx
// App.tsx
import type { Component } from 'solid-js'

const App: Component = () => {
  return (
    <main class="bg-slate-800 min-h-screen text-slate-50">
      <div class="max-w-xl m-auto pt-32">
        <h1 class="text-3xl mb-6 font-bold">Awesome charts</h1>
        <div class="w-full bg-slate-900/50 aspect-square rounded-2xl">
          {/* chart goes here */}
        </div>
      </div>
    </main>
  )
}

export default App
```
Now we have a pretty good setup for our awesome chart to go, we just need to make it.

#Coding the chart
Let's start by thinking about the props that the Chart will need. It definitely needs a `width`, `height` and `margin` to size the content properly, which should all be numbers.
We also need to receive the data to be rendered, but since we don't know _exactly_ what shape that data has (as this is meant to be a reusable component), so we need a generic. 
On that note, we also need to know what field of the data represents the value that needs to be displayed. For example, we might have an `User` type, that has name, age and a list of friends, in this scenario, we want the Chart to represent the users by the number of friends that they have, so the Chart should allow for this type of customization. In other words, we need to pass a prop (let's call it `value`) that will allow us to pick a value from our object and use it, as long as it is a number.
In the end, the props should look something like this: 
```tsx
type ChartProps<T extends Record<string, any>> = {
  width: number
  height: number
  margin: number
  data: T[]
  value: (d: T) => number
}
```
We need a generic `T` type that is an object, `width`, `height` and `margin` are all numbers, `data` is an Array of T's and finally `value` is a function that receives a parameter called d of type T and return a number. 

Now we can create the Chart component. First we need a file, and what better place to put a component than the `components` folder? So let's do it.
```tsx
// components/Chart.tsx

type ChartProps<T extends Record<string, any>> = {
  width: number
  height: number
  margin: number
  data: T[]
  value: (d: T) => number
}

function Chart(p: ChartProps) {
  return (
    <svg viewBox={`0 0 ${p.width} ${p.height}`}>
      <g transform={`translate(${p.width / 2},${p.height / 2}`}>
      </g>
    </svg>
  )
}

export default Chart
```

Now that we have a component, we can import it on the App and replace the comment.

#But what is d3js?
If you never used d3, it is an open-source javascript library for manipulating the DOM based on data. It is completely based on web standards like HTML and CSS syntax, so it's really intuitive to get started.

Normally, d3 is used to create and edit DOM nodes, in a syntax similar to JQuery, but we are not going in that route, as we're using Solid and JSX, which provide a more declarative way of writing HTML, so instead of using it to create the elements of the chart, we will use it to generate the `d` attribute for the `<path>` element. The `d` attribute is what defines the shape of a SVG `<path>`, and can be pretty cumbersome to write by hand, so d3 will do the heavy lifting for us.

Enough talking, let's go back to the code.

#Generating SVG paths based on data
Now things start to get fun. First, we need a d3 [`pie`](https://github.com/d3/d3-shape/blob/v3.2.0/README.md#pie) object, which basically calculates the start and end angle for each item on our dataset. It will look more or less like this: 

```ts
import * as d3 from 'd3'

const pieGenerator = d3
  .pie<T>()
  .value(p.value)
```

Here we are creating and customizing the `pieGenerator`, we can pass the generic `T` that we receive from the props to make everything type-safe and then we specify the field of the object that we want to use to calculate the size of each section, in this case, we use the function that is passed via props to make it customizable. 

Now we can pass the data to the generator and store the result: 

```ts
const parsedData = pieGenerator(p.data)
```

But this is not enough, because the `parsedData` only contains the start and end angle of each section and not a string that we can pass to the `path`. In order to do that we need a [`arc`](https://github.com/d3/d3-shape/blob/v3.2.0/README.md#arc) generator. The process is very similar: 

```ts
const radius = Math.min(p.width, p.height) / 2 - p.margin

const arcGenerator = d3
  .arc<d3.PieArcDatum<T>>()
  .innerRadius(0)
  .outerRadius(radius)
```

Again, we create an `arcGenerator` from d3, passing an generic of type `d3.PieArcDatum<T>`, which just represents the data that the `pieGenerator` returns to us (you can hover over the `parsedData` to see the type), then we set the inner and outer radius of the chart, for that we have the `radius` variable, which is calculated based on the width and height of the component.

Now just as a bonus, let's make a color generator, so that the sections are different from one another. We can use d3 functions for that too.

```ts
const colorGenerator = d3
  .scaleSequential(d3.interpolateWarm)
  .domain([0, p.data.length])
```

Now all we need is to pass the `parsedData` to the `arcGenerator` using a map, as the generator only accepts one item at a time and we need to add some extra properties.

```ts
const arcs = parsedData.map((d, i) => ({
  path: arcGenerator(d),
  data: d.data,
  color: colorGenerator(i),
}))
```

Done! This should be all we need for now, let's update our Chart component.

```tsx
// components/Chart.tsx
import * as d3 from 'd3'

type ChartProps<T extends Record<string, any>> = {
  width: number
  height: number
  margin: number
  data: T[]
  value: (d: T) => number
}


function Chart<T>(p: ChartProps<T>) {
  const pieGenerator = d3.pie<T>().value(p.value)

  const parsedData = pieGenerator(p.data)

  const radius = Math.min(p.width, p.height) / 2 - p.margin
  
  const colorGenerator = d3
    .scaleSequential(d3.interpolateWarm)
    .domain([0, p.data.length])

  const arcGenerator = d3
    .arc<d3.PieArcDatum<T>>()
    .innerRadius(0)
    .outerRadius(radius)

  const arcs = parsedData.map((d, i) => ({
    path: arcGenerator(d),
    data: d.data,
    color: colorGenerator(i),
  }))

    <svg viewBox={`0 0 ${p.width} ${p.height}`}>
      <g transform={`translate(${p.width / 2},${p.height / 2}`}>
      </g>
    </svg>
}

export default Chart
```

That wasn't that bad, was it? We just need to display the data.

#Displaying the chart

For that we don't need much, just add the paths to our `svg` tag. We can do that with Solid's `For` component.

```tsx
    <For each={arcs}>
      {(d) => <path d={d.path} fill={d.color} class="transition hover:scale-105" />}
    </For>
```

Here we're creating a `<path>` for each data point we receive, using the path generated before as the "d" parameter, the interpolated color as the fill and also using some tailwind classes to add a hover animation.

This works fine, we now have a colorful pie chart. But there are some problems still: first, what if the data is dynamic? With the current code, all the arcs are generated only once, as Solid components don't re-run; also, how the user is supposed to know what each section means? For that we can create a label that updates when the user hovers a section. Let's solve those problems right now.

#Making it reactive
For the first problem, it's pretty simple to solve. We need to update the `arcs` array every time the props change, for that we can use `createSignal` and `createEffect` to make our component reactive. While we're at it, let's create another signal to store the current hovered section, so that we can display the information to the user. Also, we might need another prop to display the item information correctly, let's call it `label` and its type should be `keyof T`, as T is the type of the object we're rendering. After all these changes, we have a pretty big file, but much more functional.

```tsx
type ChartProps<T extends Record<string, any>> = {
  width: number
  height: number
  margin: number
  data: T[]
  label: keyof T
  value: (d: T) => number
}

type Arc<T> = {
  path: string
  data: T
  color: string
}

function Chart<T extends Record<string, any>>(p: ChartProps<T>) {
  const [arcs, setArcs] = createSignal<Arc<T>[]>([])
  const [hovered, setHovered] = createSignal<Arc<T> | null>(null)

  const handleMouseOver = (d: Arc<T>) => {
    setHovered(d)
  }

  const handleMouseOut = () => {
    setHovered(null)
  }

  createEffect(() => {
    const radius = Math.min(p.width, p.height) / 2 - p.margin

    const colorGenerator = d3
      .scaleSequential(d3.interpolateWarm)
      .domain([0, p.data.length])

    const pieGenerator = d3.pie<T>().value(p.value)

    const parsedData = pieGenerator(p.data)

    const arcGenerator = d3
      .arc<d3.PieArcDatum<T>>()
      .innerRadius(0)
      .outerRadius(radius)

    setArcs(
      parsedData.map((d, i) => ({
        path: arcGenerator(d),
        data: d.data,
        color: colorGenerator(i),
      }))
    )
  })

  return (
    <div>
      <svg viewBox={`0 0 ${p.width} ${p.height}`}>
        <g transform={`translate(${p.width / 2},${p.height / 2})`}>
          <For each={arcs()}>
            {(d) => (
              <path
                d={d.path}
                onMouseOver={() => handleMouseOver(d)}
                onMouseOut={() => handleMouseOut()}
                fill={d.color}
                class="hover:scale-105 transition"
              />
            )}
          </For>
        </g>
      </svg>
      <Show when={hovered()}>
        {(item) => (
          <div class="w-fit m-auto flex items-center gap-2 p-3">
            <div
              class="rounded-md w-5 aspect-square"
              style={{
                'background-color': item().color,
              }}
            />
            <span>
              {item().data[p.label]} Amount: {p.value(item().data)}
            </span>
          </div>
        )}
      </Show>
    </div>
  )
}
```

Breaking all the changes down, we updated the props as said before, created another type to represent a single `arc`, with the path, color and data, on the body of the component we now have two signals, one to hold all the arcs of the graph and the other to store the currently hovered arc. Then we have two functions that update the `hovered` signal, and finally a effect, which ensures that our code re-runs whenever the props change. Now for the JSX, we are wrapping everything with a `<div>`, the paths now have the `mouseOver` and `mouseOut` event handlers and at the end we have a `<Show>` component, which only renders when something is hovered, inside we have a bit of markup to render the label of the item and its total amount. 

#Conclusion
OK that was a lot, and I think it's time to wrap things up.
So we've talked about how to make charts with d3 in SolidJs, how to make them reactive and interactive, all while keeping the code type-safe and extensible.
If you have any suggestion or doubt, feel free to write it in the comments.
Thanks for reading!