Search code examples
reactjsd3.jszooming

D3.js V4 zoom doesn't work in React + faux


I'm trying to zoom my timeline by x axis, but it doesn't show any reaction on zooming, I've looked through many tutorials, tried separate zoom functions, eventually here what I've got (with this https://coderwall.com/p/psogia/simplest-way-to-add-zoom-pan-on-d3-js manual):

const { dataset } = this.props;
  var minDt = 0;
      var maxDt = 300;
      var intervalMinWidth = 1;
      var elementWidth = 1200; // TODO: make adoptable
      var elementHeight = 200;

      var width = 500;
      var height = 600;

      var groupHeight = 20;
      var xTickSize = 5;

      var x = d3.scaleLinear()
        .domain([minDt, maxDt])
        .range([0, width]);

      var xAxis = d3.axisBottom(x)
        .tickSize(xTickSize);

      var svg = d3.select(svgRoot)
        .append('svg')
        .attr('id', 'chart')
        .attr('class', 'timeline-chart')
        .attr("width", "100%")
        .attr("height", "100%")
        .call(d3.zoom().on("zoom", function () {
          svg.attr("transform", d3.event.transform)
        }))
      .append('g');

      var gX = svg.append("g")
          .attr("class", "axis axis--x")
          .call(xAxis);

      var groupExonItems = svg
        .selectAll('.group-interval-item')
        .data(dataset)
        .enter()
        .append('g')
        .attr('clip-path', 'url(#chart-content)')
        .attr('class', 'item')
        .attr('transform', function (d, i) {
          return 'translate(0, ' + groupHeight * i + ')';
          })
        .selectAll('.rect').data(function (d) {
            return d.data.filter(function (_) {
              return _.type === 'exon';
            });
        }).enter();

      var exonBarHeight = 0.8 * groupHeight;
      var exonBarMargin = 30;

      var exons = groupExonItems
        .append('rect')
        .attr('class', 'interval')
        .attr('width', function (d) {
            return Math.max(intervalMinWidth, x(d.to) - x(d.from));
          })
        .attr('height', exonBarHeight)
        .attr('y', exonBarMargin)
        .attr('x', function (d) {
            return x(d.from);
      });

I have tried separate function, not any progress as well :

  function zoomFunction(){
        var new_xScale = d3.event.transform.rescaleX(x)
        gX.call(xAxis.scale(new_xScale));
        svg.selectAll('rect.interval').attr('x', function (d) {
                return x(d.from);
        }).attr('width', function (d) {
                return Math.max(intervalMinWidth, x(d.to) - x(d.from));
        });
      };

If I log zoom function - I can see it's reachable, and scale changes, but not for my rectangles. May be the reason is I'm using react + faux to draw svg, I have tried without faux - by putting D3 code into the componentDidMount function - still no any progress. What am I missing? Appreciate any help.


Solution

  • Even react-faux-dom is the simplest way to render D3, it breaks dynamic changes of the object, such as zoom. And without faux here is a very simple example to implement zoom: https://swizec.com/blog/two-ways-build-zoomable-dataviz-component-d3-zoom-react/swizec/7753

    As far as I need only x-axis zoom, here is solution:

    import { Component} from 'react'
    import { PropTypes} from 'prop-types'
    import React from 'react'
    import ReactDOM from 'react-dom';
    import ReactFauxDOM from 'react-faux-dom'
    import * as d3 from 'd3'
    
    import styles from './chart.css';
    
    
    const random = d3.randomNormal(2, 1);
    
    class Scatterplot extends React.Component {
      constructor(props) {
        super(props);
        this.updateD3(props);
      }
      componentWillUpdate(nextProps) {
        this.updateD3(nextProps);
      }
      updateD3(props) {
        const { data, width, height, zoomTransform } = props;
        this.xScale = d3.scaleLinear()
                        .domain([0, d3.max(data, ([x, y]) => x)])
                        .range([0, width]);
      }
    
      get transform() {
        const { x, y, zoomTransform } = this.props;
        let transform = "";
        if (zoomTransform) {
          transform = `translate(${x + zoomTransform.x}, ${y}) scale(${zoomTransform.k} 1)`;
        }
        return transform;
      }
    
      render() {
        const { data } = this.props;
        return (
          <g transform={this.transform} ref="scatterplot">
            {data.map(([x, y]) => <rect x={this.xScale(x)} y={y} width={this.xScale(4)} height = {5} />)}
          </g>
        )
      }
    }
    
    class Chart extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          data: d3.range(5).map(_ => [random(), random()]),
          zoomTransform: null
        }
        this.zoom = d3.zoom()
                      .scaleExtent([-5, 5])
                      .translateExtent([[-100, -100], [props.width+100, props.height]])
                      .extent([[-100, -100], [props.width+100, props.height]])
                      .on("zoom", this.zoomed.bind(this))
      }
      componentDidMount() {
        d3.select(this.refs.svg)
          .call(this.zoom)
      }
      componentDidUpdate() {
        d3.select(this.refs.svg)
          .call(this.zoom)
      }
      zoomed() {
        this.setState({
          zoomTransform: d3.event.transform
        });
      }
      render() {
        const { zoomTransform } = this.state,
              { width, height } = this.props;
    
        return (
          <svg width={width} height={height} ref="svg">
            <Scatterplot data={this.state.data}
                         x={0} y={0}
                         width={width/2}
                         height={height}
                         zoomTransform={zoomTransform}/>
            </svg>
        )
      }
    }
    export default Chart