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.
<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() {
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);
};
})();
(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);
})();