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,295 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/_dearpygui/_selection.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Element selection for DearPyGui editor.
|
|
7
|
+
|
|
8
|
+
Handles click-to-select, hover detection, and element finding.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ._state import EditorState
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_trace_labels(state: "EditorState") -> List[str]:
|
|
21
|
+
"""Get list of trace labels for selection combo."""
|
|
22
|
+
traces = state.current_overrides.get("traces", [])
|
|
23
|
+
if not traces:
|
|
24
|
+
return ["(no traces)"]
|
|
25
|
+
return [t.get("label", t.get("id", f"Trace {i}")) for i, t in enumerate(traces)]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_all_element_labels(state: "EditorState") -> List[str]:
|
|
29
|
+
"""Get list of all selectable element labels."""
|
|
30
|
+
labels = []
|
|
31
|
+
|
|
32
|
+
# Fixed elements
|
|
33
|
+
labels.append("Title")
|
|
34
|
+
labels.append("X Label")
|
|
35
|
+
labels.append("Y Label")
|
|
36
|
+
labels.append("X Axis")
|
|
37
|
+
labels.append("Y Axis")
|
|
38
|
+
labels.append("Legend")
|
|
39
|
+
|
|
40
|
+
# Traces
|
|
41
|
+
traces = state.current_overrides.get("traces", [])
|
|
42
|
+
for i, t in enumerate(traces):
|
|
43
|
+
label = t.get("label", t.get("id", f"Trace {i}"))
|
|
44
|
+
labels.append(f"Trace: {label}")
|
|
45
|
+
|
|
46
|
+
return labels
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def find_clicked_element(
|
|
50
|
+
state: "EditorState", click_x: float, click_y: float
|
|
51
|
+
) -> Optional[Dict]:
|
|
52
|
+
"""Find which element was clicked based on stored bboxes."""
|
|
53
|
+
if not state.element_bboxes:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
# Check each element bbox
|
|
57
|
+
for element_type, bbox in state.element_bboxes.items():
|
|
58
|
+
if bbox is None:
|
|
59
|
+
continue
|
|
60
|
+
x0, y0, x1, y1 = bbox
|
|
61
|
+
if x0 <= click_x <= x1 and y0 <= click_y <= y1:
|
|
62
|
+
return {"type": element_type, "index": None}
|
|
63
|
+
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def find_nearest_trace(
|
|
68
|
+
state: "EditorState",
|
|
69
|
+
click_x: float,
|
|
70
|
+
click_y: float,
|
|
71
|
+
preview_width: int,
|
|
72
|
+
preview_height: int,
|
|
73
|
+
) -> Optional[int]:
|
|
74
|
+
"""Find the nearest trace to the click position."""
|
|
75
|
+
if state.csv_data is None or not isinstance(state.csv_data, pd.DataFrame):
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
traces = state.current_overrides.get("traces", [])
|
|
79
|
+
if not traces:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
# Get preview bounds from last render
|
|
83
|
+
if state.preview_bounds is None:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
x_offset, y_offset, fig_width, fig_height = state.preview_bounds
|
|
87
|
+
|
|
88
|
+
# Adjust click coordinates to figure space
|
|
89
|
+
fig_x = click_x - x_offset
|
|
90
|
+
fig_y = click_y - y_offset
|
|
91
|
+
|
|
92
|
+
# Check if click is within figure bounds
|
|
93
|
+
if not (0 <= fig_x <= fig_width and 0 <= fig_y <= fig_height):
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
# Get axes transform info
|
|
97
|
+
if state.axes_transform is None:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
ax_x0, ax_y0, ax_width, ax_height, xlim, ylim = state.axes_transform
|
|
101
|
+
|
|
102
|
+
# Convert figure pixel to axes pixel
|
|
103
|
+
ax_pixel_x = fig_x - ax_x0
|
|
104
|
+
ax_pixel_y = fig_y - ax_y0
|
|
105
|
+
|
|
106
|
+
# Check if click is within axes bounds
|
|
107
|
+
if not (0 <= ax_pixel_x <= ax_width and 0 <= ax_pixel_y <= ax_height):
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
# Convert axes pixel to data coordinates
|
|
111
|
+
# Note: y is flipped (0 at top in pixel space)
|
|
112
|
+
data_x = xlim[0] + (ax_pixel_x / ax_width) * (xlim[1] - xlim[0])
|
|
113
|
+
data_y = ylim[1] - (ax_pixel_y / ax_height) * (ylim[1] - ylim[0])
|
|
114
|
+
|
|
115
|
+
# Find nearest trace
|
|
116
|
+
df = state.csv_data
|
|
117
|
+
min_dist = float("inf")
|
|
118
|
+
nearest_idx = None
|
|
119
|
+
|
|
120
|
+
for i, trace in enumerate(traces):
|
|
121
|
+
csv_cols = trace.get("csv_columns", {})
|
|
122
|
+
x_col = csv_cols.get("x")
|
|
123
|
+
y_col = csv_cols.get("y")
|
|
124
|
+
|
|
125
|
+
if x_col not in df.columns or y_col not in df.columns:
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
trace_x = df[x_col].dropna().values
|
|
129
|
+
trace_y = df[y_col].dropna().values
|
|
130
|
+
|
|
131
|
+
if len(trace_x) == 0:
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
# Normalize coordinates for distance calculation
|
|
135
|
+
x_range = xlim[1] - xlim[0]
|
|
136
|
+
y_range = ylim[1] - ylim[0]
|
|
137
|
+
|
|
138
|
+
norm_click_x = (data_x - xlim[0]) / x_range if x_range > 0 else 0
|
|
139
|
+
norm_click_y = (data_y - ylim[0]) / y_range if y_range > 0 else 0
|
|
140
|
+
|
|
141
|
+
norm_trace_x = (trace_x - xlim[0]) / x_range if x_range > 0 else trace_x
|
|
142
|
+
norm_trace_y = (trace_y - ylim[0]) / y_range if y_range > 0 else trace_y
|
|
143
|
+
|
|
144
|
+
# Calculate distances to all points
|
|
145
|
+
distances = np.sqrt(
|
|
146
|
+
(norm_trace_x - norm_click_x) ** 2 + (norm_trace_y - norm_click_y) ** 2
|
|
147
|
+
)
|
|
148
|
+
min_trace_dist = np.min(distances)
|
|
149
|
+
|
|
150
|
+
if min_trace_dist < min_dist:
|
|
151
|
+
min_dist = min_trace_dist
|
|
152
|
+
nearest_idx = i
|
|
153
|
+
|
|
154
|
+
# Only select if close enough (threshold in normalized space)
|
|
155
|
+
if min_dist < 0.1: # 10% of plot area
|
|
156
|
+
return nearest_idx
|
|
157
|
+
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def select_element(state: "EditorState", element: Dict, dpg) -> None:
|
|
162
|
+
"""Select an element and show appropriate controls."""
|
|
163
|
+
from ._rendering import update_preview
|
|
164
|
+
|
|
165
|
+
state.selected_element = element
|
|
166
|
+
elem_type = element.get("type")
|
|
167
|
+
elem_idx = element.get("index")
|
|
168
|
+
|
|
169
|
+
# Hide all control groups first
|
|
170
|
+
dpg.configure_item("trace_controls_group", show=False)
|
|
171
|
+
dpg.configure_item("text_controls_group", show=False)
|
|
172
|
+
dpg.configure_item("axis_controls_group", show=False)
|
|
173
|
+
dpg.configure_item("legend_controls_group", show=False)
|
|
174
|
+
|
|
175
|
+
# Update combo selection
|
|
176
|
+
if elem_type == "trace":
|
|
177
|
+
_select_trace(state, elem_idx, dpg)
|
|
178
|
+
elif elem_type in ("title", "xlabel", "ylabel"):
|
|
179
|
+
_select_text_element(state, elem_type, dpg)
|
|
180
|
+
elif elem_type in ("xaxis", "yaxis"):
|
|
181
|
+
_select_axis_element(state, elem_type, dpg)
|
|
182
|
+
elif elem_type == "legend":
|
|
183
|
+
_select_legend(state, dpg)
|
|
184
|
+
|
|
185
|
+
# Redraw with highlight
|
|
186
|
+
update_preview(state, dpg)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _select_trace(state: "EditorState", trace_idx: Optional[int], dpg) -> None:
|
|
190
|
+
"""Handle trace selection."""
|
|
191
|
+
traces = state.current_overrides.get("traces", [])
|
|
192
|
+
if trace_idx is not None and trace_idx < len(traces):
|
|
193
|
+
trace = traces[trace_idx]
|
|
194
|
+
label = f"Trace: {trace.get('label', trace.get('id', f'Trace {trace_idx}'))}"
|
|
195
|
+
dpg.set_value("element_selector_combo", label)
|
|
196
|
+
|
|
197
|
+
# Show trace controls and populate
|
|
198
|
+
dpg.configure_item("trace_controls_group", show=True)
|
|
199
|
+
state.selected_trace_index = trace_idx
|
|
200
|
+
dpg.set_value("trace_label_input", trace.get("label", ""))
|
|
201
|
+
|
|
202
|
+
color_hex = trace.get("color", "#0080bf")
|
|
203
|
+
try:
|
|
204
|
+
r = int(color_hex[1:3], 16)
|
|
205
|
+
g = int(color_hex[3:5], 16)
|
|
206
|
+
b = int(color_hex[5:7], 16)
|
|
207
|
+
dpg.set_value("trace_color_picker", [r, g, b])
|
|
208
|
+
except (ValueError, IndexError):
|
|
209
|
+
dpg.set_value("trace_color_picker", [128, 128, 191])
|
|
210
|
+
|
|
211
|
+
dpg.set_value("trace_linewidth_slider", trace.get("linewidth", 1.0))
|
|
212
|
+
dpg.set_value("trace_linestyle_combo", trace.get("linestyle", "-"))
|
|
213
|
+
dpg.set_value("trace_marker_combo", trace.get("marker", "") or "")
|
|
214
|
+
dpg.set_value("trace_markersize_slider", trace.get("markersize", 6.0))
|
|
215
|
+
|
|
216
|
+
dpg.set_value(
|
|
217
|
+
"selection_text",
|
|
218
|
+
f"Selected: {trace.get('label', f'Trace {trace_idx}')}",
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _select_text_element(state: "EditorState", elem_type: str, dpg) -> None:
|
|
223
|
+
"""Handle text element selection (title, xlabel, ylabel)."""
|
|
224
|
+
dpg.set_value(
|
|
225
|
+
"element_selector_combo",
|
|
226
|
+
elem_type.replace("x", "X ").replace("y", "Y ").title(),
|
|
227
|
+
)
|
|
228
|
+
dpg.configure_item("text_controls_group", show=True)
|
|
229
|
+
|
|
230
|
+
o = state.current_overrides
|
|
231
|
+
if elem_type == "title":
|
|
232
|
+
dpg.set_value("element_text_input", o.get("title", ""))
|
|
233
|
+
dpg.set_value("element_fontsize_slider", o.get("title_fontsize", 8))
|
|
234
|
+
elif elem_type == "xlabel":
|
|
235
|
+
dpg.set_value("element_text_input", o.get("xlabel", ""))
|
|
236
|
+
dpg.set_value("element_fontsize_slider", o.get("axis_fontsize", 7))
|
|
237
|
+
elif elem_type == "ylabel":
|
|
238
|
+
dpg.set_value("element_text_input", o.get("ylabel", ""))
|
|
239
|
+
dpg.set_value("element_fontsize_slider", o.get("axis_fontsize", 7))
|
|
240
|
+
|
|
241
|
+
dpg.set_value("selection_text", f"Selected: {elem_type.title()}")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _select_axis_element(state: "EditorState", elem_type: str, dpg) -> None:
|
|
245
|
+
"""Handle axis element selection (xaxis, yaxis)."""
|
|
246
|
+
label = "X Axis" if elem_type == "xaxis" else "Y Axis"
|
|
247
|
+
dpg.set_value("element_selector_combo", label)
|
|
248
|
+
dpg.configure_item("axis_controls_group", show=True)
|
|
249
|
+
|
|
250
|
+
o = state.current_overrides
|
|
251
|
+
dpg.set_value("axis_linewidth_slider", o.get("axis_width", 0.2))
|
|
252
|
+
dpg.set_value("axis_tick_length_slider", o.get("tick_length", 0.8))
|
|
253
|
+
dpg.set_value("axis_tick_fontsize_slider", o.get("tick_fontsize", 7))
|
|
254
|
+
|
|
255
|
+
if elem_type == "xaxis":
|
|
256
|
+
dpg.set_value("axis_show_spine_checkbox", not o.get("hide_bottom_spine", False))
|
|
257
|
+
else:
|
|
258
|
+
dpg.set_value("axis_show_spine_checkbox", not o.get("hide_left_spine", False))
|
|
259
|
+
|
|
260
|
+
dpg.set_value("selection_text", f"Selected: {label}")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _select_legend(state: "EditorState", dpg) -> None:
|
|
264
|
+
"""Handle legend selection."""
|
|
265
|
+
dpg.set_value("element_selector_combo", "Legend")
|
|
266
|
+
dpg.configure_item("legend_controls_group", show=True)
|
|
267
|
+
|
|
268
|
+
o = state.current_overrides
|
|
269
|
+
dpg.set_value("legend_visible_edit", o.get("legend_visible", True))
|
|
270
|
+
dpg.set_value("legend_frameon_edit", o.get("legend_frameon", False))
|
|
271
|
+
dpg.set_value("legend_loc_edit", o.get("legend_loc", "best"))
|
|
272
|
+
dpg.set_value("legend_fontsize_edit", o.get("legend_fontsize", 6))
|
|
273
|
+
|
|
274
|
+
dpg.set_value("selection_text", "Selected: Legend")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def deselect_element(state: "EditorState", dpg) -> None:
|
|
278
|
+
"""Deselect the current element."""
|
|
279
|
+
from ._rendering import update_preview
|
|
280
|
+
|
|
281
|
+
state.selected_element = None
|
|
282
|
+
state.selected_trace_index = None
|
|
283
|
+
|
|
284
|
+
# Hide all control groups
|
|
285
|
+
dpg.configure_item("trace_controls_group", show=False)
|
|
286
|
+
dpg.configure_item("text_controls_group", show=False)
|
|
287
|
+
dpg.configure_item("axis_controls_group", show=False)
|
|
288
|
+
dpg.configure_item("legend_controls_group", show=False)
|
|
289
|
+
|
|
290
|
+
dpg.set_value("selection_text", "")
|
|
291
|
+
dpg.set_value("element_selector_combo", "")
|
|
292
|
+
update_preview(state, dpg)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# EOF
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/_dearpygui/_state.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Editor state management for DearPyGui editor.
|
|
7
|
+
|
|
8
|
+
Provides EditorState dataclass to hold all editor state.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import copy
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class EditorState:
|
|
19
|
+
"""Holds all state for the DearPyGui editor."""
|
|
20
|
+
|
|
21
|
+
# Core data
|
|
22
|
+
json_path: Path
|
|
23
|
+
metadata: Dict[str, Any]
|
|
24
|
+
csv_data: Optional[Any] = None
|
|
25
|
+
png_path: Optional[Path] = None
|
|
26
|
+
manual_overrides: Dict[str, Any] = field(default_factory=dict)
|
|
27
|
+
|
|
28
|
+
# Defaults
|
|
29
|
+
scitex_defaults: Dict[str, Any] = field(default_factory=dict)
|
|
30
|
+
metadata_defaults: Dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
current_overrides: Dict[str, Any] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
# Modification tracking
|
|
34
|
+
initial_overrides: Dict[str, Any] = field(default_factory=dict)
|
|
35
|
+
user_modified: bool = False
|
|
36
|
+
texture_id: Optional[int] = None
|
|
37
|
+
|
|
38
|
+
# Selection state
|
|
39
|
+
selected_element: Optional[Dict[str, Any]] = None
|
|
40
|
+
selected_trace_index: Optional[int] = None
|
|
41
|
+
|
|
42
|
+
# Preview bounds
|
|
43
|
+
preview_bounds: Optional[Tuple[int, int, int, int]] = None
|
|
44
|
+
axes_transform: Optional[Tuple] = None
|
|
45
|
+
element_bboxes: Dict[str, Tuple[int, int, int, int]] = field(default_factory=dict)
|
|
46
|
+
element_bboxes_raw: Dict[str, Tuple] = field(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
# Hover state
|
|
49
|
+
hovered_element: Optional[Dict[str, Any]] = None
|
|
50
|
+
last_hover_check: float = 0
|
|
51
|
+
backend_name: str = "dearpygui"
|
|
52
|
+
|
|
53
|
+
# Cached rendering
|
|
54
|
+
cached_base_image: Optional[Any] = None
|
|
55
|
+
cached_base_data: Optional[List[float]] = None
|
|
56
|
+
cache_dirty: bool = True
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def create(
|
|
60
|
+
cls,
|
|
61
|
+
json_path: Path,
|
|
62
|
+
metadata: Dict[str, Any],
|
|
63
|
+
csv_data: Optional[Any] = None,
|
|
64
|
+
png_path: Optional[Path] = None,
|
|
65
|
+
manual_overrides: Optional[Dict[str, Any]] = None,
|
|
66
|
+
) -> "EditorState":
|
|
67
|
+
"""Create an EditorState with properly initialized defaults."""
|
|
68
|
+
from .._defaults import extract_defaults_from_metadata, get_scitex_defaults
|
|
69
|
+
|
|
70
|
+
state = cls(
|
|
71
|
+
json_path=Path(json_path),
|
|
72
|
+
metadata=metadata,
|
|
73
|
+
csv_data=csv_data,
|
|
74
|
+
png_path=Path(png_path) if png_path else None,
|
|
75
|
+
manual_overrides=manual_overrides or {},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Get SciTeX defaults and merge with metadata
|
|
79
|
+
state.scitex_defaults = get_scitex_defaults()
|
|
80
|
+
state.metadata_defaults = extract_defaults_from_metadata(metadata)
|
|
81
|
+
|
|
82
|
+
# Start with defaults, then overlay manual overrides
|
|
83
|
+
state.current_overrides = copy.deepcopy(state.scitex_defaults)
|
|
84
|
+
state.current_overrides.update(state.metadata_defaults)
|
|
85
|
+
state.current_overrides.update(state.manual_overrides)
|
|
86
|
+
|
|
87
|
+
# Track modifications
|
|
88
|
+
state.initial_overrides = copy.deepcopy(state.current_overrides)
|
|
89
|
+
|
|
90
|
+
return state
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# EOF
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/_dearpygui/_utils.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Utility functions for DearPyGui editor.
|
|
7
|
+
|
|
8
|
+
Provides helper functions like checkerboard pattern creation for transparency preview.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from PIL import Image
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_checkerboard(width: int, height: int, square_size: int = 10) -> "Image":
|
|
18
|
+
"""Create a checkerboard pattern image for transparency preview.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
width : int
|
|
23
|
+
Image width in pixels
|
|
24
|
+
height : int
|
|
25
|
+
Image height in pixels
|
|
26
|
+
square_size : int
|
|
27
|
+
Size of each checkerboard square (default: 10)
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
PIL.Image
|
|
32
|
+
RGBA image with checkerboard pattern (light/dark gray)
|
|
33
|
+
"""
|
|
34
|
+
import numpy as np
|
|
35
|
+
from PIL import Image
|
|
36
|
+
|
|
37
|
+
# Create checkerboard pattern
|
|
38
|
+
light_gray = (220, 220, 220, 255)
|
|
39
|
+
dark_gray = (180, 180, 180, 255)
|
|
40
|
+
|
|
41
|
+
# Create array
|
|
42
|
+
img_array = np.zeros((height, width, 4), dtype=np.uint8)
|
|
43
|
+
|
|
44
|
+
for y in range(height):
|
|
45
|
+
for x in range(width):
|
|
46
|
+
# Determine which square we're in
|
|
47
|
+
square_x = x // square_size
|
|
48
|
+
square_y = y // square_size
|
|
49
|
+
if (square_x + square_y) % 2 == 0:
|
|
50
|
+
img_array[y, x] = light_gray
|
|
51
|
+
else:
|
|
52
|
+
img_array[y, x] = dark_gray
|
|
53
|
+
|
|
54
|
+
return Image.fromarray(img_array, "RGBA")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# mm to pt conversion factor
|
|
58
|
+
MM_TO_PT = 2.83465
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# EOF
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/flask_editor/_core/__init__.py
|
|
4
|
+
|
|
5
|
+
"""Core WebEditor package for Flask-based figure editing.
|
|
6
|
+
|
|
7
|
+
This package provides the WebEditor class and supporting modules for
|
|
8
|
+
browser-based figure editing functionality.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from ._bbox_extraction import extract_bboxes_from_metadata
|
|
12
|
+
from ._editor import WebEditor
|
|
13
|
+
from ._export_helpers import compose_panels_to_figure, export_composed_figure
|
|
14
|
+
|
|
15
|
+
# Backward compatibility alias
|
|
16
|
+
_extract_bboxes_from_metadata = extract_bboxes_from_metadata
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"WebEditor",
|
|
20
|
+
"extract_bboxes_from_metadata",
|
|
21
|
+
"export_composed_figure",
|
|
22
|
+
"compose_panels_to_figure",
|
|
23
|
+
"_extract_bboxes_from_metadata",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# EOF
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py
|
|
4
|
+
|
|
5
|
+
"""Bounding box extraction from pltz metadata."""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
__all__ = ["extract_bboxes_from_metadata"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def extract_bboxes_from_metadata(
|
|
13
|
+
metadata: Dict[str, Any],
|
|
14
|
+
display_width: Optional[float] = None,
|
|
15
|
+
display_height: Optional[float] = None,
|
|
16
|
+
) -> Dict[str, Any]:
|
|
17
|
+
"""Extract element bounding boxes from pltz metadata.
|
|
18
|
+
|
|
19
|
+
Builds bboxes from selectable_regions in the metadata for click detection.
|
|
20
|
+
This allows the editor to highlight elements when clicked.
|
|
21
|
+
|
|
22
|
+
Coordinate system (new layered format):
|
|
23
|
+
- selectable_regions bbox_px: Already in final image space (figure_px)
|
|
24
|
+
- Display size: Actual displayed image size (PNG pixels or SVG viewBox)
|
|
25
|
+
- Scale = display_size / figure_px (usually 1:1, but may differ for scaled display)
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
metadata : dict
|
|
30
|
+
The pltz JSON metadata containing selectable_regions
|
|
31
|
+
display_width : float, optional
|
|
32
|
+
Actual display image width (from PNG size or SVG viewBox)
|
|
33
|
+
display_height : float, optional
|
|
34
|
+
Actual display image height (from PNG size or SVG viewBox)
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
dict
|
|
39
|
+
Mapping of element IDs to their bounding box coordinates (in display pixels)
|
|
40
|
+
"""
|
|
41
|
+
bboxes = {}
|
|
42
|
+
selectable = metadata.get("selectable_regions", {})
|
|
43
|
+
|
|
44
|
+
# Figure dimensions from new layered format (bbox_px are in this space)
|
|
45
|
+
figure_px = metadata.get("figure_px", [])
|
|
46
|
+
if isinstance(figure_px, list) and len(figure_px) >= 2:
|
|
47
|
+
fig_width = figure_px[0]
|
|
48
|
+
fig_height = figure_px[1]
|
|
49
|
+
else:
|
|
50
|
+
# Fallback for old format: try hit_regions.path_data.figure
|
|
51
|
+
hit_regions = metadata.get("hit_regions", {})
|
|
52
|
+
path_data = hit_regions.get("path_data", {})
|
|
53
|
+
orig_fig = path_data.get("figure", {})
|
|
54
|
+
fig_width = orig_fig.get("width_px", 944)
|
|
55
|
+
fig_height = orig_fig.get("height_px", 803)
|
|
56
|
+
|
|
57
|
+
# Use actual display dimensions if provided, else use figure_px
|
|
58
|
+
if display_width is None:
|
|
59
|
+
display_width = fig_width
|
|
60
|
+
if display_height is None:
|
|
61
|
+
display_height = fig_height
|
|
62
|
+
|
|
63
|
+
# Scale factor: display / figure_px
|
|
64
|
+
scale_x = display_width / fig_width if fig_width > 0 else 1
|
|
65
|
+
scale_y = display_height / fig_height if fig_height > 0 else 1
|
|
66
|
+
|
|
67
|
+
def to_display_bbox(bbox, is_list=True):
|
|
68
|
+
"""Convert bbox to display pixels."""
|
|
69
|
+
if is_list:
|
|
70
|
+
x0, y0, x1, y1 = bbox[0], bbox[1], bbox[2], bbox[3]
|
|
71
|
+
else:
|
|
72
|
+
x0 = bbox.get("x0", 0)
|
|
73
|
+
y0 = bbox.get("y0", 0)
|
|
74
|
+
x1 = bbox.get("x1", bbox.get("x0", 0) + bbox.get("width", 0))
|
|
75
|
+
y1 = bbox.get("y1", bbox.get("y0", 0) + bbox.get("height", 0))
|
|
76
|
+
|
|
77
|
+
disp_x0 = x0 * scale_x
|
|
78
|
+
disp_x1 = x1 * scale_x
|
|
79
|
+
disp_y0 = y0 * scale_y
|
|
80
|
+
disp_y1 = y1 * scale_y
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
"x0": disp_x0,
|
|
84
|
+
"y0": disp_y0,
|
|
85
|
+
"x1": disp_x1,
|
|
86
|
+
"y1": disp_y1,
|
|
87
|
+
"x": disp_x0,
|
|
88
|
+
"y": disp_y0,
|
|
89
|
+
"width": disp_x1 - disp_x0,
|
|
90
|
+
"height": disp_y1 - disp_y0,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Extract from selectable_regions.axes
|
|
94
|
+
axes_regions = selectable.get("axes", [])
|
|
95
|
+
for ax_idx, ax in enumerate(axes_regions):
|
|
96
|
+
ax_key = f"ax_{ax_idx:02d}"
|
|
97
|
+
|
|
98
|
+
# Title
|
|
99
|
+
title = ax.get("title", {})
|
|
100
|
+
if title and "bbox_px" in title:
|
|
101
|
+
bbox_disp = to_display_bbox(title["bbox_px"])
|
|
102
|
+
bboxes[f"{ax_key}_title"] = {
|
|
103
|
+
**bbox_disp,
|
|
104
|
+
"type": "title",
|
|
105
|
+
"text": title.get("text", ""),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# X label
|
|
109
|
+
xlabel = ax.get("xlabel", {})
|
|
110
|
+
if xlabel and "bbox_px" in xlabel:
|
|
111
|
+
bbox_disp = to_display_bbox(xlabel["bbox_px"])
|
|
112
|
+
bboxes[f"{ax_key}_xlabel"] = {
|
|
113
|
+
**bbox_disp,
|
|
114
|
+
"type": "xlabel",
|
|
115
|
+
"text": xlabel.get("text", ""),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Y label
|
|
119
|
+
ylabel = ax.get("ylabel", {})
|
|
120
|
+
if ylabel and "bbox_px" in ylabel:
|
|
121
|
+
bbox_disp = to_display_bbox(ylabel["bbox_px"])
|
|
122
|
+
bboxes[f"{ax_key}_ylabel"] = {
|
|
123
|
+
**bbox_disp,
|
|
124
|
+
"type": "ylabel",
|
|
125
|
+
"text": ylabel.get("text", ""),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Legend
|
|
129
|
+
legend = ax.get("legend", {})
|
|
130
|
+
if legend and "bbox_px" in legend:
|
|
131
|
+
bbox_disp = to_display_bbox(legend["bbox_px"])
|
|
132
|
+
bboxes[f"{ax_key}_legend"] = {
|
|
133
|
+
**bbox_disp,
|
|
134
|
+
"type": "legend",
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# X-axis spine
|
|
138
|
+
xaxis = ax.get("xaxis", {})
|
|
139
|
+
if xaxis:
|
|
140
|
+
spine = xaxis.get("spine", {})
|
|
141
|
+
if spine and "bbox_px" in spine:
|
|
142
|
+
bbox_disp = to_display_bbox(spine["bbox_px"])
|
|
143
|
+
bboxes[f"{ax_key}_xaxis_spine"] = {
|
|
144
|
+
**bbox_disp,
|
|
145
|
+
"type": "xaxis",
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Y-axis spine
|
|
149
|
+
yaxis = ax.get("yaxis", {})
|
|
150
|
+
if yaxis:
|
|
151
|
+
spine = yaxis.get("spine", {})
|
|
152
|
+
if spine and "bbox_px" in spine:
|
|
153
|
+
bbox_disp = to_display_bbox(spine["bbox_px"])
|
|
154
|
+
bboxes[f"{ax_key}_yaxis_spine"] = {
|
|
155
|
+
**bbox_disp,
|
|
156
|
+
"type": "yaxis",
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Extract traces from artists
|
|
160
|
+
artists = metadata.get("artists", [])
|
|
161
|
+
if not artists:
|
|
162
|
+
hit_regions = metadata.get("hit_regions", {})
|
|
163
|
+
path_data = hit_regions.get("path_data", {})
|
|
164
|
+
artists = path_data.get("artists", [])
|
|
165
|
+
|
|
166
|
+
for artist in artists:
|
|
167
|
+
artist_id = artist.get("id", 0)
|
|
168
|
+
artist_type = artist.get("type", "line")
|
|
169
|
+
bbox_px = artist.get("bbox_px", {})
|
|
170
|
+
if bbox_px:
|
|
171
|
+
bbox_disp = to_display_bbox(bbox_px, is_list=False)
|
|
172
|
+
trace_entry = {
|
|
173
|
+
**bbox_disp,
|
|
174
|
+
"type": artist_type,
|
|
175
|
+
"label": artist.get("label", f"Trace {artist_id}"),
|
|
176
|
+
"element_type": artist_type,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
path_px = artist.get("path_px", [])
|
|
180
|
+
if path_px:
|
|
181
|
+
scaled_points = [
|
|
182
|
+
[pt[0] * scale_x, pt[1] * scale_y] for pt in path_px if len(pt) >= 2
|
|
183
|
+
]
|
|
184
|
+
trace_entry["points"] = scaled_points
|
|
185
|
+
|
|
186
|
+
bboxes[f"trace_{artist_id}"] = trace_entry
|
|
187
|
+
|
|
188
|
+
bboxes["_meta"] = {
|
|
189
|
+
"display_width": display_width,
|
|
190
|
+
"display_height": display_height,
|
|
191
|
+
"figure_px_width": fig_width,
|
|
192
|
+
"figure_px_height": fig_height,
|
|
193
|
+
"scale_x": scale_x,
|
|
194
|
+
"scale_y": scale_y,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return bboxes
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# EOF
|