javascriptd3.js

How can I ensure a y-scale zoom is centered on my cursor in spite of margin / padding?


I have this d3 snippet here:

https://jsfiddle.net/h8b6gq7d/

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Order Book Visualization</title>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <style>
        .axis path,
        .axis line {
            fill: none;
            shape-rendering: crispEdges;
        }
    </style>
</head>
<body>
    <svg width="800" height="400"></svg>
    <script>
        let orderBookStateVis = {
            MinY: 100,  // Example value
            MaxY: 300,  // Example value
            MinX: -100,   // Example value
            MaxX: 100     // Example value
        };

        // Define the chart dimensions
        const svg = d3.select("svg"),
            margin = { top: 100, right: 30, bottom: 40, left: 40 },
            width = +svg.attr("width") - margin.left - margin.right,
            height = +svg.attr("height") - margin.top - margin.bottom;

        const g = svg.append("g")
            .attr("transform", `translate(${margin.left},${margin.top})`);

        const y = d3.scaleLinear()
            .domain([orderBookStateVis.MinY, orderBookStateVis.MaxY])
            .range([height, 0]);

        const x = d3.scaleLinear()
            .domain([orderBookStateVis.MinX, orderBookStateVis.MaxX])
            .range([0, width]);

        const yAxis = g.append("g")
            .attr("class", "y-axis")
            .call(d3.axisLeft(y));

        const xAxis = g.append("g")
            .attr("class", "x-axis")
            .call(d3.axisBottom(x).tickSizeOuter(0))
            .attr("transform", `translate(0,${height})`);

        // Zoom behavior
        const zoom = d3.zoom()
            .scaleExtent([0.5, 5])
            .translateExtent([[0, 0], [width, height]])
            .extent([[0, 0], [width, height]])
            .on("zoom", zoomed);

        svg.call(zoom);

        // Reset view function
        function resetZoom() {
            svg.transition().duration(750).call(zoom.transform, d3.zoomIdentity);
        }

        svg.on("contextmenu", (event) => {
            event.preventDefault();
            resetZoom();
        });

        // Zoom event handler
        function zoomed(event) {
        
            const transform = event.transform;
                            console.log(transform.y);


            // Rescale axes
            const zx = transform.rescaleX(x);
            const zy = transform.rescaleY(y);

            xAxis.call(d3.axisBottom(zx).tickSizeOuter(0));
            yAxis.call(d3.axisLeft(zy));
        }
    </script>
</body>
</html>

My one problem with it is that when I zoom in on the y-axis, my zoom is not centered where my cursor is. I'd expect that if I'm hovering on y=200, then after zoom I should still be hovering on that point - instead my y position constantly becomes more disjointed from where my cursor is. I've discovered that the issue only arises when I add in a top margin. How can I correct for this top margin?


Solution

  • You can add a rect of the same dimensions of your "canvas" g and put the zoom events on it:

    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <title>Order Book Visualization</title>
        <script src="https://d3js.org/d3.v7.min.js"></script>
        <style>
          .axis path,
          .axis line {
            fill: none;
            shape-rendering: crispEdges;
          }
        </style>
      </head>
      <body>
        <svg width="800" height="400"></svg>
        <script>
          let orderBookStateVis = {
            MinY: 100, // Example value
            MaxY: 300, // Example value
            MinX: -100, // Example value
            MaxX: 100, // Example value
          }
    
          // Define the chart dimensions
          const svg = d3.select("svg"),
            margin = { top: 100, right: 30, bottom: 40, left: 40 },
            width = +svg.attr("width") - margin.left - margin.right,
            height = +svg.attr("height") - margin.top - margin.bottom
    
          const rect = svg
            .append("rect")
            .attr("width", width)
            .attr("height", height)
            .attr("transform", `translate(${margin.left},${margin.top})`)
            .style("fill", "none")
            .style("pointer-events", "all")
    
          const g = svg
            .append("g")
            .attr("transform", `translate(${margin.left},${margin.top})`)
    
          const y = d3
            .scaleLinear()
            .domain([orderBookStateVis.MinY, orderBookStateVis.MaxY])
            .range([height, 0])
    
          const x = d3
            .scaleLinear()
            .domain([orderBookStateVis.MinX, orderBookStateVis.MaxX])
            .range([0, width])
    
          const yAxis = g.append("g").attr("class", "y-axis").call(d3.axisLeft(y))
    
          const xAxis = g
            .append("g")
            .attr("class", "x-axis")
            .call(d3.axisBottom(x).tickSizeOuter(0))
            .attr("transform", `translate(0,${height})`)
    
          // Zoom behavior
          const zoom = d3
            .zoom()
            .scaleExtent([0.5, 5])
            .translateExtent([
              [0, 0],
              [width, height],
            ])
            .extent([
              [0, 0],
              [width, height],
            ])
            .on("zoom", zoomed)
    
          rect.call(zoom)
    
          // Reset view function
          function resetZoom() {
            svg.transition().duration(750).call(zoom.transform, d3.zoomIdentity)
          }
    
          svg.on("contextmenu", (event) => {
            event.preventDefault()
            resetZoom()
          })
    
          // Zoom event handler
          function zoomed(event) {
            const transform = event.transform
    
            // Rescale axes
            const zx = transform.rescaleX(x)
            const zy = transform.rescaleY(y)
    
            xAxis.call(d3.axisBottom(zx).tickSizeOuter(0))
            yAxis.call(d3.axisLeft(zy))
          }
        </script>
      </body>
    </html>