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
graphica/gui/canvas.py ADDED
@@ -0,0 +1,519 @@
1
+ import logging
2
+
3
+ from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
4
+ from matplotlib.backends.backend_agg import FigureCanvasAgg
5
+ from matplotlib.figure import Figure
6
+
7
+ from graphica.core.axis_settings import axis_setting
8
+ from graphica.gui.app_settings import DEFAULT_POINT_LABEL_MAX_POINTS
9
+ from graphica.gui.rendering import annotations, appearance, data_1d, data_2d
10
+ # 描画の定数と補助は rendering/common.py にある。テストとほかのモジュールがここから読むので同じ名前で出す
11
+ from graphica.gui.rendering.common import ( # noqa: F401
12
+ _NO_KEY, MAX_TICKS_PER_AXIS, WATERFALL_ZORDER_BASE, WATERFALL_ZORDER_TOP,
13
+ WATERFALL_DEPTH_SHRINK_MIN_SCALE, _LAYOUT_SETTLED_TOLERANCE, _LAYOUT_MAX_PASSES, fit_tight_layout,
14
+ _waterfall_depth_scale, _WaterfallLayout, _waterfall_layout, _AxisStyle, _LOG_MINOR_SUBS_PRESETS,
15
+ _ARROW_STYLE_MAP, _INSET_CORNER_ORIGINS, LTTB_DOWNSAMPLE_THRESHOLD, LTTB_DOWNSAMPLE_TARGET_POINTS,
16
+ GRID_2D_MAX_DISPLAY_POINTS_PER_AXIS, REGION_HIGHLIGHT_DEFAULT_COLOR, REGION_HIGHLIGHT_DEFAULT_ALPHA,
17
+ _apply_legend_order, _apply_nan_policy, STAT_LABEL_TITLES, _legend_position_from_settings,
18
+ _compute_stat_label_text, _safe_multiple_locator, _sci_each_formatter, _apply_tick_format_mode,
19
+ _apply_tick_decimal_places, DEFAULT_MAJOR_TICK_LENGTH, MINOR_TICK_LENGTH_RATIO, MINOR_TICK_LENGTH_AUTO,
20
+ _resolve_tick_lengths, DARK_FIGURE_FACECOLOR, DARK_AXES_FACECOLOR, DARK_TEXT_COLOR, LIGHT_FIGURE_FACECOLOR,
21
+ LIGHT_AXES_FACECOLOR, LIGHT_TEXT_COLOR, DARK_LEGEND_FACECOLOR, DARK_LEGEND_EDGECOLOR, LIGHT_LEGEND_FACECOLOR,
22
+ LIGHT_LEGEND_EDGECOLOR, DARK_GRID_COLOR, LIGHT_GRID_COLOR)
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class _CanvasDrawingMixin:
28
+ """
29
+ 描画の本体。Qt に依存しないので、画面用の MplCanvas(FigureCanvasQTAgg)と
30
+ 一括エクスポート用の _HeadlessRenderCanvas(FigureCanvasAgg)の両方が使う。
31
+ """
32
+
33
+ def _init_drawing_state(self, width, height, dpi):
34
+ self.fig = Figure(figsize=(width, height), dpi=dpi)
35
+
36
+ self.all_axes = []
37
+ self.all_secondary_axes = []
38
+ # 軸ごとに、X が日時か(目盛りの付け方)・文字列カテゴリか(数値向けの設定を使わない)
39
+ self.axis_is_date_x = []
40
+ self.axis_is_category_x = []
41
+ self.dark_mode = False
42
+ self.point_label_max_points = DEFAULT_POINT_LABEL_MAX_POINTS
43
+ # 軸ごとの描画済みの注釈。update_appearance_only は fig.clf() しないので、消してから描き直す
44
+ self._annotation_artists = {}
45
+ # 軸ごとに直近に描いた注釈の内容。同じなら描き直しを省く(_annotation_render_key)
46
+ self._annotation_render_keys = {}
47
+ # データエディタと連動する行のハイライト(dataset_id ごと)
48
+ self._highlight_artists = {}
49
+ # 間引いたデータセットの「描いた点の添字 -> visible_df の位置」(dataset_id ごと)。
50
+ # データカーソルがクリックした点を正しい行に戻すのに使う。間引いていなければ入らない
51
+ self.downsample_index_map = {}
52
+ # 平滑化した曲線のデータセット。点が元の行と対応しないのでクリックで選ばせない。
53
+ # データカーソルモードの一括 set_picker(cursor_mixin.py)もこれを見て除く
54
+ self._non_pickable_dataset_ids = set()
55
+ # 軸ごとのカラーバーの対象(2Dマップか値で色分けした散布図)。_apply_appearance に渡すため
56
+ self._axis_2d_mappables = {}
57
+ # ウォーターフォールの変換(dataset_id ごと)。トレースは
58
+ # 表示X = データX + index * offset_x
59
+ # 表示Y = データY * depth_scale + index * offset_y
60
+ # に描かれるので、マウス位置をデータの行に対応づける側は逆変換(display_to_data)を通す。
61
+ # 値は {'index', 'offset_x', 'offset_y', 'depth_scale'}。ウォーターフォールでなければ入らない
62
+ self._waterfall_transforms = {}
63
+
64
+ # --- ウォーターフォールの表示座標 ⇔ データ座標 ---
65
+
66
+ def get_waterfall_transform(self, dataset_or_id):
67
+ """
68
+ データセットの今のウォーターフォール変換。無効または未描画なら None。
69
+
70
+ Args:
71
+ dataset_or_id: Dataset オブジェクト、または dataset_id。
72
+ """
73
+ dataset_id = getattr(dataset_or_id, 'dataset_id', dataset_or_id)
74
+ return self._waterfall_transforms.get(dataset_id)
75
+
76
+ def data_to_display(self, dataset_or_id, x, y):
77
+ """データ座標(dataset.x_data/y_data と同じ)を、描かれている位置へ変換する。スカラーでも配列でもよい。ウォーターフォールでなければそのまま返す。"""
78
+ transform = self.get_waterfall_transform(dataset_or_id)
79
+ if transform is None:
80
+ return x, y
81
+ index = transform['index']
82
+ display_x = x if transform['offset_x'] == 0 else x + index * transform['offset_x']
83
+ display_y = y * transform['depth_scale'] + index * transform['offset_y']
84
+ return display_x, display_y
85
+
86
+ def display_to_data(self, dataset_or_id, x, y):
87
+ """
88
+ data_to_display() の逆。マウス位置をデータの行に対応づける処理(範囲選択・データカーソル・
89
+ エディタ連動のハイライト・ピーク配置)は必ずこれを通す。通さないと積み重ねの2本目以降で位置がずれる。
90
+ """
91
+ transform = self.get_waterfall_transform(dataset_or_id)
92
+ if transform is None:
93
+ return x, y
94
+ index = transform['index']
95
+ data_x = x if transform['offset_x'] == 0 else x - index * transform['offset_x']
96
+ # depth_scale は 0.2 以上に抑えてあるので 0 で割ることはない
97
+ data_y = (y - index * transform['offset_y']) / transform['depth_scale']
98
+ return data_x, data_y
99
+
100
+ def _effective_text_color(self, configured_color):
101
+ """ダークモードで既定の黒のままだと見えないので明るい色にする。利用者が選んだ別の色はそのまま。"""
102
+ if self.dark_mode and configured_color == '#000000':
103
+ return DARK_TEXT_COLOR
104
+ return configured_color
105
+
106
+ def _default_free_rect(self, index):
107
+ """自由配置で新しい軸に割り当てる初期の (left, bottom, width, height)。少しずつずらして重ならないようにする。"""
108
+ offset = 0.04 * (index % 6)
109
+ left = min(0.1 + offset, 0.55)
110
+ bottom = min(0.55 - offset, 0.55) if index % 2 == 0 else min(0.1 + offset, 0.5)
111
+ return (left, max(bottom, 0.08), 0.45, 0.38)
112
+
113
+ def _safe_draw(self):
114
+ """
115
+ self.draw() の失敗をログに残す。直接の draw() は例外を投げても、呼び出し元のシグナルの経路によっては
116
+ どこにも記録されず「何も反映されない」だけになる(draw_idle 側は matplotlib が標準エラーに出すだけで、
117
+ 画面版の exe ではそれも見えない)。
118
+ """
119
+ try:
120
+ self.draw()
121
+ except Exception:
122
+ logger.exception(
123
+ "Figureの描画(self.draw())に失敗しました。"
124
+ "グラフの表示が更新されていない可能性があります。"
125
+ )
126
+
127
+ def redraw_all(self, datasets, rows, cols, all_plot_settings, layout_mode='grid', panel_labels_enabled=False,
128
+ share_x_axis=False, share_y_axis=False, full_resolution=False):
129
+ """
130
+ Figure を作り直して全部の軸を描く。full_resolution=True なら間引かない(エクスポートの「フル解像度」)。
131
+ 非表示のデータセットも含めて _draw_data に渡すこと(ウォーターフォールの段の振り方のため。_draw_data 参照)。
132
+ """
133
+ self.fig.clf()
134
+ self.all_axes.clear()
135
+ self.all_secondary_axes.clear()
136
+ self.axis_is_date_x.clear()
137
+ self.axis_is_category_x.clear()
138
+ # fig.clf() で古い artist はすべて消えるので、参照を捨てるだけでよい
139
+ self._annotation_artists.clear()
140
+ self._annotation_render_keys.clear()
141
+ self._highlight_artists.clear()
142
+ self.downsample_index_map.clear()
143
+ self._non_pickable_dataset_ids.clear()
144
+ self._axis_2d_mappables.clear()
145
+ self._waterfall_transforms.clear()
146
+ self.fig.set_facecolor(DARK_FIGURE_FACECOLOR if self.dark_mode else LIGHT_FIGURE_FACECOLOR)
147
+
148
+ is_free_layout = layout_mode == 'free'
149
+ subplot_count = len(all_plot_settings) if is_free_layout else rows * cols
150
+ if subplot_count == 0:
151
+ return False
152
+
153
+ is_secondary_visible_global = False
154
+
155
+ if is_free_layout:
156
+ for i in range(subplot_count):
157
+ rect = axis_setting(all_plot_settings[i], 'free_rect') or self._default_free_rect(i)
158
+ ax = self.fig.add_axes(rect)
159
+ self.all_axes.append(ax)
160
+ self.all_secondary_axes.append(None)
161
+ else:
162
+ # 軸の共有は、行・列ごとではなく全部の軸を最初の軸に束ねる
163
+ for i in range(subplot_count):
164
+ share_x_target = self.all_axes[0] if (share_x_axis and self.all_axes) else None
165
+ share_y_target = self.all_axes[0] if (share_y_axis and self.all_axes) else None
166
+ ax = self.fig.add_subplot(rows, cols, i + 1, sharex=share_x_target, sharey=share_y_target)
167
+ self.all_axes.append(ax)
168
+ self.all_secondary_axes.append(None)
169
+
170
+ for index, ax in enumerate(self.all_axes):
171
+ if index < len(all_plot_settings):
172
+ settings = all_plot_settings[index]
173
+ else:
174
+ continue
175
+
176
+ self._draw_data(ax, index, datasets, full_resolution=full_resolution)
177
+ self._apply_appearance(ax, index, settings)
178
+ # _apply_appearance が目盛数値の表示を毎回設定し直すので、共有で隠すのはその後
179
+ if not is_free_layout:
180
+ self._apply_shared_axis_tick_visibility(index, rows, cols, share_x_axis, share_y_axis)
181
+ self._draw_annotations(ax, index, settings, datasets=datasets, full_resolution=full_resolution)
182
+ if panel_labels_enabled:
183
+ self._draw_panel_label(ax, index)
184
+
185
+ if self.all_secondary_axes[index] is not None:
186
+ is_secondary_visible_global = True
187
+
188
+ if not is_free_layout:
189
+ # 自由配置は利用者が決めた位置を tight_layout が上書きするので、グリッドだけ。
190
+ # matplotlib の予備フォント(LastResortHE)が欠けた環境では FileNotFoundError になるので、配置を諦めて続ける
191
+ try:
192
+ fit_tight_layout(self.fig)
193
+ except (ValueError, FileNotFoundError):
194
+ pass
195
+
196
+ self._safe_draw()
197
+ return is_secondary_visible_global
198
+
199
+ def update_appearance_only(self, all_plot_settings, datasets=(), rows=1, cols=1,
200
+ layout_mode='grid', share_x_axis=False, share_y_axis=False):
201
+ """
202
+ データはそのままで見た目の設定だけを当て直す。datasets は統計値ラベルの計算にだけ使う
203
+ (省略すると統計値ラベルが「データセットなし」になるだけ)。
204
+ """
205
+ self.fig.set_facecolor(DARK_FIGURE_FACECOLOR if self.dark_mode else LIGHT_FIGURE_FACECOLOR)
206
+ is_free_layout = layout_mode == 'free'
207
+ for index, ax in enumerate(self.all_axes):
208
+ if index < len(all_plot_settings):
209
+ settings = all_plot_settings[index]
210
+ self._apply_appearance(ax, index, settings)
211
+ # redraw_all と同じく、共有で隠すのは _apply_appearance の後
212
+ if not is_free_layout:
213
+ self._apply_shared_axis_tick_visibility(index, rows, cols, share_x_axis, share_y_axis)
214
+ # この経路だけ軸を cla() しないので、内容が同じなら注釈を使い回せる
215
+ self._draw_annotations(ax, index, settings, datasets=datasets,
216
+ allow_reuse=True)
217
+ try:
218
+ fit_tight_layout(self.fig)
219
+ except (ValueError, FileNotFoundError):
220
+ # フォントが欠けた環境の対策(redraw_all 参照)
221
+ pass
222
+ self._safe_draw()
223
+
224
+ def _apply_shared_axis_tick_visibility(self, axis_index, rows, cols, share_x_axis, share_y_axis):
225
+ """軸の共有が有効なら、内側の目盛数値(最下行以外の X・最左列以外の Y)を隠す。自由配置(cols=0)では何もしない。"""
226
+ if cols <= 0:
227
+ return
228
+ row_idx, col_idx = divmod(axis_index, cols)
229
+ ax = self.all_axes[axis_index]
230
+ if share_x_axis and row_idx != rows - 1:
231
+ ax.tick_params(labelbottom=False)
232
+ if share_y_axis and col_idx != 0:
233
+ ax.tick_params(labelleft=False)
234
+
235
+ def _redraw_single_axis_no_draw(self, axis_index, datasets, settings, rows=1, cols=1,
236
+ share_x_axis=False, share_y_axis=False, panel_labels_enabled=False,
237
+ full_resolution=False):
238
+ """update_single_axis() の、draw を呼ぶ手前まで(全軸をまとめて描き直すときに draw を1回で済ませるため)。"""
239
+ if axis_index >= len(self.all_axes):
240
+ return
241
+
242
+ # twinx() の第2軸は cla() では消えないので、取り除かないと呼ぶたびに積み重なる
243
+ old_secondary = self.all_secondary_axes[axis_index]
244
+ if old_secondary is not None:
245
+ old_secondary.remove()
246
+ self.all_secondary_axes[axis_index] = None
247
+
248
+ ax = self.all_axes[axis_index]
249
+ ax.cla()
250
+
251
+ # cla() で古い artist は消えているので、参照を捨てるだけ
252
+ self._annotation_artists.pop(axis_index, None)
253
+ self._annotation_render_keys.pop(axis_index, None)
254
+ for dataset_id in [ds.dataset_id for ds in datasets if ds.subplot_target == axis_index]:
255
+ self._highlight_artists.pop(dataset_id, None)
256
+ self.downsample_index_map.pop(dataset_id, None)
257
+ self._non_pickable_dataset_ids.discard(dataset_id)
258
+
259
+ self._draw_data(ax, axis_index, datasets, full_resolution=full_resolution)
260
+ self._apply_appearance(ax, axis_index, settings)
261
+ self._draw_annotations(ax, axis_index, settings, datasets=datasets, full_resolution=full_resolution)
262
+ if panel_labels_enabled:
263
+ self._draw_panel_label(ax, axis_index)
264
+
265
+ self._apply_shared_axis_tick_visibility(axis_index, rows, cols, share_x_axis, share_y_axis)
266
+
267
+ def update_single_axis(self, axis_index, datasets, settings, rows=1, cols=1,
268
+ share_x_axis=False, share_y_axis=False, panel_labels_enabled=False,
269
+ full_resolution=False):
270
+ """
271
+ 1つの軸だけを描き直す。Figure とほかの軸には触らない。データセットの見た目や描画先・第2Y軸の変更用
272
+ (描画先の変更は旧・新の軸それぞれで呼ぶ)。軸の数や配置が変わるときは redraw_all() か
273
+ add_free_axis()/remove_last_free_axis() を使う。
274
+ """
275
+ self._redraw_single_axis_no_draw(
276
+ axis_index, datasets, settings, rows=rows, cols=cols,
277
+ share_x_axis=share_x_axis, share_y_axis=share_y_axis,
278
+ panel_labels_enabled=panel_labels_enabled, full_resolution=full_resolution,
279
+ )
280
+ self.draw_idle()
281
+
282
+ def add_free_axis(self, datasets, settings, panel_labels_enabled=False):
283
+ """自由配置の末尾に軸を1つ足す(ほかの軸には触らない)。足す軸はどのデータセットからも参照されていない。"""
284
+ rect = axis_setting(settings, 'free_rect') or self._default_free_rect(len(self.all_axes))
285
+ ax = self.fig.add_axes(rect)
286
+ self.all_axes.append(ax)
287
+ self.all_secondary_axes.append(None)
288
+ self.axis_is_date_x.append(False)
289
+ self.axis_is_category_x.append(False)
290
+
291
+ axis_index = len(self.all_axes) - 1
292
+ self._draw_data(ax, axis_index, datasets)
293
+ self._apply_appearance(ax, axis_index, settings)
294
+ self._draw_annotations(ax, axis_index, settings, datasets=datasets)
295
+ if panel_labels_enabled:
296
+ self._draw_panel_label(ax, axis_index)
297
+
298
+ self.draw_idle()
299
+
300
+ def remove_last_free_axis(self, datasets):
301
+ """
302
+ 自由配置の末尾の軸を消す(ほかの軸には触らない)。消す軸のデータセットは、呼び出し側が先に
303
+ 新しい末尾の軸へ付け替え、描き直しも update_single_axis() で行うこと。
304
+ """
305
+ if not self.all_axes:
306
+ return
307
+ removed_index = len(self.all_axes) - 1
308
+
309
+ secondary = self.all_secondary_axes[removed_index]
310
+ if secondary is not None:
311
+ secondary.remove()
312
+ self.all_axes[removed_index].remove()
313
+
314
+ self.all_axes.pop()
315
+ self.all_secondary_axes.pop()
316
+ if removed_index < len(self.axis_is_date_x):
317
+ self.axis_is_date_x.pop()
318
+ if removed_index < len(self.axis_is_category_x):
319
+ self.axis_is_category_x.pop()
320
+
321
+ self._annotation_artists.pop(removed_index, None)
322
+ self._annotation_render_keys.pop(removed_index, None)
323
+ for dataset_id in [ds.dataset_id for ds in datasets if ds.subplot_target == removed_index]:
324
+ self._highlight_artists.pop(dataset_id, None)
325
+ self.downsample_index_map.pop(dataset_id, None)
326
+ self._non_pickable_dataset_ids.discard(dataset_id)
327
+
328
+ self.draw_idle()
329
+
330
+ def update_all_axes_appearance_and_data(self, datasets, rows, cols, all_plot_settings, layout_mode='grid',
331
+ panel_labels_enabled=False, share_x_axis=False, share_y_axis=False,
332
+ full_resolution=False):
333
+ """
334
+ 軸の数と配置を変えずに、全部の軸のデータと見た目を描き直す(パネルラベルやダークモードの切り替え用)。
335
+ fig.clf() しないので、軸の数や配置が変わるときには使えない。draw などの Figure 全体の処理は最後に1回だけ。
336
+ """
337
+ is_free_layout = layout_mode == 'free'
338
+ is_secondary_visible_global = False
339
+
340
+ for index in range(len(self.all_axes)):
341
+ if index >= len(all_plot_settings):
342
+ continue
343
+ settings = all_plot_settings[index]
344
+ self._redraw_single_axis_no_draw(
345
+ index, datasets, settings, rows=rows, cols=cols,
346
+ share_x_axis=share_x_axis, share_y_axis=share_y_axis,
347
+ panel_labels_enabled=panel_labels_enabled, full_resolution=full_resolution,
348
+ )
349
+ if self.all_secondary_axes[index] is not None:
350
+ is_secondary_visible_global = True
351
+
352
+ self.fig.set_facecolor(DARK_FIGURE_FACECOLOR if self.dark_mode else LIGHT_FIGURE_FACECOLOR)
353
+ if not is_free_layout:
354
+ # 自由配置には tight_layout を掛けない・フォントが欠けた環境の対策(redraw_all 参照)
355
+ try:
356
+ fit_tight_layout(self.fig)
357
+ except (ValueError, FileNotFoundError):
358
+ pass
359
+
360
+ self._safe_draw()
361
+ return is_secondary_visible_global
362
+
363
+ def _draw_panel_label(self, ax, index):
364
+ return annotations.draw_panel_label(self, ax, index)
365
+
366
+ @staticmethod
367
+ def _panel_label_for_index(index):
368
+ return annotations.panel_label_for_index(index)
369
+
370
+ def _downsample_for_inset(self, x_data, y_data, full_resolution=False):
371
+ return annotations.downsample_for_inset(self, x_data, y_data, full_resolution)
372
+
373
+ def _annotation_render_key(self, axis_index, settings, datasets, full_resolution):
374
+ return annotations.annotation_render_key(self, axis_index, settings, datasets, full_resolution)
375
+
376
+ def _draw_annotations(self, ax, axis_index, settings, datasets=None, full_resolution=False,
377
+ allow_reuse=False):
378
+ return annotations.draw_annotations(self, ax, axis_index, settings, datasets, full_resolution, allow_reuse)
379
+
380
+ def _enable_element_picking(self, artist):
381
+ return data_1d.enable_element_picking(self, artist)
382
+
383
+ def _add_gradient_line(self, ax, x, y, color1, color2, linewidth, alpha, linestyle, label=None):
384
+ return data_1d.add_gradient_line(self, ax, x, y, color1, color2, linewidth, alpha, linestyle, label)
385
+
386
+ def _add_gradient_fill(self, ax, x, y, color1, color2, alpha, baseline=0.0):
387
+ return data_1d.add_gradient_fill(self, ax, x, y, color1, color2, alpha, baseline)
388
+
389
+ _VALID_MAP_DISPLAY_MODES = ('heatmap', 'contour', 'contour_filled', 'heatmap_contour')
390
+
391
+ def _draw_2d_data(self, ax, axis_index, datasets_2d, full_resolution=False):
392
+ return data_2d.draw_2d_data(self, ax, axis_index, datasets_2d, full_resolution)
393
+
394
+ def _draw_data(self, ax, axis_index, datasets, full_resolution=False):
395
+ return data_1d.draw_data(self, ax, axis_index, datasets, full_resolution)
396
+
397
+ def _record_x_axis_kind(self, axis_index, datasets):
398
+ return data_1d.record_x_axis_kind(self, axis_index, datasets)
399
+
400
+ def _draw_1d_dataset(self, target_ax, axis_index, ds, waterfall, is_category_x, full_resolution):
401
+ return data_1d.draw_1d_dataset(self, target_ax, axis_index, ds, waterfall, is_category_x, full_resolution)
402
+
403
+ def _waterfall_shifted_points(self, ds, waterfall, is_category_x):
404
+ return data_1d.waterfall_shifted_points(self, ds, waterfall, is_category_x)
405
+
406
+ def _downsample_for_display(self, ds, plot_x_data, plot_y_data, is_category_x, full_resolution):
407
+ return data_1d.downsample_for_display(self, ds, plot_x_data, plot_y_data, is_category_x, full_resolution)
408
+
409
+ def _draw_smoothed(self, target_ax, ds, plot_x_data, plot_y_data, plot_kwargs):
410
+ return data_1d.draw_smoothed(self, target_ax, ds, plot_x_data, plot_y_data, plot_kwargs)
411
+
412
+ def _draw_plot_type(self, target_ax, axis_index, ds, plot_x_data, plot_y_data, plot_kwargs):
413
+ return data_1d.draw_plot_type(self, target_ax, axis_index, ds, plot_x_data, plot_y_data, plot_kwargs)
414
+
415
+ def _draw_error_display(self, target_ax, ds, plot_x_data, plot_y_data, downsample_indices):
416
+ return data_1d.draw_error_display(self, target_ax, ds, plot_x_data, plot_y_data, downsample_indices)
417
+
418
+ def set_highlighted_points(self, dataset, master_indices):
419
+ """
420
+ データエディタで選んだ行の点をグラフ上で丸く囲む。
421
+
422
+ Args:
423
+ dataset (Dataset): 対象のデータセット。
424
+ master_indices (list): dataset.df.index のラベル。空ならこのデータセットのハイライトを消す。
425
+ """
426
+ old_artist = self._highlight_artists.pop(dataset.dataset_id, None)
427
+ if old_artist is not None:
428
+ try:
429
+ old_artist.remove()
430
+ except (ValueError, NotImplementedError):
431
+ pass
432
+
433
+ if not master_indices:
434
+ self.draw_idle()
435
+ return
436
+
437
+ axis_index = dataset.subplot_target
438
+ if axis_index >= len(self.all_axes):
439
+ self.draw_idle()
440
+ return
441
+
442
+ if dataset.use_secondary_y and axis_index < len(self.all_secondary_axes) \
443
+ and self.all_secondary_axes[axis_index] is not None:
444
+ ax = self.all_secondary_axes[axis_index]
445
+ else:
446
+ ax = self.all_axes[axis_index]
447
+
448
+ try:
449
+ # x_data/y_data は visible_df の並びなので、位置も visible_df.index で引く(除外した行は描かれていない)
450
+ visible_index = dataset.visible_df.index
451
+ positions = [visible_index.get_loc(idx) for idx in master_indices if idx in visible_index]
452
+ except Exception:
453
+ logger.exception("ハイライト対象の行インデックス変換に失敗しました。")
454
+ positions = []
455
+
456
+ if not positions:
457
+ self.draw_idle()
458
+ return
459
+
460
+ x_vals = dataset.x_data[positions]
461
+ y_vals = dataset.y_data[positions]
462
+ # ウォーターフォールでずらした位置に合わせる
463
+ x_vals, y_vals = self.data_to_display(dataset, x_vals, y_vals)
464
+ artist = ax.scatter(
465
+ x_vals, y_vals, s=160, facecolors='none', edgecolors='#e6194b',
466
+ linewidths=2.0, zorder=15
467
+ )
468
+ self._highlight_artists[dataset.dataset_id] = artist
469
+ self.draw_idle()
470
+
471
+ def _draw_point_labels(self, ax, ds, x_data=None, y_data=None, downsample_indices=None):
472
+ return data_1d.draw_point_labels(self, ax, ds, x_data, y_data, downsample_indices)
473
+
474
+ def _apply_appearance(self, ax, axis_index, settings):
475
+ return appearance.apply_appearance(self, ax, axis_index, settings)
476
+
477
+ def _apply_limits_and_scale(self, ax, settings, is_category_x):
478
+ return appearance.apply_limits_and_scale(self, ax, settings, is_category_x)
479
+
480
+ def _apply_tick_locators(self, ax, settings, is_date_x, is_category_x):
481
+ return appearance.apply_tick_locators(self, ax, settings, is_date_x, is_category_x)
482
+
483
+ def _axis_text_and_line_style(self, settings):
484
+ return appearance.axis_text_and_line_style(self, settings)
485
+
486
+ def _apply_titles_and_labels(self, ax, settings, style):
487
+ return appearance.apply_titles_and_labels(self, ax, settings, style)
488
+
489
+ def _apply_spines_and_tick_marks(self, ax, settings, style):
490
+ return appearance.apply_spines_and_tick_marks(self, ax, settings, style)
491
+
492
+ def _apply_legend(self, ax, secondary_ax, settings):
493
+ return appearance.apply_legend(self, ax, secondary_ax, settings)
494
+
495
+ def _apply_grid(self, ax, settings):
496
+ return appearance.apply_grid(self, ax, settings)
497
+
498
+ def _apply_secondary_y_axis(self, ax, secondary_ax, settings, style):
499
+ return appearance.apply_secondary_y_axis(self, ax, secondary_ax, settings, style)
500
+
501
+ def _apply_unit_conversion_x_axis(self, ax, settings, style, is_date_x, is_category_x):
502
+ return appearance.apply_unit_conversion_x_axis(self, ax, settings, style, is_date_x, is_category_x)
503
+
504
+ def _apply_colorbar(self, ax, axis_index, settings, style):
505
+ return appearance.apply_colorbar(self, ax, axis_index, settings, style)
506
+
507
+
508
+ class MplCanvas(FigureCanvas, _CanvasDrawingMixin):
509
+ def __init__(self, parent=None, width=5, height=4, dpi=100):
510
+ self._init_drawing_state(width, height, dpi)
511
+ super().__init__(self.fig)
512
+ self.setParent(parent)
513
+
514
+
515
+ class _HeadlessRenderCanvas(FigureCanvasAgg, _CanvasDrawingMixin):
516
+ """一括エクスポート用の Qt に依存しないキャンバス。QWidget ではないので GUI スレッドの外で描いてよい。"""
517
+ def __init__(self, width=5, height=4, dpi=100):
518
+ self._init_drawing_state(width, height, dpi)
519
+ super().__init__(self.fig)
@@ -0,0 +1,47 @@
1
+ # gui/color_history.py
2
+ """
3
+ QColorDialog で選択した色を「最近使った色」としてQSettingsに永続化し、
4
+ QColorDialogのカスタムカラー欄(プロセス内で共有される静的な状態)に
5
+ 反映するためのヘルパー。
6
+
7
+ QColorDialog.setCustomColor() はダイアログのインスタンスではなく
8
+ Qt側の静的な状態を書き換えるため、一度読み込めばアプリ内のどの
9
+ QColorDialog.getColor() 呼び出しにも反映される。
10
+ """
11
+ from PySide6.QtGui import QColor
12
+ from PySide6.QtWidgets import QColorDialog
13
+
14
+ from graphica.gui import app_settings
15
+
16
+ MAX_RECENT_COLORS = 16
17
+
18
+
19
+ def load_recent_colors_into_picker(settings):
20
+ """起動時に一度呼び、QSettingsに保存された「最近使った色」をカスタムカラー欄に復元する。"""
21
+ colors = app_settings.as_list(app_settings.RECENT_COLORS.read(settings))
22
+ if not colors:
23
+ return
24
+ for i, color_name in enumerate(colors[:MAX_RECENT_COLORS]):
25
+ QColorDialog.setCustomColor(i, QColor(color_name))
26
+
27
+
28
+ def get_color_with_history(settings, parent=None, initial=None):
29
+ """
30
+ QColorDialog.getColor() のラッパー。選択(Cancel以外)された色を
31
+ 「最近使った色」の先頭に記録し、QSettingsへ永続化する。
32
+ """
33
+ if initial is not None:
34
+ color = QColorDialog.getColor(initial, parent)
35
+ else:
36
+ color = QColorDialog.getColor(parent=parent)
37
+
38
+ if color.isValid():
39
+ color_name = color.name()
40
+ colors = [c for c in app_settings.as_list(app_settings.RECENT_COLORS.read(settings)) if c != color_name]
41
+ colors.insert(0, color_name)
42
+ colors = colors[:MAX_RECENT_COLORS]
43
+ app_settings.RECENT_COLORS.write(settings, colors)
44
+ for i, c in enumerate(colors):
45
+ QColorDialog.setCustomColor(i, QColor(c))
46
+
47
+ return color