2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[pyqtgraph] カーソルを追いかける十字線とその座標を表示するクラスを作った

Last updated at Posted at 2021-01-12

やりたい事

この動作をやってくれるクラスの作成

  • カーソルを追従する十字線の作成
  • ラベルにx, y座標を表示
    • ラベルの色はプロット線と同じになる
    • y軸の範囲外の場合はNone表示
class.gif

環境

Mac OS
Python 3.8.5

PyQt5 5.15.2
PyQt5-sip 12.8.1
pyqtgraph 0.11.1

pip install PyQt5 PyQt5-sip pyqtgraph

pyqtgraph.exsamples

import pyqtgraph.examples as ex
ex.run()

で色々なサンプルグラフが見れます。今回参考にしたのはCrosshair / Mouse interactionです。
exsamples.png

作ったもの

import dataclasses
from typing import Union
import sys

from PyQt5 import QtWidgets
import pyqtgraph as pg


@dataclasses.dataclass
class CursorCrossHairLabel(object):
    """カーソルを追従する十字線を作成してグラフに追加するクラス
    カーソルが座標内にあれば座標を表示する
    Attributes #
    ----------
    plotter: pg.PlotItem
        十字線を追加するグラフ
    label: pg.LabelItem
        座標を表示するラベル
    vertical_line: pg.InfiniteLine
        カーソルを追いかける縦線
    horizontal_line: pg.InfiniteLine
        カーソルを追いかける横線
    proxy: pg.SignalProxy
        self.cursor_movedの実行間隔を制御するオブジェクト
    y_label: str default='y'
        y値凡例の文字列
    label_font_size: Union[int, float] default=14
        ラベルフォントサイズ
    digit :int default=3
        座標の小数点以下表示を指定

    Examples
    ----------
    import dataclasses
    from typing import Union
    import sys

    from PyQt5 import QtWidgets
    import pyqtgraph as pg

    app = QtWidgets.QApplication(sys.argv)
    graph_widget = pg.GraphicsLayoutWidget()

    label = pg.LabelItem(justify='right')
    graph_widget.addItem(label)

    plotter = graph_widget.addPlot(row=0, col=0)
    plotter.showGrid(x=True, y=True, alpha=1)

    curve1 = pg.PlotCurveItem([i ** 2 for i in range(10)], pen=pg.mkPen('#f00'))
    curve2= pg.PlotCurveItem([2*i ** 2 for i in range(10)], pen=pg.mkPen('#0f0'))
    plotter.addItem(curve1)
    plotter.addItem(curve2)

    cross_hair = CursorCrossHairLabel(plotter, label)

    graph_widget.show()
    sys.exit(app.exec_())
    """
    plotter: pg.PlotItem
    label: pg.LabelItem
    y_label: str = 'y'
    label_font_size: Union[int, float] = 14
    digit: int = 3
    v_color: dataclasses.InitVar[str] = '#ffd700'
    v_width: dataclasses.InitVar[Union[int, float]] = 2
    h_color: dataclasses.InitVar[str] = '#ffd700'
    h_width: dataclasses.InitVar[Union[int, float]] = 2

    def __post_init__(self, v_color: str, v_width: Union[int, float], h_color: str, h_width: Union[int, float]) -> None:
        """コンストラクタの続き
        Parameters
        ----------
        v_color: str default='#ffd700'
            縦線の色
        v_width: Union[int, float] default=2
            縦線の幅
        h_color: str default='#ffd700'
            横線の色
        h_width: Union[int, float] default=2
            横線の幅
        """
        self.vertical_line = pg.InfiniteLine(angle=90, movable=False, pen=pg.mkPen(v_color, width=v_width))
        self.horizontal_line = pg.InfiniteLine(angle=0, movable=False, pen=pg.mkPen(h_color, width=h_width))
        # plotterに追加
        self.plotter.addItem(self.vertical_line, ignoreBounds=True)
        self.plotter.addItem(self.horizontal_line, ignoreBounds=True)

        self.proxy = pg.SignalProxy(self.plotter.scene().sigMouseMoved, rateLimit=60, slot=self.cursor_moved)

        self.set_label()

    def set_label(self):
        """座標の初期値(None)をラベルにセット"""
        coord_text = f"<span style='font-size: {self.label_font_size}pt'>x=None<br>"
        for y_index, curve in enumerate(self.plotter.curves, 1):
            curve_color: str = curve.opts['pen'].color().name()
            coord_text += f" <span style='color: {curve_color}'>{self.y_label}{y_index}=None</span><br>"
        self.label.setText(coord_text)

    def cursor_moved(self, window_coord) -> None:
        """カーソル位置に十字線移動と座標を表示する
        カーソルの座標がグラフ内であればx値を表示する
        カーソルのy座標にデータがあればy値を表示する。範囲外であればNoneを表示する。
        y座標ラベルはcurveと同じ色

        Parameters
        ----------
        window_coord: tuple
            画面のピクセル単位の座標, ex) (PyQt5.QtCore.QPointF(2.0, 44.0),)

        Notes
        ----------
        curveはpyqtgraph.PlotCurveItem()で作成しないとcurve_colorの個所でエラーが発生する
        ex) OK
        import pyqtgraph as pg
        window = pg.PlotWidget()
        curve = pg.PlotCurveItem(pen=pg.mkPen('#ff0000))
        window.addItem(curve)

        ex) Error
        import pyqtgraph as pg
        window = pg.PlotWidget()
        curve = window.plot(pen=...)

        関数内変数の説明===================
        pos: PyQt5.QtCore.QPointF
            カーソルの画面内の座標 ex) PyQt5.QtCore.QPointF(2.0, 44.0)
        cursor_point: QtCore.QPointF
            カーソルのグラフ内の座標 ex) PyQt5.QtCore.QPointF(269.0678171506131, 0.695852534562212)
        self.plotter.curves: List[pg.PlotCurveItem]
            plotter内にあるcurveのリスト ex) [<pg.PlotCurveItem object at ...>, ...]
        curve_color: str
            curveの色 ex) '#ff0000',  関係無いけどcurve.opts['pen'].color()の型はQtGui.QColor
        =================================
        """
        pos = window_coord[0]

        if self.plotter.sceneBoundingRect().contains(pos):
            cursor_point = self.plotter.vb.mapSceneToView(pos)
            x_cursor_point: float = cursor_point.x()
            coord_text = f"<span style='font-size: {self.label_font_size}pt'>x={x_cursor_point:.{self.digit}f}<br>"
            data_index = int(x_cursor_point)

            for label_index, curve in enumerate(self.plotter.curves, 1):
                curve_color = curve.opts['pen'].color().name()

                if len(curve.yData) != 0 and 0 <= data_index < len(curve.yData):
                    y_value = curve.yData[data_index]
                    coord_text += f" <span style='color: {curve_color}'>" \
                                  f"{self.y_label}{label_index}={y_value:.{self.digit}f}</span><br>"
                else:
                    coord_text += f" <span style='color: {curve_color}'>{self.y_label}{label_index}=None</span><br>"
            self.label.setText(coord_text)
            # 十字線移動
            self.vertical_line.setPos(cursor_point.x())
            self.horizontal_line.setPos(cursor_point.y())

