Skip to main content

Adding custom charts inside d3 circle pack visualization.

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:
 {  
  "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:

  1. To prepare a platform at each leaf node so that chart can rendered.
  2. To initially draw chart on every leaf node as per its dimensions.
  3. 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,

  1. We passed all leaf node data for the data function.
  2. Added a group element to each node with a unique identifier.
  3. As we already have the pack data info in context, we were able to give it an initial height & width.
  4. 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

Popular posts from this blog

Developing application in Angular4

Welcome again folks! This is in continuation of a previous blog where we created Task List crud - POC in Node, Express, mongo and Angular 1.x. We will follow the same back-end and will try to build a new front-end using Angular4. Pre-requisite : People who have gone through PART1  know the dependency which was mentioned, rest who are starting from here please go through the pre-requisite of PART1 session.  You need to have angular-cli installed. We will use it to scaffold our application and for development and production builds as well.  What are we going to build? We'll be building the same Task Crud application as we did in previous session, but with upgraded version of Angular and Material design. The source code for the application can be found at github  and to see the app in action visit the DEMO . Getting started: To get started we will require a back-end enabled with REST APIs. Let's use the same old Task Server from previous session. You c...

Node, Angular1.x,MongoDB, Express Task List - POC

Welcome folks! This is a series of 2 tutorials. The first part is about Node, Angular 1.x, MongoDB, Express and second part is to re-create application using Angular4. We will settle with 4.x rather than 2.x so that we get familiarized with few 4.x concepts. Intended both for new users trying hands on with Angular and Angular 1.x experts as well. The following is the first part of tutorial where we will create a simple backend server in Express exposed via a pure REST model serving client, created in Angular 1.x which uses database as MongoDB. If you are well versed with Node and Angular 1.x ignore this and move on to second half of the tutorial, i.e., developing application in Angular4 . Pre-requisite: If you don't have MongoDB, download it from here and install. Make sure you have node and npm installed, if not set it up from here . Install bower globally for package manager - from command prompt hit   npm install -g bower.   What are we going to design? He...