{ "cells": [ { "cell_type": "markdown", "id": "aa15a6aa", "metadata": {}, "source": [ "\n", "\n", "

Tutorial 6. Dashboards

" ] }, { "cell_type": "markdown", "id": "92106729", "metadata": {}, "source": [ "So far in this tutorial, we have seen how to generate plots with `.plot` or `.hvplot`, how to compose these plots together into layouts and overlays, how to link selections between these plots, and how to control visualizations with Panel widgets using `.interactive`. In this notebook, we will learn how to put all these pieces together to display (and serve) these components in a dashboard using Panel.\n", "\n", "## Panel `pane` objects\n", "\n", "So far we have only seen Panel used as a source of widgets, but Panel also offers [pane](https://panel.holoviz.org/reference/index.html#panes) objects that can display various types of data (including output from just about any plotting library). First let's import Panel and load the extension:" ] }, { "cell_type": "code", "execution_count": null, "id": "5d99696a", "metadata": {}, "outputs": [], "source": [ "import pathlib\n", "import pandas as pd\n", "import panel as pn\n", "import xarray as xr\n", "import holoviews as hv\n", "\n", "pn.extension('tabulator', template='material', sizing_mode='stretch_width')\n", "\n", "import colorcet as cc\n", "import hvplot.xarray # noqa\n", "import hvplot.pandas # noqa" ] }, { "cell_type": "markdown", "id": "d7475aa4", "metadata": {}, "source": [ "Here, we have enabled some optional functionality from Panel, specifically the `tabulator` extension, and selected a default template controlling the overall look and feel of the final app. We also configured how plots should be sized by default (stretching to fit the width available). We'll come back to the idea of a template later. Here, let's look at a simple pane, e.g. a [Markdown pane](https://panel.holoviz.org/reference/panes/Markdown.html) that displays Markdown-format text:" ] }, { "cell_type": "code", "execution_count": null, "id": "bae988f3", "metadata": {}, "outputs": [], "source": [ "pn.pane.Markdown('## Earthquake Dashboard')" ] }, { "cell_type": "code", "execution_count": null, "id": "de419baf", "metadata": {}, "outputs": [], "source": [ "logo_path = pathlib.Path('../assets/usgs_logo.png')" ] }, { "cell_type": "markdown", "id": "1ca78958", "metadata": {}, "source": [ "The `PNG` pane can display PNG images:" ] }, { "cell_type": "code", "execution_count": null, "id": "071c3398", "metadata": {}, "outputs": [], "source": [ "pn.pane.PNG(logo_path, height=130)" ] }, { "cell_type": "markdown", "id": "fe78cead", "metadata": {}, "source": [ "## Using `pn.panel`\n", "\n", "Instead of having to select the pane type explicitly, you can use the `pn.panel` function that tries to guess the appropriate representation given the input. For instance, here we generate the same two panels using `pn.panel` and grab handles on the resulting objects:" ] }, { "cell_type": "code", "execution_count": null, "id": "be62968b", "metadata": {}, "outputs": [], "source": [ "dashboard_title = pn.panel('## Earthquakes')\n", "usgs_logo = pn.panel(logo_path, height=130)" ] }, { "cell_type": "markdown", "id": "ed45e340", "metadata": {}, "source": [ "#### Exercise\n", "\n", "Confirm that these two objects are of type `Markdown` and `PNG` respectively by using the `type` built-in. Explore using different markdown syntax such as *italic*, **bold** or adding bullet points. Finally, try displaying your own PNG image with a `PNG` pane, using either a local filename or URL." ] }, { "cell_type": "markdown", "id": "5452cbcf", "metadata": {}, "source": [ "## `Panel` objects\n", "\n", "In addition to `pane` objects, Panel offers containers of type `panel` which allow you to position your components into various layouts. For instance, we can put a small version of our title and logo into a Panel `Row` layout:" ] }, { "cell_type": "code", "execution_count": null, "id": "2bdc2a2b", "metadata": {}, "outputs": [], "source": [ "header = pn.Row(dashboard_title, pn.pane.PNG(logo_path, height=40))\n", "header" ] }, { "cell_type": "markdown", "id": "8270dee4", "metadata": {}, "source": [ "Next let us load the earthquake dataset and make a basic plot of the sort we have seen earlier on in the tutorial:" ] }, { "cell_type": "code", "execution_count": null, "id": "d664a543", "metadata": {}, "outputs": [], "source": [ "%%time\n", "df = pd.read_parquet(pathlib.Path('../data/earthquakes-projected.parq'))\n", "df = df.set_index('time').tz_convert(None).reset_index()\n", "small_df = df.sample(frac=.01)" ] }, { "cell_type": "code", "execution_count": null, "id": "f1e98528", "metadata": {}, "outputs": [], "source": [ "%%time\n", "ds = xr.open_dataarray(pathlib.Path('../data/raster/gpw_v4_population_density_rev11_2010_2pt5_min.nc'))\n", "cleaned_ds = ds.where(ds.values != ds.nodatavals).sel(band=1)\n", "cleaned_ds.name = 'population'" ] }, { "cell_type": "code", "execution_count": null, "id": "ed0abf7d", "metadata": {}, "outputs": [], "source": [ "sample_points = small_df.hvplot.points(x='longitude', y='latitude', c='mag', cmap=cc.CET_L4, responsive=True)\n", "rasterized_pop = cleaned_ds.hvplot.image(rasterize=True, logz=True, clim=(1, None), responsive=True, min_height=400).opts(bgcolor='black')\n", "earthquake_example = rasterized_pop * sample_points" ] }, { "cell_type": "markdown", "id": "63281bef", "metadata": {}, "source": [ "Now we can combine this plot with our header in a `pn.Column`:" ] }, { "cell_type": "code", "execution_count": null, "id": "34a01c32", "metadata": {}, "outputs": [], "source": [ "mini_dashboard = pn.Column(header, earthquake_example)\n", "mini_dashboard" ] }, { "cell_type": "markdown", "id": "a4dc2990", "metadata": {}, "source": [ "## Showing and serving dashboards\n", "\n", "Now`mini_dashboard` is a Panel object that can be displayed or served as a simple dashboard. To view the dashboard in a new tab, you can simply call `.show()`:" ] }, { "cell_type": "code", "execution_count": null, "id": "924f772e", "metadata": {}, "outputs": [], "source": [ "# mini_dashboard.show()" ] }, { "cell_type": "markdown", "id": "d57453ab", "metadata": {}, "source": [ "If instead of `.show()` you use the `.servable()` method, you can serve the dashboard from this notebook using the command:\n", "\n", "```bash\n", "$ panel serve 06_Dashboard.ipynb\n", "```\n", "\n", "We will use this to serve a more sophisticated dashboard built in this notebook." ] }, { "cell_type": "markdown", "id": "70e6f29e", "metadata": {}, "source": [ "## A visual earthquake filter\n", "\n", "From the last notebook, first subset the data:" ] }, { "cell_type": "code", "execution_count": null, "id": "341666d9", "metadata": {}, "outputs": [], "source": [ "WEB_MERCATOR_LIMITS = (-20037508.342789244, 20037508.342789244)\n", "\n", "subset_df = df[\n", " (df['northing'] < WEB_MERCATOR_LIMITS[1]) &\n", " (df['mag'] > 4) &\n", " (df['time'] >= pd.Timestamp('2017-01-01')) &\n", " (df['time'] <= pd.Timestamp('2018-01-01'))]" ] }, { "cell_type": "markdown", "id": "87aee71b", "metadata": {}, "source": [ "Declare the panel widgets:" ] }, { "cell_type": "code", "execution_count": null, "id": "b8b93294", "metadata": {}, "outputs": [], "source": [ "date_subrange = pn.widgets.DateRangeSlider(name='Date', \n", " start=subset_df.time.iloc[0], \n", " end=subset_df.time.iloc[-1])\n", "mag_subrange = pn.widgets.FloatSlider(name='Magnitude', start=3, end=9, value=3)" ] }, { "cell_type": "markdown", "id": "aa2b06c0", "metadata": {}, "source": [ "Create an interactive `DataFrame` and use `hvplot` to generate a visualization of earthquakes plotted on a map and controlled with widgets:" ] }, { "cell_type": "code", "execution_count": null, "id": "9c2adc7e", "metadata": {}, "outputs": [], "source": [ "subset_dfi = subset_df.interactive(sizing_mode='stretch_width')\n", "filtered_subrange = subset_dfi[\n", " (subset_dfi['mag'] > mag_subrange) &\n", " (subset_dfi['time'] >= date_subrange.param.value_start) &\n", " (subset_dfi['time'] <= date_subrange.param.value_end)]\n", "\n", "geo = filtered_subrange.hvplot(\n", " 'easting', 'northing', color='mag', kind='points',\n", " xaxis=None, yaxis=None, responsive=True, min_height=500, tiles='ESRI')" ] }, { "cell_type": "markdown", "id": "df4fdecd", "metadata": {}, "source": [ "This `geo` object works in Panel layouts, which means we can now add a header to it:" ] }, { "cell_type": "code", "execution_count": null, "id": "db50239f", "metadata": {}, "outputs": [], "source": [ "pn.Column(header, geo)" ] }, { "cell_type": "markdown", "id": "f2fe5263", "metadata": {}, "source": [ "As before, we now have a functional dashboard that we can show with `.show()` or serve with `.servable()`." ] }, { "cell_type": "markdown", "id": "0411ecd3", "metadata": {}, "source": [ "## Final Dashboard\n", "\n", "We can now put all the concepts we have learned together to make a more sophisticated, interactive dashboard supporting linked selections:" ] }, { "cell_type": "code", "execution_count": null, "id": "d07f53c1", "metadata": {}, "outputs": [], "source": [ "pn.state.template.sidebar_width = 250\n", "pn.state.template.title = 'Earthquake Interactive Demo'\n", "\n", "ls = hv.link_selections.instance(unselected_alpha=0.02)\n", "\n", "# Table is not yet dynamically linked to the linked selection\n", "table = filtered_subrange.pipe(ls.filter, selection_expr=ls.param.selection_expr)[['time', 'place', 'mag', 'depth']].pipe(\n", " pn.widgets.Tabulator, pagination='remote', page_size=15)\n", "\n", "mag_hist = filtered_subrange.hvplot(\n", " y='mag', kind='hist', responsive=True, min_height=300)\n", "\n", "depth_hist = filtered_subrange.hvplot(\n", " y='depth', kind='hist', responsive=True, min_height=300)\n", "\n", "geo = filtered_subrange.hvplot(\n", " 'easting', 'northing', color='mag', kind='points',\n", " xaxis=None, yaxis=None, responsive=True, min_height=500,\n", " data_aspect=1, framewise=True, clim=(4, 10), line_color='black'\n", ")\n", "\n", "column = pn.Column(\n", " pn.Row(\n", " hv.element.tiles.ESRI() * ls(geo.holoviews()),\n", " table.panel()\n", " ),\n", " pn.Row(\n", " ls(depth_hist.holoviews()),\n", " ls(mag_hist.holoviews()),\n", " )\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "b03633bf", "metadata": {}, "outputs": [], "source": [ "filtered_subrange.widgets().servable(area='sidebar')" ] }, { "cell_type": "code", "execution_count": null, "id": "d5bf735c", "metadata": {}, "outputs": [], "source": [ "column.servable(title='Earthquake Interactive Demo')" ] }, { "cell_type": "markdown", "id": "5bf02c8f", "metadata": {}, "source": [ "This dashboard should work in the notebook interface (with widgets shown separately from the plots above) for debugging, but a complex layout like that is meant to be served separately, e.g. using `panel serve --port 5067 06_Dashboards.ipynb` run in this directory. If working with the `anaconda-project` version of this tutorial, you can run:\n", "\n", "```bash\n", "anaconda-project run dashboard\n", "```\n", "\n", "Note that the code above can instead be pasted into a Python text file and run with `panel serve file.py`; none of the machinery depends on Jupyter or on being stored in the notebook format, and apps can be developed fully in text editors if preferred.\n", "\n", "## Sharing\n", "\n", "Once you are happy with the dashboard you have built, it's time to share it! You can find on [Panel's website](https://panel.holoviz.org/how_to/index.html#share-your-work) more details about how to:\n", "\n", "- configure the server\n", "- deploy the application on various platforms (e.g. Microsoft Azure, Google Cloud Platform, Hugging Face)\n", "- save a dashboard as a static HTML file\n", "- deploy a dashboard leveraging a new technology called WebAssembly thanks to which (and to Pyodide or PyScript) a Python application can run entirely in the browser, making it a lot easier to deploy a dashboard as you no longer need to bother setting up a server\n", "\n", "Though this feature is brand new as of 6/2023 and not yet documented on Panel's website, you can now easily deploy Panel dashboards on [Anaconda Notebooks](https://anaconda.cloud/anaconda-tools) as well.\n", "\n", "## Lumen\n", "\n", "Panel is a highly flexible library to build apps and dashboards that provides all the required building blocks for you to assemble as you like. The [Lumen library](https://lumen.holoviz.org/) is a relatively new addition to the HoloViz ecosystem that is much more opinionated about what a data app is, handling many of the tasks involved automatically. Building on top of [Intake](https://intake.readthedocs.io/en/latest/index.html) Panel, Lumen can take care of ingesting, filtering, transforming and viewing data, all through a declarative specification that can be stored in YAML files.\n", "\n", "## Conclusion\n", "\n", "The techniques above and in the previous notebooks should enable you to build complex, deeply interlinked layouts and dashboards that help you understand your data and uncover hidden relationships, with custom interactivity for exploring any parameters of interest. You can explore further at the websites for any of the tools mentioned here, starting at [hvplot.holoviz.org](https://hvplot.holoviz.org) and moving on from there as needed.\n", "\n", "In the tutorials so far, we have focused almost exclusively on the highest-level APIs provided by HoloViz, namely `.hvplot()` and `.interactive()`. These interfaces let you focus on the data you are trying to work with, without getting bogged down in writing dozens or hundreds of lines of plotting or callback code. Of course, they don't cover every possible type of plot or interactivity, and if you want full control, you'll need to learn the lower-level APIs provided by HoloViz tools (HoloViews and Panel), or the even lower-level tools, like Bokeh. The remaining tutorials are entirely optional, but they introduce some of those lower-level APIs so that you can see how to do things more manually when needed. These later tutorials are particularly useful for understanding the examples of interesting apps at [examples.holoviz.org](https://examples.holoviz.org), most of which were written before the simple `.interactive()` interface was developed." ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 }