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,180 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Violin plot helper functions for RecordingAxes."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def add_violin_inner_box(
|
|
11
|
+
ax,
|
|
12
|
+
dataset: List,
|
|
13
|
+
positions: List,
|
|
14
|
+
style: Dict[str, Any],
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Add box plot inside violin.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
ax : matplotlib.axes.Axes
|
|
21
|
+
The axes to draw on.
|
|
22
|
+
dataset : array-like
|
|
23
|
+
Data arrays for each violin.
|
|
24
|
+
positions : array-like
|
|
25
|
+
X positions of violins.
|
|
26
|
+
style : dict
|
|
27
|
+
Violin style configuration.
|
|
28
|
+
"""
|
|
29
|
+
from ..styles._style_applier import mm_to_pt
|
|
30
|
+
|
|
31
|
+
whisker_lw = mm_to_pt(style.get("whisker_mm", 0.2))
|
|
32
|
+
median_size = mm_to_pt(style.get("median_mm", 0.8))
|
|
33
|
+
|
|
34
|
+
for data, pos in zip(dataset, positions):
|
|
35
|
+
data = np.asarray(data)
|
|
36
|
+
q1, median, q3 = np.percentile(data, [25, 50, 75])
|
|
37
|
+
iqr = q3 - q1
|
|
38
|
+
whisker_low = max(data.min(), q1 - 1.5 * iqr)
|
|
39
|
+
whisker_high = min(data.max(), q3 + 1.5 * iqr)
|
|
40
|
+
|
|
41
|
+
# Draw box (Q1 to Q3)
|
|
42
|
+
ax.vlines(pos, q1, q3, colors="black", linewidths=whisker_lw, zorder=3)
|
|
43
|
+
# Draw whiskers
|
|
44
|
+
ax.vlines(
|
|
45
|
+
pos,
|
|
46
|
+
whisker_low,
|
|
47
|
+
q1,
|
|
48
|
+
colors="black",
|
|
49
|
+
linewidths=whisker_lw * 0.5,
|
|
50
|
+
zorder=3,
|
|
51
|
+
)
|
|
52
|
+
ax.vlines(
|
|
53
|
+
pos,
|
|
54
|
+
q3,
|
|
55
|
+
whisker_high,
|
|
56
|
+
colors="black",
|
|
57
|
+
linewidths=whisker_lw * 0.5,
|
|
58
|
+
zorder=3,
|
|
59
|
+
)
|
|
60
|
+
# Draw median as a white dot with black edge
|
|
61
|
+
ax.scatter(
|
|
62
|
+
[pos],
|
|
63
|
+
[median],
|
|
64
|
+
s=median_size**2,
|
|
65
|
+
c="white",
|
|
66
|
+
edgecolors="black",
|
|
67
|
+
linewidths=whisker_lw,
|
|
68
|
+
zorder=4,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def add_violin_inner_swarm(
|
|
73
|
+
ax,
|
|
74
|
+
dataset: List,
|
|
75
|
+
positions: List,
|
|
76
|
+
style: Dict[str, Any],
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Add swarm points inside violin.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
ax : matplotlib.axes.Axes
|
|
83
|
+
The axes to draw on.
|
|
84
|
+
dataset : array-like
|
|
85
|
+
Data arrays for each violin.
|
|
86
|
+
positions : array-like
|
|
87
|
+
X positions of violins.
|
|
88
|
+
style : dict
|
|
89
|
+
Violin style configuration.
|
|
90
|
+
"""
|
|
91
|
+
from ..styles._style_applier import mm_to_pt
|
|
92
|
+
|
|
93
|
+
point_size = mm_to_pt(style.get("median_mm", 0.8))
|
|
94
|
+
|
|
95
|
+
for data, pos in zip(dataset, positions):
|
|
96
|
+
data = np.asarray(data)
|
|
97
|
+
n = len(data)
|
|
98
|
+
|
|
99
|
+
# Simple swarm: jitter x positions
|
|
100
|
+
jitter = np.random.default_rng(42).uniform(-0.15, 0.15, n)
|
|
101
|
+
x_positions = pos + jitter
|
|
102
|
+
|
|
103
|
+
ax.scatter(x_positions, data, s=point_size**2, c="black", alpha=0.5, zorder=3)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def add_violin_inner_stick(
|
|
107
|
+
ax,
|
|
108
|
+
dataset: List,
|
|
109
|
+
positions: List,
|
|
110
|
+
style: Dict[str, Any],
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Add stick (line) markers inside violin for each data point.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
ax : matplotlib.axes.Axes
|
|
117
|
+
The axes to draw on.
|
|
118
|
+
dataset : array-like
|
|
119
|
+
Data arrays for each violin.
|
|
120
|
+
positions : array-like
|
|
121
|
+
X positions of violins.
|
|
122
|
+
style : dict
|
|
123
|
+
Violin style configuration.
|
|
124
|
+
"""
|
|
125
|
+
from ..styles._style_applier import mm_to_pt
|
|
126
|
+
|
|
127
|
+
lw = mm_to_pt(style.get("whisker_mm", 0.2))
|
|
128
|
+
|
|
129
|
+
for data, pos in zip(dataset, positions):
|
|
130
|
+
data = np.asarray(data)
|
|
131
|
+
# Draw short horizontal lines at each data point
|
|
132
|
+
for val in data:
|
|
133
|
+
ax.hlines(
|
|
134
|
+
val,
|
|
135
|
+
pos - 0.05,
|
|
136
|
+
pos + 0.05,
|
|
137
|
+
colors="black",
|
|
138
|
+
linewidths=lw * 0.5,
|
|
139
|
+
alpha=0.3,
|
|
140
|
+
zorder=3,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def add_violin_inner_point(
|
|
145
|
+
ax,
|
|
146
|
+
dataset: List,
|
|
147
|
+
positions: List,
|
|
148
|
+
style: Dict[str, Any],
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Add point markers inside violin for each data point.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
ax : matplotlib.axes.Axes
|
|
155
|
+
The axes to draw on.
|
|
156
|
+
dataset : array-like
|
|
157
|
+
Data arrays for each violin.
|
|
158
|
+
positions : array-like
|
|
159
|
+
X positions of violins.
|
|
160
|
+
style : dict
|
|
161
|
+
Violin style configuration.
|
|
162
|
+
"""
|
|
163
|
+
from ..styles._style_applier import mm_to_pt
|
|
164
|
+
|
|
165
|
+
point_size = mm_to_pt(style.get("median_mm", 0.8)) * 0.5
|
|
166
|
+
|
|
167
|
+
for data, pos in zip(dataset, positions):
|
|
168
|
+
data = np.asarray(data)
|
|
169
|
+
x_positions = np.full_like(data, pos)
|
|
170
|
+
ax.scatter(x_positions, data, s=point_size**2, c="black", alpha=0.3, zorder=3)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
__all__ = [
|
|
174
|
+
"add_violin_inner_box",
|
|
175
|
+
"add_violin_inner_swarm",
|
|
176
|
+
"add_violin_inner_stick",
|
|
177
|
+
"add_violin_inner_point",
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
# EOF
|
figrecipe/plt.py
CHANGED
figrecipe/pyplot.py
CHANGED
|
@@ -34,9 +34,10 @@ Examples
|
|
|
34
34
|
import matplotlib.pyplot as _plt
|
|
35
35
|
from matplotlib.pyplot import * # noqa: F401, F403
|
|
36
36
|
|
|
37
|
+
from . import save as _ps_save
|
|
38
|
+
|
|
37
39
|
# Import figrecipe functionality
|
|
38
40
|
from . import subplots as _ps_subplots
|
|
39
|
-
from . import save as _ps_save
|
|
40
41
|
from ._wrappers import RecordingFigure
|
|
41
42
|
|
|
42
43
|
# Override subplots with recording-enabled version
|
figrecipe/styles/__init__.py
CHANGED
|
@@ -18,24 +18,23 @@ Usage:
|
|
|
18
18
|
fig, ax = ps.subplots(**style.to_subplots_kwargs())
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
+
from ._dotdict import DotDict
|
|
22
|
+
from ._finalize import finalize_special_plots, finalize_ticks
|
|
23
|
+
from ._fonts import check_font, list_available_fonts
|
|
24
|
+
from ._style_applier import apply_style_mm
|
|
21
25
|
from ._style_loader import (
|
|
22
|
-
|
|
23
|
-
unload_style,
|
|
26
|
+
STYLE,
|
|
24
27
|
get_style,
|
|
25
|
-
reload_style,
|
|
26
28
|
list_presets,
|
|
27
|
-
|
|
29
|
+
load_style,
|
|
30
|
+
reload_style,
|
|
28
31
|
to_subplots_kwargs,
|
|
32
|
+
unload_style,
|
|
29
33
|
)
|
|
30
|
-
|
|
31
|
-
from ._style_applier import (
|
|
32
|
-
apply_style_mm,
|
|
33
|
-
apply_theme_colors,
|
|
34
|
-
check_font,
|
|
35
|
-
list_available_fonts,
|
|
36
|
-
)
|
|
34
|
+
from ._themes import apply_theme_colors
|
|
37
35
|
|
|
38
36
|
__all__ = [
|
|
37
|
+
"DotDict",
|
|
39
38
|
"load_style",
|
|
40
39
|
"unload_style",
|
|
41
40
|
"get_style",
|
|
@@ -47,4 +46,6 @@ __all__ = [
|
|
|
47
46
|
"apply_theme_colors",
|
|
48
47
|
"check_font",
|
|
49
48
|
"list_available_fonts",
|
|
49
|
+
"finalize_ticks",
|
|
50
|
+
"finalize_special_plots",
|
|
50
51
|
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""DotDict class for nested dictionary access with dot notation."""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DotDict(dict):
|
|
9
|
+
"""Dictionary with dot-notation access to nested keys.
|
|
10
|
+
|
|
11
|
+
Examples
|
|
12
|
+
--------
|
|
13
|
+
>>> d = DotDict({"axes": {"width_mm": 40}})
|
|
14
|
+
>>> d.axes.width_mm
|
|
15
|
+
40
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __getattr__(self, key: str) -> Any:
|
|
19
|
+
# Handle special methods first
|
|
20
|
+
if key == "to_subplots_kwargs":
|
|
21
|
+
from ._style_loader import to_subplots_kwargs
|
|
22
|
+
|
|
23
|
+
return lambda: to_subplots_kwargs(self)
|
|
24
|
+
try:
|
|
25
|
+
value = self[key]
|
|
26
|
+
if isinstance(value, dict) and not isinstance(value, DotDict):
|
|
27
|
+
value = DotDict(value)
|
|
28
|
+
self[key] = value
|
|
29
|
+
return value
|
|
30
|
+
except KeyError:
|
|
31
|
+
raise AttributeError(f"'{type(self).__name__}' has no attribute '{key}'")
|
|
32
|
+
|
|
33
|
+
def __setattr__(self, key: str, value: Any) -> None:
|
|
34
|
+
self[key] = value
|
|
35
|
+
|
|
36
|
+
def __delattr__(self, key: str) -> None:
|
|
37
|
+
try:
|
|
38
|
+
del self[key]
|
|
39
|
+
except KeyError:
|
|
40
|
+
raise AttributeError(key)
|
|
41
|
+
|
|
42
|
+
def __repr__(self) -> str:
|
|
43
|
+
return f"DotDict({super().__repr__()})"
|
|
44
|
+
|
|
45
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
46
|
+
"""Get value with default, supporting nested keys with dots."""
|
|
47
|
+
if "." in key:
|
|
48
|
+
parts = key.split(".")
|
|
49
|
+
value = self
|
|
50
|
+
for part in parts:
|
|
51
|
+
if isinstance(value, dict) and part in value:
|
|
52
|
+
value = value[part]
|
|
53
|
+
else:
|
|
54
|
+
return default
|
|
55
|
+
return value
|
|
56
|
+
return super().get(key, default)
|
|
57
|
+
|
|
58
|
+
def flatten(self, prefix: str = "") -> dict:
|
|
59
|
+
"""Flatten nested dict to single level with underscore-joined keys."""
|
|
60
|
+
result = {}
|
|
61
|
+
for k, v in self.items():
|
|
62
|
+
new_key = f"{prefix}_{k}" if prefix else k
|
|
63
|
+
if isinstance(v, dict):
|
|
64
|
+
result.update(DotDict(v).flatten(new_key))
|
|
65
|
+
else:
|
|
66
|
+
result[new_key] = v
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = ["DotDict"]
|
|
71
|
+
|
|
72
|
+
# EOF
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Finalization utilities for figrecipe styles."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
from matplotlib.axes import Axes
|
|
8
|
+
|
|
9
|
+
from ._fonts import check_font
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def finalize_ticks(ax: Axes) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Apply deferred tick configuration after all plotting is done.
|
|
15
|
+
|
|
16
|
+
This function applies the n_ticks setting stored by apply_style_mm(),
|
|
17
|
+
but only to numeric axes (not categorical). Skips pie charts and other
|
|
18
|
+
plot types that should have hidden axes.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
ax : matplotlib.axes.Axes
|
|
23
|
+
The axes to finalize.
|
|
24
|
+
"""
|
|
25
|
+
from matplotlib.patches import Wedge
|
|
26
|
+
from matplotlib.ticker import MaxNLocator
|
|
27
|
+
|
|
28
|
+
# Skip pie charts - they should have no ticks
|
|
29
|
+
has_pie = any(isinstance(p, Wedge) for p in ax.patches)
|
|
30
|
+
if has_pie:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
# Get tick count preferences (new format: min/max)
|
|
34
|
+
n_ticks_min = getattr(ax, "_figrecipe_n_ticks_min", None)
|
|
35
|
+
n_ticks_max = getattr(ax, "_figrecipe_n_ticks_max", None)
|
|
36
|
+
|
|
37
|
+
if n_ticks_min is None and n_ticks_max is None:
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# Default values - minimum 3 ticks required
|
|
41
|
+
n_ticks_min = max(3, n_ticks_min or 3)
|
|
42
|
+
n_ticks_max = max(n_ticks_min, n_ticks_max or 4)
|
|
43
|
+
|
|
44
|
+
nbins = n_ticks_max
|
|
45
|
+
|
|
46
|
+
def _is_numeric_label(lbl: str) -> bool:
|
|
47
|
+
"""Check if a tick label represents a numeric value."""
|
|
48
|
+
if not lbl:
|
|
49
|
+
return True
|
|
50
|
+
stripped = lbl.replace(".", "").replace("-", "").replace("+", "")
|
|
51
|
+
stripped = stripped.replace("−", "") # Unicode minus sign
|
|
52
|
+
stripped = stripped.replace("e", "").replace("E", "")
|
|
53
|
+
return stripped.isdigit() or stripped == ""
|
|
54
|
+
|
|
55
|
+
# Check if x-axis is categorical
|
|
56
|
+
x_labels = [t.get_text() for t in ax.get_xticklabels()]
|
|
57
|
+
x_is_categorical = any(not _is_numeric_label(lbl) for lbl in x_labels)
|
|
58
|
+
if not x_is_categorical:
|
|
59
|
+
ax.xaxis.set_major_locator(MaxNLocator(nbins=nbins, min_n_ticks=n_ticks_min))
|
|
60
|
+
|
|
61
|
+
# Check if y-axis is categorical
|
|
62
|
+
y_labels = [t.get_text() for t in ax.get_yticklabels()]
|
|
63
|
+
y_is_categorical = any(not _is_numeric_label(lbl) for lbl in y_labels)
|
|
64
|
+
if not y_is_categorical:
|
|
65
|
+
ax.yaxis.set_major_locator(MaxNLocator(nbins=nbins, min_n_ticks=n_ticks_min))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def finalize_special_plots(ax: Axes, style: Dict[str, Any] = None) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Finalize axes visibility for special plot types (pie, imshow, etc.).
|
|
71
|
+
|
|
72
|
+
This should be called after all plotting is done, before saving.
|
|
73
|
+
It handles plot types that need axes/ticks hidden.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
ax : matplotlib.axes.Axes
|
|
78
|
+
The axes to finalize.
|
|
79
|
+
style : dict, optional
|
|
80
|
+
Style dictionary. If None, uses defaults.
|
|
81
|
+
"""
|
|
82
|
+
from matplotlib.image import AxesImage
|
|
83
|
+
from matplotlib.patches import Wedge
|
|
84
|
+
|
|
85
|
+
if style is None:
|
|
86
|
+
style = {}
|
|
87
|
+
|
|
88
|
+
# Check for pie chart
|
|
89
|
+
has_pie = any(isinstance(p, Wedge) for p in ax.patches)
|
|
90
|
+
if has_pie:
|
|
91
|
+
show_axes = style.get("pie_show_axes", False)
|
|
92
|
+
text_pt = style.get("pie_text_pt", 6)
|
|
93
|
+
font_family = check_font(style.get("font_family", "Arial"))
|
|
94
|
+
|
|
95
|
+
for text in ax.texts:
|
|
96
|
+
transform = text.get_transform()
|
|
97
|
+
if transform == ax.transAxes:
|
|
98
|
+
x, y = text.get_position()
|
|
99
|
+
if y > 1.0 or y < 0.0:
|
|
100
|
+
continue
|
|
101
|
+
text.set_fontsize(text_pt)
|
|
102
|
+
text.set_fontfamily(font_family)
|
|
103
|
+
|
|
104
|
+
if not show_axes:
|
|
105
|
+
ax.set_xticks([])
|
|
106
|
+
ax.set_yticks([])
|
|
107
|
+
ax.set_xticklabels([])
|
|
108
|
+
ax.set_yticklabels([])
|
|
109
|
+
ax.set_xlabel("")
|
|
110
|
+
ax.set_ylabel("")
|
|
111
|
+
for spine in ax.spines.values():
|
|
112
|
+
spine.set_visible(False)
|
|
113
|
+
|
|
114
|
+
# Check for imshow/matshow (has AxesImage)
|
|
115
|
+
has_image = any(isinstance(c, AxesImage) for c in ax.get_children())
|
|
116
|
+
if has_image:
|
|
117
|
+
xlabel = ax.get_xlabel()
|
|
118
|
+
ylabel = ax.get_ylabel()
|
|
119
|
+
is_specgram = xlabel or ylabel
|
|
120
|
+
|
|
121
|
+
if not is_specgram:
|
|
122
|
+
show_axes = style.get("imshow_show_axes", False)
|
|
123
|
+
if not show_axes:
|
|
124
|
+
ax.set_xticks([])
|
|
125
|
+
ax.set_yticks([])
|
|
126
|
+
ax.set_xticklabels([])
|
|
127
|
+
ax.set_yticklabels([])
|
|
128
|
+
for spine in ax.spines.values():
|
|
129
|
+
spine.set_visible(False)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
__all__ = ["finalize_ticks", "finalize_special_plots"]
|
|
133
|
+
|
|
134
|
+
# EOF
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Font utilities for figrecipe.
|
|
4
|
+
|
|
5
|
+
Provides font availability checking and listing for publication-quality figures.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"list_available_fonts",
|
|
10
|
+
"check_font",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
import warnings
|
|
14
|
+
from typing import List
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def list_available_fonts() -> List[str]:
|
|
18
|
+
"""List all available font families.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
list of str
|
|
23
|
+
Sorted list of available font family names.
|
|
24
|
+
|
|
25
|
+
Examples
|
|
26
|
+
--------
|
|
27
|
+
>>> fonts = ps.list_available_fonts()
|
|
28
|
+
>>> print(fonts[:5])
|
|
29
|
+
['Arial', 'Courier New', 'DejaVu Sans', ...]
|
|
30
|
+
"""
|
|
31
|
+
import matplotlib.font_manager as fm
|
|
32
|
+
|
|
33
|
+
fonts = set()
|
|
34
|
+
for font in fm.fontManager.ttflist:
|
|
35
|
+
fonts.add(font.name)
|
|
36
|
+
return sorted(fonts)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def check_font(font_family: str, fallback: str = "DejaVu Sans") -> str:
|
|
40
|
+
"""Check if font is available, with fallback and helpful error message.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
font_family : str
|
|
45
|
+
Requested font family name.
|
|
46
|
+
fallback : str
|
|
47
|
+
Fallback font if requested font is not available.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
str
|
|
52
|
+
The font to use (original if available, fallback otherwise).
|
|
53
|
+
|
|
54
|
+
Examples
|
|
55
|
+
--------
|
|
56
|
+
>>> font = check_font("Arial") # Returns "Arial" if available
|
|
57
|
+
>>> font = check_font("NonExistentFont") # Returns fallback with warning
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
available = list_available_fonts()
|
|
61
|
+
|
|
62
|
+
if font_family in available:
|
|
63
|
+
return font_family
|
|
64
|
+
|
|
65
|
+
# Font not found - show helpful message
|
|
66
|
+
similar = [f for f in available if font_family.lower() in f.lower()]
|
|
67
|
+
|
|
68
|
+
msg = f"Font '{font_family}' not found.\n"
|
|
69
|
+
if similar:
|
|
70
|
+
msg += f" Similar fonts available: {similar[:5]}\n"
|
|
71
|
+
msg += f" Using fallback: '{fallback}'\n"
|
|
72
|
+
msg += " To see all available fonts: ps.list_available_fonts()\n"
|
|
73
|
+
msg += " To install Arial on Linux: sudo apt install ttf-mscorefonts-installer"
|
|
74
|
+
|
|
75
|
+
warnings.warn(msg, UserWarning)
|
|
76
|
+
|
|
77
|
+
return fallback if fallback in available else "DejaVu Sans"
|