0

The below React component should allow for panning of an SVG. It does, but for some reason the SVG's movement accelerates exponentially so that a few pixels movement of the mouse pointer results in increasingly large movement of the SVG (or rather, the SVG viewBox). Eg, a slight drag of the mouse and the circle zips off the screen.

here's a fiddle of it: https://jsfiddle.net/bupham/ax473r52/4/

It seems like there may be some React feedback loop happening, but I am not sure. The panning behavior code is from another SO post here.

I tried moving the method calls to the container, or the SVG, but it still happens. I've tried passing a function to setState, still happens. I tried making shallow copies of state.viewBox and not making shallow copies -- still happens. What am I doing wrong?

export default class SVGContainer extends React.Component {
  constructor(props) {
    super(props);

    this.state= {
      viewBox: {x:0,y:0,w:500,h:500},
      svgSize: {w: 500, h: 500},
      scale: 1,
      isPanning: false, 
      startPoint: {x:0,y:0},
      endPoint: {x:0,y:0},
    }
  }

  handleMouseDown = (e) => {
    console.log('handleMouseDown e', e)
    this.setState({
      isPanning: true,
      startPoint: {x: e.clientX, y: e.clientY}, 
    })
  }

  handleMouseMove = (e) => {
      this.setState((prevState, props) => {
        if (prevState.isPanning) {
          console.log('handleMouseMove e', e.clientX, e.clientY)
          
          let startPoint = prevState.startPoint;
          const scale = prevState.scale; 
          const viewBox = prevState.viewBox;
          const endPoint = {x: e.clientX, y: e.clientY};
          const dx = (startPoint.x - endPoint.x) / scale;
          const dy = (startPoint.y - endPoint.y) / scale;
          const newViewbox = {x:viewBox.x+dx, y:viewBox.y+dy, w:viewBox.w, h:viewBox.h};
          console.log('the view box', newViewbox)
          return {viewBox: newViewbox};
        }
      }) 
  }

  handleMouseUp = (e) => {
    if (this.state.isPanning){ 
      let startPoint = this.state.startPoint;
      const scale = this.state.scale; 
      const viewBox = this.state.viewBox;
      const endPoint = {x: e.clientX, y: e.clientY};
      var dx = (startPoint.x - endPoint.x)/scale;
      var dy = (startPoint.y - endPoint.y)/scale;
      const endViewBox = {x: viewBox.x+dx, y: viewBox.y+dy, w: viewBox.w, h: viewBox.h};
      console.log('viewbox at mouseup',endViewBox)
      this.setState({
        viewBox: endViewBox,
        isPanning: false,
      });
      
    }
  }

  handleMouseLeave = (e) => {
    this.setState({
      isPanning: false,
    })
  }

  render() {

    return (
      <div className="container" >
        <svg width="500" height="500" 
          onWheel={this.handleWheelZoom}
          onMouseDown={this.handleMouseDown}
          onMouseMove={this.handleMouseMove}
          onMouseUp={this.handleMouseUp}
          onMouseLeave={this.handleMouseLeave}          
          viewBox={`${this.state.viewBox.x} ${this.state.viewBox.y} ${this.state.viewBox.w} ${this.state.viewBox.h}`}>
          <circle cx="50" cy="50" r="50" /> 
        </svg>
      </div>
    );  
  }
}
Ben Upham
  • 63
  • 1
  • 6

1 Answers1

0

This is not technically an answer, but rather a solution: I decided to use d3 (4.13.0) instead because the cross-browser/touch complexities of zoom is too much to bother writing by hand.

As for why it wasn't working, it most likely has to do with the async nature of React and React state. A friend suggested using requestAnimationFrame() and/or throttling the mouse events.

Here's what the relevant code I used looks like. I added two React refs for the two DOM nodes I needed to manipulate with D3:

export default class SVGContainer extends React.Component {
  constructor(props) {
    super(props);
    this.svgRef = React.createRef();
    this.gRef = React.createRef();

    this.state= {
      container: {width: 1000, height: 1000},
    }
  }

  componentDidMount() {
    window.addEventListener('resize', this.handleWindowResize);
    const width = window.innerWidth;
    const height = window.innerHeight;
    this.setState({
      container: {width, height},
    })

    const svg = d3.select(this.svgRef.current);
    // D3 wants you to call zoom on a container and then apply the zoom transformations
    // elsewhere...you can read why in the docs. 
    svg.call(this.handleZoom).call(this.handleZoom.transform, initialZoom);
  }
  
  componentWillUnmount() {
    window.removeEventListener('resize', this.handleWindowResize);
  }

  handleWindowResize = (e) => {
    const height = this.svgRef.current.clientHeight;
    const width = this.svgRef.current.clientWidth;

    this.setState({
      container: {width, height},
    });    
  }

  handleZoom = d3.zoom().on('zoom', e => {
      const g = d3.select(this.gRef.current);
      g.attr('transform', d3.event.transform)
  })

  render() {

    return (
      <div className="SVGContainer">
        <svg 
          width={this.state.container.width} height={this.state.container.height} 
          ref={this.svgRef}>
          <circle cx="50" cy="50" r="50" ref={this.gRef} /> 
        </svg>
      </div>
    );  
  }
}
Ben Upham
  • 63
  • 1
  • 6