Interactive matplotlib plot with two sliders - python

I used matplotlib to create some plot, which depends on 8 variables. I would like to study how the plot changes when I change some of them. I created some script that calls the matplotlib one and generates different snapshots that later I convert into a movie, it is not bad, but a bit clumsy.
I wonder if somehow I could interact with the plot regeneration using keyboard keys to increase / decrease values of some of the variables and see instantly how the plot changes.
What is the best approach for this?
Also if you can point me to interesting links or a link with a plot example with just two sliders?

In addition to what #triplepoint mentioned, have a look at the slider widget.
There's an example on the matplotlib examples page. It's a graphical slider bar rather than keyboard bindings, but it works quite well for what you want to do.
Also note that to guarantee the sliders and buttons remain responsive and not garbage-collected, references to the objects (amp_slider, freq_slider, etc.) should be maintained by yourself.
(I'm making this community wiki, as I'm just copy-pasting from the example. This particular example teaches bad habits (e.g. from pylab import *), but it gets the point across. The example has been fixed to avoid the use of pylab.)
from numpy import pi, sin
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button, RadioButtons
def signal(amp, freq):
return amp * sin(2 * pi * freq * t)
axis_color = 'lightgoldenrodyellow'
fig = plt.figure()
ax = fig.add_subplot(111)
# Adjust the subplots region to leave some space for the sliders and buttons
fig.subplots_adjust(left=0.25, bottom=0.25)
t = np.arange(0.0, 1.0, 0.001)
amp_0 = 5
freq_0 = 3
# Draw the initial plot
# The 'line' variable is used for modifying the line later
[line] = ax.plot(t, signal(amp_0, freq_0), linewidth=2, color='red')
ax.set_xlim([0, 1])
ax.set_ylim([-10, 10])
# Add two sliders for tweaking the parameters
# Define an axes area and draw a slider in it
amp_slider_ax = fig.add_axes([0.25, 0.15, 0.65, 0.03], facecolor=axis_color)
amp_slider = Slider(amp_slider_ax, 'Amp', 0.1, 10.0, valinit=amp_0)
# Draw another slider
freq_slider_ax = fig.add_axes([0.25, 0.1, 0.65, 0.03], facecolor=axis_color)
freq_slider = Slider(freq_slider_ax, 'Freq', 0.1, 30.0, valinit=freq_0)
# Define an action for modifying the line when any slider's value changes
def sliders_on_changed(val):
line.set_ydata(signal(amp_slider.val, freq_slider.val))
fig.canvas.draw_idle()
amp_slider.on_changed(sliders_on_changed)
freq_slider.on_changed(sliders_on_changed)
# Add a button for resetting the parameters
reset_button_ax = fig.add_axes([0.8, 0.025, 0.1, 0.04])
reset_button = Button(reset_button_ax, 'Reset', color=axis_color, hovercolor='0.975')
def reset_button_on_clicked(mouse_event):
freq_slider.reset()
amp_slider.reset()
reset_button.on_clicked(reset_button_on_clicked)
# Add a set of radio buttons for changing color
color_radios_ax = fig.add_axes([0.025, 0.5, 0.15, 0.15], facecolor=axis_color)
color_radios = RadioButtons(color_radios_ax, ('red', 'blue', 'green'), active=0)
def color_radios_on_clicked(label):
line.set_color(label)
fig.canvas.draw_idle()
color_radios.on_clicked(color_radios_on_clicked)
plt.show()

I followed the advice to check widgets in jupyter, and they work very well.
The example script is uploaded in GitHub https://github.com/LeonidBystrykh/course-python-for-beginners/blob/master/Interactive_dots.ipynb
from __future__ import print_function
from ipywidgets import interact, interactive, fixed, interact_manual
import ipywidgets as widgets
import matplotlib.pyplot as plt, random
def series(dots, colr):
a,b=[],[]
for i in range(dots):
a.append(random.randint(1,100))
b.append(random.randint(1,100))
plt.scatter(a,b, c=colr)
return()
interact(series, dots=(1,100,1), colr=["red","orange","brown"]);
The image copy is below

Matplotlib has some fairly nice gui functionality. There are some documentation examples in the source tarball of matplotlib, in /examples/user_interfaces and matplotlib>/examples/event_handling. Specifically on keyhandling is: http://matplotlib.sourceforge.net/examples/event_handling/keypress_demo.html
I have done something kind of similar to what you are aiming for:
import numpy as np
import pylab
class plotter:
def __init__(self, initial_values):
self.values
self.fig = pylab.figure()
pylab.gray()
self.ax = self.fig.add_subplot(111)
self.draw()
self.fig.canvas.mpl_connect('key_press_event',self.key)
def draw(self):
im = your_function(self.values)
pylab.show()
self.ax.imshow(im)
def key(self, event):
if event.key=='right':
self.values = modify()
elif event.key == 'left':
self.values = modify()
self.draw()
self.fig.canvas.draw()
I was using this to shift through displaying different images in a stack on keypresses, but you should be able to put logic in to modify your values given keyboard input.
If you want to do things like have the user input values, I think the examples have options for dialog boxes, but if you just want to increment/decrement a number of variables, just defining keyboard pairs for them in this manner might work well

For ipython or jupyter notebooks you can use ipywidgets:
from ipywidgets import *
def update(w=0,h=0):
print(h+w)
interact(update, w= widgets.IntSlider(value=1, min=0, max=7, step=1) ,
h= widgets.IntSlider(value=1, min=0, max=7, step=1) );
See documentation here:
https://ipywidgets.readthedocs.io/en/stable/examples/Using%20Interact.html

Use waitforbuttonpress(timeout=0.001) then plot will see your mouse ticks.

You could also consider using the Quibbler package.
Quibbler automatically makes your normal matplotlib graphics interactive. There is no need to write callback functions.
See some examples in the Quibbler docs. In particular, see a specific example of a plot with two sliders.
from pyquibbler import iquib, initialize_quibbler
initialize_quibbler()
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
import numpy as np
%matplotlib tk
# Figure setup:
fig = plt.figure(figsize=(4, 6))
axs = fig.add_axes([0, 0.36, 1, 0.64])
axs.axis('square')
axs.axis((-10, 10, -10, 10))
axs.axis('off')
axs_slider1 = fig.add_axes([0.3, 0.16, 0.55, 0.03])
axs_slider2 = fig.add_axes([0.3, 0.06, 0.55, 0.03])
# Defining quib input variables:
nPoints = iquib(120)
nCycles = iquib(30)
# Define downstream function quib calculations based on the quib inputs:
phi = np.linspace(0, 2 * np.pi * nCycles, nPoints)
r = np.linspace(0, 10, nPoints)
# Plot the data:
axs.plot(r * np.cos(phi), r * np.sin(phi), 'r-')
# Create quib-linked slider widgets:
Slider(ax=axs_slider1, label='nCycles', valmin=0, valmax=200, valstep=1, valinit=nCycles)
Slider(ax=axs_slider2, label='nPoints', valmin=0, valmax=200, valstep=1, valinit=nPoints)
For transparency: I am one of the developers of Quibbler.

I don't think that simply plotting graphs using plt.plot will allow you to do this. You will need to yourself make a custom GUI script/app by embedding Matplotlib into it. Currently, Matplotlib supports all the major GUI toolkits - PyGTK+, PyQt4 and wxPython.
I use wxPython and embedding matplotlib in it is fairly easy. Similar should be case with the other GUI toolkits. You can get all the information you need for this in the book -
It is available on amazon here.

Related

How do I make multiple span selectors work on the same axis in Matplotlib?

As the title suggests, I am trying to put multiple SpanSelector widgets on the same axis. In the test code below (adapted from this example), I am using a multi-plot figure with the qt backend, to zoom in on the two regions selected by the span selectors. I want to make sure that the two widgets can function independently. I have set non-overlapping initial spans for them both when the figure is generated. I also need to make sure that ignore_event_outside is set to True for both span selectors, so that trying to move/resize one does not re-draw the other span selector at that location. I thought this is would be enough to make sure the two widgets function independently and there is no ambiguity in which object should respond to a given event.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector
# Fixing random state for reproducibility
np.random.seed(19680801)
mosaic = """AA
BC"""
fig = plt.figure(figsize=(8, 6))
ax_dict = fig.subplot_mosaic(mosaic)
(ax0,ax1,ax2) = list(ax_dict.values())
x = np.arange(0.0, 5.0, 0.01)
y = np.sin(2 * np.pi * x) + 0.5 * np.random.randn(len(x))
ax0.plot(x, y, color='black')
ax0.set_ylim(-2, 2)
ax0.set_title('Press left mouse button and drag '
'to select a region in the top graph')
line1, = ax1.plot([], [], color='dodgerblue')
line2, = ax2.plot([], [], color='coral')
def onselect1(xmin, xmax):
indmin, indmax = np.searchsorted(x, (xmin, xmax))
indmax = min(len(x) - 1, indmax)
region_x = x[indmin:indmax]
region_y = y[indmin:indmax]
if len(region_x) >= 2:
line1.set_data(region_x, region_y)
ax1.set_xlim(region_x[0], region_x[-1])
ax1.set_ylim(region_y.min(), region_y.max())
fig.canvas.draw_idle()
def onselect2(xmin, xmax):
indmin, indmax = np.searchsorted(x, (xmin, xmax))
indmax = min(len(x) - 1, indmax)
region_x = x[indmin:indmax]
region_y = y[indmin:indmax]
if len(region_x) >= 2:
line2.set_data(region_x, region_y)
ax2.set_xlim(region_x[0], region_x[-1])
ax2.set_ylim(region_y.min(), region_y.max())
fig.canvas.draw_idle()
span1 = SpanSelector(
ax0,
onselect1,
"horizontal",
useblit=True,
props=dict(alpha=0.5, facecolor="dodgerblue"),
interactive=True,
drag_from_anywhere=True,
ignore_event_outside=True
)
span2 = SpanSelector(
ax0,
onselect2,
"horizontal",
useblit=True,
props=dict(alpha=0.5, facecolor="coral"),
interactive=True,
drag_from_anywhere=True,
ignore_event_outside=True
)
# Set useblit=True on most backends for enhanced performance.
# Set default values
xmin1, xmax1 = 1.4, 2.3
span1.extents = (xmin1, xmax1)
span1.onselect(xmin1, xmax1)
xmin2, xmax2 = 2.5, 3.1
span2.extents = (xmin2, xmax2)
span2.onselect(xmin2, xmax2)
plt.show()
However, whenever I try to interact with the two widgets, things don't go as planned. As seen in the GIF below, when I first click on one of the span selectors (red in this example) after the figure is generated, and move/resize it, the other widget (blue in this example) responds to it, even though ignore_event_outside is set to True. Subsequent interactions seem to work almost fine but still look strange. In this example, moving/resizing the blue span selector makes the red span selector "blink" off until the mouse button is released. Please note that the opposite does not happen. Also note that the cursor does not change (to ↔) when I hover over the handles for the blue span selector - I have click on the handle before the cursor shape changes. Again, the red span selector is not affected in the same way.
I know I can try to assign different mouse buttons to the two span selectors in this case, but I eventually plan to generate 3-4 span selectors on the same figure and I would simply run out of mouse buttons.
Additional info:
OS: Windows 10
Python version: 3.9.13
Matplotlib version: 3.5.3
Jupyter Notebook server version: 6.4.12
IPython version: 8.4.0
N.B.- Matplotlib community forum post for this issue.

UserWarning: Animation was deleted without rendering anything

Note: only the last code chunk brings an error. The earlier chunks are to give context to the animation that I want. This is all in Jupyter for Windows.
I have a matplotlib pyplot with two line segments, one is "wrong" and the other is "right." I want to animate the graph to start with both lines where the blue "wrong" line is,and have the red one pivot and move to be in the right place. The "wrong" line goes from (x,y) = (-1.25,9.1) to (0.75,8.0). The "right" line goes from (-1.25,9.7) to (0.75,7.5)
Here is the code for the static comparison:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.animation import FuncAnimation
boring_fig = plt.figure()
blue = plt.plot([-1.25,.75], [9.1,8], color = 'b', label = 'wrong')
red = plt.plot([-1.25,.75], [9.7,7.5], color = 'r', label = 'right')
plt.show()
Now I want to start them both where the blue line is, and then have the red line incrementally move to the correct position. I made these two arrays for y coordinates to incrementally change between lines. Then I graph everything but the red line, in hopes of adding it as an animation after this.
y_left = np.array([9.1, 9.16, 9.22, 9.28, 9.34, 9.4, 9.46, 9.52, 9.58, 9.64, 9.7])
y_right = np.array([8.0, 7.95, 7.9, 7.85, 7.8, 7.75, 7.7, 7.65, 7.6, 7.55, 7.5])
fig = plt.figure()
blue = plt.plot([-1.25,.75], [9.1,8], color = 'b', label = 'wrong')
plt.show()
And then I try to animate the red line segment shifting along those incremented y values. I get the error somewhere in here:
def animate_1(i):
return plt.plot([-1.25,.75], [y_left[i],y_right[i]], color = 'r'),
anim = FuncAnimation(fig = fig, func = animate_1, interval = 100, frames = 10)
plt.show(anim)
And I get this message: "UserWarning: Animation was deleted without rendering anything. This is most likely unintended. To prevent deletion, assign the Animation to a variable that exists for as long as you need the Animation."
I spent hours trying to figure this out, but I am too much of a noob. Please help.
It turns out I had to install ffmpeg for windows:
https://www.wikihow.com/Install-FFmpeg-on-Windows
Now the graph shows up but I can't see it animating. Oh well. That is an entirely different question.
here is the end graph I made (with a couple of extra details)
Add this line to the beginning of your code in order to see the animation work:
%matplotlib notebook
In Jupyter Notebook (at least on Windows) specifying the GUI toolkit will allow you to see the animation updating, not just a static graph. You can read more about toolkits and user interfaces here.
If there is a message that says:
Warning: Cannot change to a different GUI toolkit: ...
then restart the Jupyter kernel and run the cell again to update the GUI. For troubleshooting the GUI, see this answer.
In regards to the original question, if you set blit = False in FuncAnimation then the animation should display. Otherwise, you can add a return blue, statement to the animate function. See my code below and feel free to ask questions!
Complete Code
%matplotlib notebook
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.animation import FuncAnimation
def move_line(num_frames):
x_vals = [-1.25,.75]
y_left_array = np.linspace(9.1, 9.7, num=num_frames, endpoint=True)
y_right_array = np.linspace(8.0, 7.5, num=num_frames, endpoint=True)
fig = plt.figure()
red = plt.plot(x_vals, [9.7,7.5], color = 'r', label = 'right')
blue, = plt.plot([], [], color = 'b', label = 'wrong')
def animate(I):
y_vals = [y_left_array[i],y_right_array[I]]
blue.set_data(x_vals,y_vals)
return blue,
anim = FuncAnimation(
fig,
animate,
frames = num_frames,
interval = 1000/30,
repeat = False,
blit = True
)
plt.show()
return anim
animate_lines = move_line(100)

Matplotlib: Toolbar vs Toolmanager, ToolbarQt vs NavigationToolbar2QT?

I'm trying to embed the "home" function (i.e., "reset original review") to a widget button combined with other operations. Below is an example code I have.
import matplotlib.pyplot as plt
import matplotlib
from matplotlib.widgets import Button
from random import randrange
matplotlib.use('QtAgg')
plt.rcParams['toolbar'] = 'toolmanager' # switch this row to see the difference.
fig, ax = plt.subplots(nrows=1, figsize=(4,6))
ax.imshow([[1, 2], [5, 6]])
def change_color(event):
ax.imshow([[randrange(10), randrange(10)], [randrange(10), randrange(10)]])
plt.get_current_fig_manager().toolbar.home()
axprev0 = plt.axes([0.35, 0.05, 0.3, 0.0375])
bprev0 = Button(axprev0, 'Change Color')
bprev0.on_clicked(change_color)
plt.show()
My problem is that when plt.rcParams['toolbar'] = 'toolmanager' is on, it shows error message toolbar.home() won't work. The reason I need plt.rcParams['toolbar'] = 'toolmanager' is because it allows me to scroll the mouse wheel for zooming when I'm on the zoom or drag tool.
It seems that when we specify plt.rcParams['toolbar'] = 'toolmanager', it is using ToolbarQt. The ToolbarQt supports mouse wheel scrolling for zooming. While the name of the home function of ToolbarQt is different.
If we do not specify plt.rcParams['toolbar'] = 'toolmanager', plt is using NavigationToolbar2QT. However, it does not support mouse wheel scrolling for zooming.
plt.get_current_fig_manager().toolmanager.tools['home'].trigger(fig, event) # use this function when toolmanager

I couldn't find the way to use "valinit" in the rangeslider widget of matpoltlib

I'm trying to use Matplotlib RangeSlider and everything is ok except "valinit" value. I just couldn't find how to use it.
valinit : tuple of float or None, default: None
The initial positions of the slider. If None the initial positions will be at the 25th and 75th percentiles of the range.
https://matplotlib.org/stable/api/widgets_api.html#matplotlib.widgets.RangeSlider
I want to make initial position of the slider as 0%-100%, in my case (0, totaltimest) not the default 25%-75%, but I couldn't figure it out. Can somebody help me please? An example would be great.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import RangeSlider
import soundfile as sf
file='A.wav' # mono audio file
data,fs=sf.read(file)
totaltimest=len(data)/fs
x = np.linspace(0, totaltimest, len(data))
fig, ax= plt.subplots(figsize=(10, 5))
ax.plot(x,data, label='Reference', color='red',alpha=0.5,linewidth=0.6)
ax.set_facecolor('#EFEFEF')
(yl1,yh1)=ax.get_ylim()
scroll1 = plt.axes([0.22, 0.01, 0.60, 0.03], facecolor='#EFEFEF')
scroll1v = RangeSlider(scroll1, 'Window', 0, totaltimest, valinit=None) # PROBLEM
def update(val):
pos1=scroll1v.val[0]
pos2=scroll1v.val[1]
ax.axis([pos1, pos2,yl1,yh1])
fig.canvas.draw_idle()
scroll1v.on_changed(update)
plt.tight_layout
plt.show()
Figured someone else might find this useful so better late than never, but give this a shot:
scroll1v = RangeSlider(scroll1, 'Window', valinit=(0,totaltimest))
Here's an example of how I used it in conjunction with Numpy arrays for my own project. I was previously working with two separate sliders, but recognizing that the Slider widget works differently than a RangeSlider (float vs. tuple), we have to format the argument for valinit as a tuple as well. Check it out:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import RangeSlider
# Creating axis and slider:
ax_rgb = plt.axes([0.23, 0.18, 0.10, 0.70]) # X-cord, Y-cord, Width, Height
sl_rgb = RangeSlider(ax_rgb,
label = 'RGB Array',
valinit = (np.max(rgb),np.min(rgb)),
valmax = np.max(rgb),
valmin = np.min(rgb),
orientation = 'vertical')
Depending on how comfortable you are with Matplotlib widgets, might not be a bad idea to indent and label your Slider attributes. Cheers!

Can python make Matlab style GUIs [closed]

It's difficult to tell what is being asked here. This question is ambiguous, vague, incomplete, overly broad, or rhetorical and cannot be reasonably answered in its current form. For help clarifying this question so that it can be reopened, visit the help center.
Closed 9 years ago.
I'm really trying to fall in love with Python and move away from Matlab. Is it possible to make Matlab style GUIs in Python? How easy is it by comparison? (I make matlab GUIs programmatically, wouldn't dare use GUIDE) Can I put matplotlib graphics in these GUIs? Is tk or wx (or something else) better for this?
wxPython has tabs, grids or ListCtrls (i.e. tables) and supports matplotlib and PyPlot for graphs. You can read about using matplotlib at the following links:
http://eli.thegreenplace.net/2008/08/01/matplotlib-with-wxpython-guis/
Embedding a matplotlib figure inside a WxPython panel
http://www.scipy.org/Cookbook/Matplotlib/EmbeddingInWx
http://wiki.wxpython.org/MatplotlibFourierDemo
To see all the widgets that are included with wxPython, go to www.wxpython.org and click the download link on the left. You'll find that they have a standalone Docs & Demo package that can show you almost every widget and how it works.
Haven't used Matlab before, not sure about it's GUI. But if you tend to use Python interactively, you may want to give iPython a try. Ipython with Qt can render you an elegant GUI.
For simple interfaces you might want to check out the GUI neutral widgets that matplotlib provides, documented here -- http://matplotlib.org/api/widgets_api.html
Here's a simple example of using these GUI neutral widgets to draw a function with variable parameters that are controlled by three slider widgets:
import functools
import numpy as np
import pylab
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button, RadioButtons, MultiCursor
def hillfxn(x, B, K, n):
xn = float(x**n)
return (B * xn)/(K**n + xn)
def draw_function(x, y, xlabel="Activator concentration (X)",
ylabel="Promoter activity"):
fig, ax = plt.subplots(1, 1, sharex=True)
plt.subplots_adjust(left=0.15, bottom=0.25)
lines = ax.plot(x, y)
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
ax.set_ylim(0,max(y)*1.1)
return fig, ax, lines
def draw_interactive_controls(n, B, K):
axcolor = 'lightgoldenrodyellow'
axK = plt.axes([0.1, 0.01, 0.75, 0.03], axisbg=axcolor)
axB = plt.axes([0.1, 0.06, 0.75, 0.03], axisbg=axcolor)
axN = plt.axes([0.1, 0.11, 0.75, 0.03], axisbg=axcolor)
Nslider = Slider(axN, "$n$", 1, 10, valinit=n, valfmt='%1.3f')
Bslider = Slider(axB, "$\\beta$", 0, 20, valinit=B, valfmt='%1.3f')
Kslider = Slider(axK, "$K$", 0.01, 20, valinit=K, valfmt='%1.3f')
return Nslider, Bslider, Kslider
def update_plot(val, x=None, lines=None, ax=None,
Nslider=None, Bslider=None, Kslider=None):
n = Nslider.val
B = Bslider.val
K = Kslider.val
y = [hillfxn(i, B, K, n) for i in x]
lines[0].set_ydata(y)
ax.set_ylim(0,max(y)*1.1)
pylab.draw()
if __name__ == "__main__":
# initial values
B, K, n = 5, 5, 1
x= np.linspace(0,30,250)
y = [hillfxn(i, B, K, n) for i in x]
# setup initial graph and control settings
fig, ax, lines = draw_function(x,y)
Nslider, Bslider, Kslider = draw_interactive_controls(n, B, K)
# specify updating function for interactive controls
updatefxn = functools.partial(update_plot, x=x, lines=lines, ax=ax,
Nslider=Nslider, Bslider=Bslider, Kslider=Kslider)
Nslider.on_changed(updatefxn)
Bslider.on_changed(updatefxn)
Kslider.on_changed(updatefxn)
pylab.show()
This will produce an interface like the following:

Categories