figrecipe 0.6.0__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- figrecipe/__init__.py +161 -1030
- figrecipe/__main__.py +12 -0
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +113 -0
- figrecipe/_api/_save.py +287 -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/_cli/__init__.py +7 -0
- figrecipe/_cli/_compose.py +87 -0
- figrecipe/_cli/_convert.py +117 -0
- figrecipe/_cli/_crop.py +82 -0
- figrecipe/_cli/_edit.py +70 -0
- figrecipe/_cli/_extract.py +128 -0
- figrecipe/_cli/_fonts.py +47 -0
- figrecipe/_cli/_info.py +67 -0
- figrecipe/_cli/_main.py +58 -0
- figrecipe/_cli/_reproduce.py +79 -0
- figrecipe/_cli/_style.py +77 -0
- figrecipe/_cli/_validate.py +66 -0
- figrecipe/_cli/_version.py +50 -0
- figrecipe/_composition/__init__.py +32 -0
- figrecipe/_composition/_alignment.py +452 -0
- figrecipe/_composition/_compose.py +179 -0
- figrecipe/_composition/_import_axes.py +127 -0
- figrecipe/_composition/_visibility.py +125 -0
- figrecipe/_dev/__init__.py +4 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/browser/__init__.py +69 -0
- figrecipe/_dev/browser/_audio.py +240 -0
- figrecipe/_dev/browser/_caption.py +356 -0
- figrecipe/_dev/browser/_click_effect.py +146 -0
- figrecipe/_dev/browser/_cursor.py +196 -0
- figrecipe/_dev/browser/_highlight.py +105 -0
- figrecipe/_dev/browser/_narration.py +237 -0
- figrecipe/_dev/browser/_recorder.py +446 -0
- figrecipe/_dev/browser/_utils.py +178 -0
- figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
- figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
- figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
- figrecipe/_dev/demo_plotters/__init__.py +35 -166
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_editor/__init__.py +61 -13
- 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 +402 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +466 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_call_overrides.py +183 -0
- figrecipe/_editor/_datatable_plot_handlers.py +249 -0
- figrecipe/_editor/_figure_layout.py +211 -0
- figrecipe/_editor/_flask_app.py +200 -1030
- figrecipe/_editor/_helpers.py +251 -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 +194 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +507 -0
- figrecipe/_editor/_renderer.py +81 -186
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +482 -0
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +126 -0
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +335 -0
- figrecipe/_editor/_routes_files.py +443 -0
- figrecipe/_editor/_routes_image.py +200 -0
- figrecipe/_editor/_routes_snapshot.py +94 -0
- figrecipe/_editor/_routes_style.py +243 -0
- figrecipe/_editor/_templates/__init__.py +116 -1
- figrecipe/_editor/_templates/_html.py +154 -64
- figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
- figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
- figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
- figrecipe/_editor/_templates/_html_datatable.py +92 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +493 -0
- figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
- figrecipe/_editor/_templates/_scripts/_files.py +429 -0
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
- figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
- figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
- figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
- figrecipe/_editor/_templates/_styles/__init__.py +78 -0
- figrecipe/_editor/_templates/_styles/_base.py +111 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +430 -0
- figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
- figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
- figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
- figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
- figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
- figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +224 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +127 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +430 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
- figrecipe/_editor/static/audio/click.mp3 +0 -0
- figrecipe/_editor/static/click.mp3 +0 -0
- figrecipe/_editor/static/icons/favicon.ico +0 -0
- figrecipe/_integrations/__init__.py +17 -0
- figrecipe/_integrations/_scitex_stats.py +298 -0
- figrecipe/_params/_DECORATION_METHODS.py +8 -0
- figrecipe/_recorder.py +63 -109
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +509 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +252 -895
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +188 -1
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_stat_annotation.py +274 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/styles/__init__.py +8 -6
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +42 -480
- figrecipe/styles/_style_loader.py +16 -192
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
- figrecipe/styles/presets/SCITEX.yaml +40 -28
- figrecipe-0.9.0.dist-info/METADATA +427 -0
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
- figrecipe/_editor/_bbox.py +0 -978
- figrecipe/_editor/_hitmap.py +0 -937
- figrecipe/_editor/_templates/_scripts.py +0 -2778
- figrecipe/_editor/_templates/_styles.py +0 -1326
- figrecipe/_reproducer.py +0 -975
- figrecipe-0.6.0.dist-info/METADATA +0 -394
- figrecipe-0.6.0.dist-info/RECORD +0 -90
- /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Helper functions for custom plot methods in RecordingAxes."""
|
|
4
|
+
|
|
5
|
+
from typing import List, Tuple
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_colors_from_style(n_colors: int, explicit_colors=None) -> List:
|
|
11
|
+
"""Get colors from style or matplotlib defaults.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
n_colors : int
|
|
16
|
+
Number of colors needed.
|
|
17
|
+
explicit_colors : list or color, optional
|
|
18
|
+
Explicitly provided colors.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
list
|
|
23
|
+
List of colors.
|
|
24
|
+
"""
|
|
25
|
+
if explicit_colors is not None:
|
|
26
|
+
if isinstance(explicit_colors, list):
|
|
27
|
+
return explicit_colors
|
|
28
|
+
return [explicit_colors] * n_colors
|
|
29
|
+
|
|
30
|
+
from ..styles import get_style
|
|
31
|
+
|
|
32
|
+
style = get_style()
|
|
33
|
+
if style and "colors" in style and "palette" in style.colors:
|
|
34
|
+
palette = list(style.colors.palette)
|
|
35
|
+
colors = []
|
|
36
|
+
for c in palette:
|
|
37
|
+
if isinstance(c, (list, tuple)) and len(c) >= 3:
|
|
38
|
+
if all(v <= 1.0 for v in c):
|
|
39
|
+
colors.append(tuple(c))
|
|
40
|
+
else:
|
|
41
|
+
colors.append(tuple(v / 255.0 for v in c))
|
|
42
|
+
else:
|
|
43
|
+
colors.append(c)
|
|
44
|
+
return colors
|
|
45
|
+
|
|
46
|
+
# Matplotlib default color cycle
|
|
47
|
+
import matplotlib.pyplot as plt
|
|
48
|
+
|
|
49
|
+
return [c["color"] for c in plt.rcParams["axes.prop_cycle"]]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def beeswarm_positions(
|
|
53
|
+
data: np.ndarray,
|
|
54
|
+
width: float,
|
|
55
|
+
rng: np.random.Generator,
|
|
56
|
+
) -> np.ndarray:
|
|
57
|
+
"""Calculate beeswarm-style x positions to minimize overlap.
|
|
58
|
+
|
|
59
|
+
This is a simplified beeswarm that uses binning and jittering.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
data : array
|
|
64
|
+
Y values of points.
|
|
65
|
+
width : float
|
|
66
|
+
Maximum jitter width.
|
|
67
|
+
rng : Generator
|
|
68
|
+
Random number generator.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
array
|
|
73
|
+
X offsets for each point.
|
|
74
|
+
"""
|
|
75
|
+
n = len(data)
|
|
76
|
+
if n == 0:
|
|
77
|
+
return np.array([])
|
|
78
|
+
|
|
79
|
+
# Sort data and get order
|
|
80
|
+
order = np.argsort(data)
|
|
81
|
+
sorted_data = data[order]
|
|
82
|
+
|
|
83
|
+
# Group nearby points and offset them
|
|
84
|
+
x_offsets = np.zeros(n)
|
|
85
|
+
|
|
86
|
+
# Simple approach: bin by quantiles and spread within each bin
|
|
87
|
+
n_bins = max(1, int(np.sqrt(n)))
|
|
88
|
+
bin_edges = np.percentile(sorted_data, np.linspace(0, 100, n_bins + 1))
|
|
89
|
+
|
|
90
|
+
for i in range(n_bins):
|
|
91
|
+
mask = (sorted_data >= bin_edges[i]) & (sorted_data <= bin_edges[i + 1])
|
|
92
|
+
n_in_bin = mask.sum()
|
|
93
|
+
if n_in_bin > 0:
|
|
94
|
+
# Spread points evenly within bin width
|
|
95
|
+
offsets = np.linspace(-width / 2, width / 2, n_in_bin)
|
|
96
|
+
# Add small random noise
|
|
97
|
+
offsets += rng.uniform(-width * 0.1, width * 0.1, n_in_bin)
|
|
98
|
+
x_offsets[mask] = offsets
|
|
99
|
+
|
|
100
|
+
# Restore original order
|
|
101
|
+
result = np.zeros(n)
|
|
102
|
+
result[order] = x_offsets
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def compute_joyplot_kdes(arrays: List[np.ndarray], x: np.ndarray) -> Tuple[List, float]:
|
|
107
|
+
"""Compute KDEs for joyplot ridges.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
arrays : list
|
|
112
|
+
List of data arrays.
|
|
113
|
+
x : array
|
|
114
|
+
X values for KDE evaluation.
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
tuple
|
|
119
|
+
(kdes, max_density)
|
|
120
|
+
"""
|
|
121
|
+
from scipy import stats
|
|
122
|
+
|
|
123
|
+
kdes = []
|
|
124
|
+
max_density = 0
|
|
125
|
+
for arr in arrays:
|
|
126
|
+
arr = np.asarray(arr)
|
|
127
|
+
if len(arr) > 1:
|
|
128
|
+
kde = stats.gaussian_kde(arr)
|
|
129
|
+
density = kde(x)
|
|
130
|
+
kdes.append(density)
|
|
131
|
+
max_density = max(max_density, np.max(density))
|
|
132
|
+
else:
|
|
133
|
+
kdes.append(np.zeros_like(x))
|
|
134
|
+
return kdes, max_density
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
__all__ = [
|
|
138
|
+
"get_colors_from_style",
|
|
139
|
+
"beeswarm_positions",
|
|
140
|
+
"compute_joyplot_kdes",
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
# EOF
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Statistical annotation drawing utilities for comparison brackets and stars."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
6
|
+
|
|
7
|
+
from matplotlib.axes import Axes
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_theme_text_color(default: str = "black") -> str:
|
|
11
|
+
"""Get text color from loaded style's theme settings."""
|
|
12
|
+
try:
|
|
13
|
+
from ..styles._style_loader import _STYLE_CACHE
|
|
14
|
+
|
|
15
|
+
if _STYLE_CACHE is not None:
|
|
16
|
+
theme = getattr(_STYLE_CACHE, "theme", None)
|
|
17
|
+
if theme is not None:
|
|
18
|
+
mode = getattr(theme, "mode", "light")
|
|
19
|
+
theme_colors = getattr(theme, mode, None)
|
|
20
|
+
if theme_colors is not None:
|
|
21
|
+
return getattr(theme_colors, "text", default)
|
|
22
|
+
except Exception:
|
|
23
|
+
pass
|
|
24
|
+
return default
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_style_value(section: str, key: str, default: Any) -> Any:
|
|
28
|
+
"""Get a value from loaded style settings.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
section : str
|
|
33
|
+
Style section (e.g., 'fonts', 'lines', 'stat_annotation')
|
|
34
|
+
key : str
|
|
35
|
+
Key within the section (e.g., 'annotation_pt', 'bracket_mm')
|
|
36
|
+
default : Any
|
|
37
|
+
Default value if not found
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
from ..styles._style_loader import _STYLE_CACHE
|
|
41
|
+
|
|
42
|
+
if _STYLE_CACHE is not None:
|
|
43
|
+
section_obj = getattr(_STYLE_CACHE, section, None)
|
|
44
|
+
if section_obj is not None:
|
|
45
|
+
return getattr(section_obj, key, default)
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
return default
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def p_to_stars(p_value: float, ns_symbol: bool = True) -> str:
|
|
52
|
+
"""Convert p-value to significance stars.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
p_value : float
|
|
57
|
+
The p-value to convert.
|
|
58
|
+
ns_symbol : bool
|
|
59
|
+
If True, return "n.s." for non-significant. If False, return "".
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
str
|
|
64
|
+
Stars representation: "***" (p<0.001), "**" (p<0.01),
|
|
65
|
+
"*" (p<0.05), "n.s." or "" (p>=0.05).
|
|
66
|
+
"""
|
|
67
|
+
if p_value < 0.001:
|
|
68
|
+
return "***"
|
|
69
|
+
elif p_value < 0.01:
|
|
70
|
+
return "**"
|
|
71
|
+
elif p_value < 0.05:
|
|
72
|
+
return "*"
|
|
73
|
+
else:
|
|
74
|
+
return "n.s." if ns_symbol else ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def draw_stat_annotation(
|
|
78
|
+
ax: Axes,
|
|
79
|
+
x1: float,
|
|
80
|
+
x2: float,
|
|
81
|
+
y: Optional[float] = None,
|
|
82
|
+
text: Optional[str] = None,
|
|
83
|
+
p_value: Optional[float] = None,
|
|
84
|
+
style: Literal["stars", "p_value", "both", "bracket_only"] = "stars",
|
|
85
|
+
bracket_height: Optional[float] = None,
|
|
86
|
+
text_offset: Optional[float] = None,
|
|
87
|
+
color: Optional[str] = None,
|
|
88
|
+
linewidth: Optional[float] = None,
|
|
89
|
+
fontsize: Optional[float] = None,
|
|
90
|
+
fontweight: Optional[str] = None,
|
|
91
|
+
**kwargs,
|
|
92
|
+
) -> List[Any]:
|
|
93
|
+
"""Draw a statistical comparison bracket with annotation.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
ax : Axes
|
|
98
|
+
The matplotlib axes to draw on.
|
|
99
|
+
x1, x2 : float
|
|
100
|
+
X positions of the two groups being compared.
|
|
101
|
+
y : float, optional
|
|
102
|
+
Y position for the bracket. If None, auto-calculated from data.
|
|
103
|
+
text : str, optional
|
|
104
|
+
Custom text to display. Overrides p_value formatting.
|
|
105
|
+
p_value : float, optional
|
|
106
|
+
P-value for automatic star conversion.
|
|
107
|
+
style : str
|
|
108
|
+
Display style: "stars", "p_value", "both", "bracket_only".
|
|
109
|
+
bracket_height : float
|
|
110
|
+
Height of bracket tips as fraction of axes height.
|
|
111
|
+
text_offset : float
|
|
112
|
+
Offset of text above bracket as fraction of axes height.
|
|
113
|
+
color : str
|
|
114
|
+
Color for bracket and text.
|
|
115
|
+
linewidth : float
|
|
116
|
+
Line width for bracket.
|
|
117
|
+
fontsize : float
|
|
118
|
+
Font size for annotation text.
|
|
119
|
+
**kwargs
|
|
120
|
+
Additional kwargs passed to ax.text().
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
list
|
|
125
|
+
List of matplotlib artists created (lines and text).
|
|
126
|
+
"""
|
|
127
|
+
artists = []
|
|
128
|
+
|
|
129
|
+
from .._utils._units import mm_to_pt
|
|
130
|
+
|
|
131
|
+
# Resolve values from style if not explicitly provided
|
|
132
|
+
if color is None:
|
|
133
|
+
color = get_theme_text_color(default="black")
|
|
134
|
+
if bracket_height is None:
|
|
135
|
+
bracket_height = get_style_value("stat_annotation", "bracket_height", 0.03)
|
|
136
|
+
if text_offset is None:
|
|
137
|
+
text_offset = get_style_value("stat_annotation", "text_offset", 0.01)
|
|
138
|
+
if linewidth is None:
|
|
139
|
+
# Read mm value and convert to points
|
|
140
|
+
linewidth_mm = get_style_value("stat_annotation", "linewidth_mm", 0.2)
|
|
141
|
+
linewidth = mm_to_pt(linewidth_mm)
|
|
142
|
+
|
|
143
|
+
# Font settings from style: both stars and p-values use same fontsize_pt
|
|
144
|
+
# Stars are bold, p-values are normal weight
|
|
145
|
+
annotation_fontsize = get_style_value("stat_annotation", "fontsize_pt", 6)
|
|
146
|
+
stars_fontweight = get_style_value("stat_annotation", "stars_fontweight", "bold")
|
|
147
|
+
|
|
148
|
+
# Get axes limits for relative positioning
|
|
149
|
+
ylim = ax.get_ylim()
|
|
150
|
+
y_range = ylim[1] - ylim[0]
|
|
151
|
+
|
|
152
|
+
# Auto-calculate y position if not provided
|
|
153
|
+
if y is None:
|
|
154
|
+
# Find max y value in the x range and add padding
|
|
155
|
+
y = ylim[1] + y_range * 0.05
|
|
156
|
+
|
|
157
|
+
# Calculate bracket dimensions in data coordinates
|
|
158
|
+
tip_height = y_range * bracket_height
|
|
159
|
+
text_y_offset = y_range * text_offset
|
|
160
|
+
|
|
161
|
+
# Draw bracket: horizontal line with vertical tips
|
|
162
|
+
# Left tip
|
|
163
|
+
line1 = ax.plot(
|
|
164
|
+
[x1, x1], [y - tip_height, y], color=color, linewidth=linewidth, clip_on=False
|
|
165
|
+
)[0]
|
|
166
|
+
artists.append(line1)
|
|
167
|
+
|
|
168
|
+
# Horizontal bar
|
|
169
|
+
line2 = ax.plot([x1, x2], [y, y], color=color, linewidth=linewidth, clip_on=False)[
|
|
170
|
+
0
|
|
171
|
+
]
|
|
172
|
+
artists.append(line2)
|
|
173
|
+
|
|
174
|
+
# Right tip
|
|
175
|
+
line3 = ax.plot(
|
|
176
|
+
[x2, x2], [y, y - tip_height], color=color, linewidth=linewidth, clip_on=False
|
|
177
|
+
)[0]
|
|
178
|
+
artists.append(line3)
|
|
179
|
+
|
|
180
|
+
# Determine annotation text and whether it's stars-only
|
|
181
|
+
is_stars_only = False
|
|
182
|
+
if text is None and style != "bracket_only":
|
|
183
|
+
if p_value is not None:
|
|
184
|
+
if style == "stars":
|
|
185
|
+
text = p_to_stars(p_value)
|
|
186
|
+
# Only bold for actual stars, not for n.s.
|
|
187
|
+
is_stars_only = text not in ("n.s.", "")
|
|
188
|
+
elif style == "p_value":
|
|
189
|
+
# Use italic p with spaces around operators
|
|
190
|
+
if p_value < 0.001:
|
|
191
|
+
text = r"$\it{p}$ < 0.001"
|
|
192
|
+
else:
|
|
193
|
+
text = rf"$\it{{p}}$ = {p_value:.3f}"
|
|
194
|
+
elif style == "both":
|
|
195
|
+
stars = p_to_stars(p_value)
|
|
196
|
+
# Use italic p with spaces around operators
|
|
197
|
+
if p_value < 0.001:
|
|
198
|
+
text = rf"{stars} ($\it{{p}}$ < 0.001)"
|
|
199
|
+
else:
|
|
200
|
+
text = rf"{stars} ($\it{{p}}$ = {p_value:.3f})"
|
|
201
|
+
|
|
202
|
+
# Draw text if available
|
|
203
|
+
if text and style != "bracket_only":
|
|
204
|
+
text_x = (x1 + x2) / 2
|
|
205
|
+
text_y = y + text_y_offset
|
|
206
|
+
|
|
207
|
+
# Use same fontsize for stars and p-values, but stars are bold
|
|
208
|
+
effective_fontsize = fontsize if fontsize is not None else annotation_fontsize
|
|
209
|
+
if is_stars_only:
|
|
210
|
+
effective_fontweight = (
|
|
211
|
+
fontweight if fontweight is not None else stars_fontweight
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
effective_fontweight = fontweight if fontweight is not None else "normal"
|
|
215
|
+
|
|
216
|
+
text_kwargs = {
|
|
217
|
+
"ha": "center",
|
|
218
|
+
"va": "bottom",
|
|
219
|
+
"fontsize": effective_fontsize,
|
|
220
|
+
"fontweight": effective_fontweight,
|
|
221
|
+
"color": color,
|
|
222
|
+
}
|
|
223
|
+
text_kwargs.update(kwargs)
|
|
224
|
+
txt = ax.text(text_x, text_y, text, **text_kwargs)
|
|
225
|
+
artists.append(txt)
|
|
226
|
+
|
|
227
|
+
return artists
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def calculate_auto_y(
|
|
231
|
+
ax: Axes,
|
|
232
|
+
x1: float,
|
|
233
|
+
x2: float,
|
|
234
|
+
existing_annotations: List[Dict[str, Any]],
|
|
235
|
+
padding: float = 0.05,
|
|
236
|
+
) -> float:
|
|
237
|
+
"""Calculate automatic y position for a new annotation.
|
|
238
|
+
|
|
239
|
+
Avoids overlapping with existing annotations by stacking.
|
|
240
|
+
|
|
241
|
+
Parameters
|
|
242
|
+
----------
|
|
243
|
+
ax : Axes
|
|
244
|
+
The matplotlib axes.
|
|
245
|
+
x1, x2 : float
|
|
246
|
+
X positions of the comparison.
|
|
247
|
+
existing_annotations : list
|
|
248
|
+
List of existing annotation info dicts with x1, x2, y keys.
|
|
249
|
+
padding : float
|
|
250
|
+
Padding as fraction of y range.
|
|
251
|
+
|
|
252
|
+
Returns
|
|
253
|
+
-------
|
|
254
|
+
float
|
|
255
|
+
Suggested y position for the new annotation.
|
|
256
|
+
"""
|
|
257
|
+
ylim = ax.get_ylim()
|
|
258
|
+
y_range = ylim[1] - ylim[0]
|
|
259
|
+
pad = y_range * padding
|
|
260
|
+
|
|
261
|
+
# Start above the data
|
|
262
|
+
y = ylim[1] + pad
|
|
263
|
+
|
|
264
|
+
# Check for overlaps with existing annotations
|
|
265
|
+
for ann in existing_annotations:
|
|
266
|
+
ann_x1, ann_x2 = ann.get("x1", 0), ann.get("x2", 0)
|
|
267
|
+
ann_y = ann.get("y", 0)
|
|
268
|
+
|
|
269
|
+
# Check if x ranges overlap
|
|
270
|
+
if not (x2 < ann_x1 or x1 > ann_x2):
|
|
271
|
+
# Overlapping x range, need to stack
|
|
272
|
+
y = max(y, ann_y + pad * 2)
|
|
273
|
+
|
|
274
|
+
return y
|
|
@@ -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/styles/__init__.py
CHANGED
|
@@ -18,12 +18,10 @@ Usage:
|
|
|
18
18
|
fig, ax = ps.subplots(**style.to_subplots_kwargs())
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
-
from .
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
list_available_fonts,
|
|
26
|
-
)
|
|
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
|
|
27
25
|
from ._style_loader import (
|
|
28
26
|
STYLE,
|
|
29
27
|
get_style,
|
|
@@ -33,8 +31,10 @@ from ._style_loader import (
|
|
|
33
31
|
to_subplots_kwargs,
|
|
34
32
|
unload_style,
|
|
35
33
|
)
|
|
34
|
+
from ._themes import apply_theme_colors
|
|
36
35
|
|
|
37
36
|
__all__ = [
|
|
37
|
+
"DotDict",
|
|
38
38
|
"load_style",
|
|
39
39
|
"unload_style",
|
|
40
40
|
"get_style",
|
|
@@ -46,4 +46,6 @@ __all__ = [
|
|
|
46
46
|
"apply_theme_colors",
|
|
47
47
|
"check_font",
|
|
48
48
|
"list_available_fonts",
|
|
49
|
+
"finalize_ticks",
|
|
50
|
+
"finalize_special_plots",
|
|
49
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
|