Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

brush on rotated lines using d3 to create zoom effect

I am working on this plnkr. I have three lines at angle 30, 45 and 60. I want to apply a brush on these lines so that when the chart is brushed the lines get redrawn at where it crossed the brushed rectangle with appropriate the values on the axis. Any help or hint to solve this problem is greatly appreciated.

EDIT: If you have different solutions to draw the rotated lines and brush on top of them it is welcomed too. Please help.

var ga = d3.select("svg")
    .append("g")
    .attr("class", "a axis")
    .attr("transform", "translate(" + margin.left + "," + (height + margin.top) + ")")
    .selectAll("g")
    .data([30, 45, 60])
    .enter()
    .append("g")
    .attr("class", "rotatedlines")
    .attr("transform", function(d) { return "rotate(" + -d + ")"; })
    .attr("stroke-width", 1)
    .attr("stroke", "black")
    .attr("stroke-dasharray", "5,5");

Example result

like image 250
somename Avatar asked Sep 24 '15 00:09

somename


2 Answers

To explain my solution:

The fundamental steps to take are as follows:

  • update the domains of the x and y scales to the brush extent
  • redraw the axes
  • compute the scale factor and translation for the lines
  • scale and translate the line containers accordingly
  • reset the brush

Note that steps 3 and 4 are only necessary because you're not using the scales to draw everything -- a better approach would be to define two points for each line as the data that's bound to the elements and then use the scales to redraw. This would make the code simpler.

With your approach it's still possible though. In order to facilitate it, I've made a few modifications to your code -- in particular, I've cleaned up the various nested g elements with different translations and defined the lines through their x1, x2, y1, y2 attributes rather than through translation of the containers. Both of these changes make the functionality you want easier to implement as only a single transformation takes places that doesn't need to consider multiple other transformations. I've also nested the lines in multiple g elements so that they can be scaled and translated more easily.

The brush handler function now looks like this:

// update scales, redraw axes
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] ]);
xAxisG.call(xAxis);
yAxisG.call(yAxis);

This code should be fairly self-explanatory -- the domains of the scales are updated according to the current extent of the brush and the axes are redrawn.

// compute and apply scaling and transformation of the g elements containing the lines
var sx = (x2.domain()[1] - x2.domain()[0])/(x.domain()[1] - x.domain()[0]),
    sy = (y2.domain()[1] - y2.domain()[0])/(y.domain()[1] - y.domain()[0]),
    dx = -x2(x.domain()[0]) - x2.range()[0],
    dy = -y2(y.domain()[1]) - y2.range()[1];
d3.selectAll("g.container")
  .attr("transform", "translate(" + [sx * dx, sy * dy] + ")scale(" + [sx, sy] + ")");

This is the tricky part -- based on the new domains of the scales, we need to compute the scale and translation for the lines. The scaling factors are simply the ratio of the old extent to the new extent (note that I have made copies of the scales that are not modified), i.e. a number greater than 1. The translation determines the shift of the (0,0) coordinate and is computed through the difference of the old (0,0) coordinate (I get this from the range of the original scales) and the position of the new domain origin according to the original scales.

When applying the translation and scale at the same time, we need to multiply the offsets with the scaling factors.

// reset brush
brush.clear();
d3.select(".brush").call(brush);

Finally, we clear the brush and reset it to get rid of the grey rectangle.

Complete demo here.

like image 191
Lars Kotthoff Avatar answered Sep 24 '22 11:09

Lars Kotthoff


You can access the brush extent via d3.event.target.extent(). The flow for drawing the scale is this:

  • Set scale
  • Set axis
  • Draw axis

As soon as the brush is done, you have to modify the scale and then re-draw the axis according to the current x and y domain. Is that what you meant?

I cleaned up the code a bit and made a little demonstration: http://plnkr.co/edit/epKbXbcBR2MiwUOMlU5A?p=preview

like image 25
Rudolf Avatar answered Sep 25 '22 11:09

Rudolf