Constrain zoom and pan in d3

Viewed 1214

I'm trying to correctly contrain zooming and scaling on my d3-chart using d3's Zoom Behaviour. I've reduced the problem to the following minimal runnable example. I want the user to not be able to zoom in a way that will allow him to see below the 0-line on the y axis.

The sample works when in non-zoomed state, by setting translateExtent to the full height of the svg, but that of course breaks as soon as the user zooms in a little. In fact, the further you zoom in, the further you're able to look into the negative area.

What do I need to set the translateExtent to?

The reason I'm redrawing the line and axes on each zoom event is that normally I'm using react to render my svg and use d3 just for calculcations - I have however removed the dependency on react to provide a more concise example.

const data = [ 0, 15, 30, 32, 44, 57, 60, 60, 85];

// set up dimensions and margins
const full = { w: 200, h: 200 };
const pct = { w: 0.7, h: 0.7 };
const dims = { w: pct.w * full.w, h: pct.h * full.h };
const margin = { w: (full.w - dims.w)/2, h: (full.h - dims.h)/2 };

// scales
const x = d3.scaleLinear()
  .rangeRound([0, dims.w])
  .domain([0, data.length]);
  
const y = d3.scaleLinear()
  .rangeRound([dims.h, 0])
  .domain(d3.extent(data));

// axes
const axes = {
  x: d3.axisBottom().scale(x).tickSize(-dims.w),
  y: d3.axisLeft().scale(y).tickSize(-dims.h)
}


const g = d3.select('.center');

// actual "charting area"
g
  .attr('transform', `translate(${margin.w}, ${margin.h})`)
  .attr('width', dims.w)
  .attr('height', dims.h)
  
// x-axis
g.append('g')
.attr('transform', `translate(0, ${dims.h})`)
.attr('class', 'axis x')
.call(axes.x)

// y-axis
g.append('g')
.attr('class', 'axis y')
.call(axes.y)


// generator for the line
const line = d3.line()
  .x( (_, i) => x(i) )
  .y( d => y(d) );

// the actual data path
const path = g
  .append('path')
  .attr('class', 'path')
  .attr('d', line(data))
  .attr('stroke', 'black')
  .attr('fill', 'none')

const zoomBehaviour = d3.zoom()
  .scaleExtent([1, 10])
  .translateExtent([[0,0], [Infinity, full.h]])
  .on('zoom', zoom);
  
d3.select('svg.chart').call(zoomBehaviour);
  
function zoom() {
  const t = d3.event.transform;
  
  const scaledX = t.rescaleX(x);
  const scaledY = t.rescaleY(y);
  axes.x.scale(scaledX);
  axes.y.scale(scaledY);
  d3.select('.axis.x').call(axes.x);
  d3.select('.axis.y').call(axes.y);
  
  line
    .x( (_, i) => scaledX(i) )
    .y( d => scaledY(d) );
  
  const scaledPath = path.attr('d', line(data));
}
body {
  width: 200px;
  height: 200px;
}

svg.chart {
  width: 200px;
  height: 200px;
  border: 1px solid black;
}

.axis line, .axis path {
  stroke: grey;
}
<script src="https://d3js.org/d3.v4.js"></script>
<body>
<svg class="chart">
  <g class="center"></g>
</svg>
</body>

2 Answers
Related