Why aren't the matplotlib checkboxes working in pyQt5? - python

I have a little issue in my code since I have a matplotlib graph with checkboxes (to choose what to plot) but when I open it with Pyqt5 (a Pushbutton) it does open but the checkboxes do not work, we can not touch them. My function works well, but not with Pyqt5, I hope I'm enough precise and I'm sorry if I am not. Here is my code if that can help you :
from PyQt5.QtWidgets import QPushButton, QMainWindow, QApplication
import sys
from matplotlib.widgets import CheckButtons
import matplotlib.pyplot as plt
def graph_check () :
x = [1,2,3,4,5,6]
y1 = [1,1,1,1,3,1]
y2 = [0,2,1,2,2,1]
y3 = [4,3,2,0,0,5]
fig,ax = plt.subplots()
p1, = ax.plot(x,y1,color = 'red', label = 'red')
p2, = ax.plot(x,y2,color = 'green', label = 'green')
p3, = ax.plot(x,y3,color = 'blue', label = 'blue')
lines = [p1,p2,p3]
plt.subplots_adjust(left = 0.25, bottom=0.1, right=0.95,top = 0.95)
# checkbuttons widgets
labels = ['red', 'green', 'blue']
activated = [True, True,True]
axCheckbutton = plt.axes([0.03,0.4,0.15,0.15])
chxbox = CheckButtons(axCheckbutton, labels,activated)
def set_visible (label) :
index = labels.index(label)
lines[index].set_visible(not lines[index].get_visible())
plt.draw()
chxbox.on_clicked(set_visible)
plt.show()
# that function does work well, in the end we have a graph with 3 lines and we can make
# them visible or not thanks to the checkbox.
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(100, 100, 600, 400)
self.btn1 = QPushButton('Graph check', self)
self.btn1.setGeometry(130, 215, 125, 55)
self.btn1.clicked.connect(self.btn1_onClicked)
self.show()
def btn1_onClicked(self):
graph_check()
# it works, we can see the graph but it is impossible to use the checkbox...
App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())

The problem is caused because "chxbox" is a local variable whose reference will be removed when the function finishes executing. A possible solution is to make it an attribute of another object that has a greater scope:
# ...
plt.chxbox = CheckButtons(axCheckbutton, labels, activated)
def set_visible(label):
index = labels.index(label)
lines[index].set_visible(not lines[index].get_visible())
plt.draw()
plt.chxbox.on_clicked(set_visible)
# ...

Related

matplotlib.widgets.TextBox interaction is slow when figure contains several subplots

Below is python code to demonstrate the problem.
If there are 2 rows and 2 columns of images, for example, typing/erasing in the textbox is reasonably fast. However, if there are 5 rows and 5 columns, typing/erasing in the textbox is quite slow. If the xticks and yticks are drawn, interaction is even slower. So, it seems as if the entire figure is redrawn after every keystroke.
Is there a solution for this (apart from putting the textbox on a separate figure)?
(My development platform is MacOS Mojave, Python 3.7.5.)
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib.widgets import TextBox
class Textbox_Demo(object):
def __init__(self):
self.fig = plt.figure(figsize=(8,8))
self.string = 'label'
self.rows = 5 # reducing rows speeds up textbox interaction
self.cols = 5 # reducing cols speeds up textbox interaction
self.plot_count = self.rows * self.cols
self.gs = gridspec.GridSpec(self.rows, self.cols,
left=0.05, right=1-0.02, top=1-.02, bottom=0.10, wspace=0.3, hspace=0.4)
for k in range(self.plot_count):
ax = self.fig.add_subplot(self.gs[k])
#ax.set_xticks([]) # showing axes slows textbox interaction
#ax.set_yticks([]) # showing axes slows textbox interaction
data = np.atleast_2d(np.sin(np.linspace(1,255,255) * 50))
ax.imshow(data, aspect="auto", cmap='ocean')
# this is the user-input textbox
tb_axis = plt.axes([0.125, 0.02, 0.8, 0.05])
self.tb = TextBox(tb_axis, 'Enter label:', initial=self.string, label_pad=0.01)
self.tb.on_submit(self.on_submit)
plt.show()
def on_submit(self, text):
pass
if __name__ == "__main__":
Textbox_Demo()
Matplotlib's TextBox is inherently slow, because it uses the drawing tools provided by matplotlib itself and hence redraws the complete figure upon changes.
I would propose to use a text box of a GUI kit instead. For example for PyQt this might look like:
import numpy as np
import sys
from matplotlib.backends.backend_qt5agg import (
FigureCanvas, NavigationToolbar2QT as NavigationToolbar)
from matplotlib.backends.qt_compat import QtCore, QtWidgets
import matplotlib.gridspec as gridspec
from matplotlib.figure import Figure
class Textbox_Demo(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
layout = QtWidgets.QVBoxLayout(self._main)
layout.setContentsMargins(0,0,0,0)
layout.setSpacing(0)
self.fig = Figure(figsize=(8,8))
self.canvas = FigureCanvas(self.fig)
layout.addWidget(self.canvas)
self.addToolBar(NavigationToolbar(self.canvas, self))
self._textwidget = QtWidgets.QWidget()
textlayout = QtWidgets.QHBoxLayout(self._textwidget)
self.textbox = QtWidgets.QLineEdit(self)
self.textbox.editingFinished.connect(self.on_submit)
# or, if wanting to have changed apply directly:
# self.textbox.textEdited.connect(self.on_submit)
textlayout.addWidget(QtWidgets.QLabel("Enter Text: "))
textlayout.addWidget(self.textbox)
layout.addWidget(self._textwidget)
self.fill_figure()
def fill_figure(self):
self.string = 'label'
self.rows = 5 # reducing rows speeds up textbox interaction
self.cols = 5 # reducing cols speeds up textbox interaction
self.plot_count = self.rows * self.cols
self.gs = gridspec.GridSpec(self.rows, self.cols,
left=0.05, right=1-0.02, top=1-.02, bottom=0.10, wspace=0.3, hspace=0.4)
for k in range(self.plot_count):
ax = self.fig.add_subplot(self.gs[k])
#ax.set_xticks([]) # showing axes slows textbox interaction
#ax.set_yticks([]) # showing axes slows textbox interaction
data = np.atleast_2d(np.sin(np.linspace(1,255,255) * 50))
ax.imshow(data, aspect="auto", cmap='ocean')
def on_submit(self):
text = self.textbox.text()
print(text)
pass
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = Textbox_Demo()
app.show()
qapp.exec_()

How to effectively redraw multiple matplotlib plots with blit

I'm using matplotlib with pyqt5 to draw data into 3 axes, and than user can make selection in one plot that will be shown in other two plots too. Since I'm working with big data (up to 10 millions of points), drawing selection could be slow, especially when I need to draw to scatterplot.
I am trying to use matplotlib blit function, but have some issues with result. Here is minimum simple example.
import matplotlib
matplotlib.use('Qt5Agg')
import numpy as np
import sys
from matplotlib.backends.qt_compat import QtCore, QtWidgets
from matplotlib.backends.backend_qt5agg import (FigureCanvas, NavigationToolbar2QT as NavigationToolbar)
from matplotlib.figure import Figure
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
layout = QtWidgets.QVBoxLayout(self._main)
self.static_canvas = FigureCanvas(Figure(figsize=(10, 10)))
layout.addWidget(self.static_canvas)
layout.addWidget(NavigationToolbar(self.static_canvas, self))
axes = self.static_canvas.figure.subplots(2, 1)
self.ax1 = axes[0]
self.ax2 = axes[1]
self.ax1.cla()
self.ax2.cla()
button = QtWidgets.QPushButton('Click me!')
button.clicked.connect(self.update_canvas_blit)
layout.addWidget(button)
# Fixing random state for reproducibility
np.random.seed(19680801)
# Create random data
N = 50000
x = np.random.rand(N)
y = np.random.rand(N)
self.ax1.scatter(x, y)
self.points = self.ax1.scatter([],[], s=5, color='red')
x = np.linspace(0, 1000, 100000)
self.ax2.plot(x, np.sin(x))
self.lines, = self.ax2.plot([],[], color='red')
self.static_canvas.draw()
self.background1 = self.static_canvas.copy_from_bbox(self.ax1.bbox)
self.background2 = self.static_canvas.copy_from_bbox(self.ax2.bbox)
def update_canvas_blit(self):
N = 50
x = np.random.rand(N)
y = np.random.rand(N)
self.static_canvas.restore_region(self.background1)
self.points.set_offsets(np.c_[x,y])
self.ax1.draw_artist(self.points)
self.ax1.figure.canvas.blit(self.ax1.bbox)
self.static_canvas.restore_region(self.background2)
x = np.linspace(0, np.random.randint(500,1000), 1000)
self.lines.set_data(x, np.sin(x))
self.ax2.draw_artist(self.lines)
self.ax2.figure.canvas.blit(self.ax2.bbox)
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
app.show()
qapp.exec_()
When clicking button, expected output should be still same background with random points/lines redrawing. In a way it is happening but there are some strange artifacts that looks like somehow axes are drawn to each other. But when I try to save it to .png, it will restore to good state.
The problem is that the snapshot of the background is taken at a moment in time where the figure has not yet been shown on screen. At that point the figure is 10 by 10 inches large. Later, it is shown inside the QMainWindow and resized to fit into the widget.
Only once that has happened, it makes sense to take the background snapshot.
One option is to use a timer of 1 second and only then copy the background. This would look as follows.
import numpy as np
import sys
from matplotlib.backends.qt_compat import QtCore, QtWidgets
from matplotlib.backends.backend_qt5agg import (FigureCanvas, NavigationToolbar2QT as NavigationToolbar)
from matplotlib.figure import Figure
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
layout = QtWidgets.QVBoxLayout(self._main)
self.static_canvas = FigureCanvas(Figure(figsize=(10, 10)))
layout.addWidget(self.static_canvas)
layout.addWidget(NavigationToolbar(self.static_canvas, self))
axes = self.static_canvas.figure.subplots(2, 1)
self.ax1 = axes[0]
self.ax2 = axes[1]
self.ax1.cla()
self.ax2.cla()
button = QtWidgets.QPushButton('Click me!')
button.clicked.connect(self.update_canvas_blit)
layout.addWidget(button)
# Fixing random state for reproducibility
np.random.seed(19680801)
# Create random data
N = 50000
x = np.random.rand(N)
y = np.random.rand(N)
self.ax1.scatter(x, y)
self.points = self.ax1.scatter([],[], s=5, color='red')
x = np.linspace(0, 1000, 100000)
self.ax2.plot(x, np.sin(x))
self.lines, = self.ax2.plot([],[], color='red')
self.static_canvas.draw()
self._later()
def _later(self, evt=None):
self.timer = self.static_canvas.new_timer(interval=1000)
self.timer.single_shot = True
self.timer.add_callback(self.update_background)
self.timer.start()
def update_background(self, evt=None):
self.background1 = self.static_canvas.copy_from_bbox(self.ax1.bbox)
self.background2 = self.static_canvas.copy_from_bbox(self.ax2.bbox)
def update_canvas_blit(self):
N = 50
x = np.random.rand(N)
y = np.random.rand(N)
self.static_canvas.restore_region(self.background1)
self.points.set_offsets(np.c_[x,y])
self.ax1.draw_artist(self.points)
self.ax1.figure.canvas.blit(self.ax1.bbox)
self.static_canvas.restore_region(self.background2)
x = np.linspace(0, np.random.randint(500,1000), 1000)
self.lines.set_data(x, np.sin(x))
self.ax2.draw_artist(self.lines)
self.ax2.figure.canvas.blit(self.ax2.bbox)
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
app.show()
qapp.exec_()

How to aesthetically show a generic number of axes in matplotlib?

I want to do a simple GUI that allows the user to add or remove traces from a plot for any number of traces. It looks like this:
The problems I'm having:
I don't know how to make the axes not to superpose with each other for a generic number of plots.
When I plot more than one trace, and then delete all but one, there are two axes showing for some reason. There should always be one axis per trace being shown.
Is there a way to fix these issues? You can find my code below. The only function that should be changed is update_canvas(), I believe. To try it out, just modify the list name_vars in the main with the number of variables you want. The rest of the example code is self-contained.
import numpy as np
from matplotlib.backends.qt_compat import QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvas
from matplotlib.figure import Figure
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(ApplicationWindow, self).__init__(parent)
global name_vars
self.x = np.array([1,2,3,4,5])
self.y = np.random.random((5, len(name_vars)))
self.num_vars = np.size(self.y,1)
self.name_vars = name_vars
self.tags_on = [0] * self.num_vars
self.colors = ['#1F77B4','#FF7F0E','#2CA02C','#D62728','#9467BD',
'#8C564B','#E377C2','#F7F7F7','#BCBD22','#17BECF']
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
canvas = FigureCanvas(Figure(figsize=(10, 10)))
self.canvas_ax = canvas.figure.subplots()
self.canvas_ax.set_xlabel("Time")
self.canvas_ax_twin = []
self.list_tags = QtWidgets.QComboBox(self)
for name in self.name_vars:
self.list_tags.addItem(name)
button_add = QtWidgets.QPushButton('Add', self)
button_remove = QtWidgets.QPushButton('Remove', self)
button_add.clicked.connect(self.add_plot)
button_remove.clicked.connect(self.remove_plot)
layout = QtWidgets.QGridLayout(self._main)
layout.addWidget(canvas, 0, 0)
dropdown_layout = QtWidgets.QHBoxLayout()
dropdown_layout.addWidget(self.list_tags)
dropdown_layout.addWidget(button_add)
dropdown_layout.addWidget(button_remove)
layout.addLayout(dropdown_layout, 1, 0)
self.show()
def add_plot(self):
selected_tag = self.list_tags.currentIndex()
self.tags_on[selected_tag] = 1
self.update_canvas()
def remove_plot(self):
selected_tag = self.list_tags.currentIndex()
self.tags_on[selected_tag] = 0
self.update_canvas()
def update_canvas(self):
# Delete all traces
self.canvas_ax.clear()
[i.clear() for i in self.canvas_ax_twin]
self.canvas_ax_twin = []
num_plots = 0
for ii in range(self.num_vars):
if self.tags_on[ii] == 1:
# If it's not the first trace, create a twin axis
if num_plots != 0:
self.canvas_ax_twin.append(self.canvas_ax.twinx())
self.canvas_ax_twin[-1].plot(self.x, self.y[:,ii], self.colors[num_plots])
self.canvas_ax_twin[-1].set_ylabel(self.name_vars[ii])
self.canvas_ax_twin[-1].yaxis.label.set_color(self.colors[num_plots])
self.canvas_ax_twin[-1].tick_params(axis='y', colors=self.colors[num_plots])
num_plots += 1
# If it's the first trace, use the original axis
else:
self.canvas_ax.plot(self.x, self.y[:,ii], self.colors[num_plots])
self.canvas_ax.set_ylabel(self.name_vars[ii])
self.canvas_ax.yaxis.label.set_color(self.colors[num_plots])
self.canvas_ax.tick_params(axis='y', colors=self.colors[num_plots])
num_plots += 1
# Show the final plot
self.canvas_ax.figure.canvas.draw()
if __name__ == '__main__':
# Edit the number of elements in name_vars to try the code
name_vars = ['V1','V2','V3','V4']
app = QtWidgets.QApplication([])
ex = ApplicationWindow()
ex.show()
app.exec_()
I would suggest to separate the logic from the actual plotting. This makes it easier to follow through. This solves the second question about not removing all axes.
The question about not letting the axes superimpose may be solved by setting the position of additional twin axes to some distance from the axes, depending on how many axes you have.
ax.spines["right"].set_position(("axes", 1+(n-1)*0.1))
where n is the axes number starting from 0. The main axes (n=0) should be excluded, and the first axes will stay at position 1. Further axes are positionned in steps of 0.1.
Then it makes sense to also adjust the right margin of the main axes to give enough space for the extra spines.
import numpy as np
from matplotlib.backends.qt_compat import QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvas
from matplotlib.figure import Figure
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None, name_vars=[]):
super(ApplicationWindow, self).__init__(parent)
self.x = np.array([1,2,3,4,5])
self.y = np.random.random((5, len(name_vars)))
self.num_vars = np.size(self.y,1)
self.name_vars = name_vars
self.tags_on = [0] * self.num_vars
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
self.figure = Figure(figsize=(10, 10))
canvas = FigureCanvas(self.figure)
self.left = self.figure.subplotpars.left
self.right = self.figure.subplotpars.right
self.canvas_ax = canvas.figure.subplots()
self.canvas_ax.set_xlabel("Time")
self.axes = [self.canvas_ax]
self.list_tags = QtWidgets.QComboBox(self)
for name in self.name_vars:
self.list_tags.addItem(name)
button_add = QtWidgets.QPushButton('Add', self)
button_remove = QtWidgets.QPushButton('Remove', self)
button_add.clicked.connect(self.add_plot)
button_remove.clicked.connect(self.remove_plot)
layout = QtWidgets.QGridLayout(self._main)
layout.addWidget(canvas, 0, 0)
dropdown_layout = QtWidgets.QHBoxLayout()
dropdown_layout.addWidget(self.list_tags)
dropdown_layout.addWidget(button_add)
dropdown_layout.addWidget(button_remove)
layout.addLayout(dropdown_layout, 1, 0)
self.show()
def add_plot(self):
selected_tag = self.list_tags.currentIndex()
self.tags_on[selected_tag] = 1
self.update_canvas()
def remove_plot(self):
selected_tag = self.list_tags.currentIndex()
self.tags_on[selected_tag] = 0
self.update_canvas()
def create_nth_axes(self, n, dataset):
if n == 0:
ax = self.canvas_ax
else:
ax = self.canvas_ax.twinx()
ax.spines["right"].set_position(("axes", 1+(n-1)*0.1))
for direction in ["left", "bottom", "top"]:
ax.spines[direction].set_visible(False)
# adjust subplotparams to make space for new axes spine
new_right = (self.right-self.left)/(1+(n-1)*0.1)+self.left
self.figure.subplots_adjust(right=new_right)
color = next(self.canvas_ax._get_lines.prop_cycler)['color']
ax.set_ylabel(self.name_vars[dataset], color=color)
ax.plot(self.x, self.y[:,dataset], color=color)
return ax
def clear_canvas(self):
# Clear main axes
self.canvas_ax.clear()
# clear and remove other axes
for ax in self.axes[1:]:
ax.clear()
ax.remove()
self.axes = [self.canvas_ax]
self.figure.subplots_adjust(right=0.9)
def update_canvas(self):
self.clear_canvas()
k = 0
for i, tag in enumerate(self.tags_on):
if tag:
ax = self.create_nth_axes(k, i)
if k > 0:
self.axes.append(ax)
k += 1
self.canvas_ax.figure.canvas.draw()
if __name__ == '__main__':
# Edit the number of elements in name_vars to try the code
name_vars = ['V1','V2','V3','V4']
app = QtWidgets.QApplication([])
ex = ApplicationWindow(name_vars=name_vars)
ex.show()
app.exec_()

