Search code examples
javascriptd3.jsdrag-and-dropdraggable

d3 v4 drag line chart with x and y axes


I'm new to d3.js. I wanted to drag a line chart using its points. It is working fine without the x and y axes. I have used this example as reference: https://bl.ocks.org/mbostock/4342190

With the axes to the line chart the line is not plotting correcly. Please have a look into the snippet below.

Thanks in advance.

<!DOCTYPE html>
<svg width="500" height="350"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>

var svg = d3.select("svg"),
    margin = {top: 20, right: 20, bottom: 30, left: 50},
    width = +svg.attr("width") - margin.left - margin.right,
    height = +svg.attr("height") - margin.top - margin.bottom;
    

let points = d3.range(1, 10).map(function(i) {
    return [i * width / 10, 50 + Math.random() * (height - 100)];
});
var x = d3.scaleLinear()
    .rangeRound([0, width]);

var y = d3.scaleLinear()
    .rangeRound([height, 0]);

var xAxis = d3.axisBottom(x),
    yAxis = d3.axisLeft(y);

var line = d3.line()
    .x(function(d) { return x(d[0]); })
    .y(function(d) { return y(d[1]); });
    
let drag = d3.drag()
        .on('start', dragstarted)
        .on('drag', dragged)
        .on('end', dragended);
        
svg.append('rect')
    .attr('class', 'zoom')
    .attr('cursor', 'move')
    .attr('fill', 'none')
    .attr('pointer-events', 'all')
    .attr('width', width)
    .attr('height', height)
    .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')

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

x.domain(d3.extent(points, function(d) { return d[0]; }));
y.domain(d3.extent(points, function(d) { return d[1]; }));

focus.append("path")
    .datum(points)
    .attr("fill", "none")
    .attr("stroke", "steelblue")
    .attr("stroke-linejoin", "round")
    .attr("stroke-linecap", "round")
    .attr("stroke-width", 1.5)
    .attr("d", line);

focus.selectAll('circle')
    .data(points)
    .enter()
    .append('circle')
    .attr('r', 5.0)
    .attr('cx', function(d) { return x(d[0]);  })
    .attr('cy', function(d) { return y(d[1]); })
    .style('cursor', 'pointer')
    .style('fill', 'steelblue');

focus.selectAll('circle')
        .call(drag);

focus.append('g')
    .attr('class', 'axis axis--x')
    .attr('transform', 'translate(0,' + height + ')')
    .call(xAxis);
    
focus.append('g')
    .attr('class', 'axis axis--y')
    .call(yAxis);

function dragstarted(d) {
    d3.select(this).raise().classed('active', true);
}

function dragged(d) {
    d3.select(this)
        .attr('cx', d[0] = d3.event.x)
        .attr('cy', d[1] = d3.event.y)
    focus.select('path').attr('d', line);
}

function dragended(d) {
    d3.select(this).classed('active', false);
}

</script>


Solution

  • d3.event is holding pixel positions, but your plot is driven by user-space coordinates. So, you need to convert those pixel positions to user-space. You can use your scales invert method to do so.

    function dragged(d) {
        d[0] = x.invert(d3.event.x); // convert to user-space
        d[1] = y.invert(d3.event.y);
        d3.select(this)
            .attr('cx', x(d[0])) // back to pixels
            .attr('cy', y(d[1]))
        focus.select('path').attr('d', line); //line does pixel conversion too
    }
    

    Running code:

    <!DOCTYPE html>
    <svg width="500" height="350"></svg>
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <script>
    
    var svg = d3.select("svg"),
        margin = {top: 20, right: 20, bottom: 30, left: 50},
        width = +svg.attr("width") - margin.left - margin.right,
        height = +svg.attr("height") - margin.top - margin.bottom;
        
    
    let points = d3.range(1, 10).map(function(i) {
        return [i * width / 10, 50 + Math.random() * (height - 100)];
    });
    var x = d3.scaleLinear()
        .rangeRound([0, width]);
    
    var y = d3.scaleLinear()
        .rangeRound([height, 0]);
    
    var xAxis = d3.axisBottom(x),
        yAxis = d3.axisLeft(y);
    
    var line = d3.line()
        .x(function(d) { return x(d[0]); })
        .y(function(d) { return y(d[1]); });
        
    let drag = d3.drag()
            .on('start', dragstarted)
            .on('drag', dragged)
            .on('end', dragended);
            
    svg.append('rect')
        .attr('class', 'zoom')
        .attr('cursor', 'move')
        .attr('fill', 'none')
        .attr('pointer-events', 'all')
        .attr('width', width)
        .attr('height', height)
        .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
    
     var focus = svg.append("g")
                    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    
    x.domain(d3.extent(points, function(d) { return d[0]; }));
    y.domain(d3.extent(points, function(d) { return d[1]; }));
    
    focus.append("path")
        .datum(points)
        .attr("fill", "none")
        .attr("stroke", "steelblue")
        .attr("stroke-linejoin", "round")
        .attr("stroke-linecap", "round")
        .attr("stroke-width", 1.5)
        .attr("d", line);
    
    focus.selectAll('circle')
        .data(points)
        .enter()
        .append('circle')
        .attr('r', 5.0)
        .attr('cx', function(d) { return x(d[0]);  })
        .attr('cy', function(d) { return y(d[1]); })
        .style('cursor', 'pointer')
        .style('fill', 'steelblue');
    
    focus.selectAll('circle')
            .call(drag);
    
    focus.append('g')
        .attr('class', 'axis axis--x')
        .attr('transform', 'translate(0,' + height + ')')
        .call(xAxis);
        
    focus.append('g')
        .attr('class', 'axis axis--y')
        .call(yAxis);
    
    function dragstarted(d) {
        d3.select(this).raise().classed('active', true);
    }
    
    function dragged(d) {
        d[0] = x.invert(d3.event.x);
        d[1] = y.invert(d3.event.y);
        d3.select(this)
            .attr('cx', x(d[0]))
            .attr('cy', y(d[1]))
        focus.select('path').attr('d', line);
    }
    
    function dragended(d) {
        d3.select(this).classed('active', false);
    }
    
    </script>