DEV Community

Cover image for Cubecubed ๐Ÿ’– d3.js
Duc Phat
Duc Phat

Posted on

Cubecubed ๐Ÿ’– d3.js

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.

d3 Basic Methods Illustration

append(), select(), attr() and style()

In index.html, we define these tags inside the <body> element:

<h1>Hello</h1>
<h1>World</h1>
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

The rendered HTML on the browser will be like so:

<h1 class="hello">Hello</h1>
<h1>World</h1>
Enter fullscreen mode Exit fullscreen mode

How can we apply CSS styles to the element? You guess it.

d3.select('h1')
  .attr('class', 'hello')
  .style('color', 'red');
Enter fullscreen mode Exit fullscreen mode

Data Visualization

The magic of data join

Consider this HTML structure:

<h1>red</h1>
<h1>green</h1>
<h1>blue</h1>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
d3.select(".red")
  .style("color", "red");

d3.select(".green")
  .style("color", "green");

d3.select(".blue")
  .style("color", "blue");
Enter fullscreen mode Exit fullscreen mode

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"];
Enter fullscreen mode Exit fullscreen mode

After that, we bibidi-babidi-booo with these lines of code.

d3.selectAll("h1")
  .data(colors)
  .style((d, i) => d);
Enter fullscreen mode Exit fullscreen mode

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.

d3 Network Graph

Smooth Transition Illustration

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");
Enter fullscreen mode Exit fullscreen mode

To translate the rendered circle, we call the transition() on the selection variable.

circleSelection
  .transition()
  .delay(1000)
  .duration(2000)
  .attr("transform", "translate(50, 50)");
Enter fullscreen mode Exit fullscreen mode

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)