Updating plot of xarray data when it changes

I’m working on a UI which displays telemetry being regularly received from an instrument. The data arrives on disk; at regular intervals I poll for new data, and if present then I update the graphs.

The data is effectively 4-dimensional: a time series of multiple attributes of multiple sensors. I am forming the received data points (which arrive as Pandas DataFrames, one per time interval) into an xarray.DataArray and plotting it with hvplot.line.

Ideally, I would like the plots to update, but I can’t see any way to modify the DataArray.
Instead, I am clearing and recreating the view every update.

The users are complaining that the graphs update while they are interacting with them. I have partially fixed this by creating a Select widget for the chosen attribute, rather than relying on hvplot default behaviour, so that the selected option is “sticky”.

However the users are still complaining that the graph resets while they are zoomed into a point of interest.

Is there any way to read and re-apply the current zoom extents for example? I have tried using a hook function but could not get it to work.

I have looked at this existing post. The main difference is that I’m using xarray rather than a simple pandas DataFrame, and I can’t see how to update the array. I’ve also looked at this existing post, but my data updates are caused by a timer not an interactive user change.

The following code is very close to reproducing the issue. Note however that in my real application, data may arrive out of order; I can’t simply append new data on updates, as new dataframes may have appeared out of order.

import panel as pn
import pandas as pd
import xarray as xr
import hvplot.xarray
from datetime import datetime
import random


class Issue:
    UPDATE_INTERVAL_MS = 5000
    NUM_SENSORS = 5

    def __init__(self):
        self.state = {f'Sensor {i+1}': {'voltage': 0.0, 'current': 0.0} for i in range(self.NUM_SENSORS)}
        self.data = []
        self.data_timestamps = []
        self.page = pn.Column()
        self.choice_ctrl = pn.widgets.Select(name='Attribute', options=['voltage', 'current'])
        self.do_update()

    def make_dataframe(self):
        for sensor_state in self.state.values():
            sensor_state['voltage'] += random.uniform(-0.5, 0.5)
            sensor_state['current'] += random.uniform(-0.5, 0.5)
        return pd.DataFrame.from_dict(self.state, orient='index')

    def do_update(self):
        self.data.append(self.make_dataframe())
        self.data_timestamps.append(datetime.now())

        alldata = xr.concat([xr.DataArray(dataframe) for dataframe in self.data], dim='timestamp')
        alldata = alldata.rename({'dim_0': 'sensor', 'dim_1': 'attribute'})
        alldata['timestamp'] = self.data_timestamps
        alldata.name = 'value'

        self.page.clear()
        alldata_interactive = alldata.interactive(loc='right_top')
        chosen_attribute_i = alldata_interactive.sel(attribute=self.choice_ctrl)
        self.page.append(chosen_attribute_i.hvplot.line(by='sensor'))

    def run(self):
        pn.state.add_periodic_callback(lambda : self.do_update(), self.UPDATE_INTERVAL_MS)
        self.page.servable()

iss = Issue()
iss.run()

I ran this in a virtual environment with the following packages installed:

panel==1.4.5
hvplot==0.10.0
xarray==2024.7.0
jupyter==1.1.1
jupyter_bokeh==4.0.5

I think you might need to wrap it in a DynamicMap to prevent interruption

https://pydeas.readthedocs.io/en/latest/holoviz_interactions/tips_and_tricks.html#Wrap-DynamicMap-around-Panel-methods-to-maintain-extents-and-improve-runtime

Other ref: Start best practices notebook by ahuang11 · Pull Request #6819 · holoviz/panel · GitHub

Thanks @ahuang11. What is it that I wrap in the DynamicMap? I don’t have an interactive control, or a method that is returning data in response to user activity, I just have a timer-triggered update. Should I create a method that “yields” data for example?

I can’t modify the xarray (although not stated to be immutable, it seems to have no methods to modify it). I can only create a new one.

What I usually do is create a HoloViews pane on init with DynamicMap wrapping it, e.g.
hv.DynamicMap(pn.bind(update, event_button.param.clicks))

Then on update, I simply set .object =....

Here, I create a Matplotlib pane on init, and set the .object in the update function.

Also, I recommend inheriting from param.Parameterized / pn.viewer.Viewer

@ahuang11 Thanks for trying to help. Sorry to be thick, but I still don’t get what I’m supposed to do. I have a method that is called by a timer; so I don’t suppose I need to bind anything. But I don’t understand how to use a DynamicMap.
My data is not being shown in response to user action; the data itself is changing with time. So caching is not relevant.
Is your next instalment of your Medium article, where you use a DynamicMap, available yet?

Maybe this resource will help

You’re right I should get to my next Medium article haha

@ahuang11 I think that might be what I was looking for, thank you. It seems one uses a DynamicMap and a Pipe - definitely not something I’ve tried.