How to link indicators to objects (that I can't modify)

Hi everyone, first time posting here. I’m a big fan of holoviews, diving into panel for the first time, so my question might be kind of dumb.

I asked in twitter about making the new indicators watch the value of an arbitrary python object, and I got told about obj.param.watch(). I had already checked the link method, and now I’m trying this one.

The thing is, every link and param.watch() example in the documentation links classes that already build on top of param, so the linking is easy. What I’m trying to do is watch a python object value and change the indicator depending on that value, that is streaming by MQTT. The problem: I can’t modify the class I want to watch, as the source code is made by another team.

So in summary, I just have an object that has a value (let’s call it status) that periodically changes, and I want to watch it without modifying the original class. Is this possible with panel? Or do I need to subclass the original object in order to make a copy of the “status” value that is compatible with param?

Thanks in advance

1 Like

Hi @CesarRodirguez

This is a great question. I don’t have experience is this. But a have an idea. With Panel you can run a callback periodically.

The rough idea is to

Setup a period callback that checks if the python object has changed. If it has you can either update the indicator. Or update an indicator_value = param.Parameter() that can be watched` and reacted to.

I believe the new Panel 0.10 should make it super easy to setup and the documentation is here.

https://panel.holoviz.org/api/panel.io.html

There is a lengthy discussion leading up to this functionality here Streaming data with Bokeh and Panel periodic callbacks?.

FYI. @Jhsmit. I know you are a little bit of an expert in this area. Any idea or suggestions?

1 Like

@CesarRodriguez. Feel free to share some minimum reproducible examples that either demonstrates your challenge or solution. That will all help you and/ or the community find a solution. Thanks.

FYI. I’ve added a Feature Request for Streaming Examples in the Panel Gallery. https://github.com/holoviz/panel/issues/1744.

1 Like

Hey @Marc, thanks for your answer. Here is my minimum reproducible example, and the solution I’ve just tested based on your answer.

The guys I work with have a class that they have developed, on the hardware side of things. This is working for them, but we’re moving to displaying data, so that’s why I’m testing Panel. The class they have is, simplifying a lot, this one:

class Sensor():
    def __init__(self, sensorID, status):
        self.sensorID = sensorID
        self.status = status

Then they have some properties that get a stream directly from the hardware and update that status (and shows it on the device, on a screen). I can’t touch this code, as it’s already in production.

Now that we’re moving to dashboards, this status needs to be displayed. I was kind of stumped by trying multiple things, but the solution inspired by your post, the response I got on twitter and diving through the docs is this one: instead of getting the instances they make, I derive the class and parametrize it.

#Subclassing and parametrizing the status
class DerivedSensor(Sensor, param.Parameterized):
    status = param.Parameter()

test_sensor = DerivedSensor('test', 1) # Instantiate the derived class with the parent's init

number = pn.indicators.Number(
    name=test_sensor.sensorID + ' Status', value=test_sensor.status,
    colors=[(0, 'green'), (1, 'orange'), (9, 'orange'), (10000, 'red')]
)

# Now we rely on the "links" tutorial from panel's doc
def callback(event):
    if event.name == 'status':
        number.value = event.new

#last, define the watcher and show the number
watcher = test_sensor.param.watch(callback, ['status'])
number.show(threaded=True)

This works, and the number now is able to update whenever I modify it on console (akin to what the actual Sensor class does).
The thing that seems weird about this to me is that when I subclass Sensor, I’m defining “status” as a parameter. But then when I instantiate it, I “overwrite”, setting it to whatever I want, instead of using the param library, but it keeps working. If I check its type, it’s whatever I set it too (int if I throw an int at it, float if I throw a float), when I would expect a param type.

I don’t know if this is the intended use of the param library (I tried to look for examples, but they were a bit sparse, focusing on what’s likely the usual way of using the library (defining the class with param use in mind). Also, I’m probably a bit in over my head, as a couple of months ago I basically didn’t know what objects where, only having ever used functional programming, much like I would on my Uni Matlab classes.

IDK, maybe this is the right way to do these things, and it just seems convoluted to me because of lack of OOP experience. If anyone has a simpler way that would be great, but in the end, this works, and coupling the callback definition with the Number definition can make this really scalable.

Thanks for your help!

1 Like

Hi @CesarRodriguez

Great you found a solution and shared it back perfectly. That helps us all. Thanks.

I actually thought you where looking for a way to get streaming data into Panel in the spirit of http://holoviews.org/gallery/apps/bokeh/streaming_psutil.html#apps-bokeh-gallery-streaming-psutil.

Regarding param.Parameter() you are using it as expected and it works as intended. It just creates an attribute that can hold any type and enables watching when it’s value changes.

param is the foundation of HoloViews and Panel. It’s such a powerful thing. There are strong rumors that improved documentation is coming up.

1 Like

As I said the implementation works, but I just discovered you need to be careful with it. Further testing with multiple indicators and watchers seemed to fail keeping indicators independent. So if you are reading this in the future looking for answers (and no one has posted the better way that there must be), keep on going.

The indicators all changing at once because I was using the init method from my team’s parent class instead of the param one. This made my objects use the global param namespace, instead of the instance one, which made it so the watchers couldn’t watch single instances. When deriving from existing classes this can be solved by inheriting first from param.Parameterized, so it’s the first parent (parent conflict solving is kind of complicated in python, unless you ask Raymond Hettinger). That is, if you can get away with it.

This way, your derived class will use param.Parameterized.init, and will have it’s own param namespace, where watchers can live happy, watching the instance level, instead of watching the whole class. I have yet to check how will it work with the changes in the paremeter coming from the other parent (the Sensor class I alluded to earlier). Also, this locks you in onto using keyword arguments when initializing, which is a bit of a hassle that you trade off for a ton more readability.

TLDR: inherit param.Parameterized first to get watcher independence if you want to scale.

2 Likes