I have a customized Text object, which has the same logic as the following simplified object:
import matplotlib.pyplot as plt
from matplotlib.text import Text
class MyText(Text):
def __init__(self, x, y, txt, height, **kwargs):
super().__init__(x, y, txt, **kwargs)
self.height = height
def mydraw(self, ax):
txt = ax.add_artist(self)
myset_fontsize(txt, self.height)
return self
def set_height(self, height):
self.height = height
#myset_fontsize(self, height)
def myset_fontsize(txtobj, height):
trans = txtobj.get_transform()
pixels, _ = trans.transform((txtobj.height, 0)) - trans.transform((0,0))
dpi = txtobj.axes.get_figure().get_dpi()
points = pixels / dpi * 72
txtobj.set_fontsize(points)
if __name__ == '__main__':
fig, ax = plt.subplots()
ax.grid(True)
txt = MyText(0.2, 0.2, 'hello', 0.1)
txt.mydraw(ax)
MyText is different from the built-in Text in that the fontsize is dependent on the height, which, for example, specifies the height of the text in the data coordinates. Except this, MyText is almost the same as Text. The example code gives the following figure:
This works fine for a static image. However, I want MyTest to be interactive, which includes the following goals:
In an interactive plot mode, txt.set_height(0.5) shoule change the fontsize dynamically. I know I can add a snippet as the comment shows, but if MyText object is not added to the axes, txt.set_height(0.5) will throw an AttributeError. In short, txt.set_height() should behave similarly to txt.set_fontsize().
When the figure is resized by dragging the plot window, MyText should change the fontsize accordingly, that is, the height of text in the data coordinates should keep the same. But currently the fontsize is unchanged when resizing the figure. I have found this answer, but mpl_connect needs some way to get the Figure object, and I want MyText interactive after calling txt.mydraw(ax).
When I change the aspect ratio of the figure, MyText should change the fontsize accordingly, same as the second point.
Thanks for any ideas!
If you only need to change the font from the window size.
Installed an event handler triggered by resizing the window.
Fonts size-bound to one side of the window size(in this case to the width).
import matplotlib.pyplot as plt
from matplotlib.text import Text
class MyText(Text):
def __init__(self, x, y, txt, height, **kwargs):
super().__init__(x, y, txt, **kwargs)
self.height = height
self.event_text = fig.canvas.mpl_connect('resize_event', self.mysize)
def mysize(event, ax):
fig = plt.gcf()
size_ = fig.get_size_inches()
txt.set_fontsize(size_[0]*5)
def mydraw(self, ax):
txt = ax.add_artist(self)
myset_fontsize(txt, self.height)
return self
def set_height(self, height):
self.height = height
# myset_fontsize(self, height)
def myset_fontsize(txtobj, height):
trans = txtobj.get_transform()
pixels, _ = trans.transform((txtobj.height, 0)) - trans.transform((0, 0))
dpi = txtobj.axes.get_figure().get_dpi()
points = pixels / dpi * 72
txtobj.set_fontsize(points)
if __name__ == '__main__':
fig, ax = plt.subplots()
ax.grid(True)
txt = MyText(0.2, 0.2, 'hello', 0.1)
txt.mydraw(ax)
plt.show()
After reading the user guide, I found that every artist has a stale attribite, which is some signal to re-render the figure. The complete solution is as follows:
import matplotlib.pyplot as plt
from matplotlib.text import Text
class MyText(Text):
def __init__(self, x, y, txt, height, **kwargs):
super().__init__(x, y, txt, **kwargs)
self.height = height
def __call__(self, event):
# When calling myset_fontsize, `self.stale` will be `True` due to `self.set_fontsize()` in the function body.
myset_fontsize(self, self.height)
def mydraw(self, ax):
txt = ax.add_artist(self)
# Connect "draw_event" so that once a draw event happens, a new fontsize is calculated and mark the `Text` object is stale.
ax.get_figure().canvas.mpl_connect('draw_event', self)
return txt
def set_height(self, height):
self.height = height
# When a new height is set, then the
#`Text` object is stale, which will
# forward the signal of re-rendering
# the figure to its parent.
self.stale = True
def myset_fontsize(txtobj, height):
trans = txtobj.get_transform()
pixels, _ = trans.transform((txtobj.height, 0)) - trans.transform((0,0))
dpi = txtobj.axes.get_figure().get_dpi()
points = pixels / dpi * 72
txtobj.set_fontsize(points)
This solution almost solves my problem although it's not perfect. It's a little inefficient.
Any improvements are appreciated.
Related
I am trying to improve my plotting function. I want to plot data using my plotGraph function coming from an EEG board in real-time, pulling samples from an LSL # 250Hz. Previously, I had a functional version using the regular self.ax.plot(x,y), clearing the data with self.ax.clear() every time the plot needed to refresh. Nonetheless, some profiling showed that my code was taking way too much time to plot in comparison to the rest of it.
One of the suggestions I got was to use set_data instead of plot and clear. I have multiple lines of data that I want to plot simultaneously, so I tried following Matplotlib multiple animate multiple lines, which you can see below (adapted code). Also, I was told to use self.figure.canvas.draw_idle(), which I tried, but I'm not sure if I did it correctly.
Unfortunately, it didn't work, the graph is not updating and I can't seem to find why. I'm aware that the source I just mentioned uses animation.FuncAnimation but I'm not sure that would be the problem. Is it?
Any ideas of why none of my lines are showing in my canvas' graph?
import tkinter as tk
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np
class AppWindow:
def plotGraph(self, x, y):
for lnum,line in enumerate(self.lines):
line.set_data(x[:], y[:, lnum])
self.figure.canvas.draw_idle()
plt.ylabel('Magnitude', fontsize = 9, color = tx_color)
plt.xlabel('Freq', fontsize = 9, color = tx_color)
self.figure.canvas.draw()
def __init__(self):
self.root = tk.Tk() #start of application
self.canvas = tk.Canvas(self.root, height = 420, width = 780, bg =
bg_color, highlightthickness=0)
self.canvas.pack(fill = 'both', expand = True)
self.figure = plt.figure(figsize = (5,6), dpi = 100)
self.figure.patch.set_facecolor(sc_color)
self.ax = self.figure.add_subplot(111)
self.ax.clear()
self.line, = self.ax.plot([], [], lw=1, color = tx_color)
self.line.set_data([],[])
#place graph
self.chart_type = FigureCanvasTkAgg(self.figure, self.canvas)
self.chart_type.get_tk_widget().pack()
self.lines = []
numchan = 8 #let's say I have 8 channels
for index in range(numchan):
lobj = self.ax.plot([],[], lw=2, color=tx_color)[0]
self.lines.append(lobj)
for line in self.lines:
line.set_data([],[])
def start(self):
self.root.mainloop()
You chart is empty because you are plotting empty arrays:
line.set_data([],[])
If you fill in the line arrays, the chart plots correctly.
Try this code. It updates the chart with new random data every second.
import tkinter as tk
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np
import random
bg_color='grey'
tx_color='green'
sc_color='linen'
numchan = 8
chlen = 100
xvals=[(x-40)/20 for x in range(chlen)] # X coordinates
chcolors= ['gold','blue','green','maroon','red','brown','purple','cyan']
class AppWindow:
def plotGraph(self):
self.figure.canvas.draw_idle()
plt.ylabel('Magnitude', fontsize = 9, color = tx_color)
plt.xlabel('Freq', fontsize = 9, color = tx_color)
self.figure.canvas.draw()
def UpdateChannelData(self): # callback with new data
# fake random data
for i,ch in enumerate(self.chdata):
for p in range(len(ch)):
ch[p] += (random.random()-.5)/100
self.lines[i].set_data(xvals, ch)
self.plotGraph()
self.root.after(100, self.UpdateChannelData) # simulate next call
def __init__(self):
global chzero
self.root = tk.Tk() #start of application
self.canvas = tk.Canvas(self.root, height = 420, width = 780, bg = bg_color, highlightthickness=0)
self.canvas.pack(fill = 'both', expand = True)
self.figure = plt.figure(figsize = (5,6), dpi = 100)
self.figure.patch.set_facecolor(sc_color)
self.ax = self.figure.add_subplot(111)
self.ax.clear()
self.line, = self.ax.plot([], [], lw=1, color = tx_color)
self.line.set_data([],[])
#place graph
self.chart_type = FigureCanvasTkAgg(self.figure, self.canvas)
self.chart_type.get_tk_widget().pack()
self.lines = []
#numchan = 8 #let's say I have 8 channels
for index in range(numchan):
lobj = self.ax.plot([],[], lw=1, color=chcolors[index])[0]
self.lines.append(lobj)
# set flat data
self.chdata = [[0 for x in range(chlen)] for ch in range(numchan)]
self.root.after(1000, self.UpdateChannelData) # start data read
def start(self):
self.root.mainloop()
AppWindow().start()
Output:
I've got a live matplotlib graph in a PyQt5 window:
You can read more about how I got this code working here:
How to make a fast matplotlib live plot in a PyQt5 GUI
Please copy-paste the code below to a python file, and run it with Python 3.7:
#####################################################################################
# #
# PLOT A LIVE GRAPH IN A PYQT WINDOW #
# #
#####################################################################################
from __future__ import annotations
from typing import *
import sys
import os
from PyQt5 import QtWidgets, QtCore
from matplotlib.backends.backend_qt5agg import FigureCanvas
import matplotlib as mpl
import matplotlib.figure as mpl_fig
import matplotlib.animation as anim
import matplotlib.style as style
import numpy as np
style.use('ggplot')
class ApplicationWindow(QtWidgets.QMainWindow):
'''
The PyQt5 main window.
'''
def __init__(self):
super().__init__()
# 1. Window settings
self.setGeometry(300, 300, 800, 400)
self.setWindowTitle("Matplotlib live plot in PyQt")
self.frm = QtWidgets.QFrame(self)
self.frm.setStyleSheet("QWidget { background-color: #eeeeec; }")
self.lyt = QtWidgets.QVBoxLayout()
self.frm.setLayout(self.lyt)
self.setCentralWidget(self.frm)
# 2. Place the matplotlib figure
self.myFig = MyFigureCanvas(x_len=200, y_range=[0, 100], interval=20)
self.lyt.addWidget(self.myFig)
# 3. Show
self.show()
return
class MyFigureCanvas(FigureCanvas, anim.FuncAnimation):
'''
This is the FigureCanvas in which the live plot is drawn.
'''
def __init__(self, x_len:int, y_range:List, interval:int) -> None:
'''
:param x_len: The nr of data points shown in one plot.
:param y_range: Range on y-axis.
:param interval: Get a new datapoint every .. milliseconds.
'''
FigureCanvas.__init__(self, mpl_fig.Figure())
# Range settings
self._x_len_ = x_len
self._y_range_ = y_range
# Store two lists _x_ and _y_
x = list(range(0, x_len))
y = [0] * x_len
# Store a figure and ax
self._ax_ = self.figure.subplots()
self._ax_.set_ylim(ymin=self._y_range_[0], ymax=self._y_range_[1])
self._line_, = self._ax_.plot(x, y)
# Call superclass constructors
anim.FuncAnimation.__init__(self, self.figure, self._update_canvas_, fargs=(y,), interval=interval, blit=True)
return
def _update_canvas_(self, i, y) -> None:
'''
This function gets called regularly by the timer.
'''
y.append(round(get_next_datapoint(), 2)) # Add new datapoint
y = y[-self._x_len_:] # Truncate list _y_
self._line_.set_ydata(y)
# Print size of bounding box (in pixels)
bbox = self.figure.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
width, height = bbox.width * self.figure.dpi, bbox.height * self.figure.dpi
print(f"bbox size in pixels = {width} x {height}")
return self._line_,
# Data source
# ------------
n = np.linspace(0, 499, 500)
d = 50 + 25 * (np.sin(n / 8.3)) + 10 * (np.sin(n / 7.5)) - 5 * (np.sin(n / 1.5))
i = 0
def get_next_datapoint():
global i
i += 1
if i > 499:
i = 0
return d[i]
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
qapp.exec_()
1. The problem
I need to know the nr of pixels from x_min to x_max:
Please notice that the x-axis actually goes beyond the x_min and x_max borders. I don't need to know the total length. Just the length from x_min to x_max.
2. What I tried so far
I already found a way to get the graph's bounding box. Notice the following codelines in the _update_canvas_() function:
# Print size of bounding box (in pixels)
bbox = self.figure.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
width, height = bbox.width * self.figure.dpi, bbox.height * self.figure.dpi
print(f"bbox size in pixels = {width} x {height}")
That gave me a bounding box size of 778.0 x 378.0 pixels. It's a nice starting point, but I don't know how to proceed from here.
I also noticed that this bounding box size isn't printed out correctly from the first go. The first run of the _update_canvas_() function prints out a bouding box of 640.0 x 480.0 pixels, which is just plain wrong. From the second run onwards, the printed size is correct. Why?
Edit
I tried two solutions. The first one is based on a method described by #ImportanceOfBeingErnes (see Axes class - set explicitly size (width/height) of axes in given units) and the second one is based on the answer from #Eyllanesc.
#####################################################################################
# #
# PLOT A LIVE GRAPH IN A PYQT WINDOW #
# #
#####################################################################################
from __future__ import annotations
from typing import *
import sys
import os
from PyQt5 import QtWidgets, QtCore
from matplotlib.backends.backend_qt5agg import FigureCanvas
import matplotlib as mpl
import matplotlib.figure as mpl_fig
import matplotlib.animation as anim
import matplotlib.style as style
import numpy as np
style.use('ggplot')
def get_width_method_a(ax, dpi, canvas):
l = float(ax.figure.subplotpars.left)
r = float(ax.figure.subplotpars.right)
x, y, w, h = ax.figure.get_tightbbox(renderer=canvas.get_renderer()).bounds
return float(dpi) * float(w - (l + r))
def get_width_eyllanesc(ax):
""" Based on answer from #Eyllanesc"""
""" See below """
y_fake = 0
x_min, x_max = 0, 200
x_pixel_min, _ = ax.transData.transform((x_min, y_fake))
x_pixel_max, _ = ax.transData.transform((x_max, y_fake))
return x_pixel_max - x_pixel_min
class ApplicationWindow(QtWidgets.QMainWindow):
'''
The PyQt5 main window.
'''
def __init__(self):
super().__init__()
# 1. Window settings
self.setGeometry(300, 300, 800, 400)
self.setWindowTitle("Matplotlib live plot in PyQt")
self.frm = QtWidgets.QFrame(self)
self.frm.setStyleSheet("QWidget { background-color: #eeeeec; }")
self.lyt = QtWidgets.QVBoxLayout()
self.frm.setLayout(self.lyt)
self.setCentralWidget(self.frm)
# 2. Place the matplotlib figure
self.myFig = MyFigureCanvas(x_len=200, y_range=[0, 100], interval=20)
self.lyt.addWidget(self.myFig)
# 3. Show
self.show()
return
class MyFigureCanvas(FigureCanvas, anim.FuncAnimation):
'''
This is the FigureCanvas in which the live plot is drawn.
'''
def __init__(self, x_len:int, y_range:List, interval:int) -> None:
'''
:param x_len: The nr of data points shown in one plot.
:param y_range: Range on y-axis.
:param interval: Get a new datapoint every .. milliseconds.
'''
FigureCanvas.__init__(self, mpl_fig.Figure())
# Range settings
self._x_len_ = x_len
self._y_range_ = y_range
# Store two lists _x_ and _y_
x = list(range(0, x_len))
y = [0] * x_len
# Store a figure and ax
self._ax_ = self.figure.subplots()
self._ax_.set_ylim(ymin=self._y_range_[0], ymax=self._y_range_[1])
self._line_, = self._ax_.plot(x, y)
self._line_.set_ydata(y)
print("")
print(f"width in pixels (first call, method is 'method_a') = {get_width_method_a(self._ax_, self.figure.dpi, self)}")
print(f"width in pixels (first call, method is 'eyllanesc') = {get_width_eyllanesc(self._ax_)}")
# Call superclass constructors
anim.FuncAnimation.__init__(self, self.figure, self._update_canvas_, fargs=(y,), interval=interval, blit=True)
return
def _update_canvas_(self, i, y) -> None:
'''
This function gets called regularly by the timer.
'''
y.append(round(get_next_datapoint(), 2)) # Add new datapoint
y = y[-self._x_len_:] # Truncate list _y_
self._line_.set_ydata(y)
print("")
print(f"width in pixels (method is 'method_a') = {get_width_method_a(self._ax_, self.figure.dpi, self)}")
print(f"width in pixels (method is 'eyllanesc') = {get_width_eyllanesc(self._ax_)}")
return self._line_,
# Data source
# ------------
n = np.linspace(0, 499, 500)
d = 50 + 25 * (np.sin(n / 8.3)) + 10 * (np.sin(n / 7.5)) - 5 * (np.sin(n / 1.5))
i = 0
def get_next_datapoint():
global i
i += 1
if i > 499:
i = 0
return d[i]
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
qapp.exec_()
Conclusions:
The correct answer is 550 pixels, which is what I measured on a printscreen. Now, I get the following output printed when I run the program:
width in pixels (first call, method is 'method_a') = 433.0972222222222
width in pixels (first call, method is 'eyllanesc') = 453.1749657377798
width in pixels (method is 'method_a') = 433.0972222222222
width in pixels (method is 'eyllanesc') = 453.1749657377798
width in pixels (method is 'method_a') = 540.0472222222223
width in pixels (method is 'eyllanesc') = 550.8908177249887
...
The first call for both methods gives the wrong result.
From the third(!) call onwards, they both give pretty good results, with the method from #Eyllanesc being the winner.
How do I fix the problem of the wrong result for the first call?
For an old answer I had to do calculation, which in your case is:
y_fake = 0
x_min, x_max = 0, 200
x_pixel_min, _ = self._ax_.transData.transform((x_min, y_fake))
x_pixel_max, _ = self._ax_.transData.transform((x_max, y_fake))
print(
f"The length in pixels between x_min: {x_min} and x_max: {x_max} is: {x_pixel_max - x_pixel_min}"
)
Note:
The calculations take into account what is painted, so in the first moments it is still being painted so the results are correct but our eyes cannot distinguish them. If you want to obtain the correct size without the animation you must calculate that value when the painting is stabilized, which is difficult to calculate, a workaround is to use a QTimer to make the measurement a moment later:
# ...
self._ax_ = self.figure.subplots()
self._ax_.set_ylim(ymin=self._y_range_[0], ymax=self._y_range_[1])
self._line_, = self._ax_.plot(x, y)
QtCore.QTimer.singleShot(100, self.calculate_length)
# ...
def calculate_length(self):
y_fake = 0
x_min, x_max = 0, 200
x_pixel_min, _ = self._ax_.transData.transform((x_min, y_fake))
x_pixel_max, _ = self._ax_.transData.transform((x_max, y_fake))
print(
f"The length in pixels between x_min: {x_min} and x_max: {x_max} is: {x_pixel_max - x_pixel_min}"
)
Is it possible to make chaco plot automatically show full output and not hiding the parts of ticks and labels? E.g. this is the output of standard example:
from chaco.api import ArrayPlotData, Plot
from enable.component_editor import ComponentEditor
from traits.api import HasTraits, Instance
from traitsui.api import View, Item
class MyPlot(HasTraits):
plot = Instance(Plot)
traits_view = View(Item('plot', editor = ComponentEditor(), show_label = False),
width = 500, height = 500, resizable = True)
def __init__(self, x, y, *args, **kw):
super(MyPlot, self).__init__(*args, **kw)
plotdata = ArrayPlotData(x=x,y=y)
plot = Plot(plotdata)
plot.plot(("x","y"), type = "line", color = "blue")
self.plot = plot
import numpy as np
x = np.linspace(-300,300,10000)
y = np.sin(x)*x**3
lineplot = MyPlot(x,y)
lineplot.configure_traits()
As you see the part of tick labels are hidden.. the only thing I can do is to manually adjust left padding of the plot. But this becomes extremely incovinient when you plot different data and different scales or fonts with the plot in application. Is it possible somehow to make padding automatically adjusted to include ALL related info?
UPD.: I've found ensure_labels_bounded property for the axis, but seems it has no effect.
Chaco does not support advanced layout features like these. If you use Chaco, you should use it for its speed, not for nice graphs or features. That being said, here's a version as close as I could get. It requires you to re-size the window with the mouse at least once for the padding correction to take place. Maybe you can find a way to refresh the window without having to manually resize it, I didn't have any luck with that. Anyways hope that gets you on the right track.
from chaco.api import ArrayPlotData, Plot
from enable.component_editor import ComponentEditor
from traits.api import HasTraits, Instance
from traitsui.api import View, Item
class MyPlot(HasTraits):
plot = Instance(Plot)
traits_view = View(Item('plot', editor = ComponentEditor(), show_label = False),
width = 500, height = 500, resizable = True)
def __init__(self, x, y, *args, **kw):
super(MyPlot, self).__init__(*args, **kw)
plotdata = ArrayPlotData(x=x,y=y)
plot = Plot(plotdata, padding=25)
plot.plot(("x","y"), type = "line", color = "blue", name='abc')
self.plot = plot
# watch for changes to the bounding boxes of the tick labels
self.plot.underlays[2].on_trait_change(self._update_size, '_tick_label_bounding_boxes')
self.plot.underlays[3].on_trait_change(self._update_size, '_tick_label_bounding_boxes')
def _update_size(self):
if len(self.plot.underlays[2]._tick_label_bounding_boxes) > 0:
self.plot.padding_bottom = int(np.amax(np.array(self.plot.underlays[2]._tick_label_bounding_boxes),0)[1]+8+4)
if len(self.plot.underlays[3]._tick_label_bounding_boxes) > 0:
self.plot.padding_left = int(np.amax(np.array(self.plot.underlays[3]._tick_label_bounding_boxes),0)[0]+8+4)
import numpy as np
x = np.linspace(-300,300,10000)
y = np.sin(x)*x**3
lineplot = MyPlot(x,y)
lineplot.configure_traits()
I have large time-traces that must be inspected visually, so I need a fast scrolling tool.
How can I achieve the fastest Maplotlib/Pyside scrolling?
Right know, I added a PySide scroll-bar to a MPL figure and update the x-range of the plot with set_xlim() method. This is not fast enough especially because in the final application I have at least 8 time-traces in different subplots that must all scroll together. A figure of the plot is attached.
Is there room for improvement?
Here I attach the demo code that demonstrate the relatively low scrolling. It's long but it's almost all boiler-plate code. The interesting bit (that needs improvement) is in xpos_changed() method where the plot xlimits are changed.
EDIT: Below I incorporated some micro-optimizations suggested by tcaswell, but the update speed is not improved.
from PySide import QtGui, QtCore
import pylab as plt
import numpy as np
N_SAMPLES = 1e6
def test_plot():
time = np.arange(N_SAMPLES)*1e-3
sample = np.random.randn(N_SAMPLES)
plt.plot(time, sample, label="Gaussian noise")
plt.title("1000s Timetrace \n (use the slider to scroll and the spin-box to set the width)")
plt.xlabel('Time (s)')
plt.legend(fancybox=True)
q = ScrollingToolQT(plt.gcf(), scroll_step=10)
return q # WARNING: it's important to return this object otherwise
# python will delete the reference and the GUI will not respond!
class ScrollingToolQT(object):
def __init__(self, fig, scroll_step=10):
# Setup data range variables for scrolling
self.fig = fig
self.scroll_step = scroll_step
self.xmin, self.xmax = fig.axes[0].get_xlim()
self.width = 1 # axis units
self.pos = 0 # axis units
self.scale = 1e3 # conversion betweeen scrolling units and axis units
# Save some MPL shortcuts
self.ax = self.fig.axes[0]
self.draw = self.fig.canvas.draw
#self.draw_idle = self.fig.canvas.draw_idle
# Retrive the QMainWindow used by current figure and add a toolbar
# to host the new widgets
QMainWin = fig.canvas.parent()
toolbar = QtGui.QToolBar(QMainWin)
QMainWin.addToolBar(QtCore.Qt.BottomToolBarArea, toolbar)
# Create the slider and spinbox for x-axis scrolling in toolbar
self.set_slider(toolbar)
self.set_spinbox(toolbar)
# Set the initial xlimits coherently with values in slider and spinbox
self.ax.set_xlim(self.pos,self.pos+self.width)
self.draw()
def set_slider(self, parent):
self.slider = QtGui.QSlider(QtCore.Qt.Horizontal, parent=parent)
self.slider.setTickPosition(QtGui.QSlider.TicksAbove)
self.slider.setTickInterval((self.xmax-self.xmin)/10.*self.scale)
self.slider.setMinimum(self.xmin*self.scale)
self.slider.setMaximum((self.xmax-self.width)*self.scale)
self.slider.setSingleStep(self.width*self.scale/4.)
self.slider.setPageStep(self.scroll_step*self.width*self.scale)
self.slider.setValue(self.pos*self.scale) # set the initial position
self.slider.valueChanged.connect(self.xpos_changed)
parent.addWidget(self.slider)
def set_spinbox(self, parent):
self.spinb = QtGui.QDoubleSpinBox(parent=parent)
self.spinb.setDecimals(3)
self.spinb.setRange(0.001,3600.)
self.spinb.setSuffix(" s")
self.spinb.setValue(self.width) # set the initial width
self.spinb.valueChanged.connect(self.xwidth_changed)
parent.addWidget(self.spinb)
def xpos_changed(self, pos):
#pprint("Position (in scroll units) %f\n" %pos)
pos /= self.scale
self.ax.set_xlim(pos, pos+self.width)
self.draw()
def xwidth_changed(self, width):
#pprint("Width (axis units) %f\n" % step)
if width <= 0: return
self.width = width
self.slider.setSingleStep(self.width*self.scale/5.)
self.slider.setPageStep(self.scroll_step*self.width*self.scale)
old_xlim = self.ax.get_xlim()
self.xpos_changed(old_xlim[0]*self.scale)
if __name__ == "__main__":
q = test_plot()
plt.show()
As requested in the comments, here is a pyqtgraph demo which scrolls two large traces together (via mouse).
The documentation isn't complete for the pyqtgraph project but there are some good examples you can view with python -m pyqtgraph.examples which should point you in the right direction. The crosshair.py example might be particularly interesting for you.
If you go with pyqtgraph, connect your slider widget to the setXRange method in the last line of this demo.
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
import numpy as np
app = QtGui.QApplication([])
win = pg.GraphicsWindow()
x = np.arange(1e5)
y1 = np.random.randn(x.size)
y2 = np.random.randn(x.size)
p1 = win.addPlot(x=x, y=y1, name='linkToMe')
p1.setMouseEnabled(x=True, y=False)
win.nextRow()
p2 = win.addPlot(x=x, y=y2)
p2.setXLink('linkToMe')
p1.setXRange(2000,3000)
This seems a bit faster/more responsive:
from PySide import QtGui, QtCore
import pylab as plt
import numpy as np
N_SAMPLES = 1e6
def test_plot():
time = np.arange(N_SAMPLES)*1e-3
sample = np.random.randn(N_SAMPLES)
plt.plot(time, sample, label="Gaussian noise")
plt.legend(fancybox=True)
plt.title("Use the slider to scroll and the spin-box to set the width")
q = ScrollingToolQT(plt.gcf())
return q # WARNING: it's important to return this object otherwise
# python will delete the reference and the GUI will not respond!
class ScrollingToolQT(object):
def __init__(self, fig):
# Setup data range variables for scrolling
self.fig = fig
self.xmin, self.xmax = fig.axes[0].get_xlim()
self.step = 1 # axis units
self.scale = 1e3 # conversion betweeen scrolling units and axis units
# Retrive the QMainWindow used by current figure and add a toolbar
# to host the new widgets
QMainWin = fig.canvas.parent()
toolbar = QtGui.QToolBar(QMainWin)
QMainWin.addToolBar(QtCore.Qt.BottomToolBarArea, toolbar)
# Create the slider and spinbox for x-axis scrolling in toolbar
self.set_slider(toolbar)
self.set_spinbox(toolbar)
# Set the initial xlimits coherently with values in slider and spinbox
self.set_xlim = self.fig.axes[0].set_xlim
self.draw_idle = self.fig.canvas.draw_idle
self.ax = self.fig.axes[0]
self.set_xlim(0, self.step)
self.fig.canvas.draw()
def set_slider(self, parent):
# Slider only support integer ranges so use ms as base unit
smin, smax = self.xmin*self.scale, self.xmax*self.scale
self.slider = QtGui.QSlider(QtCore.Qt.Horizontal, parent=parent)
self.slider.setTickPosition(QtGui.QSlider.TicksAbove)
self.slider.setTickInterval((smax-smin)/10.)
self.slider.setMinimum(smin)
self.slider.setMaximum(smax-self.step*self.scale)
self.slider.setSingleStep(self.step*self.scale/5.)
self.slider.setPageStep(self.step*self.scale)
self.slider.setValue(0) # set the initial position
self.slider.valueChanged.connect(self.xpos_changed)
parent.addWidget(self.slider)
def set_spinbox(self, parent):
self.spinb = QtGui.QDoubleSpinBox(parent=parent)
self.spinb.setDecimals(3)
self.spinb.setRange(0.001, 3600.)
self.spinb.setSuffix(" s")
self.spinb.setValue(self.step) # set the initial width
self.spinb.valueChanged.connect(self.xwidth_changed)
parent.addWidget(self.spinb)
def xpos_changed(self, pos):
#pprint("Position (in scroll units) %f\n" %pos)
# self.pos = pos/self.scale
pos /= self.scale
self.set_xlim(pos, pos + self.step)
self.draw_idle()
def xwidth_changed(self, xwidth):
#pprint("Width (axis units) %f\n" % step)
if xwidth <= 0: return
self.step = xwidth
self.slider.setSingleStep(self.step*self.scale/5.)
self.slider.setPageStep(self.step*self.scale)
old_xlim = self.ax.get_xlim()
self.xpos_changed(old_xlim[0] * self.scale)
# self.set_xlim(self.pos,self.pos+self.step)
# self.fig.canvas.draw()
if __name__ == "__main__":
q = test_plot()
plt.show()
I'm new to Python, I still have issues with the semantics of class inheritance.
The following is the relevant class from the module games.py module that I am importing:
class Text(Sprite):
"""
Alphanumeric values displayed on the screen.
"""
def __init__(self, value, size, color, angle=0,
x=0, y=0,
top=None, bottom=None, left=None, right=None,
dx=0, dy=0,
interval=1, is_collideable=True):
self._size = size
self._color = color
self._value = value
self._font = pygame.font.Font(None, self._size)
Sprite.__init__(self, self._create_surface(), angle,
x, y,
top, bottom, left, right,
dx, dy,
interval, is_collideable)
and the following is from where I'm trying to call it in my own program:
self.scorebox = games.Text (value = self.scorevar,
pygame.font.Font(ardarlingopentype, 50),
color = color.white,
x = 550,
y = 50)
As you can see the syntax is wrong, but how do I go about fixing this such that I can inherit the class Text from my own program and make FONT an accessible argument that I can change?
Thanks.
Your problem is, that you you are ordering the arguments incorrectly: there are positional and keyword arguments. All keywords arguments must succeed the positional arguments.
This would work:
self.scorebox = games.Text (
pygame.font.Font(ardarlingopentype, 50),
value = self.scorevar,
color = color.white,
x = 550,
y = 50
)
Not sure(note that you can't used not named arguments after named and/or mix them - you have used not named argument after 'value') but seems that you need to modify code the following way:
class Text(Sprite):
"""
Alphanumeric values displayed on the screen.
"""
def __init__(self, value, size, color, angle=0,
x=0, y=0,
top=None, bottom=None, left=None, right=None, font=None,
dx=0, dy=0,
interval=1, is_collideable=True):
self._size = size
self._color = color
self._value = value
if font:
self.font_ = font
else:
self._font = pygame.font.Font(None, self._size)
Sprite.__init__(self, self._create_surface(), angle,
x, y,
top, bottom, left, right,
dx, dy,
interval, is_collideable)
And then:
import pygame
import games
self.scorebox = games.Text (value = self.scorevar,
size = 50,
color = color.white,
x = 550,
y = 50)
OR:
import pygame
import games
self.scorebox = games.Text (value = self.scorevar,
size = 50,
font = pygame.font.Font(ardarlingopentype, 50),
color = color.white,
x = 550,
y = 50)
So guys I wrote to the developers of the Livewires package; and I was fortunate enough to receive a reply from one of them.
First, make a backup copy of games.py and put it somewhere safe. That
way if you do make a mistake, you can always recover the original
code.
Now our games.py is written on top of the PyGame library, which does
provide a way of setting the font. As you might have guessed, it's to
do with that line reading:
> self._font = pygame.font.Font(None, self._size)
The documentation is available online at
http://www.pygame.org/docs/ref/font.html#pygame.font.Font but I'll
just quickly summarise here. pygame.font.Font() creates a new PyGame
font object, which PyGame uses to tell it how to draw text. The
"None" parameter tells it to use the default font, but you can replace
that with the full name of a font file instead. The easiest way to do
that is to modify the Text classes initialiser to pass it as an
optional parameter.
class Text(Sprite):
def __init__(self, value, size, color, angle=0,
> x=0, y=0,
> top=None, bottom=None, left=None, right=None,
> dx=0, dy=0,
> interval=1, is_collideable=True,
> fontfile=None):
> self._size = size
> self._color = color
> self._value = value
> self._font = pygame.font.Font(fontfile, self._size)
> Sprite.__init__(self, self._create_surface(), angle,
> x, y,
> top, bottom, left, right,
> dx, dy,
> interval, is_collideable)
You would then create your Text object by calling 'Text(blah blah
blah, fontfile="/some/font/file/name.ttf")' or whatever the filename
is. Any other Text objects that don't specify a "fontfile" will
automatically use "None" instead, which will give them the default
font exactly as before.
So what's the fully-qualified pathname of the font file for
"TimesNewRoman"? I have no idea what it would be on your computer.
Fortunately PyGame provides a way of not having to know:
pygame.font.match_font(). You can use that in your own program
(rather than modifying games.py any more), but you will have to either
"import pygame.font" for yourself or call it
"games.pygame.font.match_font()" -- either should work equally well.