Skip to content

Instantly share code, notes, and snippets.

@HarryStevens
Last active Feb 11, 2018
Embed
What would you like to do?
Map Inset
license: gpl-3.0

Per wiki.GIS.com:

An inset map is a smaller map featured on the same page as the main map. Traditionally, inset maps are shown at a larger scale (smaller area) than the main map. Often, an inset map is used as a locator map that shows the area of the main map in a broader, more familiar geographical frame of reference.

Here, the inset changes on an interval. You can stop the interval and change the map yourself by selecting a state or union territory from the dropdown menu.

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
body {
margin: 0;
font-family: "Helvetica Neue", sans-serif;
}
#controls {
position: absolute;
}
#inset {
position: absolute;
}
#inset .subunit-boundary {
fill: #000;
stroke: #000;
}
#inset .inset-rect {
stroke-width: 2px;
stroke: tomato;
fill: none;
}
#map .subunit, #map .subunit-boundary {
vector-effect: non-scaling-stroke;
}
#map .subunit {
fill: #ddd;
stroke: #fff;
stroke-width: 1px;
}
#map .subunit.selected {
stroke: #000;
stroke-width: 2px;
}
#map .subunit-boundary {
fill: none;
stroke: #000;
}
</style>
</head>
<body>
<div id="controls">
<select><option>-- Reset --</option></select>
<button id="stop-go">Stop</button>
</div>
<div id="map-wrapper">
<div id="inset"></div>
<div id="map"></div>
</div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/d3-moveto@0.0.3/build/d3-moveto.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.min.js"></script>
<script src="https://unpkg.com/jeezy@1.12.13/lib/jeezy.js"></script>
<script>
var match_property = "st_nm";
var width = window.innerWidth,
height = window.innerHeight;
var projection = d3.geoMercator();
var path = d3.geoPath()
.projection(projection);
var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.append("g");
// set up the inset
var inset_margin = {top: 2, bottom: 2, left: 2, right: 2};
// if the width is greater than the height, the width needs to be proportional to a fixed height
if (width > height){
var inset_height = 100 - inset_margin.top - inset_margin.bottom,
inset_width = (inset_height * width / height) - inset_margin.left - inset_margin.right;
}
// otherwise, the height needs to be proportional to a fixed width
else {
var inset_width = 100 - inset_margin.left - inset_margin.right,
inset_height = (inset_width * height / width) - inset_margin.top - inset_margin.bottom;
}
var inset_projection = d3.geoMercator();
var inset_path = d3.geoPath()
.projection(inset_projection);
var inset_svg = d3.select("#inset").append("svg")
.attr("width", inset_width + inset_margin.left + inset_margin.right)
.attr("height", inset_height + inset_margin.top + inset_margin.bottom)
// move the inset to the bottom right of the parent
d3.select("#inset")
.style("top", +jz.str.keepNumber(svg.style("height")) - +jz.str.keepNumber(inset_svg.style("height")) + "px")
.style("left", +jz.str.keepNumber(svg.style("width")) - +jz.str.keepNumber(inset_svg.style("width")) + "px");
// give the inset a background
inset_svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", inset_width)
.attr("height", inset_height)
.style("fill", "white")
.style("fill-opacity", .7);
// this is for the margin
var inset_g = inset_svg.append("g")
.attr("transform", "translate(" + inset_margin.left + ", " + inset_margin.top + ")");
d3.queue()
.defer(d3.json, "india_2000-2014_state.json")
.await(ready);
function ready(error, geo) {
if (error) throw error;
// main map
centerZoom(geo, width, height, projection);
drawSubUnits(geo, g, path);
drawOuterBoundary(geo, g, path);
// inset
centerZoom(geo, inset_width, inset_height, inset_projection);
drawOuterBoundary(geo, inset_g, inset_path);
drawInsetRect();
// set up the select dropwdown
var select = d3.select("select");
var uniques = geo.objects.polygons.geometries.map(function(d){ return d.properties[match_property]; }).sort();
select.selectAll("option")
.data(uniques)
.enter().append("option")
.attr("value", function(d){ return d; })
.text(function(d){ return d; });
// the interval, which stops if the user selects
var count = 0;
var interval_is_on = true;
function intervalFn(){
var val = jz.num.isEven(count) ? jz.arr.random(uniques) : "-- Reset --";
select.property("value", val);
zoomTo(geo, val);
++count;
}
var interval = d3.interval(intervalFn, 2000);
d3.select("#stop-go").on("click", function(){
if (interval_is_on) {
interval.stop();
d3.select(this).text("Go");
interval_is_on = false;
} else {
interval = d3.interval(intervalFn, 2000);
d3.select(this).text("Stop");
interval_is_on = true;
}
});
select.on("change", function(){
interval.stop();
d3.select("#stop-go").text("Go");
interval_is_on = false;
zoomTo(geo, select.property("value"));
});
}
function zoomTo(data, subunit){
svg.select(".subunit-boundary").moveToFront();
svg.selectAll(".subunit").classed("selected", false);
// zoom back out if the selection is reset
if (subunit == "-- Reset --"){
g.transition().duration(1500).attr("transform", "translate(0, 0)scale(1)");
inset_g.select(".inset-rect").transition().duration(1500)
.attr("width", inset_width)
.attr("height", inset_height)
.attr("x", 0)
.attr("y", 0);
return;
}
var selector = ".subunit." + jz.str.toSlugCase(subunit);
svg.select(selector).classed("selected", true).moveToFront();
// See: https://bl.ocks.org/mbostock/4699541
var bounds = path.bounds(topojson.feature(data, data.objects.polygons).features.filter(function(f){ return f.properties[match_property] == subunit; })[0]),
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 = 1 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
g.transition().duration(1500).attr("transform", "translate(" + translate + ")scale(" + scale + ")");
// My own math, which is kind of the same as the above but
// takes into account that the inset is proportionally smaller
// than the main map.
var inset_scale_width = inset_width / scale;
var inset_scale_height = inset_height / scale;
var co_h = inset_height / height;
var subunit_height = (bounds[1][1] - bounds[0][1]) * co_h;
var inset_dy = (inset_scale_height - subunit_height) / 2;
var inset_y = bounds[0][1] * co_h - inset_dy;
var co_w = inset_width / width;
var subunit_width = (bounds[1][0] - bounds[0][0]) * co_w;
var inset_dx = (inset_scale_width - subunit_width) / 2;
var inset_x = bounds[0][0] * co_w - inset_dx;
inset_g.select(".inset-rect").transition().duration(1500)
.attr("width", inset_scale_width)
.attr("height", inset_scale_height)
.attr("x", inset_x)
.attr("y", inset_y);
}
function centerZoom(data, width, height, this_projection) {
this_projection.fitSize([width, height], topojson.feature(data, data.objects.polygons));
}
function drawInsetRect(data){
inset_g.append("rect")
.attr("class", "inset-rect")
.attr("x", 0)
.attr("width", inset_width)
.attr("y", 0)
.attr("height", inset_height);
}
function drawOuterBoundary(data, parent, this_path) {
var boundary = topojson.mesh(data, data.objects.polygons, function(a, b) { return a === b; });
parent.append("path")
.datum(boundary)
.attr("d", this_path)
.attr("class", "subunit-boundary");
}
function drawSubUnits(data, parent, this_path) {
parent.selectAll(".subunit")
.data(topojson.feature(data, data.objects.polygons).features)
.enter().append("path")
.attr("class", function(d){ return "subunit " + jz.str.toSlugCase(d.properties[match_property]); })
.attr("d", this_path);
}
</script>
</body>
</html>
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment