Now that we’ve been able to import our first type of spatial data and begin manipulating it using our familiar libraries, we want to put it on a map!
We begin with the code we developed last time for accessing the redlining data for a single city as a geopandas object:
from IPython.display import IFrame
import ibis
from ibis import _
con = ibis.duckdb.connect()
city = (con
.read_geo("/vsicurl/https://dsl.richmond.edu/panorama/redlining/static/mappinginequality.gpkg")
.filter(_.city == "New Haven")
.execute()
)
We will be using leafmap
to create interactive maps. There are a wide range of mapping packages that have been developed for python (e.g. bokeh, kepler, folium, pydeck, lonboard, pymaplibre-gl), most of which wrap around javascript mapping libraries designed for interactive web-based maps. Just as the ibis
package provides an interface to a host of different database engines that can be used as ‘backends’, leafmap wraps around most of the common mapping engines with a friendly and mostly consistent user interface. In ibis
we have focused on the duckdb
backend as the most powerful, flexible and performant option for all our purposes. With leafmap
, we will likewise focus on the most recent and generally most powerful backend option, maplibregl
- an open source implementation of the powerful and widely used commericial javascript platform, Mapbox.
import leafmap.maplibregl as leafmap
m = leafmap.Map(style="positron")
m.add_gdf(city)
m
Because our map is an interactive javascript applet, putting m
at the end of the a cell will allow it to display inline in Jupyter, but not in ‘static’ documentation such as the course website or a GitHub rendering of the notebook. To share our map, we can export it to an HTML file. This allows any web browser to view and interact with the map without having to install jupyter
or python
.
m.to_html("../data/nh1.html", overwrite=True)
# display map on course website by using an iframe to the output URL
IFrame(src='https://espm-157.github.io/static-maps/nh1.html', width=700, height=400)
Map styling¶
Leafmap allows us to add many layers to our map from various data sources, and gives us a rich set of tools for styling the look and feel of most of those data layers. The example below illustrates some more complex patterns for generating a rich map.
- We introduce a basemap with
add_basemap()
. This is just a background image, that is tiled to re-draw to the appropriate zoom and window. However, note that basemaps are just background images, we cannot extract actual data from these sources. - We have added source data by pointing to a remote URL. This provides much better performance than embedding the JSON data directly, especially when the data become very large. This requires the data is hosted at a highly available provider, such as GitHub or another cloud service. The original host source won’t work unfortunately.
- We have used a custom filter inside the source data to select our city of interest. This could be omitted.
- We have colored our polygons according to grade given in the data. Specifically, we noticed that the column “fill” already contains color codes in HEX format corresponding to the redlining “grade” associated with each layer.
- We have added a final layer of text labels based on the “label” column.
import json
geojson = json.loads(city.to_json())
m = leafmap.Map(center=[-72.9, 41.3], zoom=12)
source = {
"type": "geojson",
# 'data': geojson,
'data': 'https://data.source.coop/cboettig/us-boundaries/mappinginequality.json',
'filter': ['==', ['get', 'city'], 'New Haven']
}
layer = {
"id": "geojson-layer",
"type": "fill",
"source": "geojson",
'paint': {
"fill-color": ["get", "fill"], # color by the column called "fill"
"fill-opacity": 0.8,
},
}
text_layer = {
"id": "labels",
"type": "symbol",
"source": "geojson",
"layout": {
"text-field": ["get", "label"],
"text_size": 14,
'text-font': ['Open Sans Bold'],
"text-anchor": "center",
},
'paint': { 'text-color': '#ffffff'}
}
m.add_basemap("USGS.USTopo")
m.add_source("geojson", source)
m.add_layer(layer)
m.add_layer(text_layer)
m.add_layer_control()
m.to_html("../data/nh2.html", overwrite=True)
m
# display map on course website by using an iframe to the output URL
IFrame(src='https://espm-157.github.io/static-maps/nh2.html', width=700, height=400)
## list all basemaps known to leafmap
list(leafmap.basemaps.keys())
['OpenStreetMap',
'Google Maps',
'Google Satellite',
'Google Terrain',
'Google Hybrid',
'FWS NWI Wetlands',
'FWS NWI Wetlands Raster',
'NLCD 2021 CONUS Land Cover',
'NLCD 2019 CONUS Land Cover',
'NLCD 2016 CONUS Land Cover',
'NLCD 2013 CONUS Land Cover',
'NLCD 2011 CONUS Land Cover',
'NLCD 2008 CONUS Land Cover',
'NLCD 2006 CONUS Land Cover',
'NLCD 2004 CONUS Land Cover',
'NLCD 2001 CONUS Land Cover',
'USGS NAIP Imagery',
'USGS NAIP Imagery False Color',
'USGS NAIP Imagery NDVI',
'USGS Hydrography',
'USGS 3DEP Elevation',
'USGS 3DEP Elevation Index',
'ESA Worldcover 2020',
'ESA Worldcover 2020 S2 FCC',
'ESA Worldcover 2020 S2 TCC',
'ESA Worldcover 2021',
'ESA Worldcover 2021 S2 FCC',
'ESA Worldcover 2021 S2 TCC',
'BaseMapDE.Color',
'BaseMapDE.Grey',
'BasemapAT.basemap',
'BasemapAT.grau',
'BasemapAT.highdpi',
'BasemapAT.orthofoto',
'BasemapAT.overlay',
'BasemapAT.surface',
'BasemapAT.terrain',
'CartoDB.DarkMatter',
'CartoDB.DarkMatterNoLabels',
'CartoDB.DarkMatterOnlyLabels',
'CartoDB.Positron',
'CartoDB.PositronNoLabels',
'CartoDB.PositronOnlyLabels',
'CartoDB.Voyager',
'CartoDB.VoyagerLabelsUnder',
'CartoDB.VoyagerNoLabels',
'CartoDB.VoyagerOnlyLabels',
'CyclOSM',
'Esri.AntarcticBasemap',
'Esri.AntarcticImagery',
'Esri.ArcticImagery',
'Esri.ArcticOceanBase',
'Esri.ArcticOceanReference',
'Esri.NatGeoWorldMap',
'Esri.OceanBasemap',
'Esri.WorldGrayCanvas',
'Esri.WorldImagery',
'Esri.WorldPhysical',
'Esri.WorldShadedRelief',
'Esri.WorldStreetMap',
'Esri.WorldTerrain',
'Esri.WorldTopoMap',
'FreeMapSK',
'Gaode.Normal',
'Gaode.Satellite',
'HikeBike.HikeBike',
'HikeBike.HillShading',
'JusticeMap.americanIndian',
'JusticeMap.asian',
'JusticeMap.black',
'JusticeMap.hispanic',
'JusticeMap.income',
'JusticeMap.multi',
'JusticeMap.nonWhite',
'JusticeMap.plurality',
'JusticeMap.white',
'MtbMap',
'NASAGIBS.ASTER_GDEM_Greyscale_Shaded_Relief',
'NASAGIBS.BlueMarble',
'NASAGIBS.BlueMarble3031',
'NASAGIBS.BlueMarble3413',
'NASAGIBS.BlueMarbleBathymetry3031',
'NASAGIBS.BlueMarbleBathymetry3413',
'NASAGIBS.MEaSUREsIceVelocity3031',
'NASAGIBS.MEaSUREsIceVelocity3413',
'NASAGIBS.ModisAquaBands721CR',
'NASAGIBS.ModisAquaTrueColorCR',
'NASAGIBS.ModisTerraAOD',
'NASAGIBS.ModisTerraBands367CR',
'NASAGIBS.ModisTerraBands721CR',
'NASAGIBS.ModisTerraChlorophyll',
'NASAGIBS.ModisTerraLSTDay',
'NASAGIBS.ModisTerraSnowCover',
'NASAGIBS.ModisTerraTrueColorCR',
'NASAGIBS.ViirsEarthAtNight2012',
'NASAGIBS.ViirsTrueColorCR',
'OPNVKarte',
'OneMapSG.Default',
'OneMapSG.Grey',
'OneMapSG.LandLot',
'OneMapSG.Night',
'OneMapSG.Original',
'OpenAIP',
'OpenFireMap',
'OpenRailwayMap',
'OpenSeaMap',
'OpenSnowMap.pistes',
'OpenStreetMap.BZH',
'OpenStreetMap.BlackAndWhite',
'OpenStreetMap.CH',
'OpenStreetMap.DE',
'OpenStreetMap.HOT',
'OpenStreetMap.Mapnik',
'OpenTopoMap',
'SafeCast',
'Stadia.AlidadeSatellite',
'Stadia.AlidadeSmooth',
'Stadia.AlidadeSmoothDark',
'Stadia.OSMBright',
'Stadia.Outdoors',
'Stadia.StamenTerrain',
'Stadia.StamenTerrainBackground',
'Stadia.StamenTerrainLabels',
'Stadia.StamenTerrainLines',
'Stadia.StamenToner',
'Stadia.StamenTonerBackground',
'Stadia.StamenTonerLabels',
'Stadia.StamenTonerLines',
'Stadia.StamenTonerLite',
'Stadia.StamenWatercolor',
'Strava.All',
'Strava.Ride',
'Strava.Run',
'Strava.Water',
'Strava.Winter',
'SwissFederalGeoportal.JourneyThroughTime',
'SwissFederalGeoportal.NationalMapColor',
'SwissFederalGeoportal.NationalMapGrey',
'SwissFederalGeoportal.SWISSIMAGE',
'TopPlusOpen.Color',
'TopPlusOpen.Grey',
'USGS.USImagery',
'USGS.USImageryTopo',
'USGS.USTopo',
'WaymarkedTrails.cycling',
'WaymarkedTrails.hiking',
'WaymarkedTrails.mtb',
'WaymarkedTrails.riding',
'WaymarkedTrails.skating',
'WaymarkedTrails.slopes',
'nlmaps.grijs',
'nlmaps.luchtfoto',
'nlmaps.pastel',
'nlmaps.standaard',
'nlmaps.water']
Performance¶
Visualizing data on an interactive map is a challenging technical task because of the potential size of the data: To show the user data while they pan and zoom around the map across scales from city to planet-wide can involve trillions of pixels that must be loaded, plotted and styled. Clever software takes advantage of the users’s current zoom and field of view to render only the data the user can currently see, loading more data ‘on demand’ as the user moves the map. Careful preparation of data layers into “tiles” pre-computed at different “zooms”, data compression, and data chunking can dramatically improve the map’s ability to render massive datasets smoothly.