Forcing adherence to x-range and y-range with fixed aspect ratio

1,296 views
Skip to first unread message

James

unread,
Aug 23, 2021, 4:55:45 PM8/23/21
to pyqtgraph
Hi,

I'd like to force the x-range and y-range to specific values meaning that the plot axes should start and stop at those limits (no padding). I also want to specify a fixed aspect ratio of 1:1 (so that dx=100 takes the same screen space as dy=100, i.e. a square will display with equal number of pixels in length and width).

When I try the following, the displayed x-axis extends beyond the specified limits. Do I need to force the ViewBox to have a specific size that is consistent with the fixed aspect ratio and x/y ranges?

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

app = pg.mkQApp("Range Example")

glw = pg.GraphicsLayoutWidget(show=True, title="GLW title")
glw.setBackground('w')
glw.setWindowTitle('pyqtgraph example: Plotting')

plt = glw.addPlot(title='plot title')

_width  = 2000
_height = 6000
glw.resize(_width/3, _height/3)
plt.setXRange(0, _width, padding=0)
plt.setYRange(0, _height, padding=0)
plt.setAspectLocked(lock=True, ratio=1)

if __name__ == '__main__':
    pg.mkQApp().exec_()





Screen Shot 2021-08-23 at 4.53.29 PM.png

Patrick

unread,
Aug 23, 2021, 11:52:26 PM8/23/21
to pyqtgraph
Hi,

Does this behave better? Using setLimits(), since setXRange() etc is just a once-off change to the view bounds.

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

app = pg.mkQApp("Range Example")

glw = pg.GraphicsLayoutWidget(show=True, title="GLW title")
#glw.setBackground('w')
glw.setWindowTitle('pyqtgraph example: Plotting')

plt = glw.addPlot(title='plot title')

_width = 2000
_height = 6000
glw.resize(_width/3, _height/3)
plt.setLimits(xMin=0, xMax=_width, yMin=0, yMax=_height)
plt.setXRange(0, _width, padding=0)
plt.setYRange(0, _height, padding=0)
plt.setAspectLocked(lock=True, ratio=1)

plt.plot([_width/4, _width/2, _width/2, _width/4, _width/4], [_width/4, _width/4, _width/2, _width/2, _width/4])

if __name__ == '__main__':
pg.mkQApp().exec_()

Patrick

James

unread,
Aug 24, 2021, 11:24:30 PM8/24/21
to pyqtgraph
Thanks for the suggestion Patrick. This does work for me in the minimal working example above, as long as the glw.resize() sets the aspect ratio of the GraphicsLayoutWidget to match that of the desired plot (and the GLW fits on my screen). But when I try this in a GUI, I have a problem. See e.g. the following MWE in which I have a PlotWidget and a QPushButton in a QHBoxLayout().

Without resizing the PlotWidget, the axis limits are not respected (should be x=[0,2000], y=[0,6000]).  And calling resize on the PlotWidget seems to have no effect on the displayed size of the PW (see code below and attached image).

So it looks like the plot axes are forced to fill the entire PlotWidget area (or at least they do by default), which makes it hard to enforce an aspect ratio for the axes that doesn't match the aspect ratio of the PlotWidget itself.
There must be a way to do this (e.g. in matplotlib I can configure a set of axes to have whatever aspect ratio I'd like, independent of the dimensions of the figure that holds those axes -- see code below and attached image).

# pyqtgraph version

import sys
import PyQt5.QtWidgets as qtw

import pyqtgraph as pg

app = qtw.QApplication(sys.argv)
window = qtw.QWidget()
window.setWindowTitle('Fixed aspect ratio')
layout = qtw.QHBoxLayout()
layout.addWidget(qtw.QPushButton('Button'))

_width  = 2000
_height = 6000

pw = pg.PlotWidget()
pw.setLimits(xMin=0, xMax=_width, yMin=0, yMax=_height)
pw.setRange(xRange=(0, _width), yRange=(0, _height), padding=0, update=True, disableAutoRange=True)
pw.resize(_width, _height)
pw.setAspectLocked(lock=True, ratio=1)
pw.setMouseEnabled(x=False, y=False)
layout.addWidget(pw)

window.setLayout(layout)
window.show()

# Plot a square                                                                                                                 
pw.plot([_width/4, _width/2, _width/2, _width/4, _width/4], [_width/4, _width/4, _width/2, _width/2, _width/4])

sys.exit(app.exec_())


# Matplotlib version
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(15,5))  # aspect ratio 3:1                            
ax1 = fig.add_subplot(1,1,1, adjustable='box', aspect=1)
ax1.plot(range(10))  # axes are 1:1 within the 3:1 figure                       
plt.show()
Screen Shot 2021-08-24 at 11.05.44 PM.png
Screen Shot 2021-08-24 at 11.21.17 PM.png

Patrick

unread,
Aug 25, 2021, 1:04:13 AM8/25/21
to pyqtgraph
Hi,

Yeah, this is actually harder than you'd think since when everything is in a full UI layout with windows resizing etc the extra space has to come or go from somewhere... You need to either restrict the zoom range of each axis so that the other doesn't exceed its limits (but that will stop a full zoom-out of that axis), or dynamically grow or shrink the whole plot area and pad out with empty space.

If you want to go the first path (restricting zoom based on size of other axis), you'll need to do a bunch of manual handling of the ViewBox signals such as sigRangeChanged, sigResized etc:
That should work but would a bit tedious.

Your comment about matplotlib reminded me I had to work around something similar previously when embedding matplotlib multi-panel plot into a Qt Ul, where I wanted the whole plot area to maintain a fixed aspect ratio. It turns out set_aspect doesn't work with twinned and shared axes. It was adapted from something I found elsewhere on the internet so it's a bit hacky and not well documented, sorry. Basically it's a QWidget that you insert another widget inside (eg. PlotWidget or similar) and it handles the extra padding required to keep the widget at a fixed ratio.

#import stuff here as needed...
class AspectRatioWidget(QWidget):
    """A widget that will maintain a specified aspect ratio.
    Good for plots where we want to fill the maximum space without stretching the aspect ratio."""

    def __init__(self, widget, parent, aspect_ratio):
        super().__init__(parent)
        self.aspect_ratio = aspect_ratio
        self.setLayout(QBoxLayout(QBoxLayout.LeftToRight, self))
        self.layout().addItem(QSpacerItem(0, 0))
        self.layout().addWidget(widget)
        self.layout().addItem(QSpacerItem(0, 0))

    def setAspectRatio(self, aspect_ratio):
        self.aspect_ratio = aspect_ratio
        self._adjust_ratio(self.geometry().width(), self.geometry().height());

    def resizeEvent(self, e):
        self._adjust_ratio(e.size().width(), e.size().height())

    def _adjust_ratio(self, w, h):
        if w / h > self.aspect_ratio:  # too wide
            self.layout().setDirection(QBoxLayout.LeftToRight)
            widget_stretch = h * self.aspect_ratio
            outer_stretch = (w - widget_stretch) / 2 + 0.5
        else:  # too tall
            self.layout().setDirection(QBoxLayout.TopToBottom)
            widget_stretch = w / self.aspect_ratio
            outer_stretch = (h - widget_stretch) / 2 + 0.5

        self.layout().setStretch(0, outer_stretch)
        self.layout().setStretch(1, widget_stretch)
        self.layout().setStretch(2, outer_stretch)


It was used something like this. You'll need to adapt the matplotlib FigureCanvas part to be a pyqtgraph PlotWidget or GraphicsLayoutWidget or similar.

        self.resultplot_figureCanvas = FigureCanvas(mpl.figure.Figure(constrained_layout=True))
        # A widget hacky thing to keep fixed aspect ratio of plot, since matplotlib.axes.Axes.set_aspect doesn't work
        # when there is both twinned and shared axes...
        self.resultplot_aspectwidget = AspectRatioWidget(self.resultplot_figureCanvas, parent=self, aspect_ratio=1.5)
        self.resultplots_groupBox.layout().addWidget(self.resultplot_aspectwidget)



Patrick

James Battat

unread,
Aug 25, 2021, 1:29:41 PM8/25/21
to pyqt...@googlegroups.com
Thank you for the information and recommendations. I was afraid that it might be fairly subtle/complicated, but your code example will be a big help.

James

--
You received this message because you are subscribed to a topic in the Google Groups "pyqtgraph" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/pyqtgraph/noNWNG3RBg0/unsubscribe.
To unsubscribe from this group and all its topics, send an email to pyqtgraph+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/pyqtgraph/21011612-577b-4069-8c31-4efa52c4bb33n%40googlegroups.com.

Reply all
Reply to author
Forward
0 new messages