Contents Menu Expand Light mode Dark mode Auto light/dark, in light mode Auto light/dark, in dark mode Skip to content
Qt for Python
Logo
Qt for Python
  • Getting Started
  • Commercial Use
  • Building from Source
  • Package Details
  • Modules API
  • Tools
  • Tutorials
  • Examples
    • Extending QML - Creating a New Type
    • Extending QML - Connecting to C++ Methods and Signals
    • Extending QML - Adding Property Bindings
    • Extending QML - Using Custom Property Types
    • Extending QML - Using List Property Types
    • Extending QML - Plugins Example
    • Extending QML (advanced) - BirthdayParty Base Project
    • Extending QML (advanced) - Inheritance and Coercion
    • Extending QML (advanced) - Default Properties
    • Extending QML (advanced) - Grouped Properties
    • Extending QML (advanced) - Attached Properties
    • Extending QML (advanced) - Property Value Source
    • Extending QML - Adding Types Example
    • Extending QML - Binding Example
    • QAbstractListModel in QML
    • Extending QML - Extension Objects Example
    • Extending QML - Methods Example
    • Extending QML - Object and List Property Types Example
    • Calling Python Methods from QML
    • Receiving return values from Python in QML
    • Handling QML Signals in Python
    • Directly Connecting QML Component Signals to Python Functions
    • Text Properties Example
    • Using Model Example
    • Object List Model Example
    • OpenGL under QML Squircle
    • Scene Graph Painted Item Example
    • QQuickRenderControl OpenGL Example
    • Scene Graph - Custom Geometry
    • String List Model Example
    • Qt Quick Examples - Window and Screen
    • Qt Quick Controls 2 - Gallery
    • Qt Quick Controls - Contact List
    • Qt Quick Controls - Filesystem Explorer
    • Widgets Gallery Example
    • Address Book Example
    • Anchor Layout Example
    • Animated Tiles Example
    • Application Chooser Example
    • Application Example
    • Basic Drawing Example
    • Basic Sort/Filter Model Example
    • Basic Layouts Example
    • Blur Picker Effect Example
    • Border Layout Example
    • Cannon Example
    • Character Map Example
    • Classwizard Example
    • Colliding Mice Example
    • Concentric Circles Examples
    • Diagram Scene Example
    • Digital Clock Example
    • Dir View Example
    • Dock Widget Example
    • Drag and Drop Robot Example
    • Draggable Text Example
    • Drop Site Example
    • Dynamic Layouts Example
    • Easing Example
    • Editable Tree Model Example
    • Elastic Nodes Example
    • Extension Example
    • Fetch More Example
    • Flow Layout Example
    • GNU gettext Example
    • Image Viewer Example
    • JSON Model Example
    • License Wizard Example
    • Lighting Example
    • Qt Linguist Example
    • MDI Example
    • Model View Tutorial Examples
    • Order Form Example
    • Painter Example
    • Plot Example
    • QRegularExpression Example
    • Screenshot Example
    • Simple RHI Widget Example
    • SpinBox Delegate Example
    • Standard Dialogs Example
    • Star Delegate Example
    • States Example
    • Syntax Highlighter Example
    • System Tray Icon Example
    • Tab Dialog Example
    • Tetrix
    • TextEdit Example
    • TextObject Example
    • Thread Signals Examples
    • Trivial Wizard Example
    • Task Menu Extension Example
    • UILoader Example
    • MIME Type Browser Example
    • Settings Editor Example
    • IPC: Shared Memory
    • Mandelbrot Threads Example
    • Async “Eratosthenes” Example
    • Async “Minimal” Example
    • Blocking Fortune Client Example
    • Downloader Example
    • Fortune Client Example
    • Fortune Server Example
    • Google Suggest Example
    • Loopback Example
    • Threaded Fortune Server Example
    • SQL Books Example
    • D-Bus List Names Example
    • D-Bus Ping Pong Example
    • DOM Bookmarks Example
    • Analog Clock Window Example
    • RHI Window Example
    • Context Info Example
    • Hello GL2 Example
    • Texture Example
    • Threaded QOpenGLWidget Example
    • Sample Bindings Example
    • Using CMake
    • Scriptable Application Example
    • WigglyWidget Example
    • Media Player Example
    • RESTful API client
    • Document Viewer Example
    • OSM Buildings
    • Simple HTTP Server Example
    • Widget Graph Gallery
    • Simple Bar Graph
    • HelloGraphs Example
    • Minimal Surface Example
    • Graph Gallery
    • Surface Graph Gallery
    • Bars 3D Example
    • Surface Example
    • Surface Example
    • Surface Example
    • Area Chart Example
    • Audio Example
    • Bar Chart Example
    • Callout Example
    • Chart Themes Example
    • Donut Chart Breakdown Example
    • Dynamic Spline Example
    • Legend Example
    • Line and Bar Chart Example
    • Line Chart Example
    • Logarithmic Axis Example
    • Memory Usag Example
    • Model Data Example
    • Nested Donuts Example
    • Percent Bar Chart Example
    • Pie Chart Example
    • Selected Point Configuration Example
    • Light Markers and Points Selection Example
    • QML Polar Chart Example
    • Temperature Records Example
    • Zoom Line Chart Example
    • Audio Output Example
    • Audio Source Example
    • Camera Example
    • Player Example
    • Screen Capture Example
    • Nano Browser Example
    • WebEngine Markdown Editor Example
    • WebEngine Notifications Example
    • Simple Browser
    • Qt Widgets Nano Browser Example
    • Ax Viewer Example
    • Bluetooth Scanner Example
    • Bluetooth Low Energy Heart Rate Game
    • Bluetooth Low Energy Heart Rate Server
    • Bluetooth Low Energy Scanner Example
    • Networkx viewer Example
    • OpenCV Face Detection Example
    • Pandas Simple Example
    • Scikit Image Example
    • Matplotlib Widget 3D Example
    • Matplotlib Widget Gaussian Example
    • Map Viewer Example
    • Reddit Example
    • PDF Viewer Example
    • PDF Viewer Example
    • Custom Geometry Example
    • Introduction Example Qt Quick 3D
    • Procedural Texture Example
    • Model-View Server Example
    • Spatial Audio Panning Example
    • Hello Speak
    • Simple Qt 3D Example
    • CAN Bus example
    • Modbus Client example
    • Terminal Example
    • Move Blocks Example
    • StateMachine Ping Pong Example
    • StateMachine Rogue Example
    • Traffic Light Example
    • WebChannel Standalone Example
    • Finance Manager Example - Part 1
    • Finance Manager Example - Part 2
    • Finance Manager Example - Part 3
    • Minibrowser Example
  • Videos
  • Deployment
  • Considerations
  • Developer Notes
  • Release Notes
  • Module Index
Back to top

Graph Gallery¶

Graph Gallery demonstrates all three graph types and some of their special features. The graphs have their own tabs in the application.

Graph Gallery Screenshot

Download this example

# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from enum import Enum
from math import sin, cos, degrees

from PySide6.QtCore import Qt
from PySide6.QtDataVisualization 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 __future__ import annotations

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.QtDataVisualization import (QAbstract3DGraph, QAbstract3DSeries, Q3DBars)


class BarGraph(QObject):

    def __init__(self):
        super().__init__()
        self._barsGraph = Q3DBars()
        self._container = None
        self._barsWidget = None

    def barsWidget(self):
        return self._barsWidget

    def initialize(self, minimum_graph_size, maximum_graph_size):
        if not self._barsGraph.hasContext():
            return False

        self._barsWidget = QWidget()
        hLayout = QHBoxLayout(self._barsWidget)
        self._container = QWidget.createWindowContainer(self._barsGraph,
                                                        self._barsWidget)
        self._container.setMinimumSize(minimum_graph_size)
        self._container.setMaximumSize(maximum_graph_size)
        self._container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self._container.setFocusPolicy(Qt.StrongFocus)
        hLayout.addWidget(self._container, 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.Orientation.Horizontal, self._barsWidget)
        rotationSliderX.setTickInterval(30)
        rotationSliderX.setTickPosition(QSlider.TicksBelow)
        rotationSliderX.setMinimum(-180)
        rotationSliderX.setValue(0)
        rotationSliderX.setMaximum(180)
        rotationSliderY = QSlider(Qt.Orientation.Horizontal, self._barsWidget)
        rotationSliderY.setTickInterval(15)
        rotationSliderY.setTickPosition(QSlider.TicksAbove)
        rotationSliderY.setMinimum(-90)
        rotationSliderY.setValue(0)
        rotationSliderY.setMaximum(90)

        fontSizeSlider = QSlider(Qt.Orientation.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.Orientation.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)
        return True
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from enum import Enum
from math import sin, cos, degrees

from PySide6.QtCore import Qt
from PySide6.QtDataVisualization 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
<RCC>
    <qresource prefix="/">
        <file>data/raindata.txt</file>
        <file>data/layer_1.png</file>
        <file>data/layer_2.png</file>
        <file>data/layer_3.png</file>
        <file>data/refinery.obj</file>
        <file>data/oilrig.obj</file>
        <file>data/pipe.obj</file>
        <file>data/maptexture.jpg</file>
        <file>data/topography.png</file>
    </qresource>
</RCC>
# 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, Signal, Slot
from PySide6.QtGui import QFont, QVector3D
from PySide6.QtDataVisualization 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 __future__ import annotations

from PySide6.QtCore import QPoint, Qt, Slot
from PySide6.QtGui import QLinearGradient, QVector3D
from PySide6.QtDataVisualization 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
from __future__ import annotations

"""PySide6 port of the Qt DataVisualization graphgallery example from Qt v6.x"""

import os
import sys

from PySide6.QtCore import QSize
from PySide6.QtWidgets import QApplication, QMessageBox, QTabWidget

from bargraph import BarGraph
from scattergraph import ScatterGraph
from surfacegraph import SurfaceGraph


if __name__ == "__main__":
    os.environ["QSG_RHI_BACKEND"] = "opengl"

    app = QApplication(sys.argv)

    # Create a tab widget for creating own tabs for Q3DBars, Q3DScatter, and Q3DSurface
    tabWidget = QTabWidget()
    tabWidget.setWindowTitle("Graph Gallery")

    screen_size = tabWidget.screen().size()
    minimum_graph_size = QSize(screen_size.width() / 2, screen_size.height() / 1.75)

    # Create bar graph
    bars = BarGraph()
    # Create scatter graph
    scatter = ScatterGraph()
    # Create surface graph
    surface = SurfaceGraph()

    if (not bars.initialize(minimum_graph_size, screen_size)
            or not scatter.initialize(minimum_graph_size, screen_size)
            or not surface.initialize(minimum_graph_size, screen_size)):
        QMessageBox.warning(None, "Graph Gallery", "Couldn't initialize the OpenGL context.")
        sys.exit(-1)

    # 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 __future__ import annotations

import sys

from pathlib import Path

from PySide6.QtCore import QFile, QIODevice, QObject
from PySide6.QtDataVisualization 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 __future__ import annotations

from math import cos, degrees, sqrt

from PySide6.QtCore import QObject, Signal, Slot, Qt
from PySide6.QtGui import QVector3D
from PySide6.QtDataVisualization 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 __future__ import annotations

from PySide6.QtCore import QObject, QSize, Qt
from PySide6.QtWidgets import (QCheckBox, QComboBox, QCommandLinkButton,
                               QLabel, QHBoxLayout, QSizePolicy,
                               QVBoxLayout, QWidget, )
from PySide6.QtDataVisualization import (QAbstract3DSeries, Q3DScatter)

from scatterdatamodifier import ScatterDataModifier


class ScatterGraph(QObject):

    def __init__(self):
        super().__init__()
        self._scatterGraph = Q3DScatter()
        self._container = None
        self._scatterWidget = None

    def initialize(self, minimum_graph_size, maximum_graph_size):
        if not self._scatterGraph.hasContext():
            return -1

        self._scatterWidget = QWidget()
        hLayout = QHBoxLayout(self._scatterWidget)
        self._container = QWidget.createWindowContainer(self._scatterGraph, self._scatterWidget)
        self._container.setMinimumSize(minimum_graph_size)
        self._container.setMaximumSize(maximum_graph_size)
        self._container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self._container.setFocusPolicy(Qt.StrongFocus)
        hLayout.addWidget(self._container, 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)
        return True

    def scatterWidget(self):
        return self._scatterWidget
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

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.QtDataVisualization 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):
        super().__init__()
        self._surfaceGraph = Q3DSurface()
        self._container = None
        self._surfaceWidget = None

    def initialize(self, minimum_graph_size, maximum_graph_size):
        if not self._surfaceGraph.hasContext():
            return False

        self._surfaceWidget = QWidget()
        hLayout = QHBoxLayout(self._surfaceWidget)
        self._container = QWidget.createWindowContainer(self._surfaceGraph,
                                                        self._surfaceWidget)
        self._container.setMinimumSize(minimum_graph_size)
        self._container.setMaximumSize(maximum_graph_size)
        self._container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self._container.setFocusPolicy(Qt.StrongFocus)
        hLayout.addWidget(self._container, 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.Orientation.Horizontal)
        axisMinSliderX.setMinimum(0)
        axisMinSliderX.setTickInterval(1)
        axisMinSliderX.setEnabled(True)
        axisMaxSliderX = QSlider(Qt.Orientation.Horizontal)
        axisMaxSliderX.setMinimum(1)
        axisMaxSliderX.setTickInterval(1)
        axisMaxSliderX.setEnabled(True)
        axisMinSliderZ = QSlider(Qt.Orientation.Horizontal)
        axisMinSliderZ.setMinimum(0)
        axisMinSliderZ.setTickInterval(1)
        axisMinSliderZ.setEnabled(True)
        axisMaxSliderZ = QSlider(Qt.Orientation.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)
        return True

    def surfaceWidget(self):
        return self._surfaceWidget
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

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.QtDataVisualization 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.obj")
            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.obj")
            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.obj")
            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.obj")
            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.obj")
            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 __future__ import annotations

from PySide6.QtCore import Qt
from PySide6.QtGui import QImage, QVector3D
from PySide6.QtDataVisualization 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 __future__ import annotations

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 __future__ import annotations

from PySide6.QtCore import Slot
from PySide6.QtDataVisualization 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 __future__ import annotations

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
Next
Surface Graph Gallery
Previous
Minimal Surface Example
Copyright © 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 (https://www.gnu.org/licenses/fdl.html) as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.
Made with Sphinx and @pradyunsg's Furo