Gapminders#

panelaltairhvplotbokehplotlymatplotlib
Published: January 1, 2018 · Modified: July 17, 2024


In his Gapminder example during a 2006 TED Talk, Hans Rosling debunked stereotypes about developed and undeveloped countries using statistics and data visualization, revealing the nuanced reality of global development. We will be recreating this example using four different plotting libraries (Matplotlib, Plotly, Vega-Altair, hvPlot, which will be controlled by widgets from Panel.

Gapminder app with 4 plots

Imports and Configuration#

import numpy as np 
import pandas as pd
import panel as pn

import altair as alt
import plotly.graph_objs as go
import plotly.io as pio
import matplotlib.pyplot as plt
import matplotlib as mpl
import hvplot.pandas  # noqa
import warnings

warnings.simplefilter('ignore')
pn.extension('vega', 'plotly', defer_load=True, sizing_mode="stretch_width")
mpl.use('agg')

Let’s also define some constant variables for our plots.

XLABEL = 'GDP per capita (2000 dollars)'
YLABEL = 'Life expectancy (years)'
YLIM = (20, 90)
HEIGHT=500 # pixels
WIDTH=500 # pixels
ACCENT="#D397F8"
PERIOD = 1000 # miliseconds

Load Dataset#

We’ll load the Gapminder dataset into a Pandas DataFrame. This dataset contains information about GDP per capita, life expectancy, population, and continent for various countries over several years.

dataset = pd.read_csv('./data/gapminders.csv')
dataset.sample(10)
country year pop continent lifeExp gdpPercap
1570 Tunisia 2002 9770575.0 Africa 73.042 5722.895655
677 Hungary 1977 10637171.0 Europe 69.950 11674.837370
783 Jamaica 1967 1861096.0 Americas 67.510 6124.703451
567 Germany 1967 76368453.0 Europe 70.800 14745.625610
963 Mauritania 1967 1230542.0 Africa 46.289 1421.145193
1406 South Africa 1962 18356657.0 Africa 49.951 5768.729717
239 Cameroon 2007 17696293.0 Africa 50.430 2042.095240
1422 Spain 1982 37983310.0 Europe 76.300 13926.169970
1487 Switzerland 2007 7554661.0 Europe 81.701 37506.419070
1096 New Zealand 1972 2929100.0 Oceania 71.890 16046.037280

We’ll also create a constant variable YEARS containing all the unique years in our dataset.

YEARS = [int(year) for year in dataset.year.unique()]
YEARS
[1952, 1957, 1962, 1967, 1972, 1977, 1982, 1987, 1992, 1997, 2002, 2007]

Define Helper Functions#

To keep our code clean and reusable, we define some helper functions. These functions will handle data extraction, title generation, and axis limits calculation.

def get_data(year):
    df = dataset[(dataset.year==year) & (dataset.gdpPercap < 10000)].copy()
    df['size'] = np.sqrt(df['pop']*2.666051223553066e-05)
    df['size_hvplot'] = df['size']*6
    return df

def get_title(library, year):
    return f"{library}: Life expectancy vs. GDP, {year}"

def get_xlim(data):
    return (dataset['gdpPercap'].min()-100, dataset[dataset['gdpPercap'] < 10000].max()['gdpPercap']+1000)

Define Plotting Functions#

We will define separate functions for each plotting library. Each function will create a plot for a given year.

Matplotlib#

def mpl_view(year=1952, show_legend=True):
    data = get_data(year)
    title = get_title("Matplotlib", year)
    xlim = get_xlim(data)

    plot = plt.figure(figsize=(10, 8), facecolor=(0, 0, 0, 0))
    ax = plot.add_subplot(111)
    ax.set_xscale("log")
    ax.set_title(title)
    ax.set_xlabel(XLABEL)
    ax.set_ylabel(YLABEL)
    ax.set_ylim(YLIM)
    ax.set_xlim(xlim)

    for continent, df in data.groupby('continent'):
        ax.scatter(df.gdpPercap, y=df.lifeExp, s=df['size']*5,
                   edgecolor='black', label=continent)

    if show_legend:
        ax.legend(loc=4)

    plt.close(plot)
    return plot

mpl_view(1952, True)
<Figure size 1000x800 with 1 Axes>

Plotly#

pio.templates.default = None

def plotly_view(year=1952, show_legend=True):
    data = get_data(year)
    title = get_title("Plotly", year)
    xlim = get_xlim(data)
    ylim = YLIM
    traces = []
    for continent, df in data.groupby('continent'):
        marker=dict(symbol='circle', sizemode='area', sizeref=0.1, size=df['size'], line=dict(width=2))
        traces.append(go.Scatter(x=df.gdpPercap, y=df.lifeExp, mode='markers', marker=marker, name=continent, text=df.country))

    axis_opts = dict(gridcolor='rgb(255, 255, 255)', zerolinewidth=1, ticklen=5, gridwidth=2)
    layout = go.Layout(
        title=title, showlegend=show_legend,
        xaxis=dict(title=XLABEL, type='linear', range=xlim, **axis_opts),
        yaxis=dict(title=YLABEL, range=ylim, **axis_opts),
        autosize=True, paper_bgcolor='rgba(0,0,0,0)',
    )
    return go.Figure(data=traces, layout=layout)

plotly_view()

Altair#

def altair_view(year=1952, show_legend=True, height="container", width="container"):
    data = get_data(year)
    title = get_title("Altair/ Vega", year)
    xlim = get_xlim(data)
    legend= ({} if show_legend else {'legend': None})
    return (
        alt.Chart(data)
            .mark_circle().encode(
                alt.X('gdpPercap:Q', scale=alt.Scale(type='log', domain=xlim), axis=alt.Axis(title=XLABEL)),
                alt.Y('lifeExp:Q', scale=alt.Scale(zero=False, domain=YLIM), axis=alt.Axis(title=YLABEL)),
                size=alt.Size('pop:Q', scale=alt.Scale(type="log"), legend=None),
                color=alt.Color('continent', scale=alt.Scale(scheme="category10"), **legend),
                tooltip=['continent','country'])
            .configure_axis(grid=False)
            .properties(title=title, height=height, width=width, background='rgba(0,0,0,0)') 
            .configure_view(fill="white")
            .interactive()
    )

altair_view(height=HEIGHT-100, width=1000)

hvPlot#

def hvplot_view(year=1952, show_legend=True):
    data = get_data(year)
    title = get_title("hvPlot/ Bokeh", year)
    xlim = get_xlim(data)

    return data.hvplot.scatter(
        'gdpPercap', 'lifeExp', by='continent', s='size_hvplot', alpha=0.8,
        logx=True, title=title, legend='bottom_right',
        hover_cols=['country'], ylim=YLIM, xlim=xlim, ylabel=YLABEL, xlabel=XLABEL, height=400

    )

hvplot_view()

Define Widgets#

We will set up a periodic callback to cycle through the years with a slider and checkbox widget for showing the legend. These widgets allow users to interact with our app.

year = pn.widgets.DiscreteSlider(value=YEARS[-1], options=YEARS, name="Year")
show_legend = pn.widgets.Checkbox(value=True, name="Show Legend")
def play():
    if year.value == YEARS[-1]:
        year.value=YEARS[0]
        return
    
    index = YEARS.index(year.value)
    year.value = YEARS[index+1]    

periodic_callback = pn.state.add_periodic_callback(play, start=False, period=PERIOD)
player = pn.widgets.Checkbox.from_param(periodic_callback.param.running, name="Autoplay")
widgets = pn.Column(year, player, show_legend, margin=(0,15))
widgets

Layout Widgets#

Now we can craete a Panel layout containing a logo, description and the widgets through the use of pn.Column.

logo = pn.pane.PNG(
    "https://panel.holoviz.org/_static/logo_stacked.png",
    link_url="https://panel.holoviz.org", embed=False, width=150, align="center"
)

desc = pn.pane.Markdown("""## 🎓 Info

The [Panel](https://panel.holoviz.org) library from [HoloViz](https://holoviz.org) \
lets you make widget-controlled apps and dashboards from a wide variety of \
plotting libraries and data types. Here you can try out four different plotting libraries \
controlled by a couple of widgets, for Hans Rosling's \
[gapminder](https://demo.bokeh.org/gapminder) example.
""")

settings = pn.Column(logo, "## ⚙️ Settings", widgets, desc)
settings

Bind Widgets to Plots#

Next, we create a function that will generate a list of plots encapsulated in pn.pane objects. This function takes parameters for the year and whether to display legends on the plots.

def update_views(year, show_legend):
    mpl_v = mpl_view(year=year, show_legend=show_legend)
    plotly_v = plotly_view(year=year, show_legend=show_legend)
    altair_v = altair_view(year=year, show_legend=show_legend)
    hvplot_v = hvplot_view(year=year, show_legend=show_legend)

    return [
        pn.pane.Vega(altair_v, sizing_mode='stretch_both', margin=10),
        pn.pane.HoloViews(hvplot_v, sizing_mode='stretch_both', margin=10),
        pn.pane.Matplotlib(mpl_v, format='png', sizing_mode='stretch_both', tight=True, margin=10),
        pn.pane.Plotly(plotly_v, sizing_mode='stretch_both', margin=10)

    ]

Then we will call pn.bind using the function created above. This will update the plots whenever the slider widget is moved. We layout the plots in a Gridbox with two columns. Panel provides many other layouts that might be perfect for your use case. Here in the notebook, we’ll display our gridbox layout of plot to the right of the widgets.

gridbox = pn.layout.GridBox(
    objects = pn.bind(update_views, year=year, show_legend=show_legend),   
    ncols=2,
    sizing_mode="stretch_both"
)

pn.Row(
    pn.Row(settings, width=250),
    gridbox
)

Configure the Template#

Finally, we layout out the app in the styled FastListTemplate.

pn.template.FastListTemplate(
    sidebar=[settings],
    main=[gridbox],
    site="Panel",
    site_url="https://panel.holoviz.org",
    title="Hans Rosling's Gapminder",
    header_background=ACCENT,
    accent_base_color=ACCENT,
    favicon="static/extensions/panel/images/favicon.ico",
    theme_toggle=False,
).servable();  # We add ; to avoid showing the app in the notebook

Congrats, you are finished! The final data app can be served via panel serve gapminders.ipynb.

It will look something like.

Gapminder app with 4 plots
This web page was generated from a Jupyter notebook and not all interactivity will work on this website.