How to add custom AxisItem to existing PlotWidget? - python

I'm trying to add custom AxisItem in pyqtgraph to existing PlotWidget that was generated by Qt Designer. There is related topic here, but there is no exact answer with code example and I cannot comment, so I've created a new topic.
This is my custom AxisItem (based on this code):
import pyqtgraph as pg
import datetime
def int2td(ts):
return(datetime.timedelta(seconds=float(ts)/1e6))
class TimeAxisItem(pg.AxisItem):
def __init__(self, *args, **kwargs):
super(TimeAxisItem, self).__init__(*args, **kwargs)
def tickStrings(self, values, scale, spacing):
return [int2dt(value).strftime("%H:%M:%S") for value in values]
This is my main QtPlotter class:
from pyqtgraph.Qt import QtGui
from template_pyqt import Ui_Form # Ui_Form is generated by Qt Designer
class QtPlotter:
def __init__(self):
self.app = QtGui.QApplication([])
self.win = QtGui.QWidget()
self.ui = Ui_Form()
self.ui.setupUi(self.win)
self.win.show()
self.ui_plot = self.ui.plot
self.ui_plot.showGrid(x=True, y=True)
And then I'm trying to add my custom AxisItem:
self.ui_plot.getPlotItem().axes['bottom']['item'] = TimeAxisItem(orientation='bottom')
I have no errors, but this does not give any effect.

It was so easy. The PlotItem class does not have a setAxis method. Instead of that there is a method named setAxisItems. this code worked for me.
date_axis = pg.graphicsItems.DateAxisItem.DateAxisItem(orientation = 'bottom')
self.mainSoundPlot.setAxisItems(axisItems = {'bottom': date_axis})

I know this is an old post but I am encountering a similar issue and came across a solution in this gist example, which is an AxisItem subclass that implements an attachToPlotItem method.
So regarding the original post, instead of
self.ui_plot.getPlotItem().axes['bottom']['item'] = TimeAxisItem(orientation='bottom')
you need the following method (copied from the example linked above) in your TimeAxisItem subclass:
def attachToPlotItem(self, plotItem):
"""Add this axis to the given PlotItem
:param plotItem: (PlotItem)
"""
self.setParentItem(plotItem)
viewBox = plotItem.getViewBox()
self.linkToView(viewBox)
self._oldAxis = plotItem.axes[self.orientation]['item']
self._oldAxis.hide()
plotItem.axes[self.orientation]['item'] = self
pos = plotItem.axes[self.orientation]['pos']
plotItem.layout.addItem(self, *pos)
self.setZValue(-1000)
It even retains the original AxisItem so that it could be re-instated if needed.
Then in your QtPlotter constructor, you'd add
axis = TimeAxisItem(orientation='bottom')
axis.attactToPlotItem(self.ui_plot.getPlotItem())

The PlotItem class does not have a setAxis method, only a getAxis method to get the current axis. You can specify a dictionary with axis items in the axisItems parameter of the PlotItem constructor, but it doesn't seem possible to update an AxisItem of an existing PlotItem.
Even though the PlotItem.axes dictionary is a public attribute, it is undocumented and just using it is therefore a bit risky. There is a possibility that the author of PyQtGraph will alter its behavior, or rename it (although the chance of this happening is small). IMHO it should have been made a private attribute.
In any case, by looking at the source of the PlotItem constructor you can see how the axis items from axisItem parameter are added to the plot item:
## Create and place axis items
if axisItems is None:
axisItems = {}
self.axes = {}
for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))):
if k in axisItems:
axis = axisItems[k]
else:
axis = AxisItem(orientation=k, parent=self)
axis.linkToView(self.vb)
self.axes[k] = {'item': axis, 'pos': pos}
self.layout.addItem(axis, *pos)
axis.setZValue(-1000)
axis.setFlag(axis.ItemNegativeZStacksBehindParent)
Perhaps you can make it work by looking at the code above. Again, this is undocumented behavior, use at your own risk! Furthermore, you should properly remove and unlink the old AxisItem when setting a new one to prevent memory leaks. Perhaps this is tricky and this could be the reason why the PlotItem.setAxis method does not exist.

