In the era of "big data," visual representations help to make sense of datasets of any size or complexity. Furthermore, libraries like d3 have turned javascript into the premiere platform for rendering complex data visualizations. If you've heard of React (maintained by Facebook), you might know it's a great tool for creating user interfaces.

The curious coder might ask if d3 and React could work together. That coder would be correct, but the two libraries are fundamentally different in how they render to your browser. (I'm purposefully simplifying the issue. See Oliver's post for a comprehensive explanation.)

How could the two come together? I just happen to have a problem, ripe for data visualization... I want to move out of the US. Where, exactly? I've heard Middle Earth is nice.

(Click to see the completed Tiddly Bit)

(Tiddly Bits is my way of sharing small examples of code to teach big concepts)

Finding a Home

I want to live in a fantasy world. I have a few options to choose from: The quiet and miraculous Macondo, Wonderland with all its peculiarities, the colorful Oz, or Gondor, as I like places with some history.

They all have their pros and cons, so I'll pick based on population. I can store this information as an array of objects:

  const data = [
  {town: 'Macondo', pop: 24},
  {town: 'Wonderland', pop: 47},
  {town: 'Oz', pop: 66},
  {town: 'Gondor', pop: 17}
];

console.log(data[0].town) // 'Macondo'

Now, it's hard to compare plain old numbers, so I would much rather have this displayed as a bar chart. Of course, I'll cater it to my needs... I've heard of d3 and React, so I figure I'll give those a shot. However, before I plan out my code, I need to sketch this out!

Making a Plan

In my bar chart, I want to render a bar representing the population size, I want to have the names of the towns next to each bar, and I'd also like to see the raw population number. I'll orient each bar horizontally, with town names on the left, the bars to the right of those, and population number at the end of the bar. This example will serve as inspiration.

As for the code, I've heard that "thinking in React" means break my elements up into small, repeatable pieces, much like Legos. I can treat a single bar as a component, made up of even smaller components:

  // The Bar component
<Bar>
  <text /> the town name
  <rect /> the bar representing population
  <text /> the actual population number
</Bar>

I think this is enough planning... time to get render some bars.

Setup

Firstly, I have to include the d3, React, and React-DOM libraries in my code. I aslo need include Babel as my preprocessor. This is already set up, so you can work with the pen (short for CodePen) I've provided, which has the correct configuration.

As with most visualizations, I'll use svg to render my components. This requires a bit of setup, so I'll define some variables for the dimensions of my svg:

  const svgWidth = 500;  // width of entire svg
const svgHeight = 200; // height of entire svg
const textWidth = 115; // width of text
const textGutter = 10; // text/bar spacing
const barMargin = 5; // bottom margin for bars

The textGutter and barMargin variables put horizontal spacing between the text and the bars, and vertical spacing between each component, respectively. We'll take these values into account in our calculations.

A hugely powerful utility of d3 is its scale functionality, which allows us to transform our data into units that svg can understand. I can use the dimensions I just defined to help me define my scales, and I'll assume the populations can be between 0 and 100:

  const xScale = d3.scaleLinear()
  .domain([0, 100])
  .range([textWidth, svgWidth - textWidth]);

console.log(xScale(0)) // 115, which is how we defined textWidth
console.log(xScale(100)) // 500, which is how we defined svgWidth

Note that we don't want our range to start at 0, because from 0 to textWidth is where the names of our towns will go, and we wouldn't want those to overlap with our bars. Also, we don't let it go all the way to svgWidth, because then it would go past our actual svg. (Try playing with the scales domains/ranges in the pen.)

I need to make another scale for my y-axis. I know the bars will have the same height, so I'll use the number of elements in data as my domain for this scale:

  const yScale = d3.scaleLinear()
  .domain([0, data.length])
  .range([0, svgHeight]);

console.log(yScale(0)) // 0, the top of the svg
console.log(yScale(3)) // 150, not quite the bottom of the svg

One last thing: I should probably get the heights of the bars, so I'll make use of yScale(). The height of a bar would be equal to yScale(1), although I'll also have to account for vertical spacing between bars, so I'll make use of my barMargin variable:

  const barHeight = const barHeight = yScale(1) - barMargin;

This seems like a lot of setup, but any d3 project will have the dimensions specified, and the scales calculated. We'll see in a moment that they'll be a huge help. We have to do one last thing, and that's setting up React. The following code should be in your javascript file:

  // make sure your html file has <div id='root'></div>

ReactDOM.render(
  <svg width={svgWidth} height={svgHeight}>
    {data.map(renderBar)}
  </svg>,
  document.getElementById("root")
);

This is how we'll render the actual bars to the page. The <svg /> component sets up our svg using svgWidth and svgHeight, and {data.map(renderBar)} will call a function renderbar for element in data. We haven't defined it yet, but there's a good chance that's what will render each bar component. Check out my other Tiddly Bit to see how to use Array.map() for rendering.

Finally... it's time to render so I can figure out where I want to move!

Draw Some Bars

