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
scitex/plt/__init__.py
CHANGED
|
@@ -1,18 +1,112 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
|
-
# Timestamp: "2025-
|
|
4
|
-
# File: /
|
|
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
|
-
"""
|
|
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
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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",
|