block by emeeks a28e61eaea2e2ac2567b

The greatest scatterplot

Full Screen

In an attempt to encode as much information on screen at the same time while using as many different kinds of scales and symbols, I have created this scatterplot masterpiece.

This scatterplot uses:

People placed on scatterplot by age and income.

Most of the data here is made up.

index.html

<html>
<head>
  <title>D3 in Action Chapter 6 - Example 1</title>
  <meta charset="utf-8" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="superformula.js" type="text/JavaScript"></script>
<script src="chernoff.js" type="text/JavaScript"></script>
</head>
<style>
  svg {
    height: 850px;
    width: 900px;
    border: 1px solid gray;
  }
  g.am-axis text {
    font-size: 8px;
  }

  .domain {
    fill: none;
  }

  .tick > line{
    stroke: black;
    stroke-width: 1px;
    stroke-opacity: .25;
  }

  g.face > path {
    fill: white;
    stroke: black;
    stroke-width: 1px;
  }

</style>
<body>

<div id="viz">
  <svg>
  </svg>
</div>
Shirt icon from Megan Mitchell via the Noun Project
<div/>
</body>
  <footer>

<script>

var face = d3.chernoff()
    .face(function(d) { return (Math.random() * 0.25) + 0.75 })
    .hair(-1)
    .mouth(function(d) { return happinessScale(d.happiness) })
    .nosew(function(d) { return (Math.random() * 0.25) + 0.75 })
    .noseh(function(d) { return (Math.random() * 0.25) + 0.75 })
    .eyew(function(d) { return (Math.random() * 0.25) + 0.75 })
    .eyeh(function(d) { return (Math.random() * 0.25) + 0.75 })
    .brow(function(d) { return (Math.random() * 0.25) - 0.5 });

dataset = [
  {label: "Susie", happiness: 95, fashion: 20, salary: 95000, age: 22, city: "San Francisco", industry: "Tech", exercise: 90},
  {label: "Randy", happiness: 75, fashion: 1, salary: 15000, age: 7, city: "San Francisco", industry: "Entertainment", exercise: 200},
  {label: "Sheila", happiness: 15, fashion: 750, salary: 45000, age: 41, city: "Rio", industry: "Sales", exercise: 130},
  {label: "Nathan", happiness: 50, fashion: 50, salary: 220000, age: 40, city: "Timbuktu", industry: "Crime", exercise: 300},
  {label: "Porsche", happiness: 1, fashion: 150, salary: 70000, age: 53, city: "Mumbai", industry: "Space Exploration", exercise: 0},
  {label: "Jafar", happiness: 60, fashion: 100, salary: 60000, age: 84, city: "Jakarta", industry: "Administration", exercise: 10},
  {label: "Rigel", happiness: 20, fashion: 100, salary: 50000, age: 74, city: "Beijing", industry: "Education", exercise: 120},
  {label: "Prim", happiness: 30, fashion: 800, salary: 20000, age: 24, city: "San Francisco", industry: "Tech", exercise: 300},
  {label: "Roy", happiness: 50, fashion: 500, salary: 20000, age: 28, city: "Atlanta", industry: "Crime", exercise: 200},
  {label: "Tim", happiness: 5, fashion: 10, salary: 200000, age: 54, city: "New York", industry: "Tech", exercise: 0},
  {label: "Sully", happiness: 100, fashion: 50, salary: 50000, age: 39, city: "Alexandria", industry: "Entertainment", exercise: 400},
  {label: "Hal", happiness: 40, fashion: 110, salary: 150000, age: 29, city: "Jakarta", industry: "Service", exercise: 40},
  {label: "Pip", happiness: 66, fashion: 180, salary: 180000, age: 45, city: "New York", industry: "Sales", exercise: 60},
  {label: "Cicero", happiness: 68, fashion: 660, salary: 90000, age: 80, city: "Mumbai", industry: "Space Exploration", exercise: 80},
  {label: "Leonard", happiness: 43, fashion: 680, salary: 45000, age: 55, city: "Alexandria", industry: "Construction", exercise: 150},
  {label: "Aditi", happiness: 22, fashion: 300, salary: 86000, age: 55, city: "Beijing", industry: "Art", exercise: 10},
  {label: "Jie", happiness: 10, fashion: 25, salary: 110000, age: 18, city: "Atlanta", industry: "Art", exercise: 50},
];

cities = ["San Francisco", "New York", "Atlanta", "Rio", "Beijing", "Paris", "Mumbai", "Timbuktu", "Alexandria", "Jakarta"];

industries = ["Tech", "Service", "Entertainment", "Construction", "Administration", "Sales", "Education", "Art", "Crime", "Space Exploration"]

exerciseValues = dataset.map(function (d) {return d.exercise});
happinessScale = d3.scale.linear().domain([1,100]).range([-1,1]);

ageScale = d3.scale.linear().domain([1,100]).range([0,800]);
fashionScaleN3 = d3.scale.linear().domain([20,200,1000]).range([2,-8,-8]).clamp(true);
fashionScaleN2 = d3.scale.linear().domain([20,200,1000]).range([2,1,-1]).clamp(true);
fashionScaleN1 = d3.scale.linear().domain([20,200,1000]).range([2,0.8,10]).clamp(true);
fashionScaleM = d3.scale.linear().domain([20,200,1000]).range([4,1,8]).clamp(true);
salaryScale = d3.scale.linear().domain([0,250000]).range([800,0]).clamp(true);
cityScale = d3.scale.category20c().domain(cities);
industryScale = d3.scale.category20b().domain(industries);
exerciseScale = d3.scale.quantile().domain(exerciseValues)
  .range(["#d73027","#fc8d59","#fee08b","#ffffbf","#d9ef8b","#91cf60","#1a9850"]);

xAxis = d3.svg.axis().scale(ageScale).orient("bottom").tickSize(800).ticks(4);
d3.select("svg").append("g").attr("id", "xAxisG").call(xAxis);

yAxis = d3.svg.axis().scale(salaryScale).orient("right").ticks(10).tickSize(800).tickSubdivide(10);

d3.select("svg").append("g").attr("id", "yAxisG").call(yAxis);

d3.select("svg").selectAll("g.people").data(dataset)
  .enter()
  .append("g")
  .attr("class", "people");

people = d3.selectAll("g.people");

people
  .attr("transform", function(d) {return "translate(" + (ageScale(d.age) - 21) + "," + (salaryScale(d.salary) - 60) + ")"})

people
  .append("path")
  .style("fill", function (d) {return cityScale(d.city)})
  .attr("transform", "translate(20,48)")
  .attr("d", "m 34.356212,6.8922268 c -0.0033,-0.0074 -0.0056,-0.0122 -0.01064,-0.02037 -0.03993,-0.07659 -0.07171,-0.137708 -0.103488,-0.194748 0.01549,0.02771 0.02852,0.05296 0.044,0.08067 -0.740694,-1.410493 -1.482202,-2.820988 -2.231045,-4.2274097 -1.819547,-3.42071617 -3.626057,-6.8789144 -5.709614,-10.1513306 -3.13878,-4.9306225 -8.874469,-5.7348725 -13.974579,-3.6651695 -3.3107122,1.3428639 -9.0195112,7.5584964 -11.48278516,7.5584964 -2.46246014,0 -7.92680614,-6.2156325 -11.23833284,-7.5584964 -5.080555,-2.06237 -10.766538,-1.246711 -13.891465,3.6651695 -2.084372,3.2724162 -3.890067,6.73061443 -5.712874,10.1513306 -0.767584,1.4447197 -1.528647,2.8926977 -2.289713,4.3406767 -0.456312,0.870254 -1.009591,1.583241 -0.620911,2.596909 0.62417,1.6247992 2.178894,2.9318092 3.574722,3.8827322 1.656579,1.127745 3.678208,2.052594 5.722651,1.99148 0.357718,-0.01142 1.342049,-0.01393 1.575094,-0.330013 0.324308,-0.43594 0.589947,-0.924848 0.870253,-1.390123 1.193749,-1.980073 2.35979,-3.9772562 3.525017,-5.9752542 -1.35264,9.5866442 -2.526016,19.2001762 -3.527461,28.8291912 -0.440829,4.238005 -0.852326,8.4809 -1.131818,12.730312 -0.152384,2.310899 -0.214305,1.654949 -0.09534,2.506462 0.123861,0.875957 1.090262,1.17419 1.86192,1.375457 2.04363,0.532908 14.2980731,1.219822 21.47359184,1.219822 7.16492496,0 19.56441116,-0.686914 21.60804116,-1.219822 0.770028,-0.200452 1.655764,-0.514167 1.861918,-1.375457 0.148301,-0.624986 0.05622,-0.195563 -0.09534,-2.506462 -0.280306,-4.249412 -0.692618,-8.492307 -1.132633,-12.730312 -1.001444,-9.6282 -2.174821,-19.242547 -3.527462,-28.8291912 1.166858,1.997184 2.330456,3.9951812 3.525831,5.9752542 0.281122,0.465275 0.545132,0.953368 0.86781,1.390123 0.235489,0.31616 1.217378,0.318605 1.578353,0.330013 2.044445,0.06112 4.064443,-0.863735 5.721022,-1.99148 1.395829,-0.950923 2.952181,-2.257933 3.576352,-3.8827322 0.386236,-1.005519 -0.154825,-1.715249 -0.611134,-2.575723 z m -58.346109,6.9954382 c -0.361792,0.527204 -2.253045,-0.144232 -4.223338,-1.498498 -1.971108,-1.355087 -3.27486,-2.8796612 -2.913068,-3.4068642 0.363419,-0.52639 2.254674,0.144232 4.224152,1.4984982 1.971108,1.355085 3.274044,2.880474 2.912254,3.406864 z m 44.607836,37.555359 c 0,0.963146 -8.777503,1.556352 -19.64834116,1.554722 -10.87083754,0.0016 -19.64834084,-0.593207 -19.64834084,-1.554722 0,-0.963147 8.7775033,-0.903662 19.64834084,-0.902847 10.86920816,-0.0033 19.64834116,-0.06112 19.64834116,0.902847 z m 10.01688,-39.053857 c -1.970293,1.354271 -3.860732,2.025702 -4.224153,1.498498 -0.360975,-0.52639 0.940331,-2.052592 2.912254,-3.406864 1.970294,-1.3542712 3.860732,-2.0248882 4.223339,-1.4984982 0.362605,0.528019 -0.941147,2.0525922 -2.91144,3.4068642 z");

