[pyqtgraph] グラフにregionを追加してグラフ領域とリンクさせる

# やりたいこと
メイングラフにregionを追加してその領域を別のグラフに表示したい
<img width=500 alt='region.gif' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779817/1aa91d82-9158-62e9-c1a1-8c88d3540ada.gif'>

`pyqtgraph.exsamples.run()`のCrosshair / Mouse interactionを参考にしながら作成する
<img width=500 alt='exsample.gif' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779817/13b7d82a-5c18-335d-65f0-b83b66ddd21e.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
```python
import pyqtgraph.examples as ex
ex.run()
```

で色々なサンプルグラフが見れます。今回参考にしたのはCrosshair / Mouse interactionです。
<img width=500 alt='exsamples.png' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779817/97cd2c42-b946-bedf-f8b5-421a22696586.png'>

# コード
プロット用にnumpyを使用しています。 `pip install numpy`

```python
"""グラフにregionを追加してグラフ領域とリンクさせる"""

import dataclasses
from typing import Optional
import sys

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

SAMPLE_DATA1 = np.random.rand(500) * 10
SAMPLE_DATA2 = 10 + np.random.rand(500) * 10
SAMPLE_DATA3 = 20 + np.random.rand(500) * 10


@dataclasses.dataclass
class AddRegionWidget(pg.GraphicsLayoutWidget):
    """メイン画面
    Attributes #
    ----------
    parent: Optional[QtWidgets.QWidget] default=None
        親画面
    main_plotter: pyqtgraph.graphicsItems.PlotItem.PlotItem.PlotItem
        メイングラフ
    zoom_plotter: pyqtgraph.graphicsItems.PlotItem.PlotItem.PlotItem
        メイングラフをregionの領域でズームしたグラフ
    region: pyqtgraph.graphicsItems.LinearRegionItem.LinearRegionItem
        zoom_plotterのx軸領域を指定するregion
    """
    parent: Optional[QtWidgets.QWidget] = None

    def __post_init__(self) -> None:
        """スーパークラス読み込みとplot, region追加"""
        super(AddRegionWidget, self).__init__(parent=self.parent)

        self.add_plot()
        self.add_region()
        self.connect_slot()

    def add_plot(self) -> None:
        """plotを追加する"""
        self.main_plotter = self.addPlot(row=0, col=0)
        self.main_plotter.showGrid(x=True, y=True, alpha=0.8)
        main_curve1 = self.main_plotter.plot(pen=pg.mkPen('#f00'))
        main_curve2 = self.main_plotter.plot(pen=pg.mkPen('#0f0'))
        main_curve3 = self.main_plotter.plot(pen=pg.mkPen('#00f'))
        main_curve1.setData(SAMPLE_DATA1)
        main_curve2.setData(SAMPLE_DATA2)
        main_curve3.setData(SAMPLE_DATA3)

        self.zoom_plotter = self.addPlot(row=0, col=1)
        # y軸は値に合わせて調整する
        self.zoom_plotter.setAutoVisible(y=True)
        self.zoom_plotter.showGrid(x=True, y=True, alpha=0.8)
        zoom_curve1 = self.zoom_plotter.plot(pen=pg.mkPen('#f00'))
        zoom_curve2 = self.zoom_plotter.plot(pen=pg.mkPen('#0f0'))
        zoom_curve3 = self.zoom_plotter.plot(pen=pg.mkPen('#00f'))
        zoom_curve1.setData(SAMPLE_DATA1)
        zoom_curve2.setData(SAMPLE_DATA2)
        zoom_curve3.setData(SAMPLE_DATA3)

        self.zoom_plotter.setXRange(0.0, len(SAMPLE_DATA1) / 8, padding=0)

        self.ci.layout.setColumnStretchFactor(0, 8)
        self.ci.layout.setColumnStretchFactor(1, 5)

    def add_region(self) -> None:
        """regionを追加する"""
        self.region = pg.LinearRegionItem()
        # regionの高さ設定。regionが複数ある&重なった場合Zが高い方が操作可能(今回は１つなので適当に10に設定)
        self.region.setZValue(10)
        self.main_plotter.addItem(self.region, ignoreBounds=True)
        self.update_region()

    def connect_slot(self) -> None:
        """slot接続"""
        self.region.sigRegionChanged.connect(self.update_zoom_plotter)
        self.zoom_plotter.sigRangeChanged.connect(self.update_region)

    @QtCore.pyqtSlot()
    def update_zoom_plotter(self) -> None:
        """regionが動いた時にself.zoom_plotterの領域を変える"""
        self.region.setZValue(10)
        min_x, max_x = self.region.getRegion()
        self.zoom_plotter.setXRange(min_x, max_x, padding=0)

    @QtCore.pyqtSlot()
    def update_region(self) -> None:
        """self.zoom_plotterが動いた時regionの領域を変える
        viewRangeはグラフの表示範囲を返す。型は
        [[Xmin, Xmax], [Ymin, Ymax]]
        """
        rgn = self.zoom_plotter.viewRange()[0]
        self.region.setRegion(rgn)


def main() -> None:
    app = QtWidgets.QApplication(sys.argv)
    window = AddRegionWidget(parent=None)
    window.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

```

# 詳細
## regionが変わった時にzoom_plotterの範囲を変更する

```python
    @QtCore.pyqtSlot()
    def update_zoom_plotter(self) -> None:
        """regionが動いた時にself.zoom_plotterの領域を変える"""
        self.region.setZValue(10)
        min_x, max_x = self.region.getRegion()
        self.zoom_plotter.setXRange(min_x, max_x, padding=0)
```

`min_x, max_x`はregionのx座標です。
ex) min_x=231.1511878655732 max_x=293.6511878655732
それをzoom_potterのx軸範囲に指定しています。

`padding=0`は余白を無しにしているイメージです。
例えば`self.zoom_plotter.setXRange(100, 1000, padding=0.1)`の時、実際に表示される範囲は[100*(1 - 0.1), 1000*(1 + 0.1)]の`[90, 1100]`です。

regionの範囲とzoom_plotterの範囲を一致させたいので0にしています。

## zoom_plotterの範囲が変わった時にregionの範囲を変更する
```python
    @QtCore.pyqtSlot()
    def update_region(self) -> None:
        """self.zoom_plotterが動いた時regionの領域を変える
        viewRangeはグラフの表示範囲を返す。型は
        [[Xmin: float, Xmax: float], [Ymin: float, Ymax: float]]
        """
        rgn = self.zoom_plotter.viewRange()[0]
        self.region.setRegion(rgn)
```

zoom_plotterの範囲を直接変更した時にregionの範囲をそれに合わせています。
[Xmin: float, Xmax: float]の範囲を`setRegion`でregionにセットしています。

# その他regionの設定
横向きのregionの作成
`horizontal_region = pg.LinearRegionItem(orientation='horizontal')`
region領域内の色設定
`region = pg.LinearRegionItem(brush=pg.mkBrush('#f004'))`
\#RGBA以外でも色を渡せます。
> | ‘c’            | one of: r, g, b, c, m, y, k, w                               |
| -------------- | ------------------------------------------------------------ |
| R, G, B, [A]   | integers 0-255                                               |
| (R, G, B, [A]) | tuple of integers 0-255                                      |
| float          | greyscale, 0.0-1.0                                           |
| int            | see [`intColor()`](https://pyqtgraph.readthedocs.io/en/latest/functions.html?highlight=mkPen#pyqtgraph.intColor) |
| (int, hues)    | see [`intColor()`](https://pyqtgraph.readthedocs.io/en/latest/functions.html?highlight=mkPen#pyqtgraph.intColor) |
| “RGB”          | hexadecimal strings; may begin with ‘#’                      |
| “RGBA”         |                                                              |
| “RRGGBB”       |                                                              |
| “RRGGBBAA”     |                                                              |
| QColor         | QColor instance; makes a copy.                               |

>[PyQtGraph’s Helper Functions — pyqtgraph 0.11.1.dev0 documentation](https://pyqtgraph.readthedocs.io/en/latest/functions.html?highlight=mkColor#pyqtgraph.mkColor)

region境界線の色設定(色の指定は上と同じ方法で可能)
`region = pg.LinearRegionItem(pen=pg.mkPen('#fff'))`

カーソルがregion上にある時の領域内色設定
`region = pg.LinearRegionItem(hoverBrush=pg.mkBrush('#fff4'))`

カーソルがregionの境界線上にある時の境界線色設定
`region = pg.LinearRegionItem(hoverPen=pg.mkPen('#f00'))`


# 参考
`pyqtgraph.exsamples.run()`のCrosshair / Mouse interaction

region
[LinearRegionItem — pyqtgraph0.11.1.dev0ドキュメント](https://pyqtgraph.readthedocs.io/en/latest/graphicsItems/linearregionitem.html)

regionの色設定
[PyQtGraph’s Helper Functions — pyqtgraph 0.11.1.dev0 documentation](https://pyqtgraph.readthedocs.io/en/latest/functions.html?highlight=mkColor#pyqtgraph.mkColor)