Search code examples
javascriptd3.jschartssunburst-diagram

D3 Sunburst diagram depth


I'm working with this example of a sunburst-diagram. What I'm trying to get is to remove the outer layer, the one that displays SKU information of a category, and only have Group and Category layers. I've never worked with D3 so my guess is tree depth is a problem but I couldn't figure out which part of the code to edit so I tried alternative approach.

What I hoped would solve the problem was to remove all JSON data that is used in the outer layer. This fiddle show the result. Unfortunately, it seems the code isn't "smart" enough to fill the blanks/ adjust the slices width. And since I'm no smarter either, I come to you for help.

Due to the character limit I'm posting only parts of the code that I think are relevant.

Original data structure:

var data1 = JSON.parse('[
{
"group":["Books","Arts","ZD111111"],"current":{"count":37}
},{
"group":["Electronics","Audio","ZD111288"],"current":{"count":36}
},{
"group":["Electronics","Camcorders","ZD111301"],"current":{"count":35}
}, ... ]);

Edited data structure:

var data1 = JSON.parse('[
{
"group":["Books","Arts"],"current":{"count":37}
},{
"group":["Electronics","Audio"],"current":{"count":36}
},{
"group":["Electronics","Camcorders"],"current":{"count":35}
}, ... ]);

Sunburst chart:

treePath = ["group","category"],

var controller = function(data, progress) {
    if(progress === 100) {
        data2 = $.extend(true, [], data2);
        var flatData = [];
        data.map(function(d) {
            var item = {};
            for(var i = 0; i < treePath.length; i++) {
                item[treePath[i]] = d.group[i];
            }
            //item.size = d3.selectAll("input").filter(function (d) { return this.checked; }).attr("value") === "count" ? d.current.count : d.current.metrics.price.sum;
            item.size = d.current.count; // always show count data            
            item.model = d;
            return flatData.push(item);
        });
        flatData.forEach(function(d) {
            d.model.group = d.model.group[d.model.group.length - 1];
        });

        var treeData = genJSON(flatData, treePath.slice(0, treePath.length - 1));
        d3.select("#vis")
            .datum(treeData)
            .call(chart);
    }
};

function genJSON(csvData, groups) {

    var genGroups = function(data) {
        return _.map(data, function(element, index) {
            return { name : index, children : element };
        });
    };

    var nest = function(node, curIndex) {
        if (curIndex === 0) {
            node.children = genGroups(_.groupBy(csvData, groups[0]));
            _.each(node.children, function (child) {
                nest(child, curIndex + 1);
            });
        }
        else {
            if (curIndex < groups.length) {
                node.children = genGroups(
                    _.groupBy(node.children, groups[curIndex])
                );
                _.each(node.children, function (child) {
                    nest(child, curIndex + 1);
                });
            }
        }
        return node;
    };
    return nest({}, 0);
}

function isInt(n) {
    return n % 1 === 0;
}

function sunburst() {
    var instance = this,
        svg = null,
        timestamp = new Date().getTime(),
        widgetHeight = 600,
        widgetWidth = 600,
        widgetSize = 'large',
        margin = {top: 0, right: 0, bottom: 0, left: 10},
        width = widgetWidth - margin.left - margin.right,
        height = widgetHeight - margin.top - margin.bottom,
        radius = Math.min(width, height) / 2,
        x = d3.scale.linear().range([0, 2 * Math.PI]),
        y = d3.scale.pow().exponent(1),
        pgColor = d3.scale.ordinal().range([
            {"family": "Blue", 1: "#0000CC", 2: "#0099FF", 3: "#CCFFFF"},
            {"family": "Orange", 1: "#FF6600", 2: "#FFCC00", 3: "#FFFFCC"},
            {"family": "Green", 1: "#009900", 2: "#99CC33", 3: "#CCFF99"},
            {"family": "Red", 1: "#FF3333", 2: "#FF9999", 3: "#FFCCCC"},
            {"family": "Purple", 1: "#CC0099", 2: "#FF66CC", 3: "#FFCCFF"},
            {"family": "Grey", 1: "#7b7b7b", 2: "#999999", 3: "#eeeeee"}]),
        luminance = d3.scale.sqrt()
            .domain([0, 1e6])
            .clamp(true)
            .range([90, 20]),
        i = 0,
        partition = d3.layout.partition().sort(function(a, b) { return d3.ascending(a.name || a[treePath[treePath.length - 1]], b.name || b[treePath[treePath.length - 1]])}),
        arc = d3.svg.arc()
            .startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
            .endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
            .innerRadius(function(d) { return Math.max(0, d.y ? y(d.y) : d.y); })
            .outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)); });

    function chart(selection) {
        selection.each(function(data) {
            instance.data = data;
            width = widgetWidth - margin.left - margin.right;
            height = widgetHeight - margin.top - margin.bottom;
            radius = Math.min(width, height) / 2;

            y.range([0, radius]);

            // Select the svg element, if it exists.
            svg = d3.select(this).selectAll("svg").data([data]);

            var gEnter = svg.enter()
                .append("svg")
                .attr("width", "600")
                .attr("height", "600")
                .append("g")
                .attr("class", "main-group");

            gEnter.append("defs")
                .append("clipPath")
                .attr("id", "clip-" + timestamp)
                .append("rect")
                .attr("x", 0)
                .attr("y", 0);

            var sunburstGroup = gEnter.append("g")
                .attr("class", "sunburst-area")
                .append("g")
                .attr("class", "sunburst-group");


            sunburstGroup.append("rect")
                .attr("class", "sunburst-background")
                .attr("x", 0)
                .attr("y", 0)
                .style("fill", "white");

            // Update the inner group dimensions.
            var g = svg.select("g.main-group")
                .attr("transform", "translate(" + (width / 2 + margin.left) + "," + (height / 2 + margin.top) + ")");

            g.select(".sunburst-background")
                .attr("width", width)
                .attr("height", height);

            partition.value(function(d) { return d.size; })
            .nodes(data)
            .forEach(function(d) {
                d.key = key(d);
            });

            var path = g.select(".sunburst-group").selectAll(".sunArcs")
                .data(partition.nodes(data), function(d) { return d.key; });

            path.enter().append("path")
                .attr("class", "sunArcs")
                .attr("d", arc)
                .style("fill", function(d) {
                    if(d.depth === 0) return "#fff";
                    var color = pgColor(d.key.split(".")[0]);
                    return color[d.depth];
                })
                .style("fill-opacity", 0)
                .on("click", click)
                .on("mouseover", mouseover)
                .on("mouseleave", mouseleave)
                .each(function(d) {
                    this.x0 = d.x;
                    this.dx0 = d.dx;
                });

            path.transition()
                .duration(duration)
                .style("fill-opacity", 1)
                .attrTween("d", arcTweenUpdate);

            path.exit()
                .transition()
                .duration(duration)
                .attrTween("d", arcTweenUpdate)
                .style("fill-opacity", 0)
                .remove();

            function key(d) {
                var k = [], p = d;
                while (p.depth) k.push(p.name || p[treePath[treePath.length - 1]]), p = p.parent;
                return k.reverse().join(".");
            }    

            function click(d) {
                path.transition()
                    .duration(duration)
                    .attrTween("d", arcTween(d));
            }

            function getParents(d) {
                var parents = [], p = d;
                while (p.depth >= 1) {
                    parents.push(p);
                    p = p.parent;
                }
                return parents;
            }

            function mouseover(d) {
                if(d.depth === 0) return;
                var parentNodes = getParents(d);
                 // Fade all the arcs.
                d3.selectAll(".sunArcs")
                .style("opacity", 0.3);

                // Highlight all arcs in path    
                d3.selectAll(".sunArcs").filter(function(d){
                    return (parentNodes.indexOf(d) >= 0);
                })
                .style("opacity", 1);


                // Initialize variables for tooltip
                var group = d.name || d[treePath[treePath.length - 1]],
                    valueFormat = d3.format(",.0f"),
                    textMargin = 5,
                    popupMargin = 10,
                    opacity = 1,
                    fill = d3.select(this).style("fill"),
                    hoveredPoint = d3.select(this),
                    pathEl = hoveredPoint.node(),

                // Fade the popup stroke mixing the shape fill with 60% white
                    popupStrokeColor = d3.rgb(
                        d3.rgb(fill).r + 0.6 * (255 - d3.rgb(fill).r),
                        d3.rgb(fill).g + 0.6 * (255 - d3.rgb(fill).g),
                        d3.rgb(fill).b + 0.6 * (255 - d3.rgb(fill).b)
                    ),

                // Fade the popup fill mixing the shape fill with 80% white
                    popupFillColor = d3.rgb(
                        d3.rgb(fill).r + 0.8 * (255 - d3.rgb(fill).r),
                        d3.rgb(fill).g + 0.8 * (255 - d3.rgb(fill).g),
                        d3.rgb(fill).b + 0.8 * (255 - d3.rgb(fill).b)
                    ),

                // The running y value for the text elements
                    y = 0,
                // The maximum bounds of the text elements
                    w = 0,
                    h = 0,
                    t,
                    box,
                    rows = [], p = d,
                    overlap;

                var hoverGroup = d3.select(this.parentNode.parentNode.parentNode.parentNode).append("g").attr("class", "hoverGroup");

                // Add a group for text   
                t = hoverGroup.append("g");
                // Create a box for the popup in the text group
                box = t.append("rect")
                    .attr("class", "tooltip");


                    if(!isInt(d.value)) {
                        valueFormat = d3.format(",.2f");
                    }


                while (p.depth >= 1) {
                    rows.push(treePath[p.depth - 1] + ": " + (p.name || p[treePath[treePath.length - 1]]));
                    p = p.parent;
                }
                rows.reverse();
                rows.push("Volume: " + valueFormat(d.value));

                t.selectAll(".textHoverShapes").data(rows).enter()
                    .append("text")
                    .attr("class", "textHoverShapes")
                    .text(function (d) { return d; })
                    .style("font-size", 14);

                // Get the max height and width of the text items
                t.each(function () {
                    w = (this.getBBox().width > w ? this.getBBox().width : w);
                    h = (this.getBBox().width > h ? this.getBBox().height : h);
                });

                // Position the text relatve to the bubble, the absolute positioning
                // will be done by translating the group
                t.selectAll("text")
                    .attr("x", 0)
                    .attr("y", function () {
                        // Increment the y position
                        y += this.getBBox().height;
                        // Position the text at the centre point
                        return y - (this.getBBox().height / 2);
                    });

                // Draw the box with a margin around the text
                box.attr("x", -textMargin)
                    .attr("y", -textMargin)
                    .attr("height", Math.floor(y + textMargin) - 0.5)
                    .attr("width", w + 2 * textMargin)
                    .attr("rx", 5)
                    .attr("ry", 5)
                    .style("fill", popupFillColor)
                    .style("stroke", popupStrokeColor)
                    .style("stroke-width", 2)
                    .style("opacity", 0.95);

                // Move the tooltip box next to the line point
                t.attr("transform", "translate(" + margin.left + " , " + 10 + ")");
            }

            // Mouseleave Handler
            function mouseleave(d) {
                d3.selectAll(".sunArcs")
                .style("opacity", 1);
                d3.selectAll(".hoverGroup")
                .remove();
            }            

            // Interpolate the scales!
            function arcTween(d) {
                xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
                yd = d3.interpolate(y.domain(), [d.y, 1]),
                yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
                return function(d, i) {
                    return i 
                    ? function(t) { return arc(d); }
                    : function(t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); return arc(d); };
                };
            }

            function arcTweenUpdate(a) {
                var updateArc = this;
                var i = d3.interpolate({x: updateArc.x0, dx: updateArc.dx0}, a);
                return function(t) {
                    var b = i(t);
                    updateArc.x0 = b.x;
                    updateArc.dx0 = b.dx;
                    return arc(i(t));
                };
            }            
        });
    }   

I'll accept either solution.

Thanks.


Solution

  • There are multiple objects with same groups after you removed SKU group. Example:

    {
        "group": ["Jewelry", "Diamonds"],
        "current": {
            "count": 26
        }
    }
    
    {
        "group": ["Jewelry", "Diamonds"],
        "current": {
            "count": 28
        }
    }
    

    You should either correct the data or sum these entries. I have updated your fiddle to test after aggregation entries with same groups and it looks fine now (https://jsfiddle.net/zwg31n7h/):

    var oldData = _.find(flatData, function(f){
                    var found = true;
                    _.each(treePath, function(t){
                        if(f[t] !== item[t]) {
                            found = false;
                        }
                    });
                    return found;
                });
                if(oldData) {
                    oldData.size += item.size;
                    oldData.model.current.count += item.size;
                } else {
                    flatData.push(item);
                }