Creating fun shapes in D3.js

I’m happy to announce that more SVG fun is coming! I’ve been blown away by the stats on my previous D3-related posts and it really motivated me to keep going with this series. I’ve fell in love with D3.js for the way it transforms storytelling. I want to get better with advanced D3 graphics so I figured I will start by getting the basics right. So today you will see me doodling around with some basic SVG elements. The goal is to create a canvas and add onto it a rectangle, a line, and a radial shape.

Base SVG element

My first step is to create an SVG element that I can use as a base for the drawing. The picture below is an idea of where I want to get: a nice, scalable canvas with a detailed grid to guide my orientation on the plane. Part of the plan is to mold my canvas to a specific shape: a 100×100 square, to be precise. To get me there, 4 elements will need to come together: a base SVG shape, x and y axes, scales to transform the input accordingly to the window size, and grids across the square.

The first goal: create a 100×100 SVG canvas

The first thing I will do is making sure my SVG is scalable and works well on all resolutions. Here I used the viewBox property and panned it a bit to the top left to accommodate for the scales that I’m planning to add.


var w = 800;
var h = 800;
var svg = d3.select("div#container").append("svg")
         .attr("preserveAspectRatio", "xMinYMin meet")
         .attr("viewBox", "-20 -20 " + w + " " + h)
         //this is to zoom out
         //.attr("viewBox", "-20 -20 1600 1600")
         .style("padding", 5)
         .style("margin", 5);

Getting SVG right can be tricky. Especially if you, like me, have the innate ability to misunderstand things at the first sight. The first time I touched the viewBox property not only I badly misconfigured it, but it also took me a couple of hours to unlearn it. 

The viewBox attribute is responsible for specifying the start (x, y) and the zoom (w, h) of an SVG. If you set the zoom properties to anything lower than the width and height of your page then it will zoom in. Anything bigger, it will zoom out. I found that zooming out is useful when I’m working on the whole composition, hence the commented out line: .attr(“viewBox”, “-20 -20 1600 1600”).

Scales, axes, and grids

Since my SVG can scale depending on the window resolution, I need everything else to scale with it: that includes both canvas elements like axes and the shapes I’m planning to draw later on. D3 takes care of linear scaling of data with the built-in function d3.scaleLinear() that takes an input domain (e.g. 0 to 100) and transforms it to a range of values (e.g. onto 0 to 200). In my case I want my input data to be translated to my current window’s height and width. I’ll point to those values in my scales’ range accessor. The viewBox function will then take care of scaling it elegantly. The input domain will be set to [0,100] for x, and [100,0] for y as I want to fit everything on a square that measures 100^2.


//PREPARE SCALES   
var xScale = d3.scaleLinear()
             //accepts
             .domain([0, 100])
             //outputs
             .range([0, w]);

var yScale = d3.scaleLinear()
             //accepts
             .domain([0, 100])
             //outputs
             .range([0, h]);

The next elements I will draw are the axes and the grids. I don’t have the type of spacial imagination to feel comfortable on an abstract SVG plane. The constant need to calculate where to put my next point gives me anxiety. Take a bar chart: it would start at a point that has an x and a y, each bar has a certain height, so you’d subtract that height from the SVG’s height (otherwise the bars will be hanging from above like stalactites), then add to it a couple of more transformations. This has me lost in space. Axes give me reasonable comfort, but I thought with grid lines I could achieve maximum control over things. 

My axes will surround the square I want to force the data points onto. The axes will run along every side of the square, with ticks every 5 steps of my (0, 100) range. 


//PREPARE AXES 
var xAxisBottom = d3.axisBottom(xScale).ticks(20); 
var xAxisTop = d3.axisTop(xScale).ticks(20);   
var yAxisLeft = d3.axisLeft(yScale).ticks(20); 
var yAxisRight = d3.axisRight(yScale).ticks(20);       
               
//DRAW AXES
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxisBottom);

svg.append("g")
.attr("class", "axis")
.call(xAxisTop);

svg.append("g")
.attr("class", "axis")
.call(yAxisLeft);

svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + w + ",0)")   
.call(yAxisRight);

You will notice that I needed to move the bottom axis to the square’s bottom with the translate function – and the right axis to the right side of the square. translate can be used to move any object from its default or previously defined position. The orientation [Bottom|Top|Left|Right] chosen with d3.axis only defines the orientation of the ticks.

My canvas with the axes added looks like this. Happy times: the square is clearly visible.

Second step: adding in the axes

The next step is to construct the grid lines. I decided that there will be a grid line crossing the square at every 10 points – that is my major grid. Every 1 point I will draw an auxiliary grid line, marked in the CSS with a lighter grey. Grids are constructed with the same function my axes used. The grids are in fact stretched ticks of an axis. See how I set their property to the square’s height and width. 


//PREPARE GRIDS
//MAIN
var ygridlines = d3.axisTop()
                 .tickFormat("")
                 .tickSize(-h)
                 .ticks(10)
                 .scale(xScale);
               
var xgridlines = d3.axisLeft()
                 .tickFormat("")
                 .tickSize(-w)
                 .ticks(10)
                 .scale(yScale);

//MINOR
var ygridlinesmin = d3.axisTop()
                    .tickFormat("")
                    .tickSize(-h)
                    .ticks(100)
                    .scale(xScale);
       
var xgridlinesmin = d3.axisLeft()
                    .tickFormat("")
                    .tickSize(-w)
                    .ticks(100)
                    .scale(yScale);
//DRAW GRIDS
//MINOR GRID
svg.append("g")
.attr("class", "minor-grid")
.call(ygridlinesmin);

svg.append("g")
.attr("class", "minor-grid")
.call(xgridlinesmin);

//MAIN GRID
svg.append("g")
.attr("class", "main-grid")
.call(ygridlines);

svg.append("g")
.attr("class", "main-grid")
.call(xgridlines);

After the grids are plotted on the SVG, the base canvas is ready:

Fun shapes

It’s time to get to drawing – the fun part. 

Drawing a rectangle is incredibly easy as D3 provides a pre-defined D3 shape, rect. A rect shape can be appended directly to the SVG. You need to give your shape a height, a width, and the starting coordinates. Then you pass it via our scaling functions (xScale and yScale, depending on whether something is on the x or y axis), and append it to the SVG.


var rect = svg.append("rect")
       .attr("height",function(d){
         return yScale(50);})
       .attr("width", function(d){
          return xScale(10);})
       .attr("y",function(d){
          return yScale(10);})
       .attr("x",function(d){
          return xScale(10);})
       .attr("class", "rectangle");

This code plots a rectangle on my canvas:

A purple rectangle is added

Drawing a line is not particularly difficult either, but it requires a bit of understanding how d3.line() function works. The function accepts an array of coordinates and draws a line through them. So the only thing we do is provide those points and pass them through the function and the appropriate scale. Note that my scales accept a domain of 0 to 100, so I had to adhere to it when defining the data points.


var lineGenerator = d3.line()
                    .x(function(d) { return xScale(d[0]) })
                    .y(function(d) { return yScale(d[1]) });

var points = [
    [40, 30],
    [50, 20],
    [60, 13],
    [70, 25],
    [80, 10],
    [90, 15]
];

var pathData = lineGenerator(points);
var freeline = svg.append("path")
               .attr("class", "freeline")
               .attr("d", function(d) { return pathData ; });

The line is plotted as expected:

A pink line joins in

Drawing a radial shape has proven a bit complex. I recommend you take a look at my previous post Drawing radial shapes in D3.js to get a good grip on the function logic. Here I went with an area radial shape to be able to fill the shape up with color. While the drawing logic is the same as in the example of the d3.radialLine(), the syntax of the function is a tad different. The function d3.radialArea() asks for 3 inputs: the angle, the inner circle’s radius, and the outer circle’s radius. The outer circle corresponds with the outer border of the shape; the inner is the inner border. Here inner is set to 0, because I want to fully fill the shape. 


var radialLineGenerator = d3.radialArea()
                         .innerRadius(function(d) {
                          return xScale(d[1])})
                         .outerRadius(function(d) {
                          return xScale(d[2])});   

var radialpoints = [
    [0, 0, 15],
    [Math.PI * 0.2, 0, 6],
    [Math.PI * 0.4, 0, 15],
    [Math.PI * 0.6, 0, 6],
    [Math.PI * 0.8, 0, 15],
    [Math.PI * 1, 0, 6],       
    [Math.PI * 1.2, 0, 15],
    [Math.PI * 1.4, 0, 6],
    [Math.PI * 1.6, 0, 15],
    [Math.PI * 1.8, 0, 6],
    [Math.PI * 2, 0, 15]
];

var radialData = radialLineGenerator(radialpoints);
var radial = svg.append("path")
             .attr("class", "freeradial")
             .attr("d", radialData)
             .attr("class", "radial")
             .attr("transform", function(d) {
              return "translate("+xScale(60)+","+yScale(60)+")"
              });

This adds a green star to the canvas:

A green star completes the party

Here is my full code if you want to replicate the exercise:

shapes.html:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Fun Shapes</title>
<script type="text/javascript" src="https://d3js.org/d3.v5.min.js"></script>
<link rel="stylesheet" type="text/css" href="shapes.css">
<style></style>
</head>
<body>
<div id="container" class="svg-container"></div>
<script type="text/javascript">
var w = 800;
var h = 800;
var svg = d3.select("div#container").append("svg")
         .attr("preserveAspectRatio", "xMinYMin meet")
         .attr("viewBox", "-20 -20 " + w + " " + h)
         //this is to zoom out
         //.attr("viewBox", "-20 -20 1600 1600")
         .style("padding", 5)
         .style("margin", 5);

