scitex 2.8.1__py3-none-any.whl → 2.10.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.
- scitex/__init__.py +15 -7
- scitex/__version__.py +1 -2
- scitex/_install_guide.py +250 -0
- scitex/_optional_deps.py +206 -39
- scitex/ai/_gen_ai/_Groq.py +2 -4
- scitex/ai/_gen_ai/_OpenAI.py +5 -2
- scitex/ai/_gen_ai/_Perplexity.py +20 -6
- scitex/audio/__init__.py +24 -15
- scitex/audio/_cross_process_lock.py +139 -0
- scitex/audio/_mcp_handlers.py +256 -0
- scitex/audio/_mcp_tool_schemas.py +203 -0
- scitex/audio/engines/elevenlabs_engine.py +5 -2
- scitex/audio/mcp_server.py +98 -457
- scitex/bridge/__init__.py +30 -19
- scitex/bridge/_figrecipe.py +245 -0
- scitex/bridge/_helpers.py +2 -1
- scitex/bridge/_plt_vis.py +23 -10
- scitex/bridge/_stats_plt.py +18 -5
- scitex/bridge/_stats_vis.py +16 -2
- scitex/browser/__init__.py +84 -44
- scitex/browser/automation/__init__.py +5 -1
- scitex/browser/core/BrowserMixin.py +17 -4
- scitex/browser/core/__init__.py +11 -2
- scitex/browser/remote/CaptchaHandler.py +1 -1
- scitex/browser/remote/ZenRowsAPIClient.py +1 -1
- scitex/capture/grid.py +487 -0
- scitex/capture/mcp_handlers.py +401 -0
- scitex/capture/mcp_tool_defs.py +192 -0
- scitex/capture/mcp_tools.py +241 -0
- scitex/capture/mcp_utils.py +30 -0
- scitex/cli/convert.py +421 -0
- scitex/cli/main.py +6 -4
- scitex/datetime/__init__.py +46 -0
- scitex/datetime/_linspace.py +100 -0
- scitex/datetime/_normalize_timestamp.py +306 -0
- scitex/db/_delete_duplicates.py +4 -4
- scitex/db/_sqlite3/_delete_duplicates.py +11 -2
- scitex/dev/plt/__init__.py +61 -62
- scitex/dev/plt/demo_plotters/__init__.py +0 -0
- scitex/dev/plt/demo_plotters/plot_mpl_axhline.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_axhspan.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_axvline.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_axvspan.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_bar.py +29 -0
- scitex/dev/plt/demo_plotters/plot_mpl_barh.py +29 -0
- scitex/dev/plt/demo_plotters/plot_mpl_boxplot.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_contour.py +31 -0
- scitex/dev/plt/demo_plotters/plot_mpl_contourf.py +31 -0
- scitex/dev/plt/demo_plotters/plot_mpl_errorbar.py +30 -0
- scitex/dev/plt/demo_plotters/plot_mpl_eventplot.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_fill.py +30 -0
- scitex/dev/plt/demo_plotters/plot_mpl_fill_between.py +31 -0
- scitex/dev/plt/demo_plotters/plot_mpl_hexbin.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_hist.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_hist2d.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_imshow.py +29 -0
- scitex/dev/plt/demo_plotters/plot_mpl_pcolormesh.py +31 -0
- scitex/dev/plt/demo_plotters/plot_mpl_pie.py +29 -0
- scitex/dev/plt/demo_plotters/plot_mpl_plot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_mpl_quiver.py +31 -0
- scitex/dev/plt/demo_plotters/plot_mpl_scatter.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_stackplot.py +31 -0
- scitex/dev/plt/demo_plotters/plot_mpl_stem.py +29 -0
- scitex/dev/plt/demo_plotters/plot_mpl_step.py +29 -0
- scitex/dev/plt/demo_plotters/plot_mpl_violinplot.py +28 -0
- scitex/dev/plt/demo_plotters/plot_sns_barplot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_sns_boxplot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_sns_heatmap.py +28 -0
- scitex/dev/plt/demo_plotters/plot_sns_histplot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_sns_kdeplot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_sns_lineplot.py +31 -0
- scitex/dev/plt/demo_plotters/plot_sns_scatterplot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_sns_stripplot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_sns_swarmplot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_sns_violinplot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_stx_bar.py +29 -0
- scitex/dev/plt/demo_plotters/plot_stx_barh.py +29 -0
- scitex/dev/plt/demo_plotters/plot_stx_box.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_boxplot.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_conf_mat.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_contour.py +31 -0
- scitex/dev/plt/demo_plotters/plot_stx_ecdf.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_errorbar.py +30 -0
- scitex/dev/plt/demo_plotters/plot_stx_fill_between.py +31 -0
- scitex/dev/plt/demo_plotters/plot_stx_fillv.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_heatmap.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_image.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_imshow.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_joyplot.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_kde.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_line.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_mean_ci.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_mean_std.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_median_iqr.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_raster.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_rectangle.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_scatter.py +29 -0
- scitex/dev/plt/demo_plotters/plot_stx_shaded_line.py +29 -0
- scitex/dev/plt/demo_plotters/plot_stx_violin.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_violinplot.py +28 -0
- scitex/dev/plt/mpl/get_dir_ax.py +46 -0
- scitex/dev/plt/mpl/get_signatures.py +176 -0
- scitex/dev/plt/mpl/get_signatures_details.py +522 -0
- scitex/dict/_pop_keys.py +1 -7
- scitex/dsp/__init__.py +15 -10
- scitex/dsp/add_noise.py +5 -2
- scitex/dsp/example.py +35 -22
- scitex/dsp/filt.py +8 -3
- scitex/dsp/reference.py +3 -2
- scitex/dsp/utils/__init__.py +2 -1
- scitex/dsp/utils/_differential_bandpass_filters.py +14 -4
- scitex/dt/__init__.py +39 -2
- scitex/errors.py +82 -521
- scitex/fig/__init__.py +4 -4
- scitex/fig/editor/edit/panel_loader.py +1 -1
- scitex/fig/io/_bundle.py +7 -7
- scitex/fts/README.md +262 -0
- scitex/fts/TODO.md +66 -0
- scitex/fts/__init__.py +90 -0
- scitex/fts/_bundle/README_IN_BUNDLE.md +102 -0
- scitex/fts/_bundle/_FTS.py +657 -0
- scitex/fts/_bundle/__init__.py +38 -0
- scitex/fts/_bundle/_children.py +216 -0
- scitex/fts/_bundle/_conversion/__init__.py +15 -0
- scitex/fts/_bundle/_conversion/_bundle2dict.py +44 -0
- scitex/fts/_bundle/_conversion/_dict2bundle.py +50 -0
- scitex/fts/_bundle/_dataclasses/_Axes.py +57 -0
- scitex/fts/_bundle/_dataclasses/_BBox.py +54 -0
- scitex/fts/_bundle/_dataclasses/_ColumnDef.py +72 -0
- scitex/fts/_bundle/_dataclasses/_DataFormat.py +40 -0
- scitex/fts/_bundle/_dataclasses/_DataInfo.py +135 -0
- scitex/fts/_bundle/_dataclasses/_DataSource.py +44 -0
- scitex/fts/_bundle/_dataclasses/_Node.py +319 -0
- scitex/fts/_bundle/_dataclasses/_NodeRefs.py +45 -0
- scitex/fts/_bundle/_dataclasses/_SizeMM.py +38 -0
- scitex/fts/_bundle/_dataclasses/__init__.py +35 -0
- scitex/fts/_bundle/_extractors/__init__.py +32 -0
- scitex/fts/_bundle/_extractors/_extract_bar.py +131 -0
- scitex/fts/_bundle/_extractors/_extract_line.py +71 -0
- scitex/fts/_bundle/_extractors/_extract_scatter.py +79 -0
- scitex/fts/_bundle/_loader.py +134 -0
- scitex/fts/_bundle/_mpl_helpers.py +389 -0
- scitex/fts/_bundle/_saver.py +269 -0
- scitex/fts/_bundle/_storage.py +200 -0
- scitex/fts/_bundle/_utils/__init__.py +55 -0
- scitex/fts/_bundle/_utils/_const.py +26 -0
- scitex/fts/_bundle/_utils/_errors.py +73 -0
- scitex/fts/_bundle/_utils/_generate.py +21 -0
- scitex/fts/_bundle/_utils/_types.py +76 -0
- scitex/fts/_bundle/_validation.py +434 -0
- scitex/fts/_bundle/_zipbundle.py +165 -0
- scitex/fts/_fig/__init__.py +22 -0
- scitex/fts/_fig/_backend/__init__.py +53 -0
- scitex/fts/_fig/_backend/_export.py +165 -0
- scitex/fts/_fig/_backend/_parser.py +188 -0
- scitex/fts/_fig/_backend/_render.py +538 -0
- scitex/fts/_fig/_composite.py +345 -0
- scitex/fts/_fig/_dataclasses/_ChannelEncoding.py +46 -0
- scitex/fts/_fig/_dataclasses/_Encoding.py +82 -0
- scitex/fts/_fig/_dataclasses/_Theme.py +441 -0
- scitex/fts/_fig/_dataclasses/_TraceEncoding.py +52 -0
- scitex/fts/_fig/_dataclasses/__init__.py +47 -0
- scitex/fts/_fig/_editor/__init__.py +14 -0
- scitex/fts/_fig/_editor/_cui/__init__.py +33 -0
- scitex/fts/_fig/_editor/_cui/_backend_detector.py +39 -0
- scitex/fts/_fig/_editor/_cui/_bundle_resolver.py +366 -0
- scitex/fts/_fig/_editor/_cui/_editor_launcher.py +175 -0
- scitex/fts/_fig/_editor/_cui/_manual_handler.py +52 -0
- scitex/fts/_fig/_editor/_cui/_panel_loader.py +246 -0
- scitex/fts/_fig/_editor/_cui/_path_resolver.py +66 -0
- scitex/fts/_fig/_editor/_defaults.py +300 -0
- scitex/fts/_fig/_editor/_gui/__init__.py +11 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/__init__.py +20 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/_bbox.py +1339 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/_core.py +1688 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/_plotter.py +664 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/_renderer.py +853 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/_utils.py +79 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/reset.css +41 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/typography.css +16 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/variables.css +85 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/buttons.css +217 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/context-menu.css +93 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/dropdown.css +57 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/forms.css +112 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/modal.css +59 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/sections.css +212 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/canvas.css +176 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/element-inspector.css +190 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/loading.css +59 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/overlay.css +45 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/panel-grid.css +95 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/selection.css +101 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/statistics.css +138 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/index.css +31 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/container.css +7 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/controls.css +56 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/preview.css +78 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/axis.js +314 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/basic.js +107 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/distribute.js +54 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/canvas.js +172 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/dragging.js +258 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/resize.js +48 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/selection.js +71 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/api.js +288 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/state.js +143 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/utils.js +245 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/dev/element-inspector.js +992 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/bbox.js +339 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/element-drag.js +286 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/overlay.js +371 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/preview.js +293 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/main.js +426 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/context-menu.js +152 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/keyboard.js +265 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/controls.js +184 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/download.js +57 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/help.js +100 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/theme.js +34 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/templates/__init__.py +124 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_html.py +851 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_scripts.py +4932 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_styles.py +1657 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor.py +36 -0
- scitex/fts/_fig/_models/_Annotations.py +115 -0
- scitex/fts/_fig/_models/_Axes.py +152 -0
- scitex/fts/_fig/_models/_Figure.py +138 -0
- scitex/fts/_fig/_models/_Guides.py +104 -0
- scitex/fts/_fig/_models/_Plot.py +123 -0
- scitex/fts/_fig/_models/_Styles.py +245 -0
- scitex/fts/_fig/_models/__init__.py +80 -0
- scitex/fts/_fig/_models/_plot_types/__init__.py +156 -0
- scitex/fts/_fig/_models/_plot_types/_bar.py +43 -0
- scitex/fts/_fig/_models/_plot_types/_box.py +38 -0
- scitex/fts/_fig/_models/_plot_types/_distribution.py +36 -0
- scitex/fts/_fig/_models/_plot_types/_errorbar.py +60 -0
- scitex/fts/_fig/_models/_plot_types/_histogram.py +30 -0
- scitex/fts/_fig/_models/_plot_types/_image.py +61 -0
- scitex/fts/_fig/_models/_plot_types/_line.py +57 -0
- scitex/fts/_fig/_models/_plot_types/_scatter.py +30 -0
- scitex/fts/_fig/_models/_plot_types/_seaborn.py +121 -0
- scitex/fts/_fig/_models/_plot_types/_violin.py +36 -0
- scitex/fts/_fig/_utils/__init__.py +129 -0
- scitex/fts/_fig/_utils/_auto_layout.py +127 -0
- scitex/fts/_fig/_utils/_calc_bounds.py +111 -0
- scitex/fts/_fig/_utils/_const_sizes.py +48 -0
- scitex/fts/_fig/_utils/_convert_coords.py +77 -0
- scitex/fts/_fig/_utils/_get_template.py +178 -0
- scitex/fts/_fig/_utils/_normalize.py +73 -0
- scitex/fts/_fig/_utils/_plot_layout.py +397 -0
- scitex/fts/_fig/_utils/_validate.py +197 -0
- scitex/fts/_kinds/__init__.py +45 -0
- scitex/fts/_kinds/_figure/__init__.py +19 -0
- scitex/fts/_kinds/_figure/_composite.py +345 -0
- scitex/fts/_kinds/_plot/__init__.py +25 -0
- scitex/fts/_kinds/_plot/_backend/__init__.py +53 -0
- scitex/fts/_kinds/_plot/_backend/_export.py +165 -0
- scitex/fts/_kinds/_plot/_backend/_parser.py +188 -0
- scitex/fts/_kinds/_plot/_backend/_render.py +538 -0
- scitex/fts/_kinds/_plot/_dataclasses/_ChannelEncoding.py +46 -0
- scitex/fts/_kinds/_plot/_dataclasses/_Encoding.py +82 -0
- scitex/fts/_kinds/_plot/_dataclasses/_Theme.py +441 -0
- scitex/fts/_kinds/_plot/_dataclasses/_TraceEncoding.py +52 -0
- scitex/fts/_kinds/_plot/_dataclasses/__init__.py +47 -0
- scitex/fts/_kinds/_plot/_models/_Annotations.py +115 -0
- scitex/fts/_kinds/_plot/_models/_Axes.py +152 -0
- scitex/fts/_kinds/_plot/_models/_Figure.py +138 -0
- scitex/fts/_kinds/_plot/_models/_Guides.py +104 -0
- scitex/fts/_kinds/_plot/_models/_Plot.py +123 -0
- scitex/fts/_kinds/_plot/_models/_Styles.py +245 -0
- scitex/fts/_kinds/_plot/_models/__init__.py +80 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/__init__.py +156 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_bar.py +43 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_box.py +38 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_distribution.py +36 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_errorbar.py +60 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_histogram.py +30 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_image.py +61 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_line.py +57 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_scatter.py +30 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_seaborn.py +121 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_violin.py +36 -0
- scitex/fts/_kinds/_plot/_utils/__init__.py +129 -0
- scitex/fts/_kinds/_plot/_utils/_auto_layout.py +127 -0
- scitex/fts/_kinds/_plot/_utils/_calc_bounds.py +111 -0
- scitex/fts/_kinds/_plot/_utils/_const_sizes.py +48 -0
- scitex/fts/_kinds/_plot/_utils/_convert_coords.py +77 -0
- scitex/fts/_kinds/_plot/_utils/_get_template.py +178 -0
- scitex/fts/_kinds/_plot/_utils/_normalize.py +73 -0
- scitex/fts/_kinds/_plot/_utils/_plot_layout.py +397 -0
- scitex/fts/_kinds/_plot/_utils/_validate.py +197 -0
- scitex/fts/_kinds/_shape/__init__.py +141 -0
- scitex/fts/_kinds/_stats/__init__.py +56 -0
- scitex/fts/_kinds/_stats/_dataclasses/_Stats.py +423 -0
- scitex/fts/_kinds/_stats/_dataclasses/__init__.py +48 -0
- scitex/fts/_kinds/_table/__init__.py +72 -0
- scitex/fts/_kinds/_table/_latex/__init__.py +93 -0
- scitex/fts/_kinds/_table/_latex/_editor/__init__.py +11 -0
- scitex/fts/_kinds/_table/_latex/_editor/_app.py +725 -0
- scitex/fts/_kinds/_table/_latex/_export.py +279 -0
- scitex/fts/_kinds/_table/_latex/_figure_exporter.py +153 -0
- scitex/fts/_kinds/_table/_latex/_stats_formatter.py +274 -0
- scitex/fts/_kinds/_table/_latex/_table_exporter.py +362 -0
- scitex/fts/_kinds/_table/_latex/_utils.py +369 -0
- scitex/fts/_kinds/_table/_latex/_validator.py +445 -0
- scitex/fts/_kinds/_text/__init__.py +77 -0
- scitex/fts/_schemas/data_info.schema.json +75 -0
- scitex/fts/_schemas/encoding.schema.json +90 -0
- scitex/fts/_schemas/node.schema.json +145 -0
- scitex/fts/_schemas/render_manifest.schema.json +62 -0
- scitex/fts/_schemas/stats.schema.json +132 -0
- scitex/fts/_schemas/theme.schema.json +141 -0
- scitex/fts/_stats/__init__.py +48 -0
- scitex/fts/_stats/_dataclasses/_Stats.py +423 -0
- scitex/fts/_stats/_dataclasses/__init__.py +48 -0
- scitex/fts/_tables/__init__.py +65 -0
- scitex/fts/_tables/_latex/__init__.py +93 -0
- scitex/fts/_tables/_latex/_editor/__init__.py +11 -0
- scitex/fts/_tables/_latex/_editor/_app.py +725 -0
- scitex/fts/_tables/_latex/_export.py +279 -0
- scitex/fts/_tables/_latex/_figure_exporter.py +153 -0
- scitex/fts/_tables/_latex/_stats_formatter.py +274 -0
- scitex/fts/_tables/_latex/_table_exporter.py +362 -0
- scitex/fts/_tables/_latex/_utils.py +369 -0
- scitex/fts/_tables/_latex/_validator.py +445 -0
- scitex/gen/__init__.py +66 -25
- scitex/gen/misc.py +28 -0
- scitex/io/__init__.py +47 -32
- scitex/io/_load.py +87 -36
- scitex/io/_load_modules/__init__.py +10 -7
- scitex/io/_load_modules/_pandas.py +6 -1
- scitex/io/_save.py +299 -1556
- scitex/io/_save_modules/__init__.py +76 -19
- scitex/io/_save_modules/_figure_utils.py +90 -0
- scitex/io/_save_modules/_image_csv.py +497 -0
- scitex/io/_save_modules/_legends.py +91 -0
- scitex/io/_save_modules/_pltz_bundle.py +356 -0
- scitex/io/_save_modules/_pltz_stx.py +536 -0
- scitex/io/_save_modules/_stx_bundle.py +104 -0
- scitex/io/_save_modules/_symlink.py +96 -0
- scitex/io/_save_modules/_yaml.py +1 -1
- scitex/io/_save_modules/_zarr.py +64 -18
- scitex/io/bundle/README.md +212 -0
- scitex/io/bundle/__init__.py +110 -0
- scitex/io/{_bundle.py → bundle/_core.py} +168 -97
- scitex/io/bundle/_nested.py +713 -0
- scitex/io/bundle/_types.py +74 -0
- scitex/io/{_zip_bundle.py → bundle/_zip.py} +93 -45
- scitex/io/utils/h5_to_zarr.py +1 -1
- scitex/logging/__init__.py +108 -13
- scitex/logging/_errors.py +508 -0
- scitex/logging/_formatters.py +30 -6
- scitex/logging/_warnings.py +261 -0
- scitex/plt/__init__.py +4 -1
- scitex/plt/_figrecipe.py +236 -0
- scitex/plt/_subplots/_AxisWrapper.py +6 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_UnitAwareMixin.py +112 -1
- scitex/plt/_subplots/_FigWrapper.py +15 -0
- scitex/plt/_subplots/_SubplotsWrapper.py +125 -489
- scitex/plt/_subplots/_export_as_csv.py +11 -0
- scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +2 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_pcolormesh.py +66 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stackplot.py +62 -0
- scitex/plt/_subplots/_export_as_csv_formatters/test_formatters.py +208 -0
- scitex/plt/_subplots/_fonts.py +71 -0
- scitex/plt/_subplots/_mm_layout.py +282 -0
- scitex/plt/gallery/__init__.py +99 -2
- scitex/plt/styles/_plot_postprocess.py +3 -1
- scitex/plt/utils/_configure_mpl.py +16 -19
- scitex/repro/_RandomStateManager.py +13 -8
- scitex/resource/__init__.py +19 -1
- scitex/resource/_utils/_get_env_info.py +13 -25
- scitex/schema/__init__.py +149 -160
- scitex/schema/_encoding.py +273 -0
- scitex/schema/_figure_elements.py +406 -0
- scitex/schema/_theme.py +360 -0
- scitex/schema/_validation.py +0 -98
- scitex/scholar/__init__.py +56 -14
- scitex/scholar/auth/ScholarAuthManager.py +1 -1
- scitex/scholar/auth/__init__.py +11 -2
- scitex/scholar/auth/providers/BaseAuthenticator.py +1 -1
- scitex/scholar/auth/providers/EZProxyAuthenticator.py +1 -1
- scitex/scholar/auth/providers/OpenAthensAuthenticator.py +1 -1
- scitex/scholar/auth/providers/ShibbolethAuthenticator.py +1 -1
- scitex/scholar/config/ScholarConfig.py +1 -1
- scitex/scholar/core/Scholar.py +1 -1
- scitex/session/_decorator.py +18 -16
- scitex/session/_lifecycle.py +9 -11
- scitex/session/template.py +9 -8
- scitex/sh/test_sh.py +72 -0
- scitex/sh/test_sh_simple.py +61 -0
- scitex/stats/__init__.py +221 -97
- scitex/stats/_schema.py +21 -22
- scitex/stats/descriptive/_circular.py +212 -351
- scitex/stats/descriptive/_describe.py +81 -132
- scitex/stats/descriptive/_nan.py +205 -433
- scitex/stats/descriptive/_real.py +127 -141
- scitex/str/_format_plot_text.py +5 -5
- scitex/str/_latex.py +26 -84
- scitex/str/_latex_fallback.py +53 -47
- scitex/web/_search_pubmed.py +5 -4
- scitex/writer/tests/test_diff_between.py +451 -0
- scitex/writer/tests/test_document_section.py +311 -0
- scitex/writer/tests/test_document_workflow.py +393 -0
- scitex/writer/tests/test_writer.py +361 -0
- scitex/writer/tests/test_writer_integration.py +303 -0
- {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/METADATA +364 -181
- {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/RECORD +412 -97
- scitex/scholar/docs/to_claude/guidelines/examples/mgmt/ARCHITECTURE_EXAMPLE.md +0 -905
- scitex/scholar/docs/to_claude/guidelines/examples/mgmt/BULLETIN_BOARD_EXAMPLE.md +0 -99
- scitex/scholar/docs/to_claude/guidelines/examples/mgmt/PROJECT_DESCRIPTION_EXAMPLE.md +0 -96
- {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/WHEEL +0 -0
- {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/entry_points.txt +0 -0
- {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2025-12-19
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-code/src/scitex/schema/_encoding.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Encoding Schema - Data to Visual Mapping for Scientific Rigor.
|
|
7
|
+
|
|
8
|
+
This module defines the encoding layer that maps data columns to visual channels.
|
|
9
|
+
This separation ensures scientific reproducibility by explicitly documenting
|
|
10
|
+
how data is transformed into visual representation.
|
|
11
|
+
|
|
12
|
+
Encoding (encoding.json) - Data→Visual Mapping:
|
|
13
|
+
- Column bindings (x, y, color, size, shape)
|
|
14
|
+
- Data transformations (log, normalize, bin)
|
|
15
|
+
- Scale types (linear, log, categorical)
|
|
16
|
+
- Missing value handling
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
|
|
23
|
+
ENCODING_VERSION = "1.0.0"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ChannelBinding:
|
|
28
|
+
"""
|
|
29
|
+
Binding from data column to visual channel.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
channel : str
|
|
34
|
+
Visual channel (x, y, color, size, shape, opacity, etc.)
|
|
35
|
+
column : str
|
|
36
|
+
Data column name
|
|
37
|
+
scale : str
|
|
38
|
+
Scale type (linear, log, sqrt, categorical, ordinal)
|
|
39
|
+
domain : list, optional
|
|
40
|
+
Data domain [min, max] or list of categories
|
|
41
|
+
range : list, optional
|
|
42
|
+
Visual range (e.g., [0, 1] for opacity, color palette for color)
|
|
43
|
+
transform : str, optional
|
|
44
|
+
Data transformation (log, sqrt, normalize, zscore, rank)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
channel: str
|
|
48
|
+
column: str
|
|
49
|
+
scale: str = "linear"
|
|
50
|
+
domain: Optional[List[Any]] = None
|
|
51
|
+
range: Optional[List[Any]] = None
|
|
52
|
+
transform: Optional[str] = None
|
|
53
|
+
missing_value: Optional[str] = "drop" # "drop", "zero", "mean", "interpolate"
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
56
|
+
result = {
|
|
57
|
+
"channel": self.channel,
|
|
58
|
+
"column": self.column,
|
|
59
|
+
"scale": self.scale,
|
|
60
|
+
}
|
|
61
|
+
if self.domain is not None:
|
|
62
|
+
result["domain"] = self.domain
|
|
63
|
+
if self.range is not None:
|
|
64
|
+
result["range"] = self.range
|
|
65
|
+
if self.transform:
|
|
66
|
+
result["transform"] = self.transform
|
|
67
|
+
if self.missing_value != "drop":
|
|
68
|
+
result["missing_value"] = self.missing_value
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ChannelBinding":
|
|
73
|
+
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class TraceEncoding:
|
|
78
|
+
"""
|
|
79
|
+
Encoding specification for a single trace.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
trace_id : str
|
|
84
|
+
ID of the trace this encoding applies to
|
|
85
|
+
bindings : list of ChannelBinding
|
|
86
|
+
Column to channel bindings
|
|
87
|
+
aggregate : str, optional
|
|
88
|
+
Aggregation method (mean, median, sum, count, etc.)
|
|
89
|
+
group_by : list, optional
|
|
90
|
+
Columns to group by before aggregation
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
trace_id: str
|
|
94
|
+
bindings: List[ChannelBinding] = field(default_factory=list)
|
|
95
|
+
aggregate: Optional[str] = None
|
|
96
|
+
group_by: Optional[List[str]] = None
|
|
97
|
+
|
|
98
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
99
|
+
result = {
|
|
100
|
+
"trace_id": self.trace_id,
|
|
101
|
+
"bindings": [b.to_dict() for b in self.bindings],
|
|
102
|
+
}
|
|
103
|
+
if self.aggregate:
|
|
104
|
+
result["aggregate"] = self.aggregate
|
|
105
|
+
if self.group_by:
|
|
106
|
+
result["group_by"] = self.group_by
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def from_dict(cls, data: Dict[str, Any]) -> "TraceEncoding":
|
|
111
|
+
bindings = [ChannelBinding.from_dict(b) for b in data.get("bindings", [])]
|
|
112
|
+
return cls(
|
|
113
|
+
trace_id=data.get("trace_id", ""),
|
|
114
|
+
bindings=bindings,
|
|
115
|
+
aggregate=data.get("aggregate"),
|
|
116
|
+
group_by=data.get("group_by"),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class PlotEncoding:
|
|
122
|
+
"""
|
|
123
|
+
Complete encoding specification for a plot.
|
|
124
|
+
|
|
125
|
+
Stored in encoding.json. Documents how data maps to visual channels
|
|
126
|
+
for scientific reproducibility.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
traces : list of TraceEncoding
|
|
131
|
+
Per-trace encoding specifications
|
|
132
|
+
data_hash : str, optional
|
|
133
|
+
Hash of source data for integrity verification
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
traces: List[TraceEncoding] = field(default_factory=list)
|
|
137
|
+
data_hash: Optional[str] = None
|
|
138
|
+
|
|
139
|
+
# Schema metadata
|
|
140
|
+
scitex_schema: str = "scitex.plt.encoding"
|
|
141
|
+
scitex_schema_version: str = ENCODING_VERSION
|
|
142
|
+
|
|
143
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
144
|
+
result = {
|
|
145
|
+
"schema": {
|
|
146
|
+
"name": self.scitex_schema,
|
|
147
|
+
"version": self.scitex_schema_version,
|
|
148
|
+
},
|
|
149
|
+
"traces": [t.to_dict() for t in self.traces],
|
|
150
|
+
}
|
|
151
|
+
if self.data_hash:
|
|
152
|
+
result["data_hash"] = self.data_hash
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
def to_json(self, indent: int = 2) -> str:
|
|
156
|
+
return json.dumps(self.to_dict(), indent=indent)
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def from_dict(cls, data: Dict[str, Any]) -> "PlotEncoding":
|
|
160
|
+
return cls(
|
|
161
|
+
traces=[TraceEncoding.from_dict(t) for t in data.get("traces", [])],
|
|
162
|
+
data_hash=data.get("data_hash"),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def from_json(cls, json_str: str) -> "PlotEncoding":
|
|
167
|
+
return cls.from_dict(json.loads(json_str))
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def from_spec_and_style(cls, spec: dict, style: dict) -> "PlotEncoding":
|
|
171
|
+
"""
|
|
172
|
+
Create encoding from spec and style for backward compatibility.
|
|
173
|
+
|
|
174
|
+
Extracts column bindings from PlotSpec traces and color/size mappings
|
|
175
|
+
from PlotStyle trace overrides.
|
|
176
|
+
"""
|
|
177
|
+
traces = []
|
|
178
|
+
spec_traces = spec.get("traces", [])
|
|
179
|
+
style_traces = {t.get("trace_id"): t for t in style.get("traces", [])}
|
|
180
|
+
|
|
181
|
+
for trace in spec_traces:
|
|
182
|
+
trace_id = trace.get("id", "")
|
|
183
|
+
bindings = []
|
|
184
|
+
|
|
185
|
+
# X column
|
|
186
|
+
if trace.get("x_col"):
|
|
187
|
+
bindings.append(
|
|
188
|
+
ChannelBinding(
|
|
189
|
+
channel="x",
|
|
190
|
+
column=trace["x_col"],
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Y column
|
|
195
|
+
if trace.get("y_col"):
|
|
196
|
+
bindings.append(
|
|
197
|
+
ChannelBinding(
|
|
198
|
+
channel="y",
|
|
199
|
+
column=trace["y_col"],
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Data columns (for boxplot, etc.)
|
|
204
|
+
for i, col in enumerate(trace.get("data_cols", [])):
|
|
205
|
+
bindings.append(
|
|
206
|
+
ChannelBinding(
|
|
207
|
+
channel=f"data_{i}",
|
|
208
|
+
column=col,
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Value column (for heatmap)
|
|
213
|
+
if trace.get("value_col"):
|
|
214
|
+
bindings.append(
|
|
215
|
+
ChannelBinding(
|
|
216
|
+
channel="value",
|
|
217
|
+
column=trace["value_col"],
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Vector columns (for quiver)
|
|
222
|
+
if trace.get("u_col"):
|
|
223
|
+
bindings.append(
|
|
224
|
+
ChannelBinding(
|
|
225
|
+
channel="u",
|
|
226
|
+
column=trace["u_col"],
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
if trace.get("v_col"):
|
|
230
|
+
bindings.append(
|
|
231
|
+
ChannelBinding(
|
|
232
|
+
channel="v",
|
|
233
|
+
column=trace["v_col"],
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Style overrides that are data-driven
|
|
238
|
+
style_trace = style_traces.get(trace_id, {})
|
|
239
|
+
if style_trace.get("color_by"):
|
|
240
|
+
bindings.append(
|
|
241
|
+
ChannelBinding(
|
|
242
|
+
channel="color",
|
|
243
|
+
column=style_trace["color_by"],
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
if style_trace.get("size_by"):
|
|
247
|
+
bindings.append(
|
|
248
|
+
ChannelBinding(
|
|
249
|
+
channel="size",
|
|
250
|
+
column=style_trace["size_by"],
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if bindings:
|
|
255
|
+
traces.append(
|
|
256
|
+
TraceEncoding(
|
|
257
|
+
trace_id=trace_id,
|
|
258
|
+
bindings=bindings,
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return cls(traces=traces)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
__all__ = [
|
|
266
|
+
"ENCODING_VERSION",
|
|
267
|
+
"ChannelBinding",
|
|
268
|
+
"TraceEncoding",
|
|
269
|
+
"PlotEncoding",
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# EOF
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2025-12-19
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-code/src/scitex/schema/_figure_elements.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Figure Elements Schema - Title, Caption, and Panel Labels.
|
|
7
|
+
|
|
8
|
+
This module defines figure-level text elements that appear in theme.json
|
|
9
|
+
for editability while maintaining consistent styling across the figure.
|
|
10
|
+
|
|
11
|
+
These elements enable auto-generation of publication-ready captions:
|
|
12
|
+
"Figure 1: Main Results. (A) Time-series analysis. (B) Frequency distribution."
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from dataclasses import asdict, dataclass, field
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
FIGURE_ELEMENTS_VERSION = "1.0.0"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class FigureTitle:
|
|
23
|
+
"""
|
|
24
|
+
Figure title specification.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
text : str
|
|
29
|
+
The main title text (e.g., "Main Results")
|
|
30
|
+
prefix : str
|
|
31
|
+
Prefix before number (e.g., "Figure", "Fig.")
|
|
32
|
+
number : int, optional
|
|
33
|
+
Figure number (None for unnumbered)
|
|
34
|
+
font_size_pt : float
|
|
35
|
+
Title font size in points
|
|
36
|
+
font_weight : str
|
|
37
|
+
Font weight (normal, bold)
|
|
38
|
+
position : str
|
|
39
|
+
Position (top, bottom)
|
|
40
|
+
visible : bool
|
|
41
|
+
Whether to render the title
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
text: str = ""
|
|
45
|
+
prefix: str = "Figure"
|
|
46
|
+
number: Optional[int] = None
|
|
47
|
+
font_size_pt: float = 10.0
|
|
48
|
+
font_weight: str = "bold"
|
|
49
|
+
position: str = "top"
|
|
50
|
+
visible: bool = True
|
|
51
|
+
|
|
52
|
+
def format(self, include_number: bool = True) -> str:
|
|
53
|
+
"""Format the title as displayed string."""
|
|
54
|
+
if include_number and self.number is not None:
|
|
55
|
+
return f"{self.prefix} {self.number}: {self.text}"
|
|
56
|
+
elif self.text:
|
|
57
|
+
return self.text
|
|
58
|
+
return ""
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
61
|
+
result = {
|
|
62
|
+
"text": self.text,
|
|
63
|
+
"prefix": self.prefix,
|
|
64
|
+
"font_size_pt": self.font_size_pt,
|
|
65
|
+
"font_weight": self.font_weight,
|
|
66
|
+
"position": self.position,
|
|
67
|
+
"visible": self.visible,
|
|
68
|
+
}
|
|
69
|
+
if self.number is not None:
|
|
70
|
+
result["number"] = self.number
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_dict(cls, data: Dict[str, Any]) -> "FigureTitle":
|
|
75
|
+
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class Caption:
|
|
80
|
+
"""
|
|
81
|
+
Figure caption specification.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
text : str
|
|
86
|
+
Manual caption text (overrides auto-generation if set)
|
|
87
|
+
auto_generate : bool
|
|
88
|
+
Whether to auto-generate from panel descriptions
|
|
89
|
+
font_size_pt : float
|
|
90
|
+
Caption font size in points
|
|
91
|
+
position : str
|
|
92
|
+
Position (top, bottom)
|
|
93
|
+
visible : bool
|
|
94
|
+
Whether to render the caption
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
text: str = ""
|
|
98
|
+
auto_generate: bool = True
|
|
99
|
+
font_size_pt: float = 8.0
|
|
100
|
+
position: str = "bottom"
|
|
101
|
+
visible: bool = True
|
|
102
|
+
|
|
103
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
104
|
+
return asdict(self)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Caption":
|
|
108
|
+
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class PanelLabels:
|
|
113
|
+
"""
|
|
114
|
+
Panel label styling for multi-panel figures.
|
|
115
|
+
|
|
116
|
+
Controls how panel letters (A, B, C, ...) are rendered on child plots.
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
style : str
|
|
121
|
+
Letter style: "uppercase" (A, B, C), "lowercase" (a, b, c),
|
|
122
|
+
"roman" (i, ii, iii), "Roman" (I, II, III)
|
|
123
|
+
format : str
|
|
124
|
+
Format template with {letter} placeholder
|
|
125
|
+
font_size_pt : float
|
|
126
|
+
Label font size in points
|
|
127
|
+
font_weight : str
|
|
128
|
+
Font weight (normal, bold)
|
|
129
|
+
position : str
|
|
130
|
+
Position on panel (top-left, top-right, bottom-left, bottom-right)
|
|
131
|
+
offset_mm : dict
|
|
132
|
+
Offset from corner {x: mm, y: mm}
|
|
133
|
+
visible : bool
|
|
134
|
+
Whether to render panel labels
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
style: str = "uppercase"
|
|
138
|
+
format: str = "({letter})"
|
|
139
|
+
font_size_pt: float = 12.0
|
|
140
|
+
font_weight: str = "bold"
|
|
141
|
+
position: str = "top-left"
|
|
142
|
+
offset_mm: Dict[str, float] = field(default_factory=lambda: {"x": 2.0, "y": 2.0})
|
|
143
|
+
visible: bool = True
|
|
144
|
+
|
|
145
|
+
def format_letter(self, index: int) -> str:
|
|
146
|
+
"""
|
|
147
|
+
Format panel letter for given index (0-based).
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
index : int
|
|
152
|
+
Zero-based panel index
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
str
|
|
157
|
+
Formatted label like "(A)", "(B)", etc.
|
|
158
|
+
"""
|
|
159
|
+
if self.style == "uppercase":
|
|
160
|
+
letter = chr(ord("A") + index)
|
|
161
|
+
elif self.style == "lowercase":
|
|
162
|
+
letter = chr(ord("a") + index)
|
|
163
|
+
elif self.style == "roman":
|
|
164
|
+
letter = _to_roman(index + 1).lower()
|
|
165
|
+
elif self.style == "Roman":
|
|
166
|
+
letter = _to_roman(index + 1)
|
|
167
|
+
else:
|
|
168
|
+
letter = chr(ord("A") + index)
|
|
169
|
+
|
|
170
|
+
return self.format.replace("{letter}", letter)
|
|
171
|
+
|
|
172
|
+
def get_letter(self, index: int) -> str:
|
|
173
|
+
"""Get just the letter without formatting."""
|
|
174
|
+
if self.style == "uppercase":
|
|
175
|
+
return chr(ord("A") + index)
|
|
176
|
+
elif self.style == "lowercase":
|
|
177
|
+
return chr(ord("a") + index)
|
|
178
|
+
elif self.style == "roman":
|
|
179
|
+
return _to_roman(index + 1).lower()
|
|
180
|
+
elif self.style == "Roman":
|
|
181
|
+
return _to_roman(index + 1)
|
|
182
|
+
return chr(ord("A") + index)
|
|
183
|
+
|
|
184
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
185
|
+
return {
|
|
186
|
+
"style": self.style,
|
|
187
|
+
"format": self.format,
|
|
188
|
+
"font_size_pt": self.font_size_pt,
|
|
189
|
+
"font_weight": self.font_weight,
|
|
190
|
+
"position": self.position,
|
|
191
|
+
"offset_mm": self.offset_mm,
|
|
192
|
+
"visible": self.visible,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def from_dict(cls, data: Dict[str, Any]) -> "PanelLabels":
|
|
197
|
+
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _to_roman(num: int) -> str:
|
|
201
|
+
"""Convert integer to Roman numeral."""
|
|
202
|
+
values = [
|
|
203
|
+
(1000, "M"),
|
|
204
|
+
(900, "CM"),
|
|
205
|
+
(500, "D"),
|
|
206
|
+
(400, "CD"),
|
|
207
|
+
(100, "C"),
|
|
208
|
+
(90, "XC"),
|
|
209
|
+
(50, "L"),
|
|
210
|
+
(40, "XL"),
|
|
211
|
+
(10, "X"),
|
|
212
|
+
(9, "IX"),
|
|
213
|
+
(5, "V"),
|
|
214
|
+
(4, "IV"),
|
|
215
|
+
(1, "I"),
|
|
216
|
+
]
|
|
217
|
+
result = ""
|
|
218
|
+
for value, numeral in values:
|
|
219
|
+
while num >= value:
|
|
220
|
+
result += numeral
|
|
221
|
+
num -= value
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclass
|
|
226
|
+
class PanelInfo:
|
|
227
|
+
"""
|
|
228
|
+
Information about a single panel in a multi-panel figure.
|
|
229
|
+
|
|
230
|
+
Used in spec.json to store per-panel metadata that combines with
|
|
231
|
+
theme-level PanelLabels to generate captions.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
panel_id : str
|
|
236
|
+
Unique identifier for the panel (matches child ID)
|
|
237
|
+
letter : str
|
|
238
|
+
Panel letter override (auto-assigned if empty)
|
|
239
|
+
description : str
|
|
240
|
+
Description for caption generation
|
|
241
|
+
order : int
|
|
242
|
+
Display order (0-based)
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
panel_id: str
|
|
246
|
+
letter: str = ""
|
|
247
|
+
description: str = ""
|
|
248
|
+
order: int = 0
|
|
249
|
+
|
|
250
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
251
|
+
result = {"panel_id": self.panel_id, "order": self.order}
|
|
252
|
+
if self.letter:
|
|
253
|
+
result["letter"] = self.letter
|
|
254
|
+
if self.description:
|
|
255
|
+
result["description"] = self.description
|
|
256
|
+
return result
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
def from_dict(cls, data: Dict[str, Any]) -> "PanelInfo":
|
|
260
|
+
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def generate_caption(
|
|
264
|
+
title: FigureTitle,
|
|
265
|
+
caption: Caption,
|
|
266
|
+
panels: List[PanelInfo],
|
|
267
|
+
panel_labels: PanelLabels,
|
|
268
|
+
) -> str:
|
|
269
|
+
"""
|
|
270
|
+
Generate full figure caption from components.
|
|
271
|
+
|
|
272
|
+
Format: "Figure 1: Main Title. (A) Description A. (B) Description B."
|
|
273
|
+
|
|
274
|
+
Parameters
|
|
275
|
+
----------
|
|
276
|
+
title : FigureTitle
|
|
277
|
+
Figure title specification
|
|
278
|
+
caption : Caption
|
|
279
|
+
Caption specification
|
|
280
|
+
panels : list of PanelInfo
|
|
281
|
+
Panel information with descriptions
|
|
282
|
+
panel_labels : PanelLabels
|
|
283
|
+
Panel label styling
|
|
284
|
+
|
|
285
|
+
Returns
|
|
286
|
+
-------
|
|
287
|
+
str
|
|
288
|
+
Complete formatted caption
|
|
289
|
+
"""
|
|
290
|
+
# If manual caption is set and auto_generate is off, use it
|
|
291
|
+
if caption.text and not caption.auto_generate:
|
|
292
|
+
return caption.text
|
|
293
|
+
|
|
294
|
+
parts = []
|
|
295
|
+
|
|
296
|
+
# Add title
|
|
297
|
+
title_str = title.format()
|
|
298
|
+
if title_str:
|
|
299
|
+
parts.append(title_str)
|
|
300
|
+
|
|
301
|
+
# Sort panels by order
|
|
302
|
+
sorted_panels = sorted(panels, key=lambda p: p.order)
|
|
303
|
+
|
|
304
|
+
# Add panel descriptions
|
|
305
|
+
panel_parts = []
|
|
306
|
+
for idx, panel in enumerate(sorted_panels):
|
|
307
|
+
if panel.description:
|
|
308
|
+
letter = panel.letter or panel_labels.get_letter(idx)
|
|
309
|
+
formatted = panel_labels.format.replace("{letter}", letter)
|
|
310
|
+
panel_parts.append(f"{formatted} {panel.description}")
|
|
311
|
+
|
|
312
|
+
if panel_parts:
|
|
313
|
+
parts.append(" ".join(panel_parts))
|
|
314
|
+
|
|
315
|
+
return " ".join(parts) if parts else ""
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def generate_caption_latex(
|
|
319
|
+
title: FigureTitle,
|
|
320
|
+
caption: Caption,
|
|
321
|
+
panels: List[PanelInfo],
|
|
322
|
+
panel_labels: PanelLabels,
|
|
323
|
+
) -> str:
|
|
324
|
+
"""
|
|
325
|
+
Generate LaTeX-formatted figure caption.
|
|
326
|
+
|
|
327
|
+
Format: "\\textbf{Figure 1: Main Title.} (A) Description A. (B) Description B."
|
|
328
|
+
|
|
329
|
+
Returns
|
|
330
|
+
-------
|
|
331
|
+
str
|
|
332
|
+
LaTeX-formatted caption
|
|
333
|
+
"""
|
|
334
|
+
parts = []
|
|
335
|
+
|
|
336
|
+
# Add bold title
|
|
337
|
+
title_str = title.format()
|
|
338
|
+
if title_str:
|
|
339
|
+
parts.append(f"\\textbf{{{title_str}}}")
|
|
340
|
+
|
|
341
|
+
# Sort and add panel descriptions
|
|
342
|
+
sorted_panels = sorted(panels, key=lambda p: p.order)
|
|
343
|
+
panel_parts = []
|
|
344
|
+
for idx, panel in enumerate(sorted_panels):
|
|
345
|
+
if panel.description:
|
|
346
|
+
letter = panel.letter or panel_labels.get_letter(idx)
|
|
347
|
+
formatted = panel_labels.format.replace("{letter}", letter)
|
|
348
|
+
panel_parts.append(f"\\textbf{{{formatted}}} {panel.description}")
|
|
349
|
+
|
|
350
|
+
if panel_parts:
|
|
351
|
+
parts.append(" ".join(panel_parts))
|
|
352
|
+
|
|
353
|
+
return " ".join(parts) if parts else ""
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def generate_caption_markdown(
|
|
357
|
+
title: FigureTitle,
|
|
358
|
+
caption: Caption,
|
|
359
|
+
panels: List[PanelInfo],
|
|
360
|
+
panel_labels: PanelLabels,
|
|
361
|
+
) -> str:
|
|
362
|
+
"""
|
|
363
|
+
Generate Markdown-formatted figure caption.
|
|
364
|
+
|
|
365
|
+
Format: "**Figure 1: Main Title.** (A) Description A. (B) Description B."
|
|
366
|
+
|
|
367
|
+
Returns
|
|
368
|
+
-------
|
|
369
|
+
str
|
|
370
|
+
Markdown-formatted caption
|
|
371
|
+
"""
|
|
372
|
+
parts = []
|
|
373
|
+
|
|
374
|
+
# Add bold title
|
|
375
|
+
title_str = title.format()
|
|
376
|
+
if title_str:
|
|
377
|
+
parts.append(f"**{title_str}**")
|
|
378
|
+
|
|
379
|
+
# Sort and add panel descriptions
|
|
380
|
+
sorted_panels = sorted(panels, key=lambda p: p.order)
|
|
381
|
+
panel_parts = []
|
|
382
|
+
for idx, panel in enumerate(sorted_panels):
|
|
383
|
+
if panel.description:
|
|
384
|
+
letter = panel.letter or panel_labels.get_letter(idx)
|
|
385
|
+
formatted = panel_labels.format.replace("{letter}", letter)
|
|
386
|
+
panel_parts.append(f"**{formatted}** {panel.description}")
|
|
387
|
+
|
|
388
|
+
if panel_parts:
|
|
389
|
+
parts.append(" ".join(panel_parts))
|
|
390
|
+
|
|
391
|
+
return " ".join(parts) if parts else ""
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
__all__ = [
|
|
395
|
+
"FIGURE_ELEMENTS_VERSION",
|
|
396
|
+
"FigureTitle",
|
|
397
|
+
"Caption",
|
|
398
|
+
"PanelLabels",
|
|
399
|
+
"PanelInfo",
|
|
400
|
+
"generate_caption",
|
|
401
|
+
"generate_caption_latex",
|
|
402
|
+
"generate_caption_markdown",
|
|
403
|
+
]
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
# EOF
|