Is there more design documentation?

319 views
Skip to first unread message

mymindis...@gmail.com

unread,
Jun 6, 2017, 5:08:16 AM6/6/17
to pyqtgraph

Hi,

I've been using pyqtgraph as a quick plotting tool for a while and it has proven to be the only one that is fast enough for the tasks. So lets start with praise: I find pyqtgraph very helpful and useful. Thanks for providing this!


I'm currently in the process of building several custom plotting widgets. I'm having a bit of a hard time, since I don't really understand some of the design choices.

Reverse engineering with pyreverse ends mostly in either unreadable or uninformative diagrams. So I've started to work my way through the code bit by bit.

I don't fully understand the design decisions. From what I have gathered, the lowest level connection to Qt is like in the given drawing (wherever it may appear in this post). Blue are the Qt base classes. As an example I have drawn only the PlotWidget, not the other two widgets (ImageView and GraphicsLayoutWidget).

Now the questions:
 - Why is the direct access to GraphicsScene not available? This effectively removes the Control part from the MVC design of the QtGraphics framework? For example, in this way one has to keep references to all the items that are placed whereever on the widget to allow for modification.

 - Why is the QtGraphicsItem not the base class, or anyhow present? I can't really follow the argument in the code (of GraphicsItem.py)

 - Why are there widgets like HistogramLUTWidget, GradientWidget (the two I'm mostly toying with right now) or the GraphicsLayoutWidget that are only adapter-like placeholders for their respective (graphics)items?

 
Maybe others have struggled in similar ways like me. My own wish would be to have a toolkit, from which I could build custom data displays (like I'm trying right now) for numpy/scipy data. So to provide single elements (e.g. GraphicsItems), that are (almost) atomic; which then can be used to be placed freely in the corresponding containers. I can see that a lot of things are already there. But there is still some gap between the very nice and informative drawing from the documentation (e.g. http://www.pyqtgraph.org/documentation/_images/plottingClasses.png) and the actual implementation. The examples help, but without some more info on the hierarchy or maybe example(s) on how to construct a custom widget, things get a lot harder, than they should -- concerning how much effort and work already has been put into pyqtgraph.

Cheers,
Christoph

PS: bad sign for a first post, but I'd like to apologize in advance for writing too complicated; I'm still working on how to express my ideas more clearly

Luke Campagnola

unread,
Jun 7, 2017, 12:17:30 AM6/7/17
to pyqt...@googlegroups.com


On Tue, Jun 6, 2017 at 2:08 AM, <mymindis...@gmail.com> wrote:


This looks great! The only thing I would add is that GraphicsObject inherits from QGraphicsObject as well as GraphicsItem.

 

Now the questions:
 - Why is the direct access to GraphicsScene not available? This effectively removes the Control part from the MVC design of the QtGraphics framework? For example, in this way one has to keep references to all the items that are placed whereever on the widget to allow for modification.

Access to the GraphicsScene comes from QGraphicsView.scene().
However, I have found it is much easier to keep your own references to the graphics items rather than try to fetch them from the scene.

 
 - Why is the QtGraphicsItem not the base class, or anyhow present? I can't really follow the argument in the code (of GraphicsItem.py)

So there are three basic Qt graphics item classes here--QGraphicsItem, QGraphicsObject, and QGraphicsWidget. The latter two are subclasses of QGraphicsItem, and these are really the only two that I use within pyqtgraph. I needed subclasses of these to provide some extra functionality (GraphicsObject and GraphicsWidget), and I also wanted to provide some extra functionality that was shared between the two, hence GraphicsItem. 

Everything makes sense up until the mystery you discovered--why does GraphicsItem not inherit from QGraphicsItem? The reason is that it is not possible for a Python class to inherit from more than one Qt class. So if GraphicsItem were subclassed from QGraphicsItem, then it would not be possible for GraphicsObject to inherit from both GraphicsItem and QGraphicsObject. It's a mess, but perhaps a necessary consequence of the imperfect translation from Qt's C++ classes to Python classes.


 - Why are there widgets like HistogramLUTWidget, GradientWidget (the two I'm mostly toying with right now) or the GraphicsLayoutWidget that are only adapter-like placeholders for their respective (graphics)items?

These are just convenience classes that allow you to embed the most commonly used GraphicsWidget classes as a QWidget. In particular, these make it possible to build many of pyqtgraph's tools into a GUI using Qt Designer.
 
 
Maybe others have struggled in similar ways like me. My own wish would be to have a toolkit, from which I could build custom data displays (like I'm trying right now) for numpy/scipy data. So to provide single elements (e.g. GraphicsItems), that are (almost) atomic; which then can be used to be placed freely in the corresponding containers. I can see that a lot of things are already there. But there is still some gap between the very nice and informative drawing from the documentation (e.g. http://www.pyqtgraph.org/documentation/_images/plottingClasses.png) and the actual implementation. The examples help, but without some more info on the hierarchy or maybe example(s) on how to construct a custom widget, things get a lot harder, than they should -- concerning how much effort and work already has been put into pyqtgraph.

I agree. Many of the projects that I work on do involve custom graphics items of some sort, and there is not any good documentation on developing these within pyqtgraph. That said, pyqtgraph is in many ways just a thin (but broad) extension of Qt's GraphicsView, and there is already a lot of documentation written on that topic. I can think of two major pieces of documentation that would help here:

1. Assuming the developer already has an understanding of QWidget and Qt GraphicsView, an explanation of the architectural features added by pyqtgraph.
2. A set of basic examples describing some custom classes (we already have a few -- customGraphicsItem.py, CustomGraphItem.py, customPlot.py, but it's not clear to me that these provide sufficient coverage of the topic).

I'm interested to hear more about where you think the gaps are in the documentation, maybe with some specific examples--what were you trying to do, and what missing pieces of information were making those tasks difficult?


Luke





mymindis...@gmail.com

unread,
Jun 8, 2017, 5:07:18 AM6/8/17
to pyqtgraph
Hi Luke,

thanks for the explanation.



On Wednesday, June 7, 2017 at 6:17:30 AM UTC+2, Luke Campagnola wrote:
On Tue, Jun 6, 2017 at 2:08 AM, <mymindis...@gmail.com> wrote:

[...]
This looks great! The only thing I would add is that GraphicsObject inherits from QGraphicsObject as well as GraphicsItem.

You're right, the double inheritance in the case of the GraphicsItem was another cause of headache for me.
 
 
Now the questions:
 - Why is the direct access to GraphicsScene not available? This effectively removes the Control part from the MVC design of the QtGraphics framework? For example, in this way one has to keep references to all the items that are placed whereever on the widget to allow for modification.

Access to the GraphicsScene comes from QGraphicsView.scene().
However, I have found it is much easier to keep your own references to the graphics items rather than try to fetch them from the scene.

ACK. In my code I had been using e.g. PlotWidget.scene() -- and didn't realize that it was inherited from QGraphicsView. More as a note to myself -- I think I'd like to have some documentation that includes all inherited methods, including the ones from pyqt...

 
 - Why is the QtGraphicsItem not the base class, or anyhow present? I can't really follow the argument in the code (of GraphicsItem.py)

So there are three basic Qt graphics item classes here--QGraphicsItem, QGraphicsObject, and QGraphicsWidget. The latter two are subclasses of QGraphicsItem, and these are really the only two that I use within pyqtgraph. I needed subclasses of these to provide some extra functionality (GraphicsObject and GraphicsWidget), and I also wanted to provide some extra functionality that was shared between the two, hence GraphicsItem. 

Everything makes sense up until the mystery you discovered--why does GraphicsItem not inherit from QGraphicsItem? The reason is that it is not possible for a Python class to inherit from more than one Qt class. So if GraphicsItem were subclassed from QGraphicsItem, then it would not be possible for GraphicsObject to inherit from both GraphicsItem and QGraphicsObject. It's a mess, but perhaps a necessary consequence of the imperfect translation from Qt's C++ classes to Python classes.

I wasn't aware of the limitation in the double inheritance in between Python and C++. I'm a relative latecomer to Python and still haven't explored every bit of its deficiency ;-)
 [...]


| I'm interested to hear more about where you think the gaps are in the documentation, maybe with some specific examples--what were you trying to do, and what missing pieces of information were making those tasks difficult?

OK. The short backstory is, that I started out evaluating plotting tools for numpy data -- and pyqtgraph was the clear winner in terms of speed, stability and being an active project. The "convience" classes are extremely useful for a simple plot, in addition to still using the QtGraphicsView framework -- so there is not a different principal logic to learn.

Then I started to implement "custom" plotting tools and got into trouble. I started with the existing classes and examples -- namely the PlotWidget and the pyqtgraph.plot() functionality. I wanted some parts that I saw, but add others and remove some elements. But I got stuck or confused, because it seemed like there were disconnected ways of placing "elements" (I try to avoid the word "item", because it means something else) onto something drawable. I read and re-read the whole documentation, especially http://www.pyqtgraph.org/documentation/plotting.html#organization-of-plotting-classes

Again, maybe it's just me -- I can't start to write a custom from that documentation. I started to draw UML diagrams (as the one before) and it starts to make more sense to me.

My current task is to plot a 2D array (basically an image), provide an x and y axis, provide a mouse pointer to get info on the value of individual pixels and have a fixed colour scale (e.g. a gradient) that encodes the pixel values, including an axis to display the numeric values. Outside controls change the displayed numpy array, the numeric value axis of the gradient and the colour scheme used in the gradient.
I'm almost done with it, currently re-factoring the pyqt parts -- hopefully I found a way that I can understand in several months.

One particular nasty bit is that I haven't found a good way to remove the triangular tick marks in the LUT/Gradient items. The best way I've come up with is to set the size of the tick marks to zero -- by accessing the internal variable, which feels kind of bad. It also introduces some errors when mouse-clicking wildly in the area of the display, where the tick marks should be.

In the end I want to add further things to the above display:
a) fixing a crosshair-like object (using the mouse) and getting the data slice as side histograms in the correct size -- I don't know yet how to draw the histograms in the right size
b) pick a ROI, which is then displayed in a separate, detached QDialog

Cheers,
Christoph





Luke Campagnola

unread,
Jun 9, 2017, 1:50:59 PM6/9/17
to pyqt...@googlegroups.com
Ok, I think I have a sense of what is missing. Generally, it is possible and easy to take any of pyqtgraph's components and recombine them in novel ways. What you are trying to do, though, is reach inside the individual components (such as PlotItem) to change the way they look/behave. There is definitely much less support for this kind of use, and it's one area where I would like to see pyqtgraph grow. Some parts can indeed be customized--for example, a lot of the functionality you listed above is provided by ImageView, but you would need to swap out the internal ViewBox if you wanted axes (see: https://groups.google.com/forum/#!topic/pyqtgraph/2KSFXY30WEk). If you found that customizing past that point was too difficult, then it may be easier to build your own from simpler components like ViewBox, AxisItem, ImageItem, etc. 

I'll think on this some more. Whereas it's relatively easy to predict the ways people will use the existing pyqtgraph components (and write documentation/examples based on those), it's harder to predict all the ways people might want to customize outside of those existing components.



On Thu, Jun 8, 2017 at 2:07 AM, <mymindis...@gmail.com> wrote:

Again, maybe it's just me -- I can't start to write a custom from that documentation. I started to draw UML diagrams (as the one before) and it starts to make more sense to me.

It's definitely not you; that documentation is not really meant to cover your use case. 

One particular nasty bit is that I haven't found a good way to remove the triangular tick marks in the LUT/Gradient items. The best way I've come up with is to set the size of the tick marks to zero -- by accessing the internal variable, which feels kind of bad. It also introduces some errors when mouse-clicking wildly in the area of the display, where the tick marks should be.

That'd be a nice feature to add!
 
In the end I want to add further things to the above display:
a) fixing a crosshair-like object (using the mouse) and getting the data slice as side histograms in the correct size -- I don't know yet how to draw the histograms in the right size

See "examples/crosshair.py" and https://github.com/pyqtgraph/pyqtgraph/blob/develop/pyqtgraph/graphicsItems/ROI.py#L2233 for crosshair / mouse interaction ideas.
See ViewBox.setXLink() and .setYLink() to get the histogram to line up with the image.
 
b) pick a ROI, which is then displayed in a separate, detached QDialog

"examples/imageAnalysis.py" and "examples/ROIExamples.py" have some relevant material.


Luke

mymindis...@gmail.com

unread,
Jun 15, 2017, 8:21:19 AM6/15/17
to pyqtgraph
Hi Luke,

I'm still working on this, but as usual there are so many other things that also need attention and suddenly gain higher priority.



On Friday, June 9, 2017 at 7:50:59 PM UTC+2, Luke Campagnola wrote:
Ok, I think I have a sense of what is missing. Generally, it is possible and easy to take any of pyqtgraph's components and recombine them in novel ways. What you are trying to do, though, is reach inside the individual components (such as PlotItem) to change the way they look/behave. There is definitely much less support for this kind of use, and it's one area where I would like to see pyqtgraph grow. Some parts can indeed be customized--for example, a lot of the functionality you listed above is provided by ImageView, but you would need to swap out the internal ViewBox if you wanted axes (see: https://groups.google.com/forum/#!topic/pyqtgraph/2KSFXY30WEk). If you found that customizing past that point was too difficult, then it may be easier to build your own from simpler components like ViewBox, AxisItem, ImageItem, etc. 

I'll think on this some more. Whereas it's relatively easy to predict the ways people will use the existing pyqtgraph components (and write documentation/examples based on those), it's harder to predict all the ways people might want to customize outside of those existing components.

This is the way I'm going now. I think that I have the basic layout now covered, and I'm trying to do this "custom building" from bottom up. The biggest nuisance in this is the lack of code/API documentation that includes all inherited functionality. I'm pretty sure there must be something like this for Python (I'm more a native C/C++).
 



[...]
 
In the end I want to add further things to the above display:
a) fixing a crosshair-like object (using the mouse) and getting the data slice as side histograms in the correct size -- I don't know yet how to draw the histograms in the right size

See "examples/crosshair.py" and https://github.com/pyqtgraph/pyqtgraph/blob/develop/pyqtgraph/graphicsItems/ROI.py#L2233 for crosshair / mouse interaction ideas.
See ViewBox.setXLink() and .setYLink() to get the histogram to line up with the image.

Yes, I've got this working in single examples, e.g. based on one of the pyqtgraph examples,
 
 
b) pick a ROI, which is then displayed in a separate, detached QDialog

"examples/imageAnalysis.py" and "examples/ROIExamples.py" have some relevant material.
Thanks for the pointer.
 
BTW, I've started to write some "internal" documentation about the/my way into pyqtgraph. Still very rough and incomplete, but you might want to have a look. I'm not the only one in our group that is using pyqtgraph, and it seems that I'm set up to become the "go-to - expert".

I'm not sure though, if it might be helpful to someone outside me or my group.

Cheers,
Christoph

Luke Campagnola

unread,
Jun 15, 2017, 3:55:46 PM6/15/17
to pyqt...@googlegroups.com
On Thu, Jun 15, 2017 at 5:21 AM, <mymindis...@gmail.com> wrote:
I'll think on this some more. Whereas it's relatively easy to predict the ways people will use the existing pyqtgraph components (and write documentation/examples based on those), it's harder to predict all the ways people might want to customize outside of those existing components.

This is the way I'm going now. I think that I have the basic layout now covered, and I'm trying to do this "custom building" from bottom up. The biggest nuisance in this is the lack of code/API documentation that includes all inherited functionality. I'm pretty sure there must be something like this for Python (I'm more a native C/C++).

Yes! I tried to make this work long ago. I'll bet something exists in sphinx by now that could help.. at a minimum, it would be nice to automatically link back to base classes and out to the Qt documentation.
I actually really like the way the built-in help() function formats documentation--it shows all methods sorted by their position in the inheritance hierarchy.
 
 
BTW, I've started to write some "internal" documentation about the/my way into pyqtgraph. Still very rough and incomplete, but you might want to have a look. I'm not the only one in our group that is using pyqtgraph, and it seems that I'm set up to become the "go-to - expert".

I'm not sure though, if it might be helpful to someone outside me or my group.

I'd love to see it (in any state), and I'm sure others would as well. If we can clean it up then it might make a nice addition to the documentation or an independent article of its own.


Luke

mymindis...@gmail.com

unread,
Jun 16, 2017, 8:57:43 AM6/16/17
to pyqtgraph
Hi Luke,
 
 
BTW, I've started to write some "internal" documentation about the/my way into pyqtgraph. Still very rough and incomplete, but you might want to have a look. I'm not the only one in our group that is using pyqtgraph, and it seems that I'm set up to become the "go-to - expert".

I'm not sure though, if it might be helpful to someone outside me or my group.

I'd love to see it (in any state), and I'm sure others would as well. If we can clean it up then it might make a nice addition to the documentation or an independent article of its own.


Luke
 
So, the only thing for me to find out is a way to pass the document to you without the whole world being able to read it just now. Is there any way to figure out your email address?

Cheers,
Christoph

mymindis...@gmail.com

unread,
Jun 16, 2017, 11:53:42 AM6/16/17
to pyqtgraph
BTW, I have written a GraphicsItem, which is basically a "removal" write of the HistogramLUTItem -- just the Histogram removed, including the ticks (by setting their size to zero) and without an axis shown.

I dubbed it GradientItem, but it might be better slightly mended to include the missing elements mentioned. So, here's the code;

"""
Extension to GraphicsWidget displaying a gradient editor, afaiu
"""



import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph.functions as fn
from pyqtgraph import GraphicsWidget
from pyqtgraph.graphicsItems.ViewBox import *
from pyqtgraph.graphicsItems.GradientEditorItem import *
from pyqtgraph.graphicsItems.AxisItem import *
from pyqtgraph.graphicsItems.GridItem import *


import pyqtgraph.functions as fn

import numpy as np


pg
.graphicsItems.GradientEditorItem.Gradients['inverted'] = {'ticks': [(0.0, (255, 255, 255, 255)), (1.0, (0, 0, 0, 255)),], 'mode': 'rgb'}
pg
.graphicsItems.GradientEditorItem.Gradients['highContrast'] = {'ticks': [(0.0, (0, 0, 0, 255)), (1.0, (255, 255, 0, 255)),], 'mode': 'rgb'}

class GradientItem(GraphicsWidget):
   
"""
    This is a blatant copy/rewrite of the HistogramLUTItem.
    Instead of a histogram and stuff it only provides a
    Gradient editor to define color lookup table for single-channel images.
    In addition, it can set different predefined gradients by function.
    """

   
    sigLookupTableChanged
= QtCore.Signal(object)
    sigLevelsChanged
= QtCore.Signal(object)
    sigLevelChangeFinished
= QtCore.Signal(object)
   
   
def __init__(self, image=None, fillHistogram=True):
       
"""
        If *image* (ImageItem) is provided, then the control will be automatically linked to the image and changes to the control will be immediately reflected in the image's appearance.
        """

       
GraphicsWidget.__init__(self)
       
self.lut = None
       
self.imageItem = None
       
       
self.layout = QtGui.QGraphicsGridLayout()
       
self.setLayout(self.layout)
       
self.layout.setContentsMargins(1,1,1,1)
       
self.layout.setSpacing(0)
       
       
self.vb = ViewBox()
       
self.vb.setMaximumWidth(152)
       
self.vb.setMinimumWidth(45)
       
self.vb.setMouseEnabled(x=False, y=False)
       
       
self.gradient = GradientEditorItem()
       
self.gradient.tickSize = 0 # CR: this is  sooooo bad, but there is no function !?
       
self.gradient.setOrientation('right')
       
self.gradient.loadPreset('highContrast')
       
       
self.layout.addItem(self.gradient, 0, 0)
       
       
self.range = None
       
self.gradient.setFlag(self.gradient.ItemStacksBehindParent)
       
       
self.vb.setFlag(self.gradient.ItemStacksBehindParent)
       
       
self.gradient.sigGradientChanged.connect(self.gradientChanged)
       
       
if image is not None:
           
self.setImageItem(image)
       
   
def paint(self, p, *args):
       
pass

   
def setImageItem(self, img):
       
self.imageItem = img
        img
.setLookupTable(self.getLookupTable)  ## send function pointer, not the result
   
   
def gradientChanged(self):
       
if self.imageItem is not None:
           
if self.gradient.isLookupTrivial():
               
self.imageItem.setLookupTable(None)
           
else:
               
self.imageItem.setLookupTable(self.getLookupTable)  ## send function pointer, not the result
           
       
self.lut = None
       
self.sigLookupTableChanged.emit(self)

   
def getLookupTable(self, img=None, n=None, alpha=None):
       
if n is None:
           
if img.dtype == np.uint8:
                n
= 256
           
else:
                n
= 512
       
if self.lut is None:
           
self.lut = self.gradient.getLookupTable(n, alpha=alpha)
       
return self.lut


   
def setGradientByName(self, name="highContrast"):
       
if name in pg.graphicsItems.GradientEditorItem.Gradients:
           
self.gradient.loadPreset(name)




Reply all
Reply to author
Forward
0 new messages