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