Public
Edited
Apr 9
1 fork
1 star
Insert cell
Insert cell
Insert cell
It's called *cell phone* because the mobile phone network is *cellular*. This map shows all the antenna installations and the cellular network in the Netherlands with a transmission power greater than 10 decibelwatt and their [geo-Voronoi cell](https://observablehq.com/@neocartocnrs/delaunay-voronoi) ([d3-geo-voronoi](https://github.com/Fil/d3-geo-voronoi)). Select an antenna type to filter.

## Credits

### Notebooks
* [Using Flatbush for faster hover events in Canvas maps](https://observablehq.com/@martgnz/using-flatbush-for-faster-canvas-maps) by Martín González
* [US Airports Voronoi](https://observablehq.com/@mbostock/u-s-airports-voronoi) by Mike Bostock

### Data
* Antenna data from the [Antennebureau](https://www.antennebureau.nl/)
* Map of the Netherlands from [cartomap](https://github.com/cartomap/nl)
Insert cell
Insert cell
Insert cell
viewof antennaMap = {
const node = DOM.element("div");
const container = d3.select(node);

const canvas = container
.append("canvas")
.style("width", `${width}px`)
.style("height", `${height}px`)
.attr("width", width * dpi)
.attr("height", height * dpi)
.on("mousemove", mousemoved);

const svg = container
.append("svg")
.style("position", "absolute")
.style("pointer-events", "none")
.style("top", 0)
.style("left", 0)
.attr("width", width)
.attr("height", height);

// svg.append("path").attr("fill", "none").attr("stroke", "black");

const ctx = canvas.node().getContext("2d");

ctx.setTransform(dpi, 0, 0, dpi, 0, 0);
ctx.clearRect(0, 0, width, height);

const path = d3.geoPath(projection).context(ctx);
const svgPath = d3.geoPath(projection);

// create our Flatbush index
const fbIndex = new Flatbush(voronoiCellsFeatures.length);

ctx.lineWidth = 0.5;
// loop over every municipality
for (const municipality of gemeentes) {
// paint municipality
ctx.beginPath();
ctx.strokeStyle = "#444";
path(municipality);
ctx.stroke();
}

for (const antenna of filteredData) {
ctx.beginPath();
path.pointRadius(0.5)(antenna);
ctx.stroke();
ctx.fill();
}

for (const cell of voronoiCellsFeatures) {
// this finds the bounding box of each feature
// we need to pass this to Flatbush
const bounds = path.bounds(cell);

// and now, add each feature to our index
fbIndex.add(
Math.floor(bounds[0][0]),
Math.floor(bounds[0][1]),
Math.ceil(bounds[1][0]),
Math.ceil(bounds[1][1])
);

// paint voronoi cell
ctx.beginPath();
ctx.lineWidth = 0.2;
ctx.strokeStyle = "coral";
path(cell);
ctx.stroke();
}

// we need to do this once we have finished adding features
fbIndex.finish();

// paint map borders
ctx.lineWidth = 1;
ctx.beginPath();
path(border);
ctx.strokeStyle = "black";
ctx.stroke();

function mousemoved(e) {
const [x, y] = d3.pointer(e);

// search for our feature
const idx = fbIndex.search(x, y, x, y);

// reset hover styles
canvas.style("cursor", null);
svg.select("path").attr("d", null);

// return if not found!
if (idx.length === 0) return;

// get our feature
// we can use this data for a tooltip later, etc
const selectedCell = voronoiCellsFeatures[idx[0]];

node.value = { ...selectedCell.properties.site.properties };
node.dispatchEvent(new CustomEvent("input"), { bubbles: true });

canvas.style("cursor", "pointer");

svg
.select("path")
.datum(selectedCell)
.attr("fill", "#333")
.attr("opacity", 0.8)
.attr("d", svgPath);
}

return node;
}
Insert cell
dpi = window.devicePixelRatio || 1
Insert cell
path = d3.geoPath(projection)
Insert cell
projection = d3
.geoMercator()
.fitSize([width, height], topojson.feature(nl, gemeenteObject))
Insert cell
height = Math.min(width, 800)
Insert cell
listFormatter = Intl.ListFormat
? new Intl.ListFormat('nl', { style: 'long', type: 'conjunction' })
: { format: list => list.join(", ") } // bootleg polyfill
Insert cell
Insert cell
voronoiCellsFeatures = d3.geoVoronoi().polygons(filteredData).features
Insert cell
filteredData = antennaType === "All"
? data
: data.filter(d => d.properties.types.includes(antennaType))
Insert cell
groupedByAntennaSite = d3.rollup(antennes, siteReducer, d => d.SITENUMMER)
Insert cell
data = [...groupedByAntennaSite.values()]
Insert cell
siteReducer = v => ({
type: "Feature",
properties: {
municipality: v[0].GEMEENTE,
types: v.map(e => e.TOEPASSING),
sitenr: v[0].SITENUMMER,
height: Number(v[0].ANT_HOOGTE)
},
geometry: {
type: "Point",
coordinates: [
toDecimalDegrees(v[0].COORDINATEN_OL),
toDecimalDegrees(v[0].COORDINATEN_NB)
]
}
})
Insert cell
antennaTypes = [...new Set(antennes.map(d => d.TOEPASSING))]
Insert cell
Insert cell
gemeentes = topojson.feature(nl, gemeenteObject).features
Insert cell
border = topojson.mesh(nl, gemeenteObject, (a, b) => a === b)
Insert cell
gemeenteObject = nl.objects.gemeente_2021 // gemeentes
Insert cell
antennes = d3.csvParse(await FileAttachment("antenne-register.csv").text())
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import { freelanceBanner } from "@julesblm/freelance-banner"
Insert cell
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