D3: Position marker does not follow graph when zooming and panning

I have a D3.js graph with a vertical red line that marks the value of variable x. The value of x is determined by a range slider on the page.

The problem is that when the graph is panned/zoomed, the vertical line does not move together with the graph’s scales and content.

I’ve tried various approaches, but to no avail. How can I get the vertical line to keep its position relative to the graph content, and still update its horizontal position based on the value of x?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Binet/Fibonacci</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/13.2.0/math.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            text-align: center;
            margin: 20px;
        }

        #slider {
            margin: 20px;
            width: 80%;
        }

        svg {
            border: 1px solid black;
            margin-top: 20px;
        }
    </style>
</head>
<body>
    <input type="range" id="slider" min="0" max="30" step="0.02" value="0" />
    <p>Value of x: <span id="xValue">0.00</span></p>
    <p>Fibonacci number (y): <span id="yValue">0</span></p>
    <p>y real: <span id="yReal">0</span></p>
    <p>y imaginary: <span id="yImag">0</span></p>
    <p>y imaginary (decimal): <span id="yImagDec">0</span></p>

    <div>
        <label for="yImagMultip">y imaginary multiplier: </label>
        <input type="number" id="yImagMultip" value="1000" step="10" />
    </div>

    <svg width="800" height="400" id="chartArea"></svg>

    <script>
        const phi = math.bignumber((1 + Math.sqrt(5)) / 2);
        const psi = math.bignumber((1 - Math.sqrt(5)) / 2);

        function fibonacci(x) {
            const n = math.bignumber(x);
            const y = math.divide(
                math.add(
                    math.pow(phi, n),
                    math.multiply(-1, math.pow(psi, n))
                ),
                math.sqrt(5)
            );
            return y;
        }

        const slider = document.getElementById("slider");
        const xValue = document.getElementById("xValue");
        const yValue = document.getElementById("yValue");
        const yReal = document.getElementById("yReal");
        const yImag = document.getElementById("yImag");
        const yImagDec = document.getElementById("yImagDec");
        const yImagMultipInput = document.getElementById("yImagMultip");

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

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

        let realPoints = [];
        let imaginaryPoints = [];
        let x; // x scale
        let y; // y scale
        let xAxis; // x axis
        let yAxis; // y axis
        let line; // line generator

        function updateData() {
            realPoints = [];
            imaginaryPoints = [];
            for (let x = 0; x <= 30; x += 0.01) {
                const yVal = fibonacci(x);
                realPoints.push({ x, y: math.re(yVal) }); // Real part
                const imaginaryPart = math.im(yVal); // Extract imaginary part
                imaginaryPoints.push({ x, y: imaginaryPart });
            }
        }

        function drawGraph() {
            // Update scales
            x = d3.scaleLinear()
                .domain([0, 30])
                .range([0, width]);

            y = d3.scaleLinear()
                .domain([-50, 250])
                .range([height, 0]);

            // Update axes
            xAxis = graphArea.selectAll(".x-axis").empty() ? graphArea.append("g").attr("class", "x-axis") : xAxis;
            yAxis = graphArea.selectAll(".y-axis").empty() ? graphArea.append("g").attr("class", "y-axis") : yAxis;

            xAxis.attr("transform", `translate(0,${height})`).call(d3.axisBottom(x));
            yAxis.call(d3.axisLeft(y));

            // Update or draw real curve
            line = d3.line()
                .x(d => x(d.x))
                .y(d => y(d.y));

            const realLinePath = graphArea.selectAll(".real-line")
                .data([realPoints]);

            realLinePath.enter()
                .append("path")
                .attr("fill", "none")
                .attr("stroke", "blue")
                .attr("class", "real-line")
                .merge(realLinePath)
                .attr("d", line);

            // Update or draw imaginary curve
            const imaginaryLinePath = graphArea.selectAll(".imaginary-line")
                .data([imaginaryPoints]);

            imaginaryLinePath.enter()
                .append("path")
                .attr("fill", "none")
                .attr("stroke", "orange")
                .attr("class", "imaginary-line")
                .merge(imaginaryLinePath)
                .attr("d", d3.line()
                    .x(d => x(d.x))
                    .y(d => y(d.y * +yImagMultipInput.value)));

            // Update vertical line
            updateVerticalLine();
        }

        function updateVerticalLine() {
            const currentX = +slider.value;
            const verticalLine = graphArea.selectAll(".current-line").data([currentX]);

            verticalLine.enter()
                .append("line")
                .attr("class", "current-line")
                .attr("stroke", "red")
                .attr("stroke-width", 1)
                .attr("stroke-dasharray", "4")
                .merge(verticalLine)
                .attr("x1", x(currentX))
                .attr("x2", x(currentX))
                .attr("y1", height)
                .attr("y2", 0);
        }

        function handleZoom(event) {
            const new_x = event.transform.rescaleX(x);
            const new_y = event.transform.rescaleY(y);

            xAxis.call(d3.axisBottom(new_x));
            yAxis.call(d3.axisLeft(new_y));

            graphArea.selectAll(".real-line")
                .attr("d", d3.line()
                    .x(d => new_x(d.x))
                    .y(d => new_y(d.y)));

            graphArea.selectAll(".imaginary-line")
                .attr("d", d3.line()
                    .x(d => new_x(d.x))
                    .y(d => new_y(d.y * +yImagMultipInput.value)));

            updateVerticalLine();
        }

        const zoom = d3.zoom()
            .scaleExtent([0.5, 20])
            .on("zoom", handleZoom);

        svg.append("rect")
            .attr("width", width)
            .attr("height", height)
            .style("fill", "none")
            .style("pointer-events", "all")
            .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
            .call(zoom);

        slider.addEventListener("input", function () {
            const x = parseFloat(this.value).toFixed(2);
            xValue.textContent = x;

            const yVal = fibonacci(x);
            yValue.textContent = math.format(yVal, { precision: 5 });
            yReal.textContent = math.format(math.re(yVal), { precision: 5 });
            const imaginaryPart = math.im(yVal);
            yImag.textContent = math.format(imaginaryPart, { precision: 5 });
            yImagDec.textContent = math.format(imaginaryPart, { precision: 15, notation: 'fixed' });
            
            if (imaginaryPart == 0) {
                yImag.textContent = "0";
                yImagDec.textContent = "0";
            }

            drawGraph(); // Refresh the graph
        });

        yImagMultipInput.addEventListener("input", drawGraph);

        updateData(); // Generate data first
        drawGraph(); // Draw the graph initially
    </script>
</body>
</html>

You may also notice that interacting with the page controls resets the zoom level. I should mention that this is not intentional. It’s a separate issue that I will probably need to ask a separate question about. But if anyone reading knows how to fix this, I welcome your feedback.