pyqtgraph: maintain constant position of TextItem while scaling

I would like a TextItem that maintains a constant position on the graph while scaling the y-axis, essentially the same functionality as legend only as a TextItem where I can change the text as needed. I cannot figure out how to do this. Any suggestions welcome.
This example shows the problem. On the lefthand graph, scaling the y-axis causes the text to move whereas on the righthand graph the legend stays in a constant position as you scale. I would like the position of the textItem to be defined like the legend position, in a constant position relative to the graph window. Alternatively if someone knows how to change the format of the legend and update the text that would also work, but from my reading of the documentation this is not possible.
import pyqtgraph as pg
from PyQt4 import QtGui
import numpy as np
import sys
def main():
app = QtGui.QApplication(sys.argv)
widg = QtGui.QWidget()
widg.move(100, 100)
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
pgWidg = pg.GraphicsLayoutWidget()
pgWidg.resize(750, 250)
graph1 = pgWidg.addPlot(row=1, col=1)
graph2 = pgWidg.addPlot(row=1, col=2)
curve1 = graph1.plot(y=np.sin(np.linspace(1, 21, 1000)), pen='k')
curve2 = graph2.plot(y=np.sin(np.linspace(1, 21, 1000)), pen='k')
graph1.addItem(curve1)
graph2.addItem(curve2)
graph1.setMouseEnabled(x=False, y=True)
graph2.setMouseEnabled(x=False, y=True)
graph1Text = pg.TextItem(text = 'A1', color=(0, 0, 0))
graph1.addItem(graph1Text)
graph1Text.setPos(150, 1)
legend = graph2.addLegend()
style = pg.PlotDataItem(pen='w')
legend.addItem(style, 'A2')
grid = QtGui.QGridLayout()
grid.addWidget(pgWidg, 0,0)
widg.setLayout(grid)
widg.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
7 bilion years later of googling...
label = pg.LabelItem("Error", size="36pt", color="FF0000")
label.setParentItem(self.plotInstance)
label.anchor(itemPos=(1,0), parentPos=(1,0), offset=(-10,10))
where self.plotInstance = pg.PlotWidget.getPlotItem()
works on PyQt5 and pyqtgraph 0.12
This is a somewhat old question now- Hopefully this will help someone. Answering this helped me answer my own question.
Please note that I used PySide2 rather than PyQt4- I don't think this is significantly different to PyQt4. I am also using pyqtgraph 0.11.1.
There is a getLabel() method of the LegendItem that returns the LabelItem inside the legend for a given plotItem. This should allow you to do what you want.
You created your legend with this code:
legend = graph2.addLegend()
style = pg.PlotDataItem(pen='w')
legend.addItem(style, 'A2')
You can then get the labelitem with:
legend_labelitem = legend.getLabel(style)
With that you should be able to change the properties - such as using .setText() to set a new legend text:
legend_labelitem.setText('Something else')
The full code would end up as this:
import pyqtgraph as pg
# from PySide2 import QtGui # <---- tested with this
from PyQt4 import QtGui
import numpy as np
import sys
def main():
app = QtGui.QApplication(sys.argv)
widg = QtGui.QWidget()
widg.move(100, 100)
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
pgWidg = pg.GraphicsLayoutWidget()
pgWidg.resize(750, 250)
graph1 = pgWidg.addPlot(row=1, col=1)
graph2 = pgWidg.addPlot(row=1, col=2)
curve1 = graph1.plot(y=np.sin(np.linspace(1, 21, 1000)), pen='k')
curve2 = graph2.plot(y=np.sin(np.linspace(1, 21, 1000)), pen='k')
graph1.addItem(curve1)
graph2.addItem(curve2)
graph1.setMouseEnabled(x=False, y=True)
graph2.setMouseEnabled(x=False, y=True)
graph1Text = pg.TextItem(text = 'A1', color=(0, 0, 0))
graph1.addItem(graph1Text)
graph1Text.setPos(150, 1)
legend = graph2.addLegend()
style = pg.PlotDataItem(pen='w')
legend.addItem(style, 'A2')
legend_labelitem = legend.getLabel(style) # <---------
legend_labelitem.setText('Something else') # <---------
grid = QtGui.QGridLayout()
grid.addWidget(pgWidg, 0,0)
widg.setLayout(grid)
widg.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
It produces this:

