Draggable Matplotlib Subplot using wxPython - python

I'm attempting to expand on a draggable plot tutorial by creating a subplot that can be dragged (the matplotlib curve, not the whole window). I feel like I'm close but just missing a critical detail.
Most of the code is just creating cookie cutter subplots, figure 3 is the only one where I'm trying to drag the plot data.
Any help would be appreciated!
import wxversion
wxversion.ensureMinimal('2.8')
import numpy as np
import matplotlib
matplotlib.interactive(True)
matplotlib.use('WXAgg')
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.figure import Figure
import wx
class DraggableCurve:
def __init__(self,curve):
self.curve = curve[0]
self.press = None
def connect(self):
'connect to all the events we need'
self.cidpress = self.curve.figure.canvas.mpl_connect(
'button_press_event', self.on_press)
self.cidrelease = self.curve.figure.canvas.mpl_connect(
'button_release_event', self.on_release)
self.cidmotion = self.curve.figure.canvas.mpl_connect(
'motion_notify_event', self.on_motion)
def on_press(self, event):
print "on_press"
'on button press we will see if the mouse is over us and store some data'
if event.inaxes != self.curve.axes: return
contains, attrd = self.curve.contains(event)
if not contains: return
print 'event contains', self.curve.xy
x0, y0 = self.curve.xy
# print x0,y0
self.press = x0, y0, event.xdata, event.ydata
def on_motion(self, event):
print "on_motion"
'on motion we will move the curve if the mouse is over us'
if self.press is None: return
if event.inaxes != self.curve.axes: return
x0, y0, xpress, ypress = self.press
print xpress, ypress
dx = event.xdata - xpress
dy = event.ydata - ypress
#print 'x0=%f, xpress=%f, event.xdata=%f, dx=%f, x0+dx=%f'%(x0, xpress, event.xdata, dx, x0+dx)
self.curve.set_x(x0+dx)
self.curve.set_y(y0+dy)
# print x0+dx, y0+dy
#self.curve.figure.canvas.draw()
self.curve.figure.canvas.draw_idle()
def on_release(self, event):
print "on_release"
'on release we reset the press data'
self.press = None
#self.curve.figure.canvas.draw()
self.curve.figure.canvas.draw_idle()
def disconnect(self):
'disconnect all the stored connection ids'
self.curve.figure.canvas.mpl_disconnect(self.cidpress)
self.curve.figure.canvas.mpl_disconnect(self.cidrelease)
self.curve.figure.canvas.mpl_disconnect(self.cidmotion)
class CanvasFrame(wx.Frame):
def __init__(self):
#create frame
frame = wx.Frame.__init__(self,None,-1,
'Test',size=(550,350))
#set background
self.SetBackgroundColour(wx.NamedColour("WHITE"))
#initialize figures
self.figure1 = Figure()
self.figure2 = Figure()
self.figure3 = Figure()
self.figure4 = Figure()
#initialize figure1
self.axes1 = self.figure1.add_subplot(111)
self.axes1.text(0.5,0.5, 'Test 1', horizontalalignment='center', fontsize=15)
self.axes1.get_xaxis().set_visible(False)
self.axes1.get_yaxis().set_visible(False)
self.canvas1 = FigureCanvas(self, -1, self.figure1)
#initialize figure2
self.axes2 = self.figure2.add_subplot(111)
self.axes2.text(0.5,0.5, 'Test 2', horizontalalignment='center', fontsize=15)
self.axes2.get_xaxis().set_visible(False)
self.axes2.get_yaxis().set_visible(False)
self.canvas2 = FigureCanvas(self, -1, self.figure2)
#initialize figure3
self.axes3 = self.figure3.add_subplot(111)
curve = self.axes3.plot(np.arange(1,11),10*np.random.rand(10),color='r',marker='o')
self.canvas3 = FigureCanvas(self, -1, self.figure3)
# self.axes3.get_xaxis().set_visible(True)
# self.axes3.get_yaxis().set_visible(True)
# self.canvas3.draw()
# self.canvas3.draw_idle()
dc = DraggableCurve(curve)
dc.connect()
#initialize figure4
self.axes4 = self.figure4.add_subplot(111)
self.axes4.text(0.5,0.5, 'Test4', horizontalalignment='center', fontsize=15)
self.axes4.get_xaxis().set_visible(False)
self.axes4.get_yaxis().set_visible(False)
self.canvas4 = FigureCanvas(self, -1, self.figure4)
#create figures into the 2x2 grid
self.sizer = wx.GridSizer(rows=2, cols=2, hgap=5, vgap=5)
self.sizer.Add(self.canvas1, 1, wx.EXPAND)
self.sizer.Add(self.canvas2, 1, wx.EXPAND)
self.sizer.Add(self.canvas3, 1, wx.EXPAND)
self.sizer.Add(self.canvas4, 1, wx.EXPAND)
self.SetSizer(self.sizer)
self.Fit()
return
class App(wx.App):
def OnInit(self):
'Create the main window and insert the custom frame'
frame = CanvasFrame()
frame.Show(True)
return True
app = App(0)
app.MainLoop()

Check this example:
# -*- coding: utf-8 -*-
import wxversion
wxversion.ensureMinimal('2.8')
import wx
import numpy as np
import matplotlib
matplotlib.use('WXAgg')
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg
from matplotlib.figure import Figure
class FigureCanvas(FigureCanvasWxAgg):
def __init__(self,parent,id,figure,**kwargs):
FigureCanvasWxAgg.__init__(self,parent=parent, id=id, figure=figure,**kwargs)
self.figure = figure
self.axes = self.figure.get_axes()[0] # Get axes
self.connect() # Connect event
def connect(self):
"""Connect pick event"""
self.MOVE_LINE_EVT = self.mpl_connect("pick_event", self.on_pick)
def on_pick(self,event):
self._selected_line = event.artist # Get selected line
# Get initial x,y data
self._p0 = (event.mouseevent.xdata, event.mouseevent.ydata)
self._xdata0 = self._selected_line.get_xdata()
self._ydata0 = self._selected_line.get_ydata()
# Connect events for motion and release.
self._on_motion = self.mpl_connect("motion_notify_event", self.on_motion)
self._on_release = self.mpl_connect("button_release_event", self.on_release)
def on_motion(self,event):
cx = event.xdata # Current xdata
cy = event.ydata # Current ydata
deltax = cx - self._p0[0]
deltay = cy - self._p0[1]
self._selected_line.set_xdata(self._xdata0 + deltax)
self._selected_line.set_ydata(self._ydata0 + deltay)
self.draw()
def on_release(self,event):
"""On release, disconnect motion and release"""
self.mpl_disconnect(self._on_motion)
self.mpl_disconnect(self._on_release)
self.axes.relim()
self.axes.autoscale_view(True,True,True)
self.draw()
class Frame(wx.Frame):
def __init__(self,parent,title):
wx.Frame.__init__(self,parent,title=title,size=(800,600))
self.initCtrls()
self.plotting()
self.Centre(True)
self.Show()
def initCtrls(self):
self.mainsizer = wx.GridSizer(rows=2, cols=2, hgap=2, vgap=2)
# 1
self.figure = Figure()
self.axes = self.figure.add_subplot(111)
self.canvas = FigureCanvas(self, wx.ID_ANY, self.figure)
# 2
self.figure2 = Figure()
self.axes2 = self.figure2.add_subplot(111)
self.canvas2 = FigureCanvas(self, wx.ID_ANY, self.figure2)
self.figure3 = Figure()
self.axes3 = self.figure3.add_subplot(111)
self.canvas3 = FigureCanvas(self, wx.ID_ANY, self.figure3)
self.figure4 = Figure()
self.axes4 = self.figure4.add_subplot(111)
self.canvas4 = FigureCanvas(self, wx.ID_ANY, self.figure4)
self.mainsizer.Add(self.canvas, 1, wx.EXPAND)
self.mainsizer.Add(self.canvas2, 1, wx.EXPAND)
self.mainsizer.Add(self.canvas3, 1, wx.EXPAND)
self.mainsizer.Add(self.canvas4, 1, wx.EXPAND)
self.SetSizer(self.mainsizer)
def plotting(self):
# Set picker property -> true
self.axes2.plot(np.arange(1,11),10*np.random.rand(10),color='b',
marker='o', picker=True)
self.axes3.plot(np.arange(1,11),10*np.random.rand(10),color='r',
marker='o', picker=True)
self.canvas.draw()
if __name__=='__main__':
app = wx.App()
frame = Frame(None, "Matplotlib Demo")
frame.Show()
app.MainLoop()
Basically the idea is to define a custom FigureCanvas, which supports the selection and movement of the lines using the pick event.
Obviously this code still needs a lot of review, to work properly.

Related

Fitting matplotlib figure to wxpnel

