Joe
Joe

Reputation: 5487

D3js Zoom With Manually Drawn Circle

I am working on a d3 scatter plot where an area of the chart will be circled (a Youden Plot). Based on available samples, I have been able to add zoom to both my data points and my axis. However, I am unable to get the circle to zoom correctly.

I suspect that I need to set up some kind of scale (scaleSqrt, possibly), but I am struggling to find documentation on this that is written at a beginner level.

My current circle code is very straightforward

var circle = drawCircle();
function drawCircle() {
    return svg
      .append('g')
      .attr('class', 'scatter-group')
      .append('circle')
      .attr("r", 75 )
      .attr('cx', 200 + margin.left) //suspect this needs to be related to a scale
      .attr('cy', 200 + margin.top) //suspect this needs to be related to 
      .attr('r', 75)//suspect this needs to be related to a scale
      .attr('stroke', 'red')
      .attr('stroke-width', 3)
      .style('fill', 'none')
  }

As is the zoomed function

function zoomed() {
    var new_xScale = d3.event.transform.rescaleX(xScale);
    var new_yScale = d3.event.transform.rescaleY(yScale);

    // update axes
    gX.call(xAxis.scale(new_xScale));
    gY.call(yAxis.scale(new_yScale));

    //redraw data ppints    
    points.data(data)
        .attr('cx', function(d) {return new_xScale(d.x)})
        .attr('cy', function(d) {return new_yScale(d.y)});

    //redraw circle
}

My work in progress is available in this fiddle . Can someone possible point me in the right direction?

Upvotes: 0

Views: 99

Answers (1)

varontron
varontron

Reputation: 1157

I believe this will get you most of the way there. You need to update your circle attributes in the zoomed function along with the other elements:

function zoomed() {
    var new_xScale = d3.event.transform.rescaleX(xScale);
    var new_yScale = d3.event.transform.rescaleY(yScale);

    // update axes
    gX.call(xAxis.scale(new_xScale));
    gY.call(yAxis.scale(new_yScale));

    //redraw data ppints    
    points.data(data)
        .attr('cx', function(d) {return new_xScale(d.x)})
        .attr('cy', function(d) {return new_yScale(d.y)});


    // The new part:

    // the transform
    let trans     = d3.event.transform  
    // the approximate domain value of the circle 'cx' for converting later                  
    let cx_domain = xScale.invert(200 + margin.left) 
    // the approximate domain value of the circle 'cy' for converting later 
    let cy_domain = yScale.invert(200 + margin.top)
    // the circle
    let circ      =  d3.select('.scatter-group circle')
    // the radius
    let rad       = 75

    // reset the circle 'cx' and 'cy' according to the transform
    circ
    .attr('cx',function(d) { return new_xScale(cx_domain)})
    .attr('cy',function(d) { return new_yScale(cy_domain)}) 
    // reset the radius by the scaling factor
    .attr('r', function(d) { return rad*trans.k }) 

} 

See this fiddle

You'll notice the circle does not scale or move at quite the same rate as the scatter dots. This is possibly because of the use of the invert function, because the conversion from range to domain and back to range is imperfect. This issue is documented

For a valid value y in the range, continuous(continuous.invert(y)) approximately equals y; similarly, for a valid value x in the domain, continuous.invert(continuous(x)) approximately equals x. The scale and its inverse may not be exact due to the limitations of floating point precision.

Your original idea to assign dynamic values to cx, cy and r will likely compensate for this, because you can then avoid the inversion.

Upvotes: 2

Related Questions