d3.js is a JavaScript library used to manipulate the DOM through cake-simply syntax. You can think of it as the next-gen version of jQuery, but not quite. d3.js specifically focuses on data visualization. In addition, it helps you work with animations played by the tweening way (aka not WebAPI's requestAnimationFrame
).
Cubecubed intensively uses d3 in all SVG-related cubicon classes. In this article, I will show you the basic things you need to know about d3, and in the end explain how Cubecubed uses it to abstract the SVG rendering process in classes.
So, let's start with the basic d3 methods.
append()
, select()
, attr()
and style()
In index.html
, we define these tags inside the <body>
element:
<h1>Hello</h1>
<h1>World</h1>
There is another way to achieve this entirely in JavaScript. We don't need to edit the HTML file then.
d3.select("body")
.append("h1")
.text("Hello");
d3.select("body")
.append("h1")
.text("World");
When we call select("body")
method, d3 will find the first body element and return a d3 selection object of it.
Next, we select the first <h1>
like so:
d3.select('h1');
Then, if we apply an attribute to the element, we can simply chain the code above with attr()
method.
d3.select('h1')
.attr('class', 'hello');
The rendered HTML on the browser will be like so:
<h1 class="hello">Hello</h1>
<h1>World</h1>
How can we apply CSS styles to the element? You guess it.
d3.select('h1')
.attr('class', 'hello')
.style('color', 'red');
The magic of data join
Consider this HTML structure:
<h1>red</h1>
<h1>green</h1>
<h1>blue</h1>
How can we add the corresponding color to each of the h1 element? One way to do this is adding classes, select()
the elements and apply colors to each of them. Something like this will work:
<h1 class="red">red</h1>
<h1 class="green">green</h1>
<h1 class="blue">blue</h1>
d3.select(".red")
.style("color", "red");
d3.select(".green")
.style("color", "green");
d3.select(".blue")
.style("color", "blue");
What are the downsides of this approach? We have to add custom classes to the three h1 elements, and then violate the DRY principle by rewriting the classes when applying colors. Imagine the larger problem where we have hundreds of elements, I don't know about you, but to me I won't bother add hundreds of classes and then write hundreds of select()
methods.
Maybe you have different approach from mine, but there is an awesome method built into d3 that perfectly works with this situation. That is the data()
method.
First, let's define an array of color strings.
const colors = ["red", "green", "blue"];
After that, we bibidi-babidi-booo with these lines of code.
d3.selectAll("h1")
.data(colors)
.style((d, i) => d);
And boom, everything just works!
What is going on here? First, we select all h1 elements, then call the data()
method on them. From this point, every next method in the current chain can take in an anonymous function with d
and i
as parameters. These two parameters are exactly like those are in JavaScript map()
method: d
is the current item, and i
is its index in the array.
With just three lines of code, we don't need to add any classes or call any method a hundred times. The reasonable result is that the array (along with objects are the data
in d3) needs to be updated to a hundred times then, which is no big deal if you have an active data store running.
This is the nature of the term "data visualization" of d3. With more and more data passed into the data()
method, we can even create many SVG elements to easily visialize the data with graphs or charts.
transition()
kickstart
The next thing that makes d3 a beast is its transition()
method. I used this in all types of SVG animation in Cubecubed.
No more talking, let's dive into the code right now.
First, we append an SVG <circle>
element into the body. The returned selection should be assigned to a variable.
const circleSelection = d3.select("body")
.append("circle")
.attr("cx", 50)
.attr("cy", 50)
.attr("r", 50)
.attr("fill", "none")
.attr("stroke", "#ffc777");
To translate the rendered circle, we call the transition()
on the selection variable.
circleSelection
.transition()
.delay(1000)
.duration(2000)
.attr("transform", "translate(50, 50)");
In the code above, we set the delay time (the amount of time before the animation can be played - in milliseconds, so 1000ms = 1s), along with the duration of the animation. Specifically, the animation waits 1 second, then slowly tweens the SVG transform
attributes until it reaches the value of translate(50, 50)
. The tween takes 2 seconds to finish itself.
The reason why we tween the translate
attribute but not the cx
and cy
is that it appears in all SVG elements, while the two latter ones are defined specifically for <circle>
. In Cubecubed, cubicon types have different base elements, from <circle>
to <line>
and so on. By that point, I want to utilize the Translate
animation class for all of them.
Cubecubed's principle for creating new cubicon types
All cubicon types should be derived from Cubicon
abstract class. There are two mandatory properties: g_cubiconWrapper
and def_cubiconBase
that you need to assign d3 selections to in the constructor. The former is an SVG <g>
element that wraps around the latter, which is any SVG element that directly renders itself on the screen.
All append()
method should be put inside the constructor. If you place it in the render()
method, the HTML structure will be filled with the appended elements every time the users call the method, which results in a mess.
Conclusion
That's all you need to know about d3 library and how Cubecubed renders its cubicons.
Top comments (0)