scitex 2.14.0__py3-none-any.whl → 2.15.1__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 +47 -0
- 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 +191 -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/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/audio/README.md +40 -36
- scitex/audio/__init__.py +127 -59
- 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/engines/elevenlabs_engine.py +6 -1
- 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/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 +443 -0
- scitex/cli/main.py +198 -109
- scitex/cli/mcp.py +60 -34
- scitex/cli/scholar/__init__.py +8 -0
- scitex/cli/scholar/_crossref_scitex.py +296 -0
- scitex/cli/scholar/_fetch.py +25 -3
- scitex/cli/social.py +314 -0
- scitex/cli/writer.py +117 -0
- scitex/config/README.md +1 -1
- scitex/config/__init__.py +16 -2
- scitex/config/_env_registry.py +191 -0
- scitex/diagram/__init__.py +42 -19
- scitex/diagram/mcp_server.py +13 -125
- scitex/introspect/__init__.py +75 -0
- scitex/introspect/_call_graph.py +303 -0
- scitex/introspect/_class_hierarchy.py +163 -0
- scitex/introspect/_core.py +42 -0
- scitex/introspect/_docstring.py +131 -0
- scitex/introspect/_examples.py +113 -0
- scitex/introspect/_imports.py +271 -0
- scitex/introspect/_mcp/__init__.py +37 -0
- scitex/introspect/_mcp/handlers.py +208 -0
- scitex/introspect/_members.py +151 -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/bundle/README.md +1 -1
- scitex/mcp_server.py +98 -5
- scitex/plt/__init__.py +248 -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/sh/README.md +1 -1
- scitex/social/__init__.py +153 -0
- scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/template/README.md +1 -1
- scitex/template/clone_writer_directory.py +5 -5
- scitex/writer/README.md +1 -1
- scitex/writer/_mcp/handlers.py +11 -744
- scitex/writer/_mcp/tool_schemas.py +5 -335
- scitex-2.15.1.dist-info/METADATA +648 -0
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/RECORD +166 -111
- 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/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-2.14.0.dist-info → scitex-2.15.1.dist-info}/WHEEL +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/entry_points.txt +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/_dearpygui/_editor.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
DearPyGui-based figure editor with GPU-accelerated rendering.
|
|
7
|
+
|
|
8
|
+
Thin orchestrator class that delegates to modular components.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
from ._state import EditorState
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DearPyGuiEditor:
|
|
18
|
+
"""
|
|
19
|
+
GPU-accelerated figure editor using DearPyGui.
|
|
20
|
+
|
|
21
|
+
Features:
|
|
22
|
+
- Modern immediate-mode GUI with GPU acceleration
|
|
23
|
+
- Real-time figure preview
|
|
24
|
+
- Property editors with sliders, color pickers, and input fields
|
|
25
|
+
- Click-to-select traces on preview
|
|
26
|
+
- Save to .manual.json
|
|
27
|
+
- SciTeX style defaults pre-filled
|
|
28
|
+
- Dark/light theme support
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
json_path: Path,
|
|
34
|
+
metadata: Dict[str, Any],
|
|
35
|
+
csv_data: Optional[Any] = None,
|
|
36
|
+
png_path: Optional[Path] = None,
|
|
37
|
+
manual_overrides: Optional[Dict[str, Any]] = None,
|
|
38
|
+
):
|
|
39
|
+
"""Initialize the DearPyGui editor.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
json_path : Path
|
|
44
|
+
Path to the JSON metadata file
|
|
45
|
+
metadata : dict
|
|
46
|
+
Figure metadata dictionary
|
|
47
|
+
csv_data : pd.DataFrame, optional
|
|
48
|
+
CSV data for plotting
|
|
49
|
+
png_path : Path, optional
|
|
50
|
+
Path to the PNG file
|
|
51
|
+
manual_overrides : dict, optional
|
|
52
|
+
Manual override settings
|
|
53
|
+
"""
|
|
54
|
+
self.state = EditorState.create(
|
|
55
|
+
json_path=json_path,
|
|
56
|
+
metadata=metadata,
|
|
57
|
+
csv_data=csv_data,
|
|
58
|
+
png_path=png_path,
|
|
59
|
+
manual_overrides=manual_overrides,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Backward compatibility properties
|
|
63
|
+
self._texture_id = None
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def json_path(self) -> Path:
|
|
67
|
+
"""Get JSON path."""
|
|
68
|
+
return self.state.json_path
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def metadata(self) -> Dict[str, Any]:
|
|
72
|
+
"""Get metadata."""
|
|
73
|
+
return self.state.metadata
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def csv_data(self) -> Optional[Any]:
|
|
77
|
+
"""Get CSV data."""
|
|
78
|
+
return self.state.csv_data
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def current_overrides(self) -> Dict[str, Any]:
|
|
82
|
+
"""Get current overrides."""
|
|
83
|
+
return self.state.current_overrides
|
|
84
|
+
|
|
85
|
+
@current_overrides.setter
|
|
86
|
+
def current_overrides(self, value: Dict[str, Any]) -> None:
|
|
87
|
+
"""Set current overrides."""
|
|
88
|
+
self.state.current_overrides = value
|
|
89
|
+
|
|
90
|
+
def run(self):
|
|
91
|
+
"""Launch the DearPyGui editor."""
|
|
92
|
+
try:
|
|
93
|
+
import dearpygui.dearpygui as dpg
|
|
94
|
+
except ImportError:
|
|
95
|
+
raise ImportError(
|
|
96
|
+
"DearPyGui is required for this editor. "
|
|
97
|
+
"Install with: pip install dearpygui"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
from ._panels import create_control_panel, create_preview_panel
|
|
101
|
+
from ._rendering import update_preview
|
|
102
|
+
|
|
103
|
+
dpg.create_context()
|
|
104
|
+
|
|
105
|
+
# Configure viewport
|
|
106
|
+
dpg.create_viewport(
|
|
107
|
+
title=f"SciTeX Editor ({self.state.backend_name}) - {self.state.json_path.name}",
|
|
108
|
+
width=1400,
|
|
109
|
+
height=900,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Create texture registry for image preview
|
|
113
|
+
with dpg.texture_registry(show=False):
|
|
114
|
+
# Create initial texture with placeholder
|
|
115
|
+
width, height = 800, 600
|
|
116
|
+
texture_data = [0.2, 0.2, 0.2, 1.0] * (width * height)
|
|
117
|
+
self._texture_id = dpg.add_dynamic_texture(
|
|
118
|
+
width=width,
|
|
119
|
+
height=height,
|
|
120
|
+
default_value=texture_data,
|
|
121
|
+
tag="preview_texture",
|
|
122
|
+
)
|
|
123
|
+
self.state.texture_id = self._texture_id
|
|
124
|
+
|
|
125
|
+
# Create main window
|
|
126
|
+
with dpg.window(label="SciTeX Figure Editor", tag="main_window"):
|
|
127
|
+
with dpg.group(horizontal=True):
|
|
128
|
+
# Left panel: Preview
|
|
129
|
+
create_preview_panel(self.state, dpg)
|
|
130
|
+
|
|
131
|
+
# Right panel: Controls
|
|
132
|
+
create_control_panel(self.state, dpg)
|
|
133
|
+
|
|
134
|
+
# Set main window as primary
|
|
135
|
+
dpg.set_primary_window("main_window", True)
|
|
136
|
+
|
|
137
|
+
# Initial render
|
|
138
|
+
update_preview(self.state, dpg)
|
|
139
|
+
|
|
140
|
+
# Setup and show
|
|
141
|
+
dpg.setup_dearpygui()
|
|
142
|
+
dpg.show_viewport()
|
|
143
|
+
dpg.start_dearpygui()
|
|
144
|
+
dpg.destroy_context()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# EOF
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/_dearpygui/_handlers.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Event handlers for DearPyGui editor.
|
|
7
|
+
|
|
8
|
+
Handles all callbacks and user interactions.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import copy
|
|
12
|
+
import time
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ._state import EditorState
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def on_value_change(state: "EditorState", dpg) -> None:
|
|
20
|
+
"""Handle value changes from widgets."""
|
|
21
|
+
from ._rendering import update_preview
|
|
22
|
+
|
|
23
|
+
state.user_modified = True
|
|
24
|
+
collect_overrides(state, dpg)
|
|
25
|
+
update_preview(state, dpg)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def collect_overrides(state: "EditorState", dpg) -> None:
|
|
29
|
+
"""Collect current values from all widgets."""
|
|
30
|
+
o = state.current_overrides
|
|
31
|
+
|
|
32
|
+
# Labels
|
|
33
|
+
o["title"] = dpg.get_value("title_input")
|
|
34
|
+
o["xlabel"] = dpg.get_value("xlabel_input")
|
|
35
|
+
o["ylabel"] = dpg.get_value("ylabel_input")
|
|
36
|
+
|
|
37
|
+
# Line style
|
|
38
|
+
o["linewidth"] = dpg.get_value("linewidth_slider")
|
|
39
|
+
|
|
40
|
+
# Font settings
|
|
41
|
+
o["title_fontsize"] = dpg.get_value("title_fontsize_slider")
|
|
42
|
+
o["axis_fontsize"] = dpg.get_value("axis_fontsize_slider")
|
|
43
|
+
o["tick_fontsize"] = dpg.get_value("tick_fontsize_slider")
|
|
44
|
+
o["legend_fontsize"] = dpg.get_value("legend_fontsize_slider")
|
|
45
|
+
|
|
46
|
+
# Tick settings
|
|
47
|
+
o["n_ticks"] = dpg.get_value("n_ticks_slider")
|
|
48
|
+
o["tick_length"] = dpg.get_value("tick_length_slider")
|
|
49
|
+
o["tick_width"] = dpg.get_value("tick_width_slider")
|
|
50
|
+
o["tick_direction"] = dpg.get_value("tick_direction_combo")
|
|
51
|
+
|
|
52
|
+
# Style
|
|
53
|
+
o["grid"] = dpg.get_value("grid_checkbox")
|
|
54
|
+
o["hide_top_spine"] = dpg.get_value("hide_top_spine_checkbox")
|
|
55
|
+
o["hide_right_spine"] = dpg.get_value("hide_right_spine_checkbox")
|
|
56
|
+
o["transparent"] = dpg.get_value("transparent_checkbox")
|
|
57
|
+
o["axis_width"] = dpg.get_value("axis_width_slider")
|
|
58
|
+
|
|
59
|
+
# Legend
|
|
60
|
+
o["legend_visible"] = dpg.get_value("legend_visible_checkbox")
|
|
61
|
+
o["legend_frameon"] = dpg.get_value("legend_frameon_checkbox")
|
|
62
|
+
o["legend_loc"] = dpg.get_value("legend_loc_combo")
|
|
63
|
+
|
|
64
|
+
# Dimensions
|
|
65
|
+
o["fig_size"] = [
|
|
66
|
+
dpg.get_value("fig_width_input"),
|
|
67
|
+
dpg.get_value("fig_height_input"),
|
|
68
|
+
]
|
|
69
|
+
o["dpi"] = dpg.get_value("dpi_slider")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def on_apply_limits(state: "EditorState", dpg) -> None:
|
|
73
|
+
"""Apply axis limits."""
|
|
74
|
+
from ._rendering import update_preview
|
|
75
|
+
|
|
76
|
+
xmin = dpg.get_value("xmin_input")
|
|
77
|
+
xmax = dpg.get_value("xmax_input")
|
|
78
|
+
ymin = dpg.get_value("ymin_input")
|
|
79
|
+
ymax = dpg.get_value("ymax_input")
|
|
80
|
+
|
|
81
|
+
if xmin < xmax:
|
|
82
|
+
state.current_overrides["xlim"] = [xmin, xmax]
|
|
83
|
+
if ymin < ymax:
|
|
84
|
+
state.current_overrides["ylim"] = [ymin, ymax]
|
|
85
|
+
|
|
86
|
+
state.user_modified = True
|
|
87
|
+
update_preview(state, dpg)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def on_add_annotation(state: "EditorState", dpg) -> None:
|
|
91
|
+
"""Add text annotation."""
|
|
92
|
+
from ._rendering import update_preview
|
|
93
|
+
|
|
94
|
+
text = dpg.get_value("annot_text_input")
|
|
95
|
+
if not text:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
x = dpg.get_value("annot_x_input")
|
|
99
|
+
y = dpg.get_value("annot_y_input")
|
|
100
|
+
|
|
101
|
+
if "annotations" not in state.current_overrides:
|
|
102
|
+
state.current_overrides["annotations"] = []
|
|
103
|
+
|
|
104
|
+
state.current_overrides["annotations"].append(
|
|
105
|
+
{
|
|
106
|
+
"type": "text",
|
|
107
|
+
"text": text,
|
|
108
|
+
"x": x,
|
|
109
|
+
"y": y,
|
|
110
|
+
"fontsize": state.current_overrides.get("axis_fontsize", 7),
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
dpg.set_value("annot_text_input", "")
|
|
115
|
+
update_annotations_list(state, dpg)
|
|
116
|
+
state.user_modified = True
|
|
117
|
+
update_preview(state, dpg)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def on_remove_annotation(state: "EditorState", dpg) -> None:
|
|
121
|
+
"""Remove selected annotation."""
|
|
122
|
+
from ._rendering import update_preview
|
|
123
|
+
|
|
124
|
+
selected = dpg.get_value("annotations_listbox")
|
|
125
|
+
annotations = state.current_overrides.get("annotations", [])
|
|
126
|
+
|
|
127
|
+
if selected and annotations:
|
|
128
|
+
# Find index by text
|
|
129
|
+
for i, ann in enumerate(annotations):
|
|
130
|
+
label = (
|
|
131
|
+
f"{ann.get('text', '')[:20]} "
|
|
132
|
+
f"({ann.get('x', 0):.2f}, {ann.get('y', 0):.2f})"
|
|
133
|
+
)
|
|
134
|
+
if label == selected:
|
|
135
|
+
del annotations[i]
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
update_annotations_list(state, dpg)
|
|
139
|
+
state.user_modified = True
|
|
140
|
+
update_preview(state, dpg)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def update_annotations_list(state: "EditorState", dpg) -> None:
|
|
144
|
+
"""Update the annotations listbox."""
|
|
145
|
+
annotations = state.current_overrides.get("annotations", [])
|
|
146
|
+
items = []
|
|
147
|
+
for ann in annotations:
|
|
148
|
+
if ann.get("type") == "text":
|
|
149
|
+
label = (
|
|
150
|
+
f"{ann.get('text', '')[:20]} "
|
|
151
|
+
f"({ann.get('x', 0):.2f}, {ann.get('y', 0):.2f})"
|
|
152
|
+
)
|
|
153
|
+
items.append(label)
|
|
154
|
+
|
|
155
|
+
dpg.configure_item("annotations_listbox", items=items)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def on_preview_click(state: "EditorState", dpg, app_data) -> None:
|
|
159
|
+
"""Handle click on preview image to select element."""
|
|
160
|
+
from ._selection import find_clicked_element, find_nearest_trace, select_element
|
|
161
|
+
|
|
162
|
+
# Only handle left clicks
|
|
163
|
+
if app_data != 0: # 0 = left button
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
# Get mouse position relative to viewport
|
|
167
|
+
mouse_pos = dpg.get_mouse_pos(local=False)
|
|
168
|
+
|
|
169
|
+
# Get preview image position and size
|
|
170
|
+
if not dpg.does_item_exist("preview_image"):
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# Get the image item's position in the window
|
|
174
|
+
img_pos = dpg.get_item_pos("preview_image")
|
|
175
|
+
panel_pos = dpg.get_item_pos("preview_panel")
|
|
176
|
+
|
|
177
|
+
# Calculate click position relative to image
|
|
178
|
+
click_x = mouse_pos[0] - panel_pos[0] - img_pos[0]
|
|
179
|
+
click_y = mouse_pos[1] - panel_pos[1] - img_pos[1]
|
|
180
|
+
|
|
181
|
+
# Check if click is within image bounds (800x600)
|
|
182
|
+
max_width, max_height = 800, 600
|
|
183
|
+
if not (0 <= click_x <= max_width and 0 <= click_y <= max_height):
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
# First check if click is on a fixed element (title, labels, axes, legend)
|
|
187
|
+
element = find_clicked_element(state, click_x, click_y)
|
|
188
|
+
|
|
189
|
+
if element:
|
|
190
|
+
select_element(state, element, dpg)
|
|
191
|
+
else:
|
|
192
|
+
# Fall back to trace selection
|
|
193
|
+
trace_idx = find_nearest_trace(state, click_x, click_y, max_width, max_height)
|
|
194
|
+
if trace_idx is not None:
|
|
195
|
+
select_element(state, {"type": "trace", "index": trace_idx}, dpg)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def on_preview_hover(state: "EditorState", dpg, app_data) -> None:
|
|
199
|
+
"""Handle mouse move for hover effects on preview (optimized with caching)."""
|
|
200
|
+
from ._rendering import update_hover_overlay
|
|
201
|
+
from ._selection import find_clicked_element, find_nearest_trace
|
|
202
|
+
|
|
203
|
+
# Throttle hover updates - reduced to 16ms (~60fps) since we use fast overlay
|
|
204
|
+
current_time = time.time()
|
|
205
|
+
if current_time - state.last_hover_check < 0.016:
|
|
206
|
+
return
|
|
207
|
+
state.last_hover_check = current_time
|
|
208
|
+
|
|
209
|
+
# Get mouse position relative to viewport
|
|
210
|
+
mouse_pos = dpg.get_mouse_pos(local=False)
|
|
211
|
+
|
|
212
|
+
# Get preview image position
|
|
213
|
+
if not dpg.does_item_exist("preview_image"):
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
img_pos = dpg.get_item_pos("preview_image")
|
|
217
|
+
panel_pos = dpg.get_item_pos("preview_panel")
|
|
218
|
+
|
|
219
|
+
# Calculate hover position relative to image
|
|
220
|
+
hover_x = mouse_pos[0] - panel_pos[0] - img_pos[0]
|
|
221
|
+
hover_y = mouse_pos[1] - panel_pos[1] - img_pos[1]
|
|
222
|
+
|
|
223
|
+
# Check if within image bounds
|
|
224
|
+
max_width, max_height = 800, 600
|
|
225
|
+
if not (0 <= hover_x <= max_width and 0 <= hover_y <= max_height):
|
|
226
|
+
if state.hovered_element is not None:
|
|
227
|
+
state.hovered_element = None
|
|
228
|
+
dpg.set_value("hover_text", "")
|
|
229
|
+
# Use fast overlay update instead of full redraw
|
|
230
|
+
update_hover_overlay(state, dpg)
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
# Find element under cursor
|
|
234
|
+
element = find_clicked_element(state, hover_x, hover_y)
|
|
235
|
+
|
|
236
|
+
if element is None:
|
|
237
|
+
# Check for trace hover
|
|
238
|
+
trace_idx = find_nearest_trace(state, hover_x, hover_y, max_width, max_height)
|
|
239
|
+
if trace_idx is not None:
|
|
240
|
+
element = {"type": "trace", "index": trace_idx}
|
|
241
|
+
|
|
242
|
+
# Check if hover changed
|
|
243
|
+
old_hover = state.hovered_element
|
|
244
|
+
if element != old_hover:
|
|
245
|
+
state.hovered_element = element
|
|
246
|
+
if element:
|
|
247
|
+
elem_type = element.get("type", "")
|
|
248
|
+
elem_idx = element.get("index")
|
|
249
|
+
if elem_type == "trace" and elem_idx is not None:
|
|
250
|
+
traces = state.current_overrides.get("traces", [])
|
|
251
|
+
if elem_idx < len(traces):
|
|
252
|
+
label = traces[elem_idx].get("label", f"Trace {elem_idx}")
|
|
253
|
+
dpg.set_value("hover_text", f"Hover: {label} (click to select)")
|
|
254
|
+
else:
|
|
255
|
+
label = elem_type.replace("x", "X ").replace("y", "Y ").title()
|
|
256
|
+
dpg.set_value("hover_text", f"Hover: {label} (click to select)")
|
|
257
|
+
else:
|
|
258
|
+
dpg.set_value("hover_text", "")
|
|
259
|
+
|
|
260
|
+
# Use fast overlay update for hover (no matplotlib re-render)
|
|
261
|
+
update_hover_overlay(state, dpg)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def on_element_selected(state: "EditorState", dpg, app_data) -> None:
|
|
265
|
+
"""Handle element selection from combo box."""
|
|
266
|
+
from ._selection import select_element
|
|
267
|
+
|
|
268
|
+
if app_data == "Title":
|
|
269
|
+
select_element(state, {"type": "title", "index": None}, dpg)
|
|
270
|
+
elif app_data == "X Label":
|
|
271
|
+
select_element(state, {"type": "xlabel", "index": None}, dpg)
|
|
272
|
+
elif app_data == "Y Label":
|
|
273
|
+
select_element(state, {"type": "ylabel", "index": None}, dpg)
|
|
274
|
+
elif app_data == "X Axis":
|
|
275
|
+
select_element(state, {"type": "xaxis", "index": None}, dpg)
|
|
276
|
+
elif app_data == "Y Axis":
|
|
277
|
+
select_element(state, {"type": "yaxis", "index": None}, dpg)
|
|
278
|
+
elif app_data == "Legend":
|
|
279
|
+
select_element(state, {"type": "legend", "index": None}, dpg)
|
|
280
|
+
elif app_data.startswith("Trace: "):
|
|
281
|
+
# Find trace index
|
|
282
|
+
trace_label = app_data[7:] # Remove "Trace: " prefix
|
|
283
|
+
traces = state.current_overrides.get("traces", [])
|
|
284
|
+
for i, t in enumerate(traces):
|
|
285
|
+
if t.get("label", t.get("id", f"Trace {i}")) == trace_label:
|
|
286
|
+
select_element(state, {"type": "trace", "index": i}, dpg)
|
|
287
|
+
break
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def on_text_element_change(state: "EditorState", dpg) -> None:
|
|
291
|
+
"""Handle changes to text element properties."""
|
|
292
|
+
from ._rendering import update_preview
|
|
293
|
+
|
|
294
|
+
if state.selected_element is None:
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
elem_type = state.selected_element.get("type")
|
|
298
|
+
if elem_type not in ("title", "xlabel", "ylabel"):
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
text = dpg.get_value("element_text_input")
|
|
302
|
+
fontsize = dpg.get_value("element_fontsize_slider")
|
|
303
|
+
|
|
304
|
+
if elem_type == "title":
|
|
305
|
+
state.current_overrides["title"] = text
|
|
306
|
+
state.current_overrides["title_fontsize"] = fontsize
|
|
307
|
+
elif elem_type == "xlabel":
|
|
308
|
+
state.current_overrides["xlabel"] = text
|
|
309
|
+
state.current_overrides["axis_fontsize"] = fontsize
|
|
310
|
+
elif elem_type == "ylabel":
|
|
311
|
+
state.current_overrides["ylabel"] = text
|
|
312
|
+
state.current_overrides["axis_fontsize"] = fontsize
|
|
313
|
+
|
|
314
|
+
state.user_modified = True
|
|
315
|
+
update_preview(state, dpg)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def on_axis_element_change(state: "EditorState", dpg) -> None:
|
|
319
|
+
"""Handle changes to axis element properties."""
|
|
320
|
+
from ._rendering import update_preview
|
|
321
|
+
|
|
322
|
+
if state.selected_element is None:
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
elem_type = state.selected_element.get("type")
|
|
326
|
+
if elem_type not in ("xaxis", "yaxis"):
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
state.current_overrides["axis_width"] = dpg.get_value("axis_linewidth_slider")
|
|
330
|
+
state.current_overrides["tick_length"] = dpg.get_value("axis_tick_length_slider")
|
|
331
|
+
state.current_overrides["tick_fontsize"] = dpg.get_value(
|
|
332
|
+
"axis_tick_fontsize_slider"
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
show_spine = dpg.get_value("axis_show_spine_checkbox")
|
|
336
|
+
if elem_type == "xaxis":
|
|
337
|
+
state.current_overrides["hide_bottom_spine"] = not show_spine
|
|
338
|
+
else:
|
|
339
|
+
state.current_overrides["hide_left_spine"] = not show_spine
|
|
340
|
+
|
|
341
|
+
state.user_modified = True
|
|
342
|
+
update_preview(state, dpg)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def on_legend_element_change(state: "EditorState", dpg) -> None:
|
|
346
|
+
"""Handle changes to legend element properties."""
|
|
347
|
+
from ._rendering import update_preview
|
|
348
|
+
|
|
349
|
+
if state.selected_element is None:
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
elem_type = state.selected_element.get("type")
|
|
353
|
+
if elem_type != "legend":
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
state.current_overrides["legend_visible"] = dpg.get_value("legend_visible_edit")
|
|
357
|
+
state.current_overrides["legend_frameon"] = dpg.get_value("legend_frameon_edit")
|
|
358
|
+
state.current_overrides["legend_loc"] = dpg.get_value("legend_loc_edit")
|
|
359
|
+
state.current_overrides["legend_fontsize"] = dpg.get_value("legend_fontsize_edit")
|
|
360
|
+
|
|
361
|
+
state.user_modified = True
|
|
362
|
+
update_preview(state, dpg)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def on_trace_property_change(state: "EditorState", dpg) -> None:
|
|
366
|
+
"""Handle changes to selected trace properties."""
|
|
367
|
+
from ._rendering import update_preview
|
|
368
|
+
|
|
369
|
+
if state.selected_trace_index is None:
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
traces = state.current_overrides.get("traces", [])
|
|
373
|
+
if state.selected_trace_index >= len(traces):
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
trace = traces[state.selected_trace_index]
|
|
377
|
+
|
|
378
|
+
# Update trace properties from widgets
|
|
379
|
+
trace["label"] = dpg.get_value("trace_label_input")
|
|
380
|
+
|
|
381
|
+
# Convert RGB to hex
|
|
382
|
+
color_rgb = dpg.get_value("trace_color_picker")
|
|
383
|
+
if color_rgb and len(color_rgb) >= 3:
|
|
384
|
+
r, g, b = int(color_rgb[0]), int(color_rgb[1]), int(color_rgb[2])
|
|
385
|
+
trace["color"] = f"#{r:02x}{g:02x}{b:02x}"
|
|
386
|
+
|
|
387
|
+
trace["linewidth"] = dpg.get_value("trace_linewidth_slider")
|
|
388
|
+
trace["linestyle"] = dpg.get_value("trace_linestyle_combo")
|
|
389
|
+
|
|
390
|
+
marker = dpg.get_value("trace_marker_combo")
|
|
391
|
+
trace["marker"] = marker if marker else None
|
|
392
|
+
|
|
393
|
+
trace["markersize"] = dpg.get_value("trace_markersize_slider")
|
|
394
|
+
|
|
395
|
+
state.user_modified = True
|
|
396
|
+
update_preview(state, dpg)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def on_save_manual(state: "EditorState", dpg) -> None:
|
|
400
|
+
"""Save current overrides to .manual.json."""
|
|
401
|
+
from ..edit import save_manual_overrides
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
collect_overrides(state, dpg)
|
|
405
|
+
manual_path = save_manual_overrides(state.json_path, state.current_overrides)
|
|
406
|
+
dpg.set_value("status_text", f"Saved: {manual_path.name}")
|
|
407
|
+
except Exception as e:
|
|
408
|
+
dpg.set_value("status_text", f"Error: {str(e)}")
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def on_reset_overrides(state: "EditorState", dpg) -> None:
|
|
412
|
+
"""Reset to initial overrides."""
|
|
413
|
+
from ._rendering import update_preview
|
|
414
|
+
|
|
415
|
+
state.current_overrides = copy.deepcopy(state.initial_overrides)
|
|
416
|
+
state.user_modified = False
|
|
417
|
+
|
|
418
|
+
# Update all widgets
|
|
419
|
+
dpg.set_value("title_input", state.current_overrides.get("title", ""))
|
|
420
|
+
dpg.set_value("xlabel_input", state.current_overrides.get("xlabel", ""))
|
|
421
|
+
dpg.set_value("ylabel_input", state.current_overrides.get("ylabel", ""))
|
|
422
|
+
dpg.set_value("linewidth_slider", state.current_overrides.get("linewidth", 1.0))
|
|
423
|
+
dpg.set_value("grid_checkbox", state.current_overrides.get("grid", False))
|
|
424
|
+
dpg.set_value(
|
|
425
|
+
"transparent_checkbox", state.current_overrides.get("transparent", True)
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
update_preview(state, dpg)
|
|
429
|
+
dpg.set_value("status_text", "Reset to original")
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def on_export_png(state: "EditorState", dpg) -> None:
|
|
433
|
+
"""Export current view to PNG."""
|
|
434
|
+
import matplotlib
|
|
435
|
+
|
|
436
|
+
matplotlib.use("Agg")
|
|
437
|
+
import matplotlib.pyplot as plt
|
|
438
|
+
|
|
439
|
+
from ._plotting import plot_from_csv
|
|
440
|
+
|
|
441
|
+
try:
|
|
442
|
+
collect_overrides(state, dpg)
|
|
443
|
+
output_path = state.json_path.with_suffix(".edited.png")
|
|
444
|
+
|
|
445
|
+
# Full resolution render
|
|
446
|
+
o = state.current_overrides
|
|
447
|
+
fig_size = o.get("fig_size", [3.15, 2.68])
|
|
448
|
+
dpi = o.get("dpi", 300)
|
|
449
|
+
|
|
450
|
+
fig, ax = plt.subplots(figsize=fig_size, dpi=dpi)
|
|
451
|
+
|
|
452
|
+
if state.csv_data is not None:
|
|
453
|
+
plot_from_csv(ax, o, state.csv_data)
|
|
454
|
+
|
|
455
|
+
if o.get("title"):
|
|
456
|
+
ax.set_title(o["title"], fontsize=o.get("title_fontsize", 8))
|
|
457
|
+
if o.get("xlabel"):
|
|
458
|
+
ax.set_xlabel(o["xlabel"], fontsize=o.get("axis_fontsize", 7))
|
|
459
|
+
if o.get("ylabel"):
|
|
460
|
+
ax.set_ylabel(o["ylabel"], fontsize=o.get("axis_fontsize", 7))
|
|
461
|
+
|
|
462
|
+
fig.tight_layout()
|
|
463
|
+
fig.savefig(
|
|
464
|
+
output_path,
|
|
465
|
+
dpi=dpi,
|
|
466
|
+
bbox_inches="tight",
|
|
467
|
+
transparent=o.get("transparent", True),
|
|
468
|
+
)
|
|
469
|
+
plt.close(fig)
|
|
470
|
+
|
|
471
|
+
dpg.set_value("status_text", f"Exported: {output_path.name}")
|
|
472
|
+
except Exception as e:
|
|
473
|
+
dpg.set_value("status_text", f"Error: {str(e)}")
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
# EOF
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/_dearpygui/_panels/__init__.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
UI panel creation for DearPyGui editor.
|
|
7
|
+
|
|
8
|
+
Subpackage for organizing control panel sections.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from ._control import create_control_panel
|
|
12
|
+
from ._preview import create_preview_panel
|
|
13
|
+
|
|
14
|
+
__all__ = ["create_preview_panel", "create_control_panel"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# EOF
|