Dynamically update multiple axis in matplotlib

I want to display sensor data on a PyQT GUI with a matplotlib animation.
I already have a working Plot which gets updates every time I receive new sensor value from an external source with this code:
def __init__(self):
self.fig = Figure(figsize=(width, height), dpi=dpi)
self.axes = self.fig.add_subplot(111)
self.axes.grid()
self.xdata = []
self.ydata = []
self.entry_limit = 50
self.line, = self.axes.plot([0], [0], 'r')
def update_figure_with_new_value(self, xval: float, yval: float):
self.xdata.append(xval)
self.ydata.append(yval)
if len(self.xdata) > self.entry_limit:
self.xdata.pop(0)
self.ydata.pop(0)
self.line.set_data(self.xdata, self.ydata)
self.axes.relim()
self.axes.autoscale_view()
self.fig.canvas.draw()
self.fig.canvas.flush_events()
I want now to extend the plot to show another data series with the same x-axis. I tried to achieve this with the following additions to the init-code above:
self.axes2 = self.axes.twinx()
self.y2data = []
self.line2, = self.axes2.plot([0], [0], 'b')
and in the update_figure_with_new_value() function (for test purpose I just tried to add 1 to yval, I will extend the params of the function later):
self.y2data.append(yval+1)
if len(self.y2data) > self.entry_limit:
self.y2data.pop(0)
self.line2.set_data(self.xdata, self.ydata)
self.axes2.relim()
self.axes2.autoscale_view()
But instead of getting two lines in the plot which should have the exact same movement but just shifted by one I get vertical lines for the second plot axis (blue). The first axis (red) remains unchanged and is ok.
How can I use matplotlib to update multiple axis so that they display the right values?
I'm using python 3.4.0 with matplotlib 2.0.0.
Since there is no minimal example available, it's hard to tell the reason for this undesired behaviour. In principle ax.relim() and ax.autoscale_view() should do what you need.
So here is a complete example which works fine and updates both scales when being run with python 2.7, matplotlib 2.0 and PyQt4:
import numpy as np
import matplotlib.pyplot as plt
from PyQt4 import QtGui, QtCore
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
class Window(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
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(0)
self.fig = Figure(figsize=(5,4), dpi=100)
self.axes = self.fig.add_subplot(111)
self.axes.grid()
self.xdata = [0]
self.ydata = [0]
self.entry_limit = 50
self.line, = self.axes.plot([], [], 'r', lw=3)
self.axes2 = self.axes.twinx()
self.y2data = [0]
self.line2, = self.axes2.plot([], [], 'b')
self.canvas = FigureCanvas(self.fig)
self.canvas.draw()
self.nav = NavigationToolbar(self.canvas, self.widget)
self.widget.layout().addWidget(self.nav)
self.widget.layout().addWidget(self.canvas)
self.show()
self.ctimer = QtCore.QTimer()
self.ctimer.timeout.connect(self.update)
self.ctimer.start(150)
def update(self):
y = np.random.rand(1)
self.update_figure_with_new_value(self.xdata[-1]+1,y)
def update_figure_with_new_value(self, xval,yval):
self.xdata.append(xval)
self.ydata.append(yval)
if len(self.xdata) > self.entry_limit:
self.xdata.pop(0)
self.ydata.pop(0)
self.y2data.pop(0)
self.line.set_data(self.xdata, self.ydata)
self.axes.relim()
self.axes.autoscale_view()
self.y2data.append(yval+np.random.rand(1)*0.17)
self.line2.set_data(self.xdata, self.y2data)
self.axes2.relim()
self.axes2.autoscale_view()
self.fig.canvas.draw()
self.fig.canvas.flush_events()
if __name__ == "__main__":
qapp = QtGui.QApplication([])
a = Window()
exit(qapp.exec_())
You may want to test this and report back if it is working or not.

Categories

Resources