Altair + hrbrthemes = Beautiful and readable charts in Python

Custom theme for altair in Python
Author

Yu-En Hsu

Published

April 23, 2024

Code
from dotenv import dotenv_values
import pandas as pd
from requests import request
import altair as alt

config = dotenv_values(".env")
response = request("GET", f"https://api.eia.gov/v2/electricity/retail-sales/data/?api_key={config['API_KEY_EIA']}&frequency=monthly&data[0]=customers&data[1]=price&facets[sectorid][]=RES&facets[stateid][]=ENC&facets[stateid][]=ESC&facets[stateid][]=MAT&facets[stateid][]=MTN&facets[stateid][]=NEW&facets[stateid][]=PACC&facets[stateid][]=PACN&facets[stateid][]=SAT&start=2020-01&sort[0][column]=period&sort[0][direction]=asc&offset=0&length=5000")
result = response.json()

raw_data = pd.json_normalize(result['response']['data'])
one_cols = raw_data.nunique().loc[lambda x: x==1].index
ref = raw_data[
    raw_data.nunique().loc[lambda x: x==1].index
].drop_duplicates().T.to_dict()[0]

df = raw_data.drop(columns=one_cols).assign(
    period=pd.to_datetime(raw_data['period']+"-01"),
    customers=pd.to_numeric(raw_data['customers']),
    price=pd.to_numeric(raw_data['price'])
)
alt.Chart(
    df, 
    width="container",
    title=alt.Title(
        "Average monthly price of electricity", 
        subtitle=["Source: U.S. Energy Information Administration"])
).mark_line().encode(
    x=alt.X("period", title="Period"), 
    y=alt.Y("price:Q", title="Cents per kWh"), 
    color=alt.Color("stateid:N", title="Region").sort("-y"),
).display(actions=False)
Figure 1: Use altair default setting

I find the chart difficult to read, mostly because of the tiny text. Is it because I am among 4% (Banashefski, Rhee, and Lema 2023) of population with severe myopia? Maybe. Or, perhaps the text size is truly too small?

hrbrthemes inspired altair theme

I am using Roboto Condensed as the example here, but hrbrthemes (Rudis et al. 2019) provides other font options.

Figure 2: hrbrthemes Roboto Condensed example

Adopted features

  • Sufficient spacing around the chart and between elements
  • Chart title: Largest size and bold weight font
  • Chart subtitle: Second largest size and light weight font
  • Axis label: Fontsize comparable to subtitle and normal weight font
  • Axis domain/baseline and ticks: Removed

Not adopted

  • Axis title alignment
  • Caption: Not available in altair

FYI

  • The first tick is aligned to the boundary of the axis. The remaining ticks are centered.
  • ggplot2 doesn’t always display zero.

Customising altair

Loading Google font

I’m using JupyterLab, so I used magic command to load the fonts not available on my system.

%%html
<style>
@import url('https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,100..900;1,100..900&display=swap')
</style>

Defining custom theme

It’s essentially a function that returns a dictionary of configuration

def theme_ipsum_rc() -> dict:
    font_family = "Roboto Condensed"
    weight_light = 300
    weight_bold = 500
    size_base = 15
    size_sm = 13
    size_xs = 12
    return {
        "config": {
            "font": font_family,
            "padding": 30,
            "title": {
                "fontSize": 22,
                "fontWeight": weight_bold,
                "subtitleFontSize": 17,
                "subtitleFontWeight": weight_light,
                "subtitlePadding": 10,
                "frame": "group",
                "dy": -20,
                "anchor": "start",
            },
            "axis": {
                "labelFontSize": size_base,
                "labelFontWeight": weight_light,
                "titleFontSize": size_xs,
                "titleFontWeight": weight_bold,
                "domain": False,
                "ticks": False,
            },
            "header": {
                "labelFontSize": 18,
                "labelFontWeight": weight_bold,
                "labelAlign": "left",
                "labelAnchor": "start",
                "labelPadding": 20,
                "title": None,
            },
            "legend": {
                "labelFontSize": size_sm,
                "titleFontSize": size_base,
                "titleFontWeight": weight_bold,
                "columnPadding": 13,
                "rowPadding": 8,
                "titlePadding": 10,
            },
            "mark": {
                "fontSize": size_base,
            },
            "view": {"stroke": None},
        }
    }

Registering and using the theme

Calling alt.themes.enable(theme_name) affects all charts throughout the session. To only apply it on a single chart, use with statement as described in Changing the Theme.

# Register
alt.themes.register("theme_ipsum_rc", theme_ipsum_rc)
# Enable
alt.themes.enable("theme_ipsum_rc")

# Chart
alt.Chart(
    df, 
    width="container",
    title=alt.Title(
        "Average monthly price of electricity", 
        subtitle=["Source: U.S. Energy Information Administration"])
).mark_line().encode(
    x=alt.X("period", title="Period"), 
    y=alt.Y("price:Q", title="Cents per kWh"), 
    color=alt.Color("stateid:N", title="Region").sort("-y"),
).display(renderer="svg", actions=False)
Figure 3: Use custom theme. If you’re viewing on mobile, it’d look cropped. See Figure 4 (b) for the full chart.

Exporting to PNG

Since the font is not available on my device, saving the chart with chart.save("output.png") uses the default system font. Downloading and adding the fonts could probably work, but I am looking for more of a temporary workaround. So here’s what I did:

First, I download the fonts from Google Fonts and unzipped the file. Then I specified the unzipped font directory with vl_convert.

To install via pip:

pip install vl-convert-python

Once installed, import the package and specify the folder:

import vl_convert
vl_convert.register_font_directory(font_dir='FONT_DIRECTORY')

Then save the chart again, and it should use the custom font. See System font requirements for more detail.

(a) System font
(b) Custom font
Figure 4: Altair outputs

References

Banashefski, Bryana, Michelle K. Rhee, and Gareth M. C. Lema. 2023. “High Myopia Prevalence Across Racial Groups in the United States: A Systematic Scoping Review.” Journal of Clinical Medicine 12 (8). https://doi.org/10.3390/jcm12083045.
Rudis, boB, Joris Meys, Matthew Pancia, Karl Dunkle Werner, Xavier A, Phil, Lukas Burk, Jacob, Bhaskar V. Karambelkar, and Ben Marwick. 2019. “Hrbrmstr/Hrbrthemes: Dark Matter.” Zenodo. https://doi.org/10.5281/zenodo.2545422.