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.
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.