Skip to content

Instantly share code, notes, and snippets.

@sxywu
Last active August 24, 2017 17:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sxywu/b27228f6e37b45a648c78bc196b0e448 to your computer and use it in GitHub Desktop.
Save sxywu/b27228f6e37b45a648c78bc196b0e448 to your computer and use it in GitHub Desktop.
Updated React+D3, Approach #2
license: mit
function randomData(nodes, width, height) {
var oldNodes = nodes;
// generate some data randomly
nodes = _.chain(_.range(_.random(10, 20)))
.map(() => {
return {
key: _.random(30),
size: _.random(8, 16),
};
}).uniqBy('key').value();
if (oldNodes) {
var end = _.random(oldNodes.length);
var start = _.random(end);
var add = _.slice(oldNodes, start, end + 1);
nodes = _.chain(nodes)
.union(add).uniqBy('key').value();
}
var nodeKeys = _.map(nodes, 'key');
links = _.chain(_.range(_.random(15, 25)))
.map(function() {
var source = nodeKeys[_.random(nodes.length - 1)];
var target = nodeKeys[_.random(nodes.length - 1)];
if (source === target) return;
return {
source,
target,
key: source + ',' + target,
size: _.random(2, 4)
};
}).filter().uniqBy('key').value();
maintainNodePositions(oldNodes, nodes, width, height);
return {nodes, links};
}
function maintainNodePositions(oldNodes, nodes, width, height) {
var kv = {};
_.each(oldNodes, function(d) {
kv[d.key] = d;
});
_.each(nodes, function(d) {
if (kv[d.key]) {
// if the node already exists, maintain current position
d.x = kv[d.key].x;
d.y = kv[d.key].y;
} else {
// else assign it a random position near the center
d.x = width / 2 + _.random(-25, 25);
d.y = height / 2 + _.random(-25, 25);
}
});
}
<meta charset='utf-8'>
<head>
<script src="https://unpkg.com/react@latest/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@latest/dist/react-dom.js"></script>
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<script src='https://unpkg.com/d3@4.10.0'></script>
<script src='https://unpkg.com/lodash@4.17.4'></script>
<script src='generateData.js'></script>
<style>
svg {
width: 400px;
height: 300px;
}
#root {
width: 400px;
text-align: center;
color: #333;
}
.update {
padding: 5px 10px;
margin: 10px;
cursor: pointer;
border: 1px solid #333;
display: inline-block;
}
.node {
fill: #ee8365;
stroke: #fff;
cursor: pointer;
}
.link {
stroke: #ee8365;
stroke-opacity: .5;
}
</style>
</head>
<body>
<div id='root' />
<script type="text/babel">
var width = 400;
var height = 300;
var simulation = d3.forceSimulation()
.force('collide', d3.forceCollide(d => 2 * d.size))
.force('charge', d3.forceManyBody(-100))
.force('center', d3.forceCenter(width / 2, height / 2))
.stop();
class Graph extends React.Component {
constructor(props) {
super(props);
this.state = {selected: null};
this.selectNode = this.selectNode.bind(this);
}
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.version === this.props.version) {
// if version is the same, no updates to data
// so it must be interaction to select+highlight a node
this.calculateHighlights(nextState.selected);
this.circles.attr('opacity', d =>
!nextState.selected || this.highlightedNodes[d.key] ? 1 : 0.2)
this.lines.attr('opacity', d =>
!nextState.selected || this.highlightedLinks[d.key] ? 0.5 : 0.1)
return false;
}
return true;
}
componentDidMount() {
this.container = d3.select(this.refs.container);
this.calculateData();
this.calculateHighlights(this.state.selected);
this.renderLinks();
this.renderNodes();
}
componentDidUpdate() {
this.calculateData();
this.calculateHighlights(this.state.selected);
this.renderLinks();
this.renderNodes();
}
calculateData() {
var {nodes, links} = this.props;
simulation.nodes(nodes)
.force('link', d3.forceLink(links).id(d => d.key).distance(100));
_.times(2000, () => simulation.tick());
this.nodes = nodes;
this.links = links;
}
calculateHighlights(selected) {
this.highlightedNodes = {};
this.highlightedLinks = {};
if (selected) {
this.highlightedNodes[selected] = 1;
_.each(this.links, link => {
if (link.source.key === selected) {
this.highlightedNodes[link.target.key] = 1;
this.highlightedLinks[link.key] = 1;
}
if (link.target.key === selected) {
this.highlightedNodes[link.source.key] = 1;
this.highlightedLinks[link.key] = 1;
}
});
}
}
renderNodes() {
this.circles = this.container.selectAll('circle')
.data(this.nodes, d => d.key);
// exit
this.circles.exit().remove();
// enter + update
this.circles = this.circles.enter().append('circle')
.classed('node', true)
.merge(this.circles)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', d => d.size)
.attr('opacity', d =>
!this.state.selected || this.highlightedNodes[d.key] ? 1 : 0.2)
.on('click', this.selectNode);
}
renderLinks() {
this.lines = this.container.selectAll('line')
.data(this.links, d => d.key);
// exit
this.lines.exit().remove();
// enter + update
this.lines = this.lines.enter().insert('line', 'circle')
.classed('link', true)
.merge(this.lines)
.attr('stroke-width', d => d.size)
.attr('x1', d => d.source.x)
.attr('x2', d => d.target.x)
.attr('y1', d => d.source.y)
.attr('y2', d => d.target.y)
.attr('opacity', d =>
!this.state.selected || this.highlightedLinks[d.key] ? 0.5 : 0.1);
}
selectNode(node) {
if (node.key === this.state.selected) {
this.setState({selected: null});
} else {
this.setState({selected: node.key});
}
}
render() {
return (
<svg ref='container' />
)
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.updateData = this.updateData.bind(this);
this.state = {nodes: [], links: [], version: 0};
}
componentWillMount() {
this.updateData();
}
updateData() {
var {nodes, links} = randomData(this.state.nodes, width, height);
this.setState({nodes, links, version: this.state.version + 1});
}
render() {
return (
<div>
<Graph {...this.state} />
<div className="update" onClick={this.updateData}>update</div>
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment