Dynamically update D3 Sunburst if the source json is updated (item added or deleted)
Asked Answered
P

3

13

I am new to D3 and trying to dynamically update the chart if the source json is modified. But I am not able to achieve this.

Please check this plunkr

Js:

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));
    });

//d3.json("/d/4063550/flare.json", function(error, root) {
var root = initItems;

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)
    .each(function(d) {
        this.x0 = d.x;
        this.dx0 = d.dx;
    });


//.append("text")
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;
}



function click(d) {
    console.log(d)
    // 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("dx", "6") // margin
        //.attr("dy", ".35em") // vertical-align
        .attr("transform", function(d) {
            return "rotate(" + computeTextRotation(d) + ")";
        })
        //p
        .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)
};

//g.selectAll("text")
//    .each(function(d,i){ insertLinebreaks(this, d, 50 ); });


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 arcTweenUpdate(a) {
    console.log(path);
    var _self = this;
    var i = d3.interpolate({ x: this.x0, dx: this.dx0 }, a);
    return function(t) {
        var b = i(t);
        console.log(window);
        _self.x0 = b.x;
        _self.dx0 = b.dx;
        return arc(b);
    };
}

setTimeout(function() {
    path.data(partition.nodes(newItems))
        .transition()
        .duration(750)
        .attrTween("d", arcTweenUpdate)

}, 2000);
Paradies answered 14/3, 2016 at 18:59 Comment(0)
A
7

In addition to what @Cyril has suggested about removing the following line:

d3.select(self.frameElement).style("height", height + "px");

I made further modifications in your fiddle: working fiddle

The idea used here is to add a function updateChart which takes the items and then generate the chart:

var updateChart = function (items) {
    // code to update the chart with new items
}

updateChart(initItems);

setTimeout(function () { updateChart(newItems); }, 2000);

This doesn't use the arcTweenUpdate function you have created but I will try to explain the underlying concept:

First, you will need to JOIN the new data with your existing data:

// DATA JOIN - Join new data with old elements, if any.
var gs = svg.selectAll("g").data(partition.nodes(root));

then, ENTER to create new elements if required:

// ENTER
var g = gs.enter().append("g").on("click", click);

But, we also need to UPDATE the existing/new path and text nodes with new data:

// UPDATE
var path = g.append("path");

 gs.select('path')
  .style("fill", function(d) {
      return color((d.children ? d : d.parent).name);
  })
  //.on("click", click)
  .each(function(d) {
      this.x0 = d.x;
      this.dx0 = d.dx;
  })
  .transition().duration(500)
  .attr("d", arc);


  var text = g.append("text");

  gs.select('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");

and, after everything is created/updated remove the g nodes which are not being used i.e. EXIT:

// EXIT - Remove old elements as needed.
gs.exit().transition().duration(500).style("fill-opacity", 1e-6).remove();

This whole pattern of JOIN + ENTER + UPDATE + EXIT is demonstrated in following articles by Mike Bostock:

  1. General Update Pattern - I
  2. General Update Pattern - II
  3. General Update Pattern - III
Alleluia answered 23/3, 2016 at 13:14 Comment(1)
Great Answer. How can we provide the tool tip (Name and Value) for each section of the graph?Radiate
R
2

In side the fiddle the setTimeout is not running because:

d3.select(self.frameElement).style("height", height + "px");

You will get Uncaught SecurityError: Failed to read the 'frame' property from 'Window': Blocked a frame with origin "https://fiddle.jshell.net" from accessing a frame with origin and the setTimeout never gets called.

So you can remove this line d3.select(self.frameElement).style("height", height + "px"); just for the fiddle.

Apart from that:

Your timeout function should look like this:

setTimeout(function() {
    //remove the old graph 
  svg.selectAll("*").remove();
  root = newItems;
  g = svg.selectAll("g")
    .data(partition.nodes(newItems))
    .enter().append("g");
  /make path
  path = g.append("path")
    .attr("d", arc)
    .style("fill", function(d) {
      return color((d.children ? d : d.parent).name);
    })
    .on("click", click)
    .each(function(d) {
      this.x0 = d.x;
      this.dx0 = d.dx;
    });
  //make text
  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");

}

working fiddle here

Runck answered 18/3, 2016 at 5:32 Comment(1)
I need solution where I am not removing the whole graph completely and redrawing the whole graph again instead it should be something similar to what happens on click where we update the graph with Twean animation.Paradies
G
1

for the enter() and transitions to work you need to give d3 a way to identify each item in your data. the .data() function has a second parameter that lets you return something to use as an id. enter() will use the id to decide whether the object is new.

try changing

path.data(partition.nodes(newItems))
.data(partition.nodes(root));

to

path.data(partition.nodes(newItems), function(d){return d.name});
.data(partition.nodes(root), function(d){return d.name});
Gammer answered 18/3, 2016 at 2:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.