block by micahstubbs 2af44fe03184fe3864d63262d94a2bd9

d3-module-faces | standard notation

Full Screen

an iteration that shows axis tick labels in standard notation.

I found the tickFormat snippet to do this at this stackoverflow answer

along the way, I also fiddled with the width and the annotation placement to get everything inside of the 960px bl.ocks.org frame

an iteration on d3-module-faces from @adamrpearce


Original README.md


To visualize the d3 modules being used, I made a qr. By transforming many different attributes of our dataset into friendly glyphs, Chernoff faces allow us to understand multidimensional datasets. The encoding scheme is probably self explanatory, but I’ve included it below just in case:

'face':  ƒ('dependentsCount')
'hair':  ƒ('description', 'length')
'mouth': ƒ('downloads')
'nosew': ƒ('githubContributers')
'noseh': ƒ('githubIssues')
'eyew':  ƒ('githubStars')
'eyeh':  d => Math.random()
'brow':  ƒ('repoSize')

I used the following modules:

Data is from nprms.io - see download-data.js to your generate your own listing of modules with different data points.

_script.js

var ƒ = d3.f
var width = 928
var height = 500
var faceRadius = 20

d3.loadData(['dense-modules.json', 'annotations.json'], function(err, res){
  d3.select('body').html('').selectAppend('div.tooltip')

  modules = res[0]
  annotations = res[1]

  c = d3.conventions({
    totalWidth: width, 
    totalHeight: height,
    margin: {left: 55, top: 5, bottom: 20, right: 11}
  })

  c.svg.append('text')
    .text('NPM downloads v. Github stars')
    .translate([10, 10])
    .st({fontWeight: 600, fontFamily: 'monospace'})

  chernoff = d3.chernoff()
  d3.entries({
    'face':  ƒ('dependentsCount'),
    'hair':  ƒ('description', 'length'),
    'mouth': ƒ('downloads'),
    'nosew': ƒ('githubContributers'),
    'noseh': ƒ('githubIssues'),
    'eyew':  ƒ('githubStars'),
    'eyeh':  d => Math.random(),
    'brow':  ƒ('repoSize'),      
  }).forEach(function(d){
    var scale = d3.scaleLog().domain(d3.extent(modules, ƒ(d.value, addOne)))
    chernoff[d.key](ƒ(d.value, addOne, scale))
  })

  c.x = d3.scaleLog()
    .domain(d3.extent(modules, ƒ('downloads', addOne)))
    .range(c.x.range())
  c.y = d3.scaleLog()
    .domain(d3.extent(modules, ƒ('githubStars', addOne)))
    .range(c.y.range())

  c.xAxis.scale(c.x)
  c.yAxis.scale(c.y)
  c.drawAxis()


  var simulation = d3.forceSimulation(modules)
    .force('x', d3.forceX(ƒ('downloads', addOne, c.x)).strength(.1))
    .force('y', d3.forceY(ƒ('githubStars', addOne, c.y)).strength(.1))
    .force('collide', d3.forceCollide(faceRadius))
    .force('container', d3.forceContainer([
      [faceRadius, faceRadius], 
      [width - faceRadius*3, height - faceRadius*2.6] ]))

  for (var i = 0; i < 400; ++i) simulation.tick()


  var color = d3.scaleOrdinal(d3.schemeCategory10)

  var moduleSel = c.svg.appendMany(modules, 'g')
    .at({
      fill: ƒ('author', color),
      stroke: ƒ('author', color)
    })
    .translate(d => [d.x, d.y])
    .on('click', d => window.open(d.link, '_blank'))
    .st({cursor: 'pointer'})

  modules.forEach(function(d){
    delete d.x
    delete d.y
    delete d.vx
    delete d.vy
    delete d.index
  })
  moduleSel.call(d3.attachTooltip)

  moduleSel.append('circle').at({r: faceRadius})
  moduleSel.append('g.face')
    .call(chernoff)
    .at({transform: function(d){
      var s = 80/faceRadius
      return ['scale(', 1/s , ') ', 'translate(', -faceRadius*3.5, -faceRadius*4, ')'].join(' ')
    } })


  var swoopy = d3.swoopyDrag()
      .x(d => 0)
      .y(d => 0)
      .draggable(0)
      .annotations(annotations)

  var swoopySel = c.svg
    .append('g.swoopy')
    .call(swoopy)

  swoopySel.selectAll('path').attr('marker-end', 'url(#arrow)')

  c.svg.append('marker')
      .attr('id', 'arrow')
      .attr('viewBox', '-10 -10 20 20')
      .attr('markerWidth', 20)
      .attr('markerHeight', 20)
      .attr('orient', 'auto')
    .append('path')
      .attr('d', 'M-6.75,-6.75 L 0,0 L -6.75,6.75')

  swoopySel.selectAll('text')
    .each(function(d){
      d3.select(this)
        .text('')                       
        .tspans(d3.wordwrap(d.text, 22))
    })  

  // d3.select('g.swoopy').selectAll('g')
  //   .attr('transform', 'translate(0,-20)');

})


function addOne(d){ return d + 1 }

index.html

<!DOCTYPE html>
<meta charset='utf-8'>
<link rel="stylesheet" type="text/css" href="style.css">
<link rel="icon" href="data:;base64,iVBORw0KGgo=">

<body></body>

<script src='d3v4.js'></script>
<script src='chernoff.js'></script>
<script src='container-force.js'></script>
<script src='swoopy-drag.js'></script>
<script src='_script.js'></script>

annotations.json

[
  {
    "path": "M 718,410 A 49.96 49.96 0 0 1 715,324",
    "text": "Modules Mike included in the default d3 build have the most stars and downloads",
    "textOffset": [
      732,
      408
    ]
  },
  {
    "path": "M 747,9 A 114.218 114.218 0 0 1 841,7",
    "text": "The most downloaded, d3-queue, predates d3v4",
    "textOffset": [
      623,
      23
    ]
  },
  {
    "path": "M 432,75 A 58.454 58.454 0 0 0 529,92",
    "text": "Susie's legend generator is the most popular module not in the default build",
    "textOffset": [
      331,
      35
    ]
  },
  {
    "path": "M 432,367 A 50.011 50.011 0 0 1 377,304",
    "text": "All three of Peter's modules have about the same number of downloads",
    "textOffset": [
      445,
      359
    ]
  },
  {
    "path": "M 121,284 A 100.224 100.224 0 0 0 47,189",
    "text": "I just put swoopy-drag and force-container on npm",
    "textOffset": [
      9,
      311
    ]
  },
  {
    "path": "M 121,341 A 94.103 94.103 0 0 1 48.999996185302734,432.0000305175781",
    "text": "",
    "textOffset": [
      38,
      310
    ]
  }
]

chernoff.js

function sign(num) {
    if(num > 0) {
        return 1;
    } else if(num < 0) {
        return -1;
    } else {
        return 0;
    }
}

