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,424 @@
1
+ """
2
+ データセットの一覧(ツリー)の管理: 追加・削除・複製、フォルダ、表示/非表示、並べ替え。
3
+ データセットに対する個々の操作は graphica.gui.datasets の機能ごとのクラスにある。
4
+
5
+ ツリーの項目の UserRole には、データセットなら Dataset そのもの、フォルダなら None が入る。
6
+ 選択を読むときは行番号ではなく _get_current_dataset() / _get_selected_datasets() を使う
7
+ (フォルダが入れ子になっていてもデータセットだけを扱えるように)。
8
+ """
9
+ import copy
10
+ import logging
11
+ import numpy as np
12
+ import pandas as pd
13
+ from PySide6.QtCore import Qt, QTimer
14
+ from PySide6.QtWidgets import (QDialog, QMenu)
15
+
16
+ from graphica.gui import notify
17
+ from graphica.core.commands import (SetDatasetPropertiesCommand, ReorderDatasetsCommand)
18
+ from graphica.core.dataset import Dataset
19
+ from graphica.gui.datasets.order import DatasetOrder
20
+ from graphica.gui.workers import BUILTIN_DATA_FILE_EXTENSIONS
21
+ from graphica.core.plugin_api import get_registered_importer_extensions
22
+ from graphica.gui.data_editor import DataEditorDialog
23
+ from graphica.gui.datasets.actions_menu import populate_dataset_actions_menu
24
+ from graphica.gui.dialogs import (NewDatasetDialog)
25
+ from graphica.gui.dataset_style_icon import (
26
+ make_dataset_style_icon, make_dataset_visibility_icon, apply_dataset_visibility_text_style,
27
+ DATASET_TREE_VISIBILITY_COLUMN,
28
+ )
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class DatasetTreePanel:
34
+ """状態はタブが持ち、この部品はメソッドと信号の配線だけを持つ。PlotterApp の同じ名前は窓口(gui/panels/__init__.py)。"""
35
+
36
+ # PlotterApp から同じ名前で使えるもの(テスト・メニュー・ほかの部品が使う)
37
+ EXPOSED_NAMES = (
38
+ '_connect_dataset_signals', '_find_dataset_row', '_on_add_dataset', '_on_create_new_dataset',
39
+ '_on_dataset_order_applied', '_on_dataset_rows_moved', '_on_dataset_search_changed',
40
+ '_on_dataset_tree_context_menu', '_on_dataset_tree_item_clicked', '_on_duplicate_dataset',
41
+ '_on_hide_all_datasets', '_on_hide_all_in_folder', '_on_new_folder', '_on_remove_dataset',
42
+ '_on_rename_dataset_folder', '_on_show_all_datasets', '_on_show_all_in_folder', '_on_show_data_editor',
43
+ '_push_dataset_property_command', '_refresh_after_dataset_property_change', '_set_all_datasets_visibility',
44
+ '_set_folder_datasets_visibility', '_sync_dataset_list_and_replot', '_top_level_selected_items',
45
+ )
46
+
47
+ def __init__(self, app):
48
+ self._app = app
49
+
50
+ def _on_add_dataset(self):
51
+ # プラグインが登録した拡張子も選べるようにする
52
+ plugin_extensions = get_registered_importer_extensions()
53
+ plugin_pattern = ''.join(f' *{ext}' for ext in plugin_extensions)
54
+ builtin_pattern = ' '.join(f'*{ext}' for ext in BUILTIN_DATA_FILE_EXTENSIONS)
55
+ file_path, _ = notify.get_open_file_name(
56
+ self._app, "データファイルを選択", "",
57
+ f"Data Files ({builtin_pattern}{plugin_pattern});;All Files (*)"
58
+ )
59
+ if file_path:
60
+ self._app.load_data(file_path)
61
+
62
+ def _on_create_new_dataset(self):
63
+ """名前・列・行数を指定した空のデータセットを作り、そのままデータエディタで手入力させる。"""
64
+ dialog = NewDatasetDialog(self._app)
65
+ if dialog.exec() != QDialog.DialogCode.Accepted:
66
+ return
67
+
68
+ name = dialog.get_dataset_name()
69
+ column_names = dialog.get_column_names()
70
+ row_count = dialog.get_row_count()
71
+
72
+ df = pd.DataFrame({col: [np.nan] * row_count for col in column_names})
73
+ x_col_name = column_names[0]
74
+ y_col_name = column_names[1] if len(column_names) > 1 else column_names[0]
75
+
76
+ new_dataset = Dataset(name=name, df=df, x_col_name=x_col_name, y_col_name=y_col_name)
77
+ self._app._add_dataset_with_undo(new_dataset, self._app._get_target_folder_for_new_dataset(),
78
+ description="新規データセットの作成")
79
+
80
+ self._on_show_data_editor()
81
+
82
+ def _on_dataset_search_changed(self, text):
83
+ """名前に検索文字列を含むデータセットだけを出す。フォルダは中に1つでも当たりがあれば出す。"""
84
+ self._app.dataset_order.filter_by_name(text)
85
+
86
+ def _on_new_folder(self):
87
+ """選択中がフォルダならその中に、そうでなければ一番上に作る。"""
88
+ name, ok = notify.get_text(self._app, "新しいフォルダ", "フォルダ名:", text="新しいフォルダ")
89
+ if not ok or not name:
90
+ return
91
+
92
+ current_item = self._app.ui.dataset_list_widget.currentItem()
93
+ parent_item = None
94
+ if current_item is not None and current_item.data(0, Qt.ItemDataRole.UserRole) is None:
95
+ parent_item = current_item
96
+
97
+ self._app._add_dataset_folder_item(name, parent_item)
98
+
99
+ def _on_rename_dataset_folder(self):
100
+ """
101
+ フォルダの構造は Undo の対象外なので、名前の変更も Undo しない。
102
+ 保存時はツリーの表示文字列を読むので、setText だけで保存にも反映される。
103
+ """
104
+ current_item = self._app.ui.dataset_list_widget.currentItem()
105
+ if current_item is None or current_item.data(0, Qt.ItemDataRole.UserRole) is not None:
106
+ return
107
+ old_name = current_item.text(0)
108
+ new_name, ok = notify.get_text(self._app, "フォルダ名を変更", "新しいフォルダ名:", text=old_name)
109
+ if not ok or not new_name:
110
+ return
111
+ current_item.setText(0, new_name)
112
+
113
+ def _set_folder_datasets_visibility(self, folder_item, visible):
114
+ """フォルダの中(サブフォルダも)のデータセットをまとめて表示/非表示にする(Undo 1回分)。"""
115
+ dataset_items = self._app._flatten_dataset_tree(folder_item)
116
+ datasets = [item.data(0, Qt.ItemDataRole.UserRole) for item in dataset_items]
117
+ if not datasets:
118
+ return
119
+ is_batch = len(datasets) > 1
120
+ if is_batch:
121
+ self._app.undo_stack.beginMacro(f"フォルダ内の表示/非表示切替 ({len(datasets)}件)")
122
+ for ds in datasets:
123
+ self._push_dataset_property_command(
124
+ ds, {'visible': ds.visible}, {'visible': visible},
125
+ description="データセットの表示/非表示切替"
126
+ )
127
+ if is_batch:
128
+ self._app.undo_stack.endMacro()
129
+
130
+ def _set_all_datasets_visibility(self, visible):
131
+ """フォルダに関係なく、全データセットをまとめて表示/非表示にする(Undo 1回分)。"""
132
+ datasets = list(self._app.project.datasets)
133
+ if not datasets:
134
+ return
135
+ is_batch = len(datasets) > 1
136
+ if is_batch:
137
+ self._app.undo_stack.beginMacro(f"全データセットの表示/非表示切替 ({len(datasets)}件)")
138
+ for ds in datasets:
139
+ self._push_dataset_property_command(
140
+ ds, {'visible': ds.visible}, {'visible': visible},
141
+ description="データセットの表示/非表示切替"
142
+ )
143
+ if is_batch:
144
+ self._app.undo_stack.endMacro()
145
+
146
+ def _on_show_all_datasets(self):
147
+ self._set_all_datasets_visibility(True)
148
+
149
+ def _on_hide_all_datasets(self):
150
+ self._set_all_datasets_visibility(False)
151
+
152
+ def _on_show_all_in_folder(self):
153
+ current_item = self._app.ui.dataset_list_widget.currentItem()
154
+ if current_item is None:
155
+ return
156
+ self._set_folder_datasets_visibility(current_item, True)
157
+
158
+ def _on_hide_all_in_folder(self):
159
+ current_item = self._app.ui.dataset_list_widget.currentItem()
160
+ if current_item is None:
161
+ return
162
+ self._set_folder_datasets_visibility(current_item, False)
163
+
164
+ def _on_dataset_tree_context_menu(self, pos):
165
+ menu = QMenu(self._app)
166
+ populate_dataset_actions_menu(self._app, menu)
167
+ menu.exec(self._app.ui.dataset_list_widget.viewport().mapToGlobal(pos))
168
+
169
+ def _top_level_selected_items(self, items):
170
+ """ほかの選択項目の子孫でないものだけを返す(フォルダと中身が両方選ばれていても二重に消さないため)。"""
171
+ return DatasetOrder.top_level_items(items)
172
+
173
+ def _on_remove_dataset(self):
174
+ """選択中のデータセットとフォルダ(中身ごと)を Undo できる形で消す。"""
175
+ selected_items = self._app.ui.dataset_list_widget.selectedItems()
176
+ if not selected_items:
177
+ return
178
+
179
+ self._app._remove_dataset_items_with_undo(self._top_level_selected_items(selected_items))
180
+
181
+ def _find_dataset_row(self, dataset):
182
+ return self._app.dataset_order.find_row(dataset)
183
+
184
+ def _on_dataset_rows_moved(self, source_parent, source_start, source_end, dest_parent, dest_row):
185
+ """
186
+ ツリーのドラッグ&ドロップ後、project.datasets をツリーの並び(=描画の重なり順)に合わせる。
187
+ 同じフォルダ内の並べ替えだけ Undo でき、フォルダをまたぐ移動はフォルダ構造と同じく Undo の対象外。
188
+ """
189
+ reordered = self._app.dataset_order.tree_order()
190
+
191
+ if len(reordered) != len(self._app.project.datasets):
192
+ logger.warning(
193
+ "データセットの並べ替え同期に失敗しました (表示数 %d, 実際 %d)。",
194
+ len(reordered), len(self._app.project.datasets)
195
+ )
196
+ return
197
+
198
+ old_order = list(self._app.project.datasets)
199
+ # Dataset の == は使えないので同一性で比べる
200
+ if [id(d) for d in reordered] == [id(d) for d in old_order]:
201
+ return
202
+
203
+ if source_parent == dest_parent:
204
+ command = ReorderDatasetsCommand(
205
+ self._app.project, old_order, reordered,
206
+ on_applied=self._on_dataset_order_applied,
207
+ description="データセットの並べ替え"
208
+ )
209
+ self._app.undo_stack.push(command)
210
+ else:
211
+ self._app.project.datasets = reordered
212
+ self._app._update_plot()
213
+
214
+ def _on_dataset_order_applied(self):
215
+ """ドロップ処理の最中にツリーを触ると Qt の処理とぶつかるので、同期と再描画は次のイベントループに回す。"""
216
+ QTimer.singleShot(0, self._sync_dataset_list_and_replot)
217
+
218
+ def _sync_dataset_list_and_replot(self):
219
+ self._app._sync_dataset_list_widget_order()
220
+ self._app._update_plot()
221
+
222
+ def _refresh_after_dataset_property_change(self, dataset, changed_keys=(), old_values=None, new_values=None):
223
+ """
224
+ Undo/Redo でデータセットの属性が変わった後に、ツリーの表示・プロパティ欄・描画を合わせる。
225
+ 描き直すのは関係する軸だけ。on_applied は Undo と Redo のどちらの後かを教えないので、
226
+ subplot_target の新旧どちらの軸も描き直す。
227
+ """
228
+ item = self._app._get_dataset_tree_item(dataset)
229
+ if item is not None:
230
+ if item.text(0) != dataset.name:
231
+ item.setText(0, dataset.name)
232
+ item.setIcon(0, make_dataset_style_icon(dataset))
233
+ item.setIcon(DATASET_TREE_VISIBILITY_COLUMN, make_dataset_visibility_icon(dataset))
234
+ apply_dataset_visibility_text_style(item, dataset)
235
+ if item is self._app.ui.dataset_list_widget.currentItem():
236
+ self._app.property_panel.update_ui_state()
237
+
238
+ axis_index = dataset.subplot_target
239
+ if axis_index >= len(self._app.canvas.all_axes) or axis_index >= len(self._app.project.all_plot_settings):
240
+ self._app._update_plot()
241
+ return
242
+
243
+ axis_indices_to_refresh = {axis_index}
244
+ if 'subplot_target' in changed_keys:
245
+ if old_values and 'subplot_target' in old_values:
246
+ axis_indices_to_refresh.add(old_values['subplot_target'])
247
+ if new_values and 'subplot_target' in new_values:
248
+ axis_indices_to_refresh.add(new_values['subplot_target'])
249
+ axis_indices_to_refresh = {
250
+ i for i in axis_indices_to_refresh
251
+ if i < len(self._app.canvas.all_axes) and i < len(self._app.project.all_plot_settings)
252
+ }
253
+
254
+ layout_mode = getattr(self._app.project, 'layout_mode', 'grid')
255
+ if layout_mode == 'free':
256
+ rows, cols = 0, 0
257
+ else:
258
+ rows = self._app.subplot_rows_spinbox.value()
259
+ cols = self._app.subplot_cols_spinbox.value()
260
+
261
+ for idx in axis_indices_to_refresh:
262
+ self._app.canvas.update_single_axis(
263
+ idx, self._app.project.datasets, self._app.project.all_plot_settings[idx],
264
+ rows=rows, cols=cols,
265
+ share_x_axis=getattr(self._app.project, 'share_x_axis', False),
266
+ share_y_axis=getattr(self._app.project, 'share_y_axis', False),
267
+ panel_labels_enabled=self._app.project.panel_labels_enabled,
268
+ )
269
+ is_secondary_visible = any(sa is not None for sa in self._app.canvas.all_secondary_axes)
270
+ self._app.tick_direction_y2_label.setVisible(is_secondary_visible)
271
+ self._app.major_tick_direction_y2_combo.setVisible(is_secondary_visible)
272
+ self._app.minor_tick_direction_y2_combo.setVisible(is_secondary_visible)
273
+ self._app.y2_label_text_label.setVisible(is_secondary_visible)
274
+ self._app.y2_label_text_edit.setVisible(is_secondary_visible)
275
+
276
+ self._app._reapply_editor_row_highlight()
277
+ self._app._refresh_minimap()
278
+ if hasattr(self._app, 'export_preview_panel'):
279
+ self._app.export_preview_panel.refresh_preview()
280
+ self._app._notify_plugins_datasets_changed()
281
+
282
+ def _push_dataset_property_command(self, dataset, old_values: dict, new_values: dict, description: str,
283
+ skip_if_unchanged=True):
284
+ """
285
+ Dataset のプロパティ変更を Undo/Redo 可能なコマンドとして発行する共通ヘルパー。
286
+ old_values と new_values が同じ (実質的に変更なし) 場合は何もしない。
287
+ 値に DataFrame を含むときは == で比べられないので skip_if_unchanged=False で呼ぶ。
288
+ """
289
+ if skip_if_unchanged and old_values == new_values:
290
+ return
291
+ command = SetDatasetPropertiesCommand(
292
+ dataset, old_values, new_values,
293
+ on_applied=lambda: self._refresh_after_dataset_property_change(
294
+ dataset, changed_keys=new_values.keys(), old_values=old_values, new_values=new_values
295
+ ),
296
+ description=description
297
+ )
298
+ self._app.undo_stack.push(command)
299
+
300
+ def _on_duplicate_dataset(self):
301
+ """選択中のデータセットを、元と同じフォルダに複製する。"""
302
+ selected_items = [
303
+ item for item in self._app.ui.dataset_list_widget.selectedItems()
304
+ if item.data(0, Qt.ItemDataRole.UserRole) is not None
305
+ ]
306
+ if not selected_items:
307
+ return
308
+
309
+ copies = []
310
+ for item in selected_items:
311
+ original_dataset = item.data(0, Qt.ItemDataRole.UserRole)
312
+ # DataFrame まで独立させる
313
+ new_dataset = copy.deepcopy(original_dataset)
314
+ new_dataset.name = f"{original_dataset.name} (copy)"
315
+ copies.append((new_dataset, item.parent()))
316
+
317
+ def add():
318
+ tree = self._app.ui.dataset_list_widget
319
+ tree.blockSignals(True)
320
+ new_items = [self._app.dataset_order.append(new_dataset, folder) for new_dataset, folder in copies]
321
+ tree.clearSelection()
322
+ for new_item in new_items:
323
+ new_item.setSelected(True)
324
+ tree.setCurrentItem(new_items[-1])
325
+ tree.blockSignals(False)
326
+ self._app.property_panel.update_ui_state()
327
+ self._app._update_plot()
328
+
329
+ self._app._push_dataset_additions(add, [new_dataset for new_dataset, _ in copies], "データセットの複製")
330
+
331
+ def _on_show_data_editor(self):
332
+ dataset = self._app._get_current_dataset()
333
+ if dataset is None:
334
+ return
335
+
336
+ # 同じデータセットのエディタが開いていれば、作り直さず(並べ替え・位置・選択が消えるので)前に出すだけ
337
+ if self._app.data_editor_dialog is not None and self._app.data_editor_dialog.dataset is dataset:
338
+ self._app.data_editor_dialog.show()
339
+ self._app.data_editor_dialog.raise_()
340
+ self._app.data_editor_dialog.activateWindow()
341
+ return
342
+
343
+ if self._app.data_editor_dialog:
344
+ self._app.data_editor_dialog.close()
345
+ # close() は隠すだけで、親(このタブ)にぶら下がったまま残り続けるので破棄する
346
+ self._app.data_editor_dialog.deleteLater()
347
+ del self._app.data_editor_dialog
348
+ self._app.data_editor_dialog = None
349
+
350
+ self._app.data_editor_dialog = DataEditorDialog(dataset, self._app)
351
+
352
+ self._app.data_editor_dialog.dataChanged.connect(self._app.property_panel.on_data_structure_changed)
353
+
354
+ # エディタで選んだ行をグラフ上で強調する
355
+ self._app.data_editor_dialog.rowsHighlighted.connect(self._app._on_editor_rows_highlighted)
356
+
357
+ # 開いたままグラフを操作できるよう非モーダル
358
+ self._app.data_editor_dialog.show()
359
+
360
+ def _on_dataset_tree_item_clicked(self, item, column):
361
+ """
362
+ 目アイコンの列のクリックで表示/非表示を切り替える。
363
+ クリックした項目が複数選択に含まれていれば、選択中のすべてをまとめて切り替える。
364
+ """
365
+ if column != DATASET_TREE_VISIBILITY_COLUMN:
366
+ return
367
+ dataset = item.data(0, Qt.ItemDataRole.UserRole)
368
+ if dataset is None:
369
+ return
370
+
371
+ new_value = not dataset.visible
372
+
373
+ # Dataset の == は使えないので、in ではなく同一性で調べる
374
+ selected_datasets = self._app._get_selected_datasets()
375
+ is_part_of_multi_selection = len(selected_datasets) > 1 and any(
376
+ ds is dataset for ds in selected_datasets
377
+ )
378
+ targets = selected_datasets if is_part_of_multi_selection else [dataset]
379
+
380
+ is_batch = len(targets) > 1
381
+ if is_batch:
382
+ self._app.undo_stack.beginMacro(f"表示/非表示の一括切替 ({len(targets)}件)")
383
+ for ds in targets:
384
+ self._push_dataset_property_command(
385
+ ds,
386
+ {'visible': ds.visible},
387
+ {'visible': new_value},
388
+ description="データセットの表示/非表示切替"
389
+ )
390
+ if is_batch:
391
+ self._app.undo_stack.endMacro()
392
+
393
+ def _connect_dataset_signals(self):
394
+ self._app.ui.add_dataset_button.clicked.connect(self._on_add_dataset)
395
+ self._app.new_dataset_button.clicked.connect(self._on_create_new_dataset)
396
+ self._app.ui.remove_dataset_button.clicked.connect(self._on_remove_dataset)
397
+ self._app.new_folder_button.clicked.connect(self._on_new_folder)
398
+ self._app.dataset_search_edit.textChanged.connect(self._on_dataset_search_changed)
399
+ self._app.ui.dataset_list_widget.currentItemChanged.connect(self._app.property_panel.on_dataset_selected)
400
+ self._app.ui.dataset_list_widget.customContextMenuRequested.connect(self._on_dataset_tree_context_menu)
401
+ self._app.ui.dataset_list_widget.itemClicked.connect(self._on_dataset_tree_item_clicked)
402
+ # ドラッグでの並べ替え(描画の重なり順)を project.datasets に合わせる
403
+ self._app.ui.dataset_list_widget.model().rowsMoved.connect(self._on_dataset_rows_moved)
404
+
405
+
406
+ # プロパティの欄は表(gui/dataset_bindings.py)の順につなぐ
407
+ self._app.property_panel.connect_signals()
408
+
409
+ self._app.fit_curve_button.clicked.connect(self._app.fitting.fit_current_dataset)
410
+ self._app.find_peaks_button.clicked.connect(self._app.peaks.find_peaks)
411
+ self._app.multi_peak_fit_button.clicked.connect(self._app.fitting.multi_peak_fit_current_dataset)
412
+
413
+ self._app.duplicate_dataset_button.clicked.connect(self._on_duplicate_dataset)
414
+ self._app.auto_color_button.clicked.connect(self._app.colors.auto_assign_colors)
415
+ self._app.manage_palette_action.triggered.connect(self._app.colors.manage_palettes)
416
+ self._app.colormap_assign_action.triggered.connect(self._app.colors.auto_assign_colors_from_colormap)
417
+ self._app.view_edit_data_button.clicked.connect(self._on_show_data_editor)
418
+
419
+ self._app.x_col_combo.currentTextChanged.connect(self._app.property_panel.on_plot_column_changed)
420
+ self._app.y_col_combo.currentTextChanged.connect(self._app.property_panel.on_plot_column_changed)
421
+ self._app.x_err_col_combo.currentTextChanged.connect(self._app.property_panel.on_error_column_changed)
422
+ self._app.y_err_col_combo.currentTextChanged.connect(self._app.property_panel.on_error_column_changed)
423
+
424
+ self._app.z_col_combo.currentTextChanged.connect(self._app.property_panel.on_z_column_changed)
@@ -0,0 +1,131 @@
1
+ """
2
+ 組み込みの plot_type ごとの描き方。プラグインの register_plot_type と同じく「種類名 → 描画関数」の表で引く。
3
+
4
+ 描画関数は (canvas, ax, ds, x, y, plot_kwargs, axis_index) を受け取り、ds.artist にする artist を返す。
5
+ x, y はウォーターフォールのずらし・欠損値の処理・表示用の間引きを済ませた点列。
6
+ plot_kwargs(ウォーターフォールの zorder)はグラデーションの線には渡さない。
7
+ """
8
+ import logging
9
+
10
+ import numpy as np
11
+ from scipy.stats import gaussian_kde
12
+
13
+ from graphica.core.dataset import COLOR_BY_COLUMN_PLOT_TYPE
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def _uses_line_gradient(ds):
19
+ return ds.gradient_enabled and ds.gradient_target in ('line', 'both')
20
+
21
+
22
+ def _gradient_line(canvas, ax, ds, x, y):
23
+ return canvas._add_gradient_line(
24
+ ax, x, y, ds.color, ds.gradient_color2, ds.linewidth, ds.alpha, ds.linestyle, label=ds.name)
25
+
26
+
27
+ def _plain_line(ax, ds, x, y, plot_kwargs, **extra):
28
+ (artist,) = ax.plot(x, y, color=ds.color, linestyle=ds.linestyle, linewidth=ds.linewidth,
29
+ alpha=ds.alpha, label=ds.name, **extra, **plot_kwargs)
30
+ return artist
31
+
32
+
33
+ def _markers(ax, ds, x, y, plot_kwargs, **extra):
34
+ return ax.scatter(x, y, color=ds.color, marker=ds.marker, s=ds.markersize**2, alpha=ds.alpha,
35
+ **extra, **plot_kwargs)
36
+
37
+
38
+ def draw_line(canvas, ax, ds, x, y, plot_kwargs, axis_index):
39
+ if _uses_line_gradient(ds):
40
+ return _gradient_line(canvas, ax, ds, x, y)
41
+ return _plain_line(ax, ds, x, y, plot_kwargs)
42
+
43
+
44
+ def draw_scatter(canvas, ax, ds, x, y, plot_kwargs, axis_index):
45
+ return _markers(ax, ds, x, y, plot_kwargs, label=ds.name)
46
+
47
+
48
+ def draw_line_and_scatter(canvas, ax, ds, x, y, plot_kwargs, axis_index):
49
+ if _uses_line_gradient(ds):
50
+ # LineCollection はマーカーを描けないので、マーカーは単色で重ねる
51
+ artist = _gradient_line(canvas, ax, ds, x, y)
52
+ _markers(ax, ds, x, y, plot_kwargs)
53
+ return artist
54
+ (artist,) = ax.plot(x, y, color=ds.color, linestyle=ds.linestyle, linewidth=ds.linewidth,
55
+ marker=ds.marker, markersize=ds.markersize, alpha=ds.alpha, label=ds.name, **plot_kwargs)
56
+ return artist
57
+
58
+
59
+ def draw_area(canvas, ax, ds, x, y, plot_kwargs, axis_index):
60
+ """0 との間を塗り、輪郭が分かるよう上端に線も重ねる。"""
61
+ if not ds.gradient_enabled:
62
+ artist = ax.fill_between(x, y, 0, color=ds.color, alpha=ds.alpha * 0.4, label=ds.name, **plot_kwargs)
63
+ ax.plot(x, y, color=ds.color, linestyle=ds.linestyle, linewidth=ds.linewidth, alpha=ds.alpha, **plot_kwargs)
64
+ return artist
65
+ if ds.gradient_target in ('fill', 'both'):
66
+ artist = canvas._add_gradient_fill(ax, x, y, ds.color, ds.gradient_color2, ds.alpha * 0.4)
67
+ else:
68
+ artist = ax.fill_between(x, y, 0, color=ds.color, alpha=ds.alpha * 0.4, **plot_kwargs)
69
+ if ds.gradient_target in ('line', 'both'):
70
+ _gradient_line(canvas, ax, ds, x, y)
71
+ else:
72
+ _plain_line(ax, ds, x, y, plot_kwargs)
73
+ return artist
74
+
75
+
76
+ def draw_bar(canvas, ax, ds, x, y, plot_kwargs, axis_index):
77
+ return ax.bar(x, y, color=ds.color, alpha=ds.alpha, label=ds.name, **plot_kwargs)
78
+
79
+
80
+ def draw_step(canvas, ax, ds, x, y, plot_kwargs, axis_index):
81
+ # 区間の左端の値を右端まで保つ(サンプリング・イベントデータの慣習)
82
+ return _plain_line(ax, ds, x, y, plot_kwargs, drawstyle='steps-post')
83
+
84
+
85
+ def draw_density_scatter(canvas, ax, ds, x, y, plot_kwargs, axis_index):
86
+ """各点の位置での2次元カーネル密度を色にする。点が少ない・全点が同じ座標だと KDE が失敗するので単色にする。"""
87
+ try:
88
+ if len(x) < 3:
89
+ raise ValueError("点数不足")
90
+ xy = np.vstack([x, y])
91
+ density = gaussian_kde(xy)(xy)
92
+ return ax.scatter(x, y, c=density, cmap=ds.colormap, marker=ds.marker, s=ds.markersize**2,
93
+ alpha=ds.alpha, label=ds.name, **plot_kwargs)
94
+ except (np.linalg.LinAlgError, ValueError):
95
+ return _markers(ax, ds, x, y, plot_kwargs, label=ds.name)
96
+
97
+
98
+ def draw_color_by_column(canvas, ax, ds, x, y, plot_kwargs, axis_index):
99
+ """
100
+ z_col_name の列の値で点を色分けする。z は visible_df 経由(ds.z_data)で取る
101
+ (df から取ると、行を除外したとたん点と色が1つずつずれる)。
102
+ """
103
+ z_values = ds.z_data
104
+ if z_values is not None and len(z_values) == len(x):
105
+ artist = ax.scatter(x, y, c=z_values, cmap=ds.colormap, vmin=ds.vmin, vmax=ds.vmax,
106
+ marker=ds.marker, s=ds.markersize**2, alpha=ds.alpha, label=ds.name, **plot_kwargs)
107
+ # カラーバーは1軸に1つ。2Dマップが先に登録していればそちらを優先する
108
+ if axis_index not in canvas._axis_2d_mappables:
109
+ canvas._axis_2d_mappables[axis_index] = artist
110
+ return artist
111
+ if z_values is not None:
112
+ # 欠損値の方針 'drop' で点が減ったときなど。黙って単色になると理由が分からないのでログに残す
113
+ logger.warning(
114
+ "'%s' のZ列の長さ(%d)がプロット点数(%d)と一致しないため、"
115
+ "単色のScatterとして描画します(欠損値の方針'drop'等で"
116
+ "配列が短くなっている可能性があります)。",
117
+ ds.name, len(z_values), len(x),
118
+ )
119
+ return _markers(ax, ds, x, y, plot_kwargs, label=ds.name)
120
+
121
+
122
+ BUILTIN_PLOT_TYPE_DRAWERS = {
123
+ 'Line': draw_line,
124
+ 'Scatter': draw_scatter,
125
+ 'Line+Scatter': draw_line_and_scatter,
126
+ 'Area': draw_area,
127
+ 'Bar': draw_bar,
128
+ 'Step': draw_step,
129
+ 'Density Scatter': draw_density_scatter,
130
+ COLOR_BY_COLUMN_PLOT_TYPE: draw_color_by_column,
131
+ }
@@ -0,0 +1,119 @@
1
+ """PluginContext の本体側の実装。1つのタブ(PlotterApp)と1つのプラグインの組ごとに作る。"""
2
+ import logging
3
+ import weakref
4
+ from dataclasses import fields
5
+
6
+
7
+ from graphica.gui import notify
8
+ from graphica.core.app_paths import get_plugin_data_dir
9
+ from graphica.core.color_palettes import normalize_palettes
10
+ from graphica.core.commands import SetDatasetPropertiesCommand
11
+ from graphica.core.dataset import Dataset
12
+ from graphica.core.named_colors import add_named_color, load_named_colors, save_named_colors
13
+ from graphica.core.plugin_context import PluginContext
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ _DATASET_FIELDS = frozenset(f.name for f in fields(Dataset))
18
+
19
+
20
+ class TabPluginContext(PluginContext):
21
+
22
+ def __init__(self, app, plugin_name):
23
+ # タブを閉じたあとも窓口をプラグインが握っていることがあるので、タブは弱参照で持つ。
24
+ self._app_ref = weakref.ref(app)
25
+ self._plugin_name = plugin_name
26
+ self._datasets_changed_callbacks = []
27
+ self._selection_changed_callbacks = []
28
+
29
+ @property
30
+ def _app(self):
31
+ app = self._app_ref()
32
+ if app is None:
33
+ raise RuntimeError("このタブはすでに閉じられています。")
34
+ return app
35
+
36
+ def datasets(self):
37
+ return list(self._app.project.datasets)
38
+
39
+ def current_dataset(self):
40
+ return self._app.current_dataset()
41
+
42
+ def selected_datasets(self):
43
+ return self._app.selected_datasets()
44
+
45
+ def add_dataset(self, dataset, description=None):
46
+ self._app.add_dataset_with_undo(
47
+ dataset, description=description or f"[{self._plugin_name}] データセットの追加"
48
+ )
49
+
50
+ def set_dataset_properties(self, dataset, values, description=None):
51
+ unknown = sorted(set(values) - _DATASET_FIELDS)
52
+ if unknown:
53
+ raise AttributeError(f"Dataset に無い属性です: {', '.join(unknown)}")
54
+ app = self._app
55
+ new_values = dict(values)
56
+ old_values = {key: getattr(dataset, key) for key in new_values}
57
+ app.undo_stack.push(SetDatasetPropertiesCommand(
58
+ dataset, old_values, new_values,
59
+ on_applied=lambda: app.refresh_after_dataset_property_change(
60
+ dataset, changed_keys=new_values.keys(), old_values=old_values, new_values=new_values
61
+ ),
62
+ description=description or f"[{self._plugin_name}] プロパティの変更",
63
+ ))
64
+
65
+ def redraw(self):
66
+ self._app.redraw()
67
+
68
+ def on_datasets_changed(self, callback):
69
+ self._datasets_changed_callbacks.append(callback)
70
+
71
+ def on_selection_changed(self, callback):
72
+ self._selection_changed_callbacks.append(callback)
73
+
74
+ def _notify_datasets_changed(self):
75
+ for callback in list(self._datasets_changed_callbacks):
76
+ self._call_isolated(callback)
77
+
78
+ def _notify_selection_changed(self, dataset):
79
+ for callback in list(self._selection_changed_callbacks):
80
+ self._call_isolated(callback, dataset)
81
+
82
+ def _call_isolated(self, callback, *args):
83
+ # 通知先のプラグインが失敗しても、描画や選択の処理を止めない。
84
+ try:
85
+ callback(*args)
86
+ except Exception:
87
+ logger.exception("[plugin:%s] 通知の処理に失敗しました", self._plugin_name)
88
+
89
+ @property
90
+ def parent_widget(self):
91
+ return self._app_ref()
92
+
93
+ def show_message(self, text, title=None):
94
+ notify.information(self.parent_widget, title or self._plugin_name, text)
95
+
96
+ def show_error(self, text, title=None):
97
+ notify.warning(self.parent_widget, title or self._plugin_name, text)
98
+
99
+ @property
100
+ def data_dir(self):
101
+ return get_plugin_data_dir(self._plugin_name)
102
+
103
+ def named_colors(self):
104
+ return load_named_colors(self._app.settings)
105
+
106
+ def set_named_colors(self, entries):
107
+ validated = []
108
+ for entry in entries:
109
+ validated = add_named_color(validated, entry.get("name"), entry.get("color"))
110
+ save_named_colors(self._app.settings, validated)
111
+
112
+ def color_palettes(self):
113
+ return {name: list(colors) for name, colors in self._app.colors.load_palettes().items()}
114
+
115
+ def set_color_palettes(self, palettes):
116
+ self._app.colors.save_palettes(normalize_palettes(palettes))
117
+
118
+ def active_color_cycle(self):
119
+ return list(self._app.colors.active_color_cycle())