graphica-plot 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. graphica/Graphica.ico +0 -0
  2. graphica/__init__.py +1 -0
  3. graphica/__main__.py +111 -0
  4. graphica/assets/__init__.py +0 -0
  5. graphica/assets/icons/__init__.py +0 -0
  6. graphica/assets/icons/arrow-right.svg +21 -0
  7. graphica/assets/icons/bold.svg +20 -0
  8. graphica/assets/icons/calculator.svg +26 -0
  9. graphica/assets/icons/chart-histogram.svg +24 -0
  10. graphica/assets/icons/chart-line.svg +20 -0
  11. graphica/assets/icons/chevron-down.svg +19 -0
  12. graphica/assets/icons/chevron-right.svg +19 -0
  13. graphica/assets/icons/color-swatch.svg +22 -0
  14. graphica/assets/icons/column-insert-right.svg +21 -0
  15. graphica/assets/icons/column-remove.svg +21 -0
  16. graphica/assets/icons/copy.svg +20 -0
  17. graphica/assets/icons/download.svg +21 -0
  18. graphica/assets/icons/edit.svg +21 -0
  19. graphica/assets/icons/eye-off.svg +21 -0
  20. graphica/assets/icons/eye.svg +20 -0
  21. graphica/assets/icons/file-plus.svg +22 -0
  22. graphica/assets/icons/folder-plus.svg +21 -0
  23. graphica/assets/icons/folder.svg +19 -0
  24. graphica/assets/icons/highlight.svg +22 -0
  25. graphica/assets/icons/history.svg +20 -0
  26. graphica/assets/icons/italic.svg +21 -0
  27. graphica/assets/icons/layout-grid.svg +22 -0
  28. graphica/assets/icons/math-function.svg +22 -0
  29. graphica/assets/icons/message-2.svg +21 -0
  30. graphica/assets/icons/mountain.svg +20 -0
  31. graphica/assets/icons/palette.svg +22 -0
  32. graphica/assets/icons/pointer.svg +19 -0
  33. graphica/assets/icons/refresh.svg +20 -0
  34. graphica/assets/icons/row-insert-bottom.svg +21 -0
  35. graphica/assets/icons/row-remove.svg +21 -0
  36. graphica/assets/icons/search.svg +20 -0
  37. graphica/assets/icons/select-all.svg +35 -0
  38. graphica/assets/icons/subscript.svg +20 -0
  39. graphica/assets/icons/superscript.svg +20 -0
  40. graphica/assets/icons/table.svg +21 -0
  41. graphica/assets/icons/trash.svg +23 -0
  42. graphica/assets/icons/typography.svg +23 -0
  43. graphica/assets/icons/x.svg +20 -0
  44. graphica/core/__init__.py +0 -0
  45. graphica/core/analysis.py +1278 -0
  46. graphica/core/app_paths.py +35 -0
  47. graphica/core/axis_settings.py +128 -0
  48. graphica/core/caption_export.py +47 -0
  49. graphica/core/color_palettes.py +56 -0
  50. graphica/core/commands.py +226 -0
  51. graphica/core/cvd_simulation.py +49 -0
  52. graphica/core/dataset.py +443 -0
  53. graphica/core/diagnostics.py +105 -0
  54. graphica/core/excel_utils.py +58 -0
  55. graphica/core/fit_models.py +379 -0
  56. graphica/core/grid_data.py +128 -0
  57. graphica/core/i18n.py +44 -0
  58. graphica/core/json_utils.py +27 -0
  59. graphica/core/label_utils.py +21 -0
  60. graphica/core/methods_text.py +115 -0
  61. graphica/core/named_colors.py +126 -0
  62. graphica/core/plugin_api.py +515 -0
  63. graphica/core/plugin_context.py +167 -0
  64. graphica/core/plugin_install.py +82 -0
  65. graphica/core/plugin_manifest.py +67 -0
  66. graphica/core/plugin_testing.py +213 -0
  67. graphica/core/plugin_types.py +118 -0
  68. graphica/core/provenance.py +19 -0
  69. graphica/core/report_export.py +51 -0
  70. graphica/core/safe_eval.py +194 -0
  71. graphica/core/script_export.py +272 -0
  72. graphica/core/translations_en.py +530 -0
  73. graphica/core/unit_conversion.py +51 -0
  74. graphica/core/update_check.py +43 -0
  75. graphica/core/version.py +14 -0
  76. graphica/gui/__init__.py +0 -0
  77. graphica/gui/app_settings.py +119 -0
  78. graphica/gui/axis_bindings.py +173 -0
  79. graphica/gui/binding.py +135 -0
  80. graphica/gui/builders/__init__.py +1 -0
  81. graphica/gui/builders/axis_panel.py +431 -0
  82. graphica/gui/builders/canvas_area.py +176 -0
  83. graphica/gui/builders/common.py +120 -0
  84. graphica/gui/builders/dataset_panel.py +433 -0
  85. graphica/gui/builders/property_sections.py +208 -0
  86. graphica/gui/canvas.py +519 -0
  87. graphica/gui/color_history.py +47 -0
  88. graphica/gui/color_picker_widget.py +198 -0
  89. graphica/gui/crash_handler.py +77 -0
  90. graphica/gui/cvd_preview.py +23 -0
  91. graphica/gui/data_editor.py +761 -0
  92. graphica/gui/data_import_flow.py +355 -0
  93. graphica/gui/dataset_bindings.py +107 -0
  94. graphica/gui/dataset_style_icon.py +80 -0
  95. graphica/gui/datasets/__init__.py +1 -0
  96. graphica/gui/datasets/actions_menu.py +114 -0
  97. graphica/gui/datasets/colors.py +218 -0
  98. graphica/gui/datasets/fitting.py +70 -0
  99. graphica/gui/datasets/host.py +162 -0
  100. graphica/gui/datasets/operations/__init__.py +1 -0
  101. graphica/gui/datasets/operations/fitting.py +468 -0
  102. graphica/gui/datasets/operations/peaks.py +109 -0
  103. graphica/gui/datasets/operations/processing.py +659 -0
  104. graphica/gui/datasets/operations/runner.py +148 -0
  105. graphica/gui/datasets/operations/transfer.py +227 -0
  106. graphica/gui/datasets/order.py +310 -0
  107. graphica/gui/datasets/overlays.py +74 -0
  108. graphica/gui/datasets/peaks.py +21 -0
  109. graphica/gui/datasets/plugin_runs.py +99 -0
  110. graphica/gui/datasets/processing.py +68 -0
  111. graphica/gui/datasets/property_panel.py +538 -0
  112. graphica/gui/datasets/transfer.py +36 -0
  113. graphica/gui/detached_canvas_window.py +22 -0
  114. graphica/gui/dialogs/__init__.py +137 -0
  115. graphica/gui/dialogs/analysis.py +1322 -0
  116. graphica/gui/dialogs/app.py +1015 -0
  117. graphica/gui/dialogs/appearance.py +851 -0
  118. graphica/gui/dialogs/data_edit.py +577 -0
  119. graphica/gui/dialogs/data_import.py +679 -0
  120. graphica/gui/dialogs/export.py +433 -0
  121. graphica/gui/dock_layout.py +176 -0
  122. graphica/gui/export_preview_panel.py +321 -0
  123. graphica/gui/export_settings.py +16 -0
  124. graphica/gui/file_association.py +97 -0
  125. graphica/gui/icon_utils.py +44 -0
  126. graphica/gui/main_app_window.py +210 -0
  127. graphica/gui/main_window.py +1151 -0
  128. graphica/gui/mathtext_preview.py +168 -0
  129. graphica/gui/menu_bar.py +320 -0
  130. graphica/gui/minimap_widget.py +151 -0
  131. graphica/gui/mixins/__init__.py +0 -0
  132. graphica/gui/mixins/export_mixin.py +520 -0
  133. graphica/gui/mixins/help_mixin.py +127 -0
  134. graphica/gui/mixins/project_io_mixin.py +289 -0
  135. graphica/gui/mixins/quick_access_mixin.py +194 -0
  136. graphica/gui/mixins/ui_setup_mixin.py +206 -0
  137. graphica/gui/notify.py +61 -0
  138. graphica/gui/panels/__init__.py +43 -0
  139. graphica/gui/panels/axis_settings.py +605 -0
  140. graphica/gui/panels/dataset_tree.py +424 -0
  141. graphica/gui/plot_type_drawers.py +131 -0
  142. graphica/gui/plugin_context.py +119 -0
  143. graphica/gui/project_files.py +361 -0
  144. graphica/gui/provenance_panel.py +65 -0
  145. graphica/gui/rendering/__init__.py +1 -0
  146. graphica/gui/rendering/annotations.py +177 -0
  147. graphica/gui/rendering/appearance.py +368 -0
  148. graphica/gui/rendering/common.py +316 -0
  149. graphica/gui/rendering/data_1d.py +376 -0
  150. graphica/gui/rendering/data_2d.py +66 -0
  151. graphica/gui/residual_panel.py +62 -0
  152. graphica/gui/resources.py +16 -0
  153. graphica/gui/single_instance.py +141 -0
  154. graphica/gui/splash.py +83 -0
  155. graphica/gui/task_runner.py +41 -0
  156. graphica/gui/theme.py +864 -0
  157. graphica/gui/tools/__init__.py +56 -0
  158. graphica/gui/tools/annotation.py +275 -0
  159. graphica/gui/tools/cursor.py +259 -0
  160. graphica/gui/tools/layout_edit.py +285 -0
  161. graphica/gui/tools/manager.py +78 -0
  162. graphica/gui/tools/peak_placement.py +132 -0
  163. graphica/gui/tools/range_select.py +191 -0
  164. graphica/gui/tools/region_highlight.py +222 -0
  165. graphica/gui/tools/slice_extraction.py +200 -0
  166. graphica/gui/widget_translation.py +46 -0
  167. graphica/gui/workers.py +280 -0
  168. graphica/models/__init__.py +0 -0
  169. graphica/models/project.py +197 -0
  170. graphica/plugin/__init__.py +20 -0
  171. graphica/plugin/testing.py +57 -0
  172. graphica/sample_data/__init__.py +0 -0
  173. graphica/sample_data/cooling_curve_sample.csv +42 -0
  174. graphica/ui_main_window.py +581 -0
  175. graphica_plot-2.0.0.dist-info/METADATA +317 -0
  176. graphica_plot-2.0.0.dist-info/RECORD +181 -0
  177. graphica_plot-2.0.0.dist-info/WHEEL +5 -0
  178. graphica_plot-2.0.0.dist-info/entry_points.txt +2 -0
  179. graphica_plot-2.0.0.dist-info/licenses/LICENSE +30 -0
  180. graphica_plot-2.0.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +91 -0
  181. graphica_plot-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,538 @@
1
+ """データセットのプロパティ欄: 選んだデータセットの値を欄に映し、欄の変更をデータセットに当てる。"""
2
+ import logging
3
+ import numpy as np
4
+
5
+ from graphica.core.analysis import sample_standard_deviation
6
+ from graphica.core.dataset import COLOR_BY_COLUMN_PLOT_TYPE
7
+ from graphica.core.label_utils import infer_axis_label_from_column_name
8
+ from graphica.gui.binding import Binder
9
+ from graphica.gui.dataset_bindings import DATASET_PROPERTY_BINDINGS, NO_ERROR_COLUMN_LABEL
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class DatasetPropertyPanel:
15
+ """プロパティ欄のウィジェットはタブ(PlotterApp)が持つので、タブを受け取って読み書きする。"""
16
+
17
+ def __init__(self, app):
18
+ self._app = app
19
+ self._binder = Binder(app, DATASET_PROPERTY_BINDINGS)
20
+
21
+ def connect_signals(self):
22
+ """表(gui/dataset_bindings.py)の欄の変更をつなぐ。"""
23
+ self._binder.connect(watch=self.watch)
24
+
25
+ def watch(self, signal, widget):
26
+ """widget が変わったら、その属性だけを選択中のデータセットに当てる。"""
27
+ signal.connect(lambda *_: self.on_property_changed(widget))
28
+
29
+ def on_subplot_target_changed(self, index):
30
+ dataset = self._app._get_current_dataset()
31
+ if dataset is None or index == -1:
32
+ return
33
+ if dataset.subplot_target == index:
34
+ return
35
+
36
+ self._app._push_dataset_property_command(
37
+ dataset,
38
+ {'subplot_target': dataset.subplot_target},
39
+ {'subplot_target': index},
40
+ description="描画先プロットの変更"
41
+ )
42
+
43
+ def on_dataset_selected(self, current_item, previous_item):
44
+ self.update_ui_state()
45
+ self._app._notify_plugins_selection_changed()
46
+
47
+ def on_legend_name_changed(self):
48
+ dataset = self._app._get_current_dataset()
49
+ if dataset is None:
50
+ return
51
+
52
+ new_name = self._app.ui.legend_name_edit.text()
53
+
54
+ self._app._push_dataset_property_command(
55
+ dataset,
56
+ {'name': dataset.name},
57
+ {'name': new_name},
58
+ description="凡例名の変更"
59
+ )
60
+
61
+ def on_point_labels_toggled(self, checked):
62
+ """
63
+ 点数が上限を超えるデータセットには、ラベルを描かずに理由を欄に出す(update_point_labels_limit_note)。
64
+ ラベル1件の描画に約2.5msかかり、上限を超えて描かせると再描画のたびに GUI が長く止まるため。
65
+ """
66
+ self.on_property_changed(self._app.point_labels_checkbox)
67
+ self.update_point_labels_limit_note()
68
+
69
+ def update_point_labels_limit_note(self):
70
+ """ラベル表示が有効なのに点数が上限を超えるデータセットがあれば、その理由をチェックボックスの下に出す。"""
71
+ note = getattr(self._app, 'point_labels_limit_note', None)
72
+ if note is None:
73
+ return
74
+ max_points = self._app.canvas.point_label_max_points
75
+ over_limit = [ds for ds in self._app._get_selected_datasets()
76
+ if ds.show_point_labels and len(ds.visible_df) > max_points]
77
+ if not over_limit:
78
+ note.setVisible(False)
79
+ return
80
+ largest = max(len(ds.visible_df) for ds in over_limit)
81
+ note.setText(
82
+ f"点数({largest:,}件)が表示上限({max_points:,}件)を超えているため、"
83
+ "ラベルは表示されません。上限は「環境設定」で変更できますが、"
84
+ "点数が多いと描画に時間がかかります(1,000件で約2秒)。"
85
+ )
86
+ note.setVisible(True)
87
+
88
+ def on_property_changed(self, widget=None):
89
+ """
90
+ widget が表す属性だけを、選択中のデータセットすべてに当てる。
91
+ 全属性をまとめて当てると、複数選択のとき触っていない属性まで1つの値に揃ってしまう。
92
+ """
93
+ selected_datasets = self._app._get_selected_datasets()
94
+ if not selected_datasets:
95
+ return
96
+
97
+ binding = self._binder.binding_for(widget)
98
+ if binding is None:
99
+ return
100
+ attr_name, new_value = binding.key, self._binder.read(binding)
101
+
102
+ is_batch = len(selected_datasets) > 1
103
+ if is_batch:
104
+ self._app.undo_stack.beginMacro(f"プロパティの一括変更 ({len(selected_datasets)}件)")
105
+ for dataset in selected_datasets:
106
+ self._app._push_dataset_property_command(
107
+ dataset, {attr_name: getattr(dataset, attr_name)}, {attr_name: new_value},
108
+ description="プロパティ変更"
109
+ )
110
+ if is_batch:
111
+ self._app.undo_stack.endMacro()
112
+
113
+ def update_gradient_controls_visibility(self):
114
+ """
115
+ グラデーションは線か塗りのある種別('Line'/'Line+Scatter'/'Area')でだけ出す。
116
+ 対象(線/塗り/両方)を選べるのは 'Area' だけなので、その欄は 'Area' のときだけ出す。
117
+ """
118
+ dataset = self._app._get_current_dataset()
119
+ plot_type = dataset.plot_type if dataset is not None else self._app.ui.plot_type_combo.currentText()
120
+ supports_gradient = plot_type in ('Line', 'Line+Scatter', 'Area')
121
+
122
+ self._app.gradient_checkbox.setVisible(supports_gradient)
123
+
124
+ show_detail = supports_gradient and self._app.gradient_checkbox.isChecked()
125
+ self._app.gradient_color2_label.setVisible(show_detail)
126
+ self._app.gradient_color2_picker.setVisible(show_detail)
127
+
128
+ show_target_combo = show_detail and plot_type == 'Area'
129
+ self._app.gradient_target_label.setVisible(show_target_combo)
130
+ self._app.gradient_target_combo.setVisible(show_target_combo)
131
+ self._app._update_property_section_visibility()
132
+
133
+ def update_smoothing_control_visibility(self):
134
+ """
135
+ 平滑化は線で結ぶ種別('Line'/'Line+Scatter')でだけ出す。ほかの種別では平滑化した線が
136
+ マーカー・棒・塗りを置き換えてしまう(描画側も同じ条件で確かめている)。
137
+ """
138
+ dataset = self._app._get_current_dataset()
139
+ plot_type = dataset.plot_type if dataset is not None else self._app.ui.plot_type_combo.currentText()
140
+ is_smoothable_type = plot_type in ('Line', 'Line+Scatter')
141
+ self._app.ui.smoothing_checkbox.setVisible(is_smoothable_type)
142
+ # 手法の欄はチェックが外れている間も隠さず無効にする(出たり消えたりで配置が揺れないように)
143
+ self._app.smoothing_method_label.setVisible(is_smoothable_type)
144
+ self._app.smoothing_method_combo.setVisible(is_smoothable_type)
145
+ self._app.smoothing_method_combo.setEnabled(self._app.ui.smoothing_checkbox.isChecked())
146
+ self._app._update_property_section_visibility()
147
+
148
+ def update_error_display_control_items(self):
149
+ """
150
+ 棒と面では誤差の帯が合わない(面は塗りと重なる)ので、「誤差バンド」「両方」を選べなくする。
151
+ 保存済みの値は変えない。
152
+ """
153
+ dataset = self._app._get_current_dataset()
154
+ plot_type = dataset.plot_type if dataset is not None else self._app.ui.plot_type_combo.currentText()
155
+ band_ok = plot_type not in ('Bar', 'Area')
156
+ model = self._app.error_display_combo.model()
157
+ for value in ('band', 'both'):
158
+ index = self._app.error_display_combo.findData(value)
159
+ if index != -1:
160
+ model.item(index).setEnabled(band_ok)
161
+
162
+ def update_waterfall_controls_visibility(self):
163
+ """
164
+ オフセットなどの詳細はウォーターフォールを有効にしたときだけ出す。
165
+ 2Dマップは描画でウォーターフォールの経路を通らないので、チェックボックスごと隠す。
166
+ """
167
+ dataset = self._app._get_current_dataset()
168
+ is_2d = dataset is not None and dataset.data_kind == '2d_grid'
169
+ if is_2d:
170
+ for widget in (
171
+ self._app.waterfall_checkbox,
172
+ self._app.waterfall_offset_x_label, self._app.waterfall_offset_x_spinbox,
173
+ self._app.waterfall_offset_y_label, self._app.waterfall_offset_y_spinbox,
174
+ self._app.waterfall_occlusion_checkbox, self._app.waterfall_depth_checkbox,
175
+ self._app.waterfall_depth_ratio_label, self._app.waterfall_depth_ratio_spinbox,
176
+ ):
177
+ widget.setVisible(False)
178
+ self._app._update_property_section_visibility()
179
+ return
180
+
181
+ self._app.waterfall_checkbox.setVisible(True)
182
+ show_offsets = self._app.waterfall_checkbox.isChecked()
183
+
184
+ self._app.waterfall_offset_x_label.setVisible(show_offsets)
185
+ self._app.waterfall_offset_x_spinbox.setVisible(show_offsets)
186
+ self._app.waterfall_offset_y_label.setVisible(show_offsets)
187
+ self._app.waterfall_offset_y_spinbox.setVisible(show_offsets)
188
+ self._app.waterfall_occlusion_checkbox.setVisible(show_offsets)
189
+ self._app.waterfall_depth_checkbox.setVisible(show_offsets)
190
+
191
+ show_depth_ratio = show_offsets and self._app.waterfall_depth_checkbox.isChecked()
192
+ self._app.waterfall_depth_ratio_label.setVisible(show_depth_ratio)
193
+ self._app.waterfall_depth_ratio_spinbox.setVisible(show_depth_ratio)
194
+ self._app._update_property_section_visibility()
195
+
196
+ def update_ui_state(self):
197
+ """選択に合わせて欄の有効/無効と中身を更新する(何も選ばれていなければ空にする)。"""
198
+ self._app._update_subplot_combos()
199
+
200
+ # フォルダも選べるので、「何か選ばれているか」(削除用)と「データセットが選ばれているか」を分ける
201
+ current_dataset = self._app._get_current_dataset()
202
+ selected_datasets = self._app._get_selected_datasets()
203
+ has_any_selection = bool(self._app.ui.dataset_list_widget.selectedItems())
204
+ has_dataset_selection = bool(selected_datasets)
205
+
206
+ # 節の開閉ボタンは選択が無くても押せるよう、入力欄だけを無効にする
207
+ self._app._set_dataset_property_fields_enabled(has_dataset_selection)
208
+
209
+ self._app.ui.remove_dataset_button.setEnabled(has_any_selection)
210
+ self._app.duplicate_dataset_button.setEnabled(has_dataset_selection)
211
+ self._app.view_edit_data_button.setEnabled(has_dataset_selection)
212
+ self._app.auto_color_button.setEnabled(has_dataset_selection)
213
+
214
+ self._app.fit_curve_button.setEnabled(has_dataset_selection)
215
+ self._app.find_peaks_button.setEnabled(has_dataset_selection)
216
+
217
+ self._app.subplot_target_combo.setEnabled(has_dataset_selection)
218
+ self._app.use_secondary_y_checkbox.setEnabled(has_dataset_selection)
219
+ self._app.x_col_combo.setEnabled(has_dataset_selection)
220
+ self._app.y_col_combo.setEnabled(has_dataset_selection)
221
+ self._app.x_err_col_combo.setEnabled(has_dataset_selection)
222
+ self._app.y_err_col_combo.setEnabled(has_dataset_selection)
223
+ self._app.error_display_combo.setEnabled(has_dataset_selection)
224
+ self._app.nan_policy_combo.setEnabled(has_dataset_selection)
225
+
226
+ # current_dataset はカレントがフォルダのときや選択が無いときは None
227
+ if current_dataset is not None:
228
+ dataset = current_dataset
229
+
230
+ # 欄に値を入れる間は、変更の通知でデータセットを書き換えないようにする
231
+ self._binder.block_signals(True)
232
+ self._binder.restore(dataset, getattr)
233
+ is_range_auto = dataset.vmin is None and dataset.vmax is None
234
+ self._app.vmin_spinbox.setEnabled(not is_range_auto)
235
+ self._app.vmax_spinbox.setEnabled(not is_range_auto)
236
+ self._binder.block_signals(False)
237
+ self.update_gradient_controls_visibility()
238
+ self.update_waterfall_controls_visibility()
239
+ self.update_smoothing_control_visibility()
240
+ self.update_error_display_control_items()
241
+ self.update_2d_controls_visibility()
242
+ self.update_point_labels_limit_note()
243
+
244
+ self._app.x_col_combo.blockSignals(True)
245
+ self._app.y_col_combo.blockSignals(True)
246
+
247
+ all_columns = dataset.df.columns.tolist()
248
+ self._app.x_col_combo.clear()
249
+ self._app.y_col_combo.clear()
250
+ self._app.x_col_combo.addItems(all_columns)
251
+ self._app.y_col_combo.addItems(all_columns)
252
+
253
+ self._app.x_col_combo.setCurrentText(dataset.x_col_name)
254
+ self._app.y_col_combo.setCurrentText(dataset.y_col_name)
255
+
256
+ self._app.x_col_combo.blockSignals(False)
257
+ self._app.y_col_combo.blockSignals(False)
258
+
259
+ self._app.x_err_col_combo.blockSignals(True)
260
+ self._app.y_err_col_combo.blockSignals(True)
261
+
262
+ self._app.x_err_col_combo.clear()
263
+ self._app.y_err_col_combo.clear()
264
+ self._app.x_err_col_combo.addItems([NO_ERROR_COLUMN_LABEL] + all_columns)
265
+ self._app.y_err_col_combo.addItems([NO_ERROR_COLUMN_LABEL] + all_columns)
266
+ self._app.x_err_col_combo.setCurrentText(dataset.x_err_col_name or NO_ERROR_COLUMN_LABEL)
267
+ self._app.y_err_col_combo.setCurrentText(dataset.y_err_col_name or NO_ERROR_COLUMN_LABEL)
268
+
269
+ self._app.x_err_col_combo.blockSignals(False)
270
+ self._app.y_err_col_combo.blockSignals(False)
271
+
272
+ self._app.z_col_combo.blockSignals(True)
273
+ self._app.z_col_combo.clear()
274
+ self._app.z_col_combo.addItems(all_columns)
275
+ if dataset.z_col_name:
276
+ self._app.z_col_combo.setCurrentText(dataset.z_col_name)
277
+ self._app.z_col_combo.blockSignals(False)
278
+
279
+ if dataset.fit_info:
280
+ self._app.fit_info_label.setVisible(True)
281
+ self._app.fit_info_textedit.setVisible(True)
282
+ self._app.fit_info_textedit.setText(dataset.fit_info)
283
+ else:
284
+ self._app.fit_info_label.setVisible(False)
285
+ self._app.fit_info_textedit.setVisible(False)
286
+ self._app.fit_info_textedit.clear()
287
+
288
+ self.update_stats_summary_label(dataset)
289
+
290
+ self._app.residual_panel.refresh(dataset)
291
+
292
+ self._app.provenance_panel.refresh(dataset, self._app.project)
293
+
294
+ else:
295
+ # 空にするのは表示だけ。信号を出すと、フォルダを今の項目にしたままデータセットを選んでいるとき
296
+ # 点ラベルの列が空の文字としてデータセットに書き込まれる
297
+ column_combos = (self._app.x_col_combo, self._app.y_col_combo, self._app.x_err_col_combo,
298
+ self._app.y_err_col_combo, self._app.point_label_col_combo, self._app.z_col_combo)
299
+ for combo in column_combos:
300
+ combo.blockSignals(True)
301
+ combo.clear()
302
+ combo.blockSignals(False)
303
+ self.update_2d_controls_visibility()
304
+
305
+ self._app.fit_info_label.setVisible(False)
306
+ self._app.fit_info_textedit.setVisible(False)
307
+ self._app.fit_info_textedit.clear()
308
+ self._app.residual_panel.refresh(None)
309
+ self._app.provenance_panel.refresh(None, self._app.project)
310
+
311
+ self._app.gradient_checkbox.setVisible(False)
312
+ self._app.gradient_color2_label.setVisible(False)
313
+ self._app.gradient_color2_picker.setVisible(False)
314
+ self._app.gradient_target_label.setVisible(False)
315
+ self._app.gradient_target_combo.setVisible(False)
316
+
317
+ self._app.waterfall_checkbox.setVisible(False)
318
+ self._app.waterfall_offset_x_label.setVisible(False)
319
+ self._app.waterfall_offset_x_spinbox.setVisible(False)
320
+ self._app.waterfall_offset_y_label.setVisible(False)
321
+ self._app.waterfall_offset_y_spinbox.setVisible(False)
322
+ self._app.waterfall_occlusion_checkbox.setVisible(False)
323
+ self._app.waterfall_depth_checkbox.setVisible(False)
324
+ self._app.waterfall_depth_ratio_label.setVisible(False)
325
+ self._app.waterfall_depth_ratio_spinbox.setVisible(False)
326
+
327
+ self._app.stats_summary_label.setText("-")
328
+ self._app.dataset_mini_stats_label.setText("-")
329
+ # 行を隠したあとで見出しもそろえる(起動直後もこの経路を通る)
330
+ self._app._update_property_section_visibility()
331
+
332
+ def update_stats_summary_label(self, dataset):
333
+ """Y の件数・平均・標準偏差・最小・最大を、プロパティ欄とデータセット一覧の下の1行に出す(NaN は除く)。"""
334
+ try:
335
+ y = np.asarray(dataset.y_data, dtype=float)
336
+ valid = y[~np.isnan(y)]
337
+ if len(valid) == 0:
338
+ self._app.stats_summary_label.setText("-")
339
+ self._app.dataset_mini_stats_label.setText(dataset.name)
340
+ return
341
+ # 標本標準偏差(n−1)。1点では定義できないので「-」
342
+ std = sample_standard_deviation(valid)
343
+ std_text = "-" if np.isnan(std) else f"{std:.4g}"
344
+ self._app.stats_summary_label.setText(
345
+ f"件数: {len(valid)} 平均: {np.mean(valid):.4g} "
346
+ f"標準偏差: {std_text} 最小: {np.min(valid):.4g} 最大: {np.max(valid):.4g}"
347
+ )
348
+ self._app.dataset_mini_stats_label.setText(
349
+ f"{dataset.name} 〈n={len(valid)}, 平均={np.mean(valid):.4g}, "
350
+ f"SD={std_text}〉"
351
+ )
352
+ except (TypeError, ValueError):
353
+ self._app.stats_summary_label.setText("-")
354
+ self._app.dataset_mini_stats_label.setText(dataset.name)
355
+
356
+ def on_plot_column_changed(self):
357
+ dataset = self._app._get_current_dataset()
358
+ if dataset is None:
359
+ return
360
+
361
+ new_x_col = self._app.x_col_combo.currentText()
362
+ new_y_col = self._app.y_col_combo.currentText()
363
+
364
+ # 変わった列だけを含める(欄が空のこともある)
365
+ old_values, new_values = {}, {}
366
+ if new_x_col and new_x_col in dataset.df.columns and new_x_col != dataset.x_col_name:
367
+ old_values['x_col_name'] = dataset.x_col_name
368
+ new_values['x_col_name'] = new_x_col
369
+ if new_y_col and new_y_col in dataset.df.columns and new_y_col != dataset.y_col_name:
370
+ old_values['y_col_name'] = dataset.y_col_name
371
+ new_values['y_col_name'] = new_y_col
372
+
373
+ self._app._push_dataset_property_command(dataset, old_values, new_values, description="プロット列の変更")
374
+
375
+ # 描画先の軸ラベルが空なら列名から埋める。軸の設定は Undo の仕組みが別なので Undo には含めない。
376
+ if 'x_col_name' in new_values:
377
+ self.maybe_autofill_axis_label(dataset.subplot_target, 'x_label', new_values['x_col_name'])
378
+ if 'y_col_name' in new_values:
379
+ self.maybe_autofill_axis_label(dataset.subplot_target, 'y_label', new_values['y_col_name'])
380
+
381
+ def maybe_autofill_axis_label(self, subplot_index, label_key, column_name):
382
+ """
383
+ その軸のラベルが空のときだけ、列名から推し量ったラベルで埋める。
384
+ 今表示している軸なら欄に書き込み(保存と再描画は欄の変更の経路に任せる)、そうでなければ設定を直接書き換える
385
+ (表示していない軸のラベルを、表示中の軸の欄に書いてしまわないように)。
386
+ """
387
+ if subplot_index is None or not (0 <= subplot_index < len(self._app.project.all_plot_settings)):
388
+ return
389
+ inferred = infer_axis_label_from_column_name(column_name)
390
+ if not inferred:
391
+ return
392
+ current_settings = self._app.project.all_plot_settings[subplot_index]
393
+ if current_settings.get(label_key):
394
+ return
395
+
396
+ if subplot_index == self._app.project.active_axis_index:
397
+ label_edit = self._app.ui.x_label_text_edit if label_key == 'x_label' else self._app.ui.y_label_text_edit
398
+ if not label_edit.text():
399
+ label_edit.setText(inferred)
400
+ else:
401
+ current_settings[label_key] = inferred
402
+ self._app._update_plot()
403
+
404
+ def on_data_2d_toggled(self, checked):
405
+ """2Dマップとして扱うかを切り替える。Z 列が未設定なら X/Y 以外の最初の列を選んでおく。"""
406
+ dataset = self._app._get_current_dataset()
407
+ if dataset is None:
408
+ return
409
+ new_data_kind = '2d_grid' if checked else '1d'
410
+ if dataset.data_kind == new_data_kind:
411
+ self.update_2d_controls_visibility()
412
+ return
413
+
414
+ old_values = {'data_kind': dataset.data_kind}
415
+ new_values = {'data_kind': new_data_kind}
416
+ if new_data_kind == '2d_grid' and not dataset.z_col_name:
417
+ candidates = [
418
+ c for c in dataset.df.columns
419
+ if c not in (dataset.x_col_name, dataset.y_col_name)
420
+ ]
421
+ if candidates:
422
+ old_values['z_col_name'] = dataset.z_col_name
423
+ new_values['z_col_name'] = candidates[0]
424
+
425
+ self._app._push_dataset_property_command(dataset, old_values, new_values, description="2Dグリッドデータの切り替え")
426
+ self.update_2d_controls_visibility()
427
+
428
+ def on_z_column_changed(self):
429
+ dataset = self._app._get_current_dataset()
430
+ if dataset is None:
431
+ return
432
+ new_z_col = self._app.z_col_combo.currentText()
433
+ if not new_z_col or new_z_col not in dataset.df.columns or new_z_col == dataset.z_col_name:
434
+ return
435
+ self._app._push_dataset_property_command(
436
+ dataset, {'z_col_name': dataset.z_col_name}, {'z_col_name': new_z_col},
437
+ description="Z軸列の変更"
438
+ )
439
+
440
+ def on_2d_value_range_changed(self):
441
+ """値域を自動にしている間は vmin/vmax を None にする(描画時に実データの範囲を使う)。"""
442
+ dataset = self._app._get_current_dataset()
443
+ if dataset is None:
444
+ return
445
+ is_auto = self._app.color_range_auto_checkbox.isChecked()
446
+ self._app.vmin_spinbox.setEnabled(not is_auto)
447
+ self._app.vmax_spinbox.setEnabled(not is_auto)
448
+
449
+ new_vmin = None if is_auto else self._app.vmin_spinbox.value()
450
+ new_vmax = None if is_auto else self._app.vmax_spinbox.value()
451
+ if new_vmin == dataset.vmin and new_vmax == dataset.vmax:
452
+ return
453
+ self._app._push_dataset_property_command(
454
+ dataset,
455
+ {'vmin': dataset.vmin, 'vmax': dataset.vmax},
456
+ {'vmin': new_vmin, 'vmax': new_vmax},
457
+ description="値域の変更"
458
+ )
459
+
460
+ def update_2d_controls_visibility(self):
461
+ """
462
+ Z 列・カラーマップ・値域は、2Dマップと、3列目の値で点を色分けする散布図の両方で使う。
463
+ 表示方式・等高線・補間方法は2Dマップだけ。
464
+ """
465
+ dataset = self._app._get_current_dataset()
466
+ # 選択が無いときは隠す(2Dのチェックには前に選んでいたデータセットの値が残っている)
467
+ is_2d = dataset is not None and dataset.data_kind == '2d_grid'
468
+ is_color_by_column = (
469
+ dataset is not None
470
+ and dataset.data_kind != '2d_grid'
471
+ and dataset.plot_type == COLOR_BY_COLUMN_PLOT_TYPE
472
+ )
473
+ shared_with_color_scatter = is_2d or is_color_by_column
474
+ for widget in (
475
+ self._app.z_col_label, self._app.z_col_combo,
476
+ self._app.colormap_label, self._app.colormap_combo,
477
+ self._app.color_range_auto_checkbox,
478
+ self._app.vmin_label, self._app.vmin_spinbox,
479
+ self._app.vmax_label, self._app.vmax_spinbox,
480
+ ):
481
+ widget.setVisible(shared_with_color_scatter)
482
+
483
+ for widget in (
484
+ self._app.map_display_mode_label, self._app.map_display_mode_combo,
485
+ self._app.contour_levels_label, self._app.contour_levels_spinbox,
486
+ self._app.grid_interp_method_label, self._app.grid_interp_method_combo,
487
+ ):
488
+ widget.setVisible(is_2d)
489
+
490
+ self._app._update_property_section_visibility()
491
+
492
+ def on_error_column_changed(self):
493
+ dataset = self._app._get_current_dataset()
494
+ if dataset is None:
495
+ return
496
+
497
+ new_x_err = self._app.x_err_col_combo.currentText()
498
+ new_y_err = self._app.y_err_col_combo.currentText()
499
+ new_x_err_col = None if (not new_x_err or new_x_err == NO_ERROR_COLUMN_LABEL) else new_x_err
500
+ new_y_err_col = None if (not new_y_err or new_y_err == NO_ERROR_COLUMN_LABEL) else new_y_err
501
+
502
+ old_values, new_values = {}, {}
503
+ if new_x_err_col != dataset.x_err_col_name:
504
+ old_values['x_err_col_name'] = dataset.x_err_col_name
505
+ new_values['x_err_col_name'] = new_x_err_col
506
+ if new_y_err_col != dataset.y_err_col_name:
507
+ old_values['y_err_col_name'] = dataset.y_err_col_name
508
+ new_values['y_err_col_name'] = new_y_err_col
509
+
510
+ self._app._push_dataset_property_command(dataset, old_values, new_values, description="誤差列(エラーバー)の変更")
511
+
512
+ def on_data_structure_changed(self):
513
+ """データエディタでデータが変わったら、列の選択肢と描画を作り直す。"""
514
+ if self._app._get_current_dataset() is None:
515
+ return
516
+
517
+ self.update_ui_state()
518
+ self._app._update_plot()
519
+
520
+ def on_secondary_y_changed(self):
521
+ selected_datasets = self._app._get_selected_datasets()
522
+ if not selected_datasets:
523
+ return
524
+
525
+ new_value = self._app.use_secondary_y_checkbox.isChecked()
526
+
527
+ is_batch = len(selected_datasets) > 1
528
+ if is_batch:
529
+ self._app.undo_stack.beginMacro(f"第2Y軸使用の一括変更 ({len(selected_datasets)}件)")
530
+ for dataset in selected_datasets:
531
+ self._app._push_dataset_property_command(
532
+ dataset,
533
+ {'use_secondary_y': dataset.use_secondary_y},
534
+ {'use_secondary_y': new_value},
535
+ description="第2Y軸使用の変更"
536
+ )
537
+ if is_batch:
538
+ self._app.undo_stack.endMacro()
@@ -0,0 +1,36 @@
1
+ """データセットをタブの外とやり取りするメニューの入口。本体は gui/datasets/operations/transfer.py。"""
2
+ from graphica.gui.datasets.operations.runner import OperationState, run_operation
3
+ from graphica.gui.datasets.operations.transfer import TRANSFER_OPERATIONS
4
+
5
+
6
+ class TransferController:
7
+ """データセットをタブの外(ファイル・別のタブ)とやり取りする。"""
8
+
9
+ def __init__(self, host):
10
+ self._host = host
11
+ self._state = OperationState()
12
+
13
+ @property
14
+ def copied_style(self):
15
+ return self._state.copied_style
16
+
17
+ def _run(self, name):
18
+ run_operation(TRANSFER_OPERATIONS[name], self._host, self._state)
19
+
20
+ def export_data(self):
21
+ self._run("export_data")
22
+
23
+ def copy_or_move_to_tab(self, move):
24
+ self._run("move_to_tab" if move else "copy_to_tab")
25
+
26
+ def copy_style(self):
27
+ self._run("copy_style")
28
+
29
+ def paste_style(self):
30
+ self._run("paste_style")
31
+
32
+ def reload_from_source(self):
33
+ self._run("reload_from_source")
34
+
35
+ def copy_methods_text(self):
36
+ self._run("copy_methods_text")
@@ -0,0 +1,22 @@
1
+ """切り離したキャンバスを入れるだけの独立したウィンドウ。
2
+
3
+ self.canvas は親を替えるだけで同じオブジェクトのまま。閉じられても破棄せず closed で知らせ、戻す処理は
4
+ PlotterApp._reattach_canvas() に任せる(「元に戻す」と×ボタンが同じ後処理を通るように)。
5
+ """
6
+ from PySide6.QtCore import Qt, Signal
7
+ from PySide6.QtWidgets import QMainWindow
8
+
9
+
10
+ class DetachedCanvasWindow(QMainWindow):
11
+ closed = Signal()
12
+
13
+ def __init__(self, title, parent=None):
14
+ super().__init__(parent)
15
+ self.setWindowTitle(title)
16
+ # parent があっても、埋め込みではなく独立したウィンドウにする
17
+ self.setWindowFlag(Qt.WindowType.Window, True)
18
+
19
+ def closeEvent(self, event):
20
+ """破棄せずに closed を出すだけ(キャンバスを抱えたまま破棄すると、戻す前にキャンバスごと消える)。"""
21
+ event.accept()
22
+ self.closed.emit()