//----------------CANVAS PREPARATION----------------//
//PREPARE SCALES   
//PREPARE SCALES   
var xScale = d3.scaleLinear()
             //accepts
             .domain([0, 100])
             //outputs
             .range([0, w]);

var yScale = d3.scaleLinear()
             //accepts
             .domain([0, 100])
             //outputs
             .range([0, h]);   
       
//PREPARE AXES 
var xAxisBottom = d3.axisBottom(xScale).ticks(20); 
var xAxisTop = d3.axisTop(xScale).ticks(20);   
var yAxisLeft = d3.axisLeft(yScale).ticks(20); 
var yAxisRight = d3.axisRight(yScale).ticks(20);       
               
//PREPARE GRIDS
//MAIN
var ygridlines = d3.axisTop()
                 .tickFormat("")
                 .tickSize(-h)
                 .ticks(10)
                 .scale(xScale);
               
var xgridlines = d3.axisLeft()
                 .tickFormat("")
                 .tickSize(-w)
                 .ticks(10)
                 .scale(yScale);

//MINOR
var ygridlinesmin = d3.axisTop()
                    .tickFormat("")
                    .tickSize(-h)
                    .ticks(100)
                    .scale(xScale);
       
var xgridlinesmin = d3.axisLeft()
                    .tickFormat("")
                    .tickSize(-w)
                    .ticks(100)
                    .scale(yScale);
//DRAW EVERYTHING
//LAYER BOTTOM UP
//MINOR GRID
svg.append("g")
.attr("class", "minor-grid")
.call(ygridlinesmin);

svg.append("g")
.attr("class", "minor-grid")
.call(xgridlinesmin);

//MAIN GRID
svg.append("g")
.attr("class", "main-grid")
.call(ygridlines);

svg.append("g")
.attr("class", "main-grid")
.call(xgridlines); 

//AXES
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxisBottom);

svg.append("g")
.attr("class", "axis")
.call(xAxisTop);

svg.append("g")
.attr("class", "axis")
.call(yAxisLeft);

svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + w + ",0)")   
.call(yAxisRight);     
       
//----------------FUN SHAPES----------------//     
//RECTANGLE
var rect = svg.append("rect")
       .attr("height",function(d){
         return yScale(50);})
       .attr("width", function(d){
          return xScale(10);})
       .attr("y",function(d){
          return yScale(10);})
       .attr("x",function(d){
          return xScale(10);})
       .attr("class", "rectangle");

       
//LINE
var lineGenerator = d3.line()
                    .x(function(d) { return xScale(d[0]) })
                    .y(function(d) { return yScale(d[1]) });

var points = [
    [40, 30],
    [50, 20],
    [60, 13],
    [70, 25],
    [80, 10],
    [90, 15]
];

var pathData = lineGenerator(points);
var freeline = svg.append("path")
               .attr("class", "freeline")
               .attr("d", function(d) { return pathData ; });
   
//RADIAL SHAPE
var radialLineGenerator = d3.radialArea()
                         .innerRadius(function(d) {
                          return xScale(d[1])})
                         .outerRadius(function(d) {
                          return xScale(d[2])});   

var radialpoints = [
    [0, 0, 15],
    [Math.PI * 0.2, 0, 6],
    [Math.PI * 0.4, 0, 15],
    [Math.PI * 0.6, 0, 6],
    [Math.PI * 0.8, 0, 15],
    [Math.PI * 1, 0, 6],       
    [Math.PI * 1.2, 0, 15],
    [Math.PI * 1.4, 0, 6],
    [Math.PI * 1.6, 0, 15],
    [Math.PI * 1.8, 0, 6],
    [Math.PI * 2, 0, 15]
];

var radialData = radialLineGenerator(radialpoints);
var radial = svg.append("path")
             .attr("class", "freeradial")
             .attr("d", radialData)
             .attr("class", "radial")
             .attr("transform", function(d) {
              return "translate("+xScale(60)+","+yScale(60)+")"
              });  
</script>
</body>
</html>

shapes.css:


/* grids */
.minor-grid {
stroke: #cccccc;
stroke-width: 0.1;
shape-rendering: crispEdges;
}

.main-grid {
color: #212121;
stroke-width: 0.4;
shape-rendering: crispEdges;
}

/* ticks */
.axis line{
stroke: #3f3f3f;
shape-rendering: crispEdges;
}

/* contour */
.axis path {
stroke: #3f3f3f;
shape-rendering: crispEdges;
}

/* rectangle */
rect.rectangle {
fill: #644dd7;
}

/* line */
path.freeline {
stroke: #d444ae;
stroke-width: 6;
fill: none;
}

/* radial shape */
path.radial {
stroke: none;
fill: #60e59b;
}

I hope you enjoyed the post. As always, please point out any problems you see with this solution in the comments. In the next post I will go on more SVG adventures!

Posted in D3

Leave a Reply