Published
Edited
Feb 22, 2021
8 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// see: https://www.naturalearthdata.com/downloads/
Object.keys(topology.objects)
Insert cell
// composed in mapshaper, see: https://observablehq.com/@nikita-sharov/metropolitan-france-map#data
topology = {
if (levelOfDetail === "10m") {
return FileAttachment("austria-10m@5.json").json(); // requires 'single string literal' arguments
} else if (levelOfDetail === "50m") {
return FileAttachment("austria-50m@3.json").json();
} else if (levelOfDetail === "110m") {
return FileAttachment("austria-110m@1.json").json();
} else {
throw `Unsupported level of detail: '${levelOfDetail}'`;
}
}
Insert cell
getName = (feature) => {
const propertyName = `name_${labelLanguage}`;
const name = feature.properties[propertyName];
if (name !== "") {
return name;
}
return getFallbackName(feature);
}
Insert cell
getFallbackName = (feature) => {
const name = feature.properties.name_en;
if (name === "") {
throw "No fallback name";
}
return name;
}
Insert cell
Insert cell
Insert cell
physicalFeaturesByLevelOfDetail = new Map([
["10m", [...physicalFeatureLayers.keys()]],
["50m", ["Geographic regions", "Lakes", "Rivers"]],
["110m", ["Geographic regions", "Rivers"]],
])
Insert cell
physicalFeatureLayers = new Map([
["Elevation points", ["elevation-points"]],
["Geographic regions", ["geographic-region-areas", "geographic-region-labels"]],
["Glaciated areas", ["glaciated-areas"]],
["Lakes", ["lakes"]],
["Rivers", ["rivers"]],
])
Insert cell
culturalFeaturesByLevelOfDetail = new Map([
["10m", [...culturalFeatureLayers.keys()]],
["50m", ["Airports", "Populated places", "Urban areas"]],
["110m", ["Populated places"]],
])
Insert cell
culturalFeatureLayers = new Map([
["Airports", ["airports"]],
["Federal states", ["federal-state-borders", "federal-state-labels"]],
["Populated places", ["populated-places"]],
["Railroads", ["railroads"]],
["Roads", ["roads"]],
["Urban areas", ["urban-areas"]],
])
Insert cell
visibleLayers = {
let layers = [
"country-area",
"clipped-layers",
"country-border"
];
for (const key of physicalFeatures) {
const featureLayers = physicalFeatureLayers.get(key);
layers = layers.concat(featureLayers);
}
// single checkbox input results are not iterable, see: https://observablehq.com/@jashkenas/inputs#checkboxDemo
if (Array.isArray(culturalFeatures)) {
for (const key of culturalFeatures) {
const featureLayers = culturalFeatureLayers.get(key);
layers = layers.concat(featureLayers);
}
} else {
const singleFeature = culturalFeatures; // 'false' when unchecked
if (culturalFeatureLayers.has(singleFeature)) {
const featureLayers = culturalFeatureLayers.get(singleFeature);
layers = layers.concat(featureLayers);
}
}
return layers;
}
Insert cell
Insert cell
chart = () => {
const svg = createCanvas();
const features = addFeatures(svg);
enableZooming(svg, features);
return svg.node();
}
Insert cell
createCanvas = () =>
d3.create("svg")
.attr("id", "map")
.attr("width", width)
.attr("height", height)
Insert cell
d3 = require("d3@6")
Insert cell
height = {
const [[x0, y0], [x1, y1]] = path.bounds(austria);
return Math.ceil(y1 - y0);
}
Insert cell
path = d3.geoPath(projection)
Insert cell
projection = d3.geoTransverseMercator()
.rotate([-13.33, 0]) // see: https://epsg.io/31255
.fitWidth(width, austria)
Insert cell
austria = topojson.feature(topology, topology.objects.admin_0_countries).features[0];
Insert cell
topojson = require("topojson-client@3")
Insert cell
addFeatures = (parent) => {
const container = addContainer(parent, "features");
addDefinitions(container);
addLayers(container);
return container;
}
Insert cell
addContainer = (parent, id) => parent.append("g").attr("id", id)
Insert cell
addDefinitions = (parent) => {
const definitions = parent.append("defs");
const pathId = "country";
addPath(definitions, austria, pathId);
const clipPathId = "clip";
addClipPath(definitions, clipPathId, pathId);
}
Insert cell
addPath = (parent, feature, id) => {
const element = parent.append("path")
if (id !== undefined) {
element.attr("id", id);
}
element.attr("d", path(feature));
return element;
}
Insert cell
addClipPath = (parent, clipPathId, pathId) => {
const clipPath = parent.append("clipPath")
.attr("id", clipPathId);
clipPath.append("use")
.attr("href", `#${pathId}`);
}
Insert cell
addLayers = (parent) => {
for (const [id, addLayer] of layersByDisplayOrder) {
if (visibleLayers.includes(id)) {
addLayer(parent, id);
}
}
}
Insert cell
layersByDisplayOrder = [
["country-area", addCountryArea],
["clipped-layers", addClippedLayers], // contains sublayers
["country-border", addCountryBorder],
["geographic-region-labels", addGeographicRegionLabels],
["federal-state-labels", addFederalStateLabels],
["elevation-points", addElevationPoints],
["airports", addAirports],
["populated-places", addPopulatedPlaces]
]
Insert cell
addCountryArea = (parent, containerId) => {
const container = addContainer(parent, containerId)
.attr("fill", "#f1ebb7");
container.append("use")
.attr("href", "#country");
}
Insert cell
addClippedLayers = (parent, containerId) => {
const container = addContainer(parent, containerId)
.attr("clip-path", "url(#clip)");
for (const [id, addLayer] of clippedLayersByDisplayOrder) {
if (visibleLayers.includes(id)) {
addLayer(container, id);
}
}
}
Insert cell
clippedLayersByDisplayOrder = [
["geographic-region-areas", addGeographicRegionAreas],
["glaciated-areas", addGlaciatedAreas],
["urban-areas", addUrbanAreas],
["rivers", addRivers],
["lakes", addLakes],
["roads", addRoads],
["railroads", addRailroads],
["federal-state-borders", addFederalStateBorders],
]
Insert cell
addGeographicRegionAreas = (parent, containerId) => {
const container = addContainer(parent, containerId);
container.append("defs")
.call(addStripesPattern);
const feature = topojson.feature(topology, topology.objects.geography_regions_polys);
addPath(container, feature)
.attr("fill", "url(#stripes)");
}
Insert cell
addStripesPattern = (parent) => {
const pattern = parent.append("pattern")
.attr("id", "stripes")
.attr("width", 5)
.attr("height", 10)
.attr("patternUnits", "userSpaceOnUse")
.attr("patternTransform", "rotate(45)")
pattern.append("line")
.attr("stroke", "black")
.attr("stroke-opacity", 0.1)
.attr("stroke-width", 5)
.attr("y2", 10);
}
Insert cell
addGlaciatedAreas = (parent, containerId) => {
const container = addContainer(parent, containerId)
.attr("fill", "#eff8ff");
const feature = topojson.feature(topology, topology.objects.glaciated_areas);
addPath(container, feature);
}
Insert cell
addUrbanAreas = (parent, containerId) => {
const container = addContainer(parent, containerId)
.attr("fill", "#f3c265");

const feature = topojson.feature(topology, topology.objects.urban_areas);
addPath(container, feature);
}
Insert cell
addRivers = (parent, containerId) => {
const features = [];
if (topology.objects.rivers_europe !== undefined) {
features.push(topojson.feature(topology, topology.objects.rivers_europe));
}
if (topology.objects.rivers_lake_centerlines !== undefined) {
features.push(topojson.feature(topology, topology.objects.rivers_lake_centerlines));
}
if (features.length > 0) {
const container = addContainer(parent, containerId)
.attr("fill", "none")
.attr("stroke", "#019ad5");

for (const feature of features) {
addPath(container, feature);
}
}
}
Insert cell
addLakes = (parent, containerId) => {
const features = [];
if (topology.objects.lakes !== undefined) {
features.push(topojson.feature(topology, topology.objects.lakes));
}
if (topology.objects.lakes_europe !== undefined) {
features.push(topojson.feature(topology, topology.objects.lakes_europe));
}
if (features.length > 0) {
const container = addContainer(parent, containerId)
.attr("fill", "#a6daf5")
.attr("stroke", "#1da6e0");

for (const feature of features) {
addPath(container, feature);
}
}
}
Insert cell
addRoads = (parent, containerId) => {
const container = addContainer(parent, containerId)
.attr("fill", "none");
const feature = topojson.feature(topology, topology.objects.roads);
const background = addPath(container, feature)
.attr("stroke", "orange")
.attr("stroke-width", 3)

background.clone()
.attr("stroke", "yellow")
.attr("stroke-width", 2);
}
Insert cell
addRailroads = (parent, containerId) => {
const container = addContainer(parent, containerId)
.attr("fill", "none");
const feature = topojson.feature(topology, topology.objects.railroads);
const background = addPath(container, feature)
.attr("stroke", "black")
.attr("stroke-width", "3");
const foreground = background.clone()
.attr("stroke", "white")
.attr("stroke-width", 2);
foreground.clone()
.attr("stroke", "black")
.attr("stroke-dasharray", 6);
}
Insert cell
addFederalStateBorders = (parent, containerId) => {
const container = addContainer(parent, containerId)
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-dasharray", 4)
.attr("stroke-width", 0.5);

const feature = topojson.feature(topology, topology.objects.admin_1_states_provinces_lines);
addPath(container, feature);
}
Insert cell
addCountryBorder = (parent, containerId) => {
const container = addContainer(parent, containerId)
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-linejoin", "bevel");
container.append("use")
.attr("href", "#country");
}
Insert cell
addGeographicRegionLabels = (parent, containerId) => {
const container = addContainer(parent, containerId)
.style("font-style", "italic")
.style("letter-spacing", "6px")
.style("text-transform", "uppercase");
const feature = topojson.feature(topology, topology.objects.geography_regions_polys);
for (const region of feature.features) {
const dy = 15;
addLabel(container, region, dy);
}
}
Insert cell
addLabel = (parent, feature, dy = 0) => {
const [x, y] = path.centroid(feature);
const container = parent.append("g")
.attr("class", "label")
.attr("transform", `translate(${x},${y + dy})`);
const name = getName(feature);
addText(container, name);
}
Insert cell
addText = (parent, text, dy) => {
const background = parent.append("text")
.attr("class", "background")
.text(text);

const foreground = parent.append("text")
.text(text);
if (dy !== undefined) {
background.attr("dy", dy)
foreground.attr("dy", dy)
}
}
Insert cell
addFederalStateLabels = (parent, containerId) => {
const container = addContainer(parent, containerId)
.style("text-transform", "uppercase");
const feature = topojson.feature(topology, topology.objects.admin_1_states_provinces);
for (const state of feature.features) {
if ((state.properties.name_en === "Vienna") && visibleLayers.includes("populated-places")) {
continue; // Vienna is the national capital, largest city, and one of nine states of Austria
}
addLabel(container, state);
}
}
Insert cell
addElevationPoints = (parent, containerId) => {
const container = addContainer(parent, containerId);
const feature = topojson.feature(topology, topology.objects.geography_regions_elevation_points);
for (const point of feature.features) {
addElevationPoint(container, point)
}
}
Insert cell
addElevationPoint = (parent, feature) => {
const [x, y] = projection(feature.geometry.coordinates);
const container = parent.append("g")
.attr("transform", `translate(${x},${y})`);

addText(container, "▲");
addText(container, getName(feature), "1em");
addText(container, feature.properties.elevation, "2.2em");
}
Insert cell
addAirports = (parent, containerId) => {
const container = addContainer(parent, containerId);
const feature = topojson.feature(topology, topology.objects.airports)
for (const airport of feature.features) {
addAirport(container, airport);
}
}
Insert cell
addAirport = (parent, feature) => {
const [x, y] = projection(feature.geometry.coordinates);
const container = parent.append("g")
.attr("transform", `translate(${x},${y})`);
addText(container, "🛧");
addText(container, getName(feature), "1em");
}
Insert cell
addPopulatedPlaces = (parent, containerId) => {
const container = addContainer(parent, containerId);
const feature = topojson.feature(topology, topology.objects.populated_places);
for (const place of feature.features) {
addPopulatedPlace(container, place);
}
}
Insert cell
addPopulatedPlace = (parent, feature) => {
const [x, y] = projection(feature.geometry.coordinates);
const container = parent.append("g")
.attr("transform", `translate(${x},${y})`);
const textOffset = {
y: -5
};

if (feature.properties.name_en === "Vienna") {
container.append("circle")
.attr("r", 5);

textOffset.y = -8;
}

container.append("circle")
.attr("class", "background")
.attr("r", 4);

container.append("circle")
.attr("r", 2);

if (feature.properties.name_en === "Wiener Neustadt") {
textOffset.y = 12;
}

const name = getName(feature);
addText(container, name, textOffset.y);
}
Insert cell
enableZooming = (canvas, plane) => {
const zoomed = (event) => {
const {transform} = event;
plane.attr("transform", transform);
};
const zoom = d3.zoom()
.scaleExtent([1, 3])
.translateExtent([[0, 0], [width, height]])
.on("zoom", zoomed);
zoom(canvas);
}
Insert cell
html`<style>
circle.background {
fill: white;
}

text {
font-family: sans-serif;
font-size: 10px;
text-anchor: middle;
}

text.background {
stroke: white;
}

#geographic-region-labels text {
font-size: 12px;
}
</style>`
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more