Skip to content

Instantly share code, notes, and snippets.

@emeeks
Last active March 17, 2016 02:09
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 emeeks/a28e61eaea2e2ac2567b to your computer and use it in GitHub Desktop.
Save emeeks/a28e61eaea2e2ac2567b to your computer and use it in GitHub Desktop.
The greatest scatterplot

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:

  • Chernoff faces to encode a happiness rating of 1 to 100 (only the mouth encoding, with unhappy people frowning and happy people smiling but the rest of the features are slightly randomized so you don't feel like people all look the same.)
  • Superformula-based patterns to encode a fashion budget between $1-$1000+ per month (people who spend little on fashion wear polka dots)
  • Category20b for industry to color the patterns
  • Category20c for city of residence to color the t-shirt
  • Colorbrewer Green-Yellow-Red (7-value range) based on quantiles of the exercise per day to color the faces

People placed on scatterplot by age and income.

Most of the data here is made up.

(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);
};
})();
<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>
(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);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment