Closed jeff1evesque closed 6 years ago
I adjusted my code to use animateFauxDom
:
import React from 'react'
import * as d3 from 'd3'
import {withFauxDOM} from 'react-faux-dom'
class MyReactComponent extends React.Component {
componentDidMount() {
const w = window.innerWidth;
const h = window.innerHeight;
const faux = this.props.connectFauxDOM('svg', 'collision');
let nodes = d3.range(200).map(function () {
return { r: Math.random() * 12 + 4 };
});
let root = nodes[0];
let color = d3.scaleOrdinal().range(d3.schemeCategory10);
root.radius = 0;
root.fixed = true;
const forceX = d3.forceX(w / 2).strength(0.015);
const forceY = d3.forceY(h / 2).strength(0.015);
var svg = d3.select(faux)
.attr('width', w)
.attr('height', h)
.append('g');
svg.selectAll('circle')
.data(nodes.slice(1))
.enter()
.append('circle')
.attr('r', function (d) { return d.r; })
.style('fill', function (d, i) { return color(i % 3); });
function ticked(e) {
svg.selectAll('circle')
.attr('cx', function (d) { return d.x; })
.attr('cy', function (d) { return d.y; });
};
let force = d3.forceSimulation()
.velocityDecay(0.2)
.force('x', forceX)
.force('y', forceY)
.force('collide', d3.forceCollide().radius(function (d) {
if (d === root) {
return Math.random() * 50 + 100;
}
return d.r + 2;
}).iterations(5))
.nodes(nodes).on('tick', ticked);
svg.on('mousemove', function () {
root.fx = d3.event.pageX;
root.fy = d3.event.pageY;
force.alphaTarget(0.3).restart();//reheat the simulation
this.props.animateFauxDOM(3500);
});
this.props.animateFauxDOM(3500);
}
render() {
return (
<div>{this.props.collision}</div>
)
}
}
MyReactComponent.defaultProps = {
collision: 'loading'
}
export default withFauxDOM(MyReactComponent)
However, it seems to only last for a few seconds before displaying an Uncaught TypeError
:
Got the animation to work without implementing react-faux-dom:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import * as d3 from 'd3';
class AnimateCollisions extends React.Component {
constructor() {
super();
const nodes = this.generateNodes(200);
this.state = {
colors: d3.scaleOrdinal().range(d3.schemeCategory10),
nodes: nodes,
root: nodes[0],
width: window.innerWidth,
height: window.innerHeight,
alpha_target: .4,
iterations: 4,
velocity_decay: .1,
forceX: d3.forceX(window.innerWidth / 2).strength(0.015),
forceY: d3.forceY(window.innerHeight / 2).strength(0.015),
}
this.getColor = this.getColor.bind(this);
this.generateNodes = this.generateNodes.bind(this);
this.storeForce = this.storeForce.bind(this);
this.renderD3 = this.renderD3.bind(this);
}
componentDidMount() {
this.renderD3();
}
generateNodes(range_limit) {
return [...Array(range_limit).keys()].map(function() {
return { r: Math.random() * 12 + 4 };
});
}
storeForce(force) {
this.setState({force: force});
}
getColor(i) {
return this.state.colors(i % 3);
}
renderD3() {
const nodes = this.state.nodes;
const forceX = this.state.forceX;
const forceY = this.state.forceY;
const root = nodes[0];
const svg = d3.select(ReactDOM.findDOMNode(this.refs.animation));
const alpha = this.state.alpha_target;
const iterations = this.state.iterations;
root.radius = 0;
root.fixed = true;
svg.selectAll('circle')
.data(nodes.slice(1))
.enter();
function ticked(e) {
svg.selectAll('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
};
svg.on('mousemove', function() {
const p1 = d3.mouse(this);
root.fx = p1[0];
root.fy = p1[1];
force.alphaTarget(alpha).restart();//reheat the simulation
});
let force = d3.forceSimulation()
.velocityDecay(this.state.velocity_decay)
.force('x', forceX)
.force('y', forceY)
.force('collide', d3.forceCollide().radius(function(d) {
if (d === root) {
return Math.random() * 50 + 100;
}
return d.r + 2;
}).iterations(iterations))
.nodes(nodes).on('tick', ticked);
this.storeForce(force);
}
render() {
// use React to draw all the nodes, d3 calculates the x and y
const nodes = this.state.nodes.slice(1).map((node, index) => {
const color = this.getColor(index);
return (
<circle
fill={color}
cx={node.x}
cy={node.y}
r={node.r}
key={`circle-${index}`}
/>
);
});
return (
<svg width={this.state.width} height={this.state.height}>
<g ref='animation'>{nodes}</g>
</svg>
)
}
}
export default AnimateCollisions;
This is my first D3v4 attempt. If anyone has advice on how to make my animation better, or smoother, please let me know:
I'm glad you got it working and I recommend just using plain D3 if you want to use v4 + complex animations. I'm not sure what was wrong though, it should render fine. Whenever you start using animation or physics you'll run into issues because D3 mutates the actual DOM pretty heavily. This is better for simple visualisations. Okay to close this now?
Got it. Sure, can close. Have one question, if you don't mind, and able to answer. My animation (first D3 attempt) has some kind of memory leak in react. When I toggle between the homepage, where the animation resides, and another page (i.e. /login
), via the main menu, and do this many times, the animation begins to slow down noticeably. Is there some kind of approach, to reclaim some kind of event when deconstructing the component, perhaps in the ComponentWillUnmount
? Sorry, for the question. Been on IRC, and since this is my first D3 attempt, haven't had much luck.
Oh wow, that's nasty. As far as I know, D3 doesn't hold references, everything is stored on the DOM itself. If the GC isn't picking up your DOM nodes I'd imagine it's because you're holding onto a reference somewhere?
I noticed if I comment out force.alphaTarget(alpha).restart();
, the animation doesn't degrade, between toggling of multiple pages, and back to the homepage where the animation resides. However, the collision aspect of the animation, with the mouseover, no longer works. But, the beginning part of the animation no longer shows degregation, no matter how many times I toggle between 2+ react pages.
I don't think I'll be able to help you here, that sounds like a fairly intricate D3 issue. First rule out React, try this on a standalone page and flick it in and out of existence. Then if that works fine you know it's how you're creating / destroying the elements with React.
I see you're using the constructor in the class? Maybe you want to use "on mount" instead? There are posts on how to embed D3 in React like this, it might be worth checking those for any traps you're falling into that are well known.
Had to implement this.state.forceSimulation.stop()
, within componentWillUnmount
. Should have taken a closer look into d3's documentation earlier this week.
I have the following
ReactFauxDom
implementation:It renders as follows:
But, when I change the above's
d3.select(fauxRoot).append('svg')
tod3.select('body').append('svg')
, it incorrectly renders, duplicatedsvg
's, at the bottom of the<body>
:The following is what I'm trying to get it to look like:
Note: the original source code was written with d3js (version 3). It's update doesn't have a nice effect, that the
gravity
callback provided. More specifically, when the mouse approaches the aggregated circles, they should collectively get repelled. This is less so, with the adjusted jsfiddle's version 4 update.