pyqtgraph:修复使用 TimeAxisItem 和 tickStrings 时显示在 x 轴上的字符串数

pyqtgraph: fixing the number of strings displayed on the x-axis when using TimeAxisItem and tickStrings

我正在制作一个滚动图,它将绘制实时传感器数据,时间在 x 轴上。我对 tickStrings.

的行为有点困惑

我的代码基于下面的示例(来自 here)。随着绘制点数的增加,x 轴字符串的数量会发生变化 - 有时会增加,有时会减少。一旦双端队列达到全长并且 'scrolling' 开始,它就会稳定下来。

随着标绘点数量的增加,是否可以保持刻度线之间的间距不变?我想可能会使用一种方法,在添加新数据时替换空白刻度字符串,但不知道该怎么做。

编辑:我希望实现的一个例子是here

import sys
import numpy as np
import datetime
from PyQt5.QtCore import QTime, QTimer
from pyqtgraph.Qt import QtGui
import pyqtgraph as pg
from collections import deque
import time

class TimeAxisItem(pg.AxisItem):
    def __init__(self, *args, **kwargs):
        super(TimeAxisItem, self).__init__(*args, **kwargs)

    def tickStrings(self, values, scale, spacing):
        return [int2dt(value).strftime("%H:%M:%S") for value in values]

def int2dt(ts):
    return(datetime.datetime.fromtimestamp(ts))

class MyApplication(QtGui.QApplication):
    def __init__(self, *args, **kwargs):
        super(MyApplication, self).__init__(*args, **kwargs)
        self.t = QTime()
        self.t.start()

        maxlen = 100
        self.data_x = deque(maxlen=maxlen)
        self.data_y = deque(maxlen=maxlen)

        self.win = pg.GraphicsLayoutWidget()
        self.win.resize(1000,600)
        self.plot = self.win.addPlot(title='Scrolling real-time plot', axisItems={'bottom': TimeAxisItem(
            orientation='bottom')})

        self.curve = self.plot.plot()

        self.tmr = QTimer()
        self.tmr.timeout.connect(self.update)
        self.tmr.start(1000)

        self.y = 100
        self.win.show()

    def update(self):
        x = int(time.time())
        self.y = self.y + np.random.uniform(-1, 1)

        self.data_x.append(x)
        self.data_y.append(self.y)
        time.sleep(2)
        print(self.data_x)
        print(self.data_y)
        self.curve.setData(x=list(self.data_x), y=list(self.data_y))

def main():
    app = MyApplication(sys.argv)
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

要设置恒定的刻度数,您还必须覆盖 tickValues 方法。此方法生成刻度值,tickStrings 方法给出这些值的字符串表示形式 - 在您的情况下转换为人类可读时间。

这是您可以在您的代码中使用的 TimeAxisItem 示例:

class TimeAxisItem(pg.AxisItem):
    nticks = 6

    def __init__(self, *args, **kwargs):
        super(TimeAxisItem, self).__init__(*args, **kwargs)

    def tickStrings(self, values, scale, spacing):
        return [int2dt(value).strftime("%H:%M:%S") for value in values]

    def tickValues(self, minVal, maxVal, size):
        if self.nticks is not None:
            ticks = np.linspace(ceil(minVal), ceil(maxVal), num=self.nticks, endpoint=False)
            return [((1.0, 0), ticks)]
        else:
            return super().tickValues(minVal, maxVal, size)

您可以看到,如果 nticks 不是 None,我们从 minVal 和 maxVal 之间的值生成 n 个报价。 Pyqtgraph 从称为 levels 的范围生成刻度。对于我们的例子,使用范围 (1.0, 0) 就足够了,基本上就是说,刻度之间的距离是 1 秒,偏移量为 0。

您可以根据需要使用 setTickSpacing 定义多个范围。 例如,每小时、每分钟、每 5 秒都有刻度,您可以将其设置为 setTickSpacing(levels=[(3600, 0), (60, 0), (5, 0)]).
如果您的 Δx 小于 60 秒,这将每 5 秒产生一次滴答声。如果 Δx 小于一小时,则每分钟打勾。否则每小时打勾。

这是了解滴答声如何运作的重要知识。

由于问题更新了更多细节,这里有更具体的答案。我保留了以前的答案,以在显示固定数量的刻度的情况下帮助某人。

要准确解决您想要的问题,您需要先将滴答频率设置为每秒 1 次。这可以通过设置 self.setTickSpacing(levels=[(1, 0)]) 来完成。在你的例子中你有一分钟​​的更新时间,在这种情况下你想使用 self.setTickSpacing(levels=[(60, 0)]).

问题的第二部分是刻度线旋转 -90 度。旋转不是 AxisItem 实现的一部分。所以我们必须重新实现 drawPicture 方法。做刻度线的旋转和平移。同样要具有 'enough' 高度,我们必须设置 self.fixedHeight = 150 以适应长刻度字符串 .

刻度之间的恒定间距是通过设置X轴的固定范围来实现的。为了实现滚动效果,我们必须隐藏第一个刻度而不附加任何数据。这是通过 ticks = [(1.0, ticks[0][1][self.hide_ticks:])] 行完成的。

请务必注意,我的解决方案适合您的具体情况。否则必须使用 fixedHeight 和 drawPicture 方法的旋转部分。另外 maxlen 您的队列必须降低到 10 左右,这样刻度就不会重叠。

完整代码如下:

import datetime
import sys
import time
from collections import deque

import numpy as np
import pyqtgraph as pg
from PyQt5.QtCore import QTime, QTimer
from pyqtgraph import debug as debug
from pyqtgraph.Qt import QtGui


class TimeAxisItem(pg.AxisItem):
    hide_ticks = 0

    def __init__(self, *args, **kwargs):
        super(TimeAxisItem, self).__init__(*args, **kwargs)
        # Paint tick every 1 second
        self.setTickSpacing(levels=[(1, 0)])
        # Paint tick every 1 minute
        # self.setTickSpacing(levels=[(60, 0)])
        # Set fixed tick height
        self.fixedHeight = 150

    def tickStrings(self, values, scale, spacing):
        return [int2dt(value).strftime("%Y-%m-%d %H:%M:%S") for value in values]

    def drawPicture(self, p, axisSpec, tickSpecs, textSpecs):

        profiler = debug.Profiler()

        p.setRenderHint(p.RenderHint.Antialiasing, False)
        p.setRenderHint(p.RenderHint.TextAntialiasing, True)

        ## draw long line along axis
        pen, p1, p2 = axisSpec
        p.setPen(pen)
        p.drawLine(p1, p2)
        # p.translate(0.5,0)  ## resolves some damn pixel ambiguity

        ## draw ticks
        for pen, p1, p2 in tickSpecs:
            p.setPen(pen)
            p.drawLine(p1, p2)
        profiler('draw ticks')

        # Draw all text
        if self.style['tickFont'] is not None:
            p.setFont(self.style['tickFont'])
        p.setPen(self.textPen())
        bounding = self.boundingRect().toAlignedRect()
        p.setClipRect(bounding)
        for rect, flags, text in textSpecs:
            p.save()  # save the painter state
            p.translate(rect.center())  # move coordinate system to center of text rect
            p.rotate(-90)  # rotate text
            p.translate(-rect.center())  # revert coordinate system
            p.translate(-65, 0)  # Move rotated tick down by 65 pixels
            p.drawText(rect, int(flags), text)
            p.restore()  # restore the painter state

        profiler('draw text')

    def tickValues(self, minVal, maxVal, size):
        if minVal == 0:
            return []
        else:
            ticks = super().tickValues(minVal, maxVal, size)
            ticks = [(1.0, ticks[0][1][self.hide_ticks:])]
            return ticks


def int2dt(ts):
    return (datetime.datetime.fromtimestamp(ts))


class MyApplication(QtGui.QApplication):
    def __init__(self, *args, **kwargs):
        super(MyApplication, self).__init__(*args, **kwargs)
        self.t = QTime()
        self.t.start()

        maxlen = 10
        self.data_x = deque(maxlen=maxlen)
        self.data_y = deque(maxlen=maxlen)
        self.win = pg.GraphicsLayoutWidget()
        self.win.resize(1000, 600)
        self.axis_x = TimeAxisItem(orientation='bottom')
        self.plot = self.win.addPlot(title='Scrolling real-time plot', axisItems={'bottom': self.axis_x})
        self.curve = self.plot.plot()

        self.tmr = QTimer()
        self.tmr.timeout.connect(self.update)
        self.tmr.start(1000)

        self.y = 100
        self.win.show()

    def update(self):
        x = int(time.time())
        self.y = self.y + np.random.uniform(-1, 1)

        self.data_x.append(x)
        self.data_y.append(self.y)
        time.sleep(1)

        # Set fixed range
        self.plot.setXRange(self.data_x[0] - self.data_x.maxlen + len(self.data_x),
                            self.data_x[0] + len(self.data_x) - 1, padding=0)
        # Hide ticks without data attached to it, to achieve scrolling effect
        self.axis_x.hide_ticks = self.data_x.maxlen - len(self.data_x)
        self.curve.setData(x=list(self.data_x), y=list(self.data_y))


def main():
    app = MyApplication(sys.argv)
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()