Limiting domain when zooming or panning in D3.js

Nils picture Nils · May 3, 2012 · Viewed 9.2k times · Source

I have implemented a simple D3.js line chart that can be zoomed and panned. It is based on Stephen Bannasch's excellent example here.

The domain of my data is [0, n] in the x dimension.

How can I limit zooming and panning to this domain using the built-in zoom behavior (i.e. using mousewheel events)?

I want to prevent users from panning past 0 on the lower end or n on the upper end, for example they should never be able to see negative values on the x-axis, and want to limit zooming to the same window.

The examples that I found based on Jason Davies work using extent( [...],[...],[...] ) seem to no longer work in version 2.9.1. Unfortunately, the zoom behavior is currently one of the few features not documented in the otherwise outstanding API documentation.

Any pointers are welcome.

PS. I have posted the same question on the D3.js mailing list but did not get a response: https://groups.google.com/d/topic/d3-js/w6LrHLF2CYc/discussion. Apologies for the cross-posting.

Answer

Andor Goetzendorff picture Andor Goetzendorff · Aug 17, 2012

Sadly, the solution posted by Bill did only half the trick: While it does indeed inhibit the panning, it causes the graph to distort if zoom is applied. It is then usually impossible to return to a properly proportioned and positioned graph.

In the following version the proportions of the axes are maintained, even if scrolling to the borders.

As soon as the scaling hits 100%, the scales' domains are reset to their original position. This guarantees a correct positioning, even if the intermediate steps return illegal parameters for the axes.

While not perfect, I hope this script can help somebody until d3 (re)implements this feature.

# x and y are the scales
# xAxis and yAxis are the axes
# graph is the graph you want attach the zoom to

x0 = x.copy()
y0 = y.copy()

successfulTranslate = [0, 0]

zoomer = d3.behavior.zoom()
  .scaleExtent([1,2])

onZoom = ->
  ev = d3.event # contains: .translate[x,y], .scale
  if ev.scale == 1.0
    x.domain x0.domain()
    y.domain y0.domain()
    successfulTranslate = [0, 0]
  else
    xTrans = x0.range().map( (xVal) -> (xVal-ev.translate[0]) / ev.scale ).map(x0.invert)
    yTrans = y0.range().map( (yVal) -> (yVal-ev.translate[1]) / ev.scale ).map(y0.invert)
    xTransOk = xTrans[0] >= x0.domain()[0] and xTrans[1] <= x0.domain()[1]
    yTransOk = yTrans[0] >= y0.domain()[0] and yTrans[1] <= y0.domain()[1]
    if xTransOk
      x.domain xTrans
      successfulTranslate[0] = ev.translate[0]
    if yTransOk
      y.domain yTrans
      successfulTranslate[1] = ev.translate[1]
  zoomer.translate successfulTranslate

graph.select('g.x.axis').call(xAxis)
graph.select('g.y.axis').call(yAxis)
drawBars()

zoomer.on('zoom', onZoom)

# ...
graph.call(zoomer)