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>
)
}
}