Search code examples
javascriptsvgd3.jssunburst-diagram

Capturing/Saving the current state of d3.js Sunburst Chart


I am a newbie to d3.js . I am working in d3.js zoomable sunburst chart http://bl.ocks.org/mbostock/4348373. When the user zoom into a particular arc and I need to capture this state of the sunburst diagram . When the user comes back to the sunburst diagram or load the graph again he should see the state where he left.

Note : I dont want to serialise the svg elements to show the state of the sunburst. If i serialise it then the chart will be static and user cant click on the sunburst and traverse to other arcs.

Proposed Solution : one solution came to my mind is simulate mouse clicks on the sunburst nodes till the last node user looks into. I am not able to devise an algorithm for this . Please guide me whether any other solution is possible ..


Solution

  • The approach you said is easy to implement. I have made a small sample for it. Click the Start Save button to start saving the state of Sunburst. Click Stop Save button after performing a few zooming actions. You can make any further changes to the chart. Now, clicking Load button will show you the saved state of the chart.

    var width = 500,
        height = 500,
        radius = Math.min(width, height) / 2;
    
    
    var x = d3.scale.linear()
        .range([0, 2 * Math.PI]);
    
    var y = d3.scale.sqrt()
        .range([0, radius]);
    
    var color = d3.scale.category10();
    
    var svg = d3.select("body").append("svg")
        .attr("width", width)
        .attr("height", height)
        .append("g")
        .attr("transform", "translate(" + width / 2 + "," + (height / 2 + 10) + ") rotate(-90 0 0)");
    
    var partition = d3.layout.partition()
        .value(function(d) {
            return d.size;
        });
    
    var 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, y(d.y));
        })
        .outerRadius(function(d) {
            return Math.max(0, y(d.y + d.dy));
        });
    
    var root = getData();
    var savedData = partition.nodes(root);
    var g = svg.selectAll("g")
        .data(partition.nodes(root))
        .enter().append("g");
    
    var path = g.append("path")
        .attr("d", arc)
        .style("fill", function(d) {
            return color((d.children ? d : d.parent).name);
        })
        .on("click", click);
    
    var text = g.append("text")
        .attr("x", function(d) {
            return y(d.y);
        })
        .attr("dx", "6") // margin
        .attr("dy", ".35em") // vertical-align
        .attr("transform", function(d) {
            return "rotate(" + computeTextRotation(d) + ")";
        })
        .text(function(d) {
            return d.name;
        })
        .style("fill", "white");
    
    function computeTextRotation(d) {
            var angle = x(d.x + d.dx / 2) - Math.PI / 2;
            return angle / Math.PI * 180;
        }
        
    var saveData = false;
    var savedData = [];
    d3.select("#saveBtn").on("click", function() {
        if (d3.select(this).attr("value") == "Start Save") {
            savedData = [];
            d3.select(this).attr("value", "Stop Save");
            saveData = true;
        } else {
            d3.select(this).attr("value", "Start Save");
            saveData = false;
        }
    });
    d3.select("#loadBtn").on("click", function() {
        var root = g.filter(function(d) {
            return d.name == "Root"
        }).data();
        click(root[0]);
        savedData.forEach(function(val) {
            var node = g.filter(function(d, i) {
                return i == val
            }).data();
            click(node[0]);
        });
    });
    
    function click(d, i) {       
            if (saveData) {
                if(d.name=="Root"){
                   savedData = [];
                } else{
                    savedData.push(i);
                }
            }
            // fade out all text elements
            if (d.size !== undefined) {
                d.size += 100;
            };
            text.transition().attr("opacity", 0);
    
            path.transition()
                .duration(750)
                .attrTween("d", arcTween(d))
                .each("end", function(e, i) {
                    // check if the animated element's data e lies within the visible angle span given in d
                    if (e.x >= d.x && e.x < (d.x + d.dx)) {
                        // get a selection of the associated text element
                        var arcText = d3.select(this.parentNode).select("text");
                        // fade in the text element and recalculate positions
                        arcText.transition().duration(750)
                            .attr("opacity", 1)
                            .attr("transform", function() {
                                return "rotate(" + computeTextRotation(e) + ")"
                            })
                            .attr("x", function(d) {
                                return y(d.y);
                            });
                    }
                });
        } 
    
    // Word wrap!
    var insertLinebreaks = function(t, d, width) {
        alert(0)
        var el = d3.select(t);
        var p = d3.select(t.parentNode);
        p.append("g")
            .attr("x", function(d) {
                return y(d.y);
            })          
            .attr("transform", function(d) {
                return "rotate(" + computeTextRotation(d) + ")";
            })           
            .append("foreignObject")
            .attr('x', -width / 2)
            .attr("width", width)
            .attr("height", 200)
            .append("xhtml:p")
            .attr('style', 'word-wrap: break-word; text-align:center;')
            .html(d.name);
        alert(1)
        el.remove();
        alert(2)
    };
    
    
    
    d3.select(self.frameElement).style("height", height + "px");
    
    // Interpolate the scales!
    function arcTween(d) {
        var 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 getData() {
        return {
            "name": "Root",
            "children": [{
                "name": "A1",
                "children": [{
                    "name": "B1",
                    "size": 30
                }, {
                    "name": "B2",
                    "size": 40
                }, {
                    "name": "B3",
                    "children": [{
                        "name": "C1",
                        "size": 10
                    }, {
                        "name": "C2",
                        "size": 15
                    }]
                }]
            }, {
                "name": "A2",
                "children": [{
                    "name": "B4",
                    "size": 40
                }, {
                    "name": "B5",
                    "size": 30
                }, {
                    "name": "B6",
                    "size": 10
                }]
            }, {
                "name": "A3",
                "children": [{
                        "name": "B7",
                        "size": 50
                    }, {
                        "name": "B8",
                        "size": 15
                    }
    
                ]
            }]
        }
    };
    path {
        stroke: #fff;
        fill-rule: evenodd;
    }
     
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
    <div id="main"></div>
    <input type="button" value="Start Save" id="saveBtn"/>
    <input type="button" value="Load" id="loadBtn"/>

    I don't know where do you plan to save the state of the sunburst. I would suggest that Localstorage would be a nice option.

    Hope this helps.