figrecipe 0.6.0__py3-none-any.whl → 0.7.4__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 +106 -973
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +46 -0
- figrecipe/_api/_save.py +191 -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/_dev/__init__.py +2 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -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 +57 -9
- 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 +256 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +342 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_flask_app.py +68 -1039
- figrecipe/_editor/_helpers.py +242 -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 +137 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +35 -185
- figrecipe/_editor/_routes_axis.py +453 -0
- figrecipe/_editor/_routes_core.py +284 -0
- figrecipe/_editor/_routes_element.py +317 -0
- figrecipe/_editor/_routes_style.py +223 -0
- figrecipe/_editor/_templates/__init__.py +78 -1
- figrecipe/_editor/_templates/_html.py +109 -13
- figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_core.py +436 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
- figrecipe/_editor/_templates/_scripts/_files.py +195 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
- figrecipe/_editor/_templates/_styles/__init__.py +69 -0
- figrecipe/_editor/_templates/_styles/_base.py +64 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_controls.py +265 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_forms.py +126 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +98 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +225 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_params/_DECORATION_METHODS.py +6 -0
- figrecipe/_recorder.py +35 -106
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +498 -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/_wrappers/_axes.py +119 -910
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_figure.py +162 -0
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -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 +32 -478
- 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 +29 -24
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
- figrecipe-0.7.4.dist-info/RECORD +188 -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/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.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Preview panel CSS styles for the figure editor.
|
|
4
|
+
|
|
5
|
+
This module contains CSS for:
|
|
6
|
+
- Preview panel and header
|
|
7
|
+
- SciTeX branding
|
|
8
|
+
- File switcher
|
|
9
|
+
- Zoom controls
|
|
10
|
+
- Preview wrapper and checkerboard background
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
STYLES_PREVIEW = """
|
|
14
|
+
/* Preview Panel */
|
|
15
|
+
.preview-panel {
|
|
16
|
+
flex: 1;
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
border-right: 1px solid var(--border-color);
|
|
20
|
+
min-width: 400px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.preview-header {
|
|
24
|
+
display: flex;
|
|
25
|
+
justify-content: space-between;
|
|
26
|
+
align-items: center;
|
|
27
|
+
padding: 12px 16px;
|
|
28
|
+
background: var(--bg-secondary);
|
|
29
|
+
border-bottom: 1px solid var(--border-color);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.preview-header h2 {
|
|
33
|
+
font-size: 16px;
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* SciTeX branding */
|
|
38
|
+
.scitex-branding {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
gap: 8px;
|
|
42
|
+
text-decoration: none;
|
|
43
|
+
color: var(--text-primary);
|
|
44
|
+
transition: opacity 0.2s;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.scitex-branding:hover {
|
|
48
|
+
opacity: 0.8;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.scitex-icon {
|
|
52
|
+
width: 28px;
|
|
53
|
+
height: 28px;
|
|
54
|
+
border-radius: 4px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.figrecipe-title {
|
|
58
|
+
font-size: 16px;
|
|
59
|
+
font-weight: 600;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* File Switcher */
|
|
63
|
+
.file-switcher {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 6px;
|
|
67
|
+
margin-left: 16px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.file-selector {
|
|
71
|
+
padding: 6px 10px;
|
|
72
|
+
font-size: 13px;
|
|
73
|
+
border: 1px solid var(--border-color);
|
|
74
|
+
border-radius: 4px;
|
|
75
|
+
background: var(--bg-primary);
|
|
76
|
+
color: var(--text-primary);
|
|
77
|
+
min-width: 180px;
|
|
78
|
+
max-width: 280px;
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.file-selector:hover {
|
|
83
|
+
border-color: var(--accent-color);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.file-selector:focus {
|
|
87
|
+
outline: none;
|
|
88
|
+
border-color: var(--accent-color);
|
|
89
|
+
box-shadow: 0 0 0 2px var(--selection-color);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.file-selector option {
|
|
93
|
+
padding: 4px 8px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.file-selector option[data-current="true"] {
|
|
97
|
+
font-weight: bold;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.btn-new {
|
|
101
|
+
width: 28px;
|
|
102
|
+
height: 28px;
|
|
103
|
+
padding: 0;
|
|
104
|
+
font-size: 18px;
|
|
105
|
+
font-weight: bold;
|
|
106
|
+
border: 1px solid var(--border-color);
|
|
107
|
+
border-radius: 4px;
|
|
108
|
+
background: var(--bg-primary);
|
|
109
|
+
color: var(--text-primary);
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
justify-content: center;
|
|
114
|
+
transition: all 0.2s;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.btn-new:hover {
|
|
118
|
+
background: var(--accent-color);
|
|
119
|
+
color: white;
|
|
120
|
+
border-color: var(--accent-color);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.preview-controls {
|
|
124
|
+
display: flex;
|
|
125
|
+
gap: 12px;
|
|
126
|
+
align-items: center;
|
|
127
|
+
flex-wrap: wrap;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.zoom-controls {
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
gap: 4px;
|
|
134
|
+
background: var(--bg-secondary);
|
|
135
|
+
border-radius: 4px;
|
|
136
|
+
padding: 2px 6px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.zoom-controls button {
|
|
140
|
+
width: 28px;
|
|
141
|
+
height: 28px;
|
|
142
|
+
padding: 0;
|
|
143
|
+
font-size: 16px;
|
|
144
|
+
display: flex;
|
|
145
|
+
align-items: center;
|
|
146
|
+
justify-content: center;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.zoom-controls #zoom-level {
|
|
150
|
+
min-width: 45px;
|
|
151
|
+
text-align: center;
|
|
152
|
+
font-size: 12px;
|
|
153
|
+
font-weight: 500;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.preview-wrapper {
|
|
157
|
+
flex: 1;
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
justify-content: center;
|
|
161
|
+
padding: 20px;
|
|
162
|
+
background: var(--bg-tertiary);
|
|
163
|
+
position: relative;
|
|
164
|
+
overflow: auto;
|
|
165
|
+
/* Checkerboard pattern for transparency */
|
|
166
|
+
background-image:
|
|
167
|
+
linear-gradient(45deg, #ccc 25%, transparent 25%),
|
|
168
|
+
linear-gradient(-45deg, #ccc 25%, transparent 25%),
|
|
169
|
+
linear-gradient(45deg, transparent 75%, #ccc 75%),
|
|
170
|
+
linear-gradient(-45deg, transparent 75%, #ccc 75%);
|
|
171
|
+
background-size: 20px 20px;
|
|
172
|
+
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* Dark checkerboard in dark mode */
|
|
176
|
+
[data-theme="dark"] .preview-wrapper {
|
|
177
|
+
background-color: #2a2a2a;
|
|
178
|
+
background-image:
|
|
179
|
+
linear-gradient(45deg, #333333 25%, transparent 25%),
|
|
180
|
+
linear-gradient(-45deg, #333333 25%, transparent 25%),
|
|
181
|
+
linear-gradient(45deg, transparent 75%, #333333 75%),
|
|
182
|
+
linear-gradient(-45deg, transparent 75%, #333333 75%);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#preview-image {
|
|
186
|
+
max-width: none;
|
|
187
|
+
max-height: none;
|
|
188
|
+
object-fit: contain;
|
|
189
|
+
cursor: crosshair;
|
|
190
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
191
|
+
position: relative;
|
|
192
|
+
z-index: 1;
|
|
193
|
+
transform-origin: center center;
|
|
194
|
+
transition: transform 0.1s ease-out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Zoom container for coordinated transform of image and overlays */
|
|
198
|
+
.zoom-container {
|
|
199
|
+
display: inline-block;
|
|
200
|
+
position: relative;
|
|
201
|
+
transform-origin: top left;
|
|
202
|
+
transition: transform 0.1s ease-out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* Show grab cursor hint when zoomed in */
|
|
206
|
+
.preview-wrapper.zoomed-in {
|
|
207
|
+
cursor: grab;
|
|
208
|
+
/* Disable centering when zoomed to allow proper scrolling from top-left */
|
|
209
|
+
align-items: flex-start;
|
|
210
|
+
justify-content: flex-start;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* Pan cursor when dragging */
|
|
214
|
+
.preview-wrapper.panning {
|
|
215
|
+
cursor: grabbing !important;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.preview-wrapper.panning * {
|
|
219
|
+
cursor: grabbing !important;
|
|
220
|
+
}
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
__all__ = ["STYLES_PREVIEW"]
|
|
224
|
+
|
|
225
|
+
# EOF
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Selection overlay CSS styles for the figure editor.
|
|
4
|
+
|
|
5
|
+
This module contains CSS for:
|
|
6
|
+
- Selection overlay container
|
|
7
|
+
- Selection rectangles, polylines, circles
|
|
8
|
+
- Primary selection highlights
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
STYLES_SELECTION = """
|
|
12
|
+
/* Selection Overlay */
|
|
13
|
+
.selection-overlay {
|
|
14
|
+
position: absolute;
|
|
15
|
+
top: 0;
|
|
16
|
+
left: 0;
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 100%;
|
|
19
|
+
pointer-events: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.selection-rect {
|
|
23
|
+
--element-color: #2563eb; /* Default fallback to accent color */
|
|
24
|
+
fill: var(--element-color);
|
|
25
|
+
fill-opacity: 0.15;
|
|
26
|
+
stroke: var(--element-color);
|
|
27
|
+
stroke-opacity: 0.6;
|
|
28
|
+
stroke-width: 2;
|
|
29
|
+
stroke-dasharray: 5, 3;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Primary selection in a group - solid border */
|
|
33
|
+
.selection-rect.selection-primary {
|
|
34
|
+
fill-opacity: 0.2;
|
|
35
|
+
stroke-opacity: 0.8;
|
|
36
|
+
stroke-width: 3;
|
|
37
|
+
stroke-dasharray: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Selection for lines - show stroke along the path */
|
|
41
|
+
.selection-polyline {
|
|
42
|
+
--element-color: #2563eb;
|
|
43
|
+
fill: none !important;
|
|
44
|
+
stroke: var(--element-color);
|
|
45
|
+
stroke-width: 8;
|
|
46
|
+
stroke-opacity: 0.5;
|
|
47
|
+
stroke-linecap: round;
|
|
48
|
+
stroke-linejoin: round;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Selection for scatter points */
|
|
52
|
+
.selection-circle {
|
|
53
|
+
--element-color: #2563eb;
|
|
54
|
+
fill: var(--element-color);
|
|
55
|
+
fill-opacity: 0.3;
|
|
56
|
+
stroke: var(--element-color);
|
|
57
|
+
stroke-opacity: 0.7;
|
|
58
|
+
stroke-width: 2;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Subtle selection for scatter points (less visually overwhelming) */
|
|
62
|
+
.selection-circle-subtle {
|
|
63
|
+
--element-color: #2563eb;
|
|
64
|
+
fill: none;
|
|
65
|
+
stroke: var(--element-color);
|
|
66
|
+
stroke-opacity: 0.4;
|
|
67
|
+
stroke-width: 1.5;
|
|
68
|
+
}
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
__all__ = ["STYLES_SELECTION"]
|
|
72
|
+
|
|
73
|
+
# EOF
|
figrecipe/_recorder.py
CHANGED
|
@@ -58,6 +58,8 @@ class AxesRecord:
|
|
|
58
58
|
position: Tuple[int, int]
|
|
59
59
|
calls: List[CallRecord] = field(default_factory=list)
|
|
60
60
|
decorations: List[CallRecord] = field(default_factory=list)
|
|
61
|
+
# Panel-level caption (e.g., "(A) Description of this panel")
|
|
62
|
+
caption: Optional[str] = None
|
|
61
63
|
|
|
62
64
|
def add_call(self, record: CallRecord) -> None:
|
|
63
65
|
"""Add a plotting call record."""
|
|
@@ -69,10 +71,13 @@ class AxesRecord:
|
|
|
69
71
|
|
|
70
72
|
def to_dict(self) -> Dict[str, Any]:
|
|
71
73
|
"""Convert to dictionary for serialization."""
|
|
72
|
-
|
|
74
|
+
result = {
|
|
73
75
|
"calls": [c.to_dict() for c in self.calls],
|
|
74
76
|
"decorations": [d.to_dict() for d in self.decorations],
|
|
75
77
|
}
|
|
78
|
+
if self.caption is not None:
|
|
79
|
+
result["caption"] = self.caption
|
|
80
|
+
return result
|
|
76
81
|
|
|
77
82
|
|
|
78
83
|
@dataclass
|
|
@@ -95,6 +100,11 @@ class FigureRecord:
|
|
|
95
100
|
suptitle: Optional[Dict[str, Any]] = None
|
|
96
101
|
supxlabel: Optional[Dict[str, Any]] = None
|
|
97
102
|
supylabel: Optional[Dict[str, Any]] = None
|
|
103
|
+
# Panel labels (A, B, C, D for multi-panel figures)
|
|
104
|
+
panel_labels: Optional[Dict[str, Any]] = None
|
|
105
|
+
# Metadata for scientific figures (not rendered, stored in recipe)
|
|
106
|
+
title_metadata: Optional[str] = None # Figure title for publication/reference
|
|
107
|
+
caption: Optional[str] = None # Figure caption (e.g., "Fig. 1. Description...")
|
|
98
108
|
|
|
99
109
|
def get_axes_key(self, row: int, col: int) -> str:
|
|
100
110
|
"""Get dictionary key for axes at position."""
|
|
@@ -138,12 +148,24 @@ class FigureRecord:
|
|
|
138
148
|
# Add supylabel if set
|
|
139
149
|
if self.supylabel is not None:
|
|
140
150
|
result["figure"]["supylabel"] = self.supylabel
|
|
151
|
+
# Add panel_labels if set
|
|
152
|
+
if self.panel_labels is not None:
|
|
153
|
+
result["figure"]["panel_labels"] = self.panel_labels
|
|
154
|
+
# Add metadata section for scientific figures
|
|
155
|
+
metadata = {}
|
|
156
|
+
if self.title_metadata is not None:
|
|
157
|
+
metadata["title"] = self.title_metadata
|
|
158
|
+
if self.caption is not None:
|
|
159
|
+
metadata["caption"] = self.caption
|
|
160
|
+
if metadata:
|
|
161
|
+
result["metadata"] = metadata
|
|
141
162
|
return result
|
|
142
163
|
|
|
143
164
|
@classmethod
|
|
144
165
|
def from_dict(cls, data: Dict[str, Any]) -> "FigureRecord":
|
|
145
166
|
"""Create from dictionary."""
|
|
146
167
|
fig_data = data.get("figure", {})
|
|
168
|
+
metadata = data.get("metadata", {})
|
|
147
169
|
record = cls(
|
|
148
170
|
id=data.get("id", f"fig_{uuid.uuid4().hex[:8]}"),
|
|
149
171
|
created=data.get("created", ""),
|
|
@@ -156,6 +178,9 @@ class FigureRecord:
|
|
|
156
178
|
suptitle=fig_data.get("suptitle"),
|
|
157
179
|
supxlabel=fig_data.get("supxlabel"),
|
|
158
180
|
supylabel=fig_data.get("supylabel"),
|
|
181
|
+
panel_labels=fig_data.get("panel_labels"),
|
|
182
|
+
title_metadata=metadata.get("title"),
|
|
183
|
+
caption=metadata.get("caption"),
|
|
159
184
|
)
|
|
160
185
|
|
|
161
186
|
# Reconstruct axes
|
|
@@ -167,7 +192,10 @@ class FigureRecord:
|
|
|
167
192
|
else:
|
|
168
193
|
row, col = 0, 0
|
|
169
194
|
|
|
170
|
-
ax_record = AxesRecord(
|
|
195
|
+
ax_record = AxesRecord(
|
|
196
|
+
position=(row, col),
|
|
197
|
+
caption=ax_data.get("caption"),
|
|
198
|
+
)
|
|
171
199
|
for call_data in ax_data.get("calls", []):
|
|
172
200
|
ax_record.calls.append(CallRecord.from_dict(call_data, (row, col)))
|
|
173
201
|
for dec_data in ax_data.get("decorations", []):
|
|
@@ -272,111 +300,12 @@ class Recorder:
|
|
|
272
300
|
args: tuple,
|
|
273
301
|
method_name: str,
|
|
274
302
|
) -> List[Dict[str, Any]]:
|
|
275
|
-
"""Process positional arguments for storage.
|
|
303
|
+
"""Process positional arguments for storage."""
|
|
304
|
+
from ._recorder_utils import process_args
|
|
276
305
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
Raw positional arguments.
|
|
281
|
-
method_name : str
|
|
282
|
-
Name of the method.
|
|
283
|
-
|
|
284
|
-
Returns
|
|
285
|
-
-------
|
|
286
|
-
list
|
|
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
|
|
306
|
+
return process_args(
|
|
307
|
+
args, method_name, self._get_arg_names, self._is_serializable
|
|
308
|
+
)
|
|
380
309
|
|
|
381
310
|
def _get_arg_names(self, method_name: str, n_args: int) -> List[str]:
|
|
382
311
|
"""Get argument names for a method from signatures.
|
|
@@ -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
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Figure reproduction module.
|
|
4
|
+
|
|
5
|
+
This module provides functionality to reproduce figures from recipe files.
|
|
6
|
+
The public API exposes three main functions:
|
|
7
|
+
- reproduce: Reproduce a figure from a recipe file
|
|
8
|
+
- reproduce_from_record: Reproduce a figure from a FigureRecord object
|
|
9
|
+
- get_recipe_info: Get information about a recipe without reproducing it
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from ._core import get_recipe_info, reproduce, reproduce_from_record
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"reproduce",
|
|
16
|
+
"reproduce_from_record",
|
|
17
|
+
"get_recipe_info",
|
|
18
|
+
]
|