scitex 2.3.0__py3-none-any.whl → 2.4.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.
- scitex/ai/classification/reporters/reporter_utils/_Plotter.py +1 -1
- scitex/ai/plt/__init__.py +2 -2
- scitex/ai/plt/{_plot_conf_mat.py → _stx_conf_mat.py} +3 -3
- scitex/config/PriorityConfig.py +195 -0
- scitex/config/__init__.py +24 -0
- scitex/io/_save.py +125 -34
- scitex/io/_save_modules/_image.py +37 -20
- scitex/plt/__init__.py +470 -17
- scitex/plt/_subplots/_AxisWrapper.py +98 -50
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +254 -124
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +49 -8
- scitex/plt/_subplots/_SubplotsWrapper.py +76 -91
- scitex/plt/_subplots/_export_as_csv.py +127 -58
- scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +25 -16
- scitex/plt/_subplots/_export_as_csv_formatters/_format_contourf.py +54 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_hexbin.py +41 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_hist2d.py +41 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow.py +59 -47
- scitex/plt/_subplots/_export_as_csv_formatters/_format_matshow.py +42 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_pie.py +42 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +72 -35
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_box.py +1 -1
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_quiver.py +53 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stem.py +42 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_step.py +42 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_streamplot.py +48 -0
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_conf_mat.py → _format_stx_conf_mat.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_ecdf.py → _format_stx_ecdf.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_fillv.py → _format_stx_fillv.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_heatmap.py → _format_stx_heatmap.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_image.py → _format_stx_image.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_joyplot.py → _format_stx_joyplot.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_line.py → _format_stx_line.py} +3 -3
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_mean_ci.py → _format_stx_mean_ci.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_mean_std.py → _format_stx_mean_std.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_median_iqr.py → _format_stx_median_iqr.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_raster.py → _format_stx_raster.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_rectangle.py → _format_stx_rectangle.py} +1 -1
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_scatter_hist.py → _format_stx_scatter_hist.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_shaded_line.py → _format_stx_shaded_line.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_violin.py → _format_stx_violin.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/verify_formatters.py +23 -23
- scitex/plt/ax/__init__.py +16 -15
- scitex/plt/ax/_plot/__init__.py +30 -30
- scitex/plt/ax/_plot/_add_fitted_line.py +65 -11
- scitex/plt/ax/_plot/_plot_statistical_shaded_line.py +104 -76
- scitex/plt/ax/_plot/{_plot_conf_mat.py → _stx_conf_mat.py} +10 -10
- scitex/plt/ax/_plot/_stx_ecdf.py +109 -0
- scitex/plt/ax/_plot/{_plot_fillv.py → _stx_fillv.py} +7 -7
- scitex/plt/ax/_plot/_stx_heatmap.py +366 -0
- scitex/plt/ax/_plot/{_plot_image.py → _stx_image.py} +1 -1
- scitex/plt/ax/_plot/_stx_joyplot.py +113 -0
- scitex/plt/ax/_plot/{_plot_raster.py → _stx_raster.py} +37 -25
- scitex/plt/ax/_plot/{_plot_rectangle.py → _stx_rectangle.py} +10 -9
- scitex/plt/ax/_plot/{_plot_scatter_hist.py → _stx_scatter_hist.py} +1 -1
- scitex/plt/ax/_plot/_stx_shaded_line.py +215 -0
- scitex/plt/ax/_plot/{_plot_violin.py → _stx_violin.py} +13 -6
- scitex/plt/ax/_style/__init__.py +3 -0
- scitex/plt/ax/_style/_style_barplot.py +13 -2
- scitex/plt/ax/_style/_style_boxplot.py +78 -32
- scitex/plt/ax/_style/_style_errorbar.py +17 -3
- scitex/plt/ax/_style/_style_scatter.py +17 -3
- scitex/plt/ax/_style/_style_violinplot.py +109 -0
- scitex/plt/color/_vizualize_colors.py +3 -3
- scitex/plt/styles/SCITEX_STYLE.yaml +104 -0
- scitex/plt/styles/__init__.py +57 -0
- scitex/plt/styles/_plot_defaults.py +209 -0
- scitex/plt/styles/_plot_postprocess.py +518 -0
- scitex/plt/styles/_style_loader.py +268 -0
- scitex/plt/styles/presets.py +208 -0
- scitex/plt/utils/_collect_figure_metadata.py +160 -18
- scitex/plt/utils/_colorbar.py +72 -10
- scitex/plt/utils/_configure_mpl.py +108 -52
- scitex/plt/utils/_crop.py +21 -7
- scitex/plt/utils/_figure_mm.py +21 -7
- scitex/stats/__init__.py +13 -1
- scitex/stats/_schema.py +578 -0
- scitex/stats/tests/__init__.py +13 -0
- scitex/stats/tests/correlation/__init__.py +13 -0
- scitex/stats/tests/correlation/_test_pearson.py +262 -0
- scitex/vis/__init__.py +6 -0
- scitex/vis/editor/__init__.py +23 -0
- scitex/vis/editor/_defaults.py +205 -0
- scitex/vis/editor/_edit.py +342 -0
- scitex/vis/editor/_mpl_editor.py +231 -0
- scitex/vis/editor/_tkinter_editor.py +466 -0
- scitex/vis/editor/_web_editor.py +1440 -0
- scitex/vis/model/plot_types.py +15 -15
- {scitex-2.3.0.dist-info → scitex-2.4.0.dist-info}/METADATA +2 -1
- {scitex-2.3.0.dist-info → scitex-2.4.0.dist-info}/RECORD +94 -67
- {scitex-2.3.0.dist-info → scitex-2.4.0.dist-info}/WHEEL +1 -1
- scitex/plt/ax/_plot/_plot_ecdf.py +0 -84
- scitex/plt/ax/_plot/_plot_heatmap.py +0 -277
- scitex/plt/ax/_plot/_plot_joyplot.py +0 -77
- scitex/plt/ax/_plot/_plot_shaded_line.py +0 -142
- scitex/plt/presets.py +0 -224
- {scitex-2.3.0.dist-info → scitex-2.4.0.dist-info}/entry_points.txt +0 -0
- {scitex-2.3.0.dist-info → scitex-2.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# Timestamp: "2025-12-01 10:00:00 (ywatanabe)"
|
|
4
|
+
# File: /home/ywatanabe/proj/scitex-code/src/scitex/plt/styles/_plot_postprocess.py
|
|
5
|
+
|
|
6
|
+
"""Post-processing styling for plot methods.
|
|
7
|
+
|
|
8
|
+
This module centralizes all styling applied AFTER matplotlib methods
|
|
9
|
+
are called. Each function modifies the plot result or axes in-place.
|
|
10
|
+
|
|
11
|
+
All default values are loaded from SCITEX_STYLE.yaml via presets.py.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from matplotlib.ticker import MaxNLocator, FixedLocator
|
|
16
|
+
from matplotlib.category import StrCategoryConverter, UnitData
|
|
17
|
+
|
|
18
|
+
from scitex.plt.utils import mm_to_pt
|
|
19
|
+
from scitex.plt.styles.presets import SCITEX_STYLE
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ============================================================================
|
|
23
|
+
# Constants (loaded from centralized SCITEX_STYLE.yaml)
|
|
24
|
+
# ============================================================================
|
|
25
|
+
DEFAULT_LINE_WIDTH_MM = SCITEX_STYLE.get("trace_thickness_mm", 0.2)
|
|
26
|
+
DEFAULT_MARKER_SIZE_MM = SCITEX_STYLE.get("marker_size_mm", 0.8)
|
|
27
|
+
DEFAULT_N_TICKS = SCITEX_STYLE.get("n_ticks", 4) - 1 # nbins = n_ticks - 1
|
|
28
|
+
SPINE_ZORDER = 1000
|
|
29
|
+
CAP_WIDTH_RATIO = 1 / 3 # 33% of bar/box width
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ============================================================================
|
|
33
|
+
# Main post-processing function
|
|
34
|
+
# ============================================================================
|
|
35
|
+
def apply_plot_postprocess(method_name, result, ax, kwargs, args=None):
|
|
36
|
+
"""Apply post-processing styling after matplotlib method call.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
method_name: Name of the matplotlib method that was called
|
|
40
|
+
result: Return value from the matplotlib method
|
|
41
|
+
ax: The matplotlib axes
|
|
42
|
+
kwargs: Original kwargs passed to the method
|
|
43
|
+
args: Original positional args passed to the method (needed for violinplot)
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The result (possibly modified)
|
|
47
|
+
"""
|
|
48
|
+
# Always ensure spines are on top
|
|
49
|
+
_ensure_spines_on_top(ax)
|
|
50
|
+
|
|
51
|
+
# Apply tick locator for numerical axes
|
|
52
|
+
_apply_tick_locator(ax)
|
|
53
|
+
|
|
54
|
+
# Method-specific post-processing
|
|
55
|
+
if method_name == 'pie' and result is not None:
|
|
56
|
+
_postprocess_pie(result)
|
|
57
|
+
elif method_name == 'stem' and result is not None:
|
|
58
|
+
_postprocess_stem(result)
|
|
59
|
+
elif method_name == 'violinplot' and result is not None:
|
|
60
|
+
_postprocess_violin(result, ax, kwargs, args)
|
|
61
|
+
elif method_name == 'boxplot' and result is not None:
|
|
62
|
+
_postprocess_boxplot(result, ax)
|
|
63
|
+
elif method_name == 'scatter' and result is not None:
|
|
64
|
+
_postprocess_scatter(result, kwargs)
|
|
65
|
+
elif method_name == 'bar' and result is not None:
|
|
66
|
+
_postprocess_bar(result, ax, kwargs)
|
|
67
|
+
elif method_name == 'barh' and result is not None:
|
|
68
|
+
_postprocess_barh(result, ax, kwargs)
|
|
69
|
+
elif method_name == 'errorbar' and result is not None:
|
|
70
|
+
_postprocess_errorbar(result)
|
|
71
|
+
elif method_name == 'hist' and result is not None:
|
|
72
|
+
_postprocess_hist(result, ax)
|
|
73
|
+
elif method_name == 'fill_between' and result is not None:
|
|
74
|
+
_postprocess_fill_between(result, kwargs)
|
|
75
|
+
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ============================================================================
|
|
80
|
+
# General post-processing
|
|
81
|
+
# ============================================================================
|
|
82
|
+
def _ensure_spines_on_top(ax):
|
|
83
|
+
"""Ensure axes spines are always drawn in front of plot elements."""
|
|
84
|
+
try:
|
|
85
|
+
ax.set_axisbelow(False)
|
|
86
|
+
|
|
87
|
+
# Set very high z-order for spines
|
|
88
|
+
for spine in ax.spines.values():
|
|
89
|
+
spine.set_zorder(SPINE_ZORDER)
|
|
90
|
+
|
|
91
|
+
# Set z-order for tick marks
|
|
92
|
+
ax.tick_params(zorder=SPINE_ZORDER)
|
|
93
|
+
|
|
94
|
+
# Ensure plot patches have lower z-order than spines
|
|
95
|
+
# But preserve intentionally set z-orders (e.g., boxplot in violin)
|
|
96
|
+
for patch in ax.patches:
|
|
97
|
+
current_z = patch.get_zorder()
|
|
98
|
+
# Only lower z-order if it's >= SPINE_ZORDER or is at matplotlib default (1)
|
|
99
|
+
if current_z >= SPINE_ZORDER:
|
|
100
|
+
patch.set_zorder(current_z - SPINE_ZORDER)
|
|
101
|
+
elif current_z == 1:
|
|
102
|
+
# Default matplotlib z-order, lower it
|
|
103
|
+
patch.set_zorder(0.5)
|
|
104
|
+
# Otherwise, preserve the intentionally set z-order
|
|
105
|
+
|
|
106
|
+
# Set axes patch behind everything
|
|
107
|
+
ax.patch.set_zorder(-1)
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _apply_tick_locator(ax):
|
|
113
|
+
"""Apply MaxNLocator only to numerical (non-categorical) axes.
|
|
114
|
+
|
|
115
|
+
Target: 3-4 ticks per axis for clean publication figures.
|
|
116
|
+
MaxNLocator's nbins=3 gives approximately 3-4 tick marks.
|
|
117
|
+
min_n_ticks=3 ensures at least 3 ticks (never 2).
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
def is_categorical_axis(axis):
|
|
121
|
+
if isinstance(axis.converter, StrCategoryConverter):
|
|
122
|
+
return True
|
|
123
|
+
if hasattr(axis, 'units') and isinstance(axis.units, UnitData):
|
|
124
|
+
return True
|
|
125
|
+
if isinstance(axis.get_major_locator(), FixedLocator):
|
|
126
|
+
return True
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
if not is_categorical_axis(ax.xaxis):
|
|
130
|
+
ax.xaxis.set_major_locator(
|
|
131
|
+
MaxNLocator(nbins=DEFAULT_N_TICKS, min_n_ticks=3, integer=False, prune=None)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if not is_categorical_axis(ax.yaxis):
|
|
135
|
+
ax.yaxis.set_major_locator(
|
|
136
|
+
MaxNLocator(nbins=DEFAULT_N_TICKS, min_n_ticks=3, integer=False, prune=None)
|
|
137
|
+
)
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ============================================================================
|
|
143
|
+
# Method-specific post-processing
|
|
144
|
+
# ============================================================================
|
|
145
|
+
def _postprocess_pie(result):
|
|
146
|
+
"""Apply styling for pie charts."""
|
|
147
|
+
# pie returns (wedges, texts, autotexts) when autopct is used
|
|
148
|
+
if len(result) >= 3:
|
|
149
|
+
autotexts = result[2]
|
|
150
|
+
for autotext in autotexts:
|
|
151
|
+
autotext.set_fontsize(6) # 6pt for inline percentages
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _postprocess_stem(result):
|
|
155
|
+
"""Apply styling for stem plots."""
|
|
156
|
+
baseline = result.baseline
|
|
157
|
+
if baseline is not None:
|
|
158
|
+
baseline.set_color('black')
|
|
159
|
+
baseline.set_linestyle('--')
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _postprocess_errorbar(result):
|
|
163
|
+
"""Apply styling for errorbar plots.
|
|
164
|
+
|
|
165
|
+
Simplifies the legend to show only a line (no caps/bars).
|
|
166
|
+
"""
|
|
167
|
+
import matplotlib.legend as mlegend
|
|
168
|
+
from matplotlib.container import ErrorbarContainer
|
|
169
|
+
from matplotlib.legend_handler import HandlerErrorbar, HandlerLine2D
|
|
170
|
+
|
|
171
|
+
# Custom handler that shows only a simple line for errorbar
|
|
172
|
+
class SimpleLineHandler(HandlerErrorbar):
|
|
173
|
+
def create_artists(self, legend, orig_handle, xdescent, ydescent,
|
|
174
|
+
width, height, fontsize, trans):
|
|
175
|
+
# Use HandlerLine2D to create just a line
|
|
176
|
+
line_handler = HandlerLine2D()
|
|
177
|
+
# Get the data line from the ErrorbarContainer
|
|
178
|
+
data_line = orig_handle[0]
|
|
179
|
+
if data_line is not None:
|
|
180
|
+
return line_handler.create_artists(
|
|
181
|
+
legend, data_line, xdescent, ydescent,
|
|
182
|
+
width, height, fontsize, trans
|
|
183
|
+
)
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
# Register the handler globally for ErrorbarContainer
|
|
187
|
+
mlegend.Legend.update_default_handler_map({ErrorbarContainer: SimpleLineHandler()})
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _postprocess_violin(result, ax, kwargs, args):
|
|
191
|
+
"""Apply styling for violin plots with optional boxplot overlay."""
|
|
192
|
+
# Get scitex palette for coloring
|
|
193
|
+
from scitex.plt.color._PARAMS import HEX
|
|
194
|
+
palette = [HEX['blue'], HEX['red'], HEX['green'], HEX['yellow'],
|
|
195
|
+
HEX['purple'], HEX['orange'], HEX['lightblue'], HEX['pink']]
|
|
196
|
+
|
|
197
|
+
if 'bodies' in result:
|
|
198
|
+
for i, body in enumerate(result['bodies']):
|
|
199
|
+
body.set_facecolor(palette[i % len(palette)])
|
|
200
|
+
body.set_edgecolor('black')
|
|
201
|
+
body.set_linewidth(mm_to_pt(DEFAULT_LINE_WIDTH_MM))
|
|
202
|
+
body.set_alpha(1.0)
|
|
203
|
+
|
|
204
|
+
# Add boxplot overlay by default (disable with boxplot=False)
|
|
205
|
+
add_boxplot = kwargs.pop('boxplot', True)
|
|
206
|
+
if add_boxplot and args:
|
|
207
|
+
try:
|
|
208
|
+
# Get data from first positional argument
|
|
209
|
+
data = args[0]
|
|
210
|
+
# Get positions if specified, otherwise use default
|
|
211
|
+
positions = kwargs.get('positions', None)
|
|
212
|
+
if positions is None:
|
|
213
|
+
positions = range(1, len(data) + 1)
|
|
214
|
+
|
|
215
|
+
# Calculate boxplot width dynamically from violin width
|
|
216
|
+
# Get violin width from kwargs or use matplotlib default (0.5)
|
|
217
|
+
violin_widths = kwargs.get('widths', 0.5)
|
|
218
|
+
if hasattr(violin_widths, '__iter__'):
|
|
219
|
+
violin_widths = violin_widths[0] if len(violin_widths) > 0 else 0.5
|
|
220
|
+
# Boxplot width = 20% of violin width
|
|
221
|
+
boxplot_widths = violin_widths * 0.2
|
|
222
|
+
|
|
223
|
+
# Draw boxplot overlay with styling
|
|
224
|
+
line_width = mm_to_pt(DEFAULT_LINE_WIDTH_MM)
|
|
225
|
+
marker_size = mm_to_pt(DEFAULT_MARKER_SIZE_MM)
|
|
226
|
+
|
|
227
|
+
# Call matplotlib's boxplot directly to avoid recursive post-processing
|
|
228
|
+
# which would override our gray styling with the default blue
|
|
229
|
+
if hasattr(ax, '_axes_mpl'):
|
|
230
|
+
mpl_ax = ax._axes_mpl
|
|
231
|
+
else:
|
|
232
|
+
mpl_ax = ax
|
|
233
|
+
bp = mpl_ax.boxplot(
|
|
234
|
+
data,
|
|
235
|
+
positions=list(positions),
|
|
236
|
+
widths=boxplot_widths,
|
|
237
|
+
patch_artist=True,
|
|
238
|
+
manage_ticks=False, # Don't modify existing ticks
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Style the boxplot: scitex gray fill with black edges for visibility
|
|
242
|
+
# Set high z-order so boxplot appears on top of violin bodies
|
|
243
|
+
from scitex.plt.color._PARAMS import HEX
|
|
244
|
+
boxplot_zorder = 10
|
|
245
|
+
for box in bp.get('boxes', []):
|
|
246
|
+
box.set_facecolor(HEX['gray']) # Scitex gray fill
|
|
247
|
+
box.set_edgecolor('black')
|
|
248
|
+
box.set_alpha(1.0)
|
|
249
|
+
box.set_linewidth(line_width)
|
|
250
|
+
box.set_zorder(boxplot_zorder)
|
|
251
|
+
for median in bp.get('medians', []):
|
|
252
|
+
median.set_color('black') # Black median line
|
|
253
|
+
median.set_linewidth(line_width) # 0.2mm thickness
|
|
254
|
+
median.set_zorder(boxplot_zorder + 1)
|
|
255
|
+
for whisker in bp.get('whiskers', []):
|
|
256
|
+
whisker.set_color('black')
|
|
257
|
+
whisker.set_linewidth(line_width)
|
|
258
|
+
whisker.set_zorder(boxplot_zorder)
|
|
259
|
+
for cap in bp.get('caps', []):
|
|
260
|
+
cap.set_color('black')
|
|
261
|
+
cap.set_linewidth(line_width)
|
|
262
|
+
cap.set_zorder(boxplot_zorder)
|
|
263
|
+
for flier in bp.get('fliers', []):
|
|
264
|
+
flier.set_markerfacecolor('none') # No fill (open circles)
|
|
265
|
+
flier.set_markeredgecolor('black')
|
|
266
|
+
flier.set_markersize(marker_size) # 0.8mm
|
|
267
|
+
flier.set_markeredgewidth(line_width) # 0.2mm
|
|
268
|
+
flier.set_zorder(boxplot_zorder + 2)
|
|
269
|
+
except Exception:
|
|
270
|
+
pass # Silently continue if boxplot overlay fails
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _postprocess_boxplot(result, ax):
|
|
274
|
+
"""Apply styling for boxplots (standalone, not violin overlay)."""
|
|
275
|
+
# Use the centralized style_boxplot function for consistent styling
|
|
276
|
+
from scitex.plt.ax import style_boxplot
|
|
277
|
+
style_boxplot(result)
|
|
278
|
+
|
|
279
|
+
# Cap width: 33% of box width
|
|
280
|
+
if 'caps' in result and 'boxes' in result and len(result['boxes']) > 0:
|
|
281
|
+
try:
|
|
282
|
+
cap_width_pts = _calculate_cap_width_from_box(result['boxes'][0], ax)
|
|
283
|
+
for cap in result['caps']:
|
|
284
|
+
cap.set_markersize(cap_width_pts)
|
|
285
|
+
except Exception:
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _postprocess_scatter(result, kwargs):
|
|
290
|
+
"""Apply styling for scatter plots."""
|
|
291
|
+
# Apply default 0.8mm marker size if 's' not specified
|
|
292
|
+
if 's' not in kwargs:
|
|
293
|
+
size_pt = mm_to_pt(DEFAULT_MARKER_SIZE_MM)
|
|
294
|
+
marker_area = size_pt ** 2
|
|
295
|
+
result.set_sizes([marker_area])
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _postprocess_hist(result, ax):
|
|
299
|
+
"""Apply styling for histogram plots.
|
|
300
|
+
|
|
301
|
+
Ensures histogram bars have proper edge color and alpha for visibility.
|
|
302
|
+
"""
|
|
303
|
+
line_width = mm_to_pt(DEFAULT_LINE_WIDTH_MM)
|
|
304
|
+
|
|
305
|
+
# result is (n, bins, patches) tuple
|
|
306
|
+
if len(result) >= 3:
|
|
307
|
+
patches = result[2]
|
|
308
|
+
# Handle both single histogram and stacked histograms
|
|
309
|
+
if hasattr(patches, '__iter__'):
|
|
310
|
+
for patch_group in patches:
|
|
311
|
+
if hasattr(patch_group, '__iter__'):
|
|
312
|
+
for patch in patch_group:
|
|
313
|
+
patch.set_edgecolor('black')
|
|
314
|
+
patch.set_linewidth(line_width)
|
|
315
|
+
# Ensure alpha is at least 0.7 for visibility
|
|
316
|
+
if patch.get_alpha() is None or patch.get_alpha() < 0.7:
|
|
317
|
+
patch.set_alpha(1.0)
|
|
318
|
+
else:
|
|
319
|
+
# Single patch
|
|
320
|
+
patch_group.set_edgecolor('black')
|
|
321
|
+
patch_group.set_linewidth(line_width)
|
|
322
|
+
if patch_group.get_alpha() is None or patch_group.get_alpha() < 0.7:
|
|
323
|
+
patch_group.set_alpha(1.0)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _postprocess_fill_between(result, kwargs):
|
|
327
|
+
"""Apply styling for fill_between plots.
|
|
328
|
+
|
|
329
|
+
Ensures shaded regions have proper alpha for visibility.
|
|
330
|
+
"""
|
|
331
|
+
# result is a PolyCollection
|
|
332
|
+
if result is not None:
|
|
333
|
+
# Set edge color to match face color or black
|
|
334
|
+
line_width = mm_to_pt(DEFAULT_LINE_WIDTH_MM)
|
|
335
|
+
|
|
336
|
+
# Only set edge if not already specified
|
|
337
|
+
if 'edgecolor' not in kwargs and 'ec' not in kwargs:
|
|
338
|
+
result.set_edgecolor('none')
|
|
339
|
+
|
|
340
|
+
# Ensure alpha is reasonable (default 0.3 is common for fill_between)
|
|
341
|
+
if 'alpha' not in kwargs:
|
|
342
|
+
result.set_alpha(0.3)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _postprocess_bar(result, ax, kwargs):
|
|
346
|
+
"""Apply styling for bar plots with colors and error bars."""
|
|
347
|
+
# Get scitex palette for coloring (only if color not explicitly set)
|
|
348
|
+
if 'color' not in kwargs and 'c' not in kwargs:
|
|
349
|
+
from scitex.plt.color._PARAMS import HEX
|
|
350
|
+
palette = [HEX['blue'], HEX['red'], HEX['green'], HEX['yellow'],
|
|
351
|
+
HEX['purple'], HEX['orange'], HEX['lightblue'], HEX['pink']]
|
|
352
|
+
|
|
353
|
+
line_width = mm_to_pt(DEFAULT_LINE_WIDTH_MM)
|
|
354
|
+
for i, patch in enumerate(result.patches):
|
|
355
|
+
patch.set_facecolor(palette[i % len(palette)])
|
|
356
|
+
patch.set_edgecolor('black')
|
|
357
|
+
patch.set_linewidth(line_width)
|
|
358
|
+
|
|
359
|
+
if 'yerr' not in kwargs or kwargs['yerr'] is None:
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
errorbar = result.errorbar
|
|
364
|
+
if errorbar is None:
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
lines = errorbar.lines
|
|
368
|
+
if not lines or len(lines) < 3:
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
caplines = lines[1]
|
|
372
|
+
if caplines and len(caplines) >= 2:
|
|
373
|
+
# Hide lower caps (one-sided error bars)
|
|
374
|
+
caplines[0].set_visible(False)
|
|
375
|
+
|
|
376
|
+
# Adjust cap width to 33% of bar width
|
|
377
|
+
if len(result.patches) > 0:
|
|
378
|
+
cap_width_pts = _calculate_cap_width_from_bar(
|
|
379
|
+
result.patches[0], ax, 'width'
|
|
380
|
+
)
|
|
381
|
+
for cap in caplines[1:]:
|
|
382
|
+
cap.set_markersize(cap_width_pts)
|
|
383
|
+
|
|
384
|
+
# Make error bar lines one-sided
|
|
385
|
+
barlinecols = lines[2]
|
|
386
|
+
_make_errorbar_one_sided(barlinecols, 'vertical')
|
|
387
|
+
except Exception:
|
|
388
|
+
pass
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _postprocess_barh(result, ax, kwargs):
|
|
392
|
+
"""Apply styling for horizontal bar plots with colors and error bars."""
|
|
393
|
+
# Get scitex palette for coloring (only if color not explicitly set)
|
|
394
|
+
if 'color' not in kwargs and 'c' not in kwargs:
|
|
395
|
+
from scitex.plt.color._PARAMS import HEX
|
|
396
|
+
palette = [HEX['blue'], HEX['red'], HEX['green'], HEX['yellow'],
|
|
397
|
+
HEX['purple'], HEX['orange'], HEX['lightblue'], HEX['pink']]
|
|
398
|
+
|
|
399
|
+
line_width = mm_to_pt(DEFAULT_LINE_WIDTH_MM)
|
|
400
|
+
for i, patch in enumerate(result.patches):
|
|
401
|
+
patch.set_facecolor(palette[i % len(palette)])
|
|
402
|
+
patch.set_edgecolor('black')
|
|
403
|
+
patch.set_linewidth(line_width)
|
|
404
|
+
|
|
405
|
+
if 'xerr' not in kwargs or kwargs['xerr'] is None:
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
errorbar = result.errorbar
|
|
410
|
+
if errorbar is None:
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
lines = errorbar.lines
|
|
414
|
+
if not lines or len(lines) < 3:
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
caplines = lines[1]
|
|
418
|
+
if caplines and len(caplines) >= 2:
|
|
419
|
+
# Hide left caps (one-sided error bars)
|
|
420
|
+
caplines[0].set_visible(False)
|
|
421
|
+
|
|
422
|
+
# Adjust cap width to 33% of bar height
|
|
423
|
+
if len(result.patches) > 0:
|
|
424
|
+
cap_width_pts = _calculate_cap_width_from_bar(
|
|
425
|
+
result.patches[0], ax, 'height'
|
|
426
|
+
)
|
|
427
|
+
for cap in caplines[1:]:
|
|
428
|
+
cap.set_markersize(cap_width_pts)
|
|
429
|
+
|
|
430
|
+
# Make error bar lines one-sided
|
|
431
|
+
barlinecols = lines[2]
|
|
432
|
+
_make_errorbar_one_sided(barlinecols, 'horizontal')
|
|
433
|
+
except Exception:
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# ============================================================================
|
|
438
|
+
# Helper functions
|
|
439
|
+
# ============================================================================
|
|
440
|
+
def _calculate_cap_width_from_box(box, ax):
|
|
441
|
+
"""Calculate cap width as 33% of box width in points."""
|
|
442
|
+
# Get box width from path
|
|
443
|
+
if hasattr(box, 'get_path'):
|
|
444
|
+
path = box.get_path()
|
|
445
|
+
vertices = path.vertices
|
|
446
|
+
x_coords = vertices[:, 0]
|
|
447
|
+
box_width_data = x_coords.max() - x_coords.min()
|
|
448
|
+
elif hasattr(box, 'get_xdata'):
|
|
449
|
+
x_data = box.get_xdata()
|
|
450
|
+
box_width_data = max(x_data) - min(x_data)
|
|
451
|
+
else:
|
|
452
|
+
box_width_data = 0.5 # Default
|
|
453
|
+
|
|
454
|
+
return _data_width_to_points(box_width_data, ax, 'x') * CAP_WIDTH_RATIO
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _calculate_cap_width_from_bar(patch, ax, dimension):
|
|
458
|
+
"""Calculate cap width as 33% of bar width/height in points."""
|
|
459
|
+
if dimension == 'width':
|
|
460
|
+
bar_size = patch.get_width()
|
|
461
|
+
return _data_width_to_points(bar_size, ax, 'x') * CAP_WIDTH_RATIO
|
|
462
|
+
else: # height
|
|
463
|
+
bar_size = patch.get_height()
|
|
464
|
+
return _data_width_to_points(bar_size, ax, 'y') * CAP_WIDTH_RATIO
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _data_width_to_points(data_size, ax, axis='x'):
|
|
468
|
+
"""Convert a data-space size to points."""
|
|
469
|
+
fig = ax.get_figure()
|
|
470
|
+
bbox = ax.get_position()
|
|
471
|
+
|
|
472
|
+
if axis == 'x':
|
|
473
|
+
ax_size_inches = bbox.width * fig.get_figwidth()
|
|
474
|
+
lim = ax.get_xlim()
|
|
475
|
+
else:
|
|
476
|
+
ax_size_inches = bbox.height * fig.get_figheight()
|
|
477
|
+
lim = ax.get_ylim()
|
|
478
|
+
|
|
479
|
+
data_range = lim[1] - lim[0]
|
|
480
|
+
size_inches = (data_size / data_range) * ax_size_inches
|
|
481
|
+
return size_inches * 72 # 72 points per inch
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _make_errorbar_one_sided(barlinecols, direction):
|
|
485
|
+
"""Make error bar line segments one-sided (outward only)."""
|
|
486
|
+
if not barlinecols or len(barlinecols) == 0:
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
for lc in barlinecols:
|
|
490
|
+
if not hasattr(lc, 'get_segments'):
|
|
491
|
+
continue
|
|
492
|
+
|
|
493
|
+
segs = lc.get_segments()
|
|
494
|
+
new_segs = []
|
|
495
|
+
for seg in segs:
|
|
496
|
+
if len(seg) < 2:
|
|
497
|
+
continue
|
|
498
|
+
|
|
499
|
+
if direction == 'vertical':
|
|
500
|
+
# Keep upper half
|
|
501
|
+
bottom_y = min(seg[0][1], seg[1][1])
|
|
502
|
+
top_y = max(seg[0][1], seg[1][1])
|
|
503
|
+
mid_y = (bottom_y + top_y) / 2
|
|
504
|
+
new_seg = np.array([[seg[0][0], mid_y], [seg[0][0], top_y]])
|
|
505
|
+
else: # horizontal
|
|
506
|
+
# Keep right half
|
|
507
|
+
left_x = min(seg[0][0], seg[1][0])
|
|
508
|
+
right_x = max(seg[0][0], seg[1][0])
|
|
509
|
+
mid_x = (left_x + right_x) / 2
|
|
510
|
+
new_seg = np.array([[mid_x, seg[0][1]], [right_x, seg[0][1]]])
|
|
511
|
+
|
|
512
|
+
new_segs.append(new_seg)
|
|
513
|
+
|
|
514
|
+
if new_segs:
|
|
515
|
+
lc.set_segments(new_segs)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# EOF
|