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