I am trying to display a plot in a wxpanel so that when initialized it has 5 pixels gap from left top and right edges.
I tried the code below but it looks like when the plot is created it sticks right to the right edge of the panel. After resizing the gap appear.
# -*- coding: utf-8 -*-
import wx
#import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from relpermtable import RelPermTable as RelPermCl
class KrPanel(wx.Panel):
def __init__(self, parent):
super().__init__(parent)
main_sizer = wx.BoxSizer(wx.VERTICAL)
self.list_ctrl = wx.ListCtrl(
self, size = (-1, 300),
style = wx.LC_REPORT | wx.BORDER_SUNKEN
)
self.list_ctrl.InsertColumn(0, 'Sw', width=100)
self.list_ctrl.InsertColumn(1, 'Krw', width=100)
self.list_ctrl.InsertColumn(2, 'Krg', width=100)
self.list_ctrl.InsertColumn(3, 'Pc', width=100)
main_sizer.Add(self.list_ctrl, 0, wx.ALL | wx.EXPAND, 5)
self.SetSizer(main_sizer)
def load_KrPc_data(self, in_dfr):
for index, row in in_dfr.iterrows():
self.list_ctrl.InsertItem(index, str(row["Sg"]))
self.list_ctrl.SetItem(index, 1, str(row["Krg"]))
self.list_ctrl.SetItem(index, 2, str(row["Krw"]))
self.list_ctrl.SetItem(index, 3, str(row["Pc"]))
class PlotPanel(wx.Panel):
def __init__(self, parent):
super().__init__(parent)
def PlotKrPcData(self, df_in, in_swco, in_sgco):
right_sizer = wx.BoxSizer(wx.VERTICAL)
self.fig, self.ax1 = plt.subplots() # figsize=(5,5)
# self.ax2 = self.ax1.twinx()
self.ax1.plot(df_in['Sg'], df_in['Krg'], 'r-')
self.ax1.plot(df_in['Sg'], df_in['Krw'], 'g-')
# self.ax2.plot(new_df['Sg'], new_df['Pc'], 'b-')
self.ax1.plot((1-in_swco), 0, 'go')
self.ax1.annotate('1-SWL', ((1-in_swco), 0.05))
self.ax1.plot((in_sgco), 0, 'go', label = 'SGL')
self.ax1.annotate('SGL', (in_sgco, 0.05))
self.canvas = FigureCanvas(self, -1, self.fig)
right_sizer.Add(self.canvas, 0, wx.ALL | wx.EXPAND, 5)
self.SetSizer(right_sizer)
class KrFrame(wx.Frame):
def __init__(self):
super().__init__(parent=None,
title='Gas Relative Permeability Editor', size=(900, 800))
self.sp = wx.SplitterWindow(self)
self.panel01 = KrPanel(self.sp)
self.panel02 = PlotPanel(self.sp)
self.sp.SplitVertically(self.panel01, self.panel02, 450)
self.create_menu()
self.Show()
def create_menu(self):
menu_bar = wx.MenuBar()
file_menu = wx.Menu()
open_file_menu_item = file_menu.Append(
wx.ID_ANY, 'Open File', 'Open a Relative Permeability File'
)
exit_menu_item = file_menu.Append(
wx.ID_EXIT, "Exit", "Exit the application"
)
menu_bar.Append(file_menu, '&File')
self.Bind(
event = wx.EVT_MENU,
handler = self.on_open_file,
source = open_file_menu_item
)
self.Bind(
event = wx.EVT_MENU,
handler = self.on_exit,
source = exit_menu_item
)
self.SetMenuBar(menu_bar)
def on_open_file(self, event):
title = "Choose a Relative Permeability file:"
dlg = wx.FileDialog(self, title, "", "",
"Eclipse include files (*.INC) | *.INC", style = wx.FD_DEFAULT_STYLE)
if dlg.ShowModal() == wx.ID_OK:
object = RelPermCl(dlg.GetPath())
new_df = object.load_KrPc_data()
new_sgco = object.calc_sgco()
new_swco = object.calc_swco()
# new_sgmax = object.calc_sgmax()
# new_swmax = object.calc_swmax()
# new_sgcr = object.calc_sgcr()
# new_swcr = object.calc_swcr()
self.panel01.load_KrPc_data(new_df)
self.panel02.PlotKrPcData(new_df, new_sgco, new_swco)
# print(new_df.head())
dlg.Destroy()
def on_exit(self, e):
self.Close()
if __name__ == '__main__':
app = wx.App(False)
frame = KrFrame()
app.MainLoop()
del app
I'd like to have 5 pixels gap on the right of the plot when the plot is created. But somehow the gap is created only after resize. Is there a way to set plot size let's say to the size of the panel independently of panel size and specify border sizes?
Try to put a frame.SendSizeEvent() before the Show call. If it doesn’t work, try with wx.CallAfter(frame.SendSizeEvent)

Multi lines realtime plotting

