Separate data per line segment in PlotDataItem?

62 views
Skip to first unread message

Nathan Jessurun

unread,
Jun 6, 2020, 9:44:36 PM6/6/20
to pyqtgraph
Hi there,

My overall goal is to have several clickable regions overlaid on an image, and if the plot boundary of any region is clicked I get a signal with the ID of that region. Something like this:
I tried making each boundary a separate plot data item, but I have 1000s of bounds that update frequently so it lagged a ton. So, I just used connect='finite' within the same plot item to make it much faster.

However, now I can't give a separate ID to each boundary. I have to make the vertices stand out, and connect to sigclicked(...pts) instead:

This works, but I'd love to be able to click anywhere on one of the line segments for the same effect. Is there any way to make this happen?

Relevant code is just a PlotDataItem on a plot widget.

Thanks!

Nathan Jessurun

unread,
Jun 6, 2020, 10:57:11 PM6/6/20
to pyqtgraph
In other words, is there any way to either
  1. Make sigClicked somehow give the data of adjacent points or
  2. Make sigPointsClicked fire when clicking an adjacent line segment?
  3. (Or some alternative I'm not thinking of)
Being new to Google groups posting I thought I could edit my original. Since I can't find a way, here is some sample code:
import pyqtgraph as pg
import numpy as np

tri
= np.array([[0,0], [0,5], [5,5], [0,0]])
nans
= np.array([[np.nan, np.nan]])
tris
= []
datas
= []
for ii in np.arange(0, 16, 5):
  tris
.append(tri + ii)
  tris
.append(nans)
  datas
.extend([ii]*5) # One data for each point
tris
= np.vstack(tris)
datas
= np.array(datas)

def ptsClicked(item, pts):
 
print(f'ID {pts[0].data()} Clicked!')

app
= pg.mkQApp()
triItem
= pg.PlotDataItem(x=tris[:,0], y=tris[:,1], data=datas, symbol='o')
triItem
.sigPointsClicked.connect(ptsClicked)
w
= pg.PlotWindow()
w
.plotItem.addItem(triItem)
w
.show()
app
.exec()


Trần Anh

unread,
Jun 6, 2020, 11:46:34 PM6/6/20
to pyqt...@googlegroups.com
I don't know the answer, but I just want to say you made a pretty nice application there.

Thanks & Regards
Anh Tran


--
You received this message because you are subscribed to the Google Groups "pyqtgraph" group.
To unsubscribe from this group and stop receiving emails from it, send an email to pyqtgraph+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/pyqtgraph/fb10aa48-efe9-40f9-ae3e-553c1998d8e0o%40googlegroups.com.
Message has been deleted

Nathan Jessurun

unread,
Jun 7, 2020, 1:29:39 AM6/7/20
to pyqtgraph
Thanks for the complement :) The project is freely clonable at https://gitlab.com/ficsresearch/cdef

It is a tool to ease the process of labeling high resolution image data and pyqtgraph definitely fits the bill for performance requirements

Message has been deleted

Nathan Jessurun

unread,
Jun 7, 2020, 4:30:14 PM6/7/20
to pyqtgraph
Attempt number 2. I tried to make a custom symbol (like the pyqtgraph.examples one with text as the scatterplot item) for each segment. It's close, but I can't find out how to make each triangle 'life-sized'. Can someone with experience using QPainterPaths weigh in? Thanks!

import pyqtgraph as pg
from pyqtgraph.Qt import QtWidgets, QtCore, QtGui

import numpy as np

tri
= np.array([[0,0], [0,5], [5,5], [0,0]])

tris
= []
xyLocs
= []

datas
= []
for ii in np.arange(0, 16, 5):

  curTri
= tri + ii
  tris
.append(curTri)
  xyLocs
.append(curTri.min(0))
  datas
.append(ii)


def ptsClicked(item, pts):
 
print(f'ID {pts[0].data()} Clicked!')

def makeSymbol(verts: np.ndarray):
  outSymbol
= QtGui.QPainterPath()
  symPath
= pg.arrayToQPath(*verts.T)
  outSymbol
.addPath(symPath)
 
# From pyqtgraph.examples for plotting text
  br
= outSymbol.boundingRect()
  tr
= QtGui.QTransform()
  tr
.scale(1/br.width(), 1/br.height())
  tr
.translate(-br.x() - br.width()/2., -br.y() - br.height()/2.)
  outSymbol
= tr.map(outSymbol)
 
return outSymbol

app
= pg.mkQApp()

symbs
= []
for xyLoc, tri in zip(xyLocs, tris):
  symbs
.append(makeSymbol(tri))

xyLocs
= np.vstack(xyLocs)
tri2
= pg.PlotDataItem(*xyLocs.T, symbol=symbs, data=datas, connect='finite',
                       pen
=None)
# Now each 'point' is one of the triangles, hopefully
tri2
.sigPointsClicked.connect(ptsClicked)

w
= pg.PlotWindow()
w
.plotItem.addItem(tri2)
w
.show()
app
.exec()
Enter code here...



On Saturday, June 6, 2020 at 9:44:36 PM UTC-4, Nathan Jessurun wrote:

Justin Gm1

unread,
Jun 7, 2020, 5:08:39 PM6/7/20
to pyqt...@googlegroups.com
Howdy,
I do not have an answer, but your images are interesting.  Are you making a stand-alone PCB viewer?  Perhaps bringing up BOM information on the component selected?  I’d interested to hear more.

Cheers
Justin


On Jun 7, 2020, at 15:30, Nathan Jessurun <ntj...@gmail.com> wrote:


--
You received this message because you are subscribed to the Google Groups "pyqtgraph" group.
To unsubscribe from this group and stop receiving emails from it, send an email to pyqtgraph+...@googlegroups.com.

Nathan Jessurun

unread,
Jun 7, 2020, 5:38:20 PM6/7/20
to pyqtgraph
Hi Justin,

You're exactly right. My research lab is attempting to automatically generate a bill of materials (for surface mount devices, not inter-layer yet) given digital camera images. You can read a bit more about the project goals from this published conference paper: http://books.google.com/books?hl=en&lr=&id=tGXIDwAAQBAJ&oi=fnd&pg=PA256&dq=info:luKsobTf07cJ:scholar.google.com&ots=1ZQApHkKum&sig=pEuRWkrOUYEUmyW6YsokFqRpuWg

We are in the process of publishing much more on this topic in the coming months, so keep an eye out for papers published by the University of Florida FICS lab :)

I also wrote a paper on the capabilities of this application, but it won't be published until later this year so I attached the draft version. It can indeed bring up BOM information for the selected component, but it is not limited to PCB use -- it can be used to annotate and view tumor information from CT cross sections, welding defects, etc.
To unsubscribe from this group and stop receiving emails from it, send an email to pyqt...@googlegroups.com.
m and m paper.pdf

Nathan Jessurun

unread,
Jun 7, 2020, 10:36:30 PM6/7/20
to pyqtgraph
Attempt number 3: I almost have it doing what I want! I looked at what PlotCurveItem does to make the curveClicked signal, and saw I had to make a stroke path.

The last piece of the puzzle is how to inform the graphics handler that my spot size extends a bit beyond the natural bounding rect. Does anyone know of a way to make this happen? I.e. the programming logic works, but the check for self.itemsNearEvent() in the graphics scene fails for a click near the top-right shape.
import pyqtgraph as pg
from pyqtgraph.Qt import QtWidgets, QtCore, QtGui
import numpy as np

class CustScatter(pg.ScatterPlotItem):
 
def pointsAt(self, pos: QtCore.QPointF):
   
# if isinstance(pos, QtCore.QPointF):
   
#   halfSz = 0.25
   
#   pos = QtCore.QRectF(pos.x()-halfSz, pos.y()-halfSz, 2*halfSz, 2*halfSz)
    pts
= []
   
for spot in self.points(): # type: pg.SpotItem
      symb
= QtGui.QPainterPath(spot.symbol())
      symb
.translate(spot.pos())
      stroker
= QtGui.QPainterPathStroker()
      mousePath
= stroker.createStroke(symb)
     
if mousePath.contains(pos):
        pts
.append(spot)
   
return pts[::-1]

tri
= np.array([[0,2.3,0,1,4,5,0], [0,4,4,8,8,3,0]]).T
tris
= []

xyLocs
= []
datas
= []
for ii in np.arange(0, 16, 5):
  curTri
= tri + ii
  tris
.append(curTri)
  xyLocs
.append(curTri.min(0))
  datas
.append(ii)

def ptsClicked(item, pts):
 
print(f'ID {pts[0].data()} Clicked!')

def makeSymbol(verts: np.ndarray):
  outSymbol
= QtGui.QPainterPath()
  symPath
= pg.arrayToQPath(*verts.T)
  outSymbol
.addPath(symPath)
 
# From pyqtgraph.examples for plotting text
  br
= outSymbol.boundingRect()
  tr
= QtGui.QTransform()

  tr
.translate(-br.x(), -br.y())

  outSymbol
= tr.map(outSymbol)
 
return outSymbol

app
= pg.mkQApp()

pg
.setConfigOption('background', 'w')


symbs
= []
for xyLoc, tri in zip(xyLocs, tris):
  symbs
.append(makeSymbol(tri))

xyLocs
= np.vstack(xyLocs)

tri2
= pg.PlotDataItem()
scat
= CustScatter(*xyLocs.T, symbol=symbs, data=datas, connect='finite',
                   pxMode
=False, brush=None, pen=pg.mkPen(width=5), size=1)
scat
.sigClicked.connect(ptsClicked)

# Now each 'point' is one of the triangles, hopefully


w
= pg.PlotWindow()
w
.plotItem.addItem(scat)
plt
: pg.PlotItem = w.plotItem
plt
.showGrid(True, True, 1)
w
.show()
app
.exec()




On Saturday, June 6, 2020 at 9:44:36 PM UTC-4, Nathan Jessurun wrote:

Patrick

unread,
Jun 7, 2020, 11:04:29 PM6/7/20
to pyqtgraph
Hi,

As a guess, I'm thinking that your CustScatter is still only assuming the scatter plot points are just points with width the size of the pen width (see measureSpotSizes in ScatterPlotItem). You may need to override that method to calculate the shape sizes.

Also, in the past I stumbled across the locate method of ViewBox which has helped diagnosing problems like this, maybe it's useful to try here too.

Patrick

Nathan Jessurun

unread,
Jun 7, 2020, 11:30:50 PM6/7/20
to pyqtgraph
I wish I heard about locate() a few days ago as it would have greatly simplified this troubleshooting process... Thanks for the tip!

The final step was indeed to subclass measureSpotSizes. It's a shame so much was copy-pasted from the base method, but at least it works:
class CustScatter(pg.ScatterPlotItem):
 
def pointsAt(self, pos: QtCore.QPointF):
   
"""
    The default implementation only checks a square around each spot. However, this is not
    precise enough for my needs. It also triggers when clicking *inside* the spot boundary,
    which I don't want.
    """

    pts
= []
   
for spot in self.points(): # type: pg.SpotItem
      symb
= QtGui.QPainterPath(spot.symbol())
      symb
.translate(spot.pos())
      stroker
= QtGui.QPainterPathStroker()
      mousePath
= stroker.createStroke(symb)

     
# Only trigger when clicking a boundary, not the inside of the shape

     
if mousePath.contains(pos):
        pts
.append(spot)
   
return pts[::-1]


 
def measureSpotSizes(self, dataSet):
   
for rec in dataSet:
     
## keep track of the maximum spot size and pixel size
      symbol
, size, pen, brush = self.getSpotOpts(rec)
      br
= symbol.boundingRect()
      size
= max(br.width(), br.height())*2
      width
= 0
      pxWidth
= 0
     
if self.opts['pxMode']:
        pxWidth
= size + pen.widthF()
     
else:
        width
= size
       
if pen.isCosmetic():
          pxWidth
+= pen.widthF()
       
else:
          width
+= pen.widthF()
     
self._maxSpotWidth = max(self._maxSpotWidth, width)
     
self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth)
   
self.bounds = [None, None]Enter code here...


Reply all
Reply to author
Forward
0 new messages