scitex 2.14.0__py3-none-any.whl → 2.15.3__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.
- scitex/__init__.py +71 -17
- scitex/_env_loader.py +156 -0
- scitex/_mcp_resources/__init__.py +37 -0
- scitex/_mcp_resources/_cheatsheet.py +135 -0
- scitex/_mcp_resources/_figrecipe.py +138 -0
- scitex/_mcp_resources/_formats.py +102 -0
- scitex/_mcp_resources/_modules.py +337 -0
- scitex/_mcp_resources/_session.py +149 -0
- scitex/_mcp_tools/__init__.py +4 -0
- scitex/_mcp_tools/audio.py +66 -0
- scitex/_mcp_tools/diagram.py +11 -95
- scitex/_mcp_tools/introspect.py +210 -0
- scitex/_mcp_tools/plt.py +260 -305
- scitex/_mcp_tools/scholar.py +74 -0
- scitex/_mcp_tools/social.py +27 -0
- scitex/_mcp_tools/template.py +24 -0
- scitex/_mcp_tools/writer.py +17 -210
- scitex/ai/_gen_ai/_PARAMS.py +10 -7
- scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
- scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
- scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
- scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
- scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
- scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
- scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
- scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
- scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
- scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
- scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
- scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
- scitex/audio/README.md +40 -36
- scitex/audio/__init__.py +129 -61
- scitex/audio/_branding.py +185 -0
- scitex/audio/_mcp/__init__.py +32 -0
- scitex/audio/_mcp/handlers.py +59 -6
- scitex/audio/_mcp/speak_handlers.py +238 -0
- scitex/audio/_relay.py +225 -0
- scitex/audio/_tts.py +18 -10
- scitex/audio/engines/base.py +17 -10
- scitex/audio/engines/elevenlabs_engine.py +7 -2
- scitex/audio/mcp_server.py +228 -75
- scitex/canvas/README.md +1 -1
- scitex/canvas/editor/_dearpygui/__init__.py +25 -0
- scitex/canvas/editor/_dearpygui/_editor.py +147 -0
- scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
- scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
- scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
- scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
- scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
- scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
- scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
- scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
- scitex/canvas/editor/_dearpygui/_selection.py +295 -0
- scitex/canvas/editor/_dearpygui/_state.py +93 -0
- scitex/canvas/editor/_dearpygui/_utils.py +61 -0
- scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
- scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
- scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
- scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
- scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
- scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
- scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
- scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
- scitex/canvas/editor/flask_editor/_core.py +25 -1684
- scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
- scitex/cli/__init__.py +38 -43
- scitex/cli/audio.py +160 -41
- scitex/cli/capture.py +133 -20
- scitex/cli/introspect.py +488 -0
- scitex/cli/main.py +200 -109
- scitex/cli/mcp.py +60 -34
- scitex/cli/plt.py +414 -0
- scitex/cli/repro.py +15 -8
- scitex/cli/resource.py +15 -8
- scitex/cli/scholar/__init__.py +154 -8
- scitex/cli/scholar/_crossref_scitex.py +296 -0
- scitex/cli/scholar/_fetch.py +25 -3
- scitex/cli/social.py +355 -0
- scitex/cli/stats.py +136 -11
- scitex/cli/template.py +129 -12
- scitex/cli/tex.py +15 -8
- scitex/cli/writer.py +49 -299
- scitex/cloud/__init__.py +41 -2
- scitex/config/README.md +1 -1
- scitex/config/__init__.py +16 -2
- scitex/config/_env_registry.py +256 -0
- scitex/context/__init__.py +22 -0
- scitex/dev/__init__.py +20 -1
- scitex/diagram/__init__.py +42 -19
- scitex/diagram/mcp_server.py +13 -125
- scitex/gen/__init__.py +50 -14
- scitex/gen/_list_packages.py +4 -4
- scitex/introspect/__init__.py +82 -0
- scitex/introspect/_call_graph.py +303 -0
- scitex/introspect/_class_hierarchy.py +163 -0
- scitex/introspect/_core.py +41 -0
- scitex/introspect/_docstring.py +131 -0
- scitex/introspect/_examples.py +113 -0
- scitex/introspect/_imports.py +271 -0
- scitex/{gen/_inspect_module.py → introspect/_list_api.py} +48 -56
- scitex/introspect/_mcp/__init__.py +41 -0
- scitex/introspect/_mcp/handlers.py +233 -0
- scitex/introspect/_members.py +155 -0
- scitex/introspect/_resolve.py +89 -0
- scitex/introspect/_signature.py +131 -0
- scitex/introspect/_source.py +80 -0
- scitex/introspect/_type_hints.py +172 -0
- scitex/io/_save.py +1 -2
- scitex/io/bundle/README.md +1 -1
- scitex/logging/_formatters.py +19 -9
- scitex/mcp_server.py +98 -5
- scitex/os/__init__.py +4 -0
- scitex/{gen → os}/_check_host.py +4 -5
- scitex/plt/__init__.py +245 -550
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
- scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/plt/gallery/README.md +1 -1
- scitex/plt/utils/_hitmap/__init__.py +82 -0
- scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
- scitex/plt/utils/_hitmap/_color_application.py +346 -0
- scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
- scitex/plt/utils/_hitmap/_constants.py +40 -0
- scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
- scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
- scitex/plt/utils/_hitmap/_query.py +113 -0
- scitex/plt/utils/_hitmap.py +46 -1616
- scitex/plt/utils/_metadata/__init__.py +80 -0
- scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
- scitex/plt/utils/_metadata/_artists/_base.py +195 -0
- scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
- scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
- scitex/plt/utils/_metadata/_artists/_images.py +80 -0
- scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
- scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
- scitex/plt/utils/_metadata/_artists/_text.py +106 -0
- scitex/plt/utils/_metadata/_csv.py +416 -0
- scitex/plt/utils/_metadata/_detect.py +225 -0
- scitex/plt/utils/_metadata/_legend.py +127 -0
- scitex/plt/utils/_metadata/_rounding.py +117 -0
- scitex/plt/utils/_metadata/_verification.py +202 -0
- scitex/schema/README.md +1 -1
- scitex/scholar/__init__.py +8 -0
- scitex/scholar/_mcp/crossref_handlers.py +265 -0
- scitex/scholar/core/Scholar.py +63 -1700
- scitex/scholar/core/_mixins/__init__.py +36 -0
- scitex/scholar/core/_mixins/_enrichers.py +270 -0
- scitex/scholar/core/_mixins/_library_handlers.py +100 -0
- scitex/scholar/core/_mixins/_loaders.py +103 -0
- scitex/scholar/core/_mixins/_pdf_download.py +375 -0
- scitex/scholar/core/_mixins/_pipeline.py +312 -0
- scitex/scholar/core/_mixins/_project_handlers.py +125 -0
- scitex/scholar/core/_mixins/_savers.py +69 -0
- scitex/scholar/core/_mixins/_search.py +103 -0
- scitex/scholar/core/_mixins/_services.py +88 -0
- scitex/scholar/core/_mixins/_url_finding.py +105 -0
- scitex/scholar/crossref_scitex.py +367 -0
- scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/scholar/examples/00_run_all.sh +120 -0
- scitex/scholar/jobs/_executors.py +27 -3
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
- scitex/scholar/pdf_download/_cli.py +154 -0
- scitex/scholar/pdf_download/strategies/__init__.py +11 -8
- scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
- scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
- scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
- scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
- scitex/scholar/pipelines/_single_steps.py +71 -36
- scitex/scholar/storage/_LibraryManager.py +97 -1695
- scitex/scholar/storage/_mixins/__init__.py +30 -0
- scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
- scitex/scholar/storage/_mixins/_library_operations.py +218 -0
- scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
- scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
- scitex/scholar/storage/_mixins/_resolution.py +376 -0
- scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
- scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
- scitex/security/README.md +3 -3
- scitex/session/README.md +1 -1
- scitex/session/__init__.py +26 -7
- scitex/session/_decorator.py +1 -1
- scitex/sh/README.md +1 -1
- scitex/sh/__init__.py +7 -4
- scitex/social/__init__.py +155 -0
- scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/stats/_mcp/_handlers/__init__.py +31 -0
- scitex/stats/_mcp/_handlers/_corrections.py +113 -0
- scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
- scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
- scitex/stats/_mcp/_handlers/_format.py +94 -0
- scitex/stats/_mcp/_handlers/_normality.py +110 -0
- scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
- scitex/stats/_mcp/_handlers/_power.py +247 -0
- scitex/stats/_mcp/_handlers/_recommend.py +102 -0
- scitex/stats/_mcp/_handlers/_run_test.py +279 -0
- scitex/stats/_mcp/_handlers/_stars.py +48 -0
- scitex/stats/_mcp/handlers.py +19 -1171
- scitex/stats/auto/_stat_style.py +175 -0
- scitex/stats/auto/_style_definitions.py +411 -0
- scitex/stats/auto/_styles.py +22 -620
- scitex/stats/descriptive/__init__.py +11 -8
- scitex/stats/descriptive/_ci.py +39 -0
- scitex/stats/power/_power.py +15 -4
- scitex/str/__init__.py +2 -1
- scitex/str/_title_case.py +63 -0
- scitex/template/README.md +1 -1
- scitex/template/__init__.py +25 -10
- scitex/template/_code_templates.py +147 -0
- scitex/template/_mcp/handlers.py +81 -0
- scitex/template/_mcp/tool_schemas.py +55 -0
- scitex/template/_templates/__init__.py +51 -0
- scitex/template/_templates/audio.py +233 -0
- scitex/template/_templates/canvas.py +312 -0
- scitex/template/_templates/capture.py +268 -0
- scitex/template/_templates/config.py +43 -0
- scitex/template/_templates/diagram.py +294 -0
- scitex/template/_templates/io.py +107 -0
- scitex/template/_templates/module.py +53 -0
- scitex/template/_templates/plt.py +202 -0
- scitex/template/_templates/scholar.py +267 -0
- scitex/template/_templates/session.py +130 -0
- scitex/template/_templates/session_minimal.py +43 -0
- scitex/template/_templates/session_plot.py +67 -0
- scitex/template/_templates/session_stats.py +77 -0
- scitex/template/_templates/stats.py +323 -0
- scitex/template/_templates/writer.py +296 -0
- scitex/template/clone_writer_directory.py +5 -5
- scitex/ui/_backends/_email.py +10 -2
- scitex/ui/_backends/_webhook.py +5 -1
- scitex/web/_search_pubmed.py +10 -6
- scitex/writer/README.md +1 -1
- scitex/writer/__init__.py +43 -34
- scitex/writer/_mcp/handlers.py +11 -744
- scitex/writer/_mcp/tool_schemas.py +5 -335
- scitex-2.15.3.dist-info/METADATA +667 -0
- {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/RECORD +241 -120
- scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
- scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
- scitex/diagram/_compile.py +0 -312
- scitex/diagram/_diagram.py +0 -355
- scitex/diagram/_mcp/__init__.py +0 -4
- scitex/diagram/_mcp/handlers.py +0 -400
- scitex/diagram/_mcp/tool_schemas.py +0 -157
- scitex/diagram/_presets.py +0 -173
- scitex/diagram/_schema.py +0 -182
- scitex/diagram/_split.py +0 -278
- scitex/gen/_ci.py +0 -12
- scitex/gen/_title_case.py +0 -89
- scitex/plt/_mcp/__init__.py +0 -4
- scitex/plt/_mcp/_handlers_annotation.py +0 -102
- scitex/plt/_mcp/_handlers_figure.py +0 -195
- scitex/plt/_mcp/_handlers_plot.py +0 -252
- scitex/plt/_mcp/_handlers_style.py +0 -219
- scitex/plt/_mcp/handlers.py +0 -74
- scitex/plt/_mcp/tool_schemas.py +0 -497
- scitex/plt/mcp_server.py +0 -231
- scitex/scholar/examples/SUGGESTIONS.md +0 -865
- scitex/scholar/examples/dev.py +0 -38
- scitex-2.14.0.dist-info/METADATA +0 -1238
- /scitex/{gen → context}/_detect_environment.py +0 -0
- /scitex/{gen → context}/_get_notebook_path.py +0 -0
- /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/WHEEL +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/entry_points.txt +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/_dearpygui/_plotting.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
CSV data plotting for DearPyGui editor.
|
|
7
|
+
|
|
8
|
+
Handles reconstructing plots from CSV data with trace info.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def plot_from_csv(
|
|
17
|
+
ax,
|
|
18
|
+
overrides: Dict[str, Any],
|
|
19
|
+
csv_data: pd.DataFrame,
|
|
20
|
+
highlight_trace: Optional[int] = None,
|
|
21
|
+
hover_trace: Optional[int] = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Reconstruct plot from CSV data using trace info.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
ax : matplotlib.axes.Axes
|
|
28
|
+
The axes to plot on
|
|
29
|
+
overrides : dict
|
|
30
|
+
Current overrides containing trace info
|
|
31
|
+
csv_data : pd.DataFrame
|
|
32
|
+
CSV data to plot
|
|
33
|
+
highlight_trace : int, optional
|
|
34
|
+
Index of trace to highlight with selection effect (yellow glow)
|
|
35
|
+
hover_trace : int, optional
|
|
36
|
+
Index of trace to highlight with hover effect (cyan glow)
|
|
37
|
+
"""
|
|
38
|
+
from .._defaults import _normalize_legend_loc
|
|
39
|
+
|
|
40
|
+
if not isinstance(csv_data, pd.DataFrame):
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
df = csv_data
|
|
44
|
+
linewidth = overrides.get("linewidth", 1.0)
|
|
45
|
+
legend_visible = overrides.get("legend_visible", True)
|
|
46
|
+
legend_fontsize = overrides.get("legend_fontsize", 6)
|
|
47
|
+
legend_frameon = overrides.get("legend_frameon", False)
|
|
48
|
+
legend_loc = _normalize_legend_loc(overrides.get("legend_loc", "best"))
|
|
49
|
+
|
|
50
|
+
traces = overrides.get("traces", [])
|
|
51
|
+
|
|
52
|
+
if traces:
|
|
53
|
+
_plot_with_traces(
|
|
54
|
+
ax,
|
|
55
|
+
df,
|
|
56
|
+
traces,
|
|
57
|
+
linewidth,
|
|
58
|
+
highlight_trace,
|
|
59
|
+
hover_trace,
|
|
60
|
+
legend_visible,
|
|
61
|
+
legend_fontsize,
|
|
62
|
+
legend_frameon,
|
|
63
|
+
legend_loc,
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
_plot_fallback(
|
|
67
|
+
ax,
|
|
68
|
+
df,
|
|
69
|
+
linewidth,
|
|
70
|
+
legend_visible,
|
|
71
|
+
legend_fontsize,
|
|
72
|
+
legend_frameon,
|
|
73
|
+
legend_loc,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _plot_with_traces(
|
|
78
|
+
ax,
|
|
79
|
+
df: pd.DataFrame,
|
|
80
|
+
traces: list,
|
|
81
|
+
linewidth: float,
|
|
82
|
+
highlight_trace: Optional[int],
|
|
83
|
+
hover_trace: Optional[int],
|
|
84
|
+
legend_visible: bool,
|
|
85
|
+
legend_fontsize: int,
|
|
86
|
+
legend_frameon: bool,
|
|
87
|
+
legend_loc: str,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Plot using trace definitions."""
|
|
90
|
+
for i, trace in enumerate(traces):
|
|
91
|
+
csv_cols = trace.get("csv_columns", {})
|
|
92
|
+
x_col = csv_cols.get("x")
|
|
93
|
+
y_col = csv_cols.get("y")
|
|
94
|
+
|
|
95
|
+
if x_col in df.columns and y_col in df.columns:
|
|
96
|
+
trace_linewidth = trace.get("linewidth", linewidth)
|
|
97
|
+
is_selected = highlight_trace is not None and i == highlight_trace
|
|
98
|
+
is_hovered = (
|
|
99
|
+
hover_trace is not None and i == hover_trace and not is_selected
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Draw selection glow (yellow, stronger)
|
|
103
|
+
if is_selected:
|
|
104
|
+
ax.plot(
|
|
105
|
+
df[x_col],
|
|
106
|
+
df[y_col],
|
|
107
|
+
color="yellow",
|
|
108
|
+
linewidth=trace_linewidth * 4,
|
|
109
|
+
alpha=0.5,
|
|
110
|
+
zorder=0,
|
|
111
|
+
)
|
|
112
|
+
# Draw hover glow (cyan, subtler)
|
|
113
|
+
elif is_hovered:
|
|
114
|
+
ax.plot(
|
|
115
|
+
df[x_col],
|
|
116
|
+
df[y_col],
|
|
117
|
+
color="cyan",
|
|
118
|
+
linewidth=trace_linewidth * 3,
|
|
119
|
+
alpha=0.3,
|
|
120
|
+
zorder=0,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
ax.plot(
|
|
124
|
+
df[x_col],
|
|
125
|
+
df[y_col],
|
|
126
|
+
label=trace.get("label", trace.get("id", "")),
|
|
127
|
+
color=trace.get("color"),
|
|
128
|
+
linestyle=trace.get("linestyle", "-"),
|
|
129
|
+
linewidth=trace_linewidth
|
|
130
|
+
* (1.5 if is_selected else (1.2 if is_hovered else 1.0)),
|
|
131
|
+
marker=trace.get("marker", None),
|
|
132
|
+
markersize=trace.get("markersize", 6),
|
|
133
|
+
zorder=10 if is_selected else (5 if is_hovered else 1),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if legend_visible and any(t.get("label") for t in traces):
|
|
137
|
+
ax.legend(fontsize=legend_fontsize, frameon=legend_frameon, loc=legend_loc)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _plot_fallback(
|
|
141
|
+
ax,
|
|
142
|
+
df: pd.DataFrame,
|
|
143
|
+
linewidth: float,
|
|
144
|
+
legend_visible: bool,
|
|
145
|
+
legend_fontsize: int,
|
|
146
|
+
legend_frameon: bool,
|
|
147
|
+
legend_loc: str,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Fallback plotting when no traces defined - parse column names."""
|
|
150
|
+
cols = df.columns.tolist()
|
|
151
|
+
trace_groups = {}
|
|
152
|
+
|
|
153
|
+
for col in cols:
|
|
154
|
+
if col.endswith("_x"):
|
|
155
|
+
trace_id = col[:-2]
|
|
156
|
+
y_col = trace_id + "_y"
|
|
157
|
+
if y_col in cols:
|
|
158
|
+
parts = trace_id.split("_")
|
|
159
|
+
label = parts[2] if len(parts) > 2 else trace_id
|
|
160
|
+
trace_groups[trace_id] = {
|
|
161
|
+
"x_col": col,
|
|
162
|
+
"y_col": y_col,
|
|
163
|
+
"label": label,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if trace_groups:
|
|
167
|
+
for trace_id, info in trace_groups.items():
|
|
168
|
+
ax.plot(
|
|
169
|
+
df[info["x_col"]],
|
|
170
|
+
df[info["y_col"]],
|
|
171
|
+
label=info["label"],
|
|
172
|
+
linewidth=linewidth,
|
|
173
|
+
)
|
|
174
|
+
if legend_visible:
|
|
175
|
+
ax.legend(fontsize=legend_fontsize, frameon=legend_frameon, loc=legend_loc)
|
|
176
|
+
elif len(cols) >= 2:
|
|
177
|
+
x_col = cols[0]
|
|
178
|
+
for y_col in cols[1:]:
|
|
179
|
+
try:
|
|
180
|
+
ax.plot(df[x_col], df[y_col], label=str(y_col), linewidth=linewidth)
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
if len(cols) > 2 and legend_visible:
|
|
184
|
+
ax.legend(fontsize=legend_fontsize, frameon=legend_frameon, loc=legend_loc)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# EOF
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/_dearpygui/_rendering.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Figure rendering for DearPyGui editor.
|
|
7
|
+
|
|
8
|
+
Handles matplotlib figure rendering, element highlights, and hover overlays.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import io
|
|
12
|
+
from typing import TYPE_CHECKING, List, Tuple
|
|
13
|
+
|
|
14
|
+
from ._utils import MM_TO_PT, create_checkerboard
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ._state import EditorState
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def update_preview(state: "EditorState", dpg) -> None:
|
|
21
|
+
"""Update the figure preview (full re-render).
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
state : EditorState
|
|
26
|
+
Editor state
|
|
27
|
+
dpg : module
|
|
28
|
+
DearPyGui module
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
# Mark cache dirty and do full render
|
|
32
|
+
state.cache_dirty = True
|
|
33
|
+
img_data, width, height = render_figure(state, dpg)
|
|
34
|
+
|
|
35
|
+
# Update texture
|
|
36
|
+
dpg.set_value("preview_texture", img_data)
|
|
37
|
+
|
|
38
|
+
# Update status
|
|
39
|
+
dpg.set_value("status_text", f"Preview updated ({width}x{height})")
|
|
40
|
+
|
|
41
|
+
except Exception as e:
|
|
42
|
+
dpg.set_value("status_text", f"Error: {str(e)}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def update_hover_overlay(state: "EditorState", dpg) -> None:
|
|
46
|
+
"""Fast hover overlay update using cached base image (no matplotlib re-render).
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
state : EditorState
|
|
51
|
+
Editor state
|
|
52
|
+
dpg : module
|
|
53
|
+
DearPyGui module
|
|
54
|
+
"""
|
|
55
|
+
import numpy as np
|
|
56
|
+
from PIL import ImageDraw
|
|
57
|
+
|
|
58
|
+
# If no cached base, do full render
|
|
59
|
+
if state.cached_base_image is None:
|
|
60
|
+
update_preview(state, dpg)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
# Start with a copy of cached base
|
|
65
|
+
img = state.cached_base_image.copy()
|
|
66
|
+
draw = ImageDraw.Draw(img, "RGBA")
|
|
67
|
+
|
|
68
|
+
# Get hover element type
|
|
69
|
+
hovered_type = (
|
|
70
|
+
state.hovered_element.get("type") if state.hovered_element else None
|
|
71
|
+
)
|
|
72
|
+
selected_type = (
|
|
73
|
+
state.selected_element.get("type") if state.selected_element else None
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Draw hover highlight (outline only, no fill) for non-trace elements
|
|
77
|
+
if hovered_type and hovered_type != "trace" and hovered_type != selected_type:
|
|
78
|
+
bbox = state.element_bboxes.get(hovered_type)
|
|
79
|
+
if bbox:
|
|
80
|
+
x0, y0, x1, y1 = bbox
|
|
81
|
+
# Transparent outline only - no fill to avoid covering content
|
|
82
|
+
draw.rectangle(
|
|
83
|
+
[x0 - 2, y0 - 2, x1 + 2, y1 + 2],
|
|
84
|
+
fill=None,
|
|
85
|
+
outline=(100, 180, 255, 100),
|
|
86
|
+
width=1,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Draw selection highlight (outline only, no fill) for non-trace elements
|
|
90
|
+
if selected_type and selected_type != "trace":
|
|
91
|
+
bbox = state.element_bboxes.get(selected_type)
|
|
92
|
+
if bbox:
|
|
93
|
+
x0, y0, x1, y1 = bbox
|
|
94
|
+
# Transparent outline only - no fill to avoid covering content
|
|
95
|
+
draw.rectangle(
|
|
96
|
+
[x0 - 2, y0 - 2, x1 + 2, y1 + 2],
|
|
97
|
+
fill=None,
|
|
98
|
+
outline=(255, 200, 80, 150),
|
|
99
|
+
width=2,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Convert to DearPyGui texture format
|
|
103
|
+
img_array = np.array(img).astype(np.float32) / 255.0
|
|
104
|
+
img_data = img_array.flatten().tolist()
|
|
105
|
+
|
|
106
|
+
# Update texture
|
|
107
|
+
dpg.set_value("preview_texture", img_data)
|
|
108
|
+
|
|
109
|
+
except Exception:
|
|
110
|
+
# Fallback to full render on error
|
|
111
|
+
update_preview(state, dpg)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def render_figure(state: "EditorState", dpg) -> Tuple[List[float], int, int]:
|
|
115
|
+
"""Render figure and return as RGBA data for texture.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
state : EditorState
|
|
120
|
+
Editor state
|
|
121
|
+
dpg : module
|
|
122
|
+
DearPyGui module
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
tuple
|
|
127
|
+
(img_data, width, height)
|
|
128
|
+
"""
|
|
129
|
+
import matplotlib
|
|
130
|
+
|
|
131
|
+
matplotlib.use("Agg")
|
|
132
|
+
import matplotlib.pyplot as plt
|
|
133
|
+
import numpy as np
|
|
134
|
+
from matplotlib.ticker import MaxNLocator
|
|
135
|
+
from PIL import Image
|
|
136
|
+
|
|
137
|
+
from ._plotting import plot_from_csv
|
|
138
|
+
|
|
139
|
+
o = state.current_overrides
|
|
140
|
+
|
|
141
|
+
# Dimensions - use fixed size for preview
|
|
142
|
+
preview_dpi = 100
|
|
143
|
+
fig_size = o.get("fig_size", [3.15, 2.68])
|
|
144
|
+
|
|
145
|
+
# Create figure with white background for preview
|
|
146
|
+
fig, ax = plt.subplots(figsize=fig_size, dpi=preview_dpi)
|
|
147
|
+
|
|
148
|
+
# For preview, use white background (transparent doesn't show well in GUI)
|
|
149
|
+
fig.patch.set_facecolor("white")
|
|
150
|
+
ax.patch.set_facecolor("white")
|
|
151
|
+
|
|
152
|
+
# Plot from CSV data (only pass selection, hover is via PIL overlay for speed)
|
|
153
|
+
if state.csv_data is not None:
|
|
154
|
+
plot_from_csv(ax, o, state.csv_data, highlight_trace=state.selected_trace_index)
|
|
155
|
+
else:
|
|
156
|
+
ax.text(
|
|
157
|
+
0.5,
|
|
158
|
+
0.5,
|
|
159
|
+
"No plot data available\n(CSV not found)",
|
|
160
|
+
ha="center",
|
|
161
|
+
va="center",
|
|
162
|
+
transform=ax.transAxes,
|
|
163
|
+
fontsize=o.get("axis_fontsize", 7),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Apply labels
|
|
167
|
+
if o.get("title"):
|
|
168
|
+
ax.set_title(o["title"], fontsize=o.get("title_fontsize", 8))
|
|
169
|
+
if o.get("xlabel"):
|
|
170
|
+
ax.set_xlabel(o["xlabel"], fontsize=o.get("axis_fontsize", 7))
|
|
171
|
+
if o.get("ylabel"):
|
|
172
|
+
ax.set_ylabel(o["ylabel"], fontsize=o.get("axis_fontsize", 7))
|
|
173
|
+
|
|
174
|
+
# Tick styling
|
|
175
|
+
ax.tick_params(
|
|
176
|
+
axis="both",
|
|
177
|
+
labelsize=o.get("tick_fontsize", 7),
|
|
178
|
+
length=o.get("tick_length", 0.8) * MM_TO_PT,
|
|
179
|
+
width=o.get("tick_width", 0.2) * MM_TO_PT,
|
|
180
|
+
direction=o.get("tick_direction", "out"),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Number of ticks
|
|
184
|
+
ax.xaxis.set_major_locator(MaxNLocator(nbins=o.get("n_ticks", 4)))
|
|
185
|
+
ax.yaxis.set_major_locator(MaxNLocator(nbins=o.get("n_ticks", 4)))
|
|
186
|
+
|
|
187
|
+
# Grid
|
|
188
|
+
if o.get("grid"):
|
|
189
|
+
ax.grid(True, linewidth=o.get("axis_width", 0.2) * MM_TO_PT, alpha=0.3)
|
|
190
|
+
|
|
191
|
+
# Axis limits
|
|
192
|
+
if o.get("xlim"):
|
|
193
|
+
ax.set_xlim(o["xlim"])
|
|
194
|
+
if o.get("ylim"):
|
|
195
|
+
ax.set_ylim(o["ylim"])
|
|
196
|
+
|
|
197
|
+
# Spines
|
|
198
|
+
if o.get("hide_top_spine", True):
|
|
199
|
+
ax.spines["top"].set_visible(False)
|
|
200
|
+
if o.get("hide_right_spine", True):
|
|
201
|
+
ax.spines["right"].set_visible(False)
|
|
202
|
+
|
|
203
|
+
for spine in ax.spines.values():
|
|
204
|
+
spine.set_linewidth(o.get("axis_width", 0.2) * MM_TO_PT)
|
|
205
|
+
|
|
206
|
+
# Annotations
|
|
207
|
+
for annot in o.get("annotations", []):
|
|
208
|
+
if annot.get("type") == "text":
|
|
209
|
+
ax.text(
|
|
210
|
+
annot.get("x", 0.5),
|
|
211
|
+
annot.get("y", 0.5),
|
|
212
|
+
annot.get("text", ""),
|
|
213
|
+
transform=ax.transAxes,
|
|
214
|
+
fontsize=annot.get("fontsize", o.get("axis_fontsize", 7)),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
fig.tight_layout()
|
|
218
|
+
|
|
219
|
+
# Draw before collecting bboxes so we have accurate positions
|
|
220
|
+
fig.canvas.draw()
|
|
221
|
+
|
|
222
|
+
# Draw hover/selection highlights for non-trace elements
|
|
223
|
+
draw_element_highlights(state, fig, ax)
|
|
224
|
+
|
|
225
|
+
# Store axes transform info for click-to-select
|
|
226
|
+
fig.canvas.draw()
|
|
227
|
+
ax_bbox = ax.get_position()
|
|
228
|
+
fig_width_px = int(fig_size[0] * preview_dpi)
|
|
229
|
+
fig_height_px = int(fig_size[1] * preview_dpi)
|
|
230
|
+
|
|
231
|
+
# Collect element bboxes for click detection
|
|
232
|
+
_collect_element_bboxes(state, fig, ax)
|
|
233
|
+
|
|
234
|
+
# Convert to RGBA data for DearPyGui texture
|
|
235
|
+
buf = io.BytesIO()
|
|
236
|
+
fig.savefig(
|
|
237
|
+
buf,
|
|
238
|
+
format="png",
|
|
239
|
+
dpi=preview_dpi,
|
|
240
|
+
bbox_inches="tight",
|
|
241
|
+
facecolor="white",
|
|
242
|
+
edgecolor="none",
|
|
243
|
+
)
|
|
244
|
+
buf.seek(0)
|
|
245
|
+
|
|
246
|
+
# Load with PIL and convert to normalized RGBA
|
|
247
|
+
img = Image.open(buf).convert("RGBA")
|
|
248
|
+
width, height = img.size
|
|
249
|
+
|
|
250
|
+
# Resize to fit within max preview size while preserving aspect ratio
|
|
251
|
+
max_width, max_height = 800, 600
|
|
252
|
+
ratio = min(max_width / width, max_height / height)
|
|
253
|
+
new_width = int(width * ratio)
|
|
254
|
+
new_height = int(height * ratio)
|
|
255
|
+
img = img.resize((new_width, new_height), Image.LANCZOS)
|
|
256
|
+
|
|
257
|
+
# Store preview bounds for coordinate conversion (after resize)
|
|
258
|
+
x_offset = (max_width - new_width) // 2
|
|
259
|
+
y_offset = (max_height - new_height) // 2
|
|
260
|
+
state.preview_bounds = (x_offset, y_offset, new_width, new_height)
|
|
261
|
+
|
|
262
|
+
# Scale element bboxes to preview coordinates
|
|
263
|
+
_scale_element_bboxes(state, ratio, x_offset, y_offset, new_height)
|
|
264
|
+
|
|
265
|
+
# Store axes transform info (scaled to resized image)
|
|
266
|
+
ax_x0 = int(ax_bbox.x0 * new_width)
|
|
267
|
+
ax_y0 = int((1 - ax_bbox.y1) * new_height) # Flip y (0 at top)
|
|
268
|
+
ax_width = int(ax_bbox.width * new_width)
|
|
269
|
+
ax_height = int(ax_bbox.height * new_height)
|
|
270
|
+
xlim = ax.get_xlim()
|
|
271
|
+
ylim = ax.get_ylim()
|
|
272
|
+
state.axes_transform = (ax_x0, ax_y0, ax_width, ax_height, xlim, ylim)
|
|
273
|
+
|
|
274
|
+
# Create background - checkerboard for transparent, white otherwise
|
|
275
|
+
transparent = o.get("transparent", True)
|
|
276
|
+
if transparent:
|
|
277
|
+
padded = create_checkerboard(max_width, max_height, square_size=10)
|
|
278
|
+
else:
|
|
279
|
+
padded = Image.new("RGBA", (max_width, max_height), (255, 255, 255, 255))
|
|
280
|
+
|
|
281
|
+
# Paste figure centered on background
|
|
282
|
+
padded.paste(img, (x_offset, y_offset), img)
|
|
283
|
+
img = padded
|
|
284
|
+
width, height = max_width, max_height
|
|
285
|
+
|
|
286
|
+
# Cache the base image (without highlights) for fast hover updates
|
|
287
|
+
state.cached_base_image = img.copy()
|
|
288
|
+
state.cache_dirty = False
|
|
289
|
+
|
|
290
|
+
# Convert to normalized float array for DearPyGui
|
|
291
|
+
img_array = np.array(img).astype(np.float32) / 255.0
|
|
292
|
+
img_data = img_array.flatten().tolist()
|
|
293
|
+
|
|
294
|
+
plt.close(fig)
|
|
295
|
+
|
|
296
|
+
# Update texture data
|
|
297
|
+
dpg.set_value("preview_texture", img_data)
|
|
298
|
+
|
|
299
|
+
return img_data, width, height
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _collect_element_bboxes(state: "EditorState", fig, ax) -> None:
|
|
303
|
+
"""Collect element bboxes for click detection."""
|
|
304
|
+
renderer = fig.canvas.get_renderer()
|
|
305
|
+
state.element_bboxes_raw = {}
|
|
306
|
+
|
|
307
|
+
# Title bbox
|
|
308
|
+
if ax.title.get_text():
|
|
309
|
+
try:
|
|
310
|
+
title_bbox = ax.title.get_window_extent(renderer)
|
|
311
|
+
state.element_bboxes_raw["title"] = (
|
|
312
|
+
title_bbox.x0,
|
|
313
|
+
title_bbox.y0,
|
|
314
|
+
title_bbox.x1,
|
|
315
|
+
title_bbox.y1,
|
|
316
|
+
)
|
|
317
|
+
except Exception:
|
|
318
|
+
pass
|
|
319
|
+
|
|
320
|
+
# X label bbox
|
|
321
|
+
if ax.xaxis.label.get_text():
|
|
322
|
+
try:
|
|
323
|
+
xlabel_bbox = ax.xaxis.label.get_window_extent(renderer)
|
|
324
|
+
state.element_bboxes_raw["xlabel"] = (
|
|
325
|
+
xlabel_bbox.x0,
|
|
326
|
+
xlabel_bbox.y0,
|
|
327
|
+
xlabel_bbox.x1,
|
|
328
|
+
xlabel_bbox.y1,
|
|
329
|
+
)
|
|
330
|
+
except Exception:
|
|
331
|
+
pass
|
|
332
|
+
|
|
333
|
+
# Y label bbox
|
|
334
|
+
if ax.yaxis.label.get_text():
|
|
335
|
+
try:
|
|
336
|
+
ylabel_bbox = ax.yaxis.label.get_window_extent(renderer)
|
|
337
|
+
state.element_bboxes_raw["ylabel"] = (
|
|
338
|
+
ylabel_bbox.x0,
|
|
339
|
+
ylabel_bbox.y0,
|
|
340
|
+
ylabel_bbox.x1,
|
|
341
|
+
ylabel_bbox.y1,
|
|
342
|
+
)
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
# Legend bbox
|
|
347
|
+
legend = ax.get_legend()
|
|
348
|
+
if legend:
|
|
349
|
+
try:
|
|
350
|
+
legend_bbox = legend.get_window_extent(renderer)
|
|
351
|
+
state.element_bboxes_raw["legend"] = (
|
|
352
|
+
legend_bbox.x0,
|
|
353
|
+
legend_bbox.y0,
|
|
354
|
+
legend_bbox.x1,
|
|
355
|
+
legend_bbox.y1,
|
|
356
|
+
)
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
359
|
+
|
|
360
|
+
# X axis (bottom spine area)
|
|
361
|
+
try:
|
|
362
|
+
xaxis_bbox = ax.spines["bottom"].get_window_extent(renderer)
|
|
363
|
+
state.element_bboxes_raw["xaxis"] = (
|
|
364
|
+
xaxis_bbox.x0,
|
|
365
|
+
xaxis_bbox.y0 - 20,
|
|
366
|
+
xaxis_bbox.x1,
|
|
367
|
+
xaxis_bbox.y1 + 10,
|
|
368
|
+
)
|
|
369
|
+
except Exception:
|
|
370
|
+
pass
|
|
371
|
+
|
|
372
|
+
# Y axis (left spine area)
|
|
373
|
+
try:
|
|
374
|
+
yaxis_bbox = ax.spines["left"].get_window_extent(renderer)
|
|
375
|
+
state.element_bboxes_raw["yaxis"] = (
|
|
376
|
+
yaxis_bbox.x0 - 20,
|
|
377
|
+
yaxis_bbox.y0,
|
|
378
|
+
yaxis_bbox.x1 + 10,
|
|
379
|
+
yaxis_bbox.y1,
|
|
380
|
+
)
|
|
381
|
+
except Exception:
|
|
382
|
+
pass
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _scale_element_bboxes(
|
|
386
|
+
state: "EditorState",
|
|
387
|
+
ratio: float,
|
|
388
|
+
x_offset: int,
|
|
389
|
+
y_offset: int,
|
|
390
|
+
new_height: int,
|
|
391
|
+
) -> None:
|
|
392
|
+
"""Scale element bboxes to preview coordinates."""
|
|
393
|
+
state.element_bboxes = {}
|
|
394
|
+
for elem_type, raw_bbox in state.element_bboxes_raw.items():
|
|
395
|
+
if raw_bbox is None:
|
|
396
|
+
continue
|
|
397
|
+
rx0, ry0, rx1, ry1 = raw_bbox
|
|
398
|
+
# Scale to resized image
|
|
399
|
+
sx0 = int(rx0 * ratio) + x_offset
|
|
400
|
+
sx1 = int(rx1 * ratio) + x_offset
|
|
401
|
+
# Flip Y coordinate (matplotlib origin is bottom, preview is top)
|
|
402
|
+
sy0 = new_height - int(ry1 * ratio) + y_offset
|
|
403
|
+
sy1 = new_height - int(ry0 * ratio) + y_offset
|
|
404
|
+
state.element_bboxes[elem_type] = (sx0, sy0, sx1, sy1)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def draw_element_highlights(state: "EditorState", fig, ax) -> None:
|
|
408
|
+
"""Draw selection highlights for non-trace elements."""
|
|
409
|
+
from matplotlib.patches import FancyBboxPatch
|
|
410
|
+
|
|
411
|
+
renderer = fig.canvas.get_renderer()
|
|
412
|
+
|
|
413
|
+
selected_type = (
|
|
414
|
+
state.selected_element.get("type") if state.selected_element else None
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# Skip if selecting traces (handled separately in plot_from_csv)
|
|
418
|
+
if selected_type == "trace":
|
|
419
|
+
selected_type = None
|
|
420
|
+
|
|
421
|
+
def add_highlight_box(text_obj, color, alpha, linewidth=2):
|
|
422
|
+
"""Add highlight rectangle around a text object (outline only)."""
|
|
423
|
+
try:
|
|
424
|
+
bbox = text_obj.get_window_extent(renderer)
|
|
425
|
+
fig_bbox = bbox.transformed(fig.transFigure.inverted())
|
|
426
|
+
padding = 0.01
|
|
427
|
+
rect = FancyBboxPatch(
|
|
428
|
+
(fig_bbox.x0 - padding, fig_bbox.y0 - padding),
|
|
429
|
+
fig_bbox.width + 2 * padding,
|
|
430
|
+
fig_bbox.height + 2 * padding,
|
|
431
|
+
boxstyle="round,pad=0.02,rounding_size=0.01",
|
|
432
|
+
facecolor="none",
|
|
433
|
+
edgecolor=color,
|
|
434
|
+
alpha=0.7,
|
|
435
|
+
linewidth=linewidth,
|
|
436
|
+
transform=fig.transFigure,
|
|
437
|
+
zorder=100,
|
|
438
|
+
)
|
|
439
|
+
fig.patches.append(rect)
|
|
440
|
+
except Exception:
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
def add_spine_highlight(spine, color, alpha, linewidth=2):
|
|
444
|
+
"""Add highlight to a spine/axis (outline only)."""
|
|
445
|
+
try:
|
|
446
|
+
bbox = spine.get_window_extent(renderer)
|
|
447
|
+
fig_bbox = bbox.transformed(fig.transFigure.inverted())
|
|
448
|
+
padding = 0.01
|
|
449
|
+
rect = FancyBboxPatch(
|
|
450
|
+
(fig_bbox.x0 - padding, fig_bbox.y0 - padding),
|
|
451
|
+
fig_bbox.width + 2 * padding,
|
|
452
|
+
fig_bbox.height + 2 * padding,
|
|
453
|
+
boxstyle="round,pad=0.01",
|
|
454
|
+
facecolor="none",
|
|
455
|
+
edgecolor=color,
|
|
456
|
+
alpha=0.7,
|
|
457
|
+
linewidth=linewidth,
|
|
458
|
+
transform=fig.transFigure,
|
|
459
|
+
zorder=100,
|
|
460
|
+
)
|
|
461
|
+
fig.patches.append(rect)
|
|
462
|
+
except Exception:
|
|
463
|
+
pass
|
|
464
|
+
|
|
465
|
+
# Map element types to matplotlib objects
|
|
466
|
+
element_map = {
|
|
467
|
+
"title": ax.title,
|
|
468
|
+
"xlabel": ax.xaxis.label,
|
|
469
|
+
"ylabel": ax.yaxis.label,
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
# Draw selection highlight (outline only, no fill)
|
|
473
|
+
select_color = "#FFC850"
|
|
474
|
+
if selected_type in element_map:
|
|
475
|
+
add_highlight_box(element_map[selected_type], select_color, 0.0, linewidth=2)
|
|
476
|
+
elif selected_type == "xaxis":
|
|
477
|
+
add_spine_highlight(ax.spines["bottom"], select_color, 0.0, linewidth=2)
|
|
478
|
+
elif selected_type == "yaxis":
|
|
479
|
+
add_spine_highlight(ax.spines["left"], select_color, 0.0, linewidth=2)
|
|
480
|
+
elif selected_type == "legend":
|
|
481
|
+
legend = ax.get_legend()
|
|
482
|
+
if legend:
|
|
483
|
+
try:
|
|
484
|
+
bbox = legend.get_window_extent(renderer)
|
|
485
|
+
fig_bbox = bbox.transformed(fig.transFigure.inverted())
|
|
486
|
+
padding = 0.01
|
|
487
|
+
rect = FancyBboxPatch(
|
|
488
|
+
(fig_bbox.x0 - padding, fig_bbox.y0 - padding),
|
|
489
|
+
fig_bbox.width + 2 * padding,
|
|
490
|
+
fig_bbox.height + 2 * padding,
|
|
491
|
+
boxstyle="round,pad=0.02",
|
|
492
|
+
facecolor="none",
|
|
493
|
+
edgecolor=select_color,
|
|
494
|
+
alpha=0.7,
|
|
495
|
+
linewidth=2,
|
|
496
|
+
transform=fig.transFigure,
|
|
497
|
+
zorder=100,
|
|
498
|
+
)
|
|
499
|
+
fig.patches.append(rect)
|
|
500
|
+
except Exception:
|
|
501
|
+
pass
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
# EOF
|