Consistently Beautiful Visualizations with Altair Themes | by Sergio Sánchez | Towards Data Science

If you are a data visualization fan or practitioner who also uses python you may have heard of Jake Vanderplas and Brian Granger's altair: "a declarative statistical visualization library for Python, based on Vega and Vega-lite".
With Altair, you can spend more time understanding your data and its meaning. Altair’s API is simple, friendly and consistent and built on top of the powerful Vega-Lite visualization grammar. This elegant simplicity produces beautiful and effective visualizations with a minimal amount of code.
In this piece we’ll be digging deeper into one of altair's less known features: themes.
A theme, in altair, is a set of chart configurations applied globally each python session. This means you can produce similar-looking visualizations consistently.
Maybe you are working on developing a personal style for your blog or maybe you are part of a company that already has a style in place or maybe you hate gridlines and are tired of turning them off every single time you create a chart. Having a style guide to follow is always a benefit when you are producing data visualizations.
Rather than explaining the value of style-guides and consistency in your visualizations, in this article we will explore how to implement one in altair by recreating the Urban Institute's Data Visualization Style Guide.
Here’s a simple example from the docs:
import altair as altfrom vega_datasets import data# define the theme by returning the dictionary of configurationsdef black_marks():    return {        'config': {            'view': {                'height': 300,                'width': 400,            },            'mark': {                'color': 'black',                'fill': 'black'            }        }    }# register the custom theme under a chosen namealt.themes.register('black_marks', black_marks)# enable the newly registered themealt.themes.enable('black_marks')# draw the chartcars = data.cars.urlalt.Chart(cars).mark_point().encode(    x='Horsepower:Q',    y='Miles_per_Gallon:Q')
height and width remained the same as the default theme but we have now included color and fill values to be applied globally (unless otherwise specified) to any charts generated from this point until the end of this python session.
This would be the equivalent of
alt.Chart(cars).mark_point(color = 'black', fill = 'black').encode(    x='Horsepower:Q',    y='Miles_per_Gallon:Q')
in the black_marks config dictionary returned you can see that we specified the value black for the keys color and fill in mark. This is the format all these specifications follow. For example, if you wanted to configure the left axis' label's font size:
def my_theme():    return {        'config': {            'view': {                'height': 300,                'width': 400,            },            'mark': {                'color': 'black',                'fill': '#000000',            },            'axisLeft': {                'labelFontSize': 30,            },        }    }# register the custom theme under a chosen namealt.themes.register('my_theme', my_theme)# enable the newly registered themealt.themes.enable('my_theme')
(side note: you can get these specifications (i.e. 'axisLeft') from Vega-Lite's documentation
This can be particularly useful if you or your company have a style guide you have to follow. Even if you don’t have a style guide, you can start building one by saving your configurations on your personal theme rather than in your viz code (it pays off long-term!).
Vega already has some themes on GitHub.
So let’s build a simplified Urban Institute’s altair theme.
All the information for this simplified version of Urban Institute’s style can be found in this graphic from their GitHub page.
The basic anatomy of the theme is as follows:
def theme_name():return {        "config": {             "TOP_LEVEL_OBJECT": { # i.e. "title", "axisX", "legend",                "CONFIGURATION": "VALUE",                "ANOTHER_ONE": "ANOTHER_VALUE",                "MAYBE_A_SIZE": 14, # values can be a string, boolean, or number,            },            "ANOTHER_OBJECT": {                "CONFIGURATION": "VALUE",            }        }    }
We will configure the top-level objects “title”, “axisX”, “axisY”, “range”, and “legend”, plus a few other one-liner specifications.
Now, off the bat we saw that Urban uses the “Lato” font on all text, we can save that as a variable so we don’t have to reuse it.
“title”
“title”
Titles at Urban are 18px in size, Lato font, left-aligned, black.
def urban_theme():    font = "Lato"return {        "config": {            "title": {                "fontSize": 18,                "font": font,                "anchor": "start", # equivalent of left-aligned.                "fontColor": "#000000"            }        }    }
At this point you could register and enable this theme and all your altair charts in this python session would have an Urban-Institute-looking title.
# registeralt.themes.register("my_custom_theme", urban_theme)# enablealt.themes.enable("my_custom_theme")
If you do not have “Lato” font installed in your computer you can run this code in a cell to import it to your browser from Google Fonts for the time being.
%%html<style>@import url('https://fonts.googleapis.com/css?family=Lato');</style>
“axisX” and “axisY”
“axisX” and “axisY”
At Urban the X-axis and Y-axis differ slightly. X-axes have what in altair is referred to as domain, the line that runs across the axis. Y-axes do not have this (similar to seaborn's sns.despine()). Y-axes have gridlines, X-axes do not. X-axes have ticks, Y-axes do not. But both have same-size labels and titles.
def urban_theme():    # Typography    font = "Lato"    # At Urban it's the same font for all text but it's good to keep them separate in case you want to change one later.    labelFont = "Lato"     sourceFont = "Lato"    # Axes    axisColor = "#000000"    gridColor = "#DEDDDD"return {        "config": {            "title": {                "fontSize": 18,                "font": font,                "anchor": "start", # equivalent of left-aligned.                "fontColor": "#000000"            },            "axisX": {                "domain": True,                "domainColor": axisColor,                "domainWidth": 1,                "grid": False,                "labelFont": labelFont,                "labelFontSize": 12,                "labelAngle": 0,                 "tickColor": axisColor,                "tickSize": 5, # default, including it just to show you can change it                "titleFont": font,                "titleFontSize": 12,                "titlePadding": 10, # guessing, not specified in styleguide                "title": "X Axis Title (units)",             },            "axisY": {                "domain": False,                "grid": True,                "gridColor": gridColor,                "gridWidth": 1,                "labelFont": labelFont,                "labelFontSize": 12,                "labelAngle": 0,                 "ticks": False, # even if you don't have a "domain" you need to turn these off.                "titleFont": font,                "titleFontSize": 12,                "titlePadding": 10, # guessing, not specified in styleguide                "title": "Y Axis Title (units)",                 # titles are by default vertical left of axis so we need to hack this                 "titleAngle": 0, # horizontal                "titleY": -10, # move it up                "titleX": 18, # move it to the right so it aligns with the labels             },}    }
If you registered and enabled this theme you’d have something that kinda, sorta, looks like an Urban Institute’s chart. But what lets you know right away that you are looking at an Urban Institute’s data visualization is the colors.
In altair, you have scales with domain and range. These are "functions that transforms values in the data domain (numbers, dates, strings, etc) to visual values (pixels, colors, sizes) of the encoding channels." So if you want to add a default color scheme all you have to do is encode in your theme the values for the top-level object "range".
We’ll save the values as a list main_palette and sequential_palette. The Urban Institute's Data Visualization Style Guide has a lot of color combinations. We will encode these two as defaults but when it comes to colors you will most likely end up modifying your data visualization on the go.
def urban_theme():    # Typography    font = "Lato"    # At Urban it's the same font for all text but it's good to keep them separate in case you want to change one later.    labelFont = "Lato"     sourceFont = "Lato"    # Axes    axisColor = "#000000"    gridColor = "#DEDDDD"    # Colors    main_palette = ["#1696d2",                     "#d2d2d2",                    "#000000",                     "#fdbf11",                     "#ec008b",                     "#55b748",                     "#5c5859",                     "#db2b27",                    ]    sequential_palette = ["#cfe8f3",                           "#a2d4ec",                           "#73bfe2",                           "#46abdb",                           "#1696d2",                           "#12719e",                          ]return {        "config": {            "title": {                "fontSize": 18,                "font": font,                "anchor": "start", # equivalent of left-aligned.                "fontColor": "#000000"            },            "axisX": {                "domain": True,                "domainColor": axisColor,                "domainWidth": 1,                "grid": False,                "labelFont": labelFont,                "labelFontSize": 12,                "labelAngle": 0,                 "tickColor": axisColor,                "tickSize": 5, # default, including it just to show you can change it                "titleFont": font,                "titleFontSize": 12,                "titlePadding": 10, # guessing, not specified in styleguide                "title": "X Axis Title (units)",             },            "axisY": {                "domain": False,                "grid": True,                "gridColor": gridColor,                "gridWidth": 1,                "labelFont": labelFont,                "labelFontSize": 12,                "labelAngle": 0,                 "ticks": False, # even if you don't have a "domain" you need to turn these off.                "titleFont": font,                "titleFontSize": 12,                "titlePadding": 10, # guessing, not specified in styleguide                "title": "Y Axis Title (units)",                 # titles are by default vertical left of axis so we need to hack this                 "titleAngle": 0, # horizontal                "titleY": -10, # move it up                "titleX": 18, # move it to the right so it aligns with the labels             },            "range": {                "category": main_palette,                "diverging": sequential_palette,            }}    }
At this point, your theme will have the Urban Institute’s title, axes, and color configurations by default. Pretty cool but that’s not all you can do.
Let’s add a default legend configuration. This time we’ll stray away from the style guide a little because the position of the legend depends on the chart at hand (and you can’t have a horizontal legend in Vega-lite).
This code also includes “view” and “background” configurations which are easy to follow without much explanation. It also includes the configurations for “area”, “line”, “trail”, “bar”, “point”, and other marks. This is just setting up the colors right for each specific mark.
def urban_theme():    # Typography    font = "Lato"    # At Urban it's the same font for all text but it's good to keep them separate in case you want to change one later.    labelFont = "Lato"     sourceFont = "Lato"    # Axes    axisColor = "#000000"    gridColor = "#DEDDDD"    # Colors    main_palette = ["#1696d2",                     "#d2d2d2",                    "#000000",                     "#fdbf11",                     "#ec008b",                     "#55b748",                     "#5c5859",                     "#db2b27",                    ]    sequential_palette = ["#cfe8f3",                           "#a2d4ec",                           "#73bfe2",                           "#46abdb",                           "#1696d2",                           "#12719e",                          ]return {        # width and height are configured outside the config dict because they are Chart configurations/properties not chart-elements' configurations/properties.        "width": 685, # from the guide        "height": 380, # not in the guide        "config": {            "title": {                "fontSize": 18,                "font": font,                "anchor": "start", # equivalent of left-aligned.                "fontColor": "#000000"            },            "axisX": {                "domain": True,                "domainColor": axisColor,                "domainWidth": 1,                "grid": False,                "labelFont": labelFont,                "labelFontSize": 12,                "labelAngle": 0,                 "tickColor": axisColor,                "tickSize": 5, # default, including it just to show you can change it                "titleFont": font,                "titleFontSize": 12,                "titlePadding": 10, # guessing, not specified in styleguide                "title": "X Axis Title (units)",             },            "axisY": {                "domain": False,                "grid": True,                "gridColor": gridColor,                "gridWidth": 1,                "labelFont": labelFont,                "labelFontSize": 12,                "labelAngle": 0,                 "ticks": False, # even if you don't have a "domain" you need to turn these off.                "titleFont": font,                "titleFontSize": 12,                "titlePadding": 10, # guessing, not specified in styleguide                "title": "Y Axis Title (units)",                 # titles are by default vertical left of axis so we need to hack this                 "titleAngle": 0, # horizontal                "titleY": -10, # move it up                "titleX": 18, # move it to the right so it aligns with the labels             },            "range": {                "category": main_palette,                "diverging": sequential_palette,            },            "legend": {                "labelFont": labelFont,                "labelFontSize": 12,                "symbolType": "square", # just 'cause                "symbolSize": 100, # default                "titleFont": font,                "titleFontSize": 12,                "title": "", # set it to no-title by default                "orient": "top-left", # so it's right next to the y-axis                "offset": 0, # literally right next to the y-axis.            },            "view": {                "stroke": "transparent", # altair uses gridlines to box the area where the data is visualized. This takes that off.            },            "background": {                "color": "#FFFFFF", # white rather than transparent            },            ### MARKS CONFIGURATIONS ###            "area": {               "fill": markColor,           },           "line": {               "color": markColor,               "stroke": markColor,               "strokeWidth": 5,           },           "trail": {               "color": markColor,               "stroke": markColor,               "strokeWidth": 0,               "size": 1,           },           "path": {               "stroke": markColor,               "strokeWidth": 0.5,           },           "point": {               "filled": True,           },           "text": {               "font": sourceFont,               "color": markColor,               "fontSize": 11,               "align": "right",               "fontWeight": 400,               "size": 11,           },            "bar": {                "size": 40,                "binSpacing": 1,                "continuousBandSize": 30,                "discreteBandSize": 30,                "fill": markColor,                "stroke": False,            },}    }
I personally save these themes in a .py script with
import altair as altalt.themes.register("my_custom_theme", urban_theme)alt.themes.enable("my_custom_theme")
at the end and just %run theme.py in my jupyter notebook.
Here are some examples of charts created with that theme.
There are a lot of ways to configure your theme and I would encourage you to try many different things. You can take this theme we just put together and play around with the values. Use a different font, font size, color-scheme. Have gridlines, don’t have gridlines. Have really, really, big labels for your axes. Do whatever you want but save it somewhere so you can build on it and grow your personal style.
While you may want to look up more on this in the altairdocumentation, I find it better to use the Vega-lite documentation. After all, altair is a python-wrapper for Vega-lite.
All images and code can be found in this GitHub repo.
Any comments, questions, tips? I’m on twitter, linkedin and Instagram.