The D3.js library has a very powerful library for zooming and panning. It makes these SVG actions very easy to work with, even when the math gets particularly ugly.

Configuring the canvas is relatively simple. Let’s start with the SVG element. The linear gradient uses the viridis color scheme with 5 stops.

<svg id="canvas" width="700" height="500">
    <defs>
        <linearGradient id="viridis-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
            <stop offset="0%" stop-color="#440154" stop-opacity="0.6"></stop>
            <stop offset="25%" stop-color="#3b528b" stop-opacity="0.6"></stop>
            <stop offset="50%" stop-color="#21918c" stop-opacity="0.6"></stop>
            <stop offset="75%" stop-color="#5ec962" stop-opacity="0.6"></stop>
            <stop offset="100%" stop-color="#fde725" stop-opacity="0.6"></stop>
        </linearGradient>
    </defs>
    <g class="view" transform="translate(0, 0) scale(1)">
        <rect fill="url(#viridis-gradient)" x="0" y="0" width="700" height="500"></rect>
    </g>
    <g class="axis axis-x"></g>
    <g class="axis axis-y"></g>
</svg>

Next, we would like to add x- and y-axes to this canvas. This is also easily accomplished with D3. Create a scale, create an axis, and apply the axis to the group.

let x = d3.scaleLinear()
    .domain([0, width])
    .range([0, width]);
let xAxis = d3.axisTop(x)
    .ticks(5)
    .tickSize(height)
    .tickPadding(4 - height)
let xAxis = svg.select(".axis-x")
    .call(xAxis);

Note: The y-axis is left as an exercise for the reader.

Next, we need to configure the zoom callback. This is done with the d3.zoom() function.

let zoom = d3.zoom()
    .scaleExtent([0.05, 20])
    .translateExtent([[0, 0], [width, height]])
    .on("zoom", () => {
        svg.select("#view").attr("transform", d3.event.transform);
        xGroup.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));
        yGroup.call(yAxis.scale(d3.event.transform.rescaleY(yScale)));
    });
svg.call(zoom);

A copy of this code is available on Github.