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,298 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Integration with scitex.stats for statistical results."""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
|
7
|
+
|
|
8
|
+
# Check if scitex.stats is available
|
|
9
|
+
try:
|
|
10
|
+
from scitex import stats as scitex_stats
|
|
11
|
+
|
|
12
|
+
SCITEX_STATS_AVAILABLE = True
|
|
13
|
+
except ImportError:
|
|
14
|
+
scitex_stats = None
|
|
15
|
+
SCITEX_STATS_AVAILABLE = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def from_scitex_stats(
|
|
19
|
+
stats_result: Union[Dict[str, Any], List[Dict[str, Any]]],
|
|
20
|
+
) -> Dict[str, Any]:
|
|
21
|
+
"""Convert scitex.stats result(s) to figrecipe stats format.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
stats_result : dict or list of dict
|
|
26
|
+
Statistical result(s) from scitex.stats. Supports:
|
|
27
|
+
- Single comparison dict
|
|
28
|
+
- List of comparison dicts
|
|
29
|
+
- scitex.stats flat format: {name, method, p_value, effect_size, ci95}
|
|
30
|
+
- scitex.stats nested format: {method: {name}, results: {p_value}}
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
dict
|
|
35
|
+
Figrecipe-compatible stats dict with 'comparisons' list.
|
|
36
|
+
|
|
37
|
+
Examples
|
|
38
|
+
--------
|
|
39
|
+
>>> from scitex import stats
|
|
40
|
+
>>> result = stats.ttest_ind(x, y)
|
|
41
|
+
>>> fr_stats = from_scitex_stats(result)
|
|
42
|
+
>>> fig.set_stats(fr_stats)
|
|
43
|
+
|
|
44
|
+
>>> # Or for multiple comparisons
|
|
45
|
+
>>> results = [stats.ttest_ind(a, b), stats.ttest_ind(a, c)]
|
|
46
|
+
>>> fr_stats = from_scitex_stats(results)
|
|
47
|
+
"""
|
|
48
|
+
# Normalize to list
|
|
49
|
+
if isinstance(stats_result, dict):
|
|
50
|
+
results = [stats_result]
|
|
51
|
+
else:
|
|
52
|
+
results = list(stats_result)
|
|
53
|
+
|
|
54
|
+
comparisons = []
|
|
55
|
+
for result in results:
|
|
56
|
+
comp = _convert_single_result(result)
|
|
57
|
+
if comp:
|
|
58
|
+
comparisons.append(comp)
|
|
59
|
+
|
|
60
|
+
return {"comparisons": comparisons}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _convert_single_result(result: Dict[str, Any]) -> Dict[str, Any]:
|
|
64
|
+
"""Convert a single scitex.stats result to figrecipe format."""
|
|
65
|
+
# Handle flat format (legacy or from load_statsz)
|
|
66
|
+
if "p_value" in result and "results" not in result:
|
|
67
|
+
return _convert_flat_format(result)
|
|
68
|
+
|
|
69
|
+
# Handle nested format (from test functions)
|
|
70
|
+
if "results" in result:
|
|
71
|
+
return _convert_nested_format(result)
|
|
72
|
+
|
|
73
|
+
# Handle already-converted format
|
|
74
|
+
if "comparisons" in result:
|
|
75
|
+
# Already in figrecipe format, return first comparison
|
|
76
|
+
comps = result.get("comparisons", [])
|
|
77
|
+
return comps[0] if comps else {}
|
|
78
|
+
|
|
79
|
+
return {}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _convert_flat_format(result: Dict[str, Any]) -> Dict[str, Any]:
|
|
83
|
+
"""Convert flat scitex.stats format."""
|
|
84
|
+
p_value = result.get("p_value")
|
|
85
|
+
stars = result.get("formatted") or result.get("stars")
|
|
86
|
+
if not stars and p_value is not None:
|
|
87
|
+
stars = _p_to_stars(p_value)
|
|
88
|
+
|
|
89
|
+
comp = {
|
|
90
|
+
"name": result.get("name", "comparison"),
|
|
91
|
+
"p_value": p_value,
|
|
92
|
+
"stars": stars,
|
|
93
|
+
"method": result.get("method", ""),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Handle effect size
|
|
97
|
+
es = result.get("effect_size")
|
|
98
|
+
if es is not None:
|
|
99
|
+
ci = result.get("ci95", [])
|
|
100
|
+
if isinstance(es, (int, float)):
|
|
101
|
+
comp["effect_size"] = {
|
|
102
|
+
"name": "d",
|
|
103
|
+
"value": float(es),
|
|
104
|
+
}
|
|
105
|
+
if len(ci) >= 2:
|
|
106
|
+
comp["effect_size"]["ci_lower"] = ci[0]
|
|
107
|
+
comp["effect_size"]["ci_upper"] = ci[1]
|
|
108
|
+
elif isinstance(es, dict):
|
|
109
|
+
comp["effect_size"] = es
|
|
110
|
+
|
|
111
|
+
return comp
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _convert_nested_format(result: Dict[str, Any]) -> Dict[str, Any]:
|
|
115
|
+
"""Convert nested scitex.stats format."""
|
|
116
|
+
method_data = result.get("method", {})
|
|
117
|
+
results_data = result.get("results", {})
|
|
118
|
+
|
|
119
|
+
method_name = (
|
|
120
|
+
method_data.get("name", "")
|
|
121
|
+
if isinstance(method_data, dict)
|
|
122
|
+
else str(method_data)
|
|
123
|
+
)
|
|
124
|
+
p_value = results_data.get("p_value")
|
|
125
|
+
stars = _p_to_stars(p_value) if p_value is not None else ""
|
|
126
|
+
|
|
127
|
+
comp = {
|
|
128
|
+
"name": result.get("name", "comparison"),
|
|
129
|
+
"p_value": p_value,
|
|
130
|
+
"stars": stars,
|
|
131
|
+
"method": method_name,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Handle effect size from results
|
|
135
|
+
es_data = results_data.get("effect_size")
|
|
136
|
+
if es_data:
|
|
137
|
+
if isinstance(es_data, dict):
|
|
138
|
+
comp["effect_size"] = es_data
|
|
139
|
+
else:
|
|
140
|
+
comp["effect_size"] = {"name": "d", "value": float(es_data)}
|
|
141
|
+
|
|
142
|
+
return comp
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _p_to_stars(p_value: float, ns_symbol: bool = True) -> str:
|
|
146
|
+
"""Convert p-value to significance stars."""
|
|
147
|
+
if p_value < 0.001:
|
|
148
|
+
return "***"
|
|
149
|
+
elif p_value < 0.01:
|
|
150
|
+
return "**"
|
|
151
|
+
elif p_value < 0.05:
|
|
152
|
+
return "*"
|
|
153
|
+
return "ns" if ns_symbol else ""
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def load_stats_bundle(path: Union[str, Path]) -> Dict[str, Any]:
|
|
157
|
+
"""Load stats from a scitex.stats bundle file.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
path : str or Path
|
|
162
|
+
Path to .statsz or .zip bundle file.
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
dict
|
|
167
|
+
Figrecipe-compatible stats dict.
|
|
168
|
+
|
|
169
|
+
Raises
|
|
170
|
+
------
|
|
171
|
+
ImportError
|
|
172
|
+
If scitex.stats is not installed.
|
|
173
|
+
"""
|
|
174
|
+
if not SCITEX_STATS_AVAILABLE:
|
|
175
|
+
raise ImportError(
|
|
176
|
+
"scitex.stats is required for bundle loading. "
|
|
177
|
+
"Install with: pip install scitex[stats]"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
data = scitex_stats.load_statsz(path)
|
|
181
|
+
comparisons = data.get("comparisons", [])
|
|
182
|
+
|
|
183
|
+
# Convert each comparison
|
|
184
|
+
return from_scitex_stats(comparisons)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def annotate_from_stats(
|
|
188
|
+
ax,
|
|
189
|
+
stats: Dict[str, Any],
|
|
190
|
+
positions: Optional[Dict[str, float]] = None,
|
|
191
|
+
style: str = "stars",
|
|
192
|
+
**kwargs,
|
|
193
|
+
) -> List[Any]:
|
|
194
|
+
"""Add stat annotations to axes from stats dict.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
ax : RecordingAxes
|
|
199
|
+
The axes to annotate.
|
|
200
|
+
stats : dict
|
|
201
|
+
Stats dict with 'comparisons' list. Each comparison should have:
|
|
202
|
+
- name: "Group A vs Group B" (parsed for group names)
|
|
203
|
+
- p_value: float
|
|
204
|
+
- Optional: groups: ["Group A", "Group B"]
|
|
205
|
+
positions : dict, optional
|
|
206
|
+
Mapping of group names to x positions. If None, uses 0, 1, 2, ...
|
|
207
|
+
style : str
|
|
208
|
+
Annotation style: "stars", "p_value", "both".
|
|
209
|
+
**kwargs
|
|
210
|
+
Additional arguments passed to add_stat_annotation().
|
|
211
|
+
|
|
212
|
+
Returns
|
|
213
|
+
-------
|
|
214
|
+
list
|
|
215
|
+
List of artist objects created.
|
|
216
|
+
|
|
217
|
+
Examples
|
|
218
|
+
--------
|
|
219
|
+
>>> stats = from_scitex_stats(result)
|
|
220
|
+
>>> annotate_from_stats(ax, stats, positions={"Control": 0, "Treatment": 1})
|
|
221
|
+
"""
|
|
222
|
+
comparisons = stats.get("comparisons", [])
|
|
223
|
+
if not comparisons:
|
|
224
|
+
return []
|
|
225
|
+
|
|
226
|
+
artists = []
|
|
227
|
+
y_offset = 0
|
|
228
|
+
|
|
229
|
+
for comp in comparisons:
|
|
230
|
+
# Get positions for this comparison
|
|
231
|
+
x1, x2 = _get_comparison_positions(comp, positions)
|
|
232
|
+
if x1 is None or x2 is None:
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
# Calculate y position (stack annotations)
|
|
236
|
+
y = kwargs.pop("y", None)
|
|
237
|
+
if y is None:
|
|
238
|
+
# Auto-calculate based on data
|
|
239
|
+
ylim = ax.get_ylim() if hasattr(ax, "get_ylim") else (0, 1)
|
|
240
|
+
y = ylim[1] + (ylim[1] - ylim[0]) * 0.05 * (1 + y_offset)
|
|
241
|
+
y_offset += 1
|
|
242
|
+
|
|
243
|
+
# Add annotation
|
|
244
|
+
result = ax.add_stat_annotation(
|
|
245
|
+
x1,
|
|
246
|
+
x2,
|
|
247
|
+
p_value=comp.get("p_value"),
|
|
248
|
+
text=comp.get("stars") if style == "stars" else None,
|
|
249
|
+
y=y,
|
|
250
|
+
style=style,
|
|
251
|
+
id=comp.get("name", "").replace(" ", "_"),
|
|
252
|
+
**kwargs,
|
|
253
|
+
)
|
|
254
|
+
artists.extend(result if isinstance(result, list) else [result])
|
|
255
|
+
|
|
256
|
+
return artists
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _get_comparison_positions(
|
|
260
|
+
comp: Dict[str, Any],
|
|
261
|
+
positions: Optional[Dict[str, float]],
|
|
262
|
+
) -> tuple:
|
|
263
|
+
"""Extract x positions for a comparison."""
|
|
264
|
+
# Try explicit groups
|
|
265
|
+
groups = comp.get("groups", [])
|
|
266
|
+
if len(groups) >= 2 and positions:
|
|
267
|
+
x1 = positions.get(groups[0])
|
|
268
|
+
x2 = positions.get(groups[1])
|
|
269
|
+
if x1 is not None and x2 is not None:
|
|
270
|
+
return x1, x2
|
|
271
|
+
|
|
272
|
+
# Try parsing from name (e.g., "Control vs Treatment")
|
|
273
|
+
name = comp.get("name", "")
|
|
274
|
+
if " vs " in name:
|
|
275
|
+
parts = name.split(" vs ")
|
|
276
|
+
if len(parts) >= 2 and positions:
|
|
277
|
+
x1 = positions.get(parts[0].strip())
|
|
278
|
+
x2 = positions.get(parts[1].strip())
|
|
279
|
+
if x1 is not None and x2 is not None:
|
|
280
|
+
return x1, x2
|
|
281
|
+
|
|
282
|
+
# Try x1, x2 directly in comparison
|
|
283
|
+
if "x1" in comp and "x2" in comp:
|
|
284
|
+
return comp["x1"], comp["x2"]
|
|
285
|
+
|
|
286
|
+
# Default to sequential positions
|
|
287
|
+
if positions is None:
|
|
288
|
+
return 0, 1
|
|
289
|
+
|
|
290
|
+
return None, None
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
__all__ = [
|
|
294
|
+
"SCITEX_STATS_AVAILABLE",
|
|
295
|
+
"from_scitex_stats",
|
|
296
|
+
"load_stats_bundle",
|
|
297
|
+
"annotate_from_stats",
|
|
298
|
+
]
|
|
@@ -22,6 +22,14 @@ DECORATION_METHODS = {
|
|
|
22
22
|
"text",
|
|
23
23
|
"annotate",
|
|
24
24
|
"clabel",
|
|
25
|
+
"axis", # For axis('off'), axis('on'), axis('equal'), etc.
|
|
26
|
+
"set_xticks",
|
|
27
|
+
"set_yticks",
|
|
28
|
+
"set_xticklabels",
|
|
29
|
+
"set_yticklabels",
|
|
30
|
+
"tick_params",
|
|
31
|
+
# Statistical annotations
|
|
32
|
+
"stat_annotation", # Comparison brackets with stars/p-values
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
# EOF
|
figrecipe/_recorder.py
CHANGED
|
@@ -25,16 +25,21 @@ class CallRecord:
|
|
|
25
25
|
kwargs: Dict[str, Any]
|
|
26
26
|
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
27
27
|
ax_position: Tuple[int, int] = (0, 0)
|
|
28
|
+
# Statistics associated with this plot call (e.g., n, mean, sem)
|
|
29
|
+
stats: Optional[Dict[str, Any]] = None
|
|
28
30
|
|
|
29
31
|
def to_dict(self) -> Dict[str, Any]:
|
|
30
32
|
"""Convert to dictionary for serialization."""
|
|
31
|
-
|
|
33
|
+
result = {
|
|
32
34
|
"id": self.id,
|
|
33
35
|
"function": self.function,
|
|
34
36
|
"args": self.args,
|
|
35
37
|
"kwargs": self.kwargs,
|
|
36
38
|
"timestamp": self.timestamp,
|
|
37
39
|
}
|
|
40
|
+
if self.stats is not None:
|
|
41
|
+
result["stats"] = self.stats
|
|
42
|
+
return result
|
|
38
43
|
|
|
39
44
|
@classmethod
|
|
40
45
|
def from_dict(
|
|
@@ -48,6 +53,7 @@ class CallRecord:
|
|
|
48
53
|
kwargs=data["kwargs"],
|
|
49
54
|
timestamp=data.get("timestamp", ""),
|
|
50
55
|
ax_position=ax_position,
|
|
56
|
+
stats=data.get("stats"),
|
|
51
57
|
)
|
|
52
58
|
|
|
53
59
|
|
|
@@ -58,6 +64,12 @@ class AxesRecord:
|
|
|
58
64
|
position: Tuple[int, int]
|
|
59
65
|
calls: List[CallRecord] = field(default_factory=list)
|
|
60
66
|
decorations: List[CallRecord] = field(default_factory=list)
|
|
67
|
+
# Panel-level caption (e.g., "(A) Description of this panel")
|
|
68
|
+
caption: Optional[str] = None
|
|
69
|
+
# Panel-level statistics (e.g., summary stats, comparison results)
|
|
70
|
+
stats: Optional[Dict[str, Any]] = None
|
|
71
|
+
# Panel visibility (for composition)
|
|
72
|
+
visible: bool = True
|
|
61
73
|
|
|
62
74
|
def add_call(self, record: CallRecord) -> None:
|
|
63
75
|
"""Add a plotting call record."""
|
|
@@ -69,10 +81,17 @@ class AxesRecord:
|
|
|
69
81
|
|
|
70
82
|
def to_dict(self) -> Dict[str, Any]:
|
|
71
83
|
"""Convert to dictionary for serialization."""
|
|
72
|
-
|
|
84
|
+
result = {
|
|
73
85
|
"calls": [c.to_dict() for c in self.calls],
|
|
74
86
|
"decorations": [d.to_dict() for d in self.decorations],
|
|
75
87
|
}
|
|
88
|
+
if self.caption is not None:
|
|
89
|
+
result["caption"] = self.caption
|
|
90
|
+
if self.stats is not None:
|
|
91
|
+
result["stats"] = self.stats
|
|
92
|
+
if not self.visible: # Only serialize if hidden (default is True)
|
|
93
|
+
result["visible"] = False
|
|
94
|
+
return result
|
|
76
95
|
|
|
77
96
|
|
|
78
97
|
@dataclass
|
|
@@ -95,6 +114,13 @@ class FigureRecord:
|
|
|
95
114
|
suptitle: Optional[Dict[str, Any]] = None
|
|
96
115
|
supxlabel: Optional[Dict[str, Any]] = None
|
|
97
116
|
supylabel: Optional[Dict[str, Any]] = None
|
|
117
|
+
# Panel labels (A, B, C, D for multi-panel figures)
|
|
118
|
+
panel_labels: Optional[Dict[str, Any]] = None
|
|
119
|
+
# Metadata for scientific figures (not rendered, stored in recipe)
|
|
120
|
+
title_metadata: Optional[str] = None # Figure title for publication/reference
|
|
121
|
+
caption: Optional[str] = None # Figure caption (e.g., "Fig. 1. Description...")
|
|
122
|
+
# Figure-level statistics (e.g., comparisons across panels, summary)
|
|
123
|
+
stats: Optional[Dict[str, Any]] = None
|
|
98
124
|
|
|
99
125
|
def get_axes_key(self, row: int, col: int) -> str:
|
|
100
126
|
"""Get dictionary key for axes at position."""
|
|
@@ -138,12 +164,26 @@ class FigureRecord:
|
|
|
138
164
|
# Add supylabel if set
|
|
139
165
|
if self.supylabel is not None:
|
|
140
166
|
result["figure"]["supylabel"] = self.supylabel
|
|
167
|
+
# Add panel_labels if set
|
|
168
|
+
if self.panel_labels is not None:
|
|
169
|
+
result["figure"]["panel_labels"] = self.panel_labels
|
|
170
|
+
# Add metadata section for scientific figures
|
|
171
|
+
metadata = {}
|
|
172
|
+
if self.title_metadata is not None:
|
|
173
|
+
metadata["title"] = self.title_metadata
|
|
174
|
+
if self.caption is not None:
|
|
175
|
+
metadata["caption"] = self.caption
|
|
176
|
+
if self.stats is not None:
|
|
177
|
+
metadata["stats"] = self.stats
|
|
178
|
+
if metadata:
|
|
179
|
+
result["metadata"] = metadata
|
|
141
180
|
return result
|
|
142
181
|
|
|
143
182
|
@classmethod
|
|
144
183
|
def from_dict(cls, data: Dict[str, Any]) -> "FigureRecord":
|
|
145
184
|
"""Create from dictionary."""
|
|
146
185
|
fig_data = data.get("figure", {})
|
|
186
|
+
metadata = data.get("metadata", {})
|
|
147
187
|
record = cls(
|
|
148
188
|
id=data.get("id", f"fig_{uuid.uuid4().hex[:8]}"),
|
|
149
189
|
created=data.get("created", ""),
|
|
@@ -156,6 +196,10 @@ class FigureRecord:
|
|
|
156
196
|
suptitle=fig_data.get("suptitle"),
|
|
157
197
|
supxlabel=fig_data.get("supxlabel"),
|
|
158
198
|
supylabel=fig_data.get("supylabel"),
|
|
199
|
+
panel_labels=fig_data.get("panel_labels"),
|
|
200
|
+
title_metadata=metadata.get("title"),
|
|
201
|
+
caption=metadata.get("caption"),
|
|
202
|
+
stats=metadata.get("stats"),
|
|
159
203
|
)
|
|
160
204
|
|
|
161
205
|
# Reconstruct axes
|
|
@@ -167,7 +211,12 @@ class FigureRecord:
|
|
|
167
211
|
else:
|
|
168
212
|
row, col = 0, 0
|
|
169
213
|
|
|
170
|
-
ax_record = AxesRecord(
|
|
214
|
+
ax_record = AxesRecord(
|
|
215
|
+
position=(row, col),
|
|
216
|
+
caption=ax_data.get("caption"),
|
|
217
|
+
stats=ax_data.get("stats"),
|
|
218
|
+
visible=ax_data.get("visible", True),
|
|
219
|
+
)
|
|
171
220
|
for call_data in ax_data.get("calls", []):
|
|
172
221
|
ax_record.calls.append(CallRecord.from_dict(call_data, (row, col)))
|
|
173
222
|
for dec_data in ax_data.get("decorations", []):
|
|
@@ -243,6 +292,9 @@ class Recorder:
|
|
|
243
292
|
if call_id is None:
|
|
244
293
|
call_id = self._generate_call_id(method_name)
|
|
245
294
|
|
|
295
|
+
# Extract stats from kwargs before processing (stats is metadata, not matplotlib arg)
|
|
296
|
+
call_stats = kwargs.pop("stats", None) if "stats" in kwargs else None
|
|
297
|
+
|
|
246
298
|
# Process args into serializable format
|
|
247
299
|
processed_args = self._process_args(args, method_name)
|
|
248
300
|
|
|
@@ -255,6 +307,7 @@ class Recorder:
|
|
|
255
307
|
args=processed_args,
|
|
256
308
|
kwargs=processed_kwargs,
|
|
257
309
|
ax_position=ax_position,
|
|
310
|
+
stats=call_stats,
|
|
258
311
|
)
|
|
259
312
|
|
|
260
313
|
# Add to appropriate axes
|
|
@@ -272,111 +325,12 @@ class Recorder:
|
|
|
272
325
|
args: tuple,
|
|
273
326
|
method_name: str,
|
|
274
327
|
) -> List[Dict[str, Any]]:
|
|
275
|
-
"""Process positional arguments for storage.
|
|
276
|
-
|
|
277
|
-
Parameters
|
|
278
|
-
----------
|
|
279
|
-
args : tuple
|
|
280
|
-
Raw positional arguments.
|
|
281
|
-
method_name : str
|
|
282
|
-
Name of the method.
|
|
328
|
+
"""Process positional arguments for storage."""
|
|
329
|
+
from ._recorder_utils import process_args
|
|
283
330
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
Processed args with name and data.
|
|
288
|
-
"""
|
|
289
|
-
from ._utils._numpy_io import should_store_inline, to_serializable
|
|
290
|
-
|
|
291
|
-
processed = []
|
|
292
|
-
# Simple arg names based on common patterns
|
|
293
|
-
arg_names = self._get_arg_names(method_name, len(args))
|
|
294
|
-
|
|
295
|
-
for i, (name, value) in enumerate(zip(arg_names, args)):
|
|
296
|
-
# Handle result references (e.g., ContourSet for clabel)
|
|
297
|
-
if isinstance(value, dict) and "__ref__" in value:
|
|
298
|
-
processed.append(
|
|
299
|
-
{
|
|
300
|
-
"name": name,
|
|
301
|
-
"data": {"__ref__": value["__ref__"]},
|
|
302
|
-
}
|
|
303
|
-
)
|
|
304
|
-
continue
|
|
305
|
-
|
|
306
|
-
if isinstance(value, np.ndarray):
|
|
307
|
-
if should_store_inline(value):
|
|
308
|
-
processed.append(
|
|
309
|
-
{
|
|
310
|
-
"name": name,
|
|
311
|
-
"data": to_serializable(value),
|
|
312
|
-
"dtype": str(value.dtype),
|
|
313
|
-
}
|
|
314
|
-
)
|
|
315
|
-
else:
|
|
316
|
-
# Mark for file storage (will be handled by serializer)
|
|
317
|
-
processed.append(
|
|
318
|
-
{
|
|
319
|
-
"name": name,
|
|
320
|
-
"data": "__FILE__",
|
|
321
|
-
"dtype": str(value.dtype),
|
|
322
|
-
"_array": value, # Temporary, removed during serialization
|
|
323
|
-
}
|
|
324
|
-
)
|
|
325
|
-
elif hasattr(value, "values"): # pandas
|
|
326
|
-
arr = np.asarray(value)
|
|
327
|
-
if should_store_inline(arr):
|
|
328
|
-
processed.append(
|
|
329
|
-
{
|
|
330
|
-
"name": name,
|
|
331
|
-
"data": to_serializable(arr),
|
|
332
|
-
"dtype": str(arr.dtype),
|
|
333
|
-
}
|
|
334
|
-
)
|
|
335
|
-
else:
|
|
336
|
-
processed.append(
|
|
337
|
-
{
|
|
338
|
-
"name": name,
|
|
339
|
-
"data": "__FILE__",
|
|
340
|
-
"dtype": str(arr.dtype),
|
|
341
|
-
"_array": arr,
|
|
342
|
-
}
|
|
343
|
-
)
|
|
344
|
-
elif (
|
|
345
|
-
isinstance(value, (list, tuple))
|
|
346
|
-
and len(value) > 0
|
|
347
|
-
and isinstance(value[0], np.ndarray)
|
|
348
|
-
):
|
|
349
|
-
# List of arrays (e.g., boxplot, violinplot data)
|
|
350
|
-
arrays_data = [to_serializable(arr) for arr in value]
|
|
351
|
-
dtypes = [str(arr.dtype) for arr in value]
|
|
352
|
-
processed.append(
|
|
353
|
-
{
|
|
354
|
-
"name": name,
|
|
355
|
-
"data": arrays_data,
|
|
356
|
-
"dtype": (dtypes[0] if len(set(dtypes)) == 1 else dtypes),
|
|
357
|
-
"_is_array_list": True,
|
|
358
|
-
}
|
|
359
|
-
)
|
|
360
|
-
else:
|
|
361
|
-
# Scalar or other serializable value
|
|
362
|
-
try:
|
|
363
|
-
processed.append(
|
|
364
|
-
{
|
|
365
|
-
"name": name,
|
|
366
|
-
"data": (
|
|
367
|
-
value if self._is_serializable(value) else str(value)
|
|
368
|
-
),
|
|
369
|
-
}
|
|
370
|
-
)
|
|
371
|
-
except (TypeError, ValueError):
|
|
372
|
-
processed.append(
|
|
373
|
-
{
|
|
374
|
-
"name": name,
|
|
375
|
-
"data": str(value),
|
|
376
|
-
}
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
return processed
|
|
331
|
+
return process_args(
|
|
332
|
+
args, method_name, self._get_arg_names, self._is_serializable
|
|
333
|
+
)
|
|
380
334
|
|
|
381
335
|
def _get_arg_names(self, method_name: str, n_args: int) -> List[str]:
|
|
382
336
|
"""Get argument names for a method from signatures.
|
|
@@ -436,8 +390,8 @@ class Recorder:
|
|
|
436
390
|
except Exception:
|
|
437
391
|
pass
|
|
438
392
|
|
|
439
|
-
# Remove internal keys
|
|
440
|
-
skip_keys = {"id", "track", "_array"}
|
|
393
|
+
# Remove internal keys (stats is handled separately as metadata)
|
|
394
|
+
skip_keys = {"id", "track", "_array", "stats"}
|
|
441
395
|
processed = {}
|
|
442
396
|
|
|
443
397
|
for key, value in kwargs.items():
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Utilities for recorder argument processing."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def process_args(
|
|
11
|
+
args: tuple,
|
|
12
|
+
method_name: str,
|
|
13
|
+
get_arg_names_func,
|
|
14
|
+
is_serializable_func,
|
|
15
|
+
) -> List[Dict[str, Any]]:
|
|
16
|
+
"""Process positional arguments for storage.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
args : tuple
|
|
21
|
+
Raw positional arguments.
|
|
22
|
+
method_name : str
|
|
23
|
+
Name of the method.
|
|
24
|
+
get_arg_names_func : callable
|
|
25
|
+
Function to get argument names.
|
|
26
|
+
is_serializable_func : callable
|
|
27
|
+
Function to check serializability.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
list
|
|
32
|
+
Processed args with name and data.
|
|
33
|
+
"""
|
|
34
|
+
from ._utils._numpy_io import should_store_inline, to_serializable
|
|
35
|
+
|
|
36
|
+
processed = []
|
|
37
|
+
arg_names = get_arg_names_func(method_name, len(args))
|
|
38
|
+
|
|
39
|
+
for name, value in zip(arg_names, args):
|
|
40
|
+
processed_arg = _process_single_arg(
|
|
41
|
+
name, value, should_store_inline, to_serializable, is_serializable_func
|
|
42
|
+
)
|
|
43
|
+
processed.append(processed_arg)
|
|
44
|
+
|
|
45
|
+
return processed
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _process_single_arg(
|
|
49
|
+
name: str,
|
|
50
|
+
value: Any,
|
|
51
|
+
should_store_inline,
|
|
52
|
+
to_serializable,
|
|
53
|
+
is_serializable_func,
|
|
54
|
+
) -> Dict[str, Any]:
|
|
55
|
+
"""Process a single argument value."""
|
|
56
|
+
# Handle result references (e.g., ContourSet for clabel)
|
|
57
|
+
if isinstance(value, dict) and "__ref__" in value:
|
|
58
|
+
return {"name": name, "data": {"__ref__": value["__ref__"]}}
|
|
59
|
+
|
|
60
|
+
if isinstance(value, np.ndarray):
|
|
61
|
+
return _process_ndarray(name, value, should_store_inline, to_serializable)
|
|
62
|
+
|
|
63
|
+
if hasattr(value, "values"): # pandas
|
|
64
|
+
arr = np.asarray(value)
|
|
65
|
+
return _process_ndarray(name, arr, should_store_inline, to_serializable)
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
isinstance(value, (list, tuple))
|
|
69
|
+
and len(value) > 0
|
|
70
|
+
and isinstance(value[0], np.ndarray)
|
|
71
|
+
):
|
|
72
|
+
# List of arrays (e.g., boxplot, violinplot data)
|
|
73
|
+
return _process_array_list(name, value, to_serializable)
|
|
74
|
+
|
|
75
|
+
# Scalar or other serializable value
|
|
76
|
+
return _process_scalar(name, value, is_serializable_func)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _process_ndarray(
|
|
80
|
+
name: str, value: np.ndarray, should_store_inline, to_serializable
|
|
81
|
+
) -> Dict[str, Any]:
|
|
82
|
+
"""Process numpy array argument."""
|
|
83
|
+
if should_store_inline(value):
|
|
84
|
+
return {
|
|
85
|
+
"name": name,
|
|
86
|
+
"data": to_serializable(value),
|
|
87
|
+
"dtype": str(value.dtype),
|
|
88
|
+
}
|
|
89
|
+
else:
|
|
90
|
+
# Mark for file storage (will be handled by serializer)
|
|
91
|
+
return {
|
|
92
|
+
"name": name,
|
|
93
|
+
"data": "__FILE__",
|
|
94
|
+
"dtype": str(value.dtype),
|
|
95
|
+
"_array": value, # Temporary, removed during serialization
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _process_array_list(name: str, value: list, to_serializable) -> Dict[str, Any]:
|
|
100
|
+
"""Process list of arrays argument."""
|
|
101
|
+
arrays_data = [to_serializable(arr) for arr in value]
|
|
102
|
+
dtypes = [str(arr.dtype) for arr in value]
|
|
103
|
+
return {
|
|
104
|
+
"name": name,
|
|
105
|
+
"data": arrays_data,
|
|
106
|
+
"dtype": (dtypes[0] if len(set(dtypes)) == 1 else dtypes),
|
|
107
|
+
"_is_array_list": True,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _process_scalar(name: str, value: Any, is_serializable_func) -> Dict[str, Any]:
|
|
112
|
+
"""Process scalar or other value."""
|
|
113
|
+
try:
|
|
114
|
+
return {
|
|
115
|
+
"name": name,
|
|
116
|
+
"data": value if is_serializable_func(value) else str(value),
|
|
117
|
+
}
|
|
118
|
+
except (TypeError, ValueError):
|
|
119
|
+
return {"name": name, "data": str(value)}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
__all__ = ["process_args"]
|
|
123
|
+
|
|
124
|
+
# EOF
|