Skip to content

Instantly share code, notes, and snippets.

@jwasilgeo
Forked from mbostock/.block
Last active February 1, 2018 20:57
Show Gist options
  • Save jwasilgeo/7589174471c4ddfe218c162003a5ee2e to your computer and use it in GitHub Desktop.
Save jwasilgeo/7589174471c4ddfe218c162003a5ee2e to your computer and use it in GitHub Desktop.
Bring Up State
license: gpl-3.0
height: 600
border: no

This example "brings up" or "serves" a geographical feature to the viewer.

Web maps typically require you to zoom in and give the sensation of "falling down" or "diving in" towards the Earth's surface. Why not make the interaction become user-focused?

Just click—no need to fly down closer to the Earth—and the visualization will do the heavy lifting of bringing up the geographical feature directly to you.

This effect was created for the gerrymandering topic in American Voting: What's Your Vote Worth?. This is an adaptation of Mike Bostock's "Zoom to Bounding Box".

<!DOCTYPE html>
<html>
<meta charset="utf-8">
<style>
.background {
fill: none;
pointer-events: all;
}
.feature {
stroke-width: 0.75px;
stroke: #fff;
fill: #a6bddb;
cursor: pointer;
}
</style>
<body>
<svg width="960" height="600" stroke-linejoin="round" stroke-linecap="round">
<rect width="960" height="600" class="background"></rect>
<g></g>
</svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/[email protected]"></script>
<script>
var svg = d3.select('svg');
// lookup svg width and height from DOM instead of hard-coding values
var svgWidth = +svg.attr('width'),
svgHeight = +svg.attr('height');
// get reference to the <rect> node, which is invisible and in the background
// wire up a click listener so that the brought up feature can be "reset" later
var rect = svg.select('rect.background')
.on('click', reset);
// get reference to the <g> node
// geojson features will be drawn here
var g = svg.select('g');
// the D3 geographic path generator is used for drawing the svg paths of geojson
// and during bounding box calculations
var geoPathGenerator = d3.geoPath();
// transition-related vars
var activeFeature = d3.select(null),
transitionDuration = 1250,
featureStrokeWidth = 0.75,
featureStroke = '#fff',
featureFill = '#a6bddb',
// during transition:
// - swap stroke and fill colors
// - change stroke width
broughtUpFeatureStrokeWidth = 3,
broughtUpFeatureStroke = '#a6bddb',
broughtUpFeatureFill = '#fff';
// load and draw US states topojson
d3.json('https://unpkg.com/us-atlas@1/us/10m.json', function(error, us) {
if (error) {
throw error;
}
var allStatesGeoJsonData = topojson.feature(us, us.objects.states);
g.selectAll('path')
.data(allStatesGeoJsonData.features)
.enter().append('path')
.attr('d', geoPathGenerator)
.attr('class', 'feature')
.on('click', bringUpFeature);
});
function bringUpFeature(geoJsonDatum) {
if (activeFeature.node() === this) {
return reset();
}
activeFeature.classed('active-feature', false);
activeFeature = d3.select(this)
.classed('active-feature', true)
.raise();
var t = getGeoBoundsTransform(geoJsonDatum, geoPathGenerator, svgWidth, svgHeight);
activeFeature.transition()
.duration(transitionDuration)
.style('stroke-width', broughtUpFeatureStrokeWidth / t.scale + 'px')
.style('stroke', broughtUpFeatureStroke)
.style('fill', broughtUpFeatureFill)
.attr('transform', 'translate(' + t.translate + ') scale(' + t.scale + ')');
var otherFeatures = g.selectAll('path.feature:not(.active-feature)');
// remove the original click listener before transitioning the other features out of view
otherFeatures.on('click', null);
otherFeatures.transition()
.duration(transitionDuration)
.style('opacity', '0')
.on('end', function(d, idx, nodeList) {
// completely remove the display of the other features after transition is over
if (idx === (nodeList.length - 1)) {
otherFeatures.style('display', 'none');
}
});
}
function reset() {
activeFeature
.transition()
.duration(transitionDuration)
.style('stroke-width', featureStrokeWidth)
.style('stroke', featureStroke)
.style('fill', featureFill)
.attr('transform', '');
var otherFeatures = g.selectAll('path.feature:not(.active-feature)');
// reset display of other features before transitioning back into view
otherFeatures.style('display', '');
otherFeatures.transition()
.duration(transitionDuration)
.style('opacity', '1')
.on('end', function(d, idx, nodeList) {
// reestablish the original click listener after transition is over
if (idx === (nodeList.length - 1)) {
otherFeatures.on('click', bringUpFeature);
}
});
activeFeature.classed('active-feature', false);
activeFeature = d3.select(null);
}
function getGeoBoundsTransform(geoJsonDatum, geoPathGenerator, width, height) {
var bounds = geoPathGenerator.bounds(geoJsonDatum),
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 = 0.9 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
return {
translate: translate,
scale: scale
};
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment