Coverage for graphica/gui/datasets/property_panel.py: 95.4%
392 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"""データセットのプロパティ欄: 選んだデータセットの値を欄に映し、欄の変更をデータセットに当てる。"""
2import logging
3import numpy as np
5from graphica.core.analysis import sample_standard_deviation
6from graphica.core.dataset import COLOR_BY_COLUMN_PLOT_TYPE
7from graphica.core.label_utils import infer_axis_label_from_column_name
8from graphica.core.marker_style import MARKER_EDGE_COLOR, MARKER_FILL_COLOR, marker_is_unfilled
9from graphica.gui.binding import Binder
10from graphica.gui.builders import canvas_area, property_sections, property_widgets
11from graphica.gui.shell import bottom_panel
12from graphica.gui.dataset_bindings import DATASET_PROPERTY_BINDINGS, NO_ERROR_COLUMN_LABEL, NO_MARKER
13from graphica.gui.dataset_style_icon import NON_MARKER_PLOT_TYPES
15logger = logging.getLogger(__name__)
18class DatasetPropertyPanel:
19 """プロパティ欄のウィジェットはタブ(PlotterApp)が持つので、タブを受け取って読み書きする。"""
21 def __init__(self, app):
22 self._app = app
23 self._binder = Binder(app, DATASET_PROPERTY_BINDINGS)
24 # 最後に欄へ反映した (今の項目, 選んでいる項目)。今の項目と選択の通知が両方来ても1回だけ反映する
25 self._shown_selection = None
27 def connect_signals(self):
28 """表(gui/dataset_bindings.py)の欄の変更をつなぐ。"""
29 self._binder.connect(watch=self.watch)
31 def watch(self, signal, widget):
32 """widget が変わったら、その属性だけを選択中のデータセットに当てる。"""
33 signal.connect(lambda *_: self.on_property_changed(widget))
35 def on_subplot_target_changed(self, index):
36 dataset = self._app._get_current_dataset()
37 if dataset is None or index == -1:
38 return
39 if dataset.subplot_target == index:
40 return
42 self._app._push_dataset_property_command(
43 dataset,
44 {'subplot_target': dataset.subplot_target},
45 {'subplot_target': index},
46 description="描画先プロットの変更"
47 )
49 def on_dataset_selected(self, current_item=None, previous_item=None):
50 """今の項目が変わったときと、選択が変わったときの両方から呼ぶ。
52 ボタンの有効/無効は選択で決まるが、削除の後などは今の項目だけがあって選ばれていない。
53 その項目をクリックしても今の項目は変わらず currentItemChanged が出ないので、選択の通知でも決め直す。
54 """
55 if self._selection_unchanged():
56 return
57 self.update_ui_state()
58 self._app._notify_plugins_selection_changed()
60 def shown_dataset(self):
61 """プロパティに出すデータセット。今の項目が選ばれていればそれ、違えば選んでいる最初のデータセット、無ければ None。
63 削除のあとなどは今の項目だけが残って選ばれていない。その値を無効の欄に出すと、選んでいるように見えて紛らわしい。
64 """
65 tree = self._app.ui.dataset_list_widget
66 current_item = tree.currentItem()
67 if current_item is not None and current_item.isSelected():
68 current = self._app._get_current_dataset()
69 if current is not None:
70 return current
71 selected = self._app._get_selected_datasets()
72 return selected[0] if selected else None
74 def _current_selection(self):
75 tree = self._app.ui.dataset_list_widget
76 return tree.currentItem(), tuple(tree.selectedItems())
78 def _selection_unchanged(self):
79 if self._shown_selection is None: 79 ↛ 80line 79 didn't jump to line 80 because the condition on line 79 was never true
80 return False
81 (current, selected), (shown_current, shown_selected) = self._current_selection(), self._shown_selection
82 return (current is shown_current and len(selected) == len(shown_selected)
83 and all(a is b for a, b in zip(selected, shown_selected)))
85 def on_legend_name_changed(self):
86 dataset = self.shown_dataset()
87 if dataset is None:
88 return
90 new_name = self._app.ui.legend_name_edit.text()
92 self._app._push_dataset_property_command(
93 dataset,
94 {'name': dataset.name},
95 {'name': new_name},
96 description="凡例名の変更"
97 )
99 def on_point_labels_toggled(self, checked):
100 """
101 点数が上限を超えるデータセットには、ラベルを描かずに理由を欄に出す(update_point_labels_limit_note)。
102 ラベル1件の描画に約2.5msかかり、上限を超えて描かせると再描画のたびに GUI が長く止まるため。
103 """
104 self.on_property_changed(self._app.point_labels_checkbox)
105 self.update_point_labels_limit_note()
107 def update_point_labels_limit_note(self):
108 """ラベル表示が有効なのに点数が上限を超えるデータセットがあれば、その理由をチェックボックスの下に出す。"""
109 note = getattr(self._app, 'point_labels_limit_note', None)
110 if note is None: 110 ↛ 111line 110 didn't jump to line 111 because the condition on line 110 was never true
111 return
112 max_points = self._app.canvas.point_label_max_points
113 over_limit = [ds for ds in self._app._get_selected_datasets()
114 if ds.show_point_labels and len(ds.visible_df) > max_points]
115 if not over_limit:
116 note.setVisible(False)
117 return
118 largest = max(len(ds.visible_df) for ds in over_limit)
119 note.setText(
120 f"点数({largest:,}件)が表示上限({max_points:,}件)を超えているため、"
121 "ラベルは表示されません。上限は「環境設定」で変更できますが、"
122 "点数が多いと描画に時間がかかります(1,000件で約2秒)。"
123 )
124 note.setVisible(True)
126 def on_property_changed(self, widget=None):
127 """
128 widget が表す属性だけを、選択中のデータセットすべてに当てる。
129 全属性をまとめて当てると、複数選択のとき触っていない属性まで1つの値に揃ってしまう。
130 """
131 selected_datasets = self._app._get_selected_datasets()
132 if not selected_datasets:
133 return
135 binding = self._binder.binding_for(widget)
136 if binding is None:
137 return
138 attr_name, new_value = binding.key, self._binder.read(binding)
140 is_batch = len(selected_datasets) > 1
141 if is_batch:
142 self._app.undo_stack.beginMacro(f"プロパティの一括変更 ({len(selected_datasets)}件)")
143 for dataset in selected_datasets:
144 self._app._push_dataset_property_command(
145 dataset, {attr_name: getattr(dataset, attr_name)}, {attr_name: new_value},
146 description="プロパティ変更"
147 )
148 if is_batch:
149 self._app.undo_stack.endMacro()
151 def update_mode_visibility(self):
152 """シンプル / アドバンスドを替えたとき: シンプルで隠した行を一度出し、今の条件で出し入れし直す
153 (各 update_*_visibility の最後の節の出し入れが、シンプルならまた隠す)。"""
154 for widget in property_sections.lite_hidden_property_widgets(self._app):
155 widget.setVisible(True)
156 self.update_gradient_controls_visibility()
157 self.update_waterfall_controls_visibility()
158 self.update_smoothing_control_visibility()
159 self.update_marker_controls_visibility()
160 self.update_2d_controls_visibility()
161 self._app._update_property_section_visibility()
163 def update_gradient_controls_visibility(self):
164 """
165 グラデーションは線か塗りのある種別('Line'/'Line+Scatter'/'Area')でだけ出す。
166 対象(線/塗り/両方)を選べるのは 'Area' だけなので、その欄は 'Area' のときだけ出す。
167 """
168 dataset = self._app._get_current_dataset()
169 plot_type = dataset.plot_type if dataset is not None else self._app.ui.plot_type_combo.currentText()
170 supports_gradient = plot_type in ('Line', 'Line+Scatter', 'Area')
172 self._app.gradient_checkbox.setVisible(supports_gradient)
174 show_detail = supports_gradient and self._app.gradient_checkbox.isChecked()
175 self._app.gradient_color2_label.setVisible(show_detail)
176 self._app.gradient_color2_picker.setVisible(show_detail)
178 show_target_combo = show_detail and plot_type == 'Area'
179 self._app.gradient_target_label.setVisible(show_target_combo)
180 self._app.gradient_target_combo.setVisible(show_target_combo)
181 self._app._update_property_section_visibility()
183 def update_smoothing_control_visibility(self):
184 """
185 平滑化は線で結ぶ種別('Line'/'Line+Scatter')でだけ出す。ほかの種別では平滑化した線が
186 マーカー・棒・塗りを置き換えてしまう(描画側も同じ条件で確かめている)。
187 """
188 dataset = self._app._get_current_dataset()
189 plot_type = dataset.plot_type if dataset is not None else self._app.ui.plot_type_combo.currentText()
190 is_smoothable_type = plot_type in ('Line', 'Line+Scatter')
191 self._app.ui.smoothing_checkbox.setVisible(is_smoothable_type)
192 # 手法の欄はチェックが外れている間も隠さず無効にする(出たり消えたりで配置が揺れないように)
193 self._app.smoothing_method_label.setVisible(is_smoothable_type)
194 self._app.smoothing_method_combo.setVisible(is_smoothable_type)
195 self._app.smoothing_method_combo.setEnabled(self._app.ui.smoothing_checkbox.isChecked())
196 self._app._update_property_section_visibility()
198 def update_error_display_control_items(self):
199 """
200 棒と面では誤差の帯が合わない(面は塗りと重なる)ので、「誤差バンド」「両方」を選べなくする。
201 保存済みの値は変えない。
202 """
203 dataset = self._app._get_current_dataset()
204 plot_type = dataset.plot_type if dataset is not None else self._app.ui.plot_type_combo.currentText()
205 band_ok = plot_type not in ('Bar', 'Area')
206 model = self._app.error_display_combo.model()
207 for value in ('band', 'both'):
208 index = self._app.error_display_combo.findData(value)
209 if index != -1: 209 ↛ 207line 209 didn't jump to line 207 because the condition on line 209 was always true
210 model.item(index).setEnabled(band_ok)
212 def update_marker_controls_visibility(self):
213 """
214 マーカーの欄は、マーカーを描かない組み込みの種類(Line・Area・Bar・Step)では隠す(プラグインの種類は使うかもしれないので出す)。
215 線だけの形(+ ×)には塗りが無いので塗りの欄を隠す。
216 値で色分けする種類は塗りを色の対応が受け持つので、塗りの「色を指定」を選べなくする(保存済みの値は変えない)。
217 """
218 app = self._app
219 dataset = app._get_current_dataset()
220 plot_type = dataset.plot_type if dataset is not None else app.ui.plot_type_combo.currentText()
221 marker = app.ui.marker_combo.currentData()
222 uses_markers = plot_type not in NON_MARKER_PLOT_TYPES
223 for widget in (app.ui.marker_label, app.ui.marker_combo, app.ui.makersize_label, app.ui.markersize_spinbox):
224 widget.setVisible(uses_markers)
225 has_markers = uses_markers and marker != NO_MARKER
226 mapped = plot_type in ('Density Scatter', COLOR_BY_COLUMN_PLOT_TYPE)
227 fillable = has_markers and not marker_is_unfilled(marker)
229 app.marker_fill_label.setVisible(fillable)
230 app.marker_fill_combo.setVisible(fillable)
231 color_item = app.marker_fill_combo.findData(MARKER_FILL_COLOR)
232 app.marker_fill_combo.model().item(color_item).setEnabled(not mapped)
233 show_face_color = fillable and not mapped and app.marker_fill_combo.currentData() == MARKER_FILL_COLOR
234 app.marker_face_color_label.setVisible(show_face_color)
235 app.marker_face_color_picker.setVisible(show_face_color)
237 for widget in (app.marker_edge_label, app.marker_edge_combo,
238 app.marker_edge_width_label, app.marker_edge_width_spinbox):
239 widget.setVisible(has_markers)
240 show_edge_color = has_markers and app.marker_edge_combo.currentData() == MARKER_EDGE_COLOR
241 app.marker_edge_color_label.setVisible(show_edge_color)
242 app.marker_edge_color_picker.setVisible(show_edge_color)
243 app._update_property_section_visibility()
245 def update_waterfall_controls_visibility(self):
246 """
247 オフセットなどの詳細はウォーターフォールを有効にしたときだけ出す。
248 2Dマップは描画でウォーターフォールの経路を通らないので、チェックボックスごと隠す。
249 """
250 dataset = self._app._get_current_dataset()
251 is_2d = dataset is not None and dataset.data_kind == '2d_grid'
252 if is_2d:
253 for widget in (
254 self._app.waterfall_checkbox,
255 self._app.waterfall_offset_mode_label, self._app.waterfall_offset_mode_combo,
256 self._app.waterfall_offset_x_label, self._app.waterfall_offset_x_spinbox,
257 self._app.waterfall_offset_y_label, self._app.waterfall_offset_y_spinbox,
258 self._app.waterfall_occlusion_checkbox, self._app.waterfall_depth_checkbox,
259 self._app.waterfall_depth_ratio_label, self._app.waterfall_depth_ratio_spinbox,
260 ):
261 widget.setVisible(False)
262 self._app._update_property_section_visibility()
263 return
265 self._app.waterfall_checkbox.setVisible(True)
266 show_offsets = self._app.waterfall_checkbox.isChecked()
268 self._app.waterfall_offset_mode_label.setVisible(show_offsets)
269 self._app.waterfall_offset_mode_combo.setVisible(show_offsets)
270 self._app.waterfall_offset_x_label.setVisible(show_offsets)
271 self._app.waterfall_offset_x_spinbox.setVisible(show_offsets)
272 self._app.waterfall_offset_y_label.setVisible(show_offsets)
273 self._app.waterfall_offset_y_spinbox.setVisible(show_offsets)
274 self._app.waterfall_occlusion_checkbox.setVisible(show_offsets)
275 self._app.waterfall_depth_checkbox.setVisible(show_offsets)
277 show_depth_ratio = show_offsets and self._app.waterfall_depth_checkbox.isChecked()
278 self._app.waterfall_depth_ratio_label.setVisible(show_depth_ratio)
279 self._app.waterfall_depth_ratio_spinbox.setVisible(show_depth_ratio)
280 self._app._update_property_section_visibility()
282 def update_ui_state(self):
283 """選択に合わせて欄の有効/無効と中身を更新する(何も選ばれていなければ空にする)。"""
284 self._app._update_subplot_combos()
286 self._shown_selection = self._current_selection()
287 canvas_area.refresh_status_bar(self._app)
288 # フォルダも選べるので、「何か選ばれているか」(削除用)と「データセットが選ばれているか」を分ける
289 current_dataset = self.shown_dataset()
290 selected_datasets = self._app._get_selected_datasets()
291 has_any_selection = bool(self._app.ui.dataset_list_widget.selectedItems())
292 has_dataset_selection = bool(selected_datasets)
294 # 節の開閉ボタンは選択が無くても押せるよう、入力欄だけを無効にする
295 self._app._set_dataset_property_fields_enabled(has_dataset_selection)
297 self._app.ui.remove_dataset_button.setEnabled(has_any_selection)
298 self._app.duplicate_dataset_button.setEnabled(has_dataset_selection)
299 self._app.view_edit_data_button.setEnabled(has_dataset_selection)
300 self._app.auto_color_button.setEnabled(has_dataset_selection)
302 # フィットの計算中は、選び直しても押せるようにしない(終わったときに戻す)
303 self._app.fit_curve_button.setEnabled(has_dataset_selection and self._app.fitting.fit_runner is None)
304 self._app.find_peaks_button.setEnabled(has_dataset_selection)
306 self._app.subplot_target_combo.setEnabled(has_dataset_selection)
307 self._app.use_secondary_y_checkbox.setEnabled(has_dataset_selection)
308 self._app.x_col_combo.setEnabled(has_dataset_selection)
309 self._app.y_col_combo.setEnabled(has_dataset_selection)
310 self._app.x_err_col_combo.setEnabled(has_dataset_selection)
311 self._app.y_err_col_combo.setEnabled(has_dataset_selection)
312 self._app.error_display_combo.setEnabled(has_dataset_selection)
313 self._app.nan_policy_combo.setEnabled(has_dataset_selection)
315 # current_dataset はカレントがフォルダのときや選択が無いときは None
316 if current_dataset is not None:
317 dataset = current_dataset
319 # 欄に値を入れる間は、変更の通知でデータセットを書き換えないようにする
320 self._binder.block_signals(True)
321 self._binder.restore(dataset, getattr)
322 is_range_auto = dataset.vmin is None and dataset.vmax is None
323 self._app.vmin_spinbox.setEnabled(not is_range_auto)
324 self._app.vmax_spinbox.setEnabled(not is_range_auto)
325 self._binder.block_signals(False)
326 property_widgets.refresh_property_widgets(self._app)
327 self.update_gradient_controls_visibility()
328 self.update_waterfall_controls_visibility()
329 self.update_smoothing_control_visibility()
330 self.update_error_display_control_items()
331 self.update_marker_controls_visibility()
332 self.update_2d_controls_visibility()
333 self.update_point_labels_limit_note()
335 self._app.x_col_combo.blockSignals(True)
336 self._app.y_col_combo.blockSignals(True)
338 all_columns = dataset.df.columns.tolist()
339 self._app.x_col_combo.clear()
340 self._app.y_col_combo.clear()
341 self._app.x_col_combo.addItems(all_columns)
342 self._app.y_col_combo.addItems(all_columns)
344 self._app.x_col_combo.setCurrentText(dataset.x_col_name)
345 self._app.y_col_combo.setCurrentText(dataset.y_col_name)
347 self._app.x_col_combo.blockSignals(False)
348 self._app.y_col_combo.blockSignals(False)
350 self._app.x_err_col_combo.blockSignals(True)
351 self._app.y_err_col_combo.blockSignals(True)
353 self._app.x_err_col_combo.clear()
354 self._app.y_err_col_combo.clear()
355 self._app.x_err_col_combo.addItems([NO_ERROR_COLUMN_LABEL] + all_columns)
356 self._app.y_err_col_combo.addItems([NO_ERROR_COLUMN_LABEL] + all_columns)
357 self._app.x_err_col_combo.setCurrentText(dataset.x_err_col_name or NO_ERROR_COLUMN_LABEL)
358 self._app.y_err_col_combo.setCurrentText(dataset.y_err_col_name or NO_ERROR_COLUMN_LABEL)
360 self._app.x_err_col_combo.blockSignals(False)
361 self._app.y_err_col_combo.blockSignals(False)
363 self._app.z_col_combo.blockSignals(True)
364 self._app.z_col_combo.clear()
365 self._app.z_col_combo.addItems(all_columns)
366 if dataset.z_col_name:
367 self._app.z_col_combo.setCurrentText(dataset.z_col_name)
368 self._app.z_col_combo.blockSignals(False)
370 self.update_stats_summary_label(dataset)
371 bottom_panel.refresh_bottom_panel(self._app, dataset)
373 else:
374 # 空にするのは表示だけ。信号を出すと、フォルダを今の項目にしたままデータセットを選んでいるとき
375 # 点ラベルの列が空の文字としてデータセットに書き込まれる
376 column_combos = (self._app.x_col_combo, self._app.y_col_combo, self._app.x_err_col_combo,
377 self._app.y_err_col_combo, self._app.point_label_col_combo, self._app.z_col_combo)
378 for combo in column_combos:
379 combo.blockSignals(True)
380 combo.clear()
381 combo.blockSignals(False)
382 self.update_2d_controls_visibility()
384 bottom_panel.refresh_bottom_panel(self._app, None)
386 self._app.gradient_checkbox.setVisible(False)
387 self._app.gradient_color2_label.setVisible(False)
388 self._app.gradient_color2_picker.setVisible(False)
389 self._app.gradient_target_label.setVisible(False)
390 self._app.gradient_target_combo.setVisible(False)
392 self._app.waterfall_checkbox.setVisible(False)
393 self._app.waterfall_offset_x_label.setVisible(False)
394 self._app.waterfall_offset_x_spinbox.setVisible(False)
395 self._app.waterfall_offset_y_label.setVisible(False)
396 self._app.waterfall_offset_y_spinbox.setVisible(False)
397 self._app.waterfall_occlusion_checkbox.setVisible(False)
398 self._app.waterfall_depth_checkbox.setVisible(False)
399 self._app.waterfall_depth_ratio_label.setVisible(False)
400 self._app.waterfall_depth_ratio_spinbox.setVisible(False)
402 self._app.stats_summary_label.setText("-")
403 self._set_dataset_page_stats(None)
404 # 前に出していたデータセットの名前を残さない(editingFinished は出ないので書き込まれない)
405 self._app.ui.legend_name_edit.clear()
406 # 行を隠したあとで見出しもそろえる(起動直後もこの経路を通る)
407 self._app._update_property_section_visibility()
409 def _set_dataset_page_stats(self, dataset):
410 # 左パネルの統計カード(タブの骨組みを作る前に呼ばれることがある)
411 page = getattr(self._app, 'dataset_page', None)
412 if page is not None: 412 ↛ exitline 412 didn't return from function '_set_dataset_page_stats' because the condition on line 412 was always true
413 page.set_stats(dataset)
415 def update_stats_summary_label(self, dataset):
416 """Y の件数・平均・標準偏差・最小・最大を、プロパティ欄の1行と左パネルの統計カードに出す(NaN は除く)。"""
417 self._set_dataset_page_stats(dataset)
418 try:
419 y = np.asarray(dataset.y_data, dtype=float)
420 valid = y[~np.isnan(y)]
421 if len(valid) == 0:
422 self._app.stats_summary_label.setText("-")
423 return
424 # 標本標準偏差(n−1)。1点では定義できないので「-」
425 std = sample_standard_deviation(valid)
426 std_text = "-" if np.isnan(std) else f"{std:.4g}"
427 self._app.stats_summary_label.setText(
428 f"件数: {len(valid)} 平均: {np.mean(valid):.4g} "
429 f"標準偏差: {std_text} 最小: {np.min(valid):.4g} 最大: {np.max(valid):.4g}"
430 )
431 except (TypeError, ValueError):
432 self._app.stats_summary_label.setText("-")
434 def on_plot_column_changed(self):
435 dataset = self._app._get_current_dataset()
436 if dataset is None:
437 return
439 new_x_col = self._app.x_col_combo.currentText()
440 new_y_col = self._app.y_col_combo.currentText()
442 # 変わった列だけを含める(欄が空のこともある)
443 old_values, new_values = {}, {}
444 if new_x_col and new_x_col in dataset.df.columns and new_x_col != dataset.x_col_name:
445 old_values['x_col_name'] = dataset.x_col_name
446 new_values['x_col_name'] = new_x_col
447 if new_y_col and new_y_col in dataset.df.columns and new_y_col != dataset.y_col_name:
448 old_values['y_col_name'] = dataset.y_col_name
449 new_values['y_col_name'] = new_y_col
451 self._app._push_dataset_property_command(dataset, old_values, new_values, description="プロット列の変更")
453 # 描画先の軸ラベルが空なら列名から埋める。軸の設定は Undo の仕組みが別なので Undo には含めない。
454 if 'x_col_name' in new_values:
455 self.maybe_autofill_axis_label(dataset.subplot_target, 'x_label', new_values['x_col_name'])
456 if 'y_col_name' in new_values:
457 self.maybe_autofill_axis_label(dataset.subplot_target, 'y_label', new_values['y_col_name'])
459 def maybe_autofill_axis_label(self, subplot_index, label_key, column_name):
460 """
461 その軸のラベルが空のときだけ、列名から推し量ったラベルで埋める。
462 今表示している軸なら欄に書き込み(保存と再描画は欄の変更の経路に任せる)、そうでなければ設定を直接書き換える
463 (表示していない軸のラベルを、表示中の軸の欄に書いてしまわないように)。
464 """
465 if subplot_index is None or not (0 <= subplot_index < len(self._app.project.all_plot_settings)): 465 ↛ 466line 465 didn't jump to line 466 because the condition on line 465 was never true
466 return
467 inferred = infer_axis_label_from_column_name(column_name)
468 if not inferred:
469 return
470 current_settings = self._app.project.all_plot_settings[subplot_index]
471 if current_settings.get(label_key):
472 return
474 if subplot_index == self._app.project.active_axis_index:
475 label_edit = self._app.ui.x_label_text_edit if label_key == 'x_label' else self._app.ui.y_label_text_edit
476 if not label_edit.text(): 476 ↛ exitline 476 didn't return from function 'maybe_autofill_axis_label' because the condition on line 476 was always true
477 label_edit.setText(inferred)
478 else:
479 current_settings[label_key] = inferred
480 self._app._update_plot()
482 def on_data_2d_toggled(self, checked):
483 """2Dマップとして扱うかを切り替える。Z 列が未設定なら X/Y 以外の最初の列を選んでおく。"""
484 dataset = self._app._get_current_dataset()
485 if dataset is None: 485 ↛ 486line 485 didn't jump to line 486 because the condition on line 485 was never true
486 return
487 new_data_kind = '2d_grid' if checked else '1d'
488 if dataset.data_kind == new_data_kind: 488 ↛ 489line 488 didn't jump to line 489 because the condition on line 488 was never true
489 self.update_2d_controls_visibility()
490 return
492 old_values = {'data_kind': dataset.data_kind}
493 new_values = {'data_kind': new_data_kind}
494 if new_data_kind == '2d_grid' and not dataset.z_col_name:
495 candidates = [
496 c for c in dataset.df.columns
497 if c not in (dataset.x_col_name, dataset.y_col_name)
498 ]
499 if candidates: 499 ↛ 503line 499 didn't jump to line 503 because the condition on line 499 was always true
500 old_values['z_col_name'] = dataset.z_col_name
501 new_values['z_col_name'] = candidates[0]
503 self._app._push_dataset_property_command(dataset, old_values, new_values, description="2Dグリッドデータの切り替え")
504 self.update_2d_controls_visibility()
506 def on_z_column_changed(self):
507 dataset = self._app._get_current_dataset()
508 if dataset is None:
509 return
510 new_z_col = self._app.z_col_combo.currentText()
511 if not new_z_col or new_z_col not in dataset.df.columns or new_z_col == dataset.z_col_name: 511 ↛ 512line 511 didn't jump to line 512 because the condition on line 511 was never true
512 return
513 self._app._push_dataset_property_command(
514 dataset, {'z_col_name': dataset.z_col_name}, {'z_col_name': new_z_col},
515 description="Z軸列の変更"
516 )
518 def on_2d_value_range_changed(self):
519 """値域を自動にしている間は vmin/vmax を None にする(描画時に実データの範囲を使う)。"""
520 dataset = self._app._get_current_dataset()
521 if dataset is None:
522 return
523 is_auto = self._app.color_range_auto_checkbox.isChecked()
524 self._app.vmin_spinbox.setEnabled(not is_auto)
525 self._app.vmax_spinbox.setEnabled(not is_auto)
527 new_vmin = None if is_auto else self._app.vmin_spinbox.value()
528 new_vmax = None if is_auto else self._app.vmax_spinbox.value()
529 if new_vmin == dataset.vmin and new_vmax == dataset.vmax: 529 ↛ 530line 529 didn't jump to line 530 because the condition on line 529 was never true
530 return
531 self._app._push_dataset_property_command(
532 dataset,
533 {'vmin': dataset.vmin, 'vmax': dataset.vmax},
534 {'vmin': new_vmin, 'vmax': new_vmax},
535 description="値域の変更"
536 )
538 def update_2d_controls_visibility(self):
539 """
540 Z 列・カラーマップ・値域は、2Dマップと、3列目の値で点を色分けする散布図の両方で使う。
541 表示方式・等高線・補間方法は2Dマップだけ。
542 """
543 dataset = self._app._get_current_dataset()
544 # 選択が無いときは隠す(2Dのチェックには前に選んでいたデータセットの値が残っている)
545 is_2d = dataset is not None and dataset.data_kind == '2d_grid'
546 is_color_by_column = (
547 dataset is not None
548 and dataset.data_kind != '2d_grid'
549 and dataset.plot_type == COLOR_BY_COLUMN_PLOT_TYPE
550 )
551 shared_with_color_scatter = is_2d or is_color_by_column
552 for widget in (
553 self._app.z_col_label, self._app.z_col_combo,
554 self._app.colormap_label, self._app.colormap_combo,
555 self._app.color_range_auto_checkbox,
556 self._app.vmin_label, self._app.vmin_spinbox,
557 self._app.vmax_label, self._app.vmax_spinbox,
558 ):
559 widget.setVisible(shared_with_color_scatter)
561 for widget in (
562 self._app.map_display_mode_label, self._app.map_display_mode_combo,
563 self._app.contour_levels_label, self._app.contour_levels_spinbox,
564 self._app.grid_interp_method_label, self._app.grid_interp_method_combo,
565 ):
566 widget.setVisible(is_2d)
568 self._app._update_property_section_visibility()
570 def on_error_column_changed(self):
571 dataset = self._app._get_current_dataset()
572 if dataset is None:
573 return
575 new_x_err = self._app.x_err_col_combo.currentText()
576 new_y_err = self._app.y_err_col_combo.currentText()
577 new_x_err_col = None if (not new_x_err or new_x_err == NO_ERROR_COLUMN_LABEL) else new_x_err
578 new_y_err_col = None if (not new_y_err or new_y_err == NO_ERROR_COLUMN_LABEL) else new_y_err
580 old_values, new_values = {}, {}
581 if new_x_err_col != dataset.x_err_col_name: 581 ↛ 582line 581 didn't jump to line 582 because the condition on line 581 was never true
582 old_values['x_err_col_name'] = dataset.x_err_col_name
583 new_values['x_err_col_name'] = new_x_err_col
584 if new_y_err_col != dataset.y_err_col_name: 584 ↛ 588line 584 didn't jump to line 588 because the condition on line 584 was always true
585 old_values['y_err_col_name'] = dataset.y_err_col_name
586 new_values['y_err_col_name'] = new_y_err_col
588 self._app._push_dataset_property_command(dataset, old_values, new_values, description="誤差列(エラーバー)の変更")
590 def on_data_structure_changed(self):
591 """データ(データエディタの編集・行のマスク)が変わったら、列の選択肢と統計値と描画を作り直す。
593 エディタは一覧の選択と関係なく開いたままなので、何も選んでいなくても描き直す。
594 """
595 self.update_ui_state()
596 self._app._update_plot()
598 def on_secondary_y_changed(self):
599 selected_datasets = self._app._get_selected_datasets()
600 if not selected_datasets:
601 return
603 new_value = self._app.use_secondary_y_checkbox.isChecked()
605 is_batch = len(selected_datasets) > 1
606 if is_batch:
607 self._app.undo_stack.beginMacro(f"第2Y軸使用の一括変更 ({len(selected_datasets)}件)")
608 for dataset in selected_datasets:
609 self._app._push_dataset_property_command(
610 dataset,
611 {'use_secondary_y': dataset.use_secondary_y},
612 {'use_secondary_y': new_value},
613 description="第2Y軸使用の変更"
614 )
615 if is_batch:
616 self._app.undo_stack.endMacro()