Search code examples
javascriptd3.jsbrush

How to fix a multiple brush chart in d3


I am so close this is killing me. I've generated a simple brush for one column and it's generating the limits it's set to perfectly. The thing is I'd like multiple brushes for multiple columns ['A', 'B', 'C', 'D']. I could write this out four times, but I've put it in a function that doesn't appear to work. Please see the working code below, I've commented out the part that doesn't work. I know I need to somehow bind the data and loop through, but how do I do this efficiently?

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

var x = d3.scale.ordinal()
  .domain(["A", "B", "C", "D"])
  .rangeBands([0, 200])

var y = d3.scale.linear()
  .range([height, 0])
  .domain([0, 100])

var xAxis = d3.svg.axis()
  .scale(x)

var svg = d3.select("#timeline").append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform", "translate(" + margin.left + "," + margin.top + ")");


// define brush control element and its events
var brush = d3.svg.brush().y(y)
  .on("brushend", () => console.log('A Extent: ', brush.extent()))

// create svg group with class brush and call brush on it
var brushg = svg.append("g")
  .attr("class", "brush")
  .call(brush);

// set brush extent to rect and define objects height
brushg.selectAll("rect")
  .attr("x", x("A"))
  .attr("width", 20);

/*

var brush = (d) => {
    var brush = d3.svg.brush().y(y)    
                      .on("brushend", () => console.log(d, ' Extent: ', brush.extent()))
      
    var brushg = svg.append("g")
        .attr("class", "brush")
        .call(brush1);

    brushg.selectAll("rect")
        .attr("x", x("A"))
        .attr("width", 20);
 }
 */
.brush {
  fill: lightgray;
  fill-opacity: .75;
  shape-rendering: crispEdges;
}
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.6/d3.min.js'></script>
<div class="container">
  <div id="timeline"></div>
</div>


Solution

  • Treat the brushes as data, as they map to each ordinal value on the x axis. Create the brushes with .enter and append all the necessary functionality. The .each function is similar to call, but runs on each element separately. This is very useful to contain the generation of the brushes.

    xData = ["A", "B", "C", "D"];
    
    var margin = {
        top: 20,
        right: 20,
        bottom: 30,
        left: 50
      },
      width = 960 - margin.left - margin.right,
      height = 180 - margin.top - margin.bottom;
    
    
    var x = d3.scale.ordinal()
      .domain(xData)
      .rangeBands([0, 200])
    
    var y = d3.scale.linear()
      .range([height, 0])
      .domain([0, 100])
    
    var xAxis = d3.svg.axis()
      .scale(x)
    
    var svg = d3.select("#timeline").append("svg")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom)
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    
      
    const brushes = svg.selectAll('g.brush')
      .data(xData)
      .enter()
      .append('g')
      .attr('class', 'brush')
      .each(function(d) {
        const el = d3.select(this);
        
        const brush = d3.svg.brush().y(y).on("brushend", () => console.log(d, ' Extent: ', brush.extent()));
        
        el.call(brush);
        
        el.selectAll("rect")
          .attr("x", x(d))
          .attr("width", 20);
       });
    .brush {
      fill: lightgray;
      fill-opacity: .75;
      shape-rendering: crispEdges;
    }
    <script src='https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.6/d3.min.js'></script>
    <div class="container">
      <div id="timeline"></div>
    </div>