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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Routes for panel snapshot generation (isolated rendering)."""
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import io
|
|
7
|
+
import threading
|
|
8
|
+
|
|
9
|
+
# Lock to prevent concurrent matplotlib figure access (not thread-safe)
|
|
10
|
+
_figure_lock = threading.Lock()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register_snapshot_routes(app, editor):
|
|
14
|
+
"""Register snapshot-related routes.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
app : Flask
|
|
19
|
+
Flask application instance.
|
|
20
|
+
editor : FigureEditor
|
|
21
|
+
Editor instance with figure state.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@app.route("/get_panel_snapshot/<int:ax_index>")
|
|
25
|
+
def get_panel_snapshot(ax_index):
|
|
26
|
+
"""Render a single panel in isolation and return as base64 PNG.
|
|
27
|
+
|
|
28
|
+
This hides all other axes to produce a clean snapshot without
|
|
29
|
+
overlap artifacts from neighboring panels.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
ax_index : int
|
|
34
|
+
Index of the axis/panel to render.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
dict
|
|
39
|
+
JSON with success status and base64-encoded PNG image.
|
|
40
|
+
"""
|
|
41
|
+
# DISABLED: Modifying figure visibility corrupts shared state
|
|
42
|
+
# TODO: Implement proper solution (deep copy figure or pre-render)
|
|
43
|
+
return {"success": False, "error": "Snapshot temporarily disabled"}
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
# Use lock to prevent concurrent matplotlib access (not thread-safe)
|
|
47
|
+
with _figure_lock:
|
|
48
|
+
# Get matplotlib figure from RecordingFigure wrapper
|
|
49
|
+
mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
|
|
50
|
+
axes = mpl_fig.get_axes()
|
|
51
|
+
|
|
52
|
+
if ax_index < 0 or ax_index >= len(axes):
|
|
53
|
+
return {"success": False, "error": f"Invalid ax_index: {ax_index}"}
|
|
54
|
+
|
|
55
|
+
# Store original visibility states
|
|
56
|
+
original_visibility = [ax.get_visible() for ax in axes]
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
# Hide all axes except the target
|
|
60
|
+
for i, ax in enumerate(axes):
|
|
61
|
+
ax.set_visible(i == ax_index)
|
|
62
|
+
|
|
63
|
+
# Render to buffer with transparent background
|
|
64
|
+
# Use full figure size (no bbox_inches="tight" to preserve dimensions)
|
|
65
|
+
buf = io.BytesIO()
|
|
66
|
+
mpl_fig.savefig(
|
|
67
|
+
buf,
|
|
68
|
+
format="png",
|
|
69
|
+
transparent=True,
|
|
70
|
+
facecolor="none",
|
|
71
|
+
edgecolor="none",
|
|
72
|
+
)
|
|
73
|
+
buf.seek(0)
|
|
74
|
+
image_base64 = base64.b64encode(buf.read()).decode("utf-8")
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
"success": True,
|
|
78
|
+
"image": image_base64,
|
|
79
|
+
"ax_index": ax_index,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
finally:
|
|
83
|
+
# Restore original visibility
|
|
84
|
+
for i, ax in enumerate(axes):
|
|
85
|
+
ax.set_visible(original_visibility[i])
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
# Return JSON error instead of 500 to avoid console errors
|
|
89
|
+
return {"success": False, "error": str(e)}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
__all__ = ["register_snapshot_routes"]
|
|
93
|
+
|
|
94
|
+
# EOF
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Style and theme Flask route handlers for the figure editor.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from flask import jsonify, request
|
|
8
|
+
|
|
9
|
+
from ._helpers import get_form_values_from_style, render_with_overrides
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register_style_routes(app, editor):
|
|
13
|
+
"""Register style/theme routes with the Flask app."""
|
|
14
|
+
from ._overrides import save_overrides
|
|
15
|
+
|
|
16
|
+
@app.route("/style")
|
|
17
|
+
def get_style():
|
|
18
|
+
"""Get current style configuration."""
|
|
19
|
+
return jsonify(
|
|
20
|
+
{
|
|
21
|
+
"base_style": editor.style_overrides.base_style,
|
|
22
|
+
"programmatic_style": editor.style_overrides.programmatic_style,
|
|
23
|
+
"manual_overrides": editor.style_overrides.manual_overrides,
|
|
24
|
+
"effective_style": editor.get_effective_style(),
|
|
25
|
+
"has_overrides": editor.style_overrides.has_manual_overrides(),
|
|
26
|
+
"manual_timestamp": editor.style_overrides.manual_timestamp,
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
@app.route("/overrides")
|
|
31
|
+
def get_overrides():
|
|
32
|
+
"""Get current manual overrides."""
|
|
33
|
+
return jsonify(editor.style_overrides.manual_overrides)
|
|
34
|
+
|
|
35
|
+
@app.route("/theme")
|
|
36
|
+
def get_theme():
|
|
37
|
+
"""Get current theme YAML content for display."""
|
|
38
|
+
import io as yaml_io
|
|
39
|
+
|
|
40
|
+
from ruamel.yaml import YAML
|
|
41
|
+
|
|
42
|
+
style = editor.get_effective_style()
|
|
43
|
+
style_name = style.get("_name", "SCITEX")
|
|
44
|
+
|
|
45
|
+
yaml = YAML()
|
|
46
|
+
yaml.default_flow_style = False
|
|
47
|
+
yaml.indent(mapping=2, sequence=4, offset=2)
|
|
48
|
+
stream = yaml_io.StringIO()
|
|
49
|
+
yaml.dump(style, stream)
|
|
50
|
+
yaml_content = stream.getvalue()
|
|
51
|
+
|
|
52
|
+
return jsonify(
|
|
53
|
+
{
|
|
54
|
+
"name": style_name,
|
|
55
|
+
"content": yaml_content,
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@app.route("/list_themes")
|
|
60
|
+
def list_themes():
|
|
61
|
+
"""List available theme presets."""
|
|
62
|
+
from ..styles._style_loader import list_presets
|
|
63
|
+
|
|
64
|
+
presets = list_presets()
|
|
65
|
+
current = editor.get_effective_style().get("_name", "SCITEX")
|
|
66
|
+
|
|
67
|
+
return jsonify(
|
|
68
|
+
{
|
|
69
|
+
"themes": presets,
|
|
70
|
+
"current": current,
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@app.route("/switch_theme", methods=["POST"])
|
|
75
|
+
def switch_theme():
|
|
76
|
+
"""Switch to a different theme preset by reproducing the figure."""
|
|
77
|
+
from .._reproducer import reproduce_from_record
|
|
78
|
+
from ..styles._style_loader import load_preset
|
|
79
|
+
|
|
80
|
+
data = request.get_json() or {}
|
|
81
|
+
theme_name = data.get("theme")
|
|
82
|
+
|
|
83
|
+
if not theme_name:
|
|
84
|
+
return jsonify({"error": "No theme specified"}), 400
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
new_style = load_preset(theme_name)
|
|
88
|
+
|
|
89
|
+
if new_style is None:
|
|
90
|
+
return jsonify({"error": f"Theme '{theme_name}' not found"}), 404
|
|
91
|
+
|
|
92
|
+
# Convert nested style to flat style dict with color_palette
|
|
93
|
+
flat_style = dict(new_style)
|
|
94
|
+
flat_style["_name"] = theme_name
|
|
95
|
+
|
|
96
|
+
# Extract color_palette from nested colors.palette
|
|
97
|
+
if "colors" in new_style and isinstance(new_style["colors"], dict):
|
|
98
|
+
colors_dict = new_style["colors"]
|
|
99
|
+
if "palette" in colors_dict and colors_dict["palette"] is not None:
|
|
100
|
+
flat_style["color_palette"] = list(colors_dict["palette"])
|
|
101
|
+
|
|
102
|
+
editor.style_overrides.base_style = flat_style
|
|
103
|
+
|
|
104
|
+
if hasattr(editor.fig, "record") and editor.fig.record is not None:
|
|
105
|
+
editor.fig.record.style = flat_style
|
|
106
|
+
new_fig, _ = reproduce_from_record(editor.fig.record)
|
|
107
|
+
editor.fig = new_fig
|
|
108
|
+
# Keep the new style (don't restore old style)
|
|
109
|
+
|
|
110
|
+
mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
|
|
111
|
+
behavior = new_style.get("behavior", {})
|
|
112
|
+
for ax in mpl_fig.get_axes():
|
|
113
|
+
# Handle all four spine directions
|
|
114
|
+
for side, default in [
|
|
115
|
+
("top", True),
|
|
116
|
+
("right", True),
|
|
117
|
+
("bottom", False),
|
|
118
|
+
("left", False),
|
|
119
|
+
]:
|
|
120
|
+
hide = behavior.get(f"hide_{side}_spine", default)
|
|
121
|
+
ax.spines[side].set_visible(not hide)
|
|
122
|
+
|
|
123
|
+
if behavior.get("grid", False):
|
|
124
|
+
ax.grid(True, alpha=0.3)
|
|
125
|
+
else:
|
|
126
|
+
ax.grid(False)
|
|
127
|
+
|
|
128
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
129
|
+
editor.fig,
|
|
130
|
+
editor.get_effective_style(),
|
|
131
|
+
editor.dark_mode,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
form_values = get_form_values_from_style(editor.get_effective_style())
|
|
135
|
+
|
|
136
|
+
return jsonify(
|
|
137
|
+
{
|
|
138
|
+
"success": True,
|
|
139
|
+
"theme": theme_name,
|
|
140
|
+
"image": base64_img,
|
|
141
|
+
"bboxes": bboxes,
|
|
142
|
+
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
143
|
+
"values": form_values,
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
import traceback
|
|
149
|
+
|
|
150
|
+
traceback.print_exc()
|
|
151
|
+
return jsonify({"error": f"Failed to switch theme: {str(e)}"}), 500
|
|
152
|
+
|
|
153
|
+
@app.route("/save", methods=["POST"])
|
|
154
|
+
def save():
|
|
155
|
+
"""Save style overrides (stored separately from recipe)."""
|
|
156
|
+
data = request.get_json() or {}
|
|
157
|
+
editor.style_overrides.update_manual_overrides(data.get("overrides", {}))
|
|
158
|
+
|
|
159
|
+
if editor.recipe_path:
|
|
160
|
+
path = save_overrides(editor.style_overrides, editor.recipe_path)
|
|
161
|
+
return jsonify(
|
|
162
|
+
{
|
|
163
|
+
"success": True,
|
|
164
|
+
"path": str(path),
|
|
165
|
+
"has_overrides": editor.style_overrides.has_manual_overrides(),
|
|
166
|
+
"timestamp": editor.style_overrides.manual_timestamp,
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return jsonify(
|
|
171
|
+
{
|
|
172
|
+
"success": True,
|
|
173
|
+
"overrides": editor.overrides,
|
|
174
|
+
"has_overrides": editor.style_overrides.has_manual_overrides(),
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@app.route("/restore", methods=["POST"])
|
|
179
|
+
def restore():
|
|
180
|
+
"""Restore to original style (clear manual overrides and axes positions)."""
|
|
181
|
+
from ._bbox import extract_bboxes
|
|
182
|
+
|
|
183
|
+
# Clear all manual overrides (including position overrides)
|
|
184
|
+
editor.style_overrides.clear_manual_overrides()
|
|
185
|
+
|
|
186
|
+
# Restore original axes positions
|
|
187
|
+
editor.restore_axes_positions()
|
|
188
|
+
|
|
189
|
+
# Restore original annotation positions (panel labels, text)
|
|
190
|
+
editor.restore_annotation_positions()
|
|
191
|
+
|
|
192
|
+
if editor._initial_base64 and not editor.dark_mode:
|
|
193
|
+
base64_img = editor._initial_base64
|
|
194
|
+
import base64 as b64
|
|
195
|
+
import io
|
|
196
|
+
|
|
197
|
+
from PIL import Image
|
|
198
|
+
|
|
199
|
+
img_data = b64.b64decode(base64_img)
|
|
200
|
+
img = Image.open(io.BytesIO(img_data))
|
|
201
|
+
img_size = img.size
|
|
202
|
+
mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
|
|
203
|
+
original_dpi = mpl_fig.dpi
|
|
204
|
+
try:
|
|
205
|
+
mpl_fig.set_dpi(150)
|
|
206
|
+
mpl_fig.canvas.draw()
|
|
207
|
+
except Exception:
|
|
208
|
+
# Ignore matplotlib/tkinter threading issues in background thread
|
|
209
|
+
pass
|
|
210
|
+
bboxes = extract_bboxes(mpl_fig, img_size[0], img_size[1])
|
|
211
|
+
try:
|
|
212
|
+
mpl_fig.set_dpi(original_dpi)
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
else:
|
|
216
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
217
|
+
editor.fig,
|
|
218
|
+
None,
|
|
219
|
+
editor.dark_mode,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return jsonify(
|
|
223
|
+
{
|
|
224
|
+
"success": True,
|
|
225
|
+
"image": base64_img,
|
|
226
|
+
"bboxes": bboxes,
|
|
227
|
+
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
228
|
+
"original_style": editor.style,
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
@app.route("/diff")
|
|
233
|
+
def get_diff():
|
|
234
|
+
"""Get differences between original and manual overrides."""
|
|
235
|
+
return jsonify(
|
|
236
|
+
{
|
|
237
|
+
"diff": editor.style_overrides.get_diff(),
|
|
238
|
+
"has_overrides": editor.style_overrides.has_manual_overrides(),
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
__all__ = ["register_style_routes"]
|
|
@@ -8,13 +8,32 @@ single source of truth. No custom key mapping is needed since all keys
|
|
|
8
8
|
now match the HTML input IDs directly.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import base64
|
|
11
12
|
import json
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
12
15
|
from typing import Any, Dict, Tuple
|
|
13
16
|
|
|
17
|
+
import figrecipe
|
|
18
|
+
|
|
14
19
|
from ._html import HTML_TEMPLATE
|
|
20
|
+
from ._html_components import HTML_FILE_BROWSER
|
|
21
|
+
from ._html_datatable import HTML_DATATABLE_PANEL
|
|
15
22
|
from ._scripts import SCRIPTS
|
|
16
23
|
from ._styles import STYLES
|
|
17
24
|
|
|
25
|
+
# Server start time for debugging template reloads
|
|
26
|
+
_SERVER_START_TIME = datetime.now().strftime("%H:%M:%S")
|
|
27
|
+
|
|
28
|
+
# Load SciTeX icon as base64
|
|
29
|
+
_SCITEX_ICON_PATH = (
|
|
30
|
+
Path(__file__).parent.parent / "static" / "icons" / "scitex-icon.png"
|
|
31
|
+
)
|
|
32
|
+
_SCITEX_ICON_BASE64 = ""
|
|
33
|
+
if _SCITEX_ICON_PATH.exists():
|
|
34
|
+
with open(_SCITEX_ICON_PATH, "rb") as f:
|
|
35
|
+
_SCITEX_ICON_BASE64 = base64.b64encode(f.read()).decode("utf-8")
|
|
36
|
+
|
|
18
37
|
|
|
19
38
|
def build_html_template(
|
|
20
39
|
image_base64: str,
|
|
@@ -24,6 +43,10 @@ def build_html_template(
|
|
|
24
43
|
overrides: Dict[str, Any],
|
|
25
44
|
img_size: Tuple[int, int],
|
|
26
45
|
style_name: str = "SCITEX",
|
|
46
|
+
hot_reload: bool = False,
|
|
47
|
+
dark_mode: bool = False,
|
|
48
|
+
figure_has_content: bool = True,
|
|
49
|
+
debug_mode: bool = False,
|
|
27
50
|
) -> str:
|
|
28
51
|
"""
|
|
29
52
|
Build complete HTML template for figure editor.
|
|
@@ -47,6 +70,12 @@ def build_html_template(
|
|
|
47
70
|
(width, height) of preview image.
|
|
48
71
|
style_name : str
|
|
49
72
|
Name of the applied style preset (e.g., "SCITEX", "MATPLOTLIB").
|
|
73
|
+
hot_reload : bool
|
|
74
|
+
Enable hot reload auto-reconnect JavaScript.
|
|
75
|
+
dark_mode : bool
|
|
76
|
+
Initial dark mode state from saved preferences.
|
|
77
|
+
figure_has_content : bool
|
|
78
|
+
Whether the figure has plot content (hides welcome overlay if True).
|
|
50
79
|
|
|
51
80
|
Returns
|
|
52
81
|
-------
|
|
@@ -57,10 +86,65 @@ def build_html_template(
|
|
|
57
86
|
# Keys should already match HTML input IDs (YAML-compatible flattened)
|
|
58
87
|
initial_values = {**style, **overrides}
|
|
59
88
|
|
|
89
|
+
# Hot reload JavaScript for auto-reconnect on server restart
|
|
90
|
+
hot_reload_script = ""
|
|
91
|
+
if hot_reload:
|
|
92
|
+
hot_reload_script = """
|
|
93
|
+
// Hot Reload: Auto-reconnect when server restarts
|
|
94
|
+
(function() {
|
|
95
|
+
let isReconnecting = false;
|
|
96
|
+
let pingInterval = null;
|
|
97
|
+
|
|
98
|
+
function showReloadBanner(show) {
|
|
99
|
+
let banner = document.getElementById('hot-reload-banner');
|
|
100
|
+
if (!banner && show) {
|
|
101
|
+
banner = document.createElement('div');
|
|
102
|
+
banner.id = 'hot-reload-banner';
|
|
103
|
+
banner.style.cssText = 'position:fixed;top:0;left:0;right:0;background:#f59e0b;' +
|
|
104
|
+
'color:#000;text-align:center;padding:8px;z-index:9999;font-weight:bold;';
|
|
105
|
+
banner.textContent = 'Server restarting... will reload automatically';
|
|
106
|
+
document.body.prepend(banner);
|
|
107
|
+
}
|
|
108
|
+
if (banner) {
|
|
109
|
+
banner.style.display = show ? 'block' : 'none';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function ping() {
|
|
114
|
+
fetch('/ping', {cache: 'no-store'})
|
|
115
|
+
.then(r => {
|
|
116
|
+
if (r.ok && isReconnecting) {
|
|
117
|
+
// Server is back! Reload the page
|
|
118
|
+
console.log('[Hot Reload] Server is back, reloading...');
|
|
119
|
+
window.location.reload();
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
.catch(() => {
|
|
123
|
+
if (!isReconnecting) {
|
|
124
|
+
console.log('[Hot Reload] Server disconnected, waiting for restart...');
|
|
125
|
+
isReconnecting = true;
|
|
126
|
+
showReloadBanner(true);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Start pinging every 500ms
|
|
132
|
+
pingInterval = setInterval(ping, 500);
|
|
133
|
+
console.log('[Hot Reload] Enabled - watching for server restarts');
|
|
134
|
+
})();
|
|
135
|
+
"""
|
|
136
|
+
|
|
60
137
|
# Inject data into template
|
|
61
138
|
html = HTML_TEMPLATE
|
|
139
|
+
html = html.replace("<!-- FILE_BROWSER_PLACEHOLDER -->", HTML_FILE_BROWSER)
|
|
140
|
+
|
|
141
|
+
# Insert datatable panel before preview panel
|
|
142
|
+
html = html.replace(
|
|
143
|
+
"<!-- Preview Panel -->",
|
|
144
|
+
HTML_DATATABLE_PANEL + "\n <!-- Preview Panel -->",
|
|
145
|
+
)
|
|
62
146
|
html = html.replace("/* STYLES_PLACEHOLDER */", STYLES)
|
|
63
|
-
html = html.replace("/* SCRIPTS_PLACEHOLDER */", SCRIPTS)
|
|
147
|
+
html = html.replace("/* SCRIPTS_PLACEHOLDER */", SCRIPTS + hot_reload_script)
|
|
64
148
|
html = html.replace("IMAGE_BASE64_PLACEHOLDER", image_base64)
|
|
65
149
|
html = html.replace("BBOXES_PLACEHOLDER", json.dumps(bboxes))
|
|
66
150
|
html = html.replace("COLOR_MAP_PLACEHOLDER", json.dumps(color_map))
|
|
@@ -68,6 +152,37 @@ def build_html_template(
|
|
|
68
152
|
html = html.replace("IMG_WIDTH_PLACEHOLDER", str(img_size[0]))
|
|
69
153
|
html = html.replace("IMG_HEIGHT_PLACEHOLDER", str(img_size[1]))
|
|
70
154
|
html = html.replace("STYLE_NAME_PLACEHOLDER", style_name)
|
|
155
|
+
html = html.replace("SCITEX_ICON_PLACEHOLDER", _SCITEX_ICON_BASE64)
|
|
156
|
+
|
|
157
|
+
# Dark mode preference - set initial state
|
|
158
|
+
html = html.replace("DARK_MODE_THEME_PLACEHOLDER", "dark" if dark_mode else "light")
|
|
159
|
+
|
|
160
|
+
# Server start time for debugging
|
|
161
|
+
html = html.replace("SERVER_START_TIME_PLACEHOLDER", _SERVER_START_TIME)
|
|
162
|
+
|
|
163
|
+
# Version number
|
|
164
|
+
html = html.replace("VERSION_PLACEHOLDER", figrecipe.__version__)
|
|
165
|
+
|
|
166
|
+
# Welcome overlay - show only for empty figures
|
|
167
|
+
welcome_display = "none" if figure_has_content else "flex"
|
|
168
|
+
html = html.replace("WELCOME_DISPLAY_PLACEHOLDER", welcome_display)
|
|
169
|
+
|
|
170
|
+
# Debug mode - enables Element Inspector and Show All Bboxes
|
|
171
|
+
html = html.replace("DEBUG_MODE_PLACEHOLDER", "true" if debug_mode else "false")
|
|
172
|
+
|
|
173
|
+
# Debug shortcuts section - only shown when debug mode is enabled
|
|
174
|
+
debug_shortcuts_html = ""
|
|
175
|
+
if debug_mode:
|
|
176
|
+
debug_shortcuts_html = """<div class="shortcut-section debug-shortcuts"><h4>Debug <span class="debug-badge">DEBUG MODE</span></h4>
|
|
177
|
+
<div class="shortcut-row"><span class="shortcut-keys"><kbd>Alt</kbd>+<kbd>I</kbd></span><span class="shortcut-desc">Element Inspector</span></div>
|
|
178
|
+
<div class="shortcut-row"><span class="shortcut-keys"><kbd>Alt</kbd>+<kbd>B</kbd></span><span class="shortcut-desc">Show All Bboxes</span></div>
|
|
179
|
+
<div class="shortcut-row"><span class="shortcut-keys"><kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>I</kbd></span><span class="shortcut-desc">Debug Snapshot</span></div></div>"""
|
|
180
|
+
html = html.replace("DEBUG_SHORTCUTS_PLACEHOLDER", debug_shortcuts_html)
|
|
181
|
+
|
|
182
|
+
# Debug meta (server start time) - only shown in debug mode
|
|
183
|
+
html = html.replace(
|
|
184
|
+
"DEBUG_META_DISPLAY_PLACEHOLDER", "" if debug_mode else 'style="display:none"'
|
|
185
|
+
)
|
|
71
186
|
|
|
72
187
|
return html
|
|
73
188
|
|