Gapminders#

In his Gapminder example during thes 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

We’ll being by importing the packages needed.

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

Extract the dataset#

First, we’ll get the data into a Pandas dataframe.

dataset = pd.read_csv('./data/gapminders.csv')
dataset.sample(10)
country year pop continent lifeExp gdpPercap
97 Bangladesh 1957 51365468.0 Asia 39.348 661.637458
1428 Sri Lanka 1952 7982342.0 Asia 57.593 1083.532030
1445 Sudan 1977 17104986.0 Africa 47.800 2202.988423
871 Lebanon 1987 3089353.0 Asia 67.926 5377.091329
845 Korea, Rep. 1977 36436000.0 Asia 64.766 4657.221020
811 Jordan 1987 2820042.0 Asia 65.869 4448.679912
1599 United Kingdom 1967 54959000.0 Europe 71.360 14142.850890
39 Angola 1967 5247469.0 Africa 35.985 5522.776375
977 Mauritius 1977 913025.0 Africa 64.930 3710.982963
1316 Saudi Arabia 1992 16945857.0 Asia 68.768 24841.617770

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]

Transform the dataset to plots#

Now let’s define helper functions and functions to plot this dataset with Matplotlib, Plotly, Altair, and hvPlot (using HoloViews and Bokeh).

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)

Let’s define the Matplotlib plotting function.

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>

Let’s define the Plotly plotting function.

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()

Let’s define the Altair plotting function.

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)

Let’s define the hvPlot plotting function. Please note that hvPlot is the recommended entry point to the HoloViz plotting ecosystem.

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 the widgets#

Next we will set up a periodic callback to allow cycling through the years with a slider and checkbox widget for showing the legend. These widgets allow user 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 the 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, let’s create a function that will generate a list of plots encapsulated in pn.pane objects. It 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 2 columns. Panel provides many other layouts that might be perfect for your use case. Currently, placing Altair and Plotly plots on the same row causes the Vega plot to behave unexpectedly.

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

gridbox

Configure the template#

Let us layout out the app in the nicely 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