Frage

I have an interactive d3 bar chart (the bars are draggable) with an associated HTML table to keep me aware of the underlying data being charted. I have a Reset button which should reset the chart data to its original state after the user has played with it. What I can't figure out is why the HTML table is resetting correctly while the chart is not. I assume it's something to do with my misunderstanding of d3's enter() / update() but that's as far as my knowledge is getting me.

The full demo is at http://jsfiddle.net/HHLrF/ but here is the relevant code:

d3.json("data/tax_stacked.json", function(error, data) {

var mydata = clone(data);  // Use a copy of the data to store user's preference.
var desc = function (a,b) { return d3.descending(+a.value, +b.value);}
mydata.sort(desc);
x.domain([0, d3.max(mydata, function(d) { return +d.value * 1.1; })]);
y.domain(mydata.map(function(d) { return d.name; }));
var defs = chart.insert("defs",".tax");
drawchart();

d3.select("button").on("click",function () {
    mydata = clone(data);
    mydata.sort(desc);
    drawchart();
});

function drawtable() {
    console.log("drawing table...");
    var columns = ["name", "value"];

    var table = d3.select("#grid");
            table.html("");
    var thead = table.append("thead"),
        tbody = table.append("tbody");

    // append the header row
    thead.append("tr")
        .selectAll("th")
        .data(columns)
        .enter()
        .append("th")
            .text(function(column) { return column; });

    // create a row for each object in the data
    var rows = tbody.selectAll("tr")
        .data(mydata)
        .enter()
        .append("tr");

    // create a cell in each row for each column
    var cells = rows.selectAll("td")
        .data(function(row) {
            return columns.map(function(column) {
                return {column: column, value: row[column]};
            });
        })
        .enter()
        .append("td")
            .text(function(d) { return d.value; }); 
}

function drawchart() {

        var drag = d3.behavior.drag()
        .on("dragstart", function(){
                    var where;
                    d3.select(this).classed({"bar": true, "moved": true});
        })
        .on("drag", function(d){
            where = d3.event.x;
            d3.select(this)
                .attr("width", where);
            d3.select(this.nextSibling)                             // Move the label
                .attr("x", where + 10);

/* Some lines omitted here for brevity */

            d.value = parseInt(x.invert(where));
            drawtable();
        })
        .on("dragend", function(d){
            });


      var taxes = chart.selectAll(".tax")
          .data(mydata)
        .enter()
            .append("g")
            .classed("tax", true);

    // Clipping paths to swap colours when the user's figures exceed original figures (work in progress!)       
        defs.selectAll(".cp")
                .data(mydata)
                .enter()
                .append("clipPath")
          .attr("id", function (d,i) { return "cp" + i; })
          .append("rect")
          .attr("x", function(d) { return x(d.value); })
          .attr("y", function(d) { return y(d.name); })
          .attr("width", function(d) { return width - x(d.value); })
          .attr("height", y.rangeBand());

    // Light blue bars for the original budget figures
      taxes.append("rect")
          .classed("original", true)
          .attr("x", function(d) { return x(0); })
          .attr("y", function(d) { return y(d.name); })
          .attr("width", function(d) { return x(d.value); })
          .attr("height", y.rangeBand())
          .attr("rx",0);

    // Medium blue bars for the user's budget figures
      taxes.append("rect")
          .attr("class", function (d) { return (d.fixed == "y") ? "fixed" : "bar"; })
          .attr("x", function(d) { return x(0); })
          .attr("y", function(d) { return y(d.name)+1; })
          .attr("width", function(d) { return x(d.value); })
          .attr("height", y.rangeBand()-1)
          .attr("rx",0)
          .attr("title", function (d) { return d.value; })
          .attr("clipper", function (d,i) { return "url(#cp" + i + ")"; });
    //      .attr("clip-path", function (d,i) { return "url(#cp" + i + ")"; });

      taxes.append("text")
          .attr("class", "label")
          .attr("x", function(d) { return x(d.value)+10; })
          .attr("y", function(d) { return y(d.name); })
          .attr("dy", "1.6em")
          .text( function (d) { return d.name; });

        var iconptr = taxes.append("g")
            .classed("smooth", true)
            .attr("transform", function (d) {
                    return "translate(" + (x(d.value)+labelpadding+parseInt(this.previousSibling.getBBox().width)) + "," + (y(d.name)+7) + ") scale(0.037,0.037)";
            });

/* Some lines omitted here for brevity */

    // Draw the axes
      chart.append("g")
          .attr("class", "x axis")
          .call(xAxis);

      chart.append("g")
          .attr("class", "y axis")
          .call(yAxis);

        d3.selectAll(".bar").call(drag);
        drawtable();
    }

});

War es hilfreich?

Lösung 2

