Question

I have made a scatterplot graph with a 2-dimensional brush in a smaller graph underneath. This enables the user to dynamically look at sub-areas of the full graph. However, when I draw an area brush, it is no longer "moveable," in that the brush cannot be moved around with the mouse in the way that the 1-dimensional brush can be. There are 2D examples in which the 2D brush area can be moved around (for example http://bl.ocks.org/mbostock/4343214), and the code to create the 2D brush is virtually identical to the 1D.

My question is why adding the second dimension removes the ability to move the brush around?

Here is my code (where the external file is just a csv of date and sales prices):

  var margin = {top: 25, right: 10, bottom: 200, left: 75},
  margin2 = {top:350, right: 10, bottom: 30, left: 75}, 
  width = 960 - margin.left - margin.right, 
  height = 500 - margin.top - margin.bottom,
  height2 = 500 - margin2.top - margin2.bottom; 

  var parseDate = d3.time.format("%d-%b-%y").parse,
  commasFormatter = d3.format(",.0f"); 

  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 xAxis = d3.svg.axis().scale(x).orient("bottom").tickSize(-height,0,0), 
  xAxis2 = d3.svg.axis().scale(x2).orient("bottom"),
  yAxis = d3.svg.axis().scale(y).orient("left").tickFormat(function(d) { return "$" +         commasFormatter(d); }).tickSize(-width,0,0),
  yAxis2 = d3.svg.axis().scale(y2).orient("left").tickFormat(function(d) { return "$" + commasFormatter(d); });

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

  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")   //defines clipping mask around large graph
      .attr("id","clip")
      .append("rect")                      //mask shape is rectangle
        .attr("width", width)              //mask width is drawable width of large graph
        .attr("height", height);           //mask height is drawable height of large graph

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

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

//BRING IN THE INITIAL DATA
d3.csv("6MonthPracticeData.csv", function(error, data) {
 data.forEach(function(d) {
    d.date = parseDate(d.date);
    d.price = +d.price; 
    });

x.domain(d3.extent(data.map(function(d) { return d.date; }))); 
y.domain([0, d3.max(data.map(function(d) { return d.price; }))]); 
x2.domain(x.domain());
y2.domain(y.domain());

largeGraph.append("g").attr("class","dot")
          .selectAll("circle")
          .data(data).enter()
          .append("circle")
          .attr("cx", function(d) { return x(d.date); })
          .attr("cy", function(d) { return y(d.price); })
          .attr("r",5)
          .attr("clip-path", "url(#clip)");

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

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

largeGraph.append("text")
   .attr("transform", "rotate(-90)")
   .attr("y",0 - margin.left)
   .attr("x", 0 - (height/2))
   .attr("dy", "1em")
   .style("text-anchor", "middle")
   .text("Sale Price");

xBrushGraph.append("g").attr("class","smalldot")
          .selectAll("circle")
          .data(data).enter()
          .append("circle")
          .attr("cx", function(d) { return x2(d.date); })
          .attr("cy", function(d) { return y2(d.price); })
          .attr("r",2.5);

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

xBrushGraph.append("g")
        .attr("class", "y axis")
        .call(yAxis2);

//rotated y-axis label
xBrushGraph.append("text")
   .attr("transform", "rotate(-90)")
   .attr("y",0 - margin2.left)
   .attr("x", 0 - (height2/2))
   .attr("dy", "1em")
   .style("text-anchor", "middle")
   .text("Sale Price");

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

xBrushGraph.append("text")
   .attr("x", width/2)
   .attr("y", height2+25)
   .style("text-anchor", "middle")
   .text("Sale Date");

});

function brushed() {
var extent = brush.extent();
x.domain(brush.empty() ? x2.domain() : [ extent[0][0], extent [1][0] ]);
y.domain(brush.empty() ? y2.domain() : [ extent[0][1], extent [1][1] ]);
largeGraph.selectAll("circle")
          .attr("cx",function(d) { return x(d.date); })
          .attr("cy",function(d) { return y(d.price); });
largeGraph.select(".x.axis").call(xAxis);
largeGraph.select(".y.axis").call(yAxis);
}
Was it helpful?

Solution

The problem is here:

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

Here, after the .call(brush), you explicitly set the attributes y and height of all rectangles within the brush. These rectangles are used to determine which areas allow you to resize and drag the brushed area around.

For the 1D case, setting the 'y' and 'height' attributes ensured that these values made sense. The brush had no way of figuring out how high the brush should be as it only had the x scale declared on it. In your case, you do not need to set the height of the brush's box since you are providing it with the y scale.

Removing the setting of attributes solves the problem: http://jsfiddle.net/g7LGC/1/

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