Skip to content

Instantly share code, notes, and snippets.

@sathomas
Last active August 20, 2018 21:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save sathomas/c294e8a62c98fc41ef1b to your computer and use it in GitHub Desktop.
Save sathomas/c294e8a62c98fc41ef1b to your computer and use it in GitHub Desktop.
GeoJSON with Voronoi

A visualization combining maps with a Voronoi diagram. Tornado sightings in 2013 from NOAA.

<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>GeoJSON with Voronoi</title>
<link href='http://fonts.googleapis.com/css?family=Varela' rel='stylesheet'
type='text/css'>
<style>
body { font: 18px Varela,sans-serif; }
path { cursor: pointer; }
</style>
</head>
<body>
<div style="position:absolute;top:455px">
<input type="checkbox" id="voronoi">
<label for="voronoi" style="position:relative;top:2px;">Voronoi</label>
</div>
<!--
Because of cross-origin restrictions, we have to use
a hack to load our map data and tornado dataset.
-->
<script src='http://jsDataV.is/data/us-states.js'></script>
<script src='http://jsDataV.is/data/tornadoes.js'></script>
<script src='http://d3js.org/d3.v3.min.js'></script>
<script>
// Define the dimensions of the visualization.
var margin = {top: 20, right: 20, bottom: 20, left: 20},
width = 636 - margin.left - margin.right,
height = 446 - margin.top - margin.bottom;
// Create the SVG container for the visualization and
// define its dimensions.
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
// Within the main SVG container, add a group
// element (`<g>`) that can be transformed via
// a translation to account for the margins.
// Within that group create another group element
// that can be used to zoom and pan the map.
var g = svg.append("g")
.attr("transform", "translate(" + margin.left +
"," + margin.top + ")")
.append("g");
// Define a variable that tracks which state is
// currently zoomed (if any) and a variable that
// indicates if the Voronoi diagram is visible.
var active = d3.select(null),
voronoi = false;
// Set up an event handler to respond to the
// Voronoi checkbox.
d3.select("input[type=checkbox]").on("change", function() {toggle();});
// Define the properties of the map projection.
var projection = d3.geo.albers()
.scale(900)
.translate([width / 2, height / 2]);
// Define a function that returns the SVG
// path based on the projection. This
// function accepts, as input, a selection
// with an associated array of longitude
// and latitude values.
var path = d3.geo.path()
.projection(projection);
// Draw the map within the SVG container.
// Each state is a separate SVG path.
g.selectAll("path")
.data(map.features)
.enter().append("path")
.attr("id", function(d) {
return d.properties.abbreviation;
})
.attr("d", path)
.attr("fill", "#cccccc")
.attr("stroke", "#ffffff")
.on("click", clicked);
// Create a function that will parse
// the text date format in the CSV file
// and return a proper JavaScript date.
// Dates in the file look like, e.g.,
//
// 30-JAN-13 04:33:00
//
// Note that we're not considering time
// zone information because we want to
// use local time for any visualization.
var formatDate = d3.time.format(
"%d-%b-%y %H:%M:%S");
// Only consider data points that have
// latitude and longitude values. While
// we're checking this condition, coerce
// the CSV strings into data types that
// we can work with directly.
var data = dataset.filter(function(d, i) {
if (d.latitude && d.longitude) {
// Convert the string date into
// a real one.
d.date = formatDate.parse(d.date);
// Convert the strings for latitude
// and longitude into numbers.
d.latitude = +d.latitude;
d.longitude = +d.longitude;
// Convert the F scale string to
// a number.
d.f_scale = +d.f_scale[2];
// Calculate the position of the
// point within the projection.
d.position = projection([
d.longitude, d.latitude
]);
return true;
}
});
// Compute the polygons for the Voronoi layout.
// Before we can use D3's Voronoi functions, we
// have to filter out any duplicate positions.
var positions = data.map(function(d) { return d.position;})
.reduce(function(positions, position) {
if (!positions.some(function(p) {
return position[0] === p[0] && position[1] === p[1];
})) {
positions.push(position);
}
return positions;
}, []);
var polygons = d3.geom.voronoi(positions);
// Now we can add the Voronoi polygons to the
// graph. Initially they're invisible because
// the stroke opacity is set to zero.
g.selectAll(".cell")
.data(polygons)
.enter().append("path")
.attr("class", "cell")
.attr("d", function(d) { return "M" + d.join("L") + "Z"; })
.attr("stroke", "#007979")
.attr("stroke-opacity", 0)
.attr("fill", "none");
// Draw circles on the map for each
// data point.
g.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) { return d.position[0]; })
.attr("cy", function(d) { return d.position[1]; })
.attr("r", function(d) { return 4 + 2*d.f_scale; })
.attr("stroke", "#dddddd")
.attr("fill", "#ca0000")
.attr("fill-opacity", "0.8");
// Event handlers.
// Click on a state.
function clicked(d) {
// If clicked on state is already active,
// reset the map to its initial condition.
if (active.node() === this) return reset();
// Otherwise, remove the highlighting from
// the currently active state.
active.attr("fill", "#cccccc");
// And add highlighting to the newly
// active state.
active = d3.select(this)
.attr("fill", "#F77B15");
// Calculate the bounds for the map that
// will contain the newly active state.
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = .9 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
// Transition to the newly active state
// by translation and scaling.
g.transition()
.duration(750)
.style("stroke-width", 1.5 / scale + "px")
.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
// To keep the circles from changing
// size, also transition their radii.
g.selectAll("circle")
.transition()
.duration(750)
.attr("r", function(d) { return (4 + 2*d.f_scale)/scale; });
};
// Reset to initial condition.
function reset() {
// Remove highlighting from active state
// and note that no state is now active.
active.attr("fill", "#cccccc");
active = d3.select(null);
// Remove the translation and scale
// transform with a transition.
g.transition()
.duration(750)
.style("stroke-width", "1px")
.attr("transform", "");
// Also keep the circles the same
// size by transitioning their
// radii at the same time.
g.selectAll("circle")
.transition()
.duration(750)
.attr("r", function(d) { return (4 + 2*d.f_scale); });
};
// Toggle the visibility of the Voronoi
// overlay.
function toggle() {
g.selectAll(".cell")
.transition()
.duration(750)
.attr("stroke-opacity", voronoi ? 0 : 1);
voronoi = !voronoi;
};
</script>
</body>
</html>
http://jsdatav.is/img/thumbnails/tornadoes.png
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment