Force update of bokeh widgets? - python

I am new to bokeh and writing a small bokeh server app, which has plot and a button. When the button is pressed, data is recalculated and plot updates. The idea is that as soon as the button pressed, it changes the color and label, also a text "calculating..." appears. When calculations are done, plot updates and the text disappears.
However, when button is pressed, it doesn't change color and the text does not appear before the calculations are done (takes several seconds). All this widget update happens after calculations. Question, is it possible to force a widget to update, like flush=True in case of print() or something similar may be?
I could not find anything in bokeh documetation. I have tried also to separate widget changes and calculations and execute them in two separate functions, but it didn't help. Setting a delay between button change and invoke of calculation function also did not help. Seems, like update on widgets only happens on exit from callback function or even later. The only thing which I did not check is CustomJS, but I don't know how to write js code for button update.
Thanks for any help!
Here is a code sample close to what I actually use:
from bokeh.plotting import figure
from bokeh.models import Button, PreText, ColumnDataSource
from bokeh.layouts import row
p = figure()
source = ColumnDataSource(data={"x":[0], "y":[0]})
p.line(x="x", y="y", source=source)
variable = False
# initialise widgets
switchButton = Button(label='Anticrossing OFF', button_type="default")
process_markup = PreText(text='Calculating...', visible=False)
def callback(arg):
global variable
global process_markup
variable = not variable
# change button style
if variable:
switchButton.update(label = 'Anticrossing ON',
button_type = 'success')
else:
switchButton.update(label = 'Anticrossing OFF',
button_type = 'default')
# show "calculating..."
process_markup.update(visible=True)
# do long calculations
x, y = calculate_data(variable)
source.data = {"x":x, "y":y}
# hide "calculating..."
process_markup.update(visible=False)
switchButton.on_click(callback)
col = column(switchButton, process_markup)
curdoc().add_root(row(col, p))

Bokeh can send data updates only when the control is returned back to the server event loop. In your case, you run the computation without every yielding the control, so it sends all of the updates when the callback is already done.
The simplest thing to do in your case is to split the callback into blocks that require synchronization and run each block in a next tick callback:
def callback(arg):
global variable
global process_markup
variable = not variable
# change button style
if variable:
switchButton.update(label = 'Anticrossing ON',
button_type = 'success')
else:
switchButton.update(label = 'Anticrossing OFF',
button_type = 'default')
# show "calculating..."
process_markup.update(visible=True)
def calc():
# do long calculations
x, y = calculate_data(variable)
source.data = {"x":x, "y":y}
# hide "calculating..."
process_markup.update(visible=False)
curdoc().add_next_tick_callback(calc)
Note however that such a solution is only suitable if you're the only user and you don't need to do anything while the computation is running. The reason is that the computation is blocking - you cannot communicate with Bokeh in any way while it's running. A proper solution would require some async, e.g. threads. For more details, check out the Updating From Threads section of the Bokeh User Guide.

Related

Wait for animation to finish - Python

I am trying to fade out some objects in pyside6, I managed to do it, but after the animation I need to change index of Stacked Widget to go to another page. This simulates transition from page to page.
def transition_in(self):
effect1 = QGraphicsOpacityEffect(self.title)
effect2 = QGraphicsOpacityEffect(self.reminder_btn)
#
self.title.setGraphicsEffect(effect1)
self.reminder_btn.setGraphicsEffect(effect2)
#
self.anim_1 = QPropertyAnimation(effect1, b"opacity")
self.anim_2 = QPropertyAnimation(effect2, b"opacity")
#
self.anim_1.setStartValue(0.7)
self.anim_1.setEndValue(0)
self.anim_1.setDuration(200)
self.anim_2.setStartValue(0.7)
self.anim_2.setEndValue(0)
self.anim_2.setDuration(200)
#
self.anim_group = QParallelAnimationGroup()
self.anim_group.addAnimation(self.anim_1)
self.anim_group.addAnimation(self.anim_2)
self.anim_group.start()
The problem is that when trying without changing index of stacked widget, animation works, but when calling setCurrentIndex in stacked widget, it instantly goes to the next page. I tried sleep, but it just freezes the program for some time. calling it in a thread does not work either.

How to Add uirevision Directly to Figure in Plotly Dash for Automatic Updates

I have a Plotly figure built in Python that updates automatically. I want to preserve dashboard zooms even with automatic updates. The documentation in Plotly says this can be done using the layout uirevision field, per the this community writeup. The docs give this as an example of the return dictionary:
return {
'data': data,
'layout': {
# `uirevsion` is where the magic happens
# this key is tracked internally by `dcc.Graph`,
# when it changes from one update to the next,
# it resets all of the user-driven interactions
# (like zooming, panning, clicking on legend items).
# if it remains the same, then that user-driven UI state
# doesn't change.
# it can be equal to anything, the important thing is
# to make sure that it changes when you want to reset the user
# state.
#
# in this example, we *only* want to reset the user UI state
# when the user has changed their dataset. That is:
# - if they toggle on or off reference, don't reset the UI state
# - if they change the color, then don't reset the UI state
# so, `uirevsion` needs to change when the `dataset` changes:
# this is easy to program, we'll just set `uirevision` to be the
# `dataset` value itself.
#
# if we wanted the `uirevision` to change when we add the "reference"
# line, then we could set this to be `'{}{}'.format(dataset, reference)`
'uirevision': dataset,
'legend': {'x': 0, 'y': 1}
}
}
However, my figure is built more like this:
import plotly.express as px
#app.callback(
Output("graph", "figure"),
[Input("interval-component", "n_intervals")])
def display_graph(n_intervals):
# Logic for obtaining data/processing is not shown
my_figure = px.line(my_data_frame, x=my_data_frame.index, y=['line_1', 'line_2'],
title='Some Title', template='plotly_dark')
return my_figure
In other words, since I am not returning a dictionary, but a plotly express figure directly, how can I directly access the uirevision value so that UI changes from the user are preserved?
You can use the update_layout member function of the figure.
my_figure.update_layout(uirevision=<your data>)
More information here: https://plotly.com/python/creating-and-updating-figures/#updating-figure-layouts
Use the figure dictionary, which can be accessed like so:
my_figure['layout']['uirevision'] = 'some_value'
This can also be used to access other useful aspects of the figure, such as changing the line color of a specific line entry:
my_figure['data'][2]['line']['color'] = '#FFFF00'
To see the other entry options, print out my_figure in a Python session.
Note: since the uirevision option isn't documented very well (at least, not in my searching online), I thought it worth posting this as an option.

How do I use `add_next_tick_callback` for a Panel-wrapped Bokeh figure?

I have a Bokeh figure in a notebook (VS Code), which I would like to update when the x_range is changed. To enable callbacks from JS to Python, I wrap the figure in a Panel panel:
from bokeh.plotting import figure, curdoc
import panel as pn
pn.extension(comms='vscode')
fig = figure()
fig.circle([1, 2, 3], [4, 5, 6])
tai = pn.widgets.input.TextAreaInput(sizing_mode='stretch_both')
panel = pn.Row(pn.pane.Bokeh(fig), tai)
def change_callback(attr, old, new):
tai.value += f'{fig.x_range.start}, {fig.x_range.end}\n'
fig.x_range.on_change('start', change_callback)
fig.x_range.on_change('end', change_callback)
panel
In this example, instead of actually updating the figure, I have an additional TextAreaInput to log callback events. This is what it looks like:
The problem is that every change of x_range leads to four events, and the figure update might take a second. (Two events make sense because I have two callbacks, but why is every event sent twice?) I would therefore like to prevent the update from being performed four times.
Looking at the Bokeh documentation, there is a method add_next_tick_callback. The idea would be to add this callback whenever an event occurs, but also remove an old callback if it exists. Something like
ntc = None
def tick_callback():
tai.value += f'{fig.x_range.start}, {fig.x_range.end}\n'
def change_callback(attr, old, new):
global ntc
doc = curdoc()
if ntc is not None:
doc.remove_next_tick_callback(ntc)
ntc = doc.add_next_tick_callback(tick_callback)
tai.value += f'{ntc}\n'
fig.x_range.on_change('start', change_callback)
fig.x_range.on_change('end', change_callback)
The figure update would then be performed in tick_callback.
This doesn't work. change_callback still gets called and creates a series of NextTickCallback objects, but tick_callback never gets called.
I thought this might be due to curdoc() not returning the correct Bokeh document in the context of Panel. There is also fig.document, which however gives the same document as curdoc(). And there is pn.state which has an add_periodic_callback method, but not add_next_tick_callback.
How do I use add_next_tick_callback for a Panel-wrapped Bokeh figure?

How can I use async Python to allow Jupyter button widget clicks to process while running an async recursive function?

I set up a Jupyter Notebook where I'm running a recursive function that clears the output like so, creating an animated output:
from IPython.display import clear_output
active = True
def animate:
myOutput = ""
# do stuff
clear_output(wait=True)
print(myOutput)
sleep(0.2)
if active:
animate()
and that's working perfectly.
But now I want to add in one more step: A speed toggle. What I'm animating is a debugging visualization of a cursor moving through interpreted code as an interpreter I'm writing parses that code. I tried conditional slow-downs to have more time to read what's going on as the parsing continues, but what I really need is to be able to click a button to toggle the speed between fast and slow. Maybe I'll use a slider, but for now I just want a button for proof of concept.
This sounds simple enough. Note that I'm writing this statefully as a class because I need to read / write the state from within another imported class.
Jupyter block 1:
import ipywidgets as widgets
from IPython.display import display
out = widgets.Output()
class ToggleState():
def __init__(self):
self.button = widgets.Button(description="Toggle")
self.button.on_click(self.toggle)
display(self.button)
self.toggleState = False
print("Toggle State:", self.toggleState)
def toggle(self, arg): # arg has to be accepted here to meet on_click requirements
self.toggleState = not self.toggleState
print("Toggle State:", self.toggleState)
def read(self):
return self.toggleState
toggleState = ToggleState()
Then, in Jupyter block 2, note I decided to to do this in a separate block because the clear_output I'm doing with the animate func clears the button if it's in the same block, and therein lies the problem:
active = True
def animate:
myOutput = ""
# do stuff
clear_output(wait=True)
print(myOutput)
if toggleState.read():
sleep(5)
else:
sleep(0.2)
if active:
animate()
But the problem with this approach was that two blocks don't actually run at the same time (without using parallel kernels which is way more complexity than I care for) so that button can't keep receiving input in the previous block. Seems obvious now, but I didn't think about it.
How can I clear input in a way that doesn't delete my button too (so I can put the button in the animating block)?
Edit:
I thought I figured the solution, but only part of it:
Using the Output widget:
out = widgets.Output()
and
with out:
clear_output(wait=True) # clears only the logged output
# logging code
We can render to two separate stdouts within the same block. This works to an extent, as in the animation renders and the button isn't cleared. But still while the animation loop is running the button seems to be incapable of processing input. So it does seem like a synchronous code / event loop blocking problem. What's the issue here?
Do I need an alternative to sleep that frees up the event loop?
Edit 2:
After searching async code in Python, I learned about asyncio but I'm still struggling. Jupyter already runs the code via asyncio.run(), but the components obviously have to be defined as async for that to matter. I defined animate as async and tried using async sleeps, but the event loops still seems to be locked for the button.

Direct calls to function to update matplotlib figure works, calling it repeatedly from a loop only shows final result

