Contents - Qt 框架 Menu - Qt 框架 Expand - Qt 框架 Light mode - Qt 框架 Dark mode - Qt 框架 Auto light/dark mode - Qt 框架
Python版Qt
Logo
Python版Qt
  • 快速入门
  • 商业用途
  • 开始使用
  • 模块API
  • 工具
  • 教程
  • 示例
    • 扩展QML - 添加类型示例
    • 扩展QML(高级)- BirthdayParty基本项目
    • 扩展QML(高级)- 继承和转换
    • 扩展QML(高级)- 默认属性
    • 扩展QML(高级)- 分组属性
    • 扩展QML(高级)- 附加属性
    • 扩展QML(高级)- 属性值源
    • 扩展QML - 绑定示例
    • 扩展QML - 创建新类型
    • 扩展QML - 连接到C++方法和信号
    • 扩展QML - 添加属性绑定
    • 扩展QML - 使用自定义属性类型
    • 扩展QML - 使用列表属性类型
    • 扩展QML - 插件示例
    • QAbstractListModel在QML中的使用
    • 扩展QML - 扩展对象示例
    • 扩展QML - 方法示例
    • 扩展QML - 对象和列表属性类型示例
    • examples/qml/signals/pytoqml1
    • examples/qml/signals/pytoqml2
    • examples/qml/signals/qmltopy1
    • examples/qml/signals/qmltopy2
    • examples/qml/signals/qmltopy3
    • examples/qml/signals/qmltopy4
    • 文本属性示例
    • 使用模型示例
    • 对象列表模型示例
    • 在QML Squircle下使用OpenGL
    • 场景图绘制项示例
    • QQuickRenderControl OpenGL示例
    • 场景图 - 自定义几何形状
    • 字符串列表模型示例
    • Qt Quick示例 - 窗口和屏幕
    • Qt Quick Controls 2 - 画廊
    • Qt Quick Controls - 联系人列表
    • Qt Quick Controls - 文件系统资源管理器
    • 小部件画廊示例
    • 地址簿示例
    • 地址簿示例
    • examples/widgets/graphicsview/anchorlayout
    • 动画方块示例
    • 应用程序选择器示例
    • examples/widgets/mainwindows/application
    • 基本绘图示例
    • examples/widgets/itemviews/basicfiltermodel
    • 基本布局示例
    • 模糊选择器效果示例
    • 边框布局示例
    • 大炮示例
    • 字符映射表示例
    • Classwizard示例
    • examples/widgets/graphicsview/collidingmice
    • 同心圆示例
    • examples/widgets/graphicsview/diagramscene
    • 数字时钟示例
    • 目录视图示例
    • 停靠小部件示例
    • examples/widgets/graphicsview/dragdroprobot
    • examples/widgets/draganddrop/draggabletext
    • 放置点示例
    • 动态布局示例
    • 缓动示例
    • 可编辑树模型示例
    • examples/widgets/graphicsview/elasticnodes
    • examples/widgets/dialogs/extension
    • examples/widgets/itemviews/fetchmore
    • 流水布局示例
    • GNU gettext示例
    • examples/widgets/imageviewer
    • JSON模型示例
    • 许可证向导示例
    • examples/widgets/effects/lighting
    • Qt Linguist示例
    • examples/widgets/mainwindows/mdi
    • 模型视图教程示例
    • examples/widgets/richtext/orderform
    • 绘图示例
    • 绘图示例
    • QRegularExpression示例
    • 屏幕截图示例
    • 简单的RHI小部件示例
    • SpinBox代理示例
    • examples/widgets/dialogs/standarddialogs
    • Star代理示例
    • 状态示例
    • 语法高亮示例
    • examples/widgets/desktop/systray
    • 选项卡对话框示例
    • 四子棋
    • 文本编辑器示例
    • 文本对象示例
    • examples/widgets/thread_signals
    • examples/widgets/dialogs/trivialwizard
    • 任务菜单扩展示例
    • examples/uitools/uiloader
    • MIME类型浏览器示例
    • 设置编辑器示例
    • examples/corelib/ipc/sharedmemory
    • Mandelbrot线程示例
    • 异步“Aristotle”示例
    • 异步“Minimal”示例
    • examples/network/blockingfortuneclient
    • 下载器示例
    • examples/network/fortuneclient
    • examples/network/fortuneserver
    • Google建议示例
    • 回环示例
    • examples/network/threadedfortuneserver
    • SQL图书示例
    • examples/dbus/listnames
    • examples/dbus/pingpong
    • DOM书签示例
    • 模拟时钟窗口示例
    • RHI窗口示例
    • 上下文信息示例
    • Hello GL2示例
    • 纹理示例
    • 线程化QOpenGLWidget示例
    • 样本绑定示例
    • 使用CMake
    • 可脚本应用程序示例
    • WigglyWidget示例
    • 媒体播放器示例
    • RESTful API客户端
    • 文档查看器示例
    • Widget画廊
    • HelloGraphs示例
    • 图形画廊
    • 表面图形画廊
    • 3D柱状图示例
    • 表面示例
    • 表面示例
    • 表面示例
    • 面积图示例
    • 音频示例
    • 柱状图示例
    • 注释示例
    • 图表主题示例
    • 饼图细分示例
    • 动态样条图示例
    • 图例示例
    • 线和柱状图示例
    • 线图示例
    • 对数坐标轴示例
    • 内存使用示例
    • 模型数据示例
    • 嵌套饼图示例
    • 百分比柱状图示例
    • 饼图示例
    • 选定点配置示例
    • 光标记和点选择示例
    • QML极坐标图示例
    • 温度记录示例
    • 音频输出示例
    • 音频源示例
    • 摄像机示例
    • 播放器示例
    • 屏幕捕获示例
    • Nano Browser示例
    • examples/webenginewidgets/markdowneditor
    • WebEngine通知示例
    • 简单浏览器
    • Qt Widgets Nano Browser示例
    • Ax Viewer示例
    • 蓝牙扫描仪示例
    • 蓝牙低能耗心率游戏
    • 蓝牙低能耗心率服务器
    • 蓝牙低能耗扫描仪示例
    • Networkx查看器示例
    • OpenCV人脸检测示例
    • Pandas简单示例
    • Scikit Image示例
    • Matplotlib小部件3D示例
    • Matplotlib小部件高斯示例
    • 地图查看器示例
    • Reddit示例
    • PDF查看器示例
    • PDF查看器示例
    • 自定义几何形状示例
    • Qt Quick 3D介绍示例
    • 过程纹理示例
    • examples/remoteobjects/modelview
    • 空间音频扫描示例
    • Hello Speak
    • 简单的Qt 3D示例
    • CAN总线路示例
    • Modbus客户端示例
    • 终端示例
    • 移动块示例
    • examples/statemachine/ping_pong
    • examples/statemachine/rogue
    • 交通灯示例
    • WebChannel独立示例
    • 简单CoAP客户端示例
    • 简单MQTT客户端示例
    • Qt OPC UA查看器示例
  • 视频
  • 部署
  • 注意事项
  • 开发者注释
  • 模块索引
返回顶部

Widget画廊#

Widget画廊展示了所有三种图表类型及其一些特殊功能。图表在应用程序中有自己的标签。

Widget Screenshot

下载 此示例

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

"""PySide6 port of the Qt Graphs widgetgallery example from Qt v6.x"""

import sys

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

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


if __name__ == "__main__":
    app = QApplication(sys.argv)

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

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

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

    # Add bars widget
    tabWidget.addTab(bars.barsWidget(), "Bar Graph")
    # Add scatter widget
    tabWidget.addTab(scatter.scatterWidget(), "Scatter Graph")
    # Add surface widget
    tabWidget.addTab(surface.surfaceWidget(), "Surface Graph")

    tabWidget.show()
    sys.exit(app.exec())
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

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

from PySide6.QtCore import Qt
from PySide6.QtGraphs import QAbstract3DGraph, Q3DInputHandler


class InputState(Enum):
    StateNormal = 0
    StateDraggingX = 1
    StateDraggingZ = 2
    StateDraggingY = 3


class AxesInputHandler(Q3DInputHandler):

    def __init__(self, graph, parent=None):
        super().__init__(parent)
        self._mousePressed = False
        self._state = InputState.StateNormal
        self._axisX = None
        self._axisZ = None
        self._axisY = None
        self._speedModifier = 15.0

        # Connect to the item selection signal from graph
        graph.selectedElementChanged.connect(self.handleElementSelected)

    def setAxes(self, axisX, axisZ, axisY):
        self._axisX = axisX
        self._axisZ = axisZ
        self._axisY = axisY

    def setDragSpeedModifier(self, modifier):
        self._speedModifier = modifier

    def mousePressEvent(self, event, mousePos):
        super().mousePressEvent(event, mousePos)
        if Qt.LeftButton == event.button():
            self._mousePressed = True

    def mouseMoveEvent(self, event, mousePos):
        # Check if we're trying to drag axis label
        if self._mousePressed and self._state != InputState.StateNormal:
            self.setPreviousInputPos(self.inputPosition())
            self.setInputPosition(mousePos)
            self.handleAxisDragging()
        else:
            super().mouseMoveEvent(event, mousePos)

    def mouseReleaseEvent(self, event, mousePos):
        super().mouseReleaseEvent(event, mousePos)
        self._mousePressed = False
        self._state = InputState.StateNormal

    def handleElementSelected(self, type):
        if type == QAbstract3DGraph.ElementAxisXLabel:
            self._state = InputState.StateDraggingX
        elif type == QAbstract3DGraph.ElementAxisYLabel:
            self._state = InputState.StateDraggingY
        elif type == QAbstract3DGraph.ElementAxisZLabel:
            self._state = InputState.StateDraggingZ
        else:
            self._state = InputState.StateNormal

    def handleAxisDragging(self):
        distance = 0.0
        # Get scene orientation from active camera
        xRotation = self.cameraXRotation()
        yRotation = self.cameraYRotation()

        # Calculate directional drag multipliers based on rotation
        xMulX = cos(degrees(xRotation))
        xMulY = sin(degrees(xRotation))
        zMulX = sin(degrees(xRotation))
        zMulY = cos(degrees(xRotation))

        # Get the drag amount
        move = self.inputPosition() - self.previousInputPos()

        # Flip the effect of y movement if we're viewing from below
        yMove = -move.y() if yRotation < 0 else move.y()

        # Adjust axes
        if self._state == InputState.StateDraggingX:
            distance = (move.x() * xMulX - yMove * xMulY) / self._speedModifier
            self._axisX.setRange(self._axisX.min() - distance,
                                 self._axisX.max() - distance)
        elif self._state == InputState.StateDraggingZ:
            distance = (move.x() * zMulX + yMove * zMulY) / self._speedModifier
            self._axisZ.setRange(self._axisZ.min() + distance,
                                 self._axisZ.max() + distance)
        elif self._state == InputState.StateDraggingY:
            # No need to use adjusted y move here
            distance = move.y() / self._speedModifier
            self._axisY.setRange(self._axisY.min() + distance,
                                 self._axisY.max() + distance)
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

from graphmodifier import GraphModifier

from PySide6.QtCore import QObject, Qt
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (QButtonGroup, QCheckBox, QComboBox, QFontComboBox,
                               QLabel, QPushButton, QHBoxLayout, QSizePolicy,
                               QRadioButton, QSlider, QVBoxLayout, QWidget)
from PySide6.QtQuickWidgets import QQuickWidget
from PySide6.QtGraphs import (QAbstract3DGraph, QAbstract3DSeries, Q3DBars)


class BarGraph(QObject):

    def __init__(self, minimum_graph_size, maximum_graph_size):
        super().__init__()
        self._barsGraph = Q3DBars()
        self._barsWidget = QWidget()
        hLayout = QHBoxLayout(self._barsWidget)
        self._barsGraph.setMinimumSize(minimum_graph_size)
        self._barsGraph.setMaximumSize(maximum_graph_size)
        self._barsGraph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self._barsGraph.setFocusPolicy(Qt.StrongFocus)
        self._barsGraph.setResizeMode(QQuickWidget.SizeRootObjectToView)
        hLayout.addWidget(self._barsGraph, 1)

        vLayout = QVBoxLayout()
        hLayout.addLayout(vLayout)

        themeList = QComboBox(self._barsWidget)
        themeList.addItem("Qt")
        themeList.addItem("Primary Colors")
        themeList.addItem("Digia")
        themeList.addItem("Stone Moss")
        themeList.addItem("Army Blue")
        themeList.addItem("Retro")
        themeList.addItem("Ebony")
        themeList.addItem("Isabelle")
        themeList.setCurrentIndex(0)

        labelButton = QPushButton(self._barsWidget)
        labelButton.setText("Change label style")

        smoothCheckBox = QCheckBox(self._barsWidget)
        smoothCheckBox.setText("Smooth bars")
        smoothCheckBox.setChecked(False)

        barStyleList = QComboBox(self._barsWidget)
        barStyleList.addItem("Bar", QAbstract3DSeries.Mesh.Bar)
        barStyleList.addItem("Pyramid", QAbstract3DSeries.Mesh.Pyramid)
        barStyleList.addItem("Cone", QAbstract3DSeries.Mesh.Cone)
        barStyleList.addItem("Cylinder", QAbstract3DSeries.Mesh.Cylinder)
        barStyleList.addItem("Bevel bar", QAbstract3DSeries.Mesh.BevelBar)
        barStyleList.addItem("Sphere", QAbstract3DSeries.Mesh.Sphere)
        barStyleList.setCurrentIndex(4)

        cameraButton = QPushButton(self._barsWidget)
        cameraButton.setText("Change camera preset")

        zoomToSelectedButton = QPushButton(self._barsWidget)
        zoomToSelectedButton.setText("Zoom to selected bar")

        selectionModeList = QComboBox(self._barsWidget)
        selectionModeList.addItem("None", QAbstract3DGraph.SelectionNone)
        selectionModeList.addItem("Bar", QAbstract3DGraph.SelectionItem)
        selectionModeList.addItem("Row", QAbstract3DGraph.SelectionRow)
        sel = QAbstract3DGraph.SelectionItemAndRow
        selectionModeList.addItem("Bar and Row", sel)
        selectionModeList.addItem("Column", QAbstract3DGraph.SelectionColumn)
        sel = QAbstract3DGraph.SelectionItemAndColumn
        selectionModeList.addItem("Bar and Column", sel)
        sel = QAbstract3DGraph.SelectionRowAndColumn
        selectionModeList.addItem("Row and Column", sel)
        sel = QAbstract3DGraph.SelectionItemRowAndColumn
        selectionModeList.addItem("Bar, Row and Column", sel)
        sel = QAbstract3DGraph.SelectionSlice | QAbstract3DGraph.SelectionRow
        selectionModeList.addItem("Slice into Row", sel)
        sel = QAbstract3DGraph.SelectionSlice | QAbstract3DGraph.SelectionItemAndRow
        selectionModeList.addItem("Slice into Row and Item", sel)
        sel = QAbstract3DGraph.SelectionSlice | QAbstract3DGraph.SelectionColumn
        selectionModeList.addItem("Slice into Column", sel)
        sel = (QAbstract3DGraph.SelectionSlice
               | QAbstract3DGraph.SelectionItemAndColumn)
        selectionModeList.addItem("Slice into Column and Item", sel)
        sel = (QAbstract3DGraph.SelectionItemRowAndColumn
               | QAbstract3DGraph.SelectionMultiSeries)
        selectionModeList.addItem("Multi: Bar, Row, Col", sel)
        sel = (QAbstract3DGraph.SelectionSlice
               | QAbstract3DGraph.SelectionItemAndRow
               | QAbstract3DGraph.SelectionMultiSeries)
        selectionModeList.addItem("Multi, Slice: Row, Item", sel)
        sel = (QAbstract3DGraph.SelectionSlice
               | QAbstract3DGraph.SelectionItemAndColumn
               | QAbstract3DGraph.SelectionMultiSeries)
        selectionModeList.addItem("Multi, Slice: Col, Item", sel)
        selectionModeList.setCurrentIndex(1)

        backgroundCheckBox = QCheckBox(self._barsWidget)
        backgroundCheckBox.setText("Show background")
        backgroundCheckBox.setChecked(False)

        gridCheckBox = QCheckBox(self._barsWidget)
        gridCheckBox.setText("Show grid")
        gridCheckBox.setChecked(True)

        seriesCheckBox = QCheckBox(self._barsWidget)
        seriesCheckBox.setText("Show second series")
        seriesCheckBox.setChecked(False)

        reverseValueAxisCheckBox = QCheckBox(self._barsWidget)
        reverseValueAxisCheckBox.setText("Reverse value axis")
        reverseValueAxisCheckBox.setChecked(False)

        reflectionCheckBox = QCheckBox(self._barsWidget)
        reflectionCheckBox.setText("Show reflections")
        reflectionCheckBox.setChecked(False)

        rotationSliderX = QSlider(Qt.Horizontal, self._barsWidget)
        rotationSliderX.setTickInterval(30)
        rotationSliderX.setTickPosition(QSlider.TicksBelow)
        rotationSliderX.setMinimum(-180)
        rotationSliderX.setValue(0)
        rotationSliderX.setMaximum(180)
        rotationSliderY = QSlider(Qt.Horizontal, self._barsWidget)
        rotationSliderY.setTickInterval(15)
        rotationSliderY.setTickPosition(QSlider.TicksAbove)
        rotationSliderY.setMinimum(-90)
        rotationSliderY.setValue(0)
        rotationSliderY.setMaximum(90)

        fontSizeSlider = QSlider(Qt.Horizontal, self._barsWidget)
        fontSizeSlider.setTickInterval(10)
        fontSizeSlider.setTickPosition(QSlider.TicksBelow)
        fontSizeSlider.setMinimum(1)
        fontSizeSlider.setValue(30)
        fontSizeSlider.setMaximum(100)

        fontList = QFontComboBox(self._barsWidget)
        fontList.setCurrentFont(QFont("Times New Roman"))

        shadowQuality = QComboBox(self._barsWidget)
        shadowQuality.addItem("None")
        shadowQuality.addItem("Low")
        shadowQuality.addItem("Medium")
        shadowQuality.addItem("High")
        shadowQuality.addItem("Low Soft")
        shadowQuality.addItem("Medium Soft")
        shadowQuality.addItem("High Soft")
        shadowQuality.setCurrentIndex(5)

        rangeList = QComboBox(self._barsWidget)
        rangeList.addItem("2015")
        rangeList.addItem("2016")
        rangeList.addItem("2017")
        rangeList.addItem("2018")
        rangeList.addItem("2019")
        rangeList.addItem("2020")
        rangeList.addItem("2021")
        rangeList.addItem("2022")
        rangeList.addItem("All")
        rangeList.setCurrentIndex(8)

        axisTitlesVisibleCB = QCheckBox(self._barsWidget)
        axisTitlesVisibleCB.setText("Axis titles visible")
        axisTitlesVisibleCB.setChecked(True)

        axisTitlesFixedCB = QCheckBox(self._barsWidget)
        axisTitlesFixedCB.setText("Axis titles fixed")
        axisTitlesFixedCB.setChecked(True)

        axisLabelRotationSlider = QSlider(Qt.Horizontal, self._barsWidget)
        axisLabelRotationSlider.setTickInterval(10)
        axisLabelRotationSlider.setTickPosition(QSlider.TicksBelow)
        axisLabelRotationSlider.setMinimum(0)
        axisLabelRotationSlider.setValue(30)
        axisLabelRotationSlider.setMaximum(90)

        modeGroup = QButtonGroup(self._barsWidget)
        modeWeather = QRadioButton("Temperature Data", self._barsWidget)
        modeWeather.setChecked(True)
        modeCustomProxy = QRadioButton("Custom Proxy Data", self._barsWidget)
        modeGroup.addButton(modeWeather)
        modeGroup.addButton(modeCustomProxy)

        vLayout.addWidget(QLabel("Rotate horizontally"))
        vLayout.addWidget(rotationSliderX, 0, Qt.AlignTop)
        vLayout.addWidget(QLabel("Rotate vertically"))
        vLayout.addWidget(rotationSliderY, 0, Qt.AlignTop)
        vLayout.addWidget(labelButton, 0, Qt.AlignTop)
        vLayout.addWidget(cameraButton, 0, Qt.AlignTop)
        vLayout.addWidget(zoomToSelectedButton, 0, Qt.AlignTop)
        vLayout.addWidget(backgroundCheckBox)
        vLayout.addWidget(gridCheckBox)
        vLayout.addWidget(smoothCheckBox)
        vLayout.addWidget(reflectionCheckBox)
        vLayout.addWidget(seriesCheckBox)
        vLayout.addWidget(reverseValueAxisCheckBox)
        vLayout.addWidget(axisTitlesVisibleCB)
        vLayout.addWidget(axisTitlesFixedCB)
        vLayout.addWidget(QLabel("Show year"))
        vLayout.addWidget(rangeList)
        vLayout.addWidget(QLabel("Change bar style"))
        vLayout.addWidget(barStyleList)
        vLayout.addWidget(QLabel("Change selection mode"))
        vLayout.addWidget(selectionModeList)
        vLayout.addWidget(QLabel("Change theme"))
        vLayout.addWidget(themeList)
        vLayout.addWidget(QLabel("Adjust shadow quality"))
        vLayout.addWidget(shadowQuality)
        vLayout.addWidget(QLabel("Change font"))
        vLayout.addWidget(fontList)
        vLayout.addWidget(QLabel("Adjust font size"))
        vLayout.addWidget(fontSizeSlider)
        vLayout.addWidget(QLabel("Axis label rotation"))
        vLayout.addWidget(axisLabelRotationSlider, 0, Qt.AlignTop)
        vLayout.addWidget(modeWeather, 0, Qt.AlignTop)
        vLayout.addWidget(modeCustomProxy, 1, Qt.AlignTop)

        self._modifier = GraphModifier(self._barsGraph, self)

        rotationSliderX.valueChanged.connect(self._modifier.rotateX)
        rotationSliderY.valueChanged.connect(self._modifier.rotateY)

        labelButton.clicked.connect(self._modifier.changeLabelBackground)
        cameraButton.clicked.connect(self._modifier.changePresetCamera)
        zoomToSelectedButton.clicked.connect(self._modifier.zoomToSelectedBar)

        backgroundCheckBox.stateChanged.connect(self._modifier.setBackgroundEnabled)
        gridCheckBox.stateChanged.connect(self._modifier.setGridEnabled)
        smoothCheckBox.stateChanged.connect(self._modifier.setSmoothBars)
        seriesCheckBox.stateChanged.connect(self._modifier.setSeriesVisibility)
        reverseValueAxisCheckBox.stateChanged.connect(self._modifier.setReverseValueAxis)
        reflectionCheckBox.stateChanged.connect(self._modifier.setReflection)

        self._modifier.backgroundEnabledChanged.connect(backgroundCheckBox.setChecked)
        self._modifier.gridEnabledChanged.connect(gridCheckBox.setChecked)

        rangeList.currentIndexChanged.connect(self._modifier.changeRange)

        barStyleList.currentIndexChanged.connect(self._modifier.changeStyle)

        selectionModeList.currentIndexChanged.connect(self._modifier.changeSelectionMode)

        themeList.currentIndexChanged.connect(self._modifier.changeTheme)

        shadowQuality.currentIndexChanged.connect(self._modifier.changeShadowQuality)

        self._modifier.shadowQualityChanged.connect(shadowQuality.setCurrentIndex)
        self._barsGraph.shadowQualityChanged.connect(self._modifier.shadowQualityUpdatedByVisual)

        fontSizeSlider.valueChanged.connect(self._modifier.changeFontSize)
        fontList.currentFontChanged.connect(self._modifier.changeFont)

        self._modifier.fontSizeChanged.connect(fontSizeSlider.setValue)
        self._modifier.fontChanged.connect(fontList.setCurrentFont)

        axisTitlesVisibleCB.stateChanged.connect(self._modifier.setAxisTitleVisibility)
        axisTitlesFixedCB.stateChanged.connect(self._modifier.setAxisTitleFixed)
        axisLabelRotationSlider.valueChanged.connect(self._modifier.changeLabelRotation)

        modeWeather.toggled.connect(self._modifier.setDataModeToWeather)
        modeCustomProxy.toggled.connect(self._modifier.setDataModeToCustom)
        modeWeather.toggled.connect(seriesCheckBox.setEnabled)
        modeWeather.toggled.connect(rangeList.setEnabled)
        modeWeather.toggled.connect(axisTitlesVisibleCB.setEnabled)
        modeWeather.toggled.connect(axisTitlesFixedCB.setEnabled)
        modeWeather.toggled.connect(axisLabelRotationSlider.setEnabled)

    def barsWidget(self):
        return self._barsWidget
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

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

from PySide6.QtCore import Qt
from PySide6.QtGraphs import (QAbstract3DGraph, Q3DInputHandler)


class InputState(Enum):
    StateNormal = 0
    StateDraggingX = 1
    StateDraggingZ = 2
    StateDraggingY = 3


class CustomInputHandler(Q3DInputHandler):

    def __init__(self, graph, parent=None):
        super().__init__(parent)
        self._highlight = None
        self._mousePressed = False
        self._state = InputState.StateNormal
        self._axisX = None
        self._axisY = None
        self._axisZ = None
        self._speedModifier = 20.0
        self._aspectRatio = 0.0
        self._axisXMinValue = 0.0
        self._axisXMaxValue = 0.0
        self._axisXMinRange = 0.0
        self._axisZMinValue = 0.0
        self._axisZMaxValue = 0.0
        self._axisZMinRange = 0.0
        self._areaMinValue = 0.0
        self._areaMaxValue = 0.0

        # Connect to the item selection signal from graph
        graph.selectedElementChanged.connect(self.handleElementSelected)

    def setAspectRatio(self, ratio):
        self._aspectRatio = ratio

    def setHighlightSeries(self, series):
        self._highlight = series

    def setDragSpeedModifier(self, modifier):
        self._speedModifier = modifier

    def setLimits(self, min, max, minRange):
        self._areaMinValue = min
        self._areaMaxValue = max
        self._axisXMinValue = self._areaMinValue
        self._axisXMaxValue = self._areaMaxValue
        self._axisZMinValue = self._areaMinValue
        self._axisZMaxValue = self._areaMaxValue
        self._axisXMinRange = minRange
        self._axisZMinRange = minRange

    def setAxes(self, axisX, axisY, axisZ):
        self._axisX = axisX
        self._axisY = axisY
        self._axisZ = axisZ

    def mousePressEvent(self, event, mousePos):
        if Qt.LeftButton == event.button():
            self._highlight.setVisible(False)
            self._mousePressed = True
        super().mousePressEvent(event, mousePos)

    def wheelEvent(self, event):
        delta = float(event.angleDelta().y())

        self._axisXMinValue += delta
        self._axisXMaxValue -= delta
        self._axisZMinValue += delta
        self._axisZMaxValue -= delta
        self.checkConstraints()

        y = (self._axisXMaxValue - self._axisXMinValue) * self._aspectRatio

        self._axisX.setRange(self._axisXMinValue, self._axisXMaxValue)
        self._axisY.setRange(100.0, y)
        self._axisZ.setRange(self._axisZMinValue, self._axisZMaxValue)

    def mouseMoveEvent(self, event, mousePos):
        # Check if we're trying to drag axis label
        if self._mousePressed and self._state != InputState.StateNormal:
            self.setPreviousInputPos(self.inputPosition())
            self.setInputPosition(mousePos)
            self.handleAxisDragging()
        else:
            super().mouseMoveEvent(event, mousePos)

    def mouseReleaseEvent(self, event, mousePos):
        super().mouseReleaseEvent(event, mousePos)
        self._mousePressed = False
        self._state = InputState.StateNormal

    def handleElementSelected(self, type):
        if type == QAbstract3DGraph.ElementAxisXLabel:
            self._state = InputState.StateDraggingX
        elif type == QAbstract3DGraph.ElementAxisZLabel:
            self._state = InputState.StateDraggingZ
        else:
            self._state = InputState.StateNormal

    def handleAxisDragging(self):
        distance = 0.0

        # Get scene orientation from active camera
        xRotation = self.scene().cameraXRotation()

        # Calculate directional drag multipliers based on rotation
        xMulX = cos(degrees(xRotation))
        xMulY = sin(degrees(xRotation))
        zMulX = xMulY
        zMulY = xMulX

        # Get the drag amount
        move = self.inputPosition() - self.previousInputPos()

        # Adjust axes
        if self._state == InputState.StateDraggingX:
            distance = (move.x() * xMulX - move.y() * xMulY) * self._speedModifier
            self._axisXMinValue -= distance
            self._axisXMaxValue -= distance
            if self._axisXMinValue < self._areaMinValue:
                dist = self._axisXMaxValue - self._axisXMinValue
                self._axisXMinValue = self._areaMinValue
                self._axisXMaxValue = self._axisXMinValue + dist

            if self._axisXMaxValue > self._areaMaxValue:
                dist = self._axisXMaxValue - self._axisXMinValue
                self._axisXMaxValue = self._areaMaxValue
                self._axisXMinValue = self._axisXMaxValue - dist

            self._axisX.setRange(self._axisXMinValue, self._axisXMaxValue)
        elif self._state == InputState.StateDraggingZ:
            distance = (move.x() * zMulX + move.y() * zMulY) * self._speedModifier
            self._axisZMinValue += distance
            self._axisZMaxValue += distance
            if self._axisZMinValue < self._areaMinValue:
                dist = self._axisZMaxValue - self._axisZMinValue
                self._axisZMinValue = self._areaMinValue
                self._axisZMaxValue = self._axisZMinValue + dist

            if self._axisZMaxValue > self._areaMaxValue:
                dist = self._axisZMaxValue - self._axisZMinValue
                self._axisZMaxValue = self._areaMaxValue
                self._axisZMinValue = self._axisZMaxValue - dist

            self._axisZ.setRange(self._axisZMinValue, self._axisZMaxValue)

    def checkConstraints(self):
        if self._axisXMinValue < self._areaMinValue:
            self._axisXMinValue = self._areaMinValue
        if self._axisXMaxValue > self._areaMaxValue:
            self._axisXMaxValue = self._areaMaxValue
        # Don't allow too much zoom in
        range = self._axisXMaxValue - self._axisXMinValue
        if range < self._axisXMinRange:
            adjust = (self._axisXMinRange - range) / 2.0
            self._axisXMinValue -= adjust
            self._axisXMaxValue += adjust

        if self._axisZMinValue < self._areaMinValue:
            self._axisZMinValue = self._areaMinValue
        if self._axisZMaxValue > self._areaMaxValue:
            self._axisZMaxValue = self._areaMaxValue
        # Don't allow too much zoom in
        range = self._axisZMaxValue - self._axisZMinValue
        if range < self._axisZMinRange:
            adjust = (self._axisZMinRange - range) / 2.0
            self._axisZMinValue -= adjust
            self._axisZMaxValue += adjust
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause


from math import atan, degrees
import numpy as np

from PySide6.QtCore import QObject, QPropertyAnimation, Signal, Slot
from PySide6.QtGui import QFont, QVector3D
from PySide6.QtGraphs import (QAbstract3DGraph, QAbstract3DSeries,
                              QBarDataItem, QBar3DSeries, QCategory3DAxis,
                              QValue3DAxis, Q3DTheme)

from rainfalldata import RainfallData

# Set up data
TEMP_OULU = np.array([
    [-7.4, -2.4, 0.0, 3.0, 8.2, 11.6, 14.7, 15.4, 11.4, 4.2, 2.1, -2.3],  # 2015
    [-13.4, -3.9, -1.8, 3.1, 10.6, 13.7, 17.8, 13.6, 10.7, 3.5, -3.1, -4.2],  # 2016
    [-5.7, -6.7, -3.0, -0.1, 4.7, 12.4, 16.1, 14.1, 9.4, 3.0, -0.3, -3.2],  # 2017
    [-6.4, -11.9, -7.4, 1.9, 11.4, 12.4, 21.5, 16.1, 11.0, 4.4, 2.1, -4.1],  # 2018
    [-11.7, -6.1, -2.4, 3.9, 7.2, 14.5, 15.6, 14.4, 8.5, 2.0, -3.0, -1.5],  # 2019
    [-2.1, -3.4, -1.8, 0.6, 7.0, 17.1, 15.6, 15.4, 11.1, 5.6, 1.9, -1.7],  # 2020
    [-9.6, -11.6, -3.2, 2.4, 7.8, 17.3, 19.4, 14.2, 8.0, 5.2, -2.2, -8.6],  # 2021
    [-7.3, -6.4, -1.8, 1.3, 8.1, 15.5, 17.6, 17.6, 9.1, 5.4, -1.5, -4.4]],  # 2022
    np.float64)


TEMP_HELSINKI = np.array([
    [-2.0, -0.1, 1.8, 5.1, 9.7, 13.7, 16.3, 17.3, 12.7, 5.4, 4.6, 2.1],  # 2015
    [-10.3, -0.6, 0.0, 4.9, 14.3, 15.7, 17.7, 16.0, 12.7, 4.6, -1.0, -0.9],  # 2016
    [-2.9, -3.3, 0.7, 2.3, 9.9, 13.8, 16.1, 15.9, 11.4, 5.0, 2.7, 0.7],  # 2017
    [-2.2, -8.4, -4.7, 5.0, 15.3, 15.8, 21.2, 18.2, 13.3, 6.7, 2.8, -2.0],  # 2018
    [-6.2, -0.5, -0.3, 6.8, 10.6, 17.9, 17.5, 16.8, 11.3, 5.2, 1.8, 1.4],  # 2019
    [1.9, 0.5, 1.7, 4.5, 9.5, 18.4, 16.5, 16.8, 13.0, 8.2, 4.4, 0.9],  # 2020
    [-4.7, -8.1, -0.9, 4.5, 10.4, 19.2, 20.9, 15.4, 9.5, 8.0, 1.5, -6.7],  # 2021
    [-3.3, -2.2, -0.2, 3.3, 9.6, 16.9, 18.1, 18.9, 9.2, 7.6, 2.3, -3.4]],  # 2022
    np.float64)


class GraphModifier(QObject):

    shadowQualityChanged = Signal(int)
    backgroundEnabledChanged = Signal(bool)
    gridEnabledChanged = Signal(bool)
    fontChanged = Signal(QFont)
    fontSizeChanged = Signal(int)

    def __init__(self, bargraph, parent):
        super().__init__(parent)
        self._graph = bargraph
        self._temperatureAxis = QValue3DAxis()
        self._yearAxis = QCategory3DAxis()
        self._monthAxis = QCategory3DAxis()
        self._primarySeries = QBar3DSeries()
        self._secondarySeries = QBar3DSeries()
        self._celsiusString = "°C"

        self._xRotation = float(0)
        self._yRotation = float(0)
        self._fontSize = 30
        self._segments = 4
        self._subSegments = 3
        self._minval = float(-20)
        self._maxval = float(20)
        self._barMesh = QAbstract3DSeries.Mesh.BevelBar
        self._smooth = False
        self._animationCameraX = QPropertyAnimation()
        self._animationCameraY = QPropertyAnimation()
        self._animationCameraZoom = QPropertyAnimation()
        self._animationCameraTarget = QPropertyAnimation()
        self._defaultAngleX = float(0)
        self._defaultAngleY = float(0)
        self._defaultZoom = float(0)
        self._defaultTarget = []
        self._customData = None

        self._graph.setShadowQuality(QAbstract3DGraph.ShadowQuality.SoftMedium)
        theme = self._graph.activeTheme()
        theme.setBackgroundEnabled(False)
        theme.setFont(QFont("Times New Roman", self._fontSize))
        theme.setLabelBackgroundEnabled(True)
        self._graph.setMultiSeriesUniform(True)

        self._months = ["January", "February", "March", "April", "May", "June",
                        "July", "August", "September", "October", "November",
                        "December"]
        self._years = ["2015", "2016", "2017", "2018", "2019", "2020",
                       "2021", "2022"]

        self._temperatureAxis.setTitle("Average temperature")
        self._temperatureAxis.setSegmentCount(self._segments)
        self._temperatureAxis.setSubSegmentCount(self._subSegments)
        self._temperatureAxis.setRange(self._minval, self._maxval)
        self._temperatureAxis.setLabelFormat("%.1f " + self._celsiusString)
        self._temperatureAxis.setLabelAutoRotation(30.0)
        self._temperatureAxis.setTitleVisible(True)

        self._yearAxis.setTitle("Year")
        self._yearAxis.setLabelAutoRotation(30.0)
        self._yearAxis.setTitleVisible(True)
        self._monthAxis.setTitle("Month")
        self._monthAxis.setLabelAutoRotation(30.0)
        self._monthAxis.setTitleVisible(True)

        self._graph.setValueAxis(self._temperatureAxis)
        self._graph.setRowAxis(self._yearAxis)
        self._graph.setColumnAxis(self._monthAxis)

        format = "Oulu - @colLabel @rowLabel: @valueLabel"
        self._primarySeries.setItemLabelFormat(format)
        self._primarySeries.setMesh(QAbstract3DSeries.Mesh.BevelBar)
        self._primarySeries.setMeshSmooth(False)

        format = "Helsinki - @colLabel @rowLabel: @valueLabel"
        self._secondarySeries.setItemLabelFormat(format)
        self._secondarySeries.setMesh(QAbstract3DSeries.Mesh.BevelBar)
        self._secondarySeries.setMeshSmooth(False)
        self._secondarySeries.setVisible(False)

        self._graph.addSeries(self._primarySeries)
        self._graph.addSeries(self._secondarySeries)

        self.changePresetCamera()

        self.resetTemperatureData()

        # Set up property animations for zooming to the selected bar
        self._defaultAngleX = self._graph.cameraXRotation()
        self._defaultAngleY = self._graph.cameraYRotation()
        self._defaultZoom = self._graph.cameraZoomLevel()
        self._defaultTarget = self._graph.cameraTargetPosition()

        self._animationCameraX.setTargetObject(self._graph)
        self._animationCameraY.setTargetObject(self._graph)
        self._animationCameraZoom.setTargetObject(self._graph)
        self._animationCameraTarget.setTargetObject(self._graph)

        self._animationCameraX.setPropertyName(b"cameraXRotation")
        self._animationCameraY.setPropertyName(b"cameraYRotation")
        self._animationCameraZoom.setPropertyName(b"cameraZoomLevel")
        self._animationCameraTarget.setPropertyName(b"cameraTargetPosition")

        duration = 1700
        self._animationCameraX.setDuration(duration)
        self._animationCameraY.setDuration(duration)
        self._animationCameraZoom.setDuration(duration)
        self._animationCameraTarget.setDuration(duration)

        # The zoom always first zooms out above the graph and then zooms in
        zoomOutFraction = 0.3
        self._animationCameraX.setKeyValueAt(zoomOutFraction, 0.0)
        self._animationCameraY.setKeyValueAt(zoomOutFraction, 90.0)
        self._animationCameraZoom.setKeyValueAt(zoomOutFraction, 50.0)
        self._animationCameraTarget.setKeyValueAt(zoomOutFraction,
                                                  QVector3D(0, 0, 0))
        self._customData = RainfallData()

    def resetTemperatureData(self):
        # Create data arrays
        dataSet = []
        dataSet2 = []

        for year in range(0, len(self._years)):
            # Create a data row
            dataRow = []
            dataRow2 = []
            for month in range(0, len(self._months)):
                # Add data to the row
                item = QBarDataItem()
                item.setValue(TEMP_OULU[year][month])
                dataRow.append(item)
                item = QBarDataItem()
                item.setValue(TEMP_HELSINKI[year][month])
                dataRow2.append(item)

            # Add the row to the set
            dataSet.append(dataRow)
            dataSet2.append(dataRow2)

        # Add data to the data proxy (the data proxy assumes ownership of it)
        self._primarySeries.dataProxy().resetArray(dataSet, self._years, self._months)
        self._secondarySeries.dataProxy().resetArray(dataSet2, self._years, self._months)

    @Slot(int)
    def changeRange(self, range):
        if range >= len(self._years):
            self._yearAxis.setRange(0, len(self._years) - 1)
        else:
            self._yearAxis.setRange(range, range)

    @Slot(int)
    def changeStyle(self, style):
        comboBox = self.sender()
        if comboBox:
            self._barMesh = comboBox.itemData(style)
            self._primarySeries.setMesh(self._barMesh)
            self._secondarySeries.setMesh(self._barMesh)
            self._customData.customSeries().setMesh(self._barMesh)

    def changePresetCamera(self):
        self._animationCameraX.stop()
        self._animationCameraY.stop()
        self._animationCameraZoom.stop()
        self._animationCameraTarget.stop()

        # Restore camera target in case animation has changed it
        self._graph.setCameraTargetPosition(QVector3D(0.0, 0.0, 0.0))

        self._preset = QAbstract3DGraph.CameraPreset.Front.value

        self._graph.setCameraPreset(QAbstract3DGraph.CameraPreset(self._preset))

        self._preset += 1
        if self._preset > QAbstract3DGraph.CameraPreset.DirectlyBelow.value:
            self._preset = QAbstract3DGraph.CameraPreset.FrontLow.value

    @Slot(int)
    def changeTheme(self, theme):
        currentTheme = self._graph.activeTheme()
        currentTheme.setType(Q3DTheme.Theme(theme))
        self.backgroundEnabledChanged.emit(currentTheme.isBackgroundEnabled())
        self.gridEnabledChanged.emit(currentTheme.isGridEnabled())
        self.fontChanged.emit(currentTheme.font())
        self.fontSizeChanged.emit(currentTheme.font().pointSize())

    def changeLabelBackground(self):
        theme = self._graph.activeTheme()
        theme.setLabelBackgroundEnabled(not theme.isLabelBackgroundEnabled())

    @Slot(int)
    def changeSelectionMode(self, selectionMode):
        comboBox = self.sender()
        if comboBox:
            flags = comboBox.itemData(selectionMode)
            self._graph.setSelectionMode(QAbstract3DGraph.SelectionFlags(flags))

    def changeFont(self, font):
        newFont = font
        self._graph.activeTheme().setFont(newFont)

    def changeFontSize(self, fontsize):
        self._fontSize = fontsize
        font = self._graph.activeTheme().font()
        font.setPointSize(self._fontSize)
        self._graph.activeTheme().setFont(font)

    @Slot(QAbstract3DGraph.ShadowQuality)
    def shadowQualityUpdatedByVisual(self, sq):
        # Updates the UI component to show correct shadow quality
        self.shadowQualityChanged.emit(sq.value)

    @Slot(int)
    def changeLabelRotation(self, rotation):
        self._temperatureAxis.setLabelAutoRotation(float(rotation))
        self._monthAxis.setLabelAutoRotation(float(rotation))
        self._yearAxis.setLabelAutoRotation(float(rotation))

    @Slot(bool)
    def setAxisTitleVisibility(self, enabled):
        self._temperatureAxis.setTitleVisible(enabled)
        self._monthAxis.setTitleVisible(enabled)
        self._yearAxis.setTitleVisible(enabled)

    @Slot(bool)
    def setAxisTitleFixed(self, enabled):
        self._temperatureAxis.setTitleFixed(enabled)
        self._monthAxis.setTitleFixed(enabled)
        self._yearAxis.setTitleFixed(enabled)

    @Slot()
    def zoomToSelectedBar(self):
        self._animationCameraX.stop()
        self._animationCameraY.stop()
        self._animationCameraZoom.stop()
        self._animationCameraTarget.stop()

        currentX = self._graph.cameraXRotation()
        currentY = self._graph.cameraYRotation()
        currentZoom = self._graph.cameraZoomLevel()
        currentTarget = self._graph.cameraTargetPosition()

        self._animationCameraX.setStartValue(currentX)
        self._animationCameraY.setStartValue(currentY)
        self._animationCameraZoom.setStartValue(currentZoom)
        self._animationCameraTarget.setStartValue(currentTarget)

        selectedBar = (self._graph.selectedSeries().selectedBar()
                       if self._graph.selectedSeries()
                       else QBar3DSeries.invalidSelectionPosition())

        if selectedBar != QBar3DSeries.invalidSelectionPosition():
            # Normalize selected bar position within axis range to determine
            # target coordinates
            endTarget = QVector3D()
            xMin = self._graph.columnAxis().min()
            xRange = self._graph.columnAxis().max() - xMin
            zMin = self._graph.rowAxis().min()
            zRange = self._graph.rowAxis().max() - zMin
            endTarget.setX((selectedBar.y() - xMin) / xRange * 2.0 - 1.0)
            endTarget.setZ((selectedBar.x() - zMin) / zRange * 2.0 - 1.0)

            # Rotate the camera so that it always points approximately to the
            # graph center
            endAngleX = 90.0 - degrees(atan(float(endTarget.z() / endTarget.x())))
            if endTarget.x() > 0.0:
                endAngleX -= 180.0
            proxy = self._graph.selectedSeries().dataProxy()
            barValue = proxy.itemAt(selectedBar.x(), selectedBar.y()).value()
            endAngleY = 30.0 if barValue >= 0.0 else -30.0
            if self._graph.valueAxis().reversed():
                endAngleY *= -1.0

            self._animationCameraX.setEndValue(float(endAngleX))
            self._animationCameraY.setEndValue(endAngleY)
            self._animationCameraZoom.setEndValue(250)
            self._animationCameraTarget.setEndValue(endTarget)
        else:
            # No selected bar, so return to the default view
            self._animationCameraX.setEndValue(self._defaultAngleX)
            self._animationCameraY.setEndValue(self._defaultAngleY)
            self._animationCameraZoom.setEndValue(self._defaultZoom)
            self._animationCameraTarget.setEndValue(self._defaultTarget)

        self._animationCameraX.start()
        self._animationCameraY.start()
        self._animationCameraZoom.start()
        self._animationCameraTarget.start()

    @Slot(bool)
    def setDataModeToWeather(self, enabled):
        if enabled:
            self.changeDataMode(False)

    @Slot(bool)
    def setDataModeToCustom(self, enabled):
        if enabled:
            self.changeDataMode(True)

    def changeShadowQuality(self, quality):
        sq = QAbstract3DGraph.ShadowQuality(quality)
        self._graph.setShadowQuality(sq)
        self.shadowQualityChanged.emit(quality)

    def rotateX(self, rotation):
        self._xRotation = rotation
        camera = self._graph.scene().activeCamera()
        camera.setCameraPosition(self._xRotation, self._yRotation)

    def rotateY(self, rotation):
        self._yRotation = rotation
        camera = self._graph.scene().activeCamera()
        camera.setCameraPosition(self._xRotation, self._yRotation)

    def setBackgroundEnabled(self, enabled):
        self._graph.activeTheme().setBackgroundEnabled(bool(enabled))

    def setGridEnabled(self, enabled):
        self._graph.activeTheme().setGridEnabled(bool(enabled))

    def setSmoothBars(self, smooth):
        self._smooth = bool(smooth)
        self._primarySeries.setMeshSmooth(self._smooth)
        self._secondarySeries.setMeshSmooth(self._smooth)
        self._customData.customSeries().setMeshSmooth(self._smooth)

    def setSeriesVisibility(self, enabled):
        self._secondarySeries.setVisible(bool(enabled))

    def setReverseValueAxis(self, enabled):
        self._graph.valueAxis().setReversed(enabled)

    def setReflection(self, enabled):
        self._graph.setReflection(enabled)

    def changeDataMode(self, customData):
        # Change between weather data and data from custom proxy
        if customData:
            self._graph.removeSeries(self._primarySeries)
            self._graph.removeSeries(self._secondarySeries)
            self._graph.addSeries(self._customData.customSeries())
            self._graph.setValueAxis(self._customData.valueAxis())
            self._graph.setRowAxis(self._customData.rowAxis())
            self._graph.setColumnAxis(self._customData.colAxis())
        else:
            self._graph.removeSeries(self._customData.customSeries())
            self._graph.addSeries(self._primarySeries)
            self._graph.addSeries(self._secondarySeries)
            self._graph.setValueAxis(self._temperatureAxis)
            self._graph.setRowAxis(self._yearAxis)
            self._graph.setColumnAxis(self._monthAxis)
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

from PySide6.QtCore import QPoint, Qt, Slot
from PySide6.QtGui import QLinearGradient, QVector3D
from PySide6.QtGraphs import (QSurface3DSeries, QSurfaceDataItem, Q3DTheme)


DARK_RED_POS = 1.0
RED_POS = 0.8
YELLOW_POS = 0.6
GREEN_POS = 0.4
DARK_GREEN_POS = 0.2


class HighlightSeries(QSurface3DSeries):

    def __init__(self):
        super().__init__()
        self._width = 100
        self._height = 100
        self._srcWidth = 0
        self._srcHeight = 0
        self._position = {}
        self._topographicSeries = None
        self._minHeight = 0.0
        self.setDrawMode(QSurface3DSeries.DrawSurface)
        self.setFlatShadingEnabled(True)
        self.setVisible(False)

    def setTopographicSeries(self, series):
        self._topographicSeries = series
        array = self._topographicSeries.dataProxy().array()
        self._srcWidth = len(array[0])
        self._srcHeight = len(array)
        self._topographicSeries.selectedPointChanged.connect(self.handlePositionChange)

    def setMinHeight(self, height):
        self. m_minHeight = height

    @Slot(QPoint)
    def handlePositionChange(self, position):
        self._position = position

        if position == self.invalidSelectionPosition():
            self.setVisible(False)
            return

        halfWidth = self._width / 2
        halfHeight = self._height / 2

        startX = position.y() - halfWidth
        if startX < 0:
            startX = 0
        endX = position.y() + halfWidth
        if endX > (self._srcWidth - 1):
            endX = self._srcWidth - 1
        startZ = position.x() - halfHeight
        if startZ < 0:
            startZ = 0
        endZ = position.x() + halfHeight
        if endZ > (self._srcHeight - 1):
            endZ = self._srcHeight - 1

        srcProxy = self._topographicSeries.dataProxy()
        srcArray = srcProxy.array()

        dataArray = []
        for i in range(int(startZ), int(endZ)):
            newRow = []
            srcRow = srcArray[i]
            for j in range(startX, endX):
                pos = srcRow.at(j).position()
                pos.setY(pos.y() + 0.1)
                item = QSurfaceDataItem(QVector3D(pos))
                newRow.append(item)
            dataArray.append(newRow)
        self.dataProxy().resetArray(dataArray)
        self.setVisible(True)

    @Slot(float)
    def handleGradientChange(self, value):
        ratio = self._minHeight / value

        gr = QLinearGradient()
        gr.setColorAt(0.0, Qt.black)
        gr.setColorAt(DARK_GREEN_POS * ratio, Qt.darkGreen)
        gr.setColorAt(GREEN_POS * ratio, Qt.green)
        gr.setColorAt(YELLOW_POS * ratio, Qt.yellow)
        gr.setColorAt(RED_POS * ratio, Qt.red)
        gr.setColorAt(DARK_RED_POS * ratio, Qt.darkRed)

        self.setBaseGradient(gr)
        self.setColorStyle(Q3DTheme.ColorStyle.RangeGradient)
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import sys

from pathlib import Path

from PySide6.QtCore import QFile, QIODevice, QObject
from PySide6.QtGraphs import (QBar3DSeries, QCategory3DAxis, QValue3DAxis)

from variantbardataproxy import VariantBarDataProxy
from variantbardatamapping import VariantBarDataMapping
from variantdataset import VariantDataSet


MONTHS = ["January", "February", "March", "April",
          "May", "June", "July", "August", "September", "October",
          "November", "December"]


class RainfallData(QObject):

    def __init__(self):
        super().__init__()
        self._columnCount = 0
        self._rowCount = 0
        self._years = []
        self._numericMonths = []
        self._proxy = VariantBarDataProxy()
        self._mapping = None
        self._dataSet = None
        self._series = QBar3DSeries()
        self._valueAxis = QValue3DAxis()
        self._rowAxis = QCategory3DAxis()
        self._colAxis = QCategory3DAxis()

        # In data file the months are in numeric format, so create custom list
        for i in range(1, 13):
            self._numericMonths.append(str(i))

        self._columnCount = len(self._numericMonths)

        self.updateYearsList(2010, 2022)

        # Create proxy and series
        self._proxy = VariantBarDataProxy()
        self._series = QBar3DSeries(self._proxy)

        self._series.setItemLabelFormat("%.1f mm")

        # Create the axes
        self._rowAxis = QCategory3DAxis(self)
        self._colAxis = QCategory3DAxis(self)
        self._valueAxis = QValue3DAxis(self)
        self._rowAxis.setAutoAdjustRange(True)
        self._colAxis.setAutoAdjustRange(True)
        self._valueAxis.setAutoAdjustRange(True)

        # Set axis labels and titles
        self._rowAxis.setTitle("Year")
        self._colAxis.setTitle("Month")
        self._valueAxis.setTitle("rainfall (mm)")
        self._valueAxis.setSegmentCount(5)
        self._rowAxis.setLabels(self._years)
        self._colAxis.setLabels(MONTHS)
        self._rowAxis.setTitleVisible(True)
        self._colAxis.setTitleVisible(True)
        self._valueAxis.setTitleVisible(True)

        self.addDataSet()

    def customSeries(self):
        return self._series

    def valueAxis(self):
        return self._valueAxis

    def rowAxis(self):
        return self._rowAxis

    def colAxis(self):
        return self._colAxis

    def updateYearsList(self, start, end):
        self._years.clear()
        for i in range(start, end + 1):
            self._years.append(str(i))
        self._rowCount = len(self._years)

    def addDataSet(self):
        # Create a new variant data set and data item list
        self._dataSet = VariantDataSet()
        itemList = []

        # Read data from a data file into the data item list
        file_path = Path(__file__).resolve().parent / "data" / "raindata.txt"
        dataFile = QFile(file_path)
        if dataFile.open(QIODevice.ReadOnly | QIODevice.Text):
            data = dataFile.readAll().data().decode("utf8")
            for line in data.split("\n"):
                if line and not line.startswith("#"):  # Ignore comments
                    tokens = line.split(",")
                    # Each line has three data items: Year, month, and
                    # rainfall value
                    if len(tokens) >= 3:
                        # Store year and month as strings, and rainfall value
                        # as double into a variant data item and add the item to
                        # the item list.
                        newItem = []
                        newItem.append(tokens[0].strip())
                        newItem.append(tokens[1].strip())
                        newItem.append(float(tokens[2].strip()))
                        itemList.append(newItem)
        else:
            print("Unable to open data file:", dataFile.fileName(),
                  file=sys.stderr)

        # Add items to the data set and set it to the proxy
        self._dataSet.addItems(itemList)
        self._proxy.setDataSet(self._dataSet)

        # Create new mapping for the data and set it to the proxy
        self._mapping = VariantBarDataMapping(0, 1, 2,
                                              self._years, self._numericMonths)
        self._proxy.setMapping(self._mapping)
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

from math import cos, degrees, sqrt

from PySide6.QtCore import QObject, Signal, Slot, Qt
from PySide6.QtGui import QVector3D
from PySide6.QtGraphs import (QAbstract3DGraph, QAbstract3DSeries,
                              QScatterDataItem, QScatterDataProxy,
                              QScatter3DSeries, Q3DTheme)

from axesinputhandler import AxesInputHandler


NUMBER_OF_ITEMS = 10000
CURVE_DIVIDER = 7.5
LOWER_NUMBER_OF_ITEMS = 900
LOWER_CURVE_DIVIDER = 0.75


class ScatterDataModifier(QObject):

    backgroundEnabledChanged = Signal(bool)
    gridEnabledChanged = Signal(bool)
    shadowQualityChanged = Signal(int)

    def __init__(self, scatter, parent):
        super().__init__(parent)

        self._graph = scatter

        self._style = QAbstract3DSeries.Mesh.Sphere
        self._smooth = True
        self._inputHandler = AxesInputHandler(scatter)
        self._autoAdjust = True
        self._itemCount = LOWER_NUMBER_OF_ITEMS
        self._CURVE_DIVIDER = LOWER_CURVE_DIVIDER
        self._inputHandler = AxesInputHandler(scatter)

        self._graph.activeTheme().setType(Q3DTheme.Theme.StoneMoss)
        self._graph.setShadowQuality(QAbstract3DGraph.ShadowQuality.SoftHigh)
        self._graph.setCameraPreset(QAbstract3DGraph.CameraPreset.Front)
        self._graph.setCameraZoomLevel(80.0)

        self._proxy = QScatterDataProxy()
        self._series = QScatter3DSeries(self._proxy)
        self._series.setItemLabelFormat("@xTitle: @xLabel @yTitle: @yLabel @zTitle: @zLabel")
        self._series.setMeshSmooth(self._smooth)
        self._graph.addSeries(self._series)
        self._preset = QAbstract3DGraph.CameraPreset.FrontLow.value

        # Give ownership of the handler to the graph and make it the active
        # handler
        self._graph.setActiveInputHandler(self._inputHandler)

        # Give our axes to the input handler
        self._inputHandler.setAxes(self._graph.axisX(), self._graph.axisZ(),
                                   self._graph.axisY())

        self.addData()

    def addData(self):
        # Configure the axes according to the data
        self._graph.axisX().setTitle("X")
        self._graph.axisY().setTitle("Y")
        self._graph.axisZ().setTitle("Z")

        dataArray = []
        limit = int(sqrt(self._itemCount) / 2.0)
        for i in range(-limit, limit):
            for j in range(-limit, limit):
                x = float(i) + 0.5
                y = cos(degrees(float(i * j) / self._CURVE_DIVIDER))
                z = float(j) + 0.5
                dataArray.append(QScatterDataItem(QVector3D(x, y, z)))

        self._graph.seriesList()[0].dataProxy().resetArray(dataArray)

    @Slot(int)
    def changeStyle(self, style):
        comboBox = self.sender()
        if comboBox:
            self._style = comboBox.itemData(style)
            if self._graph.seriesList():
                self._graph.seriesList()[0].setMesh(self._style)

    @Slot(int)
    def setSmoothDots(self, smooth):
        self._smooth = smooth == Qt.Checked.value
        series = self._graph.seriesList()[0]
        series.setMeshSmooth(self._smooth)

    @Slot(int)
    def changeTheme(self, theme):
        currentTheme = self._graph.activeTheme()
        currentTheme.setType(Q3DTheme.Theme(theme))
        self.backgroundEnabledChanged.emit(currentTheme.isBackgroundEnabled())
        self.gridEnabledChanged.emit(currentTheme.isGridEnabled())

    @Slot()
    def changePresetCamera(self):
        camera = self._graph.scene().activeCamera()
        camera.setCameraPreset(QAbstract3DGraph.CameraPreset(self._preset))

        self._preset += 1
        if self._preset > QAbstract3DGraph.CameraPreset.DirectlyBelow.value:
            self._preset = QAbstract3DGraph.CameraPreset.FrontLow.value

    @Slot(QAbstract3DGraph.ShadowQuality)
    def shadowQualityUpdatedByVisual(self, sq):
        self.shadowQualityChanged.emit(sq.value)

    @Slot(int)
    def changeShadowQuality(self, quality):
        sq = QAbstract3DGraph.ShadowQuality(quality)
        self._graph.setShadowQuality(sq)

    @Slot(int)
    def setBackgroundEnabled(self, enabled):
        self._graph.activeTheme().setBackgroundEnabled(enabled == Qt.Checked.value)

    @Slot(int)
    def setGridEnabled(self, enabled):
        self._graph.activeTheme().setGridEnabled(enabled == Qt.Checked.value)

    @Slot()
    def toggleItemCount(self):
        if self._itemCount == NUMBER_OF_ITEMS:
            self._itemCount = LOWER_NUMBER_OF_ITEMS
            self._CURVE_DIVIDER = LOWER_CURVE_DIVIDER
        else:
            self._itemCount = NUMBER_OF_ITEMS
            self._CURVE_DIVIDER = CURVE_DIVIDER

        self._graph.seriesList()[0].dataProxy().resetArray([])
        self.addData()

    @Slot()
    def toggleRanges(self):
        if not self._autoAdjust:
            self._graph.axisX().setAutoAdjustRange(True)
            self._graph.axisZ().setAutoAdjustRange(True)
            self._inputHandler.setDragSpeedModifier(1.5)
            self._autoAdjust = True
        else:
            self._graph.axisX().setRange(-10.0, 10.0)
            self._graph.axisZ().setRange(-10.0, 10.0)
            self._inputHandler.setDragSpeedModifier(15.0)
            self._autoAdjust = False
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

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

from scatterdatamodifier import ScatterDataModifier


class ScatterGraph(QObject):

    def __init__(self, minimum_graph_size, maximum_graph_size):
        super().__init__()
        self._scatterGraph = Q3DScatter()
        self._scatterWidget = QWidget()
        hLayout = QHBoxLayout(self._scatterWidget)
        self._scatterGraph.setMinimumSize(minimum_graph_size)
        self._scatterGraph.setMaximumSize(maximum_graph_size)
        self._scatterGraph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self._scatterGraph.setFocusPolicy(Qt.StrongFocus)
        self._scatterGraph.setResizeMode(QQuickWidget.SizeRootObjectToView)
        hLayout.addWidget(self._scatterGraph, 1)

        vLayout = QVBoxLayout()
        hLayout.addLayout(vLayout)

        cameraButton = QCommandLinkButton(self._scatterWidget)
        cameraButton.setText("Change camera preset")
        cameraButton.setDescription("Switch between a number of preset camera positions")
        cameraButton.setIconSize(QSize(0, 0))

        itemCountButton = QCommandLinkButton(self._scatterWidget)
        itemCountButton.setText("Toggle item count")
        itemCountButton.setDescription("Switch between 900 and 10000 data points")
        itemCountButton.setIconSize(QSize(0, 0))

        rangeButton = QCommandLinkButton(self._scatterWidget)
        rangeButton.setText("Toggle axis ranges")
        rangeButton.setDescription("Switch between automatic axis ranges and preset ranges")
        rangeButton.setIconSize(QSize(0, 0))

        backgroundCheckBox = QCheckBox(self._scatterWidget)
        backgroundCheckBox.setText("Show background")
        backgroundCheckBox.setChecked(True)

        gridCheckBox = QCheckBox(self._scatterWidget)
        gridCheckBox.setText("Show grid")
        gridCheckBox.setChecked(True)

        smoothCheckBox = QCheckBox(self._scatterWidget)
        smoothCheckBox.setText("Smooth dots")
        smoothCheckBox.setChecked(True)

        itemStyleList = QComboBox(self._scatterWidget)
        itemStyleList.addItem("Sphere", QAbstract3DSeries.Mesh.Sphere)
        itemStyleList.addItem("Cube", QAbstract3DSeries.Mesh.Cube)
        itemStyleList.addItem("Minimal", QAbstract3DSeries.Mesh.Minimal)
        itemStyleList.addItem("Point", QAbstract3DSeries.Mesh.Point)
        itemStyleList.setCurrentIndex(0)

        themeList = QComboBox(self._scatterWidget)
        themeList.addItem("Qt")
        themeList.addItem("Primary Colors")
        themeList.addItem("Digia")
        themeList.addItem("Stone Moss")
        themeList.addItem("Army Blue")
        themeList.addItem("Retro")
        themeList.addItem("Ebony")
        themeList.addItem("Isabelle")
        themeList.setCurrentIndex(3)

        shadowQuality = QComboBox(self._scatterWidget)
        shadowQuality.addItem("None")
        shadowQuality.addItem("Low")
        shadowQuality.addItem("Medium")
        shadowQuality.addItem("High")
        shadowQuality.addItem("Low Soft")
        shadowQuality.addItem("Medium Soft")
        shadowQuality.addItem("High Soft")
        shadowQuality.setCurrentIndex(6)

        vLayout.addWidget(cameraButton)
        vLayout.addWidget(itemCountButton)
        vLayout.addWidget(rangeButton)
        vLayout.addWidget(backgroundCheckBox)
        vLayout.addWidget(gridCheckBox)
        vLayout.addWidget(smoothCheckBox)
        vLayout.addWidget(QLabel("Change dot style"))
        vLayout.addWidget(itemStyleList)
        vLayout.addWidget(QLabel("Change theme"))
        vLayout.addWidget(themeList)
        vLayout.addWidget(QLabel("Adjust shadow quality"))
        vLayout.addWidget(shadowQuality, 1, Qt.AlignTop)

        self._modifier = ScatterDataModifier(self._scatterGraph, self)

        cameraButton.clicked.connect(self._modifier.changePresetCamera)
        itemCountButton.clicked.connect(self._modifier.toggleItemCount)
        rangeButton.clicked.connect(self._modifier.toggleRanges)

        backgroundCheckBox.stateChanged.connect(self._modifier.setBackgroundEnabled)
        gridCheckBox.stateChanged.connect(self._modifier.setGridEnabled)
        smoothCheckBox.stateChanged.connect(self._modifier.setSmoothDots)

        self._modifier.backgroundEnabledChanged.connect(backgroundCheckBox.setChecked)
        self._modifier.gridEnabledChanged.connect(gridCheckBox.setChecked)
        itemStyleList.currentIndexChanged.connect(self._modifier.changeStyle)

        themeList.currentIndexChanged.connect(self._modifier.changeTheme)

        shadowQuality.currentIndexChanged.connect(self._modifier.changeShadowQuality)

        self._modifier.shadowQualityChanged.connect(shadowQuality.setCurrentIndex)
        self._scatterGraph.shadowQualityChanged.connect(self._modifier.shadowQualityUpdatedByVisual)

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

from surfacegraphmodifier import SurfaceGraphModifier

from PySide6.QtCore import QObject, Qt
from PySide6.QtGui import QBrush, QIcon, QLinearGradient, QPainter, QPixmap
from PySide6.QtWidgets import (QGroupBox, QCheckBox, QLabel, QHBoxLayout,
                               QPushButton, QRadioButton, QSizePolicy, QSlider,
                               QVBoxLayout, QWidget)
from PySide6.QtQuickWidgets import QQuickWidget
from PySide6.QtGraphs import Q3DSurface


def gradientBtoYPB_Pixmap():
    grBtoY = QLinearGradient(0, 0, 1, 100)
    grBtoY.setColorAt(1.0, Qt.black)
    grBtoY.setColorAt(0.67, Qt.blue)
    grBtoY.setColorAt(0.33, Qt.red)
    grBtoY.setColorAt(0.0, Qt.yellow)
    pm = QPixmap(24, 100)
    with QPainter(pm) as pmp:
        pmp.setBrush(QBrush(grBtoY))
        pmp.setPen(Qt.NoPen)
        pmp.drawRect(0, 0, 24, 100)
    return pm


def gradientGtoRPB_Pixmap():
    grGtoR = QLinearGradient(0, 0, 1, 100)
    grGtoR.setColorAt(1.0, Qt.darkGreen)
    grGtoR.setColorAt(0.5, Qt.yellow)
    grGtoR.setColorAt(0.2, Qt.red)
    grGtoR.setColorAt(0.0, Qt.darkRed)
    pm = QPixmap(24, 100)
    with QPainter(pm) as pmp:
        pmp.setBrush(QBrush(grGtoR))
        pmp.setPen(Qt.NoPen)
        pmp.drawRect(0, 0, 24, 100)
    return pm


def highlightPixmap():
    HEIGHT = 400
    WIDTH = 110
    BORDER = 10
    gr = QLinearGradient(0, 0, 1, HEIGHT - 2 * BORDER)
    gr.setColorAt(1.0, Qt.black)
    gr.setColorAt(0.8, Qt.darkGreen)
    gr.setColorAt(0.6, Qt.green)
    gr.setColorAt(0.4, Qt.yellow)
    gr.setColorAt(0.2, Qt.red)
    gr.setColorAt(0.0, Qt.darkRed)
    pmHighlight = QPixmap(WIDTH, HEIGHT)
    pmHighlight.fill(Qt.transparent)
    with QPainter(pmHighlight) as pmpHighlight:
        pmpHighlight.setBrush(QBrush(gr))
        pmpHighlight.setPen(Qt.NoPen)
        pmpHighlight.drawRect(BORDER, BORDER, 35, HEIGHT - 2 * BORDER)
        pmpHighlight.setPen(Qt.black)
        step = (HEIGHT - 2 * BORDER) / 5
        for i in range(0, 6):
            yPos = i * step + BORDER
            pmpHighlight.drawLine(BORDER, yPos, 55, yPos)
            HEIGHT = 550 - (i * 110)
            pmpHighlight.drawText(60, yPos + 2, f"{HEIGHT} m")
    return pmHighlight


class SurfaceGraph(QObject):

    def __init__(self, minimum_graph_size, maximum_graph_size):
        super().__init__()
        self._surfaceGraph = Q3DSurface()
        self._surfaceWidget = QWidget()
        hLayout = QHBoxLayout(self._surfaceWidget)
        self._surfaceGraph.setMinimumSize(minimum_graph_size)
        self._surfaceGraph.setMaximumSize(maximum_graph_size)
        self._surfaceGraph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self._surfaceGraph.setFocusPolicy(Qt.StrongFocus)
        self._surfaceGraph.setResizeMode(QQuickWidget.SizeRootObjectToView)
        hLayout.addWidget(self._surfaceGraph, 1)
        vLayout = QVBoxLayout()
        hLayout.addLayout(vLayout)
        vLayout.setAlignment(Qt.AlignTop)
        # Create control widgets
        modelGroupBox = QGroupBox("Model")
        sqrtSinModelRB = QRadioButton(self._surfaceWidget)
        sqrtSinModelRB.setText("Sqrt and Sin")
        sqrtSinModelRB.setChecked(False)
        heightMapModelRB = QRadioButton(self._surfaceWidget)
        heightMapModelRB.setText("Multiseries\nHeight Map")
        heightMapModelRB.setChecked(False)
        texturedModelRB = QRadioButton(self._surfaceWidget)
        texturedModelRB.setText("Textured\nTopography")
        texturedModelRB.setChecked(False)
        modelVBox = QVBoxLayout()
        modelVBox.addWidget(sqrtSinModelRB)
        modelVBox.addWidget(heightMapModelRB)
        modelVBox.addWidget(texturedModelRB)
        modelGroupBox.setLayout(modelVBox)
        selectionGroupBox = QGroupBox("Graph Selection Mode")
        modeNoneRB = QRadioButton(self._surfaceWidget)
        modeNoneRB.setText("No selection")
        modeNoneRB.setChecked(False)
        modeItemRB = QRadioButton(self._surfaceWidget)
        modeItemRB.setText("Item")
        modeItemRB.setChecked(False)
        modeSliceRowRB = QRadioButton(self._surfaceWidget)
        modeSliceRowRB.setText("Row Slice")
        modeSliceRowRB.setChecked(False)
        modeSliceColumnRB = QRadioButton(self._surfaceWidget)
        modeSliceColumnRB.setText("Column Slice")
        modeSliceColumnRB.setChecked(False)
        selectionVBox = QVBoxLayout()
        selectionVBox.addWidget(modeNoneRB)
        selectionVBox.addWidget(modeItemRB)
        selectionVBox.addWidget(modeSliceRowRB)
        selectionVBox.addWidget(modeSliceColumnRB)
        selectionGroupBox.setLayout(selectionVBox)
        axisGroupBox = QGroupBox("Axis ranges")
        axisMinSliderX = QSlider(Qt.Horizontal)
        axisMinSliderX.setMinimum(0)
        axisMinSliderX.setTickInterval(1)
        axisMinSliderX.setEnabled(True)
        axisMaxSliderX = QSlider(Qt.Horizontal)
        axisMaxSliderX.setMinimum(1)
        axisMaxSliderX.setTickInterval(1)
        axisMaxSliderX.setEnabled(True)
        axisMinSliderZ = QSlider(Qt.Horizontal)
        axisMinSliderZ.setMinimum(0)
        axisMinSliderZ.setTickInterval(1)
        axisMinSliderZ.setEnabled(True)
        axisMaxSliderZ = QSlider(Qt.Horizontal)
        axisMaxSliderZ.setMinimum(1)
        axisMaxSliderZ.setTickInterval(1)
        axisMaxSliderZ.setEnabled(True)
        axisVBox = QVBoxLayout(axisGroupBox)
        axisVBox.addWidget(QLabel("Column range"))
        axisVBox.addWidget(axisMinSliderX)
        axisVBox.addWidget(axisMaxSliderX)
        axisVBox.addWidget(QLabel("Row range"))
        axisVBox.addWidget(axisMinSliderZ)
        axisVBox.addWidget(axisMaxSliderZ)
        # Mode-dependent controls
        # sqrt-sin
        colorGroupBox = QGroupBox("Custom gradient")

        pixmap = gradientBtoYPB_Pixmap()
        gradientBtoYPB = QPushButton(self._surfaceWidget)
        gradientBtoYPB.setIcon(QIcon(pixmap))
        gradientBtoYPB.setIconSize(pixmap.size())

        pixmap = gradientGtoRPB_Pixmap()
        gradientGtoRPB = QPushButton(self._surfaceWidget)
        gradientGtoRPB.setIcon(QIcon(pixmap))
        gradientGtoRPB.setIconSize(pixmap.size())

        colorHBox = QHBoxLayout(colorGroupBox)
        colorHBox.addWidget(gradientBtoYPB)
        colorHBox.addWidget(gradientGtoRPB)
        # Multiseries heightmap
        showGroupBox = QGroupBox("Show Object")
        showGroupBox.setVisible(False)
        checkboxShowOilRigOne = QCheckBox("Oil Rig 1")
        checkboxShowOilRigOne.setChecked(True)
        checkboxShowOilRigTwo = QCheckBox("Oil Rig 2")
        checkboxShowOilRigTwo.setChecked(True)
        checkboxShowRefinery = QCheckBox("Refinery")
        showVBox = QVBoxLayout()
        showVBox.addWidget(checkboxShowOilRigOne)
        showVBox.addWidget(checkboxShowOilRigTwo)
        showVBox.addWidget(checkboxShowRefinery)
        showGroupBox.setLayout(showVBox)
        visualsGroupBox = QGroupBox("Visuals")
        visualsGroupBox.setVisible(False)
        checkboxVisualsSeeThrough = QCheckBox("See-Through")
        checkboxHighlightOil = QCheckBox("Highlight Oil")
        checkboxShowShadows = QCheckBox("Shadows")
        checkboxShowShadows.setChecked(True)
        visualVBox = QVBoxLayout(visualsGroupBox)
        visualVBox.addWidget(checkboxVisualsSeeThrough)
        visualVBox.addWidget(checkboxHighlightOil)
        visualVBox.addWidget(checkboxShowShadows)
        labelSelection = QLabel("Selection:")
        labelSelection.setVisible(False)
        labelSelectedItem = QLabel("Nothing")
        labelSelectedItem.setVisible(False)
        # Textured topography heightmap
        enableTexture = QCheckBox("Surface texture")
        enableTexture.setVisible(False)

        label = QLabel(self._surfaceWidget)
        label.setPixmap(highlightPixmap())
        heightMapGroupBox = QGroupBox("Highlight color map")
        colorMapVBox = QVBoxLayout()
        colorMapVBox.addWidget(label)
        heightMapGroupBox.setLayout(colorMapVBox)
        heightMapGroupBox.setVisible(False)
        # Populate vertical layout
        # Common
        vLayout.addWidget(modelGroupBox)
        vLayout.addWidget(selectionGroupBox)
        vLayout.addWidget(axisGroupBox)
        # Sqrt Sin
        vLayout.addWidget(colorGroupBox)
        # Multiseries heightmap
        vLayout.addWidget(showGroupBox)
        vLayout.addWidget(visualsGroupBox)
        vLayout.addWidget(labelSelection)
        vLayout.addWidget(labelSelectedItem)
        # Textured topography
        vLayout.addWidget(heightMapGroupBox)
        vLayout.addWidget(enableTexture)
        # Create the controller
        modifier = SurfaceGraphModifier(self._surfaceGraph, labelSelectedItem, self)
        # Connect widget controls to controller
        heightMapModelRB.toggled.connect(modifier.enableHeightMapModel)
        sqrtSinModelRB.toggled.connect(modifier.enableSqrtSinModel)
        texturedModelRB.toggled.connect(modifier.enableTopographyModel)
        modeNoneRB.toggled.connect(modifier.toggleModeNone)
        modeItemRB.toggled.connect(modifier.toggleModeItem)
        modeSliceRowRB.toggled.connect(modifier.toggleModeSliceRow)
        modeSliceColumnRB.toggled.connect(modifier.toggleModeSliceColumn)
        axisMinSliderX.valueChanged.connect(modifier.adjustXMin)
        axisMaxSliderX.valueChanged.connect(modifier.adjustXMax)
        axisMinSliderZ.valueChanged.connect(modifier.adjustZMin)
        axisMaxSliderZ.valueChanged.connect(modifier.adjustZMax)
        # Mode dependent connections
        gradientBtoYPB.pressed.connect(modifier.setBlackToYellowGradient)
        gradientGtoRPB.pressed.connect(modifier.setGreenToRedGradient)
        checkboxShowOilRigOne.stateChanged.connect(modifier.toggleItemOne)
        checkboxShowOilRigTwo.stateChanged.connect(modifier.toggleItemTwo)
        checkboxShowRefinery.stateChanged.connect(modifier.toggleItemThree)
        checkboxVisualsSeeThrough.stateChanged.connect(modifier.toggleSeeThrough)
        checkboxHighlightOil.stateChanged.connect(modifier.toggleOilHighlight)
        checkboxShowShadows.stateChanged.connect(modifier.toggleShadows)
        enableTexture.stateChanged.connect(modifier.toggleSurfaceTexture)
        # Connections to disable features depending on mode
        sqrtSinModelRB.toggled.connect(colorGroupBox.setVisible)
        heightMapModelRB.toggled.connect(showGroupBox.setVisible)
        heightMapModelRB.toggled.connect(visualsGroupBox.setVisible)
        heightMapModelRB.toggled.connect(labelSelection.setVisible)
        heightMapModelRB.toggled.connect(labelSelectedItem.setVisible)
        texturedModelRB.toggled.connect(enableTexture.setVisible)
        texturedModelRB.toggled.connect(heightMapGroupBox.setVisible)
        modifier.setAxisMinSliderX(axisMinSliderX)
        modifier.setAxisMaxSliderX(axisMaxSliderX)
        modifier.setAxisMinSliderZ(axisMinSliderZ)
        modifier.setAxisMaxSliderZ(axisMaxSliderZ)
        sqrtSinModelRB.setChecked(True)
        modeItemRB.setChecked(True)
        enableTexture.setChecked(True)

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

import os
from math import sqrt, sin
from pathlib import Path

from PySide6.QtCore import QObject, QPropertyAnimation, Qt, Slot
from PySide6.QtGui import (QColor, QFont, QImage, QLinearGradient,
                           QQuaternion, QVector3D)
from PySide6.QtGraphs import (QAbstract3DGraph, QCustom3DItem,
                              QCustom3DLabel, QHeightMapSurfaceDataProxy,
                              QValue3DAxis, QSurfaceDataItem,
                              QSurfaceDataProxy, QSurface3DSeries,
                              Q3DInputHandler, Q3DTheme)


from highlightseries import HighlightSeries
from topographicseries import TopographicSeries
from custominputhandler import CustomInputHandler


SAMPLE_COUNT_X = 150
SAMPLE_COUNT_Z = 150
HEIGHTMAP_GRID_STEP_X = 6
HEIGHTMAP_GRID_STEP_Z = 6
SAMPLE_MIN = -8.0
SAMPLE_MAX = 8.0

AREA_WIDTH = 8000.0
AREA_HEIGHT = 8000.0
ASPECT_RATIO = 0.1389
MIN_RANGE = AREA_WIDTH * 0.49


class SurfaceGraphModifier(QObject):

    def __init__(self, surface, label, parent):
        super().__init__(parent)
        self._data_path = Path(__file__).resolve().parent / "data"
        self._graph = surface
        self._textField = label
        self._sqrtSinProxy = None
        self._sqrtSinSeries = None
        self._heightMapProxyOne = None
        self._heightMapProxyTwo = None
        self._heightMapProxyThree = None
        self._heightMapSeriesOne = None
        self._heightMapSeriesTwo = None
        self._heightMapSeriesThree = None

        self._axisMinSliderX = None
        self._axisMaxSliderX = None
        self._axisMinSliderZ = None
        self._axisMaxSliderZ = None
        self._rangeMinX = 0.0
        self._rangeMinZ = 0.0
        self._stepX = 0.0
        self._stepZ = 0.0
        self._heightMapWidth = 0
        self._heightMapHeight = 0

        self._selectionAnimation = None
        self._titleLabel = None
        self._previouslyAnimatedItem = None
        self._previousScaling = {}

        self._topography = None
        self._highlight = None
        self._highlightWidth = 0
        self._highlightHeight = 0

        self._customInputHandler = None
        self._defaultInputHandler = Q3DInputHandler()

        self._graph.setCameraZoomLevel(85.0)
        self._graph.setCameraPreset(QAbstract3DGraph.CameraPreset.IsometricRight)
        self._graph.activeTheme().setType(Q3DTheme.Theme.Retro)

        self._x_axis = QValue3DAxis()
        self._y_axis = QValue3DAxis()
        self._z_axis = QValue3DAxis()
        self._graph.setAxisX(self._x_axis)
        self._graph.setAxisY(self._y_axis)
        self._graph.setAxisZ(self._z_axis)

        #
        # Sqrt Sin
        #
        self._sqrtSinProxy = QSurfaceDataProxy()
        self._sqrtSinSeries = QSurface3DSeries(self._sqrtSinProxy)
        self.fillSqrtSinProxy()

        #
        # Multisurface heightmap
        #
        # Create the first surface layer
        heightMapImageOne = QImage(self._data_path / "layer_1.png")
        self._heightMapProxyOne = QHeightMapSurfaceDataProxy(heightMapImageOne)
        self._heightMapSeriesOne = QSurface3DSeries(self._heightMapProxyOne)
        self._heightMapSeriesOne.setItemLabelFormat("(@xLabel, @zLabel): @yLabel")
        self._heightMapProxyOne.setValueRanges(34.0, 40.0, 18.0, 24.0)

        # Create the other 2 surface layers
        heightMapImageTwo = QImage(self._data_path / "layer_2.png")
        self._heightMapProxyTwo = QHeightMapSurfaceDataProxy(heightMapImageTwo)
        self._heightMapSeriesTwo = QSurface3DSeries(self._heightMapProxyTwo)
        self._heightMapSeriesTwo.setItemLabelFormat("(@xLabel, @zLabel): @yLabel")
        self._heightMapProxyTwo.setValueRanges(34.0, 40.0, 18.0, 24.0)

        heightMapImageThree = QImage(self._data_path / "layer_3.png")
        self._heightMapProxyThree = QHeightMapSurfaceDataProxy(heightMapImageThree)
        self._heightMapSeriesThree = QSurface3DSeries(self._heightMapProxyThree)
        self._heightMapSeriesThree.setItemLabelFormat("(@xLabel, @zLabel): @yLabel")
        self._heightMapProxyThree.setValueRanges(34.0, 40.0, 18.0, 24.0)

        # The images are the same size, so it's enough to get the dimensions
        # from one
        self._heightMapWidth = heightMapImageOne.width()
        self._heightMapHeight = heightMapImageOne.height()

        # Set the gradients for multi-surface layers
        grOne = QLinearGradient()
        grOne.setColorAt(0.0, Qt.black)
        grOne.setColorAt(0.38, Qt.darkYellow)
        grOne.setColorAt(0.39, Qt.darkGreen)
        grOne.setColorAt(0.5, Qt.darkGray)
        grOne.setColorAt(1.0, Qt.gray)
        self._heightMapSeriesOne.setBaseGradient(grOne)
        self._heightMapSeriesOne.setColorStyle(Q3DTheme.ColorStyle.RangeGradient)

        grTwo = QLinearGradient()
        grTwo.setColorAt(0.39, Qt.blue)
        grTwo.setColorAt(0.4, Qt.white)
        self._heightMapSeriesTwo.setBaseGradient(grTwo)
        self._heightMapSeriesTwo.setColorStyle(Q3DTheme.ColorStyle.RangeGradient)

        grThree = QLinearGradient()
        grThree.setColorAt(0.0, Qt.white)
        grThree.setColorAt(0.05, Qt.black)
        self._heightMapSeriesThree.setBaseGradient(grThree)
        self._heightMapSeriesThree.setColorStyle(Q3DTheme.ColorStyle.RangeGradient)

        # Custom items and label
        self._graph.selectedElementChanged.connect(self.handleElementSelected)

        self._selectionAnimation = QPropertyAnimation(self)
        self._selectionAnimation.setPropertyName(b"scaling")
        self._selectionAnimation.setDuration(500)
        self._selectionAnimation.setLoopCount(-1)

        titleFont = QFont("Century Gothic", 30)
        titleFont.setBold(True)
        self._titleLabel = QCustom3DLabel("Oil Rigs on Imaginary Sea", titleFont,
                                          QVector3D(0.0, 1.2, 0.0),
                                          QVector3D(1.0, 1.0, 0.0),
                                          QQuaternion())
        self._titleLabel.setPositionAbsolute(True)
        self._titleLabel.setFacingCamera(True)
        self._titleLabel.setBackgroundColor(QColor(0x66cdaa))
        self._graph.addCustomItem(self._titleLabel)
        self._titleLabel.setVisible(False)

        # Make two of the custom object visible
        self.toggleItemOne(True)
        self.toggleItemTwo(True)

        #
        # Topographic map
        #
        self._topography = TopographicSeries()
        file_name = os.fspath(self._data_path / "topography.png")
        self._topography.setTopographyFile(file_name, AREA_WIDTH, AREA_HEIGHT)
        self._topography.setItemLabelFormat("@yLabel m")

        self._highlight = HighlightSeries()
        self._highlight.setTopographicSeries(self._topography)
        self._highlight.setMinHeight(MIN_RANGE * ASPECT_RATIO)
        self._highlight.handleGradientChange(AREA_WIDTH * ASPECT_RATIO)
        self._graph.axisY().maxChanged.connect(self._highlight.handleGradientChange)

        self._customInputHandler = CustomInputHandler(self._graph)
        self._customInputHandler.setHighlightSeries(self._highlight)
        self._customInputHandler.setAxes(self._x_axis, self._y_axis, self._z_axis)
        self._customInputHandler.setLimits(0.0, AREA_WIDTH, MIN_RANGE)
        self._customInputHandler.setAspectRatio(ASPECT_RATIO)

    def fillSqrtSinProxy(self):
        stepX = (SAMPLE_MAX - SAMPLE_MIN) / float(SAMPLE_COUNT_X - 1)
        stepZ = (SAMPLE_MAX - SAMPLE_MIN) / float(SAMPLE_COUNT_Z - 1)

        dataArray = []
        for i in range(0, SAMPLE_COUNT_Z):
            newRow = []
            # Keep values within range bounds, since just adding step can
            # cause minor drift due to the rounding errors.
            z = min(SAMPLE_MAX, (i * stepZ + SAMPLE_MIN))
            for j in range(0, SAMPLE_COUNT_X):
                x = min(SAMPLE_MAX, (j * stepX + SAMPLE_MIN))
                R = sqrt(z * z + x * x) + 0.01
                y = (sin(R) / R + 0.24) * 1.61
                item = QSurfaceDataItem(QVector3D(x, y, z))
                newRow.append(item)
            dataArray.append(newRow)
        self._sqrtSinProxy.resetArray(dataArray)

    @Slot(bool)
    def enableSqrtSinModel(self, enable):
        if enable:
            self._sqrtSinSeries.setDrawMode(QSurface3DSeries.DrawSurfaceAndWireframe)
            self._sqrtSinSeries.setFlatShadingEnabled(True)

            self._graph.axisX().setLabelFormat("%.2f")
            self._graph.axisZ().setLabelFormat("%.2f")
            self._graph.axisX().setRange(SAMPLE_MIN, SAMPLE_MAX)
            self._graph.axisY().setRange(0.0, 2.0)
            self._graph.axisZ().setRange(SAMPLE_MIN, SAMPLE_MAX)
            self._graph.axisX().setLabelAutoRotation(30.0)
            self._graph.axisY().setLabelAutoRotation(90.0)
            self._graph.axisZ().setLabelAutoRotation(30.0)

            self._graph.removeSeries(self._heightMapSeriesOne)
            self._graph.removeSeries(self._heightMapSeriesTwo)
            self._graph.removeSeries(self._heightMapSeriesThree)
            self._graph.removeSeries(self._topography)
            self._graph.removeSeries(self._highlight)

            self._graph.addSeries(self._sqrtSinSeries)

            self._titleLabel.setVisible(False)
            self._graph.axisX().setTitleVisible(False)
            self._graph.axisY().setTitleVisible(False)
            self._graph.axisZ().setTitleVisible(False)

            self._graph.axisX().setTitle("")
            self._graph.axisY().setTitle("")
            self._graph.axisZ().setTitle("")

            self._graph.setActiveInputHandler(self._defaultInputHandler)

            # Reset range sliders for Sqrt & Sin
            self._rangeMinX = SAMPLE_MIN
            self._rangeMinZ = SAMPLE_MIN
            self._stepX = (SAMPLE_MAX - SAMPLE_MIN) / float(SAMPLE_COUNT_X - 1)
            self._stepZ = (SAMPLE_MAX - SAMPLE_MIN) / float(SAMPLE_COUNT_Z - 1)
            self._axisMinSliderX.setMinimum(0)
            self._axisMinSliderX.setMaximum(SAMPLE_COUNT_X - 2)
            self._axisMinSliderX.setValue(0)
            self._axisMaxSliderX.setMinimum(1)
            self._axisMaxSliderX.setMaximum(SAMPLE_COUNT_X - 1)
            self._axisMaxSliderX.setValue(SAMPLE_COUNT_X - 1)
            self._axisMinSliderZ.setMinimum(0)
            self._axisMinSliderZ.setMaximum(SAMPLE_COUNT_Z - 2)
            self._axisMinSliderZ.setValue(0)
            self._axisMaxSliderZ.setMinimum(1)
            self._axisMaxSliderZ.setMaximum(SAMPLE_COUNT_Z - 1)
            self._axisMaxSliderZ.setValue(SAMPLE_COUNT_Z - 1)

    @Slot(bool)
    def enableHeightMapModel(self, enable):
        if enable:
            self._heightMapSeriesOne.setDrawMode(QSurface3DSeries.DrawSurface)
            self._heightMapSeriesOne.setFlatShadingEnabled(False)
            self._heightMapSeriesTwo.setDrawMode(QSurface3DSeries.DrawSurface)
            self._heightMapSeriesTwo.setFlatShadingEnabled(False)
            self._heightMapSeriesThree.setDrawMode(QSurface3DSeries.DrawSurface)
            self._heightMapSeriesThree.setFlatShadingEnabled(False)

            self._graph.axisX().setLabelFormat("%.1f N")
            self._graph.axisZ().setLabelFormat("%.1f E")
            self._graph.axisX().setRange(34.0, 40.0)
            self._graph.axisY().setAutoAdjustRange(True)
            self._graph.axisZ().setRange(18.0, 24.0)

            self._graph.axisX().setTitle("Latitude")
            self._graph.axisY().setTitle("Height")
            self._graph.axisZ().setTitle("Longitude")

            self._graph.removeSeries(self._sqrtSinSeries)
            self._graph.removeSeries(self._topography)
            self._graph.removeSeries(self._highlight)
            self._graph.addSeries(self._heightMapSeriesOne)
            self._graph.addSeries(self._heightMapSeriesTwo)
            self._graph.addSeries(self._heightMapSeriesThree)

            self._graph.setActiveInputHandler(self._defaultInputHandler)

            self._titleLabel.setVisible(True)
            self._graph.axisX().setTitleVisible(True)
            self._graph.axisY().setTitleVisible(True)
            self._graph.axisZ().setTitleVisible(True)

            # Reset range sliders for height map
            mapGridCountX = self._heightMapWidth / HEIGHTMAP_GRID_STEP_X
            mapGridCountZ = self._heightMapHeight / HEIGHTMAP_GRID_STEP_Z
            self._rangeMinX = 34.0
            self._rangeMinZ = 18.0
            self._stepX = 6.0 / float(mapGridCountX - 1)
            self._stepZ = 6.0 / float(mapGridCountZ - 1)
            self._axisMinSliderX.setMinimum(0)
            self._axisMinSliderX.setMaximum(mapGridCountX - 2)
            self._axisMinSliderX.setValue(0)
            self._axisMaxSliderX.setMinimum(1)
            self._axisMaxSliderX.setMaximum(mapGridCountX - 1)
            self._axisMaxSliderX.setValue(mapGridCountX - 1)
            self._axisMinSliderZ.setMinimum(0)
            self._axisMinSliderZ.setMaximum(mapGridCountZ - 2)
            self._axisMinSliderZ.setValue(0)
            self._axisMaxSliderZ.setMinimum(1)
            self._axisMaxSliderZ.setMaximum(mapGridCountZ - 1)
            self._axisMaxSliderZ.setValue(mapGridCountZ - 1)

    @Slot(bool)
    def enableTopographyModel(self, enable):
        if enable:
            self._graph.axisX().setLabelFormat("%i")
            self._graph.axisZ().setLabelFormat("%i")
            self._graph.axisX().setRange(0.0, AREA_WIDTH)
            self._graph.axisY().setRange(100.0, AREA_WIDTH * ASPECT_RATIO)
            self._graph.axisZ().setRange(0.0, AREA_HEIGHT)
            self._graph.axisX().setLabelAutoRotation(30.0)
            self._graph.axisY().setLabelAutoRotation(90.0)
            self._graph.axisZ().setLabelAutoRotation(30.0)

            self._graph.removeSeries(self._heightMapSeriesOne)
            self._graph.removeSeries(self._heightMapSeriesTwo)
            self._graph.removeSeries(self._heightMapSeriesThree)
            self._graph.addSeries(self._topography)
            self._graph.addSeries(self._highlight)

            self._titleLabel.setVisible(False)
            self._graph.axisX().setTitleVisible(False)
            self._graph.axisY().setTitleVisible(False)
            self._graph.axisZ().setTitleVisible(False)

            self._graph.axisX().setTitle("")
            self._graph.axisY().setTitle("")
            self._graph.axisZ().setTitle("")

            self._graph.setActiveInputHandler(self._customInputHandler)

            # Reset range sliders for topography map
            self._rangeMinX = 0.0
            self._rangeMinZ = 0.0
            self._stepX = 1.0
            self._stepZ = 1.0
            self._axisMinSliderX.setMinimum(0)
            self._axisMinSliderX.setMaximum(AREA_WIDTH - 200)
            self._axisMinSliderX.setValue(0)
            self._axisMaxSliderX.setMinimum(200)
            self._axisMaxSliderX.setMaximum(AREA_WIDTH)
            self._axisMaxSliderX.setValue(AREA_WIDTH)
            self._axisMinSliderZ.setMinimum(0)
            self._axisMinSliderZ.setMaximum(AREA_HEIGHT - 200)
            self._axisMinSliderZ.setValue(0)
            self._axisMaxSliderZ.setMinimum(200)
            self._axisMaxSliderZ.setMaximum(AREA_HEIGHT)
            self._axisMaxSliderZ.setValue(AREA_HEIGHT)

    def adjustXMin(self, min):
        minX = self._stepX * float(min) + self._rangeMinX

        max = self._axisMaxSliderX.value()
        if min >= max:
            max = min + 1
            self._axisMaxSliderX.setValue(max)

        maxX = self._stepX * max + self._rangeMinX

        self.setAxisXRange(minX, maxX)

    def adjustXMax(self, max):
        maxX = self._stepX * float(max) + self._rangeMinX

        min = self._axisMinSliderX.value()
        if max <= min:
            min = max - 1
            self._axisMinSliderX.setValue(min)

        minX = self._stepX * min + self._rangeMinX

        self.setAxisXRange(minX, maxX)

    def adjustZMin(self, min):
        minZ = self._stepZ * float(min) + self._rangeMinZ

        max = self._axisMaxSliderZ.value()
        if min >= max:
            max = min + 1
            self._axisMaxSliderZ.setValue(max)

        maxZ = self._stepZ * max + self._rangeMinZ

        self.setAxisZRange(minZ, maxZ)

    def adjustZMax(self, max):
        maxX = self._stepZ * float(max) + self._rangeMinZ

        min = self._axisMinSliderZ.value()
        if max <= min:
            min = max - 1
            self._axisMinSliderZ.setValue(min)

        minX = self._stepZ * min + self._rangeMinZ

        self.setAxisZRange(minX, maxX)

    def setAxisXRange(self, min, max):
        self._graph.axisX().setRange(min, max)

    def setAxisZRange(self, min, max):
        self._graph.axisZ().setRange(min, max)

    def setBlackToYellowGradient(self):
        gr = QLinearGradient()
        gr.setColorAt(0.0, Qt.black)
        gr.setColorAt(0.33, Qt.blue)
        gr.setColorAt(0.67, Qt.red)
        gr.setColorAt(1.0, Qt.yellow)

        self._sqrtSinSeries.setBaseGradient(gr)
        self._sqrtSinSeries.setColorStyle(Q3DTheme.ColorStyle.RangeGradient)

    def setGreenToRedGradient(self):
        gr = QLinearGradient()
        gr.setColorAt(0.0, Qt.darkGreen)
        gr.setColorAt(0.5, Qt.yellow)
        gr.setColorAt(0.8, Qt.red)
        gr.setColorAt(1.0, Qt.darkRed)

        self._sqrtSinSeries.setBaseGradient(gr)
        self._sqrtSinSeries.setColorStyle(Q3DTheme.ColorStyle.RangeGradient)

    @Slot(bool)
    def toggleItemOne(self, show):
        positionOne = QVector3D(39.0, 77.0, 19.2)
        positionOnePipe = QVector3D(39.0, 45.0, 19.2)
        positionOneLabel = QVector3D(39.0, 107.0, 19.2)
        if show:
            color = QImage(2, 2, QImage.Format_RGB32)
            color.fill(Qt.red)
            file_name = os.fspath(self._data_path / "oilrig.mesh")
            item = QCustom3DItem(file_name, positionOne,
                                 QVector3D(0.025, 0.025, 0.025),
                                 QQuaternion.fromAxisAndAngle(0.0, 1.0, 0.0, 45.0),
                                 color)
            self._graph.addCustomItem(item)
            file_name = os.fspath(self._data_path / "pipe.mesh")
            item = QCustom3DItem(file_name, positionOnePipe,
                                 QVector3D(0.005, 0.5, 0.005), QQuaternion(),
                                 color)
            item.setShadowCasting(False)
            self._graph.addCustomItem(item)

            label = QCustom3DLabel()
            label.setText("Oil Rig One")
            label.setPosition(positionOneLabel)
            label.setScaling(QVector3D(1.0, 1.0, 1.0))
            self._graph.addCustomItem(label)
        else:
            self.resetSelection()
            self._graph.removeCustomItemAt(positionOne)
            self._graph.removeCustomItemAt(positionOnePipe)
            self._graph.removeCustomItemAt(positionOneLabel)

    @Slot(bool)
    def toggleItemTwo(self, show):
        positionTwo = QVector3D(34.5, 77.0, 23.4)
        positionTwoPipe = QVector3D(34.5, 45.0, 23.4)
        positionTwoLabel = QVector3D(34.5, 107.0, 23.4)
        if show:
            color = QImage(2, 2, QImage.Format_RGB32)
            color.fill(Qt.red)
            item = QCustom3DItem()
            file_name = os.fspath(self._data_path / "oilrig.mesh")
            item.setMeshFile(file_name)
            item.setPosition(positionTwo)
            item.setScaling(QVector3D(0.025, 0.025, 0.025))
            item.setRotation(QQuaternion.fromAxisAndAngle(0.0, 1.0, 0.0, 25.0))
            item.setTextureImage(color)
            self._graph.addCustomItem(item)
            file_name = os.fspath(self._data_path / "pipe.mesh")
            item = QCustom3DItem(file_name, positionTwoPipe,
                                 QVector3D(0.005, 0.5, 0.005), QQuaternion(),
                                 color)
            item.setShadowCasting(False)
            self._graph.addCustomItem(item)

            label = QCustom3DLabel()
            label.setText("Oil Rig Two")
            label.setPosition(positionTwoLabel)
            label.setScaling(QVector3D(1.0, 1.0, 1.0))
            self._graph.addCustomItem(label)
        else:
            self.resetSelection()
            self._graph.removeCustomItemAt(positionTwo)
            self._graph.removeCustomItemAt(positionTwoPipe)
            self._graph.removeCustomItemAt(positionTwoLabel)

    @Slot(bool)
    def toggleItemThree(self, show):
        positionThree = QVector3D(34.5, 86.0, 19.1)
        positionThreeLabel = QVector3D(34.5, 116.0, 19.1)
        if show:
            color = QImage(2, 2, QImage.Format_RGB32)
            color.fill(Qt.darkMagenta)
            item = QCustom3DItem()
            file_name = os.fspath(self._data_path / "refinery.mesh")
            item.setMeshFile(file_name)
            item.setPosition(positionThree)
            item.setScaling(QVector3D(0.04, 0.04, 0.04))
            item.setRotation(QQuaternion.fromAxisAndAngle(0.0, 1.0, 0.0, 75.0))
            item.setTextureImage(color)
            self._graph.addCustomItem(item)

            label = QCustom3DLabel()
            label.setText("Refinery")
            label.setPosition(positionThreeLabel)
            label.setScaling(QVector3D(1.0, 1.0, 1.0))
            self._graph.addCustomItem(label)
        else:
            self.resetSelection()
            self._graph.removeCustomItemAt(positionThree)
            self._graph.removeCustomItemAt(positionThreeLabel)

    @Slot(bool)
    def toggleSeeThrough(self, seethrough):
        s0 = self._graph.seriesList()[0]
        s1 = self._graph.seriesList()[1]
        if seethrough:
            s0.setDrawMode(QSurface3DSeries.DrawWireframe)
            s1.setDrawMode(QSurface3DSeries.DrawWireframe)
        else:
            s0.setDrawMode(QSurface3DSeries.DrawSurface)
            s1.setDrawMode(QSurface3DSeries.DrawSurface)

    @Slot(bool)
    def toggleOilHighlight(self, highlight):
        s2 = self._graph.seriesList()[2]
        if highlight:
            grThree = QLinearGradient()
            grThree.setColorAt(0.0, Qt.black)
            grThree.setColorAt(0.05, Qt.red)
            s2.setBaseGradient(grThree)
        else:
            grThree = QLinearGradient()
            grThree.setColorAt(0.0, Qt.white)
            grThree.setColorAt(0.05, Qt.black)
            s2.setBaseGradient(grThree)

    @Slot(bool)
    def toggleShadows(self, shadows):
        sq = (QAbstract3DGraph.ShadowQualityMedium
              if shadows else QAbstract3DGraph.ShadowQualityNone)
        self._graph.setShadowQuality(sq)

    @Slot(bool)
    def toggleSurfaceTexture(self, enable):
        if enable:
            file_name = os.fspath(self._data_path / "maptexture.jpg")
            self._topography.setTextureFile(file_name)
        else:
            self._topography.setTextureFile("")

    def handleElementSelected(self, type):
        self.resetSelection()
        if type == QAbstract3DGraph.ElementCustomItem:
            item = self._graph.selectedCustomItem()
            text = ""
            if isinstance(item, QCustom3DItem):
                text += "Custom label: "
            else:
                file = item.meshFile().split("/")[-1]
                text += f"{file}: "

            text += str(self._graph.selectedCustomItemIndex())
            self._textField.setText(text)
            self._previouslyAnimatedItem = item
            self._previousScaling = item.scaling()
            self._selectionAnimation.setTargetObject(item)
            self._selectionAnimation.setStartValue(item.scaling())
            self._selectionAnimation.setEndValue(item.scaling() * 1.5)
            self._selectionAnimation.start()
        elif type == QAbstract3DGraph.ElementSeries:
            text = "Surface ("
            series = self._graph.selectedSeries()
            if series:
                point = series.selectedPoint()
                text += f"{point.x()}, {point.y()}"
            text += ")"
            self._textField.setText(text)
        elif (type.value > QAbstract3DGraph.ElementSeries.value
              and type < QAbstract3DGraph.ElementCustomItem.value):
            index = self._graph.selectedLabelIndex()
            text = ""
            if type == QAbstract3DGraph.ElementAxisXLabel:
                text += "Axis X label: "
            elif type == QAbstract3DGraph.ElementAxisYLabel:
                text += "Axis Y label: "
            else:
                text += "Axis Z label: "
            text += str(index)
            self._textField.setText(text)
        else:
            self._textField.setText("Nothing")

    def resetSelection(self):
        self._selectionAnimation.stop()
        if self._previouslyAnimatedItem:
            self._previouslyAnimatedItem.setScaling(self._previousScaling)
        self._previouslyAnimatedItem = None

    def toggleModeNone(self):
        self._graph.setSelectionMode(QAbstract3DGraph.SelectionNone)

    def toggleModeItem(self):
        self._graph.setSelectionMode(QAbstract3DGraph.SelectionItem)

    def toggleModeSliceRow(self):
        sm = (QAbstract3DGraph.SelectionItemAndRow
              | QAbstract3DGraph.SelectionSlice
              | QAbstract3DGraph.SelectionMultiSeries)
        self._graph.setSelectionMode(sm)

    def toggleModeSliceColumn(self):
        sm = (QAbstract3DGraph.SelectionItemAndColumn
              | QAbstract3DGraph.SelectionSlice
              | QAbstract3DGraph.SelectionMultiSeries)
        self._graph.setSelectionMode(sm)

    def setAxisMinSliderX(self, slider):
        self._axisMinSliderX = slider

    def setAxisMaxSliderX(self, slider):
        self._axisMaxSliderX = slider

    def setAxisMinSliderZ(self, slider):
        self._axisMinSliderZ = slider

    def setAxisMaxSliderZ(self, slider):
        self._axisMaxSliderZ = slider
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

from PySide6.QtCore import Qt
from PySide6.QtGui import QImage, QVector3D
from PySide6.QtGraphs import (QSurface3DSeries, QSurfaceDataItem)


# Value used to encode height data as RGB value on PNG file
PACKING_FACTOR = 11983.0


class TopographicSeries(QSurface3DSeries):

    def __init__(self):
        super().__init__()
        self._sampleCountX = 0.0
        self._sampleCountZ = 0.0
        self.setDrawMode(QSurface3DSeries.DrawSurface)
        self.setFlatShadingEnabled(True)
        self.setBaseColor(Qt.white)

    def sampleCountX(self):
        return self._sampleCountX

    def sampleCountZ(self):
        return self._sampleCountZ

    def setTopographyFile(self, file, width, height):
        heightMapImage = QImage(file)
        bits = heightMapImage.bits()
        imageHeight = heightMapImage.height()
        imageWidth = heightMapImage.width()
        widthBits = imageWidth * 4
        stepX = width / float(imageWidth)
        stepZ = height / float(imageHeight)

        dataArray = []
        for i in range(0, imageHeight):
            p = i * widthBits
            z = height - float(i) * stepZ
            newRow = []
            for j in range(0, imageWidth):
                aa = bits[p + 0]
                rr = bits[p + 1]
                gg = bits[p + 2]
                color = (gg << 16) + (rr << 8) + aa
                y = float(color) / PACKING_FACTOR
                item = QSurfaceDataItem(QVector3D(float(j) * stepX, y, z))
                newRow.append(item)
                p += 4
            dataArray.append(newRow)

        self.dataProxy().resetArray(dataArray)

        self._sampleCountX = float(imageWidth)
        self._sampleCountZ = float(imageHeight)
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

