Where to use `param.rx`

Hi, I’m trying to get my head around param.rx and how to integrate with it. Doing simple integrations like converting an xarray.DataArray to reactiveAPI and operating on it seems understandable, but then moving forwards from there I get stuck.

  • integarting with holoview.Layout, etc.: I have tried to convert the curves derived from hvplot to reactive and then use +=, and *= operations, but it doesn’t work for me
    TypeError: unsupported operand type(s) for *: 'ParameterizedMetaclass' and 'Curve'
    
  • complicated DataArray operations: I want to chain .sel, scaling of dimension and values, etc., but the only way I’ve found to do that is to wrap all of these in a function and pass that to param.rx contructor (or maybe bind will work as well), but this feels counter to the scope of param.rx

So I feel stuck on where and how to ingerate with param.rx

1 Like

Hi @LecrisUT

Would it be possible for you to provide some minimum, reproducible code examples? It would make it much, much easier to try to help.

I am encountering many issues and I don’t know exactly where to start. I can give 2 mock-ups of what I have been investigating

Integrated workflow

Here I want to to highlight how many indirections are involved. What I am trying to code is something along the lines of:

import param
import panel
import xarray
import hvplot
import dataclasses

ds: xarray.Dataset = _get_dataset()

@dataclasses.dataclass
class Plotter:
    ds: xarray.Dataset
    """Plot helper that I reuse across multiple jupyter cells"""
    def plot_line(self, field: str, x: str, indexers: dict[str, Any]) -> param.rx:
        da: xarray.DataArray = self.ds[field]
        
        def select_and_plot(_data: xarray.DataArray, **_indexers: Any) -> holoviews.Curve:
            plot_data = _data.sel(_indexers)
            # Do some post-processing, e.g.
            plot_data = plot_data.dropna(x, "all")
            curve: holoviews.Curve = plot_data.hvplot(kind="line", x=x)
            return curve
        rx_curve = param.rx(select_and_plot)(data, **indexers)
        return rx_curve

# Prepare widgets which have complex dependencies
# I am mixing panel and param.rx a lot in it
rx_I = panel.widgets.DiscreteSlider(name="Laser intensity", options=[1e-3, 1e-5])
def filter_run(I: float) -> list[str]:
    runs = ds["run"]
    runs = runs.where((runs.I == I), drop=True)
    return [run.item() for run in runs]
run_list = param.rx(filter_run)(I=rx_I)
rx_run = panel.widgets.Select(name="Run", options=runs)

plot = Plotter(ds)

# Here is where the uncertainty and experimentation breaks down
layout = param.rx(holoviews.Layout())
# layout = holoviews.Layout()
for dim in ["x", "y"]:
    overlay = param.rx(holoviews.Overlay())
    # overlay = holoviews.Overlay()
    overlay *= plot.plot_line("laser", x="t",
                               indexers={"run": rx_run})
    overlay *= plot.plot_line("other_value", x="t",
                               indexers={"run": rx_run.rx, "dim": dim})
    # The following does not work. There is a lot of experimentation between using .rx.value and
    # not using param.rx on holviews.Layout/Overlay
    # layout += overlay
    layout += overlay.rx.value

# Automatic widgets are not picking up the intermediate dependent of rx_I
widgets = panel.WidgetBox(rx_I, rx_run)
panel.Row(widget, layout.rx.value)

After much experimentation, I might have figured how to work with the holoviews composition, but I am not sure it is appropriate because I cannot get the whole thing to update and work appropriately

Some issues found

I have segregated some issues that I believe I am encountering

panel and param.rx do not update

import hvplot.xarray
import param
import panel

rx_I = panel.widgets.DiscreteSlider(name="Laser intensity", options=[1e-3, 1e-5])
val = param.rx()
val.rx.value = rx_I
panel.Row(rx_I, val)

Running this and updating the widget does not change the displayed text. I have tried splitting it across cells, adding param.rx around the callers, but nothing helped


I have finally narrowed down the issues being with using .rx.value, which apparently I should never be doing. I still have to use param.rx(holoviews.Layout) otherwise I get:

