diff options
Diffstat (limited to 'examples/graphs')
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 Binary files differnew file mode 100644 index 000000000..3e7666411 --- /dev/null +++ b/examples/graphs/2d/hellographs/doc/hellographs.webp 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 Binary files differnew file mode 100644 index 000000000..9138c710a --- /dev/null +++ b/examples/graphs/3d/widgetgallery/data/layer_1.png diff --git a/examples/graphs/3d/widgetgallery/data/layer_2.png b/examples/graphs/3d/widgetgallery/data/layer_2.png Binary files differnew file mode 100644 index 000000000..61631ae8b --- /dev/null +++ b/examples/graphs/3d/widgetgallery/data/layer_2.png diff --git a/examples/graphs/3d/widgetgallery/data/layer_3.png b/examples/graphs/3d/widgetgallery/data/layer_3.png Binary files differnew file mode 100644 index 000000000..066ffbe75 --- /dev/null +++ b/examples/graphs/3d/widgetgallery/data/layer_3.png 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 Binary files differnew file mode 100644 index 000000000..ae5d66ebe --- /dev/null +++ b/examples/graphs/3d/widgetgallery/data/maptexture.jpg diff --git a/examples/graphs/3d/widgetgallery/data/narrowarrow.mesh b/examples/graphs/3d/widgetgallery/data/narrowarrow.mesh Binary files differnew file mode 100644 index 000000000..288867b1e --- /dev/null +++ b/examples/graphs/3d/widgetgallery/data/narrowarrow.mesh diff --git a/examples/graphs/3d/widgetgallery/data/oilrig.mesh b/examples/graphs/3d/widgetgallery/data/oilrig.mesh Binary files differnew file mode 100644 index 000000000..4a7baeddf --- /dev/null +++ b/examples/graphs/3d/widgetgallery/data/oilrig.mesh diff --git a/examples/graphs/3d/widgetgallery/data/pipe.mesh b/examples/graphs/3d/widgetgallery/data/pipe.mesh Binary files differnew file mode 100644 index 000000000..984b6d443 --- /dev/null +++ b/examples/graphs/3d/widgetgallery/data/pipe.mesh 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 Binary files differnew file mode 100644 index 000000000..a7e249353 --- /dev/null +++ b/examples/graphs/3d/widgetgallery/data/refinery.mesh diff --git a/examples/graphs/3d/widgetgallery/data/topography.png b/examples/graphs/3d/widgetgallery/data/topography.png Binary files differnew file mode 100644 index 000000000..9349cdb31 --- /dev/null +++ b/examples/graphs/3d/widgetgallery/data/topography.png 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 Binary files differnew file mode 100644 index 000000000..eb5767264 --- /dev/null +++ b/examples/graphs/3d/widgetgallery/doc/widgetgallery.webp 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" +] +} |