Skip to content

Instantly share code, notes, and snippets.

@cmgiven
Created August 10, 2016 15:53
Show Gist options
  • Save cmgiven/a0f58034cea5331a814b30b74aacb8af to your computer and use it in GitHub Desktop.
Save cmgiven/a0f58034cea5331a814b30b74aacb8af to your computer and use it in GitHub Desktop.
Triangular Scatterplot
license: mit
height: 800

Block-a-Day #4. French departments (an administrative division that contains multiple districts) are positioned on a triangular plot according to the percentage of their labor force in three categories: I. agriculture, II. industry, and III. commerce, transportation, and services. From an idea by Jacques Bertin.

Click on a category or arrow to reorient the chart.

Data Sources: Jacques Bertin, Semiology of Graphics, p. 100

What I Learned: Rotation transforms never do what you expect on the first try. Actually, I knew that one already.

What I'd Do With More Time: I'm sure there's a way to get the triangle to rotate smoothly around its center, but I just ran out of time to figure it out. If you know, ping me @cmgiven.

Block-a-Day

Just what it sounds like. For fifteen days, I will make a D3.js v4 block every single day. Rules:

  1. Ideas over implementation. Do something novel, don't sweat the details.
  2. No more than two hours can be spent on coding (give or take).
  3. Every. Single. Day.

Previously

department i ii iii
AIN 67 43 40
AISNE 56 71 66
ALLIER 65 45 57
Bses ALPES 15 8 12
Htes ALPES 16 8 13
ALPES Mmes 31 61 122
ARDECHE 48 32 25
ARDENNES 25 53 35
ARIEGE 33 17 14
AUBE 28 48 36
AUDE 50 20 32
AVEYRON 70 32 29
BOUCHES DU RH. 42 143 226
CALVADOS 70 55 69
CANTAL 45 13 20
CHARENTE 65 36 38
CHARENTE Mme 79 39 65
CHER 43 41 36
CORREZE 64 23 29
COTE D'OR 43 41 59
COTES DU NORD 131 35 62
CREUSE 58 13 17
DORDOGNE 104 34 41
DOUBS 35 67 39
DROME 46 38 35
EURE 48 52 45
EURE & LOIR 44 27 38
FINISTERE 164 76 89
GARD 40 51 52
HAUTE GARONNE 64 67 84
GERS 63 10 16
GIRONDE 115 107 170
HERAULT 62 40 71
ILLE & V. 137 60 82
INDRE 54 30 32
INDRE & L. 61 41 55
ISERE 68 136 78
JURA 39 34 27
LANDES 70 25 28
LOIR & CHER 51 27 30
LOIRE 56 160 82
Hte LOIRE 52 23 22
LOIRE INF. 101 108 105
LOIRET 51 51 54
LOT 41 10 16
LOT & GAR. 70 24 30
LOZERE 22 5 7
MAINE & L. 104 65 65
MANCHE 116 42 56
MARNE 44 57 67
Hte MARNE 25 28 28
MAYENNE 74 23 28
MEURTHE & M. 23 127 91
MEUSE 24 31 27
MORBIHAN 132 47 59
MOSELLE 36 173 94
NIEVRE 34 27 33
NORD 81 483 296
OISE 40 69 55
ORNE 65 30 35
P.D.C. 94 242 137
PUY DE DOME 80 79 63
Bses PYRENEES 80 49 62
Htes PYRENEES 37 27 28
PYRENEES ORIENT. 35 20 33
BAS-RHIN 76 122 114
Ht-RHIN 40 121 74
RHONE 44 215 194
Hte SAONE 34 32 23
SAONE & L. 94 77 62
SARTHE 87 45 58
SAVOIE 44 38 35
Hte SAVOIE 52 42 45
PARIS 2 575 940
SEINE 8 574 550
SEINE INF. 75 152 174
SEINE & M. 37 72 76
SEINE & O. 46 328 356
DEUX-SEVRES 71 29 33
SOMME 57 68 61
TARN 55 47 33
TARN & G. 44 13 18
VAR 33 50 81
VAUCLUSE 40 30 41
VENDEE 110 38 40
VIENNE 60 29 39
Hte VIENNE 64 47 45
VOSGES 36 95 43
YONNE 41 28 37
BELFORT 3 25 13
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.axis .domain { display: none; }
line.grid {
stroke: #999;
}
line.dash {
stroke: #333;
stroke-width: .5;
}
text.label {
text-transform: uppercase;
font-family: monospace;
font-weight: 700;
font-size: 24px;
letter-spacing: 0.1;
}
path.arrow, text.label {
fill: #333;
cursor: pointer;
}
circle {
fill: rgba(100,75,170,.8);
stroke: rgb(100,75,170);
stroke-width: 1;
}
</style>
<body>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
var margin = { top: 50, bottom: 200 }
var width = 960
var height = 800 - margin.top - margin.bottom
var side = height * 2 / Math.sqrt(3)
var svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + ((width - side) / 2) + ',' + (margin.top + 0.5) + ')')
.append('g')
var sideScale = d3.scaleLinear()
.domain([0, 1])
.range([0, side])
var perpScale = d3.scaleLinear()
.domain([0, 1])
.range([height, 0])
var r = d3.scaleSqrt().range([0, 10])
var axis = d3.axisLeft()
.scale(perpScale)
.tickFormat(function (n) { return (n * 100).toFixed(0) })
.tickSize(side * -0.3)
.tickPadding(5)
var axes = svg.selectAll('.axis')
.data(['i', 'ii', 'iii'])
.enter().append('g')
.attr('class', function (d) { return 'axis ' + d })
.attr('transform', function (d) {
return d === 'iii' ? ''
: 'rotate(' + (d === 'i' ? 240 : 120) + ',' + (side * 0.5) + ',' + (height / 3 * 2) + ')'
})
.call(axis)
axes.selectAll('line')
.attr('class', 'dash')
.attr('transform', 'translate(' + (side * 0.2) + ',0)')
.attr('stroke-dasharray', '9,7')
.attr('y1', 0)
.attr('y2', 0)
axes.selectAll('text')
.attr('transform', 'translate(' + (side * 0.2) + ',-5)')
axes.selectAll('.tick')
.append('line')
.attr('class', 'grid')
.attr('x1', function (d) { return side * (d * 0.5) })
.attr('x2', function (d) { return side * (-d * 0.5 + 1) })
.attr('y1', 0)
.attr('y2', 0)
axes.append('path')
.attr('class', 'arrow')
.attr('d', 'M0 0 L5 9 L2 9 L2 15 L-2 15 L-2 9 L-5 9 Z')
.attr('transform', 'translate(' + (side * 0.5) + ',10)')
.on('click', rotate)
axes.append('text')
.attr('class', 'label')
.attr('x', side * 0.5)
.attr('y', -6)
.attr('text-anchor', 'middle')
.attr('letter-spacing', '-8px')
.text(function (d) { return d })
.on('click', rotate)
function rotate(d) {
var angle = d === 'i' ? 120 : d === 'ii' ? 240 : 0
svg.transition().duration(600)
.attr('transform', 'rotate(' + angle + ',' + (side / 2) + ',' + (height / 3 * 2) + ')')
}
d3.csv('data.csv', function (d) {
var i = +d.i
var ii = +d.ii
var iii = +d.iii
var total = i + ii + iii
var iShare = i / total
var iiShare = ii / total
var iiiShare = iii / total
return {
department: d.department,
total: total,
i: i,
ii: ii,
iii: iii,
iShare: iShare,
iiShare: iiShare,
iiiShare: iiiShare,
x: iiShare + (iiiShare * 0.5)
}
}, function (error, data) {
if (error) { throw error }
r.domain([0, d3.max(data, function (d) { return d.total })])
svg.selectAll('.point')
.data(data)
.enter().append('circle')
.attr('class', 'point')
.attr('r', function (d) { return r(d.total) })
.attr('cx', function (d) { return sideScale(d.x) })
.attr('cy', function (d) { return perpScale(d.iiiShare) })
.append('title')
.text(function (d) { return d.department })
})
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment