block by sxywu b27228f6e37b45a648c78bc196b0e448

Updated React+D3, Approach #2

Full Screen

Built with blockbuilder.org

forked from sxywu‘s block: Updated React+D3, Approach #1

index.html

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

generateData.js

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