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.
Files changed (177) hide show
  1. figrecipe/__init__.py +106 -973
  2. figrecipe/_api/__init__.py +48 -0
  3. figrecipe/_api/_extract.py +108 -0
  4. figrecipe/_api/_notebook.py +61 -0
  5. figrecipe/_api/_panel.py +46 -0
  6. figrecipe/_api/_save.py +191 -0
  7. figrecipe/_api/_seaborn_proxy.py +34 -0
  8. figrecipe/_api/_style_manager.py +153 -0
  9. figrecipe/_api/_subplots.py +333 -0
  10. figrecipe/_api/_validate.py +82 -0
  11. figrecipe/_dev/__init__.py +2 -93
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  15. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  16. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  17. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  18. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  19. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  20. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  21. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  22. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  24. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  25. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  26. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  27. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  28. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  29. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  30. figrecipe/_editor/__init__.py +57 -9
  31. figrecipe/_editor/_bbox/__init__.py +43 -0
  32. figrecipe/_editor/_bbox/_collections.py +177 -0
  33. figrecipe/_editor/_bbox/_elements.py +159 -0
  34. figrecipe/_editor/_bbox/_extract.py +256 -0
  35. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  36. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  37. figrecipe/_editor/_bbox/_lines.py +173 -0
  38. figrecipe/_editor/_bbox/_transforms.py +146 -0
  39. figrecipe/_editor/_flask_app.py +68 -1039
  40. figrecipe/_editor/_helpers.py +242 -0
  41. figrecipe/_editor/_hitmap/__init__.py +76 -0
  42. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  43. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  44. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  45. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  46. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  47. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  48. figrecipe/_editor/_hitmap/_colors.py +181 -0
  49. figrecipe/_editor/_hitmap/_detect.py +137 -0
  50. figrecipe/_editor/_hitmap/_restore.py +154 -0
  51. figrecipe/_editor/_hitmap_main.py +182 -0
  52. figrecipe/_editor/_preferences.py +135 -0
  53. figrecipe/_editor/_render_overrides.py +480 -0
  54. figrecipe/_editor/_renderer.py +35 -185
  55. figrecipe/_editor/_routes_axis.py +453 -0
  56. figrecipe/_editor/_routes_core.py +284 -0
  57. figrecipe/_editor/_routes_element.py +317 -0
  58. figrecipe/_editor/_routes_style.py +223 -0
  59. figrecipe/_editor/_templates/__init__.py +78 -1
  60. figrecipe/_editor/_templates/_html.py +109 -13
  61. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  62. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  63. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  64. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  65. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  66. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  67. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  68. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  69. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  70. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  71. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  72. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  73. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  74. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  75. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  76. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  77. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  78. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  79. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  80. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  81. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  82. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  83. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  84. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  85. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  86. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  87. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  88. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  89. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  90. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  91. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  92. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  93. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  94. figrecipe/_params/_DECORATION_METHODS.py +6 -0
  95. figrecipe/_recorder.py +35 -106
  96. figrecipe/_recorder_utils.py +124 -0
  97. figrecipe/_reproducer/__init__.py +18 -0
  98. figrecipe/_reproducer/_core.py +498 -0
  99. figrecipe/_reproducer/_custom_plots.py +279 -0
  100. figrecipe/_reproducer/_seaborn.py +100 -0
  101. figrecipe/_reproducer/_violin.py +186 -0
  102. figrecipe/_signatures/_kwargs.py +273 -0
  103. figrecipe/_signatures/_loader.py +21 -423
  104. figrecipe/_signatures/_parsing.py +147 -0
  105. figrecipe/_wrappers/_axes.py +119 -910
  106. figrecipe/_wrappers/_axes_helpers.py +136 -0
  107. figrecipe/_wrappers/_axes_plots.py +418 -0
  108. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  109. figrecipe/_wrappers/_figure.py +162 -0
  110. figrecipe/_wrappers/_panel_labels.py +127 -0
  111. figrecipe/_wrappers/_plot_helpers.py +143 -0
  112. figrecipe/_wrappers/_violin_helpers.py +180 -0
  113. figrecipe/styles/__init__.py +8 -6
  114. figrecipe/styles/_dotdict.py +72 -0
  115. figrecipe/styles/_finalize.py +134 -0
  116. figrecipe/styles/_fonts.py +77 -0
  117. figrecipe/styles/_kwargs_converter.py +178 -0
  118. figrecipe/styles/_plot_styles.py +209 -0
  119. figrecipe/styles/_style_applier.py +32 -478
  120. figrecipe/styles/_style_loader.py +16 -192
  121. figrecipe/styles/_themes.py +151 -0
  122. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  123. figrecipe/styles/presets/SCITEX.yaml +29 -24
  124. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
  125. figrecipe-0.7.4.dist-info/RECORD +188 -0
  126. figrecipe/_editor/_bbox.py +0 -978
  127. figrecipe/_editor/_hitmap.py +0 -937
  128. figrecipe/_editor/_templates/_scripts.py +0 -2778
  129. figrecipe/_editor/_templates/_styles.py +0 -1326
  130. figrecipe/_reproducer.py +0 -975
  131. figrecipe-0.6.0.dist-info/RECORD +0 -90
  132. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  133. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  134. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  135. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  136. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  137. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  138. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  139. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  140. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  141. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  142. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  143. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  144. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  145. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  146. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  147. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  148. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  149. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  150. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  151. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  152. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  153. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  154. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  155. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  156. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  157. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  158. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  159. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  160. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  161. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  162. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  163. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  164. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  165. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  166. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  167. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  168. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  169. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  170. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  171. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  172. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  173. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  174. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  175. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  176. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  177. {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
@@ -22,6 +22,12 @@ 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",
25
31
  }
26
32
 
27
33
  # 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
- return {
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(position=(row, col))
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
- Parameters
278
- ----------
279
- args : tuple
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
+ ]