scitex 2.3.0__py3-none-any.whl → 2.4.1__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 +559 -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.1.dist-info}/METADATA +2 -1
- {scitex-2.3.0.dist-info → scitex-2.4.1.dist-info}/RECORD +94 -67
- {scitex-2.3.0.dist-info → scitex-2.4.1.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.1.dist-info}/entry_points.txt +0 -0
- {scitex-2.3.0.dist-info → scitex-2.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -168,16 +168,25 @@ def collect_figure_metadata(fig, ax=None, plot_id=None) -> Dict:
|
|
|
168
168
|
if ax is not None and hasattr(ax, "_scitex_metadata"):
|
|
169
169
|
scitex_meta = ax._scitex_metadata
|
|
170
170
|
|
|
171
|
+
# Extract stats separately for top-level access
|
|
172
|
+
if 'stats' in scitex_meta:
|
|
173
|
+
metadata['stats'] = scitex_meta['stats']
|
|
174
|
+
|
|
171
175
|
# Merge into scitex section
|
|
172
176
|
for key, value in scitex_meta.items():
|
|
173
|
-
if key not in metadata["scitex"]:
|
|
177
|
+
if key not in metadata["scitex"] and key != 'stats': # Don't duplicate stats
|
|
174
178
|
metadata["scitex"][key] = value
|
|
175
179
|
|
|
176
180
|
# Alternative: check figure for metadata (for multi-axes cases)
|
|
177
181
|
elif hasattr(fig, "_scitex_metadata"):
|
|
178
182
|
scitex_meta = fig._scitex_metadata
|
|
183
|
+
|
|
184
|
+
# Extract stats separately for top-level access
|
|
185
|
+
if 'stats' in scitex_meta:
|
|
186
|
+
metadata['stats'] = scitex_meta['stats']
|
|
187
|
+
|
|
179
188
|
for key, value in scitex_meta.items():
|
|
180
|
-
if key not in metadata["scitex"]:
|
|
189
|
+
if key not in metadata["scitex"] and key != 'stats': # Don't duplicate stats
|
|
181
190
|
metadata["scitex"][key] = value
|
|
182
191
|
|
|
183
192
|
# Add actual font information
|
|
@@ -228,6 +237,7 @@ def collect_figure_metadata(fig, ax=None, plot_id=None) -> Dict:
|
|
|
228
237
|
"label": x_label,
|
|
229
238
|
"unit": x_unit,
|
|
230
239
|
"scale": ax.get_xscale(),
|
|
240
|
+
"lim": list(ax.get_xlim()),
|
|
231
241
|
}
|
|
232
242
|
|
|
233
243
|
# Y-axis
|
|
@@ -237,6 +247,7 @@ def collect_figure_metadata(fig, ax=None, plot_id=None) -> Dict:
|
|
|
237
247
|
"label": y_label,
|
|
238
248
|
"unit": y_unit,
|
|
239
249
|
"scale": ax.get_yscale(),
|
|
250
|
+
"lim": list(ax.get_ylim()),
|
|
240
251
|
}
|
|
241
252
|
|
|
242
253
|
# Add n_ticks if available from style
|
|
@@ -248,6 +259,11 @@ def collect_figure_metadata(fig, ax=None, plot_id=None) -> Dict:
|
|
|
248
259
|
|
|
249
260
|
metadata["axes"] = axes_info
|
|
250
261
|
|
|
262
|
+
# Extract title
|
|
263
|
+
title = ax.get_title()
|
|
264
|
+
if title:
|
|
265
|
+
metadata["title"] = title
|
|
266
|
+
|
|
251
267
|
# Detect plot type and method from axes history or lines
|
|
252
268
|
plot_type, method = _detect_plot_type(ax)
|
|
253
269
|
if plot_type:
|
|
@@ -261,6 +277,16 @@ def collect_figure_metadata(fig, ax=None, plot_id=None) -> Dict:
|
|
|
261
277
|
elif hasattr(fig, "_scitex_metadata") and "style_preset" in fig._scitex_metadata:
|
|
262
278
|
metadata["style_preset"] = fig._scitex_metadata["style_preset"]
|
|
263
279
|
|
|
280
|
+
# Phase 2: Extract traces (lines) with their properties and CSV column mapping
|
|
281
|
+
traces = _extract_traces(ax)
|
|
282
|
+
if traces:
|
|
283
|
+
metadata["traces"] = traces
|
|
284
|
+
|
|
285
|
+
# Phase 2: Extract legend info
|
|
286
|
+
legend_info = _extract_legend_info(ax)
|
|
287
|
+
if legend_info:
|
|
288
|
+
metadata["legend"] = legend_info
|
|
289
|
+
|
|
264
290
|
except Exception as e:
|
|
265
291
|
# If Phase 1 extraction fails, continue without it
|
|
266
292
|
import warnings
|
|
@@ -307,6 +333,122 @@ def _parse_label_unit(label_text: str) -> tuple:
|
|
|
307
333
|
return label_text.strip(), ""
|
|
308
334
|
|
|
309
335
|
|
|
336
|
+
def _extract_traces(ax) -> list:
|
|
337
|
+
"""
|
|
338
|
+
Extract trace (line) information including properties and CSV column mapping.
|
|
339
|
+
|
|
340
|
+
Parameters
|
|
341
|
+
----------
|
|
342
|
+
ax : matplotlib.axes.Axes
|
|
343
|
+
The axes to extract traces from
|
|
344
|
+
|
|
345
|
+
Returns
|
|
346
|
+
-------
|
|
347
|
+
list
|
|
348
|
+
List of trace dictionaries with id, label, color, linestyle, linewidth,
|
|
349
|
+
and csv_columns mapping
|
|
350
|
+
"""
|
|
351
|
+
import matplotlib.colors as mcolors
|
|
352
|
+
|
|
353
|
+
traces = []
|
|
354
|
+
|
|
355
|
+
# Get axes position for CSV column naming
|
|
356
|
+
ax_pos = "00" # Default for single axes
|
|
357
|
+
if hasattr(ax, '_scitex_metadata') and 'position_in_grid' in ax._scitex_metadata:
|
|
358
|
+
pos = ax._scitex_metadata['position_in_grid']
|
|
359
|
+
ax_pos = f"{pos[0]}{pos[1]}"
|
|
360
|
+
|
|
361
|
+
for i, line in enumerate(ax.lines):
|
|
362
|
+
trace = {}
|
|
363
|
+
|
|
364
|
+
# Get ID from _scitex_id attribute (set by scitex plotting functions)
|
|
365
|
+
# This matches the id= kwarg passed to ax.plot()
|
|
366
|
+
scitex_id = getattr(line, '_scitex_id', None)
|
|
367
|
+
|
|
368
|
+
# Get label for legend
|
|
369
|
+
label = line.get_label()
|
|
370
|
+
|
|
371
|
+
# Determine trace_id for CSV column matching
|
|
372
|
+
if scitex_id:
|
|
373
|
+
trace_id = scitex_id
|
|
374
|
+
elif not label.startswith('_'):
|
|
375
|
+
trace_id = label
|
|
376
|
+
else:
|
|
377
|
+
trace_id = f"line_{i}"
|
|
378
|
+
|
|
379
|
+
trace["id"] = trace_id
|
|
380
|
+
|
|
381
|
+
# Label (for legend) - use label if not internal
|
|
382
|
+
if not label.startswith('_'):
|
|
383
|
+
trace["label"] = label
|
|
384
|
+
|
|
385
|
+
# Color - always convert to hex for consistent JSON storage
|
|
386
|
+
color = line.get_color()
|
|
387
|
+
try:
|
|
388
|
+
# mcolors.to_hex handles strings, RGB tuples, RGBA tuples
|
|
389
|
+
color_hex = mcolors.to_hex(color, keep_alpha=False)
|
|
390
|
+
trace["color"] = color_hex
|
|
391
|
+
except (ValueError, TypeError):
|
|
392
|
+
# Fallback: store as-is
|
|
393
|
+
trace["color"] = color
|
|
394
|
+
|
|
395
|
+
# Line style
|
|
396
|
+
trace["linestyle"] = line.get_linestyle()
|
|
397
|
+
|
|
398
|
+
# Line width
|
|
399
|
+
trace["linewidth"] = line.get_linewidth()
|
|
400
|
+
|
|
401
|
+
# Marker
|
|
402
|
+
marker = line.get_marker()
|
|
403
|
+
if marker and marker != 'None':
|
|
404
|
+
trace["marker"] = marker
|
|
405
|
+
trace["markersize"] = line.get_markersize()
|
|
406
|
+
|
|
407
|
+
# CSV column mapping - this is how we'll reconstruct from CSV
|
|
408
|
+
# Format matches what _export_as_csv generates: ax_{row}{col}_{id}_plot_x/y
|
|
409
|
+
# The id should match the id= kwarg passed to ax.plot()
|
|
410
|
+
trace["csv_columns"] = {
|
|
411
|
+
"x": f"ax_{ax_pos}_{trace_id}_plot_x",
|
|
412
|
+
"y": f"ax_{ax_pos}_{trace_id}_plot_y",
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
traces.append(trace)
|
|
416
|
+
|
|
417
|
+
return traces
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _extract_legend_info(ax) -> Optional[dict]:
|
|
421
|
+
"""
|
|
422
|
+
Extract legend information from axes.
|
|
423
|
+
|
|
424
|
+
Parameters
|
|
425
|
+
----------
|
|
426
|
+
ax : matplotlib.axes.Axes
|
|
427
|
+
The axes to extract legend from
|
|
428
|
+
|
|
429
|
+
Returns
|
|
430
|
+
-------
|
|
431
|
+
dict or None
|
|
432
|
+
Legend info dictionary or None if no legend
|
|
433
|
+
"""
|
|
434
|
+
legend = ax.get_legend()
|
|
435
|
+
if legend is None:
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
legend_info = {
|
|
439
|
+
"visible": legend.get_visible(),
|
|
440
|
+
"loc": legend._loc if hasattr(legend, '_loc') else "best",
|
|
441
|
+
"frameon": legend.get_frame_on() if hasattr(legend, 'get_frame_on') else True,
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
# Extract legend entries (labels)
|
|
445
|
+
texts = legend.get_texts()
|
|
446
|
+
if texts:
|
|
447
|
+
legend_info["labels"] = [t.get_text() for t in texts]
|
|
448
|
+
|
|
449
|
+
return legend_info
|
|
450
|
+
|
|
451
|
+
|
|
310
452
|
def _detect_plot_type(ax) -> tuple:
|
|
311
453
|
"""
|
|
312
454
|
Detect the primary plot type and method from axes content.
|
|
@@ -337,24 +479,24 @@ def _detect_plot_type(ax) -> tuple:
|
|
|
337
479
|
if hasattr(ax, 'history') and len(ax.history) > 0:
|
|
338
480
|
# Get the first plotting command
|
|
339
481
|
first_cmd = ax.history[0].get('command', '')
|
|
340
|
-
if '
|
|
341
|
-
return "heatmap", "
|
|
342
|
-
elif '
|
|
343
|
-
return "kde", "
|
|
344
|
-
elif '
|
|
345
|
-
return "ecdf", "
|
|
346
|
-
elif '
|
|
347
|
-
return "violin", "
|
|
348
|
-
elif '
|
|
349
|
-
return "boxplot", "
|
|
350
|
-
elif '
|
|
351
|
-
return "line", "
|
|
482
|
+
if 'stx_heatmap' in first_cmd:
|
|
483
|
+
return "heatmap", "stx_heatmap"
|
|
484
|
+
elif 'stx_kde' in first_cmd:
|
|
485
|
+
return "kde", "stx_kde"
|
|
486
|
+
elif 'stx_ecdf' in first_cmd:
|
|
487
|
+
return "ecdf", "stx_ecdf"
|
|
488
|
+
elif 'stx_violin' in first_cmd:
|
|
489
|
+
return "violin", "stx_violin"
|
|
490
|
+
elif 'stx_box' in first_cmd or 'boxplot' in first_cmd:
|
|
491
|
+
return "boxplot", "stx_box"
|
|
492
|
+
elif 'stx_line' in first_cmd:
|
|
493
|
+
return "line", "stx_line"
|
|
352
494
|
elif 'plot_scatter' in first_cmd:
|
|
353
495
|
return "scatter", "plot_scatter"
|
|
354
|
-
elif '
|
|
355
|
-
return "line", "
|
|
356
|
-
elif '
|
|
357
|
-
return "line", "
|
|
496
|
+
elif 'stx_mean_std' in first_cmd:
|
|
497
|
+
return "line", "stx_mean_std"
|
|
498
|
+
elif 'stx_shaded_line' in first_cmd:
|
|
499
|
+
return "line", "stx_shaded_line"
|
|
358
500
|
elif 'sns_boxplot' in first_cmd:
|
|
359
501
|
return "boxplot", "sns_boxplot"
|
|
360
502
|
elif 'sns_violinplot' in first_cmd:
|
scitex/plt/utils/_colorbar.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
|
-
# Timestamp: "2025-
|
|
3
|
+
# Timestamp: "2025-12-01 10:00:00 (ywatanabe)"
|
|
4
4
|
# File: /src/scitex/plt/utils/_colorbar.py
|
|
5
5
|
# ----------------------------------------
|
|
6
6
|
|
|
@@ -10,49 +10,108 @@ import matplotlib.pyplot as plt
|
|
|
10
10
|
from matplotlib.cm import ScalarMappable
|
|
11
11
|
from matplotlib.colors import Normalize
|
|
12
12
|
|
|
13
|
+
from ._units import mm_to_pt
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
# ============================================================================
|
|
17
|
+
# Constants for colorbar styling
|
|
18
|
+
# ============================================================================
|
|
19
|
+
COLORBAR_LINE_WIDTH_MM = 0.2
|
|
20
|
+
COLORBAR_TICK_LENGTH_MM = 0.8
|
|
21
|
+
COLORBAR_TICK_FONTSIZE = 6 # pt
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def style_colorbar(cbar):
|
|
25
|
+
"""Apply publication-quality styling to a colorbar.
|
|
26
|
+
|
|
27
|
+
Applies:
|
|
28
|
+
- 0.2mm outline thickness
|
|
29
|
+
- 0.8mm tick length
|
|
30
|
+
- 6pt tick labels
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
cbar : matplotlib.colorbar.Colorbar
|
|
35
|
+
The colorbar to style
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
cbar : matplotlib.colorbar.Colorbar
|
|
40
|
+
The styled colorbar
|
|
41
|
+
"""
|
|
42
|
+
line_width = mm_to_pt(COLORBAR_LINE_WIDTH_MM)
|
|
43
|
+
tick_length = mm_to_pt(COLORBAR_TICK_LENGTH_MM)
|
|
44
|
+
|
|
45
|
+
# Style the colorbar outline
|
|
46
|
+
cbar.outline.set_linewidth(line_width)
|
|
47
|
+
|
|
48
|
+
# Style the ticks
|
|
49
|
+
cbar.ax.tick_params(
|
|
50
|
+
width=line_width,
|
|
51
|
+
length=tick_length,
|
|
52
|
+
labelsize=COLORBAR_TICK_FONTSIZE
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Style the colorbar axis spines
|
|
56
|
+
for spine in cbar.ax.spines.values():
|
|
57
|
+
spine.set_linewidth(line_width)
|
|
58
|
+
|
|
59
|
+
return cbar
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def colorbar(mappable, ax=None, n_ticks=4, **kwargs):
|
|
15
63
|
"""Enhanced colorbar function that ensures proper spacing.
|
|
16
|
-
|
|
64
|
+
|
|
17
65
|
This function wraps matplotlib.pyplot.colorbar with better defaults
|
|
18
66
|
to prevent overlap with axes when using constrained_layout.
|
|
19
|
-
|
|
67
|
+
|
|
20
68
|
Parameters
|
|
21
69
|
----------
|
|
22
70
|
mappable : matplotlib.cm.ScalarMappable
|
|
23
71
|
The mappable whose colorbar is to be made (e.g., from imshow, scatter)
|
|
24
72
|
ax : matplotlib.axes.Axes or list of Axes, optional
|
|
25
73
|
Parent axes from which space for a new colorbar axes will be stolen.
|
|
74
|
+
n_ticks : int, optional
|
|
75
|
+
Number of ticks on the colorbar. Default is 4 to match main axes style.
|
|
26
76
|
**kwargs : dict
|
|
27
77
|
Additional keyword arguments passed to matplotlib.pyplot.colorbar
|
|
28
|
-
|
|
78
|
+
|
|
29
79
|
Returns
|
|
30
80
|
-------
|
|
31
81
|
colorbar : matplotlib.colorbar.Colorbar
|
|
32
82
|
The colorbar instance
|
|
33
83
|
"""
|
|
84
|
+
from matplotlib.ticker import MaxNLocator
|
|
85
|
+
|
|
34
86
|
# Set better defaults for colorbar placement
|
|
35
87
|
defaults = {
|
|
36
88
|
'fraction': 0.046, # Fraction of axes to use for colorbar
|
|
37
89
|
'pad': 0.04, # Padding between axes and colorbar
|
|
38
90
|
'aspect': 20, # Aspect ratio of colorbar
|
|
39
91
|
}
|
|
40
|
-
|
|
92
|
+
|
|
41
93
|
# Update defaults with any user-provided kwargs
|
|
42
94
|
for key, value in defaults.items():
|
|
43
95
|
if key not in kwargs:
|
|
44
96
|
kwargs[key] = value
|
|
45
|
-
|
|
97
|
+
|
|
46
98
|
# Create the colorbar
|
|
47
99
|
cbar = plt.colorbar(mappable, ax=ax, **kwargs)
|
|
48
|
-
|
|
100
|
+
|
|
101
|
+
# Limit number of ticks to match main axes style (3-4 ticks)
|
|
102
|
+
cbar.locator = MaxNLocator(nbins=n_ticks, min_n_ticks=2, prune='both')
|
|
103
|
+
cbar.update_ticks()
|
|
104
|
+
|
|
105
|
+
# Apply publication-quality styling
|
|
106
|
+
style_colorbar(cbar)
|
|
107
|
+
|
|
49
108
|
# If using constrained_layout, ensure the figure updates
|
|
50
109
|
if ax is not None:
|
|
51
110
|
fig = ax.figure if hasattr(ax, 'figure') else ax[0].figure
|
|
52
111
|
if hasattr(fig, 'get_constrained_layout') and fig.get_constrained_layout():
|
|
53
112
|
# Force a layout update
|
|
54
113
|
fig.canvas.draw_idle()
|
|
55
|
-
|
|
114
|
+
|
|
56
115
|
return cbar
|
|
57
116
|
|
|
58
117
|
|
|
@@ -89,7 +148,10 @@ def add_shared_colorbar(fig, axes, mappable, location='right', **kwargs):
|
|
|
89
148
|
|
|
90
149
|
# Create the shared colorbar
|
|
91
150
|
cbar = fig.colorbar(mappable, ax=axes, location=location, **kwargs)
|
|
92
|
-
|
|
151
|
+
|
|
152
|
+
# Apply publication-quality styling
|
|
153
|
+
style_colorbar(cbar)
|
|
154
|
+
|
|
93
155
|
return cbar
|
|
94
156
|
|
|
95
157
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
|
-
# Timestamp: "2025-
|
|
3
|
+
# Timestamp: "2025-12-02 12:00:00 (ywatanabe)"
|
|
4
4
|
# File: /home/ywatanabe/proj/scitex-code/src/scitex/plt/utils/_configure_mpl.py
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
from typing import Dict
|
|
9
|
+
from typing import Optional
|
|
9
10
|
from typing import Tuple
|
|
10
11
|
|
|
11
12
|
import matplotlib.pyplot as plt
|
|
@@ -16,51 +17,48 @@ from scitex.dict import DotDict
|
|
|
16
17
|
|
|
17
18
|
def configure_mpl(
|
|
18
19
|
plt,
|
|
19
|
-
fig_size_mm
|
|
20
|
-
fig_scale=1.0,
|
|
21
|
-
dpi_display=
|
|
22
|
-
dpi_save=
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
verbose=False,
|
|
20
|
+
fig_size_mm: Optional[Tuple[float, float]] = None,
|
|
21
|
+
fig_scale: float = 1.0,
|
|
22
|
+
dpi_display: Optional[int] = None,
|
|
23
|
+
dpi_save: Optional[int] = None,
|
|
24
|
+
autolayout: bool = True,
|
|
25
|
+
n_ticks: Optional[int] = None,
|
|
26
|
+
hide_top_right_spines: Optional[bool] = None,
|
|
27
|
+
line_width: Optional[float] = None,
|
|
28
|
+
alpha: float = 1.0,
|
|
29
|
+
enable_latex: bool = False,
|
|
30
|
+
latex_preamble: Optional[str] = None,
|
|
31
|
+
verbose: bool = False,
|
|
32
32
|
**kwargs,
|
|
33
33
|
) -> Tuple[Any, Dict]:
|
|
34
34
|
"""Configures Matplotlib settings for publication-quality plots.
|
|
35
35
|
|
|
36
|
+
All default values are loaded from SCITEX_STYLE.yaml. Parameters passed
|
|
37
|
+
directly to this function override the YAML values.
|
|
38
|
+
|
|
36
39
|
Parameters
|
|
37
40
|
----------
|
|
38
41
|
plt : matplotlib.pyplot
|
|
39
42
|
Matplotlib pyplot module
|
|
40
|
-
fig_size_mm : tuple of
|
|
41
|
-
Figure width and height in millimeters
|
|
43
|
+
fig_size_mm : tuple of float, optional
|
|
44
|
+
Figure width and height in millimeters. If None, calculated from
|
|
45
|
+
YAML axes dimensions + margins.
|
|
42
46
|
fig_scale : float, optional
|
|
43
47
|
Scaling factor for figure size, by default 1.0
|
|
44
48
|
dpi_display : int, optional
|
|
45
|
-
Display resolution in DPI,
|
|
49
|
+
Display resolution in DPI. If None, uses YAML output.dpi / 3.
|
|
46
50
|
dpi_save : int, optional
|
|
47
|
-
Saving resolution in DPI,
|
|
48
|
-
# fontsize : Union[str, int, float], optional
|
|
49
|
-
# Base font size ('xx-small' to 'xx-large' or points), by default 'medium'
|
|
50
|
-
# Other sizes are derived from this:
|
|
51
|
-
# - Title: 125% of base
|
|
52
|
-
# - Labels: 100% of base
|
|
53
|
-
# - Ticks/Legend: 85% of base
|
|
51
|
+
Saving resolution in DPI. If None, uses YAML output.dpi.
|
|
54
52
|
autolayout : bool, optional
|
|
55
53
|
Whether to enable automatic tight layout, by default True
|
|
56
54
|
hide_top_right_spines : bool, optional
|
|
57
|
-
Whether to hide top and right spines,
|
|
55
|
+
Whether to hide top and right spines. If None, uses YAML behavior settings.
|
|
58
56
|
line_width : float, optional
|
|
59
|
-
Default line width,
|
|
57
|
+
Default line width in points. If None, converts YAML lines.trace_mm to pt.
|
|
60
58
|
alpha : float, optional
|
|
61
|
-
Color transparency, by default 0
|
|
59
|
+
Color transparency, by default 1.0
|
|
62
60
|
n_ticks : int, optional
|
|
63
|
-
Number of ticks on each axis,
|
|
61
|
+
Number of ticks on each axis. If None, uses YAML ticks.n_ticks.
|
|
64
62
|
verbose : bool, optional
|
|
65
63
|
Whether to print configuration details, by default False
|
|
66
64
|
|
|
@@ -68,14 +66,72 @@ def configure_mpl(
|
|
|
68
66
|
-------
|
|
69
67
|
tuple
|
|
70
68
|
(plt, DotDict of RGBA colors) - Access as COLORS.blue or COLORS['blue']
|
|
71
|
-
"""
|
|
72
|
-
# # Convert base font size
|
|
73
|
-
# base_size = _convert_font_size(fontsize)
|
|
74
69
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
Notes
|
|
71
|
+
-----
|
|
72
|
+
Style values are resolved from SCITEX_STYLE.yaml located at:
|
|
73
|
+
scitex/plt/styles/SCITEX_STYLE.yaml
|
|
74
|
+
|
|
75
|
+
The YAML file contains all default values for:
|
|
76
|
+
- Axes dimensions (width_mm, height_mm, thickness_mm)
|
|
77
|
+
- Margins and spacing
|
|
78
|
+
- Font sizes (axis_label_pt, tick_label_pt, title_pt, legend_pt)
|
|
79
|
+
- Line thicknesses (trace_mm, errorbar_mm, etc.)
|
|
80
|
+
- Tick settings (length_mm, thickness_mm, direction, n_ticks)
|
|
81
|
+
- Output settings (dpi, transparent)
|
|
82
|
+
- Behavior flags (hide_top_spine, hide_right_spine, grid)
|
|
83
|
+
"""
|
|
84
|
+
# Load style from YAML
|
|
85
|
+
from scitex.plt.styles import load_style, resolve_style_value
|
|
86
|
+
|
|
87
|
+
style = load_style()
|
|
88
|
+
|
|
89
|
+
# mm to pt conversion factor
|
|
90
|
+
mm_to_pt = 2.83465
|
|
91
|
+
|
|
92
|
+
# Resolve values with priority: direct → env → yaml → default
|
|
93
|
+
# If parameter is None, use YAML value; otherwise use the passed value
|
|
94
|
+
|
|
95
|
+
# Figure size: calculate from axes + margins if not specified
|
|
96
|
+
if fig_size_mm is None:
|
|
97
|
+
axes_w = resolve_style_value("axes.width_mm", None, 40)
|
|
98
|
+
axes_h = resolve_style_value("axes.height_mm", None, 28)
|
|
99
|
+
margin_l = resolve_style_value("margins.left_mm", None, 20)
|
|
100
|
+
margin_r = resolve_style_value("margins.right_mm", None, 20)
|
|
101
|
+
margin_b = resolve_style_value("margins.bottom_mm", None, 20)
|
|
102
|
+
margin_t = resolve_style_value("margins.top_mm", None, 20)
|
|
103
|
+
fig_size_mm = (axes_w + margin_l + margin_r, axes_h + margin_b + margin_t)
|
|
104
|
+
|
|
105
|
+
# DPI
|
|
106
|
+
yaml_dpi = int(resolve_style_value("output.dpi", None, 300))
|
|
107
|
+
if dpi_save is None:
|
|
108
|
+
dpi_save = yaml_dpi
|
|
109
|
+
if dpi_display is None:
|
|
110
|
+
dpi_display = max(100, yaml_dpi // 3) # Lower DPI for display
|
|
111
|
+
|
|
112
|
+
# Line width: convert from mm to pt if using YAML value
|
|
113
|
+
if line_width is None:
|
|
114
|
+
trace_mm = resolve_style_value("lines.trace_mm", None, 0.2)
|
|
115
|
+
line_width = trace_mm * mm_to_pt
|
|
116
|
+
|
|
117
|
+
# Ticks
|
|
118
|
+
if n_ticks is None:
|
|
119
|
+
n_ticks = int(resolve_style_value("ticks.n_ticks", None, 4))
|
|
120
|
+
|
|
121
|
+
# Spines
|
|
122
|
+
if hide_top_right_spines is None:
|
|
123
|
+
hide_top = resolve_style_value("behavior.hide_top_spine", None, True, bool)
|
|
124
|
+
hide_right = resolve_style_value("behavior.hide_right_spine", None, True, bool)
|
|
125
|
+
hide_top_right_spines = hide_top and hide_right
|
|
126
|
+
|
|
127
|
+
# Font sizes from YAML
|
|
128
|
+
font_size = resolve_style_value("fonts.axis_label_pt", None, 7)
|
|
129
|
+
title_size = resolve_style_value("fonts.title_pt", None, 8)
|
|
130
|
+
tick_size = resolve_style_value("fonts.tick_label_pt", None, 7)
|
|
131
|
+
legend_size = resolve_style_value("fonts.legend_pt", None, 6)
|
|
132
|
+
|
|
133
|
+
# Axis thickness from YAML
|
|
134
|
+
axes_linewidth = resolve_style_value("axes.thickness_mm", None, 0.2) * mm_to_pt
|
|
79
135
|
|
|
80
136
|
# Colors
|
|
81
137
|
RGBA = {
|
|
@@ -99,39 +155,39 @@ def configure_mpl(
|
|
|
99
155
|
fig_size_mm[1] / 25.4 * fig_scale,
|
|
100
156
|
)
|
|
101
157
|
|
|
102
|
-
# Prepare matplotlib configuration
|
|
158
|
+
# Prepare matplotlib configuration using YAML-derived values
|
|
103
159
|
mpl_config = {
|
|
104
160
|
# Resolution
|
|
105
161
|
"figure.dpi": dpi_display,
|
|
106
162
|
"savefig.dpi": dpi_save,
|
|
107
163
|
# Figure Size
|
|
108
164
|
"figure.figsize": figsize_inch,
|
|
109
|
-
# Font Sizes
|
|
110
|
-
"font.size":
|
|
111
|
-
"axes.titlesize":
|
|
112
|
-
"axes.labelsize":
|
|
113
|
-
"xtick.labelsize":
|
|
114
|
-
"ytick.labelsize":
|
|
115
|
-
# Legend configuration
|
|
116
|
-
"legend.fontsize":
|
|
117
|
-
"legend.frameon": False,
|
|
118
|
-
"legend.loc": "best",
|
|
165
|
+
# Font Sizes from YAML
|
|
166
|
+
"font.size": font_size,
|
|
167
|
+
"axes.titlesize": title_size,
|
|
168
|
+
"axes.labelsize": font_size,
|
|
169
|
+
"xtick.labelsize": tick_size,
|
|
170
|
+
"ytick.labelsize": tick_size,
|
|
171
|
+
# Legend configuration from YAML
|
|
172
|
+
"legend.fontsize": legend_size,
|
|
173
|
+
"legend.frameon": False,
|
|
174
|
+
"legend.loc": "best",
|
|
119
175
|
# Auto Layout
|
|
120
176
|
"figure.autolayout": autolayout,
|
|
121
|
-
# Top and Right Axes
|
|
177
|
+
# Top and Right Axes from YAML
|
|
122
178
|
"axes.spines.top": not hide_top_right_spines,
|
|
123
179
|
"axes.spines.right": not hide_top_right_spines,
|
|
124
|
-
# Spine width
|
|
125
|
-
"axes.linewidth":
|
|
180
|
+
# Spine width from YAML (converted from mm to pt)
|
|
181
|
+
"axes.linewidth": axes_linewidth,
|
|
126
182
|
# Custom color cycle
|
|
127
183
|
"axes.prop_cycle": plt.cycler(
|
|
128
184
|
color=list(RGBA_NORM_FOR_CYCLE.values())
|
|
129
185
|
),
|
|
130
|
-
# Line
|
|
186
|
+
# Line width from YAML (converted from mm to pt)
|
|
131
187
|
"lines.linewidth": line_width,
|
|
132
|
-
"lines.markersize": 6.0,
|
|
188
|
+
"lines.markersize": 6.0,
|
|
133
189
|
# Grid (if used)
|
|
134
|
-
"grid.linewidth":
|
|
190
|
+
"grid.linewidth": axes_linewidth,
|
|
135
191
|
"grid.alpha": 0.3,
|
|
136
192
|
}
|
|
137
193
|
|
scitex/plt/utils/_crop.py
CHANGED
|
@@ -42,21 +42,35 @@ def find_content_area(image_path: str) -> Tuple[int, int, int, int]:
|
|
|
42
42
|
img_array = np.array(img)
|
|
43
43
|
|
|
44
44
|
# Check if image has alpha channel (RGBA)
|
|
45
|
-
if img_array.shape[2] == 4:
|
|
45
|
+
if len(img_array.shape) == 3 and img_array.shape[2] == 4:
|
|
46
46
|
# Use alpha channel to find content (non-transparent pixels)
|
|
47
47
|
alpha = img_array[:, :, 3]
|
|
48
48
|
# Find non-transparent pixels
|
|
49
49
|
rows = np.any(alpha > 0, axis=1)
|
|
50
50
|
cols = np.any(alpha > 0, axis=0)
|
|
51
51
|
else:
|
|
52
|
-
# For RGB images, find non-
|
|
53
|
-
# Consider pixels as content if they differ from white background
|
|
52
|
+
# For RGB images, detect background color from corners and find non-background pixels
|
|
54
53
|
if len(img_array.shape) == 3:
|
|
55
|
-
#
|
|
56
|
-
|
|
54
|
+
# Sample background color from corners (more robust than assuming white)
|
|
55
|
+
h, w = img_array.shape[:2]
|
|
56
|
+
corners = [
|
|
57
|
+
img_array[0, 0], # top-left
|
|
58
|
+
img_array[0, w-1], # top-right
|
|
59
|
+
img_array[h-1, 0], # bottom-left
|
|
60
|
+
img_array[h-1, w-1], # bottom-right
|
|
61
|
+
]
|
|
62
|
+
# Use median of corners as background color (robust to one corner having content)
|
|
63
|
+
bg_color = np.median(corners, axis=0).astype(np.uint8)
|
|
64
|
+
|
|
65
|
+
# Find pixels that differ significantly from background (threshold: 10 per channel)
|
|
66
|
+
diff = np.abs(img_array.astype(np.int16) - bg_color.astype(np.int16))
|
|
67
|
+
is_content = np.any(diff > 10, axis=2)
|
|
57
68
|
else:
|
|
58
|
-
# Grayscale:
|
|
59
|
-
|
|
69
|
+
# Grayscale: detect background from corners
|
|
70
|
+
h, w = img_array.shape
|
|
71
|
+
corners = [img_array[0, 0], img_array[0, w-1], img_array[h-1, 0], img_array[h-1, w-1]]
|
|
72
|
+
bg_value = np.median(corners)
|
|
73
|
+
is_content = np.abs(img_array.astype(np.int16) - bg_value) > 10
|
|
60
74
|
|
|
61
75
|
rows = np.any(is_content, axis=1)
|
|
62
76
|
cols = np.any(is_content, axis=0)
|