scitex 2.7.0__py3-none-any.whl → 2.7.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scitex/__init__.py +6 -2
- scitex/__version__.py +1 -1
- scitex/audio/README.md +52 -0
- scitex/audio/__init__.py +384 -0
- scitex/audio/__main__.py +129 -0
- scitex/audio/_tts.py +334 -0
- scitex/audio/engines/__init__.py +44 -0
- scitex/audio/engines/base.py +275 -0
- scitex/audio/engines/elevenlabs_engine.py +143 -0
- scitex/audio/engines/gtts_engine.py +162 -0
- scitex/audio/engines/pyttsx3_engine.py +131 -0
- scitex/audio/mcp_server.py +757 -0
- scitex/bridge/_helpers.py +1 -1
- scitex/bridge/_plt_vis.py +1 -1
- scitex/bridge/_stats_vis.py +1 -1
- scitex/dev/plt/__init__.py +272 -0
- scitex/dev/plt/plot_mpl_axhline.py +28 -0
- scitex/dev/plt/plot_mpl_axhspan.py +28 -0
- scitex/dev/plt/plot_mpl_axvline.py +28 -0
- scitex/dev/plt/plot_mpl_axvspan.py +28 -0
- scitex/dev/plt/plot_mpl_bar.py +29 -0
- scitex/dev/plt/plot_mpl_barh.py +29 -0
- scitex/dev/plt/plot_mpl_boxplot.py +28 -0
- scitex/dev/plt/plot_mpl_contour.py +31 -0
- scitex/dev/plt/plot_mpl_contourf.py +31 -0
- scitex/dev/plt/plot_mpl_errorbar.py +30 -0
- scitex/dev/plt/plot_mpl_eventplot.py +28 -0
- scitex/dev/plt/plot_mpl_fill.py +30 -0
- scitex/dev/plt/plot_mpl_fill_between.py +31 -0
- scitex/dev/plt/plot_mpl_hexbin.py +28 -0
- scitex/dev/plt/plot_mpl_hist.py +28 -0
- scitex/dev/plt/plot_mpl_hist2d.py +28 -0
- scitex/dev/plt/plot_mpl_imshow.py +29 -0
- scitex/dev/plt/plot_mpl_pcolormesh.py +31 -0
- scitex/dev/plt/plot_mpl_pie.py +29 -0
- scitex/dev/plt/plot_mpl_plot.py +29 -0
- scitex/dev/plt/plot_mpl_quiver.py +31 -0
- scitex/dev/plt/plot_mpl_scatter.py +28 -0
- scitex/dev/plt/plot_mpl_stackplot.py +31 -0
- scitex/dev/plt/plot_mpl_stem.py +29 -0
- scitex/dev/plt/plot_mpl_step.py +29 -0
- scitex/dev/plt/plot_mpl_violinplot.py +28 -0
- scitex/dev/plt/plot_sns_barplot.py +29 -0
- scitex/dev/plt/plot_sns_boxplot.py +29 -0
- scitex/dev/plt/plot_sns_heatmap.py +28 -0
- scitex/dev/plt/plot_sns_histplot.py +29 -0
- scitex/dev/plt/plot_sns_kdeplot.py +29 -0
- scitex/dev/plt/plot_sns_lineplot.py +31 -0
- scitex/dev/plt/plot_sns_scatterplot.py +29 -0
- scitex/dev/plt/plot_sns_stripplot.py +29 -0
- scitex/dev/plt/plot_sns_swarmplot.py +29 -0
- scitex/dev/plt/plot_sns_violinplot.py +29 -0
- scitex/dev/plt/plot_stx_bar.py +29 -0
- scitex/dev/plt/plot_stx_barh.py +29 -0
- scitex/dev/plt/plot_stx_box.py +28 -0
- scitex/dev/plt/plot_stx_boxplot.py +28 -0
- scitex/dev/plt/plot_stx_conf_mat.py +28 -0
- scitex/dev/plt/plot_stx_contour.py +31 -0
- scitex/dev/plt/plot_stx_ecdf.py +28 -0
- scitex/dev/plt/plot_stx_errorbar.py +30 -0
- scitex/dev/plt/plot_stx_fill_between.py +31 -0
- scitex/dev/plt/plot_stx_fillv.py +28 -0
- scitex/dev/plt/plot_stx_heatmap.py +28 -0
- scitex/dev/plt/plot_stx_image.py +28 -0
- scitex/dev/plt/plot_stx_imshow.py +28 -0
- scitex/dev/plt/plot_stx_joyplot.py +28 -0
- scitex/dev/plt/plot_stx_kde.py +28 -0
- scitex/dev/plt/plot_stx_line.py +28 -0
- scitex/dev/plt/plot_stx_mean_ci.py +28 -0
- scitex/dev/plt/plot_stx_mean_std.py +28 -0
- scitex/dev/plt/plot_stx_median_iqr.py +28 -0
- scitex/dev/plt/plot_stx_raster.py +28 -0
- scitex/dev/plt/plot_stx_rectangle.py +28 -0
- scitex/dev/plt/plot_stx_scatter.py +29 -0
- scitex/dev/plt/plot_stx_shaded_line.py +29 -0
- scitex/dev/plt/plot_stx_violin.py +28 -0
- scitex/dev/plt/plot_stx_violinplot.py +28 -0
- scitex/fig/__init__.py +352 -0
- scitex/{vis → fig}/backend/_parser.py +1 -1
- scitex/{vis → fig}/canvas.py +1 -1
- scitex/{vis → fig}/editor/_defaults.py +70 -5
- scitex/fig/editor/_edit.py +751 -0
- scitex/{vis → fig}/editor/_qt_editor.py +181 -1
- scitex/fig/editor/flask_editor/_bbox.py +1276 -0
- scitex/fig/editor/flask_editor/_core.py +624 -0
- scitex/{vis → fig}/editor/flask_editor/_plotter.py +38 -4
- scitex/fig/editor/flask_editor/_renderer.py +739 -0
- scitex/{vis → fig}/editor/flask_editor/templates/__init__.py +1 -1
- scitex/fig/editor/flask_editor/templates/_html.py +834 -0
- scitex/fig/editor/flask_editor/templates/_scripts.py +3136 -0
- scitex/{vis → fig}/editor/flask_editor/templates/_styles.py +625 -18
- scitex/{vis → fig}/io/__init__.py +13 -1
- scitex/fig/io/_bundle.py +973 -0
- scitex/{vis → fig}/io/_canvas.py +1 -1
- scitex/{vis → fig}/io/_data.py +1 -1
- scitex/{vis → fig}/io/_export.py +1 -1
- scitex/{vis → fig}/io/_load.py +1 -1
- scitex/{vis → fig}/io/_panel.py +1 -1
- scitex/{vis → fig}/io/_save.py +1 -1
- scitex/{vis → fig}/model/__init__.py +1 -1
- scitex/{vis → fig}/model/_annotations.py +1 -1
- scitex/{vis → fig}/model/_axes.py +1 -1
- scitex/{vis → fig}/model/_figure.py +1 -1
- scitex/{vis → fig}/model/_guides.py +1 -1
- scitex/{vis → fig}/model/_plot.py +1 -1
- scitex/{vis → fig}/model/_styles.py +1 -1
- scitex/{vis → fig}/utils/__init__.py +1 -1
- scitex/io/__init__.py +10 -26
- scitex/io/_bundle.py +434 -0
- scitex/io/_flush.py +5 -2
- scitex/io/_load.py +98 -0
- scitex/io/_load_modules/_H5Explorer.py +5 -2
- scitex/io/_load_modules/_canvas.py +2 -2
- scitex/io/_load_modules/_image.py +3 -4
- scitex/io/_load_modules/_txt.py +4 -2
- scitex/io/_metadata.py +34 -324
- scitex/io/_metadata_modules/__init__.py +46 -0
- scitex/io/_metadata_modules/_embed.py +70 -0
- scitex/io/_metadata_modules/_read.py +64 -0
- scitex/io/_metadata_modules/_utils.py +79 -0
- scitex/io/_metadata_modules/embed_metadata_jpeg.py +74 -0
- scitex/io/_metadata_modules/embed_metadata_pdf.py +53 -0
- scitex/io/_metadata_modules/embed_metadata_png.py +26 -0
- scitex/io/_metadata_modules/embed_metadata_svg.py +62 -0
- scitex/io/_metadata_modules/read_metadata_jpeg.py +57 -0
- scitex/io/_metadata_modules/read_metadata_pdf.py +51 -0
- scitex/io/_metadata_modules/read_metadata_png.py +39 -0
- scitex/io/_metadata_modules/read_metadata_svg.py +44 -0
- scitex/io/_qr_utils.py +5 -3
- scitex/io/_save.py +548 -30
- scitex/io/_save_modules/_canvas.py +3 -3
- scitex/io/_save_modules/_image.py +5 -9
- scitex/io/_save_modules/_tex.py +7 -4
- scitex/io/utils/h5_to_zarr.py +11 -9
- scitex/msword/__init__.py +255 -0
- scitex/msword/profiles.py +357 -0
- scitex/msword/reader.py +753 -0
- scitex/msword/utils.py +289 -0
- scitex/msword/writer.py +362 -0
- scitex/plt/__init__.py +5 -2
- scitex/plt/_subplots/_AxesWrapper.py +6 -6
- scitex/plt/_subplots/_AxisWrapper.py +15 -9
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/__init__.py +36 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_labels.py +264 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_metadata.py +213 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_visual.py +128 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +59 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_base.py +34 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_scientific.py +593 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_statistical.py +654 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_stx_aliases.py +527 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_RawMatplotlibMixin.py +321 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/__init__.py +33 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_base.py +152 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +600 -0
- scitex/plt/_subplots/_AxisWrapperMixins/__init__.py +79 -5
- scitex/plt/_subplots/_FigWrapper.py +6 -6
- scitex/plt/_subplots/_SubplotsWrapper.py +28 -18
- scitex/plt/_subplots/_export_as_csv.py +35 -5
- scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +8 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_annotate.py +10 -21
- scitex/plt/_subplots/_export_as_csv_formatters/_format_eventplot.py +18 -7
- scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow2d.py +28 -12
- scitex/plt/_subplots/_export_as_csv_formatters/_format_matshow.py +10 -4
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_imshow.py +13 -1
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +12 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_scatter.py +10 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_quiver.py +10 -4
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_jointplot.py +18 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_lineplot.py +44 -36
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_pairplot.py +14 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_streamplot.py +11 -5
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_bar.py +84 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_barh.py +85 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_conf_mat.py +14 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_contour.py +54 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_ecdf.py +14 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_errorbar.py +120 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_heatmap.py +16 -6
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_image.py +29 -19
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_imshow.py +63 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_joyplot.py +22 -5
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_ci.py +18 -14
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_std.py +18 -14
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_median_iqr.py +18 -14
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_raster.py +10 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter.py +51 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter_hist.py +18 -9
- scitex/plt/ax/_plot/_stx_ecdf.py +4 -2
- scitex/plt/gallery/_generate.py +421 -14
- scitex/plt/io/__init__.py +53 -0
- scitex/plt/io/_bundle.py +490 -0
- scitex/plt/io/_layered_bundle.py +1343 -0
- scitex/plt/styles/SCITEX_STYLE.yaml +26 -0
- scitex/plt/styles/__init__.py +14 -0
- scitex/plt/styles/presets.py +78 -0
- scitex/plt/utils/__init__.py +13 -1
- scitex/plt/utils/_collect_figure_metadata.py +10 -14
- scitex/plt/utils/_configure_mpl.py +6 -18
- scitex/plt/utils/_crop.py +32 -14
- scitex/plt/utils/_csv_column_naming.py +54 -0
- scitex/plt/utils/_figure_mm.py +116 -1
- scitex/plt/utils/_hitmap.py +1643 -0
- scitex/plt/utils/metadata/__init__.py +25 -0
- scitex/plt/utils/metadata/_core.py +9 -10
- scitex/plt/utils/metadata/_dimensions.py +6 -3
- scitex/plt/utils/metadata/_editable_export.py +405 -0
- scitex/plt/utils/metadata/_geometry_extraction.py +570 -0
- scitex/schema/__init__.py +109 -16
- scitex/schema/_canvas.py +1 -1
- scitex/schema/_plot.py +1015 -0
- scitex/schema/_stats.py +2 -2
- scitex/stats/__init__.py +117 -0
- scitex/stats/io/__init__.py +29 -0
- scitex/stats/io/_bundle.py +156 -0
- scitex/tex/__init__.py +4 -0
- scitex/tex/_export.py +890 -0
- {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/METADATA +11 -1
- {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/RECORD +238 -170
- scitex/io/memo.md +0 -2827
- scitex/plt/REQUESTS.md +0 -191
- scitex/plt/_subplots/TODO.md +0 -53
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin.py +0 -559
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +0 -1609
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +0 -447
- scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_between.json +0 -110
- scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_betweenx.json +0 -88
- scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fill_between.json +0 -103
- scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fillv.json +0 -106
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/bar.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/barh.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/boxplot.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_bar.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_barh.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_box.json +0 -83
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_boxplot.json +0 -93
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violin.json +0 -91
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violinplot.json +0 -91
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/violinplot.json +0 -91
- scitex/plt/templates/research-master/scitex/vis/gallery/contour/contour.json +0 -97
- scitex/plt/templates/research-master/scitex/vis/gallery/contour/contourf.json +0 -98
- scitex/plt/templates/research-master/scitex/vis/gallery/contour/stx_contour.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist.json +0 -101
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist2d.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_ecdf.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_joyplot.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_kde.json +0 -93
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/imshow.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/matshow.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_conf_mat.json +0 -83
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_heatmap.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_image.json +0 -121
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_imshow.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/line/plot.json +0 -110
- scitex/plt/templates/research-master/scitex/vis/gallery/line/step.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_line.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_shaded_line.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/scatter/hexbin.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/scatter/scatter.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stem.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stx_scatter.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/special/pie.json +0 -94
- scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_raster.json +0 -109
- scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_rectangle.json +0 -108
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/errorbar.json +0 -93
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_errorbar.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_ci.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_std.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_median_iqr.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/vector/quiver.json +0 -99
- scitex/plt/templates/research-master/scitex/vis/gallery/vector/streamplot.json +0 -100
- scitex/vis/__init__.py +0 -177
- scitex/vis/editor/_edit.py +0 -390
- scitex/vis/editor/flask_editor/_bbox.py +0 -529
- scitex/vis/editor/flask_editor/_core.py +0 -168
- scitex/vis/editor/flask_editor/_renderer.py +0 -393
- scitex/vis/editor/flask_editor/templates/_html.py +0 -513
- scitex/vis/editor/flask_editor/templates/_scripts.py +0 -1261
- /scitex/{vis → fig}/README.md +0 -0
- /scitex/{vis → fig}/backend/__init__.py +0 -0
- /scitex/{vis → fig}/backend/_export.py +0 -0
- /scitex/{vis → fig}/backend/_render.py +0 -0
- /scitex/{vis → fig}/docs/CANVAS_ARCHITECTURE.md +0 -0
- /scitex/{vis → fig}/editor/__init__.py +0 -0
- /scitex/{vis → fig}/editor/_dearpygui_editor.py +0 -0
- /scitex/{vis → fig}/editor/_flask_editor.py +0 -0
- /scitex/{vis → fig}/editor/_mpl_editor.py +0 -0
- /scitex/{vis → fig}/editor/_tkinter_editor.py +0 -0
- /scitex/{vis → fig}/editor/flask_editor/__init__.py +0 -0
- /scitex/{vis → fig}/editor/flask_editor/_utils.py +0 -0
- /scitex/{vis → fig}/io/_directory.py +0 -0
- /scitex/{vis → fig}/model/_plot_types.py +0 -0
- /scitex/{vis → fig}/utils/_defaults.py +0 -0
- /scitex/{vis → fig}/utils/_validate.py +0 -0
- {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/WHEEL +0 -0
- {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/entry_points.txt +0 -0
- {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,529 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
# File: ./src/scitex/vis/editor/flask_editor/bbox.py
|
|
4
|
-
"""Bounding box extraction for figure elements."""
|
|
5
|
-
|
|
6
|
-
from typing import Dict, Any
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def extract_bboxes(
|
|
10
|
-
fig, ax, renderer, img_width: int, img_height: int
|
|
11
|
-
) -> Dict[str, Any]:
|
|
12
|
-
"""Extract bounding boxes for all figure elements (single-axis)."""
|
|
13
|
-
from matplotlib.transforms import Bbox
|
|
14
|
-
|
|
15
|
-
# Get figure tight bbox in inches
|
|
16
|
-
fig_bbox = fig.get_tightbbox(renderer)
|
|
17
|
-
tight_x0 = fig_bbox.x0
|
|
18
|
-
tight_y0 = fig_bbox.y0
|
|
19
|
-
tight_width = fig_bbox.width
|
|
20
|
-
tight_height = fig_bbox.height
|
|
21
|
-
|
|
22
|
-
# bbox_inches='tight' adds pad_inches (default 0.1) around the tight bbox
|
|
23
|
-
pad_inches = 0.1
|
|
24
|
-
saved_width_inches = tight_width + 2 * pad_inches
|
|
25
|
-
saved_height_inches = tight_height + 2 * pad_inches
|
|
26
|
-
|
|
27
|
-
# Scale factors for converting inches to pixels
|
|
28
|
-
scale_x = img_width / saved_width_inches
|
|
29
|
-
scale_y = img_height / saved_height_inches
|
|
30
|
-
|
|
31
|
-
bboxes = {}
|
|
32
|
-
|
|
33
|
-
def get_element_bbox(element, name):
|
|
34
|
-
"""Get element bbox in image pixel coordinates."""
|
|
35
|
-
try:
|
|
36
|
-
bbox = element.get_window_extent(renderer)
|
|
37
|
-
|
|
38
|
-
elem_x0_inches = bbox.x0 / fig.dpi
|
|
39
|
-
elem_x1_inches = bbox.x1 / fig.dpi
|
|
40
|
-
elem_y0_inches = bbox.y0 / fig.dpi
|
|
41
|
-
elem_y1_inches = bbox.y1 / fig.dpi
|
|
42
|
-
|
|
43
|
-
x0_rel = elem_x0_inches - tight_x0 + pad_inches
|
|
44
|
-
x1_rel = elem_x1_inches - tight_x0 + pad_inches
|
|
45
|
-
y0_rel = saved_height_inches - (elem_y1_inches - tight_y0 + pad_inches)
|
|
46
|
-
y1_rel = saved_height_inches - (elem_y0_inches - tight_y0 + pad_inches)
|
|
47
|
-
|
|
48
|
-
bboxes[name] = {
|
|
49
|
-
"x0": max(0, int(x0_rel * scale_x)),
|
|
50
|
-
"y0": max(0, int(y0_rel * scale_y)),
|
|
51
|
-
"x1": min(img_width, int(x1_rel * scale_x)),
|
|
52
|
-
"y1": min(img_height, int(y1_rel * scale_y)),
|
|
53
|
-
"label": name.replace("_", " ").title(),
|
|
54
|
-
}
|
|
55
|
-
except Exception as e:
|
|
56
|
-
print(f"Error getting bbox for {name}: {e}")
|
|
57
|
-
|
|
58
|
-
def bbox_to_img_coords(bbox):
|
|
59
|
-
"""Convert matplotlib bbox to image pixel coordinates."""
|
|
60
|
-
x0_inches = bbox.x0 / fig.dpi
|
|
61
|
-
y0_inches = bbox.y0 / fig.dpi
|
|
62
|
-
x1_inches = bbox.x1 / fig.dpi
|
|
63
|
-
y1_inches = bbox.y1 / fig.dpi
|
|
64
|
-
x0_rel = x0_inches - tight_x0 + pad_inches
|
|
65
|
-
y0_rel = y0_inches - tight_y0 + pad_inches
|
|
66
|
-
x1_rel = x1_inches - tight_x0 + pad_inches
|
|
67
|
-
y1_rel = y1_inches - tight_y0 + pad_inches
|
|
68
|
-
return {
|
|
69
|
-
"x0": int(x0_rel * scale_x),
|
|
70
|
-
"y0": int((saved_height_inches - y1_rel) * scale_y),
|
|
71
|
-
"x1": int(x1_rel * scale_x),
|
|
72
|
-
"y1": int((saved_height_inches - y0_rel) * scale_y),
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
# Get bboxes for title, labels
|
|
76
|
-
if ax.title.get_text():
|
|
77
|
-
get_element_bbox(ax.title, "title")
|
|
78
|
-
if ax.xaxis.label.get_text():
|
|
79
|
-
get_element_bbox(ax.xaxis.label, "xlabel")
|
|
80
|
-
if ax.yaxis.label.get_text():
|
|
81
|
-
get_element_bbox(ax.yaxis.label, "ylabel")
|
|
82
|
-
|
|
83
|
-
# Get axis bboxes
|
|
84
|
-
_extract_axis_bboxes(ax, renderer, bboxes, bbox_to_img_coords, Bbox)
|
|
85
|
-
|
|
86
|
-
# Get legend bbox
|
|
87
|
-
legend = ax.get_legend()
|
|
88
|
-
if legend:
|
|
89
|
-
get_element_bbox(legend, "legend")
|
|
90
|
-
|
|
91
|
-
# Get trace (line) bboxes
|
|
92
|
-
_extract_trace_bboxes(
|
|
93
|
-
ax,
|
|
94
|
-
fig,
|
|
95
|
-
renderer,
|
|
96
|
-
bboxes,
|
|
97
|
-
get_element_bbox,
|
|
98
|
-
tight_x0,
|
|
99
|
-
tight_y0,
|
|
100
|
-
saved_height_inches,
|
|
101
|
-
scale_x,
|
|
102
|
-
scale_y,
|
|
103
|
-
pad_inches,
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
return bboxes
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def extract_bboxes_multi(
|
|
110
|
-
fig, axes_map: Dict[str, Any], renderer, img_width: int, img_height: int
|
|
111
|
-
) -> Dict[str, Any]:
|
|
112
|
-
"""Extract bounding boxes for all elements in a multi-axis figure.
|
|
113
|
-
|
|
114
|
-
Args:
|
|
115
|
-
fig: Matplotlib figure
|
|
116
|
-
axes_map: Dict mapping axis IDs (e.g., 'ax_00') to matplotlib Axes objects
|
|
117
|
-
renderer: Matplotlib renderer
|
|
118
|
-
img_width: Image width in pixels
|
|
119
|
-
img_height: Image height in pixels
|
|
120
|
-
|
|
121
|
-
Returns:
|
|
122
|
-
Dict with bboxes keyed by "{ax_id}_{element_type}" (e.g., "ax_00_xlabel")
|
|
123
|
-
"""
|
|
124
|
-
from matplotlib.transforms import Bbox
|
|
125
|
-
|
|
126
|
-
# Get figure tight bbox in inches
|
|
127
|
-
fig_bbox = fig.get_tightbbox(renderer)
|
|
128
|
-
tight_x0 = fig_bbox.x0
|
|
129
|
-
tight_y0 = fig_bbox.y0
|
|
130
|
-
tight_width = fig_bbox.width
|
|
131
|
-
tight_height = fig_bbox.height
|
|
132
|
-
|
|
133
|
-
# bbox_inches='tight' adds pad_inches (default 0.1) around the tight bbox
|
|
134
|
-
pad_inches = 0.1
|
|
135
|
-
saved_width_inches = tight_width + 2 * pad_inches
|
|
136
|
-
saved_height_inches = tight_height + 2 * pad_inches
|
|
137
|
-
|
|
138
|
-
# Scale factors for converting inches to pixels
|
|
139
|
-
scale_x = img_width / saved_width_inches
|
|
140
|
-
scale_y = img_height / saved_height_inches
|
|
141
|
-
|
|
142
|
-
bboxes = {}
|
|
143
|
-
|
|
144
|
-
def get_element_bbox(element, name, ax_id, current_ax=None):
|
|
145
|
-
"""Get element bbox in image pixel coordinates."""
|
|
146
|
-
import numpy as np
|
|
147
|
-
full_name = f"{ax_id}_{name}"
|
|
148
|
-
try:
|
|
149
|
-
bbox = element.get_window_extent(renderer)
|
|
150
|
-
|
|
151
|
-
# Check for invalid bbox (infinity or NaN)
|
|
152
|
-
if not (np.isfinite(bbox.x0) and np.isfinite(bbox.x1) and
|
|
153
|
-
np.isfinite(bbox.y0) and np.isfinite(bbox.y1)):
|
|
154
|
-
# Try to get bbox from data for scatter/collection elements
|
|
155
|
-
if hasattr(element, 'get_offsets') and current_ax is not None:
|
|
156
|
-
offsets = element.get_offsets()
|
|
157
|
-
if len(offsets) > 0 and np.isfinite(offsets).all():
|
|
158
|
-
# Use axis transform to get display coordinates
|
|
159
|
-
display_coords = current_ax.transData.transform(offsets)
|
|
160
|
-
x0 = display_coords[:, 0].min()
|
|
161
|
-
x1 = display_coords[:, 0].max()
|
|
162
|
-
y0 = display_coords[:, 1].min()
|
|
163
|
-
y1 = display_coords[:, 1].max()
|
|
164
|
-
if np.isfinite([x0, x1, y0, y1]).all():
|
|
165
|
-
from matplotlib.transforms import Bbox
|
|
166
|
-
bbox = Bbox.from_extents(x0, y0, x1, y1)
|
|
167
|
-
else:
|
|
168
|
-
return # Skip this element
|
|
169
|
-
else:
|
|
170
|
-
return # Skip this element
|
|
171
|
-
else:
|
|
172
|
-
return # Skip this element
|
|
173
|
-
|
|
174
|
-
elem_x0_inches = bbox.x0 / fig.dpi
|
|
175
|
-
elem_x1_inches = bbox.x1 / fig.dpi
|
|
176
|
-
elem_y0_inches = bbox.y0 / fig.dpi
|
|
177
|
-
elem_y1_inches = bbox.y1 / fig.dpi
|
|
178
|
-
|
|
179
|
-
x0_rel = elem_x0_inches - tight_x0 + pad_inches
|
|
180
|
-
x1_rel = elem_x1_inches - tight_x0 + pad_inches
|
|
181
|
-
y0_rel = saved_height_inches - (elem_y1_inches - tight_y0 + pad_inches)
|
|
182
|
-
y1_rel = saved_height_inches - (elem_y0_inches - tight_y0 + pad_inches)
|
|
183
|
-
|
|
184
|
-
# Clamp values to avoid overflow
|
|
185
|
-
x0_px = max(0, min(img_width, int(x0_rel * scale_x)))
|
|
186
|
-
y0_px = max(0, min(img_height, int(y0_rel * scale_y)))
|
|
187
|
-
x1_px = max(0, min(img_width, int(x1_rel * scale_x)))
|
|
188
|
-
y1_px = max(0, min(img_height, int(y1_rel * scale_y)))
|
|
189
|
-
|
|
190
|
-
bboxes[full_name] = {
|
|
191
|
-
"x0": x0_px,
|
|
192
|
-
"y0": y0_px,
|
|
193
|
-
"x1": x1_px,
|
|
194
|
-
"y1": y1_px,
|
|
195
|
-
"label": f"{ax_id}: {name.replace('_', ' ').title()}",
|
|
196
|
-
"ax_id": ax_id,
|
|
197
|
-
}
|
|
198
|
-
except Exception as e:
|
|
199
|
-
print(f"Error getting bbox for {full_name}: {e}")
|
|
200
|
-
|
|
201
|
-
def bbox_to_img_coords(bbox):
|
|
202
|
-
"""Convert matplotlib bbox to image pixel coordinates."""
|
|
203
|
-
x0_inches = bbox.x0 / fig.dpi
|
|
204
|
-
y0_inches = bbox.y0 / fig.dpi
|
|
205
|
-
x1_inches = bbox.x1 / fig.dpi
|
|
206
|
-
y1_inches = bbox.y1 / fig.dpi
|
|
207
|
-
x0_rel = x0_inches - tight_x0 + pad_inches
|
|
208
|
-
y0_rel = y0_inches - tight_y0 + pad_inches
|
|
209
|
-
x1_rel = x1_inches - tight_x0 + pad_inches
|
|
210
|
-
y1_rel = y1_inches - tight_y0 + pad_inches
|
|
211
|
-
return {
|
|
212
|
-
"x0": int(x0_rel * scale_x),
|
|
213
|
-
"y0": int((saved_height_inches - y1_rel) * scale_y),
|
|
214
|
-
"x1": int(x1_rel * scale_x),
|
|
215
|
-
"y1": int((saved_height_inches - y0_rel) * scale_y),
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
# Extract bboxes for each axis
|
|
219
|
-
for ax_id, ax in axes_map.items():
|
|
220
|
-
# Get axes bounding box (the entire panel area)
|
|
221
|
-
try:
|
|
222
|
-
ax_bbox = ax.get_window_extent(renderer)
|
|
223
|
-
coords = bbox_to_img_coords(ax_bbox)
|
|
224
|
-
bboxes[f"{ax_id}_panel"] = {
|
|
225
|
-
**coords,
|
|
226
|
-
"label": f"Panel {ax_id}",
|
|
227
|
-
"ax_id": ax_id,
|
|
228
|
-
"is_panel": True,
|
|
229
|
-
}
|
|
230
|
-
except Exception as e:
|
|
231
|
-
print(f"Error getting panel bbox for {ax_id}: {e}")
|
|
232
|
-
|
|
233
|
-
# Get bboxes for title, labels
|
|
234
|
-
if ax.title.get_text():
|
|
235
|
-
get_element_bbox(ax.title, "title", ax_id, ax)
|
|
236
|
-
if ax.xaxis.label.get_text():
|
|
237
|
-
get_element_bbox(ax.xaxis.label, "xlabel", ax_id, ax)
|
|
238
|
-
if ax.yaxis.label.get_text():
|
|
239
|
-
get_element_bbox(ax.yaxis.label, "ylabel", ax_id, ax)
|
|
240
|
-
|
|
241
|
-
# Get legend bbox
|
|
242
|
-
legend = ax.get_legend()
|
|
243
|
-
if legend:
|
|
244
|
-
get_element_bbox(legend, "legend", ax_id, ax)
|
|
245
|
-
|
|
246
|
-
# Get trace (line) bboxes
|
|
247
|
-
_extract_trace_bboxes_for_axis(
|
|
248
|
-
ax,
|
|
249
|
-
ax_id,
|
|
250
|
-
fig,
|
|
251
|
-
renderer,
|
|
252
|
-
bboxes,
|
|
253
|
-
get_element_bbox,
|
|
254
|
-
tight_x0,
|
|
255
|
-
tight_y0,
|
|
256
|
-
saved_height_inches,
|
|
257
|
-
scale_x,
|
|
258
|
-
scale_y,
|
|
259
|
-
pad_inches,
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
return bboxes
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
def _extract_trace_bboxes_for_axis(
|
|
266
|
-
ax,
|
|
267
|
-
ax_id,
|
|
268
|
-
fig,
|
|
269
|
-
renderer,
|
|
270
|
-
bboxes,
|
|
271
|
-
get_element_bbox,
|
|
272
|
-
tight_x0,
|
|
273
|
-
tight_y0,
|
|
274
|
-
saved_height_inches,
|
|
275
|
-
scale_x,
|
|
276
|
-
scale_y,
|
|
277
|
-
pad_inches,
|
|
278
|
-
):
|
|
279
|
-
"""Extract bboxes for all data elements in a specific axis.
|
|
280
|
-
|
|
281
|
-
Handles:
|
|
282
|
-
- Lines (plot, errorbar lines)
|
|
283
|
-
- Scatter points (PathCollection)
|
|
284
|
-
- Fill areas (PolyCollection from fill_between)
|
|
285
|
-
- Bars (Rectangle patches)
|
|
286
|
-
"""
|
|
287
|
-
import numpy as np
|
|
288
|
-
|
|
289
|
-
def coords_to_img_points(data_coords):
|
|
290
|
-
"""Convert data coordinates to image pixel coordinates."""
|
|
291
|
-
if len(data_coords) == 0:
|
|
292
|
-
return []
|
|
293
|
-
transform = ax.transData
|
|
294
|
-
points_display = transform.transform(data_coords)
|
|
295
|
-
points_img = []
|
|
296
|
-
for px, py in points_display:
|
|
297
|
-
# Skip invalid points (NaN, infinity)
|
|
298
|
-
if not np.isfinite(px) or not np.isfinite(py):
|
|
299
|
-
continue
|
|
300
|
-
px_inches = px / fig.dpi
|
|
301
|
-
py_inches = py / fig.dpi
|
|
302
|
-
x_rel = px_inches - tight_x0 + pad_inches
|
|
303
|
-
y_rel = saved_height_inches - (py_inches - tight_y0 + pad_inches)
|
|
304
|
-
# Clamp to reasonable bounds to avoid overflow
|
|
305
|
-
x_img = max(-10000, min(10000, int(x_rel * scale_x)))
|
|
306
|
-
y_img = max(-10000, min(10000, int(y_rel * scale_y)))
|
|
307
|
-
points_img.append([x_img, y_img])
|
|
308
|
-
# Downsample if too many
|
|
309
|
-
if len(points_img) > 100:
|
|
310
|
-
step = len(points_img) // 100
|
|
311
|
-
points_img = points_img[::step]
|
|
312
|
-
return points_img
|
|
313
|
-
|
|
314
|
-
# 1. Extract lines (plot, errorbar lines, etc.)
|
|
315
|
-
line_idx = 0
|
|
316
|
-
for line in ax.get_lines():
|
|
317
|
-
try:
|
|
318
|
-
label = line.get_label()
|
|
319
|
-
if label.startswith("_"):
|
|
320
|
-
continue
|
|
321
|
-
|
|
322
|
-
trace_name = f"trace_{line_idx}"
|
|
323
|
-
full_name = f"{ax_id}_{trace_name}"
|
|
324
|
-
get_element_bbox(line, trace_name, ax_id, ax)
|
|
325
|
-
|
|
326
|
-
if full_name in bboxes:
|
|
327
|
-
bboxes[full_name]["label"] = f"{ax_id}: {label or f'Line {line_idx}'}"
|
|
328
|
-
bboxes[full_name]["trace_idx"] = line_idx
|
|
329
|
-
bboxes[full_name]["element_type"] = "line"
|
|
330
|
-
|
|
331
|
-
xdata, ydata = line.get_xdata(), line.get_ydata()
|
|
332
|
-
if len(xdata) > 0:
|
|
333
|
-
bboxes[full_name]["points"] = coords_to_img_points(
|
|
334
|
-
list(zip(xdata, ydata))
|
|
335
|
-
)
|
|
336
|
-
line_idx += 1
|
|
337
|
-
except Exception as e:
|
|
338
|
-
print(f"Error getting line bbox for {ax_id}: {e}")
|
|
339
|
-
|
|
340
|
-
# 2. Extract collections (scatter, fill_between, etc.)
|
|
341
|
-
coll_idx = 0
|
|
342
|
-
for coll in ax.collections:
|
|
343
|
-
try:
|
|
344
|
-
label = coll.get_label()
|
|
345
|
-
if label.startswith("_"):
|
|
346
|
-
# Still extract unlabeled collections but with generic name
|
|
347
|
-
label = None
|
|
348
|
-
|
|
349
|
-
coll_type = type(coll).__name__
|
|
350
|
-
if coll_type == "PathCollection":
|
|
351
|
-
# Scatter points
|
|
352
|
-
element_name = f"scatter_{coll_idx}"
|
|
353
|
-
full_name = f"{ax_id}_{element_name}"
|
|
354
|
-
get_element_bbox(coll, element_name, ax_id, ax)
|
|
355
|
-
|
|
356
|
-
if full_name in bboxes:
|
|
357
|
-
bboxes[full_name]["label"] = f"{ax_id}: {label or f'Scatter {coll_idx}'}"
|
|
358
|
-
bboxes[full_name]["element_type"] = "scatter"
|
|
359
|
-
|
|
360
|
-
# Get scatter point positions
|
|
361
|
-
offsets = coll.get_offsets()
|
|
362
|
-
if len(offsets) > 0:
|
|
363
|
-
bboxes[full_name]["points"] = coords_to_img_points(offsets)
|
|
364
|
-
|
|
365
|
-
elif coll_type == "PolyCollection":
|
|
366
|
-
# Fill areas (fill_between, etc.)
|
|
367
|
-
element_name = f"fill_{coll_idx}"
|
|
368
|
-
full_name = f"{ax_id}_{element_name}"
|
|
369
|
-
get_element_bbox(coll, element_name, ax_id, ax)
|
|
370
|
-
|
|
371
|
-
if full_name in bboxes:
|
|
372
|
-
bboxes[full_name]["label"] = f"{ax_id}: {label or f'Fill {coll_idx}'}"
|
|
373
|
-
bboxes[full_name]["element_type"] = "fill"
|
|
374
|
-
|
|
375
|
-
coll_idx += 1
|
|
376
|
-
except Exception as e:
|
|
377
|
-
print(f"Error getting collection bbox for {ax_id}: {e}")
|
|
378
|
-
|
|
379
|
-
# 3. Extract patches (bars, rectangles, etc.)
|
|
380
|
-
patch_idx = 0
|
|
381
|
-
for patch in ax.patches:
|
|
382
|
-
try:
|
|
383
|
-
label = patch.get_label()
|
|
384
|
-
patch_type = type(patch).__name__
|
|
385
|
-
|
|
386
|
-
if patch_type == "Rectangle":
|
|
387
|
-
# Bar chart bars
|
|
388
|
-
element_name = f"bar_{patch_idx}"
|
|
389
|
-
full_name = f"{ax_id}_{element_name}"
|
|
390
|
-
get_element_bbox(patch, element_name, ax_id, ax)
|
|
391
|
-
|
|
392
|
-
if full_name in bboxes:
|
|
393
|
-
bboxes[full_name]["label"] = f"{ax_id}: {label or f'Bar {patch_idx}'}"
|
|
394
|
-
bboxes[full_name]["element_type"] = "bar"
|
|
395
|
-
|
|
396
|
-
patch_idx += 1
|
|
397
|
-
except Exception as e:
|
|
398
|
-
print(f"Error getting patch bbox for {ax_id}: {e}")
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
def _extract_axis_bboxes(ax, renderer, bboxes, bbox_to_img_coords, Bbox):
|
|
402
|
-
"""Extract bboxes for X and Y axis elements."""
|
|
403
|
-
try:
|
|
404
|
-
# X-axis: combine spine and tick labels into one bbox
|
|
405
|
-
x_axis_bboxes = []
|
|
406
|
-
for ticklabel in ax.xaxis.get_ticklabels():
|
|
407
|
-
if ticklabel.get_visible():
|
|
408
|
-
try:
|
|
409
|
-
tb = ticklabel.get_window_extent(renderer)
|
|
410
|
-
if tb.width > 0:
|
|
411
|
-
x_axis_bboxes.append(tb)
|
|
412
|
-
except Exception:
|
|
413
|
-
pass
|
|
414
|
-
for tick in ax.xaxis.get_major_ticks():
|
|
415
|
-
if tick.tick1line.get_visible():
|
|
416
|
-
try:
|
|
417
|
-
tb = tick.tick1line.get_window_extent(renderer)
|
|
418
|
-
if tb.width > 0 or tb.height > 0:
|
|
419
|
-
x_axis_bboxes.append(tb)
|
|
420
|
-
except Exception:
|
|
421
|
-
pass
|
|
422
|
-
spine_bbox = ax.spines["bottom"].get_window_extent(renderer)
|
|
423
|
-
if spine_bbox.width > 0:
|
|
424
|
-
if x_axis_bboxes:
|
|
425
|
-
tick_union = Bbox.union(x_axis_bboxes)
|
|
426
|
-
constrained_spine = Bbox.from_extents(
|
|
427
|
-
tick_union.x0, spine_bbox.y0, tick_union.x1, spine_bbox.y1
|
|
428
|
-
)
|
|
429
|
-
x_axis_bboxes.append(constrained_spine)
|
|
430
|
-
else:
|
|
431
|
-
x_axis_bboxes.append(spine_bbox)
|
|
432
|
-
if x_axis_bboxes:
|
|
433
|
-
combined = Bbox.union(x_axis_bboxes)
|
|
434
|
-
bboxes["xaxis_ticks"] = bbox_to_img_coords(combined)
|
|
435
|
-
bboxes["xaxis_ticks"]["label"] = "X Spine & Ticks"
|
|
436
|
-
|
|
437
|
-
# Y-axis: combine spine and tick labels into one bbox
|
|
438
|
-
y_axis_bboxes = []
|
|
439
|
-
for ticklabel in ax.yaxis.get_ticklabels():
|
|
440
|
-
if ticklabel.get_visible():
|
|
441
|
-
try:
|
|
442
|
-
tb = ticklabel.get_window_extent(renderer)
|
|
443
|
-
if tb.width > 0:
|
|
444
|
-
y_axis_bboxes.append(tb)
|
|
445
|
-
except Exception:
|
|
446
|
-
pass
|
|
447
|
-
for tick in ax.yaxis.get_major_ticks():
|
|
448
|
-
if tick.tick1line.get_visible():
|
|
449
|
-
try:
|
|
450
|
-
tb = tick.tick1line.get_window_extent(renderer)
|
|
451
|
-
if tb.width > 0 or tb.height > 0:
|
|
452
|
-
y_axis_bboxes.append(tb)
|
|
453
|
-
except Exception:
|
|
454
|
-
pass
|
|
455
|
-
spine_bbox = ax.spines["left"].get_window_extent(renderer)
|
|
456
|
-
if spine_bbox.height > 0:
|
|
457
|
-
if y_axis_bboxes:
|
|
458
|
-
tick_union = Bbox.union(y_axis_bboxes)
|
|
459
|
-
constrained_spine = Bbox.from_extents(
|
|
460
|
-
spine_bbox.x0, tick_union.y0, spine_bbox.x1, tick_union.y1
|
|
461
|
-
)
|
|
462
|
-
y_axis_bboxes.append(constrained_spine)
|
|
463
|
-
else:
|
|
464
|
-
y_axis_bboxes.append(spine_bbox)
|
|
465
|
-
if y_axis_bboxes:
|
|
466
|
-
combined = Bbox.union(y_axis_bboxes)
|
|
467
|
-
padded = Bbox.from_extents(
|
|
468
|
-
combined.x0 - 10, combined.y0 - 5, combined.x1 + 5, combined.y1 + 5
|
|
469
|
-
)
|
|
470
|
-
bboxes["yaxis_ticks"] = bbox_to_img_coords(padded)
|
|
471
|
-
bboxes["yaxis_ticks"]["label"] = "Y Spine & Ticks"
|
|
472
|
-
|
|
473
|
-
except Exception as e:
|
|
474
|
-
print(f"Error getting axis bboxes: {e}")
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
def _extract_trace_bboxes(
|
|
478
|
-
ax,
|
|
479
|
-
fig,
|
|
480
|
-
renderer,
|
|
481
|
-
bboxes,
|
|
482
|
-
get_element_bbox,
|
|
483
|
-
tight_x0,
|
|
484
|
-
tight_y0,
|
|
485
|
-
saved_height_inches,
|
|
486
|
-
scale_x,
|
|
487
|
-
scale_y,
|
|
488
|
-
pad_inches,
|
|
489
|
-
):
|
|
490
|
-
"""Extract bboxes for trace (line) elements with proximity detection points."""
|
|
491
|
-
for idx, line in enumerate(ax.get_lines()):
|
|
492
|
-
try:
|
|
493
|
-
label = line.get_label()
|
|
494
|
-
if label.startswith("_"):
|
|
495
|
-
continue
|
|
496
|
-
get_element_bbox(line, f"trace_{idx}")
|
|
497
|
-
if f"trace_{idx}" in bboxes:
|
|
498
|
-
bboxes[f"trace_{idx}"]["label"] = label or f"Trace {idx}"
|
|
499
|
-
bboxes[f"trace_{idx}"]["trace_idx"] = idx
|
|
500
|
-
|
|
501
|
-
# Get line data points in pixel coordinates for proximity detection
|
|
502
|
-
xdata, ydata = line.get_xdata(), line.get_ydata()
|
|
503
|
-
if len(xdata) > 0:
|
|
504
|
-
transform = ax.transData
|
|
505
|
-
points_display = transform.transform(list(zip(xdata, ydata)))
|
|
506
|
-
|
|
507
|
-
points_img = []
|
|
508
|
-
for px, py in points_display:
|
|
509
|
-
px_inches = px / fig.dpi
|
|
510
|
-
py_inches = py / fig.dpi
|
|
511
|
-
x_rel = px_inches - tight_x0 + pad_inches
|
|
512
|
-
y_rel = saved_height_inches - (
|
|
513
|
-
py_inches - tight_y0 + pad_inches
|
|
514
|
-
)
|
|
515
|
-
x_img = int(x_rel * scale_x)
|
|
516
|
-
y_img = int(y_rel * scale_y)
|
|
517
|
-
points_img.append([x_img, y_img])
|
|
518
|
-
|
|
519
|
-
# Downsample points if too many
|
|
520
|
-
if len(points_img) > 100:
|
|
521
|
-
step = len(points_img) // 100
|
|
522
|
-
points_img = points_img[::step]
|
|
523
|
-
|
|
524
|
-
bboxes[f"trace_{idx}"]["points"] = points_img
|
|
525
|
-
except Exception as e:
|
|
526
|
-
print(f"Error getting trace bbox: {e}")
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
# EOF
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
# File: ./src/scitex/vis/editor/flask_editor/core.py
|
|
4
|
-
"""Core WebEditor class for Flask-based figure editing."""
|
|
5
|
-
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Dict, Any, Optional
|
|
8
|
-
import copy
|
|
9
|
-
import json
|
|
10
|
-
import threading
|
|
11
|
-
import webbrowser
|
|
12
|
-
|
|
13
|
-
from ._utils import find_available_port, kill_process_on_port, check_port_available
|
|
14
|
-
from ._renderer import render_preview_with_bboxes
|
|
15
|
-
from .templates import build_html_template
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class WebEditor:
|
|
19
|
-
"""
|
|
20
|
-
Browser-based figure editor using Flask.
|
|
21
|
-
|
|
22
|
-
Features:
|
|
23
|
-
- Modern responsive UI
|
|
24
|
-
- Real-time preview via WebSocket or polling
|
|
25
|
-
- Property editors with sliders and color pickers
|
|
26
|
-
- Save to .manual.json
|
|
27
|
-
- SciTeX style defaults pre-filled
|
|
28
|
-
- Auto-finds available port if default is in use
|
|
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
|
-
port: int = 5050,
|
|
39
|
-
):
|
|
40
|
-
self.json_path = Path(json_path)
|
|
41
|
-
self.metadata = metadata
|
|
42
|
-
self.csv_data = csv_data
|
|
43
|
-
self.png_path = Path(png_path) if png_path else None
|
|
44
|
-
self.manual_overrides = manual_overrides or {}
|
|
45
|
-
self._requested_port = port
|
|
46
|
-
self.port = port
|
|
47
|
-
|
|
48
|
-
# Get SciTeX defaults and merge with metadata
|
|
49
|
-
from .._defaults import get_scitex_defaults, extract_defaults_from_metadata
|
|
50
|
-
|
|
51
|
-
self.scitex_defaults = get_scitex_defaults()
|
|
52
|
-
self.metadata_defaults = extract_defaults_from_metadata(metadata)
|
|
53
|
-
|
|
54
|
-
# Start with defaults, then overlay manual overrides
|
|
55
|
-
self.current_overrides = copy.deepcopy(self.scitex_defaults)
|
|
56
|
-
self.current_overrides.update(self.metadata_defaults)
|
|
57
|
-
self.current_overrides.update(self.manual_overrides)
|
|
58
|
-
|
|
59
|
-
# Track initial state to detect modifications
|
|
60
|
-
self._initial_overrides = copy.deepcopy(self.current_overrides)
|
|
61
|
-
self._user_modified = False
|
|
62
|
-
|
|
63
|
-
def run(self):
|
|
64
|
-
"""Launch the web editor."""
|
|
65
|
-
try:
|
|
66
|
-
from flask import Flask, render_template_string, request, jsonify
|
|
67
|
-
except ImportError:
|
|
68
|
-
raise ImportError(
|
|
69
|
-
"Flask is required for web editor. Install: pip install flask"
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
# Handle port conflicts
|
|
73
|
-
if not check_port_available(self._requested_port):
|
|
74
|
-
print(f"Port {self._requested_port} is in use. Attempting to free it...")
|
|
75
|
-
if kill_process_on_port(self._requested_port):
|
|
76
|
-
import time
|
|
77
|
-
|
|
78
|
-
time.sleep(0.5)
|
|
79
|
-
self.port = self._requested_port
|
|
80
|
-
print(f"Successfully freed port {self.port}")
|
|
81
|
-
else:
|
|
82
|
-
self.port = find_available_port(self._requested_port + 1)
|
|
83
|
-
print(f"Using alternative port: {self.port}")
|
|
84
|
-
else:
|
|
85
|
-
self.port = self._requested_port
|
|
86
|
-
|
|
87
|
-
app = Flask(__name__)
|
|
88
|
-
editor = self
|
|
89
|
-
|
|
90
|
-
@app.route("/")
|
|
91
|
-
def index():
|
|
92
|
-
# Rebuild template each time for hot reload support
|
|
93
|
-
html_template = build_html_template()
|
|
94
|
-
return render_template_string(
|
|
95
|
-
html_template,
|
|
96
|
-
filename=str(editor.json_path.resolve()),
|
|
97
|
-
overrides=json.dumps(editor.current_overrides),
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
@app.route("/preview")
|
|
101
|
-
def preview():
|
|
102
|
-
"""Generate figure preview as base64 PNG with element bboxes."""
|
|
103
|
-
img_data, bboxes, img_size = render_preview_with_bboxes(
|
|
104
|
-
editor.csv_data, editor.current_overrides,
|
|
105
|
-
metadata=editor.metadata
|
|
106
|
-
)
|
|
107
|
-
return jsonify({"image": img_data, "bboxes": bboxes, "img_size": img_size})
|
|
108
|
-
|
|
109
|
-
@app.route("/update", methods=["POST"])
|
|
110
|
-
def update():
|
|
111
|
-
"""Update overrides and return new preview."""
|
|
112
|
-
data = request.json
|
|
113
|
-
editor.current_overrides.update(data.get("overrides", {}))
|
|
114
|
-
editor._user_modified = True
|
|
115
|
-
img_data, bboxes, img_size = render_preview_with_bboxes(
|
|
116
|
-
editor.csv_data, editor.current_overrides,
|
|
117
|
-
metadata=editor.metadata
|
|
118
|
-
)
|
|
119
|
-
return jsonify(
|
|
120
|
-
{
|
|
121
|
-
"image": img_data,
|
|
122
|
-
"bboxes": bboxes,
|
|
123
|
-
"img_size": img_size,
|
|
124
|
-
"status": "updated",
|
|
125
|
-
}
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
@app.route("/save", methods=["POST"])
|
|
129
|
-
def save():
|
|
130
|
-
"""Save to .manual.json."""
|
|
131
|
-
from .._edit import save_manual_overrides
|
|
132
|
-
|
|
133
|
-
try:
|
|
134
|
-
manual_path = save_manual_overrides(
|
|
135
|
-
editor.json_path, editor.current_overrides
|
|
136
|
-
)
|
|
137
|
-
return jsonify({"status": "saved", "path": str(manual_path)})
|
|
138
|
-
except Exception as e:
|
|
139
|
-
return jsonify({"status": "error", "message": str(e)}), 500
|
|
140
|
-
|
|
141
|
-
@app.route("/shutdown", methods=["POST"])
|
|
142
|
-
def shutdown():
|
|
143
|
-
"""Shutdown the server."""
|
|
144
|
-
func = request.environ.get("werkzeug.server.shutdown")
|
|
145
|
-
if func is None:
|
|
146
|
-
raise RuntimeError("Not running with Werkzeug Server")
|
|
147
|
-
func()
|
|
148
|
-
return jsonify({"status": "shutdown"})
|
|
149
|
-
|
|
150
|
-
# Open browser after short delay
|
|
151
|
-
def open_browser():
|
|
152
|
-
import time
|
|
153
|
-
|
|
154
|
-
time.sleep(0.5)
|
|
155
|
-
webbrowser.open(f"http://127.0.0.1:{self.port}")
|
|
156
|
-
|
|
157
|
-
threading.Thread(target=open_browser, daemon=True).start()
|
|
158
|
-
|
|
159
|
-
print(f"Starting SciTeX Editor at http://127.0.0.1:{self.port}")
|
|
160
|
-
print("Press Ctrl+C to stop")
|
|
161
|
-
|
|
162
|
-
# Note: use_reloader=False because the reloader re-runs the entire script
|
|
163
|
-
# which causes infinite loops when the demo generates figures
|
|
164
|
-
# Templates are rebuilt on each page refresh anyway
|
|
165
|
-
app.run(host="127.0.0.1", port=self.port, debug=False, use_reloader=False)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
# EOF
|