I would like to plot multi-lines in Python in a real-time manner though I'm just too new to this language.
I have found some codes as example to work fine but can only get one line plotted. Would someone help me with multi-lines please? Also, I need to adjust the line width, color, etc. of the lines.
The code is as follows:
# -*- coding: utf-8 -*-
"""
This demo demonstrates how to draw a dynamic mpl (matplotlib)
plot in a wxPython application.
It allows "live" plotting as well as manual zooming to specific
regions.
Both X and Y axes allow "auto" or "manual" settings. For Y, auto
mode sets the scaling of the graph to see all the data points.
For X, auto mode makes the graph "follow" the data. Set it X min
to manual 0 to always see the whole data from the beginning.
Note: press Enter in the 'manual' text box to make a new value
affect the plot.
Eli Bendersky (eliben#gmail.com)
License: this code is in the public domain
Last modified: 31.07.2008
"""
import os
import pprint
import random
import sys
import wx
# The recommended way to use wx with mpl is with the WXAgg
# backend.
#
import matplotlib
matplotlib.use('WXAgg')
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import \
FigureCanvasWxAgg as FigCanvas, \
NavigationToolbar2WxAgg as NavigationToolbar
import numpy as np
import pylab
class DataGen(object):
""" A silly class that generates pseudo-random data for
display in the plot.
"""
def __init__(self, init=50):
self.data = self.init = init
def next(self):
self._recalc_data()
return self.data
def _recalc_data(self):
delta = random.uniform(-0.5, 0.5)
r = random.random()
if r > 0.9:
self.data += delta * 15
elif r > 0.8:
# attraction to the initial value
delta += (0.5 if self.init > self.data else -0.5)
self.data += delta
else:
self.data += delta
class BoundControlBox(wx.Panel):
""" A static box with a couple of radio buttons and a text
box. Allows to switch between an automatic mode and a
manual mode with an associated value.
"""
def __init__(self, parent, ID, label, initval):
wx.Panel.__init__(self, parent, ID)
self.value = initval
box = wx.StaticBox(self, -1, label)
sizer = wx.StaticBoxSizer(box, wx.VERTICAL)
self.radio_auto = wx.RadioButton(self, -1,
label="Auto", style=wx.RB_GROUP)
self.radio_manual = wx.RadioButton(self, -1,
label="Manual")
self.manual_text = wx.TextCtrl(self, -1,
size=(35,-1),
value=str(initval),
style=wx.TE_PROCESS_ENTER)
self.Bind(wx.EVT_UPDATE_UI, self.on_update_manual_text, self.manual_text)
self.Bind(wx.EVT_TEXT_ENTER, self.on_text_enter, self.manual_text)
manual_box = wx.BoxSizer(wx.HORIZONTAL)
manual_box.Add(self.radio_manual, flag=wx.ALIGN_CENTER_VERTICAL)
manual_box.Add(self.manual_text, flag=wx.ALIGN_CENTER_VERTICAL)
sizer.Add(self.radio_auto, 0, wx.ALL, 10)
sizer.Add(manual_box, 0, wx.ALL, 10)
self.SetSizer(sizer)
sizer.Fit(self)
def on_update_manual_text(self, event):
self.manual_text.Enable(self.radio_manual.GetValue())
def on_text_enter(self, event):
self.value = self.manual_text.GetValue()
def is_auto(self):
return self.radio_auto.GetValue()
def manual_value(self):
return self.value
class GraphFrame(wx.Frame):
""" The main frame of the application
"""
title = 'Demo: dynamic matplotlib graph'
#修改下面self.redraw_timer.Start(100)数值影响plot下一个数据点的速度,越大速度越慢
def __init__(self):
wx.Frame.__init__(self, None, -1, self.title)
self.datagen = DataGen()
self.data = [self.datagen.next()]
self.paused = False
self.create_menu()
self.create_status_bar()
self.create_main_panel()
self.redraw_timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.on_redraw_timer, self.redraw_timer)
self.redraw_timer.Start(100)
def create_menu(self):
self.menubar = wx.MenuBar()
menu_file = wx.Menu()
m_expt = menu_file.Append(-1, "&Save plot\tCtrl-S", "Save plot to file")
self.Bind(wx.EVT_MENU, self.on_save_plot, m_expt)
menu_file.AppendSeparator()
m_exit = menu_file.Append(-1, "E&xit\tCtrl-X", "Exit")
self.Bind(wx.EVT_MENU, self.on_exit, m_exit)
self.menubar.Append(menu_file, "&File")
self.SetMenuBar(self.menubar)
def create_main_panel(self):
self.panel = wx.Panel(self)
self.init_plot()
self.canvas = FigCanvas(self.panel, -1, self.fig)
self.xmin_control = BoundControlBox(self.panel, -1, "X min", 0)
self.xmax_control = BoundControlBox(self.panel, -1, "X max", 50)
self.ymin_control = BoundControlBox(self.panel, -1, "Y min", 0)
self.ymax_control = BoundControlBox(self.panel, -1, "Y max", 100)
self.pause_button = wx.Button(self.panel, -1, "Pause")
self.Bind(wx.EVT_BUTTON, self.on_pause_button, self.pause_button)
self.Bind(wx.EVT_UPDATE_UI, self.on_update_pause_button, self.pause_button)
self.cb_grid = wx.CheckBox(self.panel, -1,
"Show Grid",
style=wx.ALIGN_RIGHT)
self.Bind(wx.EVT_CHECKBOX, self.on_cb_grid, self.cb_grid)
self.cb_grid.SetValue(True)
self.cb_xlab = wx.CheckBox(self.panel, -1,
"Show X labels",
style=wx.ALIGN_RIGHT)
self.Bind(wx.EVT_CHECKBOX, self.on_cb_xlab, self.cb_xlab)
self.cb_xlab.SetValue(True)
self.hbox1 = wx.BoxSizer(wx.HORIZONTAL)
self.hbox1.Add(self.pause_button, border=5, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL)
self.hbox1.AddSpacer(20)
self.hbox1.Add(self.cb_grid, border=5, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL)
self.hbox1.AddSpacer(10)
self.hbox1.Add(self.cb_xlab, border=5, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL)
self.hbox2 = wx.BoxSizer(wx.HORIZONTAL)
self.hbox2.Add(self.xmin_control, border=5, flag=wx.ALL)
self.hbox2.Add(self.xmax_control, border=5, flag=wx.ALL)
self.hbox2.AddSpacer(24)
self.hbox2.Add(self.ymin_control, border=5, flag=wx.ALL)
self.hbox2.Add(self.ymax_control, border=5, flag=wx.ALL)
self.vbox = wx.BoxSizer(wx.VERTICAL)
self.vbox.Add(self.canvas, 1, flag=wx.LEFT | wx.TOP | wx.GROW)
self.vbox.Add(self.hbox1, 0, flag=wx.ALIGN_LEFT | wx.TOP)
self.vbox.Add(self.hbox2, 0, flag=wx.ALIGN_LEFT | wx.TOP)
self.panel.SetSizer(self.vbox)
self.vbox.Fit(self)
def create_status_bar(self):
self.statusbar = self.CreateStatusBar()
def init_plot(self):
self.dpi = 100
self.fig = Figure((3.0, 3.0), dpi=self.dpi)
self.axes = self.fig.add_subplot(111)
self.axes.set_axis_bgcolor('gainsboro')
self.axes.set_title('Very important random data', size=12)
pylab.setp(self.axes.get_xticklabels(), fontsize=8)
pylab.setp(self.axes.get_yticklabels(), fontsize=8)
# plot the data as a line series, and save the reference
# to the plotted line series
#
self.plot_data = self.axes.plot(
self.data,
linewidth=1,
color=(1, 1, 0),
)[0]
# 修改下面1000可以修改图标put出来的数据量
def draw_plot(self):
""" Redraws the plot
"""
# when xmin is on auto, it "follows" xmax to produce a
# sliding window effect. therefore, xmin is assigned after
# xmax.
#
if self.xmax_control.is_auto():
xmax = len(self.data) if len(self.data) > 1000 else 1000
else:
xmax = int(self.xmax_control.manual_value())
if self.xmin_control.is_auto():
xmin = xmax - 1000
else:
xmin = int(self.xmin_control.manual_value())
# for ymin and ymax, find the minimal and maximal values
# in the data set and add a mininal margin.
#
# note that it's easy to change this scheme to the
# minimal/maximal value in the current display, and not
# the whole data set.
#
if self.ymin_control.is_auto():
ymin = round(min(self.data), 0) - 1
else:
ymin = int(self.ymin_control.manual_value())
if self.ymax_control.is_auto():
ymax = round(max(self.data), 0) + 1
else:
ymax = int(self.ymax_control.manual_value())
self.axes.set_xbound(lower=xmin, upper=xmax)
self.axes.set_ybound(lower=ymin, upper=ymax)
# anecdote: axes.grid assumes b=True if any other flag is
# given even if b is set to False.
# so just passing the flag into the first statement won't
# work.
#
if self.cb_grid.IsChecked():
self.axes.grid(True, color='gray')
else:
self.axes.grid(False)
# Using setp here is convenient, because get_xticklabels
# returns a list over which one needs to explicitly
# iterate, and setp already handles this.
#
pylab.setp(self.axes.get_xticklabels(),
visible=self.cb_xlab.IsChecked())
self.plot_data.set_xdata(np.arange(len(self.data)))
self.plot_data.set_ydata(np.array(self.data))
self.canvas.draw()
def on_pause_button(self, event):
self.paused = not self.paused
def on_update_pause_button(self, event):
label = "Resume" if self.paused else "Pause"
self.pause_button.SetLabel(label)
def on_cb_grid(self, event):
self.draw_plot()
def on_cb_xlab(self, event):
self.draw_plot()
def on_save_plot(self, event):
file_choices = "PNG (*.png)|*.png"
dlg = wx.FileDialog(
self,
message="Save plot as...",
defaultDir=os.getcwd(),
defaultFile="plot.png",
wildcard=file_choices,
style=wx.SAVE)
if dlg.ShowModal() == wx.ID_OK:
path = dlg.GetPath()
self.canvas.print_figure(path, dpi=self.dpi)
self.flash_status_message("Saved to %s" % path)
def on_redraw_timer(self, event):
# if paused do not add data, but still redraw the plot
# (to respond to scale modifications, grid change, etc.)
#
if not self.paused:
self.data.append(self.datagen.next())
self.draw_plot()
def on_exit(self, event):
self.Destroy()
def flash_status_message(self, msg, flash_len_ms=1500):
self.statusbar.SetStatusText(msg)
self.timeroff = wx.Timer(self)
self.Bind(
wx.EVT_TIMER,
self.on_flash_status_off,
self.timeroff)
self.timeroff.Start(flash_len_ms, oneShot=True)
def on_flash_status_off(self, event):
self.statusbar.SetStatusText('')
if __name__ == '__main__':
app = wx.PySimpleApp()
app.frame = GraphFrame()
app.frame.Show()
app.MainLoop()
You may find wxmplot (http://newville.github.io/wxmplot/) useful for this. It exposes and simplifies many aspects of using matplotlib with wxPython. It supports setting line attributes either programmatically or from a GUI panel for each plot. The demo includes a doing relatively fast "updating" for real-time plotting (10 fps or better, usually depending on hardware).

Matplotlib: Relating Canvas Click Event to Closest Axes

is there any way to relate a click on the Canvas but outside of a plot axes to the closest axes of the click? I have a canvas with x number of subplots and I'm trying to find out the closest subplot near the mouse click. Ultimately, this would help me create a zoom-in-rectangle that allows the user to zoom in the area of selected subplots (the navigation toolbar only zoom in one subplot).
#import os
#os.environ['QT_API'] = 'pyside'
from PyQt4 import QtGui, QtCore
import sys
import matplotlib
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
import numpy as np
class Canvas(FigureCanvasQTAgg):
def __init__(self, parent=None):
self.figure = Figure()
super(Canvas, self).__init__(self.figure)
self.ax1 = self.figure.add_subplot(1,1,1)
self.figure.subplots_adjust(left = 0.05, bottom = 0.02, right = 0.98, top = 0.99)
self.setMinimumWidth(1000)
self.ax1.plot([1,2,3])
self.draw()
def add_subplot(self, data=[]):
rows = len(self.figure.axes) + 1
for index, axes in enumerate(self.figure.axes, start=1):
axes.change_geometry(rows, 1, index)
ax = self.figure.add_subplot(rows, 1, index+1)
x = np.arange(1000,1)
y = np.arange(1000,1)
ax.step(x,y)
ax.patch.set_facecolor('None')
self.figure.set_figheight(self.figure.get_figheight()*self.figScalingfactor)
def figScaling(self, numSubplot):
self.figScalingfactor = round(1.1729*pow(numSubplot, -0.028),3)
class Window(QtGui.QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.showMaximized()
self.widget = QtGui.QWidget()
self.setCentralWidget(self.widget)
self.widget.setLayout(QtGui.QVBoxLayout())
self.widget.layout().setContentsMargins(0,0,0,0)
self.widget.layout().setSpacing(5)
self.canvas = Canvas(self)
self.scroll = QtGui.QScrollArea(self.widget)
self.scroll.setWidget(self.canvas)
self.scroll.setWidgetResizable(False)
self.nav = NavigationToolbar(self.canvas, self.widget)
self.numSubplots = 30
self.canvas.figScaling(self.numSubplots)
for x in range(self.numSubplots):
self.canvas.add_subplot()
self.canvas.adjustSize()
self.canvas.draw_idle()
self.widget.layout().addWidget(self.nav)
self.widget.layout().addWidget(self.scroll)
self.showVline = False
self.hoveringLine = None
self.canvas.mpl_connect("scroll_event", self.scrolling)
self.canvas.mpl_connect("button_press_event", self.onClick)
self.canvas.mpl_connect("motion_notify_event", self.onMove)
def scrolling(self, event):
val = self.scroll.verticalScrollBar().value()
if event.button =="down":
self.scroll.verticalScrollBar().setValue(val+100)
else:
self.scroll.verticalScrollBar().setValue(val-100)
def onClick(self, event):
if event.dblclick and self.showVline == False:
self.background = self.canvas.copy_from_bbox(self.canvas.figure.bbox)
self.showVline = True
self.hoveringLine = self.canvas.ax1.axvline(x=event.xdata, ymin=-1.2*self.numSubplots, ymax=1.2,
lw=2, zorder=0, clip_on=False)
elif event.dblclick and self.showVline:
self.showVline = False
self.hoveringLine = None
self.canvas.ax1.axvline(x=event.xdata, ymin=-1.2*self.numSubplots, ymax=1.2,
lw=2, zorder=0, clip_on=False)
self.canvas.draw()
else:
print(event.xdata)
print(event.ydata)
def onMove(self, event):
if (self.showVline):
self.canvas.restore_region(self.background)
self.hoveringLine.set_xdata(event.xdata)
self.canvas.ax1.draw_artist(self.hoveringLine)
self.canvas.blit(self.canvas.figure.bbox)
def main():
app = QtGui.QApplication(sys.argv)
app.aboutToQuit.connect(app.deleteLater)
GUI = Window()
GUI.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

Making a plot in a second window using data from main window

I'm trying to make a program in which I have a main window and a second window. The second window should be opened by checking a Check-Box in the main window and closed by unchecking it.
The following minimal example works already fine (thanks to ImportanceOfBeingErnest !), but I want to spin the arrow (the one, which is already bent when you run the example) by changing the SpinBox in the main window.
Solution: See 5th comment in the first answer!
import sys
from PyQt4 import QtGui, QtCore
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib import animation
import numpy as np
class Newsphere(QtGui.QMainWindow):
def __init__(self):
super(Newsphere, self).__init__()
self.mainbox = QtGui.QWidget()
self.mainbox.setLayout(QtGui.QHBoxLayout())
self.setCentralWidget(self.mainbox)
self.spin = QtGui.QSpinBox()
self.spin.setValue(20)
self.spin.setMaximum(100)
self.spin.setMinimum(-100)
self.checkPlot = QtGui.QCheckBox("Check")
self.mainbox.layout().addWidget(self.spin)
self.mainbox.layout().addWidget(self.checkPlot)
self.Plot = None
self.checkPlot.clicked.connect(self.showPlot)
def showPlot(self):
if self.Plot == None:
self.Plot = Plot(self.kinematic())
self.Plot.show()
# register signal for closure
self.Plot.signalClose.connect(self.uncheck)
# register signal for spin value changed
self.spin.valueChanged.connect(self.kinematic)
else:
self.Plot.close()
self.Plot = None
def kinematic(self):
x = self.spin.value() / 100
v = np.matrix([[1.,x,0.],[0.,1.,0.],[0.,0.,1.]])
zero = np.matrix([[0.,0.,0.],[0.,0.,0.],[0.,0.,0.]])
pos = np.hstack([v, zero])
return pos
def uncheck(self):
self.checkPlot.setChecked(False)
self.Plot = None
class Plot(QtGui.QWidget):
signalClose = QtCore.pyqtSignal()
def __init__(self, pos=None):
super(Plot, self).__init__()
self.setLayout(QtGui.QHBoxLayout())
self.fig = plt.figure()
self.ax = self.fig.add_subplot(111,projection = '3d')
self.fig.tight_layout()
self.ax.view_init(40, 225)
''' dashed coordinate system '''
self.ax.plot([0,1], [0,0], [0,0], label='$X_0$', linestyle="dashed", color="red")
self.ax.plot([0,0], [0,-10], [0,0], label='$Y_0$', linestyle="dashed", color="green")
self.ax.plot([0,0], [0,0], [0,1], label='$Z_0$', linestyle="dashed", color="blue")
self.ax.set_xlim3d(-3,3)
self.ax.set_ylim3d(-3,3)
self.ax.set_zlim3d(-3,3)
self.canvas = FigureCanvas(self.fig)
self.layout().addWidget(self.canvas)
self.pos = pos
self.setup_plot()
self.ani = animation.FuncAnimation(self.fig, self.update_plot, init_func=self.setup_plot, blit=True)
def setup_plot(self):
self.ax.legend(loc='best')
self.position = self.ax.quiver(0, 0, 0, 0, 0, 0, pivot="tail", color="black")
return self.position,
def update_plot(self, i):
x_zero = self.pos[:,3]
y_zero = self.pos[:,4]
z_zero = self.pos[:,5]
v_x = self.pos[0,0:3]
v_y = self.pos[1,0:3]
v_z = self.pos[2,0:3]
self.position = self.ax.quiver(-x_zero, -y_zero, z_zero, -v_x[0,:], v_y[0,:], v_z[0,:], pivot="tail", color="black")
self.canvas.draw()
return self.position,
# We need to make sure the animation stops, when the window is closed
def closeEvent(self, event):
self.signalClose.emit()
self.close()
super(Plot, self).closeEvent(event)
def close(self):
self.ani.event_source.stop()
super(Plot, self).close()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
main = Newsphere()
main.show()
sys.exit(app.exec_())
Here is an working example of what I think you are trying to achieve.
The Main Window has a spin box and a check box. Once the checkbox is clicked, a new window with a plot will show up and an animation will start. The current value and some array will be given to the plot window. If you change the spin box value while the animation is running, it will be updated. When the plot window is closed or when the checkbox is unchecked, the animation will stop (and be deleted).
import sys
from PyQt4 import QtGui, QtCore
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib import animation
import numpy as np
class Newsphere(QtGui.QMainWindow):
def __init__(self):
super(Newsphere, self).__init__()
self.mainbox = QtGui.QWidget()
self.mainbox.setLayout(QtGui.QHBoxLayout())
self.setCentralWidget(self.mainbox)
self.spin = QtGui.QSpinBox()
self.spin.setValue(5)
self.spin.setMaximum(10)
self.spin.setMinimum(1)
self.checkPlot = QtGui.QCheckBox("Check")
self.mainbox.layout().addWidget(self.spin)
self.mainbox.layout().addWidget(self.checkPlot)
self.Plot = None
self.checkPlot.clicked.connect(self.showPlot)
def showPlot(self):
if self.Plot == None:
self.Plot = Plot(self.kinematic(), self.spin.value())
self.Plot.show()
# register signal for closure
self.Plot.signalClose.connect(self.uncheck)
# register signal for spin value changed
self.spin.valueChanged.connect(self.Plot.update_factor)
else:
self.Plot.close()
self.Plot = None
def kinematic(self):
v = np.array([[1.,2.,3.],[2.,1.,3.],[3.,2.,1.]])
return v
def uncheck(self):
self.checkPlot.setChecked(False)
self.Plot = None
class Plot(QtGui.QWidget):
signalClose = QtCore.pyqtSignal()
def __init__(self, v=None, factor=1):
super(Plot, self).__init__()
self.setLayout(QtGui.QHBoxLayout())
self.fig = plt.figure()
self.ax = self.fig.add_subplot(111,projection = '3d')
self.ax.set_aspect('equal')
self.fig.tight_layout()
self.ax.view_init(40, 225)
self.ax.set_xlim3d(0,3)
self.ax.set_ylim3d(0,3)
self.ax.set_zlim3d(0,4)
self.canvas = FigureCanvas(self.fig)
self.layout().addWidget(self.canvas)
self.pos = v
self.setup_plot()
self.update_factor(factor)
self.ani = animation.FuncAnimation(self.fig, self.update_plot, blit=False)
def setup_plot(self):
xpos, ypos = np.meshgrid(np.arange(self.pos.shape[0]),np.arange(self.pos.shape[1]) )
self.xpos = xpos.flatten('F')
self.ypos = ypos.flatten('F')
self.zpos = np.zeros_like(self.xpos)
self.bar = None
def update_factor(self, factor):
self.factor = factor
self.dx = np.ones_like(self.xpos)*np.min(np.abs(self.factor/10.), 0.1)
self.dy = self.dx.copy()
def update_plot(self, i):
if self.bar != None:
self.bar.remove()
del self.bar
pos = self.pos+np.sin(i/8.)
dz = pos.flatten()
self.bar = self.ax.bar3d(self.xpos, self.ypos, self.zpos, self.dx, self.dy, dz,
color=(1.-self.factor/10.,0,self.factor/10.), zsort='average', linewidth=0)
self.canvas.draw()
# We need to make sure the animation stops, when the window is closed
def closeEvent(self, event):
self.signalClose.emit()
self.close()
super(Plot, self).closeEvent(event)
def close(self):
self.ani.event_source.stop()
super(Plot, self).close()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
main = Newsphere()
main.show()
sys.exit(app.exec_())
Since I wasn't sure about what you want to animate, I changed the plot to a barplot, but you can change it back to whatever you need. Hope that helps.

Python/Matplotlib - Quickly Updating Text on Axes

I have a matplotlib figure/canvas in a wxpython window. I want to update some information on the plot as the mouse moves around. I've connected to 'motion_notify_event' to get this information.
In the code below, a lot of random data is plotted and then the x,y location of the cursor is displayed in the statusbar of the window. This is very smooth and works well. However, I really want to display this information at the top of the plot. The behavior I want is shown if you uncomment the last two lines of cbUpdateCursor. However, when this is done, the response time to moving the cursor is terribly slow (because draw gets called and there is a lot of data, but draw must be called or the text doesn't get updated).
How can I speed this up so the cursor position can be displayed on the plot, but not slow it down so much? I think I might need to do something with bbox?
Code:
import wx
import numpy as np
import matplotlib
matplotlib.use('WXAgg')
from matplotlib.figure import Figure
from matplotlib.widgets import Cursor
from matplotlib.backends.backend_wxagg import \
FigureCanvasWxAgg as FigCanvas, \
NavigationToolbar2WxAgg as NavigationToolbar
class wxPlotting(wx.Frame):
title = 'Test'
def __init__(self):
wx.Frame.__init__(self, None, -1, self.title)
self.time = np.arange(10000)
self.data = np.random.random(10000)
self.sb = self.CreateStatusBar()
self.create_main_panel()
self.axes.plot(self.time, self.data)
self.canvas.draw()
def create_main_panel(self):
self.panel = wx.Panel(self)
self.fig = Figure((5.0, 4.0), dpi=100)
self.canvas = FigCanvas(self.panel, -1, self.fig)
self.axes = self.fig.add_subplot(111)
self.text = self.axes.text(0., 1.005, '', transform = self.axes.transAxes)
self.cursor = Cursor(self.axes, useblit=True, color='red')
self.canvas.mpl_connect('motion_notify_event', self.cbUpdateCursor)
self.vbox = wx.BoxSizer(wx.VERTICAL)
self.vbox.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
self.panel.SetSizer(self.vbox)
self.vbox.Fit(self)
def cbUpdateCursor(self, event):
if event.inaxes:
text = 'x = %5.4f, y = %5.4f' % (event.xdata, event.ydata)
self.sb.SetStatusText(text)
#self.text.set_text(text)
#self.canvas.draw()
if __name__ == '__main__':
app = wx.PySimpleApp()
app.frame = wxPlotting()
app.frame.Show()
app.MainLoop()
Basically I want something similar to the text that gets displayed using pyplot, i.e. the bottom right corner when the code below is run:
Code:
import matplotlib.pyplot as plt
plt.plot(range(10000), range(10000))
plt.show()
EDIT:
In my actual program, I want the static text to be within the matplotlib axes, not really above it. So I don't think I can just use a wxpython statictext to display it.
You could use blitting, similar to the animation examples here.
This make a very large performance difference in this case, as only a small portion of the window needs to be redrawn.
Unfortunately, I can't figure out how to get a gray background behind the text when it's redrawn, to match the default figure background behind it... The performance is excellent, though.
As a stand-alone example based on your code above:
import wx
import numpy as np
import matplotlib
matplotlib.use('WXAgg')
from matplotlib.figure import Figure
from matplotlib.widgets import Cursor
from matplotlib.backends.backend_wxagg import \
FigureCanvasWxAgg as FigCanvas, \
NavigationToolbar2WxAgg as NavigationToolbar
class wxPlotting(wx.Frame):
title = 'Test'
def __init__(self):
wx.Frame.__init__(self, None, -1, self.title)
self.time = np.arange(10000)
self.data = np.random.random(10000)
self.sb = self.CreateStatusBar()
self.create_main_panel()
self.axes.plot(self.time, self.data)
self.background = self.canvas.copy_from_bbox(self.fig.bbox)
self.canvas.draw()
def create_main_panel(self):
self.panel = wx.Panel(self)
self.fig = Figure((5.0, 4.0), dpi=100)
self.canvas = FigCanvas(self.panel, -1, self.fig)
self.axes = self.fig.add_subplot(111)
self.text = self.axes.text(0., 1.005, '', transform = self.axes.transAxes, animated=True)
self.cursor = Cursor(self.axes, useblit=True, color='red')
self.canvas.mpl_connect('motion_notify_event', self.cbUpdateCursor)
self.vbox = wx.BoxSizer(wx.VERTICAL)
self.vbox.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
self.panel.SetSizer(self.vbox)
self.vbox.Fit(self)
def cbUpdateCursor(self, event):
if event.inaxes:
text = 'x = %5.4f, y = %5.4f' % (event.xdata, event.ydata)
self.sb.SetStatusText(text)
self.canvas.restore_region(self.background)
self.text.set_text(text)
self.axes.draw_artist(self.text)
self.canvas.blit(self.text.get_window_extent())
if __name__ == '__main__':
app = wx.PySimpleApp()
app.frame = wxPlotting()
app.frame.Show()
app.MainLoop()
You could add a static text box on top, and just update it's label:
import wx
import numpy as np
import matplotlib
matplotlib.use('WXAgg')
from matplotlib.figure import Figure
from matplotlib.widgets import Cursor
from matplotlib.backends.backend_wxagg import \
FigureCanvasWxAgg as FigCanvas, \
NavigationToolbar2WxAgg as NavigationToolbar
class wxPlotting(wx.Frame):
title = 'Test'
def __init__(self):
wx.Frame.__init__(self, None, -1, self.title)
self.time = np.arange(10000)
self.data = np.random.random(10000)
self.sb = self.CreateStatusBar()
self.create_main_panel()
self.axes.plot(self.time, self.data)
self.canvas.draw()
def create_main_panel(self):
self.panel = wx.Panel(self)
self.fig = Figure((5.0, 4.0), dpi=100)
self.canvas = FigCanvas(self.panel, -1, self.fig)
self.axes = self.fig.add_subplot(111)
self.text = self.axes.text(0., 1.005, '', transform = self.axes.transAxes)
self.cursor = Cursor(self.axes, useblit=True, color='red')
self.canvas.mpl_connect('motion_notify_event', self.cbUpdateCursor)
self.vbox = wx.BoxSizer(wx.VERTICAL)
self.cursor_pos = wx.StaticText(self.panel,-1, label="")
self.vbox.Add(self.cursor_pos, 0, wx.LEFT | wx.TOP | wx.GROW)
self.vbox.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
self.panel.SetSizer(self.vbox)
self.vbox.Fit(self)
def cbUpdateCursor(self, event):
if event.inaxes:
text = 'x = %5.4f, y = %5.4f' % (event.xdata, event.ydata)
self.sb.SetStatusText(text)
self.cursor_pos.SetLabel(text)
#self.text.set_text(text)
#self.canvas.draw()
if __name__ == '__main__':
app = wx.PySimpleApp()
app.frame = wxPlotting()
app.frame.Show()
app.MainLoop()

Categories