Background: I am developing GUI for analyzing experimental imaging data. I have a viewing window (i.e., the matplotlib figure) where I overlay manually selected data points (if any) and optical values (if normalization has occurred) on top of a background image. Below the axis I have a QScrollBar that I can use to manually move to different time points in the data. I initialize it like so:
self.movie_scroll_obj.valueChanged.connect(self.update_axes)
With the relevant part of the associated function looking like the following:
def update_axes(self):
# Determine if data is prepped or unprepped
data = self.data_filt
# UPDATE THE OPTICAL IMAGE AXIS
# Clear axis for update
self.mpl_canvas.axes.cla()
# Update the UI with an image off the top of the stack
self.mpl_canvas.axes.imshow(self.data[0], cmap='gray')
# Match the matplotlib figure background color to the GUI
self.mpl_canvas.fig.patch.set_facecolor(self.bkgd_color)
# If normalized, overlay the potential values
if self.norm_flag == 1:
# Get the current value of the movie slider
sig_id = self.movie_scroll_obj.value()
# Create the transparency mask
mask = ~self.mask
thresh = self.data_filt[sig_id, :, :] > 0.3
transp = mask == thresh
transp = transp.astype(float)
# Overlay the voltage on the background image
self.mpl_canvas.axes.imshow(self.data_filt[sig_id, :, :],
alpha=transp, vmin=0, vmax=1,
cmap='jet')
# Plot the select signal points
for cnt, ind in enumerate(self.signal_coord):
if self.signal_toggle[cnt] == 0:
continue
else:
self.mpl_canvas.axes.scatter(
ind[0], ind[1], color=self.cnames[cnt])
# Tighten the border on the figure
self.mpl_canvas.fig.tight_layout()
self.mpl_canvas.draw()
The overlay of normalized data occurs within the IF statement in the above block of code. When I interact with the QScrollBar object it works exactly as intended. However, I have a play button that I want to be able to click and have the plot update like a movie from whatever the current scrollbar value is. I also want the string on the button to change to "Stop Movie" and have a subsequent click stop the movie. To accomplish this I have connected the pushbutton to the following code:
def play_toggle(self, event):
# Grab the button string
button_str = self.play_movie_button.text()
if button_str == 'Play Movie':
# Update the button string
self.play_movie_button.setText('Stop Movie')
# time.sleep(1)
# Run the play movie function
self.play_movie()
else:
# Update the play movie boolean
self.play_bool = 0
# Update the button string
self.play_movie_button.setText('Play Movie')
Which calls this function:
def play_movie(self):
# Set the play boolean to true
self.play_bool = 1
# Grab the current value of the movie scroll bar
cur_val = self.movie_scroll_obj.value()
# Grab the maximum value of the movie scroll bar
max_val = self.movie_scroll_obj.maximum()
# Being updating the scroll bar value and the movie window
# while self.play_bool == 1:
for n in np.arange(cur_val+5, cur_val+30, 5):
# Check to make sure you haven't reached the end of the signal
# if self.movie_scroll_obj.value() < max_val:
# Update the slider value
if self.play_bool == 1:
self.movie_scroll_obj.setValue(n)
plt.pause(1)
# If button is hit again, break loop
else:
break
Because I used the "valueChanged" signal for the QScrollBar, updating its value in this function calls the update_axes() function. The current iteration is the biproduct of a handful of hours scouring the Internet for solutions to the two following issues:
) The reason I have limited the np.arange call in the for loop (and that I'm using a for loop rather than a while loop) is because if I let it run its course without a built in stop point the GUI crashes and I get a "SpyderKernelApp WARNING No such comm" error.
) The figure does not update with each iteration through the loop. It only updates at the end of the short for loop I currently have implemented.
I am currently developing this using Spyder 4.1.4, Python 3.8, and Qt Designer (using Qt 5.11.1). I'll include a picture of the GUI on the off chance it helps folks orient themselves. The code is currently up on GitHub, but I would need to push my current version and fix a *.yml if anyone decided they were that invested in rescuing me. Happy to provide additional code or access to code as needed.
Image of GUI with visualized data present.:
A potential reason for the figure not updating:
def play_movie(self):
[...]
self.movie_scroll_obj.setValue(n)
This only changes the value of the move_scroll_obj. You are expecting that to call self.update_axes, since you connected it via
self.movie_scroll_obj.valueChanged.connect(self.update_axes)
However, since you are in the play_movie function, all these connect-updates are postponed until the play_movie function returns to the main event loop, i.e. until the loop finishes. Hence you only see the update at the end of function.
This is potentially also the reason for the spyder crash, as with a while loop you would indefinitely fill up the event-queue.
Directly implementing the function call into the loop should fix this behaviour:
def play_movie(self):
[...]
self.movie_scroll_obj.setValue(n)
self.update_axes()
The self.movie_scroll_obj.valueChanged.connect gets redundant at this point and should be deleted (elsewise the event-queue would still be filled up during the loop).

Categories

Resources