function d3_chernoff() {
    var facef = 0.5, // 0 - 1
        hairf = 0, // -1 - 1
        mouthf = 0, // -1 - 1
        nosehf = 0.5, // 0 - 1
        nosewf = 0.5, // 0 - 1
        eyehf = 0.5, // 0 - 1
        eyewf = 0.5, // 0 - 1
        browf = 0, // -1 - 1

        line = d3.line()
            .curve(d3.curveCardinalClosed)
            .x(function(d) { return d.x; })
            .y(function(d) { return d.y; }),
        bline = d3.line()
            .curve(d3.curveBasisClosed)
            .x(function(d) { return d.x; })
            .y(function(d) { return d.y; });

    function chernoff(a) {
        if(true || a instanceof Array) {
            a.each(__chernoff);
        } else {
            d3.select(this).each(__chernoff);
        }
    }

    function __chernoff(d) {
        var ele = d3.select(this),
            facevar = (typeof(facef) === "function" ? facef(d) : facef) * 30,
            hairvar = (typeof(hairf) === "function" ? hairf(d) : hairf) * 80,
            mouthvar = (typeof(mouthf) === "function" ? mouthf(d) : mouthf) * 7,
            nosehvar = (typeof(nosehf) === "function" ? nosehf(d) : nosehf) * 10,
            nosewvar = (typeof(nosewf) === "function" ? nosewf(d) : nosewf) * 10,
            eyehvar = (typeof(eyehf) === "function" ? eyehf(d) : eyehf) * 10,
            eyewvar = (typeof(eyewf) === "function" ? eyewf(d) : eyewf) * 10,
            browvar = (typeof(browf) === "function" ? browf(d) : browf) * 3;

        var face = [{x: 70, y: 60}, {x: 120, y: 80},
                    {x: 120-facevar, y: 110}, {x: 120-facevar, y: 160},
                    {x: 20+facevar, y: 160}, {x: 20+facevar, y: 110},
                    {x: 20, y: 80}];
        ele.selectAll("path.face").data([face]).enter()
            .append("svg:path")
            .attr("class", "face")
            .attr("d", bline);

        var hair = [{x: 70, y: 60}, {x: 120, y: 80},
                    {x: 140, y: 45-hairvar}, {x: 120, y: 45},
                    {x: 70, y: 30}, {x: 20, y: 45},
                    {x: 0, y: 45-hairvar}, {x: 20, y: 80}];
        ele.selectAll("path.hair").data([hair]).enter()
            .append("svg:path")
            .attr("class", "hair")
            .attr("d", bline);

        var mouth = [{x: 70, y: 130+mouthvar},
                     {x: 110-facevar, y: 135-mouthvar},
                     {x: 70, y: 140+mouthvar},
                     {x: 30+facevar, y: 135-mouthvar}];
        ele.selectAll("path.mouth").data([mouth]).enter()
            .append("svg:path")
            .attr("class", "mouth")
            .attr("d", line);

        var nose = [{x: 70, y: 110-nosehvar},
                    {x: 70+nosewvar, y: 110+nosehvar},
                    {x: 70-nosewvar, y: 110+nosehvar}];
        ele.selectAll("path.nose").data([nose]).enter()
            .append("svg:path")
            .attr("class", "nose")
            .attr("d", line);

        var leye = [{x: 55, y: 90-eyehvar}, {x: 55+eyewvar, y: 90},
                    {x: 55, y: 90+eyehvar}, {x: 55-eyewvar, y: 90}];
        var reye = [{x: 85, y: 90-eyehvar}, {x: 85+eyewvar, y: 90},
                    {x: 85, y: 90+eyehvar}, {x: 85-eyewvar, y: 90}];
        ele.selectAll("path.leye").data([leye]).enter()
            .append("svg:path")
            .attr("class", "leye")
            .attr("d", bline);
        ele.selectAll("path.reye").data([reye]).enter()
            .append("svg:path")
            .attr("class", "reye")
            .attr("d", bline);

        ele.append("svg:path")
            .attr("class", "lbrow")
            .attr("d", "M" + (55-eyewvar/1.7-sign(browvar)) + "," +
                       (87-eyehvar+browvar) + " " +
                       (55+eyewvar/1.7-sign(browvar)) + "," +
                       (87-eyehvar-browvar));
        ele.append("svg:path")
            .attr("class", "rbrow")
            .attr("d", "M" + (85-eyewvar/1.7+sign(browvar)) + "," +
                       (87-eyehvar-browvar) + " " +
                       (85+eyewvar/1.7+sign(browvar)) + "," +
                       (87-eyehvar+browvar));
    }

    chernoff.face = function(x) {
        if(!arguments.length) return facef;
        facef = x;
        return chernoff;
    };

    chernoff.hair = function(x) {
        if(!arguments.length) return hairf;
        hairf = x;
        return chernoff;
    };

    chernoff.mouth = function(x) {
        if(!arguments.length) return mouthf;
        mouthf = x;
        return chernoff;
    };

    chernoff.noseh = function(x) {
        if(!arguments.length) return nosehf;
        nosehf = x;
        return chernoff;
    };

    chernoff.nosew = function(x) {
        if(!arguments.length) return nosewf;
        nosewf = x;
        return chernoff;
    };

    chernoff.eyeh = function(x) {
        if(!arguments.length) return eyehf;
        eyehf = x;
        return chernoff;
    };

    chernoff.eyew = function(x) {
        if(!arguments.length) return eyewf;
        eyewf = x;
        return chernoff;
    };

    chernoff.brow = function(x) {
        if(!arguments.length) return browf;
        browf = x;
        return chernoff;
    };

    return chernoff;
}

d3.chernoff = function() {
    return d3_chernoff(Object);
};

container-force.js

