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,194 @@
1
+ """利用者が入力した式を eval / exec を使わずに評価する(AST を辿り、許した構文だけ通す)。
2
+
3
+ 使えるのは数値・変数・四則演算・べき乗・剰余・比較・and / or / not(要素ごとの & / | / ~)と許した関数。
4
+ 列の計算では ALLOWED_SERIES_METHODS のメソッド(A.mean()、A.rolling(5).mean() など)も使える。
5
+ 属性の任意呼び出し・添字・lambda・内包表記・import・_ で始まる名前は拒否する。
6
+ 列の計算では、空白や記号を含む列名を `強度 (a.u.)` のようにバッククォートで囲んで書ける(pandas の query/eval と同じ)。
7
+ """
8
+ import ast
9
+ import keyword
10
+ import operator
11
+ import re
12
+
13
+ import numpy as np
14
+ from typing import Any, Callable
15
+
16
+
17
+ class SafeEvalError(ValueError):
18
+ """式に許していない構文・名前・関数・属性がある。"""
19
+
20
+
21
+ DEFAULT_FUNCTIONS: dict[str, Any] = {
22
+ 'exp': np.exp, 'log': np.log, 'log10': np.log10, 'sqrt': np.sqrt,
23
+ 'sin': np.sin, 'cos': np.cos, 'tan': np.tan, 'abs': np.abs,
24
+ 'pi': np.pi, 'e': np.e,
25
+ }
26
+
27
+ # 列の計算でだけ許すメソッド。I/O、任意のコールバックを取るもの(apply など)、ダンダー名は入れない
28
+ ALLOWED_SERIES_METHODS = frozenset({
29
+ 'mean', 'std', 'var', 'min', 'max', 'median', 'sum', 'abs',
30
+ 'diff', 'cumsum', 'cumprod', 'cummax', 'cummin', 'pct_change',
31
+ 'shift', 'rolling', 'expanding', 'ewm', 'rank', 'round', 'clip',
32
+ 'quantile', 'skew', 'kurt', 'count', 'size',
33
+ })
34
+
35
+ _BINOPS: dict[type, Callable[..., Any]] = {
36
+ ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
37
+ ast.Div: operator.truediv, ast.Pow: operator.pow, ast.Mod: operator.mod,
38
+ ast.FloorDiv: operator.floordiv,
39
+ }
40
+ # Series に素の not / and / or を使うと「真偽値が曖昧」になるので、pandas.eval と同じく ~ / & / | として扱う
41
+ _UNARYOPS: dict[type, Callable[..., Any]] = {ast.UAdd: operator.pos, ast.USub: operator.neg, ast.Not: operator.invert}
42
+ _BOOLOPS: dict[type, Callable[[Any, Any], Any]] = {ast.And: operator.and_, ast.Or: operator.or_}
43
+ _COMPAREOPS: dict[type, Callable[[Any, Any], Any]] = {
44
+ ast.Eq: operator.eq, ast.NotEq: operator.ne,
45
+ ast.Lt: operator.lt, ast.LtE: operator.le,
46
+ ast.Gt: operator.gt, ast.GtE: operator.ge,
47
+ }
48
+
49
+
50
+ def _parse(formula: str) -> ast.expr:
51
+ try:
52
+ tree = ast.parse(formula, mode='eval')
53
+ except SyntaxError as e:
54
+ raise SafeEvalError(f"数式の構文が不正です: {e}") from e
55
+ return tree.body
56
+
57
+
58
+ class _Evaluator:
59
+ def __init__(self, variables: dict[str, Any], functions: dict[str, Any],
60
+ allowed_methods: frozenset[str] | None) -> None:
61
+ self.variables = variables
62
+ self.functions = functions
63
+ # None なら属性アクセスを一切許さない(フィットの式)
64
+ self.allowed_methods = allowed_methods
65
+
66
+ def eval(self, node: ast.AST) -> Any:
67
+ if isinstance(node, ast.Constant):
68
+ if isinstance(node.value, (int, float, complex)):
69
+ return node.value
70
+ raise SafeEvalError(f"許可されていない定数です: {node.value!r}")
71
+
72
+ if isinstance(node, ast.Name):
73
+ if node.id in self.variables:
74
+ return self.variables[node.id]
75
+ if node.id in self.functions:
76
+ return self.functions[node.id]
77
+ raise SafeEvalError(f"未定義の変数/関数です: {node.id}")
78
+
79
+ if isinstance(node, ast.BinOp):
80
+ op = _BINOPS.get(type(node.op))
81
+ if op is None:
82
+ raise SafeEvalError(f"許可されていない演算子です: {type(node.op).__name__}")
83
+ return op(self.eval(node.left), self.eval(node.right))
84
+
85
+ if isinstance(node, ast.UnaryOp):
86
+ op = _UNARYOPS.get(type(node.op))
87
+ if op is None:
88
+ raise SafeEvalError(f"許可されていない単項演算子です: {type(node.op).__name__}")
89
+ return op(self.eval(node.operand))
90
+
91
+ if isinstance(node, ast.Compare):
92
+ left = self.eval(node.left)
93
+ result = None
94
+ for op_node, comparator in zip(node.ops, node.comparators):
95
+ op = _COMPAREOPS.get(type(op_node))
96
+ if op is None:
97
+ raise SafeEvalError(f"許可されていない比較演算子です: {type(op_node).__name__}")
98
+ right = self.eval(comparator)
99
+ partial = op(left, right)
100
+ result = partial if result is None else (result & partial)
101
+ left = right
102
+ return result
103
+
104
+ if isinstance(node, ast.BoolOp):
105
+ op = _BOOLOPS.get(type(node.op))
106
+ if op is None:
107
+ raise SafeEvalError(f"許可されていない論理演算子です: {type(node.op).__name__}")
108
+ values = [self.eval(v) for v in node.values]
109
+ result = values[0]
110
+ for v in values[1:]:
111
+ result = op(result, v)
112
+ return result
113
+
114
+ if isinstance(node, ast.Attribute):
115
+ if self.allowed_methods is None:
116
+ raise SafeEvalError("この数式では属性アクセスは使用できません。")
117
+ if node.attr.startswith('_') or node.attr not in self.allowed_methods:
118
+ raise SafeEvalError(f"許可されていないメソッド/属性です: {node.attr}")
119
+ return getattr(self.eval(node.value), node.attr)
120
+
121
+ if isinstance(node, ast.Call):
122
+ args = [self.eval(a) for a in node.args]
123
+ # **expr の展開は kw.arg が None。拒否しないと関数呼び出しで TypeError が漏れる
124
+ for kw in node.keywords:
125
+ if kw.arg is None:
126
+ raise SafeEvalError("この形式の関数呼び出し(**による引数展開)は許可されていません。")
127
+ kwargs = {str(kw.arg): self.eval(kw.value) for kw in node.keywords}
128
+ if isinstance(node.func, ast.Name):
129
+ name = node.func.id
130
+ if name not in self.functions or not callable(self.functions[name]):
131
+ raise SafeEvalError(f"許可されていない関数です: {name}")
132
+ return self.functions[name](*args, **kwargs)
133
+ if isinstance(node.func, ast.Attribute):
134
+ method = self.eval(node.func)
135
+ if not callable(method):
136
+ raise SafeEvalError(f"'{node.func.attr}' は呼び出せません。")
137
+ return method(*args, **kwargs)
138
+ raise SafeEvalError("この形式の関数呼び出しは許可されていません。")
139
+
140
+ raise SafeEvalError(f"許可されていない構文です: {type(node).__name__}")
141
+
142
+
143
+ def safe_eval_formula(formula: str, variables: dict[str, Any], functions: dict[str, Any] | None = None) -> Any:
144
+ """フィットの式を評価する。属性アクセスとメソッド呼び出しは許さない。functions は DEFAULT_FUNCTIONS に足す。"""
145
+ funcs = dict(DEFAULT_FUNCTIONS)
146
+ if functions:
147
+ funcs.update(functions)
148
+ node = _parse(formula)
149
+ return _Evaluator(variables, funcs, allowed_methods=None).eval(node)
150
+
151
+
152
+ _BACKTICK_COLUMN = re.compile(r"`([^`]*)`")
153
+
154
+
155
+ def column_reference(name: str) -> str:
156
+ """列の計算の式でその列を指す書き方。名前のまま書けない列名はバッククォートで囲む。"""
157
+ if name.isidentifier() and not keyword.iskeyword(name) and not name.startswith('_'):
158
+ return name
159
+ return f"`{name}`"
160
+
161
+
162
+ def _replace_backtick_columns(formula: str, variables: dict[str, Any]) -> str:
163
+ """`列名` を、ほかの名前とぶつからない変数名に置き換え、その変数を variables に足す。"""
164
+ taken = set(variables) | set(DEFAULT_FUNCTIONS)
165
+ placeholders: dict[str, str] = {}
166
+
167
+ def replace(match: re.Match[str]) -> str:
168
+ name = match.group(1)
169
+ if name not in placeholders:
170
+ if name not in variables:
171
+ raise SafeEvalError(f"未定義の列です: {name}")
172
+ index = len(placeholders)
173
+ while f"backtick_column_{index}" in taken:
174
+ index += 1
175
+ placeholder = f"backtick_column_{index}"
176
+ taken.add(placeholder)
177
+ placeholders[name] = placeholder
178
+ return placeholders[name]
179
+
180
+ replaced = _BACKTICK_COLUMN.sub(replace, formula)
181
+ if "`" in replaced:
182
+ raise SafeEvalError("列名を囲むバッククォート ` が閉じていません。")
183
+ for name, placeholder in placeholders.items():
184
+ variables[placeholder] = variables[name]
185
+ return replaced
186
+
187
+
188
+ def safe_eval_column_formula(df: Any, formula: str) -> Any:
189
+ """列名を変数として列の計算式を評価する(ALLOWED_SERIES_METHODS のメソッドも使える)。"""
190
+ variables = {str(col): df[col] for col in df.columns}
191
+ if "`" in formula:
192
+ formula = _replace_backtick_columns(formula, variables)
193
+ node = _parse(formula)
194
+ return _Evaluator(variables, dict(DEFAULT_FUNCTIONS), allowed_methods=ALLOWED_SERIES_METHODS).eval(node)
@@ -0,0 +1,272 @@
1
+ """プロジェクトを matplotlib だけで動くスクリプトにする。GUI には依存しない。
2
+
3
+ 再現するのはデータ、組み込みの種類、2D マップ、色・線・マーカー、タイトル・軸ラベル・範囲・対数軸・凡例・グリッド・
4
+ 第2Y軸まで。グラデーション・ウォーターフォール・誤差・注釈などは対象外(生成したスクリプトの先頭にも書く)。
5
+ プラグインの種類はスクリプトに持ち出せないので Line で代わりに出す。
6
+ """
7
+
8
+ from graphica.core.axis_settings import axis_setting
9
+ from graphica.core.dataset import COLOR_BY_COLUMN_PLOT_TYPE
10
+ from typing import TYPE_CHECKING, Any
11
+ if TYPE_CHECKING:
12
+ from graphica.core.dataset import Dataset
13
+ from graphica.models.project import ProjectModel
14
+
15
+
16
+ def _to_native(value: Any) -> Any:
17
+ """numpy 2 では np.float64 の repr が "np.float64(1.5)" になるので、Python の型にしてから repr する。"""
18
+ if hasattr(value, 'item'):
19
+ try:
20
+ return value.item()
21
+ except (ValueError, TypeError):
22
+ return value
23
+ return value
24
+
25
+
26
+ def _format_array_literal(values: Any) -> str:
27
+ """数値のリストのリテラル。NaN は repr すると未定義の nan になるので float('nan') と書く。"""
28
+ parts = []
29
+ for v in values:
30
+ v = _to_native(v)
31
+ if isinstance(v, float) and v != v: # NaN
32
+ parts.append("float('nan')")
33
+ else:
34
+ parts.append(repr(v))
35
+ return "[" + ", ".join(parts) + "]"
36
+
37
+
38
+ # 組み込みの種類(これ以外はプラグインのもの)
39
+ _BUILTIN_PLOT_TYPES = ('Line', 'Scatter', 'Line+Scatter', 'Area', 'Bar', 'Step',
40
+ COLOR_BY_COLUMN_PLOT_TYPE)
41
+
42
+
43
+ def _emit_dataset_plot_call(lines: list[str], ax_var: str, ds: "Dataset", mappable_var: str | None = None) -> bool:
44
+ """1次元の系列を描く呼び出しを書く。カラーバー用の mappable を mappable_var に入れたら True。"""
45
+ kwargs = f"color={ds.color!r}, alpha={ds.alpha!r}, label={ds.name!r}"
46
+ plot_type = ds.plot_type if ds.plot_type in _BUILTIN_PLOT_TYPES else None
47
+
48
+ if plot_type is None:
49
+ lines.append(
50
+ f"# plot_type {ds.plot_type!r} はプラグイン依存のため、Lineとして代替出力しています"
51
+ )
52
+ plot_type = 'Line'
53
+
54
+ if plot_type == COLOR_BY_COLUMN_PLOT_TYPE:
55
+ # scipy を使わない matplotlib だけの呼び出しなので、そのまま再現できる(Density Scatter は Line で代わりに出す)
56
+ z_values = ds.z_data
57
+ if z_values is None or len(z_values) != len(ds.x_data):
58
+ lines.append(
59
+ "# Z列が未設定/長さ不一致のため、単色のScatterとして出力しています"
60
+ )
61
+ lines.append(
62
+ f"{ax_var}.scatter(x, y, marker={ds.marker!r}, s={ds.markersize!r} ** 2, {kwargs})"
63
+ )
64
+ return False
65
+ assign = f"{mappable_var} = " if mappable_var else ""
66
+ lines.append(
67
+ f"{assign}{ax_var}.scatter(x, y, c=z, cmap={ds.colormap!r}, "
68
+ f"vmin={_to_native(ds.vmin)!r}, vmax={_to_native(ds.vmax)!r}, "
69
+ f"marker={ds.marker!r}, s={ds.markersize!r} ** 2, "
70
+ f"alpha={ds.alpha!r}, label={ds.name!r})"
71
+ )
72
+ return bool(mappable_var)
73
+
74
+ if plot_type == 'Line':
75
+ lines.append(f"{ax_var}.plot(x, y, linestyle={ds.linestyle!r}, linewidth={ds.linewidth!r}, {kwargs})")
76
+ elif plot_type == 'Scatter':
77
+ lines.append(f"{ax_var}.scatter(x, y, marker={ds.marker!r}, s={ds.markersize!r} ** 2, {kwargs})")
78
+ elif plot_type == 'Line+Scatter':
79
+ lines.append(
80
+ f"{ax_var}.plot(x, y, linestyle={ds.linestyle!r}, linewidth={ds.linewidth!r}, "
81
+ f"marker={ds.marker!r}, markersize={ds.markersize!r}, {kwargs})"
82
+ )
83
+ elif plot_type == 'Area':
84
+ lines.append(f"{ax_var}.fill_between(x, y, 0, color={ds.color!r}, alpha={ds.alpha!r} * 0.4, label={ds.name!r})")
85
+ lines.append(f"{ax_var}.plot(x, y, linestyle={ds.linestyle!r}, linewidth={ds.linewidth!r}, color={ds.color!r}, alpha={ds.alpha!r})")
86
+ elif plot_type == 'Bar':
87
+ lines.append(f"{ax_var}.bar(x, y, {kwargs})")
88
+ elif plot_type == 'Step':
89
+ lines.append(f"{ax_var}.plot(x, y, drawstyle='steps-post', linestyle={ds.linestyle!r}, linewidth={ds.linewidth!r}, {kwargs})")
90
+ return False
91
+
92
+
93
+ _VALID_MAP_DISPLAY_MODES = ('heatmap', 'contour', 'contour_filled', 'heatmap_contour')
94
+
95
+
96
+ def _emit_2d_dataset_plot_call(lines: list[str], ax_var: str, mesh_var: str, ds: "Dataset") -> bool:
97
+ """2D マップを pcolormesh / contour / contourf で書く。Dataset.z_grid の格子をそのまま埋め込む。
98
+
99
+ カラーバーの対象(塗りのあるモード)を mesh_var に作れたら True。線だけの contour や格子が作れないときは False。
100
+ """
101
+ grid = ds.z_grid
102
+ if grid is None:
103
+ lines.append(
104
+ f"# {ds.name!r} は2Dグリッドデータですが、有効なグリッドを構築できなかったため出力をスキップしています"
105
+ )
106
+ return False
107
+ x_grid, y_grid, z_grid = grid['x_grid'], grid['y_grid'], grid['z_grid']
108
+ lines.append(f"x = np.array({_format_array_literal(list(x_grid))})")
109
+ lines.append(f"y = np.array({_format_array_literal(list(y_grid))})")
110
+ z_rows = ", ".join(_format_array_literal(list(row)) for row in z_grid)
111
+ lines.append(f"z = np.array([{z_rows}])")
112
+
113
+ mode = ds.map_display_mode if ds.map_display_mode in _VALID_MAP_DISPLAY_MODES else 'heatmap'
114
+ vmin_kw = f", vmin={_to_native(ds.vmin)!r}" if ds.vmin is not None else ""
115
+ vmax_kw = f", vmax={_to_native(ds.vmax)!r}" if ds.vmax is not None else ""
116
+ has_mappable = False
117
+
118
+ if mode in ('heatmap', 'heatmap_contour'):
119
+ lines.append(
120
+ f"{mesh_var} = {ax_var}.pcolormesh(x, y, z, cmap={ds.colormap!r}, "
121
+ f"alpha={ds.alpha!r}, shading='auto'{vmin_kw}{vmax_kw})"
122
+ )
123
+ has_mappable = True
124
+ elif mode == 'contour_filled':
125
+ lines.append(
126
+ f"{mesh_var} = {ax_var}.contourf(x, y, z, levels={ds.contour_levels!r}, "
127
+ f"cmap={ds.colormap!r}, alpha={ds.alpha!r}{vmin_kw}{vmax_kw})"
128
+ )
129
+ has_mappable = True
130
+ if mode in ('contour', 'heatmap_contour'):
131
+ lines.append(
132
+ f"{ax_var}.contour(x, y, z, levels={ds.contour_levels!r}, colors={ds.color!r}, "
133
+ f"alpha={ds.alpha!r}, linewidths={ds.linewidth!r})"
134
+ )
135
+ return has_mappable
136
+
137
+
138
+ def _emit_secondary_y_calls(lines: list[str], ax_var: str, settings: dict[str, Any]) -> None:
139
+ """第 2 Y 軸のラベル・対数・範囲・反転(画面と同じ設定を使う)。"""
140
+ if axis_setting(settings, 'y2_label'):
141
+ lines.append(f"{ax_var}.set_ylabel({axis_setting(settings, 'y2_label')!r})")
142
+ if axis_setting(settings, 'y2_log'):
143
+ lines.append(f"{ax_var}.set_yscale('log')")
144
+ if not axis_setting(settings, 'y2_autoscale') and axis_setting(settings, 'y2_min') < axis_setting(settings, 'y2_max'):
145
+ lines.append(f"{ax_var}.set_ylim({axis_setting(settings, 'y2_min')!r}, {axis_setting(settings, 'y2_max')!r})")
146
+ if axis_setting(settings, 'y2_invert'):
147
+ lines.append(f"{ax_var}.invert_yaxis()")
148
+
149
+
150
+ def _emit_appearance_calls(lines: list[str], ax_var: str, settings: dict[str, Any], mesh_var: str | None = None) -> None:
151
+ if axis_setting(settings, 'title'):
152
+ lines.append(f"{ax_var}.set_title({settings['title']!r})")
153
+ if axis_setting(settings, 'x_label') and axis_setting(settings, 'x_label_visible'):
154
+ lines.append(f"{ax_var}.set_xlabel({settings['x_label']!r})")
155
+ if axis_setting(settings, 'y_label') and axis_setting(settings, 'y_label_visible'):
156
+ lines.append(f"{ax_var}.set_ylabel({settings['y_label']!r})")
157
+ if axis_setting(settings, 'x_log'):
158
+ lines.append(f"{ax_var}.set_xscale('log')")
159
+ if axis_setting(settings, 'y_log'):
160
+ lines.append(f"{ax_var}.set_yscale('log')")
161
+ if not axis_setting(settings, 'x_autoscale'):
162
+ lines.append(f"{ax_var}.set_xlim({axis_setting(settings, 'x_min')!r}, {axis_setting(settings, 'x_max')!r})")
163
+ if not axis_setting(settings, 'y_autoscale'):
164
+ lines.append(f"{ax_var}.set_ylim({axis_setting(settings, 'y_min')!r}, {axis_setting(settings, 'y_max')!r})")
165
+ if axis_setting(settings, 'grid_visible'):
166
+ lines.append(f"{ax_var}.grid(True)")
167
+ if axis_setting(settings, 'legend_visible'):
168
+ lines.append(f"{ax_var}.legend()")
169
+
170
+ if mesh_var is not None and axis_setting(settings, 'colorbar_enabled'):
171
+ position = axis_setting(settings, 'colorbar_position')
172
+ if position not in ('right', 'left', 'top', 'bottom'):
173
+ position = 'right'
174
+ fraction = axis_setting(settings, 'colorbar_width_fraction')
175
+ lines.append(
176
+ f"cbar = fig.colorbar({mesh_var}, ax={ax_var}, location={position!r}, "
177
+ f"fraction={fraction!r}, pad=0.04)"
178
+ )
179
+ if axis_setting(settings, 'colorbar_label'):
180
+ lines.append(f"cbar.set_label({settings['colorbar_label']!r})")
181
+
182
+
183
+ def generate_python_script(project: "ProjectModel") -> str:
184
+ """スクリプトのソースを返す。データは np.array としてスクリプトに埋め込む(1ファイルで再現できるが、点が多いと大きくなる)。"""
185
+ lines = [
186
+ '"""',
187
+ 'Graphicaから書き出されたスタンドアロンのPythonスクリプト。',
188
+ 'matplotlib/numpyのみで実行できます(Graphica本体は不要です)。',
189
+ '',
190
+ '既知の制限: グラデーション・ウォーターフォール・エラーバー・注釈・',
191
+ 'パネルラベル・第2X軸の単位変換・グリッド線の詳細設定・日付軸/カテゴリ軸の',
192
+ '専用フォーマット・プラグイン提供の描画方式は再現されません',
193
+ '(未対応のplot_typeはLineとして代替出力されます)。',
194
+ '"""',
195
+ 'import numpy as np',
196
+ 'import matplotlib.pyplot as plt',
197
+ '',
198
+ ]
199
+
200
+ all_plot_settings = project.all_plot_settings or [{}]
201
+ layout_mode = getattr(project, 'layout_mode', 'grid')
202
+
203
+ if layout_mode == 'free':
204
+ subplot_count = len(all_plot_settings)
205
+ lines.append('fig = plt.figure(figsize=(10, 8))')
206
+ lines.append('axes = []')
207
+ for settings in all_plot_settings:
208
+ rect = axis_setting(settings, 'free_rect') or (0.1, 0.1, 0.8, 0.8)
209
+ lines.append(f'axes.append(fig.add_axes({tuple(rect)!r}))')
210
+ else:
211
+ rows = getattr(project, 'layout_rows', 1) or 1
212
+ cols = getattr(project, 'layout_cols', 1) or 1
213
+ subplot_count = rows * cols
214
+ lines.append(f'fig, axes_grid = plt.subplots({rows}, {cols}, figsize=({6 * cols}, {4 * rows}))')
215
+ lines.append('axes = list(np.atleast_1d(axes_grid).flatten())')
216
+ lines.append('')
217
+
218
+ visible_datasets = [ds for ds in project.datasets if getattr(ds, 'visible', True)]
219
+ secondary_axis_indices = sorted({
220
+ ds.subplot_target for ds in visible_datasets
221
+ if ds.use_secondary_y and ds.subplot_target < subplot_count
222
+ })
223
+ for idx in secondary_axis_indices:
224
+ lines.append(f'ax{idx}_secondary = axes[{idx}].twinx()')
225
+ if secondary_axis_indices:
226
+ lines.append('')
227
+
228
+ mesh_var_by_axis: dict[int, str | None] = {}
229
+ # 1つの軸にカラーバーは1つなので、2D マップがある軸では2D マップを優先する(画面と同じ)。
230
+ # 系列の順に書くので、先にこの集合を作らないと順序次第で画面と食い違う
231
+ axes_with_2d = {
232
+ d.subplot_target for d in visible_datasets if d.data_kind == '2d_grid'
233
+ }
234
+ for ds in visible_datasets:
235
+ if ds.subplot_target >= subplot_count:
236
+ continue
237
+ ax_var = (
238
+ f'ax{ds.subplot_target}_secondary'
239
+ if (ds.use_secondary_y and ds.subplot_target in secondary_axis_indices)
240
+ else f'axes[{ds.subplot_target}]'
241
+ )
242
+ lines.append(f'# --- {ds.name} ---')
243
+ if ds.data_kind == '2d_grid':
244
+ mesh_var = f'mesh{ds.subplot_target}'
245
+ if _emit_2d_dataset_plot_call(lines, ax_var, mesh_var, ds):
246
+ mesh_var_by_axis[ds.subplot_target] = mesh_var
247
+ else:
248
+ lines.append(f'x = np.array({_format_array_literal(list(ds.x_data))})')
249
+ lines.append(f'y = np.array({_format_array_literal(list(ds.y_data))})')
250
+ # visible_df 経由なので、マスクした行があっても点と色がずれない
251
+ if ds.plot_type == COLOR_BY_COLUMN_PLOT_TYPE:
252
+ z_values = ds.z_data
253
+ if z_values is not None and len(z_values) == len(ds.x_data):
254
+ lines.append(f'z = np.array({_format_array_literal(list(z_values))})')
255
+ # 2D マップがある軸ではカラーバーを譲る(mesh{N} を上書きしないよう変数名も分ける)
256
+ wants_colorbar = ds.subplot_target not in axes_with_2d
257
+ scatter_mesh_var = f'scatter_mesh{ds.subplot_target}' if wants_colorbar else None
258
+ if _emit_dataset_plot_call(lines, ax_var, ds, mappable_var=scatter_mesh_var):
259
+ mesh_var_by_axis[ds.subplot_target] = scatter_mesh_var
260
+ lines.append('')
261
+
262
+ for i, settings in enumerate(all_plot_settings[:subplot_count]):
263
+ _emit_appearance_calls(lines, f'axes[{i}]', settings, mesh_var_by_axis.get(i))
264
+ if i in secondary_axis_indices:
265
+ _emit_secondary_y_calls(lines, f'ax{i}_secondary', settings)
266
+ lines.append('')
267
+
268
+ lines.append('plt.tight_layout()')
269
+ lines.append('plt.show()')
270
+ lines.append('')
271
+
272
+ return '\n'.join(lines)