figrecipe 0.7.4__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.
Files changed (143) hide show
  1. figrecipe/__init__.py +74 -76
  2. figrecipe/__main__.py +12 -0
  3. figrecipe/_api/_panel.py +67 -0
  4. figrecipe/_api/_save.py +100 -4
  5. figrecipe/_cli/__init__.py +7 -0
  6. figrecipe/_cli/_compose.py +87 -0
  7. figrecipe/_cli/_convert.py +117 -0
  8. figrecipe/_cli/_crop.py +82 -0
  9. figrecipe/_cli/_edit.py +70 -0
  10. figrecipe/_cli/_extract.py +128 -0
  11. figrecipe/_cli/_fonts.py +47 -0
  12. figrecipe/_cli/_info.py +67 -0
  13. figrecipe/_cli/_main.py +58 -0
  14. figrecipe/_cli/_reproduce.py +79 -0
  15. figrecipe/_cli/_style.py +77 -0
  16. figrecipe/_cli/_validate.py +66 -0
  17. figrecipe/_cli/_version.py +50 -0
  18. figrecipe/_composition/__init__.py +32 -0
  19. figrecipe/_composition/_alignment.py +452 -0
  20. figrecipe/_composition/_compose.py +179 -0
  21. figrecipe/_composition/_import_axes.py +127 -0
  22. figrecipe/_composition/_visibility.py +125 -0
  23. figrecipe/_dev/__init__.py +2 -0
  24. figrecipe/_dev/browser/__init__.py +69 -0
  25. figrecipe/_dev/browser/_audio.py +240 -0
  26. figrecipe/_dev/browser/_caption.py +356 -0
  27. figrecipe/_dev/browser/_click_effect.py +146 -0
  28. figrecipe/_dev/browser/_cursor.py +196 -0
  29. figrecipe/_dev/browser/_highlight.py +105 -0
  30. figrecipe/_dev/browser/_narration.py +237 -0
  31. figrecipe/_dev/browser/_recorder.py +446 -0
  32. figrecipe/_dev/browser/_utils.py +178 -0
  33. figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
  34. figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
  35. figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
  36. figrecipe/_editor/__init__.py +36 -36
  37. figrecipe/_editor/_bbox/_extract.py +155 -9
  38. figrecipe/_editor/_bbox/_extract_text.py +124 -0
  39. figrecipe/_editor/_call_overrides.py +183 -0
  40. figrecipe/_editor/_datatable_plot_handlers.py +249 -0
  41. figrecipe/_editor/_figure_layout.py +211 -0
  42. figrecipe/_editor/_flask_app.py +157 -16
  43. figrecipe/_editor/_helpers.py +17 -8
  44. figrecipe/_editor/_hitmap/_detect.py +89 -32
  45. figrecipe/_editor/_hitmap_main.py +4 -4
  46. figrecipe/_editor/_overrides.py +4 -1
  47. figrecipe/_editor/_plot_types_registry.py +190 -0
  48. figrecipe/_editor/_render_overrides.py +38 -11
  49. figrecipe/_editor/_renderer.py +46 -1
  50. figrecipe/_editor/_routes_annotation.py +114 -0
  51. figrecipe/_editor/_routes_axis.py +35 -6
  52. figrecipe/_editor/_routes_captions.py +130 -0
  53. figrecipe/_editor/_routes_composition.py +270 -0
  54. figrecipe/_editor/_routes_core.py +15 -173
  55. figrecipe/_editor/_routes_datatable.py +364 -0
  56. figrecipe/_editor/_routes_element.py +37 -19
  57. figrecipe/_editor/_routes_files.py +443 -0
  58. figrecipe/_editor/_routes_image.py +200 -0
  59. figrecipe/_editor/_routes_snapshot.py +94 -0
  60. figrecipe/_editor/_routes_style.py +28 -8
  61. figrecipe/_editor/_templates/__init__.py +40 -2
  62. figrecipe/_editor/_templates/_html.py +97 -103
  63. figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
  64. figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
  65. figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
  66. figrecipe/_editor/_templates/_html_datatable.py +92 -0
  67. figrecipe/_editor/_templates/_scripts/__init__.py +58 -0
  68. figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
  69. figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
  70. figrecipe/_editor/_templates/_scripts/_api.py +1 -1
  71. figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
  72. figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
  73. figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
  74. figrecipe/_editor/_templates/_scripts/_core.py +94 -37
  75. figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
  76. figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
  77. figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
  78. figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
  79. figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
  80. figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
  81. figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
  82. figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
  83. figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
  84. figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
  85. figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
  86. figrecipe/_editor/_templates/_scripts/_element_editor.py +17 -2
  87. figrecipe/_editor/_templates/_scripts/_files.py +274 -40
  88. figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
  89. figrecipe/_editor/_templates/_scripts/_hitmap.py +87 -84
  90. figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
  91. figrecipe/_editor/_templates/_scripts/_legend_drag.py +5 -0
  92. figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
  93. figrecipe/_editor/_templates/_scripts/_panel_drag.py +219 -48
  94. figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
  95. figrecipe/_editor/_templates/_scripts/_panel_position.py +238 -54
  96. figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
  97. figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
  98. figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
  99. figrecipe/_editor/_templates/_scripts/_selection.py +8 -1
  100. figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
  101. figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
  102. figrecipe/_editor/_templates/_scripts/_zoom.py +52 -19
  103. figrecipe/_editor/_templates/_styles/__init__.py +9 -0
  104. figrecipe/_editor/_templates/_styles/_base.py +47 -0
  105. figrecipe/_editor/_templates/_styles/_buttons.py +127 -6
  106. figrecipe/_editor/_templates/_styles/_composition.py +87 -0
  107. figrecipe/_editor/_templates/_styles/_controls.py +168 -3
  108. figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
  109. figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
  110. figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
  111. figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
  112. figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
  113. figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
  114. figrecipe/_editor/_templates/_styles/_dynamic_props.py +5 -5
  115. figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
  116. figrecipe/_editor/_templates/_styles/_forms.py +98 -0
  117. figrecipe/_editor/_templates/_styles/_hitmap.py +7 -0
  118. figrecipe/_editor/_templates/_styles/_modals.py +29 -0
  119. figrecipe/_editor/_templates/_styles/_overlays.py +5 -5
  120. figrecipe/_editor/_templates/_styles/_preview.py +213 -8
  121. figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
  122. figrecipe/_editor/static/audio/click.mp3 +0 -0
  123. figrecipe/_editor/static/click.mp3 +0 -0
  124. figrecipe/_editor/static/icons/favicon.ico +0 -0
  125. figrecipe/_integrations/__init__.py +17 -0
  126. figrecipe/_integrations/_scitex_stats.py +298 -0
  127. figrecipe/_params/_DECORATION_METHODS.py +2 -0
  128. figrecipe/_recorder.py +28 -3
  129. figrecipe/_reproducer/_core.py +60 -49
  130. figrecipe/_utils/__init__.py +3 -0
  131. figrecipe/_utils/_bundle.py +205 -0
  132. figrecipe/_wrappers/_axes.py +150 -2
  133. figrecipe/_wrappers/_caption_generator.py +218 -0
  134. figrecipe/_wrappers/_figure.py +26 -1
  135. figrecipe/_wrappers/_stat_annotation.py +274 -0
  136. figrecipe/styles/_style_applier.py +10 -2
  137. figrecipe/styles/presets/SCITEX.yaml +11 -4
  138. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/METADATA +144 -146
  139. figrecipe-0.9.0.dist-info/RECORD +277 -0
  140. figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
  141. figrecipe-0.7.4.dist-info/RECORD +0 -188
  142. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
  143. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -18,22 +18,102 @@ STYLES_PREVIEW = """
18
18
  flex-direction: column;
19
19
  border-right: 1px solid var(--border-color);
20
20
  min-width: 400px;
21
+ max-width: none;
22
+ width: auto;
23
+ order: 0; /* Default order when expanded */
24
+ transition: flex 0.2s ease-out, min-width 0.2s ease-out, width 0.2s ease-out, order 0s;
25
+ position: relative;
26
+ }
27
+
28
+ /* Collapsed preview panel - collapses to RIGHT side */
29
+ .preview-panel.collapsed {
30
+ flex: 0 0 42px !important;
31
+ min-width: 42px !important;
32
+ max-width: 42px !important;
33
+ width: 42px !important;
34
+ order: 10; /* Move to end (after data panel expands) */
35
+ }
36
+
37
+ /* When canvas collapses, data panel expands to fill space */
38
+ .editor-container:has(.preview-panel.collapsed) .datatable-panel {
39
+ flex: 1 !important;
40
+ max-width: none !important;
41
+ width: auto !important;
42
+ }
43
+
44
+ .preview-panel.collapsed .preview-wrapper,
45
+ .preview-panel.collapsed .preview-controls,
46
+ .preview-panel.collapsed .scitex-branding,
47
+ .preview-panel.collapsed #server-start-time {
48
+ display: none;
49
+ }
50
+
51
+ .preview-panel.collapsed .preview-header {
52
+ flex-direction: column;
53
+ justify-content: flex-start;
54
+ padding: 12px 8px;
55
+ height: 100%;
56
+ }
57
+
58
+ /* Flip button to point right (expand direction) when collapsed */
59
+ .preview-panel.collapsed .btn-collapse {
60
+ transform: rotate(180deg);
21
61
  }
22
62
 
23
63
  .preview-header {
24
64
  display: flex;
25
65
  justify-content: space-between;
26
66
  align-items: center;
27
- padding: 12px 16px;
28
- background: var(--bg-secondary);
67
+ padding: 0 12px;
68
+ height: var(--panel-header-height);
69
+ min-height: var(--panel-header-height);
70
+ background: var(--panel-header-bg);
29
71
  border-bottom: 1px solid var(--border-color);
30
72
  }
31
73
 
74
+ /* Preview header collapse button */
75
+ .preview-header .btn-collapse {
76
+ width: 28px;
77
+ height: 28px;
78
+ padding: 0;
79
+ border: none;
80
+ background: transparent;
81
+ color: var(--text-secondary);
82
+ cursor: pointer;
83
+ font-size: 14px;
84
+ display: flex;
85
+ align-items: center;
86
+ justify-content: center;
87
+ border-radius: 4px;
88
+ transition: all 0.2s;
89
+ flex-shrink: 0;
90
+ margin-right: 8px;
91
+ }
92
+
93
+ .preview-header .btn-collapse:hover {
94
+ background: var(--bg-tertiary);
95
+ color: var(--text-primary);
96
+ }
97
+
32
98
  .preview-header h2 {
33
99
  font-size: 16px;
34
100
  font-weight: 600;
35
101
  }
36
102
 
103
+ /* Panel label for CANVAS */
104
+ .preview-header .panel-label {
105
+ font-size: 11px;
106
+ font-weight: 600;
107
+ text-transform: uppercase;
108
+ letter-spacing: 0.5px;
109
+ color: var(--text-secondary);
110
+ margin-right: auto;
111
+ }
112
+
113
+ .preview-panel.collapsed .panel-label {
114
+ display: none;
115
+ }
116
+
37
117
  /* SciTeX branding */
38
118
  .scitex-branding {
39
119
  display: flex;
@@ -146,18 +226,42 @@ STYLES_PREVIEW = """
146
226
  justify-content: center;
147
227
  }
148
228
 
149
- .zoom-controls #zoom-level {
150
- min-width: 45px;
151
- text-align: center;
229
+ /* Text buttons in zoom controls need smaller font */
230
+ .zoom-controls #btn-zoom-fit {
231
+ width: auto;
232
+ padding: 0 8px;
152
233
  font-size: 12px;
153
- font-weight: 500;
234
+ }
235
+
236
+ /* Zoom dropdown select */
237
+ .zoom-controls select {
238
+ height: 28px;
239
+ padding: 0 8px;
240
+ font-size: 12px;
241
+ border: 1px solid var(--border-color);
242
+ border-radius: 4px;
243
+ background: var(--bg-primary);
244
+ color: var(--text-primary);
245
+ cursor: pointer;
246
+ }
247
+
248
+ .zoom-controls select:hover {
249
+ border-color: var(--accent-color);
250
+ }
251
+
252
+ /* Toolbar separator */
253
+ .toolbar-separator {
254
+ width: 1px;
255
+ height: 24px;
256
+ background: var(--border-color);
257
+ margin: 0 4px;
154
258
  }
155
259
 
156
260
  .preview-wrapper {
157
261
  flex: 1;
158
262
  display: flex;
159
- align-items: center;
160
- justify-content: center;
263
+ align-items: flex-start;
264
+ justify-content: flex-start;
161
265
  padding: 20px;
162
266
  background: var(--bg-tertiary);
163
267
  position: relative;
@@ -218,6 +322,107 @@ STYLES_PREVIEW = """
218
322
  .preview-wrapper.panning * {
219
323
  cursor: grabbing !important;
220
324
  }
325
+
326
+ /* Welcome Overlay - covers the entire preview area */
327
+ .welcome-overlay {
328
+ position: absolute;
329
+ top: 0;
330
+ left: 0;
331
+ right: 0;
332
+ bottom: 0;
333
+ display: flex;
334
+ align-items: center;
335
+ justify-content: center;
336
+ z-index: 100;
337
+ pointer-events: none;
338
+ background: var(--bg-tertiary);
339
+ }
340
+
341
+ .welcome-content {
342
+ background: var(--bg-secondary);
343
+ border: 1px solid var(--border-color);
344
+ border-radius: 12px;
345
+ padding: 32px 40px;
346
+ text-align: center;
347
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
348
+ max-width: 360px;
349
+ }
350
+
351
+ .welcome-content h2 {
352
+ margin: 0 0 24px 0;
353
+ font-size: 20px;
354
+ font-weight: 600;
355
+ color: var(--text-primary);
356
+ }
357
+
358
+ .welcome-steps {
359
+ display: flex;
360
+ flex-direction: column;
361
+ gap: 16px;
362
+ text-align: left;
363
+ }
364
+
365
+ .welcome-step {
366
+ display: flex;
367
+ align-items: center;
368
+ gap: 12px;
369
+ }
370
+
371
+ .step-number {
372
+ width: 28px;
373
+ height: 28px;
374
+ background: var(--accent-color);
375
+ color: white;
376
+ border-radius: 50%;
377
+ display: flex;
378
+ align-items: center;
379
+ justify-content: center;
380
+ font-size: 14px;
381
+ font-weight: 600;
382
+ flex-shrink: 0;
383
+ }
384
+
385
+ .step-text {
386
+ color: var(--text-secondary);
387
+ font-size: 14px;
388
+ line-height: 1.4;
389
+ }
390
+
391
+ .step-text strong {
392
+ color: var(--text-primary);
393
+ }
394
+
395
+ .welcome-hint {
396
+ margin: 20px 0 0 0;
397
+ padding-top: 16px;
398
+ border-top: 1px solid var(--border-color);
399
+ color: var(--text-tertiary);
400
+ font-size: 13px;
401
+ }
402
+
403
+ /* Caption Pane (below canvas) */
404
+ .caption-pane {
405
+ padding: 10px 20px;
406
+ background: var(--bg-secondary);
407
+ border-top: 1px solid var(--border-color);
408
+ font-size: 13px;
409
+ line-height: 1.5;
410
+ color: var(--text-primary);
411
+ min-height: 32px;
412
+ }
413
+
414
+ .caption-pane b {
415
+ font-weight: 600;
416
+ }
417
+
418
+ .caption-pane .panel-caption {
419
+ color: var(--text-secondary);
420
+ }
421
+
422
+ /* Hide caption pane when canvas collapsed */
423
+ .preview-panel.collapsed .caption-pane {
424
+ display: none;
425
+ }
221
426
  """
222
427
 
223
428
  __all__ = ["STYLES_PREVIEW"]
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Spinner/loading indicator styles for the figure editor."""
4
+
5
+ STYLES_SPINNER = """
6
+ /* ==================== SPINNER/LOADING OVERLAY ==================== */
7
+
8
+ /* Spinner overlay container */
9
+ .spinner-overlay {
10
+ position: fixed;
11
+ top: 0;
12
+ left: 0;
13
+ right: 0;
14
+ bottom: 0;
15
+ background: rgba(0, 0, 0, 0.4);
16
+ display: none;
17
+ justify-content: center;
18
+ align-items: center;
19
+ z-index: 10000;
20
+ backdrop-filter: blur(2px);
21
+ }
22
+
23
+ /* Show spinner when body has loading class */
24
+ body.loading .spinner-overlay {
25
+ display: flex;
26
+ }
27
+
28
+ /* Spinner container */
29
+ .spinner-container {
30
+ display: flex;
31
+ flex-direction: column;
32
+ align-items: center;
33
+ gap: 12px;
34
+ }
35
+
36
+ /* The spinner itself */
37
+ .spinner {
38
+ width: 48px;
39
+ height: 48px;
40
+ border: 4px solid rgba(255, 255, 255, 0.2);
41
+ border-top-color: var(--accent-color, #2563eb);
42
+ border-radius: 50%;
43
+ animation: spinner-rotate 0.8s linear infinite;
44
+ }
45
+
46
+ /* Spinner animation */
47
+ @keyframes spinner-rotate {
48
+ from {
49
+ transform: rotate(0deg);
50
+ }
51
+ to {
52
+ transform: rotate(360deg);
53
+ }
54
+ }
55
+
56
+ /* Loading text */
57
+ .spinner-text {
58
+ color: white;
59
+ font-size: 14px;
60
+ font-weight: 500;
61
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
62
+ }
63
+
64
+ /* Dark mode adjustments */
65
+ [data-theme="dark"] .spinner-overlay {
66
+ background: rgba(0, 0, 0, 0.6);
67
+ }
68
+
69
+ [data-theme="dark"] .spinner {
70
+ border-color: rgba(255, 255, 255, 0.1);
71
+ border-top-color: var(--accent-color, #3b82f6);
72
+ }
73
+
74
+ /* Mini spinner for inline use */
75
+ .spinner-mini {
76
+ width: 16px;
77
+ height: 16px;
78
+ border-width: 2px;
79
+ display: inline-block;
80
+ vertical-align: middle;
81
+ margin-right: 6px;
82
+ }
83
+
84
+ /* Button loading state */
85
+ button.btn-loading {
86
+ position: relative;
87
+ color: transparent !important;
88
+ }
89
+
90
+ button.btn-loading::after {
91
+ content: '';
92
+ position: absolute;
93
+ top: 50%;
94
+ left: 50%;
95
+ width: 16px;
96
+ height: 16px;
97
+ margin: -8px 0 0 -8px;
98
+ border: 2px solid rgba(255, 255, 255, 0.3);
99
+ border-top-color: white;
100
+ border-radius: 50%;
101
+ animation: spinner-rotate 0.8s linear infinite;
102
+ }
103
+
104
+ /* Loading state - keep existing behavior but remove pointer events */
105
+ body.loading {
106
+ pointer-events: none;
107
+ }
108
+
109
+ /* Keep controls interactive even when loading */
110
+ body.loading .spinner-overlay {
111
+ pointer-events: auto;
112
+ }
113
+ """
114
+
115
+ __all__ = ["STYLES_SPINNER"]
116
+
117
+ # EOF
Binary file
Binary file
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Integrations with external packages."""
4
+
5
+ from ._scitex_stats import (
6
+ SCITEX_STATS_AVAILABLE,
7
+ annotate_from_stats,
8
+ from_scitex_stats,
9
+ load_stats_bundle,
10
+ )
11
+
12
+ __all__ = [
13
+ "SCITEX_STATS_AVAILABLE",
14
+ "annotate_from_stats",
15
+ "from_scitex_stats",
16
+ "load_stats_bundle",
17
+ ]
@@ -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
+ ]
@@ -28,6 +28,8 @@ DECORATION_METHODS = {
28
28
  "set_xticklabels",
29
29
  "set_yticklabels",
30
30
  "tick_params",
31
+ # Statistical annotations
32
+ "stat_annotation", # Comparison brackets with stars/p-values
31
33
  }
32
34
 
33
35
  # EOF