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,605 @@
1
+ """軸の設定の部品: 画面の欄と設定の辞書の相互変換、サブプロットの行数と列数、フォントと色の選択、その信号の配線。"""
2
+ import functools
3
+ import logging
4
+ from PySide6.QtCore import QTimer
5
+ from PySide6.QtGui import QFont
6
+ from PySide6.QtWidgets import QDialog, QFontDialog
7
+
8
+ from graphica.core.i18n import tr
9
+ from graphica.gui import notify
10
+ from graphica.core.axis_settings import AXIS_SETTING_DEFAULTS, axis_setting
11
+ from graphica.gui import theme
12
+ from graphica.gui.axis_bindings import CARRIED_AXIS_KEYS
13
+ from graphica.gui.color_history import get_color_with_history
14
+ from graphica.gui.dialogs import LegendOrderDialog, LabelEditDialog
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ # Windows の「Arial Narrow」は matplotlib には family="Arial", stretch="condensed" として登録されていて、
20
+ # 名前のままでは見つからず黙って既定のフォントになる。末尾の語を stretch に読み替えて探し直す。
21
+ _FONT_STRETCH_KEYWORDS = {
22
+ 'narrow': 'condensed',
23
+ 'condensed': 'condensed',
24
+ 'semicondensed': 'semi-condensed',
25
+ 'extracondensed': 'extra-condensed',
26
+ 'ultracondensed': 'ultra-condensed',
27
+ 'wide': 'expanded',
28
+ 'expanded': 'expanded',
29
+ 'semiexpanded': 'semi-expanded',
30
+ 'extraexpanded': 'extra-expanded',
31
+ 'ultraexpanded': 'ultra-expanded',
32
+ }
33
+
34
+
35
+ @functools.lru_cache(maxsize=256)
36
+ def _resolve_font_family_for_matplotlib(family_name: str):
37
+ """(matplotlib に渡す family, stretch または None, 解決できたか) を返す。
38
+
39
+ 軸の設定を変えるたびに呼ばれるので、findfont の結果をキャッシュする。
40
+ """
41
+ import matplotlib.font_manager as fm
42
+
43
+ try:
44
+ fm.findfont(fm.FontProperties(family=family_name), fallback_to_default=False)
45
+ return family_name, None, True
46
+ except ValueError:
47
+ pass
48
+
49
+ words = family_name.rsplit(None, 1)
50
+ if len(words) == 2:
51
+ base, suffix = words
52
+ stretch = _FONT_STRETCH_KEYWORDS.get(suffix.lower())
53
+ if stretch is not None:
54
+ try:
55
+ fm.findfont(fm.FontProperties(family=base, stretch=stretch), fallback_to_default=False)
56
+ return base, stretch, True
57
+ except ValueError:
58
+ pass
59
+
60
+ return family_name, None, False
61
+
62
+
63
+ def _order_labels(labels, order):
64
+ """order の順に並べ、order に無いラベルは元の順のまま末尾に置く。"""
65
+ if not order:
66
+ return list(labels)
67
+ order_index = {name: i for i, name in enumerate(order)}
68
+ indices = sorted(
69
+ range(len(labels)),
70
+ key=lambda i: (0, order_index[labels[i]]) if labels[i] in order_index else (1, i)
71
+ )
72
+ return [labels[i] for i in indices]
73
+
74
+
75
+ class AxisSettingsPanel:
76
+ """状態はタブが持ち、この部品はメソッドと信号の配線だけを持つ。PlotterApp の同じ名前は窓口(gui/panels/__init__.py)。"""
77
+
78
+ # PlotterApp から同じ名前で使えるもの(テスト・メニュー・ほかの部品が使う)
79
+ EXPOSED_NAMES = (
80
+ '_on_y2_autoscale_changed', '_on_y2_axis_state_changed', '_refresh_y2_axis_state',
81
+ '_apply_settings_to_ui_controls', '_block_all_signals', '_connect_axis_setting_signals',
82
+ '_connect_layout_signals', '_current_active_axis', '_font_props_to_dict', '_gather_settings_from_ui',
83
+ '_grid_linestyle_code', '_grid_linestyle_index', '_on_active_axis_changed', '_on_axis_setting_changed',
84
+ '_on_change_axis_label_color', '_on_change_axis_label_font', '_on_change_legend_color',
85
+ '_on_change_legend_font', '_on_change_spine_color', '_on_change_tick_color', '_on_change_tick_font',
86
+ '_on_edit_legend_order', '_on_grid_visibility_changed', '_on_layout_changed', '_on_legend_drag_release',
87
+ '_on_legend_loc_changed', '_on_legend_visibility_changed', '_on_share_axis_changed',
88
+ '_on_x_autoscale_changed', '_on_x_minor_tick_visibility_changed', '_on_x_tick_mode_changed',
89
+ '_on_y_autoscale_changed', '_on_y_minor_tick_visibility_changed', '_on_y_tick_mode_changed',
90
+ '_open_label_edit_dialog', '_refresh_all_label_previews', '_refresh_grid_state', '_refresh_label_preview',
91
+ '_refresh_legend_state', '_refresh_x_autoscale_enabled_state', '_refresh_x_minor_tick_state',
92
+ '_refresh_x_tick_mode_state', '_refresh_y_autoscale_enabled_state', '_refresh_y_minor_tick_state',
93
+ '_refresh_y_tick_mode_state', '_seed_min_max_spinboxes', '_store_dragged_legend_positions',
94
+ '_update_subplot_combos', '_warn_if_font_family_unavailable_for_graph',
95
+ )
96
+
97
+ def __init__(self, app):
98
+ self._app = app
99
+
100
+ def _on_layout_changed(self):
101
+ if getattr(self._app.project, 'layout_mode', 'grid') == 'free':
102
+ return
103
+
104
+ # 欄の値を変える間、変更の通知が連鎖しないように
105
+ self._block_all_signals(True)
106
+
107
+ rows = self._app.subplot_rows_spinbox.value()
108
+ cols = self._app.subplot_cols_spinbox.value()
109
+ total_plots = rows * cols
110
+
111
+ current_plot_count = len(self._app.project.all_plot_settings)
112
+
113
+ if total_plots > current_plot_count:
114
+ # 増えた分は今の軸の設定をもとにする
115
+ default_settings = self._gather_settings_from_ui()
116
+ for _ in range(total_plots - current_plot_count):
117
+ new_settings = default_settings.copy()
118
+ # 注釈は引き継がない。浅いコピーなので、空のリストにしないと全部が同じリストを共有する
119
+ new_settings['annotations'] = []
120
+ new_settings['legend_order'] = []
121
+ self._app.project.all_plot_settings.append(new_settings)
122
+
123
+ elif total_plots < current_plot_count:
124
+ self._app.project.all_plot_settings = self._app.project.all_plot_settings[:total_plots]
125
+
126
+ # 無くなったサブプロットを描画先にしていたデータセットは、どこにも描かれず
127
+ # エクスポートにも入らないのに気づけないので、最後のサブプロットに移す
128
+ for dataset in self._app.project.datasets:
129
+ if dataset.subplot_target >= total_plots:
130
+ dataset.subplot_target = total_plots - 1
131
+
132
+ if self._app.project.active_axis_index >= total_plots:
133
+ self._app.project.active_axis_index = 0
134
+
135
+ self._update_subplot_combos()
136
+ self._apply_settings_to_ui_controls(self._app.project.all_plot_settings[self._app.project.active_axis_index])
137
+
138
+ self._block_all_signals(False)
139
+
140
+ self._app._update_plot()
141
+
142
+ def _on_share_axis_changed(self):
143
+ self._app.project.share_x_axis = self._app.share_x_checkbox.isChecked()
144
+ self._app.project.share_y_axis = self._app.share_y_checkbox.isChecked()
145
+ self._app._update_plot()
146
+
147
+ def _on_active_axis_changed(self, index):
148
+ """切り替える前の欄の値を元の軸に保存してから、新しい軸の設定を欄に読み込む。"""
149
+
150
+ if index == -1 or index >= len(self._app.project.all_plot_settings):
151
+ return
152
+
153
+ self._app.project.all_plot_settings[self._app.project.active_axis_index] = self._gather_settings_from_ui()
154
+
155
+ self._app.project.active_axis_index = index
156
+
157
+ settings_to_load = self._app.project.all_plot_settings[self._app.project.active_axis_index]
158
+ self._apply_settings_to_ui_controls(settings_to_load)
159
+
160
+ def _on_axis_setting_changed(self):
161
+ current_settings = self._gather_settings_from_ui()
162
+
163
+ self._app.project.all_plot_settings[self._app.project.active_axis_index] = current_settings
164
+
165
+ self._app._update_plot_appearance()
166
+
167
+ def _update_subplot_combos(self):
168
+ total_plots = len(self._app.project.all_plot_settings)
169
+ plot_names = [tr("プロット {number}").format(number=i + 1) for i in range(total_plots)]
170
+
171
+ self._app.active_axis_combo.blockSignals(True)
172
+ self._app.subplot_target_combo.blockSignals(True)
173
+
174
+ self._app.active_axis_combo.clear()
175
+ self._app.subplot_target_combo.clear()
176
+
177
+ self._app.active_axis_combo.addItems(plot_names)
178
+ self._app.subplot_target_combo.addItems(plot_names)
179
+
180
+ self._app.active_axis_combo.setCurrentIndex(self._app.project.active_axis_index)
181
+
182
+ # 「描画先」の選択はデータセットを選んだときに property_panel.update_ui_state が決める
183
+ self._app.active_axis_combo.blockSignals(False)
184
+ self._app.subplot_target_combo.blockSignals(False)
185
+
186
+
187
+ def _seed_min_max_spinboxes(self, min_spinbox, max_spinbox, limits):
188
+ """オートスケールを切った瞬間に、いま表示している範囲を最小値・最大値の欄に入れる。
189
+
190
+ 欄が初期値 0/0 のままだと、最小値を先に変えたとき「最小 >= 最大」で反映されない。
191
+ """
192
+ if limits is None:
193
+ return
194
+ min_lim, max_lim = limits
195
+ min_spinbox.blockSignals(True)
196
+ max_spinbox.blockSignals(True)
197
+ min_spinbox.setValue(min_lim)
198
+ max_spinbox.setValue(max_lim)
199
+ min_spinbox.blockSignals(False)
200
+ max_spinbox.blockSignals(False)
201
+
202
+ def _current_active_axis(self):
203
+ axis_index = self._app.project.active_axis_index
204
+ if 0 <= axis_index < len(self._app.canvas.all_axes):
205
+ return self._app.canvas.all_axes[axis_index]
206
+ return None
207
+
208
+ def _refresh_x_autoscale_enabled_state(self):
209
+ """最小値・最大値の欄の有効/無効だけをオートスケールに合わせる。値は書き換えない。
210
+
211
+ 保存した設定を戻すときはこちらを使う。_on_x_autoscale_changed() は表示中の範囲を欄に入れるので、
212
+ 保存されていた範囲が上書きされる。
213
+ """
214
+ is_autoscale = self._app.ui.x_autoscale_checkbox.isChecked()
215
+ self._app.ui.x_min_spinbox.setEnabled(not is_autoscale)
216
+ self._app.ui.x_max_spinbox.setEnabled(not is_autoscale)
217
+ return is_autoscale
218
+
219
+ def _refresh_y_autoscale_enabled_state(self):
220
+ is_autoscale = self._app.ui.y_autoscale_checkbox.isChecked()
221
+ self._app.ui.y_min_spinbox.setEnabled(not is_autoscale)
222
+ self._app.ui.y_max_spinbox.setEnabled(not is_autoscale)
223
+ return is_autoscale
224
+
225
+ def _on_x_autoscale_changed(self):
226
+ """利用者がチェックを変えたとき(設定を戻すときは _refresh_x_autoscale_enabled_state)。"""
227
+ is_autoscale = self._refresh_x_autoscale_enabled_state()
228
+
229
+ if not is_autoscale:
230
+ axis = self._current_active_axis()
231
+ self._seed_min_max_spinboxes(
232
+ self._app.ui.x_min_spinbox, self._app.ui.x_max_spinbox, axis.get_xlim() if axis is not None else None)
233
+
234
+ # 欄の値を書き換えたので、描き直すだけでなく設定も集め直す
235
+ self._on_axis_setting_changed()
236
+
237
+ def _on_y_autoscale_changed(self):
238
+ is_autoscale = self._refresh_y_autoscale_enabled_state()
239
+
240
+ if not is_autoscale:
241
+ axis = self._current_active_axis()
242
+ self._seed_min_max_spinboxes(
243
+ self._app.ui.y_min_spinbox, self._app.ui.y_max_spinbox, axis.get_ylim() if axis is not None else None)
244
+
245
+ self._on_axis_setting_changed()
246
+
247
+ def _on_x_tick_mode_changed(self):
248
+ self._refresh_x_tick_mode_state()
249
+ self._app._update_plot_appearance()
250
+
251
+ def _refresh_x_tick_mode_state(self):
252
+ is_fixed_interval = (self._app.ui.x_major_tick_mode_combo.currentIndex() == 1)
253
+ self._app.ui.x_major_tick_interval_spinbox.setEnabled(is_fixed_interval)
254
+
255
+ def _on_y_tick_mode_changed(self):
256
+ self._refresh_y_tick_mode_state()
257
+ self._app._update_plot_appearance()
258
+
259
+ def _refresh_y_tick_mode_state(self):
260
+ is_fixed_interval = (self._app.ui.y_major_tick_mode_combo.currentIndex() == 1)
261
+ self._app.ui.y_major_tick_interval_spinbox.setEnabled(is_fixed_interval)
262
+
263
+ def _on_x_minor_tick_visibility_changed(self):
264
+ """補助目盛の表示と対数表示の両方に依存するので、同じスロットで受ける。"""
265
+ self._refresh_x_minor_tick_state()
266
+ self._app._update_plot_appearance()
267
+
268
+ def _refresh_x_minor_tick_state(self):
269
+ is_visible = self._app.ui.x_minor_ticks_visible_checkbox.isChecked()
270
+ is_log = self._app.ui.x_log_checkbox.isChecked()
271
+ # 対数軸は MultipleLocator ではなく LogLocator なので間隔は使わない
272
+ self._app.ui.x_minor_tick_interval_spinbox.setEnabled(is_visible and not is_log)
273
+ self._app.x_log_minor_subs_label.setVisible(is_log)
274
+ self._app.x_log_minor_subs_combo.setVisible(is_log)
275
+ self._app.x_log_minor_labels_checkbox.setVisible(is_log)
276
+ self._app.x_log_minor_subs_combo.setEnabled(is_log and is_visible)
277
+ self._app.x_log_minor_labels_checkbox.setEnabled(is_log and is_visible)
278
+
279
+ def _on_y_minor_tick_visibility_changed(self):
280
+ self._refresh_y_minor_tick_state()
281
+ self._app._update_plot_appearance()
282
+
283
+ def _refresh_y_minor_tick_state(self):
284
+ is_visible = self._app.ui.y_minor_ticks_visible_checkbox.isChecked()
285
+ is_log = self._app.ui.y_log_checkbox.isChecked()
286
+ self._app.ui.y_minor_tick_interval_spinbox.setEnabled(is_visible and not is_log)
287
+ self._app.y_log_minor_subs_label.setVisible(is_log)
288
+ self._app.y_log_minor_subs_combo.setVisible(is_log)
289
+ self._app.y_log_minor_labels_checkbox.setVisible(is_log)
290
+ self._app.y_log_minor_subs_combo.setEnabled(is_log and is_visible)
291
+ self._app.y_log_minor_labels_checkbox.setEnabled(is_log and is_visible)
292
+
293
+ def _refresh_y2_axis_state(self):
294
+ """第 2 Y 軸の欄の有効/無効を、自動スケール・目盛りの方式・補助目盛り・対数に合わせる。値は書き換えない。"""
295
+ app = self._app
296
+ is_autoscale = app.y2_autoscale_checkbox.isChecked()
297
+ app.y2_min_spinbox.setEnabled(not is_autoscale)
298
+ app.y2_max_spinbox.setEnabled(not is_autoscale)
299
+ app.y2_major_tick_interval_spinbox.setEnabled(app.y2_major_tick_mode_combo.currentIndex() == 1)
300
+ # 対数軸の補助目盛りは間隔ではなく対数の刻みで打つ
301
+ app.y2_minor_tick_interval_spinbox.setEnabled(
302
+ app.y2_minor_ticks_visible_checkbox.isChecked() and not app.y2_log_checkbox.isChecked())
303
+
304
+ def _on_y2_axis_state_changed(self):
305
+ self._refresh_y2_axis_state()
306
+
307
+ def _on_y2_autoscale_changed(self):
308
+ """自動スケールを切った瞬間に、いまの第 2 Y 軸の範囲を最小値・最大値の欄に入れる(主の Y 軸と同じ)。"""
309
+ self._refresh_y2_axis_state()
310
+ if not self._app.y2_autoscale_checkbox.isChecked():
311
+ axis_index = self._app.project.active_axis_index
312
+ secondary_axes = getattr(self._app.canvas, 'all_secondary_axes', [])
313
+ secondary = secondary_axes[axis_index] if 0 <= axis_index < len(secondary_axes) else None
314
+ self._seed_min_max_spinboxes(self._app.y2_min_spinbox, self._app.y2_max_spinbox,
315
+ secondary.get_ylim() if secondary is not None else None)
316
+ self._on_axis_setting_changed()
317
+
318
+ def _on_legend_visibility_changed(self):
319
+ self._refresh_legend_state()
320
+ self._app._update_plot_appearance()
321
+
322
+ def _refresh_legend_state(self):
323
+ is_visible = self._app.ui.legend_visible_checkbox.isChecked()
324
+
325
+ self._app.legend_loc_label.setEnabled(is_visible)
326
+ self._app.legend_loc_combo.setEnabled(is_visible)
327
+ self._app.legend_font_label.setEnabled(is_visible)
328
+ self._app.legend_font_button.setEnabled(is_visible)
329
+ self._app.legend_color_label.setEnabled(is_visible)
330
+ self._app.legend_color_button.setEnabled(is_visible)
331
+
332
+ def _grid_linestyle_code(self, combo_index: int) -> str:
333
+ choices = self._app.grid_linestyle_choices
334
+ if 0 <= combo_index < len(choices):
335
+ return choices[combo_index][1]
336
+ return '-'
337
+
338
+ def _grid_linestyle_index(self, linestyle_code: str) -> int:
339
+ """未知の値は 0(実線)。"""
340
+ for i, (_label, code) in enumerate(self._app.grid_linestyle_choices):
341
+ if code == linestyle_code:
342
+ return i
343
+ return 0
344
+
345
+ def _on_grid_visibility_changed(self):
346
+ self._refresh_grid_state()
347
+ self._app._update_plot_appearance()
348
+
349
+ def _refresh_grid_state(self):
350
+ is_visible = self._app.ui.grid_visible_checkbox.isChecked()
351
+
352
+ self._app.ui.minor_grid_visible_checkbox.setEnabled(is_visible)
353
+
354
+ # 主のグリッドの欄はグリッドの表示に、補助の欄はさらに補助グリッドの表示にも従う
355
+ is_minor_visible = is_visible and self._app.ui.minor_grid_visible_checkbox.isChecked()
356
+ for widget in (
357
+ self._app.x_major_grid_linestyle_combo, self._app.x_major_grid_width_spinbox, self._app.x_major_grid_alpha_spinbox,
358
+ self._app.y_major_grid_linestyle_combo, self._app.y_major_grid_width_spinbox, self._app.y_major_grid_alpha_spinbox,
359
+ ):
360
+ widget.setEnabled(is_visible)
361
+ for widget in (
362
+ self._app.x_minor_grid_linestyle_combo, self._app.x_minor_grid_width_spinbox, self._app.x_minor_grid_alpha_spinbox,
363
+ self._app.y_minor_grid_linestyle_combo, self._app.y_minor_grid_width_spinbox, self._app.y_minor_grid_alpha_spinbox,
364
+ ):
365
+ widget.setEnabled(is_minor_visible)
366
+
367
+
368
+ def _warn_if_font_family_unavailable_for_graph(self, font):
369
+ """グラフのフォントが matplotlib で見つからなければ知らせる(選択は保存する)。
370
+
371
+ QFontDialog は OS のフォント一覧を出すが、matplotlib は別の一覧で探し、見つからないと黙って既定のフォントになる。
372
+ """
373
+ family = font.family()
374
+ _resolved_name, _stretch, resolved = _resolve_font_family_for_matplotlib(family)
375
+ if not resolved:
376
+ notify.warning(
377
+ self._app, "フォントが見つかりません",
378
+ f"フォント「{family}」はグラフの描画エンジン(matplotlib)には認識されず、"
379
+ "代わりに既定のフォントで表示されます。\n\n"
380
+ "OS側のフォント一覧には表示されていても、グラフの描画には使えない"
381
+ "フォントがあります。別のフォントをお試しください。"
382
+ )
383
+
384
+ def _on_change_tick_font(self):
385
+ ok, font = QFontDialog.getFont(self._app._tick_font, self._app)
386
+ if ok:
387
+ self._warn_if_font_family_unavailable_for_graph(font)
388
+ self._app._tick_font = font
389
+ self._on_axis_setting_changed()
390
+
391
+ def _on_change_tick_color(self):
392
+ color = get_color_with_history(self._app.settings, self._app)
393
+ if color.isValid():
394
+ self._app._tick_color = color.name()
395
+ self._on_axis_setting_changed()
396
+
397
+ def _on_change_axis_label_font(self):
398
+ ok, font = QFontDialog.getFont(self._app._axis_label_font, self._app)
399
+ if ok:
400
+ self._warn_if_font_family_unavailable_for_graph(font)
401
+ self._app._axis_label_font = font
402
+ # _update_plot_appearance() だけでは all_plot_settings に保存されない
403
+ self._on_axis_setting_changed()
404
+
405
+ def _on_change_axis_label_color(self):
406
+ color = get_color_with_history(self._app.settings, self._app)
407
+ if color.isValid():
408
+ self._app._axis_label_color = color.name()
409
+ self._on_axis_setting_changed()
410
+
411
+ def _on_change_legend_font(self):
412
+ ok, font = QFontDialog.getFont(self._app._legend_font, self._app)
413
+ if ok:
414
+ self._warn_if_font_family_unavailable_for_graph(font)
415
+ self._app._legend_font = font
416
+ self._on_axis_setting_changed()
417
+
418
+ def _on_change_legend_color(self):
419
+ color = get_color_with_history(self._app.settings, self._app)
420
+ if color.isValid():
421
+ self._app._legend_color = color.name()
422
+ self._on_axis_setting_changed()
423
+
424
+ def _on_change_spine_color(self):
425
+ color = get_color_with_history(self._app.settings, self._app)
426
+ if color.isValid():
427
+ self._app._spine_color = color.name()
428
+ self._on_axis_setting_changed()
429
+
430
+ def _open_label_edit_dialog(self, line_edit, dialog_title):
431
+ """編集ダイアログの結果を line_edit に setText() する(textChanged からいつもの経路で反映される)。
432
+
433
+ LABEL_SYMBOL_PALETTE は main_window がこの mixin を import しているので、ここで遅れて import する。
434
+ """
435
+ from graphica.gui.main_window import LABEL_SYMBOL_PALETTE
436
+
437
+ dialog = LabelEditDialog(line_edit.text(), dialog_title, LABEL_SYMBOL_PALETTE, parent=self._app)
438
+ if dialog.exec() == QDialog.DialogCode.Accepted:
439
+ line_edit.setText(dialog.get_text())
440
+
441
+ def _refresh_label_preview(self, preview_label, text, placeholder):
442
+ """プレビューを matplotlib で描いた見た目にする。空なら placeholder を text_muted の色で出す。"""
443
+ from graphica.gui.mathtext_preview import render_mathtext_to_pixmap
444
+
445
+ tokens = theme.current_tokens()
446
+ if text:
447
+ pixmap = render_mathtext_to_pixmap(text, color=tokens["text_primary"])
448
+ else:
449
+ pixmap = render_mathtext_to_pixmap(placeholder, color=tokens["text_muted"])
450
+ # 欄の幅が決まるたびに収まるよう縮める(setPixmap だと、この時点の幅がまだ確定していないとはみ出す)
451
+ preview_label.set_natural_pixmap(pixmap)
452
+
453
+ def _refresh_all_label_previews(self):
454
+ """文字色がテーマに従うので、ダークモードの切り替えで描き直す。"""
455
+ for preview_label, line_edit, placeholder in self._app._label_preview_widgets:
456
+ self._refresh_label_preview(preview_label, line_edit.text(), placeholder)
457
+
458
+ def _on_legend_loc_changed(self, *_args):
459
+ """ドラッグした位置(legend_position)が残っていると選んだ位置が効かないので消す。"""
460
+ axis_index = self._app.project.active_axis_index
461
+ if axis_index < len(self._app.project.all_plot_settings):
462
+ self._app.project.all_plot_settings[axis_index].pop('legend_position', None)
463
+ self._on_axis_setting_changed()
464
+
465
+ def _on_legend_drag_release(self, _event):
466
+ """凡例のドラッグは matplotlib 側の button_release_event で確定し、こちらが先に呼ばれうるので、処理の後で読む。"""
467
+ QTimer.singleShot(0, self._store_dragged_legend_positions)
468
+
469
+ def _store_dragged_legend_positions(self):
470
+ """凡例がドラッグで置かれていれば、その位置を legend_position に保存する(しないと次の描画で戻る)。どれか変えたら True。"""
471
+ changed = False
472
+ settings_list = self._app.project.all_plot_settings
473
+ secondary_axes = getattr(self._app.canvas, 'all_secondary_axes', [])
474
+ for index, ax in enumerate(getattr(self._app.canvas, 'all_axes', [])):
475
+ if index >= len(settings_list):
476
+ break
477
+ legend = ax.get_legend()
478
+ if legend is None and index < len(secondary_axes) and secondary_axes[index] is not None:
479
+ legend = secondary_axes[index].get_legend()
480
+ loc = getattr(legend, '_loc', None) if legend is not None else None
481
+ if not isinstance(loc, tuple) or len(loc) != 2:
482
+ continue
483
+ position = [round(float(loc[0]), 4), round(float(loc[1]), 4)]
484
+ if axis_setting(settings_list[index], 'legend_position') != position:
485
+ settings_list[index]['legend_position'] = position
486
+ changed = True
487
+ return changed
488
+
489
+ def _on_edit_legend_order(self):
490
+ """今の軸の凡例の並びをダイアログで決め、legend_order に保存する。"""
491
+ axis_index = self._app.project.active_axis_index
492
+ if axis_index >= len(self._app.canvas.all_axes):
493
+ return
494
+ ax = self._app.canvas.all_axes[axis_index]
495
+ _, labels = ax.get_legend_handles_labels()
496
+ if axis_index < len(self._app.canvas.all_secondary_axes) and self._app.canvas.all_secondary_axes[axis_index] is not None:
497
+ _, secondary_labels = self._app.canvas.all_secondary_axes[axis_index].get_legend_handles_labels()
498
+ labels = labels + secondary_labels
499
+ if not labels:
500
+ notify.information(self._app, "凡例の順序", "この軸には凡例に表示するデータセットがありません。")
501
+ return
502
+
503
+ current_order = axis_setting(self._app.project.all_plot_settings[axis_index], 'legend_order') or []
504
+ dialog = LegendOrderDialog(_order_labels(labels, current_order), self._app)
505
+ if dialog.exec() != QDialog.DialogCode.Accepted:
506
+ return
507
+ self._app.project.all_plot_settings[axis_index]['legend_order'] = dialog.get_order()
508
+ self._app._update_plot()
509
+
510
+ def _font_props_to_dict(self, qfont: QFont) -> dict:
511
+ """QFont を JSON に保存できる辞書にする。
512
+
513
+ 'family' は families() の候補リストのまま保存する(.family() の先頭1つにすると、macOS に無い
514
+ "Yu Gothic" だけが残って日本語が化ける)。各候補は _resolve_font_family_for_matplotlib で解決する。
515
+ """
516
+ resolved_families = []
517
+ stretch = None
518
+ for name in qfont.families():
519
+ resolved_name, resolved_stretch, _ok = _resolve_font_family_for_matplotlib(name)
520
+ resolved_families.append(resolved_name)
521
+ if stretch is None and resolved_stretch is not None:
522
+ stretch = resolved_stretch
523
+
524
+ result = {
525
+ 'family': resolved_families,
526
+ 'size': qfont.pointSize(),
527
+ 'weight': 'bold' if qfont.bold() else 'normal',
528
+ 'style': 'italic' if qfont.italic() else 'normal'
529
+ }
530
+ if stretch is not None:
531
+ result['stretch'] = stretch
532
+ return result
533
+
534
+ def _gather_settings_from_ui(self) -> dict:
535
+ """今の軸の設定を、画面の欄から集めた辞書(保存形式そのもの)で返す。キーは AXIS_SETTING_DEFAULTS と同じ順。"""
536
+ values = self._app._axis_binder.gather()
537
+ # 欄を持たず操作から直接書き込まれるもの。辞書は丸ごと入れ替わるので、引き継がないと消える。
538
+ settings_list = self._app.project.all_plot_settings
539
+ current = settings_list[self._app.project.active_axis_index] if self._app.project.active_axis_index < len(settings_list) else {}
540
+ for key in CARRIED_AXIS_KEYS:
541
+ values[key] = axis_setting(current, key)
542
+ return {key: values[key] for key in AXIS_SETTING_DEFAULTS}
543
+
544
+ def _apply_settings_to_ui_controls(self, settings: dict):
545
+ """設定の辞書を画面の欄に戻す。無いキーは AXIS_SETTING_DEFAULTS の既定値になる。"""
546
+ try:
547
+ # 欄に値を入れる間は、変更の通知で設定を書き換えないようにする
548
+ self._block_all_signals(True)
549
+
550
+ self._app._axis_binder.restore(settings, axis_setting)
551
+
552
+ # 復元では _on_x_autoscale_changed(今の表示範囲を欄に入れる)を呼ばない。
553
+ # 呼ぶと、オートスケールを切って保存した範囲が今の表示範囲で上書きされる。
554
+ self._refresh_x_autoscale_enabled_state()
555
+ self._refresh_y_autoscale_enabled_state()
556
+ self._refresh_x_tick_mode_state()
557
+ self._refresh_y_tick_mode_state()
558
+ self._refresh_x_minor_tick_state()
559
+ self._refresh_y_minor_tick_state()
560
+ self._refresh_legend_state()
561
+ self._refresh_grid_state()
562
+ self._refresh_y2_axis_state()
563
+ # 欄の状態をそろえてから 1 回だけ描く(どれも同じ設定で描くので、欄ごとに描いても結果は同じ)
564
+ self._app._update_plot_appearance()
565
+
566
+ except Exception as e:
567
+ notify.warning(self._app, "設定適用エラー", f"設定の適用中にエラーが発生しました:\n{e}")
568
+ logger.exception("設定の適用中にエラー")
569
+ finally:
570
+ # 途中で失敗しても必ず戻す
571
+ self._block_all_signals(False)
572
+
573
+ def _block_all_signals(self, block: bool):
574
+ self._app._axis_binder.block_signals(block)
575
+
576
+ def _connect_layout_signals(self):
577
+ self._app.subplot_rows_spinbox.valueChanged.connect(self._on_layout_changed)
578
+ self._app.subplot_cols_spinbox.valueChanged.connect(self._on_layout_changed)
579
+
580
+ self._app.share_x_checkbox.toggled.connect(self._on_share_axis_changed)
581
+ self._app.share_y_checkbox.toggled.connect(self._on_share_axis_changed)
582
+
583
+ self._app.active_axis_combo.currentIndexChanged.connect(self._on_active_axis_changed)
584
+
585
+ self._app.free_layout_checkbox.toggled.connect(self._app._on_toggle_free_layout)
586
+ self._app.add_free_subplot_button.clicked.connect(self._app._on_add_free_subplot)
587
+ self._app.remove_free_subplot_button.clicked.connect(self._app._on_remove_free_subplot)
588
+
589
+ self._app.free_layout_x_spinbox.valueChanged.connect(self._app._on_free_layout_position_spinbox_changed)
590
+ self._app.free_layout_y_spinbox.valueChanged.connect(self._app._on_free_layout_position_spinbox_changed)
591
+ self._app.free_layout_width_spinbox.valueChanged.connect(self._app._on_free_layout_position_spinbox_changed)
592
+ self._app.free_layout_height_spinbox.valueChanged.connect(self._app._on_free_layout_position_spinbox_changed)
593
+
594
+ def _connect_axis_setting_signals(self):
595
+ # 値を持つ欄は表(gui/axis_bindings.py)の順につなぐ。タイトルと軸ラベルの編集ボタンは _build_label_editors でつなぐ
596
+ self._app._axis_binder.connect()
597
+
598
+ self._app.ui.tick_font_button.clicked.connect(self._on_change_tick_font)
599
+ self._app.ui.tick_color_button.clicked.connect(self._on_change_tick_color)
600
+ self._app.ui.axis_label_font_button.clicked.connect(self._on_change_axis_label_font)
601
+ self._app.ui.axis_label_color_button.clicked.connect(self._on_change_axis_label_color)
602
+ self._app.legend_font_button.clicked.connect(self._on_change_legend_font)
603
+ self._app.legend_color_button.clicked.connect(self._on_change_legend_color)
604
+ self._app.legend_order_button.clicked.connect(self._on_edit_legend_order)
605
+ self._app.ui.spine_color_button.clicked.connect(self._on_change_spine_color)