Search code examples
d3.jsbrush

Removing the D3 brush on button click


I'm wrestling with a problem of a brush not being removed correctly on a bar chart. You can see the Bl.ock here and see what's not working correctly.

In short, the brush highlights the bars that have been selected by the brush, as well as snaps to the edge of the rect to make selecting spans of time easier (there's a secondary bug here where the brush snapping isn't quite mapping correctly to the dates -- you'll see this if you try to draw the brush up to the edge of the barchart). Somewhere along the way (maybe with the rect snapping?) the background click-to-remove-brush feature stopped working (it now selects a single year span, although doesn't highlight the rect correctly). To make it easier for users, I wanted to add a button that a user can click to remove the brush when they're done (the resetBrush() function below).

My understanding was the brush selection can be cleared with brush.extent(), but when you clear the extent you then have to redraw the brush. I thought I was doing that correctly, but alas, I'm running into some problem somewhere that I can't seem to track down. Any pointers on where I'm tripping up would be greatly appreciated!

Code:

<!DOCTYPE html>
<meta charset="utf-8">
<style>

body {
  font-family: sans-serif;
  color: #000;
  text-rendering: optimizeLegibility;
}

.barchart {
  z-index: 30;
  display: block;
  visibility: visible;
  position: relative;
  padding-top: 15px;
  margin-top: 15px;

}