I'll define a function called renderBars, which will take in an element of data, and also its index in the array (Array.map() is smart enough to know the second argument is the index of the element in the array). It will look like this:

  const renderBar = (datum, index) => {
  // recall that datum = {town, pop}

  // we'll have to do some work to get properties
  const textProps = {...};
  const barProps = {...};
  const numberProps = {...};

  // key={index} is a technical inclusion... don't worry about what it does for now!
  return(
  <g key={index}> 
    <text {...textProps}>{datum.town}</text>
    <rect {...barProps}/>
    <text {...numberProps}>{datum.pop}</text>
  </g>
  );
};

The pattern is, for each item in data, use that information to determine the properties for each component (textProps, barProps, numberProps). Once calculated, I'll use these properties to render svg elements, namely text, a rectangle, and more text. Let's dive into the properties.

textProps

An svg text element requires an x-coordinate, a y-coordinate, and the text to render.

The x-coordinate is determined by the textWidth variable. I set it so the text is aligned at the very end of text width, leaving a little room between it and the bar with textGutter. I'll use the textAnchor: 'end' attribute to make sure the text renders to the left of the x-coordinate.

Furthermore, I need to determine the y-coordinate for the text. Since the index represents the index of the item in the array, I can use yScale(index) to get the y-coordinate. However, I want to center the text vertically, so I'll have to add barHeight / 2 to get it to the middle, and then add barMargin to scoot it down to align perfectly with each bar. (Recall that the y-coordinate 0 is at the top of the svg.)

  const textProps = {
  x: textWidth - textGutter, 
  y: yScale(index) + barMargin + barHeight / 2,
  textAnchor: 'end',
};

barProps

An svg rectangle requires an x-coordinate, a y-coordinate, a width, and a height.

My bar will start at textWidth, so that will be my x-coordinate. Like textProps, yScale(index) will determine the y-coordinate, although in this case I don't want to scoot it down any more. As I planned, datum.pop will determine the width of the bar chart, so I'll use xScale(datum.pop) for the width attribute. Lastly, I defined barHeight earlier, so I'll use that for the height attribute.

  const barProps = {
  x: textWidth,
  y: yScale(index),
  width: xScale(datum.pop),
  height: barHeight,
  fill: datum.pop < 50 ? '#6497ea' : '#bc4545',
  rx: 5,
  ry: 5,
};

There are two more attributes that aren't required, although I'd like to have them. The rx and ry attributes give me rounded corners, which isn't that amazing, but the fill attribute is more interesting. If you'll recall, I wanted to live in a place with a lot of people, so if the town has more than 50 people, I want to make it a different color so I notice it better. The statement datum.pop < 50 ? '#6497ea' : '#bc4545' reads

if datum.pop is less then 50, then return the color '#6497ea' (a shade of blue); if it is not less than 50, then return '#bc4545' (a shade of red)

Feel free to add your own conditions or colors. This is called a ternary operator, and it comes up a lot in React programming.

numberProps

These properties will closely resemble those in textProps, as I use the same y-coordinate and textAnchor: 'end' to make sure the text is aligned properly. However, the x-coordinate differs slightly...

I need to render it at the very end of the bar. While xScale(datum.pop) calculates the width of the bar, I have the x-coordinate of the bar starting at textWidth. Starting from the left of the svg, the end of the bar is textWidth + xScale(datum.pop) units away. That's where I want to place my text (although I subtract textGutter from that distance to make it look better).

  const numberProps = {
  x: textWidth + xScale(datum.pop) - textGutter,
  y: yScale(index) + barMargin + barHeight / 2,
  textAnchor: 'end',
};

And now we're done. Let's return to the renderBars overview.

Wrapping up

With all of the properties defined, I can render each component (text, bar, and number). Now renderBar will take in an element of data and return something that React can render. Here's an overview of the code:

  const renderBar = (datum, index) => {
  // recall that datum = {town, pop}

  // These are now calculated
  // See the pen for more details
  const textProps = {...};
  const barProps = {...};
  const numberProps = {...};

  // The {...props} syntax is an object spread, used widely in React development
  // remember that key={index} is a technical arifact, so don't worry about it
  return(
  <g key={index}>
    <text {...textProps}>{datum.town}</text>
    <rect {...barProps}/>
    <text {...numberProps}>{datum.pop}</text>
  </g>
  );
};

As I mentioned before, I can use {data.map(renderBar)} to call renderBar for each element in my array of town/population data. View the whole product below:

It looks like Oz has the biggest population. Maybe I'll move there...

Bigger Picture

So what's the big deal? Well, we've gone over quite a bit of material in a pretty simple example. We got an overview of React's component-based model, and we saw the use of d3 scales. Furthermore, we saw how to render data with svg's, including using conditionals and variable styles.

We saw how data could drive our design. With renderBar, a single function, we could render four completely different data points. This data visualization told me exactly what I wanted to know, and with the power of d3 and React, I could make it drastically more complex.

Thanks for reading! Check out my profile for more Tiddly Bits!

Follow me on Twitter for updates on Tiddly Bits.

Technical Note

While this is a good place to get started, it's not what I would recommend for larger visualizations/apps. This makes heavy use of global variables, which is fine for this small example, but not great for more complex use-cases. I would recommend using higher-order components for visualizations. If there's enough interest in that, let me know and I might write another article.


3,111 2 40