aboutsummaryrefslogtreecommitdiffstats
path: root/examples/graphs
diff options
context:
space:
mode:
Diffstat (limited to 'examples/graphs')
-rw-r--r--examples/graphs/2d/hellographs/HelloGraphs/Main.qml153
-rw-r--r--examples/graphs/2d/hellographs/HelloGraphs/qmldir2
-rw-r--r--examples/graphs/2d/hellographs/doc/hellographs.rst51
-rw-r--r--examples/graphs/2d/hellographs/doc/hellographs.webpbin0 -> 60020 bytes
-rw-r--r--examples/graphs/2d/hellographs/hellographs.pyproject3
-rw-r--r--examples/graphs/2d/hellographs/main.py22
-rw-r--r--examples/graphs/3d/minimalsurfacegraph/doc/minimalsurfacegraph.rst4
-rw-r--r--examples/graphs/3d/minimalsurfacegraph/main.py54
-rw-r--r--examples/graphs/3d/widgetgallery/axesinputhandler.py100
-rw-r--r--examples/graphs/3d/widgetgallery/bargraph.py272
-rw-r--r--examples/graphs/3d/widgetgallery/custominputhandler.py177
-rw-r--r--examples/graphs/3d/widgetgallery/data/layer_1.pngbin0 -> 34540 bytes
-rw-r--r--examples/graphs/3d/widgetgallery/data/layer_2.pngbin0 -> 10553 bytes
-rw-r--r--examples/graphs/3d/widgetgallery/data/layer_3.pngbin0 -> 7119 bytes
-rw-r--r--examples/graphs/3d/widgetgallery/data/license.txt77
-rw-r--r--examples/graphs/3d/widgetgallery/data/maptexture.jpgbin0 -> 352922 bytes
-rw-r--r--examples/graphs/3d/widgetgallery/data/narrowarrow.meshbin0 -> 15420 bytes
-rw-r--r--examples/graphs/3d/widgetgallery/data/oilrig.meshbin0 -> 69728 bytes
-rw-r--r--examples/graphs/3d/widgetgallery/data/pipe.meshbin0 -> 4760 bytes
-rw-r--r--examples/graphs/3d/widgetgallery/data/raindata.txt158
-rw-r--r--examples/graphs/3d/widgetgallery/data/refinery.meshbin0 -> 75216 bytes
-rw-r--r--examples/graphs/3d/widgetgallery/data/topography.pngbin0 -> 395504 bytes
-rw-r--r--examples/graphs/3d/widgetgallery/doc/widgetgallery.rst11
-rw-r--r--examples/graphs/3d/widgetgallery/doc/widgetgallery.webpbin0 -> 93150 bytes
-rw-r--r--examples/graphs/3d/widgetgallery/graphmodifier.py391
-rw-r--r--examples/graphs/3d/widgetgallery/highlightseries.py94
-rw-r--r--examples/graphs/3d/widgetgallery/main.py41
-rw-r--r--examples/graphs/3d/widgetgallery/rainfalldata.py125
-rw-r--r--examples/graphs/3d/widgetgallery/scatterdatamodifier.py149
-rw-r--r--examples/graphs/3d/widgetgallery/scattergraph.py121
-rw-r--r--examples/graphs/3d/widgetgallery/surfacegraph.py256
-rw-r--r--examples/graphs/3d/widgetgallery/surfacegraphmodifier.py641
-rw-r--r--examples/graphs/3d/widgetgallery/topographicseries.py57
-rw-r--r--examples/graphs/3d/widgetgallery/variantbardatamapping.py67
-rw-r--r--examples/graphs/3d/widgetgallery/variantbardataproxy.py100
-rw-r--r--examples/graphs/3d/widgetgallery/variantdataset.py39
-rw-r--r--examples/graphs/3d/widgetgallery/widgetgallery.pyproject29
37 files changed, 3194 insertions, 0 deletions
diff --git a/examples/graphs/2d/hellographs/HelloGraphs/Main.qml b/examples/graphs/2d/hellographs/HelloGraphs/Main.qml
new file mode 100644
index 000000000..b1844aec4
--- /dev/null
+++ b/examples/graphs/2d/hellographs/HelloGraphs/Main.qml
@@ -0,0 +1,153 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Layouts
+import QtGraphs
+
+Item {
+ id: mainView
+ width: 1280
+ height: 720
+
+ RowLayout {
+ id: graphsRow
+
+ readonly property real margin: mainView.width * 0.02
+
+ anchors.fill: parent
+ anchors.margins: margin
+ spacing: margin
+
+ Rectangle {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ color: "#262626"
+ border.color: "#4d4d4d"
+ border.width: 1
+ radius: graphsRow.margin
+ //! [bargraph]
+ GraphsView {
+ anchors.fill: parent
+ anchors.margins: 16
+ theme: GraphTheme {
+ colorTheme: GraphTheme.ColorThemeDark
+ }
+ //! [bargraph]
+ //! [barseries]
+ BarSeries {
+ axisX: BarCategoryAxis {
+ categories: [2024, 2025, 2026]
+ gridVisible: false
+ minorGridVisible: false
+ }
+ axisY: ValueAxis {
+ min: 20
+ max: 100
+ tickInterval: 10
+ minorTickCount: 9
+ }
+ //! [barseries]
+ //! [barset]
+ BarSet {
+ values: [82, 50, 75]
+ borderWidth: 2
+ color: "#373F26"
+ borderColor: "#DBEB00"
+ }
+ //! [barset]
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ color: "#262626"
+ border.color: "#4d4d4d"
+ border.width: 1
+ radius: graphsRow.margin
+
+ //! [linegraph]
+ GraphsView {
+ anchors.fill: parent
+ anchors.margins: 16
+ theme: GraphTheme {
+ readonly property color c1: "#DBEB00"
+ readonly property color c2: "#373F26"
+ readonly property color c3: Qt.lighter(c2, 1.5)
+ colorTheme: GraphTheme.ColorThemeDark
+ gridMajorBarsColor: c3
+ gridMinorBarsColor: c2
+ axisXMajorColor: c3
+ axisYMajorColor: c3
+ axisXMinorColor: c2
+ axisYMinorColor: c2
+ axisXLabelsColor: c1
+ axisYLabelsColor: c1
+ }
+ //! [linegraph]
+
+ //! [linemarker]
+ component Marker : Rectangle {
+ width: 16
+ height: 16
+ color: "#ffffff"
+ radius: width * 0.5
+ border.width: 4
+ border.color: "#000000"
+ }
+ //! [linemarker]
+
+ //! [lineseriestheme]
+ SeriesTheme {
+ id: seriesTheme
+ colors: ["#2CDE85", "#DBEB00"]
+ }
+ //! [lineseriestheme]
+
+ //! [lineseries1]
+ LineSeries {
+ id: lineSeries1
+ theme: seriesTheme
+ axisX: ValueAxis {
+ max: 5
+ tickInterval: 1
+ minorTickCount: 9
+ labelDecimals: 1
+ }
+ axisY: ValueAxis {
+ max: 10
+ tickInterval: 1
+ minorTickCount: 4
+ labelDecimals: 1
+ }
+ width: 4
+ pointMarker: Marker { }
+ XYPoint { x: 0; y: 0 }
+ XYPoint { x: 1; y: 2.1 }
+ XYPoint { x: 2; y: 3.3 }
+ XYPoint { x: 3; y: 2.1 }
+ XYPoint { x: 4; y: 4.9 }
+ XYPoint { x: 5; y: 3.0 }
+ }
+ //! [lineseries1]
+
+ //! [lineseries2]
+ LineSeries {
+ id: lineSeries2
+ theme: seriesTheme
+ width: 4
+ pointMarker: Marker { }
+ XYPoint { x: 0; y: 5.0 }
+ XYPoint { x: 1; y: 3.3 }
+ XYPoint { x: 2; y: 7.1 }
+ XYPoint { x: 3; y: 7.5 }
+ XYPoint { x: 4; y: 6.1 }
+ XYPoint { x: 5; y: 3.2 }
+ }
+ //! [lineseries2]
+ }
+ }
+ }
+}
diff --git a/examples/graphs/2d/hellographs/HelloGraphs/qmldir b/examples/graphs/2d/hellographs/HelloGraphs/qmldir
new file mode 100644
index 000000000..007f5fb11
--- /dev/null
+++ b/examples/graphs/2d/hellographs/HelloGraphs/qmldir
@@ -0,0 +1,2 @@
+module HelloGraphs
+Main 1.0 Main.qml
diff --git a/examples/graphs/2d/hellographs/doc/hellographs.rst b/examples/graphs/2d/hellographs/doc/hellographs.rst
new file mode 100644
index 000000000..d0820c3b7
--- /dev/null
+++ b/examples/graphs/2d/hellographs/doc/hellographs.rst
@@ -0,0 +1,51 @@
+HelloGraphs Example
+===================
+
+The example shows how to make a simple 2D bar graph and line graph.
+
+BarGraph
+--------
+
+The first graph in the example is a bar graph. Creating it starts with a GraphsView
+component and setting the theme to one which is suitable on
+dark backgrounds. This theme adjusts the graph background grid and axis lines and
+labels.
+
+To make this a bar graph, add a ``BarSeries.`` The X axis of the series is a
+``BarCategoryAxis`` with 3 categories. We hide both the vertical grid and the
+axis lines. The Y axis of the series is ``ValueAxis`` with visible range
+between 20 and 100. Major ticks with labels will be shown on every 10 values
+using the ``tickInterval`` property. Minor ticks will be shown on every 1
+values setting the ``minorTickCount`` propertyt to 9, which means that between
+every major ticks there will be 9 minor ones.
+
+Then data is added into ``BarSeries`` using ``BarSet.`` There are 3 bars, and we define
+custom bars color and border properties. These properties will override the possible
+theme set for the ``AbstractSeries.``
+
+LineGraph
+---------
+
+The second graph of the example is a line graph. It also starts by defining a
+``GraphsView`` element. A custom ``GraphTheme`` is created to get a custom appearance.
+``GraphTheme`` offers quite a wide range of customization possibilities for the background
+grid and axis, which get applied after the ``colorTheme``.
+
+A custom ``Marker`` component is used to visualize the data points.
+
+The previous bar graph didn't define a separate ``SeriesTheme``, so it uses the
+default theme. This line graph uses a custom theme with the desired line colors.
+
+To make this a line graph, add a ``LineSeries.`` The first series defines
+``axisX`` and ``axisY`` for this graph. It also sets the ``pointMarker`` to use
+the custom ``Marker`` component that was created earlier. Data points are added
+using ``XYPoint`` elements.
+
+The second line series is similar to the first. The ``axisX`` and ``axisY``
+don't need to be defined as the graph already contains them. As this is the
+second ``LineSeries`` inside the ``GraphsView``, second color from the
+``seriesTheme`` gets automatically picked.
+
+.. image:: hellographs.webp
+ :width: 1293
+ :alt: HelloGraphs Screenshot
diff --git a/examples/graphs/2d/hellographs/doc/hellographs.webp b/examples/graphs/2d/hellographs/doc/hellographs.webp
new file mode 100644
index 000000000..3e7666411
--- /dev/null
+++ b/examples/graphs/2d/hellographs/doc/hellographs.webp
Binary files differ
diff --git a/examples/graphs/2d/hellographs/hellographs.pyproject b/examples/graphs/2d/hellographs/hellographs.pyproject
new file mode 100644
index 000000000..e8e8cb228
--- /dev/null
+++ b/examples/graphs/2d/hellographs/hellographs.pyproject
@@ -0,0 +1,3 @@
+{
+ "files": ["main.py", "HelloGraphs/Main.qml", "HelloGraphs/qmldir"]
+}
diff --git a/examples/graphs/2d/hellographs/main.py b/examples/graphs/2d/hellographs/main.py
new file mode 100644
index 000000000..acc349beb
--- /dev/null
+++ b/examples/graphs/2d/hellographs/main.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+"""PySide6 port of the Qt Hello Graphs example from Qt v6.x"""
+
+from pathlib import Path
+import sys
+from PySide6.QtGui import QGuiApplication
+from PySide6.QtQuick import QQuickView
+
+
+if __name__ == '__main__':
+ app = QGuiApplication(sys.argv)
+
+ viewer = QQuickView()
+ viewer.engine().addImportPath(Path(__file__).parent)
+ viewer.setColor("black")
+ viewer.loadFromModule("HelloGraphs", "Main")
+ viewer.show()
+ r = app.exec()
+ del viewer
+ sys.exit(r)
diff --git a/examples/graphs/3d/minimalsurfacegraph/doc/minimalsurfacegraph.rst b/examples/graphs/3d/minimalsurfacegraph/doc/minimalsurfacegraph.rst
new file mode 100644
index 000000000..bfc7a044d
--- /dev/null
+++ b/examples/graphs/3d/minimalsurfacegraph/doc/minimalsurfacegraph.rst
@@ -0,0 +1,4 @@
+Minimal Surface Example
+=======================
+
+The example shows the minimal code to create a surface.
diff --git a/examples/graphs/3d/minimalsurfacegraph/main.py b/examples/graphs/3d/minimalsurfacegraph/main.py
new file mode 100644
index 000000000..5fb4b4472
--- /dev/null
+++ b/examples/graphs/3d/minimalsurfacegraph/main.py
@@ -0,0 +1,54 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import sys
+
+from PySide6.QtCore import QSize
+from PySide6.QtGui import QVector3D
+from PySide6.QtGraphs import (Q3DSurface, QSurfaceDataItem,
+ QSurface3DSeries)
+from PySide6.QtWidgets import QApplication
+from PySide6.QtQuickWidgets import QQuickWidget
+
+
+DESCRIPTION = """Minimal Qt Graphs Surface Example
+
+Use the mouse wheel to zoom. Rotate using the right mouse button.
+"""
+
+
+if __name__ == '__main__':
+ app = QApplication(sys.argv)
+
+ print(DESCRIPTION)
+
+ surface = Q3DSurface()
+ axis = surface.axisX()
+ axis.setTitle("X")
+ axis.setTitleVisible(True)
+ axis = surface.axisY()
+ axis.setTitle("Y")
+ axis.setTitleVisible(True)
+ axis = surface.axisZ()
+ axis.setTitle("Z")
+ axis.setTitleVisible(True)
+
+ data = []
+ data_row1 = [QSurfaceDataItem(QVector3D(0, 0.1, 0.5)),
+ QSurfaceDataItem(QVector3D(1, 0.5, 0.5))]
+ data.append(data_row1)
+ data_row2 = [QSurfaceDataItem(QVector3D(0, 1.8, 1)),
+ QSurfaceDataItem(QVector3D(1, 1.2, 1))]
+ data.append(data_row2)
+
+ series = QSurface3DSeries()
+ series.dataProxy().resetArray(data)
+ surface.addSeries(series)
+
+ available_height = app.primaryScreen().availableGeometry().height()
+ width = available_height * 4 / 5
+ surface.resize(QSize(width, width))
+ surface.setResizeMode(QQuickWidget.SizeRootObjectToView)
+ surface.show()
+
+ sys.exit(app.exec())
diff --git a/examples/graphs/3d/widgetgallery/axesinputhandler.py b/examples/graphs/3d/widgetgallery/axesinputhandler.py
new file mode 100644
index 000000000..4c4202974
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/axesinputhandler.py
@@ -0,0 +1,100 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from enum import Enum
+from math import sin, cos, degrees
+
+from PySide6.QtCore import Qt
+from PySide6.QtGraphs import QAbstract3DGraph, Q3DInputHandler
+
+
+class InputState(Enum):
+ StateNormal = 0
+ StateDraggingX = 1
+ StateDraggingZ = 2
+ StateDraggingY = 3
+
+
+class AxesInputHandler(Q3DInputHandler):
+
+ def __init__(self, graph, parent=None):
+ super().__init__(parent)
+ self._mousePressed = False
+ self._state = InputState.StateNormal
+ self._axisX = None
+ self._axisZ = None
+ self._axisY = None
+ self._speedModifier = 15.0
+
+ # Connect to the item selection signal from graph
+ graph.selectedElementChanged.connect(self.handleElementSelected)
+
+ def setAxes(self, axisX, axisZ, axisY):
+ self._axisX = axisX
+ self._axisZ = axisZ
+ self._axisY = axisY
+
+ def setDragSpeedModifier(self, modifier):
+ self._speedModifier = modifier
+
+ def mousePressEvent(self, event, mousePos):
+ super().mousePressEvent(event, mousePos)
+ if Qt.LeftButton == event.button():
+ self._mousePressed = True
+
+ def mouseMoveEvent(self, event, mousePos):
+ # Check if we're trying to drag axis label
+ if self._mousePressed and self._state != InputState.StateNormal:
+ self.setPreviousInputPos(self.inputPosition())
+ self.setInputPosition(mousePos)
+ self.handleAxisDragging()
+ else:
+ super().mouseMoveEvent(event, mousePos)
+
+ def mouseReleaseEvent(self, event, mousePos):
+ super().mouseReleaseEvent(event, mousePos)
+ self._mousePressed = False
+ self._state = InputState.StateNormal
+
+ def handleElementSelected(self, type):
+ if type == QAbstract3DGraph.ElementAxisXLabel:
+ self._state = InputState.StateDraggingX
+ elif type == QAbstract3DGraph.ElementAxisYLabel:
+ self._state = InputState.StateDraggingY
+ elif type == QAbstract3DGraph.ElementAxisZLabel:
+ self._state = InputState.StateDraggingZ
+ else:
+ self._state = InputState.StateNormal
+
+ def handleAxisDragging(self):
+ distance = 0.0
+ # Get scene orientation from active camera
+ xRotation = self.cameraXRotation()
+ yRotation = self.cameraYRotation()
+
+ # Calculate directional drag multipliers based on rotation
+ xMulX = cos(degrees(xRotation))
+ xMulY = sin(degrees(xRotation))
+ zMulX = sin(degrees(xRotation))
+ zMulY = cos(degrees(xRotation))
+
+ # Get the drag amount
+ move = self.inputPosition() - self.previousInputPos()
+
+ # Flip the effect of y movement if we're viewing from below
+ yMove = -move.y() if yRotation < 0 else move.y()
+
+ # Adjust axes
+ if self._state == InputState.StateDraggingX:
+ distance = (move.x() * xMulX - yMove * xMulY) / self._speedModifier
+ self._axisX.setRange(self._axisX.min() - distance,
+ self._axisX.max() - distance)
+ elif self._state == InputState.StateDraggingZ:
+ distance = (move.x() * zMulX + yMove * zMulY) / self._speedModifier
+ self._axisZ.setRange(self._axisZ.min() + distance,
+ self._axisZ.max() + distance)
+ elif self._state == InputState.StateDraggingY:
+ # No need to use adjusted y move here
+ distance = move.y() / self._speedModifier
+ self._axisY.setRange(self._axisY.min() + distance,
+ self._axisY.max() + distance)
diff --git a/examples/graphs/3d/widgetgallery/bargraph.py b/examples/graphs/3d/widgetgallery/bargraph.py
new file mode 100644
index 000000000..822acb4a9
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/bargraph.py
@@ -0,0 +1,272 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from graphmodifier import GraphModifier
+
+from PySide6.QtCore import QObject, Qt
+from PySide6.QtGui import QFont
+from PySide6.QtWidgets import (QButtonGroup, QCheckBox, QComboBox, QFontComboBox,
+ QLabel, QPushButton, QHBoxLayout, QSizePolicy,
+ QRadioButton, QSlider, QVBoxLayout, QWidget)
+from PySide6.QtQuickWidgets import QQuickWidget
+from PySide6.QtGraphs import (QAbstract3DGraph, QAbstract3DSeries, Q3DBars)
+
+
+class BarGraph(QObject):
+
+ def __init__(self, minimum_graph_size, maximum_graph_size):
+ super().__init__()
+ self._barsGraph = Q3DBars()
+ self._barsWidget = QWidget()
+ hLayout = QHBoxLayout(self._barsWidget)
+ self._barsGraph.setMinimumSize(minimum_graph_size)
+ self._barsGraph.setMaximumSize(maximum_graph_size)
+ self._barsGraph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ self._barsGraph.setFocusPolicy(Qt.StrongFocus)
+ self._barsGraph.setResizeMode(QQuickWidget.SizeRootObjectToView)
+ hLayout.addWidget(self._barsGraph, 1)
+
+ vLayout = QVBoxLayout()
+ hLayout.addLayout(vLayout)
+
+ themeList = QComboBox(self._barsWidget)
+ themeList.addItem("Qt")
+ themeList.addItem("Primary Colors")
+ themeList.addItem("Digia")
+ themeList.addItem("Stone Moss")
+ themeList.addItem("Army Blue")
+ themeList.addItem("Retro")
+ themeList.addItem("Ebony")
+ themeList.addItem("Isabelle")
+ themeList.setCurrentIndex(0)
+
+ labelButton = QPushButton(self._barsWidget)
+ labelButton.setText("Change label style")
+
+ smoothCheckBox = QCheckBox(self._barsWidget)
+ smoothCheckBox.setText("Smooth bars")
+ smoothCheckBox.setChecked(False)
+
+ barStyleList = QComboBox(self._barsWidget)
+ barStyleList.addItem("Bar", QAbstract3DSeries.Mesh.Bar)
+ barStyleList.addItem("Pyramid", QAbstract3DSeries.Mesh.Pyramid)
+ barStyleList.addItem("Cone", QAbstract3DSeries.Mesh.Cone)
+ barStyleList.addItem("Cylinder", QAbstract3DSeries.Mesh.Cylinder)
+ barStyleList.addItem("Bevel bar", QAbstract3DSeries.Mesh.BevelBar)
+ barStyleList.addItem("Sphere", QAbstract3DSeries.Mesh.Sphere)
+ barStyleList.setCurrentIndex(4)
+
+ cameraButton = QPushButton(self._barsWidget)
+ cameraButton.setText("Change camera preset")
+
+ zoomToSelectedButton = QPushButton(self._barsWidget)
+ zoomToSelectedButton.setText("Zoom to selected bar")
+
+ selectionModeList = QComboBox(self._barsWidget)
+ selectionModeList.addItem("None", QAbstract3DGraph.SelectionNone)
+ selectionModeList.addItem("Bar", QAbstract3DGraph.SelectionItem)
+ selectionModeList.addItem("Row", QAbstract3DGraph.SelectionRow)
+ sel = QAbstract3DGraph.SelectionItemAndRow
+ selectionModeList.addItem("Bar and Row", sel)
+ selectionModeList.addItem("Column", QAbstract3DGraph.SelectionColumn)
+ sel = QAbstract3DGraph.SelectionItemAndColumn
+ selectionModeList.addItem("Bar and Column", sel)
+ sel = QAbstract3DGraph.SelectionRowAndColumn
+ selectionModeList.addItem("Row and Column", sel)
+ sel = QAbstract3DGraph.SelectionItemRowAndColumn
+ selectionModeList.addItem("Bar, Row and Column", sel)
+ sel = QAbstract3DGraph.SelectionSlice | QAbstract3DGraph.SelectionRow
+ selectionModeList.addItem("Slice into Row", sel)
+ sel = QAbstract3DGraph.SelectionSlice | QAbstract3DGraph.SelectionItemAndRow
+ selectionModeList.addItem("Slice into Row and Item", sel)
+ sel = QAbstract3DGraph.SelectionSlice | QAbstract3DGraph.SelectionColumn
+ selectionModeList.addItem("Slice into Column", sel)
+ sel = (QAbstract3DGraph.SelectionSlice
+ | QAbstract3DGraph.SelectionItemAndColumn)
+ selectionModeList.addItem("Slice into Column and Item", sel)
+ sel = (QAbstract3DGraph.SelectionItemRowAndColumn
+ | QAbstract3DGraph.SelectionMultiSeries)
+ selectionModeList.addItem("Multi: Bar, Row, Col", sel)
+ sel = (QAbstract3DGraph.SelectionSlice
+ | QAbstract3DGraph.SelectionItemAndRow
+ | QAbstract3DGraph.SelectionMultiSeries)
+ selectionModeList.addItem("Multi, Slice: Row, Item", sel)
+ sel = (QAbstract3DGraph.SelectionSlice
+ | QAbstract3DGraph.SelectionItemAndColumn
+ | QAbstract3DGraph.SelectionMultiSeries)
+ selectionModeList.addItem("Multi, Slice: Col, Item", sel)
+ selectionModeList.setCurrentIndex(1)
+
+ backgroundCheckBox = QCheckBox(self._barsWidget)
+ backgroundCheckBox.setText("Show background")
+ backgroundCheckBox.setChecked(False)
+
+ gridCheckBox = QCheckBox(self._barsWidget)
+ gridCheckBox.setText("Show grid")
+ gridCheckBox.setChecked(True)
+
+ seriesCheckBox = QCheckBox(self._barsWidget)
+ seriesCheckBox.setText("Show second series")
+ seriesCheckBox.setChecked(False)
+
+ reverseValueAxisCheckBox = QCheckBox(self._barsWidget)
+ reverseValueAxisCheckBox.setText("Reverse value axis")
+ reverseValueAxisCheckBox.setChecked(False)
+
+ reflectionCheckBox = QCheckBox(self._barsWidget)
+ reflectionCheckBox.setText("Show reflections")
+ reflectionCheckBox.setChecked(False)
+
+ rotationSliderX = QSlider(Qt.Horizontal, self._barsWidget)
+ rotationSliderX.setTickInterval(30)
+ rotationSliderX.setTickPosition(QSlider.TicksBelow)
+ rotationSliderX.setMinimum(-180)
+ rotationSliderX.setValue(0)
+ rotationSliderX.setMaximum(180)
+ rotationSliderY = QSlider(Qt.Horizontal, self._barsWidget)
+ rotationSliderY.setTickInterval(15)
+ rotationSliderY.setTickPosition(QSlider.TicksAbove)
+ rotationSliderY.setMinimum(-90)
+ rotationSliderY.setValue(0)
+ rotationSliderY.setMaximum(90)
+
+ fontSizeSlider = QSlider(Qt.Horizontal, self._barsWidget)
+ fontSizeSlider.setTickInterval(10)
+ fontSizeSlider.setTickPosition(QSlider.TicksBelow)
+ fontSizeSlider.setMinimum(1)
+ fontSizeSlider.setValue(30)
+ fontSizeSlider.setMaximum(100)
+
+ fontList = QFontComboBox(self._barsWidget)
+ fontList.setCurrentFont(QFont("Times New Roman"))
+
+ shadowQuality = QComboBox(self._barsWidget)
+ shadowQuality.addItem("None")
+ shadowQuality.addItem("Low")
+ shadowQuality.addItem("Medium")
+ shadowQuality.addItem("High")
+ shadowQuality.addItem("Low Soft")
+ shadowQuality.addItem("Medium Soft")
+ shadowQuality.addItem("High Soft")
+ shadowQuality.setCurrentIndex(5)
+
+ rangeList = QComboBox(self._barsWidget)
+ rangeList.addItem("2015")
+ rangeList.addItem("2016")
+ rangeList.addItem("2017")
+ rangeList.addItem("2018")
+ rangeList.addItem("2019")
+ rangeList.addItem("2020")
+ rangeList.addItem("2021")
+ rangeList.addItem("2022")
+ rangeList.addItem("All")
+ rangeList.setCurrentIndex(8)
+
+ axisTitlesVisibleCB = QCheckBox(self._barsWidget)
+ axisTitlesVisibleCB.setText("Axis titles visible")
+ axisTitlesVisibleCB.setChecked(True)
+
+ axisTitlesFixedCB = QCheckBox(self._barsWidget)
+ axisTitlesFixedCB.setText("Axis titles fixed")
+ axisTitlesFixedCB.setChecked(True)
+
+ axisLabelRotationSlider = QSlider(Qt.Horizontal, self._barsWidget)
+ axisLabelRotationSlider.setTickInterval(10)
+ axisLabelRotationSlider.setTickPosition(QSlider.TicksBelow)
+ axisLabelRotationSlider.setMinimum(0)
+ axisLabelRotationSlider.setValue(30)
+ axisLabelRotationSlider.setMaximum(90)
+
+ modeGroup = QButtonGroup(self._barsWidget)
+ modeWeather = QRadioButton("Temperature Data", self._barsWidget)
+ modeWeather.setChecked(True)
+ modeCustomProxy = QRadioButton("Custom Proxy Data", self._barsWidget)
+ modeGroup.addButton(modeWeather)
+ modeGroup.addButton(modeCustomProxy)
+
+ vLayout.addWidget(QLabel("Rotate horizontally"))
+ vLayout.addWidget(rotationSliderX, 0, Qt.AlignTop)
+ vLayout.addWidget(QLabel("Rotate vertically"))
+ vLayout.addWidget(rotationSliderY, 0, Qt.AlignTop)
+ vLayout.addWidget(labelButton, 0, Qt.AlignTop)
+ vLayout.addWidget(cameraButton, 0, Qt.AlignTop)
+ vLayout.addWidget(zoomToSelectedButton, 0, Qt.AlignTop)
+ vLayout.addWidget(backgroundCheckBox)
+ vLayout.addWidget(gridCheckBox)
+ vLayout.addWidget(smoothCheckBox)
+ vLayout.addWidget(reflectionCheckBox)
+ vLayout.addWidget(seriesCheckBox)
+ vLayout.addWidget(reverseValueAxisCheckBox)
+ vLayout.addWidget(axisTitlesVisibleCB)
+ vLayout.addWidget(axisTitlesFixedCB)
+ vLayout.addWidget(QLabel("Show year"))
+ vLayout.addWidget(rangeList)
+ vLayout.addWidget(QLabel("Change bar style"))
+ vLayout.addWidget(barStyleList)
+ vLayout.addWidget(QLabel("Change selection mode"))
+ vLayout.addWidget(selectionModeList)
+ vLayout.addWidget(QLabel("Change theme"))
+ vLayout.addWidget(themeList)
+ vLayout.addWidget(QLabel("Adjust shadow quality"))
+ vLayout.addWidget(shadowQuality)
+ vLayout.addWidget(QLabel("Change font"))
+ vLayout.addWidget(fontList)
+ vLayout.addWidget(QLabel("Adjust font size"))
+ vLayout.addWidget(fontSizeSlider)
+ vLayout.addWidget(QLabel("Axis label rotation"))
+ vLayout.addWidget(axisLabelRotationSlider, 0, Qt.AlignTop)
+ vLayout.addWidget(modeWeather, 0, Qt.AlignTop)
+ vLayout.addWidget(modeCustomProxy, 1, Qt.AlignTop)
+
+ self._modifier = GraphModifier(self._barsGraph, self)
+
+ rotationSliderX.valueChanged.connect(self._modifier.rotateX)
+ rotationSliderY.valueChanged.connect(self._modifier.rotateY)
+
+ labelButton.clicked.connect(self._modifier.changeLabelBackground)
+ cameraButton.clicked.connect(self._modifier.changePresetCamera)
+ zoomToSelectedButton.clicked.connect(self._modifier.zoomToSelectedBar)
+
+ backgroundCheckBox.stateChanged.connect(self._modifier.setBackgroundEnabled)
+ gridCheckBox.stateChanged.connect(self._modifier.setGridEnabled)
+ smoothCheckBox.stateChanged.connect(self._modifier.setSmoothBars)
+ seriesCheckBox.stateChanged.connect(self._modifier.setSeriesVisibility)
+ reverseValueAxisCheckBox.stateChanged.connect(self._modifier.setReverseValueAxis)
+ reflectionCheckBox.stateChanged.connect(self._modifier.setReflection)
+
+ self._modifier.backgroundEnabledChanged.connect(backgroundCheckBox.setChecked)
+ self._modifier.gridEnabledChanged.connect(gridCheckBox.setChecked)
+
+ rangeList.currentIndexChanged.connect(self._modifier.changeRange)
+
+ barStyleList.currentIndexChanged.connect(self._modifier.changeStyle)
+
+ selectionModeList.currentIndexChanged.connect(self._modifier.changeSelectionMode)
+
+ themeList.currentIndexChanged.connect(self._modifier.changeTheme)
+
+ shadowQuality.currentIndexChanged.connect(self._modifier.changeShadowQuality)
+
+ self._modifier.shadowQualityChanged.connect(shadowQuality.setCurrentIndex)
+ self._barsGraph.shadowQualityChanged.connect(self._modifier.shadowQualityUpdatedByVisual)
+
+ fontSizeSlider.valueChanged.connect(self._modifier.changeFontSize)
+ fontList.currentFontChanged.connect(self._modifier.changeFont)
+
+ self._modifier.fontSizeChanged.connect(fontSizeSlider.setValue)
+ self._modifier.fontChanged.connect(fontList.setCurrentFont)
+
+ axisTitlesVisibleCB.stateChanged.connect(self._modifier.setAxisTitleVisibility)
+ axisTitlesFixedCB.stateChanged.connect(self._modifier.setAxisTitleFixed)
+ axisLabelRotationSlider.valueChanged.connect(self._modifier.changeLabelRotation)
+
+ modeWeather.toggled.connect(self._modifier.setDataModeToWeather)
+ modeCustomProxy.toggled.connect(self._modifier.setDataModeToCustom)
+ modeWeather.toggled.connect(seriesCheckBox.setEnabled)
+ modeWeather.toggled.connect(rangeList.setEnabled)
+ modeWeather.toggled.connect(axisTitlesVisibleCB.setEnabled)
+ modeWeather.toggled.connect(axisTitlesFixedCB.setEnabled)
+ modeWeather.toggled.connect(axisLabelRotationSlider.setEnabled)
+
+ def barsWidget(self):
+ return self._barsWidget
diff --git a/examples/graphs/3d/widgetgallery/custominputhandler.py b/examples/graphs/3d/widgetgallery/custominputhandler.py
new file mode 100644
index 000000000..15fe00e70
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/custominputhandler.py
@@ -0,0 +1,177 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from enum import Enum
+from math import sin, cos, degrees
+
+from PySide6.QtCore import Qt
+from PySide6.QtGraphs import (QAbstract3DGraph, Q3DInputHandler)
+
+
+class InputState(Enum):
+ StateNormal = 0
+ StateDraggingX = 1
+ StateDraggingZ = 2
+ StateDraggingY = 3
+
+
+class CustomInputHandler(Q3DInputHandler):
+
+ def __init__(self, graph, parent=None):
+ super().__init__(parent)
+ self._highlight = None
+ self._mousePressed = False
+ self._state = InputState.StateNormal
+ self._axisX = None
+ self._axisY = None
+ self._axisZ = None
+ self._speedModifier = 20.0
+ self._aspectRatio = 0.0
+ self._axisXMinValue = 0.0
+ self._axisXMaxValue = 0.0
+ self._axisXMinRange = 0.0
+ self._axisZMinValue = 0.0
+ self._axisZMaxValue = 0.0
+ self._axisZMinRange = 0.0
+ self._areaMinValue = 0.0
+ self._areaMaxValue = 0.0
+
+ # Connect to the item selection signal from graph
+ graph.selectedElementChanged.connect(self.handleElementSelected)
+
+ def setAspectRatio(self, ratio):
+ self._aspectRatio = ratio
+
+ def setHighlightSeries(self, series):
+ self._highlight = series
+
+ def setDragSpeedModifier(self, modifier):
+ self._speedModifier = modifier
+
+ def setLimits(self, min, max, minRange):
+ self._areaMinValue = min
+ self._areaMaxValue = max
+ self._axisXMinValue = self._areaMinValue
+ self._axisXMaxValue = self._areaMaxValue
+ self._axisZMinValue = self._areaMinValue
+ self._axisZMaxValue = self._areaMaxValue
+ self._axisXMinRange = minRange
+ self._axisZMinRange = minRange
+
+ def setAxes(self, axisX, axisY, axisZ):
+ self._axisX = axisX
+ self._axisY = axisY
+ self._axisZ = axisZ
+
+ def mousePressEvent(self, event, mousePos):
+ if Qt.LeftButton == event.button():
+ self._highlight.setVisible(False)
+ self._mousePressed = True
+ super().mousePressEvent(event, mousePos)
+
+ def wheelEvent(self, event):
+ delta = float(event.angleDelta().y())
+
+ self._axisXMinValue += delta
+ self._axisXMaxValue -= delta
+ self._axisZMinValue += delta
+ self._axisZMaxValue -= delta
+ self.checkConstraints()
+
+ y = (self._axisXMaxValue - self._axisXMinValue) * self._aspectRatio
+
+ self._axisX.setRange(self._axisXMinValue, self._axisXMaxValue)
+ self._axisY.setRange(100.0, y)
+ self._axisZ.setRange(self._axisZMinValue, self._axisZMaxValue)
+
+ def mouseMoveEvent(self, event, mousePos):
+ # Check if we're trying to drag axis label
+ if self._mousePressed and self._state != InputState.StateNormal:
+ self.setPreviousInputPos(self.inputPosition())
+ self.setInputPosition(mousePos)
+ self.handleAxisDragging()
+ else:
+ super().mouseMoveEvent(event, mousePos)
+
+ def mouseReleaseEvent(self, event, mousePos):
+ super().mouseReleaseEvent(event, mousePos)
+ self._mousePressed = False
+ self._state = InputState.StateNormal
+
+ def handleElementSelected(self, type):
+ if type == QAbstract3DGraph.ElementAxisXLabel:
+ self._state = InputState.StateDraggingX
+ elif type == QAbstract3DGraph.ElementAxisZLabel:
+ self._state = InputState.StateDraggingZ
+ else:
+ self._state = InputState.StateNormal
+
+ def handleAxisDragging(self):
+ distance = 0.0
+
+ # Get scene orientation from active camera
+ xRotation = self.scene().cameraXRotation()
+
+ # Calculate directional drag multipliers based on rotation
+ xMulX = cos(degrees(xRotation))
+ xMulY = sin(degrees(xRotation))
+ zMulX = xMulY
+ zMulY = xMulX
+
+ # Get the drag amount
+ move = self.inputPosition() - self.previousInputPos()
+
+ # Adjust axes
+ if self._state == InputState.StateDraggingX:
+ distance = (move.x() * xMulX - move.y() * xMulY) * self._speedModifier
+ self._axisXMinValue -= distance
+ self._axisXMaxValue -= distance
+ if self._axisXMinValue < self._areaMinValue:
+ dist = self._axisXMaxValue - self._axisXMinValue
+ self._axisXMinValue = self._areaMinValue
+ self._axisXMaxValue = self._axisXMinValue + dist
+
+ if self._axisXMaxValue > self._areaMaxValue:
+ dist = self._axisXMaxValue - self._axisXMinValue
+ self._axisXMaxValue = self._areaMaxValue
+ self._axisXMinValue = self._axisXMaxValue - dist
+
+ self._axisX.setRange(self._axisXMinValue, self._axisXMaxValue)
+ elif self._state == InputState.StateDraggingZ:
+ distance = (move.x() * zMulX + move.y() * zMulY) * self._speedModifier
+ self._axisZMinValue += distance
+ self._axisZMaxValue += distance
+ if self._axisZMinValue < self._areaMinValue:
+ dist = self._axisZMaxValue - self._axisZMinValue
+ self._axisZMinValue = self._areaMinValue
+ self._axisZMaxValue = self._axisZMinValue + dist
+
+ if self._axisZMaxValue > self._areaMaxValue:
+ dist = self._axisZMaxValue - self._axisZMinValue
+ self._axisZMaxValue = self._areaMaxValue
+ self._axisZMinValue = self._axisZMaxValue - dist
+
+ self._axisZ.setRange(self._axisZMinValue, self._axisZMaxValue)
+
+ def checkConstraints(self):
+ if self._axisXMinValue < self._areaMinValue:
+ self._axisXMinValue = self._areaMinValue
+ if self._axisXMaxValue > self._areaMaxValue:
+ self._axisXMaxValue = self._areaMaxValue
+ # Don't allow too much zoom in
+ range = self._axisXMaxValue - self._axisXMinValue
+ if range < self._axisXMinRange:
+ adjust = (self._axisXMinRange - range) / 2.0
+ self._axisXMinValue -= adjust
+ self._axisXMaxValue += adjust
+
+ if self._axisZMinValue < self._areaMinValue:
+ self._axisZMinValue = self._areaMinValue
+ if self._axisZMaxValue > self._areaMaxValue:
+ self._axisZMaxValue = self._areaMaxValue
+ # Don't allow too much zoom in
+ range = self._axisZMaxValue - self._axisZMinValue
+ if range < self._axisZMinRange:
+ adjust = (self._axisZMinRange - range) / 2.0
+ self._axisZMinValue -= adjust
+ self._axisZMaxValue += adjust
diff --git a/examples/graphs/3d/widgetgallery/data/layer_1.png b/examples/graphs/3d/widgetgallery/data/layer_1.png
new file mode 100644
index 000000000..9138c710a
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/data/layer_1.png
Binary files differ
diff --git a/examples/graphs/3d/widgetgallery/data/layer_2.png b/examples/graphs/3d/widgetgallery/data/layer_2.png
new file mode 100644
index 000000000..61631ae8b
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/data/layer_2.png
Binary files differ
diff --git a/examples/graphs/3d/widgetgallery/data/layer_3.png b/examples/graphs/3d/widgetgallery/data/layer_3.png
new file mode 100644
index 000000000..066ffbe75
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/data/layer_3.png
Binary files differ
diff --git a/examples/graphs/3d/widgetgallery/data/license.txt b/examples/graphs/3d/widgetgallery/data/license.txt
new file mode 100644
index 000000000..749daf31f
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/data/license.txt
@@ -0,0 +1,77 @@
+License information regarding the data obtained from National Land Survey of
+Finland http://www.maanmittauslaitos.fi/en
+- topographic model from Elevation model 2 m (U4421B, U4421D, U4422A and
+ U4422C) 08/2014
+- map image extracted from Topographic map raster 1:50 000 (U442) 08/2014
+
+National Land Survey open data licence - version 1.0 - 1 May 2012
+
+1. General information
+
+The National Land Survey of Finland (hereinafter the Licensor), as the holder
+of the immaterial rights to the data, has granted on the terms mentioned below
+the right to use a copy (hereinafter data or dataset(s)) of the data (or a part
+of it).
+
+The Licensee is a natural or legal person who makes use of the data covered by
+this licence. The Licensee accepts the terms of this licence by receiving the
+dataset(s) covered by the licence.
+
+This Licence agreement does not create a co-operation or business relationship
+between the Licensee and the Licensor.
+
+2. Terms of the licence
+
+2.1. Right of use
+
+This licence grants a worldwide, free of charge and irrevocable parallel right
+of use to open data. According to the terms of the licence, data received by
+the Licensee can be freely:
+ - copied, distributed and published,
+ - modified and utilised commercially and non-commercially,
+ - inserted into other products and
+ - used as a part of a software application or service.
+
+2.2. Duties and responsibilities of the Licensee
+
+Through reasonable means suitable to the distribution medium or method which is
+used in conjunction with a product containing data or a service utilising data
+covered by this licence or while distributing data, the Licensee shall:
+ - mention the name of the Licensor, the name of the dataset(s) and the time
+ when the National Land Survey has delivered the dataset(s) (e.g.: contains
+ data from the National Land Survey of Finland Topographic Database 06/2012)
+ - provide a copy of this licence or a link to it, as well as
+ - require third parties to provide the same information when granting rights
+ to copies of dataset(s) or products and services containing such data and
+ - remove the name of the Licensor from the product or service, if required to
+ do so by the Licensor.
+
+The terms of this licence do not allow the Licensee to state in conjunction
+with the use of dataset(s) that the Licensor supports or recommends such use.
+
+2.3. Duties and responsibilities of the Licensor
+
+The Licensor shall ensure that
+ - the Licensor has the right to grant rights to the dataset(s) in accordance
+ with this licence.
+
+The data has been licensed "as is" and the Licensor
+ - shall not be held responsible for any errors or omissions in the data,
+ disclaims any warranty for the validity or up to date status of the data and
+ shall be free from liability for direct or consequential damages arising
+ from the use of data provided by the Licensor,
+ - and is not obligated to ensure the continuous availability of the data, nor
+ to announce in advance the interruption or cessation of availability, and
+ the Licensor shall be free from liability for direct or consequential
+ damages arising from any such interruption or cessation.
+
+3. Jurisdiction
+
+Finnish law shall apply to this licence.
+
+4. Changes to this licence
+
+The Licensor may at any time change the terms of the licence or apply a
+different licence to the data. The terms of this licence shall, however, still
+apply to such data that has been received prior to the change of the terms of
+the licence or the licence itself.
diff --git a/examples/graphs/3d/widgetgallery/data/maptexture.jpg b/examples/graphs/3d/widgetgallery/data/maptexture.jpg
new file mode 100644
index 000000000..ae5d66ebe
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/data/maptexture.jpg
Binary files differ
diff --git a/examples/graphs/3d/widgetgallery/data/narrowarrow.mesh b/examples/graphs/3d/widgetgallery/data/narrowarrow.mesh
new file mode 100644
index 000000000..288867b1e
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/data/narrowarrow.mesh
Binary files differ
diff --git a/examples/graphs/3d/widgetgallery/data/oilrig.mesh b/examples/graphs/3d/widgetgallery/data/oilrig.mesh
new file mode 100644
index 000000000..4a7baeddf
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/data/oilrig.mesh
Binary files differ
diff --git a/examples/graphs/3d/widgetgallery/data/pipe.mesh b/examples/graphs/3d/widgetgallery/data/pipe.mesh
new file mode 100644
index 000000000..984b6d443
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/data/pipe.mesh
Binary files differ
diff --git a/examples/graphs/3d/widgetgallery/data/raindata.txt b/examples/graphs/3d/widgetgallery/data/raindata.txt
new file mode 100644
index 000000000..d95589219
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/data/raindata.txt
@@ -0,0 +1,158 @@
+# Rainfall per month from 2010 to 2022 in Northern Finland (Oulu)
+# Format: year, month, rainfall
+2010,1, 0,
+2010,2, 3.4,
+2010,3, 52,
+2010,4, 33.8,
+2010,5, 45.6,
+2010,6, 43.8,
+2010,7, 104.6,
+2010,8, 105.4,
+2010,9, 107.2,
+2010,10,38.6,
+2010,11,17.8,
+2010,12,0,
+2011,1, 8.2,
+2011,2, 1.6,
+2011,3, 27.4,
+2011,4, 15.8,
+2011,5, 57.6,
+2011,6, 85.2,
+2011,7, 127,
+2011,8, 72.2,
+2011,9, 82.2,
+2011,10,62.4,
+2011,11,31.6,
+2011,12,53.8,
+2012,1, 0,
+2012,2, 5,
+2012,3, 32.4,
+2012,4, 57.6,
+2012,5, 71.4,
+2012,6, 60.8,
+2012,7, 109,
+2012,8, 43.6,
+2012,9, 79.4,
+2012,10,117.2,
+2012,11,59,
+2012,12,0.2,
+2013,1, 28,
+2013,2, 19,
+2013,3, 0,
+2013,4, 37.6,
+2013,5, 44.2,
+2013,6, 104.8,
+2013,7, 84.2,
+2013,8, 57.2,
+2013,9, 37.2,
+2013,10,64.6,
+2013,11,77.8,
+2013,12,92.8,
+2014,1, 23.8,
+2014,2, 23.6,
+2014,3, 15.4,
+2014,4, 13.2,
+2014,5, 36.4,
+2014,6, 26.4,
+2014,7, 95.8,
+2014,8, 81.8,
+2014,9, 13.8,
+2014,10,94.6,
+2014,11,44.6,
+2014,12,31,
+2015,1, 37.4,
+2015,2, 21,
+2015,3, 42,
+2015,4, 8.8,
+2015,5, 82.4,
+2015,6, 150,
+2015,7, 56.8,
+2015,8, 67.2,
+2015,9, 131.2,
+2015,10,38.4,
+2015,11,83.4,
+2015,12,47.8,
+2016,1, 12.4,
+2016,2, 34.8,
+2016,3, 29,
+2016,4, 40.4,
+2016,5, 32.4,
+2016,6, 80.2,
+2016,7, 102.6,
+2016,8, 95.6,
+2016,9, 40.2,
+2016,10,7.8,
+2016,11,39.6,
+2016,12,8.8,
+2017,1, 9.4,
+2017,2, 6.6,
+2017,3, 29,
+2017,4, 46.2,
+2017,5, 43.2,
+2017,6, 25.2,
+2017,7, 72.4,
+2017,8, 58.8,
+2017,9, 68.8,
+2017,10,45.8,
+2017,11,36.8,
+2017,12,29.6,
+2018,1, 19.8,
+2018,2, 0.8,
+2018,3, 4,
+2018,4, 23.2,
+2018,5, 13.2,
+2018,6, 62.8,
+2018,7, 33,
+2018,8, 96.6,
+2018,9, 72.6,
+2018,10,48.8,
+2018,11,31.8,
+2018,12,12.8,
+2019,1, 0.2,
+2019,2, 24.8,
+2019,3, 32,
+2019,4, 8.8,
+2019,5, 71.4,
+2019,6, 65.8,
+2019,7, 17.6,
+2019,8, 90,
+2019,9, 50,
+2019,10,77,
+2019,11,27,
+2019,12,43.2,
+2020,1, 28.8,
+2020,2, 45,
+2020,3, 18.6,
+2020,4, 13,
+2020,5, 30.8,
+2020,6, 21.4,
+2020,7, 163.6,
+2020,8, 12,
+2020,9, 102.4,
+2020,10,133.2,
+2020,11,69.8,
+2020,12,40.6,
+2021,1, 0.4,
+2021,2, 21.6,
+2021,3, 24,
+2021,4, 51.4,
+2021,5, 76.4,
+2021,6, 29.2,
+2021,7, 36.4,
+2021,8, 116,
+2021,9, 72.4,
+2021,10,93.4,
+2021,11,21,
+2021,12,10.2,
+2022,1, 8.6,
+2022,2, 6.6,
+2022,3, 5.2,
+2022,4, 15.2,
+2022,5, 37.6,
+2022,6, 45,
+2022,7, 67.4,
+2022,8, 161.6,
+2022,9, 22.8,
+2022,10,75.2,
+2022,11,21.8,
+2022,12,0.2
diff --git a/examples/graphs/3d/widgetgallery/data/refinery.mesh b/examples/graphs/3d/widgetgallery/data/refinery.mesh
new file mode 100644
index 000000000..a7e249353
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/data/refinery.mesh
Binary files differ
diff --git a/examples/graphs/3d/widgetgallery/data/topography.png b/examples/graphs/3d/widgetgallery/data/topography.png
new file mode 100644
index 000000000..9349cdb31
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/data/topography.png
Binary files differ
diff --git a/examples/graphs/3d/widgetgallery/doc/widgetgallery.rst b/examples/graphs/3d/widgetgallery/doc/widgetgallery.rst
new file mode 100644
index 000000000..1470001d6
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/doc/widgetgallery.rst
@@ -0,0 +1,11 @@
+Widget Gallery
+==============
+
+
+Widget Gallery demonstrates all three graph types and some of their special
+features. The graphs have their own tabs in the application.
+
+
+.. image:: widgetgallery.webp
+ :width: 400
+ :alt: Widget Screenshot
diff --git a/examples/graphs/3d/widgetgallery/doc/widgetgallery.webp b/examples/graphs/3d/widgetgallery/doc/widgetgallery.webp
new file mode 100644
index 000000000..eb5767264
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/doc/widgetgallery.webp
Binary files differ
diff --git a/examples/graphs/3d/widgetgallery/graphmodifier.py b/examples/graphs/3d/widgetgallery/graphmodifier.py
new file mode 100644
index 000000000..2eaafa792
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/graphmodifier.py
@@ -0,0 +1,391 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+
+from math import atan, degrees
+import numpy as np
+
+from PySide6.QtCore import QObject, QPropertyAnimation, Signal, Slot
+from PySide6.QtGui import QFont, QVector3D
+from PySide6.QtGraphs import (QAbstract3DGraph, QAbstract3DSeries,
+ QBarDataItem, QBar3DSeries, QCategory3DAxis,
+ QValue3DAxis, Q3DTheme)
+
+from rainfalldata import RainfallData
+
+# Set up data
+TEMP_OULU = np.array([
+ [-7.4, -2.4, 0.0, 3.0, 8.2, 11.6, 14.7, 15.4, 11.4, 4.2, 2.1, -2.3], # 2015
+ [-13.4, -3.9, -1.8, 3.1, 10.6, 13.7, 17.8, 13.6, 10.7, 3.5, -3.1, -4.2], # 2016
+ [-5.7, -6.7, -3.0, -0.1, 4.7, 12.4, 16.1, 14.1, 9.4, 3.0, -0.3, -3.2], # 2017
+ [-6.4, -11.9, -7.4, 1.9, 11.4, 12.4, 21.5, 16.1, 11.0, 4.4, 2.1, -4.1], # 2018
+ [-11.7, -6.1, -2.4, 3.9, 7.2, 14.5, 15.6, 14.4, 8.5, 2.0, -3.0, -1.5], # 2019
+ [-2.1, -3.4, -1.8, 0.6, 7.0, 17.1, 15.6, 15.4, 11.1, 5.6, 1.9, -1.7], # 2020
+ [-9.6, -11.6, -3.2, 2.4, 7.8, 17.3, 19.4, 14.2, 8.0, 5.2, -2.2, -8.6], # 2021
+ [-7.3, -6.4, -1.8, 1.3, 8.1, 15.5, 17.6, 17.6, 9.1, 5.4, -1.5, -4.4]], # 2022
+ np.float64)
+
+
+TEMP_HELSINKI = np.array([
+ [-2.0, -0.1, 1.8, 5.1, 9.7, 13.7, 16.3, 17.3, 12.7, 5.4, 4.6, 2.1], # 2015
+ [-10.3, -0.6, 0.0, 4.9, 14.3, 15.7, 17.7, 16.0, 12.7, 4.6, -1.0, -0.9], # 2016
+ [-2.9, -3.3, 0.7, 2.3, 9.9, 13.8, 16.1, 15.9, 11.4, 5.0, 2.7, 0.7], # 2017
+ [-2.2, -8.4, -4.7, 5.0, 15.3, 15.8, 21.2, 18.2, 13.3, 6.7, 2.8, -2.0], # 2018
+ [-6.2, -0.5, -0.3, 6.8, 10.6, 17.9, 17.5, 16.8, 11.3, 5.2, 1.8, 1.4], # 2019
+ [1.9, 0.5, 1.7, 4.5, 9.5, 18.4, 16.5, 16.8, 13.0, 8.2, 4.4, 0.9], # 2020
+ [-4.7, -8.1, -0.9, 4.5, 10.4, 19.2, 20.9, 15.4, 9.5, 8.0, 1.5, -6.7], # 2021
+ [-3.3, -2.2, -0.2, 3.3, 9.6, 16.9, 18.1, 18.9, 9.2, 7.6, 2.3, -3.4]], # 2022
+ np.float64)
+
+
+class GraphModifier(QObject):
+
+ shadowQualityChanged = Signal(int)
+ backgroundEnabledChanged = Signal(bool)
+ gridEnabledChanged = Signal(bool)
+ fontChanged = Signal(QFont)
+ fontSizeChanged = Signal(int)
+
+ def __init__(self, bargraph, parent):
+ super().__init__(parent)
+ self._graph = bargraph
+ self._temperatureAxis = QValue3DAxis()
+ self._yearAxis = QCategory3DAxis()
+ self._monthAxis = QCategory3DAxis()
+ self._primarySeries = QBar3DSeries()
+ self._secondarySeries = QBar3DSeries()
+ self._celsiusString = "°C"
+
+ self._xRotation = float(0)
+ self._yRotation = float(0)
+ self._fontSize = 30
+ self._segments = 4
+ self._subSegments = 3
+ self._minval = float(-20)
+ self._maxval = float(20)
+ self._barMesh = QAbstract3DSeries.Mesh.BevelBar
+ self._smooth = False
+ self._animationCameraX = QPropertyAnimation()
+ self._animationCameraY = QPropertyAnimation()
+ self._animationCameraZoom = QPropertyAnimation()
+ self._animationCameraTarget = QPropertyAnimation()
+ self._defaultAngleX = float(0)
+ self._defaultAngleY = float(0)
+ self._defaultZoom = float(0)
+ self._defaultTarget = []
+ self._customData = None
+
+ self._graph.setShadowQuality(QAbstract3DGraph.ShadowQuality.SoftMedium)
+ theme = self._graph.activeTheme()
+ theme.setBackgroundEnabled(False)
+ theme.setFont(QFont("Times New Roman", self._fontSize))
+ theme.setLabelBackgroundEnabled(True)
+ self._graph.setMultiSeriesUniform(True)
+
+ self._months = ["January", "February", "March", "April", "May", "June",
+ "July", "August", "September", "October", "November",
+ "December"]
+ self._years = ["2015", "2016", "2017", "2018", "2019", "2020",
+ "2021", "2022"]
+
+ self._temperatureAxis.setTitle("Average temperature")
+ self._temperatureAxis.setSegmentCount(self._segments)
+ self._temperatureAxis.setSubSegmentCount(self._subSegments)
+ self._temperatureAxis.setRange(self._minval, self._maxval)
+ self._temperatureAxis.setLabelFormat("%.1f " + self._celsiusString)
+ self._temperatureAxis.setLabelAutoRotation(30.0)
+ self._temperatureAxis.setTitleVisible(True)
+
+ self._yearAxis.setTitle("Year")
+ self._yearAxis.setLabelAutoRotation(30.0)
+ self._yearAxis.setTitleVisible(True)
+ self._monthAxis.setTitle("Month")
+ self._monthAxis.setLabelAutoRotation(30.0)
+ self._monthAxis.setTitleVisible(True)
+
+ self._graph.setValueAxis(self._temperatureAxis)
+ self._graph.setRowAxis(self._yearAxis)
+ self._graph.setColumnAxis(self._monthAxis)
+
+ format = "Oulu - @colLabel @rowLabel: @valueLabel"
+ self._primarySeries.setItemLabelFormat(format)
+ self._primarySeries.setMesh(QAbstract3DSeries.Mesh.BevelBar)
+ self._primarySeries.setMeshSmooth(False)
+
+ format = "Helsinki - @colLabel @rowLabel: @valueLabel"
+ self._secondarySeries.setItemLabelFormat(format)
+ self._secondarySeries.setMesh(QAbstract3DSeries.Mesh.BevelBar)
+ self._secondarySeries.setMeshSmooth(False)
+ self._secondarySeries.setVisible(False)
+
+ self._graph.addSeries(self._primarySeries)
+ self._graph.addSeries(self._secondarySeries)
+
+ self.changePresetCamera()
+
+ self.resetTemperatureData()
+
+ # Set up property animations for zooming to the selected bar
+ self._defaultAngleX = self._graph.cameraXRotation()
+ self._defaultAngleY = self._graph.cameraYRotation()
+ self._defaultZoom = self._graph.cameraZoomLevel()
+ self._defaultTarget = self._graph.cameraTargetPosition()
+
+ self._animationCameraX.setTargetObject(self._graph)
+ self._animationCameraY.setTargetObject(self._graph)
+ self._animationCameraZoom.setTargetObject(self._graph)
+ self._animationCameraTarget.setTargetObject(self._graph)
+
+ self._animationCameraX.setPropertyName(b"cameraXRotation")
+ self._animationCameraY.setPropertyName(b"cameraYRotation")
+ self._animationCameraZoom.setPropertyName(b"cameraZoomLevel")
+ self._animationCameraTarget.setPropertyName(b"cameraTargetPosition")
+
+ duration = 1700
+ self._animationCameraX.setDuration(duration)
+ self._animationCameraY.setDuration(duration)
+ self._animationCameraZoom.setDuration(duration)
+ self._animationCameraTarget.setDuration(duration)
+
+ # The zoom always first zooms out above the graph and then zooms in
+ zoomOutFraction = 0.3
+ self._animationCameraX.setKeyValueAt(zoomOutFraction, 0.0)
+ self._animationCameraY.setKeyValueAt(zoomOutFraction, 90.0)
+ self._animationCameraZoom.setKeyValueAt(zoomOutFraction, 50.0)
+ self._animationCameraTarget.setKeyValueAt(zoomOutFraction,
+ QVector3D(0, 0, 0))
+ self._customData = RainfallData()
+
+ def resetTemperatureData(self):
+ # Create data arrays
+ dataSet = []
+ dataSet2 = []
+
+ for year in range(0, len(self._years)):
+ # Create a data row
+ dataRow = []
+ dataRow2 = []
+ for month in range(0, len(self._months)):
+ # Add data to the row
+ item = QBarDataItem()
+ item.setValue(TEMP_OULU[year][month])
+ dataRow.append(item)
+ item = QBarDataItem()
+ item.setValue(TEMP_HELSINKI[year][month])
+ dataRow2.append(item)
+
+ # Add the row to the set
+ dataSet.append(dataRow)
+ dataSet2.append(dataRow2)
+
+ # Add data to the data proxy (the data proxy assumes ownership of it)
+ self._primarySeries.dataProxy().resetArray(dataSet, self._years, self._months)
+ self._secondarySeries.dataProxy().resetArray(dataSet2, self._years, self._months)
+
+ @Slot(int)
+ def changeRange(self, range):
+ if range >= len(self._years):
+ self._yearAxis.setRange(0, len(self._years) - 1)
+ else:
+ self._yearAxis.setRange(range, range)
+
+ @Slot(int)
+ def changeStyle(self, style):
+ comboBox = self.sender()
+ if comboBox:
+ self._barMesh = comboBox.itemData(style)
+ self._primarySeries.setMesh(self._barMesh)
+ self._secondarySeries.setMesh(self._barMesh)
+ self._customData.customSeries().setMesh(self._barMesh)
+
+ def changePresetCamera(self):
+ self._animationCameraX.stop()
+ self._animationCameraY.stop()
+ self._animationCameraZoom.stop()
+ self._animationCameraTarget.stop()
+
+ # Restore camera target in case animation has changed it
+ self._graph.setCameraTargetPosition(QVector3D(0.0, 0.0, 0.0))
+
+ self._preset = QAbstract3DGraph.CameraPreset.Front.value
+
+ self._graph.setCameraPreset(QAbstract3DGraph.CameraPreset(self._preset))
+
+ self._preset += 1
+ if self._preset > QAbstract3DGraph.CameraPreset.DirectlyBelow.value:
+ self._preset = QAbstract3DGraph.CameraPreset.FrontLow.value
+
+ @Slot(int)
+ def changeTheme(self, theme):
+ currentTheme = self._graph.activeTheme()
+ currentTheme.setType(Q3DTheme.Theme(theme))
+ self.backgroundEnabledChanged.emit(currentTheme.isBackgroundEnabled())
+ self.gridEnabledChanged.emit(currentTheme.isGridEnabled())
+ self.fontChanged.emit(currentTheme.font())
+ self.fontSizeChanged.emit(currentTheme.font().pointSize())
+
+ def changeLabelBackground(self):
+ theme = self._graph.activeTheme()
+ theme.setLabelBackgroundEnabled(not theme.isLabelBackgroundEnabled())
+
+ @Slot(int)
+ def changeSelectionMode(self, selectionMode):
+ comboBox = self.sender()
+ if comboBox:
+ flags = comboBox.itemData(selectionMode)
+ self._graph.setSelectionMode(QAbstract3DGraph.SelectionFlags(flags))
+
+ def changeFont(self, font):
+ newFont = font
+ self._graph.activeTheme().setFont(newFont)
+
+ def changeFontSize(self, fontsize):
+ self._fontSize = fontsize
+ font = self._graph.activeTheme().font()
+ font.setPointSize(self._fontSize)
+ self._graph.activeTheme().setFont(font)
+
+ @Slot(QAbstract3DGraph.ShadowQuality)
+ def shadowQualityUpdatedByVisual(self, sq):
+ # Updates the UI component to show correct shadow quality
+ self.shadowQualityChanged.emit(sq.value)
+
+ @Slot(int)
+ def changeLabelRotation(self, rotation):
+ self._temperatureAxis.setLabelAutoRotation(float(rotation))
+ self._monthAxis.setLabelAutoRotation(float(rotation))
+ self._yearAxis.setLabelAutoRotation(float(rotation))
+
+ @Slot(bool)
+ def setAxisTitleVisibility(self, enabled):
+ self._temperatureAxis.setTitleVisible(enabled)
+ self._monthAxis.setTitleVisible(enabled)
+ self._yearAxis.setTitleVisible(enabled)
+
+ @Slot(bool)
+ def setAxisTitleFixed(self, enabled):
+ self._temperatureAxis.setTitleFixed(enabled)
+ self._monthAxis.setTitleFixed(enabled)
+ self._yearAxis.setTitleFixed(enabled)
+
+ @Slot()
+ def zoomToSelectedBar(self):
+ self._animationCameraX.stop()
+ self._animationCameraY.stop()
+ self._animationCameraZoom.stop()
+ self._animationCameraTarget.stop()
+
+ currentX = self._graph.cameraXRotation()
+ currentY = self._graph.cameraYRotation()
+ currentZoom = self._graph.cameraZoomLevel()
+ currentTarget = self._graph.cameraTargetPosition()
+
+ self._animationCameraX.setStartValue(currentX)
+ self._animationCameraY.setStartValue(currentY)
+ self._animationCameraZoom.setStartValue(currentZoom)
+ self._animationCameraTarget.setStartValue(currentTarget)
+
+ selectedBar = (self._graph.selectedSeries().selectedBar()
+ if self._graph.selectedSeries()
+ else QBar3DSeries.invalidSelectionPosition())
+
+ if selectedBar != QBar3DSeries.invalidSelectionPosition():
+ # Normalize selected bar position within axis range to determine
+ # target coordinates
+ endTarget = QVector3D()
+ xMin = self._graph.columnAxis().min()
+ xRange = self._graph.columnAxis().max() - xMin
+ zMin = self._graph.rowAxis().min()
+ zRange = self._graph.rowAxis().max() - zMin
+ endTarget.setX((selectedBar.y() - xMin) / xRange * 2.0 - 1.0)
+ endTarget.setZ((selectedBar.x() - zMin) / zRange * 2.0 - 1.0)
+
+ # Rotate the camera so that it always points approximately to the
+ # graph center
+ endAngleX = 90.0 - degrees(atan(float(endTarget.z() / endTarget.x())))
+ if endTarget.x() > 0.0:
+ endAngleX -= 180.0
+ proxy = self._graph.selectedSeries().dataProxy()
+ barValue = proxy.itemAt(selectedBar.x(), selectedBar.y()).value()
+ endAngleY = 30.0 if barValue >= 0.0 else -30.0
+ if self._graph.valueAxis().reversed():
+ endAngleY *= -1.0
+
+ self._animationCameraX.setEndValue(float(endAngleX))
+ self._animationCameraY.setEndValue(endAngleY)
+ self._animationCameraZoom.setEndValue(250)
+ self._animationCameraTarget.setEndValue(endTarget)
+ else:
+ # No selected bar, so return to the default view
+ self._animationCameraX.setEndValue(self._defaultAngleX)
+ self._animationCameraY.setEndValue(self._defaultAngleY)
+ self._animationCameraZoom.setEndValue(self._defaultZoom)
+ self._animationCameraTarget.setEndValue(self._defaultTarget)
+
+ self._animationCameraX.start()
+ self._animationCameraY.start()
+ self._animationCameraZoom.start()
+ self._animationCameraTarget.start()
+
+ @Slot(bool)
+ def setDataModeToWeather(self, enabled):
+ if enabled:
+ self.changeDataMode(False)
+
+ @Slot(bool)
+ def setDataModeToCustom(self, enabled):
+ if enabled:
+ self.changeDataMode(True)
+
+ def changeShadowQuality(self, quality):
+ sq = QAbstract3DGraph.ShadowQuality(quality)
+ self._graph.setShadowQuality(sq)
+ self.shadowQualityChanged.emit(quality)
+
+ def rotateX(self, rotation):
+ self._xRotation = rotation
+ camera = self._graph.scene().activeCamera()
+ camera.setCameraPosition(self._xRotation, self._yRotation)
+
+ def rotateY(self, rotation):
+ self._yRotation = rotation
+ camera = self._graph.scene().activeCamera()
+ camera.setCameraPosition(self._xRotation, self._yRotation)
+
+ def setBackgroundEnabled(self, enabled):
+ self._graph.activeTheme().setBackgroundEnabled(bool(enabled))
+
+ def setGridEnabled(self, enabled):
+ self._graph.activeTheme().setGridEnabled(bool(enabled))
+
+ def setSmoothBars(self, smooth):
+ self._smooth = bool(smooth)
+ self._primarySeries.setMeshSmooth(self._smooth)
+ self._secondarySeries.setMeshSmooth(self._smooth)
+ self._customData.customSeries().setMeshSmooth(self._smooth)
+
+ def setSeriesVisibility(self, enabled):
+ self._secondarySeries.setVisible(bool(enabled))
+
+ def setReverseValueAxis(self, enabled):
+ self._graph.valueAxis().setReversed(enabled)
+
+ def setReflection(self, enabled):
+ self._graph.setReflection(enabled)
+
+ def changeDataMode(self, customData):
+ # Change between weather data and data from custom proxy
+ if customData:
+ self._graph.removeSeries(self._primarySeries)
+ self._graph.removeSeries(self._secondarySeries)
+ self._graph.addSeries(self._customData.customSeries())
+ self._graph.setValueAxis(self._customData.valueAxis())
+ self._graph.setRowAxis(self._customData.rowAxis())
+ self._graph.setColumnAxis(self._customData.colAxis())
+ else:
+ self._graph.removeSeries(self._customData.customSeries())
+ self._graph.addSeries(self._primarySeries)
+ self._graph.addSeries(self._secondarySeries)
+ self._graph.setValueAxis(self._temperatureAxis)
+ self._graph.setRowAxis(self._yearAxis)
+ self._graph.setColumnAxis(self._monthAxis)
diff --git a/examples/graphs/3d/widgetgallery/highlightseries.py b/examples/graphs/3d/widgetgallery/highlightseries.py
new file mode 100644
index 000000000..8c7b91633
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/highlightseries.py
@@ -0,0 +1,94 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from PySide6.QtCore import QPoint, Qt, Slot
+from PySide6.QtGui import QLinearGradient, QVector3D
+from PySide6.QtGraphs import (QSurface3DSeries, QSurfaceDataItem, Q3DTheme)
+
+
+DARK_RED_POS = 1.0
+RED_POS = 0.8
+YELLOW_POS = 0.6
+GREEN_POS = 0.4
+DARK_GREEN_POS = 0.2
+
+
+class HighlightSeries(QSurface3DSeries):
+
+ def __init__(self):
+ super().__init__()
+ self._width = 100
+ self._height = 100
+ self._srcWidth = 0
+ self._srcHeight = 0
+ self._position = {}
+ self._topographicSeries = None
+ self._minHeight = 0.0
+ self.setDrawMode(QSurface3DSeries.DrawSurface)
+ self.setFlatShadingEnabled(True)
+ self.setVisible(False)
+
+ def setTopographicSeries(self, series):
+ self._topographicSeries = series
+ array = self._topographicSeries.dataProxy().array()
+ self._srcWidth = len(array[0])
+ self._srcHeight = len(array)
+ self._topographicSeries.selectedPointChanged.connect(self.handlePositionChange)
+
+ def setMinHeight(self, height):
+ self. m_minHeight = height
+
+ @Slot(QPoint)
+ def handlePositionChange(self, position):
+ self._position = position
+
+ if position == self.invalidSelectionPosition():
+ self.setVisible(False)
+ return
+
+ halfWidth = self._width / 2
+ halfHeight = self._height / 2
+
+ startX = position.y() - halfWidth
+ if startX < 0:
+ startX = 0
+ endX = position.y() + halfWidth
+ if endX > (self._srcWidth - 1):
+ endX = self._srcWidth - 1
+ startZ = position.x() - halfHeight
+ if startZ < 0:
+ startZ = 0
+ endZ = position.x() + halfHeight
+ if endZ > (self._srcHeight - 1):
+ endZ = self._srcHeight - 1
+
+ srcProxy = self._topographicSeries.dataProxy()
+ srcArray = srcProxy.array()
+
+ dataArray = []
+ for i in range(int(startZ), int(endZ)):
+ newRow = []
+ srcRow = srcArray[i]
+ for j in range(startX, endX):
+ pos = srcRow.at(j).position()
+ pos.setY(pos.y() + 0.1)
+ item = QSurfaceDataItem(QVector3D(pos))
+ newRow.append(item)
+ dataArray.append(newRow)
+ self.dataProxy().resetArray(dataArray)
+ self.setVisible(True)
+
+ @Slot(float)
+ def handleGradientChange(self, value):
+ ratio = self._minHeight / value
+
+ gr = QLinearGradient()
+ gr.setColorAt(0.0, Qt.black)
+ gr.setColorAt(DARK_GREEN_POS * ratio, Qt.darkGreen)
+ gr.setColorAt(GREEN_POS * ratio, Qt.green)
+ gr.setColorAt(YELLOW_POS * ratio, Qt.yellow)
+ gr.setColorAt(RED_POS * ratio, Qt.red)
+ gr.setColorAt(DARK_RED_POS * ratio, Qt.darkRed)
+
+ self.setBaseGradient(gr)
+ self.setColorStyle(Q3DTheme.ColorStyle.RangeGradient)
diff --git a/examples/graphs/3d/widgetgallery/main.py b/examples/graphs/3d/widgetgallery/main.py
new file mode 100644
index 000000000..7bb2238a7
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/main.py
@@ -0,0 +1,41 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+"""PySide6 port of the Qt Graphs widgetgallery example from Qt v6.x"""
+
+import sys
+
+from PySide6.QtCore import QSize
+from PySide6.QtWidgets import QApplication, QTabWidget
+
+from bargraph import BarGraph
+from scattergraph import ScatterGraph
+from surfacegraph import SurfaceGraph
+
+
+if __name__ == "__main__":
+ app = QApplication(sys.argv)
+
+ # Create a tab widget for creating own tabs for Q3DBars, Q3DScatter, and Q3DSurface
+ tabWidget = QTabWidget()
+ tabWidget.setWindowTitle("Widget Gallery")
+
+ screen_size = tabWidget.screen().size()
+ minimum_graph_size = QSize(screen_size.width() / 2, screen_size.height() / 1.75)
+
+ # Create bar graph
+ bars = BarGraph(minimum_graph_size, screen_size)
+ # Create scatter graph
+ scatter = ScatterGraph(minimum_graph_size, screen_size)
+ # Create surface graph
+ surface = SurfaceGraph(minimum_graph_size, screen_size)
+
+ # Add bars widget
+ tabWidget.addTab(bars.barsWidget(), "Bar Graph")
+ # Add scatter widget
+ tabWidget.addTab(scatter.scatterWidget(), "Scatter Graph")
+ # Add surface widget
+ tabWidget.addTab(surface.surfaceWidget(), "Surface Graph")
+
+ tabWidget.show()
+ sys.exit(app.exec())
diff --git a/examples/graphs/3d/widgetgallery/rainfalldata.py b/examples/graphs/3d/widgetgallery/rainfalldata.py
new file mode 100644
index 000000000..d74f45a8b
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/rainfalldata.py
@@ -0,0 +1,125 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import sys
+
+from pathlib import Path
+
+from PySide6.QtCore import QFile, QIODevice, QObject
+from PySide6.QtGraphs import (QBar3DSeries, QCategory3DAxis, QValue3DAxis)
+
+from variantbardataproxy import VariantBarDataProxy
+from variantbardatamapping import VariantBarDataMapping
+from variantdataset import VariantDataSet
+
+
+MONTHS = ["January", "February", "March", "April",
+ "May", "June", "July", "August", "September", "October",
+ "November", "December"]
+
+
+class RainfallData(QObject):
+
+ def __init__(self):
+ super().__init__()
+ self._columnCount = 0
+ self._rowCount = 0
+ self._years = []
+ self._numericMonths = []
+ self._proxy = VariantBarDataProxy()
+ self._mapping = None
+ self._dataSet = None
+ self._series = QBar3DSeries()
+ self._valueAxis = QValue3DAxis()
+ self._rowAxis = QCategory3DAxis()
+ self._colAxis = QCategory3DAxis()
+
+ # In data file the months are in numeric format, so create custom list
+ for i in range(1, 13):
+ self._numericMonths.append(str(i))
+
+ self._columnCount = len(self._numericMonths)
+
+ self.updateYearsList(2010, 2022)
+
+ # Create proxy and series
+ self._proxy = VariantBarDataProxy()
+ self._series = QBar3DSeries(self._proxy)
+
+ self._series.setItemLabelFormat("%.1f mm")
+
+ # Create the axes
+ self._rowAxis = QCategory3DAxis(self)
+ self._colAxis = QCategory3DAxis(self)
+ self._valueAxis = QValue3DAxis(self)
+ self._rowAxis.setAutoAdjustRange(True)
+ self._colAxis.setAutoAdjustRange(True)
+ self._valueAxis.setAutoAdjustRange(True)
+
+ # Set axis labels and titles
+ self._rowAxis.setTitle("Year")
+ self._colAxis.setTitle("Month")
+ self._valueAxis.setTitle("rainfall (mm)")
+ self._valueAxis.setSegmentCount(5)
+ self._rowAxis.setLabels(self._years)
+ self._colAxis.setLabels(MONTHS)
+ self._rowAxis.setTitleVisible(True)
+ self._colAxis.setTitleVisible(True)
+ self._valueAxis.setTitleVisible(True)
+
+ self.addDataSet()
+
+ def customSeries(self):
+ return self._series
+
+ def valueAxis(self):
+ return self._valueAxis
+
+ def rowAxis(self):
+ return self._rowAxis
+
+ def colAxis(self):
+ return self._colAxis
+
+ def updateYearsList(self, start, end):
+ self._years.clear()
+ for i in range(start, end + 1):
+ self._years.append(str(i))
+ self._rowCount = len(self._years)
+
+ def addDataSet(self):
+ # Create a new variant data set and data item list
+ self._dataSet = VariantDataSet()
+ itemList = []
+
+ # Read data from a data file into the data item list
+ file_path = Path(__file__).resolve().parent / "data" / "raindata.txt"
+ dataFile = QFile(file_path)
+ if dataFile.open(QIODevice.ReadOnly | QIODevice.Text):
+ data = dataFile.readAll().data().decode("utf8")
+ for line in data.split("\n"):
+ if line and not line.startswith("#"): # Ignore comments
+ tokens = line.split(",")
+ # Each line has three data items: Year, month, and
+ # rainfall value
+ if len(tokens) >= 3:
+ # Store year and month as strings, and rainfall value
+ # as double into a variant data item and add the item to
+ # the item list.
+ newItem = []
+ newItem.append(tokens[0].strip())
+ newItem.append(tokens[1].strip())
+ newItem.append(float(tokens[2].strip()))
+ itemList.append(newItem)
+ else:
+ print("Unable to open data file:", dataFile.fileName(),
+ file=sys.stderr)
+
+ # Add items to the data set and set it to the proxy
+ self._dataSet.addItems(itemList)
+ self._proxy.setDataSet(self._dataSet)
+
+ # Create new mapping for the data and set it to the proxy
+ self._mapping = VariantBarDataMapping(0, 1, 2,
+ self._years, self._numericMonths)
+ self._proxy.setMapping(self._mapping)
diff --git a/examples/graphs/3d/widgetgallery/scatterdatamodifier.py b/examples/graphs/3d/widgetgallery/scatterdatamodifier.py
new file mode 100644
index 000000000..15064b412
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/scatterdatamodifier.py
@@ -0,0 +1,149 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from math import cos, degrees, sqrt
+
+from PySide6.QtCore import QObject, Signal, Slot, Qt
+from PySide6.QtGui import QVector3D
+from PySide6.QtGraphs import (QAbstract3DGraph, QAbstract3DSeries,
+ QScatterDataItem, QScatterDataProxy,
+ QScatter3DSeries, Q3DTheme)
+
+from axesinputhandler import AxesInputHandler
+
+
+NUMBER_OF_ITEMS = 10000
+CURVE_DIVIDER = 7.5
+LOWER_NUMBER_OF_ITEMS = 900
+LOWER_CURVE_DIVIDER = 0.75
+
+
+class ScatterDataModifier(QObject):
+
+ backgroundEnabledChanged = Signal(bool)
+ gridEnabledChanged = Signal(bool)
+ shadowQualityChanged = Signal(int)
+
+ def __init__(self, scatter, parent):
+ super().__init__(parent)
+
+ self._graph = scatter
+
+ self._style = QAbstract3DSeries.Mesh.Sphere
+ self._smooth = True
+ self._inputHandler = AxesInputHandler(scatter)
+ self._autoAdjust = True
+ self._itemCount = LOWER_NUMBER_OF_ITEMS
+ self._CURVE_DIVIDER = LOWER_CURVE_DIVIDER
+ self._inputHandler = AxesInputHandler(scatter)
+
+ self._graph.activeTheme().setType(Q3DTheme.Theme.StoneMoss)
+ self._graph.setShadowQuality(QAbstract3DGraph.ShadowQuality.SoftHigh)
+ self._graph.setCameraPreset(QAbstract3DGraph.CameraPreset.Front)
+ self._graph.setCameraZoomLevel(80.0)
+
+ self._proxy = QScatterDataProxy()
+ self._series = QScatter3DSeries(self._proxy)
+ self._series.setItemLabelFormat("@xTitle: @xLabel @yTitle: @yLabel @zTitle: @zLabel")
+ self._series.setMeshSmooth(self._smooth)
+ self._graph.addSeries(self._series)
+ self._preset = QAbstract3DGraph.CameraPreset.FrontLow.value
+
+ # Give ownership of the handler to the graph and make it the active
+ # handler
+ self._graph.setActiveInputHandler(self._inputHandler)
+
+ # Give our axes to the input handler
+ self._inputHandler.setAxes(self._graph.axisX(), self._graph.axisZ(),
+ self._graph.axisY())
+
+ self.addData()
+
+ def addData(self):
+ # Configure the axes according to the data
+ self._graph.axisX().setTitle("X")
+ self._graph.axisY().setTitle("Y")
+ self._graph.axisZ().setTitle("Z")
+
+ dataArray = []
+ limit = int(sqrt(self._itemCount) / 2.0)
+ for i in range(-limit, limit):
+ for j in range(-limit, limit):
+ x = float(i) + 0.5
+ y = cos(degrees(float(i * j) / self._CURVE_DIVIDER))
+ z = float(j) + 0.5
+ dataArray.append(QScatterDataItem(QVector3D(x, y, z)))
+
+ self._graph.seriesList()[0].dataProxy().resetArray(dataArray)
+
+ @Slot(int)
+ def changeStyle(self, style):
+ comboBox = self.sender()
+ if comboBox:
+ self._style = comboBox.itemData(style)
+ if self._graph.seriesList():
+ self._graph.seriesList()[0].setMesh(self._style)
+
+ @Slot(int)
+ def setSmoothDots(self, smooth):
+ self._smooth = smooth == Qt.Checked.value
+ series = self._graph.seriesList()[0]
+ series.setMeshSmooth(self._smooth)
+
+ @Slot(int)
+ def changeTheme(self, theme):
+ currentTheme = self._graph.activeTheme()
+ currentTheme.setType(Q3DTheme.Theme(theme))
+ self.backgroundEnabledChanged.emit(currentTheme.isBackgroundEnabled())
+ self.gridEnabledChanged.emit(currentTheme.isGridEnabled())
+
+ @Slot()
+ def changePresetCamera(self):
+ camera = self._graph.scene().activeCamera()
+ camera.setCameraPreset(QAbstract3DGraph.CameraPreset(self._preset))
+
+ self._preset += 1
+ if self._preset > QAbstract3DGraph.CameraPreset.DirectlyBelow.value:
+ self._preset = QAbstract3DGraph.CameraPreset.FrontLow.value
+
+ @Slot(QAbstract3DGraph.ShadowQuality)
+ def shadowQualityUpdatedByVisual(self, sq):
+ self.shadowQualityChanged.emit(sq.value)
+
+ @Slot(int)
+ def changeShadowQuality(self, quality):
+ sq = QAbstract3DGraph.ShadowQuality(quality)
+ self._graph.setShadowQuality(sq)
+
+ @Slot(int)
+ def setBackgroundEnabled(self, enabled):
+ self._graph.activeTheme().setBackgroundEnabled(enabled == Qt.Checked.value)
+
+ @Slot(int)
+ def setGridEnabled(self, enabled):
+ self._graph.activeTheme().setGridEnabled(enabled == Qt.Checked.value)
+
+ @Slot()
+ def toggleItemCount(self):
+ if self._itemCount == NUMBER_OF_ITEMS:
+ self._itemCount = LOWER_NUMBER_OF_ITEMS
+ self._CURVE_DIVIDER = LOWER_CURVE_DIVIDER
+ else:
+ self._itemCount = NUMBER_OF_ITEMS
+ self._CURVE_DIVIDER = CURVE_DIVIDER
+
+ self._graph.seriesList()[0].dataProxy().resetArray([])
+ self.addData()
+
+ @Slot()
+ def toggleRanges(self):
+ if not self._autoAdjust:
+ self._graph.axisX().setAutoAdjustRange(True)
+ self._graph.axisZ().setAutoAdjustRange(True)
+ self._inputHandler.setDragSpeedModifier(1.5)
+ self._autoAdjust = True
+ else:
+ self._graph.axisX().setRange(-10.0, 10.0)
+ self._graph.axisZ().setRange(-10.0, 10.0)
+ self._inputHandler.setDragSpeedModifier(15.0)
+ self._autoAdjust = False
diff --git a/examples/graphs/3d/widgetgallery/scattergraph.py b/examples/graphs/3d/widgetgallery/scattergraph.py
new file mode 100644
index 000000000..79e8933eb
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/scattergraph.py
@@ -0,0 +1,121 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from PySide6.QtCore import QObject, QSize, Qt
+from PySide6.QtWidgets import (QCheckBox, QComboBox, QCommandLinkButton,
+ QLabel, QHBoxLayout, QSizePolicy,
+ QVBoxLayout, QWidget, )
+from PySide6.QtQuickWidgets import QQuickWidget
+from PySide6.QtGraphs import (QAbstract3DSeries, Q3DScatter)
+
+from scatterdatamodifier import ScatterDataModifier
+
+
+class ScatterGraph(QObject):
+
+ def __init__(self, minimum_graph_size, maximum_graph_size):
+ super().__init__()
+ self._scatterGraph = Q3DScatter()
+ self._scatterWidget = QWidget()
+ hLayout = QHBoxLayout(self._scatterWidget)
+ self._scatterGraph.setMinimumSize(minimum_graph_size)
+ self._scatterGraph.setMaximumSize(maximum_graph_size)
+ self._scatterGraph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ self._scatterGraph.setFocusPolicy(Qt.StrongFocus)
+ self._scatterGraph.setResizeMode(QQuickWidget.SizeRootObjectToView)
+ hLayout.addWidget(self._scatterGraph, 1)
+
+ vLayout = QVBoxLayout()
+ hLayout.addLayout(vLayout)
+
+ cameraButton = QCommandLinkButton(self._scatterWidget)
+ cameraButton.setText("Change camera preset")
+ cameraButton.setDescription("Switch between a number of preset camera positions")
+ cameraButton.setIconSize(QSize(0, 0))
+
+ itemCountButton = QCommandLinkButton(self._scatterWidget)
+ itemCountButton.setText("Toggle item count")
+ itemCountButton.setDescription("Switch between 900 and 10000 data points")
+ itemCountButton.setIconSize(QSize(0, 0))
+
+ rangeButton = QCommandLinkButton(self._scatterWidget)
+ rangeButton.setText("Toggle axis ranges")
+ rangeButton.setDescription("Switch between automatic axis ranges and preset ranges")
+ rangeButton.setIconSize(QSize(0, 0))
+
+ backgroundCheckBox = QCheckBox(self._scatterWidget)
+ backgroundCheckBox.setText("Show background")
+ backgroundCheckBox.setChecked(True)
+
+ gridCheckBox = QCheckBox(self._scatterWidget)
+ gridCheckBox.setText("Show grid")
+ gridCheckBox.setChecked(True)
+
+ smoothCheckBox = QCheckBox(self._scatterWidget)
+ smoothCheckBox.setText("Smooth dots")
+ smoothCheckBox.setChecked(True)
+
+ itemStyleList = QComboBox(self._scatterWidget)
+ itemStyleList.addItem("Sphere", QAbstract3DSeries.Mesh.Sphere)
+ itemStyleList.addItem("Cube", QAbstract3DSeries.Mesh.Cube)
+ itemStyleList.addItem("Minimal", QAbstract3DSeries.Mesh.Minimal)
+ itemStyleList.addItem("Point", QAbstract3DSeries.Mesh.Point)
+ itemStyleList.setCurrentIndex(0)
+
+ themeList = QComboBox(self._scatterWidget)
+ themeList.addItem("Qt")
+ themeList.addItem("Primary Colors")
+ themeList.addItem("Digia")
+ themeList.addItem("Stone Moss")
+ themeList.addItem("Army Blue")
+ themeList.addItem("Retro")
+ themeList.addItem("Ebony")
+ themeList.addItem("Isabelle")
+ themeList.setCurrentIndex(3)
+
+ shadowQuality = QComboBox(self._scatterWidget)
+ shadowQuality.addItem("None")
+ shadowQuality.addItem("Low")
+ shadowQuality.addItem("Medium")
+ shadowQuality.addItem("High")
+ shadowQuality.addItem("Low Soft")
+ shadowQuality.addItem("Medium Soft")
+ shadowQuality.addItem("High Soft")
+ shadowQuality.setCurrentIndex(6)
+
+ vLayout.addWidget(cameraButton)
+ vLayout.addWidget(itemCountButton)
+ vLayout.addWidget(rangeButton)
+ vLayout.addWidget(backgroundCheckBox)
+ vLayout.addWidget(gridCheckBox)
+ vLayout.addWidget(smoothCheckBox)
+ vLayout.addWidget(QLabel("Change dot style"))
+ vLayout.addWidget(itemStyleList)
+ vLayout.addWidget(QLabel("Change theme"))
+ vLayout.addWidget(themeList)
+ vLayout.addWidget(QLabel("Adjust shadow quality"))
+ vLayout.addWidget(shadowQuality, 1, Qt.AlignTop)
+
+ self._modifier = ScatterDataModifier(self._scatterGraph, self)
+
+ cameraButton.clicked.connect(self._modifier.changePresetCamera)
+ itemCountButton.clicked.connect(self._modifier.toggleItemCount)
+ rangeButton.clicked.connect(self._modifier.toggleRanges)
+
+ backgroundCheckBox.stateChanged.connect(self._modifier.setBackgroundEnabled)
+ gridCheckBox.stateChanged.connect(self._modifier.setGridEnabled)
+ smoothCheckBox.stateChanged.connect(self._modifier.setSmoothDots)
+
+ self._modifier.backgroundEnabledChanged.connect(backgroundCheckBox.setChecked)
+ self._modifier.gridEnabledChanged.connect(gridCheckBox.setChecked)
+ itemStyleList.currentIndexChanged.connect(self._modifier.changeStyle)
+
+ themeList.currentIndexChanged.connect(self._modifier.changeTheme)
+
+ shadowQuality.currentIndexChanged.connect(self._modifier.changeShadowQuality)
+
+ self._modifier.shadowQualityChanged.connect(shadowQuality.setCurrentIndex)
+ self._scatterGraph.shadowQualityChanged.connect(self._modifier.shadowQualityUpdatedByVisual)
+
+ def scatterWidget(self):
+ return self._scatterWidget
diff --git a/examples/graphs/3d/widgetgallery/surfacegraph.py b/examples/graphs/3d/widgetgallery/surfacegraph.py
new file mode 100644
index 000000000..4052da821
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/surfacegraph.py
@@ -0,0 +1,256 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from surfacegraphmodifier import SurfaceGraphModifier
+
+from PySide6.QtCore import QObject, Qt
+from PySide6.QtGui import QBrush, QIcon, QLinearGradient, QPainter, QPixmap
+from PySide6.QtWidgets import (QGroupBox, QCheckBox, QLabel, QHBoxLayout,
+ QPushButton, QRadioButton, QSizePolicy, QSlider,
+ QVBoxLayout, QWidget)
+from PySide6.QtQuickWidgets import QQuickWidget
+from PySide6.QtGraphs import Q3DSurface
+
+
+def gradientBtoYPB_Pixmap():
+ 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)
+ with QPainter(pm) as pmp:
+ pmp.setBrush(QBrush(grBtoY))
+ pmp.setPen(Qt.NoPen)
+ pmp.drawRect(0, 0, 24, 100)
+ return pm
+
+
+def gradientGtoRPB_Pixmap():
+ 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)
+ pm = QPixmap(24, 100)
+ with QPainter(pm) as pmp:
+ pmp.setBrush(QBrush(grGtoR))
+ pmp.setPen(Qt.NoPen)
+ pmp.drawRect(0, 0, 24, 100)
+ return pm
+
+
+def highlightPixmap():
+ HEIGHT = 400
+ WIDTH = 110
+ BORDER = 10
+ gr = QLinearGradient(0, 0, 1, HEIGHT - 2 * BORDER)
+ gr.setColorAt(1.0, Qt.black)
+ gr.setColorAt(0.8, Qt.darkGreen)
+ gr.setColorAt(0.6, Qt.green)
+ gr.setColorAt(0.4, Qt.yellow)
+ gr.setColorAt(0.2, Qt.red)
+ gr.setColorAt(0.0, Qt.darkRed)
+ pmHighlight = QPixmap(WIDTH, HEIGHT)
+ pmHighlight.fill(Qt.transparent)
+ with QPainter(pmHighlight) as pmpHighlight:
+ pmpHighlight.setBrush(QBrush(gr))
+ pmpHighlight.setPen(Qt.NoPen)
+ pmpHighlight.drawRect(BORDER, BORDER, 35, HEIGHT - 2 * BORDER)
+ pmpHighlight.setPen(Qt.black)
+ step = (HEIGHT - 2 * BORDER) / 5
+ for i in range(0, 6):
+ yPos = i * step + BORDER
+ pmpHighlight.drawLine(BORDER, yPos, 55, yPos)
+ HEIGHT = 550 - (i * 110)
+ pmpHighlight.drawText(60, yPos + 2, f"{HEIGHT} m")
+ return pmHighlight
+
+
+class SurfaceGraph(QObject):
+
+ def __init__(self, minimum_graph_size, maximum_graph_size):
+ super().__init__()
+ self._surfaceGraph = Q3DSurface()
+ self._surfaceWidget = QWidget()
+ hLayout = QHBoxLayout(self._surfaceWidget)
+ self._surfaceGraph.setMinimumSize(minimum_graph_size)
+ self._surfaceGraph.setMaximumSize(maximum_graph_size)
+ self._surfaceGraph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ self._surfaceGraph.setFocusPolicy(Qt.StrongFocus)
+ self._surfaceGraph.setResizeMode(QQuickWidget.SizeRootObjectToView)
+ hLayout.addWidget(self._surfaceGraph, 1)
+ vLayout = QVBoxLayout()
+ hLayout.addLayout(vLayout)
+ vLayout.setAlignment(Qt.AlignTop)
+ # Create control widgets
+ modelGroupBox = QGroupBox("Model")
+ sqrtSinModelRB = QRadioButton(self._surfaceWidget)
+ sqrtSinModelRB.setText("Sqrt and Sin")
+ sqrtSinModelRB.setChecked(False)
+ heightMapModelRB = QRadioButton(self._surfaceWidget)
+ heightMapModelRB.setText("Multiseries\nHeight Map")
+ heightMapModelRB.setChecked(False)
+ texturedModelRB = QRadioButton(self._surfaceWidget)
+ texturedModelRB.setText("Textured\nTopography")
+ texturedModelRB.setChecked(False)
+ modelVBox = QVBoxLayout()
+ modelVBox.addWidget(sqrtSinModelRB)
+ modelVBox.addWidget(heightMapModelRB)
+ modelVBox.addWidget(texturedModelRB)
+ modelGroupBox.setLayout(modelVBox)
+ selectionGroupBox = QGroupBox("Graph Selection Mode")
+ modeNoneRB = QRadioButton(self._surfaceWidget)
+ modeNoneRB.setText("No selection")
+ modeNoneRB.setChecked(False)
+ modeItemRB = QRadioButton(self._surfaceWidget)
+ modeItemRB.setText("Item")
+ modeItemRB.setChecked(False)
+ modeSliceRowRB = QRadioButton(self._surfaceWidget)
+ modeSliceRowRB.setText("Row Slice")
+ modeSliceRowRB.setChecked(False)
+ modeSliceColumnRB = QRadioButton(self._surfaceWidget)
+ modeSliceColumnRB.setText("Column Slice")
+ modeSliceColumnRB.setChecked(False)
+ selectionVBox = QVBoxLayout()
+ selectionVBox.addWidget(modeNoneRB)
+ selectionVBox.addWidget(modeItemRB)
+ selectionVBox.addWidget(modeSliceRowRB)
+ selectionVBox.addWidget(modeSliceColumnRB)
+ selectionGroupBox.setLayout(selectionVBox)
+ axisGroupBox = QGroupBox("Axis ranges")
+ axisMinSliderX = QSlider(Qt.Horizontal)
+ axisMinSliderX.setMinimum(0)
+ axisMinSliderX.setTickInterval(1)
+ axisMinSliderX.setEnabled(True)
+ axisMaxSliderX = QSlider(Qt.Horizontal)
+ axisMaxSliderX.setMinimum(1)
+ axisMaxSliderX.setTickInterval(1)
+ axisMaxSliderX.setEnabled(True)
+ axisMinSliderZ = QSlider(Qt.Horizontal)
+ axisMinSliderZ.setMinimum(0)
+ axisMinSliderZ.setTickInterval(1)
+ axisMinSliderZ.setEnabled(True)
+ axisMaxSliderZ = QSlider(Qt.Horizontal)
+ axisMaxSliderZ.setMinimum(1)
+ axisMaxSliderZ.setTickInterval(1)
+ axisMaxSliderZ.setEnabled(True)
+ axisVBox = QVBoxLayout(axisGroupBox)
+ axisVBox.addWidget(QLabel("Column range"))
+ axisVBox.addWidget(axisMinSliderX)
+ axisVBox.addWidget(axisMaxSliderX)
+ axisVBox.addWidget(QLabel("Row range"))
+ axisVBox.addWidget(axisMinSliderZ)
+ axisVBox.addWidget(axisMaxSliderZ)
+ # Mode-dependent controls
+ # sqrt-sin
+ colorGroupBox = QGroupBox("Custom gradient")
+
+ pixmap = gradientBtoYPB_Pixmap()
+ gradientBtoYPB = QPushButton(self._surfaceWidget)
+ gradientBtoYPB.setIcon(QIcon(pixmap))
+ gradientBtoYPB.setIconSize(pixmap.size())
+
+ pixmap = gradientGtoRPB_Pixmap()
+ gradientGtoRPB = QPushButton(self._surfaceWidget)
+ gradientGtoRPB.setIcon(QIcon(pixmap))
+ gradientGtoRPB.setIconSize(pixmap.size())
+
+ colorHBox = QHBoxLayout(colorGroupBox)
+ colorHBox.addWidget(gradientBtoYPB)
+ colorHBox.addWidget(gradientGtoRPB)
+ # Multiseries heightmap
+ showGroupBox = QGroupBox("Show Object")
+ showGroupBox.setVisible(False)
+ checkboxShowOilRigOne = QCheckBox("Oil Rig 1")
+ checkboxShowOilRigOne.setChecked(True)
+ checkboxShowOilRigTwo = QCheckBox("Oil Rig 2")
+ checkboxShowOilRigTwo.setChecked(True)
+ checkboxShowRefinery = QCheckBox("Refinery")
+ showVBox = QVBoxLayout()
+ showVBox.addWidget(checkboxShowOilRigOne)
+ showVBox.addWidget(checkboxShowOilRigTwo)
+ showVBox.addWidget(checkboxShowRefinery)
+ showGroupBox.setLayout(showVBox)
+ visualsGroupBox = QGroupBox("Visuals")
+ visualsGroupBox.setVisible(False)
+ checkboxVisualsSeeThrough = QCheckBox("See-Through")
+ checkboxHighlightOil = QCheckBox("Highlight Oil")
+ checkboxShowShadows = QCheckBox("Shadows")
+ checkboxShowShadows.setChecked(True)
+ visualVBox = QVBoxLayout(visualsGroupBox)
+ visualVBox.addWidget(checkboxVisualsSeeThrough)
+ visualVBox.addWidget(checkboxHighlightOil)
+ visualVBox.addWidget(checkboxShowShadows)
+ labelSelection = QLabel("Selection:")
+ labelSelection.setVisible(False)
+ labelSelectedItem = QLabel("Nothing")
+ labelSelectedItem.setVisible(False)
+ # Textured topography heightmap
+ enableTexture = QCheckBox("Surface texture")
+ enableTexture.setVisible(False)
+
+ label = QLabel(self._surfaceWidget)
+ label.setPixmap(highlightPixmap())
+ heightMapGroupBox = QGroupBox("Highlight color map")
+ colorMapVBox = QVBoxLayout()
+ colorMapVBox.addWidget(label)
+ heightMapGroupBox.setLayout(colorMapVBox)
+ heightMapGroupBox.setVisible(False)
+ # Populate vertical layout
+ # Common
+ vLayout.addWidget(modelGroupBox)
+ vLayout.addWidget(selectionGroupBox)
+ vLayout.addWidget(axisGroupBox)
+ # Sqrt Sin
+ vLayout.addWidget(colorGroupBox)
+ # Multiseries heightmap
+ vLayout.addWidget(showGroupBox)
+ vLayout.addWidget(visualsGroupBox)
+ vLayout.addWidget(labelSelection)
+ vLayout.addWidget(labelSelectedItem)
+ # Textured topography
+ vLayout.addWidget(heightMapGroupBox)
+ vLayout.addWidget(enableTexture)
+ # Create the controller
+ modifier = SurfaceGraphModifier(self._surfaceGraph, labelSelectedItem, self)
+ # Connect widget controls to controller
+ heightMapModelRB.toggled.connect(modifier.enableHeightMapModel)
+ sqrtSinModelRB.toggled.connect(modifier.enableSqrtSinModel)
+ texturedModelRB.toggled.connect(modifier.enableTopographyModel)
+ 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)
+ # Mode dependent connections
+ gradientBtoYPB.pressed.connect(modifier.setBlackToYellowGradient)
+ gradientGtoRPB.pressed.connect(modifier.setGreenToRedGradient)
+ checkboxShowOilRigOne.stateChanged.connect(modifier.toggleItemOne)
+ checkboxShowOilRigTwo.stateChanged.connect(modifier.toggleItemTwo)
+ checkboxShowRefinery.stateChanged.connect(modifier.toggleItemThree)
+ checkboxVisualsSeeThrough.stateChanged.connect(modifier.toggleSeeThrough)
+ checkboxHighlightOil.stateChanged.connect(modifier.toggleOilHighlight)
+ checkboxShowShadows.stateChanged.connect(modifier.toggleShadows)
+ enableTexture.stateChanged.connect(modifier.toggleSurfaceTexture)
+ # Connections to disable features depending on mode
+ sqrtSinModelRB.toggled.connect(colorGroupBox.setVisible)
+ heightMapModelRB.toggled.connect(showGroupBox.setVisible)
+ heightMapModelRB.toggled.connect(visualsGroupBox.setVisible)
+ heightMapModelRB.toggled.connect(labelSelection.setVisible)
+ heightMapModelRB.toggled.connect(labelSelectedItem.setVisible)
+ texturedModelRB.toggled.connect(enableTexture.setVisible)
+ texturedModelRB.toggled.connect(heightMapGroupBox.setVisible)
+ modifier.setAxisMinSliderX(axisMinSliderX)
+ modifier.setAxisMaxSliderX(axisMaxSliderX)
+ modifier.setAxisMinSliderZ(axisMinSliderZ)
+ modifier.setAxisMaxSliderZ(axisMaxSliderZ)
+ sqrtSinModelRB.setChecked(True)
+ modeItemRB.setChecked(True)
+ enableTexture.setChecked(True)
+
+ def surfaceWidget(self):
+ return self._surfaceWidget
diff --git a/examples/graphs/3d/widgetgallery/surfacegraphmodifier.py b/examples/graphs/3d/widgetgallery/surfacegraphmodifier.py
new file mode 100644
index 000000000..b2706c6fa
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/surfacegraphmodifier.py
@@ -0,0 +1,641 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import os
+from math import sqrt, sin
+from pathlib import Path
+
+from PySide6.QtCore import QObject, QPropertyAnimation, Qt, Slot
+from PySide6.QtGui import (QColor, QFont, QImage, QLinearGradient,
+ QQuaternion, QVector3D)
+from PySide6.QtGraphs import (QAbstract3DGraph, QCustom3DItem,
+ QCustom3DLabel, QHeightMapSurfaceDataProxy,
+ QValue3DAxis, QSurfaceDataItem,
+ QSurfaceDataProxy, QSurface3DSeries,
+ Q3DInputHandler, Q3DTheme)
+
+
+from highlightseries import HighlightSeries
+from topographicseries import TopographicSeries
+from custominputhandler import CustomInputHandler
+
+
+SAMPLE_COUNT_X = 150
+SAMPLE_COUNT_Z = 150
+HEIGHTMAP_GRID_STEP_X = 6
+HEIGHTMAP_GRID_STEP_Z = 6
+SAMPLE_MIN = -8.0
+SAMPLE_MAX = 8.0
+
+AREA_WIDTH = 8000.0
+AREA_HEIGHT = 8000.0
+ASPECT_RATIO = 0.1389
+MIN_RANGE = AREA_WIDTH * 0.49
+
+
+class SurfaceGraphModifier(QObject):
+
+ def __init__(self, surface, label, parent):
+ super().__init__(parent)
+ self._data_path = Path(__file__).resolve().parent / "data"
+ self._graph = surface
+ self._textField = label
+ self._sqrtSinProxy = None
+ self._sqrtSinSeries = None
+ self._heightMapProxyOne = None
+ self._heightMapProxyTwo = None
+ self._heightMapProxyThree = None
+ self._heightMapSeriesOne = None
+ self._heightMapSeriesTwo = None
+ self._heightMapSeriesThree = None
+
+ self._axisMinSliderX = None
+ self._axisMaxSliderX = None
+ self._axisMinSliderZ = None
+ self._axisMaxSliderZ = None
+ self._rangeMinX = 0.0
+ self._rangeMinZ = 0.0
+ self._stepX = 0.0
+ self._stepZ = 0.0
+ self._heightMapWidth = 0
+ self._heightMapHeight = 0
+
+ self._selectionAnimation = None
+ self._titleLabel = None
+ self._previouslyAnimatedItem = None
+ self._previousScaling = {}
+
+ self._topography = None
+ self._highlight = None
+ self._highlightWidth = 0
+ self._highlightHeight = 0
+
+ self._customInputHandler = None
+ self._defaultInputHandler = Q3DInputHandler()
+
+ self._graph.setCameraZoomLevel(85.0)
+ self._graph.setCameraPreset(QAbstract3DGraph.CameraPreset.IsometricRight)
+ self._graph.activeTheme().setType(Q3DTheme.Theme.Retro)
+
+ self._x_axis = QValue3DAxis()
+ self._y_axis = QValue3DAxis()
+ self._z_axis = QValue3DAxis()
+ self._graph.setAxisX(self._x_axis)
+ self._graph.setAxisY(self._y_axis)
+ self._graph.setAxisZ(self._z_axis)
+
+ #
+ # Sqrt Sin
+ #
+ self._sqrtSinProxy = QSurfaceDataProxy()
+ self._sqrtSinSeries = QSurface3DSeries(self._sqrtSinProxy)
+ self.fillSqrtSinProxy()
+
+ #
+ # Multisurface heightmap
+ #
+ # Create the first surface layer
+ heightMapImageOne = QImage(self._data_path / "layer_1.png")
+ self._heightMapProxyOne = QHeightMapSurfaceDataProxy(heightMapImageOne)
+ self._heightMapSeriesOne = QSurface3DSeries(self._heightMapProxyOne)
+ self._heightMapSeriesOne.setItemLabelFormat("(@xLabel, @zLabel): @yLabel")
+ self._heightMapProxyOne.setValueRanges(34.0, 40.0, 18.0, 24.0)
+
+ # Create the other 2 surface layers
+ heightMapImageTwo = QImage(self._data_path / "layer_2.png")
+ self._heightMapProxyTwo = QHeightMapSurfaceDataProxy(heightMapImageTwo)
+ self._heightMapSeriesTwo = QSurface3DSeries(self._heightMapProxyTwo)
+ self._heightMapSeriesTwo.setItemLabelFormat("(@xLabel, @zLabel): @yLabel")
+ self._heightMapProxyTwo.setValueRanges(34.0, 40.0, 18.0, 24.0)
+
+ heightMapImageThree = QImage(self._data_path / "layer_3.png")
+ self._heightMapProxyThree = QHeightMapSurfaceDataProxy(heightMapImageThree)
+ self._heightMapSeriesThree = QSurface3DSeries(self._heightMapProxyThree)
+ self._heightMapSeriesThree.setItemLabelFormat("(@xLabel, @zLabel): @yLabel")
+ self._heightMapProxyThree.setValueRanges(34.0, 40.0, 18.0, 24.0)
+
+ # The images are the same size, so it's enough to get the dimensions
+ # from one
+ self._heightMapWidth = heightMapImageOne.width()
+ self._heightMapHeight = heightMapImageOne.height()
+
+ # Set the gradients for multi-surface layers
+ grOne = QLinearGradient()
+ grOne.setColorAt(0.0, Qt.black)
+ grOne.setColorAt(0.38, Qt.darkYellow)
+ grOne.setColorAt(0.39, Qt.darkGreen)
+ grOne.setColorAt(0.5, Qt.darkGray)
+ grOne.setColorAt(1.0, Qt.gray)
+ self._heightMapSeriesOne.setBaseGradient(grOne)
+ self._heightMapSeriesOne.setColorStyle(Q3DTheme.ColorStyle.RangeGradient)
+
+ grTwo = QLinearGradient()
+ grTwo.setColorAt(0.39, Qt.blue)
+ grTwo.setColorAt(0.4, Qt.white)
+ self._heightMapSeriesTwo.setBaseGradient(grTwo)
+ self._heightMapSeriesTwo.setColorStyle(Q3DTheme.ColorStyle.RangeGradient)
+
+ grThree = QLinearGradient()
+ grThree.setColorAt(0.0, Qt.white)
+ grThree.setColorAt(0.05, Qt.black)
+ self._heightMapSeriesThree.setBaseGradient(grThree)
+ self._heightMapSeriesThree.setColorStyle(Q3DTheme.ColorStyle.RangeGradient)
+
+ # Custom items and label
+ self._graph.selectedElementChanged.connect(self.handleElementSelected)
+
+ self._selectionAnimation = QPropertyAnimation(self)
+ self._selectionAnimation.setPropertyName(b"scaling")
+ self._selectionAnimation.setDuration(500)
+ self._selectionAnimation.setLoopCount(-1)
+
+ titleFont = QFont("Century Gothic", 30)
+ titleFont.setBold(True)
+ self._titleLabel = QCustom3DLabel("Oil Rigs on Imaginary Sea", titleFont,
+ QVector3D(0.0, 1.2, 0.0),
+ QVector3D(1.0, 1.0, 0.0),
+ QQuaternion())
+ self._titleLabel.setPositionAbsolute(True)
+ self._titleLabel.setFacingCamera(True)
+ self._titleLabel.setBackgroundColor(QColor(0x66cdaa))
+ self._graph.addCustomItem(self._titleLabel)
+ self._titleLabel.setVisible(False)
+
+ # Make two of the custom object visible
+ self.toggleItemOne(True)
+ self.toggleItemTwo(True)
+
+ #
+ # Topographic map
+ #
+ self._topography = TopographicSeries()
+ file_name = os.fspath(self._data_path / "topography.png")
+ self._topography.setTopographyFile(file_name, AREA_WIDTH, AREA_HEIGHT)
+ self._topography.setItemLabelFormat("@yLabel m")
+
+ self._highlight = HighlightSeries()
+ self._highlight.setTopographicSeries(self._topography)
+ self._highlight.setMinHeight(MIN_RANGE * ASPECT_RATIO)
+ self._highlight.handleGradientChange(AREA_WIDTH * ASPECT_RATIO)
+ self._graph.axisY().maxChanged.connect(self._highlight.handleGradientChange)
+
+ self._customInputHandler = CustomInputHandler(self._graph)
+ self._customInputHandler.setHighlightSeries(self._highlight)
+ self._customInputHandler.setAxes(self._x_axis, self._y_axis, self._z_axis)
+ self._customInputHandler.setLimits(0.0, AREA_WIDTH, MIN_RANGE)
+ self._customInputHandler.setAspectRatio(ASPECT_RATIO)
+
+ def fillSqrtSinProxy(self):
+ stepX = (SAMPLE_MAX - SAMPLE_MIN) / float(SAMPLE_COUNT_X - 1)
+ stepZ = (SAMPLE_MAX - SAMPLE_MIN) / float(SAMPLE_COUNT_Z - 1)
+
+ dataArray = []
+ for i in range(0, SAMPLE_COUNT_Z):
+ newRow = []
+ # Keep values within range bounds, since just adding step can
+ # cause minor drift due to the rounding errors.
+ z = min(SAMPLE_MAX, (i * stepZ + SAMPLE_MIN))
+ for j in range(0, SAMPLE_COUNT_X):
+ x = min(SAMPLE_MAX, (j * stepX + SAMPLE_MIN))
+ R = sqrt(z * z + x * x) + 0.01
+ y = (sin(R) / R + 0.24) * 1.61
+ item = QSurfaceDataItem(QVector3D(x, y, z))
+ newRow.append(item)
+ dataArray.append(newRow)
+ self._sqrtSinProxy.resetArray(dataArray)
+
+ @Slot(bool)
+ def enableSqrtSinModel(self, enable):
+ if enable:
+ self._sqrtSinSeries.setDrawMode(QSurface3DSeries.DrawSurfaceAndWireframe)
+ self._sqrtSinSeries.setFlatShadingEnabled(True)
+
+ self._graph.axisX().setLabelFormat("%.2f")
+ self._graph.axisZ().setLabelFormat("%.2f")
+ self._graph.axisX().setRange(SAMPLE_MIN, SAMPLE_MAX)
+ self._graph.axisY().setRange(0.0, 2.0)
+ self._graph.axisZ().setRange(SAMPLE_MIN, SAMPLE_MAX)
+ self._graph.axisX().setLabelAutoRotation(30.0)
+ self._graph.axisY().setLabelAutoRotation(90.0)
+ self._graph.axisZ().setLabelAutoRotation(30.0)
+
+ self._graph.removeSeries(self._heightMapSeriesOne)
+ self._graph.removeSeries(self._heightMapSeriesTwo)
+ self._graph.removeSeries(self._heightMapSeriesThree)
+ self._graph.removeSeries(self._topography)
+ self._graph.removeSeries(self._highlight)
+
+ self._graph.addSeries(self._sqrtSinSeries)
+
+ self._titleLabel.setVisible(False)
+ self._graph.axisX().setTitleVisible(False)
+ self._graph.axisY().setTitleVisible(False)
+ self._graph.axisZ().setTitleVisible(False)
+
+ self._graph.axisX().setTitle("")
+ self._graph.axisY().setTitle("")
+ self._graph.axisZ().setTitle("")
+
+ self._graph.setActiveInputHandler(self._defaultInputHandler)
+
+ # Reset range sliders for Sqrt & Sin
+ self._rangeMinX = SAMPLE_MIN
+ self._rangeMinZ = SAMPLE_MIN
+ self._stepX = (SAMPLE_MAX - SAMPLE_MIN) / float(SAMPLE_COUNT_X - 1)
+ self._stepZ = (SAMPLE_MAX - SAMPLE_MIN) / float(SAMPLE_COUNT_Z - 1)
+ self._axisMinSliderX.setMinimum(0)
+ self._axisMinSliderX.setMaximum(SAMPLE_COUNT_X - 2)
+ self._axisMinSliderX.setValue(0)
+ self._axisMaxSliderX.setMinimum(1)
+ self._axisMaxSliderX.setMaximum(SAMPLE_COUNT_X - 1)
+ self._axisMaxSliderX.setValue(SAMPLE_COUNT_X - 1)
+ self._axisMinSliderZ.setMinimum(0)
+ self._axisMinSliderZ.setMaximum(SAMPLE_COUNT_Z - 2)
+ self._axisMinSliderZ.setValue(0)
+ self._axisMaxSliderZ.setMinimum(1)
+ self._axisMaxSliderZ.setMaximum(SAMPLE_COUNT_Z - 1)
+ self._axisMaxSliderZ.setValue(SAMPLE_COUNT_Z - 1)
+
+ @Slot(bool)
+ def enableHeightMapModel(self, enable):
+ if enable:
+ self._heightMapSeriesOne.setDrawMode(QSurface3DSeries.DrawSurface)
+ self._heightMapSeriesOne.setFlatShadingEnabled(False)
+ self._heightMapSeriesTwo.setDrawMode(QSurface3DSeries.DrawSurface)
+ self._heightMapSeriesTwo.setFlatShadingEnabled(False)
+ self._heightMapSeriesThree.setDrawMode(QSurface3DSeries.DrawSurface)
+ self._heightMapSeriesThree.setFlatShadingEnabled(False)
+
+ self._graph.axisX().setLabelFormat("%.1f N")
+ self._graph.axisZ().setLabelFormat("%.1f E")
+ self._graph.axisX().setRange(34.0, 40.0)
+ self._graph.axisY().setAutoAdjustRange(True)
+ self._graph.axisZ().setRange(18.0, 24.0)
+
+ self._graph.axisX().setTitle("Latitude")
+ self._graph.axisY().setTitle("Height")
+ self._graph.axisZ().setTitle("Longitude")
+
+ self._graph.removeSeries(self._sqrtSinSeries)
+ self._graph.removeSeries(self._topography)
+ self._graph.removeSeries(self._highlight)
+ self._graph.addSeries(self._heightMapSeriesOne)
+ self._graph.addSeries(self._heightMapSeriesTwo)
+ self._graph.addSeries(self._heightMapSeriesThree)
+
+ self._graph.setActiveInputHandler(self._defaultInputHandler)
+
+ self._titleLabel.setVisible(True)
+ self._graph.axisX().setTitleVisible(True)
+ self._graph.axisY().setTitleVisible(True)
+ self._graph.axisZ().setTitleVisible(True)
+
+ # Reset range sliders for height map
+ mapGridCountX = self._heightMapWidth / HEIGHTMAP_GRID_STEP_X
+ mapGridCountZ = self._heightMapHeight / HEIGHTMAP_GRID_STEP_Z
+ self._rangeMinX = 34.0
+ self._rangeMinZ = 18.0
+ self._stepX = 6.0 / float(mapGridCountX - 1)
+ self._stepZ = 6.0 / float(mapGridCountZ - 1)
+ self._axisMinSliderX.setMinimum(0)
+ self._axisMinSliderX.setMaximum(mapGridCountX - 2)
+ self._axisMinSliderX.setValue(0)
+ self._axisMaxSliderX.setMinimum(1)
+ self._axisMaxSliderX.setMaximum(mapGridCountX - 1)
+ self._axisMaxSliderX.setValue(mapGridCountX - 1)
+ self._axisMinSliderZ.setMinimum(0)
+ self._axisMinSliderZ.setMaximum(mapGridCountZ - 2)
+ self._axisMinSliderZ.setValue(0)
+ self._axisMaxSliderZ.setMinimum(1)
+ self._axisMaxSliderZ.setMaximum(mapGridCountZ - 1)
+ self._axisMaxSliderZ.setValue(mapGridCountZ - 1)
+
+ @Slot(bool)
+ def enableTopographyModel(self, enable):
+ if enable:
+ self._graph.axisX().setLabelFormat("%i")
+ self._graph.axisZ().setLabelFormat("%i")
+ self._graph.axisX().setRange(0.0, AREA_WIDTH)
+ self._graph.axisY().setRange(100.0, AREA_WIDTH * ASPECT_RATIO)
+ self._graph.axisZ().setRange(0.0, AREA_HEIGHT)
+ self._graph.axisX().setLabelAutoRotation(30.0)
+ self._graph.axisY().setLabelAutoRotation(90.0)
+ self._graph.axisZ().setLabelAutoRotation(30.0)
+
+ self._graph.removeSeries(self._heightMapSeriesOne)
+ self._graph.removeSeries(self._heightMapSeriesTwo)
+ self._graph.removeSeries(self._heightMapSeriesThree)
+ self._graph.addSeries(self._topography)
+ self._graph.addSeries(self._highlight)
+
+ self._titleLabel.setVisible(False)
+ self._graph.axisX().setTitleVisible(False)
+ self._graph.axisY().setTitleVisible(False)
+ self._graph.axisZ().setTitleVisible(False)
+
+ self._graph.axisX().setTitle("")
+ self._graph.axisY().setTitle("")
+ self._graph.axisZ().setTitle("")
+
+ self._graph.setActiveInputHandler(self._customInputHandler)
+
+ # Reset range sliders for topography map
+ self._rangeMinX = 0.0
+ self._rangeMinZ = 0.0
+ self._stepX = 1.0
+ self._stepZ = 1.0
+ self._axisMinSliderX.setMinimum(0)
+ self._axisMinSliderX.setMaximum(AREA_WIDTH - 200)
+ self._axisMinSliderX.setValue(0)
+ self._axisMaxSliderX.setMinimum(200)
+ self._axisMaxSliderX.setMaximum(AREA_WIDTH)
+ self._axisMaxSliderX.setValue(AREA_WIDTH)
+ self._axisMinSliderZ.setMinimum(0)
+ self._axisMinSliderZ.setMaximum(AREA_HEIGHT - 200)
+ self._axisMinSliderZ.setValue(0)
+ self._axisMaxSliderZ.setMinimum(200)
+ self._axisMaxSliderZ.setMaximum(AREA_HEIGHT)
+ self._axisMaxSliderZ.setValue(AREA_HEIGHT)
+
+ def adjustXMin(self, min):
+ minX = self._stepX * float(min) + self._rangeMinX
+
+ max = self._axisMaxSliderX.value()
+ if min >= max:
+ max = min + 1
+ self._axisMaxSliderX.setValue(max)
+
+ maxX = self._stepX * max + self._rangeMinX
+
+ self.setAxisXRange(minX, maxX)
+
+ def adjustXMax(self, max):
+ maxX = self._stepX * float(max) + self._rangeMinX
+
+ min = self._axisMinSliderX.value()
+ if max <= min:
+ min = max - 1
+ self._axisMinSliderX.setValue(min)
+
+ minX = self._stepX * min + self._rangeMinX
+
+ self.setAxisXRange(minX, maxX)
+
+ def adjustZMin(self, min):
+ minZ = self._stepZ * float(min) + self._rangeMinZ
+
+ max = self._axisMaxSliderZ.value()
+ if min >= max:
+ max = min + 1
+ self._axisMaxSliderZ.setValue(max)
+
+ maxZ = self._stepZ * max + self._rangeMinZ
+
+ self.setAxisZRange(minZ, maxZ)
+
+ def adjustZMax(self, max):
+ maxX = self._stepZ * float(max) + self._rangeMinZ
+
+ min = self._axisMinSliderZ.value()
+ if max <= min:
+ min = max - 1
+ self._axisMinSliderZ.setValue(min)
+
+ minX = self._stepZ * min + self._rangeMinZ
+
+ self.setAxisZRange(minX, maxX)
+
+ def setAxisXRange(self, min, max):
+ self._graph.axisX().setRange(min, max)
+
+ def setAxisZRange(self, min, max):
+ self._graph.axisZ().setRange(min, max)
+
+ 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._sqrtSinSeries.setBaseGradient(gr)
+ self._sqrtSinSeries.setColorStyle(Q3DTheme.ColorStyle.RangeGradient)
+
+ 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)
+
+ self._sqrtSinSeries.setBaseGradient(gr)
+ self._sqrtSinSeries.setColorStyle(Q3DTheme.ColorStyle.RangeGradient)
+
+ @Slot(bool)
+ def toggleItemOne(self, show):
+ positionOne = QVector3D(39.0, 77.0, 19.2)
+ positionOnePipe = QVector3D(39.0, 45.0, 19.2)
+ positionOneLabel = QVector3D(39.0, 107.0, 19.2)
+ if show:
+ color = QImage(2, 2, QImage.Format_RGB32)
+ color.fill(Qt.red)
+ file_name = os.fspath(self._data_path / "oilrig.mesh")
+ item = QCustom3DItem(file_name, positionOne,
+ QVector3D(0.025, 0.025, 0.025),
+ QQuaternion.fromAxisAndAngle(0.0, 1.0, 0.0, 45.0),
+ color)
+ self._graph.addCustomItem(item)
+ file_name = os.fspath(self._data_path / "pipe.mesh")
+ item = QCustom3DItem(file_name, positionOnePipe,
+ QVector3D(0.005, 0.5, 0.005), QQuaternion(),
+ color)
+ item.setShadowCasting(False)
+ self._graph.addCustomItem(item)
+
+ label = QCustom3DLabel()
+ label.setText("Oil Rig One")
+ label.setPosition(positionOneLabel)
+ label.setScaling(QVector3D(1.0, 1.0, 1.0))
+ self._graph.addCustomItem(label)
+ else:
+ self.resetSelection()
+ self._graph.removeCustomItemAt(positionOne)
+ self._graph.removeCustomItemAt(positionOnePipe)
+ self._graph.removeCustomItemAt(positionOneLabel)
+
+ @Slot(bool)
+ def toggleItemTwo(self, show):
+ positionTwo = QVector3D(34.5, 77.0, 23.4)
+ positionTwoPipe = QVector3D(34.5, 45.0, 23.4)
+ positionTwoLabel = QVector3D(34.5, 107.0, 23.4)
+ if show:
+ color = QImage(2, 2, QImage.Format_RGB32)
+ color.fill(Qt.red)
+ item = QCustom3DItem()
+ file_name = os.fspath(self._data_path / "oilrig.mesh")
+ item.setMeshFile(file_name)
+ item.setPosition(positionTwo)
+ item.setScaling(QVector3D(0.025, 0.025, 0.025))
+ item.setRotation(QQuaternion.fromAxisAndAngle(0.0, 1.0, 0.0, 25.0))
+ item.setTextureImage(color)
+ self._graph.addCustomItem(item)
+ file_name = os.fspath(self._data_path / "pipe.mesh")
+ item = QCustom3DItem(file_name, positionTwoPipe,
+ QVector3D(0.005, 0.5, 0.005), QQuaternion(),
+ color)
+ item.setShadowCasting(False)
+ self._graph.addCustomItem(item)
+
+ label = QCustom3DLabel()
+ label.setText("Oil Rig Two")
+ label.setPosition(positionTwoLabel)
+ label.setScaling(QVector3D(1.0, 1.0, 1.0))
+ self._graph.addCustomItem(label)
+ else:
+ self.resetSelection()
+ self._graph.removeCustomItemAt(positionTwo)
+ self._graph.removeCustomItemAt(positionTwoPipe)
+ self._graph.removeCustomItemAt(positionTwoLabel)
+
+ @Slot(bool)
+ def toggleItemThree(self, show):
+ positionThree = QVector3D(34.5, 86.0, 19.1)
+ positionThreeLabel = QVector3D(34.5, 116.0, 19.1)
+ if show:
+ color = QImage(2, 2, QImage.Format_RGB32)
+ color.fill(Qt.darkMagenta)
+ item = QCustom3DItem()
+ file_name = os.fspath(self._data_path / "refinery.mesh")
+ item.setMeshFile(file_name)
+ item.setPosition(positionThree)
+ item.setScaling(QVector3D(0.04, 0.04, 0.04))
+ item.setRotation(QQuaternion.fromAxisAndAngle(0.0, 1.0, 0.0, 75.0))
+ item.setTextureImage(color)
+ self._graph.addCustomItem(item)
+
+ label = QCustom3DLabel()
+ label.setText("Refinery")
+ label.setPosition(positionThreeLabel)
+ label.setScaling(QVector3D(1.0, 1.0, 1.0))
+ self._graph.addCustomItem(label)
+ else:
+ self.resetSelection()
+ self._graph.removeCustomItemAt(positionThree)
+ self._graph.removeCustomItemAt(positionThreeLabel)
+
+ @Slot(bool)
+ def toggleSeeThrough(self, seethrough):
+ s0 = self._graph.seriesList()[0]
+ s1 = self._graph.seriesList()[1]
+ if seethrough:
+ s0.setDrawMode(QSurface3DSeries.DrawWireframe)
+ s1.setDrawMode(QSurface3DSeries.DrawWireframe)
+ else:
+ s0.setDrawMode(QSurface3DSeries.DrawSurface)
+ s1.setDrawMode(QSurface3DSeries.DrawSurface)
+
+ @Slot(bool)
+ def toggleOilHighlight(self, highlight):
+ s2 = self._graph.seriesList()[2]
+ if highlight:
+ grThree = QLinearGradient()
+ grThree.setColorAt(0.0, Qt.black)
+ grThree.setColorAt(0.05, Qt.red)
+ s2.setBaseGradient(grThree)
+ else:
+ grThree = QLinearGradient()
+ grThree.setColorAt(0.0, Qt.white)
+ grThree.setColorAt(0.05, Qt.black)
+ s2.setBaseGradient(grThree)
+
+ @Slot(bool)
+ def toggleShadows(self, shadows):
+ sq = (QAbstract3DGraph.ShadowQualityMedium
+ if shadows else QAbstract3DGraph.ShadowQualityNone)
+ self._graph.setShadowQuality(sq)
+
+ @Slot(bool)
+ def toggleSurfaceTexture(self, enable):
+ if enable:
+ file_name = os.fspath(self._data_path / "maptexture.jpg")
+ self._topography.setTextureFile(file_name)
+ else:
+ self._topography.setTextureFile("")
+
+ def handleElementSelected(self, type):
+ self.resetSelection()
+ if type == QAbstract3DGraph.ElementCustomItem:
+ item = self._graph.selectedCustomItem()
+ text = ""
+ if isinstance(item, QCustom3DItem):
+ text += "Custom label: "
+ else:
+ file = item.meshFile().split("/")[-1]
+ text += f"{file}: "
+
+ text += str(self._graph.selectedCustomItemIndex())
+ self._textField.setText(text)
+ self._previouslyAnimatedItem = item
+ self._previousScaling = item.scaling()
+ self._selectionAnimation.setTargetObject(item)
+ self._selectionAnimation.setStartValue(item.scaling())
+ self._selectionAnimation.setEndValue(item.scaling() * 1.5)
+ self._selectionAnimation.start()
+ elif type == QAbstract3DGraph.ElementSeries:
+ text = "Surface ("
+ series = self._graph.selectedSeries()
+ if series:
+ point = series.selectedPoint()
+ text += f"{point.x()}, {point.y()}"
+ text += ")"
+ self._textField.setText(text)
+ elif (type.value > QAbstract3DGraph.ElementSeries.value
+ and type < QAbstract3DGraph.ElementCustomItem.value):
+ index = self._graph.selectedLabelIndex()
+ text = ""
+ if type == QAbstract3DGraph.ElementAxisXLabel:
+ text += "Axis X label: "
+ elif type == QAbstract3DGraph.ElementAxisYLabel:
+ text += "Axis Y label: "
+ else:
+ text += "Axis Z label: "
+ text += str(index)
+ self._textField.setText(text)
+ else:
+ self._textField.setText("Nothing")
+
+ def resetSelection(self):
+ self._selectionAnimation.stop()
+ if self._previouslyAnimatedItem:
+ self._previouslyAnimatedItem.setScaling(self._previousScaling)
+ self._previouslyAnimatedItem = None
+
+ def toggleModeNone(self):
+ self._graph.setSelectionMode(QAbstract3DGraph.SelectionNone)
+
+ def toggleModeItem(self):
+ self._graph.setSelectionMode(QAbstract3DGraph.SelectionItem)
+
+ def toggleModeSliceRow(self):
+ sm = (QAbstract3DGraph.SelectionItemAndRow
+ | QAbstract3DGraph.SelectionSlice
+ | QAbstract3DGraph.SelectionMultiSeries)
+ self._graph.setSelectionMode(sm)
+
+ def toggleModeSliceColumn(self):
+ sm = (QAbstract3DGraph.SelectionItemAndColumn
+ | QAbstract3DGraph.SelectionSlice
+ | QAbstract3DGraph.SelectionMultiSeries)
+ self._graph.setSelectionMode(sm)
+
+ def setAxisMinSliderX(self, slider):
+ self._axisMinSliderX = slider
+
+ def setAxisMaxSliderX(self, slider):
+ self._axisMaxSliderX = slider
+
+ def setAxisMinSliderZ(self, slider):
+ self._axisMinSliderZ = slider
+
+ def setAxisMaxSliderZ(self, slider):
+ self._axisMaxSliderZ = slider
diff --git a/examples/graphs/3d/widgetgallery/topographicseries.py b/examples/graphs/3d/widgetgallery/topographicseries.py
new file mode 100644
index 000000000..4f286a222
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/topographicseries.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QImage, QVector3D
+from PySide6.QtGraphs import (QSurface3DSeries, QSurfaceDataItem)
+
+
+# Value used to encode height data as RGB value on PNG file
+PACKING_FACTOR = 11983.0
+
+
+class TopographicSeries(QSurface3DSeries):
+
+ def __init__(self):
+ super().__init__()
+ self._sampleCountX = 0.0
+ self._sampleCountZ = 0.0
+ self.setDrawMode(QSurface3DSeries.DrawSurface)
+ self.setFlatShadingEnabled(True)
+ self.setBaseColor(Qt.white)
+
+ def sampleCountX(self):
+ return self._sampleCountX
+
+ def sampleCountZ(self):
+ return self._sampleCountZ
+
+ def setTopographyFile(self, file, width, height):
+ heightMapImage = QImage(file)
+ bits = heightMapImage.bits()
+ imageHeight = heightMapImage.height()
+ imageWidth = heightMapImage.width()
+ widthBits = imageWidth * 4
+ stepX = width / float(imageWidth)
+ stepZ = height / float(imageHeight)
+
+ dataArray = []
+ for i in range(0, imageHeight):
+ p = i * widthBits
+ z = height - float(i) * stepZ
+ newRow = []
+ for j in range(0, imageWidth):
+ aa = bits[p + 0]
+ rr = bits[p + 1]
+ gg = bits[p + 2]
+ color = (gg << 16) + (rr << 8) + aa
+ y = float(color) / PACKING_FACTOR
+ item = QSurfaceDataItem(QVector3D(float(j) * stepX, y, z))
+ newRow.append(item)
+ p += 4
+ dataArray.append(newRow)
+
+ self.dataProxy().resetArray(dataArray)
+
+ self._sampleCountX = float(imageWidth)
+ self._sampleCountZ = float(imageHeight)
diff --git a/examples/graphs/3d/widgetgallery/variantbardatamapping.py b/examples/graphs/3d/widgetgallery/variantbardatamapping.py
new file mode 100644
index 000000000..50bdefa6a
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/variantbardatamapping.py
@@ -0,0 +1,67 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from PySide6.QtCore import QObject, Signal
+
+
+class VariantBarDataMapping(QObject):
+
+ rowIndexChanged = Signal()
+ columnIndexChanged = Signal()
+ valueIndexChanged = Signal()
+ rowCategoriesChanged = Signal()
+ columnCategoriesChanged = Signal()
+ mappingChanged = Signal()
+
+ def __init__(self, rowIndex, columnIndex, valueIndex,
+ rowCategories=[], columnCategories=[]):
+ super().__init__(None)
+ self._rowIndex = rowIndex
+ self._columnIndex = columnIndex
+ self._valueIndex = valueIndex
+ self._rowCategories = rowCategories
+ self._columnCategories = columnCategories
+
+ def setRowIndex(self, index):
+ self._rowIndex = index
+ self.mappingChanged.emit()
+
+ def rowIndex(self):
+ return self._rowIndex
+
+ def setColumnIndex(self, index):
+ self._columnIndex = index
+ self.mappingChanged.emit()
+
+ def columnIndex(self):
+ return self._columnIndex
+
+ def setValueIndex(self, index):
+ self._valueIndex = index
+ self.mappingChanged.emit()
+
+ def valueIndex(self):
+ return self._valueIndex
+
+ def setRowCategories(self, categories):
+ self._rowCategories = categories
+ self.mappingChanged.emit()
+
+ def rowCategories(self):
+ return self._rowCategories
+
+ def setColumnCategories(self, categories):
+ self._columnCategories = categories
+ self.mappingChanged.emit()
+
+ def columnCategories(self):
+ return self._columnCategories
+
+ def remap(self, rowIndex, columnIndex, valueIndex,
+ rowCategories=[], columnCategories=[]):
+ self._rowIndex = rowIndex
+ self._columnIndex = columnIndex
+ self._valueIndex = valueIndex
+ self._rowCategories = rowCategories
+ self._columnCategories = columnCategories
+ self.mappingChanged.emit()
diff --git a/examples/graphs/3d/widgetgallery/variantbardataproxy.py b/examples/graphs/3d/widgetgallery/variantbardataproxy.py
new file mode 100644
index 000000000..5ab2a2cd2
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/variantbardataproxy.py
@@ -0,0 +1,100 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from PySide6.QtCore import Slot
+from PySide6.QtGraphs import QBarDataProxy, QBarDataItem
+
+
+class VariantBarDataProxy(QBarDataProxy):
+
+ def __init__(self):
+ super().__init__()
+ self._dataSet = None
+ self._mapping = None
+
+ def setDataSet(self, newSet):
+ if self._dataSet:
+ self._dataSet.itemsAdded.disconnect(self.handleItemsAdded)
+ self._dataSet.dataCleared.disconnect(self.handleDataCleared)
+
+ self._dataSet = newSet
+
+ if self._dataSet:
+ self._dataSet.itemsAdded.connect(self.handleItemsAdded)
+ self._dataSet.dataCleared.connect(self.handleDataCleared)
+ self.resolveDataSet()
+
+ def dataSet(self):
+ return self._dataSet.data()
+
+ # Map key (row, column, value) to value index in data item (VariantItem).
+ # Doesn't gain ownership of mapping, but does connect to it to listen for
+ # mapping changes. Modifying mapping that is set to proxy will trigger
+ # dataset re-resolving.
+ def setMapping(self, mapping):
+ if self._mapping:
+ self._mapping.mappingChanged.disconnect(self.handleMappingChanged)
+
+ self._mapping = mapping
+
+ if self._mapping:
+ self._mapping.mappingChanged.connect(self.handleMappingChanged)
+
+ self.resolveDataSet()
+
+ def mapping(self):
+ return self._mapping.data()
+
+ @Slot(int, int)
+ def handleItemsAdded(self, index, count):
+ # Resolve new items
+ self.resolveDataSet()
+
+ @Slot()
+ def handleDataCleared(self):
+ # Data cleared, reset array
+ self.resetArray(None)
+
+ @Slot()
+ def handleMappingChanged(self):
+ self.resolveDataSet()
+
+ # Resolve entire dataset into QBarDataArray.
+ def resolveDataSet(self):
+ # If we have no data or mapping, or the categories are not defined,
+ # simply clear the array
+ if (not self._dataSet or not self._mapping
+ or not self._mapping.rowCategories()
+ or not self._mapping.columnCategories()):
+ self.resetArray()
+ return
+
+ itemList = self._dataSet.itemList()
+
+ rowIndex = self._mapping.rowIndex()
+ columnIndex = self._mapping.columnIndex()
+ valueIndex = self._mapping.valueIndex()
+ rowList = self._mapping.rowCategories()
+ columnList = self._mapping.columnCategories()
+
+ # Sort values into rows and columns
+ itemValueMap = {}
+ for item in itemList:
+ key = str(item[rowIndex])
+ v = itemValueMap.get(key)
+ if not v:
+ v = {}
+ itemValueMap[key] = v
+ v[str(item[columnIndex])] = float(item[valueIndex])
+
+ # Create a new data array in format the parent class understands
+ newProxyArray = []
+ for rowKey in rowList:
+ newProxyRow = []
+ for i in range(0, len(columnList)):
+ item = QBarDataItem(itemValueMap[rowKey][columnList[i]])
+ newProxyRow.append(item)
+ newProxyArray.append(newProxyRow)
+
+ # Finally, reset the data array in the parent class
+ self.resetArray(newProxyArray)
diff --git a/examples/graphs/3d/widgetgallery/variantdataset.py b/examples/graphs/3d/widgetgallery/variantdataset.py
new file mode 100644
index 000000000..752bc3887
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/variantdataset.py
@@ -0,0 +1,39 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from PySide6.QtCore import QObject, Signal
+
+
+class VariantDataSet(QObject):
+
+ itemsAdded = Signal(int, int)
+ dataCleared = Signal()
+
+ def __init__(self):
+ super().__init__()
+ self._variantData = []
+
+ def clear(self):
+ for item in self._variantData:
+ item.clear()
+ del item
+
+ self._variantData.clear()
+ self.dataCleared.emit()
+
+ def addItem(self, item):
+ self._variantData.append(item)
+ addIndex = len(self._variantData)
+
+ self.itemsAdded.emit(addIndex, 1)
+ return addIndex
+
+ def addItems(self, itemList):
+ newCount = len(itemList)
+ addIndex = len(self._variantData)
+ self._variantData.extend(itemList)
+ self.itemsAdded.emit(addIndex, newCount)
+ return addIndex
+
+ def itemList(self):
+ return self._variantData
diff --git a/examples/graphs/3d/widgetgallery/widgetgallery.pyproject b/examples/graphs/3d/widgetgallery/widgetgallery.pyproject
new file mode 100644
index 000000000..581b21483
--- /dev/null
+++ b/examples/graphs/3d/widgetgallery/widgetgallery.pyproject
@@ -0,0 +1,29 @@
+{
+ "files": ["main.py",
+ "axesinputhandler.py",
+ "bargraph.py",
+ "custominputhandler.py",
+ "graphmodifier.py",
+ "highlightseries.py",
+ "rainfalldata.py",
+ "scatterdatamodifier.py",
+ "scattergraph.py",
+ "surfacegraph.py",
+ "surfacegraphmodifier.py",
+ "topographicseries.py",
+ "variantbardatamapping.py",
+ "variantbardataproxy.py",
+ "variantdataset.py",
+ "data/layer_1.png",
+ "data/layer_2.png",
+ "data/layer_3.png",
+ "data/license.txt",
+ "data/maptexture.jpg",
+ "data/narrowarrow.mesh",
+ "data/oilrig.mesh",
+ "data/pipe.mesh",
+ "data/raindata.txt",
+ "data/refinery.mesh",
+ "data/topography.png"
+]
+}