Surface Example

Using Q3DSurface in a widget application.

The surface example shows how to make a simple 3D surface graph using Q3DSurface and combining the use of widgets for adjusting several adjustable qualities. This example demonstrates the following features:

  • How to set up a basic QSurfaceDataProxy and set data for it.

  • How to use QHeightMapSurfaceDataProxy for showing 3D height maps.

  • Three different selection modes for studying the graph.

  • Axis range usage for displaying selected portions of the graph.

  • Changing theme.

  • How to set a custom surface gradient.

For instructions about how to interact with the graph, see this page.

Surface Example Screenshot
import sys

from PySide6.QtCore import QSize, Qt
from PySide6.QtDataVisualization import Q3DSurface
from PySide6.QtGui import QBrush, QIcon, QLinearGradient, QPainter, QPixmap
from PySide6.QtWidgets import (QApplication, QComboBox, QGroupBox, QHBoxLayout,
                               QLabel, QMessageBox, QPushButton, QRadioButton,
                               QSizePolicy, QSlider, QVBoxLayout, QWidget)

from surfacegraph import SurfaceGraph

THEMES = ["Qt", "Primary Colors", "Digia", "Stone Moss", "Army Blue", "Retro", "Ebony", "Isabelle"]


if __name__ == "__main__":
    app = QApplication(sys.argv)
    graph = Q3DSurface()
    container = QWidget.createWindowContainer(graph)

    if not graph.hasContext():
        msgBox = QMessageBox()
        msgBox.setText("Couldn't initialize the OpenGL context.")
        msgBox.exec()
        sys.exit(-1)

    screenSize = graph.screen().size()
    container.setMinimumSize(QSize(screenSize.width() / 2, screenSize.height() / 1.6))
    container.setMaximumSize(screenSize)
    container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
    container.setFocusPolicy(Qt.StrongFocus)

    widget = QWidget()
    hLayout = QHBoxLayout(widget)
    vLayout = QVBoxLayout()
    hLayout.addWidget(container, 1)
    hLayout.addLayout(vLayout)
    vLayout.setAlignment(Qt.AlignTop)

    widget.setWindowTitle("Surface example")

    modelGroupBox = QGroupBox("Model")

    sqrtSinModelRB = QRadioButton(widget)
    sqrtSinModelRB.setText("Sqrt& Sin")
    sqrtSinModelRB.setChecked(False)

    heightMapModelRB = QRadioButton(widget)
    heightMapModelRB.setText("Height Map")
    heightMapModelRB.setChecked(False)

    modelVBox = QVBoxLayout()
    modelVBox.addWidget(sqrtSinModelRB)
    modelVBox.addWidget(heightMapModelRB)
    modelGroupBox.setLayout(modelVBox)

    selectionGroupBox = QGroupBox("Selection Mode")

    modeNoneRB = QRadioButton(widget)
    modeNoneRB.setText("No selection")
    modeNoneRB.setChecked(False)

    modeItemRB = QRadioButton(widget)
    modeItemRB.setText("Item")
    modeItemRB.setChecked(False)

    modeSliceRowRB = QRadioButton(widget)
    modeSliceRowRB.setText("Row Slice")
    modeSliceRowRB.setChecked(False)

    modeSliceColumnRB = QRadioButton(widget)
    modeSliceColumnRB.setText("Column Slice")
    modeSliceColumnRB.setChecked(False)

    selectionVBox = QVBoxLayout()
    selectionVBox.addWidget(modeNoneRB)
    selectionVBox.addWidget(modeItemRB)
    selectionVBox.addWidget(modeSliceRowRB)
    selectionVBox.addWidget(modeSliceColumnRB)
    selectionGroupBox.setLayout(selectionVBox)

    axisMinSliderX = QSlider(Qt.Horizontal, widget)
    axisMinSliderX.setMinimum(0)
    axisMinSliderX.setTickInterval(1)
    axisMinSliderX.setEnabled(True)
    axisMaxSliderX = QSlider(Qt.Horizontal, widget)
    axisMaxSliderX.setMinimum(1)
    axisMaxSliderX.setTickInterval(1)
    axisMaxSliderX.setEnabled(True)
    axisMinSliderZ = QSlider(Qt.Horizontal, widget)
    axisMinSliderZ.setMinimum(0)
    axisMinSliderZ.setTickInterval(1)
    axisMinSliderZ.setEnabled(True)
    axisMaxSliderZ = QSlider(Qt.Horizontal, widget)
    axisMaxSliderZ.setMinimum(1)
    axisMaxSliderZ.setTickInterval(1)
    axisMaxSliderZ.setEnabled(True)

    themeList = QComboBox(widget)
    themeList.addItems(THEMES)

    colorGroupBox = QGroupBox("Custom gradient")

    grBtoY = QLinearGradient(0, 0, 1, 100)
    grBtoY.setColorAt(1.0, Qt.black)
    grBtoY.setColorAt(0.67, Qt.blue)
    grBtoY.setColorAt(0.33, Qt.red)
    grBtoY.setColorAt(0.0, Qt.yellow)

    pm = QPixmap(24, 100)
    pmp = QPainter(pm)
    pmp.setBrush(QBrush(grBtoY))
    pmp.setPen(Qt.NoPen)
    pmp.drawRect(0, 0, 24, 100)
    pmp.end()

    gradientBtoYPB = QPushButton(widget)
    gradientBtoYPB.setIcon(QIcon(pm))
    gradientBtoYPB.setIconSize(QSize(24, 100))

    grGtoR = QLinearGradient(0, 0, 1, 100)
    grGtoR.setColorAt(1.0, Qt.darkGreen)
    grGtoR.setColorAt(0.5, Qt.yellow)
    grGtoR.setColorAt(0.2, Qt.red)
    grGtoR.setColorAt(0.0, Qt.darkRed)
    pmp.begin(pm)
    pmp.setBrush(QBrush(grGtoR))
    pmp.drawRect(0, 0, 24, 100)
    pmp.end()

    gradientGtoRPB = QPushButton(widget)
    gradientGtoRPB.setIcon(QIcon(pm))
    gradientGtoRPB.setIconSize(QSize(24, 100))

    colorHBox = QHBoxLayout()
    colorHBox.addWidget(gradientBtoYPB)
    colorHBox.addWidget(gradientGtoRPB)
    colorGroupBox.setLayout(colorHBox)

    vLayout.addWidget(modelGroupBox)
    vLayout.addWidget(selectionGroupBox)
    vLayout.addWidget(QLabel("Column range"))
    vLayout.addWidget(axisMinSliderX)
    vLayout.addWidget(axisMaxSliderX)
    vLayout.addWidget(QLabel("Row range"))
    vLayout.addWidget(axisMinSliderZ)
    vLayout.addWidget(axisMaxSliderZ)
    vLayout.addWidget(QLabel("Theme"))
    vLayout.addWidget(themeList)
    vLayout.addWidget(colorGroupBox)

    widget.show()

    modifier = SurfaceGraph(graph)

    heightMapModelRB.toggled.connect(modifier.enableHeightMapModel)
    sqrtSinModelRB.toggled.connect(modifier.enableSqrtSinModel)
    modeNoneRB.toggled.connect(modifier.toggleModeNone)
    modeItemRB.toggled.connect(modifier.toggleModeItem)
    modeSliceRowRB.toggled.connect(modifier.toggleModeSliceRow)
    modeSliceColumnRB.toggled.connect(modifier.toggleModeSliceColumn)
    axisMinSliderX.valueChanged.connect(modifier.adjustXMin)
    axisMaxSliderX.valueChanged.connect(modifier.adjustXMax)
    axisMinSliderZ.valueChanged.connect(modifier.adjustZMin)
    axisMaxSliderZ.valueChanged.connect(modifier.adjustZMax)
    themeList.currentIndexChanged[int].connect(modifier.changeTheme)
    gradientBtoYPB.pressed.connect(modifier.setBlackToYellowGradient)
    gradientGtoRPB.pressed.connect(modifier.setGreenToRedGradient)

    modifier.setAxisMinSliderX(axisMinSliderX)
    modifier.setAxisMaxSliderX(axisMaxSliderX)
    modifier.setAxisMinSliderZ(axisMinSliderZ)
    modifier.setAxisMaxSliderZ(axisMaxSliderZ)

    sqrtSinModelRB.setChecked(True)
    modeItemRB.setChecked(True)
    themeList.setCurrentIndex(2)

    sys.exit(app.exec())
import math

from PySide6.QtCore import QObject, Qt, Slot
from PySide6.QtDataVisualization import (Q3DTheme, QAbstract3DGraph,
                                         QHeightMapSurfaceDataProxy,
                                         QSurface3DSeries, QSurfaceDataItem,
                                         QSurfaceDataProxy, QValue3DAxis)
from PySide6.QtGui import QImage, QLinearGradient, QVector3D
from PySide6.QtWidgets import QSlider

sampleCountX = 50
sampleCountZ = 50
heightMapGridStepX = 6
heightMapGridStepZ = 6
sampleMin = -8.0
sampleMax = 8.0


class SurfaceGraph(QObject):
    def __init__(self, surface, parent=None):
        QObject.__init__(self, parent)

        self.m_graph = surface
        self.m_graph.setAxisX(QValue3DAxis())
        self.m_graph.setAxisY(QValue3DAxis())
        self.m_graph.setAxisZ(QValue3DAxis())

        self.m_sqrtSinProxy = QSurfaceDataProxy()
        self.m_sqrtSinSeries = QSurface3DSeries(self.m_sqrtSinProxy)
        self.fillSqrtSinProxy()

        heightMapImage = QImage("mountain.png")
        self.m_heightMapProxy = QHeightMapSurfaceDataProxy(heightMapImage)
        self.m_heightMapSeries = QSurface3DSeries(self.m_heightMapProxy)
        self.m_heightMapSeries.setItemLabelFormat("(@xLabel, @zLabel): @yLabel")
        self.m_heightMapProxy.setValueRanges(34.0, 40.0, 18.0, 24.0)

        self.m_heightMapWidth = heightMapImage.width()
        self.m_heightMapHeight = heightMapImage.height()

        self.m_axisMinSliderX = QSlider()
        self.m_axisMaxSliderX = QSlider()
        self.m_axisMinSliderZ = QSlider()
        self.m_axisMaxSliderZ = QSlider()
        self.m_rangeMinX = 0.0
        self.m_rangeMinZ = 0.0
        self.m_stepX = 0.0
        self.m_stepZ = 0.0

    def fillSqrtSinProxy(self):
        stepX = (sampleMax - sampleMin) / float(sampleCountX - 1)
        stepZ = (sampleMax - sampleMin) / float(sampleCountZ - 1)

        dataArray = []
        for i in range(sampleCountZ):
            newRow = []
            # Keep values within range bounds, since just adding step can cause
            # minor drift due to the rounding errors.
            z = min(sampleMax, (i * stepZ + sampleMin))
            for j in range(sampleCountX):
                x = min(sampleMax, (j * stepX + sampleMin))
                R = math.sqrt(z * z + x * x) + 0.01
                y = (math.sin(R) / R + 0.24) * 1.61
                newRow.append(QSurfaceDataItem(QVector3D(x, y, z)))
            dataArray.append(newRow)

        self.m_sqrtSinProxy.resetArray(dataArray)

    def enableSqrtSinModel(self, enable):
        if enable:
            self.m_sqrtSinSeries.setDrawMode(QSurface3DSeries.DrawSurfaceAndWireframe)
            self.m_sqrtSinSeries.setFlatShadingEnabled(True)

            self.m_graph.axisX().setLabelFormat("%.2f")
            self.m_graph.axisZ().setLabelFormat("%.2f")
            self.m_graph.axisX().setRange(sampleMin, sampleMax)
            self.m_graph.axisY().setRange(0.0, 2.0)
            self.m_graph.axisZ().setRange(sampleMin, sampleMax)
            self.m_graph.axisX().setLabelAutoRotation(30)
            self.m_graph.axisY().setLabelAutoRotation(90)
            self.m_graph.axisZ().setLabelAutoRotation(30)

            self.m_graph.removeSeries(self.m_heightMapSeries)
            self.m_graph.addSeries(self.m_sqrtSinSeries)

            # Reset range sliders for Sqrt&Sin
            self.m_rangeMinX = sampleMin
            self.m_rangeMinZ = sampleMin
            self.m_stepX = (sampleMax - sampleMin) / float(sampleCountX - 1)
            self.m_stepZ = (sampleMax - sampleMin) / float(sampleCountZ - 1)
            self.m_axisMinSliderX.setMaximum(sampleCountX - 2)
            self.m_axisMinSliderX.setValue(0)
            self.m_axisMaxSliderX.setMaximum(sampleCountX - 1)
            self.m_axisMaxSliderX.setValue(sampleCountX - 1)
            self.m_axisMinSliderZ.setMaximum(sampleCountZ - 2)
            self.m_axisMinSliderZ.setValue(0)
            self.m_axisMaxSliderZ.setMaximum(sampleCountZ - 1)
            self.m_axisMaxSliderZ.setValue(sampleCountZ - 1)

    def enableHeightMapModel(self, enable):
        if enable:
            self.m_heightMapSeries.setDrawMode(QSurface3DSeries.DrawSurface)
            self.m_heightMapSeries.setFlatShadingEnabled(False)

            self.m_graph.axisX().setLabelFormat("%.1f N")
            self.m_graph.axisZ().setLabelFormat("%.1f E")
            self.m_graph.axisX().setRange(34.0, 40.0)
            self.m_graph.axisY().setAutoAdjustRange(True)
            self.m_graph.axisZ().setRange(18.0, 24.0)

            self.m_graph.axisX().setTitle("Latitude")
            self.m_graph.axisY().setTitle("Height")
            self.m_graph.axisZ().setTitle("Longitude")

            self.m_graph.removeSeries(self.m_sqrtSinSeries)
            self.m_graph.addSeries(self.m_heightMapSeries)

            # Reset range sliders for height map
            mapGridCountX = self.m_heightMapWidth / heightMapGridStepX
            mapGridCountZ = self.m_heightMapHeight / heightMapGridStepZ
            self.m_rangeMinX = 34.0
            self.m_rangeMinZ = 18.0
            self.m_stepX = 6.0 / float(mapGridCountX - 1)
            self.m_stepZ = 6.0 / float(mapGridCountZ - 1)
            self.m_axisMinSliderX.setMaximum(mapGridCountX - 2)
            self.m_axisMinSliderX.setValue(0)
            self.m_axisMaxSliderX.setMaximum(mapGridCountX - 1)
            self.m_axisMaxSliderX.setValue(mapGridCountX - 1)
            self.m_axisMinSliderZ.setMaximum(mapGridCountZ - 2)
            self.m_axisMinSliderZ.setValue(0)
            self.m_axisMaxSliderZ.setMaximum(mapGridCountZ - 1)
            self.m_axisMaxSliderZ.setValue(mapGridCountZ - 1)

    def adjustXMin(self, minimum):
        minX = self.m_stepX * float(minimum) + self.m_rangeMinX

        maximum = self.m_axisMaxSliderX.value()
        if minimum >= maximum:
            maximum = minimum + 1
            self.m_axisMaxSliderX.setValue(maximum)
        maxX = self.m_stepX * maximum + self.m_rangeMinX

        self.setAxisXRange(minX, maxX)

    def adjustXMax(self, maximum):
        maxX = self.m_stepX * float(maximum) + self.m_rangeMinX

        minimum = self.m_axisMinSliderX.value()
        if maximum <= minimum:
            minimum = maximum - 1
            self.m_axisMinSliderX.setValue(minimum)
        minX = self.m_stepX * minimum + self.m_rangeMinX

        self.setAxisXRange(minX, maxX)

    def adjustZMin(self, minimum):
        minZ = self.m_stepZ * float(minimum) + self.m_rangeMinZ

        maximum = self.m_axisMaxSliderZ.value()
        if minimum >= maximum:
            maximum = minimum + 1
            self.m_axisMaxSliderZ.setValue(maximum)
        maxZ = self.m_stepZ * maximum + self.m_rangeMinZ

        self.setAxisZRange(minZ, maxZ)

    def adjustZMax(self, maximum):
        maxX = self.m_stepZ * float(maximum) + self.m_rangeMinZ

        minimum = self.m_axisMinSliderZ.value()
        if maximum <= minimum:
            minimum = maximum - 1
            self.m_axisMinSliderZ.setValue(minimum)
        minX = self.m_stepZ * minimum + self.m_rangeMinZ

        self.setAxisZRange(minX, maxX)

    def setAxisXRange(self, minimum, maximum):
        self.m_graph.axisX().setRange(minimum, maximum)

    def setAxisZRange(self, minimum, maximum):
        self.m_graph.axisZ().setRange(minimum, maximum)

    @Slot()
    def changeTheme(self, theme):
        self.m_graph.activeTheme().setType(Q3DTheme.Theme(theme))

    def setBlackToYellowGradient(self):
        gr = QLinearGradient()
        gr.setColorAt(0.0, Qt.black)
        gr.setColorAt(0.33, Qt.blue)
        gr.setColorAt(0.67, Qt.red)
        gr.setColorAt(1.0, Qt.yellow)

        self.m_graph.seriesList()[0].setBaseGradient(gr)
        self.m_graph.seriesList()[0].setColorStyle(Q3DTheme.ColorStyleRangeGradient)

    def setGreenToRedGradient(self):
        gr = QLinearGradient()
        gr.setColorAt(0.0, Qt.darkGreen)
        gr.setColorAt(0.5, Qt.yellow)
        gr.setColorAt(0.8, Qt.red)
        gr.setColorAt(1.0, Qt.darkRed)

        series = self.m_graph.seriesList()[0]
        series.setBaseGradient(gr)
        series.setColorStyle(Q3DTheme.ColorStyleRangeGradient)

    def toggleModeNone(self):
        self.m_graph.setSelectionMode(QAbstract3DGraph.SelectionNone)

    def toggleModeItem(self):
        self.m_graph.setSelectionMode(QAbstract3DGraph.SelectionItem)

    def toggleModeSliceRow(self):
        self.m_graph.setSelectionMode(
            QAbstract3DGraph.SelectionItemAndRow | QAbstract3DGraph.SelectionSlice
        )

    def toggleModeSliceColumn(self):
        self.m_graph.setSelectionMode(
            QAbstract3DGraph.SelectionItemAndColumn | QAbstract3DGraph.SelectionSlice
        )

    def setAxisMinSliderX(self, slider):
        self.m_axisMinSliderX = slider

    def setAxisMaxSliderX(self, slider):
        self.m_axisMaxSliderX = slider

    def setAxisMinSliderZ(self, slider):
        self.m_axisMinSliderZ = slider

    def setAxisMaxSliderZ(self, slider):
        self.m_axisMaxSliderZ = slider