people
  .append("g")
  .attr("class", "pattern")
  .attr("transform", "translate(5,42)")
  .each(function (d) {
    var superPattern = d3.superformula()
      .param("m", fashionScaleM(d.fashion))
      .param("n1", fashionScaleN1(d.fashion))
      .param("n2", fashionScaleN2(d.fashion))
      .param("n3", fashionScaleN3(d.fashion))
      .param("b", 1)
      .param("a", 1)
      .size(25);
      d3.select(this).selectAll("path.superpattern").data(d3.range(20))
      .enter()
      .append("path")
      .attr("transform", function (d, i) {
        var yPos = (Math.floor(i/5) * 15) + 3;
        var xPos = i%5;
        return "translate(" + (xPos * 8) + "," + (yPos) + ")"; })
      .attr("d", superPattern)
      .style("fill", industryScale(d.industry))
      .style("stroke", industryScale(d.industry))
      .style("stroke-width", 0.5)
      .style("stroke-opacity", 0.5);
  })

  people
  .append("g")
  .attr("class", "face")
  .attr("transform", "scale(.3)")
  .call(face);

  people.select("path.face")
  .style("fill", function (d) {console.log(d, d.exercise, exerciseScale, exerciseScale(d.exercise)); return exerciseScale(d.exercise)})
  .style("stroke", "none");

  people.append("text")
  .attr("y", 65)
  .attr("x", 20)
  .style("text-anchor", "middle")
  .text(function (d) {return d.label})
  .style("stroke", "white")
  .style("stroke-width", "4px")
  .style("stroke-opacity", .85)

  people.append("text")
  .attr("y", 65)
  .attr("x", 20)
  .style("text-anchor", "middle")
  .text(function (d) {return d.label})

</script>
  </footer>

</html>

chernoff.js

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

