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.
- graphica/Graphica.ico +0 -0
- graphica/__init__.py +1 -0
- graphica/__main__.py +111 -0
- graphica/assets/__init__.py +0 -0
- graphica/assets/icons/__init__.py +0 -0
- graphica/assets/icons/arrow-right.svg +21 -0
- graphica/assets/icons/bold.svg +20 -0
- graphica/assets/icons/calculator.svg +26 -0
- graphica/assets/icons/chart-histogram.svg +24 -0
- graphica/assets/icons/chart-line.svg +20 -0
- graphica/assets/icons/chevron-down.svg +19 -0
- graphica/assets/icons/chevron-right.svg +19 -0
- graphica/assets/icons/color-swatch.svg +22 -0
- graphica/assets/icons/column-insert-right.svg +21 -0
- graphica/assets/icons/column-remove.svg +21 -0
- graphica/assets/icons/copy.svg +20 -0
- graphica/assets/icons/download.svg +21 -0
- graphica/assets/icons/edit.svg +21 -0
- graphica/assets/icons/eye-off.svg +21 -0
- graphica/assets/icons/eye.svg +20 -0
- graphica/assets/icons/file-plus.svg +22 -0
- graphica/assets/icons/folder-plus.svg +21 -0
- graphica/assets/icons/folder.svg +19 -0
- graphica/assets/icons/highlight.svg +22 -0
- graphica/assets/icons/history.svg +20 -0
- graphica/assets/icons/italic.svg +21 -0
- graphica/assets/icons/layout-grid.svg +22 -0
- graphica/assets/icons/math-function.svg +22 -0
- graphica/assets/icons/message-2.svg +21 -0
- graphica/assets/icons/mountain.svg +20 -0
- graphica/assets/icons/palette.svg +22 -0
- graphica/assets/icons/pointer.svg +19 -0
- graphica/assets/icons/refresh.svg +20 -0
- graphica/assets/icons/row-insert-bottom.svg +21 -0
- graphica/assets/icons/row-remove.svg +21 -0
- graphica/assets/icons/search.svg +20 -0
- graphica/assets/icons/select-all.svg +35 -0
- graphica/assets/icons/subscript.svg +20 -0
- graphica/assets/icons/superscript.svg +20 -0
- graphica/assets/icons/table.svg +21 -0
- graphica/assets/icons/trash.svg +23 -0
- graphica/assets/icons/typography.svg +23 -0
- graphica/assets/icons/x.svg +20 -0
- graphica/core/__init__.py +0 -0
- graphica/core/analysis.py +1278 -0
- graphica/core/app_paths.py +35 -0
- graphica/core/axis_settings.py +128 -0
- graphica/core/caption_export.py +47 -0
- graphica/core/color_palettes.py +56 -0
- graphica/core/commands.py +226 -0
- graphica/core/cvd_simulation.py +49 -0
- graphica/core/dataset.py +443 -0
- graphica/core/diagnostics.py +105 -0
- graphica/core/excel_utils.py +58 -0
- graphica/core/fit_models.py +379 -0
- graphica/core/grid_data.py +128 -0
- graphica/core/i18n.py +44 -0
- graphica/core/json_utils.py +27 -0
- graphica/core/label_utils.py +21 -0
- graphica/core/methods_text.py +115 -0
- graphica/core/named_colors.py +126 -0
- graphica/core/plugin_api.py +515 -0
- graphica/core/plugin_context.py +167 -0
- graphica/core/plugin_install.py +82 -0
- graphica/core/plugin_manifest.py +67 -0
- graphica/core/plugin_testing.py +213 -0
- graphica/core/plugin_types.py +118 -0
- graphica/core/provenance.py +19 -0
- graphica/core/report_export.py +51 -0
- graphica/core/safe_eval.py +194 -0
- graphica/core/script_export.py +272 -0
- graphica/core/translations_en.py +530 -0
- graphica/core/unit_conversion.py +51 -0
- graphica/core/update_check.py +43 -0
- graphica/core/version.py +14 -0
- graphica/gui/__init__.py +0 -0
- graphica/gui/app_settings.py +119 -0
- graphica/gui/axis_bindings.py +173 -0
- graphica/gui/binding.py +135 -0
- graphica/gui/builders/__init__.py +1 -0
- graphica/gui/builders/axis_panel.py +431 -0
- graphica/gui/builders/canvas_area.py +176 -0
- graphica/gui/builders/common.py +120 -0
- graphica/gui/builders/dataset_panel.py +433 -0
- graphica/gui/builders/property_sections.py +208 -0
- graphica/gui/canvas.py +519 -0
- graphica/gui/color_history.py +47 -0
- graphica/gui/color_picker_widget.py +198 -0
- graphica/gui/crash_handler.py +77 -0
- graphica/gui/cvd_preview.py +23 -0
- graphica/gui/data_editor.py +761 -0
- graphica/gui/data_import_flow.py +355 -0
- graphica/gui/dataset_bindings.py +107 -0
- graphica/gui/dataset_style_icon.py +80 -0
- graphica/gui/datasets/__init__.py +1 -0
- graphica/gui/datasets/actions_menu.py +114 -0
- graphica/gui/datasets/colors.py +218 -0
- graphica/gui/datasets/fitting.py +70 -0
- graphica/gui/datasets/host.py +162 -0
- graphica/gui/datasets/operations/__init__.py +1 -0
- graphica/gui/datasets/operations/fitting.py +468 -0
- graphica/gui/datasets/operations/peaks.py +109 -0
- graphica/gui/datasets/operations/processing.py +659 -0
- graphica/gui/datasets/operations/runner.py +148 -0
- graphica/gui/datasets/operations/transfer.py +227 -0
- graphica/gui/datasets/order.py +310 -0
- graphica/gui/datasets/overlays.py +74 -0
- graphica/gui/datasets/peaks.py +21 -0
- graphica/gui/datasets/plugin_runs.py +99 -0
- graphica/gui/datasets/processing.py +68 -0
- graphica/gui/datasets/property_panel.py +538 -0
- graphica/gui/datasets/transfer.py +36 -0
- graphica/gui/detached_canvas_window.py +22 -0
- graphica/gui/dialogs/__init__.py +137 -0
- graphica/gui/dialogs/analysis.py +1322 -0
- graphica/gui/dialogs/app.py +1015 -0
- graphica/gui/dialogs/appearance.py +851 -0
- graphica/gui/dialogs/data_edit.py +577 -0
- graphica/gui/dialogs/data_import.py +679 -0
- graphica/gui/dialogs/export.py +433 -0
- graphica/gui/dock_layout.py +176 -0
- graphica/gui/export_preview_panel.py +321 -0
- graphica/gui/export_settings.py +16 -0
- graphica/gui/file_association.py +97 -0
- graphica/gui/icon_utils.py +44 -0
- graphica/gui/main_app_window.py +210 -0
- graphica/gui/main_window.py +1151 -0
- graphica/gui/mathtext_preview.py +168 -0
- graphica/gui/menu_bar.py +320 -0
- graphica/gui/minimap_widget.py +151 -0
- graphica/gui/mixins/__init__.py +0 -0
- graphica/gui/mixins/export_mixin.py +520 -0
- graphica/gui/mixins/help_mixin.py +127 -0
- graphica/gui/mixins/project_io_mixin.py +289 -0
- graphica/gui/mixins/quick_access_mixin.py +194 -0
- graphica/gui/mixins/ui_setup_mixin.py +206 -0
- graphica/gui/notify.py +61 -0
- graphica/gui/panels/__init__.py +43 -0
- graphica/gui/panels/axis_settings.py +605 -0
- graphica/gui/panels/dataset_tree.py +424 -0
- graphica/gui/plot_type_drawers.py +131 -0
- graphica/gui/plugin_context.py +119 -0
- graphica/gui/project_files.py +361 -0
- graphica/gui/provenance_panel.py +65 -0
- graphica/gui/rendering/__init__.py +1 -0
- graphica/gui/rendering/annotations.py +177 -0
- graphica/gui/rendering/appearance.py +368 -0
- graphica/gui/rendering/common.py +316 -0
- graphica/gui/rendering/data_1d.py +376 -0
- graphica/gui/rendering/data_2d.py +66 -0
- graphica/gui/residual_panel.py +62 -0
- graphica/gui/resources.py +16 -0
- graphica/gui/single_instance.py +141 -0
- graphica/gui/splash.py +83 -0
- graphica/gui/task_runner.py +41 -0
- graphica/gui/theme.py +864 -0
- graphica/gui/tools/__init__.py +56 -0
- graphica/gui/tools/annotation.py +275 -0
- graphica/gui/tools/cursor.py +259 -0
- graphica/gui/tools/layout_edit.py +285 -0
- graphica/gui/tools/manager.py +78 -0
- graphica/gui/tools/peak_placement.py +132 -0
- graphica/gui/tools/range_select.py +191 -0
- graphica/gui/tools/region_highlight.py +222 -0
- graphica/gui/tools/slice_extraction.py +200 -0
- graphica/gui/widget_translation.py +46 -0
- graphica/gui/workers.py +280 -0
- graphica/models/__init__.py +0 -0
- graphica/models/project.py +197 -0
- graphica/plugin/__init__.py +20 -0
- graphica/plugin/testing.py +57 -0
- graphica/sample_data/__init__.py +0 -0
- graphica/sample_data/cooling_curve_sample.csv +42 -0
- graphica/ui_main_window.py +581 -0
- graphica_plot-2.0.0.dist-info/METADATA +317 -0
- graphica_plot-2.0.0.dist-info/RECORD +181 -0
- graphica_plot-2.0.0.dist-info/WHEEL +5 -0
- graphica_plot-2.0.0.dist-info/entry_points.txt +2 -0
- graphica_plot-2.0.0.dist-info/licenses/LICENSE +30 -0
- graphica_plot-2.0.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +91 -0
- 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]
|
graphica/gui/menu_bar.py
ADDED
|
@@ -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
|