One of the many interesting things Github does are punchcards for repositories that can tell you when people work on their code. Unfortunately, they're only per-repository and I was interested in per-user Github punchcards.
So I made my own.
Collecting the data was fairly straightforward, finding a simple tutorial/example of a scatterplot in d3.js proved to be less than trivial.
Scatterplotting
Drawing a scatterplot is nothing more than distributing data into buckets in a two dimensional space, then drawing a circle based on how many entities ended up in a particular bucket. Adding some colour gives us an extra dimension.
For starters, we're going to need some simple HTML and a bit of CSS to make things prettier.
<style>
.axis path,
.axis line {
fill: none;
stroke: #eee;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 11px;
}
.loading {
font-family: sans-serif;
font-size: 15px;
}
.circle {
fill: #222;
}
</style>
<div id="punchcard"></div>
<script src="https://d3js.org/d3.v2.min.js"></script>
<script src="script.js"></script>
The div is where our scatterplot will end up. It doesn't need any styling since we'll do that with d3 directly, but splashing some CSS on stuff in the graph is going to make things look much better.
Going into script.js we start off by defining the width, height and padding for our graph. Due to d3's magic we'll later be able to change these and have the graph scale itself properly without further intervention.
var w = 940,
h = 300,
pad = 20,
left_pad = 100,
Data_url = "/data.json";
Next we define our scatterplot
var svg = d3
.select("#punchcard")
.append("svg")
.attr("width", w)
.attr("height", h);
This tells d3 that we want to put some svg in the punchcard div and how big we want it.
The next step is defining our x _and _y scales. They're helpful for translating data into x,y positions on the graph since they're usually not the same.
var x = d3.scale
.linear()
.domain([0, 23])
.range([left_pad, w - pad]),
y = d3.scale
.linear()
.domain([0, 6])
.range([pad, h - pad * 2]);
We specified that x _coordinates map from values between 0 and 23 to coordinates between the left padding and however big the graph is. Similarly for _y coordinates.
D3 will handle everything else for us.
Next we should define our axes since graphs without labeled axes are rather useless.
var xAxis = d3.svg.axis().scale(x).orient("bottom"),
yAxis = d3.svg.axis().scale(y).orient("left");
Essentially we've told the axes to use their corresponding scales and where we want the labels to end up.
Now it's finally time to draw something!
svg
.append("g")
.attr("class", "axis")
.attr("transform", "translate(0, " + (h - pad) + ")")
.call(xAxis);
svg
.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + (left_pad - pad) + ", 0)")
.call(yAxis);
As you can see, d3 was nice enough to figure out on its own how many ticks to draw and where to put them. This usually makes graphs more readable, but in our case we do want all the ticks.
All it takes is telling the axes how many ticks we want and while we're at it let's improve the labels as well.
var xAxis = d3.svg
.axis()
.scale(x)
.orient("bottom")
.ticks(24)
.tickFormat(function (d, i) {
var m = d > 12 ? "p" : "a";
return d % 12 == 0 ? 12 + m : (d % 12) + m;
}),
yAxis = d3.svg
.axis()
.scale(y)
.orient("left")
.ticks(7)
.tickFormat(function (d, i) {
return [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
][d];
});
The .tickFormat method allows us to give d3 a function that calculates what a label should look like and we used .ticks to specify how many ticks we want.
Much better, but since we'll be loading the data asynchronously, let's tell the user what's going on by placing "Loading ..." roughly in the middle of the plot.
svg
.append("text")
.attr("class", "loading")
.text("Loading ...")
.attr("x", function () {
return w / 2;
})
.attr("y", function () {
return h / 2 - 5;
});
Loading the data can be done with one of d3's many data loading functions.
d3.json(Data_url, function (punchcard_data) {
Within that function we are now going to draw the actual scatterplot.
The data I loaded is organized into triplets [day, hour, N] where the combination of day and hour tells us where to draw a circle and N tells us how big it should be.
We should define another scale for the radius of the circles.
var max_r = d3.max(
punchcard_data.map(function (d) {
return d[2];
})
),
r = d3.scale
.linear()
.domain([
0,
d3.max(punchcard_data, function (d) {
return d[2];
}),
])
.range([0, 12]);
Everything from zero to max_r will be mapped to radiuses between 0 and 12 pixels.
To make d3 put some data on our graph we need to tell it to load up our data into the graph and give a transformation that results in circles.
svg.selectAll(".loading").remove();
svg
.selectAll("circle")
.data(punchcard_data)
.enter()
.append("circle")
.attr("class", "circle")
.attr("cx", function (d) {
return x(d[1]);
})
.attr("cy", function (d) {
return y(d[0]);
})
.attr("r", function (d) {
return r(d[2]);
});
After removing the "Loading ..." _we gave d3 our data, then said that for each datum a _circle should be appended to the graph and given attributes cx, cy and r that determine where this circle will be displayed and how big it's going to be.
Notice we're using functions to compute these values, but because we're lazy we just rely on d3 doing all of the actual calculations with the x, y and r scales we defined earlier.
On pictures everything looks great now, but the circles appear very suddenly. Wouldn't it be nice if there was a sexy transition going on?
To do that we shove a .transition() and a .delay(800) before defining the radius.
svg
.selectAll("circle")
.data(punchcard_data)
.enter()
.append("circle")
.attr("class", "circle")
.attr("cx", function (d) {
return x(d[1]);
})
.attr("cy", function (d) {
return y(d[0]);
})
.transition()
.duration(800)
.attr("r", function (d) {
return r(d[2]);
});
Now the circles appear in a lovely 800 millisecond transition.
Here's the full javascript code:
var w = 940,
h = 300,
pad = 20,
left_pad = 100,
Data_url = "/data.json";
var svg = d3
.select("#punchcard")
.append("svg")
.attr("width", w)
.attr("height", h);
var x = d3.scale
.linear()
.domain([0, 23])
.range([left_pad, w - pad]),
y = d3.scale
.linear()
.domain([0, 6])
.range([pad, h - pad * 2]);
var xAxis = d3.svg
.axis()
.scale(x)
.orient("bottom")
.ticks(24)
.tickFormat(function (d, i) {
var m = d > 12 ? "p" : "a";
return d % 12 == 0 ? 12 + m : (d % 12) + m;
}),
yAxis = d3.svg
.axis()
.scale(y)
.orient("left")
.ticks(7)
.tickFormat(function (d, i) {
return [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
][d];
});
svg
.append("g")
.attr("class", "axis")
.attr("transform", "translate(0, " + (h - pad) + ")")
.call(xAxis);
svg
.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + (left_pad - pad) + ", 0)")
.call(yAxis);
svg
.append("text")
.attr("class", "loading")
.text("Loading ...")
.attr("x", function () {
return w / 2;
})
.attr("y", function () {
return h / 2 - 5;
});
d3.json(Data_url, function (punchcard_data) {
var max_r = d3.max(
punchcard_data.map(function (d) {
return d[2];
})
),
r = d3.scale
.linear()
.domain([
0,
d3.max(punchcard_data, function (d) {
return d[2];
}),
])
.range([0, 12]);
svg.selectAll(".loading").remove();
svg
.selectAll("circle")
.data(punchcard_data)
.enter()
.append("circle")
.attr("class", "circle")
.attr("cx", function (d) {
return x(d[1]);
})
.attr("cy", function (d) {
return y(d[0]);
})
.transition()
.duration(800)
.attr("r", function (d) {
return r(d[2]);
});
});
Continue reading about Quick scatterplot tutorial for d3.js
Semantically similar articles hand-picked by GPT-4
- JavaScript’s most popular dataviz library
- (ab)Using d3.js to make a Pong game
- Flotr2 - my favorite javascript graph library
- Livecoding #34: A Map of Global Migrations, Part 3
- Sexy animated spirographs in 35 sloc of d3.js
Learned something new?
Read more Software Engineering Lessons from Production
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below 👇
Software Engineering Lessons from Production
Join Swizec's Newsletter and get insightful emails 💌 on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. 👌"
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️