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,577 @@
1
+ """データの編集と列の操作のダイアログ。呼び出し側は `from graphica.gui.dialogs import X` で参照する。"""
2
+
3
+ from PySide6.QtWidgets import (
4
+ QComboBox,
5
+ QDialog,
6
+ QDialogButtonBox,
7
+ QFormLayout,
8
+ QGroupBox,
9
+ QHBoxLayout,
10
+ QLabel,
11
+ QLineEdit,
12
+ QListWidget,
13
+ QListWidgetItem,
14
+ QPushButton,
15
+ QSpinBox,
16
+ QStackedWidget,
17
+ QTextBrowser,
18
+ QVBoxLayout,
19
+ QWidget,
20
+ )
21
+ from PySide6.QtCore import Qt
22
+ from graphica.core.safe_eval import column_reference
23
+ from graphica.gui.theme import apply_form_spacing
24
+
25
+
26
+ class ColumnCalculatorDialog(QDialog):
27
+ """列の計算の出力先の列と式(式は safe_eval_column_formula で評価する)。"""
28
+
29
+ def __init__(self, column_names, parent=None):
30
+ super().__init__(parent)
31
+ self.setWindowTitle("列の計算")
32
+
33
+ self.column_names = column_names
34
+
35
+
36
+ self.output_col_label = QLabel("出力先の列 (既存または新規)")
37
+
38
+ self.output_col_combo = QComboBox()
39
+ self.output_col_combo.addItems(self.column_names)
40
+ # 新しい列名も打ち込めるように
41
+ self.output_col_combo.setEditable(True)
42
+
43
+ self.formula_label = QLabel("計算式 (例: A + B * 2)")
44
+
45
+ self.formula_edit = QLineEdit()
46
+ self.formula_edit.setPlaceholderText("例: (A + B) / 2 や log(C)")
47
+
48
+ help_text = QLabel("列名はそのまま使えます (例: `A`)。\n数値や `log(A)`, `sin(A)` なども利用可能です。")
49
+ help_text.setStyleSheet("font-size: 9pt; color: gray;")
50
+
51
+ preset_group = QGroupBox("プリセット (対象列を選んでボタンを押すと計算式が自動入力されます)")
52
+ preset_layout = QVBoxLayout()
53
+
54
+ preset_source_row = QHBoxLayout()
55
+ preset_source_row.addWidget(QLabel("対象列"))
56
+ self.preset_source_combo = QComboBox()
57
+ self.preset_source_combo.addItems(self.column_names)
58
+ preset_source_row.addWidget(self.preset_source_combo)
59
+ preset_source_row.addWidget(QLabel("移動平均の窓幅"))
60
+ self.preset_window_spinbox = QSpinBox()
61
+ self.preset_window_spinbox.setRange(2, 100000)
62
+ self.preset_window_spinbox.setValue(5)
63
+ preset_source_row.addWidget(self.preset_window_spinbox)
64
+ preset_layout.addLayout(preset_source_row)
65
+
66
+ preset_button_row = QHBoxLayout()
67
+ moving_avg_button = QPushButton("移動平均")
68
+ moving_avg_button.clicked.connect(self._apply_preset_moving_average)
69
+ diff_button = QPushButton("微分(差分)")
70
+ diff_button.clicked.connect(self._apply_preset_diff)
71
+ normalize_button = QPushButton("正規化")
72
+ normalize_button.clicked.connect(self._apply_preset_normalize)
73
+ cumsum_button = QPushButton("累積和")
74
+ cumsum_button.clicked.connect(self._apply_preset_cumsum)
75
+ for btn in (moving_avg_button, diff_button, normalize_button, cumsum_button):
76
+ preset_button_row.addWidget(btn)
77
+ preset_layout.addLayout(preset_button_row)
78
+
79
+ preset_group.setLayout(preset_layout)
80
+
81
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
82
+ QDialogButtonBox.StandardButton.Cancel)
83
+ button_box.accepted.connect(self.accept)
84
+ button_box.rejected.connect(self.reject)
85
+
86
+ layout = QVBoxLayout(self)
87
+ layout.addWidget(self.output_col_label)
88
+ layout.addWidget(self.output_col_combo)
89
+ layout.addWidget(self.formula_label)
90
+ layout.addWidget(self.formula_edit)
91
+ layout.addWidget(help_text)
92
+ layout.addWidget(preset_group)
93
+ layout.addWidget(button_box)
94
+
95
+ def _apply_preset_moving_average(self):
96
+ name = self.preset_source_combo.currentText()
97
+ if not name:
98
+ return
99
+ col = column_reference(name)
100
+ window = self.preset_window_spinbox.value()
101
+ self.formula_edit.setText(f"{col}.rolling({window}).mean()")
102
+ self.output_col_combo.setCurrentText(f"{name}_moving_avg{window}")
103
+
104
+ def _apply_preset_diff(self):
105
+ name = self.preset_source_combo.currentText()
106
+ if not name:
107
+ return
108
+ col = column_reference(name)
109
+ self.formula_edit.setText(f"{col}.diff()")
110
+ self.output_col_combo.setCurrentText(f"{name}_diff")
111
+
112
+ def _apply_preset_normalize(self):
113
+ """平均0・標準偏差1にする式。"""
114
+ name = self.preset_source_combo.currentText()
115
+ if not name:
116
+ return
117
+ col = column_reference(name)
118
+ self.formula_edit.setText(f"({col} - {col}.mean()) / {col}.std()")
119
+ self.output_col_combo.setCurrentText(f"{name}_normalized")
120
+
121
+ def _apply_preset_cumsum(self):
122
+ name = self.preset_source_combo.currentText()
123
+ if not name:
124
+ return
125
+ col = column_reference(name)
126
+ self.formula_edit.setText(f"{col}.cumsum()")
127
+ self.output_col_combo.setCurrentText(f"{name}_cumsum")
128
+
129
+ def get_formula(self):
130
+ """(出力先の列名, 式)"""
131
+ output_column_name = self.output_col_combo.currentText()
132
+ formula_string = self.formula_edit.text()
133
+
134
+ return output_column_name, formula_string
135
+
136
+
137
+ class CalcHelpDialog(QDialog):
138
+ """列の計算で使える書き方のリファレンス。"""
139
+
140
+ def __init__(self, parent=None):
141
+ super().__init__(parent)
142
+ self.setWindowTitle("列計算機能 リファレンス")
143
+ self.resize(600, 700)
144
+
145
+ layout = QVBoxLayout(self)
146
+
147
+ text_browser = QTextBrowser()
148
+ text_browser.setReadOnly(True)
149
+ text_browser.setOpenExternalLinks(True)
150
+
151
+ help_html = r"""
152
+ <h1>列計算機能 リファレンス</h1>
153
+ <p>
154
+ 計算式を使って、列データをまとめて計算します。
155
+ 「出力先の列」に指定した列に、計算式の結果が一度に適用されます(Excelのオートフィルのように、全行に適用されます)。
156
+ </p>
157
+
158
+ <hr>
159
+
160
+ <h2>1. 基本的な算術演算子 🧮</h2>
161
+ <p>列名(例: <code>A</code>, <code>B</code>)や数値をそのまま使えます。空白や記号を含む列名は <code>`強度 (a.u.)`</code> のようにバッククォートで囲みます。</p>
162
+
163
+ <table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse;">
164
+ <tr class="header-row"><th>計算式 (入力例)</th><th>実行内容</th></tr>
165
+ <tr><td><code>A + B</code></td><td>A列とB列の各行を足し算します。</td></tr>
166
+ <tr><td><code>A * 100</code></td><td>A列の全データを100倍します。</td></tr>
167
+ <tr><td><code>(A + B) / 2</code></td><td>A列とB列の平均値を計算します。</td></tr>
168
+ <tr><td><code>A ** 2</code></td><td>A列の値を2乗します。</td></tr>
169
+ <tr><td><code>A % 5</code></td><td>A列の値を5で割った余りを計算します。</td></tr>
170
+ </table>
171
+
172
+ <hr>
173
+
174
+ <h2>2. 一般的な数学関数 📈</h2>
175
+ <p>
176
+ 以下の関数が利用可能です。
177
+ </p>
178
+
179
+ <table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse;">
180
+ <tr class="header-row"><th>関数 (入力例)</th><th>意味</th></tr>
181
+ <tr><td><code>sqrt(A)</code></td><td>Aの平方根 (&radic;A)</td></tr>
182
+ <tr><td><code>log(A)</code></td><td>Aの自然対数 (ln A)</td></tr>
183
+ <tr><td><code>log10(A)</code></td><td>Aの常用対数 (log₁₀ A)</td></tr>
184
+ <tr><td><code>exp(A)</code></td><td>Aの指数関数 (e<sup>A</sup>)</td></tr>
185
+ <tr><td><code>abs(A)</code></td><td>Aの絶対値</td></tr>
186
+ <tr><td><code>sin(A)</code></td><td>Aのサイン (ラジアン)</td></tr>
187
+ <tr><td><code>cos(A)</code></td><td>Aのコサイン (ラジアン)</td></tr>
188
+ <tr><td><code>tan(A)</code></td><td>Aのタンジェント (ラジアン)</td></tr>
189
+ </table>
190
+
191
+ <hr>
192
+
193
+ <h2>3. 比較・論理演算子 🔍</h2>
194
+ <p>
195
+ 条件に合うかどうかを <code>True</code> / <code>False</code> で返す新しい列を作成できます。
196
+ 複数の条件を組み合わせる場合は <code>and</code>, <code>or</code>, <code>not</code> を使います。
197
+ </p>
198
+
199
+ <table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse;">
200
+ <tr class="header-row"><th>計算式 (入力例)</th><th>実行内容</th></tr>
201
+ <tr><td><code>A > 10</code></td><td>A列の値が10より大きい行はTrueになります。</td></tr>
202
+ <tr><td><code>A == B</code></td><td>A列とB列の値が等しい行はTrueになります。</td></tr>
203
+ <tr><td><code>A > 5 and B < 3</code></td><td>Aが5より大きく、<b>かつ</b> Bが3未満の行だけTrueになります。</td></tr>
204
+ <tr><td><code>A < 0 or A > 10</code></td><td>Aが0未満、<b>または</b> Aが10より大きい行がTrueになります。</td></tr>
205
+ <tr><td><code>not (A > 5)</code></td><td>Aが5より大きい、という条件を否定します (A <= 5 と同じ)。</td></tr>
206
+ </table>
207
+ """
208
+ self._text_browser = text_browser
209
+ self.refresh_theme()
210
+ text_browser.setHtml(help_html)
211
+
212
+ layout.addWidget(text_browser)
213
+
214
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
215
+ button_box.rejected.connect(self.reject)
216
+ layout.addWidget(button_box)
217
+
218
+ def refresh_theme(self):
219
+ """開いたままダークモードを切り替えられるので、見出し行の色を今のテーマで当て直す。"""
220
+ from graphica.gui import theme
221
+ _tokens = theme.current_tokens()
222
+ self._text_browser.document().setDefaultStyleSheet(
223
+ f"tr.header-row {{ background-color: {_tokens['surface_2']}; "
224
+ f"color: {_tokens['text_primary']}; }}"
225
+ )
226
+
227
+
228
+ class ColumnStringOpsDialog(QDialog):
229
+ """列の分割・結合・数値の抽出の設定。処理は呼び出し側(DataEditorDialog)が行う。"""
230
+
231
+ MODE_SPLIT = "列の分割"
232
+ MODE_MERGE = "列の結合"
233
+ MODE_EXTRACT_NUMERIC = "数値抽出"
234
+ MODES = [MODE_SPLIT, MODE_MERGE, MODE_EXTRACT_NUMERIC]
235
+
236
+ def __init__(self, column_names, parent=None):
237
+ super().__init__(parent)
238
+ self.setWindowTitle("文字列操作")
239
+ self.resize(420, 400)
240
+ self.column_names = column_names
241
+
242
+ layout = QVBoxLayout(self)
243
+
244
+ mode_form = QFormLayout()
245
+ self.mode_combo = QComboBox()
246
+ self.mode_combo.addItems(self.MODES)
247
+ mode_form.addRow("操作", self.mode_combo)
248
+ layout.addLayout(mode_form)
249
+
250
+ self.stack = QStackedWidget()
251
+ layout.addWidget(self.stack)
252
+
253
+ split_page = QWidget()
254
+ split_form = QFormLayout(split_page)
255
+ self.split_source_combo = QComboBox()
256
+ self.split_source_combo.addItems(column_names)
257
+ split_form.addRow("対象列", self.split_source_combo)
258
+ self.split_delimiter_edit = QLineEdit()
259
+ self.split_delimiter_edit.setPlaceholderText("例: , や _ (空白なら半角スペース)")
260
+ split_form.addRow("区切り文字", self.split_delimiter_edit)
261
+ self.split_prefix_edit = QLineEdit()
262
+ self.split_prefix_edit.setPlaceholderText("例: part (空欄なら対象列名を使用)")
263
+ split_form.addRow("出力列名の接頭辞", self.split_prefix_edit)
264
+ self.stack.addWidget(split_page)
265
+
266
+ merge_page = QWidget()
267
+ merge_layout = QVBoxLayout(merge_page)
268
+ merge_layout.addWidget(QLabel("結合する列を2つ以上選択してください:"))
269
+ self.merge_column_list = QListWidget()
270
+ for name in column_names:
271
+ item = QListWidgetItem(str(name))
272
+ item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
273
+ item.setCheckState(Qt.CheckState.Unchecked)
274
+ self.merge_column_list.addItem(item)
275
+ merge_layout.addWidget(self.merge_column_list)
276
+ merge_form = QFormLayout()
277
+ self.merge_separator_edit = QLineEdit()
278
+ self.merge_separator_edit.setPlaceholderText("例: _ (空欄可)")
279
+ merge_form.addRow("区切り文字", self.merge_separator_edit)
280
+ self.merge_output_edit = QLineEdit()
281
+ merge_form.addRow("出力列名", self.merge_output_edit)
282
+ merge_layout.addLayout(merge_form)
283
+ self.stack.addWidget(merge_page)
284
+
285
+ extract_page = QWidget()
286
+ extract_form = QFormLayout(extract_page)
287
+ self.extract_source_combo = QComboBox()
288
+ self.extract_source_combo.addItems(column_names)
289
+ extract_form.addRow("対象列", self.extract_source_combo)
290
+ self.extract_pattern_edit = QLineEdit(r'[-+]?\d*\.?\d+')
291
+ extract_form.addRow("正規表現", self.extract_pattern_edit)
292
+ extract_help = QLabel('既定は数値(整数/小数、符号付き)を抽出します。例: "400nm" → 400')
293
+ extract_help.setWordWrap(True)
294
+ extract_help.setStyleSheet("font-size: 9pt; color: gray;")
295
+ extract_form.addRow(extract_help)
296
+ self.extract_output_edit = QLineEdit()
297
+ extract_form.addRow("出力列名", self.extract_output_edit)
298
+ self.stack.addWidget(extract_page)
299
+
300
+ self.mode_combo.currentIndexChanged.connect(self.stack.setCurrentIndex)
301
+
302
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
303
+ QDialogButtonBox.StandardButton.Cancel)
304
+ button_box.accepted.connect(self.accept)
305
+ button_box.rejected.connect(self.reject)
306
+ layout.addWidget(button_box)
307
+
308
+ apply_form_spacing(self)
309
+
310
+ def get_mode(self):
311
+ return self.mode_combo.currentText()
312
+
313
+ def get_split_settings(self):
314
+ """(対象の列, 区切り文字, 出力の列名の接頭辞)"""
315
+ return (
316
+ self.split_source_combo.currentText(),
317
+ self.split_delimiter_edit.text(),
318
+ self.split_prefix_edit.text().strip(),
319
+ )
320
+
321
+ def get_merge_settings(self):
322
+ """(列名のリスト, 区切り文字, 出力の列名)"""
323
+ selected = []
324
+ for i in range(self.merge_column_list.count()):
325
+ item = self.merge_column_list.item(i)
326
+ if item.checkState() == Qt.CheckState.Checked:
327
+ selected.append(item.text())
328
+ return selected, self.merge_separator_edit.text(), self.merge_output_edit.text().strip()
329
+
330
+ def get_extract_settings(self):
331
+ """(対象の列, 正規表現, 出力の列名)"""
332
+ return (
333
+ self.extract_source_combo.currentText(),
334
+ self.extract_pattern_edit.text(),
335
+ self.extract_output_edit.text().strip(),
336
+ )
337
+
338
+
339
+ class FindReplaceDialog(QDialog):
340
+ """検索と置換。入力欄とボタンだけで、処理は DataEditorDialog が持つ。表を見ながら使うので非モーダル。"""
341
+
342
+ def __init__(self, column_names, parent=None):
343
+ super().__init__(parent)
344
+ self.setWindowTitle("検索/置換")
345
+ # 非モーダルなので、データエディタの裏に隠れないよう上に浮かせる
346
+ self.setWindowFlag(Qt.WindowType.Tool, True)
347
+ self.resize(340, 200)
348
+
349
+ layout = QVBoxLayout(self)
350
+ form = QFormLayout()
351
+ self.search_edit = QLineEdit()
352
+ form.addRow("検索文字列", self.search_edit)
353
+ self.replace_edit = QLineEdit()
354
+ form.addRow("置換後の文字列", self.replace_edit)
355
+ self.column_combo = QComboBox()
356
+ self.column_combo.addItem("(すべての列)")
357
+ self.column_combo.addItems(column_names)
358
+ form.addRow("対象列", self.column_combo)
359
+ layout.addLayout(form)
360
+
361
+ button_row = QHBoxLayout()
362
+ self.find_next_button = QPushButton("次を検索")
363
+ self.replace_all_button = QPushButton("すべて置換")
364
+ button_row.addWidget(self.find_next_button)
365
+ button_row.addWidget(self.replace_all_button)
366
+ button_row.addStretch()
367
+ close_button = QPushButton("閉じる")
368
+ close_button.clicked.connect(self.close)
369
+ button_row.addWidget(close_button)
370
+ layout.addLayout(button_row)
371
+
372
+ self.status_label = QLabel("")
373
+ self.status_label.setWordWrap(True)
374
+ layout.addWidget(self.status_label)
375
+
376
+ apply_form_spacing(self)
377
+
378
+ def get_search_text(self):
379
+ return self.search_edit.text()
380
+
381
+ def get_replace_text(self):
382
+ return self.replace_edit.text()
383
+
384
+ def get_target_column(self):
385
+ """対象の列。「(すべての列)」なら None。"""
386
+ text = self.column_combo.currentText()
387
+ return None if text == "(すべての列)" else text
388
+
389
+ def set_status(self, text):
390
+ self.status_label.setText(text)
391
+
392
+
393
+ class RowFilterDialog(QDialog):
394
+ """条件式(例: "y > 0.5")を満たさない行をマスクする。式は列の計算と同じ規則。"""
395
+
396
+ def __init__(self, column_names, parent=None):
397
+ super().__init__(parent)
398
+ self.setWindowTitle("行フィルタ")
399
+ self.resize(440, 240)
400
+
401
+ layout = QVBoxLayout(self)
402
+ layout.addWidget(QLabel("条件式を満たさない行をマスク(除外)します(非破壊、いつでも解除できます)。"))
403
+
404
+ self.formula_edit = QLineEdit()
405
+ self.formula_edit.setPlaceholderText("例: y > 0.5 や (x > 0) and (y < 100)")
406
+ layout.addWidget(self.formula_edit)
407
+
408
+ help_text = QLabel(
409
+ "列名はそのまま使えます(例: y)。比較演算子(>, <, ==, != 等)や\n"
410
+ "and / or / not が使えます(core/safe_eval.pyの安全な数式評価器)。"
411
+ )
412
+ help_text.setStyleSheet("font-size: 9pt; color: gray;")
413
+ help_text.setWordWrap(True)
414
+ layout.addWidget(help_text)
415
+
416
+ if column_names:
417
+ columns_label = QLabel("利用可能な列: " + ", ".join(column_reference(str(c)) for c in column_names))
418
+ columns_label.setWordWrap(True)
419
+ columns_label.setStyleSheet("font-size: 9pt; color: gray;")
420
+ layout.addWidget(columns_label)
421
+
422
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
423
+ QDialogButtonBox.StandardButton.Cancel)
424
+ button_box.accepted.connect(self.accept)
425
+ button_box.rejected.connect(self.reject)
426
+ layout.addWidget(button_box)
427
+
428
+ apply_form_spacing(self)
429
+
430
+ def get_formula(self):
431
+ return self.formula_edit.text().strip()
432
+
433
+
434
+ class ColumnVisibilityDialog(QDialog):
435
+ """チェックを外した列を表で隠す(表示だけで dataset.df は変えない)。"""
436
+
437
+ def __init__(self, column_names, hidden_columns, parent=None):
438
+ super().__init__(parent)
439
+ self.setWindowTitle("列の表示/非表示")
440
+ self.resize(280, 380)
441
+
442
+ layout = QVBoxLayout(self)
443
+ layout.addWidget(QLabel("表示する列にチェックを入れてください:"))
444
+
445
+ self.column_list = QListWidget()
446
+ for name in column_names:
447
+ item = QListWidgetItem(str(name))
448
+ item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
449
+ state = Qt.CheckState.Unchecked if name in hidden_columns else Qt.CheckState.Checked
450
+ item.setCheckState(state)
451
+ self.column_list.addItem(item)
452
+ layout.addWidget(self.column_list)
453
+
454
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
455
+ QDialogButtonBox.StandardButton.Cancel)
456
+ button_box.accepted.connect(self.accept)
457
+ button_box.rejected.connect(self.reject)
458
+ layout.addWidget(button_box)
459
+
460
+ def get_hidden_columns(self):
461
+ """隠す列名のリスト。"""
462
+ hidden = []
463
+ for i in range(self.column_list.count()):
464
+ item = self.column_list.item(i)
465
+ if item.checkState() == Qt.CheckState.Unchecked:
466
+ hidden.append(item.text())
467
+ return hidden
468
+
469
+
470
+ class DuplicateXDialog(QDialog):
471
+ """同じ X の行を、平均した新しいデータセットにするか、先頭以外をマスクする。"""
472
+
473
+ MODE_AVERAGE = "平均化(新しいデータセットを作成)"
474
+ MODE_REMOVE = "除去(先頭以外をマスク)"
475
+ MODES = [MODE_AVERAGE, MODE_REMOVE]
476
+
477
+ def __init__(self, name, n_duplicate_rows, parent=None):
478
+ super().__init__(parent)
479
+ self.setWindowTitle("重複X値の検出")
480
+ self.resize(420, 240)
481
+ self._name = name
482
+
483
+ layout = QVBoxLayout(self)
484
+ layout.addWidget(QLabel(f"対象: {name}"))
485
+ layout.addWidget(QLabel(f"重複するX値を持つ行: {n_duplicate_rows}件"))
486
+
487
+ form = QFormLayout()
488
+ self.mode_combo = QComboBox()
489
+ self.mode_combo.addItems(self.MODES)
490
+ form.addRow("処理方法", self.mode_combo)
491
+ layout.addLayout(form)
492
+
493
+ self.stack = QStackedWidget()
494
+
495
+ average_page = QWidget()
496
+ average_form = QFormLayout(average_page)
497
+ self.output_name_edit = QLineEdit(f"{name}_averaged")
498
+ average_form.addRow("出力データセット名", self.output_name_edit)
499
+ self.stack.addWidget(average_page)
500
+
501
+ remove_page = QWidget()
502
+ remove_form = QFormLayout(remove_page)
503
+ remove_note = QLabel("同じX値のうち最初に出現した行だけを残し、残りをマスク(除外)します。")
504
+ remove_note.setWordWrap(True)
505
+ remove_form.addRow(remove_note)
506
+ self.stack.addWidget(remove_page)
507
+
508
+ layout.addWidget(self.stack)
509
+ self.mode_combo.currentIndexChanged.connect(self.stack.setCurrentIndex)
510
+
511
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
512
+ QDialogButtonBox.StandardButton.Cancel)
513
+ button_box.accepted.connect(self.accept)
514
+ button_box.rejected.connect(self.reject)
515
+ layout.addWidget(button_box)
516
+
517
+ apply_form_spacing(self)
518
+
519
+ def get_settings(self):
520
+ """(方法 "average" | "remove", 出力名("remove" では使わない))"""
521
+ mode = "average" if self.mode_combo.currentText() == self.MODE_AVERAGE else "remove"
522
+ return mode, self.output_name_edit.text().strip()
523
+
524
+
525
+ class ReplicateErrorDialog(QDialog):
526
+ """反復測定の列を選び、行ごとの平均と誤差(SD / SEM / 95%CI)の列を作る。"""
527
+
528
+ def __init__(self, column_names, parent=None):
529
+ super().__init__(parent)
530
+ self.setWindowTitle("誤差の自動計算 (反復測定)")
531
+ self.resize(360, 420)
532
+
533
+ layout = QVBoxLayout(self)
534
+ layout.addWidget(QLabel("反復測定として扱う列を2つ以上選んでください:"))
535
+
536
+ self.column_list = QListWidget()
537
+ self.column_list.setSelectionMode(QListWidget.SelectionMode.NoSelection)
538
+ for name in column_names:
539
+ item = QListWidgetItem(str(name))
540
+ item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
541
+ item.setCheckState(Qt.CheckState.Unchecked)
542
+ self.column_list.addItem(item)
543
+ layout.addWidget(self.column_list)
544
+
545
+ form = QFormLayout()
546
+ self.stat_combo = QComboBox()
547
+ self.stat_combo.addItems(["SD (標準偏差)", "SEM (標準誤差)", "95%CI (信頼区間)"])
548
+ form.addRow("誤差の種類", self.stat_combo)
549
+
550
+ self.base_name_edit = QLineEdit("measurement")
551
+ form.addRow("出力列名 (ベース)", self.base_name_edit)
552
+ layout.addLayout(form)
553
+
554
+ info_label = QLabel(
555
+ "「平均」列と「誤差」列の2つが追加されます (例: measurement_mean, measurement_SD)。"
556
+ "NaNを含む行は、その行にある有効な測定値だけで計算します。"
557
+ )
558
+ info_label.setWordWrap(True)
559
+ layout.addWidget(info_label)
560
+
561
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
562
+ QDialogButtonBox.StandardButton.Cancel)
563
+ button_box.accepted.connect(self.accept)
564
+ button_box.rejected.connect(self.reject)
565
+ layout.addWidget(button_box)
566
+
567
+ apply_form_spacing(self)
568
+
569
+ def get_settings(self):
570
+ """(列名のリスト, 誤差の種類 'SD' / 'SEM' / '95%CI', 出力の列名のもと)"""
571
+ selected = []
572
+ for i in range(self.column_list.count()):
573
+ item = self.column_list.item(i)
574
+ if item.checkState() == Qt.CheckState.Checked:
575
+ selected.append(item.text())
576
+ stat_type = self.stat_combo.currentText().split(" ")[0] # "SD (標準偏差)" -> "SD"
577
+ return selected, stat_type, self.base_name_edit.text().strip()