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,168 @@
1
+ """タイトルと軸ラベルのテキスト(mathtext を含む)を、グラフと同じ matplotlib の描画で QPixmap にする。"""
2
+ import re
3
+
4
+ import numpy as np
5
+ from matplotlib.backends.backend_agg import FigureCanvasAgg
6
+ from matplotlib.figure import Figure
7
+ from PySide6.QtCore import Qt
8
+ from PySide6.QtGui import QImage, QPixmap
9
+ from PySide6.QtWidgets import QLabel
10
+
11
+ # 描いた範囲だけを後で切り取るので大きめでよい。ただし幅を超えた文字は切れ、縮小の判定を誤るので余裕を持たせる
12
+ _CANVAS_SIZE_INCHES = (10.0, 0.6)
13
+ _CROP_PADDING_PX = 3
14
+
15
+ # 日本語を描けるフォントの候補。matplotlib の既定(DejaVu Sans)には日本語が無い。Qt の UI 用の名前
16
+ # ("Yu Gothic UI")は matplotlib では見つからないので使わない。先頭から順にグリフのあるフォントが使われ、
17
+ # 無い名前は飛ばされるので OS ごとの候補を並べる(Yu Gothic などは Windows、Hiragino は macOS、Noto は Linux)。
18
+ # グラフ本体の既定フォント(main_window の PLOT_DEFAULT_FONT_FAMILIES)と注釈(rendering/annotations.py)もこれを使う。
19
+ # $...$ を含む文字列の数式の外の文字は、候補の先頭 1 つで描かれる(families_for_text)。mathtext.fontset は
20
+ # プロセス全体の設定で、変えると全部の描画に効くので触らない。
21
+ JP_CAPABLE_FONT_FAMILIES = [
22
+ "DejaVu Sans", "Yu Gothic", "Hiragino Sans", "Hiragino Kaku Gothic ProN",
23
+ "Meiryo", "MS Gothic", "Noto Sans CJK JP",
24
+ ]
25
+
26
+ _CJK_PATTERN = re.compile(r"[\u3000-\u30ff\u3400-\u9fff\uf900-\ufaff\uff00-\uffef]")
27
+ # 数式の外の文字を描ける日本語の字体(DejaVu Sans は日本語を持たない)
28
+ _JAPANESE_FONT_FAMILIES = [f for f in JP_CAPABLE_FONT_FAMILIES if f != "DejaVu Sans"]
29
+
30
+
31
+ def _has_mathtext(text):
32
+ """エスケープしていない $ が 2 つ以上あれば、matplotlib はその文字列を数式として描く。"""
33
+ return len(re.findall(r"(?<!\\)\$", text)) >= 2
34
+
35
+
36
+ def families_for_text(text, families):
37
+ """数式を含む文字列では、matplotlib は数式の外の文字を字体の候補の先頭 1 つだけで描く(ほかの候補に頼らない)。
38
+ 日本語と数式を混ぜたときだけ、日本語の字体を先頭に置いて □ にならないようにする。それ以外は families のまま。"""
39
+ if not text or not _CJK_PATTERN.search(text) or not _has_mathtext(text):
40
+ return families
41
+ if isinstance(families, str):
42
+ families = [families]
43
+ families = list(families or [])
44
+ if families and families[0].lower() in {f.lower() for f in _JAPANESE_FONT_FAMILIES}:
45
+ return families
46
+ return _JAPANESE_FONT_FAMILIES + [f for f in families if f not in _JAPANESE_FONT_FAMILIES]
47
+
48
+
49
+ def families_for_texts(texts, families):
50
+ """凡例のように 1 つの字体で複数の文字列を描くとき。どれかが日本語と数式を混ぜていれば、その並びにする。"""
51
+ for text in texts:
52
+ adjusted = families_for_text(text, families)
53
+ if adjusted is not families:
54
+ return adjusted
55
+ return families
56
+
57
+
58
+ def font_kwargs_for_text(text, font_kwargs):
59
+ """ax.set_title(text, **font_kwargs) などに渡す字体の辞書を、families_for_text() に合わせて返す(元は変えない)。"""
60
+ family = font_kwargs.get('family') if font_kwargs else None
61
+ adjusted = families_for_text(text, family)
62
+ if adjusted is family:
63
+ return font_kwargs
64
+ return dict(font_kwargs, family=adjusted)
65
+
66
+
67
+ # これより小さくすると読めないので、そこからは pixmap ごと縮める
68
+ _MIN_FONTSIZE = 7
69
+
70
+
71
+ def render_mathtext_to_pixmap(text, color="#000000", fontsize=13, dpi=150, max_width_px=None):
72
+ """描いた範囲だけを切り取った QPixmap。
73
+
74
+ mathtext の構文が壊れていれば、$ をエスケープして文字のまま描く(入力の邪魔をしない)。
75
+ max_width_px を超えればフォントを小さくし、下限でも収まらなければ pixmap を縮める。
76
+ """
77
+ pixmap = _render_once(text, color, fontsize, dpi)
78
+ if max_width_px and pixmap.width() > max_width_px > 0:
79
+ # 幅はフォントサイズにほぼ比例するので、比例で縮めて1回だけ描き直す
80
+ scaled_fontsize = max(_MIN_FONTSIZE, fontsize * (max_width_px / pixmap.width()) * 0.95)
81
+ pixmap = _render_once(text, color, scaled_fontsize, dpi)
82
+ if pixmap.width() > max_width_px > 0:
83
+ pixmap = pixmap.scaledToWidth(
84
+ max_width_px, Qt.TransformationMode.SmoothTransformation
85
+ )
86
+ return pixmap
87
+
88
+
89
+ def _render_once(text, color, fontsize, dpi):
90
+ fig = Figure(figsize=_CANVAS_SIZE_INCHES, dpi=dpi)
91
+ canvas = FigureCanvasAgg(fig)
92
+ fig.patch.set_alpha(0.0)
93
+ display_text = text if text else " "
94
+ try:
95
+ fig.text(0.01, 0.5, display_text, fontsize=fontsize, color=color,
96
+ family=families_for_text(display_text, JP_CAPABLE_FONT_FAMILIES), va='center', ha='left')
97
+ canvas.draw()
98
+ except ValueError: # mathtext の構文エラー。$ をそのまま文字として描き直す
99
+ fig = Figure(figsize=_CANVAS_SIZE_INCHES, dpi=dpi)
100
+ canvas = FigureCanvasAgg(fig)
101
+ fig.patch.set_alpha(0.0)
102
+ fig.text(0.01, 0.5, display_text.replace("$", "\\$"), fontsize=fontsize,
103
+ color=color, family=JP_CAPABLE_FONT_FAMILIES, va='center', ha='left')
104
+ canvas.draw()
105
+
106
+ buf = np.asarray(canvas.buffer_rgba())
107
+ cropped = _crop_to_content(buf)
108
+ cropped = np.ascontiguousarray(cropped)
109
+ height, width, _ = cropped.shape
110
+ image = QImage(cropped.data, width, height, width * 4, QImage.Format.Format_RGBA8888)
111
+ # QImage は cropped の配列を参照しているだけなので、関数を抜けると宙に浮く。copy() する
112
+ return QPixmap.fromImage(image.copy())
113
+
114
+
115
+ class FitWidthPixmapLabel(QLabel):
116
+ """set_natural_pixmap() で渡した等倍の pixmap を、ラベルの幅に収まるよう縮めて出す。
117
+
118
+ 描く時点の width() は、非表示のタブなどでは確定していないので、resizeEvent で合わせ直す。
119
+ """
120
+
121
+ def __init__(self, parent=None):
122
+ super().__init__(parent)
123
+ self._natural_pixmap = None
124
+
125
+ def set_natural_pixmap(self, pixmap):
126
+ self._natural_pixmap = pixmap
127
+ self._apply_fitted_pixmap()
128
+
129
+ def _apply_fitted_pixmap(self):
130
+ if self._natural_pixmap is None or self._natural_pixmap.isNull():
131
+ return
132
+ # QSS の枠と余白を除いた contentsRect() に合わせる(でないと端が欠ける)。
133
+ # 幅だけでなく高さもはみ出していれば、はみ出した方に合わせて縮める。
134
+ available_rect = self.contentsRect()
135
+ available_width = available_rect.width()
136
+ available_height = available_rect.height()
137
+ natural = self._natural_pixmap
138
+ if (available_width > 0 and natural.width() > available_width) or (
139
+ available_height > 0 and natural.height() > available_height
140
+ ):
141
+ fitted = natural.scaled(
142
+ max(available_width, 1), max(available_height, 1),
143
+ Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation,
144
+ )
145
+ else:
146
+ fitted = natural
147
+ super().setPixmap(fitted)
148
+
149
+ def resizeEvent(self, event):
150
+ super().resizeEvent(event)
151
+ self._apply_fitted_pixmap()
152
+
153
+
154
+ def _crop_to_content(rgba_array):
155
+ """不透明なピクセルの外接矩形(と少しの余白)で切り取る。"""
156
+ alpha = rgba_array[:, :, 3]
157
+ rows = np.any(alpha > 0, axis=1)
158
+ cols = np.any(alpha > 0, axis=0)
159
+ if not rows.any() or not cols.any():
160
+ return rgba_array[:1, :1]
161
+
162
+ row_indices = np.where(rows)[0]
163
+ col_indices = np.where(cols)[0]
164
+ row_min = max(0, row_indices[0] - _CROP_PADDING_PX)
165
+ row_max = min(rgba_array.shape[0] - 1, row_indices[-1] + _CROP_PADDING_PX)
166
+ col_min = max(0, col_indices[0] - _CROP_PADDING_PX)
167
+ col_max = min(rgba_array.shape[1] - 1, col_indices[-1] + _CROP_PADDING_PX)
168
+ return rgba_array[row_min:row_max + 1, col_min:col_max + 1]
@@ -0,0 +1,320 @@
1
+ """
2
+ メニューバーの中身の一覧表と、それを組み立てる関数。
3
+
4
+ 項目は PlotterApp のメソッド名で呼び先を指す。コマンドパレット・クイックアクセス・ショートカット一覧は
5
+ メニューを辿って項目を集め、そのパス(「ファイル > エクスポート > 印刷(&R)...」)を識別子として保存するので、
6
+ 文字列を変えると保存済みのピン留めが外れる。
7
+
8
+ PySide6 は、Python 側の参照が無くなった QMenu・menuAction・QAction を後から回収することがある
9
+ (メニューを辿ったとたん "already deleted" になる)。組み立てたものはすべて app._menu_keepalive に持たせる。
10
+ """
11
+ from dataclasses import dataclass
12
+ from typing import Callable, Optional
13
+
14
+ from PySide6.QtGui import QAction, QKeySequence
15
+ from PySide6.QtWidgets import QApplication
16
+
17
+ from graphica.core.i18n import tr
18
+ from graphica.core.version import APP_NAME
19
+ from graphica.gui.theme import apply_theme
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class Item:
24
+ """
25
+ メニューの1項目。slot は triggered(checkable なら toggled)に繋ぐメソッド名。
26
+ attr を指定すると app にその名前で持たせる(ほかの場所から有効/無効やチェックを変えるもの)。
27
+ checked は初めのチェック状態を持つ app の属性の点区切りの名前。after は作った後に呼ぶメソッド名。
28
+ """
29
+ text: str
30
+ slot: str
31
+ shortcut: object = None
32
+ attr: Optional[str] = None
33
+ checked: Optional[str] = None
34
+ after: Optional[str] = None
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class Submenu:
39
+ """on_show は開くたびに中身を作り直すメソッド名。fill_now なら作った時点でも一度呼ぶ。"""
40
+ text: str
41
+ items: tuple = ()
42
+ attr: Optional[str] = None
43
+ action_attr: Optional[str] = None
44
+ on_show: Optional[str] = None
45
+ fill_now: bool = False
46
+ after: Optional[str] = None
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class DockToggle:
51
+ """ドックの表示切り替え(ドック自身の toggleViewAction)。dock は app からの点区切りの名前。"""
52
+ dock: str
53
+ text: str
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class Call:
58
+ """メニューとは別に、この位置で呼ぶ app のメソッド(組み立ての順番に意味があるもの)。"""
59
+ method: str
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class Custom:
64
+ """表で書けない項目。build(app, menu, keep) が menu に足す。"""
65
+ build: Callable
66
+
67
+
68
+ SEPARATOR = object()
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class TopMenu:
73
+ text: str
74
+ attr: str
75
+ items: tuple = ()
76
+ action_attr: Optional[str] = None
77
+ on_show: Optional[str] = None
78
+ fill_now: bool = False
79
+
80
+
81
+ def _add_undo_redo(app, menu, keep):
82
+ undo_action = app.undo_stack.createUndoAction(app, tr("元に戻す"))
83
+ undo_action.setShortcut(QKeySequence.StandardKey.Undo)
84
+ menu.addAction(undo_action)
85
+ redo_action = app.undo_stack.createRedoAction(app, tr("やり直し"))
86
+ redo_action.setShortcut(QKeySequence.StandardKey.Redo)
87
+ menu.addAction(redo_action)
88
+ keep.extend([undo_action, redo_action])
89
+
90
+
91
+ def _add_command_palette(app, menu, keep):
92
+ # ウィンドウにも足して、メニューを開いていなくてもショートカットが効くようにする
93
+ app.command_palette_action = QAction(tr("コマンドパレット(&K)..."), app)
94
+ app.command_palette_action.setShortcut(QKeySequence("Ctrl+Shift+P"))
95
+ app.command_palette_action.triggered.connect(app._on_show_command_palette)
96
+ app.addAction(app.command_palette_action)
97
+ menu.addAction(app.command_palette_action)
98
+
99
+
100
+ FILE_MENU = TopMenu("ファイル(&F)", "_file_menu", (
101
+ # Ctrl+O は「プロジェクトを開く」のもの
102
+ Item("データファイルを開く(&D)...", "_on_add_dataset", attr="open_data_file_action"),
103
+ SEPARATOR,
104
+ Item("プロジェクトを開く(&O)...", "_on_load_project", QKeySequence.StandardKey.Open, attr="open_project_action"),
105
+ Item("上書き保存(&P)", "_on_save_project", QKeySequence.StandardKey.Save, attr="save_project_action"),
106
+ Item("名前を付けて保存(&A)...", "_on_save_project_as", QKeySequence.StandardKey.SaveAs,
107
+ attr="save_project_as_action"),
108
+ Item("クリップボードから貼り付け(&V)...", "_on_paste_data_from_clipboard"),
109
+ Item("フォルダから一括インポート(&F)...", "_on_import_folder"),
110
+ Submenu("最近使ったファイル", attr="recent_files_menu", action_attr="_recent_files_menu_action",
111
+ after="_update_recent_files_menu"),
112
+ Item("スタートアップ画面(&W)...", "_on_show_startup_screen"),
113
+ SEPARATOR,
114
+ Item("書式テンプレートを保存(&T)...", "_on_save_plot_template"),
115
+ Item("書式テンプレートを適用(&A)...", "_on_load_plot_template"),
116
+ SEPARATOR,
117
+ Submenu("エクスポート", attr="_export_menu", action_attr="_export_menu_action", items=(
118
+ # Ctrl+Shift+S は「名前を付けて保存」のもの(同じキーが2つあると Qt はどちらも発火させない)
119
+ Item("名前を付けてエクスポート(&S)...", "_on_export_plot", attr="save_action"),
120
+ # Ctrl+C は文字のコピーとぶつかるので割り当てない
121
+ Item("グラフをコピー(&C)", "_on_copy_plot_to_clipboard"),
122
+ Item("印刷(&R)...", "_on_print_plot", QKeySequence.StandardKey.Print, attr="print_action"),
123
+ Item("バッチエクスポート(&B)...", "_on_batch_export"),
124
+ Item("Pythonスクリプトとしてエクスポート...", "_on_export_python_script"),
125
+ Item("LaTeX/Word用キャプションを生成...", "_on_generate_caption"),
126
+ Item("実験レポートを生成 (HTML/PDF)...", "_on_generate_report"),
127
+ )),
128
+ SEPARATOR,
129
+ Item("オートセーブ間隔を設定(&I)...", "_on_configure_autosave_interval", attr="autosave_interval_action",
130
+ after="_update_autosave_menu_text"),
131
+ Item("自動バックアップ履歴から復元(&H)...", "_on_show_autosave_history"),
132
+ SEPARATOR,
133
+ Item("設定・スタイルをエクスポート(&X)...", "_on_export_settings"),
134
+ Item("設定・スタイルをインポート(&M)...", "_on_import_settings"),
135
+ ))
136
+
137
+ EDIT_MENU = TopMenu("編集(&E)", "_edit_menu", (
138
+ Custom(_add_undo_redo),
139
+ SEPARATOR,
140
+ Item("環境設定(&P)...", "_on_show_preferences"),
141
+ Custom(_add_command_palette),
142
+ ))
143
+
144
+ # 右クリックと同じ中身を開くたびに作る(片方だけに項目を足さないため)
145
+ DATASET_MENU = TopMenu("データセット(&D)", "_dataset_menu", action_attr="_dataset_menu_action",
146
+ on_show="_populate_dataset_menu", fill_now=True)
147
+
148
+ VIEW_MENU = TopMenu("表示(&V)", "_view_menu", (
149
+ DockToggle("ui.control_dock_widget", "プロパティパネル"),
150
+ DockToggle("export_preview_dock_widget", "エクスポートプレビュー"),
151
+ DockToggle("residual_dock_widget", "残差プロット"),
152
+ DockToggle("provenance_dock_widget", "処理履歴"),
153
+ Item("ミニマップ(レンジスライダー)", "_on_toggle_minimap", attr="minimap_action", checked="minimap_visible"),
154
+ Item("キャンバスを別ウィンドウに切り離す", "_on_toggle_canvas_detached", attr="canvas_detach_action",
155
+ checked="canvas_detached"),
156
+ Item("パネルラベルを自動表示 ((a)(b)(c)...)", "_on_toggle_panel_labels", attr="panel_labels_action",
157
+ checked="project.panel_labels_enabled"),
158
+ Item("色覚シミュレーションプレビュー...", "_on_show_cvd_simulation", attr="cvd_simulation_action"),
159
+ Submenu("ドックレイアウト", attr="_dock_layout_menu", action_attr="_dock_layout_menu_action", items=(
160
+ Item("現在のレイアウトを保存...", "_on_save_dock_layout_preset", attr="_save_layout_action"),
161
+ Submenu("レイアウトを読み込み", attr="load_layout_menu", action_attr="_load_layout_menu_action",
162
+ on_show="_populate_load_layout_menu"),
163
+ SEPARATOR,
164
+ Item("既定のレイアウトにリセット", "_on_reset_dock_layout", attr="_reset_layout_action"),
165
+ )),
166
+ # クイックアクセスのツールバーはここで作る(表示メニューの項目と同じ順番で組み立てる必要がある)
167
+ Call("_create_quick_access_toolbar"),
168
+ SEPARATOR,
169
+ Item("ダークモード", "_on_toggle_dark_mode", attr="dark_mode_action", checked="canvas.dark_mode"),
170
+ ))
171
+
172
+ HELP_MENU = TopMenu("ヘルプ(&H)", "_help_menu", (
173
+ Item("mathtext リファレンス...", "_on_show_help"),
174
+ Item("列計算機能 リファレンス...", "_on_show_calc_help"),
175
+ Item("キーボードショートカット一覧...", "_on_show_shortcuts"),
176
+ SEPARATOR,
177
+ Item("診断情報をエクスポート...", "_on_export_diagnostic_bundle"),
178
+ Item("アップデートを確認...", "_on_check_for_update"),
179
+ SEPARATOR,
180
+ Item("{app} について...", "_on_show_about"),
181
+ ))
182
+
183
+
184
+ def _resolve(app, dotted):
185
+ obj = app
186
+ for part in dotted.split('.'):
187
+ obj = getattr(obj, part)
188
+ return obj
189
+
190
+
191
+ def _set_attr(app, name, value):
192
+ if name:
193
+ setattr(app, name, value)
194
+
195
+
196
+ def _add_items(app, menu, items, keep):
197
+ for item in items:
198
+ if item is SEPARATOR:
199
+ menu.addSeparator()
200
+ elif isinstance(item, Item):
201
+ action = menu.addAction(tr(item.text).format(app=APP_NAME) if '{app}' in item.text else tr(item.text))
202
+ if item.shortcut is not None:
203
+ action.setShortcut(item.shortcut)
204
+ slot = getattr(app, item.slot)
205
+ if item.checked is not None:
206
+ action.setCheckable(True)
207
+ action.setChecked(_resolve(app, item.checked))
208
+ action.toggled.connect(slot)
209
+ else:
210
+ action.triggered.connect(slot)
211
+ keep.append(action)
212
+ _set_attr(app, item.attr, action)
213
+ if item.after:
214
+ getattr(app, item.after)()
215
+ elif isinstance(item, Submenu):
216
+ submenu = menu.addMenu(tr(item.text))
217
+ _hold_menu(app, submenu, item.attr, item.action_attr, keep)
218
+ _add_items(app, submenu, item.items, keep)
219
+ _connect_on_show(app, submenu, item.on_show, item.fill_now)
220
+ if item.after:
221
+ getattr(app, item.after)()
222
+ elif isinstance(item, DockToggle):
223
+ action = _resolve(app, item.dock).toggleViewAction()
224
+ action.setText(tr(item.text))
225
+ menu.addAction(action)
226
+ keep.append(action)
227
+ elif isinstance(item, Call):
228
+ getattr(app, item.method)()
229
+ elif isinstance(item, Custom):
230
+ item.build(app, menu, keep)
231
+ else:
232
+ raise TypeError(f"メニューの一覧表に知らない要素があります: {item!r}")
233
+
234
+
235
+ def _hold_menu(app, menu, attr, action_attr, keep):
236
+ # QMenu だけでなく、開くための menuAction も持つ(こちらが回収されるとメニューごと消える)
237
+ keep.extend([menu, menu.menuAction()])
238
+ _set_attr(app, attr, menu)
239
+ _set_attr(app, action_attr, menu.menuAction())
240
+
241
+
242
+ def _connect_on_show(app, menu, on_show, fill_now):
243
+ if on_show:
244
+ menu.aboutToShow.connect(getattr(app, on_show))
245
+ if fill_now:
246
+ getattr(app, on_show)()
247
+
248
+
249
+ def _add_top_menu(app, menu_bar, top, keep):
250
+ menu = menu_bar.addMenu(tr(top.text))
251
+ _hold_menu(app, menu, top.attr, top.action_attr, keep)
252
+ _add_items(app, menu, top.items, keep)
253
+ _connect_on_show(app, menu, top.on_show, top.fill_now)
254
+ return menu
255
+
256
+
257
+ def _add_plugin_menu(app, menu_bar, keep):
258
+ """プラグインがメニュー項目・データ処理・解析・パネルのどれかを登録しているときだけ作る。"""
259
+ api = app.plugin_api
260
+ processors = api.get_processors()
261
+ analyzers = api.get_analyzers()
262
+ if not (api.menu_actions or processors or analyzers or app._plugin_panel_docks):
263
+ return
264
+ plugin_menu = menu_bar.addMenu(tr("プラグイン(&P)"))
265
+ _hold_menu(app, plugin_menu, "_plugin_menu", None, keep)
266
+ for menu_action in api.menu_actions:
267
+ action = plugin_menu.addAction(menu_action.text)
268
+ if menu_action.shortcut:
269
+ action.setShortcut(QKeySequence(menu_action.shortcut))
270
+ action.triggered.connect(lambda checked=False, ma=menu_action: app._run_plugin_menu_action(ma))
271
+ keep.append(action)
272
+
273
+ if processors:
274
+ if api.menu_actions:
275
+ plugin_menu.addSeparator()
276
+ processing_menu = plugin_menu.addMenu(tr("データ処理"))
277
+ _hold_menu(app, processing_menu, None, None, keep)
278
+ by_category = {}
279
+ for proc in processors:
280
+ by_category.setdefault(proc.category, []).append(proc)
281
+ for category in sorted(by_category.keys()):
282
+ category_menu = processing_menu.addMenu(category)
283
+ _hold_menu(app, category_menu, None, None, keep)
284
+ for proc in sorted(by_category[category], key=lambda p: p.name):
285
+ action = category_menu.addAction(proc.name)
286
+ action.triggered.connect(lambda checked=False, p=proc: app.plugin_runs.run_processor(p))
287
+ keep.append(action)
288
+
289
+ if analyzers:
290
+ if api.menu_actions or processors:
291
+ plugin_menu.addSeparator()
292
+ analysis_menu = plugin_menu.addMenu(tr("解析"))
293
+ _hold_menu(app, analysis_menu, None, None, keep)
294
+ for analyzer in sorted(analyzers, key=lambda a: a.name):
295
+ action = analysis_menu.addAction(analyzer.name)
296
+ action.triggered.connect(lambda checked=False, a=analyzer: app.plugin_runs.run_analyzer(a))
297
+ keep.append(action)
298
+
299
+ if app._plugin_panel_docks:
300
+ if api.menu_actions or processors or analyzers:
301
+ plugin_menu.addSeparator()
302
+ panel_menu = plugin_menu.addMenu(tr("パネル"))
303
+ _hold_menu(app, panel_menu, None, None, keep)
304
+ for name in sorted(app._plugin_panel_docks.keys()):
305
+ action = app._plugin_panel_docks[name].toggleViewAction()
306
+ panel_menu.addAction(action)
307
+ keep.append(action)
308
+
309
+
310
+ def build_menu_bar(app):
311
+ """app のメニューバーを一覧表から組み立てる。__init__ から1回だけ呼ぶ。"""
312
+ keep = []
313
+ app._menu_keepalive = keep
314
+ menu_bar = app.menuBar()
315
+ for top in (FILE_MENU, EDIT_MENU, DATASET_MENU, VIEW_MENU):
316
+ _add_top_menu(app, menu_bar, top, keep)
317
+ # アプリ全体のスタイル(Fusion)とテーマを当て直す(何度呼んでも同じ)
318
+ apply_theme(QApplication.instance(), app.canvas.dark_mode)
319
+ _add_plugin_menu(app, menu_bar, keep)
320
+ _add_top_menu(app, menu_bar, HELP_MENU, keep)
@@ -0,0 +1,151 @@
1
+ """グラフの下の小さな全体図。ドラッグで選んだ範囲を range_selected で知らせる(軸に当てるのは呼び出し側)。
2
+
3
+ メインのキャンバスとは別の Figure なので、redraw_all() では描き直されない。データが変わったら refresh() を呼ぶこと。
4
+ """
5
+ import logging
6
+
7
+ import numpy as np
8
+ from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
9
+ from matplotlib.figure import Figure
10
+ from matplotlib.widgets import SpanSelector
11
+ from PySide6.QtCore import Signal
12
+
13
+ from graphica.core.analysis import calculate_lttb_downsample
14
+ from graphica.core.dataset import COLOR_BY_COLUMN_PLOT_TYPE
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # 点で描く種類。Line+Scatter は線が主なので含めない
19
+ MINIMAP_POINT_PLOT_TYPES = frozenset({'Scatter', 'Density Scatter', COLOR_BY_COLUMN_PLOT_TYPE})
20
+
21
+ MINIMAP_HEIGHT_PX = 70
22
+
23
+ # 間引く。SpanSelector の背景は全体を描いた画像で、refresh() のたびに作り直すので、点が多いとドラッグが重くなる。
24
+ # 幅が数百 px しかないので、canvas の LTTB より少ない点でよい
25
+ MINIMAP_DOWNSAMPLE_THRESHOLD = 2000
26
+ MINIMAP_DOWNSAMPLE_TARGET_POINTS = 500
27
+
28
+ # gui/canvas.py の配色と揃える
29
+ DARK_FIGURE_FACECOLOR = '#2b2b2b'
30
+ # 周りのパネルと同じ寒色寄りの色味で、少しだけ暗くして一段くぼんだ領域に見せる
31
+ DARK_AXES_FACECOLOR = '#0E1114'
32
+ DARK_LINE_COLOR = '#8ab4f8'
33
+ DARK_SPAN_COLOR = '#8ab4f8'
34
+ LIGHT_FIGURE_FACECOLOR = '#ffffff'
35
+ LIGHT_AXES_FACECOLOR = '#E3E6EB'
36
+ LIGHT_LINE_COLOR = '#1a73e8'
37
+ LIGHT_SPAN_COLOR = '#1a73e8'
38
+
39
+
40
+ class MinimapWidget(FigureCanvas):
41
+ range_selected = Signal(float, float)
42
+
43
+ def __init__(self, parent=None, dpi=100):
44
+ self.fig = Figure(figsize=(5, 0.7), dpi=dpi)
45
+ super().__init__(self.fig)
46
+ self.setParent(parent)
47
+ self.setFixedHeight(MINIMAP_HEIGHT_PX)
48
+
49
+ self.ax = self.fig.add_subplot(111)
50
+ self.dark_mode = False
51
+ self._span_selector = None
52
+
53
+ self._configure_axes_style()
54
+ self._create_span_selector()
55
+ self._apply_theme_colors()
56
+
57
+
58
+ def _configure_axes_style(self):
59
+ self.ax.set_yticks([])
60
+ self.ax.tick_params(axis='x', labelsize=6)
61
+ for spine in ('top', 'right', 'left'):
62
+ self.ax.spines[spine].set_visible(False)
63
+ self.fig.subplots_adjust(left=0.02, right=0.98, top=0.92, bottom=0.28)
64
+
65
+ def _create_span_selector(self):
66
+ """SpanSelector を作り直す(ax.cla() が選択の矩形も消すので、refresh() のたびに)。
67
+
68
+ 古いものの接続は切る。matplotlib は cla() でも切らないので、残すと溜まって range_selected が重複して出る。
69
+ """
70
+ if getattr(self, '_span_selector', None) is not None:
71
+ self._span_selector.disconnect_events()
72
+ span_color = DARK_SPAN_COLOR if self.dark_mode else LIGHT_SPAN_COLOR
73
+ self._span_selector = SpanSelector(
74
+ self.ax,
75
+ self._on_select,
76
+ 'horizontal',
77
+ useblit=True,
78
+ props=dict(alpha=0.3, facecolor=span_color),
79
+ interactive=True,
80
+ drag_from_anywhere=True,
81
+ )
82
+
83
+ def _apply_theme_colors(self):
84
+ fig_face = DARK_FIGURE_FACECOLOR if self.dark_mode else LIGHT_FIGURE_FACECOLOR
85
+ axes_face = DARK_AXES_FACECOLOR if self.dark_mode else LIGHT_AXES_FACECOLOR
86
+ self.fig.set_facecolor(fig_face)
87
+ self.ax.set_facecolor(axes_face)
88
+
89
+ def _on_select(self, xmin, xmax):
90
+ if xmin == xmax:
91
+ # クリックだけ(ドラッグなし)は選択とみなさない
92
+ return
93
+ self.range_selected.emit(xmin, xmax)
94
+
95
+ @staticmethod
96
+ def _downsample_for_overview(x, y):
97
+ """点を減らす。X が昇順なら LTTB、そうでなければ等間隔に間引く(LTTB は昇順が前提)。"""
98
+ n = len(x)
99
+ if n <= MINIMAP_DOWNSAMPLE_THRESHOLD:
100
+ return x, y
101
+ if np.all(np.diff(x) >= 0):
102
+ idx = calculate_lttb_downsample(x, y, MINIMAP_DOWNSAMPLE_TARGET_POINTS)
103
+ else:
104
+ step = max(1, n // MINIMAP_DOWNSAMPLE_TARGET_POINTS)
105
+ idx = np.arange(0, n, step)
106
+ return x[idx], y[idx]
107
+
108
+
109
+ def refresh(self, datasets, dark_mode=False):
110
+ """各データセットの X/Y を薄い線か点で重ねるだけの簡単な全体図にする。"""
111
+ self.dark_mode = dark_mode
112
+ self.ax.cla()
113
+ self._configure_axes_style()
114
+ self._apply_theme_colors()
115
+
116
+ line_color = DARK_LINE_COLOR if dark_mode else LIGHT_LINE_COLOR
117
+ has_data = False
118
+ for ds in datasets:
119
+ # メインと同じく隠した系列は出さない
120
+ if not getattr(ds, 'visible', True):
121
+ continue
122
+ # 2D マップの x/y は測定点の列なので、線にすると意味が無い。描かない
123
+ if getattr(ds, 'data_kind', '1d') == '2d_grid':
124
+ continue
125
+ try:
126
+ x = ds.x_data
127
+ y = ds.y_data
128
+ except Exception:
129
+ logger.debug("ミニマップ用のデータを取り出せません: %s", ds.name, exc_info=True)
130
+ continue
131
+ if x is None or len(x) == 0:
132
+ continue
133
+ try:
134
+ x, y = self._downsample_for_overview(x, y)
135
+ if getattr(ds, 'plot_type', 'Line') in MINIMAP_POINT_PLOT_TYPES:
136
+ # 点の種類は点で描く(線にするとピーク検出の結果がピーク同士を結ぶ線になる)
137
+ self.ax.plot(x, y, linestyle='None', marker='o', markersize=1.8,
138
+ color=line_color, alpha=0.8)
139
+ else:
140
+ self.ax.plot(x, y, color=line_color, linewidth=0.7, alpha=0.6)
141
+ has_data = True
142
+ except Exception:
143
+ # 全体図なので、描けなくても止めない
144
+ logger.debug("ミニマップへのデータセット描画に失敗しました", exc_info=True)
145
+
146
+ if has_data:
147
+ self.ax.relim()
148
+ self.ax.autoscale_view()
149
+
150
+ self._create_span_selector()
151
+ self.draw_idle()
File without changes