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.
- figrecipe/__init__.py +74 -76
- figrecipe/__main__.py +12 -0
- figrecipe/_api/_panel.py +67 -0
- figrecipe/_api/_save.py +100 -4
- 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 +2 -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/_editor/__init__.py +36 -36
- figrecipe/_editor/_bbox/_extract.py +155 -9
- figrecipe/_editor/_bbox/_extract_text.py +124 -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 +157 -16
- figrecipe/_editor/_helpers.py +17 -8
- figrecipe/_editor/_hitmap/_detect.py +89 -32
- figrecipe/_editor/_hitmap_main.py +4 -4
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_render_overrides.py +38 -11
- figrecipe/_editor/_renderer.py +46 -1
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +35 -6
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +15 -173
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +37 -19
- 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 +28 -8
- figrecipe/_editor/_templates/__init__.py +40 -2
- figrecipe/_editor/_templates/_html.py +97 -103
- 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 +58 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +1 -1
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +94 -37
- 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/_element_editor.py +17 -2
- figrecipe/_editor/_templates/_scripts/_files.py +274 -40
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +87 -84
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +5 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +219 -48
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +238 -54
- 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 +8 -1
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +52 -19
- figrecipe/_editor/_templates/_styles/__init__.py +9 -0
- figrecipe/_editor/_templates/_styles/_base.py +47 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +127 -6
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +168 -3
- 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 +5 -5
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +98 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +7 -0
- figrecipe/_editor/_templates/_styles/_modals.py +29 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +5 -5
- figrecipe/_editor/_templates/_styles/_preview.py +213 -8
- 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 +2 -0
- figrecipe/_recorder.py +28 -3
- figrecipe/_reproducer/_core.py +60 -49
- figrecipe/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +150 -2
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +26 -1
- figrecipe/_wrappers/_stat_annotation.py +274 -0
- figrecipe/styles/_style_applier.py +10 -2
- figrecipe/styles/presets/SCITEX.yaml +11 -4
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/METADATA +144 -146
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
- figrecipe-0.7.4.dist-info/RECORD +0 -188
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
- {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
|
|
28
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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:
|
|
160
|
-
justify-content:
|
|
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
|
|
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
|
+
]
|