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
scitex/plt/__init__.py CHANGED
@@ -1,18 +1,112 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
- # Timestamp: "2025-06-13 22:42:39 (ywatanabe)"
4
- # File: /ssh:sp:/home/ywatanabe/proj/SciTeX-Code/src/scitex/plt/__init__.py
3
+ # Timestamp: "2025-12-02 12:30:00 (ywatanabe)"
4
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/plt/__init__.py
5
5
  # ----------------------------------------
6
6
  import os
7
7
  __FILE__ = __file__
8
8
  __DIR__ = os.path.dirname(__FILE__)
9
9
  # ----------------------------------------
10
- """Scitex plt module."""
10
+ """
11
+ SciTeX plt module - Publication-quality plotting.
12
+
13
+ Simply importing this module automatically configures matplotlib with
14
+ SciTeX publication defaults from SCITEX_STYLE.yaml:
15
+
16
+ import scitex.plt as splt
17
+ fig, ax = splt.subplots()
18
+ ax.plot([1, 2, 3], [1, 4, 9])
19
+ fig.savefig("figure.png")
20
+
21
+ No need to call scitex.session() or configure_mpl() - it's automatic.
22
+
23
+ Style values can be customized by:
24
+ 1. Editing SCITEX_STYLE.yaml
25
+ 2. Setting environment variables (SCITEX_PLT_FONTS_AXIS_LABEL_PT=8)
26
+ 3. Passing parameters directly to subplots()
27
+ """
11
28
 
12
- # Register Arial fonts eagerly (before lazy imports)
13
29
  import matplotlib.font_manager as fm
14
30
  import matplotlib as mpl
15
-
31
+ import matplotlib.pyplot as plt
32
+
33
+ # =============================================================================
34
+ # Auto-configure matplotlib with SciTeX style on import
35
+ # =============================================================================
36
+
37
+ def _auto_configure_mpl():
38
+ """Apply SciTeX style configuration automatically on import."""
39
+ from .styles import resolve_style_value
40
+
41
+ # mm to pt conversion factor
42
+ mm_to_pt = 2.83465
43
+
44
+ # Load all style values from YAML (with env override support)
45
+ font_size = resolve_style_value("fonts.axis_label_pt", None, 7)
46
+ title_size = resolve_style_value("fonts.title_pt", None, 8)
47
+ tick_size = resolve_style_value("fonts.tick_label_pt", None, 7)
48
+ legend_size = resolve_style_value("fonts.legend_pt", None, 6)
49
+
50
+ trace_mm = resolve_style_value("lines.trace_mm", None, 0.2)
51
+ line_width = trace_mm * mm_to_pt
52
+
53
+ axes_thickness_mm = resolve_style_value("axes.thickness_mm", None, 0.2)
54
+ axes_linewidth = axes_thickness_mm * mm_to_pt
55
+
56
+ hide_top = resolve_style_value("behavior.hide_top_spine", None, True, bool)
57
+ hide_right = resolve_style_value("behavior.hide_right_spine", None, True, bool)
58
+
59
+ dpi = int(resolve_style_value("output.dpi", None, 300))
60
+
61
+ # Calculate figure size from axes + margins
62
+ axes_w = resolve_style_value("axes.width_mm", None, 40)
63
+ axes_h = resolve_style_value("axes.height_mm", None, 28)
64
+ margin_l = resolve_style_value("margins.left_mm", None, 20)
65
+ margin_r = resolve_style_value("margins.right_mm", None, 20)
66
+ margin_b = resolve_style_value("margins.bottom_mm", None, 20)
67
+ margin_t = resolve_style_value("margins.top_mm", None, 20)
68
+ fig_w_mm = axes_w + margin_l + margin_r
69
+ fig_h_mm = axes_h + margin_b + margin_t
70
+ figsize_inch = (fig_w_mm / 25.4, fig_h_mm / 25.4)
71
+
72
+ # Apply rcParams
73
+ mpl_config = {
74
+ # Resolution
75
+ "figure.dpi": max(100, dpi // 3),
76
+ "savefig.dpi": dpi,
77
+ # Figure Size
78
+ "figure.figsize": figsize_inch,
79
+ # Font Sizes
80
+ "font.size": font_size,
81
+ "axes.titlesize": title_size,
82
+ "axes.labelsize": font_size,
83
+ "xtick.labelsize": tick_size,
84
+ "ytick.labelsize": tick_size,
85
+ # Legend
86
+ "legend.fontsize": legend_size,
87
+ "legend.frameon": False,
88
+ "legend.loc": "best",
89
+ # Auto Layout
90
+ "figure.autolayout": True,
91
+ # Spines
92
+ "axes.spines.top": not hide_top,
93
+ "axes.spines.right": not hide_right,
94
+ # Line widths
95
+ "axes.linewidth": axes_linewidth,
96
+ "lines.linewidth": line_width,
97
+ "lines.markersize": 6.0,
98
+ # Grid
99
+ "grid.linewidth": axes_linewidth,
100
+ "grid.alpha": 0.3,
101
+ # Math text
102
+ "mathtext.fontset": "dejavusans",
103
+ "mathtext.default": "regular",
104
+ }
105
+
106
+ mpl.rcParams.update(mpl_config)
107
+
108
+
109
+ # Register Arial fonts eagerly (before style configuration)
16
110
  _arial_enabled = False
17
111
  try:
18
112
  fm.findfont("Arial", fallback_to_default=False)
@@ -38,27 +132,38 @@ except Exception:
38
132
  except Exception:
39
133
  pass
40
134
 
41
- # Configure matplotlib to use Arial if available
135
+ # Configure font family
42
136
  if _arial_enabled:
43
137
  mpl.rcParams["font.family"] = "Arial"
44
138
  mpl.rcParams["font.sans-serif"] = ["Arial", "Helvetica", "DejaVu Sans", "Liberation Sans"]
45
139
  else:
46
- # Warn about missing Arial
47
- try:
48
- from scitex.logging import getLogger
49
- _logger = getLogger(__name__)
50
- _logger.warning(
51
- "Arial font not found. Using fallback fonts (Helvetica/DejaVu Sans). "
52
- "For publication figures with Arial: sudo apt-get install ttf-mscorefonts-installer && fc-cache -fv"
53
- )
54
- except:
55
- pass # Skip warning if logging not available
140
+ mpl.rcParams["font.family"] = "sans-serif"
141
+ mpl.rcParams["font.sans-serif"] = ["Helvetica", "DejaVu Sans", "Liberation Sans", "sans-serif"]
142
+ # Suppress font warnings
143
+ import logging
144
+ logging.getLogger('matplotlib.font_manager').setLevel(logging.ERROR)
145
+
146
+ # Apply SciTeX style configuration automatically
147
+ _auto_configure_mpl()
148
+
149
+ # Set up color cycle from scitex colors
150
+ try:
151
+ from . import color as _color_module
152
+ _rgba_norm_cycle = {
153
+ k: tuple(_color_module.update_alpha(v, 1.0))
154
+ for k, v in _color_module.PARAMS.get("RGBA_NORM_FOR_CYCLE", {}).items()
155
+ }
156
+ if _rgba_norm_cycle:
157
+ mpl.rcParams["axes.prop_cycle"] = plt.cycler(color=list(_rgba_norm_cycle.values()))
158
+ except Exception:
159
+ pass # Use matplotlib default colors if color module fails
56
160
 
57
161
  from ._tpl import termplot
58
162
  from . import color
59
163
  from . import utils
60
164
  from . import ax
61
- from . import presets
165
+ from .styles import presets
166
+ from . import styles
62
167
 
63
168
  # Lazy import for subplots to avoid circular dependencies
64
169
  _subplots = None
@@ -125,6 +230,353 @@ def crop(input_path, output_path=None, margin=12, overwrite=False, verbose=False
125
230
  _crop = _crop_func
126
231
  return _crop(input_path, output_path, margin, overwrite, verbose)
127
232
 
233
+ def load(path, apply_manual=True):
234
+ """
235
+ Load a figure from saved JSON + CSV files.
236
+
237
+ Parameters
238
+ ----------
239
+ path : str or Path
240
+ Path to JSON file, PNG file, or CSV file.
241
+ Will auto-detect sibling files in same directory or organized subdirectories.
242
+ apply_manual : bool, optional
243
+ If True, apply .manual.json overrides if exists (default: True)
244
+
245
+ Returns
246
+ -------
247
+ tuple
248
+ (fig, axes) where fig is FigWrapper and axes is AxisWrapper or array
249
+
250
+ Raises
251
+ ------
252
+ FileNotFoundError
253
+ If required JSON file is not found
254
+ ValueError
255
+ If manual.json hash doesn't match (stale manual edits)
256
+
257
+ Examples
258
+ --------
259
+ >>> # Load from JSON (sibling pattern)
260
+ >>> fig, axes = stx.plt.load("output/figure.json")
261
+
262
+ >>> # Load from PNG (finds sibling JSON + CSV)
263
+ >>> fig, axes = stx.plt.load("output/figure.png")
264
+
265
+ >>> # Load from organized directory pattern
266
+ >>> fig, axes = stx.plt.load("output/json/figure.json")
267
+
268
+ >>> # Skip manual overrides
269
+ >>> fig, axes = stx.plt.load("figure.json", apply_manual=False)
270
+
271
+ Notes
272
+ -----
273
+ Supports two directory patterns:
274
+
275
+ Pattern 1 (flat/sibling):
276
+ output/figure.png
277
+ output/figure.json
278
+ output/figure.csv
279
+
280
+ Pattern 2 (organized):
281
+ output/png/figure.png
282
+ output/json/figure.json
283
+ output/csv/figure.csv
284
+
285
+ Manual overrides (.manual.json) are applied if:
286
+ - apply_manual=True
287
+ - figure.manual.json exists alongside figure.json
288
+ - Hash validation passes (warns if stale)
289
+ """
290
+ from pathlib import Path
291
+ import hashlib
292
+ import warnings
293
+ import scitex as stx
294
+
295
+ path = Path(path)
296
+
297
+ # Resolve JSON path from any input (png, csv, or json)
298
+ json_path, csv_path = _resolve_figure_paths(path)
299
+
300
+ if not json_path.exists():
301
+ raise FileNotFoundError(f"JSON file not found: {json_path}")
302
+
303
+ # Load JSON metadata
304
+ metadata = stx.io.load(json_path)
305
+
306
+ # Load CSV data if exists
307
+ csv_data = None
308
+ if csv_path and csv_path.exists():
309
+ csv_data = stx.io.load(csv_path)
310
+
311
+ # Check for manual overrides
312
+ manual_path = json_path.with_suffix('.manual.json')
313
+ manual_overrides = None
314
+ if apply_manual and manual_path.exists():
315
+ manual_data = stx.io.load(manual_path)
316
+
317
+ # Validate hash
318
+ if 'base_hash' in manual_data:
319
+ current_hash = _compute_file_hash(json_path)
320
+ if manual_data['base_hash'] != current_hash:
321
+ warnings.warn(
322
+ f"Manual overrides may be stale: base data changed since manual edits.\n"
323
+ f" Expected hash: {manual_data['base_hash'][:16]}...\n"
324
+ f" Current hash: {current_hash[:16]}...\n"
325
+ f" Review: {manual_path}"
326
+ )
327
+ manual_overrides = manual_data.get('overrides', {})
328
+
329
+ # Reconstruct figure
330
+ fig, axes = _reconstruct_figure(metadata, csv_data, manual_overrides)
331
+
332
+ return fig, axes
333
+
334
+
335
+ def _resolve_figure_paths(path):
336
+ """
337
+ Resolve JSON and CSV paths from any input file path.
338
+
339
+ Supports both flat (sibling) and organized (subdirectory) patterns.
340
+ """
341
+ from pathlib import Path
342
+
343
+ path = Path(path)
344
+ stem = path.stem
345
+ suffix = path.suffix.lower()
346
+ parent = path.parent
347
+
348
+ # Determine base name (remove .manual if present)
349
+ if stem.endswith('.manual'):
350
+ stem = stem[:-7]
351
+
352
+ json_path = None
353
+ csv_path = None
354
+
355
+ if suffix == '.json':
356
+ json_path = path
357
+ # Try sibling CSV first
358
+ csv_sibling = parent / f"{stem}.csv"
359
+ if csv_sibling.exists():
360
+ csv_path = csv_sibling
361
+ # Try organized pattern (../csv/)
362
+ elif parent.name == 'json':
363
+ csv_organized = parent.parent / 'csv' / f"{stem}.csv"
364
+ if csv_organized.exists():
365
+ csv_path = csv_organized
366
+
367
+ elif suffix in ('.png', '.jpg', '.jpeg', '.pdf', '.svg'):
368
+ # Look for sibling JSON
369
+ json_sibling = parent / f"{stem}.json"
370
+ csv_sibling = parent / f"{stem}.csv"
371
+
372
+ if json_sibling.exists():
373
+ json_path = json_sibling
374
+ if csv_sibling.exists():
375
+ csv_path = csv_sibling
376
+ # Try organized pattern (parent has png/, look for json/)
377
+ elif parent.name in ('png', 'jpg', 'jpeg', 'pdf', 'svg'):
378
+ json_organized = parent.parent / 'json' / f"{stem}.json"
379
+ csv_organized = parent.parent / 'csv' / f"{stem}.csv"
380
+ if json_organized.exists():
381
+ json_path = json_organized
382
+ if csv_organized.exists():
383
+ csv_path = csv_organized
384
+
385
+ elif suffix == '.csv':
386
+ csv_path = path
387
+ # Try sibling JSON
388
+ json_sibling = parent / f"{stem}.json"
389
+ if json_sibling.exists():
390
+ json_path = json_sibling
391
+ # Try organized pattern (../json/)
392
+ elif parent.name == 'csv':
393
+ json_organized = parent.parent / 'json' / f"{stem}.json"
394
+ if json_organized.exists():
395
+ json_path = json_organized
396
+
397
+ # Fallback: assume it's the JSON path
398
+ if json_path is None:
399
+ json_path = path if suffix == '.json' else path.with_suffix('.json')
400
+
401
+ return json_path, csv_path
402
+
403
+
404
+ def _compute_file_hash(path):
405
+ """Compute SHA256 hash of a file."""
406
+ import hashlib
407
+ from pathlib import Path
408
+
409
+ path = Path(path)
410
+ sha256 = hashlib.sha256()
411
+ with open(path, 'rb') as f:
412
+ for chunk in iter(lambda: f.read(8192), b''):
413
+ sha256.update(chunk)
414
+ return f"sha256:{sha256.hexdigest()}"
415
+
416
+
417
+ def _reconstruct_figure(metadata, csv_data, manual_overrides=None):
418
+ """
419
+ Reconstruct figure from metadata and CSV data.
420
+
421
+ Parameters
422
+ ----------
423
+ metadata : dict
424
+ JSON metadata from stx.io.save()
425
+ csv_data : DataFrame or None
426
+ CSV data with plot values
427
+ manual_overrides : dict or None
428
+ Manual style/annotation overrides
429
+
430
+ Returns
431
+ -------
432
+ tuple
433
+ (fig, axes)
434
+ """
435
+ import numpy as np
436
+
437
+ # Extract dimensions from metadata
438
+ dims = metadata.get('dimensions', {})
439
+ fig_size_mm = dims.get('figure_size_mm', [80, 68])
440
+ dpi = dims.get('dpi', 300)
441
+
442
+ # Get style from metadata
443
+ scitex_meta = metadata.get('scitex', {})
444
+ style_mm = scitex_meta.get('style_mm', {})
445
+
446
+ # Create figure with same dimensions
447
+ fig, axes = subplots(
448
+ axes_width_mm=style_mm.get('axes_width_mm', dims.get('axes_size_mm', [40, 28])[0]),
449
+ axes_height_mm=style_mm.get('axes_height_mm', dims.get('axes_size_mm', [40, 28])[1]),
450
+ dpi=dpi,
451
+ )
452
+
453
+ # Handle single vs multiple axes
454
+ ax = axes if not hasattr(axes, 'flat') else list(axes.flat)[0]
455
+
456
+ # Set axis labels from metadata
457
+ axes_meta = metadata.get('axes', {})
458
+ x_meta = axes_meta.get('x', {})
459
+ y_meta = axes_meta.get('y', {})
460
+
461
+ xlabel = x_meta.get('label', '')
462
+ ylabel = y_meta.get('label', '')
463
+ x_unit = x_meta.get('unit', '')
464
+ y_unit = y_meta.get('unit', '')
465
+
466
+ if xlabel:
467
+ full_xlabel = f"{xlabel} [{x_unit}]" if x_unit else xlabel
468
+ ax.set_xlabel(full_xlabel)
469
+ if ylabel:
470
+ full_ylabel = f"{ylabel} [{y_unit}]" if y_unit else ylabel
471
+ ax.set_ylabel(full_ylabel)
472
+
473
+ # Reconstruct plots from CSV data
474
+ if csv_data is not None and not csv_data.empty:
475
+ _reconstruct_plots_from_csv(ax, csv_data, metadata)
476
+
477
+ # Apply manual overrides
478
+ if manual_overrides:
479
+ _apply_manual_overrides(fig, axes, manual_overrides)
480
+
481
+ return fig, axes
482
+
483
+
484
+ def _reconstruct_plots_from_csv(ax, csv_data, metadata):
485
+ """
486
+ Reconstruct plot elements from CSV data.
487
+
488
+ CSV columns follow pattern: ax_00_<type>_<name>
489
+ """
490
+ import pandas as pd
491
+ import numpy as np
492
+
493
+ # Group columns by plot type
494
+ plot_type = metadata.get('plot_type', metadata.get('method', 'line'))
495
+
496
+ # Parse column names to find plot data
497
+ columns = csv_data.columns.tolist()
498
+
499
+ # Find x/y data columns
500
+ x_cols = [c for c in columns if '_x' in c.lower() and 'text' not in c.lower()]
501
+ y_cols = [c for c in columns if '_y' in c.lower() and 'text' not in c.lower()]
502
+
503
+ if plot_type == 'line' or plot_type == 'plot':
504
+ # For line plots, look for paired x/y or just y with index
505
+ if x_cols and y_cols:
506
+ for x_col, y_col in zip(x_cols, y_cols):
507
+ x = csv_data[x_col].dropna().values
508
+ y = csv_data[y_col].dropna().values
509
+ if len(x) > 0 and len(y) > 0:
510
+ ax.plot(x[:len(y)], y[:len(x)])
511
+ elif y_cols:
512
+ for y_col in y_cols:
513
+ y = csv_data[y_col].dropna().values
514
+ if len(y) > 0:
515
+ ax.plot(y)
516
+
517
+ elif plot_type == 'scatter':
518
+ if x_cols and y_cols:
519
+ x = csv_data[x_cols[0]].dropna().values
520
+ y = csv_data[y_cols[0]].dropna().values
521
+ ax.scatter(x[:min(len(x), len(y))], y[:min(len(x), len(y))])
522
+
523
+ # Add text annotations
524
+ text_cols = [c for c in columns if 'text' in c.lower() and 'content' in c.lower()]
525
+ for text_col in text_cols:
526
+ # Find corresponding x, y columns
527
+ prefix = text_col.rsplit('_content', 1)[0]
528
+ x_col = f"{prefix}_x"
529
+ y_col = f"{prefix}_y"
530
+
531
+ if x_col in columns and y_col in columns:
532
+ x_vals = csv_data[x_col].dropna()
533
+ y_vals = csv_data[y_col].dropna()
534
+ text_vals = csv_data[text_col].dropna()
535
+
536
+ for i in range(min(len(x_vals), len(y_vals), len(text_vals))):
537
+ ax.text(
538
+ x_vals.iloc[i], y_vals.iloc[i], str(text_vals.iloc[i]),
539
+ transform=ax.transAxes, fontsize=6,
540
+ verticalalignment='top', horizontalalignment='right'
541
+ )
542
+
543
+
544
+ def _apply_manual_overrides(fig, axes, overrides):
545
+ """
546
+ Apply manual style/annotation overrides to figure.
547
+
548
+ Parameters
549
+ ----------
550
+ fig : FigWrapper
551
+ Figure to modify
552
+ axes : AxisWrapper or array
553
+ Axes to modify
554
+ overrides : dict
555
+ Override specifications like {"axes[0].style.linewidth": 0.5}
556
+ """
557
+ # Simple override application - can be extended
558
+ for key, value in overrides.items():
559
+ # Parse key like "axes[0].title" or "style.linewidth"
560
+ parts = key.split('.')
561
+
562
+ if parts[0].startswith('axes['):
563
+ # Extract axis index
564
+ import re
565
+ match = re.match(r'axes\[(\d+)\]', parts[0])
566
+ if match:
567
+ idx = int(match.group(1))
568
+ ax = axes if not hasattr(axes, 'flat') else list(axes.flat)[idx]
569
+
570
+ if len(parts) > 1:
571
+ attr = parts[1]
572
+ if attr == 'title':
573
+ ax.set_title(value)
574
+ elif attr == 'xlabel':
575
+ ax.set_xlabel(value)
576
+ elif attr == 'ylabel':
577
+ ax.set_ylabel(value)
578
+
579
+
128
580
  def tight_layout(**kwargs):
129
581
  """
130
582
  Wrapper for matplotlib.pyplot.tight_layout that handles colorbar layout compatibility.
@@ -202,6 +654,7 @@ def colorbar(mappable=None, cax=None, ax=None, **kwargs):
202
654
  __all__ = [
203
655
  "termplot",
204
656
  "subplots",
657
+ "load",
205
658
  "figure",
206
659
  "tight_layout",
207
660
  "utils",