scitex 2.14.0__py3-none-any.whl → 2.15.2__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 +244 -0
- scitex/_mcp_tools/template.py +24 -0
- scitex/_mcp_tools/writer.py +21 -204
- 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 +76 -27
- scitex/cli/capture.py +13 -20
- scitex/cli/introspect.py +481 -0
- scitex/cli/main.py +200 -109
- scitex/cli/mcp.py +60 -34
- scitex/cli/plt.py +357 -0
- scitex/cli/repro.py +15 -8
- scitex/cli/resource.py +15 -8
- scitex/cli/scholar/__init__.py +23 -8
- scitex/cli/scholar/_crossref_scitex.py +296 -0
- scitex/cli/scholar/_fetch.py +25 -3
- scitex/cli/social.py +314 -0
- scitex/cli/stats.py +15 -8
- scitex/cli/template.py +129 -12
- scitex/cli/tex.py +15 -8
- scitex/cli/writer.py +132 -8
- 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} +43 -54
- 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/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
- scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
- scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
- scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
- scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
- scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -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/_mcp/handlers.py +11 -744
- scitex/writer/_mcp/tool_schemas.py +5 -335
- scitex-2.15.2.dist-info/METADATA +648 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/RECORD +246 -150
- scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
- scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
- scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
- scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
- scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
- 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/data/.gitkeep +0 -0
- scitex/scholar/data/README.md +0 -44
- scitex/scholar/data/bib_files/bibliography.bib +0 -1952
- scitex/scholar/data/bib_files/neurovista.bib +0 -277
- scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
- scitex/scholar/data/bib_files/openaccess.bib +0 -89
- scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
- scitex/scholar/data/bib_files/pac.bib +0 -698
- scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
- scitex/scholar/data/bib_files/pac_processed.bib +0 -0
- scitex/scholar/data/bib_files/pac_titles.txt +0 -75
- scitex/scholar/data/bib_files/paywalled.bib +0 -98
- scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
- scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
- scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
- scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
- scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_seizure.bib +0 -46
- scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
- scitex/scholar/data/impact_factor.db +0 -0
- 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.2.dist-info}/WHEEL +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/entry_points.txt +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_hitmap/_hitmap_core.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Core hitmap generation functions.
|
|
7
|
+
|
|
8
|
+
This module provides the main hitmap generation functions using unique ID colors
|
|
9
|
+
for pixel-perfect element selection.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import io
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Dict, Tuple
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from ._artist_extraction import get_all_artists
|
|
18
|
+
from ._color_application import apply_id_color
|
|
19
|
+
from ._color_conversion import id_to_rgb
|
|
20
|
+
from ._constants import HITMAP_AXES_COLOR, HITMAP_BACKGROUND_COLOR
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from PIL import Image
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"generate_hitmap_id_colors",
|
|
27
|
+
"generate_hitmap_with_bbox_tight",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def generate_hitmap_id_colors(
|
|
32
|
+
fig,
|
|
33
|
+
dpi: int = 100,
|
|
34
|
+
include_text: bool = False,
|
|
35
|
+
) -> Tuple[np.ndarray, Dict[int, Dict[str, Any]]]:
|
|
36
|
+
"""
|
|
37
|
+
Generate a hit map using unique ID colors (fastest method).
|
|
38
|
+
|
|
39
|
+
Assigns unique RGB colors to each element, renders once, and creates
|
|
40
|
+
a pixel-perfect hit map where each pixel's RGB values encode the
|
|
41
|
+
element ID using 24-bit color space (~16.7M unique IDs).
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
fig : matplotlib.figure.Figure
|
|
46
|
+
The figure to generate hit map for.
|
|
47
|
+
dpi : int
|
|
48
|
+
Resolution for hit map rendering.
|
|
49
|
+
include_text : bool
|
|
50
|
+
Whether to include text elements in hit map.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
tuple
|
|
55
|
+
(hitmap_array, color_map) where:
|
|
56
|
+
- hitmap_array: uint32 array with element IDs (0 = background)
|
|
57
|
+
- color_map: dict mapping ID to element info
|
|
58
|
+
|
|
59
|
+
Notes
|
|
60
|
+
-----
|
|
61
|
+
Performance: ~89ms for complex figures (33x faster than sequential)
|
|
62
|
+
"""
|
|
63
|
+
artists = get_all_artists(fig, include_text)
|
|
64
|
+
|
|
65
|
+
if not artists:
|
|
66
|
+
h = int(fig.get_figheight() * dpi)
|
|
67
|
+
w = int(fig.get_figwidth() * dpi)
|
|
68
|
+
return np.zeros((h, w), dtype=np.uint32), {}
|
|
69
|
+
|
|
70
|
+
original_props = []
|
|
71
|
+
color_map = {}
|
|
72
|
+
|
|
73
|
+
for i, (artist, ax_idx, artist_type) in enumerate(artists):
|
|
74
|
+
element_id = i + 1
|
|
75
|
+
r, g, b = id_to_rgb(element_id)
|
|
76
|
+
hex_color = f"#{r:02x}{g:02x}{b:02x}"
|
|
77
|
+
|
|
78
|
+
# Store original properties
|
|
79
|
+
props = {"artist": artist, "type": artist_type}
|
|
80
|
+
try:
|
|
81
|
+
if hasattr(artist, "get_color"):
|
|
82
|
+
props["color"] = artist.get_color()
|
|
83
|
+
if hasattr(artist, "get_facecolor"):
|
|
84
|
+
props["facecolor"] = artist.get_facecolor()
|
|
85
|
+
if hasattr(artist, "get_edgecolor"):
|
|
86
|
+
props["edgecolor"] = artist.get_edgecolor()
|
|
87
|
+
if hasattr(artist, "get_alpha"):
|
|
88
|
+
props["alpha"] = artist.get_alpha()
|
|
89
|
+
if hasattr(artist, "get_antialiased"):
|
|
90
|
+
props["antialiased"] = artist.get_antialiased()
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
original_props.append(props)
|
|
94
|
+
|
|
95
|
+
# Build color map entry
|
|
96
|
+
label = ""
|
|
97
|
+
if hasattr(artist, "get_label"):
|
|
98
|
+
label = artist.get_label()
|
|
99
|
+
if label.startswith("_"):
|
|
100
|
+
label = f"{artist_type}_{i}"
|
|
101
|
+
|
|
102
|
+
color_map[element_id] = {
|
|
103
|
+
"id": element_id,
|
|
104
|
+
"type": artist_type,
|
|
105
|
+
"label": label,
|
|
106
|
+
"axes_index": ax_idx,
|
|
107
|
+
"rgb": [r, g, b],
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Apply ID color and disable anti-aliasing
|
|
111
|
+
try:
|
|
112
|
+
apply_id_color(artist, hex_color)
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
# Make non-artist elements the reserved axes color
|
|
117
|
+
axes_color = HITMAP_AXES_COLOR
|
|
118
|
+
for ax in fig.axes:
|
|
119
|
+
ax.grid(False)
|
|
120
|
+
for spine in ax.spines.values():
|
|
121
|
+
spine.set_color(axes_color)
|
|
122
|
+
ax.set_facecolor(HITMAP_BACKGROUND_COLOR)
|
|
123
|
+
ax.tick_params(colors=axes_color, labelcolor=axes_color)
|
|
124
|
+
ax.xaxis.label.set_color(axes_color)
|
|
125
|
+
ax.yaxis.label.set_color(axes_color)
|
|
126
|
+
ax.title.set_color(axes_color)
|
|
127
|
+
if ax.get_legend():
|
|
128
|
+
ax.get_legend().set_visible(False)
|
|
129
|
+
|
|
130
|
+
fig.patch.set_facecolor(HITMAP_BACKGROUND_COLOR)
|
|
131
|
+
|
|
132
|
+
# Render
|
|
133
|
+
fig.canvas.draw()
|
|
134
|
+
img = np.array(fig.canvas.buffer_rgba())
|
|
135
|
+
# Convert RGB to element ID using 24-bit encoding
|
|
136
|
+
hitmap = (
|
|
137
|
+
(img[:, :, 0].astype(np.uint32) << 16)
|
|
138
|
+
| (img[:, :, 1].astype(np.uint32) << 8)
|
|
139
|
+
| img[:, :, 2].astype(np.uint32)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Restore original properties
|
|
143
|
+
for props in original_props:
|
|
144
|
+
artist = props["artist"]
|
|
145
|
+
try:
|
|
146
|
+
if "color" in props and hasattr(artist, "set_color"):
|
|
147
|
+
artist.set_color(props["color"])
|
|
148
|
+
if "facecolor" in props and hasattr(artist, "set_facecolor"):
|
|
149
|
+
artist.set_facecolor(props["facecolor"])
|
|
150
|
+
if "edgecolor" in props and hasattr(artist, "set_edgecolor"):
|
|
151
|
+
artist.set_edgecolor(props["edgecolor"])
|
|
152
|
+
if "alpha" in props and hasattr(artist, "set_alpha"):
|
|
153
|
+
artist.set_alpha(props["alpha"])
|
|
154
|
+
if "antialiased" in props and hasattr(artist, "set_antialiased"):
|
|
155
|
+
artist.set_antialiased(props["antialiased"])
|
|
156
|
+
except Exception:
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
return hitmap, color_map
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def generate_hitmap_with_bbox_tight(
|
|
163
|
+
fig,
|
|
164
|
+
dpi: int = 150,
|
|
165
|
+
include_text: bool = False,
|
|
166
|
+
) -> Tuple["Image.Image", Dict[int, Dict[str, Any]]]:
|
|
167
|
+
"""
|
|
168
|
+
Generate a hitmap image with bbox_inches='tight' to match PNG output.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
fig : matplotlib.figure.Figure
|
|
173
|
+
The figure to generate hit map for.
|
|
174
|
+
dpi : int
|
|
175
|
+
Resolution for hit map rendering.
|
|
176
|
+
include_text : bool
|
|
177
|
+
Whether to include text elements in hit map.
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
tuple
|
|
182
|
+
(hitmap_image, color_map) where:
|
|
183
|
+
- hitmap_image: PIL.Image.Image with RGB-encoded element IDs
|
|
184
|
+
- color_map: dict mapping ID to element info
|
|
185
|
+
"""
|
|
186
|
+
from PIL import Image
|
|
187
|
+
|
|
188
|
+
artists = get_all_artists(fig, include_text)
|
|
189
|
+
|
|
190
|
+
if not artists:
|
|
191
|
+
buf = io.BytesIO()
|
|
192
|
+
fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight")
|
|
193
|
+
buf.seek(0)
|
|
194
|
+
img = Image.open(buf).convert("RGB")
|
|
195
|
+
black_img = Image.new("RGB", img.size, (0, 0, 0))
|
|
196
|
+
return black_img, {}
|
|
197
|
+
|
|
198
|
+
original_props = []
|
|
199
|
+
original_ax_props = []
|
|
200
|
+
|
|
201
|
+
# Store original axes properties
|
|
202
|
+
for ax in fig.axes:
|
|
203
|
+
ax_props = {
|
|
204
|
+
"ax": ax,
|
|
205
|
+
"facecolor": ax.get_facecolor(),
|
|
206
|
+
"grid_visible": (
|
|
207
|
+
ax.xaxis.get_gridlines()[0].get_visible()
|
|
208
|
+
if ax.xaxis.get_gridlines()
|
|
209
|
+
else False
|
|
210
|
+
),
|
|
211
|
+
"spines": {name: spine.get_visible() for name, spine in ax.spines.items()},
|
|
212
|
+
"xlabel": ax.get_xlabel(),
|
|
213
|
+
"ylabel": ax.get_ylabel(),
|
|
214
|
+
"title": ax.get_title(),
|
|
215
|
+
"tick_params": {},
|
|
216
|
+
}
|
|
217
|
+
if ax.get_legend():
|
|
218
|
+
ax_props["legend_visible"] = ax.get_legend().get_visible()
|
|
219
|
+
original_ax_props.append(ax_props)
|
|
220
|
+
|
|
221
|
+
original_fig_facecolor = fig.patch.get_facecolor()
|
|
222
|
+
|
|
223
|
+
# Build color map
|
|
224
|
+
color_map = {}
|
|
225
|
+
|
|
226
|
+
for i, (artist, ax_idx, artist_type) in enumerate(artists):
|
|
227
|
+
element_id = i + 1
|
|
228
|
+
r, g, b = id_to_rgb(element_id)
|
|
229
|
+
hex_color = f"#{r:02x}{g:02x}{b:02x}"
|
|
230
|
+
|
|
231
|
+
# Store original properties
|
|
232
|
+
props = {"artist": artist, "type": artist_type}
|
|
233
|
+
try:
|
|
234
|
+
if hasattr(artist, "get_color"):
|
|
235
|
+
props["color"] = artist.get_color()
|
|
236
|
+
if hasattr(artist, "get_facecolor"):
|
|
237
|
+
props["facecolor"] = artist.get_facecolor()
|
|
238
|
+
if hasattr(artist, "get_edgecolor"):
|
|
239
|
+
props["edgecolor"] = artist.get_edgecolor()
|
|
240
|
+
if hasattr(artist, "get_alpha"):
|
|
241
|
+
props["alpha"] = artist.get_alpha()
|
|
242
|
+
if hasattr(artist, "get_antialiased"):
|
|
243
|
+
props["antialiased"] = artist.get_antialiased()
|
|
244
|
+
except Exception:
|
|
245
|
+
pass
|
|
246
|
+
original_props.append(props)
|
|
247
|
+
|
|
248
|
+
# Build color map entry
|
|
249
|
+
label = ""
|
|
250
|
+
if hasattr(artist, "get_label"):
|
|
251
|
+
label = artist.get_label()
|
|
252
|
+
if label.startswith("_"):
|
|
253
|
+
label = f"{artist_type}_{i}"
|
|
254
|
+
|
|
255
|
+
color_map[element_id] = {
|
|
256
|
+
"id": element_id,
|
|
257
|
+
"type": artist_type,
|
|
258
|
+
"label": label,
|
|
259
|
+
"axes_index": ax_idx,
|
|
260
|
+
"rgb": [r, g, b],
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
# Apply ID color
|
|
264
|
+
try:
|
|
265
|
+
apply_id_color(artist, hex_color)
|
|
266
|
+
except Exception:
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
# Make non-artist elements reserved axes color
|
|
270
|
+
axes_color = HITMAP_AXES_COLOR
|
|
271
|
+
for ax in fig.axes:
|
|
272
|
+
ax.grid(False)
|
|
273
|
+
for spine in ax.spines.values():
|
|
274
|
+
spine.set_color(axes_color)
|
|
275
|
+
ax.set_facecolor(HITMAP_BACKGROUND_COLOR)
|
|
276
|
+
ax.tick_params(colors=axes_color, labelcolor=axes_color)
|
|
277
|
+
ax.xaxis.label.set_color(axes_color)
|
|
278
|
+
ax.yaxis.label.set_color(axes_color)
|
|
279
|
+
ax.title.set_color(axes_color)
|
|
280
|
+
if ax.get_legend():
|
|
281
|
+
ax.get_legend().set_visible(False)
|
|
282
|
+
|
|
283
|
+
fig.patch.set_facecolor(HITMAP_BACKGROUND_COLOR)
|
|
284
|
+
|
|
285
|
+
# Save hitmap with bbox_inches='tight'
|
|
286
|
+
buf = io.BytesIO()
|
|
287
|
+
fig.savefig(
|
|
288
|
+
buf,
|
|
289
|
+
format="png",
|
|
290
|
+
dpi=dpi,
|
|
291
|
+
bbox_inches="tight",
|
|
292
|
+
facecolor=HITMAP_BACKGROUND_COLOR,
|
|
293
|
+
)
|
|
294
|
+
buf.seek(0)
|
|
295
|
+
hitmap_img = Image.open(buf).convert("RGB")
|
|
296
|
+
|
|
297
|
+
# Restore original properties
|
|
298
|
+
for props in original_props:
|
|
299
|
+
artist = props["artist"]
|
|
300
|
+
try:
|
|
301
|
+
if "color" in props and hasattr(artist, "set_color"):
|
|
302
|
+
artist.set_color(props["color"])
|
|
303
|
+
if "facecolor" in props and hasattr(artist, "set_facecolor"):
|
|
304
|
+
artist.set_facecolor(props["facecolor"])
|
|
305
|
+
if "edgecolor" in props and hasattr(artist, "set_edgecolor"):
|
|
306
|
+
artist.set_edgecolor(props["edgecolor"])
|
|
307
|
+
if "alpha" in props and hasattr(artist, "set_alpha"):
|
|
308
|
+
artist.set_alpha(props["alpha"])
|
|
309
|
+
if "antialiased" in props and hasattr(artist, "set_antialiased"):
|
|
310
|
+
artist.set_antialiased(props["antialiased"])
|
|
311
|
+
except Exception:
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
# Restore axes properties
|
|
315
|
+
for ax_props in original_ax_props:
|
|
316
|
+
ax = ax_props["ax"]
|
|
317
|
+
try:
|
|
318
|
+
ax.set_facecolor(ax_props["facecolor"])
|
|
319
|
+
for name, visible in ax_props["spines"].items():
|
|
320
|
+
ax.spines[name].set_visible(visible)
|
|
321
|
+
ax.set_xlabel(ax_props["xlabel"])
|
|
322
|
+
ax.set_ylabel(ax_props["ylabel"])
|
|
323
|
+
ax.set_title(ax_props["title"])
|
|
324
|
+
if "legend_visible" in ax_props and ax.get_legend():
|
|
325
|
+
ax.get_legend().set_visible(ax_props["legend_visible"])
|
|
326
|
+
except Exception:
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
fig.patch.set_facecolor(original_fig_facecolor)
|
|
330
|
+
|
|
331
|
+
return hitmap_img, color_map
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# EOF
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_hitmap/_path_extraction.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Path data and selectable regions extraction for hitmap generation.
|
|
7
|
+
|
|
8
|
+
This module provides functions to extract path/geometry data and selectable
|
|
9
|
+
regions for client-side hit testing.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import warnings
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from ._artist_extraction import get_all_artists
|
|
18
|
+
from ._constants import to_native
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"extract_path_data",
|
|
22
|
+
"extract_selectable_regions",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def extract_path_data(
|
|
27
|
+
fig,
|
|
28
|
+
include_text: bool = False,
|
|
29
|
+
) -> Dict[str, Any]:
|
|
30
|
+
"""
|
|
31
|
+
Extract path/geometry data for client-side hit testing.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
fig : matplotlib.figure.Figure
|
|
36
|
+
The figure to extract data from.
|
|
37
|
+
include_text : bool
|
|
38
|
+
Whether to include text elements.
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
dict
|
|
43
|
+
Exported data structure with figure info and artist geometries.
|
|
44
|
+
|
|
45
|
+
Notes
|
|
46
|
+
-----
|
|
47
|
+
Performance: ~192ms extraction, ~0.01ms client-side queries
|
|
48
|
+
"""
|
|
49
|
+
with warnings.catch_warnings():
|
|
50
|
+
warnings.filterwarnings("ignore", message=".*tight_layout.*")
|
|
51
|
+
fig.canvas.draw()
|
|
52
|
+
|
|
53
|
+
artists = get_all_artists(fig, include_text)
|
|
54
|
+
|
|
55
|
+
dpi = fig.dpi
|
|
56
|
+
fig_width_px = int(fig.get_figwidth() * dpi)
|
|
57
|
+
fig_height_px = int(fig.get_figheight() * dpi)
|
|
58
|
+
|
|
59
|
+
export = {
|
|
60
|
+
"figure": {
|
|
61
|
+
"width_px": fig_width_px,
|
|
62
|
+
"height_px": fig_height_px,
|
|
63
|
+
"dpi": dpi,
|
|
64
|
+
},
|
|
65
|
+
"axes": [],
|
|
66
|
+
"artists": [],
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Export axes info
|
|
70
|
+
for ax in fig.axes:
|
|
71
|
+
bbox = ax.get_position()
|
|
72
|
+
export["axes"].append(
|
|
73
|
+
{
|
|
74
|
+
"xlim": list(ax.get_xlim()),
|
|
75
|
+
"ylim": list(ax.get_ylim()),
|
|
76
|
+
"bbox_norm": {
|
|
77
|
+
"x0": bbox.x0,
|
|
78
|
+
"y0": bbox.y0,
|
|
79
|
+
"x1": bbox.x1,
|
|
80
|
+
"y1": bbox.y1,
|
|
81
|
+
},
|
|
82
|
+
"bbox_px": {
|
|
83
|
+
"x0": int(bbox.x0 * fig_width_px),
|
|
84
|
+
"y0": int((1 - bbox.y1) * fig_height_px),
|
|
85
|
+
"x1": int(bbox.x1 * fig_width_px),
|
|
86
|
+
"y1": int((1 - bbox.y0) * fig_height_px),
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Export artist geometries
|
|
92
|
+
renderer = fig.canvas.get_renderer()
|
|
93
|
+
|
|
94
|
+
for i, (artist, ax_idx, artist_type) in enumerate(artists):
|
|
95
|
+
artist_data = {
|
|
96
|
+
"id": i,
|
|
97
|
+
"type": artist_type,
|
|
98
|
+
"axes_index": ax_idx,
|
|
99
|
+
"label": "",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Get label
|
|
103
|
+
if hasattr(artist, "get_label"):
|
|
104
|
+
label = artist.get_label()
|
|
105
|
+
artist_data["label"] = (
|
|
106
|
+
label if not label.startswith("_") else f"{artist_type}_{i}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Get bounding box
|
|
110
|
+
try:
|
|
111
|
+
bbox = artist.get_window_extent(renderer)
|
|
112
|
+
artist_data["bbox_px"] = {
|
|
113
|
+
"x0": float(bbox.x0),
|
|
114
|
+
"y0": float(fig_height_px - bbox.y1),
|
|
115
|
+
"x1": float(bbox.x1),
|
|
116
|
+
"y1": float(fig_height_px - bbox.y0),
|
|
117
|
+
}
|
|
118
|
+
except Exception:
|
|
119
|
+
artist_data["bbox_px"] = None
|
|
120
|
+
|
|
121
|
+
# Extract type-specific geometry
|
|
122
|
+
try:
|
|
123
|
+
if artist_type == "line" and hasattr(artist, "get_xydata"):
|
|
124
|
+
xy = artist.get_xydata()
|
|
125
|
+
transform = artist.get_transform()
|
|
126
|
+
xy_px = transform.transform(xy)
|
|
127
|
+
xy_px[:, 1] = fig_height_px - xy_px[:, 1]
|
|
128
|
+
if len(xy_px) > 100:
|
|
129
|
+
indices = np.linspace(0, len(xy_px) - 1, 100, dtype=int)
|
|
130
|
+
xy_px = xy_px[indices]
|
|
131
|
+
artist_data["path_px"] = xy_px.tolist()
|
|
132
|
+
artist_data["linewidth"] = artist.get_linewidth()
|
|
133
|
+
|
|
134
|
+
elif artist_type == "scatter" and hasattr(artist, "get_offsets"):
|
|
135
|
+
offsets = artist.get_offsets()
|
|
136
|
+
transform = artist.get_transform()
|
|
137
|
+
offsets_px = transform.transform(offsets)
|
|
138
|
+
offsets_px[:, 1] = fig_height_px - offsets_px[:, 1]
|
|
139
|
+
artist_data["points_px"] = offsets_px.tolist()
|
|
140
|
+
sizes = artist.get_sizes()
|
|
141
|
+
artist_data["sizes"] = sizes.tolist() if len(sizes) > 0 else [36]
|
|
142
|
+
|
|
143
|
+
elif artist_type == "fill" and hasattr(artist, "get_paths"):
|
|
144
|
+
paths = artist.get_paths()
|
|
145
|
+
if paths:
|
|
146
|
+
transform = artist.get_transform()
|
|
147
|
+
vertices = paths[0].vertices
|
|
148
|
+
vertices_px = transform.transform(vertices)
|
|
149
|
+
vertices_px[:, 1] = fig_height_px - vertices_px[:, 1]
|
|
150
|
+
if len(vertices_px) > 100:
|
|
151
|
+
indices = np.linspace(0, len(vertices_px) - 1, 100, dtype=int)
|
|
152
|
+
vertices_px = vertices_px[indices]
|
|
153
|
+
artist_data["polygon_px"] = vertices_px.tolist()
|
|
154
|
+
|
|
155
|
+
elif artist_type == "bar" and hasattr(artist, "patches"):
|
|
156
|
+
bars = []
|
|
157
|
+
for patch in artist.patches:
|
|
158
|
+
x_data = patch.get_x()
|
|
159
|
+
y_data = patch.get_y()
|
|
160
|
+
w_data = patch.get_width()
|
|
161
|
+
h_data = patch.get_height()
|
|
162
|
+
bars.append(
|
|
163
|
+
{
|
|
164
|
+
"x": x_data,
|
|
165
|
+
"y": y_data,
|
|
166
|
+
"width": w_data,
|
|
167
|
+
"height": h_data,
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
artist_data["bars_data"] = bars
|
|
171
|
+
|
|
172
|
+
elif artist_type == "rectangle":
|
|
173
|
+
artist_data["rectangle"] = {
|
|
174
|
+
"x": artist.get_x(),
|
|
175
|
+
"y": artist.get_y(),
|
|
176
|
+
"width": artist.get_width(),
|
|
177
|
+
"height": artist.get_height(),
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
except Exception as e:
|
|
181
|
+
artist_data["error"] = str(e)
|
|
182
|
+
|
|
183
|
+
export["artists"].append(artist_data)
|
|
184
|
+
|
|
185
|
+
return to_native(export)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def extract_selectable_regions(fig) -> Dict[str, Any]:
|
|
189
|
+
"""
|
|
190
|
+
Extract bounding boxes for axis/annotation elements (non-data elements).
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
fig : matplotlib.figure.Figure
|
|
195
|
+
The figure to extract regions from.
|
|
196
|
+
|
|
197
|
+
Returns
|
|
198
|
+
-------
|
|
199
|
+
dict
|
|
200
|
+
Dictionary with selectable regions.
|
|
201
|
+
"""
|
|
202
|
+
with warnings.catch_warnings():
|
|
203
|
+
warnings.filterwarnings("ignore", message=".*tight_layout.*")
|
|
204
|
+
fig.canvas.draw()
|
|
205
|
+
|
|
206
|
+
dpi = fig.dpi
|
|
207
|
+
fig_width_px = int(fig.get_figwidth() * dpi)
|
|
208
|
+
fig_height_px = int(fig.get_figheight() * dpi)
|
|
209
|
+
|
|
210
|
+
renderer = fig.canvas.get_renderer()
|
|
211
|
+
|
|
212
|
+
def get_bbox_px(artist) -> Optional[List[float]]:
|
|
213
|
+
"""Get bounding box in pixels (y-flipped for image coordinates)."""
|
|
214
|
+
try:
|
|
215
|
+
bbox = artist.get_window_extent(renderer)
|
|
216
|
+
if bbox.width > 0 and bbox.height > 0:
|
|
217
|
+
return [
|
|
218
|
+
float(bbox.x0),
|
|
219
|
+
float(fig_height_px - bbox.y1),
|
|
220
|
+
float(bbox.x1),
|
|
221
|
+
float(fig_height_px - bbox.y0),
|
|
222
|
+
]
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
def get_text_info(text_artist) -> Optional[Dict[str, Any]]:
|
|
228
|
+
"""Extract text element info with bounding box."""
|
|
229
|
+
if text_artist is None:
|
|
230
|
+
return None
|
|
231
|
+
text = text_artist.get_text()
|
|
232
|
+
if not text or not text.strip():
|
|
233
|
+
return None
|
|
234
|
+
bbox = get_bbox_px(text_artist)
|
|
235
|
+
if bbox is None:
|
|
236
|
+
return None
|
|
237
|
+
return {
|
|
238
|
+
"bbox_px": bbox,
|
|
239
|
+
"text": text,
|
|
240
|
+
"fontsize": text_artist.get_fontsize(),
|
|
241
|
+
"color": text_artist.get_color(),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
regions = {"axes": []}
|
|
245
|
+
|
|
246
|
+
for ax_idx, ax in enumerate(fig.axes):
|
|
247
|
+
ax_region = {"index": ax_idx}
|
|
248
|
+
|
|
249
|
+
# Title
|
|
250
|
+
title_info = get_text_info(ax.title)
|
|
251
|
+
if title_info:
|
|
252
|
+
ax_region["title"] = title_info
|
|
253
|
+
|
|
254
|
+
# X label
|
|
255
|
+
xlabel_info = get_text_info(ax.xaxis.label)
|
|
256
|
+
if xlabel_info:
|
|
257
|
+
ax_region["xlabel"] = xlabel_info
|
|
258
|
+
|
|
259
|
+
# Y label
|
|
260
|
+
ylabel_info = get_text_info(ax.yaxis.label)
|
|
261
|
+
if ylabel_info:
|
|
262
|
+
ax_region["ylabel"] = ylabel_info
|
|
263
|
+
|
|
264
|
+
# X axis elements
|
|
265
|
+
xaxis_info = {"spine": None, "ticks": [], "ticklabels": []}
|
|
266
|
+
|
|
267
|
+
if ax.spines["bottom"].get_visible():
|
|
268
|
+
spine_bbox = get_bbox_px(ax.spines["bottom"])
|
|
269
|
+
if spine_bbox:
|
|
270
|
+
xaxis_info["spine"] = {"bbox_px": spine_bbox}
|
|
271
|
+
|
|
272
|
+
for tick in ax.xaxis.get_major_ticks():
|
|
273
|
+
if tick.tick1line.get_visible():
|
|
274
|
+
tick_bbox = get_bbox_px(tick.tick1line)
|
|
275
|
+
if tick_bbox:
|
|
276
|
+
xaxis_info["ticks"].append(
|
|
277
|
+
{
|
|
278
|
+
"bbox_px": tick_bbox,
|
|
279
|
+
"position": (
|
|
280
|
+
float(tick.get_loc())
|
|
281
|
+
if hasattr(tick, "get_loc")
|
|
282
|
+
else None
|
|
283
|
+
),
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if tick.label1.get_visible():
|
|
288
|
+
label_info = get_text_info(tick.label1)
|
|
289
|
+
if label_info:
|
|
290
|
+
xaxis_info["ticklabels"].append(label_info)
|
|
291
|
+
|
|
292
|
+
if xaxis_info["spine"] or xaxis_info["ticks"] or xaxis_info["ticklabels"]:
|
|
293
|
+
ax_region["xaxis"] = xaxis_info
|
|
294
|
+
|
|
295
|
+
# Y axis elements
|
|
296
|
+
yaxis_info = {"spine": None, "ticks": [], "ticklabels": []}
|
|
297
|
+
|
|
298
|
+
if ax.spines["left"].get_visible():
|
|
299
|
+
spine_bbox = get_bbox_px(ax.spines["left"])
|
|
300
|
+
if spine_bbox:
|
|
301
|
+
yaxis_info["spine"] = {"bbox_px": spine_bbox}
|
|
302
|
+
|
|
303
|
+
for tick in ax.yaxis.get_major_ticks():
|
|
304
|
+
if tick.tick1line.get_visible():
|
|
305
|
+
tick_bbox = get_bbox_px(tick.tick1line)
|
|
306
|
+
if tick_bbox:
|
|
307
|
+
yaxis_info["ticks"].append(
|
|
308
|
+
{
|
|
309
|
+
"bbox_px": tick_bbox,
|
|
310
|
+
"position": (
|
|
311
|
+
float(tick.get_loc())
|
|
312
|
+
if hasattr(tick, "get_loc")
|
|
313
|
+
else None
|
|
314
|
+
),
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if tick.label1.get_visible():
|
|
319
|
+
label_info = get_text_info(tick.label1)
|
|
320
|
+
if label_info:
|
|
321
|
+
yaxis_info["ticklabels"].append(label_info)
|
|
322
|
+
|
|
323
|
+
if yaxis_info["spine"] or yaxis_info["ticks"] or yaxis_info["ticklabels"]:
|
|
324
|
+
ax_region["yaxis"] = yaxis_info
|
|
325
|
+
|
|
326
|
+
# Legend
|
|
327
|
+
legend = ax.get_legend()
|
|
328
|
+
if legend and legend.get_visible():
|
|
329
|
+
legend_info = {"bbox_px": None, "entries": []}
|
|
330
|
+
|
|
331
|
+
legend_bbox = get_bbox_px(legend)
|
|
332
|
+
if legend_bbox:
|
|
333
|
+
legend_info["bbox_px"] = legend_bbox
|
|
334
|
+
|
|
335
|
+
for text in legend.get_texts():
|
|
336
|
+
entry_info = get_text_info(text)
|
|
337
|
+
if entry_info:
|
|
338
|
+
legend_info["entries"].append(entry_info)
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
handles = legend.legendHandles
|
|
342
|
+
for i, handle in enumerate(handles):
|
|
343
|
+
handle_bbox = get_bbox_px(handle)
|
|
344
|
+
if handle_bbox and i < len(legend_info["entries"]):
|
|
345
|
+
legend_info["entries"][i]["handle_bbox_px"] = handle_bbox
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
if legend_info["bbox_px"] or legend_info["entries"]:
|
|
350
|
+
ax_region["legend"] = legend_info
|
|
351
|
+
|
|
352
|
+
regions["axes"].append(ax_region)
|
|
353
|
+
|
|
354
|
+
return to_native(regions)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# EOF
|