Nature of Code with d3.js
This post is a personal follow up to the book The nature of code. Instead of using p5.js from the book, I decide to use d3.js, as just copying the code from the book is of no fun and d3.js
is more popular among web developers. However, I have never developed anything using d3.js before, so let’s see how far I can go this time!
Warm up exercise
First of all, let me put up a simple diagram to make sure this is working in this blog.
Here I encountered first problem, the inner svg doesn’t have the same width and height as the outer container, after a bit exploration, I used the following solution, which simply get the container size using offsetWidth
and offsetHeight
, and set them as the inner svg attr
.
const container = document.getElementById("noc-warmup");
const svg = d3.select("#noc-warmup").append("svg")
.attr("width", container.offsetWidth)
.attr("height", container.offsetHeight)
Random walker
Next is a simple illustration of a random walker, who will start from the center of the canvas and randomly walking on it.
Random number distribution
It is said the js Math.random
is uniformly distributed, is that true? Let’s find out by building a distribution graph.
Wow, I didn’t expect the bar chart to be so hard to generate, but still I did it, and compared with before, when I always use some kind of third party chart library to draw all kind of chart, now this is a whole new level of char drawing, I am using d3.js to manipulate the svg element directly now.
The most tricky part of this diagram is the way to link to the data. I am not fully understand what’s happening there yet, but generally speaking, the dynamic chart uses to steps enter
and merge
to make the bar chart dynamic.
Check this article if you are confused as myself.
// Update the bars
const bars = svg.selectAll("rect")
.data(random_numbers);
bars.enter().append("rect")
.attr("x", (d, i) => x(i * 10) + (tick_width - 20) / 2)
.attr("y", (d) => container.offsetHeight - 24)
.attr("width", 20)
.attr("height", 0)
.style("fill", "steelblue")
.merge(bars)
.attr("y", (d) => y(d))
.attr("height", (d) => container.offsetHeight - 24 - y(d));
Gaussian distribution
Uniform distribution is uncommon in the nature, so let’s taste a bit about the Gaussian distribution, also known as the normal distribution.
Sometimes, even gaussian distribution is not natural enough, in that case, we could try using the Perlin noise
, following I will compare the perlin noise with the uniform random numbers side by side.
It is clear that perlin noise looks much natural than uniform random noise.
Bouncing ball
Next chapter, let’s talk about vectors!
Initially, I was using the approach which removes the object first and then redraw the whole object again in a new position, but this feels laggy and compared with the animation from p5.js
, my version looks worse.
svg.selectAll("*").remove();
svg.append("circle")
.attr("cx", x)
.attr("cy", y)
.attr("r", 48)
.attr("fill", "steelblue");
According to the documentation, the correct way should use the d3 events like enter
, update
. Let’s update the script now!
let circle = svg.selectAll("circle");
let databoundedCircle = circle.data([{ x, y }]);
databoundedCircle.join(
enter => enter.append("circle")
.attr("cx", (d) => d.x)
.attr("cy", (d) => d.y)
.attr("r", 48)
.attr("fill", "steelblue"),
update => update
.attr("cx", (d) => d.x)
.attr("cy", (d) => d.y),
)
New update approach does feel a bit more smooth, not sure if that’s just an illusion.
As an upgrade, the book asks to use vector instead of using the naive x
and y
values, however it seems that d3.js doesn’t offer native vector calculation support, any other libraries I can turn to this time? It turns out that there is actually one library vistor.js that does the vector calculation job. I will try to use the victor library for the following tasks, I really don’t want to create my own vector math library XD.
Vector subtraction
For this demonstration, I need to figure out how to dynamically update the graph based on my mouse position.
Unlike the code with p5.js, I didn’t use the vector subtraction function, as I don’t find a place to use that function.
Vector magnitude
Until now, I have already created a bunch of small svg based animations, if you check the js source code, you will find that compared with the p5.js source code, the code with d3.js is much longer and hard to read, that’s because I am using a much lower level of API than the one provided from p5.js, but that should could be optimized if I created a few helpers and abstractions, so let’s create a mini engine (helpers) now!
Forces
This diagram tricked me for a while, as I find it really tricky to apply gravity force using the native force
API, and also I need to set the velocity delay to zero, otherwise the velocity increase drops very quickly.
As the codebase using d3.js is much harder than using p5.js alone I find it difficult to follow using d3.js alone. Therefore I am going to switch to using p5.js Now.
(Done)