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.
Files changed (99) hide show
  1. scitex/ai/classification/reporters/reporter_utils/_Plotter.py +1 -1
  2. scitex/ai/plt/__init__.py +2 -2
  3. scitex/ai/plt/{_plot_conf_mat.py → _stx_conf_mat.py} +3 -3
  4. scitex/config/PriorityConfig.py +195 -0
  5. scitex/config/__init__.py +24 -0
  6. scitex/io/_save.py +125 -34
  7. scitex/io/_save_modules/_image.py +37 -20
  8. scitex/plt/__init__.py +470 -17
  9. scitex/plt/_subplots/_AxisWrapper.py +98 -50
  10. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +254 -124
  11. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +49 -8
  12. scitex/plt/_subplots/_SubplotsWrapper.py +76 -91
  13. scitex/plt/_subplots/_export_as_csv.py +127 -58
  14. scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +25 -16
  15. scitex/plt/_subplots/_export_as_csv_formatters/_format_contourf.py +54 -0
  16. scitex/plt/_subplots/_export_as_csv_formatters/_format_hexbin.py +41 -0
  17. scitex/plt/_subplots/_export_as_csv_formatters/_format_hist2d.py +41 -0
  18. scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow.py +59 -47
  19. scitex/plt/_subplots/_export_as_csv_formatters/_format_matshow.py +42 -0
  20. scitex/plt/_subplots/_export_as_csv_formatters/_format_pie.py +42 -0
  21. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +72 -35
  22. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_box.py +1 -1
  23. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +2 -2
  24. scitex/plt/_subplots/_export_as_csv_formatters/_format_quiver.py +53 -0
  25. scitex/plt/_subplots/_export_as_csv_formatters/_format_stem.py +42 -0
  26. scitex/plt/_subplots/_export_as_csv_formatters/_format_step.py +42 -0
  27. scitex/plt/_subplots/_export_as_csv_formatters/_format_streamplot.py +48 -0
  28. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_conf_mat.py → _format_stx_conf_mat.py} +2 -2
  29. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_ecdf.py → _format_stx_ecdf.py} +2 -2
  30. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_fillv.py → _format_stx_fillv.py} +2 -2
  31. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_heatmap.py → _format_stx_heatmap.py} +2 -2
  32. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_image.py → _format_stx_image.py} +2 -2
  33. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_joyplot.py → _format_stx_joyplot.py} +2 -2
  34. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_line.py → _format_stx_line.py} +3 -3
  35. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_mean_ci.py → _format_stx_mean_ci.py} +2 -2
  36. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_mean_std.py → _format_stx_mean_std.py} +2 -2
  37. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_median_iqr.py → _format_stx_median_iqr.py} +2 -2
  38. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_raster.py → _format_stx_raster.py} +2 -2
  39. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_rectangle.py → _format_stx_rectangle.py} +1 -1
  40. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_scatter_hist.py → _format_stx_scatter_hist.py} +2 -2
  41. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_shaded_line.py → _format_stx_shaded_line.py} +2 -2
  42. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_violin.py → _format_stx_violin.py} +2 -2
  43. scitex/plt/_subplots/_export_as_csv_formatters/verify_formatters.py +23 -23
  44. scitex/plt/ax/__init__.py +16 -15
  45. scitex/plt/ax/_plot/__init__.py +30 -30
  46. scitex/plt/ax/_plot/_add_fitted_line.py +65 -11
  47. scitex/plt/ax/_plot/_plot_statistical_shaded_line.py +104 -76
  48. scitex/plt/ax/_plot/{_plot_conf_mat.py → _stx_conf_mat.py} +10 -10
  49. scitex/plt/ax/_plot/_stx_ecdf.py +109 -0
  50. scitex/plt/ax/_plot/{_plot_fillv.py → _stx_fillv.py} +7 -7
  51. scitex/plt/ax/_plot/_stx_heatmap.py +366 -0
  52. scitex/plt/ax/_plot/{_plot_image.py → _stx_image.py} +1 -1
  53. scitex/plt/ax/_plot/_stx_joyplot.py +113 -0
  54. scitex/plt/ax/_plot/{_plot_raster.py → _stx_raster.py} +37 -25
  55. scitex/plt/ax/_plot/{_plot_rectangle.py → _stx_rectangle.py} +10 -9
  56. scitex/plt/ax/_plot/{_plot_scatter_hist.py → _stx_scatter_hist.py} +1 -1
  57. scitex/plt/ax/_plot/_stx_shaded_line.py +215 -0
  58. scitex/plt/ax/_plot/{_plot_violin.py → _stx_violin.py} +13 -6
  59. scitex/plt/ax/_style/__init__.py +3 -0
  60. scitex/plt/ax/_style/_style_barplot.py +13 -2
  61. scitex/plt/ax/_style/_style_boxplot.py +78 -32
  62. scitex/plt/ax/_style/_style_errorbar.py +17 -3
  63. scitex/plt/ax/_style/_style_scatter.py +17 -3
  64. scitex/plt/ax/_style/_style_violinplot.py +109 -0
  65. scitex/plt/color/_vizualize_colors.py +3 -3
  66. scitex/plt/styles/SCITEX_STYLE.yaml +104 -0
  67. scitex/plt/styles/__init__.py +57 -0
  68. scitex/plt/styles/_plot_defaults.py +209 -0
  69. scitex/plt/styles/_plot_postprocess.py +518 -0
  70. scitex/plt/styles/_style_loader.py +268 -0
  71. scitex/plt/styles/presets.py +208 -0
  72. scitex/plt/utils/_collect_figure_metadata.py +160 -18
  73. scitex/plt/utils/_colorbar.py +72 -10
  74. scitex/plt/utils/_configure_mpl.py +108 -52
  75. scitex/plt/utils/_crop.py +21 -7
  76. scitex/plt/utils/_figure_mm.py +21 -7
  77. scitex/stats/__init__.py +13 -1
  78. scitex/stats/_schema.py +578 -0
  79. scitex/stats/tests/__init__.py +13 -0
  80. scitex/stats/tests/correlation/__init__.py +13 -0
  81. scitex/stats/tests/correlation/_test_pearson.py +262 -0
  82. scitex/vis/__init__.py +6 -0
  83. scitex/vis/editor/__init__.py +23 -0
  84. scitex/vis/editor/_defaults.py +205 -0
  85. scitex/vis/editor/_edit.py +342 -0
  86. scitex/vis/editor/_mpl_editor.py +231 -0
  87. scitex/vis/editor/_tkinter_editor.py +466 -0
  88. scitex/vis/editor/_web_editor.py +1440 -0
  89. scitex/vis/model/plot_types.py +15 -15
  90. {scitex-2.3.0.dist-info → scitex-2.4.0.dist-info}/METADATA +2 -1
  91. {scitex-2.3.0.dist-info → scitex-2.4.0.dist-info}/RECORD +94 -67
  92. {scitex-2.3.0.dist-info → scitex-2.4.0.dist-info}/WHEEL +1 -1
  93. scitex/plt/ax/_plot/_plot_ecdf.py +0 -84
  94. scitex/plt/ax/_plot/_plot_heatmap.py +0 -277
  95. scitex/plt/ax/_plot/_plot_joyplot.py +0 -77
  96. scitex/plt/ax/_plot/_plot_shaded_line.py +0 -142
  97. scitex/plt/presets.py +0 -224
  98. {scitex-2.3.0.dist-info → scitex-2.4.0.dist-info}/entry_points.txt +0 -0
  99. {scitex-2.3.0.dist-info → scitex-2.4.0.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 'plot_heatmap' in first_cmd:
341
- return "heatmap", "plot_heatmap"
342
- elif 'plot_kde' in first_cmd:
343
- return "kde", "plot_kde"
344
- elif 'plot_ecdf' in first_cmd:
345
- return "ecdf", "plot_ecdf"
346
- elif 'plot_violin' in first_cmd:
347
- return "violin", "plot_violin"
348
- elif 'plot_box' in first_cmd or 'boxplot' in first_cmd:
349
- return "boxplot", "plot_box"
350
- elif 'plot_line' in first_cmd:
351
- return "line", "plot_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 'plot_mean_std' in first_cmd:
355
- return "line", "plot_mean_std"
356
- elif 'plot_shaded_line' in first_cmd:
357
- return "line", "plot_shaded_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:
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
- # Timestamp: "2025-06-08 11:15:00 (ywatanabe)"
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
- def colorbar(mappable, ax=None, **kwargs):
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-11-19 12:01:10 (ywatanabe)"
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=(160, 100),
20
- fig_scale=1.0,
21
- dpi_display=100,
22
- dpi_save=300,
23
- # fontsize="medium",
24
- autolayout=True,
25
- n_ticks=4,
26
- hide_top_right_spines=True,
27
- line_width=1.0, # Increased from 0.5 for better visibility
28
- alpha=0.85, # Adjusted for better contrast
29
- enable_latex=False, # Disable LaTeX, use Arial font instead
30
- latex_preamble=None, # Custom LaTeX preamble
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 int, optional
41
- Figure width and height in millimeters, by default (160, 100)
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, by default 100
49
+ Display resolution in DPI. If None, uses YAML output.dpi / 3.
46
50
  dpi_save : int, optional
47
- Saving resolution in DPI, by default 300
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, by default True
55
+ Whether to hide top and right spines. If None, uses YAML behavior settings.
58
56
  line_width : float, optional
59
- Default line width, by default 1.0
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.85
59
+ Color transparency, by default 1.0
62
60
  n_ticks : int, optional
63
- Number of ticks on each axis, by default 4
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
- # # Ensure minimum sizes for different elements with better proportions
76
- # title_size = max(base_size * 1.25, 10.0) # Increased for better hierarchy
77
- # label_size = max(base_size * 1.0, 9.0) # Minimum 9pt for good readability
78
- # small_size = max(base_size * 0.85, 8.0) # Increased ratio for better legibility
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 (7pt for titles/labels, 6pt for legend)
110
- "font.size": 7, # Base font size
111
- "axes.titlesize": 7, # Title size (prevent "large" default)
112
- "axes.labelsize": 7, # Axis label size
113
- "xtick.labelsize": 7, # Tick label size
114
- "ytick.labelsize": 7, # Tick label size
115
- # Legend configuration
116
- "legend.fontsize": 6, # 6pt for legend labels
117
- "legend.frameon": False, # No frame by default
118
- "legend.loc": "best", # Auto-position to avoid overlap
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": 0.8, # Slightly thicker axes lines
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, # Better default marker size
188
+ "lines.markersize": 6.0,
133
189
  # Grid (if used)
134
- "grid.linewidth": 0.6,
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-white pixels
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
- # Check if pixel is not white (255, 255, 255)
56
- is_content = np.any(img_array < 250, axis=2)
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: check if not white
59
- is_content = img_array < 250
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)