Zoom & Pan

Posted .

Adding zooming and panning to a graph is pretty easy. It does require we handle our axis and graph rendering slightly differently so it’s easier to redraw them. And we need to add clipping.

This graph is a modification of the single line graph described in Line Graph with Legend. We will only be adding zooming and panning along the x axis but adding it to the other axis works in much the same way.

Here is the source of the final graph (and the source of the fallback).

Axis rendering

In order to easily redraw an axis we are separating the axis and the label into separate groups g.

let gx = svg.append("g")
  .attr("transform", `translate(0,${height - margin.bottom})`)

gx.append("g")
  .append("text")
    .attr("x", (width + margin.left - margin.right) / 2)
    .attr("y", margin.bottom - 4)
    .attr("text-anchor", "middle")
    .style("font-size", "16px")
    .style("fill", "currentColor")
    .text("Date")

let xaxis = gx.append("g")
  .call(d3.axisBottom(x))

We keep a reference to the group containing the axis so we can easily clear and redraw it later.

Clipping

Through panning it is possible that part of the graph ends up inside the margins and overlapping with the axis. In order to prevent this we can use clipping to stop the graph from being rendered in the margins.

First we need to add a clipPath element with a rectangle that covers the area within which the graph is rendered.

svg.append("clipPath")
  .attr("id", "clip")
  .append("rect")
    .attr("x", margin.left)
    .attr("y", margin.top)
    .attr("width", width - margin.left - margin.right)
    .attr("height", height - margin.top - margin.bottom);

Then we can add a clip-path attribute set to url(#clip) to any element we want clipped.

Graph drawing

In order to redraw the graph we need to able to easily redefine the attributes of the graph that define its shape. Since its a line graph we first add the a line, set all the attributes that we will not be changing, and make sure we keep a reference to it. Note the clip-path attribute.

let l =    svg.append("path")
  .attr("clip-path", "url(#clip)")
  .attr("stroke", "var(--theme-0)")
  .attr("stroke-linejoin", "round")
  .attr("fill", "none")

Then we define a draw function that sets the d attribute of the line and call it.

function draw(x) {
  let line = d3.line()
    .x(d => x(d.date))
    .y(d => y(d.temperature))

  l.attr("d", line(data))
}

draw(x)

Make sure the x scale function is a parameter of this function because we will be using an updated scale function to pan and zoom.

Zoom and Pan

The d3.zoom is used to create zoom behaviour, it will result in a function we can call on the svg that will add all the listeners and such to handle zooming. There are a number of methods we will have to call to configure this behaviour. The extent sets the limits of the view that shows the graph, scaleExtent method sets the limits of the zooming behaviour, translateExtent sets the limits of the panning behaviour and on is used to attach a zoom handling function.

let zoom = d3.zoom()
  .extent([
    [margin.left, margin.top],
    [width - margin.right, height - margin.bottom]
  ])
  .scaleExtent([1, 363])
  .translateExtent([
    [margin.left, margin.top],
    [width - margin.right, height - margin.bottom]
  ])
  .on("zoom", zoomhandler)

Note how the extent and translate extent are the same. This is because the graph starts maximally zoomed out with everything that could be shown visible. If this were not the case the translate extent would likely be larger.

The zoomhandler function is fairly straightforward, first we rescale the x axis using a built in helper function, then we redraw the graph and axis. Note how we first clear the xaxis using html("").

function zoomhandler() {
  const xz = d3.event.transform.rescaleX(x)
  draw(xz)
  xaxis.html("").call(d3.axisBottom(xz))
}

Finally we call the zoom function on the svg and use it to scale down to the current day.

let day = new Date().getDate()
let month = new Date().getMonth()

svg.call(zoom)
  .call(zoom.scaleTo, 363,
    [x(new Date(2010, month, day, 11, 0)), 0]
  );

The result is the graph shown in fig. 1.

Tue 1503 AM06 AM09 AM12 PM03 PM06 PM09 PMWed 16Date35404550556065707580Temperature (F°)