Hi all.
I’m still struggling a bit with getting my head around param and panel.
Everything I want to do in terms of making apps is going to be visualising hierarchies of custom data types. Here I’ve made very simplified fictional example that doesn’t use any of the visualisation power of the holoviz ecosystem, to illustrate my problem.
- The example is an app for visualising data about stamp collectors.
- There are three parameterized classes: Collector, Album and Stamp. An Album has the parameter
stamps
, which is a list of Stamp objects contained in the Album, and Collector has the parameter
albums
, a list of Album objects associated with that Collector. - The app should present a dropdown to choose a Collector from a list.
- Once a Collector has been chosen, it should present a list of that Collector’s Albums.
- Based on the selection of one of those Albums, the app will display information on all the Stamps in the relevant Album.
So in this simple implementation, there are two reactive functions:
-
collector_album_selector
:- gets passed the panel.widgets.Select object that provides a choice of on Collector from the list of all Collectors.
- returns another panel.widget.Selector object with a list of all the chosen Collector’s Albums
-
stamps_layout
:- gets passed the reactive function above, which provides the user’s choice of Album
- returns a GridBox layout of Stamp info
My questions:
A. For reactive function (1), I was able to pass the widget and the bind function extracted the value automatically. For reactive function (2), the thing I was passing in was not a widget, but another reactive function that returns a widget. I found that this did not automatically provide the widget value, and I had to get that in the bound function using the .value
property. Is there a better way of doing this, and is this going to cause me any problems?
B. More importantly, the first reactive function works as intended, updating the Album dropdown when the Collector dropdown is changed. However, the second reactive function only works when the first reactive function is triggered. At that time it returns the layout for the list of Stamps corresponding to the default (first) Album, but when the selection of Album is updated, it doesn’t react.
Perhaps this has something to do with instantiation, and perhaps I could get it working by fiddling with that, but I wanted to ask for guidance in order to really understand what’s going on here. Perhaps there’s somewhere I should be looking in the documentation, but I don’t think this use case is discussed there. As I said, for me this—chaining widgets in order to be able to drill down into hierarchically arranged data types—would be a central use case for these packages.
Many thanks for any clarification. Here’s the code…
import random
import panel as pn
import param as pm
import names
# Define the classes
class Stamp(pm.Parameterized):
"""Represents a stamp"""
issued = pm.Integer(1900, bounds=(1800, 2030), doc="year of issue")
value_in_euros = pm.Number(default=0.69, bounds=(0, None), doc="value in euros")
class Album(pm.Parameterized):
"""Represents a stamp album"""
stamps = pm.List(default=[], item_type=Stamp)
class Collector(pm.Parameterized):
"""Represents a collector of stamps"""
name = pm.String(default="", doc="Name of collector")
albums = pm.List(default=[], item_type=Album)
# Generate some sample data…
def _get_stamps(num):
"""Return a list of num Stamp objects with random parameter values"""
years = [random.randint(1800, 2030) for _ in range(0, num)]
values = [round(random.random()*3, 2) for _ in range(0, num)]
stamps = [Stamp(issued=year, value_in_euros=value) for year, value in zip(years, values)]
return stamps
def _get_albums(num):
"""Return a list of num Album objects, each populated with a random number of Stamp objects"""
albums = [Album(stamps=_get_stamps(random.randint(5, 18))) for _ in range(0, num)]
return albums
def _get_collectors(num):
"""Return a list of num Collector objects, each """
fullnames = [names.get_full_name() for _ in range(0, num)]
collectors = [Collector(name=fullname, albums=_get_albums(random.randint(1,6))) for fullname in fullnames]
return collectors
collectors = _get_collectors(8)
# Panel app
template = pn.template.BootstrapTemplate(title="JM\'s template")
# For the sidebar: a pn.widgets.Select object that lists the stamp collectors by name
collector_selector = pn.widgets.Select(name="Collector", options={collector.name: collector for collector in collectors})
template.sidebar.append(collector_selector)
def _album_block(collector):
"""Based on the list of albums held by a collector. Currently just provodes a pn.widgets.Select object to choose one
from the list."""
return pn.widgets.Select(name="Album", options={album.name: album for album in collector.albums})
def _stamp_block(stamp):
"""Returns a Markdown pane with a representation of the Stamp object passed"""
return pn.pane.Markdown(f"Issued in {stamp.issued}, value: {stamp.value_in_euros}")
def _stamps_grid(album_selector):
"""Returns a grid of stamp representations from the list of Stamps contained in the Album object that is the
current value of the album_selector, a pn.widgets.Select object."""
album = album_selector.value
return pn.GridBox(*[_stamp_block(stamp) for stamp in album.stamps], ncols=4)
# Define reactive functions
collector_album_selector = pn.bind(_album_block, collector_selector)
stamps_layout = pn.bind(_stamps_grid, collector_album_selector)
# Place reactive functions in the template
content = pn.Column(collector_album_selector, stamps_layout)
template.main.append(content)
template.show()