(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 forceContainer (bbox){
    var nodes, strength = 1;;

    if (!bbox || bbox.length < 2) bbox = [[0, 0], [100, 100]]


    function force(alpha) {
      var i,
          n = nodes.length,
          node,
          x = 0,
          y = 0;

      for (i = 0; i < n; ++i) {
        node = nodes[i], x = node.x, y = node.y;

        if (x < bbox[0][0]) node.vx += (bbox[0][0] - x)*alpha
        if (y < bbox[0][1]) node.vy += (bbox[0][1] - y)*alpha
        if (x > bbox[1][0]) node.vx += (bbox[1][0] - x)*alpha       
        if (y > bbox[1][1]) node.vy += (bbox[1][1] - y)*alpha
      }
    }

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

    force.bbox = function(_){
      return arguments.length ? (bbox = +_, force) : bbox;
    };
    force.strength = function(_){
      return arguments.length ? (strength = +_, force) : strength;
    }

    return force;
  }

  exports.forceContainer = forceContainer;

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

}));

download-data.js

var request = require('sync-request');
var fs = require('fs');


var res = request('GET', 'https://api.npms.io/v2/search?from=0&q=keywords%3Ad3-module&size=100');
var modules = JSON.parse(res.getBody().toString('utf-8')).results

modules.forEach(function(d, i){
  console.log(i, d.package.name)
  var res = request('GET', 'https://api.npms.io/v2/package/' + d.package.name)
  d.metadata = JSON.parse(res.getBody().toString('utf-8'))
})

fs.writeFileSync('modules.json', JSON.stringify(modules))


var modules = JSON.parse(fs.readFileSync('modules.json'))

var denseModules = modules
  .filter(d => d.metadata.collected.github)
  .map(function(d){
    var rv = {}
    rv.author = d.package.author ? d.package.author.name : 'none'
    rv.name   = d.package.name
    rv.description = d.metadata.collected.metadata.description
    rv.githubStars = d.metadata.collected.github.starsCount
    rv.githubIssues = d.metadata.collected.github.issues.count
    rv.githubContributers = d.metadata.collected.github.contributors ? d.metadata.collected.github.contributors.length : 1
    rv.downloads = Math.ceil(d.metadata.evaluation.popularity.downloadsCount)
    rv.dependentsCount = d.metadata.collected.npm.dependentsCount
    rv.npmStars = d.metadata.collected.npm.starsCount
    rv.repoSize = d.metadata.collected.source.repositorySize
    rv.link = d.package.links.repository
    return rv  
  })

fs.writeFileSync('dense-modules.json', JSON.stringify(denseModules))

package.json

{
  "name": "d3-modules",
  "version": "1.0.0",
  "description": "An interactive demonstration of [d3-voronoi](https://github.com/d3/d3-voronoi), showing the Delaunay triangulation.",
  "main": "d3v4.js",
  "dependencies": {
    "d3": "^4.2.7",
    "request-sync": "^1.0.0",
    "sync-request": "^3.0.1"
  },
  "devDependencies": {},
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

style.css

text {
  font: 12px sans-serif;
  /*pointer-events: none;*/
  text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
}
.axis line,
.axis path {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}
div.tooltip {
  top: -1000px;
  position: absolute;
  padding: 10px;
  background: rgba(255, 255, 255, .90);
  border: 1px solid lightgray;
  pointer-events: none;
  width: 300px;
  font-family: monospace;
  font-family: 10px;
  opacity: 1;
}
div.tooltip-hidden{
  opacity: 0;
  transition: all .3s;
}


svg .axis text{
  font-family: monospace;
}
svg{
  overflow: visible;
  font-family: monospace;
}

body{
  margin: 0px;
  overflow: hidden;
}

circle{
  fill-opacity: .05;
  stroke-width: .1;
}

.face{
  fill: none;
  stroke-width: 3;
}

.swoopy path{
  fill: none;
  stroke-width: .5;
  stroke: black;
}

.swoopy circle{
  stroke-width: 1;
}

swoopy-drag.js

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

  function swoopyDrag(){
    var x = function(d){ return d }
    var y = function(d){ return d }

    var annotations = []
    var annotationSel

    var draggable = false

    var dispatch = d3.dispatch('drag')

    var textDrag = d3.drag()
        .on('drag', function(d){
          var x = d3.event.x
          var y = d3.event.y
          d.textOffset = [x, y].map(Math.round)

          d3.select(this).call(translate, d.textOffset)

          dispatch.call('drag')
        })
        .subject(function(d){ return {x: d.textOffset[0], y: d.textOffset[1]} })

    var circleDrag = d3.drag()
        .on('drag', function(d){
          var x = d3.event.x
          var y = d3.event.y
          d.pos = [x, y].map(Math.round)

          var parentSel = d3.select(this.parentNode)

          var path = ''
          var points = parentSel.selectAll('circle').data()
          if (points[0].type == 'A'){
            path = calcCirclePath(points)
          } else{
            points.forEach(function(d){ path = path + d.type  + d.pos })
          }

          parentSel.select('path').attr('d', path).datum().path = path
          d3.select(this).call(translate, d.pos)

          dispatch.call('drag')
        })
        .subject(function(d){ return {x: d.pos[0], y: d.pos[1]} })


    var rv = function(sel){
      annotationSel = sel.html('').selectAll('g')
          .data(annotations).enter()
        .append('g')
          .call(translate, function(d){ return [x(d), y(d)] })

      var textSel = annotationSel.append('text')
          .call(translate, ƒ('textOffset'))
          .text(ƒ('text'))

      annotationSel.append('path')
          .attr('d', ƒ('path'))

      if (!draggable) return

      annotationSel.style('cursor', 'pointer')
      textSel.call(textDrag)

      annotationSel.selectAll('circle').data(function(d){
        var points = []

        if (~d.path.indexOf('A')){
          //handle arc paths seperatly -- only one circle supported
          var pathNode = d3.select(this).select('path').node()
          var l = pathNode.getTotalLength()

          points = [0, .5, 1].map(function(d){
            var p = pathNode.getPointAtLength(d*l)
            return {pos: [p.x, p.y], type: 'A'}
          })
        } else{
          var i = 1
          var type = 'M'
          var commas = 0

          for (var j = 1; j < d.path.length; j++){
            var curChar = d.path[j]
            if (curChar == ',') commas++
            if (curChar == 'L' || curChar == 'C' || commas == 2){
              points.push({pos: d.path.slice(i, j).split(','), type: type})
              type = curChar
              i = j + 1
              commas = 0
            }
          }

          points.push({pos: d.path.slice(i, j).split(','), type: type})
        }

        return points
      }).enter().append('circle')
          .attr('r', 8)
          .attr('fill', 'rgba(0,0,0,0)')
          .attr('stroke', '#333')
          .attr('stroke-dasharray', '2 2')
          .call(translate, ƒ('pos'))
          .call(circleDrag)

      dispatch.call('drag')
    }


    rv.annotations = function(_x){
      if (typeof(_x) == 'undefined') return annotations
      annotations = _x
      return rv
    }
    rv.x = function(_x){
      if (typeof(_x) == 'undefined') return x
      x = _x
      return rv
    }
    rv.y = function(_x){
      if (typeof(_x) == 'undefined') return y
      y = _x
      return rv
    }
    rv.draggable = function(_x){
      if (typeof(_x) == 'undefined') return draggable
      draggable = _x
      return rv
    }
    rv.on = function() {
      var value = dispatch.on.apply(dispatch, arguments);
      return value === dispatch ? rv : value;
    }

    return rv

    //convert 3 points to an Arc Path 
    function calcCirclePath(points){
      var a = points[0].pos
      var b = points[2].pos
      var c = points[1].pos

      var A = dist(b, c)
      var B = dist(c, a)
      var C = dist(a, b)

      var angle = Math.acos((A*A + B*B - C*C)/(2*A*B))
      
      //calc radius of circle
      var K = .5*A*B*Math.sin(angle)
      var r = A*B*C/4/K
      r = Math.round(r*1000)/1000

      //large arc flag
      var laf = +(Math.PI/2 > angle)

      //sweep flag
      var saf = +((b[0] - a[0])*(c[1] - a[1]) - (b[1] - a[1])*(c[0] - a[0]) < 0) 

      return ['M', a, 'A', r, r, 0, laf, saf, b].join(' ')
    }

    function dist(a, b){
      return Math.sqrt(
        Math.pow(a[0] - b[0], 2) +
        Math.pow(a[1] - b[1], 2))
    }


    //no jetpack dependency 
    function translate(sel, pos){
      sel.attr('transform', function(d){
        var posStr = typeof(pos) == 'function' ? pos(d) : pos
        return 'translate(' + posStr + ')' 
      }) 
    }

    function ƒ(str){ return function(d){ return d[str] } } 
  }

  exports.swoopyDrag = swoopyDrag;

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

}));