Consistently sized circles representing U.S. states are overlayed over a map of the contiguous United States. The size of the circles and underlying map can be adjusted to achieve ideal display. Good for when you don't want the discrepancy in state size to bias your visualization too much.
Part of larger visualization about USA water consumption created as part of UC Berkeley’s Masters of Information in Data Science (MIDS) Program.
Forked from john-guerra's block: Colombia as a force directed network
xxxxxxxxxx
<html>
<head>
<meta charset="utf-8">
<title>Water Use by State</title>
<style>
.background {
fill: #FCFCFC;
pointer-events: all;
}
.map-layer {
fill: #EFEFEF;
stroke: #5A5A5A;
opacity: 0.25;
pointer-events: none;
}
.network-layer .link {
/*fill: #fff;*/
stroke: #ccc;
opacity: 0.9;
}
.fd_state{
stroke: #006EB4;
stroke-width: 2;
}
svg text {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-weight: bold;
font-size: 10px;
}
</style>
</head>
<body>
<div id="force_directed_div">
<svg id="force_directed_svg"></svg>
</div>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
var width = 1000,
height = 400,
centered,
//adjust size of circles
sqWidth = 18;
var formatCount = d3.format(",.2f");
// Project and center the map in USA
var projection = d3.geo.mercator()
.scale(650)
.center([-95, 39])
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
// Set svg width & height
var svg = d3.select('#force_directed_svg')
.attr('width', width)
.attr('height', height);
// Add background
svg.append('rect')
.attr('class', 'background')
.attr('width', width)
.attr('height', height);
var g = svg.append('g');
var networkLayer = g.append('g')
.classed('network-layer', true);
var mapLayer = g.append('g')
.classed('map-layer', true);
// Ability to change statistic based on user selection
var selected_statistic = "TO.WTotl"
var state_summary_data;
// Load state water consumption data
d3.csv("state_2010_limited.csv", function(data) {
state_summary_data = data;
});
// Load map data
d3.json('geo_state_contiguous.json', function(error, mapData) {
for (var i = 0; i < state_summary_data.length; i++) {
//Grab values from state summary data
var state_data_fips = state_summary_data[i]['STATEFIPS'];
//fix fips code that got pulled in as numeric
if (state_data_fips.length == 1) {
state_data_fips = "0" + state_data_fips;
}
var state_data_value = parseFloat(state_summary_data[i][selected_statistic]);
var state_data_abr = state_summary_data[i]['STATE'];
// find state within map file and copy in new values
for (var j = 0; j < mapData.features.length; j++) {
var map_state = mapData.features[j].id;
if (map_state == state_data_fips) {
mapData.features[j].properties.abr = state_data_abr;
mapData.features[j].properties.value = state_data_value;
break;
}
}
}
// Define color scale
var max_state_data = d3.max(state_summary_data, function(d) {
return d[selected_statistic]
});
var color = d3.scale.linear()
.domain([0, max_state_data])
.clamp(true)
.range(['#fff', '#006EB4']);
var links = [];
var features = mapData.features;
features.forEach(function(d) {
d.size = sqWidth;
d.x = path.centroid(d)[0];
d.y = path.centroid(d)[1];
d.feature = d;
});
d3.geom.voronoi().links(features).forEach(function(link) {
var dx = link.source.x - link.target.x,
dy = link.source.y - link.target.y;
link.distance = Math.sqrt(dx * dx + dy * dy);
links.push(link);
});
var force = d3.layout.force()
.nodes(features)
.size([width, height])
.gravity(0.01)
.on("tick", onTick)
.start();
// Draw each state as a path
mapLayer.selectAll('path')
.data(features)
.enter().append('path')
.attr('d', path)
.attr('vector-effect', 'non-scaling-stroke');
var selEnter = networkLayer.selectAll('.states')
.data(features)
.enter().append('g')
.attr("class", "states");
function fillFn(d) {
return color(d.properties.value);
}
selEnter
.append("circle")
.attr('r', sqWidth)
.style('fill', fillFn)
.classed('fd_state',true)
.call(force.drag);
selEnter.append("text")
.style("text-anchor", "middle")
.text(function(d) {
return d.properties.abr;
});
function onTick(e) {
var k = e.alpha * 0.2,
q = d3.geom.quadtree(features);
features.forEach(function(o) {
o.x += (path.centroid(o)[0] - o.px) * k;
o.y += (path.centroid(o)[1] - o.py) * k;
q.visit(sqCollide(o));
});
networkLayer.selectAll('.states')
.attr("transform", function(d) {
return "translate(" + (d.x) + "," + (d.y) + ")";
});
}
});
//Approx collide function for squares
function sqCollide(node) {
var s = node.size,
nx1 = node.x - s,
nx2 = node.x + s,
ny1 = node.y - s,
ny2 = node.y + s;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
s = node.size + quad.point.size;
if (l < s) {
l = (l - s) / l * 0.5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
</script>
</body>
</html>
https://d3js.org/d3.v3.min.js