I'm trying to plot data of diferentes sensors en realtime,so I decided to plot the data using PyQtGraph in PyQt, in order to make it work with several sensor's data from differnte sources.
Searching example on the internet, i found one and i tried to adapt it ,
Because of QtGui.QApplication.instance().exec_(), which carries the inconvenient side effect of blocking the execution of the rest of the code after it. I tried to manage to used threads using Multiproccessing. I could make the rest of the code works, but how coud I update the plot using external data (Plo2D.update2),I tried to used multiprocessing.Queue , but I didn't work, instead appears massage of the window must be closed.
from pyqtgraph.Qt import QtGui, QtCore
import numpy as np
from numpy import arange
import pyqtgraph as pg
import sys
import multiprocessing
class Plot2D():
def __init__(self,):
self.traces = dict()
self.app = QtGui.QApplication([])
self.win = pg.GraphicsWindow(title="Dibujar")
self.win.resize(1000, 600)
self.win.setWindowTitle('Ejemplo')
pg.setConfigOptions(antialias=True)
#self.canvas = self.win.addPlot(title="Pytelemetry")
self.waveform1 = self.win.addPlot(title='WAVEFORM1', row=1, col=1)
self.waveform2 = self.win.addPlot(title='WAVEFORM2', row=2, col=1)
def start(self):
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
def set_plotdata(self, name, datax, datay):
if name in self.traces:
self.traces[name].setData(datax, datay)
else:
if name == '910D':
self.traces[name] = self.waveform1.plot(pen='c', width=3)
if name == 'MPU':
self.traces[name] = self.waveform2.plot(pen='c', width=3)
def update2(self):
# Trying to get external data
ptm1 = globals()['DatExt1']
ptm2 = globals()['DatExt2']
while ptm1.empty() is False:
self.data1 = ptm1.get()
self.set_plotdata('MPU', self.data1[0], self.data1[1])
# csvWriterG910D.writerows(Informa)
# file1.flush()
while ptm2.empty() is False:
self.data2 = ptm2.get()
self.set_plotdata('910D', self.data1[0], self.data1[1])
def animation(self):
timer = QtCore.QTimer()
timer.timeout.connect(self.update2)
timer.start(60)
self.start()
# It is thread started from main.py
def ShowData(Data1, Data2): # Data1,Data2 : multiprocessing.Queue
DatExt1 = Data1
DatExt2 = Data2
p = Plot2D()
p.animation()
the main.py:
if __name__ == '__main__':
Data1 = multiprocessing.Queue()
Data2 = multiprocessing.Queue()
Plottingdata = Process(target=PlotData.ShowData, args=(Data1, Data2, ))
Plottingdata.start()
t = np.arange(-3.0, 2.0, 0.01)
i = 0.0
while True:
s = np.sin(2 * 2 * 3.1416 * t) / (2 * 3.1416 * t + i)
time.sleep(1)
Data1.put([t, s])
i = i + 0.1
thanks ind advanced for help
Instead of using MultiProcessing you should use MultiThreading, that is, create threads that are responsible for collecting data (in your example emulate data) and then send the data to the GUI by signals.
PlotData.py
from pyqtgraph.Qt import QtGui, QtCore
import numpy as np
import pyqtgraph as pg
import sys
class Plot2D(pg.GraphicsWindow):
def __init__(self):
pg.GraphicsWindow.__init__(self, title="Dibujar")
self.traces = dict()
self.resize(1000, 600)
pg.setConfigOptions(antialias=True)
#self.canvas = self.win.addPlot(title="Pytelemetry")
self.waveform1 = self.addPlot(title='WAVEFORM1', row=1, col=1)
self.waveform2 = self.addPlot(title='WAVEFORM2', row=2, col=1)
def set_plotdata(self, name, x, y):
if name in self.traces:
self.traces[name].setData(x, y)
else:
if name == "910D":
self.traces[name] = self.waveform1.plot(x, y, pen='y', width=3)
elif name == "MPU":
self.traces[name] = self.waveform2.plot(x, y, pen='y', width=3)
#QtCore.pyqtSlot(str, tuple)
def updateData(self, name, ptm):
x, y = ptm
self.set_plotdata(name, x, y)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
plot = Plot2D()
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
main.py
import sys
from pyqtgraph.Qt import QtCore, QtGui
import threading
import numpy as np
import time
from PlotData import Plot2D
class Helper(QtCore.QObject):
changedSignal = QtCore.pyqtSignal(str, tuple)
def create_data1(helper, name):
t = np.arange(-3.0, 2.0, 0.01)
i = 0.0
while True:
s = np.sin(2 * 2 * 3.1416 * t) / (2 * 3.1416 * t + i)
time.sleep(.1)
helper.changedSignal.emit(name, (t, s))
i = i + 0.1
def create_data2(helper, name):
t = np.arange(-3.0, 2.0, 0.01)
i = 0.0
while True:
s = np.cos(2 * 2 * 3.1416 * t) / (2 * 3.1416 * t - i)
time.sleep(.1)
helper.changedSignal.emit(name, (t, s))
i = i + 0.1
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
helper = Helper()
plot = Plot2D()
helper.changedSignal.connect(plot.updateData, QtCore.Qt.QueuedConnection)
threading.Thread(target=create_data1, args=(helper, "910D"), daemon=True).start()
threading.Thread(target=create_data2, args=(helper, "MPU"), daemon=True).start()
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
Related
I want to create an animation that people can use to align their breathing with. I have made a class with PyQt5 that does exactly this, and has the breathing period as parameter. (See code below).
It works well, apart from the timing. When setting a specific delta_t and window size during the FuncAnimation I can get accurate timings. But when I change the window size, it either speeds up or slows down...
Im probably going to model this in another language, but I am still curious if I can get this right in Python. Can anyone here point me in the right direction?
import os
import time
import sys
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import PyQt5.QtGui
import PyQt5.QtCore
import PyQt5.QtWidgets
class BreathingAnimation(PyQt5.QtWidgets.QMainWindow):
# Could inherit from GenericInterface
# Animate three ways:
# o Fixed sinusoid -> moving ball
# o Fixed ball (up/down) -> moving wave
# o Expanding ball
def __init__(self, period=1, delta_t=0.01, animation_type='ball'):
super().__init__()
self._main = PyQt5.QtWidgets.QWidget()
self.setCentralWidget(self._main)
"""
Plot variables
sin(omega * x)
period = 2 * np.pi * omega
omega = period / (2 * np.pi)
"""
self.delta_t = delta_t
self.frequentie = 1/period
self.max_plot_time = 5
self.max_periods = 20
self.t_range = np.arange(0, self.max_periods * period, delta_t)
self.animation_type = animation_type
self.cool_down = 10
self.prev_time = time.time()
self.prev_time_cor_time = time.time()
"""
Graphical definitions
"""
self.canvas, self.fig, self.axes = self.get_figure(self.max_plot_time)
self.axes.set_axis_off()
self.line_obj, self.scatter_obj = self.get_plot_objects(self.axes)
# Get the animation object
self.anim_obj = self.get_animation()
h_layout = PyQt5.QtWidgets.QHBoxLayout(self._main)
# Create buttons
# self.button_box_layout = self.get_button_box_layout()
# write_button = self.get_push_button(name='Write', shortcut='W', connect=self.write_animation)
# self.edit_button = self.get_line_edit(name=f'{period}', connect=self.update_period, max_length=4)
# self.button_box_layout.addWidget(self.edit_button)
# self.button_box_layout.addWidget(write_button)
# Add canvas to the figure
temp_canvas_layout = PyQt5.QtWidgets.QVBoxLayout()
temp_canvas_layout.addWidget(self.canvas)
h_layout.addLayout(temp_canvas_layout, stretch=1)
# h_layout.addLayout(self.button_box_layout, stretch=0.01)
#staticmethod
def get_figure(max_plot_time, debug=False):
if debug:
fig = plt.figure()
else:
fig = Figure(figsize=(5, 5), dpi=100)
canvas = FigureCanvas(fig)
axes = canvas.figure.subplots()
# self.axes.set_axis_off()
axes.set_ylim(-2, 2)
axes.set_xlim(0, max_plot_time)
return canvas, fig, axes
#staticmethod
def get_plot_objects(axes):
# Create a line object
line_obj = axes.plot([], [], zorder=1)[0]
# Create a scatter object
scatter_obj = axes.scatter([], [], s=40, marker='o', c='r')
return line_obj, scatter_obj
def get_y_value(self, i_t):
omega = 2 * np.pi * self.frequentie
y_value = np.sin(omega * i_t)
return y_value
def animate_moving_ball(self, i, line_obj=None, scatter_obj=None):
i = i % len(self.t_range)
if line_obj is None:
line_obj = self.line_obj
if scatter_obj is None:
scatter_obj = self.scatter_obj
line_obj.set_data(self.t_range, self.get_y_value(self.t_range))
sel_time = self.t_range[i]
scatter_obj.set_offsets(np.c_[sel_time, self.get_y_value(sel_time)])
return scatter_obj
def animate_moving_wave(self, i, line_obj=None, scatter_obj=None):
i = i % len(self.t_range)
if line_obj is None:
line_obj = self.line_obj
if scatter_obj is None:
scatter_obj = self.scatter_obj
line_obj.set_data(self.t_range, np.roll(self.get_y_value(self.t_range), -i))
sel_time = self.t_range[i] + self.max_plot_time/2.
# print(f'max plot time {i}', self.max_plot_time/2, self.get_y_value(sel_time))
scatter_obj.set_offsets(np.c_[self.max_plot_time/2., self.get_y_value(sel_time)])
# self.cool_down -= 1
# # print(self.cool_down)
if ((1 - self.get_y_value(sel_time)) < 0.0001):
time_difference = time.time() - self.prev_time
self.prev_time = time.time()
print('Time interval in seconds ', time_difference)
return scatter_obj
def update_period(self):
new_periode = float(self.edit_button.text())
self.frequentie = 1./new_periode
self.t_range = np.arange(0, self.max_periods * new_periode, self.delta_t)
def get_animation(self):
if self.animation_type == 'ball':
# Return a moving ball..
animation_fun = self.animate_moving_ball
elif self.animation_type == 'wave':
# Return a wave..
animation_fun = self.animate_moving_wave
else:
animation_fun = None
self.animation_obj = animation.FuncAnimation(self.canvas.figure, animation_fun,
blit=False, repeat=True,
interval=self.delta_t, # Delay in ms
frames=len(self.t_range))
self.animation_obj.new_frame_seq()
return self.animation_obj
def write_animation(self):
num_frames = len(self.t_range)
max_time = np.max(self.t_range) # in seconds?
print('frames ', num_frames / max_time)
ffmpeg_writer = animation.FFMpegWriter(fps=num_frames / max_time)
self.animation_obj.save(os.path.expanduser('~/breathing_animation.mp4'), writer=ffmpeg_writer)
print('Written')
if __name__ == "__main__":
qapp = PyQt5.QtWidgets.QApplication(sys.argv)
app = BreathingAnimation(period=3, animation_type='wave', delta_t=0.009)
app.show()
qapp.exec_()
I'm trying to display some Plot.ly or Plot.ly Dash plots ( I haven't settled on using one or the other, so I'm experimenting with both right now) in a PyQt5 GUI using QWebEngineView. This doesn't work for any plots larger than 2MB due to some Chromium-level hardcoded restriction.
I found one similar question that is pretty much identical in terms of our needs. It looks like the OP actually found an answer, but unfortunately for me, they didn't post an example of working code or explain what they did to make it work. I do not understand enough of the underlying theory to piece together an answer with the resources linked in this other question, and my Stack reputation isn't high enough to comment and ask the OP what exactly worked.
Here is a minimum reproducible example that displays a plot embedded in the GUI. It's a modification of an answer to a question about embedding Plotly plots in PyQt5 GUIs here:
import numpy as np
import plotly.offline as po
import plotly.graph_objs as go
from PyQt5.QtWebEngineWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
def show_qt(fig):
raw_html = '<html><head><meta charset="utf-8" />'
raw_html += '<script src="https://cdn.plot.ly/plotly-latest.min.js"></script></head>'
raw_html += '<body>'
raw_html += po.plot(fig, include_plotlyjs=False, output_type='div')
raw_html += '</body></html>'
fig_view = QWebEngineView()
# setHtml has a 2MB size limit, need to switch to setUrl on tmp file
# for large figures.
fig_view.setHtml(raw_html)
# fig_view.setUrl(QUrl('temp-plot.html'))
fig_view.show()
fig_view.raise_()
return fig_view
if __name__ == '__main__':
app = QApplication(sys.argv)
# Working small plot:
fig = go.Figure(data=[{'type': 'scattergl', 'y': [2, 1, 3, 1]}])
# Not working large plot:
# t = np.arange(0, 200000, 1)
# y = np.sin(t/20000)
fig = go.Figure(data=[{'type': 'scattergl', 'y': y}])
# po.plot(fig)
fig_view = show_qt(fig)
sys.exit(app.exec_())
Here is a modified version that demonstrates how a large data set cannot be displayed the same way:
import numpy as np
import plotly.offline as po
import plotly.graph_objs as go
from PyQt5.QtWebEngineWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
def show_qt(fig):
raw_html = '<html><head><meta charset="utf-8" />'
raw_html += '<script src="https://cdn.plot.ly/plotly-latest.min.js"></script></head>'
raw_html += '<body>'
raw_html += po.plot(fig, include_plotlyjs=False, output_type='div')
raw_html += '</body></html>'
fig_view = QWebEngineView()
# setHtml has a 2MB size limit, need to switch to setUrl on tmp file
# for large figures.
fig_view.setHtml(raw_html)
# fig_view.setUrl(QUrl('temp-plot.html'))
fig_view.show()
fig_view.raise_()
return fig_view
if __name__ == '__main__':
app = QApplication(sys.argv)
# Working small plot:
# fig = go.Figure(data=[{'type': 'scattergl', 'y': [2, 1, 3, 1]}])
# Not working large plot:
t = np.arange(0, 200000, 1)
y = np.sin(t/20000)
fig = go.Figure(data=[{'type': 'scattergl', 'y': y}])
# po.plot(fig)
fig_view = show_qt(fig)
sys.exit(app.exec_())
Lastly, here is something I tried to get the large plot to display with QUrl pointing to a local html plot on the disk:
import numpy as np
import plotly.offline as po
import plotly.graph_objs as go
from PyQt5.QtWebEngineWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
def show_qt(fig):
raw_html = '<html><head><meta charset="utf-8" />'
raw_html += '<script src="https://cdn.plot.ly/plotly-latest.min.js"></script></head>'
raw_html += '<body>'
raw_html += po.plot(fig, include_plotlyjs=False, output_type='div')
raw_html += '</body></html>'
fig_view = QWebEngineView()
# setHtml has a 2MB size limit, need to switch to setUrl on tmp file
# for large figures.
# fig_view.setHtml(raw_html)
fig_view.setUrl(QUrl('temp-plot.html'))
fig_view.show()
fig_view.raise_()
return fig_view
if __name__ == '__main__':
app = QApplication(sys.argv)
# Working small plot:
# fig = go.Figure(data=[{'type': 'scattergl', 'y': [2, 1, 3, 1]}])
# Not working large plot:
t = np.arange(0, 200000, 1)
y = np.sin(t/20000)
fig = go.Figure(data=[{'type': 'scattergl', 'y': y}])
# po.plot(fig)
fig_view = show_qt(fig)
sys.exit(app.exec_())
The plot was generated with:
import numpy as np
import plotly.offline as po
import plotly.graph_objs as go
t = np.arange(0, 200000, 1)
y = np.sin(t/20000)
fig = go.Figure(data=[{'type': 'scattergl', 'y': y}])
po.plot(fig)
As this answer indicates a possible solution is to use a QWebEngineUrlSchemeHandler, in the next section I have created a class that allows you to register functions that are invoked through custom urls:
qtplotly.py
from PyQt5 import QtCore, QtWebEngineCore, QtWebEngineWidgets
import plotly.offline as po
import plotly.graph_objs as go
class PlotlySchemeHandler(QtWebEngineCore.QWebEngineUrlSchemeHandler):
def __init__(self, app):
super().__init__(app)
self.m_app = app
def requestStarted(self, request):
url = request.requestUrl()
name = url.host()
if self.m_app.verify_name(name):
fig = self.m_app.fig_by_name(name)
if isinstance(fig, go.Figure):
raw_html = '<html><head><meta charset="utf-8" />'
raw_html += '<script src="https://cdn.plot.ly/plotly-latest.min.js"></script></head>'
raw_html += "<body>"
raw_html += po.plot(fig, include_plotlyjs=False, output_type="div")
raw_html += "</body></html>"
buf = QtCore.QBuffer(parent=self)
request.destroyed.connect(buf.deleteLater)
buf.open(QtCore.QIODevice.WriteOnly)
buf.write(raw_html.encode())
buf.seek(0)
buf.close()
request.reply(b"text/html", buf)
return
request.fail(QtWebEngineCore.QWebEngineUrlRequestJob.UrlNotFound)
class PlotlyApplication(QtCore.QObject):
scheme = b"plotly"
def __init__(self, parent=None):
super().__init__(parent)
scheme = QtWebEngineCore.QWebEngineUrlScheme(PlotlyApplication.scheme)
QtWebEngineCore.QWebEngineUrlScheme.registerScheme(scheme)
self.m_functions = dict()
def init_handler(self, profile=None):
if profile is None:
profile = QtWebEngineWidgets.QWebEngineProfile.defaultProfile()
handler = profile.urlSchemeHandler(PlotlyApplication.scheme)
if handler is not None:
profile.removeUrlSchemeHandler(handler)
self.m_handler = PlotlySchemeHandler(self)
profile.installUrlSchemeHandler(PlotlyApplication.scheme, self.m_handler)
def verify_name(self, name):
return name in self.m_functions
def fig_by_name(self, name):
return self.m_functions.get(name, lambda: None)()
def register(self, name):
def decorator(f):
self.m_functions[name] = f
return f
return decorator
def create_url(self, name):
url = QtCore.QUrl()
url.setScheme(PlotlyApplication.scheme.decode())
url.setHost(name)
return url
main.py
import numpy as np
import plotly.graph_objs as go
from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets
from qtplotly import PlotlyApplication
# PlotlyApplication must be created before the creation
# of QGuiApplication or QApplication
plotly_app = PlotlyApplication()
#plotly_app.register("scatter")
def scatter():
t = np.arange(0, 200000, 1)
y = np.sin(t / 20000)
fig = go.Figure(data=[{"type": "scattergl", "y": y}])
return fig
#plotly_app.register("scatter2")
def scatter2():
N = 100000
r = np.random.uniform(0, 1, N)
theta = np.random.uniform(0, 2 * np.pi, N)
fig = go.Figure(
data=[
{
"type": "scattergl",
"x": r * np.cos(theta),
"y": r * np.sin(theta),
"marker": dict(color=np.random.randn(N), colorscale="Viridis"),
}
]
)
return fig
#plotly_app.register("scatter3")
def scatter3():
x0 = np.random.normal(2, 0.45, 30000)
y0 = np.random.normal(2, 0.45, 30000)
x1 = np.random.normal(6, 0.4, 20000)
y1 = np.random.normal(6, 0.4, 20000)
x2 = np.random.normal(4, 0.3, 20000)
y2 = np.random.normal(4, 0.3, 20000)
traces = []
for x, y in ((x0, y0), (x1, y1), (x2, y2)):
trace = go.Scatter(x=x, y=y, mode="markers")
traces.append(trace)
fig = go.Figure(data=traces)
return fig
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.m_view = QtWebEngineWidgets.QWebEngineView()
combobox = QtWidgets.QComboBox()
combobox.currentIndexChanged[str].connect(self.onCurrentIndexChanged)
combobox.addItems(["scatter", "scatter2", "scatter3"])
vlay = QtWidgets.QVBoxLayout(self)
hlay = QtWidgets.QHBoxLayout()
hlay.addWidget(QtWidgets.QLabel("Select:"))
hlay.addWidget(combobox)
vlay.addLayout(hlay)
vlay.addWidget(self.m_view)
self.resize(640, 480)
#QtCore.pyqtSlot(str)
def onCurrentIndexChanged(self, name):
self.m_view.load(plotly_app.create_url(name))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
# Init_handler must be invoked before after the creation
# of QGuiApplication or QApplication
plotly_app.init_handler()
w = Widget()
w.show()
sys.exit(app.exec_())
Structure:
├── main.py
└── qtplotly.py
Output:
I need to implement quite tiny pyqtgraph plots in a GUI. If doing so, by default the axis' label offset is too large. How can I set the offset of the axis label, not the axis ticks.
The following code example creates a basic pyqtgraph plot. I was able to set the offset of the tick text but not the offset of the label text only. I would like to only get the axis labels closer to the axis.
import numpy as np
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
app = QtGui.QApplication([])
x = np.linspace(0, 1, 10000)
y = np.linspace(350, 2500, 10000)
win = pg.GraphicsWindow()
plot = win.addPlot(x=x, y=y, title="Plot")
label_style = {'color': '#EEE', 'font-size': '14pt'}
plot.setLabel('bottom', "some x axis label", **label_style)
plot.setLabel('left', "some y axis label")
plot.getAxis('left').setLabel(**label_style)
font=QtGui.QFont()
font.setPixelSize(14)
plot.getAxis("bottom").tickFont = font
# Here I increased the tickTextOffset of the x axis
plot.getAxis("bottom").setStyle(tickTextOffset=50)
plot.getAxis("left").tickFont = font
plot.getAxis("left").setStyle(tickTextOffset=14)
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
Any help is much appreciated!
Update:
I found a pyqtgraph internal solution in pyqtgraph.AxisItem.resizeEvent() but the function does not accept any passed arguments.
def resizeEvent(self, ev=None):
#s = self.size()
## Set the position of the label
nudge = 5
br = self.label.boundingRect()
p = QtCore.QPointF(0, 0)
if self.orientation == 'left':
p.setY(int(self.size().height()/2 + br.width()/2))
p.setX(-nudge)
elif self.orientation == 'right':
p.setY(int(self.size().height()/2 + br.width()/2))
p.setX(int(self.size().width()-br.height()+nudge))
elif self.orientation == 'top':
p.setY(-nudge)
p.setX(int(self.size().width()/2. - br.width()/2.))
elif self.orientation == 'bottom':
p.setX(int(self.size().width()/2. - br.width()/2.))
p.setY(int(self.size().height()-br.height()+nudge))
self.label.setPos(p)
self.picture = None
the corresponding variable is nudge. Unfortunately it is not accessible or is there a way to bypass resizeEvent() without changing the source code of pyqtgraph?
As a proposal to make nudge passable I created a pyqtgraph issue on
github:
https://github.com/pyqtgraph/pyqtgraph/issues/986
One solution is to create a Custom AxisItem and override that method. To call resizeEvent you can make false resizing:
import numpy as np
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
class CustomAxis(pg.AxisItem):
#property
def nudge(self):
if not hasattr(self, "_nudge"):
self._nudge = 5
return self._nudge
#nudge.setter
def nudge(self, nudge):
self._nudge = nudge
s = self.size()
# call resizeEvent indirectly
self.resize(s + QtCore.QSizeF(1, 1))
self.resize(s)
def resizeEvent(self, ev=None):
# s = self.size()
## Set the position of the label
nudge = self.nudge
br = self.label.boundingRect()
p = QtCore.QPointF(0, 0)
if self.orientation == "left":
p.setY(int(self.size().height() / 2 + br.width() / 2))
p.setX(-nudge)
elif self.orientation == "right":
p.setY(int(self.size().height() / 2 + br.width() / 2))
p.setX(int(self.size().width() - br.height() + nudge))
elif self.orientation == "top":
p.setY(-nudge)
p.setX(int(self.size().width() / 2.0 - br.width() / 2.0))
elif self.orientation == "bottom":
p.setX(int(self.size().width() / 2.0 - br.width() / 2.0))
p.setY(int(self.size().height() - br.height() + nudge))
self.label.setPos(p)
self.picture = None
app = QtGui.QApplication([])
x = np.linspace(0, 1, 10000)
y = np.linspace(350, 2500, 10000)
win = pg.GraphicsWindow()
plot = win.addPlot(
x=x, y=y, title="Plot", axisItems={"bottom": CustomAxis(orientation="bottom")}
)
label_style = {"color": "#EEE", "font-size": "14pt"}
plot.setLabel("bottom", "some x axis label", **label_style)
plot.setLabel("left", "some y axis label")
plot.getAxis("left").setLabel(**label_style)
font = QtGui.QFont()
font.setPixelSize(14)
plot.getAxis("bottom").tickFont = font
plot.getAxis("bottom").setStyle(tickTextOffset=50)
plot.getAxis("left").tickFont = font
plot.getAxis("left").setStyle(tickTextOffset=14)
def on_timeout():
plot.getAxis("bottom").nudge += 1
timer = QtCore.QTimer(timeout=on_timeout, interval=500)
timer.start()
if __name__ == "__main__":
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, "PYQT_VERSION"):
QtGui.QApplication.instance().exec_()
I'm doing my first tests with pyqtchart but becouse of the poor documentation about animated charts I've encountered some issuses. I built a chart that shows the 'sin', 'cos' and 'tan' functions (approximating the value of the tangent) and to make it live I've built a thread that clear and repaint the chart every time.
It works but I don't know if it's the correct way or the most efficent way to do it. I found an example hosted on github but it's not realy clear for me.
I'dont understand if this's the 'offical way' to do it or if pyqtgraph provides some built-in functions to automate it.
I will be realy greatful for anyone who could give me some advice.
This is my code:
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QThread, pyqtSignal
import pyqtgraph as pg
import math
import numpy as np
import sys
import time
class Gui(QWidget):
def __init__(self):
super().__init__()
self.setupUI()
def setupUI(self):
pg.setConfigOption('background', 0.95)
pg.setConfigOptions(antialias=True)
self.plot = pg.PlotWidget()
self.plot.setAspectLocked(lock=True, ratio=0.01)
self.plot.setYRange(-3, 3)
self.widget_layout = QVBoxLayout()
self.widget_layout.addWidget(self.plot)
self.setLayout(self.widget_layout)
def plot_data(self, data):
self.plot.clear()
self.plot.plot(range(0, 720), data[0], pen=pg.mkPen(color='g', width=2))
self.plot.plot(range(0, 720), data[1], pen=pg.mkPen(color='r', width=2))
self.plot.plot(range(0, 720), data[2], pen=pg.mkPen(color='y', width=2))
class Thread(QThread):
sig_plot = pyqtSignal(list)
def __init__(self):
super().__init__()
self.sig_plot.connect(gui.plot_data)
def run(self):
sin_func = np.empty(720)
cos_func = np.empty(720)
tan_func = np.empty(720)
cont = 0
while True:
indx = 0
for ang in range(cont, cont + 720):
rad = math.radians(ang)
cos = math.cos(rad)
sin = math.sin(rad)
if cos != 0: tan = sin / cos
else: tan = sin / 0.00000000001
sin_func[indx] = sin
cos_func[indx] = cos
if tan >= -3 and tan <= 3: tan_func[indx] = tan
else: tan_func[indx] = np.NaN
indx += 1
data = [sin_func, cos_func, tan_func]
self.sig_plot.emit(data)
time.sleep(0.01)
if cont == 720: cont = 0
else: cont += 1
if __name__ == '__main__':
app = QApplication(sys.argv)
gui = Gui()
gui.show()
thread = Thread()
thread.start()
sys.exit(app.exec_())
There is no official way to make animations in pyqtgraph, but the one you sample is not the best because the threads in a GUI are only necessary when there is a heavy task but the task of creating the arrays is not, another mistake is to clean and create the plots, in these cases it is better to reuse. And finally it is better to use the power of calculation of numpy at the level of matrices and arrays than to make a loop.
Considering the above I have implemented a class that calls the function generate_data every certain interval of time with the appropriate index and generates an infinite loop with the help of a QTimer.
from PyQt5 import QtCore, QtWidgets
import pyqtgraph as pg
import numpy as np
class TimeLine(QtCore.QObject):
frameChanged = QtCore.pyqtSignal(int)
def __init__(self, interval=60, loopCount=1, parent=None):
super(TimeLine, self).__init__(parent)
self._startFrame = 0
self._endFrame = 0
self._loopCount = loopCount
self._timer = QtCore.QTimer(self, timeout=self.on_timeout)
self._counter = 0
self._loop_counter = 0
self.setInterval(interval)
def on_timeout(self):
if self._startFrame <= self._counter < self._endFrame:
self.frameChanged.emit(self._counter)
self._counter += 1
else:
self._counter = 0
self._loop_counter += 1
if self._loopCount > 0:
if self._loop_counter >= self.loopCount():
self._timer.stop()
def setLoopCount(self, loopCount):
self._loopCount = loopCount
def loopCount(self):
return self._loopCount
interval = QtCore.pyqtProperty(int, fget=loopCount, fset=setLoopCount)
def setInterval(self, interval):
self._timer.setInterval(interval)
def interval(self):
return self._timer.interval()
interval = QtCore.pyqtProperty(int, fget=interval, fset=setInterval)
def setFrameRange(self, startFrame, endFrame):
self._startFrame = startFrame
self._endFrame = endFrame
#QtCore.pyqtSlot()
def start(self):
self._counter = 0
self._loop_counter = 0
self._timer.start()
class Gui(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.setupUI()
def setupUI(self):
pg.setConfigOption('background', 0.95)
pg.setConfigOptions(antialias=True)
self.plot = pg.PlotWidget()
self.plot.setAspectLocked(lock=True, ratio=0.01)
self.plot.setYRange(-3, 3)
widget_layout = QtWidgets.QVBoxLayout(self)
widget_layout.addWidget(self.plot)
self._plots = [self.plot.plot([], [], pen=pg.mkPen(color=color, width=2)) for color in ("g", "r", "y")]
self._timeline = TimeLine(loopCount=0, interval=10)
self._timeline.setFrameRange(0, 720)
self._timeline.frameChanged.connect(self.generate_data)
self._timeline.start()
def plot_data(self, data):
for plt, val in zip(self._plots, data):
plt.setData(range(len(val)), val)
#QtCore.pyqtSlot(int)
def generate_data(self, i):
ang = np.arange(i, i + 720)
cos_func = np.cos(np.radians(ang))
sin_func = np.sin(np.radians(ang))
tan_func = sin_func/cos_func
tan_func[(tan_func < -3) | (tan_func > 3)] = np.NaN
self.plot_data([sin_func, cos_func, tan_func])
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
gui = Gui()
gui.show()
sys.exit(app.exec_())
How can I generate a plot with two Y-scales in pyqtgraph?
I also need the two in different colors (corresponding to lines' colors).
In matplotlib it can be done using twinx, as in this example.
If there's no way to do it with a single plot object, perhaps there's a way to overlay a plot (with y-axis on right side) on another one (with the y-axis on left)?
See pyqtgraph/examples/MultiplePlotAxes.py.
The solution is just what you described--overlay two PlotItems.
Here is some code that I think shows a practical example of what it is you are after. This is an expansion of two pyqtgraph examples: PlotSpeedTest.py and MultiplePlotAxes.py.
from pyqtgraph.Qt import QtGui, QtCore
import numpy as np
import pyqtgraph as pg
pg.setConfigOptions(antialias=True)
pg.setConfigOption('background', '#c7c7c7')
pg.setConfigOption('foreground', '#000000')
from pyqtgraph.ptime import time
app = QtGui.QApplication([])
p = pg.plot()
p.setXRange(0,10)
p.setYRange(-10,10)
p.setWindowTitle('Current-Voltage')
p.setLabel('bottom', 'Bias', units='V', **{'font-size':'20pt'})
p.getAxis('bottom').setPen(pg.mkPen(color='#000000', width=3))
p.setLabel('left', 'Current', units='A',
color='#c4380d', **{'font-size':'20pt'})
p.getAxis('left').setPen(pg.mkPen(color='#c4380d', width=3))
curve = p.plot(x=[], y=[], pen=pg.mkPen(color='#c4380d'))
p.showAxis('right')
p.setLabel('right', 'Dynamic Resistance', units="<font>Ω</font>",
color='#025b94', **{'font-size':'20pt'})
p.getAxis('right').setPen(pg.mkPen(color='#025b94', width=3))
p2 = pg.ViewBox()
p.scene().addItem(p2)
p.getAxis('right').linkToView(p2)
p2.setXLink(p)
p2.setYRange(-10,10)
curve2 = pg.PlotCurveItem(pen=pg.mkPen(color='#025b94', width=1))
p2.addItem(curve2)
def updateViews():
global p2
p2.setGeometry(p.getViewBox().sceneBoundingRect())
p2.linkedViewChanged(p.getViewBox(), p2.XAxis)
updateViews()
p.getViewBox().sigResized.connect(updateViews)
x = np.arange(0, 10.01,0.01)
data = 5+np.sin(30*x)
data2 = -5+np.cos(30*x)
ptr = 0
lastTime = time()
fps = None
def update():
global p, x, curve, data, curve2, data2, ptr, lastTime, fps
if ptr < len(x):
curve.setData(x=x[:ptr], y=data[:ptr])
curve2.setData(x=x[:ptr], y=data2[:ptr])
ptr += 1
now = time()
dt = now - lastTime
lastTime = now
if fps is None:
fps = 1.0/dt
else:
s = np.clip(dt*3., 0, 1)
fps = fps * (1-s) + (1.0/dt) * s
p.setTitle('%0.2f fps' % fps)
else:
ptr = 0
app.processEvents() ## force complete redraw for every plot. Try commenting out to see if a different in speed occurs.
timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(0)
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()