Search code examples
pythonopenglpyqtgraphpyopenglqgraphicstextitem

How to add Axis features (labels, ticks, values) to a 3D plot with GLViewWidget of pyqtgraph?


I want to add axis info such as labels, ticks and values to a 3D scene created with the pyqtgraph.opengl.GLViewWidget module. There is already a very simple axis drawing option with GLAxisItem, but with this you can only control the length of the axes.

I've extended GLAxisItem to change the axis color, but can't see the way to include these other features.

Here's an example:

from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph.opengl as gl
import pyqtgraph as pg
import OpenGL.GL as ogl
import numpy as np

class CustomTextItem(gl.GLGraphicsItem.GLGraphicsItem):
    def __init__(self, X, Y, Z, text):
        gl.GLGraphicsItem.GLGraphicsItem.__init__(self)
        self.text = text
        self.X = X
        self.Y = Y
        self.Z = Z

    def setGLViewWidget(self, GLViewWidget):
        self.GLViewWidget = GLViewWidget

    def setText(self, text):
        self.text = text
        self.update()

    def setX(self, X):
        self.X = X
        self.update()

    def setY(self, Y):
        self.Y = Y
        self.update()

    def setZ(self, Z):
        self.Z = Z
        self.update()

    def paint(self):
        self.GLViewWidget.qglColor(QtCore.Qt.black)
        self.GLViewWidget.renderText(self.X, self.Y, self.Z, self.text)


class Custom3DAxis(gl.GLAxisItem):
    """Class defined to extend 'gl.GLAxisItem'."""
    def __init__(self, parent, color=(0,0,0,.6)):
        gl.GLAxisItem.__init__(self)
        self.parent = parent
        self.c = color

    def draw_labels(self):
        x,y,z = self.size()
        #X label
        self.xLabel = CustomTextItem(X=x/2, Y=-y/20, Z=-z/20, text="X")
        self.xLabel.setGLViewWidget(self.parent)
        self.parent.addItem(self.xLabel)
        #Y label
        self.yLabel = CustomTextItem(X=-x/20, Y=y/2, Z=-z/20, text="Y")
        self.yLabel.setGLViewWidget(self.parent)
        self.parent.addItem(self.yLabel)
        #Z label
        self.zLabel = CustomTextItem(X=-x/20, Y=-y/20, Z=z/2, text="Z")
        self.zLabel.setGLViewWidget(self.parent)
        self.parent.addItem(self.zLabel)

    def paint(self):
        self.setupGLState()
        if self.antialias:
            ogl.glEnable(ogl.GL_LINE_SMOOTH)
            ogl.glHint(ogl.GL_LINE_SMOOTH_HINT, ogl.GL_NICEST)
        ogl.glBegin(ogl.GL_LINES)

        x,y,z = self.size()
        #Draw Z
        ogl.glColor4f(self.c[0], self.c[1], self.c[2], self.c[3])
        ogl.glVertex3f(0, 0, 0)
        ogl.glVertex3f(0, 0, z)
        #Draw Y
        ogl.glColor4f(self.c[0], self.c[1], self.c[2], self.c[3])
        ogl.glVertex3f(0, 0, 0)
        ogl.glVertex3f(0, y, 0)
        #Draw X
        ogl.glColor4f(self.c[0], self.c[1], self.c[2], self.c[3])
        ogl.glVertex3f(0, 0, 0)
        ogl.glVertex3f(x, 0, 0)
        #Draw labels
        self.draw_labels()
        ogl.glEnd()


app = QtGui.QApplication([])
fig1 = gl.GLViewWidget()
background_color = app.palette().color(QtGui.QPalette.Background)
fig1.setBackgroundColor(background_color)

n = 51
y = np.linspace(-10,10,n)
x = np.linspace(-10,10,100)
for i in range(n):
    yi = np.array([y[i]]*100)
    d = (x**2 + yi**2)**0.5
    z = 10 * np.cos(d) / (d+1)
    pts = np.vstack([x,yi,z]).transpose()
    plt = gl.GLLinePlotItem(pos=pts, color=pg.glColor((i,n*1.3)), width=(i+1)/10., antialias=True)
    fig1.addItem(plt)


axis = Custom3DAxis(fig1, color=(0.2,0.2,0.2,.6))
axis.setSize(x=12, y=12, z=12)
fig1.addItem(axis)
fig1.opts['distance'] = 40

fig1.show()

if __name__ == '__main__':
    import sys
    if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
        QtGui.QApplication.instance().exec_()

It would be perfect if those features didn't change size with zooming.

--- Update ---

From here, I got how to create custom text items and extended my Custom3DAxis class to include X, Y and Z labels (code above is updated with it). I imagine that's the way to go to further include values and other things.

However, this solution causes the rendering to become very slow at each rotation/span (that is, at each scene update), just because of these 3 text items!

Does anyone have a clue on why is that? What should I do to avoid this?


Solution

  • Ok, I got a reasonable solution: creating text items for each label and value to be added. The slowing problem was caused because, in the questions code, more of the new items were being added (instead of just updating the initial ones) at each scene update. Here's a code that solves the question:

    from pyqtgraph.Qt import QtCore, QtGui
    import pyqtgraph.opengl as gl
    import pyqtgraph as pg
    import OpenGL.GL as ogl
    import numpy as np
    
    class CustomTextItem(gl.GLGraphicsItem.GLGraphicsItem):
        def __init__(self, X, Y, Z, text):
            gl.GLGraphicsItem.GLGraphicsItem.__init__(self)
            self.text = text
            self.X = X
            self.Y = Y
            self.Z = Z
    
        def setGLViewWidget(self, GLViewWidget):
            self.GLViewWidget = GLViewWidget
    
        def setText(self, text):
            self.text = text
            self.update()
    
        def setX(self, X):
            self.X = X
            self.update()
    
        def setY(self, Y):
            self.Y = Y
            self.update()
    
        def setZ(self, Z):
            self.Z = Z
            self.update()
    
        def paint(self):
            self.GLViewWidget.qglColor(QtCore.Qt.black)
            self.GLViewWidget.renderText(self.X, self.Y, self.Z, self.text)
    
    
    class Custom3DAxis(gl.GLAxisItem):
        """Class defined to extend 'gl.GLAxisItem'."""
        def __init__(self, parent, color=(0,0,0,.6)):
            gl.GLAxisItem.__init__(self)
            self.parent = parent
            self.c = color
    
        def add_labels(self):
            """Adds axes labels."""
            x,y,z = self.size()
            #X label
            self.xLabel = CustomTextItem(X=x/2, Y=-y/20, Z=-z/20, text="X")
            self.xLabel.setGLViewWidget(self.parent)
            self.parent.addItem(self.xLabel)
            #Y label
            self.yLabel = CustomTextItem(X=-x/20, Y=y/2, Z=-z/20, text="Y")
            self.yLabel.setGLViewWidget(self.parent)
            self.parent.addItem(self.yLabel)
            #Z label
            self.zLabel = CustomTextItem(X=-x/20, Y=-y/20, Z=z/2, text="Z")
            self.zLabel.setGLViewWidget(self.parent)
            self.parent.addItem(self.zLabel)
    
        def add_tick_values(self, xticks=[], yticks=[], zticks=[]):
            """Adds ticks values."""
            x,y,z = self.size()
            xtpos = np.linspace(0, x, len(xticks))
            ytpos = np.linspace(0, y, len(yticks))
            ztpos = np.linspace(0, z, len(zticks))
            #X label
            for i, xt in enumerate(xticks):
                val = CustomTextItem(X=xtpos[i], Y=-y/20, Z=-z/20, text=str(xt))
                val.setGLViewWidget(self.parent)
                self.parent.addItem(val)
            #Y label
            for i, yt in enumerate(yticks):
                val = CustomTextItem(X=-x/20, Y=ytpos[i], Z=-z/20, text=str(yt))
                val.setGLViewWidget(self.parent)
                self.parent.addItem(val)
            #Z label
            for i, zt in enumerate(zticks):
                val = CustomTextItem(X=-x/20, Y=-y/20, Z=ztpos[i], text=str(zt))
                val.setGLViewWidget(self.parent)
                self.parent.addItem(val)
    
        def paint(self):
            self.setupGLState()
            if self.antialias:
                ogl.glEnable(ogl.GL_LINE_SMOOTH)
                ogl.glHint(ogl.GL_LINE_SMOOTH_HINT, ogl.GL_NICEST)
            ogl.glBegin(ogl.GL_LINES)
    
            x,y,z = self.size()
            #Draw Z
            ogl.glColor4f(self.c[0], self.c[1], self.c[2], self.c[3])
            ogl.glVertex3f(0, 0, 0)
            ogl.glVertex3f(0, 0, z)
            #Draw Y
            ogl.glColor4f(self.c[0], self.c[1], self.c[2], self.c[3])
            ogl.glVertex3f(0, 0, 0)
            ogl.glVertex3f(0, y, 0)
            #Draw X
            ogl.glColor4f(self.c[0], self.c[1], self.c[2], self.c[3])
            ogl.glVertex3f(0, 0, 0)
            ogl.glVertex3f(x, 0, 0)
            ogl.glEnd()
    
    
    app = QtGui.QApplication([])
    fig1 = gl.GLViewWidget()
    background_color = app.palette().color(QtGui.QPalette.Background)
    fig1.setBackgroundColor(background_color)
    
    n = 51
    y = np.linspace(-10,10,n)
    x = np.linspace(-10,10,100)
    for i in range(n):
        yi = np.array([y[i]]*100)
        d = (x**2 + yi**2)**0.5
        z = 10 * np.cos(d) / (d+1)
        pts = np.vstack([x,yi,z]).transpose()
        plt = gl.GLLinePlotItem(pos=pts, color=pg.glColor((i,n*1.3)), width=(i+1)/10., antialias=True)
        fig1.addItem(plt)
    
    
    axis = Custom3DAxis(fig1, color=(0.2,0.2,0.2,.6))
    axis.setSize(x=12, y=12, z=12)
    # Add axes labels
    axis.add_labels()
    # Add axes tick values
    axis.add_tick_values(xticks=[0,4,8,12], yticks=[0,6,12], zticks=[0,3,6,9,12])
    fig1.addItem(axis)
    fig1.opts['distance'] = 40
    
    fig1.show()
    
    if __name__ == '__main__':
        import sys
        if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
            QtGui.QApplication.instance().exec_()