Widget Gallery#
Widget Gallery demonstrates all three graph types and some of their special features. The graphs have their own tabs in the application.
# 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())
# 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
ac = self.scene().activeCamera()
xRotation = ac.xRotation()
yRotation = ac.yRotation()
# 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)
# 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.MeshBar)
barStyleList.addItem("Pyramid", QAbstract3DSeries.MeshPyramid)
barStyleList.addItem("Cone", QAbstract3DSeries.MeshCone)
barStyleList.addItem("Cylinder", QAbstract3DSeries.MeshCylinder)
barStyleList.addItem("Bevel bar", QAbstract3DSeries.MeshBevelBar)
barStyleList.addItem("Sphere", QAbstract3DSeries.MeshSphere)
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
# 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().activeCamera().xRotation()
# 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
# 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, Q3DCamera, 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.MeshBevelBar
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.ShadowQualitySoftMedium)
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.MeshBevelBar)
self._primarySeries.setMeshSmooth(False)
format = "Helsinki - @colLabel @rowLabel: @valueLabel"
self._secondarySeries.setItemLabelFormat(format)
self._secondarySeries.setMesh(QAbstract3DSeries.MeshBevelBar)
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
camera = self._graph.scene().activeCamera()
self._defaultAngleX = camera.xRotation()
self._defaultAngleY = camera.yRotation()
self._defaultZoom = camera.zoomLevel()
self._defaultTarget = camera.target()
self._animationCameraX.setTargetObject(camera)
self._animationCameraY.setTargetObject(camera)
self._animationCameraZoom.setTargetObject(camera)
self._animationCameraTarget.setTargetObject(camera)
self._animationCameraX.setPropertyName(b"xRotation")
self._animationCameraY.setPropertyName(b"yRotation")
self._animationCameraZoom.setPropertyName(b"zoomLevel")
self._animationCameraTarget.setPropertyName(b"target")
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.scene().activeCamera().setTarget(QVector3D(0.0, 0.0, 0.0))
self._preset = Q3DCamera.CameraPresetFront.value
camera = self._graph.scene().activeCamera()
camera.setCameraPreset(Q3DCamera.CameraPreset(self._preset))
self._preset += 1
if self._preset > Q3DCamera.CameraPresetDirectlyBelow.value:
self._preset = Q3DCamera.CameraPresetFrontLow.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()
camera = self._graph.scene().activeCamera()
currentX = camera.xRotation()
currentY = camera.yRotation()
currentZoom = camera.zoomLevel()
currentTarget = camera.target()
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)
# 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.ColorStyleRangeGradient)
# 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)
# 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, Q3DCamera, 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.MeshSphere
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.ThemeStoneMoss)
self._graph.setShadowQuality(QAbstract3DGraph.ShadowQualitySoftHigh)
self._graph.scene().activeCamera().setCameraPreset(Q3DCamera.CameraPresetFront)
self._graph.scene().activeCamera().setZoomLevel(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)
# 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):
preset = Q3DCamera.CameraPresetFrontLow.value
camera = self._graph.scene().activeCamera()
camera.setCameraPreset(Q3DCamera.CameraPreset(preset))
preset += 1
if preset > Q3DCamera.CameraPresetDirectlyBelow.value:
preset = Q3DCamera.CameraPresetFrontLow.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
# 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.MeshSphere)
itemStyleList.addItem("Cube", QAbstract3DSeries.MeshCube)
itemStyleList.addItem("Minimal", QAbstract3DSeries.MeshMinimal)
itemStyleList.addItem("Point", QAbstract3DSeries.MeshPoint)
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
# 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
# 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, Q3DCamera, 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()
ac = self._graph.scene().activeCamera()
ac.setZoomLevel(85.0)
ac.setCameraPreset(Q3DCamera.CameraPresetIsometricRight)
self._graph.activeTheme().setType(Q3DTheme.ThemeRetro)
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.ColorStyleRangeGradient)
grTwo = QLinearGradient()
grTwo.setColorAt(0.39, Qt.blue)
grTwo.setColorAt(0.4, Qt.white)
self._heightMapSeriesTwo.setBaseGradient(grTwo)
self._heightMapSeriesTwo.setColorStyle(Q3DTheme.ColorStyleRangeGradient)
grThree = QLinearGradient()
grThree.setColorAt(0.0, Qt.white)
grThree.setColorAt(0.05, Qt.black)
self._heightMapSeriesThree.setBaseGradient(grThree)
self._heightMapSeriesThree.setColorStyle(Q3DTheme.ColorStyleRangeGradient)
# 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.ColorStyleRangeGradient)
def setGreenToRedGradient(self):
gr = QLinearGradient()
gr.setColorAt(0.0, Qt.darkGreen)
gr.setColorAt(0.5, Qt.yellow)
gr.setColorAt(0.8, Qt.red)
gr.setColorAt(1.0, Qt.darkRed)
self._sqrtSinSeries.setBaseGradient(gr)
self._sqrtSinSeries.setColorStyle(Q3DTheme.ColorStyleRangeGradient)
@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
# 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)
# 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()
# 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)
# 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
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.
# 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