ValueError: not enough values to unpack (expected 2, got 0)

This is not so bad because it is initially intuitive because we need to create reactive objects, but once you dig deeper into the indirection, it is no longer as intuitive since internal variables need to be the original data type, e.g. in order to use xarray.DataArray.sel()

Hi @LecrisUT, just found your post. I just made a recent post here that I think is running into the same problems. For more complicated pipelines of calculations, I’m also finding that just writing a function and binding it to be more transparent and predictable.

On the param documentation, they write:

Enter param.bind, which allows you to define functions that are automatically invoked when their input Parameters change. This serves as a bridge between the reactive rx model and the lower-level ‘push’ model. Unlike the ‘push’ model, where you would explicitly set up watchers and callbacks, param.bind simplifies the process by letting Param manage the mechanics, but also making the dependencies more transparent than in a purely rx approach.

In essence, param.bind offers the declarative nature of reactive expressions and the explicitness of the ‘push’ model. This makes it particularly useful for complex applications where you might want the clarity of explicit function calls for key parts of your pipeline, but also wish to retain the high-level, declarative relationships offered by reactive expressions.

From working with .rx() so far, it seems like great syntactic sugar, wherever the desired operations are supported. But for doing operations on more complicated data structures (list, dict, df), it’s easy to run into situations that the rx() doesn’t natively support (e.g. setting an item in a dictionary doesn’t seem to push updates reactively, while setting an entire dictionary does).

1 Like

A lot of my issues come from nesting param.rx inside a function, e.g.

def _build_layout(loop_values)
    layout = param.rx(holoviews.Layout())
    for indx in loop_values:
        overlay = param.rx(holoviews.Overlay())
        ...
        layout += overlay

I have found that using a dereference function helps in a lot of weird situations:

def rx_dereference(rx_val: param.rx | Any, as_rx: bool = True) -> param.rx | Any:
    if isinstance(rx_val, param.rx):
        rx_val = param.rx(rx_dereference)(rx_val, as_rx=False)
        return rx_val if as_rx else rx_val.rx.value
    return rx_val

my_rx_val = param.rx(do_some_magic)(rx_dereference(weirdly_nested_rx_val))

I still have issues in that using the .rx.value makes it loose the watchers, so it is not an ideal solution. Another hack I use is to add a refresh button and just do the whole top-level loop when needed. Maybe someone has found a better hack.

Had to use the dereferencing trick today to get a rx(data frame) to play properly with a string formatting. Thanks @LecrisUT for the idea!

import param
import panel as pn
import pandas as pd
URL = 'https://datasets.holoviz.org/penguins/v1/penguins.csv'
df = pd.read_csv(URL)
pn.extension()
pn.extension(css_files=["https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"])

select = pn.widgets.Select(value="Adelie",options=df["species"].unique().tolist())
dfrx = dfrx.sample(5)

button_table = pn.widgets.Tabulator(dfrx3[["species","island","year"]], buttons={
    'print': '<i class="fa fa-print"></i>',
    'check': '<i class="fa fa-check"></i>'
})

string = pn.widgets.StaticText()

def dereference(rx):
    return rx.rx.value

button_table.on_click(
    lambda e: string.param.update(value=f'Clicked {e.column!r} on row {e.row}, with val {dereference(dfrx3.iloc[e.row]["species"])}')
)

pn.Column(select, string, button_table)

What I observed is that if a reactive expression gets consumed by a pane, the pane knows to handle the dereferencing at the right time. However, strings don’t know how when to process the dereferencing. Calling the de-reference manually .rx.value would give you an unreactive snapshot of the reactive expression’s value, while the dereference function properly defers that dereferencing until it’s actually needed.

I also tried passing string.param.update a reactive string, but it wasn’t very happy with that option.

1 Like

I believe I’ve made your example a bit more .rx like @kshen-noble. I would not normally prefix with rx_, but I did it here to make it very clear where I create or use reactive expressions.

import param
import panel as pn
import pandas as pd

URL = "https://datasets.holoviz.org/penguins/v1/penguins.csv"