// Implements Chernoff faces (http://en.wikipedia.org/wiki/Chernoff_face).
// Exposes 8 parameters through functons to control the facial expression.
// face -- shape of the face {0..1}
// hair -- shape of the hair {-1..1}
// mouth -- shape of the mouth {-1..1}
// noseh -- height of the nose {0..1}
// nosew -- width of the nose {0..1}
// eyeh -- height of the eyes {0..1}
// eyew -- width of the eyes {0..1}
// brow -- slant of the brows {-1..1}
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.svg.line()
            .interpolate("cardinal-closed")
            .x(function(d) { return d.x; })
            .y(function(d) { return d.y; }),
        bline = d3.svg.line()
            .interpolate("basis-closed")
            .x(function(d) { return d.x; })
            .y(function(d) { return d.y; });

    function chernoff(a) {
        if(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("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("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("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("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("path")
            .attr("class", "leye")
            .attr("d", bline);
        ele.selectAll("path.reye").data([reye]).enter()
            .append("path")
            .attr("class", "reye")
            .attr("d", bline);

        ele.append("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("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);
};
})();

superformula.js

(function() {
  var _symbol = d3.svg.symbol(),
      _line = d3.svg.line();

  d3.superformula = function() {
    var type = _symbol.type(),
        size = _symbol.size(),
        segments = size,
        params = {};

    function superformula(d, i) {
      var n, p = _superformulaTypes[type.call(this, d, i)];
      for (n in params) p[n] = params[n].call(this, d, i);
      return _superformulaPath(p, segments.call(this, d, i), Math.sqrt(size.call(this, d, i)));
    }

    superformula.type = function(x) {
      if (!arguments.length) return type;
      type = d3.functor(x);
      return superformula;
    };

    superformula.param = function(name, value) {
      if (arguments.length < 2) return params[name];
      params[name] = d3.functor(value);
      return superformula;
    };

    // size of superformula in square pixels
    superformula.size = function(x) {
      if (!arguments.length) return size;
      size = d3.functor(x);
      return superformula;
    };

    // number of discrete line segments
    superformula.segments = function(x) {
      if (!arguments.length) return segments;
      segments = d3.functor(x);
      return superformula;
    };

    return superformula;
  };

  function _superformulaPath(params, n, diameter) {
    var i = -1,
        dt = 2 * Math.PI / n,
        t,
        r = 0,
        x,
        y,
        points = [];

    while (++i < n) {
      t = params.m * (i * dt - Math.PI) / 4;
      t = Math.pow(Math.abs(Math.pow(Math.abs(Math.cos(t) / params.a), params.n2)
        + Math.pow(Math.abs(Math.sin(t) / params.b), params.n3)), -1 / params.n1);
      if (t > r) r = t;
      points.push(t);
    }

    r = diameter * Math.SQRT1_2 / r;
    i = -1; while (++i < n) {
      x = (t = points[i] * r) * Math.cos(i * dt);
      y = t * Math.sin(i * dt);
      points[i] = [Math.abs(x) < 1e-6 ? 0 : x, Math.abs(y) < 1e-6 ? 0 : y];
    }

    return _line(points) + "Z";
  }

  var _superformulaTypes = {
    asterisk: {m: 12, n1: .3, n2: 0, n3: 10, a: 1, b: 1},
    bean: {m: 2, n1: 1, n2: 4, n3: 8, a: 1, b: 1},
    butterfly: {m: 3, n1: 1, n2: 6, n3: 2, a: .6, b: 1},
    circle: {m: 4, n1: 2, n2: 2, n3: 2, a: 1, b: 1},
    clover: {m: 6, n1: .3, n2: 0, n3: 10, a: 1, b: 1},
    cloverFour: {m: 8, n1: 10, n2: -1, n3: -8, a: 1, b: 1},
    cross: {m: 8, n1: 1.3, n2: .01, n3: 8, a: 1, b: 1},
    diamond: {m: 4, n1: 1, n2: 1, n3: 1, a: 1, b: 1},
    drop: {m: 1, n1: .5, n2: .5, n3: .5, a: 1, b: 1},
    ellipse: {m: 4, n1: 2, n2: 2, n3: 2, a: 9, b: 6},
    gear: {m: 19, n1: 100, n2: 50, n3: 50, a: 1, b: 1},
    heart: {m: 1, n1: .8, n2: 1, n3: -8, a: 1, b: .18},
    heptagon: {m: 7, n1: 1000, n2: 400, n3: 400, a: 1, b: 1},
    hexagon: {m: 6, n1: 1000, n2: 400, n3: 400, a: 1, b: 1},
    malteseCross: {m: 8, n1: .9, n2: .1, n3: 100, a: 1, b: 1},
    pentagon: {m: 5, n1: 1000, n2: 600, n3: 600, a: 1, b: 1},
    rectangle: {m: 4, n1: 100, n2: 100, n3: 100, a: 2, b: 1},
    roundedStar: {m: 5, n1: 2, n2: 7, n3: 7, a: 1, b: 1},
    square: {m: 4, n1: 100, n2: 100, n3: 100, a: 1, b: 1},
    star: {m: 5, n1: 30, n2: 100, n3: 100, a: 1, b: 1},
    triangle: {m: 3, n1: 100, n2: 200, n3: 200, a: 1, b: 1}
  };

  d3.superformulaTypes = d3.keys(_superformulaTypes);
})();