batplot 1.3.6__tar.gz → 1.3.8__tar.gz
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.
- {batplot-1.3.6 → batplot-1.3.8}/PKG-INFO +1 -1
- {batplot-1.3.6 → batplot-1.3.8}/batplot/__init__.py +1 -1
- {batplot-1.3.6 → batplot-1.3.8}/batplot/batplot.py +17 -1
- {batplot-1.3.6 → batplot-1.3.8}/batplot/operando.py +22 -6
- {batplot-1.3.6 → batplot-1.3.8}/batplot/readers.py +40 -2
- {batplot-1.3.6 → batplot-1.3.8}/batplot/session.py +40 -2
- {batplot-1.3.6 → batplot-1.3.8}/batplot.egg-info/PKG-INFO +1 -1
- {batplot-1.3.6 → batplot-1.3.8}/pyproject.toml +1 -1
- {batplot-1.3.6 → batplot-1.3.8}/LICENSE +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/README.md +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot/args.py +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot/batch.py +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot/batplot_new.py +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot/cif.py +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot/cli.py +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot/converters.py +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot/cpc_interactive.py +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot/electrochem_interactive.py +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot/interactive.py +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot/modes.py +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot/operando_ec_interactive.py +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot/plotting.py +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot/style.py +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot/ui.py +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot/utils.py +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot.egg-info/SOURCES.txt +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot.egg-info/dependency_links.txt +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot.egg-info/entry_points.txt +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot.egg-info/requires.txt +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/batplot.egg-info/top_level.txt +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/setup.cfg +0 -0
- {batplot-1.3.6 → batplot-1.3.8}/setup.py +0 -0
|
@@ -79,6 +79,22 @@ except ImportError:
|
|
|
79
79
|
keep_canvas_fixed = False
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def _natural_sort_key(filename: str) -> list:
|
|
83
|
+
"""Generate a natural sorting key for filenames with numbers.
|
|
84
|
+
|
|
85
|
+
Converts 'file_10.xy' to ['file_', 10, '.xy'] so numerical parts are sorted numerically.
|
|
86
|
+
This ensures file_2.xy comes before file_10.xy (natural order).
|
|
87
|
+
"""
|
|
88
|
+
parts = []
|
|
89
|
+
for match in re.finditer(r'(\d+|\D+)', filename):
|
|
90
|
+
text = match.group(0)
|
|
91
|
+
if text.isdigit():
|
|
92
|
+
parts.append(int(text))
|
|
93
|
+
else:
|
|
94
|
+
parts.append(text.lower())
|
|
95
|
+
return parts
|
|
96
|
+
|
|
97
|
+
|
|
82
98
|
def batplot_main() -> int:
|
|
83
99
|
"""Main entry point for batplot CLI.
|
|
84
100
|
|
|
@@ -1131,7 +1147,7 @@ def batplot_main() -> int:
|
|
|
1131
1147
|
all_xy_files = []
|
|
1132
1148
|
unknown_ext_files = []
|
|
1133
1149
|
|
|
1134
|
-
for f in sorted(os.listdir(os.getcwd())):
|
|
1150
|
+
for f in sorted(os.listdir(os.getcwd()), key=_natural_sort_key):
|
|
1135
1151
|
if not os.path.isfile(os.path.join(os.getcwd(), f)):
|
|
1136
1152
|
continue
|
|
1137
1153
|
ext = os.path.splitext(f)[1].lower()
|
|
@@ -32,6 +32,21 @@ KNOWN_DIFFRACTION_EXT = {".xy", ".xye", ".qye", ".dat"}
|
|
|
32
32
|
_two_theta_re = re.compile(r"2[tT]heta|2th", re.IGNORECASE)
|
|
33
33
|
_q_re = re.compile(r"^q$", re.IGNORECASE)
|
|
34
34
|
|
|
35
|
+
def _natural_sort_key(path: Path) -> list:
|
|
36
|
+
"""Generate a natural sorting key for filenames with numbers.
|
|
37
|
+
|
|
38
|
+
Converts 'file_10.xy' to ['file_', 10, '.xy'] so numerical parts are sorted numerically.
|
|
39
|
+
This ensures file_2.xy comes before file_10.xy (natural order).
|
|
40
|
+
"""
|
|
41
|
+
parts = []
|
|
42
|
+
for match in re.finditer(r'(\d+|\D+)', path.name):
|
|
43
|
+
text = match.group(0)
|
|
44
|
+
if text.isdigit():
|
|
45
|
+
parts.append(int(text))
|
|
46
|
+
else:
|
|
47
|
+
parts.append(text.lower())
|
|
48
|
+
return parts
|
|
49
|
+
|
|
35
50
|
def _infer_axis_mode(args, any_qye: bool, has_unknown_ext: bool):
|
|
36
51
|
# Priority: explicit --xaxis, else .qye presence (Q), else wavelength (Q), else default 2theta with warning
|
|
37
52
|
# If unknown extensions are present, use "user defined" mode
|
|
@@ -83,12 +98,12 @@ def plot_operando_folder(folder: str, args) -> Tuple[plt.Figure, plt.Axes, Dict[
|
|
|
83
98
|
p = Path(folder)
|
|
84
99
|
if not p.is_dir():
|
|
85
100
|
raise FileNotFoundError(f"Not a directory: {folder}")
|
|
86
|
-
# First try to find known diffraction files
|
|
87
|
-
files = sorted([f for f in p.iterdir() if f.suffix.lower() in KNOWN_DIFFRACTION_EXT])
|
|
101
|
+
# First try to find known diffraction files (filter out macOS resource fork files starting with ._)
|
|
102
|
+
files = sorted([f for f in p.iterdir() if f.suffix.lower() in KNOWN_DIFFRACTION_EXT and not f.name.startswith("._")], key=_natural_sort_key)
|
|
88
103
|
has_unknown_ext = False
|
|
89
104
|
# If no known files found, accept any file extension (except .mpt which is for electrochemistry)
|
|
90
105
|
if not files:
|
|
91
|
-
files = sorted([f for f in p.iterdir() if f.is_file() and f.suffix.lower() != ".mpt"])
|
|
106
|
+
files = sorted([f for f in p.iterdir() if f.is_file() and f.suffix.lower() != ".mpt" and not f.name.startswith("._")], key=_natural_sort_key)
|
|
92
107
|
has_unknown_ext = True
|
|
93
108
|
if not files:
|
|
94
109
|
raise FileNotFoundError("No data files found in folder (excluding .mpt files)")
|
|
@@ -137,7 +152,8 @@ def plot_operando_folder(folder: str, args) -> Tuple[plt.Figure, plt.Axes, Dict[
|
|
|
137
152
|
Z = np.vstack(stack) # shape (n_scans, n_x)
|
|
138
153
|
|
|
139
154
|
# Detect an electrochemistry .mpt file in the same folder (if any)
|
|
140
|
-
|
|
155
|
+
# Filter out macOS resource fork files (starting with ._)
|
|
156
|
+
mpt_files = sorted([f for f in p.iterdir() if f.suffix.lower() == ".mpt" and not f.name.startswith("._")], key=_natural_sort_key) # pick first if present
|
|
141
157
|
has_ec = len(mpt_files) > 0
|
|
142
158
|
ec_ax = None
|
|
143
159
|
|
|
@@ -151,8 +167,8 @@ def plot_operando_folder(folder: str, args) -> Tuple[plt.Figure, plt.Axes, Dict[
|
|
|
151
167
|
# Use imshow for speed; mask nans
|
|
152
168
|
Zm = np.ma.masked_invalid(Z)
|
|
153
169
|
extent = (grid_x.min(), grid_x.max(), 0, Zm.shape[0]-1)
|
|
154
|
-
#
|
|
155
|
-
im = ax.imshow(Zm, aspect='auto', origin='
|
|
170
|
+
# Bottom-to-top visual order (scan 0 at bottom) to match EC time progression -> origin='lower'
|
|
171
|
+
im = ax.imshow(Zm, aspect='auto', origin='lower', extent=extent, cmap='viridis', interpolation='nearest')
|
|
156
172
|
# Place colorbar on the left
|
|
157
173
|
cbar = fig.colorbar(im, ax=ax, location='left', pad=0.15)
|
|
158
174
|
cbar.ax.yaxis.set_ticks_position('left')
|
|
@@ -95,12 +95,50 @@ def read_mpt_file(fname: str, mode: str = 'gc', mass_mg: float = None):
|
|
|
95
95
|
"""
|
|
96
96
|
import re
|
|
97
97
|
|
|
98
|
+
# Check if this is a full EC-Lab format or a simple 2-column export
|
|
99
|
+
is_eclab_format = False
|
|
100
|
+
with open(fname, 'r', encoding='utf-8', errors='ignore') as f:
|
|
101
|
+
first_line = f.readline().strip()
|
|
102
|
+
if first_line.startswith('EC-Lab ASCII FILE'):
|
|
103
|
+
is_eclab_format = True
|
|
104
|
+
|
|
105
|
+
# Handle simple 2-column time/voltage export format (for operando time mode)
|
|
106
|
+
if not is_eclab_format and mode == 'time':
|
|
107
|
+
try:
|
|
108
|
+
# Read with tab delimiter and handle European comma decimal separator
|
|
109
|
+
with open(fname, 'r', encoding='utf-8', errors='ignore') as f:
|
|
110
|
+
# Skip header line
|
|
111
|
+
f.readline()
|
|
112
|
+
time_vals = []
|
|
113
|
+
voltage_vals = []
|
|
114
|
+
for line in f:
|
|
115
|
+
line = line.strip()
|
|
116
|
+
if not line:
|
|
117
|
+
continue
|
|
118
|
+
parts = line.split('\t')
|
|
119
|
+
if len(parts) >= 2:
|
|
120
|
+
# Replace comma with period for European locale
|
|
121
|
+
time_vals.append(float(parts[0].replace(',', '.')))
|
|
122
|
+
voltage_vals.append(float(parts[1].replace(',', '.')))
|
|
123
|
+
|
|
124
|
+
if not time_vals:
|
|
125
|
+
raise ValueError("No data found in file")
|
|
126
|
+
|
|
127
|
+
time_s = np.array(time_vals)
|
|
128
|
+
voltage_v = np.array(voltage_vals)
|
|
129
|
+
current_mA = np.zeros_like(time_s) # No current data in simple format
|
|
130
|
+
return time_s, voltage_v, current_mA
|
|
131
|
+
except Exception as e:
|
|
132
|
+
raise ValueError(f"Failed to read simple .mpt format: {e}")
|
|
133
|
+
|
|
134
|
+
# For non-time modes or EC-Lab format, require full EC-Lab format
|
|
135
|
+
if not is_eclab_format:
|
|
136
|
+
raise ValueError(f"Not a valid EC-Lab .mpt file: {fname}")
|
|
137
|
+
|
|
98
138
|
# Read header to find number of header lines
|
|
99
139
|
header_lines = 0
|
|
100
140
|
with open(fname, 'r', encoding='utf-8', errors='ignore') as f:
|
|
101
141
|
first_line = f.readline().strip()
|
|
102
|
-
if not first_line.startswith('EC-Lab ASCII FILE'):
|
|
103
|
-
raise ValueError(f"Not a valid EC-Lab .mpt file: {fname}")
|
|
104
142
|
|
|
105
143
|
# Find header lines count
|
|
106
144
|
for line in f:
|
|
@@ -135,12 +135,24 @@ def dump_session(
|
|
|
135
135
|
for side in ('top', 'bottom', 'left', 'right'):
|
|
136
136
|
sp_obj = axis.spines.get(side)
|
|
137
137
|
prefix = {'top': 't', 'bottom': 'b', 'left': 'l', 'right': 'r'}[side]
|
|
138
|
+
# Consistent tick/title state logic for all sides
|
|
139
|
+
if side == 'left':
|
|
140
|
+
ylabel_text = axis.get_ylabel()
|
|
141
|
+
title_state = bool(ylabel_text)
|
|
142
|
+
elif side == 'bottom':
|
|
143
|
+
title_state = bool(axis.get_xlabel())
|
|
144
|
+
elif side == 'top':
|
|
145
|
+
title_state = bool(getattr(axis, '_top_xlabel_on', False))
|
|
146
|
+
elif side == 'right':
|
|
147
|
+
title_state = bool(getattr(axis, '_right_ylabel_on', False))
|
|
148
|
+
else:
|
|
149
|
+
title_state = False
|
|
138
150
|
wasd[side] = {
|
|
139
151
|
'spine': bool(sp_obj.get_visible() if sp_obj else False),
|
|
140
152
|
'ticks': bool(ts.get(f'{prefix}_ticks', ts.get({'top':'tx','bottom':'bx','left':'ly','right':'ry'}[side], side=='bottom' or side=='left'))),
|
|
141
153
|
'minor': bool(ts.get(f'm{prefix}x' if side in ('top','bottom') else f'm{prefix}y', False)),
|
|
142
154
|
'labels': bool(ts.get(f'{prefix}_labels', ts.get({'top':'tx','bottom':'bx','left':'ly','right':'ry'}[side], side=='bottom' or side=='left'))),
|
|
143
|
-
'title':
|
|
155
|
+
'title': title_state,
|
|
144
156
|
}
|
|
145
157
|
return wasd
|
|
146
158
|
|
|
@@ -321,12 +333,26 @@ def dump_operando_session(
|
|
|
321
333
|
for side in ('top', 'bottom', 'left', 'right'):
|
|
322
334
|
sp = axis.spines.get(side)
|
|
323
335
|
prefix = {'top': 't', 'bottom': 'b', 'left': 'l', 'right': 'r'}[side]
|
|
336
|
+
# For 'left' side ylabel: check if it's currently visible (has text)
|
|
337
|
+
# If hidden but has stored text, the title state should be False (hidden)
|
|
338
|
+
if side == 'left':
|
|
339
|
+
ylabel_text = axis.get_ylabel()
|
|
340
|
+
title_state = bool(ylabel_text) # True only if currently visible with text
|
|
341
|
+
elif side == 'bottom':
|
|
342
|
+
title_state = bool(axis.get_xlabel())
|
|
343
|
+
elif side == 'top':
|
|
344
|
+
title_state = bool(getattr(axis, '_top_xlabel_on', False))
|
|
345
|
+
elif side == 'right':
|
|
346
|
+
title_state = bool(getattr(axis, '_right_ylabel_on', False))
|
|
347
|
+
else:
|
|
348
|
+
title_state = False
|
|
349
|
+
|
|
324
350
|
wasd[side] = {
|
|
325
351
|
'spine': bool(sp.get_visible() if sp else False),
|
|
326
352
|
'ticks': bool(ts.get(f'{prefix}_ticks', ts.get({'top':'tx','bottom':'bx','left':'ly','right':'ry'}[side], side=='bottom' or side=='left'))),
|
|
327
353
|
'minor': bool(ts.get(f'm{prefix}x' if side in ('top','bottom') else f'm{prefix}y', False)),
|
|
328
354
|
'labels': bool(ts.get(f'{prefix}_labels', ts.get({'top':'tx','bottom':'bx','left':'ly','right':'ry'}[side], side=='bottom' or side=='left'))),
|
|
329
|
-
'title':
|
|
355
|
+
'title': title_state,
|
|
330
356
|
}
|
|
331
357
|
return wasd
|
|
332
358
|
|
|
@@ -414,6 +440,7 @@ def dump_operando_session(
|
|
|
414
440
|
'wasd_state': ec_wasd_state,
|
|
415
441
|
'spines': ec_spines,
|
|
416
442
|
'ticks': {'widths': ec_ticks},
|
|
443
|
+
'stored_ylabel': getattr(ec_ax, '_stored_ylabel', None), # Save hidden ylabel text
|
|
417
444
|
}
|
|
418
445
|
|
|
419
446
|
sess = {
|
|
@@ -440,6 +467,7 @@ def dump_operando_session(
|
|
|
440
467
|
'wasd_state': op_wasd_state,
|
|
441
468
|
'spines': op_spines,
|
|
442
469
|
'ticks': {'widths': op_ticks},
|
|
470
|
+
'stored_ylabel': getattr(ax, '_stored_ylabel', None), # Save hidden ylabel text
|
|
443
471
|
},
|
|
444
472
|
'colorbar': {
|
|
445
473
|
'label': cb_label,
|
|
@@ -549,6 +577,11 @@ def load_operando_session(filename: str):
|
|
|
549
577
|
# Persist custom labels
|
|
550
578
|
setattr(ax, '_custom_labels', dict(op.get('custom_labels', {'x': None, 'y': None})))
|
|
551
579
|
|
|
580
|
+
# Restore stored ylabel if present (for cases where ylabel was hidden with a5)
|
|
581
|
+
stored_ylabel = op.get('stored_ylabel')
|
|
582
|
+
if stored_ylabel is not None:
|
|
583
|
+
setattr(ax, '_stored_ylabel', stored_ylabel)
|
|
584
|
+
|
|
552
585
|
# Apply operando WASD state if version 2+
|
|
553
586
|
version = sess.get('version', 1)
|
|
554
587
|
if version >= 2:
|
|
@@ -750,6 +783,11 @@ def load_operando_session(filename: str):
|
|
|
750
783
|
except Exception:
|
|
751
784
|
pass
|
|
752
785
|
|
|
786
|
+
# Restore stored ylabel if present (for cases where ylabel was hidden)
|
|
787
|
+
stored_ylabel = ec.get('stored_ylabel')
|
|
788
|
+
if stored_ylabel is not None:
|
|
789
|
+
setattr(ec_ax, '_stored_ylabel', stored_ylabel)
|
|
790
|
+
|
|
753
791
|
# Apply EC WASD state if version 2+
|
|
754
792
|
if version >= 2:
|
|
755
793
|
ec_wasd = ec.get('wasd_state')
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "batplot"
|
|
7
|
-
version = "1.3.
|
|
7
|
+
version = "1.3.8"
|
|
8
8
|
description = "Interactive plotting for XRD, PDF, and XAS data (.xye, .xy, .qye, .dat, .csv, .gr, .nor, .chik, .chir)"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Tian Dai", email = "tianda@uio.no" }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|