I am trying to create a widget callback function that resets the entire plot to its initialized state but it is not working. I expect the users to click Sample as many times as they want then be able to reset the vbar plot to its initialized state.
I have already created the python callback function and used some print functions to debug a bit but the plot is not resetting.
plot2 = figure(plot_height=400, plot_width=int(1.618*600), title="Block Party",
tools="crosshair,reset,save",
x_range=[0, 11], y_range=[0, max(counts)])
plot2.vbar(x='x', top='y', source=source2, width=0.8)
"""
Set up widgets
"""
title2 = TextInput(title="Plot Title", value='Blocks')
sample = Button(label="Sample", button_type="success")
reset = Button(label="Reset", button_type="success")
# Callback
def reset_window_2():
global source2
print("I was clicked")
np.random.seed(42)
unique, counts = np.unique(np.random.randint(low=1, high=11, size=100), return_counts=True)
source2 = ColumnDataSource(data=dict(x=unique, y=counts))
plot2 = figure(plot_height=400, plot_width=int(1.618 * 600), title="Block Party",
tools="crosshair,reset,save",
x_range=[0, 11], y_range=[0, max(counts)])
plot2.vbar(x='x', top='y', source=source2, width=0.618)
reset.js_on_click(CustomJS(args=dict(p=plot2), code="""
plots2.reset.emit()
"""))
print("Check 2")
reset.on_click(reset_window_2)
# Set up layouts and add to document
inputs1 = column(title1, sigma, mu)
inputs2 = column(title2, sample, reset)
tab1 = row(inputs1, plot1, width=int(phi*400))
tab2 = row(inputs2, plot2, width=int(phi*400))
tab1 = Panel(child=tab1, title="Like a Gauss")
tab2 = Panel(child=tab2, title="Sampling")
tabs = Tabs(tabs=[tab1, tab2])
curdoc().add_root(tabs)
curdoc().title = "Sample Dash"
The print functions occur but the reset does not. Any ideas on how to reset the entire plot to init?
Bokeh plots don't show up merely by virtue of being created. In Bokeh server apps, they have to be put in a layout and added to curdoc. You presumably did this:
curdoc.add_root(plot2)
If you want to replace plot2 in the browser, it has to be replaced in curdoc. The plot2 you create in your callback is just a local variable in a function. It pops into existence for the duration of the function, only exists inside the function, then gets thrown away when the function ends. You haven't actually done anything with it. To actually replace in curdoc, it will be easier to store the plot in an explicit layout:
lauyot = row(plot)
curdoc().add_root(layout)
Then in your callback, you can replace what is in the layout:
layout.children[0] = new_plot
All that said, I would actually advise against doing things this way. The general, always-applicable best-practice for Bokeh is:
Always make the smallest change possible.
A Bokeh plot has dozen of sub-components (ranges, axes, glyphs, data sources, tools, ...) Swapping out an entire plot is a very heavyweight operation Instead, what you should do, is just update the data source for the plot you already have, to restore the data it started with:
source2.data = original_data_dict # NOTE: set from plain python dict
That will restore the bars to their original state, making the smallest change possible. This is the usage Bokeh has been optimized for, both in terms of efficient internal implementation, as well as efficient APIs for coding.
Related
Question:
I am struggling for a more than a week now to do something probably pretty simple:
I want to make a time series plot in which i can control the x axis
range/zoom with a datetime picker widget.
I also want the datetime picker to be updated when the x range is
changed with the plot zoom controls
So far I can do either but not both. It did work for other widgets such as the intslider etc.
Requirements:
If the solution has 1 DatetimeRangePicker to define the x range or 2 DatetimePicker widgets (one for start one for end) would both work great for me.
my datasets are huge so it would be great if it works with datashader
Any help is much appreciated :)
What I tried:
MRE & CODE BELOW
Create a DatetimeRangePicker widget, plot the data using pvplot and set the xlim=datatimerangepicker.
Result: the zoom changes with the selected dates on the widget, but zooming / panning the plot does not change the values of the widget.
Use hv.streams.RangeX stream to capture changes in x range when panning / zooming. Use a pn.depends function to generate plot when changing DatetimeRangePicker widget.
Result: the figure loads and zooming/panning changes the widget (but is very slow), but setting the widget causes AttributeError.
Create a DatetimePicker widget for start and end. Link them with widget.jslink() bidirectionally to x_range.start and x_range.end of the figure.
Result: figure loads but nothing changes when changing values on the widget or panning/zooming.
MRE & Failed Attempts
Create Dataset
import pandas as pd
import numpy as np
import panel as pn
import holoviews as hv
import hvplot.pandas
hv.extension('bokeh')
df = pd.DataFrame({'data': np.random.randint(0, 100, 100)}, index=pd.date_range(start="2022", freq='D', periods=100))
Failed Method 1:
plot changes with widget, but widget does not change with plot
range_select = pn.widgets.DatetimeRangePicker(value=(df.index[0], df.index[-1]))
pn.Column(df.data.hvplot.line(datashade=True, xlim=range_select), range_select)
Failed Method 2:
Slow and causes AttributeError: 'NoneType' object has no attribute 'id' when changing widget
range_select = pn.widgets.DatetimeRangePicker(value=(df.index[0], df.index[-1]))
#pn.depends(range_x=range_select.param.value)
def make_fig(range_x):
fig = df.data.hvplot.line(datashade=True, xlim=range_x)
pointer = hv.streams.RangeX(source=fig)
tabl = hv.DynamicMap(show_x, streams=[pointer]) # plot useless table to make it work
return fig + tabl
def show_x(x_range):
if x_range is not None:
range_select.value = tuple([pd.Timestamp(i).to_pydatetime() for i in x_range])
return hv.Table({"start": [x_range[0]], "stop": [x_range[1]]}, ["start", "stop"]) if x_range else hv.Table({})
pn.Column(range_select, make_fig)
Failed Method 3:
does not work with DatetimePicker widget, but does work other widgets (e.g. intslider)
pn.widgets.DatetimePicker._source_transforms = ({}) # see https://discourse.holoviz.org/t/using-jslink-with-pn-widgets-datepicker/1116
# datetime range widgets
range_strt = pn.widgets.DatetimePicker()
range_end = pn.widgets.DatetimePicker()
# int sliders as example that some widgets work
int_start_widget = pn.widgets.IntSlider(start=0, step=int(1e6), end=int(1.7e12))
int_end_widget = pn.widgets.IntSlider(start=0, step=int(1e6), end=int(1.7e12))
points = df.data.hvplot.line(datashade=True) # generate plot
# link widgets to plot:
int_start_widget.jslink(points, value="x_range.start", bidirectional=True)
int_end_widget.jslink(points, value="x_range.end", bidirectional=True)
range_strt.jslink(points, value="x_range.start", bidirectional=True)
range_end.jslink(points, value="x_range.end", bidirectional=True)
pn.Row(points,pn.Column( range_strt, range_end, int_start_widget, int_end_widget,))
Here is what I came up with:
range_select = pn.widgets.DatetimeRangePicker(value=(df.index[0].to_pydatetime(), df.index[-1].to_pydatetime()))
curve = df.data.hvplot.line(datashade=True).apply.opts(xlim=range_select, framewise=True)
rxy = hv.streams.RangeX(source=curve)
def update_widget(event):
new_dates = tuple([pd.Timestamp(i).to_pydatetime() for i in event.new])
if new_dates != range_select.value:
range_select.value = new_dates
rxy.param.watch(update_widget, 'x_range')
pn.Column(range_select, curve)
Basically we use .apply.opts to apply current widget value as the xlim dynamically (and set framewise=True so the plot ranges update dynamically). Then we instantiate a RangeX stream which we use to update the widget value. Annoyingly we have to do some datetime conversions because np.datetime64 and Timestamps aren't supported in some cases.
I am trying to animate a plot using visvis.
This is the example code they have:
import visvis as vv
# read image
ims = [vv.imread('astronaut.png')]
# make list of images: decrease red channel in subsequent images
for i in range(9):
im = ims[i].copy()
im[:,:,0] = im[:,:,0]*0.9
ims.append(im)
# create figure, axes, and data container object
a = vv.gca()
m = vv.MotionDataContainer(a)
# create textures, loading them into opengl memory, and insert into container.
for im in ims:
t = vv.imshow(im)
t.parent = m
and I added:
app = vv.use()
app.Run()
This worked. But I needed to animate a plot, not an image, so I tried doing this:
import visvis as vv
from visvis.functions import getframe
# create figure, axes, and data container object
a = vv.gca()
m = vv.MotionDataContainer(a, interval=100)
for i in range(3):
vv.plot([0, 2+i*10], [0, 2+i*10])
f = getframe(a)
t = vv.imshow(f)
t.parent = m
a.SetLimits(rangeX=[-2, 25], rangeY=[-2, 25])
app = vv.use()
app.Run()
The axes are being initialized very big, that is why I am using set limits, and the output is not animated. I am getting only the last frame so a line from (0,0) to (22, 22).
Does anyone know a way of doing this with visvis?
It turns out adding the frame as a child of MotionDataContainer was not the way to go. The function vv.plot returns an instance of the class Line, and one should add the line as a child. If anyone is having the same problem, I could write a more detailed answer.
EDIT Adding a more detailed answer as requested:
To animate a plot made of lines, one must simply add the lines as children of MotionDataContainer. Taking my example in the question above, one would write:
import visvis as vv
# create figure, axes, and data container object
a = vv.gca()
m = vv.MotionDataContainer(a, interval=100)
for i in range(3):
line = vv.plot([0, 2+i*10], [0, 2+i*10])
line.parent = m
app = vv.use()
app.Run()
In my special case, I even needed to animate multiple lines being drawn at the same time.
To do this, I ended up defining a new class that, like MotionDataContainer, also inherits from MotionMixin, and change the class attribute delta which specifies how many objects should be made visible at the same time. For that, one has to also rewrite the function _SetMotionIndex.
(See visvis official source code: https://github.com/almarklein/visvis/blob/master/wobjects/motion.py)
Disclaimer: Concerning the animation of multiple objects, I have no idea if this is the intended use or if this is the easiest solution, but this is what worked for me.
I wrote a code to display live feed of analog data. The code uses pyfirmata to define pins and pull readings. I've set the funcanimation to pull all 12 channels when the port is open. Currently, matplotlib checkbutton is used to show/hide live feed of the channels.
I'd like to manipulate the matplotlib checkbutton so that only the channels that are checked are actually read instead of just being hidden.
The matplotlib widget module is a little too sophisticated for me to break down to a level where I can modify it. What I'd like to do is write a true/false status on each index depending on its visibility then put a nested if statements in the funcanimation to read only the visible lines. I'd appreciate if anyone could share me a sample code to allow me to do that.
Here is a segment of my code:
##check buttons
lines = [ln0, ln1, ln2, ln3, ln4, ln5, ln6, ln7, ln8, ln9, ln10, ln11]
labels = [str(ln0.get_label()) for ln0 in lines]
visibility = [ln0.get_visible() for ln0 in lines]
check = CheckButtons(ax1, labels, visibility)
for i, c in enumerate(colour):
check.labels[i].set_color(c)
def func(label):
index = labels.index(label)
lines[index].set_visible(not lines[index].get_visible())
check.on_clicked(func)
## define pins
a0 = due.get_pin('a:0:i')
a1 = due.get_pin('a:1:i')
a2 = due.get_pin('a:2:i')
a3 = ...
##funcanimation
def rt(i):
t.append(datetime.now())
if due.is_open == True:
T0.append(round(a0.read()*3.3/0.005, 1))
T1.append(round(a1.read()*3.3/0.005, 1))
...
Here is the graph and checkbuttons when run:
click here
Thanks,
I figured it out. There is a get_status function embedded in the matplotlib widget which returns a tuple of trues and falses to indicate the status of check buttons. I used this to write a nested if statements in the funcanimation so that only checked ones are read.
I'm still somewhat new to Bokeh, and I've run into a problem I haven't been able to solve.
I have a Bokeh plot visualizing some streaming data in two separate figures. For various reasons the users of the plot may want to clear the two plots of the current datapoints upon clicking a button.
What would be the good way to clear the figures? I am yet to come upon a good solution.
My code looks something like:
#Defining plots
plot_data = ColumnDataSource(dict(x=[],y=[],z=[]))
p = figure(plot_height = 600, plot_width = 800,
x_axis_label = 'X',
y_axis_label = 'Y')
p2 = figure(plot_height = 600, plot_width = 800,
x_axis_label = 'X',
y_axis_label = 'Z')
doc = curdoc()
The data source is getting updated in an async loop:
async def loop():
while True:
data = await socket.recv_pyobj()
new_data = get_last_data(data)
#update ColumnDataSource
doc.add_next_tick_callback(partial(update,new_data))
doc.add_root(column(gridplot([p,p2], plot_width=1000)))
try:
testloop = IOLoop.current()
testloop.spawn_callback(loop)
except KeyboardInterrupt:
testloop.close()
and the ColumnDataSource is getting updated through the following function when new datapoints appear in the stream (parsed as a dataframe)
def update(new_data):
input_data = dict(x=new_data['x'], y=new_data['y'], z=new_data['z'])
plot_data.stream(input_data, rollover=500)
My initial idea for clearing the figures through a button click is the following:
#Defining button for clearing plot
button = Button(label="CLEAR PLOT", button_type="danger")
def clear_plot(event):
plot_data = ColumnDataSource(dict(x=[],y=[],z=[]))
button.on_event(ButtonClick,clear_plot)
This is not working, and if I understand the stream method correctly, that is at the heart of the problem, as new data is continuously getting appended to the source and the above clear_plot function will not really clear the stream data source. How would one go about clearing the stream data source such that the figures are cleared?
By assigning a new value to plot_data, you're just changing the variable itself. Anything that got the reference to the previous value of plot_data will still have that old reference.
Instead, try changing the data attribute of the data source:
def clear_plot(event):
plot_data.data = {k: [] for k in plot_data.data}
I have an embedded widget in a QT5 gui connected via PlotWidget.
I am trying to plot 2 streams of live data Voltage (self.p1) & Current (self.p2). Voltage on the left axis & Current on the right. So far I have each data stream associated with its relevant axis.
However my problem is that the Current plot (self.p2) is not in the correct area of the display. This particular plot appears in the upper left hand corner of the widget, it appears before the LHD axis. Its best to view the image to view the problem.
View Me
.
I know the problem lies in the setup function & self.p2 (Current) is being placed in the wrong location but my searching hasn't produced an answer.
Could someone please help?
Code used to generate graph, is called once on start up:
def pg_plot_setup(self): # not working still on left axis
self.p1 = self.graphicsView.plotItem
# x axis
self.p1.setLabel('bottom', 'Time', units='s', color='g', **{'font-size':'12pt'})
self.p1.getAxis('bottom').setPen(pg.mkPen(color='g', width=3))
# Y1 axis
self.p1.setLabel('left', 'Voltage', units='V', color='r', **{'font-size':'12pt'})
self.p1.getAxis('left').setPen(pg.mkPen(color='r', width=3))
self.p2 = pg.ViewBox()
self.p1.showAxis('right')
self.p1.scene().addItem(self.p2)
self.p1.getAxis('right').linkToView(self.p2)
self.p2.setXLink(self.p1)
# Y2 axis
self.p1.setLabel('right', 'Current', units="A", color='c', **{'font-size':'12pt'}) #<font>Ω</font>
self.p1.getAxis('right').setPen(pg.mkPen(color='c', width=3))
and code used to update display, is called via QTimer:
def update_graph_plot(self):
start = time.time()
X = np.asarray(self.graph_X, dtype=np.float32)
Y1 = np.asarray(self.graph_Y1, dtype=np.float32)
Y2 = np.asarray(self.graph_Y2, dtype=np.float32)
pen1=pg.mkPen(color='r',width=1.0)
pen2=pg.mkPen(color='c',width=1.0)
self.p1.plot(X,Y1,pen=pen1, name="V", clear=True)
self.p2.addItem(pg.PlotCurveItem(X,Y2,pen=pen2, name="I"))
I found the answer buried in here MultiplePlotAxes.py
adding this self.p2.setGeometry(self.p1.vb.sceneBoundingRect()) to function 'update_graph_plot' adjusts the size of viewbox each time the scene is updated, but it has to be in the update loop.
or add this self.p1.vb.sigResized.connect(self.updateViews) to function 'pg_plot_setup' as part of the setup, which shall then automatically call this function
def updateViews(self):
self.p2.setGeometry(self.p1.vb.sceneBoundingRect())
to resize viewbox (self.p2) each time self.p1 updates.