Hello folks,
I'm back with a new article. Recently one of my close friend was working on d3 circle pack visualization whereby he wanted to show charts at the leaf node, which he pretty much accomplished but was struggling to make it work to resize of zoom-in and zoom-out circle. That was the point when I got involved in this use case and it gave me an opportunity to share here how we got the break through.
What are we going to build ?
A circle pack visualization whose leaf node contains a custom chart like this:
To see a live demo visit here.
Context:
The above visualization is drawn under the context of e-commerce store where nested elements are products under category and sub category. The leaf node is a product whose children are sales data of the week.
Lets get started: The data for the chart is as follows:
The basic code to get started with is directly available on https://bl.ocks.org/mbostock/7607535. This will give you a basic circle pack which we shall modify as per our requirement.
The Task can be thought of 3 major milestones:
#1 : To prepare a platform at each leaf node:
The intent is to attach an identifier to each leaf node so that it can be used as base on which our bar chart can be rendered. To do so we'll attach a 'g' element to each leaf node and will give it a unique identifier (<parent.name>_<name>). Below code needs to be added after all circle nodes are rendered:
To summarize,
#2: To draw bar chart, i.e., implementation of drawBarData:
The following function is responsible for painting chart with relevant data on predefined platforms we created in #1 via unique identifier. All the data is passed via parameters:
Notable parameter zoomRadius will come in picture when the zoom action takes place, which is optional. If not passed as we see in #1 usage, then radius calculated by circle pack data is used by default. On seeing code, you can see that we used the same identifier which we created in #1. Rest of the code is a normal bar chart drawing. Just a small tweak, we draw axis only if we have at least 100px width else axis is not drawn.
#3: To handle zoom-in/zoom-out for chart rendering:
The block of code which handles the zoom is the zoomTo function in the base code. We will modify this function so that whenever the circle radius is adjusted the charts can be re-drawn in the new dimensions as:
Here while setting the radius after zoom for each circle, we also call drawBarData with the additional zoomRadius as 4th param. This will redraw the charts with new dimension.
Summary: Although, here I took an example of bar chart inside leaf node of circle pack, you can have any chart in the leaf node as the key factor remains the same for any case. They are:
(a) unique identifier
(b) chart render
(c) callback on zoom.
The demo can be seen live here and source code is available on Github.
Hope you guys find this post useful. Comment in any case.
Happy coding! :)
I'm back with a new article. Recently one of my close friend was working on d3 circle pack visualization whereby he wanted to show charts at the leaf node, which he pretty much accomplished but was struggling to make it work to resize of zoom-in and zoom-out circle. That was the point when I got involved in this use case and it gave me an opportunity to share here how we got the break through.
What are we going to build ?
A circle pack visualization whose leaf node contains a custom chart like this:
To see a live demo visit here.
Context:
The above visualization is drawn under the context of e-commerce store where nested elements are products under category and sub category. The leaf node is a product whose children are sales data of the week.
Lets get started: The data for the chart is as follows:
{
"name": "products",
"children": [
{
"name": "electronics",
"children": [
{
"name": "mobiles",
"children": [
{
"name": "samsung",
"children": [
{
"name": "sun",
"size": 83
},
{
"name": "mon",
"size": 52
},
{
"name": "tue",
"size": 47
},
{
"name": "wed",
"size": 87
},
{
"name": "thu",
"size": 32
},
{
"name": "fri",
"size": 12
},
{
"name": "sat",
"size": 65
}
]
},
{
"name": "nokia",
"children": [
{
"name": "sun",
"size": 83
},
{
"name": "mon",
"size": 34
},
{
"name": "tue",
"size": 24
},
{
"name": "wed",
"size": 43
},
{
"name": "thu",
"size": 65
},
{
"name": "fri",
"size": 34
},
{
"name": "sat",
"size": 67
}
]
},
{
"name": "apple",
"children": [
{
"name": "sun",
"size": 68
},
{
"name": "mon",
"size": 26
},
{
"name": "tue",
"size": 37
},
{
"name": "wed",
"size": 45
},
{
"name": "thu",
"size": 12
},
{
"name": "fri",
"size": 92
},
{
"name": "sat",
"size": 46
}
]
},
{
"name": "sony",
"children": [
{
"name": "sun",
"size": 92
},
{
"name": "mon",
"size": 29
},
{
"name": "tue",
"size": 46
},
{
"name": "wed",
"size": 76
},
{
"name": "thu",
"size": 43
},
{
"name": "fri",
"size": 56
},
{
"name": "sat",
"size": 21
}
]
},
{
"name": "oppo",
"children": [
{
"name": "sun",
"size": 101
},
{
"name": "mon",
"size": 90
},
{
"name": "tue",
"size": 32
},
{
"name": "wed",
"size": 87
},
{
"name": "thu",
"size": 54
},
{
"name": "fri",
"size": 32
},
{
"name": "sat",
"size": 54
}
]
},
{
"name": "mi",
"children": [
{
"name": "sun",
"size": 87
},
{
"name": "mon",
"size": 65
},
{
"name": "tue",
"size": 13
},
{
"name": "wed",
"size": 67
},
{
"name": "thu",
"size": 23
},
{
"name": "fri",
"size": 45
},
{
"name": "sat",
"size": 32
}
]
}
]
},
{
"name": "laptops",
"children": [
{
"name": "dell",
"children": [
{
"name": "sun",
"size": 70
},
{
"name": "mon",
"size": 43
},
{
"name": "tue",
"size": 30
},
{
"name": "wed",
"size": 54
},
{
"name": "thu",
"size": 87
},
{
"name": "fri",
"size": 43
},
{
"name": "sat",
"size": 98
}
]
},
{
"name": "apple",
"children": [
{
"name": "sun",
"size": 170
},
{
"name": "mon",
"size": 99
},
{
"name": "tue",
"size": 112
},
{
"name": "wed",
"size": 176
},
{
"name": "thu",
"size": 78
},
{
"name": "fri",
"size": 150
},
{
"name": "sat",
"size": 130
}
]
},
{
"name": "lenovo",
"children": [
{
"name": "sun",
"size": 75
},
{
"name": "mon",
"size": 42
},
{
"name": "tue",
"size": 38
},
{
"name": "wed",
"size": 35
},
{
"name": "thu",
"size": 8
},
{
"name": "fri",
"size": 3
},
{
"name": "sat",
"size": 17
}
]
},
{
"name": "acer",
"children": [
{
"name": "sun",
"size": 12
},
{
"name": "mon",
"size": 56
},
{
"name": "tue",
"size": 87
},
{
"name": "wed",
"size": 36
},
{
"name": "thu",
"size": 88
},
{
"name": "fri",
"size": 21
},
{
"name": "sat",
"size": 89
}
]
}
]
},
{
"name": "camera",
"children": [
{
"name": "sony",
"children": [
{
"name": "sun",
"size": 35
},
{
"name": "mon",
"size": 65
},
{
"name": "tue",
"size": 37
},
{
"name": "wed",
"size": 64
},
{
"name": "thu",
"size": 38
},
{
"name": "fri",
"size": 69
},
{
"name": "sat",
"size": 48
}
]
},
{
"name": "samsung",
"children": [
{
"name": "sun",
"size": 87
},
{
"name": "mon",
"size": 15
},
{
"name": "tue",
"size": 65
},
{
"name": "wed",
"size": 48
},
{
"name": "thu",
"size": 69
},
{
"name": "fri",
"size": 74
},
{
"name": "sat",
"size": 36
}
]
},
{
"name": "nikon",
"children": [
{
"name": "sun",
"size": 74
},
{
"name": "mon",
"size": 96
},
{
"name": "tue",
"size": 74
},
{
"name": "wed",
"size": 51
},
{
"name": "thu",
"size": 76
},
{
"name": "fri",
"size": 64
},
{
"name": "sat",
"size": 55
}
]
}
]
},
{
"name": "television",
"children": [
{
"name": "sony",
"children": [
{
"name": "sun",
"size": 46
},
{
"name": "mon",
"size": 87
},
{
"name": "tue",
"size": 121
},
{
"name": "wed",
"size": 87
},
{
"name": "thu",
"size": 90
},
{
"name": "fri",
"size": 99
},
{
"name": "sat",
"size": 111
}
]
},
{
"name": "lg",
"children": [
{
"name": "sun",
"size": 174
},
{
"name": "mon",
"size": 99
},
{
"name": "tue",
"size": 48
},
{
"name": "wed",
"size": 69
},
{
"name": "thu",
"size": 57
},
{
"name": "fri",
"size": 48
},
{
"name": "sat",
"size": 60
}
]
},
{
"name": "samsung",
"children": [
{
"name": "sun",
"size": 123
},
{
"name": "mon",
"size": 178
},
{
"name": "tue",
"size": 49
},
{
"name": "wed",
"size": 88
},
{
"name": "thu",
"size": 67
},
{
"name": "fri",
"size": 73
},
{
"name": "sat",
"size": 50
}
]
}
]
}
]
},
{
"name": "applinaces",
"children": [
{
"name": "kitchen",
"children": [
{
"name": "induction",
"children": [
{
"name": "prestige",
"children": [
{
"name": "sun",
"size": 87
},
{
"name": "mon",
"size": 145
},
{
"name": "tue",
"size": 98
},
{
"name": "wed",
"size": 82
},
{
"name": "thu",
"size": 76
},
{
"name": "fri",
"size": 68
},
{
"name": "sat",
"size": 88
}
]
},
{
"name": "philips",
"children": [
{
"name": "sun",
"size": 91
},
{
"name": "mon",
"size": 80
},
{
"name": "tue",
"size": 77
},
{
"name": "wed",
"size": 65
},
{
"name": "thu",
"size": 58
},
{
"name": "fri",
"size": 78
},
{
"name": "sat",
"size": 87
}
]
}
]
},
{
"name": "grinders",
"children": [
{
"name": "preethi",
"children": [
{
"name": "sun",
"size": 64
},
{
"name": "mon",
"size": 74
},
{
"name": "tue",
"size": 58
},
{
"name": "wed",
"size": 54
},
{
"name": "thu",
"size": 47
},
{
"name": "fri",
"size": 87
},
{
"name": "sat",
"size": 57
}
]
},
{
"name": "butterfly",
"children": [
{
"name": "sun",
"size": 98
},
{
"name": "mon",
"size": 101
},
{
"name": "tue",
"size": 57
},
{
"name": "wed",
"size": 34
},
{
"name": "thu",
"size": 64
},
{
"name": "fri",
"size": 42
},
{
"name": "sat",
"size": 25
}
]
}
]
}
]
},
{
"name": "refrigerator",
"children": [
{
"name": "samsung",
"children": [
{
"name": "sun",
"size": 98
},
{
"name": "mon",
"size": 101
},
{
"name": "tue",
"size": 57
},
{
"name": "wed",
"size": 34
},
{
"name": "thu",
"size": 64
},
{
"name": "fri",
"size": 42
},
{
"name": "sat",
"size": 25
}
]
},
{
"name": "lg",
"children": [
{
"name": "sun",
"size": 74
},
{
"name": "mon",
"size": 10
},
{
"name": "tue",
"size": 78
},
{
"name": "wed",
"size": 48
},
{
"name": "thu",
"size": 57
},
{
"name": "fri",
"size": 98
},
{
"name": "sat",
"size": 75
}
]
},
{
"name": "kelvinator",
"children": [
{
"name": "sun",
"size": 44
},
{
"name": "mon",
"size": 28
},
{
"name": "tue",
"size": 39
},
{
"name": "wed",
"size": 18
},
{
"name": "thu",
"size": 72
},
{
"name": "fri",
"size": 49
},
{
"name": "sat",
"size": 57
}
]
}
]
},
{
"name": "ac",
"children": [
{
"name": "voltas",
"children": [
{
"name": "sun",
"size": 44
},
{
"name": "mon",
"size": 87
},
{
"name": "tue",
"size": 64
},
{
"name": "wed",
"size": 48
},
{
"name": "thu",
"size": 81
},
{
"name": "fri",
"size": 57
},
{
"name": "sat",
"size": 68
}
]
},
{
"name": "samsung",
"children": [
{
"name": "sun",
"size": 103
},
{
"name": "mon",
"size": 75
},
{
"name": "tue",
"size": 66
},
{
"name": "wed",
"size": 48
},
{
"name": "thu",
"size": 52
},
{
"name": "fri",
"size": 33
},
{
"name": "sat",
"size": 28
}
]
}
]
}
]
},
{
"name": "books",
"children": [
{
"name": "fiction",
"children": [
{
"name": "mcs",
"children": [
{
"name": "sun",
"size": 103
},
{
"name": "mon",
"size": 75
},
{
"name": "tue",
"size": 66
},
{
"name": "wed",
"size": 48
},
{
"name": "thu",
"size": 52
},
{
"name": "fri",
"size": 33
},
{
"name": "sat",
"size": 28
}
]
},
{
"name": "hill",
"children": [
{
"name": "sun",
"size": 98
},
{
"name": "mon",
"size": 101
},
{
"name": "tue",
"size": 57
},
{
"name": "wed",
"size": 34
},
{
"name": "thu",
"size": 64
},
{
"name": "fri",
"size": 42
},
{
"name": "sat",
"size": 25
}
]
}
]
},
{
"name": "academics",
"children": [
{
"name": "greenwood",
"children": [
{
"name": "sun",
"size": 100
},
{
"name": "mon",
"size": 60
},
{
"name": "tue",
"size": 74
},
{
"name": "wed",
"size": 85
},
{
"name": "thu",
"size": 72
},
{
"name": "fri",
"size": 68
},
{
"name": "sat",
"size": 48
}
]
},
{
"name": "longisland",
"children": [
{
"name": "sun",
"size": 98
},
{
"name": "mon",
"size": 85
},
{
"name": "tue",
"size": 92
},
{
"name": "wed",
"size": 80
},
{
"name": "thu",
"size": 77
},
{
"name": "fri",
"size": 67
},
{
"name": "sat",
"size": 28
}
]
}
]
}
]
}
]
}
The basic code to get started with is directly available on https://bl.ocks.org/mbostock/7607535. This will give you a basic circle pack which we shall modify as per our requirement.
The Task can be thought of 3 major milestones:
- To prepare a platform at each leaf node so that chart can rendered.
- To initially draw chart on every leaf node as per its dimensions.
- On resize (zoom-in/zoom-out) repaint each leaf node chart with new dimensions.
#1 : To prepare a platform at each leaf node:
The intent is to attach an identifier to each leaf node so that it can be used as base on which our bar chart can be rendered. To do so we'll attach a 'g' element to each leaf node and will give it a unique identifier (<parent.name>_<name>). Below code needs to be added after all circle nodes are rendered:
//platform to draw charts
var leaf = g.selectAll(".bars")
.data(nodes.filter(function(d) {
//get all leaf node data
return d.height == 1
}))
.enter()
.append("g")
.attr("id", (d) => d.parent.data.name+"_"+d.data.name)
.attr("height", function(d) {
return d.x + d.r
})
.attr("width", function(d) {
return d.y + d.r
})
.attr("class", "bars")
.each(function(d) {
drawBarData(this, this.__data__, d);
});
To summarize,
- We passed all leaf node data for the data function.
- Added a group element to each node with a unique identifier.
- As we already have the pack data info in context, we were able to give it an initial height & width.
- For each render 'g' we call function drawBarData, which draws bar charts on the basis of the passed arguments.
#2: To draw bar chart, i.e., implementation of drawBarData:
The following function is responsible for painting chart with relevant data on predefined platforms we created in #1 via unique identifier. All the data is passed via parameters:
//ele : The g element
//data : data for which we need to draw chart
//d : high level pack data(just in case if we need)
//zoomRadius : Its used under condition when we zoom-in/zoom-out
function drawBarData(ele, data, d, zoomRadius) {
if (!data && !data.parent)
return;
var rectwidth = (zoomRadius) ? zoomRadius : d.r;
var rectheight = rectwidth;
var maxDataPoint = d3.max(data.data.children, function(d) {
return d.size
});
var linearScale = d3.scaleLinear()
.domain([0, maxDataPoint])
.range([0, rectheight]);
var x = d3.scaleBand()
.range([0, rectwidth])
.padding(0.1);
var y = d3.scaleLinear()
.range([rectheight, 0]);
// Scale the range of the data in the domains
x.domain(data.data.children.map(function(d) {
return d.name;
}));
y.domain([0, d3.max(data.data.children, function(d) {
return d.size;
})]);
$("#" + data.parent.data.name+"_"+data.data.name).html("");
var bg = d3.select("#" + data.parent.data.name+"_"+data.data.name).append("g")
.attr("class", "chart-wrapper")
.attr("transform", function(d) {
return "translate(" + -rectwidth / 2 + "," + -rectwidth / 2 + ")";
});
bg.selectAll(".bar")
.data(data.data.children)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) {
return x(d.name);
})
.attr("width", x.bandwidth())
.attr("y", function(d) {
return y(d.size);
})
.attr("height", function(d) {
return rectheight - y(d.size);
})
.attr("fill",(d,i)=>colorBar(i))
.append("svg:title")
.text((d)=>d.name + ' : sold pieces ' +d.size)
//just a safe check to render axis only if we have space
if(rectheight > 100){
bg.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + rectheight + ")")
.call(d3.axisBottom(x))
.selectAll("text")
.attr("y", 0)
.attr("x", -10)
.attr("dy", ".35em")
.attr("transform", "rotate(-90)")
.style("text-anchor", "end");
bg.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y))
}
}
Notable parameter zoomRadius will come in picture when the zoom action takes place, which is optional. If not passed as we see in #1 usage, then radius calculated by circle pack data is used by default. On seeing code, you can see that we used the same identifier which we created in #1. Rest of the code is a normal bar chart drawing. Just a small tweak, we draw axis only if we have at least 100px width else axis is not drawn.
#3: To handle zoom-in/zoom-out for chart rendering:
The block of code which handles the zoom is the zoomTo function in the base code. We will modify this function so that whenever the circle radius is adjusted the charts can be re-drawn in the new dimensions as:
function zoomTo(v, focus, ele) {
var k = diameter / v[2];
view = v;
node.attr("transform", function(d) {
return "translate(" + (d.x - v[0]) * k + "," + (d.y - v[1]) * k + ")";
});
circle.attr("r", function(d) {
if (d && d.height == 1) {
setTimeout(function() {
//reste bar charts
drawBarData("", d, d, d.r * k);
}, 0)
}
return d.r * k;
});
}
Here while setting the radius after zoom for each circle, we also call drawBarData with the additional zoomRadius as 4th param. This will redraw the charts with new dimension.
Summary: Although, here I took an example of bar chart inside leaf node of circle pack, you can have any chart in the leaf node as the key factor remains the same for any case. They are:
(a) unique identifier
(b) chart render
(c) callback on zoom.
The demo can be seen live here and source code is available on Github.
Hope you guys find this post useful. Comment in any case.
Happy coding! :)
Comments
Post a Comment