block by cmgiven a0f58034cea5331a814b30b74aacb8af

Triangular Scatterplot

Full Screen

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

index.html

<!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>

data.csv

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