Map of the Netherland with a population density choropleth, train tracks and locations of train station. A Voronoi diagram is drawn around the train stations to show what train station is closest. Hover over an area for some extra information like name, population and avg distance to train station. Zoom by clicking on a neighbourhood.
GNU General Public License v3.0
Kaart van Nederland met choropleth voor bevolkingsdichtheid, locaties van stations en treinsporen. Om de stations zijn Voronoi cellen getekend, zo kun je makkelijk zien welke station het dichste bij is voor een gebied. Door de muis boven een station of wijk te houden verschijnt extra informatie. Inzoomen kan door op een wijk te klikken, uitzoomen door nog eens op een wijk te klikken.
De broncode is open source onder het GNU General Public License v3.0
xxxxxxxxxx
<meta charset="utf-8">
<html lang="en">
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.background {
fill: none;
pointer-events: all;
}
.path {
stroke-linejoin: round;
}
body {
text-align: center;
}
.station {
fill: white;
opacity: 0.9;
r: 1.6;
}
.voronoi {
fill: none;
stroke: #ff7f7f;
stroke-width: 0.7px;
opacity: 0.55;
}
.spoor {
fill: none;
stroke: black;
stroke-width: 0.8px;
}
.wijk:hover {
opacity: 0.5;
}
.wijk-borders {
fill: none;
stroke: #fff;
stroke-width: 0.2px;
stroke-linejoin: round;
stroke-linecap: round;
pointer-events: none;
}
.gemeente-borders {
fill: none;
stroke: black;
stroke-width: 0.5px;
stroke-linejoin: round;
stroke-linecap: round;
pointer-events: none;
opacity: 0.4;
}
</style>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://d3js.org/d3-geo-projection.v1.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<h1>Voronoi Stations Nederland + Bevolkingsdichtheid per wijk</h1>
<svg width="900" height="80" id="legend" id="legend"></svg>
<svg id="map"></svg>
<strong>Voronoi Diagram</strong>
<form id="controls">
<label><input type="radio" name="mode" value="Show" checked>Aan</label>
<label><input type="radio" name="mode" value="Hide">Uit</label>
</form>
<p> Bron: CBS, NS</p>
<script>
var width = 960,
height = 700,
format = d3.format(",d")
active = d3.select(null);
var projection = d3.geoAlbers()
.center([5.5, 52.05])
// .parallels([50, 53])
.rotate(120)
.scale(12000)
.translate([width / 2, height / 2]);
var zoom = d3.zoom()
.scaleExtent([1, 8])
.on("zoom", zoomed);
var path = d3.geoPath()
.projection(projection);
var svg = d3.select("#map")
.attr("width", width)
.attr("height", height)
.on("click", stopped, true);
svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height)
.on("click", reset);
var g = svg.append("g");
svg.call(zoom);
var svgLegend = d3.select("#legend");
var color = d3.scaleThreshold()
.domain([0, 100, 500, 750, 1500, 2500, 7500, 12500])
.range(d3.schemeGnBu[9]);
/* LEGEND */
var x = d3.scaleSqrt()
.domain([0, 12500])
.rangeRound([10, 890]);
var gLegend = svgLegend.append("g")
.attr("class", "key")
.attr("transform", "translate(0,40)");
gLegend.selectAll("rect")
.data(color.range().map(function(d) {
d = color.invertExtent(d);
if (d[0] == null) d[0] = x.domain()[0];
if (d[1] == null) d[1] = x.domain()[1];
return d;
}))
.enter().append("rect")
.attr("height", 8)
.attr("x", function(d) { return x(d[0]); })
.attr("width", function(d) { return x(d[1]) - x(d[0]); })
.attr("fill", function(d) { return color(d[0]); });
// Draw legend text
gLegend.append("text")
.attr("class", "caption")
.attr("x", x.range()[0])
.attr("y", -6)
.attr("fill", "#000")
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text("Inwoners per vierkante kilometer");
// Draw axis tick marks
gLegend.call(d3.axisBottom(x)
.tickSize(13)
.tickValues(color.domain()))
.select(".domain")
.remove();
// Toggle voronoi with radio button
var inputElems = d3.selectAll("input")
.on("change", function() {
if (this.value === "Hide") { d3.select(".voronoi").style("opacity", 0); }
else if (this.value === "Show") { d3.select(".voronoi").style("opacity", 1); }
})
d3.queue()
.defer(d3.json, "wijk-topo-c.json")
.defer(d3.csv, "locations.csv")
.defer(d3.json, "spoor.geojson")
.await(ready);
function ready(error, wijken, stations, spoor) {
if (error) throw error;
// Draw the map
g.selectAll(".wijk")
.data(topojson.feature(wijken, wijken.objects.wijk).features)
.enter().insert("path")
.attr("class", "wijk")
.attr("fill", function(d) { return color(d.properties.BEV_DICHTH); }) //d3.schemeOrRd[9][d.properties.AANT_INW]
.attr("d", path)
.on("click", clicked)
.append("title")
.text(function(d) { return (
d.properties.GM_NAAM + " "
+ ((d.properties.WK_NAAM !== null) ? d.properties.WK_NAAM : "")
+ ((d.properties.AF_TREINST !== -99999998) ? "\nGem. afstand tot station: " + d.properties.AF_TREINST + " km" : "")
+ ((d.properties.BEV_DICHTH !== -99999998) ? "\nBev. dichtheid: " + format(d.properties.BEV_DICHTH) : "")
+ ((d.properties.AANT_INW !== -99999998) ? "\nAantal Inwoners: " + format(d.properties.AANT_INW) : "")
);
});
// Draw borders
g.append("path")
.attr("class", "wijk-borders")
.attr("d", path(topojson.mesh(wijken, wijken.objects.wijk, function(a, b) { return a !== b; })));
// Draw borders
g.append("path")
.attr("class", "gemeente-borders")
.attr("d", path(topojson.mesh(wijken, wijken.objects.wijk, function(a, b) { return a.properties.GM_NAAM !== b.properties.GM_NAAM; })));
// Draw the tracks
g.selectAll(".spoor")
.data(spoor.features)
.enter().insert("path")
.attr("class", "spoor")
.attr("d", path);
// Draw the points for the stations
g.selectAll(".station")
.data(stations)
.enter().append("circle")
.attr("class", "station")
.attr("transform", function(d) { return "translate(" + projection([d["0"], d["1"]]) + ")"; })
.append("title")
.text(function(d) { return "Station " + d.name; });
var polygons = d3.voronoi()
.extent([[-1, -1], [width + 1, height + 1]])
.polygons(stations.map(projection));
// Draw the Voronoi cells
g.append("path")
.datum(polygons)
.attr("class", "voronoi")
.attr("d", function(d) {
return "M" + d
.filter(function(d) { return d != null; })
.map(function(d) { return d.join("L"); })
.join("ZM") + "Z";
});
};
function clicked(d) {
if (active.node() === this) return reset();
active.classed("active", false);
active = d3.select(this).classed("active", true);
var bounds = path.bounds(d),
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 = Math.max(1, Math.min(8, 0.9 / Math.max(dx / width, dy / height))),
translate = [width / 2 - scale * x, height / 2 - scale * y];
svg.transition()
.duration(750)
.call( zoom.transform, d3.zoomIdentity.translate(translate[0],translate[1]).scale(scale) );
}
function reset() {
active.classed("active", false);
active = d3.select(null);
svg.transition()
.duration(750)
.call( zoom.transform, d3.zoomIdentity );
}
function zoomed() {
g.style("stroke-width", 1.5 / d3.event.transform.k + "px");
g.attr("transform", d3.event.transform);
}
// If the drag behavior prevents the default click,
// also stop propagation so we don’t click-to-zoom.
function stopped() {
if (d3.event.defaultPrevented) d3.event.stopPropagation();
}
</script>
https://d3js.org/d3.v4.min.js
https://d3js.org/d3-scale-chromatic.v1.min.js
https://d3js.org/d3-geo-projection.v1.min.js
https://d3js.org/topojson.v2.min.js