Accepted answer

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;;

    //redraw data ppints
        .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 +
    // the circle
    let circ      ='.scatter-group circle')
    // the radius
    let rad       = 75

    // reset the circle 'cx' and 'cy' according to the transform
    .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.

Related Query