This map dynamically simplifies the geometry in response to zooming, so that the smallest displayed detail is approximately one square pixel. Use the mousewheel, or pinch on touch devices, to zoom.
There is a click handler that highlight a specific feature. Once you get the feature, you can zoom to the bounding box!
Started from mbostock's block: Dynamic Simplification IV
Thanks to Fil's work:
forked from Dynamic Simplification IV with canvas selection
forked from Lacroute's block: Dynamic Simplification IV zoom to bounding box
forked from anonymous's block: Dynamic Simplification IV zoom to bounding box
xxxxxxxxxx
<div style="position: absolute">
<button id="resetZoom">Reset zoom</button>
<p style="display: inline-block">Selected id: <span id="selectedId"></span></p>
</div>
<canvas id="render" width="960" height="500"></canvas>
<canvas id="hidden" width="960" height="500"></canvas>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/topojson-client@2"></script>
<script src="https://unpkg.com/topojson-simplify@2"></script>
<script>
var canvas = d3.select("canvas#render"),
hidden = d3.select("canvas#hidden"),
context = canvas.node().getContext("2d"),
contextHdn = hidden.node().getContext("2d"),
width = canvas.property("width"),
height = canvas.property("height"),
zoomPercentage = .9 // let 10% padding
var selectedId = null,
selectedFeature = null,
selectedSpan = document.getElementById('selectedId'),
resetButton = d3.select("button#resetZoom");
var land,
borders;
var scale,
translate,
visibleArea, // minimum area threshold for points inside viewport
invisibleArea; // minimum area threshold for points outside viewport
var simplify = d3.geoTransform({
point: function(x, y, z) {
if (z < visibleArea) return
x = x * scale + translate[0]
y = y * scale + translate[1]
if (x >= -10 &&
x <= width + 10 &&
y >= -10 &&
y <= height + 10 ||
z >= invisibleArea) {
this.stream.point(x, y)
}
}
})
// Needed to get the lat lng of bounding box
// https://bl.ocks.org/Fil/a8cfbbfd0100d38241beb48d23c9d4d1
simplify.invert = function(u) {
return [(u[0] - translate[0])/scale, (u[1] - translate[1])/scale]
}
//*** Original zoom from Mike Bostock ***//
//*** https://bl.ocks.org/mbostock/7755778 ***//
// An arbitrary scale and center point to set the initial view.
// This projection is baked into the TopoJSON file,
// but is used here to compute the desired zoom translate.
var backed = {
projection: d3.geoMercator().translate([0, 0]).scale(4000)
}
var zoom = d3.zoom()
.on("zoom", zoomed);
// styling
context.lineJoin = "round";
context.lineCap = "round";
var styles = {
fill: "#bbb",
stroke: "#fff",
selected: {
fill: 'rgba(0, 0, 0, 0.5)',
stroke: 'red'
}
}
var path = d3.geoPath()
.projection(simplify)
.context(context)
d3.json("us-states.json", function(error, us) {
if (error) throw error;
topojson.presimplify(us);
land = topojson.feature(us, us.objects.states)
borders = topojson.mesh(us, us.objects.states, (a, b) => a !== b)
canvas
.call(zoom)
.on('click', onClick)
resetButton.on('click', () => {
// For exemple, NY latlng
zoomTo([-75.959, 38.250])
})
// Init, reset zoom
resetButton.dispatch('click')
});
function zoomed(d) {
var z = d3.event.transform
translate = [z.x, z.y]
scale = z.k
visibleArea = 1 / scale / scale
invisibleArea = 200 * visibleArea
draw()
}
function onClick () {
getHiddenData()
zoomTo(selectedFeature)
}
// Retrieve the id with the color, and so, the feature. https://bl.ocks.org/Lacroute/579bc326fb547110a959c0a9ac2b30ce
function getHiddenData() {
path.context(contextHdn)
drawHidden()
let mouse = d3.mouse(canvas.node())
let data = contextHdn.getImageData(mouse[0], mouse[1], 1, 1).data
selectedId = data[0]
selectedFeature = land.features.find(f => +f.id === selectedId)
path.context(context)
}
// Zoom to bounding box of the selected area.
function zoomTo (location) {
if (location === undefined) return
let point, sc = 1
if (!Array.isArray(location)) {
let bounds = path.bounds(location),
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
let factor = Math.min(width / dx, height / dy)
sc = factor * scale * zoomPercentage
location = backed.projection.invert(simplify.invert([x, y]))
// If you prefer zoom to the centroid
// location = backed.projection.invert(simplify.invert(path.centroid(location)))
}
point = backed.projection(location)
// Apply the new transform
canvas.transition()
.duration(750)
.call(
zoom.transform,
d3.zoomIdentity
.translate(width / 2 - point[0] * sc, height / 2 - point[1] * sc)
.scale(sc)
)
}
// Main drawing loop
function draw () {
context.clearRect(0, 0, width, height);
context.fillStyle = styles.fill;
context.strokeStyle = styles.stroke;
context.beginPath();
path(land)
context.fill();
context.beginPath();
path(borders);
context.stroke();
if (selectedFeature) drawSelected()
drawCenter()
}
// Highlight the selected feature
function drawSelected () {
selectedSpan.innerHTML = selectedId
context.fillStyle = styles.selected.fill;
context.strokeStyle = styles.selected.stroke;
context.beginPath();
path(selectedFeature)
context.fill();
context.stroke();
// Draw his centroid
context.beginPath()
let cent = path.centroid(selectedFeature)
context.arc(cent[0], cent[1], 3, 0, 2 * Math.PI)
context.fillStyle = 'green'
context.strokeStyle = 'white'
context.stroke()
context.fill()
}
// Draw a cross in the center of the canvas
function drawCenter () {
context.beginPath();
let x = width / 2
let y = height / 2
context.strokeStyle = 'green';
context.moveTo(x - 10, y - 10);
context.lineTo(x + 10, y + 10);
context.stroke();
context.moveTo(x + 10, y - 10);
context.lineTo(x - 10, y + 10);
context.stroke();
}
// Drawing method to encode id to color
function drawHidden() {
contextHdn.clearRect(0, 0, width, height);
land.features.map(f => {
contextHdn.beginPath();
path(f);
// basic exemple because there is less than 255 counties
contextHdn.fillStyle = `rgba(${f.id}, 0, 0, 1)`
contextHdn.fill();
})
}
</script>
https://d3js.org/d3.v4.min.js
https://unpkg.com/topojson-client@2
https://unpkg.com/topojson-simplify@2