Datavis 2020 Episode 7 — Let’s Make a Face Part III (With React & D3)

We’ll discuss
Getting started with the D3 library 
Using arc from the d3-shape package 
SVG path elements
SVG group elements and the transform attribute
Using ES6 Template Literals
Adding the mouth to our face
Assignment: Tweak the face

Getting started with the D3 library

Alright we’re almost there. All we need now is the mouth. The shape of the mouth should be an arc. But unfortunately, there’s no arc primitive in SVG. But there is an arc helper function from D3 that we can use. D3 is subdivided into multiple packages, and I know for a fact that D3 shape provides the arc primitive so I just googled for D3 shape landed at the Github page for d3-shape and then I’m gonna find on this page “arc”. So this links to the documentation for arcs. 

We’re going to need to pull in the D3 library onto this page. So I’m gonna go back to unpkg.com and I’m gonna say unpkg.com/d3. And it looks like this resolves by default to the nice minified build that we want. So I’m just going to copy that, and make yet another script tag on this page. 

<script src="https://unpkg.com/d3@5.11.0/dist/d3.min.js"></script>

Using arc from the d3-shape package 

And now in our index.js, we should be able to say import { arc } from ‘d3'; And just to see if this works, I’m gonna say console.log(arc). And sure enough, something appears. So it looks like our import has worked correctly. But how do we use this arc function? Here’s some useful example code for D3 arc. 

var arc = d3.arc()
    .innerRadius(0)
    .outerRadius(100)
    .startAngle(0)
    .endAngle(Math.PI / 2);

So what’s being referred to as d3.arc here, is just .arc for us. So this is what we need to do. We need to create an arc generator instance with code that looks something like this. So I’m gonna copy this code, I’ll get rid of our console.log there, and I’m gonna make a new variable called mouthArcconst mouthArc . And I’m going to set that equal to.. I’m going to paste that code from the documentation page so that creates a new arc generator and sets up the inner radius, outer radius, the start angle, and the end angle.

const mouthArc = arc()
    .innerRadius(0)
    .outerRadius(100)
    .startAngle(0)
    .endAngle(Math.PI / 2);

What you see here is typical of the D3 API where arc() is sort of a constructor function, and then this pattern —.innerRadius(0).outerRadius(100).startAngle(0).endAngle(Math.PI / 2) is called method chaining – where you call a function on this object, you pass it some argument, and it actually returns back the original object. The same thing returned from the constructor arc(), so you can do this chaining pattern, where you can set multiple things, and then the thing that’s returned by the last function gets assigned to this variable mouthArc. But luckily, it’s the same thing that was returned by the constructor arc(). So yeah, that’s the pattern of method chaining. 

SVG path elements

The way that we use this arc generator, is that we need to create an SVG <path> element so in our SVG element, I’m going to create a path and then set the d attribute of this mouthArc invoked as a function. <path d={mouthArc()}/>

 I think that should work. Let me just try something – console.log(mouthArc()) invoked as a function.

Output when we console.log(mouthArc())

Okay, it outputs the correct thing – namely this cryptic string that’s actually a program in a domain-specific language for SVG paths. It means – M means for move to the x-coordinate. 6.12 and the y-coordinate – 100 or something like that. But we don’t really need to understand the details of this language unless we’re writing a library like d3-shape. But the thing to understand is this crazy string can be sent to the value of the d attribute for an SVG path. 

I’ll get rid of this console dialog. My question now is, why are we not seeing anything? My theory is it might be going off the screen, so let me just try changing endAngle(Math.PI * 2)

Ok, now we’re seeing something. Here in the corner. And if we change innerRadius to be say 90, now we’re seeing this arc.

SVG group elements and the transform attribute

But the problem now is we want the center of this arc to be the center of this circle. And this is kind of tricky, because arcs don’t have X and Y. But there is this cool thing we can use called an SVG group element. And if you put things inside of an SVG group element, and then move the group element, everything inside the group element moves as well. So let me put everything inside of a group element. 

Okay, I’ll open the group element like this <g> and then at the end of everything I’m gonna close the group element like this </g>. Then I’m gonna indent everything inside of this group element. Now I’m going to translate everything inside of this group element by centerX and centerY. And we can do that like this.

 <g transform={`translate(${centerX},${centerY})`}>

Now everything moved. See that our arc is in the right place, but now our whole smiley face is in the wrong place. 

Using ES6 Template Literals

Let me just break down what is going on here. These back ticks `` are an ES6 string literal. It’s pretty much a string template, where you can inject values of variables using this syntax here – $ dollar sign. And then open curly brace { and then close curly brace }. `${}`

So what this is doing, is its producing a string that says, translate, and this opening and closing parenthesis, it’s actually inside the string. 

See, the transform attribute expects a string that is a sort of a expression in a domain-specific language. And one of the valid expressions is translate. You could also say rotate(90), that rotates everything and rotate(45) but all we need right now is translate

Alright, so now that everything inside of this group element is being translated, we don’t need to use centerX and centerY for these circles anymore. We can just use the default values of zero – meaning we could just get rid of cx and cy for that one. See now it’s in the right spot. And then we don’t need centerX  and centerY here in the circle that draws the eyes. But these should be -eyeOffsetX and -eyeOffsetY.

      <circle
        cx={-eyeOffsetX}
        cy={-eyeOffsetY}
        r={eyeRadius}
      />
      <circle
        cx={eyeOffsetX}
        cy={-eyeOffsetY}
        r={eyeRadius}
      />

Okay, there’s our left eye. And now let’s get our right eye working. We can just delete centerX + and then delete centerY, but then it’s gonna be - eyeOffsetY

Adding the mouth to our face

All right! Now we’re just left with tweaking the mouthArc. Which we can do up here. 

const mouthArc = arc()
  .innerRadius(90)
  .outerRadius(100)
  .startAngle(Math.PI / 2)
  .endAngle(Math.PI * 3 / 2);

We’re almost there. We just need to change the innerRadius and the outerRadius. But I think what I’d like to do is set the outerRadius, and the innerRadius programmatically, based on some width. So let me just make a new variable called mouthWidth, and I want it to be say 20 pixels. const mouthWidth = 20;

And I’ll make another variable called mouthRadius. And this is the one that we can tweak. So I’ll set it to 200const mouthRadius = 200;So the innerRadius should be mouthRadius, and then the outerRadius should be mouthRadius + mouthWidth

const mouthArc = arc()
  .innerRadius(mouthRadius)
  .outerRadius(mouthRadius + mouthWidth)
  .startAngle(Math.PI / 2)
  .endAngle(Math.PI * 3 / 2);

Okay, now we could just tweak mouthRadius and the thickness of the mouth itself should remain constant. Okay! great. This looks like a smiley face to me all right. 

import React from 'react';
import ReactDOM from 'react-dom';
import { arc } from 'd3';

const width = 960;
const height = 500;
const centerX = width / 2;
const centerY = height / 2;
const strokeWidth = 20;
const eyeOffsetX = 90;
const eyeOffsetY = 100;
const eyeRadius = 40;
const mouthWidth = 20;
const mouthRadius = 140;

const mouthArc = arc()
  .innerRadius(mouthRadius)
  .outerRadius(mouthRadius + mouthWidth)
  .startAngle(Math.PI / 2)
  .endAngle(Math.PI * 3 / 2);

const App = () => (
  <svg width={width} height={height}>
    <g transform={`translate(${centerX},${centerY})`}>
      <circle
        r={centerY - strokeWidth / 2}
        fill="yellow"
        stroke="black"
        stroke-width={strokeWidth}
      />
      <circle
        cx={-eyeOffsetX}
        cy={-eyeOffsetY}
        r={eyeRadius}
      />
      <circle
        cx={eyeOffsetX}
        cy={-eyeOffsetY}
        r={eyeRadius}
      />
      <path d={mouthArc()}/>
    </g>
  </svg>
);

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

Assignment: Tweak the face

There we have it. That’s how you can make a smiley face with React and D3. Now here is an assignment for you. 

I’d like you to fork this work in Vizhub and tweak it to make a really cool-looking face. Part of what I want to teach here is how you can teach yourself to use these technologies. Because I’m not gonna cover every possible thing you could do with SVG. But I can get you started, and provide the foundation for doing really cool stuff. So I would encourage you to you know do a Google search for SVG ellipse, SVG lines, SVG rect, maybe even SVG gradients. If you want to get fancy or drop shadows, things like that, I want you to get creative. Maybe do something like add irises to the eyes, of the face, or add a nose, or add teeth, or I don’t know make the face a square. It’s up to you. But I just want you to fork this face, and experiment and be creative. 

To submit this assignment, just copy the Vizhub link, and share that in our course Slack channel. Also, please take a look at other students’ work, and maybe comment on one of the other students’ faces.

Next up in Datavis 2020Episode 8 – Let’s Make a Face Part IV (React Components & ES6)