2024-06-15

Matplotlib のズーム機能とマーカーサイズ ~ PySide6

PySide (Qt for Python) は、Qt(キュート)の Python バインディングで、GUI などを構築するためのクロスプラットフォームなライブラリです。Linux/X11, macOS および Microsoft Windows をサポートしています。配布ライセンスは LGPL で公開されています。

データを視覚化したいときには、まず JupyterLab 上で Matplotlib を利用してあれこれチャートを作ります。そして、再利用するニーズがあるチャートについては、PySide6 に埋め込んで簡単な GUI アプリにするのが常です。

GUI 化するときには Matplotlib の NavigatorToolBar を利用して、主にズーム機能を利用しています。

NavigatorToolBar に表示されている虫眼鏡アイコンをクリックして、プロット上の矩形領域をマウスでドラッグして指定すると、簡単に拡大(ズームイン)できるので重宝しているのですが、不満が一つあります。

それは、拡大してもデータ点(マーカー)のサイズが変わらないことです。

同じようなことを不満に思う人はいるだろうなと思って調べたところ、Stack Overflow に、しかもずいぶん前にポストされていました(参考サイト [1])。当然ながら、PySide6 にチャートを埋め込む内容ではないので、参考サイトの回答にあったサンプルをベースにして、PySide6 にチャートを埋め込むサンプルを作ってみました。

下記の OS 環境で動作確認をしています。

RHEL 9.4 x86_64
Python 3.12.1
PySide6 6.7.1
matplotlib 3.9.0

このサンプルでは、単純化するために、プロットする path はひとつ、xy 軸は一対のみで、散布図 (scatter) を対象として、データ点の間は線を結ばず、ズーム機能ではマーカーのサイズのみを考慮するものとしています。

qt_matplotlib_zoom.py
# Reference:
# https://stackoverflow.com/questions/48474699/marker-size-alpha-scaling-with-window-size-zoom-in-plot-scatter
import sys
from math import sin
from typing import Any
from matplotlib.axes import Axes
from matplotlib.backends.backend_qtagg import (
FigureCanvasQTAgg as FigureCanvas,
NavigationToolbar2QT as NavigationToolbar,
)
from matplotlib.collections import PathCollection
from matplotlib.figure import Figure
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QMainWindow, QApplication
class MyChart(FigureCanvas):
def __init__(self):
self.fig = Figure()
self.fig.canvas.mpl_connect('resize_event', self.onResize)
super().__init__(self.fig)
self.fig_w = self.fig.get_figwidth()
self.fig_h = self.fig.get_figheight()
self.ax = self.fig.add_subplot(111)
self.ax.callbacks.connect('xlim_changed', self.onLimitChanged)
self.ax.callbacks.connect('ylim_changed', self.onLimitChanged)
self.xlim = self.ax.get_xlim()
self.ylim = self.ax.get_ylim()
self.fig_factor = None
self.path = None
self.sizes = None
def getZoomFactor(self,
limx: tuple[float, float],
limy: tuple[float, float]) -> float:
return min(
(self.xlim[1] - self.xlim[0]) / (limx[1] - limx[0]),
(self.ylim[1] - self.ylim[0]) / (limy[1] - limy[0])
)
def onLimitChanged(self, ax: Axes):
lx = ax.get_xlim()
ly = ax.get_ylim()
zfactor = self.getZoomFactor(lx, ly)
try:
self.path.set_sizes(
[s * zfactor * self.fig_factor for s in self.sizes]
)
except KeyError:
pass
def onResize(self, event: Any):
w = self.fig.get_figwidth()
h = self.fig.get_figheight()
self.fig_factor = min(w / self.fig_w, h / self.fig_h)
def setPath(self, path: PathCollection):
self.path = path
self.sizes = self.path.get_sizes()
def draw_chart(chart: MyChart):
list_x = list()
list_y = list()
for i in range(100):
x = i / 10.
y = sin(x)
list_x.append(x)
list_y.append(y)
path = chart.ax.scatter(list_x, list_y, s=20, c='blue')
chart.setPath(path)
class Example(QMainWindow):
def __init__(self):
super().__init__()
chart = MyChart()
chart.ax.grid()
self.setCentralWidget(chart)
navbar = NavigationToolbar(chart, self)
self.addToolBar(Qt.ToolBarArea.BottomToolBarArea, navbar)
draw_chart(chart)
def main():
app = QApplication()
ex = Example()
ex.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()

サンプルの実行例を示しました。

qt_matplotlib_zoom.py の実行例 (1)
qt_matplotlib_zoom.py の実行例 (2)
qt_matplotlib_zoom.py の実行例 (3)

時系列データを扱う場合などで、データ点の間を線で結ぶ場合でも、ズームインした際にマーカーのサイズさえ大きくなれば、線の太さはそのままで個人的には十分だと思っています。それでも、一つのチャートで複数のデータパスを扱うことが多いので、このサンプルで紹介した MyChart クラスを実用的に使えるように機能追加していく必要があります。

参考サイト

  1. pandas - Marker size/alpha scaling with window size/zoom in plot/scatter - Stack Overflow [2018-01-27]

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



0 件のコメント: