clear datapoints from bokeh plot - python

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}

Related

How to I make a PyQtGraph scrolling graph clear the previous line within a loop

I wish to plot some data from an array with multiple columns, and would like each column to be a different line on the same scrolling graph. As there are many columns, I think it would make sense to plot them within a loop. I'd also like to plot a second scrolling graph with a single line.
I can get the single line graph to scroll correctly, but the graph containing the multiple lines over-plots from the updated array without clearing the previous lines.
How do I get the lines to clear within the for loop. I thought that setData, might do the clearing. Do I have to have a pg.QtGui.QApplication.processEvents() or something similar within the loop? I tried to add that call but had it no effect.
My code:
#Based on example from PyQtGraph documentation
import numpy as np
import pyqtgraph as pg
win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('pyqtgraph example: Scrolling Plots')
timer = pg.QtCore.QTimer()
plot_1 = win.addPlot()
plot_2 = win.addPlot()
data1 = np.random.normal(size=(300))
curve1 = plot_1.plot(data1)
data_2d = np.random.normal(size=(3,300))
def update_plot():
global data1, data_2d
data1[:-1] = data1[1:]
data1[-1] = np.random.normal()
curve1.setData(data1)
for idx, n in enumerate(data_2d):
n[:-1] = n[1:]
n[-1] = np.random.normal()
curve2 = plot_2.plot(n,pen=(idx))
curve2.setData(n)
#pg.QtGui.QApplication.processEvents() #Does nothing
timer = pg.QtCore.QTimer()
timer.timeout.connect(update_plot)
timer.start(50)
if __name__ == '__main__':
pg.exec()
You could clear the plot of all curves each time with .clear(), but that wouldn't be very performant. A better solution would be to keep all the curve objects around and call setData on them each time, like you're doing with the single-curve plot. E.g.
curves_2d = [plot_2.plot(pen=idx) for idx, n in enumerate(data_2d)]
# ... in update_plot
curves_2d[idx].setData(n)

Problem with LivePlots in Tkinter (Python)

I'm still quite new to Python, so please do not be too harsh with me.
I'm trying to implement a live plot in a GUI via Tkinter.
With the help of some example codes and google I got it working.
Now I want to erase/clear the Old Plot with click on a Button.
However I get a raise value error when trying to do so.
in the init I initialize one Plot like this:
self.x = 0
self.i = 0
self.delta_i = 1
self.xy_data = []
self.figure = pyplot.figure()
self.figure.set_size_inches((12,7), forward=True)
self.subplot = self.figure.add_subplot(221)
self.line,=self.subplot.plot([],[], color="blue")
self.line2, = self.subplot.plot([],[], color="red")
In the actual drawing function (which is executed over and over for a specific amount of time) I'm doing this:
self.Timer = Tdiff
self.FixTemp=TargetTemp
self.ActualTemp =Value1
self.xy_data += [[self.Timer,self.ActualTemp]]
self.xy2_data += [[self.Timer,self.FixTemp]]
self.subplot.lines.remove(self.line)
self.subplot.lines.remove(self.line2)
self.line, = self.subplot.plot(
[row[0] for row in self.xy_data[0::self.delta_i]],
[row[1] for row in self.xy_data[0::self.delta_i]],
color="blue")
self.line2, = self.subplot.plot(
[row[0] for row in self.xy2_data[0::self.delta_i]],
[row[1] for row in self.xy2_data[0::self.delta_i]],
color="red")
In the Button Function I have tried to do:
self.subplot.cla() # But that clears even the Subplot title and x/y labels. As I have in total 3
subplots it would not be very code effective to initialize them again, I think.
self.subplot.lines.remove(self.line) and self.subplot.lines.remove(self.line2) # But I'm getting a rase Value Error
I would be glad if someone could help out here.
Thank you

Modifying matplotlib checkbutton

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.

Bokeh reset figure on success widget click

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.

call back from slider change not updating my plot in bokeh in jupyter lab?

I am working on a Bokeh visualisation of datasets across a number of categories. The initial part of the visual is a donut chart of the categories showing the total number of items in each category. I am trying to get the chart to update based on a min-max range using RangeSlider - but the chart does not update.
The input source for the glyphs is the output from a create_cat_df - which is returned as a Pandas DF, then converted into a CDS using ColumnDataSource.from_df().
The chart appears okay when this code is run (with slider alongside) - but moving the slider changes nothing.
There is a similar post here.
The answer here was useful in putting me onto from_df - but even after following this I can't get the code to work.
def create_doc(doc):
### INPUT widget
cat_min_max = RangeSlider(start=0, end=1000, value=[0, 1000], step=1, title="Category min-max items (m)")
inputs = column(cat_min_max, width=300, height=850) # in preparation for multiple widgets
### Tooltip & tools
TOOLTIPS_2 = [("Item", "$item") # a sample
]
hover_2 = HoverTool(tooltips=TOOLTIPS_2, names = ['cat'])
tools = [hover_2, TapTool(), WheelZoomTool(), PanTool(), ResetTool()]
### Create Figure
p = figure(plot_width=width, plot_height=height, title="",
x_axis_type=None, y_axis_type=None,
x_range=(-420, 420), y_range=(-420, 420),
min_border=0, outline_line_color=None,
background_fill_color="#f0e1d2",
tools = tools, toolbar_location="left")
p.xgrid.grid_line_color = None
p.ygrid.grid_line_color = None
# taptool
url = "https://google.com/" #dummy URL
taptool = p.select(type=TapTool)
taptool.callback = OpenURL(url=url)
# create cat_source CDS using create_cat_df function (returns pandas df) and 'from_df' method
cat_source = ColumnDataSource.from_df(create_cat_df(cat_min_max.value[0], cat_min_max.value[1]))
## plot category wedges
p.annular_wedge('centre_x', 'centre_y', 'inner', 'outer', 'start', 'end', color='color',
alpha='alpha', direction='clock', source=cat_source, name='cat')
r = row([inputs, p])
def callback(attr, old, new):
cat_source.data = ColumnDataSource.from_df(create_cat_df(cat_min_max.value[0], cat_min_max.value[1]))
cat_min_max.on_change('value', callback)
doc.add_root(r)
show(create_doc)
I would like to get the code working and the chart updating. There are a number more glyphs & different data layers to layer in, but I want to get the basics working first.
According to Bokeh documentation the ColumnDataSource.from_df() method returns a dictionary while you need to pass a ColumnDatSource to the source argument in p.annular_wedge(source = cat_source)
So instead of:
cat_source = ColumnDataSource.from_df(create_cat_df(cat_min_max.value[0], cat_min_max.value[1]))
You should do:
cat_source = ColumnDataSource(data = ColumnDataSource.from_df(create_cat_df(cat_min_max.value[0], cat_min_max.value[1])))

Categories

Resources