Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Grids, rasters and web maps

Hexagons, density surfaces, elevation, basemaps and interactive maps

Eindhoven University of Technology

Points on a background take you a long way, but several challenge days ask for something else: hexagons (Day 20), rasters (Day 29), OpenStreetMap basemaps (Day 21), light and dark (Day 17). This page covers the techniques behind them.

Hexagon grids

Administrative areas come in every size, which makes them hard to compare (see counts or rates). A grid of equal cells removes that problem, and hexagons are the favourite shape: every neighbour is the same distance away, and the map looks less blocky than squares.

<Figure size 700x700 with 2 Axes>

Drawing the empty hexagons in light grey keeps the shape of the city visible and shows that “no café” is a value, not missing data.

Rasters

A raster is a grid of pixels, each with a value: elevation, temperature, land cover, a satellite band. Where vector data says there is a thing here, raster data says here is how much of something there is, everywhere. Every raster has a CRS, a pixel size (its resolution) and an extent, just as vector data has a CRS.

A raster from points: density

You can turn points into a raster by estimating their density at every pixel: a kernel density estimate (KDE). Each café spreads a small bell-shaped hill of “café-ness” around it, and the hills are added up. This uses only NumPy, so it is a good way to see what a raster really is: an array of numbers with coordinates attached.

raster shape: (216, 268) pixels of 50 m
<Figure size 700x700 with 2 Axes>

Two details are worth noticing. origin="lower" puts row 0 at the bottom, because y grows northwards; leave it out and the map is upside down. And extent is what gives the array its coordinates, so the boundary lines up with it. The bandwidth is a choice, like the class edges of a choropleth: try 150 m and 600 m and the story changes. Say which you used.

For real analysis, use scipy.stats.gaussian_kde or sklearn.neighbors.KernelDensity in Python, or spatstat in R, which also handle edge effects properly.

Elevation: AHN

The Netherlands has one of the best elevation datasets in the world, the AHN, at 0.5 m. Download a tile as shown in Finding data, then read and plot it with rasterio (Python) or terra (R).

Python
R
# pip install rasterio
import rasterio
from rasterio.plot import show

with rasterio.open("ahn_dtm.tif") as src:
    print(src.crs, src.res, src.bounds)     # EPSG:28992, (0.5, 0.5), ...
    dtm = src.read(1, masked=True)          # masked: nodata pixels (water, buildings) hidden

    fig, ax = plt.subplots(figsize=(7, 7))
    show(dtm, transform=src.transform, ax=ax, cmap="terrain")

A hillshade makes small differences in height visible, and is the classic look for Day 29:

from matplotlib.colors import LightSource

shade = LightSource(azdeg=315, altdeg=45).hillshade(dtm.filled(np.nan), vert_exag=5, dx=0.5, dy=0.5)
ax.imshow(shade, cmap="gray", extent=(src.bounds.left, src.bounds.right,
                                      src.bounds.bottom, src.bounds.top))

Eindhoven is flat, so use a strong vertical exaggeration or look for the river Dommel valley. The dsm_05m coverage includes buildings and trees: subtract the DTM from it and you get the height of everything standing on the ground, a striking map for almost any city.

Basemaps

A basemap is a background of streets, buildings or terrain drawn from web map tiles. It helps readers orient themselves, but it also competes with your data. Use a muted one (such as CartoDB Positron), and only when the reader needs to know where things are.

Python
R
# pip install contextily
import contextily as cx

ax = cafes.plot(figsize=(7, 7), color="#c8102e", markersize=12)
cx.add_basemap(ax, crs=cafes.crs, source=cx.providers.CartoDB.Positron)
ax.set_axis_off()

contextily downloads tiles in Web Mercator and reprojects them to your data’s CRS, so you can keep the map in EPSG:28992.

Tiles have their own licences and attribution requirements: OSM tiles need “© OpenStreetMap contributors”, CartoDB tiles also need “© CARTO”. Do not hammer the public OSM tile server with many requests; it is run by volunteers, and its usage policy forbids bulk downloading.

Interactive web maps

A static PNG is right for the challenge, but an interactive map is often better for exploring your own data. Both libraries below draw on an OpenStreetMap basemap and use Web Mercator underneath, which is fine for looking but not for measuring.

Python
R
JavaScript
# pip install folium
m = cafes.explore(column="outdoor_seating", tooltip="name", tiles="CartoDB positron")
m.save("cafes.html")        # open it in a browser, or display m in a notebook

QGIS

QGIS is free, runs everywhere, and is the right tool when you would rather click than code, or want to polish a map made in code.

  1. Drag the GeoJSON files into the window.

  2. Set the project CRS to EPSG:28992, bottom right.

  3. Layer Properties → Symbology for colours, sizes and classification (the same equal interval, quantile and natural breaks choices as in Map design).

  4. Vector → Research Tools → Create Grid makes square or hexagon grids; Vector → Analysis Tools → Count Points in Polygon fills them.

  5. Browser → XYZ Tiles → OpenStreetMap adds a basemap.

  6. Project → New Print Layout adds a title, legend, scale bar and credits, then Export as Image.

Days that suit QGIS especially well: 3 Polygons, 20 Hexagons, 29 Raster.