from PySide6.QtCore import QObject, Signal


class VariantBarDataMapping(QObject):

    rowIndexChanged = Signal()
    columnIndexChanged = Signal()
    valueIndexChanged = Signal()
    rowCategoriesChanged = Signal()
    columnCategoriesChanged = Signal()
    mappingChanged = Signal()

    def __init__(self, rowIndex, columnIndex, valueIndex,
                 rowCategories=[], columnCategories=[]):
        super().__init__(None)
        self._rowIndex = rowIndex
        self._columnIndex = columnIndex
        self._valueIndex = valueIndex
        self._rowCategories = rowCategories
        self._columnCategories = columnCategories

    def setRowIndex(self, index):
        self._rowIndex = index
        self.mappingChanged.emit()

    def rowIndex(self):
        return self._rowIndex

    def setColumnIndex(self, index):
        self._columnIndex = index
        self.mappingChanged.emit()

    def columnIndex(self):
        return self._columnIndex

    def setValueIndex(self, index):
        self._valueIndex = index
        self.mappingChanged.emit()

    def valueIndex(self):
        return self._valueIndex

    def setRowCategories(self, categories):
        self._rowCategories = categories
        self.mappingChanged.emit()

    def rowCategories(self):
        return self._rowCategories

    def setColumnCategories(self, categories):
        self._columnCategories = categories
        self.mappingChanged.emit()

    def columnCategories(self):
        return self._columnCategories

    def remap(self, rowIndex, columnIndex, valueIndex,
              rowCategories=[], columnCategories=[]):
        self._rowIndex = rowIndex
        self._columnIndex = columnIndex
        self._valueIndex = valueIndex
        self._rowCategories = rowCategories
        self._columnCategories = columnCategories
        self.mappingChanged.emit()
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

from PySide6.QtCore import Slot
from PySide6.QtGraphs import QBarDataProxy, QBarDataItem


class VariantBarDataProxy(QBarDataProxy):

    def __init__(self):
        super().__init__()
        self._dataSet = None
        self._mapping = None

    def setDataSet(self, newSet):
        if self._dataSet:
            self._dataSet.itemsAdded.disconnect(self.handleItemsAdded)
            self._dataSet.dataCleared.disconnect(self.handleDataCleared)

        self._dataSet = newSet

        if self._dataSet:
            self._dataSet.itemsAdded.connect(self.handleItemsAdded)
            self._dataSet.dataCleared.connect(self.handleDataCleared)
        self.resolveDataSet()

    def dataSet(self):
        return self._dataSet.data()

    # Map key (row, column, value) to value index in data item (VariantItem).
    # Doesn't gain ownership of mapping, but does connect to it to listen for
    # mapping changes. Modifying mapping that is set to proxy will trigger
    # dataset re-resolving.
    def setMapping(self, mapping):
        if self._mapping:
            self._mapping.mappingChanged.disconnect(self.handleMappingChanged)

        self._mapping = mapping

        if self._mapping:
            self._mapping.mappingChanged.connect(self.handleMappingChanged)

        self.resolveDataSet()

    def mapping(self):
        return self._mapping.data()

    @Slot(int, int)
    def handleItemsAdded(self, index, count):
        # Resolve new items
        self.resolveDataSet()

    @Slot()
    def handleDataCleared(self):
        # Data cleared, reset array
        self.resetArray(None)

    @Slot()
    def handleMappingChanged(self):
        self.resolveDataSet()

    # Resolve entire dataset into QBarDataArray.
    def resolveDataSet(self):
        # If we have no data or mapping, or the categories are not defined,
        # simply clear the array
        if (not self._dataSet or not self._mapping
                or not self._mapping.rowCategories()
                or not self._mapping.columnCategories()):
            self.resetArray()
            return

        itemList = self._dataSet.itemList()

        rowIndex = self._mapping.rowIndex()
        columnIndex = self._mapping.columnIndex()
        valueIndex = self._mapping.valueIndex()
        rowList = self._mapping.rowCategories()
        columnList = self._mapping.columnCategories()

        # Sort values into rows and columns
        itemValueMap = {}
        for item in itemList:
            key = str(item[rowIndex])
            v = itemValueMap.get(key)
            if not v:
                v = {}
                itemValueMap[key] = v
            v[str(item[columnIndex])] = float(item[valueIndex])

        # Create a new data array in format the parent class understands
        newProxyArray = []
        for rowKey in rowList:
            newProxyRow = []
            for i in range(0, len(columnList)):
                item = QBarDataItem(itemValueMap[rowKey][columnList[i]])
                newProxyRow.append(item)
            newProxyArray.append(newProxyRow)

        # Finally, reset the data array in the parent class
        self.resetArray(newProxyArray)
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

from PySide6.QtCore import QObject, Signal


class VariantDataSet(QObject):

    itemsAdded = Signal(int, int)
    dataCleared = Signal()

    def __init__(self):
        super().__init__()
        self._variantData = []

    def clear(self):
        for item in self._variantData:
            item.clear()
            del item

        self._variantData.clear()
        self.dataCleared.emit()

    def addItem(self, item):
        self._variantData.append(item)
        addIndex = len(self._variantData)

        self.itemsAdded.emit(addIndex, 1)
        return addIndex

    def addItems(self, itemList):
        newCount = len(itemList)
        addIndex = len(self._variantData)
        self._variantData.extend(itemList)
        self.itemsAdded.emit(addIndex, newCount)
        return addIndex

    def itemList(self):
        return self._variantData
License information regarding the data obtained from National Land Survey of
Finland http://www.maanmittauslaitos.fi/en
- topographic model from Elevation model 2 m (U4421B, U4421D, U4422A and
  U4422C) 08/2014
- map image extracted from Topographic map raster 1:50 000 (U442) 08/2014

National Land Survey open data licence - version 1.0 - 1 May 2012

1. General information

The National Land Survey of Finland (hereinafter the Licensor), as the holder
of the immaterial rights to the data, has granted on the terms mentioned below
the right to use a copy (hereinafter data or dataset(s)) of the data (or a part
of it).

The Licensee is a natural or legal person who makes use of the data covered by
this licence. The Licensee accepts the terms of this licence by receiving the
dataset(s) covered by the licence.

This Licence agreement does not create a co-operation or business relationship
between the Licensee and the Licensor.

2. Terms of the licence

2.1. Right of use

This licence grants a worldwide, free of charge and irrevocable parallel right
of use to open data. According to the terms of the licence, data received by
the Licensee can be freely:
 - copied, distributed and published,
 - modified and utilised commercially and non-commercially,
 - inserted into other products and
 - used as a part of a software application or service.

2.2. Duties and responsibilities of the Licensee

Through reasonable means suitable to the distribution medium or method which is
used in conjunction with a product containing data or a service utilising data
covered by this licence or while distributing data, the Licensee shall:
 - mention the name of the Licensor, the name of the dataset(s) and the time
   when the National Land Survey has delivered the dataset(s) (e.g.: contains
   data from the National Land Survey of Finland Topographic Database 06/2012)
 - provide a copy of this licence or a link to it, as well as
 - require third parties to provide the same information when granting rights
   to copies of dataset(s) or products and services containing such data and
 - remove the name of the Licensor from the product or service, if required to
   do so by the Licensor.

The terms of this licence do not allow the Licensee to state in conjunction
with the use of dataset(s) that the Licensor supports or recommends such use.

2.3. Duties and responsibilities of the Licensor

The Licensor shall ensure that
 - the Licensor has the right to grant rights to the dataset(s) in accordance
   with this licence.

The data has been licensed "as is" and the Licensor
 - shall not be held responsible for any errors or omissions in the data,
   disclaims any warranty for the validity or up to date status of the data and
   shall be free from liability for direct or consequential damages arising
   from the use of data provided by the Licensor,
 - and is not obligated to ensure the continuous availability of the data, nor
   to announce in advance the interruption or cessation of availability, and
   the Licensor shall be free from liability for direct or consequential
   damages arising from any such interruption or cessation.

3. Jurisdiction

Finnish law shall apply to this licence.

4. Changes to this licence

The Licensor may at any time change the terms of the licence or apply a
different licence to the data. The terms of this licence shall, however, still
apply to such data that has been received prior to the change of the terms of
the licence or the licence itself.
# Rainfall per month from 2010 to 2022 in Northern Finland (Oulu)
# Format: year, month, rainfall
2010,1, 0,
2010,2, 3.4,
2010,3, 52,
2010,4, 33.8,
2010,5, 45.6,
2010,6, 43.8,
2010,7, 104.6,
2010,8, 105.4,
2010,9, 107.2,
2010,10,38.6,
2010,11,17.8,
2010,12,0,
2011,1, 8.2,
2011,2, 1.6,
2011,3, 27.4,
2011,4, 15.8,
2011,5, 57.6,
2011,6, 85.2,
2011,7, 127,
2011,8, 72.2,
2011,9, 82.2,
2011,10,62.4,
2011,11,31.6,
2011,12,53.8,
2012,1, 0,
2012,2, 5,
2012,3, 32.4,
2012,4, 57.6,
2012,5, 71.4,
2012,6, 60.8,
2012,7, 109,
2012,8, 43.6,
2012,9, 79.4,
2012,10,117.2,
2012,11,59,
2012,12,0.2,
2013,1, 28,
2013,2, 19,
2013,3, 0,
2013,4, 37.6,
2013,5, 44.2,
2013,6, 104.8,
2013,7, 84.2,
2013,8, 57.2,
2013,9, 37.2,
2013,10,64.6,
2013,11,77.8,
2013,12,92.8,
2014,1, 23.8,
2014,2, 23.6,
2014,3, 15.4,
2014,4, 13.2,
2014,5, 36.4,
2014,6, 26.4,
2014,7, 95.8,
2014,8, 81.8,
2014,9, 13.8,
2014,10,94.6,
2014,11,44.6,
2014,12,31,
2015,1, 37.4,
2015,2, 21,
2015,3, 42,
2015,4, 8.8,
2015,5, 82.4,
2015,6, 150,
2015,7, 56.8,
2015,8, 67.2,
2015,9, 131.2,
2015,10,38.4,
2015,11,83.4,
2015,12,47.8,
2016,1, 12.4,
2016,2, 34.8,
2016,3, 29,
2016,4, 40.4,
2016,5, 32.4,
2016,6, 80.2,
2016,7, 102.6,
2016,8, 95.6,
2016,9, 40.2,
2016,10,7.8,
2016,11,39.6,
2016,12,8.8,
2017,1, 9.4,
2017,2, 6.6,
2017,3, 29,
2017,4, 46.2,
2017,5, 43.2,
2017,6, 25.2,
2017,7, 72.4,
2017,8, 58.8,
2017,9, 68.8,
2017,10,45.8,
2017,11,36.8,
2017,12,29.6,
2018,1, 19.8,
2018,2, 0.8,
2018,3, 4,
2018,4, 23.2,
2018,5, 13.2,
2018,6, 62.8,
2018,7, 33,
2018,8, 96.6,
2018,9, 72.6,
2018,10,48.8,
2018,11,31.8,
2018,12,12.8,
2019,1, 0.2,
2019,2, 24.8,
2019,3, 32,
2019,4, 8.8,
2019,5, 71.4,
2019,6, 65.8,
2019,7, 17.6,
2019,8, 90,
2019,9, 50,
2019,10,77,
2019,11,27,
2019,12,43.2,
2020,1, 28.8,
2020,2, 45,
2020,3, 18.6,
2020,4, 13,
2020,5, 30.8,
2020,6, 21.4,
2020,7, 163.6,
2020,8, 12,
2020,9, 102.4,
2020,10,133.2,
2020,11,69.8,
2020,12,40.6,
2021,1, 0.4,
2021,2, 21.6,
2021,3, 24,
2021,4, 51.4,
2021,5, 76.4,
2021,6, 29.2,
2021,7, 36.4,
2021,8, 116,
2021,9, 72.4,
2021,10,93.4,
2021,11,21,
2021,12,10.2,
2022,1, 8.6,
2022,2, 6.6,
2022,3, 5.2,
2022,4, 15.2,
2022,5, 37.6,
2022,6, 45,
2022,7, 67.4,
2022,8, 161.6,
2022,9, 22.8,
2022,10,75.2,
2022,11,21.8,
2022,12,0.2
下一页
HelloGraphs示例
上一页
文档查看器示例
版权所有 © 2024 The Qt Company Ltd。本文件包含的文档贡献属于各自所有者的版权。本文件提供的文档是根据自由软件基金会发布的GNU自由文档许可协议版本1.3(https://gnu.ac.cn/licenses/fdl.html)进行许可的。Qt及其相关标志是The Qt Company Ltd.在芬兰和/或全球其他国家的商标。所有其他商标属于其各自所有者。
使用Sphinx和@pradyunsg的Furo制作