Trivariate choropleth map from the Pop vs Soda Page
CC-BY Alan McConchie (@mappingmashups)
The original code is here (and it's really really old)
Built with blockbuilder.org
forked from almccon's block: Pop vs Soda trivariate choropleth
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="//d3js.org/queue.v1.min.js"></script>
<script src="//d3js.org/topojson.v0.min.js"></script>
<style type="text/css">
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
.landshadow {
fill: none;
stroke: #ccc;
stroke-width: 4px;
stroke-linejoin: round;
}
.states {
fill: none;
stroke: #fff;
stroke-linejoin: round;
}
.counties {
fill: #fff;
}
#map-container {
height: 500px;
width: 1500px;
text-align: center;
position: relative;
}
#d3map {
display: block;
position: absolute;
width: 100%;
height: 100%;
margin: 0;
}
</style>
</head>
<body>
<div id="map-container">
<!-- the D3 map will be rendered here -->
<svg id="d3map"></svg>
</div>
<div id="tabs">
<ul>
<li>
<a href="#tabs-1">Cyan/Magenta/Yellow</a>
</li>
<li>
<a href="#tabs-2">Red/Green/Blue</a>
</li>
<li>
<a href="#tabs-3">Black and White</a>
</li>
</ul>
<div id="tabs-1">
<ul>
<li id="cmykbutton"> show all </li>
<li id="cmykpopbutton"> pop </li>
<li id="cmyksodabutton"> soda </li>
<li id="cmykcokebutton"> coke </li>
<li id="cmykotherbutton"> other </li>
</ul>
</div>
<div id="tabs-2">
<ul>
<li id="rgbbutton"> show all </li>
<li id="rgbpopbutton"> pop </li>
<li id="rgbsodabutton"> soda </li>
<li id="rgbcokebutton"> coke </li>
<li id="rgbotherbutton"> other </li>
</ul>
</div>
<div id="tabs-3">
<ul>
<li class="not_avail"> show all </li>
<li id="bwpopbutton"> pop </li>
<li id="bwsodabutton"> soda </li>
<li id="bwcokebutton"> coke </li>
<li id="bwotherbutton"> other </li>
</ul>
</div>
</div>
<script type="application/javascript">
// Create some scales for univariate modes
var bwscale = d3.scale.linear()
.domain([0, 1])
.range(["white", "black"]);
var cmykpop = d3.scale.linear()
.domain([0, 1])
.range(["white", "yellow"]);
var cmyksoda = d3.scale.linear()
.domain([0, 1])
.range(["white", "cyan"]);
var cmykcoke = d3.scale.linear()
.domain([0, 1])
.range(["white", "magenta"]);
var rgbpop = d3.scale.linear()
.domain([0, 1])
.range(["white", "lime"]); // Note, #OOFFOO is not "green"
var rgbsoda = d3.scale.linear()
.domain([0, 1])
.range(["white", "blue"]);
var rgbcoke = d3.scale.linear()
.domain([0, 1])
.range(["white", "red"]);
// Ideally, I would rewrite these as a d3.scale.trivariate() plugin.
// Although, this is a misnomer for two reasons:
// First, the three different variables are not independent.
// Second, the PVS encodes other (a 4th possible value)
// as black, in both the "rgb" and "cmyk" modes.
function cmyk(d) {
if (d.PCTPOP == 0 && d.PCTSODA == 0 && d.PCTCOKE == 0 && d.PCTOTHER == 0) {
return "white";
} else {
return "rgb("
+ Math.round((d.PCTPOP + d.PCTCOKE) * 100) + "%,"
+ Math.round((d.PCTSODA + d.PCTPOP) * 100) + "%,"
+ Math.round((d.PCTCOKE + d.PCTSODA) * 100) + "%)";
}
}
function rgb(d) {
if (d.PCTPOP == 0 && d.PCTSODA == 0 && d.PCTCOKE == 0 && d.PCTOTHER == 0) {
return "white";
} else {
return "rgb("
+ Math.round(d.PCTCOKE * 100) + "%,"
+ Math.round(d.PCTPOP * 100) + "%,"
+ Math.round(d.PCTSODA * 100) + "%)";
}
}
var path = d3.geo.path()
.projection(d3.geo.albersUsa().translate([400,250]).scale(800));
var svg = d3.select("#d3map");
// The d3_hexbinAngles and hexagon function are (obviously)
// borrowed from https://github.com/d3/d3-plugins/tree/master/hexbin
var d3_hexbinAngles = d3.range(0, 2 * Math.PI, Math.PI / 3);
function hexagon(radius) {
if (arguments.length < 1) {
// This is where I should calculate the correct radius
// based on the number of divisions in the overall triangle... but then
// the hexagon would have to be linked to the legend
// somehow.
radius = 8;
}
var x0 = 0, y0 = 0; // create shape centered at 0,0
var points = d3_hexbinAngles.map(function(angle) {
var x1 = Math.sin(angle) * radius,
y1 = -Math.cos(angle) * radius,
dx = x1 - x0,
dy = y1 - y0;
// I'm not sure why this line is in the hexbin code,
// but it was breaking my hexagons, and they work fine
// without it:
//x0 = x1, y0 = y1;
return [dx, dy];
});
return "M" + points.join("L") + "z";
}
function rectangle(height, width) {
if (arguments.length < 2) {
// This is where I should calculate the correct size
// based on the number of division in the overall triangle... but then
// the rectangle would have to be linked to the legend
// somehow.
width = 8;
height = 8;
}
halfwidth = width / 2;
halfheight = height / 2;
var x0 = 0, y0 = 0; // create shape centered at 0,0
var points = [
[x0 - halfwidth, y0 - halfheight],
[x0 + halfwidth, y0 - halfheight],
[x0 + halfwidth, y0 + halfheight],
[x0 - halfwidth, y0 + halfheight]
];
return "M" + points.join("L") + "z";
}
function triangle(x, y, height) {
// Create the svg text representation of an equilateral triangle.
// Unlike the shapes above, do not create this centered on 0,0.
// The x and y are the coordinates of the top of the triangle.
x = x;
y = y;
dy = height + 1; // 1 pixel padding so shapes overlap a little
dx = dy / Math.sqrt(3);
var points = [
[x, y],
[x + dx, y + dy],
[x - dx, y + dy]
];
return "M" + points.join("L") + "z";
}
function upsidedown_triangle(x, y, height) {
// Create the svg text representation of an equilateral triangle.
// Unlike the shapes above, do not create this centered on 0,0.
// The x and y are the coordinates of the center of the top of the triangle.
x = x;
y = y;
dy = height;
dx = dy / Math.sqrt(3);
var points = [
[x - dx, y],
[x + dx, y],
[x, y + dy]
];
return "M" + points.join("L") + "z";
}
function trivariate_legend(startx, starty, width, count) {
// Create a one-dimensional array storing the elements
// of a trivariate legend (represented as an equilateral triangle).
// Returns the array, suitable for joining with a svg.
// Note: count is # of divisions. # of swatches will be count + 1.
// startx and starty are the upper left corner of the triangle's
// bounding box. Width is the width of the triangle's bounding box.
// (Technically, the bounding box of the swatch centers.)
// The x and y coordinates of the center of each swatch is stored
// in each array element. The caller of this function is responsible
// for creating svg symbols at those points (circles, hexagons, etc)
make_triangle = true;
array = new Array();
id = 0;
xstep = width / count;
ystep = Math.sqrt(3) * xstep / 2;
// Create the rows
for (var i = 0; i <= count; i++) {
// Create the columns
// (1st column has 1 row, 2nd column has 2 rows, etc)
for (var j = 0; j <= i; j++ ) {
x = startx + j * xstep + width / 2 - i * xstep / 2;
y = starty + i * ystep;
shape = triangle(x, y, ystep);
array.push(
{
x: x,
y: y,
i: i,
j: j,
PCTSODA: (count - i) / count,
PCTPOP: (i - j) / count,
PCTCOKE: (j) / count,
PCTOTHER: 0,
id: id++,
shp: shape
}
);
if (j > 0) {
x = startx + j * xstep + width / 2 - i * xstep / 2 - xstep / 2;
y = starty + i * ystep;
shape = upsidedown_triangle(x, y, ystep);
array.push(
{
x: x,
y: y,
i: i,
j: j,
PCTSODA: (count - i) / count,
PCTPOP: (i - j + 0.5) / count,
PCTCOKE: (j - 0.5) / count,
PCTOTHER: 0,
id: id++,
shp: shape
}
);
}
}
}
return array;
}
function univariate_legend(startx, starty, height, width, count) {
// Create a one-dimensional array storing the elements
// of a univariate legend (represented as a series of adjoining squares).
// Returns the array, suitable for joining with a svg.
// Note: count is # of divisions. # of swatches will be count + 1.
// startx and starty are the upper left corner of the legend's
// bounding box. Width is the width of the legend's bounding box.
// The x and y coordinates of the center of each swatch is stored
// in each array element. The caller of this function is responsible
// for creating svg symbols at those points (squares, etc)
//
// The more appropriate d3/svg method for a univariate legend
// (I think) would be to create a line with the appropriate
// thickness to give the impression of a row of squares. But
// here I want the squares to be consistent with the color
// swatches in the trivariate legend.
array = new Array();
id = 0;
xstep = width / count;
// Create the rows
for (var i = 0; i <= count; i++) {
array.push(
{
x: startx + i * xstep,
y: starty,
i: i,
PCTSODA: 0,
PCTPOP: 0,
PCTCOKE: 0,
PCTOTHER: i / count,
id: id++
}
);
}
return array;
}
// Parameters for the trivariate legend
var x = 660,
y = 400,
width = 60,
count = 10;
tri_legend_g = svg.append("g")
.attr("class", "legend");
// Add a drop shadow for the trivariate legend
tri_legend_g.append("path")
// These calculations are a complete mess because I am calculating all my triangles
// in different ways. Need to make these more coherent.
.attr("d", triangle(x + width / 2, y, 1 + Math.sqrt(3) * (width + count / 2) / 2))
.attr("class", "landshadow");
tri_legend_data = trivariate_legend(x, y, width, count);
// Select (and create) swatch class.
// Do this instead of selectAll("path") to avoid binding data to the shadow path
tri_legend_svg = tri_legend_g.selectAll(".swatch")
.data(tri_legend_data)
.enter().append("path")
.attr("d", function(d) { return d.shp })
.style("fill", function(d) {
return cmyk(d);
})
var spacing = 10;
corner_labels = [
{
label: "pop",
x: x-spacing,
y: y+width+spacing
},
{
label: "soda",
x: x+width/2,
y: y-spacing
},
{
label: "coke",
x: x+width+spacing,
y: y+width+spacing
}
];
tri_legend_g.selectAll("text")
.data(corner_labels)
.enter().append("text")
.attr("text-anchor", "middle")
.attr("x", function(d) { return d.x })
.attr("y", function(d) { return d.y })
.attr("font-size", "10px")
.attr("transform", function(d) { return "translate(" + -56 + "," + -50 + ")" }) // why are they shifted?
.text(function(d) { return d.label });
// Parameters for the univariate legend
var x = 550,
y = 452,
width = 60,
height = 10,
count = 20;
uni_legend_g = svg.append("g")
.attr("class", "legend");
// Add a drop shadow for the univariate legend
uni_legend_g.append("path")
.attr("transform", "translate(" + ( x + width / 2 ) + "," + y + ")")
.attr("d", rectangle( 1 + height, 2 + width ))
.attr("class", "landshadow");
uni_legend_data = univariate_legend(x, y, height, width, count);
// Select (and create) swatch class.
// Do this instead of selectAll("path") to avoid binding data to the shadow path
uni_legend_svg = uni_legend_g.selectAll(".swatch")
.data(uni_legend_data)
.enter().append("path")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")" })
// Add 1 just to pad the hexagons a bit so they blend
.attr("d", rectangle( 1 + height, 1 + width/count ))
.style("fill", function(d) {
return bwscale(d.PCTOTHER);
});
uni_labels = [
{
label: "other",
x: x+width/2,
y: y+spacing+10
}
];
uni_legend_label = uni_legend_g.selectAll("text")
.data(uni_labels)
.enter().append("text")
.attr("text-anchor", "middle")
.attr("x", function(d) { return d.x })
.attr("y", function(d) { return d.y })
.attr("font-size", "10px")
.text(function(d) { return d.label });
// Parameters for the no data swatch
var x = 490,
y = 452,
width = 20,
height = 9;
nodata_legend_g = svg.append("g")
.attr("class", "legend")
// Add a drop shadow for the no data swatch
nodata_legend_g.append("path")
.attr("transform", "translate(" + ( x + width / 2 ) + "," + y + ")")
.attr("d", rectangle( 1 + height, 2 + width ))
.attr("class", "landshadow");
nodata_legend_data = new Array(
{
PCTSODA: 0,
PCTPOP: 0,
PCTCOKE: 0,
PCTOTHER: 0
}
);
// Select (and create) swatch class.
// Do this instead of selectAll("path") to avoid binding data to the shadow path
nodata_legend_svg = nodata_legend_g.selectAll(".swatch")
.data(nodata_legend_data)
.enter().append("path")
.attr("transform", "translate(" + ( x + width / 2 ) + "," + y + ")")
.attr("d", rectangle( 1 + height, 2 + width ))
.style("fill", function(d) {
return cmyk(d);
});
nodata_labels = [
{
label: "no data",
x: x+width/2,
y: y+spacing+10
}
];
nodata_legend_g.selectAll("text")
.data(nodata_labels)
.enter().append("text")
.attr("text-anchor", "middle")
.attr("x", function(d) { return d.x })
.attr("y", function(d) { return d.y })
.attr("font-size", "10px")
.text(function(d) { return d.label });
// Done creating legend
// Load the TopoJSON of states and the tsv of popvssoda data in parallel
queue()
.defer(d3.json, "us.json")
.defer(d3.tsv, "pvscounty_fips.tsv")
.await(ready);
function ready(error, us, pvscounty_fips) {
pvscounty_fips.forEach(function(d) {
d.PCTPOP = +d.PCTPOP;
d.PCTSODA = +d.PCTSODA;
d.PCTCOKE = +d.PCTCOKE;
d.PCTOTHER = +d.PCTOTHER;
});
// Extract just the land from the us TopoJSON (resulting in a GeoJSON)
// and load that as the datum into a new SVG path. This will be the
// background shadow.
svg.append("path").datum(topojson.object(us, us.objects.land))
.attr("class", "landshadow")
.attr("d", path);
var counties = svg.append("g")
.attr("class", "counties")
.selectAll("path")
.data(topojson.object(us, us.objects.counties).geometries)
.enter().append("path")
.attr("d", path)
.attr("class", "addhover");
// Step through all the TopoJSON objects and add the pvs attributes
// to the correct one.
// I'm not sure if the "each" function is the ideal approach, but it works.
counties.each(function(d, i) {
var d = this.__data__;
for (var j = 0; j < pvscounty_fips.length; j++) {
var p = pvscounty_fips[j];
if (d.id == p.id) {
// Copy the data values into the TopoJSON
// Must be a more efficient way of doing this.
d.PCTPOP = p.PCTPOP;
d.PCTSODA = p.PCTSODA;
d.PCTCOKE = p.PCTCOKE;
d.PCTOTHER = p.PCTOTHER;
d.County_Name = p.County_Name;
d.ENGTYPE = p.ENGTYPE;
d.State = p.State;
d.SUMPOP = p.SUMPOP;
d.SUMSODA = p.SUMSODA;
d.SUMCOKE = p.SUMCOKE;
d.SUMOTHER = p.SUMOTHER;
if (p.PCTPOP == 0 && p.PCTSODA == 0 && p.PCTCOKE == 0 && p.PCTOTHER == 0) {
d.NODATA = true
} else {
d.NODATA = false
}
}
}
});
// Start out by filling the counties with the cmyk scheme
counties
.style("fill", function(d) {
return cmyk(d);
}).on("mouseover", function(d, i) {
d3.select(this).attr("stroke", "gray");
}).on("mouseout", function(d, i) {
d3.select(this).attr("stroke", "none");
});
// Add the shapes of states on top, just to have outlines.
svg.append("path").datum(topojson.mesh(us, us.objects.states, function(a, b) {
return a.id !== b.id;
})).attr("class", "states").attr("d", path);
dursecs = 500;
// Add functions to each button
d3.select("#cmykbutton").on("mouseover", function() {
tri_legend_svg.style("fill", function(d) { return cmyk(d); });
tri_legend_g.transition().duration(dursecs).attr("opacity", 1);
uni_legend_svg.style("fill", function(d) { return bwscale(d.PCTOTHER); });
uni_legend_label.text("other");
nodata_legend_g.transition().duration(dursecs).attr("opacity", 1);
counties.style("fill", function(d) { return cmyk(d); });
});
d3.select("#cmykpopbutton").on("mouseover", function() {
tri_legend_g.transition().duration(dursecs).attr("opacity", 0);
uni_legend_svg.style("fill", function(d) { return cmykpop(d.PCTOTHER); });
uni_legend_label.text("pop");
nodata_legend_g.transition().duration(dursecs).attr("opacity", 0);
counties.style("fill", function(d) { return cmykpop(d.PCTPOP); });
});
d3.select("#cmyksodabutton").on("mouseover", function() {
tri_legend_g.transition().duration(dursecs).attr("opacity", 0);
uni_legend_svg.style("fill", function(d) { return cmyksoda(d.PCTOTHER); });
uni_legend_label.text("soda");
nodata_legend_g.transition().duration(dursecs).attr("opacity", 0);
counties.style("fill", function(d) { return cmyksoda(d.PCTSODA); });
});
d3.select("#cmykcokebutton").on("mouseover", function() {
tri_legend_g.transition().duration(dursecs).attr("opacity", 0);
uni_legend_svg.style("fill", function(d) { return cmykcoke(d.PCTOTHER); });
uni_legend_label.text("coke");
nodata_legend_g.transition().duration(dursecs).attr("opacity", 0);
counties.style("fill", function(d) { return cmykcoke(d.PCTCOKE); });
});
d3.select("#cmykotherbutton").on("mouseover", function() {
tri_legend_g.transition().duration(dursecs).attr("opacity", 0);
uni_legend_svg.style("fill", function(d) { return bwscale(d.PCTOTHER); });
uni_legend_label.text("other");
nodata_legend_g.transition().duration(dursecs).attr("opacity", 0);
counties.style("fill", function(d) { return bwscale(d.PCTOTHER) });
});
d3.select("#rgbbutton").on("mouseover", function() {
tri_legend_svg.style("fill", function(d) { return rgb(d); });
tri_legend_g.transition().duration(dursecs).attr("opacity", 1);
uni_legend_svg.style("fill", function(d) { return bwscale(d.PCTOTHER); });
uni_legend_label.text("other");
nodata_legend_g.transition().duration(dursecs).attr("opacity", 1);
counties.style("fill", function(d) { return rgb(d) });
});
d3.select("#rgbpopbutton").on("mouseover", function() {
tri_legend_g.transition().duration(dursecs).attr("opacity", 0);
uni_legend_svg.style("fill", function(d) { return rgbpop(d.PCTOTHER); });
uni_legend_label.text("pop");
nodata_legend_g.transition().duration(dursecs).attr("opacity", 0);
counties.style("fill", function(d) { return rgbpop(d.PCTPOP); });
});
d3.select("#rgbsodabutton").on("mouseover", function() {
tri_legend_g.transition().duration(dursecs).attr("opacity", 0);
uni_legend_svg.style("fill", function(d) { return rgbsoda(d.PCTOTHER); });
uni_legend_label.text("soda");
nodata_legend_g.transition().duration(dursecs).attr("opacity", 0);
counties.style("fill", function(d) { return rgbsoda(d.PCTSODA); });
});
d3.select("#rgbcokebutton").on("mouseover", function() {
tri_legend_g.transition().duration(dursecs).attr("opacity", 0);
uni_legend_svg.style("fill", function(d) { return rgbcoke(d.PCTOTHER); });
uni_legend_label.text("coke");
nodata_legend_g.transition().duration(dursecs).attr("opacity", 0);
counties.style("fill", function(d) { return rgbcoke(d.PCTCOKE); });
});
d3.select("#rgbotherbutton").on("mouseover", function() {
tri_legend_g.transition().duration(dursecs).attr("opacity", 0);
uni_legend_svg.style("fill", function(d) { return bwscale(d.PCTOTHER); });
uni_legend_label.text("other");
nodata_legend_g.transition().duration(dursecs).attr("opacity", 0);
counties.style("fill", function(d) { return bwscale(d.PCTOTHER) });
});
d3.select("#bwpopbutton").on("mouseover", function() {
tri_legend_g.transition().duration(dursecs).attr("opacity", 0);
uni_legend_svg.style("fill", function(d) { return bwscale(d.PCTOTHER); });
uni_legend_label.text("pop");
nodata_legend_g.transition().duration(dursecs).attr("opacity", 0);
counties.style("fill", function(d) { return bwscale(d.PCTPOP) });
});
d3.select("#bwsodabutton").on("mouseover", function() {
tri_legend_g.transition().duration(dursecs).attr("opacity", 0);
uni_legend_svg.style("fill", function(d) { return bwscale(d.PCTOTHER); });
uni_legend_label.text("soda");
nodata_legend_g.transition().duration(dursecs).attr("opacity", 0);
counties.style("fill", function(d) { return bwscale(d.PCTSODA) });
});
d3.select("#bwcokebutton").on("mouseover", function() {
tri_legend_g.transition().duration(dursecs).attr("opacity", 0);
uni_legend_svg.style("fill", function(d) { return bwscale(d.PCTOTHER); });
uni_legend_label.text("coke");
nodata_legend_g.transition().duration(dursecs).attr("opacity", 0);
counties.style("fill", function(d) { return bwscale(d.PCTCOKE) });
});
d3.select("#bwotherbutton").on("mouseover", function() {
tri_legend_g.transition().duration(dursecs).attr("opacity", 0);
uni_legend_svg.style("fill", function(d) { return bwscale(d.PCTOTHER); });
uni_legend_label.text("other");
nodata_legend_g.transition().duration(dursecs).attr("opacity", 0);
counties.style("fill", function(d) { return bwscale(d.PCTOTHER) });
});
}
</script>
</body>
</html>
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js
https://d3js.org/queue.v1.min.js
https://d3js.org/topojson.v0.min.js