【PySide6】QChart筆記(一)—— 用QDateTimeAxis作為x軸繪製多條折線圖

林風冰翼發表於2023-11-05

一、QDateTimeAxis簡介

1. 官方描述

https://doc.qt.io/qtforpython-6/PySide6/QtCharts/QDateTimeAxis.html

QDateTimeAxis可以用作帶有刻度線、網格線以及陰影的軸。可以透過設定適當的日期時間格式來配置標籤。QDateTimeAxis有效的時間範圍為4714 BCE(公元前4714)到287396 CE(公元287396)。對於其他有關於QDateTime的限制,請參考QDateTime的官方文件。

1.1 屬性

屬性 描述
format 從QDateTime物件建立軸標籤時使用的字串
max 軸的最大值
min 軸的最小值
tickCount 軸的刻度線數量

1.2 訊號

訊號 描述
formatChanged 屬性format值改變時觸發
maxChanged 屬性max值改變時觸發
minChanged 屬性min值改變時觸發
rangeChanged 屬性max、min值改變時觸發
tickCountChanged 屬性tickCount值改變時觸發

1.3 使用方法

QDateTimeAxis可以與所有QXYSeries(QScatterSeries, QLineSeries, QSplineSeries)搭配使用。使用時,透過呼叫toMSecsSinceEpoch()方法向series中加點。

2. 官方用例

https://doc.qt.io/qtforpython-6/overviews/qtcharts-datetimeaxis-example.html

【官方警告】本節包含從C++自動轉換為Python的片段,其中可能包含錯誤
【譯註:事實上示例程式碼確實有問題 ? 】。

2.1 建立折線圖

為了建立折線圖,我們需要使用QLineSeries物件:

series = QLineSeries()

在圖表中,我們將展示太陽黑子的數量隨時間變化的情況,該資料從文字檔案中讀入。資料來源於Space Weather Prediction Center。

在下面的程式碼片段中,請注意如何使用toMSecsSinceEpoch()方法將QDateTime物件轉換為可以傳遞給QLineSeries.append()方法的數字型別【譯註:相當於整數型或浮點型時間戳】。

# 獲取資料,存入series
# data from http://www.swpc.noaa.gov/ftpdir/weekly/RecentIndices.txt
# http://www.swpc.noaa.gov/ftpdir/weekly/README
# http://www.weather.gov/disclaimer
sunSpots = QFile(":sun_spots")
if not sunSpots.open(QIODevice.ReadOnly | QIODevice.Text):
    m_loadError = "Failed to load '%1' file.".arg(sunSpots.fileName())
    return False

stream = QTextStream(sunSpots)
while not stream.atEnd():
    line = stream.readLine()
    if line.startsWith("#") or line.startsWith(":"):
        continue
    values = line.split(' ', Qt.SkipEmptyParts)
    momentInTime = QDateTime()
    momentInTime.setDate(QDate(values[0].toInt(), values[1].toInt() , 15))
    # 下面這句為官方給出的語句,會報錯 OverflowError: Python int too large to convert to C long
    # 請參考 “三、問題與總結” 中的內容
    # series.append(momentInTime.toMSecsSinceEpoch(), values[2].toDouble())

    # 解決方式如下
    series.append(numpy.int64(momentInTime.toMSecsSinceEpoch()), values[2].toDouble())

sunSpots.close()

為了在圖表上顯示資料,我們需要QChart例項。我們向其中新增series,隱藏圖例(legend),建立預設軸,並設定圖表的標題。

chart = QChart()
chart.addSeries(series)
chart.legend().hide()
chart.setTitle("Sunspots count (by Space Weather Prediction Center)")

由於我們使用的是QLineSeries物件,呼叫createDefaultAxes()方法將建立QValueAxis物件作為X軸和Y軸。要使用QDateTimeAxis,我們需要將它手動設定到圖表中【意思是,不能圖省事,直接呼叫 createDefaultAxes() 方法了。其中 createDefaultAxes() 方法會根據 QChart 物件已繫結的 QAbstractSeries 型別自動重新建立合適的座標軸】。

首先,建立QDateTimeAxis的例項,然後設定要顯示的刻度數。太陽黑子數量的含義是某月的平均值,因此,軸標籤中無需包含時間(time)和日號(day)的資訊,可以透過設定自定義標籤格式來實現只展示年月。

更多自定義標籤格式請參考QDateTime.toString()方法文件來了解可用的格式選項。
【譯註:以下程式碼中,MMM表示縮寫的本地化月份名稱(e.g. 'Jan' to 'Dec')。可在 QDateTime.toString() 方法文件中查閱】

axisX = QDateTimeAxis()
axisX.setTickCount(10)
axisX.setFormat("MMM yyyy")
axisX.setTitleText("Date")
chart.addAxis(axisX, Qt.AlignBottom)
series.attachAxis(axisX)
axisY = QValueAxis()
axisY.setLabelFormat("%i")
axisY.setTitleText("Sunspots count")
chart.addAxis(axisY, Qt.AlignLeft)
series.attachAxis(axisY)

然後我們建立一個QChartView物件,將chart作為引數。這樣我們就不需要自己建立QGraphicsView場景了。我們還設定了抗鋸齒功能,讓渲染後的線條看起來更漂亮。

createDefaultChartView(chart)

現在可以展示圖表了。

二、實踐

1. 用例說明

在同一個QChart中顯示兩條折線,其中x軸為QDateTimeAxis型別。

image

2. 程式碼實現

from PySide6.QtCharts import QChart, QChartView, QLineSeries, QDateTimeAxis, QValueAxis
from PySide6.QtGui import QPainter
from PySide6.QtCore import Qt, QDateTime
from PySide6.QtWidgets import QApplication, QMainWindow
import numpy as np

app = QApplication([])
window = QMainWindow()
chart = QChart()
chart.setTitle("Two Lines Chart")

# 準備資料
axisX_date = [QDateTime.currentDateTime().addDays(i) for i in range(5)]
axisY_value1 = [10 - 2 * i for i in range(5)]
axisY_value2 = [5 + i * (-1) ** i for i in range(5)]

series1 = QLineSeries()
for i in range(5):
    series1.append(np.int64(axisX_date[i].toMSecsSinceEpoch()), axisY_value1[i])

series2 = QLineSeries()
for i in range(5):
    series2.append(np.int64(axisX_date[i].toMSecsSinceEpoch()), axisY_value2[i])

# 將series新增到chart中
chart.addSeries(series1)
chart.addSeries(series2)

# 建立x軸
axisX = QDateTimeAxis()
axisX.setFormat("yyyy/MM/dd")
axisX.setTitleText("Date")
axisX.setTickCount(5)
axisX.setRange(QDateTime.currentDateTime(), QDateTime.currentDateTime().addDays(4))
# 將x軸與chart和series繫結
chart.addAxis(axisX, Qt.AlignBottom)
series1.attachAxis(axisX)
series2.attachAxis(axisX)

# 建立y軸
axisY = QValueAxis()
axisY.setTitleText("Value")
axisY.setRange(0, 10)
# 將y軸與chart和series繫結
chart.addAxis(axisY, Qt.AlignLeft)
series1.attachAxis(axisY)
series2.attachAxis(axisY)

# 顯示圖表
chartView = QChartView(chart)
chartView.setRenderHint(QPainter.Antialiasing)
window.setCentralWidget(chartView)
window.show()
app.exec()

三、問題與總結

1. OverflowError: Python int too large to convert to C long

問題描述

根據官方文件的指引,將QDateTime物件加入QChart圖表的series時,需要用toMSecsSinceEpoch()方法轉換為數值型,C++中完全沒有問題,但在Python中卻會出現該錯誤。

解決方法

透過numpy.int64()將呼叫toMSecsSinceEpoch()後過大的值轉換為numpy的Int64型別,然後再傳給QLineSeries.append()方法即可。即呼叫:

numpy.int64(QDateTime().toMSecsSinceEpoch())

2. 更新series後,影像或座標軸缺失

問題描述

修改series中的點集後,重新繪製QChart時,出現僅顯示折線而不顯示座標軸、或僅顯示座標軸而不顯示折線圖的情況。

解決方法

其原因是,繪製影像時,各物件的建立、繫結順序不正確,同時座標軸也需要重新建立。應當遵循如下順序:

  • 清除 QXYseries 物件中的舊資料,即呼叫QXYseries.clear()
  • 將資料寫入 QXYseries 物件, 即呼叫QXYseries.append()
  • 【僅需一次】將 QXYseries 物件與 QChart 物件繫結,即呼叫QChart.addSeries()
  • 刪除舊座標軸 QAbstractAxis 物件,並重新建立
  • 將新 QAbstractAxis 物件與 QChart 物件繫結,即呼叫QChart.addAxis()
  • 將新 QAbstractAxis 物件與 QXYseries 物件繫結,即呼叫QXYseries.attachAxis()

對於一般的座標軸型別,可直接呼叫 QChart.createDefaultAxes() 方法,相當於直接完成了後面三步。而對於 QDateTimeAxis 型別的座標軸,需要自行實現類似的方法。一個例子:

    def createDateTimeAxis(self, x_range, y_range):
        """
        Describe: 更新series後需要重新建立座標軸標軸,這樣才能展示出新的series;
                  又因為x軸型別為QDateTimeAxis,無法直接呼叫QChart.createDefaultAxes()方法建立座標軸,
                  因此需要自行實現該方法

        Args:
            x_range: tuple[QDateTime]
                x軸範圍
            y_range: tuple[float]
                y軸範圍
        """
        # 先刪除舊座標軸
        self.removeAxis(self._axisX)
        self.removeAxis(self._axisY)
        # 建立x軸
        self._axisX = QDateTimeAxis()
        self._axisX.setRange(x_range[0], x_range[1])
        self._axisX.setFormat("yyyy/MM")
        # 建立y軸
        self._axisY = QValueAxis()
        self._axisY.setRange(y_range[0], y_range[1])
        # 將新座標軸與QChart和series繫結
        self.addAxis(self._axisX, Qt.AlignBottom)
        self.addAxis(self._axisY, Qt.AlignLeft)
        for series in self.list_series_line:
            series.attachAxis(self._axisX)
            series.attachAxis(self._axisY)

3. 同一QChart顯示多條折線時,其中一條顯示不完全

問題描述

同一QChart顯示多條折線時,y軸的範圍限定在第一條折線series點集的數值範圍中,導致另一條顯示不完全。

解決方法

每次更新series後,在自定義的createDateTimeAxis()方法中,透過形參y_range,傳入合適的範圍。

相關文章