引数

必須引数

  • plotter: pyqtgraph.PlotItem, 十字線を表示するグラフ
  • label: pyqtgraph.LabelItem, 座標を表示するラベル

その他

  • y_label: str, y値の凡例文字変更, デフォルト='y'
  • label_font_size: Union[int, float], ラベルフォントサイズ, デフォルト=14
  • digit :int 座標の小数点以下表示を指定, デフォルト=3

あとは十字線の色とか幅とか

使用例

実行結果が上のgifです

import dataclasses
from typing import Union
import sys

import numpy as np
from PyQt5 import QtWidgets
import pyqtgraph as pg

app = QtWidgets.QApplication(sys.argv)

graph_widget = pg.GraphicsLayoutWidget()

label = pg.LabelItem(justify='right')
graph_widget.addItem(label)

plotter = graph_widget.addPlot(row=0, col=0)
plotter.showGrid(x=True, y=True, alpha=1)

data = np.linspace(0, 100, 1000)

curve = pg.PlotCurveItem(data, pen=pg.mkPen('#f00', width=3))
curve2 = pg.PlotCurveItem(data * 2, pen=pg.mkPen('#0f0', width=3))
curve3 = pg.PlotCurveItem(data * 3, pen=pg.mkPen('#00f', width=3))
plotter.addItem(curve)
plotter.addItem(curve2)
plotter.addItem(curve3)
# これ
cross_hair = CursorCrossHairLabel(plotter, label, y_label='sample', label_font_size=18)

