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,191 @@
1
+ """範囲選択モード: X の範囲をドラッグで選び、その範囲の点を今のデータセットのマスクに加える。
2
+
3
+ SpanSelector は Axes に束縛されるので、Axes を作り直す redraw_all() のたびに作り直すことになる。ほかのモードと同じく
4
+ キャンバス全体の button_press / motion / release で event.inaxes を読む。
5
+ """
6
+ import logging
7
+
8
+ from matplotlib.patches import Rectangle
9
+
10
+ from graphica.gui import notify
11
+ from graphica.core.commands import SetMaskedRowsCommand
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class RangeSelectTool:
17
+ """状態はこのツールが持ち、PlotterApp の同じ名前は窓口(gui/tools/__init__.py)。"""
18
+
19
+ # PlotterApp から同じ名前で読み書きできるもの(テスト・メニュー・ほかの mixin が使う)
20
+ EXPOSED_NAMES = (
21
+ '_apply_range_mask', '_clear_range_select_preview', '_on_range_select_motion', '_on_range_select_press',
22
+ '_on_range_select_release', '_range_select_axes', '_range_select_background', '_range_select_motion_cid',
23
+ '_range_select_press_cid', '_range_select_preview_artist', '_range_select_release_cid',
24
+ '_range_select_start_x', '_toggle_range_select_mode', 'range_select_mode_enabled',
25
+ )
26
+
27
+ def __init__(self, app):
28
+ self._app = app
29
+ self.range_select_mode_enabled = False
30
+ self._range_select_press_cid = None
31
+ self._range_select_motion_cid = None
32
+ self._range_select_release_cid = None
33
+ self._range_select_axes = None
34
+ self._range_select_start_x = None
35
+ self._range_select_preview_artist = None
36
+
37
+ def _toggle_range_select_mode(self, checked):
38
+ self.range_select_mode_enabled = checked
39
+
40
+ if checked:
41
+ self._app._deactivate_other_mouse_modes('range_select')
42
+
43
+ self._range_select_press_cid = self._app.canvas.mpl_connect(
44
+ 'button_press_event', self._on_range_select_press
45
+ )
46
+ self._range_select_motion_cid = self._app.canvas.mpl_connect(
47
+ 'motion_notify_event', self._on_range_select_motion
48
+ )
49
+ self._range_select_release_cid = self._app.canvas.mpl_connect(
50
+ 'button_release_event', self._on_range_select_release
51
+ )
52
+ self._app.statusBar().showMessage(
53
+ "範囲選択モード: カレントデータセット上でドラッグした範囲を"
54
+ "マスク(除外)します", 5000
55
+ )
56
+ else:
57
+ if getattr(self, '_range_select_press_cid', None) is not None:
58
+ self._app.canvas.mpl_disconnect(self._range_select_press_cid)
59
+ self._range_select_press_cid = None
60
+ if getattr(self, '_range_select_motion_cid', None) is not None:
61
+ self._app.canvas.mpl_disconnect(self._range_select_motion_cid)
62
+ self._range_select_motion_cid = None
63
+ if getattr(self, '_range_select_release_cid', None) is not None:
64
+ self._app.canvas.mpl_disconnect(self._range_select_release_cid)
65
+ self._range_select_release_cid = None
66
+ self._clear_range_select_preview()
67
+ self._range_select_axes = None
68
+ self._range_select_start_x = None
69
+
70
+ def _clear_range_select_preview(self):
71
+ """ドラッグ中のプレビューの矩形と、blit 用の背景を捨てる。描き直しで既に消えていても例外にしない。"""
72
+ artist = getattr(self, '_range_select_preview_artist', None)
73
+ if artist is not None:
74
+ try:
75
+ artist.remove()
76
+ except (ValueError, NotImplementedError):
77
+ pass
78
+ self._range_select_preview_artist = None
79
+ self._app.canvas.draw_idle()
80
+ self._range_select_background = None
81
+
82
+ def _on_range_select_press(self, event):
83
+ if not getattr(self, 'range_select_mode_enabled', False):
84
+ return
85
+ if event.button != 1 or event.inaxes is None or event.xdata is None:
86
+ return
87
+ self._range_select_axes = event.inaxes
88
+ self._range_select_start_x = event.xdata
89
+ # 始めに1回だけ背景を撮り、以降は選択の矩形だけを blit で描き直す(毎回全体を描くと系列が多いほど重い)
90
+ self._app.canvas.draw()
91
+ self._range_select_background = self._app.canvas.copy_from_bbox(event.inaxes.bbox)
92
+
93
+ def _on_range_select_motion(self, event):
94
+ axes = getattr(self, '_range_select_axes', None)
95
+ if axes is None or event.inaxes is not axes or event.xdata is None:
96
+ return
97
+
98
+ x0, x1 = sorted((self._range_select_start_x, event.xdata))
99
+ ymin, ymax = axes.get_ylim()
100
+
101
+ rect = getattr(self, '_range_select_preview_artist', None)
102
+ if rect is None:
103
+ # animated=True の Artist は draw() では描かれず、blit だけで出る。矩形は最初に1回作り、座標だけ変える
104
+ rect = Rectangle(
105
+ (x0, ymin), x1 - x0, ymax - ymin,
106
+ facecolor='#3948B3', alpha=0.15, edgecolor='#3948B3',
107
+ linewidth=1, zorder=100, animated=True,
108
+ )
109
+ axes.add_patch(rect)
110
+ self._range_select_preview_artist = rect
111
+ else:
112
+ rect.set_bounds(x0, ymin, x1 - x0, ymax - ymin)
113
+
114
+ background = getattr(self, '_range_select_background', None)
115
+ if background is None:
116
+ # 背景が撮れていなければ全体を描き直す
117
+ self._app.canvas.draw_idle()
118
+ return
119
+
120
+ self._app.canvas.restore_region(background)
121
+ axes.draw_artist(rect)
122
+ self._app.canvas.blit(axes.bbox)
123
+
124
+ def _on_range_select_release(self, event):
125
+ axes = getattr(self, '_range_select_axes', None)
126
+ if axes is None:
127
+ return
128
+
129
+ self._clear_range_select_preview()
130
+ start_x = self._range_select_start_x
131
+ self._range_select_axes = None
132
+ self._range_select_start_x = None
133
+
134
+ end_x = event.xdata if (event.inaxes is axes and event.xdata is not None) else None
135
+ if end_x is None or start_x is None or start_x == end_x:
136
+ return # クリックだけ(ドラッグなし)は選択とみなさない
137
+
138
+ x_min, x_max = sorted((start_x, end_x))
139
+ self._apply_range_mask(axes, x_min, x_max)
140
+
141
+ def _apply_range_mask(self, axes, x_min, x_max):
142
+ """[x_min, x_max] の点をマスクに加える。今のデータセットがこの軸に描かれていなければ、何もせず案内を出す。"""
143
+ dataset = self._app._get_current_dataset()
144
+ if dataset is None:
145
+ notify.information(self._app, "範囲選択", "マスク対象のデータセットを選択してください。")
146
+ return
147
+
148
+ target_axis = dataset.subplot_target
149
+ if dataset.use_secondary_y:
150
+ expected_axes = (
151
+ self._app.all_secondary_axes[target_axis]
152
+ if 0 <= target_axis < len(self._app.all_secondary_axes) else None
153
+ )
154
+ else:
155
+ expected_axes = (
156
+ self._app.all_axes[target_axis]
157
+ if 0 <= target_axis < len(self._app.all_axes) else None
158
+ )
159
+
160
+ if axes is not expected_axes:
161
+ notify.information(
162
+ self._app, "範囲選択",
163
+ "ドラッグしたサブプロットに、選択中のデータセットが描画されていません。"
164
+ )
165
+ return
166
+
167
+ # ドラッグの x はウォーターフォールのずらしが掛かった表示座標なので、データ座標に戻して比べる
168
+ data_x_min, _ = self._app.canvas.display_to_data(dataset, x_min, 0.0)
169
+ data_x_max, _ = self._app.canvas.display_to_data(dataset, x_max, 0.0)
170
+
171
+ x_data = dataset.x_data
172
+ visible_index = dataset.visible_df.index
173
+ in_range_mask = (x_data >= data_x_min) & (x_data <= data_x_max)
174
+ newly_masked = [int(idx) for idx, flag in zip(visible_index, in_range_mask) if flag]
175
+ if not newly_masked:
176
+ return
177
+
178
+ old_masked = list(dataset.masked_row_indices)
179
+ new_masked = sorted(set(old_masked) | set(newly_masked))
180
+ if new_masked == old_masked:
181
+ return
182
+
183
+ command = SetMaskedRowsCommand(
184
+ dataset, old_masked, new_masked,
185
+ description=f"範囲選択でのマスク({len(newly_masked)}件)",
186
+ )
187
+ self._app.undo_stack.push(command)
188
+ self._app._update_plot()
189
+ self._app.statusBar().showMessage(
190
+ f"「{dataset.name}」の{len(newly_masked)}点をマスクしました", 3000
191
+ )
@@ -0,0 +1,222 @@
1
+ """領域強調モード: X 方向に大きくドラッグすると縦の帯、Y 方向なら横の帯を足す。
2
+
3
+ 帯は注釈と同じ 'annotations' に type='vspan' / 'hspan' で入れるので、保存・Undo・描画は注釈と共有する。
4
+ データ座標で持つので、データを読み直しても位置は変わらない。
5
+ """
6
+ import logging
7
+
8
+ from matplotlib.patches import Rectangle
9
+ from PySide6.QtWidgets import QMessageBox
10
+
11
+ from graphica.gui import notify
12
+ from graphica.core.axis_settings import axis_setting
13
+ from graphica.core.commands import SetAnnotationsCommand
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # これより動かなければクリック(誤クリックで極小の帯ができないように)
18
+ REGION_HIGHLIGHT_DRAG_THRESHOLD_PX = 5
19
+
20
+ REGION_HIGHLIGHT_DEFAULT_COLOR = '#F2A72B'
21
+ REGION_HIGHLIGHT_DEFAULT_ALPHA = 0.18
22
+
23
+
24
+ class RegionHighlightTool:
25
+ """状態はこのツールが持ち、PlotterApp の同じ名前は窓口(gui/tools/__init__.py)。"""
26
+
27
+ # PlotterApp から同じ名前で読み書きできるもの(テスト・メニュー・ほかの mixin が使う)
28
+ EXPOSED_NAMES = (
29
+ '_clear_region_highlight_preview', '_on_region_highlight_motion', '_on_region_highlight_press',
30
+ '_on_region_highlight_release', '_region_highlight_axes', '_region_highlight_motion_cid',
31
+ '_region_highlight_orientation', '_region_highlight_press_cid', '_region_highlight_preview_artist',
32
+ '_region_highlight_release_cid', '_region_highlight_start', '_toggle_region_highlight_mode',
33
+ '_try_delete_region_near', 'region_highlight_mode_enabled',
34
+ )
35
+
36
+ def __init__(self, app):
37
+ self._app = app
38
+ self.region_highlight_mode_enabled = False
39
+ self._region_highlight_press_cid = None
40
+ self._region_highlight_motion_cid = None
41
+ self._region_highlight_release_cid = None
42
+ self._region_highlight_axes = None
43
+ self._region_highlight_start = None # (x, y) データ座標
44
+ self._region_highlight_preview_artist = None
45
+
46
+ def _toggle_region_highlight_mode(self, checked):
47
+ self.region_highlight_mode_enabled = checked
48
+
49
+ if checked:
50
+ self._app._deactivate_other_mouse_modes('region_highlight')
51
+
52
+ self._region_highlight_press_cid = self._app.canvas.mpl_connect(
53
+ 'button_press_event', self._on_region_highlight_press
54
+ )
55
+ self._region_highlight_motion_cid = self._app.canvas.mpl_connect(
56
+ 'motion_notify_event', self._on_region_highlight_motion
57
+ )
58
+ self._region_highlight_release_cid = self._app.canvas.mpl_connect(
59
+ 'button_release_event', self._on_region_highlight_release
60
+ )
61
+ self._app.statusBar().showMessage(
62
+ "領域ハイライトモード: 横方向にドラッグで縦帯、縦方向にドラッグで横帯を追加"
63
+ "(右クリックで削除)", 5000
64
+ )
65
+ else:
66
+ if getattr(self, '_region_highlight_press_cid', None) is not None:
67
+ self._app.canvas.mpl_disconnect(self._region_highlight_press_cid)
68
+ self._region_highlight_press_cid = None
69
+ if getattr(self, '_region_highlight_motion_cid', None) is not None:
70
+ self._app.canvas.mpl_disconnect(self._region_highlight_motion_cid)
71
+ self._region_highlight_motion_cid = None
72
+ if getattr(self, '_region_highlight_release_cid', None) is not None:
73
+ self._app.canvas.mpl_disconnect(self._region_highlight_release_cid)
74
+ self._region_highlight_release_cid = None
75
+ self._clear_region_highlight_preview()
76
+ self._region_highlight_axes = None
77
+ self._region_highlight_start = None
78
+
79
+ def _clear_region_highlight_preview(self):
80
+ """ドラッグ中のプレビューを消す。描き直しで既に消えていても例外にしない。"""
81
+ artist = getattr(self, '_region_highlight_preview_artist', None)
82
+ if artist is not None:
83
+ try:
84
+ artist.remove()
85
+ except (ValueError, NotImplementedError):
86
+ pass
87
+ self._region_highlight_preview_artist = None
88
+ self._app.canvas.draw_idle()
89
+
90
+ def _on_region_highlight_press(self, event):
91
+ if not getattr(self, 'region_highlight_mode_enabled', False):
92
+ return
93
+ if event.inaxes is None or event.xdata is None or event.ydata is None:
94
+ return
95
+
96
+ if event.button == 3: # 右クリックは削除
97
+ self._try_delete_region_near(event)
98
+ return
99
+
100
+ self._region_highlight_axes = event.inaxes
101
+ self._region_highlight_start = (event.xdata, event.ydata)
102
+
103
+ def _on_region_highlight_motion(self, event):
104
+ axes = getattr(self, '_region_highlight_axes', None)
105
+ if axes is None or event.inaxes is not axes or event.xdata is None or event.ydata is None:
106
+ return
107
+
108
+ start_x, start_y = self._region_highlight_start
109
+ orientation = self._region_highlight_orientation(axes, start_x, start_y, event.xdata, event.ydata)
110
+ if orientation is None:
111
+ return
112
+
113
+ self._clear_region_highlight_preview()
114
+ xmin, xmax = axes.get_xlim()
115
+ ymin, ymax = axes.get_ylim()
116
+ if orientation == 'vspan':
117
+ x0, x1 = sorted((start_x, event.xdata))
118
+ rect = Rectangle(
119
+ (x0, ymin), x1 - x0, ymax - ymin,
120
+ facecolor=REGION_HIGHLIGHT_DEFAULT_COLOR, alpha=0.25,
121
+ edgecolor=REGION_HIGHLIGHT_DEFAULT_COLOR, linewidth=1, zorder=100,
122
+ )
123
+ else:
124
+ y0, y1 = sorted((start_y, event.ydata))
125
+ rect = Rectangle(
126
+ (xmin, y0), xmax - xmin, y1 - y0,
127
+ facecolor=REGION_HIGHLIGHT_DEFAULT_COLOR, alpha=0.25,
128
+ edgecolor=REGION_HIGHLIGHT_DEFAULT_COLOR, linewidth=1, zorder=100,
129
+ )
130
+ axes.add_patch(rect)
131
+ self._region_highlight_preview_artist = rect
132
+ self._app.canvas.draw_idle()
133
+
134
+ def _on_region_highlight_release(self, event):
135
+ axes = getattr(self, '_region_highlight_axes', None)
136
+ if axes is None:
137
+ return
138
+
139
+ self._clear_region_highlight_preview()
140
+ start = self._region_highlight_start
141
+ self._region_highlight_axes = None
142
+ self._region_highlight_start = None
143
+ if start is None:
144
+ return
145
+ start_x, start_y = start
146
+
147
+ if event.inaxes is not axes or event.xdata is None or event.ydata is None:
148
+ return # 別の軸か軸の外で離した
149
+
150
+ end_x, end_y = event.xdata, event.ydata
151
+ orientation = self._region_highlight_orientation(axes, start_x, start_y, end_x, end_y)
152
+ if orientation is None:
153
+ return # クリックだけ(ドラッグなし)
154
+
155
+ axis_index = self._app._find_axis_index(axes)
156
+ if axis_index is None:
157
+ return
158
+
159
+ if orientation == 'vspan':
160
+ value_range = tuple(sorted((float(start_x), float(end_x))))
161
+ else:
162
+ value_range = tuple(sorted((float(start_y), float(end_y))))
163
+
164
+ self._app._add_annotation(axis_index, {
165
+ 'type': orientation, 'range': value_range,
166
+ 'color': REGION_HIGHLIGHT_DEFAULT_COLOR, 'alpha': REGION_HIGHLIGHT_DEFAULT_ALPHA,
167
+ }, description="領域ハイライトの追加")
168
+
169
+ def _region_highlight_orientation(self, axes, start_x, start_y, current_x, current_y):
170
+ """'vspan'(横に大きく動いた)/ 'hspan'(縦に大きく動いた)/ None(まだドラッグとみなせない)。"""
171
+ start_px = axes.transData.transform((start_x, start_y))
172
+ current_px = axes.transData.transform((current_x, current_y))
173
+ dx = abs(current_px[0] - start_px[0])
174
+ dy = abs(current_px[1] - start_px[1])
175
+ if max(dx, dy) < REGION_HIGHLIGHT_DRAG_THRESHOLD_PX:
176
+ return None
177
+ return 'vspan' if dx >= dy else 'hspan'
178
+
179
+ def _try_delete_region_near(self, event):
180
+ """右クリックした位置の帯を、確認してから消す。重なっていれば最後に足したもの(いちばん手前)。"""
181
+ axis_index = self._app._find_axis_index(event.inaxes)
182
+ if axis_index is None:
183
+ return
184
+
185
+ settings = self._app.project.all_plot_settings[axis_index]
186
+ annotations = axis_setting(settings, 'annotations')
187
+
188
+ target_index = None
189
+ for i, ann in enumerate(annotations):
190
+ ann_type = ann.get('type')
191
+ if ann_type not in ('vspan', 'hspan'):
192
+ continue
193
+ value = event.xdata if ann_type == 'vspan' else event.ydata
194
+ lo, hi = ann.get('range', (None, None))
195
+ if lo is None or hi is None or value is None:
196
+ continue
197
+ if lo <= value <= hi:
198
+ target_index = i # 後ろのものほど手前
199
+
200
+ if target_index is None:
201
+ return
202
+
203
+ target = annotations[target_index]
204
+ label = "縦帯" if target.get('type') == 'vspan' else "横帯"
205
+ lo, hi = target.get('range', (None, None))
206
+ reply = notify.question(
207
+ self._app, "領域ハイライトの削除",
208
+ f"この{label}を削除しますか?\n\n範囲: {lo:.4g} 〜 {hi:.4g}",
209
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
210
+ QMessageBox.StandardButton.Yes
211
+ )
212
+ if reply != QMessageBox.StandardButton.Yes:
213
+ return
214
+
215
+ new_list = list(annotations)
216
+ del new_list[target_index]
217
+
218
+ command = SetAnnotationsCommand(
219
+ self._app.project, axis_index, annotations, new_list,
220
+ self._app._update_plot_appearance, description="領域ハイライトの削除"
221
+ )
222
+ self._app.undo_stack.push(command)
@@ -0,0 +1,200 @@
1
+ """スライス抽出モード: 2D マップの上でドラッグした線分に沿った断面を、新しい1次元のデータセットにする。
2
+
3
+ 対象は今選んでいるデータセット(その軸に描かれていること)。線分がほぼ水平・垂直ならその軸の値、
4
+ 斜めなら始点からの距離を X にする。
5
+ """
6
+ import logging
7
+
8
+
9
+ from graphica.gui import notify
10
+ from graphica.core.provenance import build_provenance
11
+ from graphica.core.dataset import Dataset
12
+ from graphica.core.grid_data import extract_slice, GridDataError
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ SLICE_EXTRACTION_N_POINTS = 200
17
+
18
+ # extract_slice() の 'axis_kind' ごとの、新しいデータセットの X の列名とラベル
19
+ _AXIS_KIND_LABELS = {
20
+ 'x': 'X',
21
+ 'y': 'Y',
22
+ 'distance': '始点からの距離',
23
+ }
24
+
25
+
26
+ class SliceExtractionTool:
27
+ """状態はこのツールが持ち、PlotterApp の同じ名前は窓口(gui/tools/__init__.py)。"""
28
+
29
+ # PlotterApp から同じ名前で読み書きできるもの(テスト・メニュー・ほかの mixin が使う)
30
+ EXPOSED_NAMES = (
31
+ '_apply_slice_extraction', '_clear_slice_extraction_preview', '_create_slice_dataset',
32
+ '_on_slice_extraction_motion', '_on_slice_extraction_press', '_on_slice_extraction_release',
33
+ '_slice_extraction_axes', '_slice_extraction_motion_cid', '_slice_extraction_press_cid',
34
+ '_slice_extraction_preview_artist', '_slice_extraction_release_cid', '_slice_extraction_start',
35
+ '_toggle_slice_extraction_mode', 'slice_extraction_mode_enabled',
36
+ )
37
+
38
+ def __init__(self, app):
39
+ self._app = app
40
+ self.slice_extraction_mode_enabled = False
41
+ self._slice_extraction_press_cid = None
42
+ self._slice_extraction_motion_cid = None
43
+ self._slice_extraction_release_cid = None
44
+ self._slice_extraction_axes = None
45
+ self._slice_extraction_start = None # (x, y) データ座標
46
+ self._slice_extraction_preview_artist = None
47
+
48
+ def _toggle_slice_extraction_mode(self, checked):
49
+ self.slice_extraction_mode_enabled = checked
50
+
51
+ if checked:
52
+ self._app._deactivate_other_mouse_modes('slice_extraction')
53
+
54
+ self._slice_extraction_press_cid = self._app.canvas.mpl_connect(
55
+ 'button_press_event', self._on_slice_extraction_press
56
+ )
57
+ self._slice_extraction_motion_cid = self._app.canvas.mpl_connect(
58
+ 'motion_notify_event', self._on_slice_extraction_motion
59
+ )
60
+ self._slice_extraction_release_cid = self._app.canvas.mpl_connect(
61
+ 'button_release_event', self._on_slice_extraction_release
62
+ )
63
+ self._app.statusBar().showMessage(
64
+ "スライス抽出モード: カレントの2Dマップ上でドラッグした線分に沿って"
65
+ "1Dデータセットを抽出します", 5000
66
+ )
67
+ else:
68
+ for attr in ('_slice_extraction_press_cid', '_slice_extraction_motion_cid',
69
+ '_slice_extraction_release_cid'):
70
+ cid = getattr(self, attr, None)
71
+ if cid is not None:
72
+ self._app.canvas.mpl_disconnect(cid)
73
+ setattr(self, attr, None)
74
+ self._clear_slice_extraction_preview()
75
+ self._slice_extraction_axes = None
76
+ self._slice_extraction_start = None
77
+
78
+ def _clear_slice_extraction_preview(self):
79
+ """ドラッグ中のプレビューを消す。描き直しで既に消えていても例外にしない。"""
80
+ artist = getattr(self, '_slice_extraction_preview_artist', None)
81
+ if artist is not None:
82
+ try:
83
+ artist.remove()
84
+ except (ValueError, NotImplementedError):
85
+ pass
86
+ self._slice_extraction_preview_artist = None
87
+ self._app.canvas.draw_idle()
88
+
89
+ def _on_slice_extraction_press(self, event):
90
+ if not getattr(self, 'slice_extraction_mode_enabled', False):
91
+ return
92
+ if event.button != 1 or event.inaxes is None or event.xdata is None or event.ydata is None:
93
+ return
94
+ self._slice_extraction_axes = event.inaxes
95
+ self._slice_extraction_start = (event.xdata, event.ydata)
96
+
97
+ def _on_slice_extraction_motion(self, event):
98
+ axes = getattr(self, '_slice_extraction_axes', None)
99
+ if axes is None or event.inaxes is not axes or event.xdata is None or event.ydata is None:
100
+ return
101
+
102
+ self._clear_slice_extraction_preview()
103
+ start = self._slice_extraction_start
104
+ (line,) = axes.plot(
105
+ [start[0], event.xdata], [start[1], event.ydata],
106
+ color='#E4572E', linestyle='--', linewidth=1.5, zorder=100,
107
+ )
108
+ self._slice_extraction_preview_artist = line
109
+ self._app.canvas.draw_idle()
110
+
111
+ def _on_slice_extraction_release(self, event):
112
+ axes = getattr(self, '_slice_extraction_axes', None)
113
+ if axes is None:
114
+ return
115
+
116
+ self._clear_slice_extraction_preview()
117
+ start = self._slice_extraction_start
118
+ self._slice_extraction_axes = None
119
+ self._slice_extraction_start = None
120
+
121
+ if event.inaxes is not axes or event.xdata is None or event.ydata is None:
122
+ return
123
+ end = (event.xdata, event.ydata)
124
+ if start == end:
125
+ return # クリックだけ(ドラッグなし)
126
+
127
+ self._apply_slice_extraction(axes, start, end)
128
+
129
+ def _apply_slice_extraction(self, axes, start, end):
130
+ """今のデータセットが 2D の格子でないか、この軸に描かれていなければ、何もせず案内を出す。"""
131
+ dataset = self._app._get_current_dataset()
132
+ if dataset is None or dataset.data_kind != '2d_grid':
133
+ notify.information(
134
+ self._app, "スライス抽出", "スライス抽出の対象となる2Dマップのデータセットを選択してください。"
135
+ )
136
+ return
137
+
138
+ target_axis = dataset.subplot_target
139
+ if dataset.use_secondary_y:
140
+ expected_axes = (
141
+ self._app.all_secondary_axes[target_axis]
142
+ if 0 <= target_axis < len(self._app.all_secondary_axes) else None
143
+ )
144
+ else:
145
+ expected_axes = (
146
+ self._app.all_axes[target_axis]
147
+ if 0 <= target_axis < len(self._app.all_axes) else None
148
+ )
149
+ if axes is not expected_axes:
150
+ notify.information(
151
+ self._app, "スライス抽出",
152
+ "ドラッグしたサブプロットに、選択中の2Dマップが描画されていません。"
153
+ )
154
+ return
155
+
156
+ grid = dataset.z_grid
157
+ if grid is None:
158
+ notify.warning(self._app, "スライス抽出", "有効な2Dグリッドデータがありません。")
159
+ return
160
+
161
+ try:
162
+ result = extract_slice(
163
+ grid['x_grid'], grid['y_grid'], grid['z_grid'],
164
+ start=start, end=end, n_points=SLICE_EXTRACTION_N_POINTS,
165
+ )
166
+ except GridDataError as e:
167
+ notify.warning(self._app, "スライス抽出", f"スライスの抽出に失敗しました:\n{e}")
168
+ return
169
+
170
+ self._create_slice_dataset(dataset, start, end, result)
171
+
172
+ def _create_slice_dataset(self, source_dataset, start, end, result):
173
+ import pandas as pd
174
+
175
+ axis_kind = result['axis_kind']
176
+ x_label = _AXIS_KIND_LABELS.get(axis_kind, axis_kind)
177
+ df = pd.DataFrame({'x': result['axis_values'], 'y': result['z_values']})
178
+ params = {
179
+ 'start': [float(start[0]), float(start[1])],
180
+ 'end': [float(end[0]), float(end[1])],
181
+ 'axis_kind': axis_kind,
182
+ 'n_points': SLICE_EXTRACTION_N_POINTS,
183
+ }
184
+ slice_dataset = Dataset(
185
+ name=f"Slice ({source_dataset.name})",
186
+ df=df, x_col_name='x', y_col_name='y',
187
+ provenance=build_provenance('2d_slice', params, [source_dataset]),
188
+ )
189
+
190
+ original_item = self._app._get_dataset_tree_item(source_dataset)
191
+ folder = original_item.parent() if original_item else None
192
+
193
+ def add():
194
+ self._app.dataset_order.append(slice_dataset, folder)
195
+ self._app._update_plot()
196
+
197
+ self._app._push_dataset_additions(add, [slice_dataset], "スライスの抽出")
198
+ self._app.statusBar().showMessage(
199
+ f"「{source_dataset.name}」からスライスを抽出しました(X軸: {x_label})", 4000
200
+ )
@@ -0,0 +1,46 @@
1
+ """組み立てた画面の文字を、あとからまとめて tr() で訳す(Designer の .ui で作った部品にも効かせるため)。
2
+
3
+ 日本語のときは何もしない。選択肢(QComboBox)は文字そのものを値として保存するものがあるので、ここでは
4
+ 番号やデータで値を持つと分かっているものだけを訳す(ほかは作る所で tr() を通す)。
5
+ """
6
+ from PySide6.QtWidgets import (QAbstractButton, QComboBox, QDockWidget, QGroupBox, QLabel, QLineEdit, QTabWidget,
7
+ QToolBar, QWidget)
8
+
9
+ from graphica.core.i18n import DEFAULT_LANGUAGE, get_language, tr
10
+
11
+ # 選んだ番号で値を持つ選択肢(gui/axis_bindings.py の index)。表示を訳しても保存する値は変わらない
12
+ INDEX_BOUND_COMBOS = ("x_major_tick_mode_combo", "y_major_tick_mode_combo")
13
+
14
+
15
+ def _translate(text):
16
+ translated = tr(text)
17
+ return translated if translated != text else None
18
+
19
+
20
+ def translate_widget_texts(root):
21
+ if get_language() == DEFAULT_LANGUAGE:
22
+ return
23
+ for widget in [root] + root.findChildren(QWidget):
24
+ tooltip = widget.toolTip()
25
+ if tooltip and (new := _translate(tooltip)):
26
+ widget.setToolTip(new)
27
+ if isinstance(widget, (QLabel, QAbstractButton)):
28
+ if (new := _translate(widget.text())):
29
+ widget.setText(new)
30
+ elif isinstance(widget, QGroupBox):
31
+ if (new := _translate(widget.title())):
32
+ widget.setTitle(new)
33
+ elif isinstance(widget, (QDockWidget, QToolBar)):
34
+ if (new := _translate(widget.windowTitle())):
35
+ widget.setWindowTitle(new)
36
+ elif isinstance(widget, QLineEdit):
37
+ if widget.placeholderText() and (new := _translate(widget.placeholderText())):
38
+ widget.setPlaceholderText(new)
39
+ elif isinstance(widget, QComboBox) and widget.objectName() in INDEX_BOUND_COMBOS:
40
+ for i in range(widget.count()):
41
+ if (new := _translate(widget.itemText(i))):
42
+ widget.setItemText(i, new)
43
+ if isinstance(widget, QTabWidget):
44
+ for i in range(widget.count()):
45
+ if (new := _translate(widget.tabText(i))):
46
+ widget.setTabText(i, new)