Patrick O'Hara
Patrick O'Hara

Reputation: 2229

How to update existing plot with Panel?

I have a dashboard application that works with Bokeh. I am trying to change it to use Panel and Geoviews. I am using the Panel Callback API, as this seems most like my existing code with Bokeh. I am running a regular Python script with the Panel server.

When my callback creates the new plot for the widgets selection then Panel displays an additional plot instead of updating the existing plot. Using "servable" causes an additional plot in the existing browser window, using "show" displays an additional window. How do I update the existing plot?

Here is some test code. (The full application displays a choropleth map with Geo data, and has many more widgets with code that reads different data, but this code illustrates the problem.)

import census_read_data as crd
import census_read_geopandas as crg
import pandas as pd
import geopandas as gpd
import geoviews as gv
from bokeh.plotting import show
from bokeh.models import PrintfTickFormatter
import panel as pn

import hvplot.pandas

# Get Census Merged Ward and Local Authority Data
# Replaced by test DataFrame
geography = pd.DataFrame(data=[
    ['E36007378', 'Chiswick Riverside', 'E09000018', 'Hounslow'],
    ['E36007379', 'Cranford', 'E09000018', 'Hounslow'],
    ['E36007202', 'Ealing Broadway', 'E09000009', 'Ealing'],
    ['E36007203', 'Ealing Common', 'E09000009', 'Ealing'],
    ['E36007204', 'East Acton', 'E09000009', 'Ealing'],
    ['E09000018', 'Hounslow', 'E09000018', 'Hounslow'],
    ['E09000009', 'Ealing', 'E09000009', 'Ealing']
], columns=["GeographyCode", "Name", "LAD11CD", "LAD11NM"])

# Get London Ward GeoPandas DataFrame
# Replaced by test DataFrame
london_wards_data_gdf = pd.DataFrame(data=[
    ['E36007378', 'E09000018', 378],
    ['E36007379', 'E09000018', 379],
    ['E36007202', 'E09000009', 202],
    ['E36007203', 'E09000009', 203],
    ['E36007204', 'E09000009', 204]
], columns=["cmwd11cd", "lad11cd", "data"])

# Get LAD GeoPandas DataFrame
# Replaced by test DataFrame
london_lads_data_gdf = pd.DataFrame(data=[
    ['E09000018', 757],
    ['E09000009', 609]
], columns=["lad11cd", "data"])

locationcol = "GeographyCode"
namecol = "Name"
datacol = 'data'

# Panel
pn.extension('bokeh')
gv.extension('bokeh')

lad_max_value = london_lads_data_gdf[datacol].max()
ward_max_value = london_wards_data_gdf[datacol].max()
title = datacol + " by Local Authority"

local_authorities = geography['LAD11CD'].unique()
granularities = ['Local Authorities', 'Wards']

# Create Widgets
granularity_widget = pn.widgets.RadioButtonGroup(options=granularities)
local_authority_widget = pn.widgets.Select(name='Wards for Local Authority',
                                           options=['All'] +
                                           [geography[geography[locationcol] == lad][namecol].iat[0]
                                            for lad in local_authorities],
                                           value='All')
widgets = pn.Column(granularity_widget, local_authority_widget)
layout = widgets


def update_graph(event):
    # Callback recreates map when granularity or local_authority are changed
    global layout
    granularity = granularity_widget.value
    local_authority_name = local_authority_widget.value
    print(f'granularity={granularity}')

    if granularity == 'Local Authorities':
        gdf = london_lads_data_gdf
        max_value = lad_max_value
        title = datacol + " by Local Authority"
    else:
        max_value = ward_max_value
        if local_authority_name == 'All':
            gdf = london_wards_data_gdf
            title = datacol + " by Ward"
        else:
            local_authority_id = geography[geography['Name'] ==
                                           local_authority_name].iloc[0]['GeographyCode']
            gdf = london_wards_data_gdf[london_wards_data_gdf['lad11cd'].str.match(
                local_authority_id)]
            title = datacol + " by Ward for " + local_authority_name

    # Replace gv.Polygons with hvplot.bar for test purposes
    map = gdf.hvplot.bar(y=datacol, height=500)
    layout = pn.Column(widgets, map)

    # With servable, a new plot is added to the browser window each time the widgets are changed
    # layout.servable()

    # With servable, a new browser window is shown each time the widgets are changed
    layout.show()


granularity_widget.param.watch(update_graph, 'value')
local_authority_widget.param.watch(update_graph, 'value')
update_graph(None)

# panel serve panel_test_script.py --show

Upvotes: 2

Views: 2458

Answers (1)

Patrick O'Hara
Patrick O'Hara

Reputation: 2229

I ended up implementing my solution using Params, rather than callbacks, which worked great. However, I eventually saw Dynamically updating a Holoviz Panel layout which showed me a solution to my original question.

The callback should not show() a new layout (with the new map), but should simply update the existing layout, replacing the existing map with the new map. As I write this it seems obvious!

This code fragment shows the solution:

...
widgets = pn.Column(granularity_widget, local_authority_widget)
empty_map = pn.pane.Markdown('### Map placeholder...')
layout = pn.Column(widgets, empty_map)

def update_graph(event):
    ...
    # Replace gv.Polygons with hvplot.bar for test purposes
    map = gdf.hvplot.bar(y=datacol, height=500)

    # Update existing layout with new map
    layout[1] = map

granularity_widget.param.watch(update_graph, 'value')
local_authority_widget.param.watch(update_graph, 'value')

# Invoke callback to show map for initial widget values
update_graph(None)
layout.show()

Upvotes: 3

Related Questions