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

1"""データセットのプロパティ欄: 選んだデータセットの値を欄に映し、欄の変更をデータセットに当てる。""" 

2import logging 

3import numpy as np 

4 

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 

14 

15logger = logging.getLogger(__name__) 

16 

17 

18class DatasetPropertyPanel: 

19 """プロパティ欄のウィジェットはタブ(PlotterApp)が持つので、タブを受け取って読み書きする。""" 

20 

21 def __init__(self, app): 

22 self._app = app 

23 self._binder = Binder(app, DATASET_PROPERTY_BINDINGS) 

24 # 最後に欄へ反映した (今の項目, 選んでいる項目)。今の項目と選択の通知が両方来ても1回だけ反映する 

25 self._shown_selection = None 

26 

27 def connect_signals(self): 

28 """表(gui/dataset_bindings.py)の欄の変更をつなぐ。""" 

29 self._binder.connect(watch=self.watch) 

30 

31 def watch(self, signal, widget): 

32 """widget が変わったら、その属性だけを選択中のデータセットに当てる。""" 

33 signal.connect(lambda *_: self.on_property_changed(widget)) 

34 

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 

41 

42 self._app._push_dataset_property_command( 

43 dataset, 

44 {'subplot_target': dataset.subplot_target}, 

45 {'subplot_target': index}, 

46 description="描画先プロットの変更" 

47 ) 

48 

49 def on_dataset_selected(self, current_item=None, previous_item=None): 

50 """今の項目が変わったときと、選択が変わったときの両方から呼ぶ。 

51 

52 ボタンの有効/無効は選択で決まるが、削除の後などは今の項目だけがあって選ばれていない。 

53 その項目をクリックしても今の項目は変わらず currentItemChanged が出ないので、選択の通知でも決め直す。 

54 """ 

55 if self._selection_unchanged(): 

56 return 

57 self.update_ui_state() 

58 self._app._notify_plugins_selection_changed() 

59 

60 def shown_dataset(self): 

61 """プロパティに出すデータセット。今の項目が選ばれていればそれ、違えば選んでいる最初のデータセット、無ければ None。 

62 

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 

73 

74 def _current_selection(self): 

75 tree = self._app.ui.dataset_list_widget 

76 return tree.currentItem(), tuple(tree.selectedItems()) 

77 

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))) 

84 

85 def on_legend_name_changed(self): 

86 dataset = self.shown_dataset() 

87 if dataset is None: 

88 return 

89 

90 new_name = self._app.ui.legend_name_edit.text() 

91 

92 self._app._push_dataset_property_command( 

93 dataset, 

94 {'name': dataset.name}, 

95 {'name': new_name}, 

96 description="凡例名の変更" 

97 ) 

98 

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() 

106 

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) 

125 

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 

134 

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) 

139 

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() 

150 

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() 

162 

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') 

171 

172 self._app.gradient_checkbox.setVisible(supports_gradient) 

173 

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) 

177 

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() 

182 

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() 

197 

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) 

211 

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) 

228 

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) 

236 

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() 

244 

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 

264 

265 self._app.waterfall_checkbox.setVisible(True) 

266 show_offsets = self._app.waterfall_checkbox.isChecked() 

267 

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) 

276 

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() 

281 

282 def update_ui_state(self): 

283 """選択に合わせて欄の有効/無効と中身を更新する(何も選ばれていなければ空にする)。""" 

284 self._app._update_subplot_combos() 

285 

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) 

293 

294 # 節の開閉ボタンは選択が無くても押せるよう、入力欄だけを無効にする 

295 self._app._set_dataset_property_fields_enabled(has_dataset_selection) 

296 

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) 

301 

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) 

305 

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) 

314 

315 # current_dataset はカレントがフォルダのときや選択が無いときは None 

316 if current_dataset is not None: 

317 dataset = current_dataset 

318 

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() 

334 

335 self._app.x_col_combo.blockSignals(True) 

336 self._app.y_col_combo.blockSignals(True) 

337 

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) 

343 

344 self._app.x_col_combo.setCurrentText(dataset.x_col_name) 

345 self._app.y_col_combo.setCurrentText(dataset.y_col_name) 

346 

347 self._app.x_col_combo.blockSignals(False) 

348 self._app.y_col_combo.blockSignals(False) 

349 

350 self._app.x_err_col_combo.blockSignals(True) 

351 self._app.y_err_col_combo.blockSignals(True) 

352 

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) 

359 

360 self._app.x_err_col_combo.blockSignals(False) 

361 self._app.y_err_col_combo.blockSignals(False) 

362 

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) 

369 

370 self.update_stats_summary_label(dataset) 

371 bottom_panel.refresh_bottom_panel(self._app, dataset) 

372 

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() 

383 

384 bottom_panel.refresh_bottom_panel(self._app, None) 

385 

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) 

391 

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) 

401 

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() 

408 

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) 

414 

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("-") 

433 

434 def on_plot_column_changed(self): 

435 dataset = self._app._get_current_dataset() 

436 if dataset is None: 

437 return 

438 

439 new_x_col = self._app.x_col_combo.currentText() 

440 new_y_col = self._app.y_col_combo.currentText() 

441 

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 

450 

451 self._app._push_dataset_property_command(dataset, old_values, new_values, description="プロット列の変更") 

452 

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']) 

458 

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 

473 

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() 

481 

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 

491 

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] 

502 

503 self._app._push_dataset_property_command(dataset, old_values, new_values, description="2Dグリッドデータの切り替え") 

504 self.update_2d_controls_visibility() 

505 

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 ) 

517 

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) 

526 

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 ) 

537 

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) 

560 

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) 

567 

568 self._app._update_property_section_visibility() 

569 

570 def on_error_column_changed(self): 

571 dataset = self._app._get_current_dataset() 

572 if dataset is None: 

573 return 

574 

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 

579 

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 

587 

588 self._app._push_dataset_property_command(dataset, old_values, new_values, description="誤差列(エラーバー)の変更") 

589 

590 def on_data_structure_changed(self): 

591 """データ(データエディタの編集・行のマスク)が変わったら、列の選択肢と統計値と描画を作り直す。 

592 

593 エディタは一覧の選択と関係なく開いたままなので、何も選んでいなくても描き直す。 

594 """ 

595 self.update_ui_state() 

596 self._app._update_plot() 

597 

598 def on_secondary_y_changed(self): 

599 selected_datasets = self._app._get_selected_datasets() 

600 if not selected_datasets: 

601 return 

602 

603 new_value = self._app.use_secondary_y_checkbox.isChecked() 

604 

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()