# Copyright (C) 2023 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause from __future__ import annotations from math import atan, degrees import numpy as np from PySide6.QtCore import QObject, QPropertyAnimation, Qt, Signal, Slot from PySide6.QtGui import QFont, QVector3D from PySide6.QtGraphs import (QAbstract3DSeries, QBarDataItem, QBar3DSeries, QCategory3DAxis, QValue3DAxis, QtGraphs3D, QGraphsTheme) 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) backgroundVisibleChanged = Signal(bool) gridVisibleChanged = 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(QtGraphs3D.ShadowQuality.SoftMedium) theme = self._graph.activeTheme() theme.setPlotAreaBackgroundVisible(False) theme.setLabelFont(QFont("Times New Roman", self._fontSize)) theme.setLabelBackgroundVisible(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.setLabelAutoAngle(30.0) self._temperatureAxis.setTitleVisible(True) self._yearAxis.setTitle("Year") self._yearAxis.setLabelAutoAngle(30.0) self._yearAxis.setTitleVisible(True) self._monthAxis.setTitle("Month") self._monthAxis.setLabelAutoAngle(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 = QtGraphs3D.CameraPreset.Front.value self._graph.setCameraPreset(QtGraphs3D.CameraPreset(self._preset)) self._preset += 1 if self._preset > QtGraphs3D.CameraPreset.DirectlyBelow.value: self._preset = QtGraphs3D.CameraPreset.FrontLow.value @Slot(int) def changeTheme(self, theme): currentTheme = self._graph.activeTheme() currentTheme.setTheme(QGraphsTheme.Theme(theme)) self.backgroundVisibleChanged.emit(currentTheme.isBackgroundVisible()) self.gridVisibleChanged.emit(currentTheme.isGridVisible()) self.fontChanged.emit(currentTheme.labelFont()) self.fontSizeChanged.emit(currentTheme.labelFont().pointSize()) def changeLabelBackground(self): theme = self._graph.activeTheme() theme.setLabelBackgroundVisible(not theme.isLabelBackgroundVisible()) @Slot(int) def changeSelectionMode(self, selectionMode): comboBox = self.sender() if comboBox: flags = comboBox.itemData(selectionMode) self._graph.setSelectionMode(QtGraphs3D.SelectionFlags(flags)) def changeFont(self, font): newFont = font self._graph.activeTheme().setLabelFont(newFont) def changeFontSize(self, fontsize): self._fontSize = fontsize font = self._graph.activeTheme().labelFont() font.setPointSize(self._fontSize) self._graph.activeTheme().setLabelFont(font) @Slot(QtGraphs3D.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.setLabelAutoAngle(float(rotation)) self._monthAxis.setLabelAutoAngle(float(rotation)) self._yearAxis.setLabelAutoAngle(float(rotation)) @Slot(bool) def setAxisTitleVisibility(self, state): enabled = state == Qt.CheckState.Checked self._temperatureAxis.setTitleVisible(enabled) self._monthAxis.setTitleVisible(enabled) self._yearAxis.setTitleVisible(enabled) @Slot(bool) def setAxisTitleFixed(self, state): enabled = state == Qt.CheckState.Checked 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 = QtGraphs3D.ShadowQuality(quality) self._graph.setShadowQuality(sq) self.shadowQualityChanged.emit(quality) def rotateX(self, rotation): self._xRotation = rotation self._graph.setCameraPosition(self._xRotation, self._yRotation) def rotateY(self, rotation): self._yRotation = rotation self._graph.setCameraPosition(self._xRotation, self._yRotation) def setPlotAreaBackgroundVisible(self, state): enabled = state == Qt.CheckState.Checked self._graph.activeTheme().setPlotAreaBackgroundVisible(enabled) def setGridVisible(self, state): self._graph.activeTheme().setGridVisible(state == Qt.CheckState.Checked) def setSmoothBars(self, state): self._smooth = state == Qt.CheckState.Checked self._primarySeries.setMeshSmooth(self._smooth) self._secondarySeries.setMeshSmooth(self._smooth) self._customData.customSeries().setMeshSmooth(self._smooth) def setSeriesVisibility(self, state): self._secondarySeries.setVisible(state == Qt.CheckState.Checked) def setReverseValueAxis(self, state): self._graph.valueAxis().setReversed(state == Qt.CheckState.Checked) 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)