figrecipe 0.5.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 +220 -819
- 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 +29 -0
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/demo_plotters/__init__.py +64 -0
- 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/bar_categorical/plot_bar.py +25 -0
- figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
- figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
- figrecipe/_editor/__init__.py +278 -0
- 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 +258 -0
- 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/_overrides.py +318 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +199 -0
- 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 +152 -0
- figrecipe/_editor/_templates/_html.py +502 -0
- 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 +33 -0
- figrecipe/_params/_PLOTTING_METHODS.py +58 -0
- figrecipe/_params/__init__.py +9 -0
- figrecipe/_recorder.py +92 -110
- 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/_seaborn.py +14 -9
- figrecipe/_serializer.py +2 -2
- figrecipe/_signatures/README.md +68 -0
- figrecipe/_signatures/__init__.py +12 -2
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +114 -57
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_utils/__init__.py +6 -4
- figrecipe/_utils/_crop.py +10 -4
- figrecipe/_utils/_image_diff.py +37 -33
- figrecipe/_utils/_numpy_io.py +0 -1
- figrecipe/_utils/_units.py +11 -3
- figrecipe/_validator.py +12 -3
- figrecipe/_wrappers/_axes.py +193 -170
- 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 +277 -18
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/plt.py +0 -1
- figrecipe/pyplot.py +2 -1
- figrecipe/styles/__init__.py +12 -11
- 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 +60 -202
- figrecipe/styles/_style_loader.py +73 -121
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
- figrecipe/styles/presets/SCITEX.yaml +181 -0
- figrecipe-0.7.4.dist-info/METADATA +429 -0
- figrecipe-0.7.4.dist-info/RECORD +188 -0
- figrecipe/_reproducer.py +0 -358
- figrecipe-0.5.0.dist-info/METADATA +0 -336
- figrecipe-0.5.0.dist-info/RECORD +0 -26
- {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Custom plot replay functions (joyplot, swarmplot) for figure reproduction."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, List
|
|
6
|
+
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import numpy as np
|
|
9
|
+
from matplotlib.axes import Axes
|
|
10
|
+
|
|
11
|
+
from .._recorder import CallRecord
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def replay_joyplot_call(ax: Axes, call: CallRecord) -> Any:
|
|
15
|
+
"""Replay a joyplot call on an axes.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
ax : Axes
|
|
20
|
+
The matplotlib axes.
|
|
21
|
+
call : CallRecord
|
|
22
|
+
The joyplot call to replay.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
Any
|
|
27
|
+
Result of the joyplot call.
|
|
28
|
+
"""
|
|
29
|
+
from scipy import stats
|
|
30
|
+
|
|
31
|
+
from ._core import _reconstruct_kwargs, _reconstruct_value
|
|
32
|
+
|
|
33
|
+
# Reconstruct args
|
|
34
|
+
arrays = []
|
|
35
|
+
for arg_data in call.args:
|
|
36
|
+
value = _reconstruct_value(arg_data)
|
|
37
|
+
if isinstance(value, list):
|
|
38
|
+
# Could be a list of arrays
|
|
39
|
+
arrays = [np.asarray(arr) for arr in value]
|
|
40
|
+
else:
|
|
41
|
+
arrays.append(np.asarray(value))
|
|
42
|
+
|
|
43
|
+
if not arrays:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
# Get kwargs
|
|
47
|
+
kwargs = _reconstruct_kwargs(call.kwargs)
|
|
48
|
+
overlap = kwargs.get("overlap", 0.5)
|
|
49
|
+
fill_alpha = kwargs.get("fill_alpha", 0.7)
|
|
50
|
+
line_alpha = kwargs.get("line_alpha", 1.0)
|
|
51
|
+
labels = kwargs.get("labels")
|
|
52
|
+
|
|
53
|
+
n_ridges = len(arrays)
|
|
54
|
+
|
|
55
|
+
# Get colors from style
|
|
56
|
+
from ..styles import get_style
|
|
57
|
+
|
|
58
|
+
style = get_style()
|
|
59
|
+
if style and "colors" in style and "palette" in style.colors:
|
|
60
|
+
palette = list(style.colors.palette)
|
|
61
|
+
colors = []
|
|
62
|
+
for c in palette:
|
|
63
|
+
if isinstance(c, (list, tuple)) and len(c) >= 3:
|
|
64
|
+
if all(v <= 1.0 for v in c):
|
|
65
|
+
colors.append(tuple(c))
|
|
66
|
+
else:
|
|
67
|
+
colors.append(tuple(v / 255.0 for v in c))
|
|
68
|
+
else:
|
|
69
|
+
colors.append(c)
|
|
70
|
+
else:
|
|
71
|
+
colors = [c["color"] for c in plt.rcParams["axes.prop_cycle"]]
|
|
72
|
+
|
|
73
|
+
# Calculate global x range
|
|
74
|
+
all_data = np.concatenate([np.asarray(arr) for arr in arrays])
|
|
75
|
+
x_min, x_max = np.min(all_data), np.max(all_data)
|
|
76
|
+
x_range = x_max - x_min
|
|
77
|
+
x_padding = x_range * 0.1
|
|
78
|
+
x = np.linspace(x_min - x_padding, x_max + x_padding, 200)
|
|
79
|
+
|
|
80
|
+
# Calculate KDEs and find max density for scaling
|
|
81
|
+
kdes = []
|
|
82
|
+
max_density = 0
|
|
83
|
+
for arr in arrays:
|
|
84
|
+
arr = np.asarray(arr)
|
|
85
|
+
if len(arr) > 1:
|
|
86
|
+
kde = stats.gaussian_kde(arr)
|
|
87
|
+
density = kde(x)
|
|
88
|
+
kdes.append(density)
|
|
89
|
+
max_density = max(max_density, np.max(density))
|
|
90
|
+
else:
|
|
91
|
+
kdes.append(np.zeros_like(x))
|
|
92
|
+
|
|
93
|
+
# Scale factor for ridge height
|
|
94
|
+
ridge_height = 1.0 / (1.0 - overlap * 0.5) if overlap < 1 else 2.0
|
|
95
|
+
|
|
96
|
+
# Get line width from style
|
|
97
|
+
from .._utils._units import mm_to_pt
|
|
98
|
+
|
|
99
|
+
lw = mm_to_pt(0.2) # Default
|
|
100
|
+
if style and "lines" in style:
|
|
101
|
+
lw = mm_to_pt(style.lines.get("trace_mm", 0.2))
|
|
102
|
+
|
|
103
|
+
# Plot each ridge from back to front
|
|
104
|
+
for i in range(n_ridges - 1, -1, -1):
|
|
105
|
+
color = colors[i % len(colors)]
|
|
106
|
+
baseline = i * (1.0 - overlap)
|
|
107
|
+
|
|
108
|
+
# Scale density to fit nicely
|
|
109
|
+
scaled_density = (
|
|
110
|
+
kdes[i] / max_density * ridge_height if max_density > 0 else kdes[i]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Fill
|
|
114
|
+
ax.fill_between(
|
|
115
|
+
x,
|
|
116
|
+
baseline,
|
|
117
|
+
baseline + scaled_density,
|
|
118
|
+
facecolor=color,
|
|
119
|
+
edgecolor="none",
|
|
120
|
+
alpha=fill_alpha,
|
|
121
|
+
)
|
|
122
|
+
# Line on top
|
|
123
|
+
ax.plot(
|
|
124
|
+
x, baseline + scaled_density, color=color, alpha=line_alpha, linewidth=lw
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Set y limits
|
|
128
|
+
ax.set_ylim(-0.1, n_ridges * (1.0 - overlap) + ridge_height)
|
|
129
|
+
|
|
130
|
+
# Set y-axis labels if provided
|
|
131
|
+
if labels:
|
|
132
|
+
y_positions = [(i * (1.0 - overlap)) + 0.3 for i in range(n_ridges)]
|
|
133
|
+
ax.set_yticks(y_positions)
|
|
134
|
+
ax.set_yticklabels(labels)
|
|
135
|
+
else:
|
|
136
|
+
ax.set_yticks([])
|
|
137
|
+
|
|
138
|
+
return ax
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def replay_swarmplot_call(ax: Axes, call: CallRecord) -> List[Any]:
|
|
142
|
+
"""Replay a swarmplot call on an axes.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
ax : Axes
|
|
147
|
+
The matplotlib axes.
|
|
148
|
+
call : CallRecord
|
|
149
|
+
The swarmplot call to replay.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
list
|
|
154
|
+
List of PathCollection objects.
|
|
155
|
+
"""
|
|
156
|
+
from ._core import _reconstruct_kwargs, _reconstruct_value
|
|
157
|
+
|
|
158
|
+
# Reconstruct args
|
|
159
|
+
data = []
|
|
160
|
+
for arg_data in call.args:
|
|
161
|
+
value = _reconstruct_value(arg_data)
|
|
162
|
+
if isinstance(value, list):
|
|
163
|
+
# Could be a list of arrays
|
|
164
|
+
data = [np.asarray(arr) for arr in value]
|
|
165
|
+
else:
|
|
166
|
+
data.append(np.asarray(value))
|
|
167
|
+
|
|
168
|
+
if not data:
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
# Get kwargs
|
|
172
|
+
kwargs = _reconstruct_kwargs(call.kwargs)
|
|
173
|
+
positions = kwargs.get("positions")
|
|
174
|
+
size = kwargs.get("size", 0.8)
|
|
175
|
+
alpha = kwargs.get("alpha", 0.7)
|
|
176
|
+
jitter = kwargs.get("jitter", 0.3)
|
|
177
|
+
|
|
178
|
+
if positions is None:
|
|
179
|
+
positions = list(range(1, len(data) + 1))
|
|
180
|
+
|
|
181
|
+
# Get style
|
|
182
|
+
from .._utils._units import mm_to_pt
|
|
183
|
+
from ..styles import get_style
|
|
184
|
+
|
|
185
|
+
style = get_style()
|
|
186
|
+
size_pt = mm_to_pt(size) ** 2 # matplotlib uses area
|
|
187
|
+
|
|
188
|
+
# Get colors
|
|
189
|
+
if style and "colors" in style and "palette" in style.colors:
|
|
190
|
+
palette = list(style.colors.palette)
|
|
191
|
+
colors = []
|
|
192
|
+
for c in palette:
|
|
193
|
+
if isinstance(c, (list, tuple)) and len(c) >= 3:
|
|
194
|
+
if all(v <= 1.0 for v in c):
|
|
195
|
+
colors.append(tuple(c))
|
|
196
|
+
else:
|
|
197
|
+
colors.append(tuple(v / 255.0 for v in c))
|
|
198
|
+
else:
|
|
199
|
+
colors.append(c)
|
|
200
|
+
else:
|
|
201
|
+
colors = [c["color"] for c in plt.rcParams["axes.prop_cycle"]]
|
|
202
|
+
|
|
203
|
+
# Random generator for reproducible jitter
|
|
204
|
+
rng = np.random.default_rng(42)
|
|
205
|
+
|
|
206
|
+
results = []
|
|
207
|
+
for i, (arr, pos) in enumerate(zip(data, positions)):
|
|
208
|
+
arr = np.asarray(arr)
|
|
209
|
+
|
|
210
|
+
# Create jittered x positions using simplified beeswarm
|
|
211
|
+
x_offsets = _beeswarm_positions(arr, jitter, rng)
|
|
212
|
+
x_positions = pos + x_offsets
|
|
213
|
+
|
|
214
|
+
c = colors[i % len(colors)]
|
|
215
|
+
result = ax.scatter(
|
|
216
|
+
x_positions,
|
|
217
|
+
arr,
|
|
218
|
+
s=size_pt,
|
|
219
|
+
c=[c],
|
|
220
|
+
alpha=alpha,
|
|
221
|
+
)
|
|
222
|
+
results.append(result)
|
|
223
|
+
|
|
224
|
+
return results
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _beeswarm_positions(
|
|
228
|
+
data: np.ndarray,
|
|
229
|
+
width: float,
|
|
230
|
+
rng: np.random.Generator,
|
|
231
|
+
) -> np.ndarray:
|
|
232
|
+
"""Calculate beeswarm-style x positions to minimize overlap.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
data : array
|
|
237
|
+
Y values of points.
|
|
238
|
+
width : float
|
|
239
|
+
Maximum jitter width.
|
|
240
|
+
rng : Generator
|
|
241
|
+
Random number generator.
|
|
242
|
+
|
|
243
|
+
Returns
|
|
244
|
+
-------
|
|
245
|
+
array
|
|
246
|
+
X offsets for each point.
|
|
247
|
+
"""
|
|
248
|
+
n = len(data)
|
|
249
|
+
if n == 0:
|
|
250
|
+
return np.array([])
|
|
251
|
+
|
|
252
|
+
# Sort data and get order
|
|
253
|
+
order = np.argsort(data)
|
|
254
|
+
sorted_data = data[order]
|
|
255
|
+
|
|
256
|
+
# Group nearby points and offset them
|
|
257
|
+
x_offsets = np.zeros(n)
|
|
258
|
+
|
|
259
|
+
# Simple approach: bin by quantiles and spread within each bin
|
|
260
|
+
n_bins = max(1, int(np.sqrt(n)))
|
|
261
|
+
bin_edges = np.percentile(sorted_data, np.linspace(0, 100, n_bins + 1))
|
|
262
|
+
|
|
263
|
+
for i in range(n_bins):
|
|
264
|
+
mask = (sorted_data >= bin_edges[i]) & (sorted_data <= bin_edges[i + 1])
|
|
265
|
+
n_in_bin = mask.sum()
|
|
266
|
+
if n_in_bin > 0:
|
|
267
|
+
# Spread points evenly within bin width
|
|
268
|
+
offsets = np.linspace(-width / 2, width / 2, n_in_bin)
|
|
269
|
+
# Add small random noise
|
|
270
|
+
offsets += rng.uniform(-width * 0.1, width * 0.1, n_in_bin)
|
|
271
|
+
x_offsets[mask] = offsets
|
|
272
|
+
|
|
273
|
+
# Restore original order
|
|
274
|
+
result = np.zeros(n)
|
|
275
|
+
result[order] = x_offsets
|
|
276
|
+
return result
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
__all__ = ["replay_joyplot_call", "replay_swarmplot_call"]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Seaborn plot replay for figure reproduction."""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from matplotlib.axes import Axes
|
|
8
|
+
|
|
9
|
+
from .._recorder import CallRecord
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def replay_seaborn_call(ax: Axes, call: CallRecord) -> Any:
|
|
13
|
+
"""Replay a seaborn call on an axes.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
ax : Axes
|
|
18
|
+
The matplotlib axes.
|
|
19
|
+
call : CallRecord
|
|
20
|
+
The seaborn call to replay.
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
Any
|
|
25
|
+
Result of the seaborn call.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
import pandas as pd
|
|
29
|
+
import seaborn as sns
|
|
30
|
+
except ImportError:
|
|
31
|
+
import warnings
|
|
32
|
+
|
|
33
|
+
warnings.warn("seaborn/pandas required to replay seaborn calls")
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
from ._core import _reconstruct_value
|
|
37
|
+
|
|
38
|
+
# Get the seaborn function name (remove "sns." prefix)
|
|
39
|
+
func_name = call.function[4:] # Remove "sns."
|
|
40
|
+
func = getattr(sns, func_name, None)
|
|
41
|
+
|
|
42
|
+
if func is None:
|
|
43
|
+
import warnings
|
|
44
|
+
|
|
45
|
+
warnings.warn(f"Seaborn function {func_name} not found")
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
# Reconstruct data from args
|
|
49
|
+
# Args contain column data with "param" field indicating the parameter name
|
|
50
|
+
data_dict = {}
|
|
51
|
+
param_mapping = {} # Maps param name to column name
|
|
52
|
+
|
|
53
|
+
for arg_data in call.args:
|
|
54
|
+
param = arg_data.get("param")
|
|
55
|
+
name = arg_data.get("name")
|
|
56
|
+
value = _reconstruct_value(arg_data)
|
|
57
|
+
|
|
58
|
+
if param is not None:
|
|
59
|
+
# This is a DataFrame column
|
|
60
|
+
col_name = name if name else param
|
|
61
|
+
data_dict[col_name] = value
|
|
62
|
+
param_mapping[param] = col_name
|
|
63
|
+
|
|
64
|
+
# Build kwargs
|
|
65
|
+
kwargs = call.kwargs.copy()
|
|
66
|
+
|
|
67
|
+
# Remove internal keys
|
|
68
|
+
internal_keys = [k for k in kwargs.keys() if k.startswith("_")]
|
|
69
|
+
for key in internal_keys:
|
|
70
|
+
kwargs.pop(key, None)
|
|
71
|
+
|
|
72
|
+
# If we have data columns, create a DataFrame
|
|
73
|
+
if data_dict:
|
|
74
|
+
df = pd.DataFrame(data_dict)
|
|
75
|
+
kwargs["data"] = df
|
|
76
|
+
|
|
77
|
+
# Update column name references in kwargs
|
|
78
|
+
for param, col_name in param_mapping.items():
|
|
79
|
+
if param in ["x", "y", "hue", "size", "style", "row", "col"]:
|
|
80
|
+
kwargs[param] = col_name
|
|
81
|
+
|
|
82
|
+
# Add the axes
|
|
83
|
+
kwargs["ax"] = ax
|
|
84
|
+
|
|
85
|
+
# Convert certain list parameters back to tuples (YAML serializes tuples as lists)
|
|
86
|
+
# 'sizes' in seaborn expects a tuple (min, max) for range, not a list
|
|
87
|
+
if "sizes" in kwargs and isinstance(kwargs["sizes"], list):
|
|
88
|
+
kwargs["sizes"] = tuple(kwargs["sizes"])
|
|
89
|
+
|
|
90
|
+
# Call the seaborn function
|
|
91
|
+
try:
|
|
92
|
+
return func(**kwargs)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
import warnings
|
|
95
|
+
|
|
96
|
+
warnings.warn(f"Failed to replay sns.{func_name}: {e}")
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
__all__ = ["replay_seaborn_call"]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Violin plot replay for figure reproduction."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from matplotlib.axes import Axes
|
|
9
|
+
|
|
10
|
+
from .._recorder import CallRecord
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def replay_violinplot_call(ax: Axes, call: CallRecord) -> Any:
|
|
14
|
+
"""Replay a violinplot call with inner option support.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
ax : Axes
|
|
19
|
+
The matplotlib axes.
|
|
20
|
+
call : CallRecord
|
|
21
|
+
The violinplot call to replay.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
Any
|
|
26
|
+
Result of the violinplot call.
|
|
27
|
+
"""
|
|
28
|
+
# Import from _core module (will be available after full package setup)
|
|
29
|
+
from ._core import _reconstruct_kwargs, _reconstruct_value
|
|
30
|
+
|
|
31
|
+
# Reconstruct args
|
|
32
|
+
args = []
|
|
33
|
+
for arg_data in call.args:
|
|
34
|
+
value = _reconstruct_value(arg_data)
|
|
35
|
+
args.append(value)
|
|
36
|
+
|
|
37
|
+
# Get kwargs and reconstruct arrays
|
|
38
|
+
kwargs = _reconstruct_kwargs(call.kwargs)
|
|
39
|
+
|
|
40
|
+
# Extract inner option (not a matplotlib kwarg)
|
|
41
|
+
inner = kwargs.pop("inner", "box")
|
|
42
|
+
|
|
43
|
+
# Get display options
|
|
44
|
+
showmeans = kwargs.pop("showmeans", False)
|
|
45
|
+
showmedians = kwargs.pop("showmedians", True)
|
|
46
|
+
showextrema = kwargs.pop("showextrema", False)
|
|
47
|
+
|
|
48
|
+
# When using inner box/swarm, suppress default median/extrema lines
|
|
49
|
+
if inner in ("box", "swarm"):
|
|
50
|
+
showmedians = False
|
|
51
|
+
showextrema = False
|
|
52
|
+
|
|
53
|
+
# Call matplotlib's violinplot
|
|
54
|
+
try:
|
|
55
|
+
result = ax.violinplot(
|
|
56
|
+
*args,
|
|
57
|
+
showmeans=showmeans,
|
|
58
|
+
showmedians=showmedians,
|
|
59
|
+
showextrema=showextrema,
|
|
60
|
+
**kwargs,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Get style settings for inner display
|
|
64
|
+
from ..styles import get_style
|
|
65
|
+
|
|
66
|
+
style = get_style()
|
|
67
|
+
violin_style = style.get("violinplot", {}) if style else {}
|
|
68
|
+
|
|
69
|
+
# Apply alpha from style to violin bodies
|
|
70
|
+
alpha = violin_style.get("alpha", 0.7)
|
|
71
|
+
if "bodies" in result:
|
|
72
|
+
for body in result["bodies"]:
|
|
73
|
+
body.set_alpha(alpha)
|
|
74
|
+
|
|
75
|
+
# Determine positions
|
|
76
|
+
dataset = args[0] if args else []
|
|
77
|
+
positions = kwargs.get("positions")
|
|
78
|
+
if positions is None:
|
|
79
|
+
positions = list(range(1, len(dataset) + 1))
|
|
80
|
+
|
|
81
|
+
# Overlay inner elements based on inner type
|
|
82
|
+
if inner == "box":
|
|
83
|
+
_add_violin_inner_box(ax, dataset, positions, violin_style)
|
|
84
|
+
elif inner == "swarm":
|
|
85
|
+
_add_violin_inner_swarm(ax, dataset, positions, violin_style)
|
|
86
|
+
elif inner == "stick":
|
|
87
|
+
_add_violin_inner_stick(ax, dataset, positions, violin_style)
|
|
88
|
+
elif inner == "point":
|
|
89
|
+
_add_violin_inner_point(ax, dataset, positions, violin_style)
|
|
90
|
+
|
|
91
|
+
return result
|
|
92
|
+
except Exception as e:
|
|
93
|
+
import warnings
|
|
94
|
+
|
|
95
|
+
warnings.warn(f"Failed to replay violinplot: {e}")
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _add_violin_inner_box(ax: Axes, dataset, positions, style: Dict[str, Any]) -> None:
|
|
100
|
+
"""Add box plot inside violin for reproduction."""
|
|
101
|
+
from ..styles._style_applier import mm_to_pt
|
|
102
|
+
|
|
103
|
+
whisker_lw = mm_to_pt(style.get("whisker_mm", 0.2))
|
|
104
|
+
median_size = mm_to_pt(style.get("median_mm", 0.8))
|
|
105
|
+
|
|
106
|
+
for data, pos in zip(dataset, positions):
|
|
107
|
+
data = np.asarray(data)
|
|
108
|
+
q1, median, q3 = np.percentile(data, [25, 50, 75])
|
|
109
|
+
iqr = q3 - q1
|
|
110
|
+
whisker_low = max(data.min(), q1 - 1.5 * iqr)
|
|
111
|
+
whisker_high = min(data.max(), q3 + 1.5 * iqr)
|
|
112
|
+
|
|
113
|
+
# Draw box (Q1 to Q3)
|
|
114
|
+
ax.vlines(pos, q1, q3, colors="black", linewidths=whisker_lw, zorder=3)
|
|
115
|
+
# Draw whiskers
|
|
116
|
+
ax.vlines(
|
|
117
|
+
pos, whisker_low, q1, colors="black", linewidths=whisker_lw * 0.5, zorder=3
|
|
118
|
+
)
|
|
119
|
+
ax.vlines(
|
|
120
|
+
pos, q3, whisker_high, colors="black", linewidths=whisker_lw * 0.5, zorder=3
|
|
121
|
+
)
|
|
122
|
+
# Draw median as a white dot with black edge
|
|
123
|
+
ax.scatter(
|
|
124
|
+
[pos],
|
|
125
|
+
[median],
|
|
126
|
+
s=median_size**2,
|
|
127
|
+
c="white",
|
|
128
|
+
edgecolors="black",
|
|
129
|
+
linewidths=whisker_lw,
|
|
130
|
+
zorder=4,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _add_violin_inner_swarm(
|
|
135
|
+
ax: Axes, dataset, positions, style: Dict[str, Any]
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Add swarm points inside violin for reproduction."""
|
|
138
|
+
from ..styles._style_applier import mm_to_pt
|
|
139
|
+
|
|
140
|
+
point_size = mm_to_pt(style.get("median_mm", 0.8))
|
|
141
|
+
|
|
142
|
+
for data, pos in zip(dataset, positions):
|
|
143
|
+
data = np.asarray(data)
|
|
144
|
+
n = len(data)
|
|
145
|
+
jitter = np.random.default_rng(42).uniform(-0.15, 0.15, n)
|
|
146
|
+
x_positions = pos + jitter
|
|
147
|
+
ax.scatter(x_positions, data, s=point_size**2, c="black", alpha=0.5, zorder=3)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _add_violin_inner_stick(
|
|
151
|
+
ax: Axes, dataset, positions, style: Dict[str, Any]
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Add stick markers inside violin for reproduction."""
|
|
154
|
+
from ..styles._style_applier import mm_to_pt
|
|
155
|
+
|
|
156
|
+
lw = mm_to_pt(style.get("whisker_mm", 0.2))
|
|
157
|
+
|
|
158
|
+
for data, pos in zip(dataset, positions):
|
|
159
|
+
data = np.asarray(data)
|
|
160
|
+
for val in data:
|
|
161
|
+
ax.hlines(
|
|
162
|
+
val,
|
|
163
|
+
pos - 0.05,
|
|
164
|
+
pos + 0.05,
|
|
165
|
+
colors="black",
|
|
166
|
+
linewidths=lw * 0.5,
|
|
167
|
+
alpha=0.3,
|
|
168
|
+
zorder=3,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _add_violin_inner_point(
|
|
173
|
+
ax: Axes, dataset, positions, style: Dict[str, Any]
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Add point markers inside violin for reproduction."""
|
|
176
|
+
from ..styles._style_applier import mm_to_pt
|
|
177
|
+
|
|
178
|
+
point_size = mm_to_pt(style.get("median_mm", 0.8)) * 0.5
|
|
179
|
+
|
|
180
|
+
for data, pos in zip(dataset, positions):
|
|
181
|
+
data = np.asarray(data)
|
|
182
|
+
x_positions = np.full_like(data, pos)
|
|
183
|
+
ax.scatter(x_positions, data, s=point_size**2, c="black", alpha=0.3, zorder=3)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
__all__ = ["replay_violinplot_call"]
|
figrecipe/_seaborn.py
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
"""Seaborn wrapper for figrecipe recording."""
|
|
4
4
|
|
|
5
5
|
from functools import wraps
|
|
6
|
-
from typing import Any, Callable, Dict,
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
|
|
10
10
|
try:
|
|
11
|
-
import seaborn as sns
|
|
12
11
|
import pandas as pd
|
|
12
|
+
import seaborn as sns
|
|
13
|
+
|
|
13
14
|
HAS_SEABORN = True
|
|
14
15
|
except ImportError:
|
|
15
16
|
HAS_SEABORN = False
|
|
@@ -17,7 +18,7 @@ except ImportError:
|
|
|
17
18
|
pd = None
|
|
18
19
|
|
|
19
20
|
if TYPE_CHECKING:
|
|
20
|
-
|
|
21
|
+
pass
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
# Seaborn axes-level plotting functions to wrap
|
|
@@ -91,8 +92,15 @@ def _extract_data_from_dataframe(
|
|
|
91
92
|
|
|
92
93
|
# All column parameters
|
|
93
94
|
param_values = [
|
|
94
|
-
("x", x),
|
|
95
|
-
("
|
|
95
|
+
("x", x),
|
|
96
|
+
("y", y),
|
|
97
|
+
("hue", hue),
|
|
98
|
+
("size", size),
|
|
99
|
+
("style", style),
|
|
100
|
+
("row", row),
|
|
101
|
+
("col", col),
|
|
102
|
+
("weight", weight),
|
|
103
|
+
("weights", weights),
|
|
96
104
|
]
|
|
97
105
|
|
|
98
106
|
for param_name, col_name in param_values:
|
|
@@ -200,10 +208,7 @@ def _is_serializable(value: Any) -> bool:
|
|
|
200
208
|
if isinstance(value, (list, tuple)):
|
|
201
209
|
return all(_is_serializable(v) for v in value)
|
|
202
210
|
if isinstance(value, dict):
|
|
203
|
-
return all(
|
|
204
|
-
isinstance(k, str) and _is_serializable(v)
|
|
205
|
-
for k, v in value.items()
|
|
206
|
-
)
|
|
211
|
+
return all(isinstance(k, str) and _is_serializable(v) for k, v in value.items())
|
|
207
212
|
return False
|
|
208
213
|
|
|
209
214
|
|
figrecipe/_serializer.py
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
"""Serialization for recipe files (YAML + data files)."""
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Any, Dict,
|
|
6
|
+
from typing import Any, Dict, Union
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
from ruamel.yaml import YAML
|
|
10
10
|
|
|
11
11
|
from ._recorder import FigureRecord
|
|
12
|
-
from ._utils._numpy_io import
|
|
12
|
+
from ._utils._numpy_io import DataFormat, load_array, save_array
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def _convert_numpy_types(obj: Any) -> Any:
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!-- ---
|
|
2
|
+
!-- Timestamp: 2025-12-23 10:52:06
|
|
3
|
+
!-- Author: ywatanabe
|
|
4
|
+
!-- File: /home/ywatanabe/proj/figrecipe/src/figrecipe/_signatures/README.md
|
|
5
|
+
!-- --- -->
|
|
6
|
+
|
|
7
|
+
``` python
|
|
8
|
+
from figrecipe._signatures._loader import list_plotting_methods
|
|
9
|
+
|
|
10
|
+
print(list_plotting_methods())
|
|
11
|
+
# [
|
|
12
|
+
# 'acorr',
|
|
13
|
+
# 'angle_spectrum',
|
|
14
|
+
# 'bar',
|
|
15
|
+
# 'barbs',
|
|
16
|
+
# 'barh',
|
|
17
|
+
# 'boxplot',
|
|
18
|
+
# 'cohere',
|
|
19
|
+
# 'contour',
|
|
20
|
+
# 'contourf',
|
|
21
|
+
# 'csd',
|
|
22
|
+
# 'ecdf',
|
|
23
|
+
# 'errorbar',
|
|
24
|
+
# 'eventplot',
|
|
25
|
+
# 'fill',
|
|
26
|
+
# 'fill_between',
|
|
27
|
+
# 'fill_betweenx',
|
|
28
|
+
# 'hexbin',
|
|
29
|
+
# 'hist',
|
|
30
|
+
# 'hist2d',
|
|
31
|
+
# 'imshow',
|
|
32
|
+
# 'loglog',
|
|
33
|
+
# 'magnitude_spectrum',
|
|
34
|
+
# 'matshow',
|
|
35
|
+
# 'pcolor',
|
|
36
|
+
# 'pcolormesh',
|
|
37
|
+
# 'phase_spectrum',
|
|
38
|
+
# 'pie',
|
|
39
|
+
# 'plot',
|
|
40
|
+
# 'psd',
|
|
41
|
+
# 'quiver',
|
|
42
|
+
# 'scatter',
|
|
43
|
+
# 'semilogx',
|
|
44
|
+
# 'semilogy',
|
|
45
|
+
# 'specgram',
|
|
46
|
+
# 'spy',
|
|
47
|
+
# 'stackplot',
|
|
48
|
+
# 'stairs',
|
|
49
|
+
# 'stem',
|
|
50
|
+
# 'step',
|
|
51
|
+
# 'streamplot',
|
|
52
|
+
# 'tricontour',
|
|
53
|
+
# 'tricontourf',
|
|
54
|
+
# 'tripcolor',
|
|
55
|
+
# 'triplot',
|
|
56
|
+
# 'violinplot',
|
|
57
|
+
# 'xcorr'
|
|
58
|
+
# ]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
from figrecipe._signatures._loader import get_signature
|
|
62
|
+
# Signature: get_signature(method_name: str) -> Dict[str, Any]
|
|
63
|
+
|
|
64
|
+
get_signature("plot")
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
<!-- EOF -->
|