Hooray! It now works due to a better understanding of "update" and enter(). The updated Fiddle is http://jsfiddle.net/HHLrF/3/ or read below. Apart from @FernOfTheAndes' help, I found http://bl.ocks.org/mbostock/3808218 helpful.

d3.json("data/tax_stacked.json", function(error, data) {

var mydata = clone(data);  // Use a copy of the data to store user's preference.
var desc = function (a,b) { return d3.descending(+a.value, +b.value);}
mydata.sort(desc);
x.domain([0, d3.max(mydata, function(d) { return +d.value * 1.1; })]);
y.domain(mydata.map(function(d) { return d.name; }));
var defs = chart.append("defs");

// Draw the axes
chart.append("g")
 .attr("class", "x axis");

chart.append("g")
 .attr("class", "y axis");

drawchart();
drawtable();

d3.select("button").on("click",function () {
    mydata = clone(data);
    mydata.sort(desc);
        d3.selectAll("rect.bar")
        .data(mydata)  // We're rebinding at this level rather than taxes level because the reset value is not propagating down to the <rect class="bar moved"> by itself.
      .attr("width", function(d) { console.log(x(d.value)); return x(d.value); });      
    drawtable();
});

function drawtable() {
// From http://stackoverflow.com/questions/9268645/creating-a-table-linked-to-a-csv-file
    var columns = ["name", "value"];

    var table = d3.select("#grid");
            table.html("");
    var thead = table.append("thead"),
        tbody = table.append("tbody");

    // append the header row
    thead.append("tr")
        .selectAll("th")
        .data(columns)
        .enter()
        .append("th")
            .text(function(column) { return column; });

    // create a row for each object in the data
    var rows = tbody.selectAll("tr")
        .data(mydata)
        .enter()
        .append("tr");

    // create a cell in each row for each column
    var cells = rows.selectAll("td")
        .data(function(row) {
            return columns.map(function(column) {
                return {column: column, value: row[column]};
            });
        })
        .enter()
        .append("td")
            .text(function(d) { return d.value; }); 
}

function drawchart() {

        var drag = d3.behavior.drag()
        .on("dragstart", function(){
                    var where;
                    d3.select(this).classed({"bar": true, "moved": true});
        })
        .on("drag", function(d){
            where = d3.event.x;
            d3.select(this)
                .attr("width", where);
                    d.value = "" + parseInt(x.invert(where));
            drawtable();
        })
        .on("dragend", function(d){
            });

        var taxes = chart.selectAll(".tax")
          .data(mydata);

      var taxenter = taxes.enter()
            .append("g")
            .classed("tax", true);

    // Light blue bars for the original budget figures
      taxenter.append("rect")
          .classed("original", true)
          .attr("x", function(d) { return x(0); })
          .attr("y", function(d) { return y(d.name); })
          .attr("width", function(d) { return x(d.value); })
          .attr("height", y.rangeBand())
          .attr("rx",0);

    // Medium blue bars for the user's budget figures
      taxenter.append("rect")
          .attr("class", function (d) { return (d.fixed == "y") ? "fixed" : "bar"; })
          .attr("x", function(d) { return x(0); })
          .attr("y", function(d) { return y(d.name)+1; })
                .attr("width", function(d) { return x(d.value); })
          .attr("height", y.rangeBand()-1)
          .attr("rx",0)
          .attr("title", function (d) { return d.value; });

    // Draw the axes
      chart.selectAll(".x")
          .call(xAxis);

      chart.select(".y")
          .call(yAxis);

    // Enable dragging

      d3.selectAll(".bar").call(drag);

    }

});

Andere Tipps

There are several issues in the current implementation:

  • both axis are being appended every time the reset button is clicked
  • the chart is not updated after the reset button is clicked
  • the table update does not work after the reset button is clicked

There is much that could be said, but focusing on a few main points, your chart is not updating because, as you suspected, the enter/update selection is not being handled correctly. You do everything off the enter selection and since you are not keying the data (e.g. by values) nor are you adding/removing elements, there will be nothing in the enter selection after the chart has been built at startup. I have fixed that and placed comments where appropriate.

I have also fixed the axis duplication plus have them refresh appropriately after the user plays with the charts and clicks the renamed Redraw button. Why did I rename the button? Basically, the cloning technique you are using is not really working. You can investigate that further but, for expediency sake, I removed every mention of the cloned data and only operated on the original data. So, no, the original data is not being re-set BUT the following is now working as the user interacts with the graph:

  • table update works (including sorting)
  • chart update works (including sorting)
  • x axis updates and reflects the new data magnitudes

    // preserve the enter selection in a variable for re-use!!!
    var taxesEnter = taxes
      .enter()
      .append("g")
        .classed("tax", true);
    

Complete FIDDLE with the fixes.

So, this is not doing the re-setting that you want (again, investigate the cloning situation) but it has various pieces that are useful to grasp. To that extent, I hope it is useful to you.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top