I looked into the source of the PlotItem constructor for a complete day, but I couldn't get it to work.
In the end, I found that QtDesigner/QtCreator only outputs three lines on the PlotWidget. Instead of trying to add a TimeAxisItem to the existing PlotWidget, it is easier (but definitely not nicer) to just delete the existing PlotWidget and then make a new one with a TimeAxisItem, as described here.
from PyQt5 import QtCore
import pyqtgraph as pg
parent = self.ui_plot.parent()
geom_object = self.ui_plot.frameGeometry()
geometry = QtCore.QRect(geom_object.left(), geom_object.top(), geom_object.width(), geom_object.height())
object_name = self.ui_plot.objectName()
del self.ui_plot
time_axis = TimeAxisItem(orientation='bottom')
self.ui_plot = pg.PlotWidget(parent, axisItems={'bottom': time_axis})
self.ui_plot.setGeometry(geometry)
self.ui_plot.setObjectName(object_name)

Since there are people that try to find something here, I will post my simple but not elegant workaround.
After converting my template generated by Qt Designer to python template file with pyuic, I'm adding my custom AxisItem directly to python template file by replacing corresponding PlotWidget. This all can be done with simple bash script:
pyuic4 template.ui -o template.py
match="from PyQt4 import QtCore, QtGui"
insert_class="from timeaxisitem_class import TimeAxisItem"
match_widget="self.plot = PlotWidget(Form)"
insert_timeaxis="self.plot = PlotWidget(Form, axisItems={'bottom': TimeAxisItem(orientation='bottom')})"
file="template.py"
sed -i "s/$match/$match\n$insert_class/" $file
sed -i "s/$match_widget/$insert_timeaxis/" $file

Related

Animating FontSize of a QLabel in PyQt5

I wanted to ask if we could just animate the fontsize of the QLabel in PyQt5 with QPropertyAnimation.
I tried by adding QPropertyAnimation(self.label , b"fontSize") but It was not working and I got the warning
I want you increase the text of the QLabel and then again switch to normal size
From the QPropertyAnimation description:
QPropertyAnimation interpolates over Qt properties. As property values are stored in QVariants, the class inherits QVariantAnimation, and supports animation of the same meta types as its super class.
A class declaring properties must be a QObject. To make it possible to animate a property, it must provide a setter (so that QPropertyAnimation can set the property's value).
All classes that inherit from QObject (including widgets) support Qt properties, and in fact most of them have "builtin" properties, but, obviously, not all properties support animations: they must be based on numeric values. You can animate a numeric change (eg: from 0 to 100), not a text (eg. from 'ABC' to 'PYZ').
For instance, all QWidget have a pos property, and since such property (which is a QPoint) is based on numbers, you can create an animation. You can see the list of supported variant types for animations in the QVariantAnimation docs. Consider that all documentation pages regarding QObject subclasses contain a property section (see the QWidget properties for example), and that properties obviously are inherited from their super classes.
There is no property for the font size, though, so there are two possibilities:
create a custom property (by using the pyqtProperty decorator in PyQt);
use a QVariantAnimation, which is not bound to any property;
Depending on the situation, one might prefer one approach or the other, but for this specific case it doesn't change that much; the QVariantAnimation is certainly simpler, the Qt property approach is more "compliant".
Here you can see how the property is created using the pyqtProperty decorator. Since the animation writes the property, having a setter is mandatory.
class AnimationTest(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.startButton = QtWidgets.QPushButton('Start')
self.label = QtWidgets.QLabel('Hello!')
self.labelFont = self.font()
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.startButton)
layout.addWidget(self.label)
self.ani = QtCore.QPropertyAnimation(self, b'labelFontSize')
self.ani.setStartValue(10)
self.ani.setEndValue(80)
self.ani.setDuration(1500)
self.startButton.clicked.connect(self.ani.start)
#QtCore.pyqtProperty(int)
def labelFontSize(self):
return self.labelFont.pointSize()
#labelFontSize.setter
def labelFontSize(self, size):
self.labelFont.setPointSize(size)
self.label.setFont(self.labelFont)
import sys
app = QtWidgets.QApplication(sys.argv)
test = AnimationTest()
test.show()
sys.exit(app.exec_())
The variant animation is certainly shorter:
class AnimationTest(QtWidgets.QWidget):
def __init__(self):
# ...
self.ani = QtCore.QVariantAnimation()
self.ani.setStartValue(10)
self.ani.setEndValue(80)
self.ani.setDuration(1500)
self.ani.valueChanged.connect(self.updateLabelFont)
self.startButton.clicked.connect(self.ani.start)
def updateLabelFont(self, value):
self.labelFont.setPointSize(value)
self.label.setFont(self.labelFont)
Please consider that font size animations are often problematic, as most fonts have slightly different glyphs and spacings depending on the size. In the examples above I used a pretty long animation that will probably show the effect: while the size increases linearly, the change in the font usually is not very smooth.

Including a docstring in another docstring

Problem: I want to use one docstring in another docstring.
Suppose I have the following snippet:
def window(dimensions: tuple):
'''
Function to create an app window and return it
PARAMETERS
----------
dimensions : tuple
The width and height of the window to create
RETURNS
-------
display.Window
Class to implement a screen # Make this equal to Window.__doc__
'''
class Window:
'''
Class to implement a screen
'''
def __init__(self, dimensions: tuple):
pass
return Window(dimensions)
I want to automatically set the docstring for window to include the docstring for Window
I read that you can set the docstring manually, like so:
window.__doc__ = "".join((window.__doc__, Window.__doc__))
But it is only executed when the function is called.
Also, I could use decorators, but is there a simpler intuitive way to do this?
Bonus: Is there a way to decide exactly where in the docstring I can include another?
EDIT: So, it looks like there is a duplicate suggestion to this question, but since I specifically asked without decorators, that does make my question somewhat different. Also, my use of nested class in window means that any attempt to change __doc__:
inside of window: will not occur until function is called.
outside of window: will not run as Window is nested.
So this rules both these methods out, as things stand.
But the answer of course has to be one of these. So the answer is a duplicate, not the question. :P
Therefore, I had to restructure my code. See below.
Thanks to #aparpara, I found that answer (which didn't show up when I searched it online), and it made me realise there is (possibly?) no solution to my specific question.
Therefore, I had to remove the nested class to be able to access it outside the function.
Here is the final version.
# module display_module.py
class Window:
'''
Class to implement pygame's screen
'''
def __init__(self, dimensions: tuple):
pass
def window(dimensions: tuple):
'''
Function to create an app window and return it
PARAMETERS
----------
dimensions : tuple
The width and height of the window to create
RETURNS
-------
display.Window
{0}
'''
return Window(dimensions)
window.__doc__ = window.__doc__.format(Window.__doc__.strip())
Still open to any answers to the old question!

How to setStyleSheet to QStandardItem

The code below creates a simple QComboBox. But instead of using a "traditional" .addItem('myItemName') method it creates QStandardItem first and then adds it via QComboBox's .model().appendRow(). Since now I can access each QStandardItem individually I wonder if there is a way to assign CSS to each of them (to each QStandardItem) individually. The goal is to customize each item displayed in ComboBox pulldown menu. So far I am only able to assign a single CSS style to entire ComboBox globally.
from PyQt4 import QtCore, QtGui
app = QtGui.QApplication([])
class Combo(QtGui.QComboBox):
def __init__(self, *args, **kwargs):
super(Combo, self).__init__()
for each in ['Item_1','Item_2','Item_3','Item_4','Item_5']:
item=QtGui.QStandardItem(each)
self.model().appendRow(item)
tree=Combo()
sys.exit(app.exec_())
It looks like this class has no setStyleSheet method, but you can use setBackground, setForeground and setTextAlignment methods. With QBrush you be able to customize elements. Of course it isn't so powerful as styleSheets but better than nothing.
http://pyqt.sourceforge.net/Docs/PyQt4/qstandarditem.html

Recommend way to handle unit conversion

I'm making a GUI with pyqt4 and python. Right now I have a QLineEdit and QComboBox, where the QLineEdit displays the values and the QComboBox can be used to change units. I'm using signals and slots to handle real time unit/value feedback for the user but I'm having problems understanding how to programmatically work with the values as I need them all to be in standard units. Here's what I've got so far, the combo_box_line_edit_list is a list of list where I wrap the combo box and line list together
class UnitConverterSignaler(QtCore.QObject):
def __init__(self, combo_box_line_edit_list):
super(QtCore.QObject, self).__init__()
self.combo_box_line_edit_list = combo_box_line_edit_list
self.combo_box_list = [line_edit_combo_box[0] for line_edit_combo_box in combo_box_line_edit_list]
for combo_box, line_edit in self.combo_box_line_edit_list:
combo_box.currentIndexChanged['QString'].connect(line_edit.convert_units)
line_edit.store_unit_state(combo_box.currentText())
line_edit.standard_unit = combo_box.itemText(1)
def convert_to_standard(self):
for combo_box in self.combo_box_list:
combo_box.setCurrentIndex(0)
def convert_to_international(self):
for combo_box in self.combo_box_list:
combo_box.setCurrentIndex(1)
def toggle_unit_conversion(self, hold_line_values_steady):
for combo_box in self.combo_box_list:
if hold_line_values_steady:
combo_box.do_not_convert_units_on_change()
else:
combo_box.convert_units_on_change()
def convert_units_on_change(self):
"""
Changes the value of the line edit each time the combo box is changed
"""
for combo_box, line_edit in self.combo_box_line_edit_list:
combo_box.currentIndexChanged['QString'].connect(line_edit.convert_units)
combo_box.currentIndexChanged['QString'].disconnect(line_edit.store_unit_state)
def do_not_convert_units_on_change(self):
"""
Holds the line edit value constant in spite of combo box changes
"""
for combo_box, line_edit in self.combo_box_line_edit_list:
combo_box.currentIndexChanged['QString'].disconnect(line_edit.convert_units)
combo_box.currentIndexChanged['QString'].connect(line_edit.store_unit_state)
Instantiated & used in another class
self.lockCellCheckBox.toggled.connect(self.unit_converter_signaler.toggle_unit_conversion)
self.internationalPushButton.clicked.connect(self.unit_converter_signaler.convert_to_international)
self.standardPushButton.clicked.connect(self.unit_converter_signaler.convert_to_standard)
I've also monkey patched the QLineEdit instead of subclassing so I can make quick changes with QtDesigner.
# monkey patch slot onto line_edit
def convert_units(line_edit, end_unit):
converted_unit_value = line_edit.unit_registry.convert(float(line_edit.text()), line_edit.stored_unit_state, str(end_unit))
line_edit.setText(str(converted_unit_value))
line_edit.stored_unit_state = str(end_unit)
# monkey patch slot onto line_edit
def store_unit_state(line_edit, unit):
line_edit.stored_unit_state = str(unit)
Would the most generalized way to get the standard units out in my main program be the creation of a signal for each combo box/line edit in the UnitConverter?
From what I understood so far: you have many combo-box/line-edit pairs and the entered values should always be converted to standard units (e.g. displayed on a third QLabel or whatever).
Would the most generalized way to get the standard units out in my main program be the creation of a signal for each combo box/line edit in the UnitConverter?
No, you don't have to. A slot in python (or especially in pyqt) can be any callable object. A callable object is an object with method __call__(self) implemented.
Therefore I would suggest you to create a class which takes the related object(s) as parameter(s) in the contructor and changes them in __call__(self). Something like this:
class ConverterSignal:
def __init__(whatever_you_want_to_refer_to):
self.whatever_you_want_to_refer_to = whatever_you_want_to_refer_to
def __call(self)__:
""" Here you can refer to whatever_you_want_to_refer_to and do whatever you want with it """
The connection is done as following (for the combo box as an example):
self.connect(combo_box, QtCore.SIGNAL('activated(int)'), ConverterSignal(whatever_you_want_to_refer_to))
Here an instance of the class ConverterSignal is created and will be called if the corresponding signal is emitted.

matplotlib forcing pan/zoom to constrain to x-axes

Matplotlib has a feature where if you hold down the "x" or "y" keys it constrains panning or zooming to the corresponding axis.
Is there a way to cause this to be the default? For some reason my CPU does not allow touchpad movement when a letter key is held down. And I only want the x-axis to be pan/zoomed, not the y-axis.
edit: found the pan/zoom function at https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/axes.py#L3001
which contains an internal function format_deltas. But I have no idea how to override the Axes class with a subclass when Axes objects are created automatically from Figure objects.
It is possible to use your own axes class. In your case you can inherit from matplotlib.axes.Axes and change the drag_pan method to always act as though the 'x' key is being pressed. However the zooming doesn't seem to be defined in that class. The following will only allow x axis panning:
import matplotlib
import matplotlib.pyplot as plt
class My_Axes(matplotlib.axes.Axes):
name = "My_Axes"
def drag_pan(self, button, key, x, y):
matplotlib.axes.Axes.drag_pan(self, button, 'x', x, y) # pretend key=='x'
matplotlib.projections.register_projection(My_Axes)
figure = plt.figure()
ax = figure.add_subplot(111, projection="My_Axes")
ax.plot([0, 1, 2], [0, 1, 0])
plt.show()
For the zooming, you may have to look at the toolbar control itself. The NavigationToolbar2 class has the drag_zoom method which seems to be what's relevant here, but tracking down how that works is quickly complicated by the fact that the different backends all have their own versions (e.g. NavigationToolbar2TkAgg
edit
You can monkeypatch the desired behaviour in:
import types
def press_zoom(self, event):
event.key='x'
matplotlib.backends.backend_tkagg.NavigationToolbar2TkAgg.press_zoom(self,event)
figure.canvas.toolbar.press_zoom=types.MethodType(press_zoom, figure.canvas.toolbar)
You could do it properly and make a subclass of the toolbar, but you have to then create instances of Figure, FigureCanvas and your NavigationToolbar and put them in a Tk app or something. I don't think there's a really straightforward way to just use your own toolbar with the simple plotting interface.
simonb's approach worked but I had to tweak it for the press_zoom behavior, so that it keeps the press_zoom method a feature of the class, not the instance, but I added a hook to fixup the per-instance behavior.
import types
def constrainXPanZoomBehavior(fig):
# make sure all figures' toolbars of this class call a postPressZoomHandler()
def overrideZoomMode(oldZoom, target):
def newZoom(self, event):
oldZoom(self, event)
if hasattr(self, 'postPressZoomHandler'):
self.postPressZoomHandler()
return newZoom
def overrideToolbarZoom(fig, methodname, functransform, *args):
toolbar = fig.canvas.toolbar
oldMethod = getattr(toolbar.__class__, methodname)
newMethod = functransform(oldMethod, toolbar, *args)
setattr(toolbar.__class__, methodname, newMethod)
overrideToolbarZoom(fig, 'press_zoom', overrideZoomMode)
# for this specific instance, override the zoom mode to 'x' always
def postPressZoomHandler(self):
self._zoom_mode = 'x'
fig.canvas.toolbar.postPressZoomHandler = types.MethodType(postPressZoomHandler, fig.canvas.toolbar)
return fig

Categories

Resources