Dynamically add widgets and update plot

Hello,

I would like to build a GUI where users can upload the CSV files (FileInput), select columns (MultiChoice), and generate a plot.
The corresponding ColorPicker widget will be generated or deleted when adding or deleting a column.

The challenge I’m facing is related to updating the plot color.
When users pick a new color from the ‘ColorPicker’ widget, the plot does not immediately reflect the new color choice. (But when adding a new column, the color changes are applied correctly.)

The following are my codes,

import param
import pandas as pd
import hvplot.pandas
import panel as pn
import holoviews as hv
from io import StringIO


class DataViewer(param.Parameterized):
    file_input = param.Parameter(pn.widgets.FileInput(accept='.csv', multiple=True, filename=[]))
    trial = param.Parameter(pn.widgets.MultiChoice(name='Trial'))
    plot = param.Parameter()
    color_pickers = param.Dict({})

    def __init__(self, **params):
        super().__init__(**params)
        self.file_input.param.watch(self._load_file, 'filename')
        self.trial.param.watch(self._update_plot, 'value')
        self.plot = hv.Curve([])
        self.dfs = {}

    def _load_file(self, event):
        if self.file_input.value is None:
            return
        
        self.trial.value = []  # reset MultiChoice
        options = []
        for filename, content in zip(self.file_input.filename, self.file_input.value):
            df = pd.read_csv(StringIO(content.decode()))
            filename_without_extension = filename.rsplit('.', 1)[0]  # remove file extension
            self.dfs[filename_without_extension] = df
            options += [f'{filename_without_extension}-{col}' for col in df.columns[1:]]

        self.trial.options = options

    def _update_color(self, event):
        self._update_plot()  # regenerate plot

    def _update_plot(self, event=None):
        if not self.trial.value:  # exit if no trial selected
            return
        
        # update ColorPicker widgets when adding or delecting a trial from MultiChoice
        for trial in self.trial.value:
            if trial not in self.color_pickers:
                color_picker = pn.widgets.ColorPicker(name=trial, value="#000000")
                color_picker.param.watch(self._update_plot, 'value')
                self.color_pickers[trial] = color_picker
        for trial in list(self.color_pickers):
            if trial not in self.trial.value:
                del self.color_pickers[trial]

        self.plot = hv.Overlay([self.dfs[trial.split('-')[0]].hvplot.line(x='Time', y=trial.split('-')[1], color=self.color_pickers[trial].value, label=trial) for trial in self.trial.value])

    @pn.depends('plot')
    def plot_view(self):
        return self.plot

    def color_picker_view(self):
        return pn.Row(*self.color_pickers.values())

    def panel(self):
        return pn.Column(self.file_input, self.trial, self.color_picker_view, self.plot_view)


viewer = DataViewer()
viewer.panel().servable()

Here is the example data:

Time,Trial_1,Trial_2
0,10,20
1,20,30
2,20,40
3,20,90
4,60,10

The GUI:

Sorry for not providing a minimal code example to replicate this issue.
Because if I just create a ColorPicker and connect it to a _update_plot function, it works well.
I am struggling to identify the part of the process that might be causing the problem.
Any thoughts or suggestions are greatly appreciated.
Thank you in advance!

Hi @PikaPei,

I think when you say the colour is applied when you add a new column that is all your code being re-run. I don’t think you have anything inside your class ‘watching’ for a colour change so it is currently unlinked, you need to watch for the colour being changed run the plot update on the back of that.

Hi @carl,

I think color_picker and _update_color are correctly linked.
In the new codes, I add messages in the _update_color and _update_plot for debugging.
(and also revise one wrong line in the previous code: color_picker.param.watch(self._update_plot, 'value'), ‘self._update_plot’ → ‘self._update_color’ )

import param
import pandas as pd
import hvplot.pandas
import panel as pn
import holoviews as hv
from io import StringIO


class DataViewer(param.Parameterized):
    file_input = param.Parameter(pn.widgets.FileInput(accept='.csv', multiple=True, filename=[]))
    trial = param.Parameter(pn.widgets.MultiChoice(name='Trial'))
    plot = param.Parameter()
    color_pickers = param.Dict({})
    debug_view = param.Parameter(pn.pane.Markdown('Initialize debug_view'))
    debug_view_2 = param.Parameter(pn.pane.Markdown('Initialize debug_view_2'))

    def __init__(self, **params):
        super().__init__(**params)
        self.file_input.param.watch(self._load_file, 'filename')
        self.trial.param.watch(self._update_plot, 'value')
        self.plot = hv.Curve([])
        self.dfs = {}

    def _load_file(self, event):
        if self.file_input.value is None:
            return
        
        self.trial.value = []  # reset MultiChoice
        options = []
        for filename, content in zip(self.file_input.filename, self.file_input.value):
            df = pd.read_csv(StringIO(content.decode()))
            filename_without_extension = filename.rsplit('.', 1)[0]  # remove file extension
            self.dfs[filename_without_extension] = df
            options += [f'{filename_without_extension}-{col}' for col in df.columns[1:]]

        self.trial.options = options

    def _update_color(self, event):
        self.debug_view.object = 'Enter _update_color(), before self._update_plot()'
        self._update_plot()  # regenerate plot
        self.debug_view.object = 'Enter _update_color(), after self._update_plot()'

    def _update_plot(self, event=None):
        if not self.trial.value:  # exit if no trial selected
            return
        
        # update ColorPicker widgets when adding or delecting a trial from MultiChoice
        for trial in self.trial.value:
            if trial not in self.color_pickers:
                color_picker = pn.widgets.ColorPicker(name=trial, value="#000000")
                color_picker.param.watch(self._update_color, 'value')
                self.color_pickers[trial] = color_picker
        for trial in list(self.color_pickers):
            if trial not in self.trial.value:
                del self.color_pickers[trial]
        
        self.debug_view_2.object = 'Enter _update_plot(), before hv.Overlay'
        self.plot = hv.Overlay([self.dfs[trial.split('-')[0]].hvplot.line(x='Time', y=trial.split('-')[1], color=self.color_pickers[trial].value, label=trial) for trial in self.trial.value])
        self.debug_view_2.object = 'Enter _update_plot(), after hv.Overlay'

    @pn.depends('plot')
    def plot_view(self):
        return self.plot

    def color_picker_view(self):
        return pn.Row(*self.color_pickers.values())

    def panel(self):
        return pn.Column(self.file_input, self.trial, self.color_picker_view, self.plot_view, self.debug_view, self.debug_view_2)


viewer = DataViewer()
viewer.panel().servable()

Here is a quick test. I think something happened in hv.Overlay?
Because ‘Enter _update_plot(), after hv.Overlay’ was not shown after selecting the new color.
But in the next block, when accessing viewer.plot, the plot was already updated. This made me feel confused.

Not sure of the reason. But it is solved after updating to panel 1.0.4.

3 Likes