D3js Basic to Mastery Part 2 Mastery
In the last article we learned about fundamentals of D3js such as selections, DOM Manipulation, SVG, Scales and data binding. If you are getting started with D3js, I highly recommend reading that article first and then come back here.
Let’s dive into the world of Let's dive into the world of, Data Manipulation and Layouts in this article.
Event Handling
Events can be attached by calling .on(event, function)
on the selection element and by passing the type of listener.
Let's start with the most basic interactive functionalities.
In the above example, click on js tab. See what are all the event handlers attached on the circles. Reload page and open console by pressing F12 and click on “Run Pen”.circles.on(‘mouseover’, console.log)
This console logs all the parameters passed to callback functions. circles.on(‘mouseover’, (event, data)=> {})
1st Event details, 2nd bond Data. This event
is used is get the current event details which can used for variety of things like drag and drop and zoom. Alternatively, you can directly access event
in js like this circles.on(‘click’, () => console.log(“Event”, event))
circles.on(‘dblclick’, ondblClick)
: On double click event, here in the example we passed a function which turns the element “red”.circles.on(‘wheel’, (d, i, n) => console.log(“weeeee”, i))
: On scroll event, which is very handy for user friendly zooming event and last example ondrag
event which is bit different where we have to call d3.drag()
function on the element. Which has its on events like start
, drag
, end
.
circles.call(d3.drag()
.on(“start”, console.log)
.on(“drag”, onDrag)
.on(“end”, console.log))
In onDrag
function we select the element and change its cx
property with x
property from its event
.
function onDrag(d,i,n) {
d3.select(this).attr(“cx”, event.x)
}
Remember only traditional function
passes context in this
while arrow function refers to its relative parent context. If its an arrow function it should look like.
const onDrag = (d,i,n) => {
d3.select(n[i]).attr(“cx”, event.x)
}
Here is the list of most used mouse events.
click
: Mouse click on the element.dblclick
: Mouse double click on the element.contextmenu
: Mouse right click on the element.mouseenter
: Mouse enters the element.mouseleave
: Mouse leaves the element.mousemove
: Mouse movement over the element.mouseout
: Mouse leaves the element or any of its children.mouseover
: Mouse enters the element or any of its children.
Check more about it here.
There are more D3js provided interactive features
d3.drag
: Drag event with mouse.d3.zoom
: Zoom event with mouse wheel.d3.brush
: Can be used for select area or zooming.d3.force
: To simulate gravitational animation
Follow me Sai Kiran Goud to learn about these interactive visualization developments.
Tooltip Example:
https://codepen.io/krngd2/pen/YzqYNRe
Data Manipulation
D3js comes with variety of data manipulation function which comes in pretty handy while dealing with any kind off data.
Take some example data of Covid-19 cases in India.
[{
“dailyconfirmed”: “78168”,
”dailydeceased”: “892”,
”dailyrecovered”: “62145”,
”date”: “01 September “,
”totalconfirmed”: “3766121”,
”totaldeceased”: “66337”,
”totalrecovered”: “2899515”
},
.......
......
.....
{
“dailyconfirmed”: “90600”,
”dailydeceased”: “916”,
”dailyrecovered”: “73161”,
”date”: “05 September “,
”totalconfirmed”: “4110852”,
”totaldeceased”: “70095”,
”totalrecovered”: “3177666”
}]
This is an array of Objects, If we want to get the maximum dailyconfirmed
cases, you have d3.max(data, accessor)
, example
d3.max(data,(p)=> p["dailyconfirmed"] ) // returns "90600"
Similarly we have
Max
d3.max(iterable[, accessor])
: returns max valued3.maxIndex(iterable[, accessor])
: returns Index of max valued3.greatest(iterable[, comparator])
: returns object of max index
Min
d3.min(iterable[, accessor])
: returns min valued3.minIndex(iterable[, accessor])
: returns Index of min valued3.least(iterable[, comparator])
: returns object of min index
Others
d3.sum(iterable[, accessor])
: returns Sumd3.extent(iterable)
: returns [min, max] combined in an arrayd3.mean(iterable[, accessor])
: returns mean valued3.median(iterable[, accessor])
: returns median value
Transformation
Here the interesting part begins. Transformation methods comes very handy when you want to modify your data to desired format. Lets learn about most used transformation methods.
Taken above data as example.
d3.group(data, d => d["date"])
output
This returns a Map with key of date and value of the rest of the values. This comes handy when you want to access values by passing the date and using Map instead Object for such data improves performance too. You can pass more callback functions to get more nested data. Convert to Array using Array.from(mapData)
.
Note keys should be unique, otherwise they will be over ridden. If you want an error to be thrown, use d3.index(iterable, keyAccessory)
with similar functionality. If you want to customized output of values array use d3.rollup(iterable, reduce, …keys) // Learn about .reduce() here
You can check more about it Here.
d3.range([start, ]stop[, step]) // generates array of values
d3.merge([[1], [2, 3]]) //returns [1, 2, 3]
d3.pairs([1, 2, 3, 4]); // returns [[1, 2], [2, 3], [3, 4]]
d3.shuffle(array[, start[, stop]]) // shuffles the array
d3.ticks(start, stop, count)
// returns array of equal interval rounded values
// example
d3.ticks(1, 10, 5)
// returns [2, 4, 6, 8, 10]
Here I only listed out mostly used methods according my experience. You can read about all of them here Statistics, Search, Transform
Layouts
There are so many types of visualization. D3 gives us some handy inbuilt visualizations like Packed Circles, TreeMap, Network Graph etc.
In-order to develop them you need to understand an important data manipulation method to produce Hierarchical Data.
Hierarchical Data
Just like how Scales takes data and outputs the positional points, Hierarchical functions, takes hierarchical data and adds positional x,y and other points such that it can produces certain layout. We need to prepare some hierarchical data first. For that we have d3.stratify()
.
Lets say we have data something like
const data = [
{"name": "Eve", "parent": ""},
{"name": "Cain", "parent": "Eve"},
{"name": "Seth", "parent": "Eve"},
{"name": "Enos", "parent": "Seth"},
{"name": "Noam", "parent": "Seth"},
{"name": "Abel", "parent": "Eve"},
{"name": "Awan", "parent": "Eve"},
{"name": "Enoch", "parent": "Awan"},
{"name": "Azura", "parent": "Eve"}
]
To convert this into hierarchical data we need pass this data to d3.stratify()
const hierarchicalDataGenrator = d3.stratify()
.id(d => d.name)
.parentId(d => d.parent)
hierarchicalDataGenrator(data)
Above is output, we get Hierarchical data object with we can get hierarchicalDataGenrator.ancestors()
, hierarchicalDataGenrator.descendants()
, hierarchicalDataGenrator.leaves()
. Typically you don’t need to use these directly. Let build some visualizations now.
Packed Circles
Lets take a simple same data as above but add an extra point value. Value is the percentage of circle occupation under its parent. Lets keep everything 1. Like this {“name”: “Eve”, “parent”: “”, value: 1}
.
- Make Data Hierarchical
const hDataGenerator = d3.stratify()
.id(d=>d.name)
.parentId(d => d.parent)
const hData = hDataGenerator(data)
2. Sum the value
hData.sum(d => d.value)
console.log(hData)
You can see extra value
key added. That’s the sum of all its children values.
3. Prepare the pack layout
const packLayout = d3.pack()
.size([400, 400]);
packLayout(hData)
4. Use it to create layout
const nodes = d3.select('svg')
.append('g')
.selectAll('circle')
.data(hData.descendants())
.enter()
.append("g")nodes.append('circle')
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y)
.attr('r', (d) => d.r)
.style("stroke", "black")
.style("fill", "none")nodes.append('text')
.attr('y', (d) => d.y - d.r + 16)
.attr('x', (d) => d.x)
.text(d => d.data.name)
Output
TreeMap
Almost everything same as above but instead of d3.pack()
we use d3.tree()
Prepare Layout
const treeLayout = d3.tree().size([400, 400]);
treeLayout(hData)
Create
const svg = d3.select('svg')//circles
svg.append('g')
.selectAll('circle.node')
.data(hData.descendants())
.enter()
.append('circle')
.classed('node', true)
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y)
.attr('r', 4);// Links
svg.append("g")
.selectAll('line.link')
.data(hData.links())
.enter()
.append('line')
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y)
.style('stroke', 'black')
Output
Learn More about Hierarchy here
I am hoping by now you got pretty good idea about how to play around with D3js. I know we haven't built any exciting chart in these series. In my next article we will build Racing Bar chart. So follow me for the more exciting stuff.