D3.js drag realtive to graph svg

Viewed 232

I have code that should move a circle that's plotted on a graph in a svg using D3.js v6. The circle should be dragged to where the cursor is relative to the graph but I think the cursor position that is given is relative to the whole window and not the graph/svg. I'm not sure how to modify the mouse position to be relative to the svg. I have also tried using the suggestion from this answer here as well:

d3 v6 pointer function not adjusting for scale and translate

Edit:

If I were to start the circle at a position such as (0.5, 0.5) how would I go about making it so the circle only moves in a path along the circumference of a circle centered at (0, 0) with radius 0.5? I tried scaling the position of the mouse to be of magnitude 0.5 using:

x = x*(0.5/(Math.sqrt(x**2 + y**2)));
y = y*(0.5/(Math.sqrt(x**2 + y**2)));

As this is how you scale a vector to be a certain length while keeping the same direction as shown here: https://math.stackexchange.com/questions/897723/how-to-resize-a-vector-to-a-specific-length

However this doesn't seem to work even though it should scale any point the mouse is at to be on the circle centered at the origin with radius 0.5.

var margin = {top: -20, right: 30, bottom: 40, left: 40};
//just setup
var svg = d3.select("#mysvg") 
    .attr("width", 300)
    .attr("height", 300)
    .append("g")
    .attr("transform",
        "translate(" + margin.left + "," + margin.top + ")");
    
var xAxis = d3.scaleLinear() 
    .domain([-1.5, 1.5])
    .range([0, 300]);

svg.append("g")
    .attr("transform", "translate(0," + 300 + ")")
    .call(d3.axisBottom(xAxis)); 

var yAxis = d3.scaleLinear() 
    .domain([-1.5, 1.5])
    .range([300, 0]);

svg.append("g")
    .call(d3.axisLeft(yAxis)); 
    
var circle1 = svg.append('circle')
    .attr('id', 'circle1')
    .attr('cx', xAxis(0))
    .attr('cy', yAxis(0))
    .attr('r', 10)
    .style('fill', '#000000')
    .call( d3.drag().on('drag', dragCircle) ); //add drag listener
    
function dragCircle(event) {
    
        let x = d3.pointer(event, svg.node())[0];
        let y = d3.pointer(event, svg.node())[1];    

        console.log("x: " + x + " y: " + y);
        d3.select("#circle1")
            .attr("cx", xAxis(x))
            .attr("cy", yAxis(y));
    } 
    
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>
<svg id="mysvg"></svg>

1 Answers

Two things,

D3.pointer returns units in pixels, these do not need to be scaled - the purpopse of the scale is to take an arbitrary unit and convert it to pixels:

So instead of:

    d3.select("#circle1")
        .attr("cx", xAxis(x))
        .attr("cy", yAxis(y));

Try:

    d3.select("#circle1")
        .attr("cx", x)
        .attr("cy", y);

Also, we want the drag to be relative to the g which holds the plot and has a transform applied to it. This part is detailed in question you link to. We can specify that we want the drag to be relative to the parent g with:

    let x = d3.pointer(event,svg.node())[0];
    let y = d3.pointer(event,svg.node())[1];

We can then use some trigonometry to constrain the point to a ring and have the drag be based on the angle to any arbitrary point:

        let x = d3.pointer(event,svg.node())[0];
        let y = d3.pointer(event,svg.node())[1]; 
        
        let cx = xAxis(0);
        let cy = yAxis(0);
        let r = xAxis(0.5)-xAxis(0);
        
        let dx = x- cx;
        let dy = y-cy;
        
        var angle = Math.atan2(dy,dx);

        d3.select("#circle1")
            .attr("cx", cx + Math.cos(angle)*r)
            .attr("cy", cy + Math.sin(angle)*r);

Which gives us:

var margin = {top: -20, right: 30, bottom: 40, left: 40};
//just setup
var svg = d3.select("#mysvg") 
    .attr("width", 300)
    .attr("height", 300)
    .append("g")
    .attr("transform",
        "translate(" + margin.left + "," + margin.top + ")");
    
var xAxis = d3.scaleLinear() 
    .domain([-1.5, 1.5])
    .range([0, 300]);

svg.append("g")
    .attr("transform", "translate(0," + 300 + ")")
    .call(d3.axisBottom(xAxis)); 

var yAxis = d3.scaleLinear() 
    .domain([-1.5, 1.5])
    .range([300, 0]);

svg.append("g")
    .call(d3.axisLeft(yAxis)); 
    
var circle1 = svg.append('circle')
    .attr('id', 'circle1')
    .attr('cx', xAxis(0))
    .attr('cy', yAxis(0))
    .attr('r', 10)
    .style('fill', '#000000')
    .call( d3.drag().on('drag', dragCircle) ); //add drag listener
    
var dragCircle = svg.append('circle')
    .attr('id', 'circle1')
    .attr('cx', xAxis(0))
    .attr('cy', yAxis(0))
    .attr('r', xAxis(0.5)-xAxis(0))
    .style('fill', 'none')
    .style('stroke', 'black')
    .style('stroke-line',1)
    .call( d3.drag().on('drag', dragCircle) ); //add drag listener
    
function dragCircle(event) {
    
        let x = d3.pointer(event,svg.node())[0];
        let y = d3.pointer(event,svg.node())[1]; 
        
        let cx = xAxis(0);
        let cy = yAxis(0);
        let r = xAxis(0.5)-xAxis(0);
        
        let dx = x- cx;
        let dy = y-cy;
        
        var angle = Math.atan2(dy,dx);

        d3.select("#circle1")
            .attr("cx", cx + Math.cos(angle)*r)
            .attr("cy", cy + Math.sin(angle)*r);
    }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>
<svg id="mysvg"></svg>

Related