.axis {
  font: 10px sans-serif;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.x.axis path {
  display: none;
}

.resize path {
  fill: #666;
  fill-opacity: .8;
  stroke: #000;
  stroke-width: 1.5px;
}

.brush .extent {
  stroke: #fff;
  stroke-opacity: .6;
  stroke-width: 2px;
  fill-opacity: .1;
  shape-rendering: crispEdges;
}

</style>

<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/d3.geo.projection.v0.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script>

var margin = {top: 20, right: 20, bottom: 30, left: 40},
    width = 960 - margin.left - margin.right,
    height = 200 - margin.top - margin.bottom;

brushYearStart = 1848;
brushYearEnd = 1905;

// Scales
var x = d3.scale.ordinal().rangeRoundBands([0, width - 60], .1);
var y = d3.scale.linear().range([height, 0]);

// Prepare the barchart canvas
var barchart = d3.select("body").append("svg")
    .attr("class", "barchart")
    .attr("width", "100%")
.attr("height", height + margin.top + margin.bottom)
    .attr("y", height - height - 100)
    .append("g");

var z = d3.scale.ordinal().range(["steelblue", "indianred"]);

var brushYears = barchart.append("g")
brushYears.append("text")
    .attr("id", "brushYears")
    .classed("yearText", true)
    .text(brushYearStart + " - " + brushYearEnd)
    .attr("x", 35)
    .attr("y", 12);

d3.csv("years_count.csv", function (error, post) {

    // Coercion since CSV is untyped
    post.forEach(function (d) {
        d["frequency"] = +d["frequency"];
        d["frequency_discontinued"] = +d["frequency_discontinued"];
        d["year"] = d3.time.format("%Y").parse(d["year"]).getFullYear();
    });

    var freqs = d3.layout.stack()(["frequency", "frequency_discontinued"].map(function (type) {
        return post.map(function (d) {
            return {
                x: d["year"],
                y: +d[type]
            };
        });
    }));

    x.domain(freqs[0].map(function (d) {
        return d.x;
    }));
    y.domain([0, d3.max(freqs[freqs.length - 1], function (d) {
        return d.y0 + d.y;
    })]);

    // Axis variables for the bar chart
    x_axis = d3.svg.axis().scale(x).tickValues([1850, 1855, 1860, 1865, 1870, 1875, 1880, 1885, 1890, 1895, 1900]).orient("bottom");
    y_axis = d3.svg.axis().scale(y).orient("right");

    // x axis
    barchart.append("g")
        .attr("class", "x axis")
        .style("fill", "#000")
        .attr("transform", "translate(0," + height + ")")
        .call(x_axis);

    // y axis
    barchart.append("g")
        .attr("class", "y axis")
        .style("fill", "#000")
        .attr("transform", "translate(" + (width - 85) + ",0)")
        .call(y_axis);

    // Add a group for each cause.
    var freq = barchart.selectAll("g.freq")
        .data(freqs)
      .enter().append("g")
        .attr("class", "freq")
        .style("fill", function (d, i) {
            return z(i);
        })
        .style("stroke", "#CCE5E5");

    // Add a rect for each date.
    rect = freq.selectAll("rect")
        .data(Object)
      .enter().append("rect")
        .attr("class", "bar")
        .attr("x", function (d) {
            return x(d.x);
        })
        .attr("y", function (d) {
            return y(d.y0) + y(d.y) - height;
        })
        .attr("height", function (d) {
            return height - y(d.y);
        })
        .attr("width", x.rangeBand())
        .attr("id", function (d) {
            return d["year"];
        });

    // Draw the brush
    brush = d3.svg.brush()
        .x(x)
        .on("brush", brushmove)
        .on("brushend", brushend);

    var arc = d3.svg.arc()
      .outerRadius(height / 15)
      .startAngle(0)
      .endAngle(function(d, i) { return i ? -Math.PI : Math.PI; });

    brushg = barchart.append("g")
      .attr("class", "brush")
      .call(brush);

    brushg.selectAll(".resize").append("path")
        .attr("transform", "translate(0," +  height / 2 + ")")
        .attr("d", arc);

    brushg.selectAll("rect")
        .attr("height", height);

});

// ****************************************
// Brush functions
// ****************************************

function brushmove() {
    y.domain(x.range()).range(x.domain()).clamp(true);
    b = brush.extent();

    brushYearStart = Math.ceil(y(b[0]));
    brushYearEnd = Math.ceil(y(b[1]));

    // Snap to rect edge
    d3.select("g.brush").call(brush.extent([y.invert(brushYearStart), y.invert(brushYearEnd)]));

    // Fade all years in the histogram not within the brush
    d3.selectAll("rect.bar").style("opacity", function (d, i) {
      return d.x >= brushYearStart && d.x < brushYearEnd ? "1" : ".4"
  });
}

function brushend() {

  // Additional calculations happen here...
  // filterPoints();
  // colorPoints();
  // styleOpacity();

  // Update start and end years in upper right-hand corner of the map
  d3.select("#brushYears").text(brushYearStart + " - " + brushYearEnd);

}

function resetBrush() {
  d3.selectAll(".brush").remove();
  d3.selectAll("brushg.resize").remove();
  brush.clear();
  brushg.call(brush);
}

</script>

<div id="resetMap">
    <button
      id="returnBrush"
      class="btn btn-default"
      onclick="resetBrush()"/>Remove Brush
</div>

</body>
</html>

Solution

  • If you execute d3.selectAll(".brush").remove(); you remove <g class="brush"></g> and his childs. This d3.selectAll("brushg.resize").remove(); is a bug. Must to be brushg.selectAll(".resize").remove(); but is the same case that d3.selectAll(".brush").remove();.

    You have to do this:

    1. For reset the brush.extent() and fire the brush event.

      function resetBrush() {
        brush
          .clear()
          .event(d3.select(".brush"));
      }
      
    2. For reset #brushYears to the initial state

      function brushend() {
        var localBrushYearStart = (brush.empty()) ? brushYearStart : Math.ceil(y(b[0])),
            localBrushYearEnd = (brush.empty()) ? brushYearEnd : Math.ceil(y(b[1]));
      
        // Update start and end years in upper right-hand corner of the map
        d3.select("#brushYears").text(localBrushYearStart + " - " + localBrushYearEnd);
      }
      
    3. For reset to initial values on brush event

      function brushmove() {
        y.domain(x.range()).range(x.domain()).clamp(true);
        b = brush.extent();
      

      3.1. To set the localBrushYearStart and localBrushYearEnd variables to initial state on brush.empty() or set to Math.ceil(brush.extent()))

        var localBrushYearStart = (brush.empty()) ? brushYearStart : Math.ceil(y(b[0])),
          localBrushYearEnd = (brush.empty()) ? brushYearEnd : Math.ceil(y(b[1]));
      

      3.2. To execute brush.extent() on selection, or brush.clear() on brush.empty()

        // Snap to rect edge
        d3.select("g.brush").call((brush.empty()) ? brush.clear() : brush.extent([y.invert(localBrushYearStart), y.invert(localBrushYearEnd)]));
      

      3.3. To set opacity=1 years on brush.empty() or selection, and opacity=.4 on not selected years

        // Fade all years in the histogram not within the brush
        d3.selectAll("rect.bar").style("opacity", function(d, i) {
          return d.x >= localBrushYearStart && d.x < localBrushYearEnd || brush.empty() ? "1" : ".4";
        });
      }
      

    Check the corrections on my BL.OCKS