Coverage for graphica/gui/tools/slice_extraction.py: 88.6%
131 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"""スライス抽出モード: 2D マップの上でドラッグした線分に沿った断面を、新しい1次元のデータセットにする。
3対象は今選んでいるデータセット(その軸に描かれていること)。線分がほぼ水平・垂直ならその軸の値、
4斜めなら始点からの距離を X にする。
5"""
6import logging
8from matplotlib.lines import Line2D
10from graphica.gui import notify
11from graphica.core.provenance import build_provenance
12from graphica.core.dataset import Dataset
13from graphica.core.grid_data import extract_slice, GridDataError
14from graphica.gui.tools.pointer import clamped_data_point, legend_at
16logger = logging.getLogger(__name__)
18SLICE_EXTRACTION_N_POINTS = 200
20# extract_slice() の 'axis_kind' ごとの、新しいデータセットの X の列名とラベル
21_AXIS_KIND_LABELS = {
22 'x': 'X',
23 'y': 'Y',
24 'distance': '始点からの距離',
25}
28class SliceExtractionTool:
29 """状態はこのツールが持ち、PlotterApp の同じ名前は窓口(gui/tools/__init__.py)。"""
31 # PlotterApp から同じ名前で読み書きできるもの(テスト・メニュー・ほかの mixin が使う)
32 EXPOSED_NAMES = (
33 '_apply_slice_extraction', '_clear_slice_extraction_preview', '_create_slice_dataset',
34 '_on_slice_extraction_motion', '_on_slice_extraction_press', '_on_slice_extraction_release',
35 '_slice_extraction_axes', '_slice_extraction_motion_cid', '_slice_extraction_press_cid',
36 '_slice_extraction_preview_artist', '_slice_extraction_release_cid', '_slice_extraction_start',
37 '_toggle_slice_extraction_mode', 'slice_extraction_mode_enabled',
38 )
40 def __init__(self, app):
41 self._app = app
42 self.slice_extraction_mode_enabled = False
43 self._slice_extraction_press_cid = None
44 self._slice_extraction_motion_cid = None
45 self._slice_extraction_release_cid = None
46 self._slice_extraction_axes = None
47 self._slice_extraction_start = None # (x, y) データ座標
48 self._slice_extraction_preview_artist = None
49 self._slice_extraction_background = None # blit 用に押したときに撮った背景
51 def _toggle_slice_extraction_mode(self, checked):
52 self.slice_extraction_mode_enabled = checked
54 if checked:
55 self._app._deactivate_other_mouse_modes('slice_extraction')
57 self._slice_extraction_press_cid = self._app.canvas.mpl_connect(
58 'button_press_event', self._on_slice_extraction_press
59 )
60 self._slice_extraction_motion_cid = self._app.canvas.mpl_connect(
61 'motion_notify_event', self._on_slice_extraction_motion
62 )
63 self._slice_extraction_release_cid = self._app.canvas.mpl_connect(
64 'button_release_event', self._on_slice_extraction_release
65 )
66 self._app.statusBar().showMessage(
67 "スライス抽出モード: カレントの2Dマップ上でドラッグした線分に沿って"
68 "1Dデータセットを抽出します", 5000
69 )
70 else:
71 for attr in ('_slice_extraction_press_cid', '_slice_extraction_motion_cid',
72 '_slice_extraction_release_cid'):
73 cid = getattr(self, attr, None)
74 if cid is not None:
75 self._app.canvas.mpl_disconnect(cid)
76 setattr(self, attr, None)
77 self._clear_slice_extraction_preview()
78 self._slice_extraction_axes = None
79 self._slice_extraction_start = None
81 def _clear_slice_extraction_preview(self):
82 """ドラッグ中のプレビューと blit 用の背景を捨てる。描き直しで既に消えていても例外にしない。"""
83 artist = getattr(self, '_slice_extraction_preview_artist', None)
84 if artist is not None:
85 try:
86 artist.remove()
87 except (ValueError, NotImplementedError):
88 pass
89 self._slice_extraction_preview_artist = None
90 self._app.canvas.draw_idle()
91 self._slice_extraction_background = None
93 def _on_slice_extraction_press(self, event):
94 if not getattr(self, 'slice_extraction_mode_enabled', False):
95 return
96 if event.button != 1 or event.inaxes is None or event.xdata is None or event.ydata is None:
97 return
98 if legend_at(self._app.canvas, event) is not None:
99 return
100 self._slice_extraction_axes = event.inaxes
101 self._slice_extraction_start = (event.xdata, event.ydata)
102 # 背景は始めに1回だけ撮り、以降は線分だけを blit で描き直す(毎回全体を描くと系列が多いほど重い)
103 self._app.canvas.draw()
104 self._slice_extraction_background = self._app.canvas.copy_from_bbox(event.inaxes.bbox)
106 def _on_slice_extraction_motion(self, event):
107 axes = getattr(self, '_slice_extraction_axes', None)
108 if axes is None:
109 return
110 point = clamped_data_point(axes, event)
111 if point is None: 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true
112 return
114 start = self._slice_extraction_start
115 xs, ys = [start[0], point[0]], [start[1], point[1]]
116 line = self._slice_extraction_preview_artist
117 if line is None:
118 # animated=True の線は draw() では描かれず blit だけで出る。add_artist なのでデータの範囲にも入らない
119 line = Line2D(xs, ys, color='#E4572E', linestyle='--', linewidth=1.5, zorder=100, animated=True)
120 axes.add_artist(line)
121 self._slice_extraction_preview_artist = line
122 else:
123 line.set_data(xs, ys)
125 background = self._slice_extraction_background
126 if background is None: 126 ↛ 127line 126 didn't jump to line 127 because the condition on line 126 was never true
127 self._app.canvas.draw_idle()
128 return
129 self._app.canvas.restore_region(background)
130 axes.draw_artist(line)
131 self._app.canvas.blit(axes.bbox)
133 def _on_slice_extraction_release(self, event):
134 axes = getattr(self, '_slice_extraction_axes', None)
135 if axes is None: 135 ↛ 136line 135 didn't jump to line 136 because the condition on line 135 was never true
136 return
138 self._clear_slice_extraction_preview()
139 start = self._slice_extraction_start
140 self._slice_extraction_axes = None
141 self._slice_extraction_start = None
143 # 軸の外で離したら、軸の縁で止めた位置を終点にする
144 end = clamped_data_point(axes, event)
145 if end is None: 145 ↛ 146line 145 didn't jump to line 146 because the condition on line 145 was never true
146 return
147 if start == end:
148 return # クリックだけ(ドラッグなし)
150 self._apply_slice_extraction(axes, start, end)
152 def _apply_slice_extraction(self, axes, start, end):
153 """今のデータセットが 2D の格子でないか、この軸に描かれていなければ、何もせず案内を出す。"""
154 dataset = self._app._get_current_dataset()
155 if dataset is None or dataset.data_kind != '2d_grid':
156 notify.information(
157 self._app, "スライス抽出", "スライス抽出の対象となる2Dマップのデータセットを選択してください。"
158 )
159 return
161 target_axis = dataset.subplot_target
162 if dataset.use_secondary_y: 162 ↛ 163line 162 didn't jump to line 163 because the condition on line 162 was never true
163 expected_axes = (
164 self._app.all_secondary_axes[target_axis]
165 if 0 <= target_axis < len(self._app.all_secondary_axes) else None
166 )
167 else:
168 expected_axes = (
169 self._app.all_axes[target_axis]
170 if 0 <= target_axis < len(self._app.all_axes) else None
171 )
172 if axes is not expected_axes:
173 notify.information(
174 self._app, "スライス抽出",
175 "ドラッグしたサブプロットに、選択中の2Dマップが描画されていません。"
176 )
177 return
179 grid = dataset.z_grid
180 if grid is None: 180 ↛ 181line 180 didn't jump to line 181 because the condition on line 180 was never true
181 notify.warning(self._app, "スライス抽出", "有効な2Dグリッドデータがありません。")
182 return
184 try:
185 result = extract_slice(
186 grid['x_grid'], grid['y_grid'], grid['z_grid'],
187 start=start, end=end, n_points=SLICE_EXTRACTION_N_POINTS,
188 )
189 except GridDataError as e:
190 notify.warning(self._app, "スライス抽出", f"スライスの抽出に失敗しました:\n{e}")
191 return
193 self._create_slice_dataset(dataset, start, end, result)
195 def _create_slice_dataset(self, source_dataset, start, end, result):
196 import pandas as pd
198 axis_kind = result['axis_kind']
199 x_label = _AXIS_KIND_LABELS.get(axis_kind, axis_kind)
200 df = pd.DataFrame({'x': result['axis_values'], 'y': result['z_values']})
201 params = {
202 'start': [float(start[0]), float(start[1])],
203 'end': [float(end[0]), float(end[1])],
204 'axis_kind': axis_kind,
205 'n_points': SLICE_EXTRACTION_N_POINTS,
206 }
207 slice_dataset = Dataset(
208 name=f"Slice ({source_dataset.name})",
209 df=df, x_col_name='x', y_col_name='y',
210 provenance=build_provenance('2d_slice', params, [source_dataset]),
211 )
213 original_item = self._app._get_dataset_tree_item(source_dataset)
214 folder = original_item.parent() if original_item else None
216 def add():
217 self._app.dataset_order.append(slice_dataset, folder)
218 self._app._update_plot()
220 self._app._push_dataset_additions(add, [slice_dataset], "スライスの抽出")
221 self._app.statusBar().showMessage(
222 f"「{source_dataset.name}」からスライスを抽出しました(X軸: {x_label})", 4000
223 )