Making a selectable D3 chart

One of the nice built-in features of D3.js is the ability to render a chart where you can highlight parts of the graph:

The feature is called “brushes” in the d3 docs.

Here I’ve tried to replicate this in the simplest possible way.

We need some CSS, for both the blue color and the highlight/selection box:

.area {
  fill: steelblue;
  clip-path: url(#clip);
}
 
.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}
 
.brush .extent {
  stroke: #fff;
  fill-opacity: .125;
  shape-rendering: crispEdges;
}
 
.brush .extent {
  stroke: #fff;
  fill-opacity: .125;
  shape-rendering: crispEdges;
}
</html>
 
Next we need some data. I'm just using something that looks like a matrix:
<pre lang="javascript">
let data = [
  [0, 1],
  [2, 4]
]

Then we decide how big it should be. If you’re going to use this inside React, it’s nice to make a named div and size the div correctly up front, so that the screen doesn’t flicker.

let margin = {top: 0, right: 0, bottom: 0, left: 0},
    width = 200 - margin.left - margin.right,
    height = 30 - margin.top - margin.bottom;
 
let histogramStyle = {
  "width": width,
  "height": height  
};

We’ll also need to define a selector that we can use to place the chart:

let divId = '#chart';

Now, we add the code to render the chart:

    let x = d3.scale.linear()
            .range([width, 0]),
        y = d3.scale.linear()
            .range([height, 0]);
 
    let 
      xAxis = d3.svg.axis()
        .scale(x)
        .orient("bottom"),
      yAxis = d3.svg.axis()
        .scale(y)
        .orient("left");
 
    let area = d3.svg.area()
        .x((d) => x(d[0]))
        .y0(height)
        .y1((d) => y(d[1]));
 
    let svg = d3.select(divId).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 + ")");
 
    x.domain(d3.extent(data, (d) => d[0]));
    y.domain([0, d3.max(data, (d) => d[1])]);
 
    let brush = d3.svg.brush()
        .x(x)
        .on("brushend", brushend);
 
    svg.append("path")
       .datum(data)
       .attr("class", "area")
       .attr("d", area);
 
    svg.append("g")
       .attr("class", "x brush")
       .call(brush)
       .selectAll("rect")
       .attr("y", -6)
       .attr("height", height + 7);

To handle the user’s selections, the chart lets you define a callback and you receive the min and max values, so you can call out to your code and do whatever you need.

function brushed() {
  x.domain(brush.empty() ? x.domain() : brush.extent());
}
 
function brushend() {        
  if (self.props.facetHandler) {
    self.props.facetHandler(brush.extent()[0], brush.extent[1]);
  }
}

The callback gets run every time the user drags, not just when they are finished. One simple way to resolve this is to debounce the callback:

let _ = require('lodash');
let facetHandler = _.debounce(this.props.facetHandler, 250);

Interested in JavaScript? I send out weekly, personalized emails with articles and conference talks. Click here to see an example and subscribe.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *