Skip to content

BarGraphItem: calculate boundingRect without drawing#2599

Merged
j9ac9k merged 2 commits intopyqtgraph:masterfrom
pijyoi:bgi-drawrects
Feb 26, 2023
Merged

BarGraphItem: calculate boundingRect without drawing#2599
j9ac9k merged 2 commits intopyqtgraph:masterfrom
pijyoi:bgi-drawrects

Conversation

@pijyoi
Copy link
Copy Markdown
Contributor

@pijyoi pijyoi commented Jan 29, 2023

This is a rewrite/restructuring of BarGraphItem to address some issues with the existing implementation:

  1. boundingRect, dataBounds and pixelPadding all trigger the rendering code.
    • in this PR, bounding rectangle is calculated directly from the data values.
    • in this PR, pen width is calculated outside of the rendering code.
  2. pens and brushes are always recreated even if only the data values were modified.
    • in this PR, pens and brushes are only recreated if they are modified
  3. Even if the user passes in pre-created QPens and QBrushes, a copy will be made.
    • in this PR, QPens and QBrushes are not copied. The user is able to use the same QPen or QBrush instance for multiple bars.
  4. Computation of the QRectFs are done one by one in a Python loop.
    • in this PR, numpy vector operations are used to calculate all the QRectFs
  5. drawRects() is not used.
    • in this PR, drawRects() is used if only a single color is being used. This path also skips creation of QPicture.

A animated script to exercise and benchmark the various combinations of using BarGraphItem.
It seems to demonstrate a largish speedup vs master.

import itertools
import time
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui, QtWidgets

pg.mkQApp()
glw = pg.GraphicsLayoutWidget()
glw.show()
plt1 = glw.addPlot()
plt2 = glw.addPlot()
glw.nextRow()
plt3 = glw.addPlot()
plt4 = glw.addPlot()

ttt = np.linspace(0, 1, 100)
s = np.sin(2*np.pi*1.0*ttt)
dt = ttt[1] - ttt[0]

# user pen and default brush
bgi1 = pg.BarGraphItem(x=ttt, width=dt, y1=s, y0=0, pen={'color':'w'})
# default pen and user brush
bgi2 = pg.BarGraphItem(y=ttt, height=dt, x1=s, x0=0, brush=(0,0,255,150))

# pre-made QBrush objects shared amongst all the bars
# pens are not pre-made and will be created by BarGraphItem
# (old implementation will re-create them every cycle)
pens = itertools.cycle([{'color': x} for x in 'rgby'])
pens = [next(pens) for _ in range(len(ttt))]
brushes = itertools.cycle([pg.mkBrush(255,0,0), pg.mkBrush(0,255,0), pg.mkBrush(0,0,255)])
brushes3 = [next(brushes) for _ in range(len(ttt))]
bgi3 = pg.BarGraphItem(x=ttt, width=dt, y1=s, y0=0, pens=pens, brushes=brushes3)

# pre-made QBrush objects that we will roll together with the plot
# to not have a border, we need to create a NoPen
no_pen = pg.mkPen(None)
brushes4 = [pg.mkBrush(abs(s[idx])*255,0,0) for idx in range(len(ttt))]
bgi4 = pg.BarGraphItem(y=ttt, height=dt, x1=s, x0=0, pen=no_pen, brushes=brushes4)

plt1.addItem(bgi1)
plt2.addItem(bgi2)
plt3.addItem(bgi3)
plt4.addItem(bgi4)

frame_cnt = 0
last_time = time.perf_counter()
def update():
    global frame_cnt, last_time
    global brushes4
    global s
    now = time.perf_counter()
    elapsed = now - last_time
    if elapsed > 2.0:
        fps = frame_cnt / elapsed
        print(fps)
        frame_cnt = 0
        last_time = now
    s = np.roll(s, 1)
    brushes4.insert(0, brushes4.pop())
    bgi1.setOpts(y1=s)
    bgi2.setOpts(x1=s)
    bgi3.setOpts(y1=s)
    bgi4.setOpts(x1=s, brushes=brushes4)
    QtWidgets.QApplication.instance().processEvents()
    frame_cnt += 1

timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(0)

pg.exec()

Remarks:

  1. This PR motivated some of the changes made in Prepare support for PySide6 drawLines and friends #2596.
  2. BarGraphItem probably wasn't intended for use in animated updates.

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Jan 29, 2023

A demonstration of the over-generalized nature of BarGraphItem that it can be used to draw arbitrary rectangles.

import numpy as np
import random
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui, QtWidgets

size = 32
x = np.arange(size)
y = np.arange(size)[:, np.newaxis]
bitmap = (y & 1) ^ (x & 1)
X, Y = np.meshgrid(x, y)
nzidx = bitmap.nonzero()

pg.mkQApp()
pw = pg.PlotWidget()
pw.show()

no_pen = pg.mkPen(None)
back = QtWidgets.QGraphicsRectItem(0, 0, size, size)
back.setPen(no_pen)
back.setBrush(QtCore.Qt.GlobalColor.white)

num_rects = len(nzidx[0])
brush = (255, 255, 255)
brushes = [(0,0,random.randint(100,255)) for _ in range(num_rects)]
bgi = pg.BarGraphItem(x0=X[nzidx], y0=Y[nzidx], width=1, height=1, pen=no_pen, brushes=brushes)

pw.addItem(back)
pw.addItem(bgi)
pg.exec()

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Feb 1, 2023

An additional bugfix wrt master concerning LegendItem. If a brush is not supplied, then a default gray value is used, but this default value is not set to self.opts. Thus when LegendItem peeks into self.opts['brush'], it gets None, with the result that an invisible brush gets used for the legend.

import pyqtgraph as pg

pg.mkQApp()
pw = pg.PlotWidget()
pw.show()
pw.addLegend()
bgi = pg.BarGraphItem(name='BGI', x=[0,1,2,3,4], width=1, y1=[1,2,3,4,5])
pw.addItem(bgi)
pg.exec()

@pijyoi pijyoi marked this pull request as ready for review February 10, 2023 11:25
@ixjlyons ixjlyons self-assigned this Feb 10, 2023
@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Feb 16, 2023

An example of drawing many rectangles with each different color handled by one BarGraphItem.
Dragging the image around is smoother with this PR compared to master.

import pyqtgraph as pg
import numpy as np
import scipy.misc

z = scipy.misc.ascent()
ncolors = 256

# reduce the number of colors if needed
decimate = 1
z //= decimate
ncolors //= decimate

pg.mkQApp()

cm = pg.colormap.get("viridis")
lut_qcolor = cm.getLookupTable(0, 1, ncolors, mode=pg.ColorMap.QCOLOR)
lut_brushes = [pg.mkBrush(x) for x in lut_qcolor]

x = np.arange(z.shape[1])
y = np.arange(z.shape[0])[:, np.newaxis]
X, Y = np.meshgrid(x, y)
X = X.ravel()
Y = Y.ravel()
Z = z.ravel()
brushes = [lut_brushes[x] for x in Z]

pw = pg.PlotWidget()
pw.invertY(True)
pw.show()
no_pen = pg.mkPen(None)
for idx in range(ncolors):
    mask = Z == idx
    bgi = pg.BarGraphItem(x0=X[mask], y0=Y[mask], width=1, height=1, pen=no_pen, brush=lut_brushes[idx])
    pw.addItem(bgi)

pg.exec()

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Feb 16, 2023

Just for fun, the equivalent using NonUniformImage.
For NonUniformImage, border pixels are half-width.

import pyqtgraph as pg
from pyqtgraph.graphicsItems.NonUniformImage import NonUniformImage
import numpy as np
import scipy.misc

z = scipy.misc.ascent()

# decimate, because it's really slow
deci = 2
z = z[::deci, ::deci]

# non-uniform uses transposed axes
z = z.T.copy()
x = np.arange(z.shape[0])
y = np.arange(z.shape[1])

pg.mkQApp()
pw = pg.PlotWidget()
pw.invertY(True)
pw.show()
img = NonUniformImage(x, y, z)
cm = pg.colormap.get("viridis")
img.setColorMap(cm)
pw.addItem(img)
pg.exec()

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Feb 26, 2023

@ixjlyons self-assigned this PR, but I haven't heard a peep from them, so I think I'm going to merge this as this LGTM. Thanks for your work on this @pijyoi !

@j9ac9k j9ac9k merged commit 1c979bb into pyqtgraph:master Feb 26, 2023
@pijyoi pijyoi deleted the bgi-drawrects branch February 26, 2023 04:10
@pijyoi pijyoi mentioned this pull request Mar 27, 2023
12 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants