How to set / replace text in bqplot label? - python

Original question:
How do you set the text in a bqplot label using a slider?
I have solved that problem and show my answer below.
New question:
Why is this the behavior? Doesn't it seem crazy for bqplot to expose the text as a list and then not allow item assignment?
I want to replace the label text with something new.
In particular, I want the label to reflect a changing slider value.
After being confused for some time, this is what I have found:
To create a label, the text (and x and y position too) have to be given as a list:
label(text=['my label']).
You can't just pass the string:
label(text='my label').
To update this text, you must replace the whole list:
my_label.text = ['new label'].
You cannot just assign the element:
my_label.text[0] = 'new label'.
Here is code that demonstrates the behavior.
# Imports
import numpy as np
from bqplot import pyplot as plt
from ipywidgets import IntSlider, FloatSlider, Layout
from IPython.display import display
# Make plot and label
xs = np.linspace(0, 10, 100)
ys = np.sin(xs)
layout=Layout(width='40%', height='300px')
fig = plt.figure(layout=layout)
line = plt.plot(xs, ys)
value = 123.45678
lab = plt.label(text=['{:3.3f}'.format(value)], x=[5], y=[0.2], colors=['Red'], align='middle')
# Create and link sliders
good_slider = FloatSlider(description='good')#, min=0, max=100, step=0.001)
bad_slider = FloatSlider(description='bad')#, min=0, max=100, step=0.001)
def good_update_label(change):
lab.text = ['{:3.3f}'.format(change['new'])]
def bad_update_label(change):
lab.text[0] = '{:3.3f}'.format(change['new'])
good_slider.observe(good_update_label, 'value')
bad_slider.observe(bad_update_label, 'value')
# See what happens!
display(good_slider, bad_slider, fig)

Related

How to remove points from a dataframe based on a selected area on a plot

I have some experimental data that is often flawed with artifacts exemplified with something like this:
I need a quick way to manually select these random spikes and remove them from datasets.
I figured that any plotting library with a focus on interactive plots should have an easy way to do this but so far I keep struggling with finding a simple way to do what I want.
I'm a Matplotlib/Seaborn guy and this calls for interactive solution. I briefly checked Plotly, Bokeh and Altair and decided to go with the first one. My first attempt looks like this:
import pandas as pd
import plotly.graph_objects as go
from ipywidgets import interactive, HBox, VBox, Button
url='https://drive.google.com/file/d/1hCX8Bn_y30aXVN_TyHTTx015u44pO9yB/view?usp=sharing'
url='https://drive.google.com/uc?id=' + url.split('/')[-2]
df = pd.read_csv(url, index_col=0)
f = go.FigureWidget()
for col in df.columns[-1:]:
f.add_scatter(x = df.index, y=df[col], mode='markers+lines',
selected_marker=dict(size=5, color='red'),
marker=dict(size=1, color='lightgrey', line=dict(width=1, color='lightgrey')))
t = go.FigureWidget([go.Table(
header=dict(values=['selector range'],
fill = dict(color='#C2D4FF'),
align = ['left'] * 5),
cells=dict(values=['None selected' for col in ['ID']],
fill = dict(color='#F5F8FF'),
align = ['left'] * 5)
)])
def selection_fn(trace,points,selector):
t.data[0].cells.values = [selector.xrange]
def update_axes(dataset):
scatter = f.data[0]
scatter.x = df.index
scatter.y = df[dataset]
f.data[0].on_selection(selection_fn)
axis_dropdowns = interactive(update_axes, dataset = df.columns)
button1 = Button(description="Remove points")
button2 = Button(description="Reset")
button3 = Button(description="Fit data")
VBox((HBox((axis_dropdowns.children)), HBox((button1, button2, button3)), f,t))
Which gives:
So I managed to get Selector Box x coordinates (and temporarily print them inside the table widget). But what I couldn't figure out is how to easily bind a function to button1 that would take as an argument Box Selector coordinates and remove selected points from a dataframe and replot the data. So something like this:
def on_button_click_remove(scatter.selector.xrange):
mask = (df.index >= scatter.selector.xrange[0]) & (df.index <= scatter.selector.xrange[1])
clean_df = df.drop(df.index[mask])
scatter(data = clean_df...) #update scatter plot
button1 = Button(description="Remove points", on_click = on_button_click_remove)
I checked https://plotly.com/python/custom-buttons/ but I am still not sure how to use it for my purpose.
I suggest to use Holoviews and Panel.
They are high level visualization tools that facilitate the creation and control of low level bokeh, matplotlib or plotly figures.
Here are an example:
import panel as pn
import holoviews as hv
import pandas as pd
from bokeh.models import ColumnDataSource
# This example use bokeh as backend.
# You can try plotly or matplotlib with minor modification on the codes below.
# For example you can use on_selection callback from Plotly
# https://plotly.com/python/v3/selection-events/
hv.extension('bokeh')
display( pn.extension( ) ) # activate panel
df=pd.read_csv('spiked_data.csv',index_col=0).reset_index()
pt = hv.Points(
data=df, kdims=['index', 'A' ]
).options( marker='x', size=2,
tools=['hover', 'box_select', 'lasso_select', 'reset'],
height=250, width=600
)
fig = hv.render(pt)
source = fig.select({'type':ColumnDataSource})
bt = pn.widgets.Button(name='remove selected')
def rm_sel(evt):
i = df.iloc[source.selected.indices].index # get index to delete
df.drop(i, inplace=True, errors='ignore') # modify dataframe
source.data = df # update data source
source.selected.indices=[] # clear selection
pn.io.push_notebook(app) # update figure
bt.on_click(rm_sel)
app=pn.Column(fig,'Click to delete the selected points', bt)
display(app)
A related example can be found in this SO answer

Is there a better way to use Jupyter IntSlider with Python Plotly?

In the following code block I use a Jupyter IntSlider to adjust the number of dots visualized in a Plotly express scatter 3d plot. The example already fits my use case, but I noticed that Plotly has built-in slider functionalities that could improve the performance.
As a Plotly beginner I find it quite hard to map the slider example from Plotly to my use case.
Any suggestions?
import numpy as np
import plotly.express as px
import pandas as pd
from ipywidgets import interact, widgets
NUM_DOTS = 100
NUM_DIMS = 3
random_data = pd.DataFrame(np.random.random((NUM_DOTS,NUM_DIMS) ), columns=['x_1','x_2','x_3'])
def update_plotly(x):
fig = px.scatter_3d(random_data[:x], x='x_1', y='x_2', z='x_3')
fig.show()
interact(update_plotly, x=widgets.IntSlider(min=1, max=NUM_DOTS, step=1, value=NUM_DOTS))
Actually it's not that hard to build the slider, just follow the path of the example shown by plotly:
import plotly.graph_objects as go
import numpy as np
NUM_DOTS = 100
NUM_DIMS = 3
# Create figure
fig = go.Figure()
# Add traces, one for each slider step
for step in np.arange(1, NUM_DOTS, 1):
#Random data
random_data = pd.DataFrame(np.random.random((step, NUM_DIMS)), columns=['x_1','x_2','x_3'])
fig.add_trace(
go.Scatter3d(
visible=False,
line=dict(color="#00CED1", width=6),
name="𝜈 = " + str(step),
z=random_data['x_3'],
x=random_data['x_1'],
y=random_data['x_2']))
# Make 10th trace visible
fig.data[10].visible = True
# Create and add slider
steps = []
for i in range(len(fig.data)):
step = dict(
method="restyle",
args=["visible", [False] * len(fig.data)],
)
step["args"][1][i] = True # Toggle i'th trace to "visible"
steps.append(step)
sliders = [dict(
active=10,
currentvalue={"prefix": "Frequency: "},
pad={"t": 50},
steps=steps
)]
fig.update_layout(
sliders=sliders
)
fig.show()
resulting:
or with more points:
As you correctly figured out, it is way more performant than the widget slider, because with this method, you just toggle the trace visibility in the 3D Scatter chart.

Continious update of matplotlib plot in Jupyter

I am working on a Jupyter Notebook and I am using the following ipywidget to set a threshold value:
Thr = widgets.IntSlider(value=-17, min=-30, max=-13, step=1, description='Threshold: ', disabled=False, continuous_update=True, orientation='horizontal', readout=True, readout_format='d')
Thr
Next, I am masking a numpy array using that value with:
import numpy.ma as ma
test= ma.masked_less_equal(S_images[0], Thr.value)
And finally I plot the result with:
plt.figure(figsize = (15,15))
plt.imshow(test[0], cmap='gray')
The ipywidget is in a different Jupyter cell than the other code so when I change the value of Thr I have to manually run again the cell where the masking and ploting takes place.
My question is: I have always seen those interactive plots where you change a parameter (in my case the ipywidget Thr) and automatically the plot gets updated.
I see that widgets.IntSlider has a continuous_update parameter which seems to be close to what I want but still cannot get the behaviour I want.
Any idea if this is doable or possible?
_ EDIT _
Starting from the comment of ac24, I am adapting the example he proposes:
from IPython.display import display, clear_output
import ipywidgets as ipy
import matplotlib.pyplot as plt
import numpy as np
# setup figure
n = 10
out = ipy.Output()
# show random mesh
def update(idx):
with out:
clear_output()
fig, ax = plt.subplots(figsize = (5,5))
h = ax.imshow(S_images[0]) # here I put my image
h.set_data(np.ma.masked_less_equal(S_images[0], slider.value)) # here I set the task to masked accordint to the `slider.value`
fig.canvas.flush_events()
fig.canvas.draw()
plt.show()
slider = ipy.IntSlider(min = 0, max = 10, orientation = 'vertical')
widget = ipy.interactive(update, idx = slider)
layout = ipy.Layout(
# display = 'flex',
# flex_flow = 'row',
# justify_content = 'space-between',
# align_items = 'center',
)
widgets = ipy.HBox(children=(slider, out), layout = layout)
display(widgets)
The example works very nice and is just what I was looking for. However, I have a small question regargind the layout. Originally I am working with 3 Images so I would like to have them displayed as follows, each one with its slider next to it to do the task: (the image below is not real, just made it up to represent what I would like)
EDIT 2
in this occasion, the question is, once I select a value in the slider, I would write to geotiff that raster. For this I am using the following code:
with rasterio.open('/Path/20190331_VV_Crop') as src:
ras_meta = src.profile
with rasterio.open('/path/Threshold.tif', 'w', **ras_meta) as dst:
dst.write(X)
However, I am not sure how to reference the numpy array in dst.write(X)
I've adapted the example I gave into a class, as you want to link a specific output and slider instance, but create multiple groups of them. Setting the layout of the output widget avoids the widget resizing all the time as you slide the slider.
from IPython.display import display, clear_output
import ipywidgets as ipy
import matplotlib.pyplot as plt
import numpy as np
# setup figure
n = 10
class SliderAndImage():
# show random mesh
def update(self, idx):
with self.out:
clear_output()
fig, ax = plt.subplots(figsize = (5,5))
h = ax.imshow(np.random.rand(n, n))
h.set_data(np.random.rand(n, n))
fig.canvas.flush_events()
fig.canvas.draw()
plt.show()
def make_slider_and_image(self):
self.out = ipy.Output(layout=ipy.Layout(width='200px', height='200px'))
slider = ipy.IntSlider(min = 0, max = 10, orientation = 'vertical')
widget = ipy.interactive(self.update, idx = slider)
layout = ipy.Layout(
# display = 'flex',
# flex_flow = 'row',
# justify_content = 'space-between',
# align_items = 'center',
)
widgets = ipy.HBox(children=(slider, self.out), layout = layout)
return widgets
children = []
for _ in range(3):
widgets = SliderAndImage()
children.append(widgets.make_slider_and_image())
display(ipy.HBox(children))

Plot does not update when slider callback is used with self-defined function

I try to implement several sliders and use an own function, foo, to calculate new values to update the plot. I tried to modify the example from the documentation, but without success; the page loads, I can move the sliders but the plot does not update (please note that I want to use a function I cannot define in javascript; I now use foo just for illustration purposes).
There must be an issue with the callback (see entire code below):
def callback(source=source):
data = source.data
a_dynamic = cb_obj.a_slider.value # cb_obj.get('a_slider')
b_dynamic = cb_obj.b_slider.value
x, y = data['x'], data['y']
y = foo(x, a_dynamic, b_dynamic)
source.change.emit()
I don't know how
1) to access the slider values correctly (using cb_obj.<slider_id>.value, or cb_obj.get(<slider_id>) or something completely else?)
2) to give the slider an actual ID; the title argument is only the text that appears above the slider but is probably not its ID and using the id argument does not work the way I use it.
How would I do this correctly?
import numpy as np
from bokeh.layouts import row, widgetbox
from bokeh.models import CustomJS, Slider
from bokeh.plotting import figure, output_file, show, ColumnDataSource
import bokeh
bokeh.io.reset_output()
def foo(xval, a, b):
return np.power(xval, a) + b
# some artificial data
a0 = 2.
b0 = 1.
x = np.linspace(-100., 100, 1000)
y = foo(x, a0, b0)
source = ColumnDataSource(data=dict(x=x, y=y))
plot = figure(plot_width=400, plot_height=400)
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)
def callback(source=source):
data = source.data
a_dynamic = cb_obj.a_slider.value # cb_obj.get('a_slider')
b_dynamic = cb_obj.b_slider.value
x, y = data['x'], data['y']
y = foo(x, a_dynamic, b_dynamic)
source.change.emit()
a_slider_obj = Slider(start=0, end=3, value=a0, step=0.1, id='a_slider',
title="a_slider", callback=CustomJS.from_py_func(callback))
# callback.args["a_slider"] = a_slider_obj
b_slider_obj = Slider(start=-4, end=4, value=b0, step=0.5, id='b_slider',
title="b_slider", callback=CustomJS.from_py_func(callback))
# callback.args["b_slider"] = b_slider_obj
layout = row(
plot,
widgetbox(a_slider_obj, b_slider_obj),
)
show(layout)
EDIT:
Seems that this actually does not work but that one should use the bokeh server for this. I leave the question open for now in case someone wants to post the solution using the server. I then either accept this answer, add an own one or delete the question again if no answer appears.
You're correct that you most likely need the server, especially if you want to use some Python functions that you can't easily express with JavaScript.
Here's the code that works. You can run it with bokeh serve. Note also that you pass some invalid numbers to np.power since e.g. -100 ** 2.1 is nan.
import numpy as np
from bokeh.layouts import row, widgetbox
from bokeh.models import Slider
from bokeh.plotting import figure, ColumnDataSource, curdoc
def foo(xval, a, b):
print(xval.min(), xval.max(), np.isnan(xval).any(), a, b)
return np.power(xval, a) + b
# some artificial data
a0 = 2.
b0 = 1.
x = np.linspace(-100., 100, 1000)
y = foo(x, a0, b0)
source = ColumnDataSource(data=dict(x=x, y=y))
plot = figure(plot_width=400, plot_height=400)
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)
a_slider_obj = Slider(start=0, end=3, value=a0, step=0.1, id='a_slider', title="a_slider")
b_slider_obj = Slider(start=-4, end=4, value=b0, step=0.5, id='b_slider', title="b_slider")
def callback(attr, old, new):
data = source.data
# Since the callback is used by two sliders, we can't just use the `new` argument
a_dynamic = a_slider_obj.value
b_dynamic = b_slider_obj.value
# Here I assume that you wanted to change the value and not just create an unused variable
data['y'] = foo(data['x'], a_dynamic, b_dynamic)
a_slider_obj.on_change('value', callback)
b_slider_obj.on_change('value', callback)
layout = row(
plot,
widgetbox(a_slider_obj, b_slider_obj),
)
curdoc().add_root(layout)

How to assign a plot to a variable and use the variable as the return value in a Python function

I am creating two Python scripts to produce some plots for a technical report. In the first script I am defining functions that produce plots from raw data on my hard-disk. Each function produces one specific kind of plot that I need. The second script is more like a batch file which is supposed to loop around those functions and store the produced plots on my hard-disk.
What I need is a way to return a plot in Python. So basically I want to do this:
fig = some_function_that_returns_a_plot(args)
fig.savefig('plot_name')
But what I do not know is how to make a plot a variable that I can return. Is this possible? Is so, how?
You can define your plotting functions like
import numpy as np
import matplotlib.pyplot as plt
# an example graph type
def fig_barh(ylabels, xvalues, title=''):
# create a new figure
fig = plt.figure()
# plot to it
yvalues = 0.1 + np.arange(len(ylabels))
plt.barh(yvalues, xvalues, figure=fig)
yvalues += 0.4
plt.yticks(yvalues, ylabels, figure=fig)
if title:
plt.title(title, figure=fig)
# return it
return fig
then use them like
from matplotlib.backends.backend_pdf import PdfPages
def write_pdf(fname, figures):
doc = PdfPages(fname)
for fig in figures:
fig.savefig(doc, format='pdf')
doc.close()
def main():
a = fig_barh(['a','b','c'], [1, 2, 3], 'Test #1')
b = fig_barh(['x','y','z'], [5, 3, 1], 'Test #2')
write_pdf('test.pdf', [a, b])
if __name__=="__main__":
main()
If you don't want the picture to be displayed and only get a variable in return, then you can try the following (with some additional stuff to remove axis):
def myplot(t,x):
fig = Figure(figsize=(2,1), dpi=80)
canvas = FigureCanvasAgg(fig)
ax = fig.add_subplot()
ax.fill_between(t,x)
ax.autoscale(tight=True)
ax.axis('off')
canvas.draw()
buf = canvas.buffer_rgba()
X = np.asarray(buf)
return X
The returned variable X can be used with OpenCV for example and do a
cv2.imshow('',X)
These import must be included:
from matplotlib.figure import Figure
from matplotlib.backends.backend_agg import FigureCanvasAgg
The currently accepted answer didn't work for me as such, as I was using scipy.stats.probplot() to plot. I used matplotlib.pyplot.gca() to access an Axes instance directly instead:
"""
For my plotting ideas, see:
https://pythonfordatascience.org/independent-t-test-python/
For the dataset, see:
https://github.com/Opensourcefordatascience/Data-sets
"""
# Import modules.
from scipy import stats
import matplotlib.pyplot as plt
import pandas as pd
from tempfile import gettempdir
from os import path
from slugify import slugify
# Define plot func.
def get_plots(df):
# plt.figure(): Create a new P-P plot. If we're inside a loop, and want
# a new plot for every iteration, this is important!
plt.figure()
stats.probplot(diff, plot=plt)
plt.title('Sepal Width P-P Plot')
pp_p = plt.gca() # Assign an Axes instance of the plot.
# Plot histogram. This uses pandas.DataFrame.plot(), which returns
# an instance of the Axes directly.
hist_p = df.plot(kind = 'hist', title = 'Sepal Width Histogram Plot',
figure=plt.figure()) # Create a new plot again.
return pp_p, hist_p
# Import raw data.
df = pd.read_csv('https://raw.githubusercontent.com/'
'Opensourcefordatascience/Data-sets/master//Iris_Data.csv')
# Subset the dataset.
setosa = df[(df['species'] == 'Iris-setosa')]
setosa.reset_index(inplace= True)
versicolor = df[(df['species'] == 'Iris-versicolor')]
versicolor.reset_index(inplace= True)
# Calculate a variable for analysis.
diff = setosa['sepal_width'] - versicolor['sepal_width']
# Create plots, save each of them to a temp file, and show them afterwards.
# As they're just Axes instances, we need to call get_figure() at first.
for plot in get_plots(diff):
outfn = path.join(gettempdir(), slugify(plot.title.get_text()) + '.png')
print('Saving a plot to "' + outfn + '".')
plot.get_figure().savefig(outfn)
plot.get_figure().show()

Categories

Resources