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.
Files changed (32) hide show
  1. {batplot-1.3.6 → batplot-1.3.8}/PKG-INFO +1 -1
  2. {batplot-1.3.6 → batplot-1.3.8}/batplot/__init__.py +1 -1
  3. {batplot-1.3.6 → batplot-1.3.8}/batplot/batplot.py +17 -1
  4. {batplot-1.3.6 → batplot-1.3.8}/batplot/operando.py +22 -6
  5. {batplot-1.3.6 → batplot-1.3.8}/batplot/readers.py +40 -2
  6. {batplot-1.3.6 → batplot-1.3.8}/batplot/session.py +40 -2
  7. {batplot-1.3.6 → batplot-1.3.8}/batplot.egg-info/PKG-INFO +1 -1
  8. {batplot-1.3.6 → batplot-1.3.8}/pyproject.toml +1 -1
  9. {batplot-1.3.6 → batplot-1.3.8}/LICENSE +0 -0
  10. {batplot-1.3.6 → batplot-1.3.8}/README.md +0 -0
  11. {batplot-1.3.6 → batplot-1.3.8}/batplot/args.py +0 -0
  12. {batplot-1.3.6 → batplot-1.3.8}/batplot/batch.py +0 -0
  13. {batplot-1.3.6 → batplot-1.3.8}/batplot/batplot_new.py +0 -0
  14. {batplot-1.3.6 → batplot-1.3.8}/batplot/cif.py +0 -0
  15. {batplot-1.3.6 → batplot-1.3.8}/batplot/cli.py +0 -0
  16. {batplot-1.3.6 → batplot-1.3.8}/batplot/converters.py +0 -0
  17. {batplot-1.3.6 → batplot-1.3.8}/batplot/cpc_interactive.py +0 -0
  18. {batplot-1.3.6 → batplot-1.3.8}/batplot/electrochem_interactive.py +0 -0
  19. {batplot-1.3.6 → batplot-1.3.8}/batplot/interactive.py +0 -0
  20. {batplot-1.3.6 → batplot-1.3.8}/batplot/modes.py +0 -0
  21. {batplot-1.3.6 → batplot-1.3.8}/batplot/operando_ec_interactive.py +0 -0
  22. {batplot-1.3.6 → batplot-1.3.8}/batplot/plotting.py +0 -0
  23. {batplot-1.3.6 → batplot-1.3.8}/batplot/style.py +0 -0
  24. {batplot-1.3.6 → batplot-1.3.8}/batplot/ui.py +0 -0
  25. {batplot-1.3.6 → batplot-1.3.8}/batplot/utils.py +0 -0
  26. {batplot-1.3.6 → batplot-1.3.8}/batplot.egg-info/SOURCES.txt +0 -0
  27. {batplot-1.3.6 → batplot-1.3.8}/batplot.egg-info/dependency_links.txt +0 -0
  28. {batplot-1.3.6 → batplot-1.3.8}/batplot.egg-info/entry_points.txt +0 -0
  29. {batplot-1.3.6 → batplot-1.3.8}/batplot.egg-info/requires.txt +0 -0
  30. {batplot-1.3.6 → batplot-1.3.8}/batplot.egg-info/top_level.txt +0 -0
  31. {batplot-1.3.6 → batplot-1.3.8}/setup.cfg +0 -0
  32. {batplot-1.3.6 → batplot-1.3.8}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.3.6
3
+ Version: 1.3.8
4
4
  Summary: Interactive plotting for XRD, PDF, and XAS data (.xye, .xy, .qye, .dat, .csv, .gr, .nor, .chik, .chir)
5
5
  Author-email: Tian Dai <tianda@uio.no>
6
6
  License: MIT License
@@ -1,5 +1,5 @@
1
1
  """batplot: Interactive plotting for battery data visualization."""
2
2
 
3
- __version__ = "1.3.6"
3
+ __version__ = "1.3.8"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -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
- mpt_files = sorted([f for f in p.iterdir() if f.suffix.lower() == ".mpt"]) # pick first if present
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
- # Top-to-down visual order (scan 0 at top) -> origin='upper'
155
- im = ax.imshow(Zm, aspect='auto', origin='upper', extent=extent, cmap='viridis', interpolation='nearest')
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': bool(getattr(axis, '_top_xlabel_on' if side=='top' else '_right_ylabel_on' if side=='right' else '', False)) if side in ('top','right') else bool(axis.get_xlabel() if side=='bottom' else axis.get_ylabel() if side=='left' else False),
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': bool(getattr(axis, '_top_xlabel_on' if side=='top' else '_right_ylabel_on' if side=='right' else '', False)) if side in ('top','right') else bool(axis.get_xlabel() if side=='bottom' else axis.get_ylabel() if side=='left' else False),
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')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.3.6
3
+ Version: 1.3.8
4
4
  Summary: Interactive plotting for XRD, PDF, and XAS data (.xye, .xy, .qye, .dat, .csv, .gr, .nor, .chik, .chir)
5
5
  Author-email: Tian Dai <tianda@uio.no>
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "batplot"
7
- version = "1.3.6"
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