Question

I'm struggling to take a stacked area chart in D3 and add brushing features. I've managed to generate both the large (focus) and small (context) axes, and the plot looks fine to start. My problems is that the brushing features don't work at all. I've been banging my head on this for some time, to no avail, and can't figure out where the problem is. Any help would be much appreciated!

Click here to see the current state of the plot. Here's the code:

<!-- language: lang-js -->
<!DOCTYPE html>
<meta charset="utf-8">
<style>

body {
    font: 10px sans-serif;
}

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

.tag text {
    text-anchor: end;
}

</style>
<body onLoad="updateData()">
        <div id="option" style="font-size:14px;">
        <form>
        Number of tags:<input value="5" id="nTags_select"></input>
        <input name="updateButton" 
                                 type="button" 
                                value="Update" 
                                onclick="updateData()" />
                        </form>
</div>  
<script src="http://d3js.org/d3.v3.js"></script>
<script>

var margin = {top: 10, right: 10, bottom: 100, left: 40},
    margin2 = {top: 430, right: 10, bottom: 20, left: 40},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom,
    height2 = 500 - margin2.top - margin2.bottom;


var parseDate = d3.time.format("%Y-%m-%d").parse;

var x = d3.time.scale().range([0, width]),
    x2 = d3.time.scale().range([0, width]),
    y = d3.scale.linear().range([height, 0]),
    y2 = d3.scale.linear().range([height2, 0]);

var color = d3.scale.category20();

var xAxis = d3.svg.axis().scale(x).orient("bottom"),
    xAxis2 = d3.svg.axis().scale(x2).orient("bottom"),
    yAxis = d3.svg.axis().scale(y).orient("left");

var brush = d3.svg.brush()
   .x(x2)
  .on("brush", brushed);

var area = d3.svg.area()
    .interpolate("basis")
    .x(function(d) { return x(d.date); })
        .y0(function(d) { return y(d.y0); })
        .y1(function(d) { return y(d.y0 + d.y); });

var area2 = d3.svg.area()
    .interpolate("basis")
    .x(function(d) { return x2(d.date); })  
        .y0(function(d) { return y2(d.y0); })
        .y1(function(d) { return y2(d.y0 + d.y); });

var stack = d3.layout.stack()
        .values(function(d) { return d.values; });



 var oRequest = new XMLHttpRequest();
 var sURL  = "data100.tsv";
 oRequest.open("GET",sURL,false);
 oRequest.setRequestHeader("User-Agent",navigator.userAgent);
 oRequest.send(null)
 var data = oRequest.responseText;

 var data = d3.tsv.parse(data);

    data.forEach(function(d) {
        d.date = parseDate(d.date);
    });

    color.domain(d3.keys(data[0]).filter(function(key) { return key !== "date"; }));
    var allTags = stack(color.domain().map(function(name) {
        return {
            name: name,
            values: data.map(function(d) {
                return {date: d.date, y: +d[name]};
            })
        };
    }));


    function updateData(){
        var nTags = document.getElementById('nTags_select').value;
        tags=allTags.slice(0,nTags);

    x.domain(d3.extent(data, function(d) { return d.date; }));
    y.domain([0,d3.max(tags, function(d) { return d3.max(d.values, function (d) { return d.y + d.y0; }); })]);
    x2.domain(x.domain());
    y2.domain(y.domain());  

    d3.select("svg").remove();

    var svg = d3.select("body").append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)

    svg.append("defs").append("clipPath")
    .attr("id", "clip")
  .append("rect")
    .attr("width", width)
    .attr("height", height);

var focus = svg.append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var context = svg.append("g")
    .attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");

focus.selectAll('path')
            .data(tags)
            .enter()
            .append('path')
            .attr('clip-path','url(#clip)')
            .attr("d", function(d) { return area(d.values); })
            .attr('class','focus')
            .style("fill", function(d) { return color(d.name); });

  focus.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis);

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


context.selectAll('path')
            .data(tags)
            .enter()
            .append('path')
            .attr('class','context')
            .attr("d", function(d) { return area2(d.values); })
            .style("fill", function(d) { return color(d.name); });


  context.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height2 + ")")
      .call(xAxis2);

  context.append("g")
      .attr("class", "x brush")
      .call(brush)
    .selectAll("rect")
      .attr("y", -6)
      .attr("height", height2 + 7);


  focus.append("text")
            .datum(function(d) { return {name: d.name, value: d.values[d.values.length - 1]}; })
            .attr("transform", function(d) { return "translate(" + x(d.value.date) + "," + y(d.value.y0 + d.value.y / 2) + ")"; })
            .attr("x", -6)
            .attr("dy", "-.35em")
            .text(function(d) { return d.name; });

};




function brushed() {
  x.domain(brush.empty() ? x2.domain() : brush.extent());
  focus.selectAll("path.focus").attr("d", area);
  focus.select(".x.axis").call(xAxis);
}

</script>
Was it helpful?

Solution

In Mike's original code, he chose the name focus for his svg element which is unfortunate - it already exists in the dom, and might be confusing matters.

In any case, the problem here is primarily one of scope.

Mike defines focus outside the scope of getting the data, thus putting it into the global scope. Therefore, it's available to the brushed() function.

The first step I would take would be to move the definition of the focus var outside of the updateData() function.

However, there are a few other problems:

  • You should be using d3.tsv with an anonymous function to get the data. It will make things much simpler.
  • In the brushed function, you need to use an anonymous function and pass the area function d.values, rather than just d. You did this in the updateData function, but not in brushed. i.e.:

    function brushed() {   
        x.domain(brush.empty() ? x2.domain() : brush.extent());
        focus.selectAll("path.focus").attr("d", function(d){return area(d.values)});
        focus.select(".x.axis").call(xAxis);
    }
    

I have a rough version of your chart with the brushing working here: http://jsfiddle.net/zQ2EF/ but, I would go over Mike's example with careful attention to where things are defined, and which scope they are in - it will help you understand where the problem lies!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top