graph_widget.show()
sys.exit(app.exec_())

ラベルとグラフが重なってほしく無い時

class2.gif
graph_widget = pg.GraphicsLayoutWidget()

plotter = graph_widget.addPlot()
plotter.showGrid(x=True, y=True, alpha=1)

label = pg.LabelItem(justify='right')
graph_widget.addItem(label)

ラベル無いver

import dataclasses
from typing import Union
import sys

from PyQt5 import QtWidgets
import pyqtgraph as pg


@dataclasses.dataclass
class CursorCrossHair(object):
    """カーソルを追従する十字線を作成してグラフに追加するクラス
    Attributes #
    ----------
    plotter: pg.PlotItem
        十字線を追加するグラフ
    vertical_line: pg.InfiniteLine
        カーソルを追いかける縦線
    horizontal_line: pg.InfiniteLine
        カーソルを追いかける横線
    proxy: pg.SignalProxy
        self.cursor_movedの実行間隔を制御するオブジェクト
    
    Examples
    ----------
    import dataclasses
    from typing import Union
    import sys

    from PyQt5 import QtWidgets
    import pyqtgraph as pg

    app = QtWidgets.QApplication(sys.argv)
    graph_widget = pg.GraphicsLayoutWidget()

    plotter = graph_widget.addPlot(row=0, col=0)
    plotter.showGrid(x=True, y=True, alpha=1)

    curve1 = pg.PlotCurveItem([i ** 2 for i in range(10)], pen=pg.mkPen('#f00'))
    curve2= pg.PlotCurveItem([2*i ** 2 for i in range(10)], pen=pg.mkPen('#0f0'))
    plotter.addItem(curve1)
    plotter.addItem(curve2)

    cross_hair = CursorCrossHair(plotter)

    graph_widget.show()
    sys.exit(app.exec_())
    """
    plotter: pg.PlotItem
    v_color: dataclasses.InitVar[str] = '#ffd700'
    v_width: dataclasses.InitVar[Union[int, float]] = 2
    h_color: dataclasses.InitVar[str] = '#ffd700'
    h_width: dataclasses.InitVar[Union[int, float]] = 2

    def __post_init__(self, v_color: str, v_width: Union[int, float], h_color: str, h_width: Union[int, float]) -> None:
        """コンストラクタの続き
        Parameters
        ----------
        v_color: str default='#ffd700'
            縦線の色
        v_width: Union[int, float] default=2
            縦線の幅
        h_color: str default='#ffd700'
            横線の色
        h_width: Union[int, float] default=2
            横線の幅
        """
        self.vertical_line = pg.InfiniteLine(angle=90, movable=False, pen=pg.mkPen(v_color, width=v_width))
        self.horizontal_line = pg.InfiniteLine(angle=0, movable=False, pen=pg.mkPen(h_color, width=h_width))
        # plotterに追加
        self.plotter.addItem(self.vertical_line, ignoreBounds=True)
        self.plotter.addItem(self.horizontal_line, ignoreBounds=True)

        self.proxy = pg.SignalProxy(self.plotter.scene().sigMouseMoved, rateLimit=60, slot=self.cursor_moved)

    def cursor_moved(self, window_coord) -> None:
        """カーソル位置に十字線を移動させる
         Parameters
         ----------
         window_coord: tuple
             画面のピクセル単位の座標, ex) (PyQt5.QtCore.QPointF(2.0, 44.0),)

         Notes
         ----------
         関数内変数の説明===================
         pos: PyQt5.QtCore.QPointF
             カーソルの画面内の座標 ex) PyQt5.QtCore.QPointF(2.0, 44.0)
         cursor_point: QtCore.QPointF
             カーソルのグラフ内の座標 ex) PyQt5.QtCore.QPointF(269.0678171506131, 0.695852534562212)
         =================================
         """
        pos = window_coord[0]
        if self.plotter.sceneBoundingRect().contains(pos):
            cursor_point = self.plotter.vb.mapSceneToView(pos)
            self.vertical_line.setPos(cursor_point.x())
            self.horizontal_line.setPos(cursor_point.y())

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?