Coverage for graphica/gui/minimap_widget.py: 93.9%
92 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
1"""グラフの下の小さな全体図。ドラッグで選んだ範囲を range_selected で知らせる(軸に当てるのは呼び出し側)。
3メインのキャンバスとは別の Figure なので、redraw_all() では描き直されない。データが変わったら refresh() を呼ぶこと。
4"""
5import logging
7import numpy as np
8from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
9from matplotlib.figure import Figure
10from matplotlib.widgets import SpanSelector
11from PySide6.QtCore import Signal
13from graphica.core.analysis import calculate_lttb_downsample
14from graphica.core.dataset import COLOR_BY_COLUMN_PLOT_TYPE
15from graphica.gui import theme
17logger = logging.getLogger(__name__)
19# 点で描く種類。Line+Scatter は線が主なので含めない
20MINIMAP_POINT_PLOT_TYPES = frozenset({'Scatter', 'Density Scatter', COLOR_BY_COLUMN_PLOT_TYPE})
22MINIMAP_HEIGHT_PX = 70
24# 間引く。SpanSelector の背景は全体を描いた画像で、refresh() のたびに作り直すので、点が多いとドラッグが重くなる。
25# 幅が数百 px しかないので、canvas の LTTB より少ない点でよい
26MINIMAP_DOWNSAMPLE_THRESHOLD = 2000
27MINIMAP_DOWNSAMPLE_TARGET_POINTS = 500
31def minimap_colors(dark_mode):
32 """(図の地, 軸の地, 線と選択範囲) の色。地はカードの面、軸の地は一段くぼんだ面、線と選択範囲はテーマカラー。"""
33 tokens = theme.tokens_for(dark_mode, theme.current_theme_color())
34 return tokens['surface'], tokens['surface_2'], tokens['accent']
37class MinimapWidget(FigureCanvas):
38 range_selected = Signal(float, float)
40 def __init__(self, parent=None, dpi=100):
41 self.fig = Figure(figsize=(5, 0.7), dpi=dpi)
42 super().__init__(self.fig)
43 self.setParent(parent)
44 self.setFixedHeight(MINIMAP_HEIGHT_PX)
46 self.ax = self.fig.add_subplot(111)
47 self.dark_mode = False
48 self._span_selector = None
50 self._configure_axes_style()
51 self._create_span_selector()
52 self._apply_theme_colors()
55 def _configure_axes_style(self):
56 self.ax.set_yticks([])
57 self.ax.tick_params(axis='x', labelsize=6)
58 for spine in ('top', 'right', 'left'):
59 self.ax.spines[spine].set_visible(False)
60 self.fig.subplots_adjust(left=0.02, right=0.98, top=0.92, bottom=0.28)
62 def _create_span_selector(self):
63 """SpanSelector を作り直す(ax.cla() が選択の矩形も消すので、refresh() のたびに)。
65 古いものの接続は切る。matplotlib は cla() でも切らないので、残すと溜まって range_selected が重複して出る。
66 """
67 if getattr(self, '_span_selector', None) is not None:
68 self._span_selector.disconnect_events()
69 span_color = minimap_colors(self.dark_mode)[2]
70 self._span_selector = SpanSelector(
71 self.ax,
72 self._on_select,
73 'horizontal',
74 useblit=True,
75 props=dict(alpha=0.3, facecolor=span_color),
76 interactive=True,
77 drag_from_anywhere=True,
78 )
80 def _apply_theme_colors(self):
81 fig_face, axes_face, _ = minimap_colors(self.dark_mode)
82 self.fig.set_facecolor(fig_face)
83 self.ax.set_facecolor(axes_face)
85 def _on_select(self, xmin, xmax):
86 if xmin == xmax:
87 # クリックだけ(ドラッグなし)は選択とみなさない
88 return
89 self.range_selected.emit(xmin, xmax)
91 @staticmethod
92 def _downsample_for_overview(x, y):
93 """点を減らす。X が昇順なら LTTB、そうでなければ等間隔に間引く(LTTB は昇順が前提)。"""
94 n = len(x)
95 if n <= MINIMAP_DOWNSAMPLE_THRESHOLD:
96 return x, y
97 if np.all(np.diff(x) >= 0):
98 idx = calculate_lttb_downsample(x, y, MINIMAP_DOWNSAMPLE_TARGET_POINTS)
99 else:
100 step = max(1, n // MINIMAP_DOWNSAMPLE_TARGET_POINTS)
101 idx = np.arange(0, n, step)
102 return x[idx], y[idx]
105 def refresh(self, datasets, dark_mode=False):
106 """各データセットの X/Y を薄い線か点で重ねるだけの簡単な全体図にする。"""
107 self.dark_mode = dark_mode
108 self.ax.cla()
109 self._configure_axes_style()
110 self._apply_theme_colors()
112 line_color = minimap_colors(dark_mode)[2]
113 has_data = False
114 for ds in datasets:
115 # メインと同じく隠した系列は出さない
116 if not getattr(ds, 'visible', True):
117 continue
118 # 2D マップの x/y は測定点の列なので、線にすると意味が無い。描かない
119 if getattr(ds, 'data_kind', '1d') == '2d_grid':
120 continue
121 try:
122 x = ds.x_data
123 y = ds.y_data
124 except Exception:
125 logger.debug("ミニマップ用のデータを取り出せません: %s", ds.name, exc_info=True)
126 continue
127 if x is None or len(x) == 0: 127 ↛ 128line 127 didn't jump to line 128 because the condition on line 127 was never true
128 continue
129 try:
130 x, y = self._downsample_for_overview(x, y)
131 if getattr(ds, 'plot_type', 'Line') in MINIMAP_POINT_PLOT_TYPES:
132 # 点の種類は点で描く(線にするとピーク検出の結果がピーク同士を結ぶ線になる)
133 self.ax.plot(x, y, linestyle='None', marker='o', markersize=1.8,
134 color=line_color, alpha=0.8)
135 else:
136 self.ax.plot(x, y, color=line_color, linewidth=0.7, alpha=0.6)
137 has_data = True
138 except Exception:
139 # 全体図なので、描けなくても止めない
140 logger.debug("ミニマップへのデータセット描画に失敗しました", exc_info=True)
142 if has_data:
143 self.ax.relim()
144 self.ax.autoscale_view()
146 self._create_span_selector()
147 self.draw_idle()