diff --git a/.gitignore b/.gitignore index d752a31e9..0cca82f8f 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,5 @@ __pycache__/ # nbsite builtdocs/ -doc/index.rst \ No newline at end of file +doc/index.rst +.venv/ diff --git a/gapminders/gapminders.ipynb b/gapminders/gapminders.ipynb index 85a706cb2..dc2edd5cc 100644 --- a/gapminders/gapminders.ipynb +++ b/gapminders/gapminders.ipynb @@ -4,24 +4,16 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Gapminders\n", - "Written by Philipp Rudiger
\n", - "Created: 2018
\n", - "Last updated: January 14, 2021" + "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The [Panel](http://panel.holoviz.org) library from [HoloViz](http://holoviz.org) lets you make widget-controlled apps and dashboards from a wide variety of [plotting libraries and data types](https://github.com/holoviz/panel/issues/2). Here let's set up four different plotting libraries controlled by a couple of widgets, for Hans Rosling's [gapminder](https://www.gapminder.org/tag/hans-rosling/) example." + "The [Panel](http://panel.holoviz.org) library from [HoloViz](http://holoviz.org) lets you make widget-controlled apps and dashboards from a wide variety of [plotting libraries and data types](https://panel.holoviz.org/reference/index.html#panes). \n", + "\n", + "**Here we set up four different plotting libraries controlled by a couple of widgets, for Hans Rosling's [gapminder](https://demo.bokeh.org/gapminder) example.**" ] }, { @@ -34,26 +26,87 @@ "import numpy as np \n", "import pandas as pd\n", "import panel as pn\n", - "import holoviews as hv\n", "\n", "import altair as alt\n", "import plotly.graph_objs as go\n", + "import plotly.io as pio\n", "import matplotlib.pyplot as plt\n", + "import hvplot.pandas\n", "\n", - "pn.extension('vega', 'plotly')\n", + "pn.extension('vega', 'plotly', defer_load=True, sizing_mode=\"stretch_width\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We need to define some configuration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "XLABEL = 'GDP per capita (2000 dollars)'\n", + "YLABEL = 'Life expectancy (years)'\n", + "YLIM = (20, 90)\n", + "HEIGHT=500 # pixels\n", + "WIDTH=500 # pixels\n", + "ACCENT=\"#00A170\"\n", "\n", - "import hvplot.pandas # noqa: adds hvplot to pandas objects as a side effect\n", + "PERIOD = 1000 # miliseconds" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Extract the dataset\n", "\n", - "# Disable default plotly theme\n", - "import plotly.io as pio\n", - "pio.templates.default = None" + "First, we'll get the data into a Pandas dataframe. We use the [built in `cache`](https://panel.holoviz.org/user_guide/Performance_and_Debugging.html#caching) to speed up the app." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "@pn.cache\n", + "def get_dataset():\n", + " url = 'https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv'\n", + " return pd.read_csv(url)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dataset = get_dataset()\n", + "dataset.sample(10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "YEARS = [int(year) for year in dataset.year.unique()]\n", + "YEARS" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "First, we'll get the data into a Pandas dataframe:" + "## Transform the dataset to plots\n", + "\n", + "Now let's define helper functions and functions to plot this dataset with Matplotlib, Plotly, Altair, and hvPlot (using HoloViews and Bokeh)." ] }, { @@ -62,15 +115,25 @@ "metadata": {}, "outputs": [], "source": [ - "url = 'https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv'\n", - "dataset = pd.read_csv(url)" + "@pn.cache\n", + "def get_data(year):\n", + " df = dataset[(dataset.year==year) & (dataset.gdpPercap < 10000)].copy()\n", + " df['size'] = np.sqrt(df['pop']*2.666051223553066e-05)\n", + " df['size_hvplot'] = df['size']*6\n", + " return df\n", + "\n", + "def get_title(library, year):\n", + " return f\"{library}: Life expectancy vs. GDP, {year}\"\n", + "\n", + "def get_xlim(data):\n", + " return (data['gdpPercap'].min()-100,data['gdpPercap'].max()+1000)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now let's define a couple of user-settable parameters (the year and whether to show a legend), then write methods to plot this data with Matplotlib, Plotly, Altair (using Vega), and hvPlot (using HoloViews and Bokeh):" + "Let's define the **Matplotlib** plotting function." ] }, { @@ -79,93 +142,44 @@ "metadata": {}, "outputs": [], "source": [ - "class Gapminder(param.Parameterized):\n", - " year = param.ObjectSelector(default=1952, objects=list(dataset.year.unique()))\n", - " show_legend = param.Boolean(default=True)\n", - " \n", - " title = 'Life expectancy vs. GDP, %s'\n", - " xlabel = 'GDP per capita (2000 dollars)'\n", - " ylabel = 'Life expectancy (years)'\n", - " ylim = (20, 90)\n", - " xlim = (200, 15000)\n", + "plt.rcParams.update({\n", + " \"savefig.facecolor\": (0.0, 0.0, 0.0, 0.0), \n", + "})\n", "\n", - " def get_data(self):\n", - " df = dataset[(dataset.year==self.year) & (dataset.gdpPercap < 10000)].copy()\n", - " df['size'] = np.sqrt(df['pop']*2.666051223553066e-05)\n", - " return df\n", - " \n", - " def mpl_view(self):\n", - " data = self.get_data()\n", - " title = \"Matplotlib: \" + (self.title % self.year)\n", - " \n", - " plot = plt.figure(figsize=(7, 6))\n", - " ax = plot.add_subplot(111)\n", - " ax.set_xscale(\"log\", nonpositive='clip')\n", - " ax.set_title(title)\n", - " ax.set_xlabel(self.xlabel)\n", - " ax.set_ylabel(self.ylabel)\n", - " ax.set_ylim(self.ylim)\n", - " ax.set_xlim(self.xlim)\n", "\n", - " for continent, df in data.groupby('continent'):\n", - " ax.scatter(df.gdpPercap, y=df.lifeExp, s=df['size']*5,\n", - " edgecolor='black', label=continent)\n", - " \n", - " if self.show_legend:\n", - " ax.legend(loc=4)\n", - " \n", - " plt.close(plot)\n", - " return plot\n", - " \n", - " def plotly_view(self):\n", - " data = self.get_data()\n", - " title = 'Plotly: ' + (self.title % self.year)\n", + "@pn.cache\n", + "def mpl_view(year=1952, show_legend=True):\n", + " data = get_data(year)\n", + " title = get_title(\"Matplotlib\", year)\n", + " xlim = get_xlim(data)\n", "\n", - " traces = []\n", - " for continent, df in data.groupby('continent'):\n", - " marker=dict(symbol='circle', sizemode='area', sizeref=0.1, size=df['size'], line=dict(width=2))\n", - " traces.append(go.Scatter(x=df.gdpPercap, y=df.lifeExp, mode='markers', marker=marker, name=continent))\n", - " \n", - " axis_opts = dict(gridcolor='rgb(255, 255, 255)', zerolinewidth=1, ticklen=5, gridwidth=2)\n", - " layout = go.Layout(title=title, showlegend=self.show_legend, width=550,\n", - " xaxis=dict(title=self.xlabel, type='log', **axis_opts),\n", - " yaxis=dict( title=self.ylabel, **axis_opts))\n", - " \n", - " return go.Figure(data=traces, layout=layout)\n", - " \n", - " def altair_view(self):\n", - " data = self.get_data()\n", - " title = \"Altair/Vega: \" + (self.title % self.year)\n", - " legend= ({} if self.show_legend else {'legend': None})\n", + " plot = plt.figure(figsize=(10, 6))\n", + " ax = plot.add_subplot(111)\n", + " ax.set_xscale(\"log\")\n", + " ax.set_title(title)\n", + " ax.set_xlabel(XLABEL)\n", + " ax.set_ylabel(YLABEL)\n", + " ax.set_ylim(YLIM)\n", + " ax.set_xlim(xlim)\n", "\n", - " plot = alt.Chart(data).mark_circle().encode(\n", - " alt.X('gdpPercap:Q', scale=alt.Scale(type='log'), axis=alt.Axis(title=self.xlabel)),\n", - " alt.Y('lifeExp:Q', scale=alt.Scale(zero=False, domain=self.ylim), axis=alt.Axis(title=self.ylabel)),\n", - " size=alt.Size('pop:Q', scale=alt.Scale(type=\"log\"), legend=None),\n", - " color=alt.Color('continent', scale=alt.Scale(scheme=\"category10\"), **legend))\\\n", - " .properties(title=title).configure_axis(grid=False)\n", - " return plot\n", - " \n", - " def hvplot_view(self):\n", - " data = self.get_data()\n", - " title = \"hvPlot/Bokeh: \" + (self.title % self.year)\n", - " \n", - " legend = 'bottom_right' if self.show_legend else None\n", - " return data.hvplot.scatter(\n", - " 'gdpPercap', 'lifeExp', by='continent', size=np.sqrt(hv.dim('size'))*3, line_color='black',\n", - " logx=True, title=title, width=500, height=400, legend=legend,\n", - " xlabel=self.xlabel, ylabel=self.ylabel, xticks=[500, 1000, 2000, 5000, 10000],\n", - " ylim=self.ylim, xlim=(200, 12000)\n", - " )\n", + " for continent, df in data.groupby('continent'):\n", + " ax.scatter(df.gdpPercap, y=df.lifeExp, s=df['size']*5,\n", + " edgecolor='black', label=continent)\n", + "\n", + " if show_legend:\n", + " ax.legend(loc=4)\n", "\n", - "gm = Gapminder(name='')" + " plt.close(plot)\n", + " return plot\n", + "\n", + "mpl_view(1952, True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We can now make a panel showing the parameters of this `gm` object along with the result of calling four different `_view()` methods:" + "Let's define the **Plotly** plotting function." ] }, { @@ -174,20 +188,36 @@ "metadata": {}, "outputs": [], "source": [ - "pn.Column(\n", - " gm.param,\n", - " pn.Row(gm.hvplot_view, gm.altair_view),\n", - " pn.Row(gm.mpl_view, gm.plotly_view)\n", - ")" + "pio.templates.default = None\n", + "\n", + "@pn.cache\n", + "def plotly_view(year=1952, show_legend=True):\n", + " data = get_data(year)\n", + " title = get_title(\"Plotly\", year)\n", + " xlim = get_xlim(data)\n", + "\n", + " traces = []\n", + " for continent, df in data.groupby('continent'):\n", + " marker=dict(symbol='circle', sizemode='area', sizeref=0.1, size=df['size'], line=dict(width=2))\n", + " traces.append(go.Scatter(x=df.gdpPercap, y=df.lifeExp, mode='markers', marker=marker, name=continent, text=df.country))\n", + "\n", + " axis_opts = dict(gridcolor='rgb(255, 255, 255)', zerolinewidth=1, ticklen=5, gridwidth=2)\n", + " layout = go.Layout(title=title, showlegend=show_legend,\n", + " xaxis=dict(title=XLABEL, type='log', **axis_opts),\n", + " yaxis=dict( title=YLABEL, **axis_opts), autosize=True,\n", + " paper_bgcolor='rgba(0,0,0,0)',\n", + " )\n", + " \n", + " return go.Figure(data=traces, layout=layout)\n", + "\n", + "plotly_view()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Here the Matplotlib output is being rendered as PNG, but the rest are interactive, with hovering, zooming, and similar features (if allowed by the notebook viewer being used). Note that due to interactions between the various JS libraries used here, the Plotly output can get mixed up when resizing or reloading the page, but it should be reset if you adjust the year slider once you are done.\n", - "\n", - "We can also make these plots available as a standalone server, if we add `.servable()` to the panel object and call this notebook as `bokeh serve --show Panel_Gapminders.ipynb`. Before doing that, let's make another panel where we can add a logo and a title, so that it makes a nicer page layout when served separately. We'll also add an R ggplot2-based plot to fill out the page:" + "Let's define the **Altair** plotting function." ] }, { @@ -196,37 +226,90 @@ "metadata": {}, "outputs": [], "source": [ - "logo = \"\"\"\n", - " \"\"\"\n", + "@pn.cache\n", + "def altair_view(year=1952, show_legend=True, height=\"container\", width=\"container\"):\n", + " data = get_data(year)\n", + " title = get_title(\"Altair/ Vega\", year)\n", + " xlim = get_xlim(data)\n", + " legend= ({} if show_legend else {'legend': None})\n", "\n", - "title = '

Plotting library comparison

'\n", + " plot = alt.Chart(data, ).mark_circle().encode(\n", + " alt.X('gdpPercap:Q', scale=alt.Scale(type='log'), axis=alt.Axis(title=XLABEL)),\n", + " alt.Y('lifeExp:Q', scale=alt.Scale(zero=False, domain=YLIM), axis=alt.Axis(title=YLABEL)),\n", + " size=alt.Size('pop:Q', scale=alt.Scale(type=\"log\"), legend=None),\n", + " color=alt.Color('continent', scale=alt.Scale(scheme=\"category10\"), **legend),\n", + " tooltip=['continent','country'])\\\n", + " .configure_axis(grid=False) \\\n", + " .properties(title=title, height=height, width=width, background='rgba(0,0,0,0)') \\\n", + " .configure_view(fill=\"white\")\n", + " return plot.interactive()\n", "\n", - "desc = pn.pane.HTML(\"\"\"\n", - " The
Panel library from PyViz \n", - " lets you make widget-controlled apps and dashboards from a wide variety of \n", - " plotting libraries and data types. Here you can try out five different plotting libraries\n", - " controlled by a couple of widgets, for Hans Rosling's \n", - " gapminder example.\"\"\", width=250)\n", + "altair_view(height=HEIGHT-100, width=1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's define the **hvPlot** plotting function. Please note that [hvPlot](https://hvplot.holoviz.org/) is the recommended entry point to the HoloViz plotting ecosystem." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "@pn.cache\n", + "def hvplot_view(year=1952, show_legend=True):\n", + " data = get_data(year)\n", + " title = get_title(\"hvPlot/ Bokeh\", year)\n", + " xlim = get_xlim(data)\n", "\n", - "widgets = pn.Param(gm.param, widgets={'year': {'type': pn.widgets.DiscreteSlider, 'width': 250}}, margin=0)\n", + " plot = data.hvplot.scatter('gdpPercap', 'lifeExp', by='continent', s='size_hvplot', alpha=0.6,\n", + " logx=True, title=title, height=HEIGHT, responsive=True, legend=show_legend, hover_cols=['country'])\n", + " plot = plot.opts(legend_position='bottom_right', xticks=[500, 1000, 2000, 5000, 10000])\n", + " plot = plot.redim.label(gdpPercap=XLABEL, lifeExp=YLABEL)\n", + " plot = plot.redim.range(lifeExp=YLIM, gdpPercap=(200, 12000))\n", + " return plot\n", "\n", - "pn.Row(\n", - " pn.Column(logo, title, desc, widgets),\n", - " pn.Column(\n", - " pn.Row(gm.hvplot_view, gm.altair_view),\n", - " pn.Row(gm.mpl_view, gm.plotly_view)\n", - " )\n", - ")" + "hvplot_view()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Template\n", + "## Define the widgets" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "year = pn.widgets.DiscreteSlider(value=YEARS[-1], options=YEARS, name=\"Year\")\n", + "show_legend = pn.widgets.Checkbox(value=True, name=\"Show Legend\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def play():\n", + " print(\"play\")\n", + " if year.value == YEARS[-1]:\n", + " year.value=YEARS[0]\n", + " return\n", + " \n", + " index = YEARS.index(year.value)\n", + " year.value = YEARS[index+1] \n", "\n", - "To make the application look more polished we can put our components into a template:" + "periodic_callback = pn.state.add_periodic_callback(play, start=False, period=PERIOD)\n", + "player = pn.widgets.Checkbox.from_param(periodic_callback.param.running, name=\"Autoplay\")" ] }, { @@ -235,44 +318,156 @@ "metadata": {}, "outputs": [], "source": [ - "template = pn.template.MaterialTemplate(\n", - " title='Gapminder - Plotting library comparison',\n", - ")\n", + "widgets = pn.Column(year, player, show_legend, margin=(0,15))\n", + "widgets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Bind the plot functions to the widgets" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mpl_view = pn.bind(mpl_view, year=year, show_legend=show_legend)\n", + "plotly_view = pn.bind(plotly_view, year=year, show_legend=show_legend)\n", + "altair_view = pn.bind(altair_view, year=year, show_legend=show_legend, height=HEIGHT-100)\n", + "hvplot_view = pn.bind(hvplot_view, year=year, show_legend=show_legend)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Layout the widgets" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "logo = pn.pane.PNG(\"https://panel.holoviz.org/_static/logo_stacked.png\", link_url=\"https://panel.holoviz.org\", embed=False, width=150, align=\"center\")\n", + " \n", + "desc = \"\"\"## 🎓 Info\n", "\n", - "template.sidebar.append(logo)\n", - "template.sidebar.append(desc.clone(width=250, margin=(20, 5)))\n", + "The [Panel](http://panel.holoviz.org) library from [HoloViz](http://holoviz.org)\n", + "lets you make widget-controlled apps and dashboards from a wide variety of \n", + "plotting libraries and data types. Here you can try out four different plotting libraries\n", + "controlled by a couple of widgets, for Hans Rosling's \n", + "[gapminder](https://demo.bokeh.org/gapminder) example.\n", "\n", - "template.sidebar.append(widgets)\n", + "Source: [pyviz-topics - gapminder](https://github.com/pyviz-topics/examples/blob/master/gapminders/gapminders.ipynb)\n", + "\"\"\"\n", "\n", - "template.main.append(\n", - " pn.Column(\n", - " pn.Row(gm.hvplot_view, gm.altair_view),\n", - " pn.Row(gm.mpl_view, gm.plotly_view)\n", - " )\n", + "settings = pn.Column(\n", + " logo, \"## ⚙️ Settings\", widgets, desc\n", ")\n", + "settings" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Layout the plots\n", + "\n", + "We layout the plots in a [Gridbox](https://panel.holoviz.org/reference/layouts/GridBox.html) with 2 columns. Please note Panel provides many other [layouts](https://panel.holoviz.org/reference/index.html#layouts) that might be perfect for your use case." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plots = pn.layout.GridBox(\n", + " pn.Row(pn.Spacer(), pn.panel(mpl_view, height=HEIGHT, sizing_mode=\"scale_height\"), pn.Spacer()),\n", + " hvplot_view,\n", + " pn.panel(plotly_view, config={'responsive': True}, margin=(5,5,0,0)),\n", + " pn.panel(altair_view, height=500),\n", + " ncols=2, sizing_mode=\"stretch_both\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note how we mark the components `.servable()`. This will allow us to serve this notebook as a data app via `panel serve gapminder.ipynb`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configure the template\n", "\n", - "template.servable();" + "Let us layout out the app in the nicely styled [FastListTemplate](https://panel.holoviz.org/reference/templates/FastListTemplate.html)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pn.template.FastListTemplate(\n", + " sidebar=[settings],\n", + " main=[plots],\n", + " site=\"Panel\",\n", + " site_url=\"https://panel.holoviz.org\",\n", + " title=\"Hans Rosling's Gapminder\",\n", + " header_background=ACCENT,\n", + " accent_base_color=ACCENT,\n", + " favicon=\"static/extensions/panel/images/favicon.ico\",\n", + " theme_toggle=False,\n", + ").servable(); # We add the ; to avoid showing the app in the notebook" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Once you run `panel serve` on this notebook , you should get a panel like the following in your web browser that you can explore or share with other users of your machine:" + "The final data app can be served via `panel serve gapminder.ipynb`.\n", + "\n", + "It will look something like." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "" + "\"\"" ] } ], "metadata": { + "kernelspec": { + "display_name": "Python [conda env:root] *", + "language": "python", + "name": "conda-root-py" + }, "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", "name": "python", - "pygments_lexer": "ipython3" + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" } }, "nbformat": 4, diff --git a/gapminders/thumbnail.jpg b/gapminders/thumbnail.jpg new file mode 100644 index 000000000..8590689fd Binary files /dev/null and b/gapminders/thumbnail.jpg differ