figrecipe 0.6.0__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- figrecipe/__init__.py +161 -1030
- figrecipe/__main__.py +12 -0
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +113 -0
- figrecipe/_api/_save.py +287 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- figrecipe/_cli/__init__.py +7 -0
- figrecipe/_cli/_compose.py +87 -0
- figrecipe/_cli/_convert.py +117 -0
- figrecipe/_cli/_crop.py +82 -0
- figrecipe/_cli/_edit.py +70 -0
- figrecipe/_cli/_extract.py +128 -0
- figrecipe/_cli/_fonts.py +47 -0
- figrecipe/_cli/_info.py +67 -0
- figrecipe/_cli/_main.py +58 -0
- figrecipe/_cli/_reproduce.py +79 -0
- figrecipe/_cli/_style.py +77 -0
- figrecipe/_cli/_validate.py +66 -0
- figrecipe/_cli/_version.py +50 -0
- figrecipe/_composition/__init__.py +32 -0
- figrecipe/_composition/_alignment.py +452 -0
- figrecipe/_composition/_compose.py +179 -0
- figrecipe/_composition/_import_axes.py +127 -0
- figrecipe/_composition/_visibility.py +125 -0
- figrecipe/_dev/__init__.py +4 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/browser/__init__.py +69 -0
- figrecipe/_dev/browser/_audio.py +240 -0
- figrecipe/_dev/browser/_caption.py +356 -0
- figrecipe/_dev/browser/_click_effect.py +146 -0
- figrecipe/_dev/browser/_cursor.py +196 -0
- figrecipe/_dev/browser/_highlight.py +105 -0
- figrecipe/_dev/browser/_narration.py +237 -0
- figrecipe/_dev/browser/_recorder.py +446 -0
- figrecipe/_dev/browser/_utils.py +178 -0
- figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
- figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
- figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
- figrecipe/_dev/demo_plotters/__init__.py +35 -166
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_editor/__init__.py +61 -13
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +402 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +466 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_call_overrides.py +183 -0
- figrecipe/_editor/_datatable_plot_handlers.py +249 -0
- figrecipe/_editor/_figure_layout.py +211 -0
- figrecipe/_editor/_flask_app.py +200 -1030
- figrecipe/_editor/_helpers.py +251 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +194 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +507 -0
- figrecipe/_editor/_renderer.py +81 -186
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +482 -0
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +126 -0
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +335 -0
- figrecipe/_editor/_routes_files.py +443 -0
- figrecipe/_editor/_routes_image.py +200 -0
- figrecipe/_editor/_routes_snapshot.py +94 -0
- figrecipe/_editor/_routes_style.py +243 -0
- figrecipe/_editor/_templates/__init__.py +116 -1
- figrecipe/_editor/_templates/_html.py +154 -64
- figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
- figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
- figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
- figrecipe/_editor/_templates/_html_datatable.py +92 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +493 -0
- figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
- figrecipe/_editor/_templates/_scripts/_files.py +429 -0
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
- figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
- figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
- figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
- figrecipe/_editor/_templates/_styles/__init__.py +78 -0
- figrecipe/_editor/_templates/_styles/_base.py +111 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +430 -0
- figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
- figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
- figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
- figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
- figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
- figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +224 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +127 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +430 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
- figrecipe/_editor/static/audio/click.mp3 +0 -0
- figrecipe/_editor/static/click.mp3 +0 -0
- figrecipe/_editor/static/icons/favicon.ico +0 -0
- figrecipe/_integrations/__init__.py +17 -0
- figrecipe/_integrations/_scitex_stats.py +298 -0
- figrecipe/_params/_DECORATION_METHODS.py +8 -0
- figrecipe/_recorder.py +63 -109
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +509 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +252 -895
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +188 -1
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_stat_annotation.py +274 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/styles/__init__.py +8 -6
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +42 -480
- figrecipe/styles/_style_loader.py +16 -192
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
- figrecipe/styles/presets/SCITEX.yaml +40 -28
- figrecipe-0.9.0.dist-info/METADATA +427 -0
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
- figrecipe/_editor/_bbox.py +0 -978
- figrecipe/_editor/_hitmap.py +0 -937
- figrecipe/_editor/_templates/_scripts.py +0 -2778
- figrecipe/_editor/_templates/_styles.py +0 -1326
- figrecipe/_reproducer.py +0 -975
- figrecipe-0.6.0.dist-info/METADATA +0 -394
- figrecipe-0.6.0.dist-info/RECORD +0 -90
- /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
figrecipe/_editor/_renderer.py
CHANGED
|
@@ -10,10 +10,51 @@ applied, enabling real-time preview updates in the GUI editor.
|
|
|
10
10
|
import io
|
|
11
11
|
from typing import Any, Dict, Optional, Tuple
|
|
12
12
|
|
|
13
|
-
from matplotlib.figure import Figure
|
|
14
|
-
|
|
15
13
|
from .._wrappers import RecordingFigure
|
|
16
14
|
from ._bbox import extract_bboxes
|
|
15
|
+
from ._render_overrides import apply_dark_mode, apply_overrides
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _restore_light_mode(fig) -> None:
|
|
19
|
+
"""Restore light mode colors to figure (undo dark mode changes)."""
|
|
20
|
+
text_color = "black"
|
|
21
|
+
|
|
22
|
+
# Figure background (transparent)
|
|
23
|
+
fig.patch.set_facecolor("none")
|
|
24
|
+
|
|
25
|
+
# Figure-level text
|
|
26
|
+
if hasattr(fig, "_suptitle") and fig._suptitle is not None:
|
|
27
|
+
fig._suptitle.set_color(text_color)
|
|
28
|
+
if hasattr(fig, "_supxlabel") and fig._supxlabel is not None:
|
|
29
|
+
fig._supxlabel.set_color(text_color)
|
|
30
|
+
if hasattr(fig, "_supylabel") and fig._supylabel is not None:
|
|
31
|
+
fig._supylabel.set_color(text_color)
|
|
32
|
+
|
|
33
|
+
for ax in fig.get_axes():
|
|
34
|
+
# Axes background (transparent)
|
|
35
|
+
ax.set_facecolor("none")
|
|
36
|
+
# Text colors
|
|
37
|
+
ax.xaxis.label.set_color(text_color)
|
|
38
|
+
ax.yaxis.label.set_color(text_color)
|
|
39
|
+
ax.title.set_color(text_color)
|
|
40
|
+
ax.tick_params(colors=text_color)
|
|
41
|
+
# Spines
|
|
42
|
+
for spine in ax.spines.values():
|
|
43
|
+
spine.set_edgecolor(text_color)
|
|
44
|
+
# Text objects (panel labels, annotations)
|
|
45
|
+
for text in ax.texts:
|
|
46
|
+
text.set_color(text_color)
|
|
47
|
+
# Bracket lines (Line2D with clip_on=False)
|
|
48
|
+
for line in ax.get_lines():
|
|
49
|
+
if not line.get_clip_on():
|
|
50
|
+
line.set_color(text_color)
|
|
51
|
+
# Legend
|
|
52
|
+
legend = ax.get_legend()
|
|
53
|
+
if legend is not None:
|
|
54
|
+
legend.get_frame().set_facecolor("none")
|
|
55
|
+
legend.get_frame().set_edgecolor(text_color)
|
|
56
|
+
for text in legend.get_texts():
|
|
57
|
+
text.set_color(text_color)
|
|
17
58
|
|
|
18
59
|
|
|
19
60
|
def render_preview(
|
|
@@ -48,13 +89,22 @@ def render_preview(
|
|
|
48
89
|
# Get underlying matplotlib figure
|
|
49
90
|
mpl_fig = fig.fig if hasattr(fig, "fig") else fig
|
|
50
91
|
|
|
92
|
+
# Get record for call_id grouping (if fig is a RecordingFigure)
|
|
93
|
+
record = fig.record if hasattr(fig, "record") else None
|
|
94
|
+
|
|
51
95
|
# Apply style overrides
|
|
52
96
|
if overrides:
|
|
53
|
-
|
|
97
|
+
apply_overrides(mpl_fig, overrides, record)
|
|
54
98
|
|
|
55
|
-
# Apply dark mode if requested
|
|
99
|
+
# Apply dark mode if requested, or restore light mode colors
|
|
56
100
|
if dark_mode:
|
|
57
|
-
|
|
101
|
+
apply_dark_mode(mpl_fig)
|
|
102
|
+
else:
|
|
103
|
+
# Restore light mode colors (needed because figure is reused)
|
|
104
|
+
_restore_light_mode(mpl_fig)
|
|
105
|
+
|
|
106
|
+
# Finalize ticks and special plots (must be done after all plotting)
|
|
107
|
+
_finalize_figure(fig, mpl_fig)
|
|
58
108
|
|
|
59
109
|
# Render to buffer first
|
|
60
110
|
buf = io.BytesIO()
|
|
@@ -120,185 +170,6 @@ def render_to_base64(
|
|
|
120
170
|
return base64_str, bboxes, img_size
|
|
121
171
|
|
|
122
172
|
|
|
123
|
-
def _apply_overrides(fig: Figure, overrides: Dict[str, Any]) -> None:
|
|
124
|
-
"""
|
|
125
|
-
Apply style overrides to figure.
|
|
126
|
-
|
|
127
|
-
Parameters
|
|
128
|
-
----------
|
|
129
|
-
fig : Figure
|
|
130
|
-
Matplotlib figure.
|
|
131
|
-
overrides : dict
|
|
132
|
-
Style overrides with keys like:
|
|
133
|
-
- axes_width_mm, axes_height_mm
|
|
134
|
-
- fonts_axis_label_pt, fonts_tick_label_pt
|
|
135
|
-
- lines_trace_mm
|
|
136
|
-
- etc.
|
|
137
|
-
"""
|
|
138
|
-
from ..styles._style_applier import apply_style_mm
|
|
139
|
-
|
|
140
|
-
axes_list = fig.get_axes()
|
|
141
|
-
|
|
142
|
-
for ax in axes_list:
|
|
143
|
-
# Apply mm-based styling
|
|
144
|
-
apply_style_mm(ax, overrides)
|
|
145
|
-
|
|
146
|
-
# Apply specific overrides that aren't handled by apply_style_mm
|
|
147
|
-
# YAML-compatible keys are canonical, legacy keys supported for backwards compatibility
|
|
148
|
-
|
|
149
|
-
# Font sizes (YAML: fonts_axis_label_pt, legacy: axis_font_size_pt)
|
|
150
|
-
axis_fs = overrides.get(
|
|
151
|
-
"fonts_axis_label_pt", overrides.get("axis_font_size_pt")
|
|
152
|
-
)
|
|
153
|
-
if axis_fs is not None:
|
|
154
|
-
ax.xaxis.label.set_fontsize(axis_fs)
|
|
155
|
-
ax.yaxis.label.set_fontsize(axis_fs)
|
|
156
|
-
|
|
157
|
-
tick_fs = overrides.get(
|
|
158
|
-
"fonts_tick_label_pt", overrides.get("tick_font_size_pt")
|
|
159
|
-
)
|
|
160
|
-
if tick_fs is not None:
|
|
161
|
-
ax.tick_params(labelsize=tick_fs)
|
|
162
|
-
|
|
163
|
-
title_fs = overrides.get("fonts_title_pt", overrides.get("title_font_size_pt"))
|
|
164
|
-
if title_fs is not None:
|
|
165
|
-
ax.title.set_fontsize(title_fs)
|
|
166
|
-
|
|
167
|
-
family = overrides.get("fonts_family", overrides.get("font_family"))
|
|
168
|
-
if family is not None:
|
|
169
|
-
ax.xaxis.label.set_fontfamily(family)
|
|
170
|
-
ax.yaxis.label.set_fontfamily(family)
|
|
171
|
-
ax.title.set_fontfamily(family)
|
|
172
|
-
for label in ax.get_xticklabels() + ax.get_yticklabels():
|
|
173
|
-
label.set_fontfamily(family)
|
|
174
|
-
|
|
175
|
-
# Ticks (YAML: ticks_direction, legacy: tick_direction)
|
|
176
|
-
tick_dir = overrides.get("ticks_direction", overrides.get("tick_direction"))
|
|
177
|
-
if tick_dir is not None and tick_dir in ("in", "out", "inout"):
|
|
178
|
-
ax.tick_params(direction=tick_dir)
|
|
179
|
-
|
|
180
|
-
tick_len = overrides.get("ticks_length_mm", overrides.get("tick_length_mm"))
|
|
181
|
-
if tick_len is not None:
|
|
182
|
-
from .._utils._units import mm_to_pt
|
|
183
|
-
|
|
184
|
-
length = mm_to_pt(tick_len)
|
|
185
|
-
ax.tick_params(length=length)
|
|
186
|
-
|
|
187
|
-
# Grid (YAML: behavior_grid, legacy: grid)
|
|
188
|
-
grid_value = overrides.get("behavior_grid", overrides.get("grid"))
|
|
189
|
-
if grid_value is not None:
|
|
190
|
-
if grid_value:
|
|
191
|
-
ax.grid(True, alpha=0.3)
|
|
192
|
-
else:
|
|
193
|
-
ax.grid(False)
|
|
194
|
-
|
|
195
|
-
# Spines (YAML: behavior_hide_top_spine, legacy: hide_top_spine)
|
|
196
|
-
hide_top = overrides.get(
|
|
197
|
-
"behavior_hide_top_spine", overrides.get("hide_top_spine")
|
|
198
|
-
)
|
|
199
|
-
if hide_top is not None:
|
|
200
|
-
ax.spines["top"].set_visible(not hide_top)
|
|
201
|
-
|
|
202
|
-
hide_right = overrides.get(
|
|
203
|
-
"behavior_hide_right_spine", overrides.get("hide_right_spine")
|
|
204
|
-
)
|
|
205
|
-
if hide_right is not None:
|
|
206
|
-
ax.spines["right"].set_visible(not hide_right)
|
|
207
|
-
|
|
208
|
-
# Legend
|
|
209
|
-
legend = ax.get_legend()
|
|
210
|
-
if legend is not None:
|
|
211
|
-
if "legend_frameon" in overrides:
|
|
212
|
-
legend.set_frame_on(overrides["legend_frameon"])
|
|
213
|
-
|
|
214
|
-
if "legend_alpha" in overrides:
|
|
215
|
-
frame = legend.get_frame()
|
|
216
|
-
fc = frame.get_facecolor()
|
|
217
|
-
frame.set_facecolor((*fc[:3], overrides["legend_alpha"]))
|
|
218
|
-
|
|
219
|
-
# Line widths (YAML: lines_trace_mm, legacy: trace_thickness_mm)
|
|
220
|
-
trace_mm = overrides.get("lines_trace_mm", overrides.get("trace_thickness_mm"))
|
|
221
|
-
if trace_mm is not None:
|
|
222
|
-
from .._utils._units import mm_to_pt
|
|
223
|
-
|
|
224
|
-
lw = mm_to_pt(trace_mm)
|
|
225
|
-
for line in ax.get_lines():
|
|
226
|
-
line.set_linewidth(lw)
|
|
227
|
-
|
|
228
|
-
# Marker sizes (YAML: markers_scatter_mm, legacy: marker_size_mm)
|
|
229
|
-
# Only apply to PathCollection (scatter), not PolyCollection (violin/fill)
|
|
230
|
-
scatter_mm = overrides.get(
|
|
231
|
-
"markers_scatter_mm",
|
|
232
|
-
overrides.get("markers_size_mm", overrides.get("marker_size_mm")),
|
|
233
|
-
)
|
|
234
|
-
if scatter_mm is not None:
|
|
235
|
-
from matplotlib.collections import PathCollection
|
|
236
|
-
|
|
237
|
-
from .._utils._units import mm_to_scatter_size
|
|
238
|
-
|
|
239
|
-
size = mm_to_scatter_size(scatter_mm)
|
|
240
|
-
for coll in ax.collections:
|
|
241
|
-
# Only apply to scatter plots (PathCollection), not violin/fill (PolyCollection)
|
|
242
|
-
if isinstance(coll, PathCollection):
|
|
243
|
-
try:
|
|
244
|
-
coll.set_sizes([size])
|
|
245
|
-
except Exception:
|
|
246
|
-
pass
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def _apply_dark_mode(fig: Figure) -> None:
|
|
250
|
-
"""
|
|
251
|
-
Apply dark mode colors to figure.
|
|
252
|
-
|
|
253
|
-
Parameters
|
|
254
|
-
----------
|
|
255
|
-
fig : Figure
|
|
256
|
-
Matplotlib figure.
|
|
257
|
-
"""
|
|
258
|
-
# Dark theme colors
|
|
259
|
-
bg_color = "#1a1a1a"
|
|
260
|
-
text_color = "#e8e8e8"
|
|
261
|
-
|
|
262
|
-
# Figure background
|
|
263
|
-
fig.patch.set_facecolor(bg_color)
|
|
264
|
-
|
|
265
|
-
# Figure-level text elements (suptitle, supxlabel, supylabel)
|
|
266
|
-
if hasattr(fig, "_suptitle") and fig._suptitle is not None:
|
|
267
|
-
fig._suptitle.set_color(text_color)
|
|
268
|
-
if hasattr(fig, "_supxlabel") and fig._supxlabel is not None:
|
|
269
|
-
fig._supxlabel.set_color(text_color)
|
|
270
|
-
if hasattr(fig, "_supylabel") and fig._supylabel is not None:
|
|
271
|
-
fig._supylabel.set_color(text_color)
|
|
272
|
-
|
|
273
|
-
for ax in fig.get_axes():
|
|
274
|
-
# Axes background
|
|
275
|
-
ax.set_facecolor(bg_color)
|
|
276
|
-
|
|
277
|
-
# Text colors
|
|
278
|
-
ax.xaxis.label.set_color(text_color)
|
|
279
|
-
ax.yaxis.label.set_color(text_color)
|
|
280
|
-
ax.title.set_color(text_color)
|
|
281
|
-
|
|
282
|
-
# Tick labels
|
|
283
|
-
ax.tick_params(colors=text_color)
|
|
284
|
-
|
|
285
|
-
# Spines
|
|
286
|
-
for spine in ax.spines.values():
|
|
287
|
-
spine.set_color(text_color)
|
|
288
|
-
|
|
289
|
-
# Grid
|
|
290
|
-
ax.tick_params(color=text_color)
|
|
291
|
-
|
|
292
|
-
# Legend
|
|
293
|
-
legend = ax.get_legend()
|
|
294
|
-
if legend is not None:
|
|
295
|
-
frame = legend.get_frame()
|
|
296
|
-
frame.set_facecolor(bg_color)
|
|
297
|
-
frame.set_edgecolor(text_color)
|
|
298
|
-
for text in legend.get_texts():
|
|
299
|
-
text.set_color(text_color)
|
|
300
|
-
|
|
301
|
-
|
|
302
173
|
def render_download(
|
|
303
174
|
fig: RecordingFigure,
|
|
304
175
|
fmt: str = "png",
|
|
@@ -329,11 +200,17 @@ def render_download(
|
|
|
329
200
|
"""
|
|
330
201
|
mpl_fig = fig.fig if hasattr(fig, "fig") else fig
|
|
331
202
|
|
|
203
|
+
# Get record for call_id grouping (if fig is a RecordingFigure)
|
|
204
|
+
record = fig.record if hasattr(fig, "record") else None
|
|
205
|
+
|
|
332
206
|
if overrides:
|
|
333
|
-
|
|
207
|
+
apply_overrides(mpl_fig, overrides, record)
|
|
334
208
|
|
|
335
209
|
if dark_mode:
|
|
336
|
-
|
|
210
|
+
apply_dark_mode(mpl_fig)
|
|
211
|
+
|
|
212
|
+
# Finalize ticks and special plots (must be done after all plotting)
|
|
213
|
+
_finalize_figure(fig, mpl_fig)
|
|
337
214
|
|
|
338
215
|
buf = io.BytesIO()
|
|
339
216
|
mpl_fig.savefig(buf, format=fmt, dpi=dpi, bbox_inches="tight")
|
|
@@ -342,8 +219,26 @@ def render_download(
|
|
|
342
219
|
return buf.read()
|
|
343
220
|
|
|
344
221
|
|
|
222
|
+
def _finalize_figure(fig: RecordingFigure, mpl_fig: Any) -> None:
|
|
223
|
+
"""Finalize ticks and special plots for all axes in the figure."""
|
|
224
|
+
from ..styles._style_applier import finalize_special_plots, finalize_ticks
|
|
225
|
+
|
|
226
|
+
# Get style dict for finalization
|
|
227
|
+
style_dict = {}
|
|
228
|
+
if hasattr(fig, "style") and fig.style:
|
|
229
|
+
from ..styles import get_style
|
|
230
|
+
|
|
231
|
+
style_dict = get_style(fig.style)
|
|
232
|
+
|
|
233
|
+
for ax in mpl_fig.get_axes():
|
|
234
|
+
finalize_ticks(ax)
|
|
235
|
+
finalize_special_plots(ax, style_dict)
|
|
236
|
+
|
|
237
|
+
|
|
345
238
|
__all__ = [
|
|
346
239
|
"render_preview",
|
|
347
240
|
"render_to_base64",
|
|
348
241
|
"render_download",
|
|
349
242
|
]
|
|
243
|
+
|
|
244
|
+
# EOF
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Annotation-related Flask route handlers for the figure editor.
|
|
4
|
+
|
|
5
|
+
Handles updating positions of decorative elements like panel labels,
|
|
6
|
+
text annotations, and arrows.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from flask import jsonify, request
|
|
10
|
+
|
|
11
|
+
from ._helpers import render_with_overrides
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register_annotation_routes(app, editor):
|
|
15
|
+
"""Register annotation-related routes with the Flask app."""
|
|
16
|
+
|
|
17
|
+
@app.route("/update_annotation_position", methods=["POST"])
|
|
18
|
+
def update_annotation_position():
|
|
19
|
+
"""Update annotation position (panel label, text, arrow).
|
|
20
|
+
|
|
21
|
+
Expects JSON: {
|
|
22
|
+
ax_index: int,
|
|
23
|
+
annotation_type: str ('panel_label', 'text', 'arrow'),
|
|
24
|
+
text_index: int,
|
|
25
|
+
x: float (axes-relative 0-1),
|
|
26
|
+
y: float (axes-relative 0-1)
|
|
27
|
+
}
|
|
28
|
+
"""
|
|
29
|
+
data = request.get_json() or {}
|
|
30
|
+
ax_index = data.get("ax_index", 0)
|
|
31
|
+
annotation_type = data.get("annotation_type", "text")
|
|
32
|
+
text_index = data.get("text_index", 0)
|
|
33
|
+
new_x = data.get("x")
|
|
34
|
+
new_y = data.get("y")
|
|
35
|
+
|
|
36
|
+
if new_x is None or new_y is None:
|
|
37
|
+
return jsonify({"error": "Missing x or y coordinate"}), 400
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
# Get the matplotlib figure
|
|
41
|
+
mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
|
|
42
|
+
axes_list = mpl_fig.get_axes()
|
|
43
|
+
|
|
44
|
+
if ax_index >= len(axes_list):
|
|
45
|
+
return jsonify({"error": f"Invalid axis index: {ax_index}"}), 400
|
|
46
|
+
|
|
47
|
+
ax = axes_list[ax_index]
|
|
48
|
+
|
|
49
|
+
if annotation_type in ("panel_label", "text"):
|
|
50
|
+
# Update text position
|
|
51
|
+
if text_index >= len(ax.texts):
|
|
52
|
+
return jsonify({"error": f"Invalid text index: {text_index}"}), 400
|
|
53
|
+
|
|
54
|
+
text_obj = ax.texts[text_index]
|
|
55
|
+
|
|
56
|
+
# Set new position (in axes coordinates for transAxes transform)
|
|
57
|
+
if text_obj.get_transform() == ax.transAxes:
|
|
58
|
+
text_obj.set_position((new_x, new_y))
|
|
59
|
+
else:
|
|
60
|
+
# For data coordinates, convert from axes-relative
|
|
61
|
+
xlim = ax.get_xlim()
|
|
62
|
+
ylim = ax.get_ylim()
|
|
63
|
+
data_x = xlim[0] + new_x * (xlim[1] - xlim[0])
|
|
64
|
+
data_y = ylim[0] + new_y * (ylim[1] - ylim[0])
|
|
65
|
+
text_obj.set_position((data_x, data_y))
|
|
66
|
+
|
|
67
|
+
# Store in manual_overrides for persistence and undo support
|
|
68
|
+
override_key = f"annotation_pos_ax{ax_index}_text{text_index}"
|
|
69
|
+
editor.style_overrides.manual_overrides[override_key] = {
|
|
70
|
+
"x": new_x,
|
|
71
|
+
"y": new_y,
|
|
72
|
+
"type": annotation_type,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
elif annotation_type == "arrow":
|
|
76
|
+
# Update arrow position (FancyArrowPatch)
|
|
77
|
+
from matplotlib.patches import FancyArrowPatch
|
|
78
|
+
|
|
79
|
+
arrow_patches = [
|
|
80
|
+
p for p in ax.patches if isinstance(p, FancyArrowPatch)
|
|
81
|
+
]
|
|
82
|
+
arrow_index = data.get("arrow_index", 0)
|
|
83
|
+
|
|
84
|
+
if arrow_index >= len(arrow_patches):
|
|
85
|
+
return jsonify(
|
|
86
|
+
{"error": f"Invalid arrow index: {arrow_index}"}
|
|
87
|
+
), 400
|
|
88
|
+
|
|
89
|
+
# Arrow position update is more complex - skip for now
|
|
90
|
+
return jsonify({"error": "Arrow drag not yet implemented"}), 501
|
|
91
|
+
|
|
92
|
+
# Re-render the figure
|
|
93
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
94
|
+
editor.fig,
|
|
95
|
+
editor.get_effective_style(),
|
|
96
|
+
editor.dark_mode,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return jsonify(
|
|
100
|
+
{
|
|
101
|
+
"success": True,
|
|
102
|
+
"image": base64_img,
|
|
103
|
+
"bboxes": bboxes,
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
import traceback
|
|
109
|
+
|
|
110
|
+
traceback.print_exc()
|
|
111
|
+
return jsonify({"error": str(e)}), 500
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
__all__ = ["register_annotation_routes"]
|