block by rpgove e66f1d921718813ecf507178c7dc8d12

EuroVis 2019 Twitter interaction network

Full Screen

This node-link diagram shows the Twitter users who used the #EuroVis hashtag during EuroVis 2019 (thanks to John Alexis Guerra Gómez for collecting the data!). If a user retweeted, replied, or quoted another user’s tweet, there is a link connecting them. Nodes with darker colors had more #EuroVis tweets.

This example shows how to combine the Random Vertex Sampling algorithm from d3.forceManyBodySampled() with the Barnes-Hut algorithm from d3.forceManyBody(). The example first computes a fast layout using Random Vertex sampling, and then runs 10 itereations of the Barnes-Hut algorithm to refine the layout.

More information about the algorithm is available in the blog post.

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<style>

body {
  background: white;
  font: 12px sans-serif;
}

.d3-tip strong {
  color: #fafafa;
}

.d3-tip {
  line-height: 1;
  font-weight: normal;
  padding: 8px;
  background: rgba(0, 0, 0, 0.8);
  color: #eee;
  border-radius: 2px;
  pointer-events: none !important;
}

/* Creates a small triangle extender for the tooltip */
.d3-tip:after {
  box-sizing: border-box;
  display: inline;
  font-size: 10px;
  width: 100%;
  line-height: 1;
  color: rgba(0, 0, 0, 0.8);
  position: absolute;
  pointer-events: none;
}

/* Northward tooltips */
.d3-tip.n:after {
  content: "\25BC";
  margin: -1px 0 0 0;
  top: 100%;
  left: 0;
  text-align: center;
}

.links line {
  stroke: #999;
  stroke-opacity: 0.4;
}

.nodes circle {
  stroke: #333;
  stroke-width: 2px;
}

</style>
<svg></svg>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="d3-tip.js"></script>
<script src="d3-force-sampled.js"></script>
<script>

var width = 960;
var height = 600;
var nodeRadius = 6;
var nodeColor = d3.scaleSequential().interpolator(d3.interpolateMagma);
var linkWidth = d3.scaleLinear().range([1, 2 * nodeRadius]);

var svg = d3.select('svg')
  .attr('width', width)
  .attr('height', height)
  .append('g')
  .attr('transform', 'scale(0.5)');

var nodeTip = d3.tip()
  .attr('class', 'd3-tip')
  .offset([-10, 0])
  .html(function(d) {
    return '<strong>'
      + d.screen_name
      + '</strong> had <strong>'
      + d.tweet_count
      + (d.tweet_count === 1 ? '</strong> tweet' : '</strong> tweets');
  });

var linkTip = d3.tip()
  .attr('class', 'd3-tip')
  .offset(function() {
    return [this.getBBox().height / 4 - 10, 0];
  })
  .html(function(d) {
    return '<p style="width:200px;"><strong>'
      + d.source.screen_name
      + '</strong> and <strong>'
      + d.target.screen_name
      + '</strong> retweeted, replied, or quoted each other <strong>'
      + d.interact_count
      + (d.interact_count === 1 ? '</strong> time' : '</strong> times')
      + '</p>';
  });

svg.call(nodeTip);
svg.call(linkTip);

var linkForce = d3.forceLink()
  .id(function(d) { return d.id; });

var forceSim = d3.forceSimulation()
  .velocityDecay(0.2)
  .force('link', linkForce)
  .force('charge', d3.forceManyBodySampled())
  .force('forceX', d3.forceX().strength(0.015))
  .force('forceY', d3.forceY().strength(0.015 * width / height))
  .force('center', d3.forceCenter(width, height));

d3.json('Eurovis2019_rt_network.json').then(function (graph) {
  var drag = d3.drag()
    .on('drag', dragging);

  var nodes = graph.nodes.reduce(function (userMap, d) {
    var user;
    if (userMap.has(d.user.screen_name)) {
      user = userMap.get(d.user.screen_name);
      ++user.tweet_count;
      user.retweet_count += d.retweet_count;
      user.favorite_count += d.favorite_count;
    } else {
      user = {
        favorite_count: d.favorite_count,
        retweet_count: d.retweet_count,
        screen_name: d.user.screen_name,
        tweet_count: 1
      };
      userMap.set(user.screen_name, user);
    }
    return userMap;
  }, d3.map());

  var tweetsToUsers = graph.nodes.reduce(function (tweetMap, d) {
    tweetMap.set(d.id, d.user.screen_name);
    return tweetMap;
  }, d3.map());

  var links = graph.links.reduce(function (interactMap, d) {
    var link;
    var sourceUser = nodes.get(tweetsToUsers.get(+d.source));
    var targetUser = nodes.get(tweetsToUsers.get(+d.target));

    if (sourceUser && targetUser && sourceUser !== targetUser) {
      var link;

      if (interactMap.has(sourceUser.screen_name + '-' + targetUser.screen_name))
        ++interactMap.get(sourceUser.screen_name + '-' + targetUser.screen_name).interact_count;
      else if (interactMap.has(targetUser.screen_name + '-' + sourceUser.screen_name))
        ++interactMap.get(targetUser.screen_name + '-' + sourceUser.screen_name).interact_count;
      else
        interactMap.set(sourceUser.screen_name + '-' + targetUser.screen_name, {source: sourceUser, target: targetUser, interact_count: 1});
    }

    return interactMap;
  }, d3.map());

  nodes = nodes.values();
  links = links.values();

  // Make sure small nodes are drawn on top of larger nodes
  nodes.sort(function (a, b) { return b.tweet_count - a.tweet_count; });

  nodeColor.domain([d3.max(nodes, function (d) { return d.tweet_count; }), 0]);
  linkWidth.domain([0, d3.max(links, function (d) { return d.interact_count; })]);

  var link = svg.append('g')
    .attr('class', 'links')
    .selectAll('line')
    .data(links)
    .enter().append('line')
    .attr('stroke-width', function (d) { return linkWidth(d.interact_count); })
    .on('mouseover', linkTip.show)
    .on('mouseout', linkTip.hide);

  var node = svg.append('g')
    .attr('class', 'nodes')
    .selectAll('circle')
    .data(nodes)
    .enter().append('circle')
    .attr('r', nodeRadius)
    .attr('fill', function (d) { return nodeColor(d.tweet_count); })
    .call(drag)
    .on('mouseover', nodeTip.show)
    .on('mouseout', nodeTip.hide);

  forceSim.nodes(nodes)
    .on('tick', draw)
    .stop();

  forceSim.force('link').links(links);

  for (var t = 100; t > 0; --t) forceSim.tick();

  forceSim.velocityDecay(0.4)
    .force('charge', d3.forceManyBody());

  for (var t = 10; t > 0; --t) forceSim.tick();

  draw();

  function draw () {
    link
      .attr('x1', function (d) { return d.source.x; })
      .attr('x2', function (d) { return d.target.x; })
      .attr('y1', function (d) { return d.source.y; })
      .attr('y2', function (d) { return d.target.y; });

    node
      .attr('cx', function (d) { return d.x; })
      .attr('cy', function (d) { return d.y; });
  }

  function dragging (d) {
    d.x = d3.event.x;
    d.y = d3.event.y;
    draw();
    nodeTip.hide();
  }
});

</script>

d3-force-sampled.js

// Copyright 2019 Two Six Labs, LLC. v1.0.0 d3-force-sampled https://github.com/twosixlabs/d3-force-sampled/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(factory((global.d3 = global.d3 || {})));
}(this, (function (exports) { 'use strict';

function constant(x) {
  return function() {
    return x;
  };
}

function manyBodySampled() {
  var nodes,
      alpha,
      strength = constant(-30),
      strengths,
      indicesRepulse,
      prevIndex = 0,
      distanceMin2 = 1,
      distanceMax2 = Infinity,
      neighborSize = function () {
        return 15;
      },
      updateSize = function (nodes) { return Math.pow(nodes.length, 0.75); },
      sampleSize = function (nodes) { return Math.pow(nodes.length, 0.25); },
      numNeighbors,
      numUpdate,
      numSamples,
      chargeMultiplier = function (nodes) {
        return nodes.length < 100 ? 1 : nodes.length < 200 ? 3 : Math.sqrt(nodes.length);
      },
      cMult,
      rand = Math.random;

  function addRandomNode(node) {
    var randIdx = Math.floor(rand() * nodes.length),
        randNode = nodes[randIdx],
        randDist = (node.x - randNode.x) * (node.x - randNode.x) + (node.y - randNode.y) * (node.y - randNode.y),
        currIdx,
        currNode,
        currDist,
        maxI,
        maxDist = -Infinity,
        i = -1;

    // Is this already in the list?
    if (node.nearest.indexOf(randIdx) >= 0) return;

    // If there is room for another, add it.
    if (node.nearest.length < numNeighbors) {
      node.nearest.push(randIdx);
      return;
    }

    // Replace the farthest away "neighbor" with the new node.
    while (++i < node.nearest.length) {
      currIdx = node.nearest[i];
      currNode = nodes[currIdx];
      currDist = Math.hypot(node.x - currNode.x, node.y - currNode.y);
      if (currDist > maxDist) {
        maxI = i;
        maxDist = currDist;
      }
    }

    if (randDist < maxDist) {
      node.nearest[maxI] = randIdx;
    }
  }

  function getRandIndices(indices, num) {
    num = Math.floor(num);
    var i,
        n = nodes.length,
        cnt = n - num,
        randIdx,
        temp;

    // Choose random indices.
    for (i = n-1; i >= cnt; --i) {
      randIdx = Math.floor(rand() * i);
      temp = indices[randIdx];
      indices[randIdx] = indices[i];
      indices[i] = temp;
    }

    return indices.slice(cnt);
  }

  function approxRepulse(node) {
    var i,
        randIndices,
        currNode,
        w,
        x,
        y,
        l;

    // Choose random nodes to update.
    randIndices = getRandIndices(indicesRepulse, numSamples);

    for (i = randIndices.length - 1; i >= 0; --i) {
      currNode = nodes[randIndices[i]];

      if (currNode === node) continue;

      x = currNode.x - node.x;
      y = currNode.y - node.y;
      l = x * x + y * y;

      if (l >= distanceMax2) continue;

      // Limit forces for very close nodes; randomize direction if coincident.
      if (x === 0) x = (rand() - 0.5) * 1e-6, l += x * x;
      if (y === 0) y = (rand() - 0.5) * 1e-6, l += y * y;
      if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);

      w = strengths[node.index] * alpha * cMult / l;
      node.vx += x * w;
      node.vy += y * w;
    }
  }

  function constantRepulse(node) {
    var i,
        nearest,
        currNode,
        w,
        x,
        y,
        l;

    // Update the list of nearest nodes.
    if (numNeighbors) addRandomNode(node);

    nearest = node.nearest;

    if (numNeighbors) for (i = nearest.length - 1; i >= 0; --i) {
      currNode = nodes[nearest[i]];

      if (currNode === node) continue;

      x = currNode.x - node.x;
      y = currNode.y - node.y;
      l = x * x + y * y;

      if (l >= distanceMax2) continue;

      // Limit forces for very close nodes; randomize direction if coincident.
      if (x === 0) x = (rand() - 0.5) * 1e-6, l += x * x;
      if (y === 0) y = (rand() - 0.5) * 1e-6, l += y * y;
      if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);

      w = strengths[node.index] * alpha * cMult / l;
      node.vx += x * w;
      node.vy += y * w;
    }
  }

  function force(_) {
    var i = 0, j = prevIndex, n = nodes.length, upperIndex = prevIndex + numUpdate;
    for (alpha = _; i < n || j < upperIndex; ++i, ++j) {
      if (j < upperIndex) approxRepulse(nodes[j%n]);
      if (numNeighbors && i < n) constantRepulse(nodes[i]);
    }
    prevIndex = upperIndex % n;
  }

  function initialize() {
    if (!nodes) return;
    var i, n = nodes.length, node;
    indicesRepulse = new Array(n);
    for (i = 0; i < n; ++i) indicesRepulse[i] = i;
    strengths = new Array(n);
    
    // Cannot be negative.
    numNeighbors = Math.min(Math.ceil(neighborSize(nodes)), n);
    numNeighbors = numNeighbors < 0 ? 0 : Math.min(numNeighbors, nodes.length);
    numUpdate = Math.ceil(updateSize(nodes));
    numUpdate = numUpdate < 0 ? 0 : Math.min(numUpdate, n);
    numSamples = Math.ceil(sampleSize(nodes));
    numSamples = numSamples < 0 ? 0 : Math.min(numSamples, n);

    cMult = chargeMultiplier(nodes);

    alpha = 1;
    for (i = 0; i < n; ++i) {
      node = nodes[i];
      strengths[node.index] = +strength(node, i, nodes);
      node.nearest = [];
      while (node.nearest.length < numNeighbors) addRandomNode(node);
    }
  }

  force.initialize = function(_) {
    nodes = _;
    initialize();
  };

  force.strength = function(_) {
    return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength;
  };

  force.distanceMin = function(_) {
    return arguments.length ? (distanceMin2 = _ * _, force) : Math.sqrt(distanceMin2);
  };

  force.distanceMax = function(_) {
    return arguments.length ? (distanceMax2 = _ * _, force) : Math.sqrt(distanceMax2);
  };

  force.neighborSize = function(_) {
    return arguments.length ? (neighborSize = typeof _ === "function" ? _ : constant(+_), initialize(), force) : neighborSize;
  };

  force.updateSize = function(_) {
    return arguments.length ? (updateSize = typeof _ === "function" ? _ : constant(+_), initialize(), force) : updateSize;
  };

  force.sampleSize = function(_) {
    return arguments.length ? (sampleSize = typeof _ === "function" ? _ : constant(+_), initialize(), force) : sampleSize;
  };

  force.chargeMultiplier = function(_) {
    return arguments.length ? (chargeMultiplier = typeof _ === "function" ? _ : constant(+_), initialize(), force) : chargeMultiplier;
  };

  force.source = function(_) {
    return arguments.length ? (rand = _, force) : rand;
  };

  return force;
}

exports.forceManyBodySampled = manyBodySampled;

Object.defineProperty(exports, '__esModule', { value: true });

})));

d3-tip.js

(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('d3-collection'), require('d3-selection')) :
  typeof define === 'function' && define.amd ? define(['d3-collection', 'd3-selection'], factory) :
  (global.d3 = global.d3 || {}, global.d3.tip = factory(global.d3,global.d3));
}(this, (function (d3Collection,d3Selection) { 'use strict';

  /**
   * d3.tip
   * Copyright (c) 2013-2017 Justin Palmer
   *
   * Tooltips for d3.js SVG visualizations
   */
  // Public - constructs a new tooltip
  //
  // Returns a tip
  function index() {
    var direction   = d3TipDirection,
        offset      = d3TipOffset,
        html        = d3TipHTML,
        rootElement = document.body,
        node        = initNode(),
        svg         = null,
        point       = null,
        target      = null;

    function tip(vis) {
      svg = getSVGNode(vis);
      if (!svg) return
      point = svg.createSVGPoint();
      rootElement.appendChild(node);
    }

    // Public - show the tooltip on the screen
    //
    // Returns a tip
    tip.show = function() {
      var args = Array.prototype.slice.call(arguments);
      if (args[args.length - 1] instanceof SVGElement) target = args.pop();

      var content = html.apply(this, args),
          poffset = offset.apply(this, args),
          dir     = direction.apply(this, args),
          nodel   = getNodeEl(),
          i       = directions.length,
          coords,
          scrollTop  = document.documentElement.scrollTop ||
        rootElement.scrollTop,
          scrollLeft = document.documentElement.scrollLeft ||
        rootElement.scrollLeft;

      nodel.html(content)
        .style('opacity', 1).style('pointer-events', 'all');

      while (i--) nodel.classed(directions[i], false);
      coords = directionCallbacks.get(dir).apply(this);
      nodel.classed(dir, true)
        .style('top', (coords.top + poffset[0]) + scrollTop + 'px')
        .style('left', (coords.left + poffset[1]) + scrollLeft + 'px');

      return tip
    };

    // Public - hide the tooltip
    //
    // Returns a tip
    tip.hide = function() {
      var nodel = getNodeEl();
      nodel.style('opacity', 0).style('pointer-events', 'none');
      return tip
    };

    // Public: Proxy attr calls to the d3 tip container.
    // Sets or gets attribute value.
    //
    // n - name of the attribute
    // v - value of the attribute
    //
    // Returns tip or attribute value
    // eslint-disable-next-line no-unused-vars
    tip.attr = function(n, v) {
      if (arguments.length < 2 && typeof n === 'string') {
        return getNodeEl().attr(n)
      }

      var args =  Array.prototype.slice.call(arguments);
      d3Selection.selection.prototype.attr.apply(getNodeEl(), args);
      return tip
    };

    // Public: Proxy style calls to the d3 tip container.
    // Sets or gets a style value.
    //
    // n - name of the property
    // v - value of the property
    //
    // Returns tip or style property value
    // eslint-disable-next-line no-unused-vars
    tip.style = function(n, v) {
      if (arguments.length < 2 && typeof n === 'string') {
        return getNodeEl().style(n)
      }

      var args = Array.prototype.slice.call(arguments);
      d3Selection.selection.prototype.style.apply(getNodeEl(), args);
      return tip
    };

    // Public: Set or get the direction of the tooltip
    //
    // v - One of n(north), s(south), e(east), or w(west), nw(northwest),
    //     sw(southwest), ne(northeast) or se(southeast)
    //
    // Returns tip or direction
    tip.direction = function(v) {
      if (!arguments.length) return direction
      direction = v == null ? v : functor(v);

      return tip
    };

    // Public: Sets or gets the offset of the tip
    //
    // v - Array of [x, y] offset
    //
    // Returns offset or
    tip.offset = function(v) {
      if (!arguments.length) return offset
      offset = v == null ? v : functor(v);

      return tip
    };

    // Public: sets or gets the html value of the tooltip
    //
    // v - String value of the tip
    //
    // Returns html value or tip
    tip.html = function(v) {
      if (!arguments.length) return html
      html = v == null ? v : functor(v);

      return tip
    };

    // Public: sets or gets the root element anchor of the tooltip
    //
    // v - root element of the tooltip
    //
    // Returns root node of tip
    tip.rootElement = function(v) {
      if (!arguments.length) return rootElement
      rootElement = v == null ? v : functor(v);

      return tip
    };

    // Public: destroys the tooltip and removes it from the DOM
    //
    // Returns a tip
    tip.destroy = function() {
      if (node) {
        getNodeEl().remove();
        node = null;
      }
      return tip
    };

    function d3TipDirection() { return 'n' }
    function d3TipOffset() { return [0, 0] }
    function d3TipHTML() { return ' ' }

    var directionCallbacks = d3Collection.map({
          n:  directionNorth,
          s:  directionSouth,
          e:  directionEast,
          w:  directionWest,
          nw: directionNorthWest,
          ne: directionNorthEast,
          sw: directionSouthWest,
          se: directionSouthEast
        }),
        directions = directionCallbacks.keys();

    function directionNorth() {
      var bbox = getScreenBBox(this);
      return {
        top:  bbox.n.y - node.offsetHeight,
        left: bbox.n.x - node.offsetWidth / 2
      }
    }

    function directionSouth() {
      var bbox = getScreenBBox(this);
      return {
        top:  bbox.s.y,
        left: bbox.s.x - node.offsetWidth / 2
      }
    }

    function directionEast() {
      var bbox = getScreenBBox(this);
      return {
        top:  bbox.e.y - node.offsetHeight / 2,
        left: bbox.e.x
      }
    }

    function directionWest() {
      var bbox = getScreenBBox(this);
      return {
        top:  bbox.w.y - node.offsetHeight / 2,
        left: bbox.w.x - node.offsetWidth
      }
    }

    function directionNorthWest() {
      var bbox = getScreenBBox(this);
      return {
        top:  bbox.nw.y - node.offsetHeight,
        left: bbox.nw.x - node.offsetWidth
      }
    }

    function directionNorthEast() {
      var bbox = getScreenBBox(this);
      return {
        top:  bbox.ne.y - node.offsetHeight,
        left: bbox.ne.x
      }
    }

    function directionSouthWest() {
      var bbox = getScreenBBox(this);
      return {
        top:  bbox.sw.y,
        left: bbox.sw.x - node.offsetWidth
      }
    }

    function directionSouthEast() {
      var bbox = getScreenBBox(this);
      return {
        top:  bbox.se.y,
        left: bbox.se.x
      }
    }

    function initNode() {
      var div = d3Selection.select(document.createElement('div'));
      div
        .style('position', 'absolute')
        .style('top', 0)
        .style('opacity', 0)
        .style('pointer-events', 'none')
        .style('box-sizing', 'border-box');

      return div.node()
    }

    function getSVGNode(element) {
      var svgNode = element.node();
      if (!svgNode) return null
      if (svgNode.tagName.toLowerCase() === 'svg') return svgNode
      return svgNode.ownerSVGElement
    }

    function getNodeEl() {
      if (node == null) {
        node = initNode();
        // re-add node to DOM
        rootElement.appendChild(node);
      }
      return d3Selection.select(node)
    }

    // Private - gets the screen coordinates of a shape
    //
    // Given a shape on the screen, will return an SVGPoint for the directions
    // n(north), s(south), e(east), w(west), ne(northeast), se(southeast),
    // nw(northwest), sw(southwest).
    //
    //    +-+-+
    //    |   |
    //    +   +
    //    |   |
    //    +-+-+
    //
    // Returns an Object {n, s, e, w, nw, sw, ne, se}
    function getScreenBBox(targetShape) {
      var targetel   = target || targetShape;

      while (targetel.getScreenCTM == null && targetel.parentNode != null) {
        targetel = targetel.parentNode;
      }

      var bbox       = {},
          matrix     = targetel.getScreenCTM(),
          tbbox      = targetel.getBBox(),
          width      = tbbox.width,
          height     = tbbox.height,
          x          = tbbox.x,
          y          = tbbox.y;

      point.x = x;
      point.y = y;
      bbox.nw = point.matrixTransform(matrix);
      point.x += width;
      bbox.ne = point.matrixTransform(matrix);
      point.y += height;
      bbox.se = point.matrixTransform(matrix);
      point.x -= width;
      bbox.sw = point.matrixTransform(matrix);
      point.y -= height / 2;
      bbox.w = point.matrixTransform(matrix);
      point.x += width;
      bbox.e = point.matrixTransform(matrix);
      point.x -= width / 2;
      point.y -= height / 2;
      bbox.n = point.matrixTransform(matrix);
      point.y += height;
      bbox.s = point.matrixTransform(matrix);

      return bbox
    }

    // Private - replace D3JS 3.X d3.functor() function
    function functor(v) {
      return typeof v === 'function' ? v : function() {
        return v
      }
    }

    return tip
  }

  return index;

})));