Vega-Altair — a surprisingly powerful Python library for plotting interactive maps

Rachel Wilcock
Data science at Nesta
7 min readMay 4, 2023

When it comes to plotting geospatial data, Python offers a wide range of options e.g. GeoPandas, Folium, Cartopy and Plotly. Each of these libraries offers a different way of plotting geospatial data, for example, Folium is built on the powerful leaflet.js library and can produce zoomable maps with a base layer from OpenStreetMap, with the ability to layer on a number of different options including choropleths, markers and heat maps.

However, this is where Vega-Altair enters as an unusual candidate. Built on Vega and Vega-Lite, it’s a plotting tool whose strength lies in its simple but elegantly efficient declarative code which creates powerful, interactive visualisations. To browse the full range of visualisations and to provide inspiration for your next data viz project — visit their gallery. In this blog post, we demonstrate its functionality through a tutorial, concentrating on two types of graph — choropleth maps and bar charts — and most importantly, the interactivity between them.

Interactivity between charts

Figure 1: A gif demonstrating the interactivity between two charts plotted using Vega-Altair. As selections are made on the bar chart, the chart above changes to reflect only counts of IMDB and Rotten Tomato ratings in that film genre.

One of the biggest advantages Vega-Altair has, aside from its easy declarative code, is the ability to have interactions between two charts. This is incredibly useful when you want to add a new level of exploration into your data viz. At Nesta, we find it particularly helpful to put the data back into the hands of the domain experts. It allows them to explore their own data and use their knowledge to guide their exploration. In a world where data viz no longer needs to be printed out or kept in static documents, interactive graphs are an incredibly powerful tool in any data scientist’s toolbox.

The gif (Figure 1) shows how two different types of graphs can interact with each other in Vega-Altair. For more information on how to plot the figure in the gif, follow this link. There are lots of further examples on the website, but in the a fairer start team at Nesta, we spend a lot of time creating maps, so here we focus on the interactivity between a choropleth map and a simple bar chart.

Choropleth maps provide people with a new way of looking at their data. By plotting data geospatially, previously undiscovered trends and patterns reveal themselves. By combining with an interactive bar chart which responds to selections made on the choropleth map, relationships between variables are taken into account making it possible to view trends through the filter of a geographical boundary.

Plotting a choropleth map tutorial

We start this tutorial by demonstrating how to plot a choropleth map in Vega-Altair. As a simple starting point we’re going to use the data from the English Indices of Deprivation (IoD) from 2019 (the latest release).

For more information around the IoD, please follow this link, but broadly they cover seven domains of deprivation (income, employment, education, health, crime, barriers to housing and services and living environment). These are then combined through different weightings and used to rank every Lower Layer Super Output Area (LSOA) in England. These LSOAs can then be aggregated to find the average Index of Multiple Deprivation (IMD) for each Local Authority or any other geographical area of interest.

The dataset we’re using has been cleaned and preprocessed and is available to download here. Before we get started plotting the map, we first need to import the necessary packages and import the data.

import pandas as pd
import altair as alt

iod_lsoa_data = pd.read_csv("lsoa_english_iod_2019.csv")

Next, we need to read in the shapefile data which will enable us to plot the LSOA outlines. In this tutorial we’re going to focus on plotting data for the LSOAs in the City of York, located in the region of Yorkshire and the Humber, in England. In the a fairer start mission at Nesta, we’ve collaborated a lot with the City of York Council, and our project designing a dashboard for them to investigate take-up of the two year old health review is the inspiration for this, and the accompanying blog on Streamlit.

geojson_lsoa = "https://raw.githubusercontent.com/nestauk/dap_medium_articles/dev/streamlit_app_tutorial/shapefiles/lsoa_clean_shapefiles_2011_yorkshire_and_the_humber_crs3857.geojson"

geodata_lsoa = alt.Data(url=geojson_lsoa, format=alt.DataFormat(property="features", type="json"))

Now we need to filter the original IoD dataset to only include York data.

iod_lsoa_data_york = iod_lsoa_data.query("lad19nm == 'York'")

We also need a list of the LSOAs that are in York so we can filter the choropleth map too.

lsoas_to_plot = list(iod_lsoa_data_york.lsoa11nm)

From the IoD data, we are going to plot the income deprivation affecting children index (IDACI). This is an index from 1 to 10, where 1 is the most deprived and 10 is the least deprived.

color_lsoa = alt.Color("income_deprivation_affecting_children:O", scale = alt.Scale(scheme="yellowgreenblue"), title="IDACI", legend = alt.Legend(orient="top"))

Here, to plot a discrete colour scale, we have specified that the IDACI is ordinal using “:O”, and that we want the colour scheme to use the “yellowgreenblue” colour palette (full list of options can be found here). We’ve also added a title to the legend — “IDACI” — and chosen for the legend to be orientated at the top of the map.

choro_lsoa = alt.Chart(geodata_lsoa).mark_geoshape(
stroke="black"
).transform_lookup(
lookup="properties.lsoa11nm",
from_=alt.LookupData(iod_lsoa_data_york,
"lsoa11nm",
["lsoa11cd", "lsoa11nm", "lad19cd", "lad19nm", "income_deprivation_affecting_children"])
).transform_filter(
alt.FieldOneOfPredicate(
field="properties.lsoa11nm", oneOf=lsoas_to_plot
)).encode(color=color_lsoa,tooltip=[alt.Tooltip("lsoa11nm:N", title="LSOA"), alt.Tooltip("income_deprivation_affecting_children:O", title="IDACI")]).project(type="identity", reflectY=True).properties(width=500,height=500)

We’re also going to configure the map to ensure it looks nice. By setting the titleLimit and labelLimit to 0, if you have a long title, it won’t be cutoff and if you set the strokeWidth to 0, it removes the box around the plot.

choro_lsoa = choro_lsoa.configure_legend(
labelLimit = 0,
titleLimit = 0,
titleFontSize = 13,
labelFontSize = 13,
symbolStrokeWidth = 1.5,
symbolSize=150
).configure_view(
strokeWidth = 0
).configure_axis(
labelLimit=0,
titleLimit=0)

And with that, the first step is done — you have a choropleth map (Figure 2)!

Figure 2: The choropleth map of York produced from the above code with a preview of the selections which are added in the next section.

Creating interactions between a map and a bar chart

Next up, we’re going to plot a bar chart showing the average income deprivation affecting children for the whole of York. This is just setting up the chart so we can add in our selections later. First we create a pandas DataFrame with two columns which are the same as the columns we’re using in the choropleth map “lsoa11nm” and “income_deprivation_affecting_children”. The values of these columns are simply “York average” and the calculated mean to decimal places, respectively.

iod_lsoa_data_york_av = pd.DataFrame({"lsoa11nm":["York average"], "income_deprivation_affecting_children":[f"{iod_lsoa_data_york.income_deprivation_affecting_children.mean():.2f}"]})

bar_chart_york_average = (alt.Chart(iod_lsoa_data_york_av).mark_bar(
color="#0000FF"
).encode(
x=alt.X("income_deprivation_affecting_children:Q",
title="IDACI",
axis=alt.Axis(tickMinStep=1)),
y=alt.Y("lsoa11nm:N",
title="LSOA Name")).properties(width=400, height=200))

So now we have two figures — an incredibly simple bar chart and a choropleth — and we are now going to enable them to interact with each other.

We do this using the “selections” part of the Vega-Altair library. There are many options, we could use point selection, single selection or multi selection. We are going to use multi selection which will enable users to hold down “Shift” and select as many LSOAs as they like which will then appear on the bar chart.

However, we want the bar chart to be originally empty, rather than show every LSOA in York, so we need a separate selection for this.

So let us define these two selections to add to our charts, defining that the selection will be on the field “lsoa11nm”.

lsoa_select = alt.selection_multi(fields=["lsoa11nm"])

lsoa_select_empty = alt.selection_multi(fields=["lsoa11nm"], empty = "none")

We also want the colour of the LSOA which are selected to remain as their IDACI colour and we want the remaining LSOAs to turn to grey. We therefore have to rewrite our previous code to reflect this.

color_lsoa = alt.condition(lsoa_select, alt.Color("income_deprivation_affecting_children:O", scale = alt.Scale(scheme="yellowgreenblue"), title="IDACI", legend = alt.Legend(orient="top")), alt.value("lightgray"))

Using alt.condition, we are adding a conditional selection onto the colour. We’re telling Vega-Altair that on a selection choice, colour the LSOAs selected using the original colour scale, and if they’re not in that selection, make them light grey.

As the main change to the code is in the colour_lsoa definition, there is very little we need to change in the main choropleth plotting code. The only addition is a single line where we define that we want to add our two selections, the first one “lsoa_select” to change the colours, and the second one “lsoa_select_empty” to add our selections to the bar chart.

choro_lsoa = (alt.Chart(geodata_lsoa).mark_geoshape(
stroke="black"
).transform_lookup(
lookup="properties.lsoa11nm",
from_=alt.LookupData(
iod_lsoa_data_york,
"lsoa11nm",
["lsoa11cd", "lsoa11nm", "lad19cd", "lad19nm", "income_deprivation_affecting_children"]
)).transform_filter(alt.FieldOneOfPredicate(field="properties.lsoa11nm", oneOf=lsoas_to_plot)
).encode(
color=color_lsoa,
tooltip=[alt.Tooltip("lsoa11nm:N", title="LSOA"), alt.Tooltip("income_deprivation_affecting_children:O", title="IDACI")]).add_selection(lsoa_select, lsoa_select_empty).project(type="identity", reflectY=True).properties(width=500,height=500))

Now we already have the York average bar chart, which we would like to stay static. We’re therefore not adding any selections to this. However, we would like a bar chart where the LSOAs selected on the map appear on the figure for comparison — note how we’re using “lsoa_select_empty” as a “transform_filter”.

bar_chart = (alt.Chart(iod_lsoa_data_york).mark_bar(color="#0000FF").encode(
x=alt.X("income_deprivation_affecting_children:Q",
title="IDACI",
axis=alt.Axis(tickMinStep=1)),
y=alt.Y("lsoa11nm:N", title="LSOA Name")).transform_filter(lsoa_select_empty).properties(width=400, height=200))

We can now layer the two bar charts into one, the York average one and the LSOA selected one so the York average will always appear even if no LSOAs are selected.

This is one line of code and can be done incredibly simply in Vega-Altair:

bar_charts_combined = bar_chart_york_average + bar_chart

We can now combine the bar chart with the choropleth map using alt.vconcat and adding in some extra configuration to make it look pretty.

combined_charts = alt.vconcat(choro_lsoa, bar_charts_combined, center=True).configure_legend(labelLimit=0,titleLimit=0,titleFontSize=13,labelFontSize=13,symbolStrokeWidth=1.5,symbolSize=150).configure_view(strokeWidth=0).configure_axis(labelLimit=0,titleLimit=0)

And ta da 🎉, you now have an interactive choropleth map which should look something similar to below (Figure 3).

Figure 3: The final completed figure showing the interactivity of the choropleth map with the layered bar chart. The layered bar chart is a plot of the York IDACI average and a plot of the IDACI for the York LSOAs selected on the choropleth plotted on top of each other in the same chart.

If you’d like to save your altair figure as html, use the single line of code below:

combined_charts.save("York_IDACI_map_and_bar_chart.html")

Thanks for following along, hopefully this has helped to show you one of the many use cases for interactive figures using Vega-Altair. Fully commented code for this example can be found in a Jupyter notebook here. If you’re interested in learning how you can add this easily into a Streamlit app, check out our accompanying blog to this piece “How to create a Streamlit app using Python”.

--

--

Rachel Wilcock
Data science at Nesta

Senior Data Science Lead in the A Fairer Start Mission @ Nesta. Interested in using data in a fair and equitable way to improve outcomes for children.