pn.extension(
    "tabulator",
    css_files=[
        "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"
    ],
)

penguins = pn.cache(pd.read_csv)(URL)
unique_species = penguins["species"].unique().tolist()

select = pn.widgets.Select(value="Adelie", options=unique_species)

rx_query_string = pn.rx("species == '{species}'").format(species=select)
rx_selected_penguins = pn.rx(penguins).query(rx_query_string).reset_index(drop=True)

button_table = pn.widgets.Tabulator(
    rx_selected_penguins[["species", "island", "year"]],
    buttons={
        "print": '<i class="fa fa-print"></i>',
        "check": '<i class="fa fa-check"></i>',
    },
    height=500,
)

rx_event = pn.rx()


def rx_event_set(value):
    # Should be a built in method rx_event.rx.set. See https://github.com/holoviz/param/issues/956
    rx_event.rx.value = value

button_table.on_click(rx_event_set)

def text(event):
    if not event:
        return "Click a row"
    else:
        return "Clicked {column!r} on row {row}. Value: {value})".format(
            column=event.column,
            row=event.row,
            value=rx_selected_penguins.iloc[event.row].rx.value,
        )

rx_row_text = pn.rx(text)(rx_event)
string = pn.widgets.StaticText(value=rx_row_text)

pn.Column(select, button_table, string).servable()

My own rule of thumb is to use .rx where it

  • enhances readability or
  • enhances efficiency (because it automatically only recalculates what is needed) or
  • makes something easier to do than with other methods

and not use it

  • Where there is a lot of nested logic (if).

I agree with @Marc 's rules of thumb, and also with his request to @LecrisUT for reproducible examples, e.g. of how you are trying to use * (not *=, right?). My guess is that overlaying two reactive expressions using * isn’t supported even though both will result in something that should be overlayable, but I’d have to see the actual code involved to know whether that is a gap in our implementation of .rx that we can fix or something we can’t fix that we should mention in the docs. Similar for the complicated DataArray operations; I’d have to see what those are to know if there’s some issue we can fix.

Thanks, @Marc! This is helpful. I modified my code to use .format and .rx.value directly as your example suggested and got things working.

Could you help clarify some questions to help me understand the mechanics of the reactivity?

  1. Why is that in this case (and when in general) we have to use .rx.value? The string format examples I’ve seen in several tutorials (e.g. here, here, here) don’t seem to do this.

  2. Why do we have to “break up” the update into two steps, with event_set to push the changes to a placeholder event rx_event and then a second reactive expression that works with the rx_event? Is that for organization, or is that mandatory given the mechanics?

I’m particularly confused about point 1. I did this exercise in a notebook:

i = pn.rx(5)
j = pn.rx(6)
k = i * j
k.rx.value #displays 30

And then, in a subsequent cell, if I do i.rx.value=20, the previous output doesn’t update.

Same if I do "{val}".format(val=k.rx.value) or pn.pane.Markdown("{val}".format(val=k.rx.value)), none of these work.

However, if I have a cell that is k.rx() or k, then that output changes dynamically when I set i.rx.value = something_else in another cell.

Thank you!

I’m particularly confused about point 1. I did this exercise in a notebook:
i = pn.rx(5)
j = pn.rx(6)
k = i * j
k.rx.value #displays 30
And then, in a subsequent cell, if I do i.rx.value=20, the previous output doesn’t update.
Same if I do "{val}".format(val=k.rx.value) or pn.pane.Markdown("{val}".format(val=k.rx.value)), none of these work.
However, if I have a cell that is k.rx() or k, then that output changes dynamically when I set i.rx.value = something_else in another cell.

The main question I guess is if you use a RX object as an INPUT or OUTPUT.

As I understand it (and it lines up with your tests), there are 3 options for OUTPUT:
k … automatically updates because Panel in the background monitors for changes in k.rx.value and updates the display
k.rx … same as above I guess
k.rx.value … you output the actual value (copy of the object, whether its a string, number, ,), so there is no linkage between the displayed value object and the source RX-object anymore (and even as panel monitors for the k.rx.value changes it has no clue about your displayed copy)