简体   繁体   中英

Constrain zoom and pan in d3

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> 

I was able to make it work based on the suggestions made by Robert. I had to adjust the conditions a bit to fit my use-case:

const dims = /* object holding svg width and height */;
const [xMin, xMax] = this.scales.x.domain().map(d => this.scales.x(d));
const [yMin, yMax] = this.scales.y.domain().map(d => this.scales.y(d));

if (t.invertX(xMin) < 0) {
  t.x = -xMin * t.k;
}
if (t.invertX(xMax) > dims.width) {
  t.x = xMax - dims.width * t.k;
}
if (t.invertY(yMax) < 0) {
  t.y = -yMax * t.k;
}
if (t.invertY(yMin) > dims.height) {
  t.y = yMin - dims.height * t.k;
}

Check out the zoomed function in this example by Mike Bostock which I believe does what you want, the key part is the mutation of tx and/or ty if there's a breach of constraints:

function zoomed() {
    var t = d3.event.transform;
    if (t.invertX(0) > x0) t.x = -x0 * t.k;
    else if (t.invertX(width) < x1) t.x = width - x1 * t.k;
    if (t.invertY(0) > y0) t.y = -y0 * t.k;
    else if (t.invertY(height) < y1) t.y = height - y1 * t.k;
    g.attr("transform", t);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM