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,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
書き込み可能なユーザー単位のフォルダ(Windows では %LOCALAPPDATA%\\Graphica 配下)。
|
|
3
|
+
|
|
4
|
+
読み取り専用の同梱リソースは resource_path() で読む。インストール先(Program Files)は
|
|
5
|
+
書き込めないので、アプリが書くものは必ずここに置く。カレントディレクトリには依存しない。
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from graphica.core.version import APP_NAME
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_app_data_dir() -> str:
|
|
14
|
+
"""無ければ作る。LOCALAPPDATA の無い環境では ~/.local/share/Graphica。"""
|
|
15
|
+
base = os.environ.get('LOCALAPPDATA')
|
|
16
|
+
if not base:
|
|
17
|
+
base = os.environ.get('XDG_DATA_HOME') or os.path.join(os.path.expanduser('~'), '.local', 'share')
|
|
18
|
+
app_dir = os.path.join(base, APP_NAME)
|
|
19
|
+
os.makedirs(app_dir, exist_ok=True)
|
|
20
|
+
return app_dir
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_user_plugins_dir() -> str:
|
|
24
|
+
"""利用者が zip から入れたプラグインの置き場所。無ければ作る。"""
|
|
25
|
+
plugins_dir = os.path.join(get_app_data_dir(), 'plugins')
|
|
26
|
+
os.makedirs(plugins_dir, exist_ok=True)
|
|
27
|
+
return plugins_dir
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_plugin_data_dir(plugin_name: str) -> str:
|
|
31
|
+
"""プラグインごとの書き込み用フォルダ。無ければ作る。名前はフォルダ名に使える文字だけ残す。"""
|
|
32
|
+
safe_name = re.sub(r'[^\w.-]', '_', plugin_name).strip('.') or '_'
|
|
33
|
+
data_dir = os.path.join(get_app_data_dir(), 'plugin_data', safe_name)
|
|
34
|
+
os.makedirs(data_dir, exist_ok=True)
|
|
35
|
+
return data_dir
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
1つの軸の設定(保存形式は文字列キーの辞書)のキーと既定値。
|
|
3
|
+
|
|
4
|
+
キーを持たない古いプロジェクトは、画面に戻すときも描画するときもここの既定値で読む。
|
|
5
|
+
既定値を変えると既存のプロジェクトの見た目が変わるので、変えるときは保存形式の移行として扱う。
|
|
6
|
+
"""
|
|
7
|
+
import copy
|
|
8
|
+
|
|
9
|
+
from graphica.core.unit_conversion import X_AXIS_UNIT_NONE
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
AXIS_SETTING_DEFAULTS = {
|
|
13
|
+
'title': '',
|
|
14
|
+
'x_label': '',
|
|
15
|
+
'y_label': '',
|
|
16
|
+
'x_label_visible': True,
|
|
17
|
+
'y_label_visible': True,
|
|
18
|
+
'y2_label': '',
|
|
19
|
+
|
|
20
|
+
'x_autoscale': True,
|
|
21
|
+
'x_min': 0,
|
|
22
|
+
'x_max': 1,
|
|
23
|
+
'x_log': False,
|
|
24
|
+
'x_invert': False,
|
|
25
|
+
'x_major_tick_mode': 0,
|
|
26
|
+
'x_major_tick_interval': 1,
|
|
27
|
+
'x_minor_ticks_visible': False,
|
|
28
|
+
'x_minor_tick_interval': 0.5,
|
|
29
|
+
'x_log_minor_subs': 'auto',
|
|
30
|
+
'x_log_minor_labels': False,
|
|
31
|
+
'x_tick_format_mode': 0,
|
|
32
|
+
'x_tick_decimals': -1,
|
|
33
|
+
'x_secondary_axis_source_unit': X_AXIS_UNIT_NONE,
|
|
34
|
+
'x_secondary_axis_target_unit': X_AXIS_UNIT_NONE,
|
|
35
|
+
|
|
36
|
+
'y_autoscale': True,
|
|
37
|
+
'y_min': 0,
|
|
38
|
+
'y_max': 1,
|
|
39
|
+
'y_log': False,
|
|
40
|
+
'y_invert': False,
|
|
41
|
+
'y_major_tick_mode': 0,
|
|
42
|
+
'y_major_tick_interval': 1,
|
|
43
|
+
'y_minor_ticks_visible': False,
|
|
44
|
+
'y_minor_tick_interval': 0.5,
|
|
45
|
+
'y_log_minor_subs': 'auto',
|
|
46
|
+
'y_log_minor_labels': False,
|
|
47
|
+
'y_tick_format_mode': 0,
|
|
48
|
+
'y_tick_decimals': -1,
|
|
49
|
+
|
|
50
|
+
'legend_visible': True,
|
|
51
|
+
'legend_loc': 'best',
|
|
52
|
+
'grid_visible': False,
|
|
53
|
+
'minor_grid_visible': False,
|
|
54
|
+
'x_major_grid_linestyle': '-',
|
|
55
|
+
'x_major_grid_width': 0.8,
|
|
56
|
+
'x_major_grid_alpha': 1.0,
|
|
57
|
+
'x_minor_grid_linestyle': '--',
|
|
58
|
+
'x_minor_grid_width': 0.5,
|
|
59
|
+
'x_minor_grid_alpha': 1.0,
|
|
60
|
+
'y_major_grid_linestyle': '-',
|
|
61
|
+
'y_major_grid_width': 0.8,
|
|
62
|
+
'y_major_grid_alpha': 1.0,
|
|
63
|
+
'y_minor_grid_linestyle': '--',
|
|
64
|
+
'y_minor_grid_width': 0.5,
|
|
65
|
+
'y_minor_grid_alpha': 1.0,
|
|
66
|
+
'major_tick_direction': 'out',
|
|
67
|
+
'minor_tick_direction': 'out',
|
|
68
|
+
'major_tick_direction_y2': 'out',
|
|
69
|
+
'minor_tick_direction_y2': 'out',
|
|
70
|
+
'x_ticks_visible': True,
|
|
71
|
+
'x_tick_labels_visible': True,
|
|
72
|
+
'y_ticks_visible': True,
|
|
73
|
+
'y_tick_labels_visible': True,
|
|
74
|
+
|
|
75
|
+
# フォントは {family, size, weight, style}。空なら画面側・描画側の既定のフォント
|
|
76
|
+
'tick_font': {},
|
|
77
|
+
'tick_color': '#000000',
|
|
78
|
+
'tick_width': 0.8,
|
|
79
|
+
# matplotlib の既定(xtick.major.size)と同じ。-1(負値)の補助目盛は「主目盛から自動」
|
|
80
|
+
'major_tick_length': 3.5,
|
|
81
|
+
'minor_tick_length': -1,
|
|
82
|
+
'axis_label_font': {},
|
|
83
|
+
'axis_label_color': '#000000',
|
|
84
|
+
'legend_font': {},
|
|
85
|
+
'legend_color': '#000000',
|
|
86
|
+
'spine_width': 1.0,
|
|
87
|
+
'spine_color': '#000000',
|
|
88
|
+
|
|
89
|
+
'colorbar_enabled': True,
|
|
90
|
+
'colorbar_position': 'right',
|
|
91
|
+
'colorbar_width_fraction': 0.05,
|
|
92
|
+
'colorbar_label': '',
|
|
93
|
+
|
|
94
|
+
# 画面の欄を持たず、操作から直接書き込まれるもの
|
|
95
|
+
'annotations': [],
|
|
96
|
+
'legend_order': [],
|
|
97
|
+
'free_rect': None,
|
|
98
|
+
'legend_position': None,
|
|
99
|
+
# 第 2 Y 軸(主の Y 軸と同じ意味)。既定は範囲・目盛りとも自動
|
|
100
|
+
'y2_autoscale': True,
|
|
101
|
+
'y2_min': 0,
|
|
102
|
+
'y2_max': 1,
|
|
103
|
+
'y2_log': False,
|
|
104
|
+
'y2_invert': False,
|
|
105
|
+
'y2_major_tick_mode': 0,
|
|
106
|
+
'y2_major_tick_interval': 1,
|
|
107
|
+
'y2_minor_ticks_visible': False,
|
|
108
|
+
'y2_minor_tick_interval': 0.5,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# 軸ごとに分ける前の共通キー。新しいキーが無いプロジェクトでは、その値を X/Y 両方に使う。
|
|
112
|
+
LEGACY_FALLBACK_KEYS = {
|
|
113
|
+
'x_ticks_visible': 'ticks_visible',
|
|
114
|
+
'y_ticks_visible': 'ticks_visible',
|
|
115
|
+
'x_tick_labels_visible': 'tick_labels_visible',
|
|
116
|
+
'y_tick_labels_visible': 'tick_labels_visible',
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def axis_setting(settings: dict[str, Any], key: str) -> Any:
|
|
121
|
+
"""settings の key の値。無ければ(古いキーがあればその値、それも無ければ)既定値。"""
|
|
122
|
+
if key in settings:
|
|
123
|
+
return settings[key]
|
|
124
|
+
legacy_key = LEGACY_FALLBACK_KEYS.get(key)
|
|
125
|
+
if legacy_key is not None and legacy_key in settings:
|
|
126
|
+
return settings[legacy_key]
|
|
127
|
+
# 既定値のリストや辞書を呼び出し側が書き換えても、既定値が変わらないように
|
|
128
|
+
return copy.deepcopy(AXIS_SETTING_DEFAULTS[key])
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""論文用の LaTeX のコードとキャプションを作る。GUI には依存しない。"""
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
# 1文字ずつ1回だけ置き換える(順に replace すると、置き換えで生まれた \ まで二重にエスケープしてしまう)
|
|
5
|
+
_LATEX_SPECIAL_CHARS = {
|
|
6
|
+
'\\': r'\textbackslash{}',
|
|
7
|
+
'{': r'\{',
|
|
8
|
+
'}': r'\}',
|
|
9
|
+
'$': r'\$',
|
|
10
|
+
'&': r'\&',
|
|
11
|
+
'%': r'\%',
|
|
12
|
+
'#': r'\#',
|
|
13
|
+
'_': r'\_',
|
|
14
|
+
'~': r'\textasciitilde{}',
|
|
15
|
+
'^': r'\textasciicircum{}',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
DEFAULT_WIDTH = r'\linewidth'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def escape_latex(text: str) -> str:
|
|
22
|
+
if not text:
|
|
23
|
+
return ''
|
|
24
|
+
return ''.join(_LATEX_SPECIAL_CHARS.get(ch, ch) for ch in text)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def sanitize_label(text: str, prefix: str = 'fig:') -> str:
|
|
28
|
+
"""タイトルなどから \\label{} に使える文字列を作る(英数字・-・_ だけ残す)。何も残らなければ既定の名前。"""
|
|
29
|
+
cleaned = re.sub(r'[^A-Za-z0-9_-]+', '', (text or '').strip().replace(' ', '-'))
|
|
30
|
+
# 日本語だけのタイトルなどで、ハイフンだけの意味の無いラベルにならないように
|
|
31
|
+
if not re.search(r'[A-Za-z0-9]', cleaned):
|
|
32
|
+
return f"{prefix}plot"
|
|
33
|
+
return f"{prefix}{cleaned}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def generate_latex_figure(image_filename: str, caption: str, label: str, width: str = DEFAULT_WIDTH) -> str:
|
|
37
|
+
"""figure 環境一式。image_filename が空なら 'figure.pdf'(後で差し替える前提)、label が空なら \\label 行を省く。"""
|
|
38
|
+
lines = [
|
|
39
|
+
r'\begin{figure}[htbp]',
|
|
40
|
+
r' \centering',
|
|
41
|
+
f" \\includegraphics[width={width}]{{{image_filename or 'figure.pdf'}}}",
|
|
42
|
+
f" \\caption{{{escape_latex(caption)}}}",
|
|
43
|
+
]
|
|
44
|
+
if label:
|
|
45
|
+
lines.append(f" \\label{{{label}}}")
|
|
46
|
+
lines.append(r'\end{figure}')
|
|
47
|
+
return '\n'.join(lines)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
組み込みの配色パレット(読み取り専用)と、利用者のパレットの検証。
|
|
3
|
+
利用者のパレットは QSettings の custom_color_palettes_json に保存する(gui/mixins/dataset_mixin.py)。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from graphica.core.named_colors import normalize_color
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
# 出典: Tableau 10、ColorBrewer(Set2/Dark2/Paired、パブリックドメイン)、
|
|
10
|
+
# Okabe & Ito (2008) Color Universal Design(1型/2型色覚でも判別しやすい8色)。
|
|
11
|
+
BUILTIN_PALETTES = {
|
|
12
|
+
'Okabe-Ito(色覚多様性対応)': [
|
|
13
|
+
'#000000', '#e69f00', '#56b4e9', '#009e73',
|
|
14
|
+
'#f0e442', '#0072b2', '#d55e00', '#cc79a7',
|
|
15
|
+
],
|
|
16
|
+
'Tableau 10': [
|
|
17
|
+
'#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f',
|
|
18
|
+
'#edc949', '#af7aa1', '#ff9da7', '#9c755f', '#bab0ab',
|
|
19
|
+
],
|
|
20
|
+
'ColorBrewer Set2': [
|
|
21
|
+
'#66c2a5', '#fc8d62', '#8da0cb', '#e78ac3', '#a6d854',
|
|
22
|
+
'#ffd92f', '#e5c494', '#b3b3b3',
|
|
23
|
+
],
|
|
24
|
+
'ColorBrewer Dark2': [
|
|
25
|
+
'#1b9e77', '#d95f02', '#7570b3', '#e7298a', '#66a61e',
|
|
26
|
+
'#e6ab02', '#a6761d', '#666666',
|
|
27
|
+
],
|
|
28
|
+
'ColorBrewer Paired': [
|
|
29
|
+
'#a6cee3', '#1f78b4', '#b2df8a', '#33a02c', '#fb9a99',
|
|
30
|
+
'#e31a1c', '#fdbf6f', '#ff7f00', '#cab2d6', '#6a3d9a',
|
|
31
|
+
],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def normalize_palettes(palettes: Any) -> dict[str, list[str]]:
|
|
36
|
+
"""利用者のパレット {名前: [色, ...]} を検証し、色を小文字の #rrggbb にしたコピーを返す。"""
|
|
37
|
+
if not isinstance(palettes, dict):
|
|
38
|
+
raise ValueError("パレットは {名前: [色, ...]} の形で渡してください。")
|
|
39
|
+
result = {}
|
|
40
|
+
for name, colors in palettes.items():
|
|
41
|
+
if not isinstance(name, str) or not name.strip():
|
|
42
|
+
raise ValueError("パレット名が空です。")
|
|
43
|
+
if name in BUILTIN_PALETTES:
|
|
44
|
+
raise ValueError(f"「{name}」は組み込みのパレットと同じ名前です。")
|
|
45
|
+
if not isinstance(colors, (list, tuple)):
|
|
46
|
+
raise ValueError(f"パレット「{name}」の色はリストで渡してください。")
|
|
47
|
+
result[name] = [normalize_color(c) for c in colors]
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def default_color_cycle() -> list[str]:
|
|
52
|
+
"""matplotlib の既定の色の並びを #rrggbb で返す。3.11 からは RGB の組で返り、QColor などに渡せない。"""
|
|
53
|
+
import matplotlib as mpl
|
|
54
|
+
from matplotlib.colors import to_hex
|
|
55
|
+
|
|
56
|
+
return [to_hex(color) for color in mpl.rcParams['axes.prop_cycle'].by_key()['color']]
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from PySide6.QtGui import QUndoCommand
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from graphica.core.dataset import Dataset
|
|
6
|
+
from graphica.models.project import ProjectModel
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# コマンドは Dataset の公開メソッドだけを呼び、GUI を知らない。表の描き直しと通知は
|
|
12
|
+
# DataEditorDialog が QUndoStack.indexChanged で行う。
|
|
13
|
+
|
|
14
|
+
class EditCellCommand(QUndoCommand):
|
|
15
|
+
def __init__(self, dataset: "Dataset", row_idx: Any, col_name: str, old_value: Any, new_value: Any,
|
|
16
|
+
description: str = "セル編集") -> None:
|
|
17
|
+
"""row_idx は dataset.df の index ラベル。"""
|
|
18
|
+
super().__init__(description)
|
|
19
|
+
self.dataset = dataset
|
|
20
|
+
self.row_idx = row_idx
|
|
21
|
+
self.col_name = col_name
|
|
22
|
+
self.old_value = old_value
|
|
23
|
+
self.new_value = new_value
|
|
24
|
+
|
|
25
|
+
def redo(self) -> None:
|
|
26
|
+
self.dataset.set_cell(self.row_idx, self.col_name, self.new_value)
|
|
27
|
+
|
|
28
|
+
def undo(self) -> None:
|
|
29
|
+
self.dataset.set_cell(self.row_idx, self.col_name, self.old_value)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AddRowCommand(QUndoCommand):
|
|
33
|
+
def __init__(self, dataset: "Dataset", description: str = "行追加") -> None:
|
|
34
|
+
super().__init__(description)
|
|
35
|
+
self.dataset = dataset
|
|
36
|
+
|
|
37
|
+
def redo(self) -> None:
|
|
38
|
+
self.dataset.add_row()
|
|
39
|
+
|
|
40
|
+
def undo(self) -> None:
|
|
41
|
+
self.dataset.delete_last_row()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DeleteRowsCommand(QUndoCommand):
|
|
45
|
+
def __init__(self, dataset: "Dataset", row_indices: list[Any], deleted_data: Any, description: str = "行削除") -> None:
|
|
46
|
+
"""row_indices は df の index ラベル、deleted_data は Undo 用の消した行。"""
|
|
47
|
+
super().__init__(description)
|
|
48
|
+
self.dataset = dataset
|
|
49
|
+
self.row_indices = row_indices
|
|
50
|
+
self.deleted_data = deleted_data
|
|
51
|
+
|
|
52
|
+
def redo(self) -> None:
|
|
53
|
+
self.dataset.delete_rows(self.row_indices)
|
|
54
|
+
|
|
55
|
+
def undo(self) -> None:
|
|
56
|
+
self.dataset.restore_rows(self.deleted_data)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AddColumnCommand(QUndoCommand):
|
|
60
|
+
def __init__(self, dataset: "Dataset", col_name: str, description: str = "列追加") -> None:
|
|
61
|
+
super().__init__(description)
|
|
62
|
+
self.dataset = dataset
|
|
63
|
+
self.col_name = col_name
|
|
64
|
+
|
|
65
|
+
def redo(self) -> None:
|
|
66
|
+
self.dataset.add_column(self.col_name)
|
|
67
|
+
|
|
68
|
+
def undo(self) -> None:
|
|
69
|
+
# 足した後にその列を X/Y にされていたら、消すと描画が壊れるので取り消さない
|
|
70
|
+
if self.dataset.is_column_in_use(self.col_name):
|
|
71
|
+
logger.warning("Undo不可: 列 '%s' はプロットに使用されています。", self.col_name)
|
|
72
|
+
self.setObsolete(True)
|
|
73
|
+
return
|
|
74
|
+
self.dataset.remove_column(self.col_name)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class DeleteColumnCommand(QUndoCommand):
|
|
78
|
+
def __init__(self, dataset: "Dataset", col_name: str, deleted_column_data: Any, description: str = "列削除") -> None:
|
|
79
|
+
super().__init__(description)
|
|
80
|
+
self.dataset = dataset
|
|
81
|
+
self.col_name = col_name
|
|
82
|
+
self.deleted_column_data = deleted_column_data
|
|
83
|
+
|
|
84
|
+
def redo(self) -> None:
|
|
85
|
+
# 取り消し → 列を X/Y に設定 → やり直し、の順でも壊れないよう、ここでも確かめる
|
|
86
|
+
if self.dataset.is_column_in_use(self.col_name):
|
|
87
|
+
logger.warning("Redo不可: 列 '%s' はプロットに使用されています。", self.col_name)
|
|
88
|
+
self.setObsolete(True)
|
|
89
|
+
return
|
|
90
|
+
self.dataset.remove_column(self.col_name)
|
|
91
|
+
|
|
92
|
+
def undo(self) -> None:
|
|
93
|
+
self.dataset.restore_column(self.col_name, self.deleted_column_data)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class RenameColumnCommand(QUndoCommand):
|
|
97
|
+
"""X/Y などの参照は Dataset.rename_column が新しい名前に付け替える。"""
|
|
98
|
+
def __init__(self, dataset: "Dataset", old_name: str, new_name: str, description: str = "列名の変更") -> None:
|
|
99
|
+
super().__init__(description)
|
|
100
|
+
self.dataset = dataset
|
|
101
|
+
self.old_name = old_name
|
|
102
|
+
self.new_name = new_name
|
|
103
|
+
|
|
104
|
+
def redo(self) -> None:
|
|
105
|
+
self.dataset.rename_column(self.old_name, self.new_name)
|
|
106
|
+
|
|
107
|
+
def undo(self) -> None:
|
|
108
|
+
self.dataset.rename_column(self.new_name, self.old_name)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class SetDatasetPropertiesCommand(QUndoCommand):
|
|
112
|
+
"""Dataset の属性(色、線種、名前、描画先など)をまとめて変える。"""
|
|
113
|
+
def __init__(self, dataset: "Dataset", old_values: dict[str, Any], new_values: dict[str, Any],
|
|
114
|
+
on_applied: Callable[[], Any], description: str = "プロパティ変更") -> None:
|
|
115
|
+
"""old_values / new_values は {属性名: 値}。on_applied は適用後の再描画など(GUI 側の仕事)。"""
|
|
116
|
+
super().__init__(description)
|
|
117
|
+
self.dataset = dataset
|
|
118
|
+
self.old_values = old_values
|
|
119
|
+
self.new_values = new_values
|
|
120
|
+
self.on_applied = on_applied
|
|
121
|
+
|
|
122
|
+
def redo(self) -> None:
|
|
123
|
+
for attr, value in self.new_values.items():
|
|
124
|
+
setattr(self.dataset, attr, value)
|
|
125
|
+
self.on_applied()
|
|
126
|
+
|
|
127
|
+
def undo(self) -> None:
|
|
128
|
+
for attr, value in self.old_values.items():
|
|
129
|
+
setattr(self.dataset, attr, value)
|
|
130
|
+
self.on_applied()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class SetMaskedRowsCommand(QUndoCommand):
|
|
134
|
+
"""行のマスクをまとめて切り替える。"""
|
|
135
|
+
def __init__(self, dataset: "Dataset", old_masked_indices: list[Any], new_masked_indices: list[Any],
|
|
136
|
+
description: str = "行の除外/解除") -> None:
|
|
137
|
+
"""マスクする行は df の index ラベルのリスト。"""
|
|
138
|
+
super().__init__(description)
|
|
139
|
+
self.dataset = dataset
|
|
140
|
+
self.old_masked_indices = list(old_masked_indices)
|
|
141
|
+
self.new_masked_indices = list(new_masked_indices)
|
|
142
|
+
|
|
143
|
+
def redo(self) -> None:
|
|
144
|
+
self.dataset.masked_row_indices = list(self.new_masked_indices)
|
|
145
|
+
|
|
146
|
+
def undo(self) -> None:
|
|
147
|
+
self.dataset.masked_row_indices = list(self.old_masked_indices)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class SetAnnotationsCommand(QUndoCommand):
|
|
151
|
+
"""1つの軸の注釈のリストを丸ごと置き換える(追加も削除もこれ)。"""
|
|
152
|
+
def __init__(self, project: "ProjectModel", axis_index: int, old_annotations: list[dict[str, Any]],
|
|
153
|
+
new_annotations: list[dict[str, Any]], on_applied: Callable[[], Any],
|
|
154
|
+
description: str = "注釈の変更") -> None:
|
|
155
|
+
super().__init__(description)
|
|
156
|
+
self.project = project
|
|
157
|
+
self.axis_index = axis_index
|
|
158
|
+
self.old_annotations = list(old_annotations)
|
|
159
|
+
self.new_annotations = list(new_annotations)
|
|
160
|
+
self.on_applied = on_applied
|
|
161
|
+
|
|
162
|
+
def redo(self) -> None:
|
|
163
|
+
self.project.all_plot_settings[self.axis_index]['annotations'] = list(self.new_annotations)
|
|
164
|
+
self.on_applied()
|
|
165
|
+
|
|
166
|
+
def undo(self) -> None:
|
|
167
|
+
self.project.all_plot_settings[self.axis_index]['annotations'] = list(self.old_annotations)
|
|
168
|
+
self.on_applied()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class AddDatasetCommand(QUndoCommand):
|
|
172
|
+
"""データセットを1つ追加する(プラグインの処理と解析の結果用)。
|
|
173
|
+
|
|
174
|
+
追加にはツリーの項目(フォルダの中の位置)が伴うので、追加と削除の処理そのものを GUI から受け取る。
|
|
175
|
+
"""
|
|
176
|
+
def __init__(self, add_callback: Callable[[], Any], remove_callback: Callable[[], Any],
|
|
177
|
+
description: str = "データセットの追加") -> None:
|
|
178
|
+
"""remove_callback は、直前に add_callback が足したものを取り除く。"""
|
|
179
|
+
super().__init__(description)
|
|
180
|
+
self.add_callback = add_callback
|
|
181
|
+
self.remove_callback = remove_callback
|
|
182
|
+
|
|
183
|
+
def redo(self) -> None:
|
|
184
|
+
self.add_callback()
|
|
185
|
+
|
|
186
|
+
def undo(self) -> None:
|
|
187
|
+
self.remove_callback()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class RemoveDatasetCommand(QUndoCommand):
|
|
191
|
+
"""データセットやフォルダの削除。
|
|
192
|
+
|
|
193
|
+
元の親フォルダの元の位置へ戻す必要があるので、削除と復元の処理そのものを GUI から受け取る。
|
|
194
|
+
"""
|
|
195
|
+
def __init__(self, remove_callback: Callable[[], Any], restore_callback: Callable[[], Any],
|
|
196
|
+
description: str = "データセットの削除") -> None:
|
|
197
|
+
"""restore_callback は、datasets の順とツリーの親・位置の両方を元に戻す。"""
|
|
198
|
+
super().__init__(description)
|
|
199
|
+
self.remove_callback = remove_callback
|
|
200
|
+
self.restore_callback = restore_callback
|
|
201
|
+
|
|
202
|
+
def redo(self) -> None:
|
|
203
|
+
self.remove_callback()
|
|
204
|
+
|
|
205
|
+
def undo(self) -> None:
|
|
206
|
+
self.restore_callback()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class ReorderDatasetsCommand(QUndoCommand):
|
|
210
|
+
"""project.datasets の並び(描画の順と重なり)を変える。"""
|
|
211
|
+
def __init__(self, project: "ProjectModel", old_order: "list[Dataset]", new_order: "list[Dataset]",
|
|
212
|
+
on_applied: Callable[[], Any], description: str = "データセットの並べ替え") -> None:
|
|
213
|
+
"""on_applied は一覧の同期と再描画。"""
|
|
214
|
+
super().__init__(description)
|
|
215
|
+
self.project = project
|
|
216
|
+
self.old_order = list(old_order)
|
|
217
|
+
self.new_order = list(new_order)
|
|
218
|
+
self.on_applied = on_applied
|
|
219
|
+
|
|
220
|
+
def redo(self) -> None:
|
|
221
|
+
self.project.datasets = list(self.new_order)
|
|
222
|
+
self.on_applied()
|
|
223
|
+
|
|
224
|
+
def undo(self) -> None:
|
|
225
|
+
self.project.datasets = list(self.old_order)
|
|
226
|
+
self.on_applied()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""色覚のシミュレーション。GUI には依存しない。
|
|
2
|
+
|
|
3
|
+
sRGB に直接掛ける近似の行列(Brettel / Viénot を簡略化したもの)で、プレビュー用。各行の和が 1 なので白・黒・灰は変わらない。
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
# 1型(赤)・2型(緑)・3型(青)
|
|
8
|
+
CVD_MATRICES = {
|
|
9
|
+
'protanopia': np.array([
|
|
10
|
+
[0.567, 0.433, 0.000],
|
|
11
|
+
[0.558, 0.442, 0.000],
|
|
12
|
+
[0.000, 0.242, 0.758],
|
|
13
|
+
]),
|
|
14
|
+
'deuteranopia': np.array([
|
|
15
|
+
[0.625, 0.375, 0.000],
|
|
16
|
+
[0.700, 0.300, 0.000],
|
|
17
|
+
[0.000, 0.300, 0.700],
|
|
18
|
+
]),
|
|
19
|
+
'tritanopia': np.array([
|
|
20
|
+
[0.950, 0.050, 0.000],
|
|
21
|
+
[0.000, 0.433, 0.567],
|
|
22
|
+
[0.000, 0.475, 0.525],
|
|
23
|
+
]),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
CVD_TYPE_LABELS = {
|
|
27
|
+
'protanopia': '1型(P型、赤色覚異常)',
|
|
28
|
+
'deuteranopia': '2型(D型、緑色覚異常)',
|
|
29
|
+
'tritanopia': '3型(T型、青色覚異常)',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def simulate_rgb_array(rgb_array: np.ndarray, cvd_type: str) -> np.ndarray:
|
|
34
|
+
"""形が (..., 3) の RGB に行列を掛ける。0〜255 のままでよい(スケールは保たれる)。クリップと型の変換は呼び出し側。"""
|
|
35
|
+
if cvd_type not in CVD_MATRICES:
|
|
36
|
+
raise ValueError(f"未知の色覚タイプです: {cvd_type}")
|
|
37
|
+
matrix = CVD_MATRICES[cvd_type]
|
|
38
|
+
original_shape = rgb_array.shape
|
|
39
|
+
flat = np.asarray(rgb_array, dtype=np.float64).reshape(-1, 3)
|
|
40
|
+
simulated = flat @ matrix.T
|
|
41
|
+
return simulated.reshape(original_shape)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def simulate_hex_color(hex_color: str, cvd_type: str) -> str:
|
|
45
|
+
"""#RRGGBB を1つ変換する(画像を介さない単色用)。"""
|
|
46
|
+
hex_color = hex_color.lstrip('#')
|
|
47
|
+
rgb = np.array([int(hex_color[i:i + 2], 16) for i in (0, 2, 4)], dtype=np.float64)
|
|
48
|
+
simulated = np.clip(simulate_rgb_array(rgb, cvd_type), 0, 255).astype(np.uint8)
|
|
49
|
+
return '#' + ''.join(f'{v:02x}' for v in simulated)
|