React + d3v4: Trouble moving node positions in Beeswarm Chart using the correct motion

Viewed 207

I have managed to create a Beeswarm plot which updates the node positions based on new data selected through a dropdown. However, i am unable to program the nodes to shift positions using the correct motion. Currently, on initial render, i see a radial clustering/collision motion that positions all nodes. Each time i change the dropdown selection, this motion reoccurs to render the new nodes. However, i would like to see a nice 'pull' motion moving the nodes from where they currently are to their new positions.

I'm using React to draw all the nodes, d3 calculates the x and y.

Beeswarm Component

class BeeswarmPlot extends Component {

  createNodes = (data) => {
        ...
  }

  render() {
    // Receives data from another component and creates nodes for each record
    var nodes = this.createNodes(this.props.data) 
    return (
      <svg>
          <ForceGraph
            nodes={nodes}
          />
      </svg>
    )
  }

}

export default BeeswarmPlot;

Force layout component

class ForceGraph extends Component {

  constructor() {
    super();
    this.state = {nodes: []};
  }

  componentDidMount() {
    this.updateNodePositions(this.props.nodes)
  }

  componentDidUpdate(prevProps, prevState) {

    if (this.props.nodes != prevProps.nodes) {
        this.updateNodePositions(this.props.nodes)
      }
  }

  componentWillUnmount() {
    this.force.stop()
  }

  updateNodePositions = (nodes) => {
    this.force = d3.forceSimulation(nodes)
                  .force("x", d3.forceX(d => d.cx))
                  .force("y", d3.forceY(d => d.cy))
                  .force("collide", d3.forceCollide(3))

    this.force.on('tick', () => this.setState({nodes}))
  }

  render() {

    const {nodes} = this.state

    return (
      <Dots
        data={nodes}
      />
    )
  }

}

export default ForceGraph;

EDIT: Using state to update node positions received from parent as props is a bad idea, as the page re-renders with each state change. I will never be able to achieve the transition motion of node positions!

Hence, i have decided to change method to using props to update node positions without state change. However, now, I am unable to shift the node positions, which remain where they are no matter what i select in dropdown.

export default class BubbleChart extends Component {

  constructor(props) {
    super(props)
    this.bubbles = null;
    this.nodes = [];
    this.forceStrength = 0.03;

    // Initialize a template of force layout. 
    this.simulation = d3.forceSimulation()
      .velocityDecay(0.2)
      .force('x', d3.forceX().strength(this.forceStrength).x(d => d.x))
      .force('y', d3.forceY().strength(this.forceStrength).y(d => d.y))
      .force('charge', d3.forceManyBody().strength(this.charge))
      .force("collide", d3.forceCollide(3))

  }

  componentDidMount() {
    this.container = select(this.refs.container)
    // Calculate node positions from raw data received. The output of this function will create positions that overlap which each other. Separation of the nodes will be dealt with in force layout
    this.createNodes()
    // Create circles for the nodes
    this.renderNodes()
    // ticked function helps to adjust node positions based on what i specifies in force layout template above 
    this.simulation.nodes(this.nodes)
                   .on('tick', this.ticked)
  }

  // When new props (raw data) are received, this fires off
  componentDidUpdate() {
    // Recalculates node positions based on new props
    this.createNodes()
    // Adjust node positions
    this.simulation.nodes(this.nodes)
                   .on('tick', this.ticked)
    // I think this 're-energises' force layout to enable nodes to move to their new positions
    this.simulation.alpha(1).restart()
  }

  charge = (d) => {
    return -Math.pow(d.radius, 2.0) * this.forceStrength;
  }

  createNodes = () => {

    var data = this.props.lapsData.slice()
    ....MORE CODE.....
    this.nodes = nodes

  }

  renderNodes = () => {

    this.bubbles = this.container.selectAll('.bubble')
      .data(this.nodes, d => d.id)
    .enter().append('circle')
      .attr('r', d => d.radius)
      .attr('fill', d => d.color)

  }

  ticked = () => {
    this.bubbles
      .attr('cx', d => d.x)
      .attr('cy', d => d.y)
  }

  render() {
    return (
      <svg width={this.wrapper.width} height={this.wrapper.height}>
        <g transform={"translate(" + (this.axisSpace.width + this.margins.left) + "," + (this.margins.top) + ")"} ref='container' />
      </svg>
    )
  }


}
0 Answers
Related