batplot 1.4.1__tar.gz → 1.4.3__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.4.1 → batplot-1.4.3}/PKG-INFO +4 -4
  2. {batplot-1.4.1 → batplot-1.4.3}/README.md +3 -3
  3. {batplot-1.4.1 → batplot-1.4.3}/batplot/__init__.py +1 -1
  4. {batplot-1.4.1 → batplot-1.4.3}/batplot/args.py +18 -14
  5. {batplot-1.4.1 → batplot-1.4.3}/batplot/batch.py +5 -0
  6. {batplot-1.4.1 → batplot-1.4.3}/batplot/batplot.py +8 -3
  7. {batplot-1.4.1 → batplot-1.4.3}/batplot/operando.py +90 -25
  8. {batplot-1.4.1 → batplot-1.4.3}/batplot/readers.py +168 -27
  9. {batplot-1.4.1 → batplot-1.4.3}/batplot.egg-info/PKG-INFO +4 -4
  10. {batplot-1.4.1 → batplot-1.4.3}/pyproject.toml +1 -1
  11. {batplot-1.4.1 → batplot-1.4.3}/LICENSE +0 -0
  12. {batplot-1.4.1 → batplot-1.4.3}/batplot/batplot_new.py +0 -0
  13. {batplot-1.4.1 → batplot-1.4.3}/batplot/cif.py +0 -0
  14. {batplot-1.4.1 → batplot-1.4.3}/batplot/cli.py +0 -0
  15. {batplot-1.4.1 → batplot-1.4.3}/batplot/converters.py +0 -0
  16. {batplot-1.4.1 → batplot-1.4.3}/batplot/cpc_interactive.py +0 -0
  17. {batplot-1.4.1 → batplot-1.4.3}/batplot/electrochem_interactive.py +0 -0
  18. {batplot-1.4.1 → batplot-1.4.3}/batplot/interactive.py +0 -0
  19. {batplot-1.4.1 → batplot-1.4.3}/batplot/modes.py +0 -0
  20. {batplot-1.4.1 → batplot-1.4.3}/batplot/operando_ec_interactive.py +0 -0
  21. {batplot-1.4.1 → batplot-1.4.3}/batplot/plotting.py +0 -0
  22. {batplot-1.4.1 → batplot-1.4.3}/batplot/session.py +0 -0
  23. {batplot-1.4.1 → batplot-1.4.3}/batplot/style.py +0 -0
  24. {batplot-1.4.1 → batplot-1.4.3}/batplot/ui.py +0 -0
  25. {batplot-1.4.1 → batplot-1.4.3}/batplot/utils.py +0 -0
  26. {batplot-1.4.1 → batplot-1.4.3}/batplot.egg-info/SOURCES.txt +0 -0
  27. {batplot-1.4.1 → batplot-1.4.3}/batplot.egg-info/dependency_links.txt +0 -0
  28. {batplot-1.4.1 → batplot-1.4.3}/batplot.egg-info/entry_points.txt +0 -0
  29. {batplot-1.4.1 → batplot-1.4.3}/batplot.egg-info/requires.txt +0 -0
  30. {batplot-1.4.1 → batplot-1.4.3}/batplot.egg-info/top_level.txt +0 -0
  31. {batplot-1.4.1 → batplot-1.4.3}/setup.cfg +0 -0
  32. {batplot-1.4.1 → batplot-1.4.3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.4.1
3
+ Version: 1.4.3
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
@@ -72,7 +72,7 @@ pip install batplot
72
72
 
73
73
  ## Quick Start
74
74
 
75
- ### XRD / PDF / XAS
75
+ ### XRD / PDF / XAS and much more
76
76
 
77
77
  ```bash
78
78
  # Single diffraction pattern in 2theta
@@ -146,7 +146,7 @@ batplot --operando --interactive
146
146
 
147
147
  | Type | Formats |
148
148
  |------|---------|
149
- | **Electrochemistry** | `.csv` (Neware), `.mpt` (Biologic) |
149
+ | **Electrochemistry** | `.csv` (Neware), `.mpt` (Biologic), `.xlsx` (Landt/Lanhe) CPC only |
150
150
  | **XRD / PDF** | `.xye`, `.xy`, `.qye`, `.dat` |
151
151
  | **XAS** | `.nor`, `.chik`, `.chir` |
152
152
  | **Others** | `user defined` (plot first two columns as x and y) |
@@ -181,4 +181,4 @@ University of Oslo
181
181
 
182
182
  **GitHub**: https://github.com/tiandai-chem/batplot
183
183
 
184
- **Subscribe for Updates**: Join batplot-lab@kjemi.uio.no for updates, feature announcements, and community feedback
184
+ **Subscribe for Updates**: Join batplot-lab@kjemi.uio.no for updates, feature announcements, and community feedback. If you are not from UiO, send an email to sympa@kjemi.uio.no with the exact subject line with your name: "subscribe batplot-lab@kjemi.uio.no your-name"
@@ -22,7 +22,7 @@ pip install batplot
22
22
 
23
23
  ## Quick Start
24
24
 
25
- ### XRD / PDF / XAS
25
+ ### XRD / PDF / XAS and much more
26
26
 
27
27
  ```bash
28
28
  # Single diffraction pattern in 2theta
@@ -96,7 +96,7 @@ batplot --operando --interactive
96
96
 
97
97
  | Type | Formats |
98
98
  |------|---------|
99
- | **Electrochemistry** | `.csv` (Neware), `.mpt` (Biologic) |
99
+ | **Electrochemistry** | `.csv` (Neware), `.mpt` (Biologic), `.xlsx` (Landt/Lanhe) CPC only |
100
100
  | **XRD / PDF** | `.xye`, `.xy`, `.qye`, `.dat` |
101
101
  | **XAS** | `.nor`, `.chik`, `.chir` |
102
102
  | **Others** | `user defined` (plot first two columns as x and y) |
@@ -131,4 +131,4 @@ University of Oslo
131
131
 
132
132
  **GitHub**: https://github.com/tiandai-chem/batplot
133
133
 
134
- **Subscribe for Updates**: Join batplot-lab@kjemi.uio.no for updates, feature announcements, and community feedback
134
+ **Subscribe for Updates**: Join batplot-lab@kjemi.uio.no for updates, feature announcements, and community feedback. If you are not from UiO, send an email to sympa@kjemi.uio.no with the exact subject line with your name: "subscribe batplot-lab@kjemi.uio.no your-name"
@@ -1,5 +1,5 @@
1
1
  """batplot: Interactive plotting for battery data visualization."""
2
2
 
3
- __version__ = "1.3.12"
3
+ __version__ = "1.4.3"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -104,15 +104,16 @@ def _print_general_help() -> None:
104
104
  " • Batch styling: apply .bps/.bpsg files to all exports (use --all flag)\n"
105
105
  " • Format option: use --format png/pdf/jpg/etc to change export format\n\n"
106
106
 
107
- "More help:\n"
108
- " batplot -h xy # XY file plotting guide\n"
109
- " batplot -h ec # Electrochemistry (GC/dQdV/CV/CPC) guide\n"
110
- " batplot -h op # Operando guide\n\n"
111
- "Contact & Updates:\n"
112
- " Subscribe to batplot-lab@kjemi.uio.no for updates\n"
113
- " (If you are not from UiO, send an email to sympa@kjemi.uio.no with the subject line \"subscribe batplot-lab@kjemi.uio.no your-name\")\n"
114
- " GitHub: https://github.com/tiandai-chem/batplot\n"
115
- " Email: tianda@uio.no\n"
107
+ "More help:\n"
108
+ " batplot -h xy # XY file plotting guide\n"
109
+ " batplot -h ec # Electrochemistry (GC/dQdV/CV/CPC) guide\n"
110
+ " batplot -h op # Operando guide\n"
111
+ " Manual: https://github.com/tiandai-chem/batplot/blob/main/USER_MANUAL.md\n\n"
112
+ "Contact & Updates:\n"
113
+ " Subscribe to batplot-lab@kjemi.uio.no for updates\n"
114
+ " (If you are not from UiO, send an email to sympa@kjemi.uio.no with the subject line \"subscribe batplot-lab@kjemi.uio.no your-name\")\n"
115
+ " GitHub: https://github.com/tiandai-chem/batplot\n"
116
+ " Email: tianda@uio.no\n"
116
117
  )
117
118
  _print_help(msg)
118
119
 
@@ -180,17 +181,20 @@ def _print_ec_help() -> None:
180
181
  "Cyclic voltammetry (CV) from .mpt or .txt: plots voltage vs current for each cycle.\n"
181
182
  " batplot --cv file.mpt\n"
182
183
  " batplot --cv file.txt\n\n"
183
- "Capacity-per-cycle (CPC) with coulombic efficiency from .csv or .mpt.\n"
184
+ "Capacity-per-cycle (CPC) with coulombic efficiency from .csv, .xlsx, or .mpt.\n"
184
185
  "Supports multiple files with individual color customization:\n"
185
- " batplot --cpc file.csv\n"
186
- " batplot --cpc file.mpt --mass 1.2\n"
187
- " batplot --cpc file1.csv file2.csv file3.mpt --mass 1.2 --interactive\n"
186
+ " batplot --cpc file.csv # Neware CSV\n"
187
+ " batplot --cpc file.xlsx # Landt/Lanhe Excel (Chinese tester)\n"
188
+ " batplot --cpc file.mpt --mass 1.2 # Biologic MPT\n"
189
+ " batplot --cpc file1.csv file2.xlsx file3.mpt --mass 1.2 --interactive\n\n"
190
+ "Excel support: Landt/Lanhe (蓝电/蓝河) .xlsx files with Chinese headers:\n"
191
+ " Expected structure: Row 1=filename, Row 2=headers, Row 3+=data\n"
188
192
  "Batch mode: Process all files and export to Figures/ subdirectory (default: .svg).\n"
189
193
  " batplot --gc --all --mass 7.0 # All .mpt/.csv files (.mpt requires --mass)\n"
190
194
  " batplot --gc --all --mass 7 --format png # Export as .png instead of .svg\n"
191
195
  " batplot --cv --all # All .mpt/.txt files (CV mode)\n"
192
196
  " batplot --dqdv --all # All .csv files (dQdV mode)\n"
193
- " batplot --cpc --all --mass 5.4 # All .mpt/.csv files (.mpt requires --mass)\n"
197
+ " batplot --cpc --all --mass 5.4 # All .mpt/.csv/.xlsx (.mpt requires --mass)\n"
194
198
  " batplot --gc /path/to/folder --mass 6 # Process specific directory\n\n"
195
199
  "Batch mode with style/geometry: Apply .bps/.bpsg files to all batch exports.\n"
196
200
  " batplot --all --gc style.bps --mass 7 # Apply style to all GC plots\n"
@@ -344,6 +344,11 @@ def batch_process(directory: str, args):
344
344
  # Transparent background for SVG exports
345
345
  _, _ext = os.path.splitext(target)
346
346
  if _ext.lower() == '.svg':
347
+ # Fix for Affinity Designer/Photo compatibility issues
348
+ # Use 'none' to embed fonts as text (not paths) - prevents phantom labels
349
+ # Set hashsalt to empty to avoid duplicate text elements
350
+ plt.rcParams['svg.fonttype'] = 'none'
351
+ plt.rcParams['svg.hashsalt'] = None
347
352
  try:
348
353
  _fig_fc = fig_b.get_facecolor()
349
354
  except Exception:
@@ -538,6 +538,11 @@ def batplot_main() -> int:
538
538
  # Transparent background for SVG exports
539
539
  _, _ext = _os.path.splitext(outname)
540
540
  if _ext.lower() == '.svg':
541
+ # Fix for Affinity Designer/Photo compatibility issues
542
+ # Use 'none' to embed fonts as text (not paths) - prevents phantom labels
543
+ # Set hashsalt to empty to avoid duplicate text elements
544
+ _plt.rcParams['svg.fonttype'] = 'none'
545
+ _plt.rcParams['svg.hashsalt'] = None
541
546
  try:
542
547
  _fig_fc = fig.get_facecolor()
543
548
  except Exception:
@@ -625,7 +630,7 @@ def batplot_main() -> int:
625
630
  import numpy as _np
626
631
 
627
632
  if len(args.files) < 1:
628
- print("CPC mode: provide at least one file (.csv or .mpt).")
633
+ print("CPC mode: provide at least one file (.csv, .xlsx, or .mpt).")
629
634
  exit(1)
630
635
 
631
636
  # Process multiple files
@@ -651,7 +656,7 @@ def batplot_main() -> int:
651
656
  file_basename = _os.path.basename(ec_file)
652
657
 
653
658
  try:
654
- if ext == '.csv':
659
+ if ext in ['.csv', '.xlsx', '.xls']:
655
660
  cap_x, voltage, cycles, chg_mask, dchg_mask = read_ec_csv_file(ec_file, prefer_specific=True)
656
661
  cyc = _np.array(cycles, dtype=int)
657
662
  unique_cycles = _np.unique(cyc)
@@ -683,7 +688,7 @@ def batplot_main() -> int:
683
688
  continue
684
689
  cyc_nums, cap_charge, cap_discharge, eff = read_mpt_file(ec_file, mode='cpc', mass_mg=mass_mg)
685
690
  else:
686
- print(f"Skipped {file_basename}: unsupported format (must be .csv or .mpt)")
691
+ print(f"Skipped {file_basename}: unsupported format (must be .csv, .xlsx, or .mpt)")
687
692
  continue
688
693
 
689
694
  # Assign colors: distinct hue for each file
@@ -211,33 +211,98 @@ def plot_operando_folder(folder: str, args) -> Tuple[plt.Figure, plt.Axes, Dict[
211
211
  if has_ec:
212
212
  try:
213
213
  ec_path = mpt_files[0]
214
- # Read time series from .mpt (now returns labels too)
215
- result = read_mpt_file(str(ec_path), mode='time')
216
214
 
217
- # Check if we got labels (5 elements) or old format (3 elements)
218
- if len(result) == 5:
219
- x_data, y_data, current_mA, x_label, y_label = result
220
- # For EC-Lab files: x_label='Time (h)', y_label='Voltage (V)'
221
- # EC-Lab returns time in seconds, needs conversion to hours
222
- # operando plots with voltage on X-axis and time on Y-axis
223
- if x_label == 'Time (h)' and y_label == 'Voltage (V)':
224
- # EC-Lab file: convert time to hours and swap axes
225
- time_h = np.asarray(x_data, float) / 3600.0
226
- voltage_v = np.asarray(y_data, float)
227
- x_data = voltage_v
228
- y_data = time_h
229
- x_label = 'Voltage (V)'
230
- y_label = 'Time (h)'
231
- else:
232
- # Simple file: use raw data as-is, keep original labels
233
- x_data = np.asarray(x_data, float)
234
- y_data = np.asarray(y_data, float)
215
+ # Check if user specified custom columns via --readcolmpt
216
+ readcol_mpt = None
217
+ if hasattr(args, 'readcol_by_ext') and '.mpt' in args.readcol_by_ext:
218
+ readcol_mpt = args.readcol_by_ext['.mpt']
219
+
220
+ if readcol_mpt:
221
+ # User explicitly specified columns - respect their choice
222
+ data = robust_loadtxt_skipheader(str(ec_path))
223
+ if data.ndim == 1:
224
+ data = data.reshape(1, -1)
225
+ if data.shape[1] < 2:
226
+ raise ValueError(f"MPT file {ec_path.name} has insufficient columns")
227
+
228
+ # Apply column selection (1-indexed -> 0-indexed)
229
+ x_col, y_col = readcol_mpt
230
+ x_col_idx = x_col - 1
231
+ y_col_idx = y_col - 1
232
+ if x_col_idx < 0 or x_col_idx >= data.shape[1]:
233
+ raise ValueError(f"X column {x_col} out of range in {ec_path.name} (has {data.shape[1]} columns)")
234
+ if y_col_idx < 0 or y_col_idx >= data.shape[1]:
235
+ raise ValueError(f"Y column {y_col} out of range in {ec_path.name} (has {data.shape[1]} columns)")
236
+
237
+ x_data = data[:, x_col_idx]
238
+ y_data = data[:, y_col_idx]
239
+ current_mA = None
240
+ # User-specified: plot exactly as specified (X on x-axis, Y on y-axis)
241
+ x_label = f'Column {x_col}'
242
+ y_label = f'Column {y_col}'
235
243
  else:
236
- # Old format compatibility (shouldn't happen anymore)
237
- x_data, y_data, current_mA = result
238
- x_data = np.asarray(y_data, float)
239
- y_data = np.asarray(x_data, float) / 3600.0
240
- x_label, y_label = 'Voltage (V)', 'Time (h)'
244
+ # Auto-detect format: Read time series from .mpt
245
+ result = read_mpt_file(str(ec_path), mode='time')
246
+
247
+ # Check if we got labels (5 elements) or old format (3 elements)
248
+ if len(result) == 5:
249
+ x_data, y_data, current_mA, x_label, y_label = result
250
+ # For EC-Lab files: x_label='Time (h)', y_label='Voltage (V)'
251
+ # For simple files: x_label could be 'Time(h)', 'time', etc.
252
+ # EC-Lab returns time in seconds, needs conversion to hours
253
+ # operando plots with voltage on X-axis and time on Y-axis
254
+
255
+ # Check if labels indicate time/voltage data (flexible matching)
256
+ x_lower = x_label.lower().replace(' ', '').replace('_', '')
257
+ y_lower = y_label.lower().replace(' ', '').replace('_', '')
258
+ has_time_in_x = 'time' in x_lower
259
+ has_voltage_in_x = 'voltage' in x_lower or 'ewe' in x_lower
260
+ has_time_in_y = 'time' in y_lower
261
+ has_voltage_in_y = 'voltage' in y_lower or 'ewe' in y_lower
262
+
263
+ is_time_voltage = (has_time_in_x or has_time_in_y) and (has_voltage_in_x or has_voltage_in_y)
264
+
265
+ if x_label == 'Time (h)' and y_label == 'Voltage (V)':
266
+ # EC-Lab file: convert time to hours and swap axes
267
+ time_h = np.asarray(x_data, float) / 3600.0
268
+ voltage_v = np.asarray(y_data, float)
269
+ x_data = voltage_v
270
+ y_data = time_h
271
+ x_label = 'Voltage (V)'
272
+ y_label = 'Time (h)'
273
+ elif is_time_voltage:
274
+ # Simple file with time/voltage columns
275
+ # Determine which column is which, then arrange: voltage on X, time on Y
276
+ if has_time_in_x and has_voltage_in_y:
277
+ # Columns are: Time, Voltage -> swap to Voltage, Time
278
+ time_h = np.asarray(x_data, float)
279
+ voltage_v = np.asarray(y_data, float)
280
+ x_data = voltage_v
281
+ y_data = time_h
282
+ x_label = 'Voltage (V)'
283
+ y_label = 'Time (h)'
284
+ elif has_voltage_in_x and has_time_in_y:
285
+ # Columns are: Voltage, Time -> already correct order
286
+ voltage_v = np.asarray(x_data, float)
287
+ time_h = np.asarray(y_data, float)
288
+ x_data = voltage_v
289
+ y_data = time_h
290
+ x_label = 'Voltage (V)'
291
+ y_label = 'Time (h)'
292
+ else:
293
+ # Ambiguous or both in same column - default behavior
294
+ x_data = np.asarray(x_data, float)
295
+ y_data = np.asarray(y_data, float)
296
+ else:
297
+ # Generic file: use raw data as-is, keep original labels
298
+ x_data = np.asarray(x_data, float)
299
+ y_data = np.asarray(y_data, float)
300
+ else:
301
+ # Old format compatibility (shouldn't happen anymore)
302
+ x_data, y_data, current_mA = result
303
+ x_data = np.asarray(y_data, float)
304
+ y_data = np.asarray(x_data, float) / 3600.0
305
+ x_label, y_label = 'Voltage (V)', 'Time (h)'
241
306
 
242
307
  # Add the EC axes on the right
243
308
  ec_ax = fig.add_subplot(gs[0, 1])
@@ -6,6 +6,53 @@ import numpy as np
6
6
  from typing import Tuple
7
7
 
8
8
 
9
+ def read_excel_to_csv_like(fname: str, header_row: int = 2, data_start_row: int = 3) -> Tuple[list, list]:
10
+ """Read Excel file and convert to CSV-like structure for batplot.
11
+
12
+ This is designed for Chinese cycler data Excel files where:
13
+ - Row 1: File/sample name
14
+ - Row 2: Column headers
15
+ - Row 3+: Data
16
+
17
+ Args:
18
+ fname: Path to Excel file (.xlsx)
19
+ header_row: Row number containing headers (1-indexed, default=2)
20
+ data_start_row: First row containing data (1-indexed, default=3)
21
+
22
+ Returns:
23
+ Tuple of (header_list, rows_list) compatible with CSV processing
24
+ """
25
+ try:
26
+ import openpyxl
27
+ except ImportError:
28
+ raise ImportError("openpyxl is required to read Excel files. Install with: pip install openpyxl")
29
+
30
+ wb = openpyxl.load_workbook(fname, read_only=True, data_only=True)
31
+ ws = wb.active
32
+
33
+ # Read header row
34
+ header = []
35
+ for cell in ws[header_row]:
36
+ header.append(str(cell.value) if cell.value is not None else '')
37
+
38
+ # Read data rows
39
+ rows = []
40
+ for row in ws.iter_rows(min_row=data_start_row, values_only=True):
41
+ # Convert row to list of strings (handle None, datetime, etc.)
42
+ row_data = []
43
+ for val in row:
44
+ if val is None:
45
+ row_data.append('')
46
+ elif isinstance(val, (int, float)):
47
+ row_data.append(str(val))
48
+ else:
49
+ row_data.append(str(val))
50
+ rows.append(row_data)
51
+
52
+ wb.close()
53
+ return header, rows
54
+
55
+
9
56
  def read_csv_file(fname: str):
10
57
  for delim in [",", ";", "\t"]:
11
58
  try:
@@ -583,7 +630,7 @@ def read_biologic_txt_file(fname: str, mode: str = 'cv') -> Tuple[np.ndarray, np
583
630
 
584
631
 
585
632
  def read_ec_csv_file(fname: str, prefer_specific: bool = True) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
586
- """Read a cycler-exported CSV (e.g., Neware-like) and extract arrays for GC plotting.
633
+ """Read a cycler-exported CSV or Excel file (e.g., Neware-like) and extract arrays for GC plotting.
587
634
 
588
635
  The CSV is expected to have a two-line header where the second header line starts
589
636
  with an empty first cell. Columns of interest include:
@@ -594,8 +641,13 @@ def read_ec_csv_file(fname: str, prefer_specific: bool = True) -> Tuple[np.ndarr
594
641
  * absolute: 'Capacity(mAh)' and optionally split 'Chg. Cap.(mAh)' / 'DChg. Cap.(mAh)'
595
642
  * specific: 'Spec. Cap.(mAh/g)', 'Chg. Spec. Cap.(mAh/g)', 'DChg. Spec. Cap.(mAh/g)'
596
643
 
644
+ Excel files (.xlsx) are supported and expected to have:
645
+ - Row 1: File/sample name (ignored)
646
+ - Row 2: Column headers (Chinese or English)
647
+ - Row 3+: Data
648
+
597
649
  Args:
598
- fname: Path to the CSV file.
650
+ fname: Path to the CSV or Excel file.
599
651
  prefer_specific: If True, will use specific capacities (mAh g⁻¹) if available; otherwise
600
652
  uses absolute capacities (mAh) when present.
601
653
 
@@ -612,26 +664,54 @@ def read_ec_csv_file(fname: str, prefer_specific: bool = True) -> Tuple[np.ndarr
612
664
  discharge_mask: boolean mask indicating discharging segments (inverse of charge_mask)
613
665
  """
614
666
  import csv
615
-
616
- # Read first two rows to compose header
617
- with open(fname, newline='', encoding='utf-8', errors='ignore') as f:
618
- r = csv.reader(f)
619
- try:
620
- r1 = next(r)
621
- r2 = next(r)
622
- except StopIteration:
623
- raise ValueError(f"CSV '{fname}' is empty or missing header rows")
624
- # If second line begins with an empty first cell, treat it as a continuation of header.
625
- if len(r2) > 0 and (r2[0] == '' or str(r2[0]).strip() == ''):
626
- header = [c.strip() for c in r1] + [c.strip() for c in r2[1:]]
627
- rows = list(r)
628
- else:
629
- # Single-line header: r2 is the first data row; include it back in rows
630
- header = [c.strip() for c in r1]
631
- rows = [r2] + list(r)
667
+ import os
668
+
669
+ # Check if file is Excel
670
+ _, ext = os.path.splitext(fname)
671
+ if ext.lower() in ['.xlsx', '.xls']:
672
+ # Read Excel file
673
+ header, rows = read_excel_to_csv_like(fname)
674
+ else:
675
+ # Read first two rows to compose header from CSV
676
+ with open(fname, newline='', encoding='utf-8', errors='ignore') as f:
677
+ r = csv.reader(f)
678
+ try:
679
+ r1 = next(r)
680
+ r2 = next(r)
681
+ except StopIteration:
682
+ raise ValueError(f"CSV '{fname}' is empty or missing header rows")
683
+ # If second line begins with an empty first cell, treat it as a continuation of header.
684
+ if len(r2) > 0 and (r2[0] == '' or str(r2[0]).strip() == ''):
685
+ header = [c.strip() for c in r1] + [c.strip() for c in r2[1:]]
686
+ rows = list(r)
687
+ else:
688
+ # Single-line header: r2 is the first data row; include it back in rows
689
+ header = [c.strip() for c in r1]
690
+ rows = [r2] + list(r)
632
691
 
633
692
  # Build fast name->index map (case-insensitive match on exact header text)
634
693
  name_to_idx = {h: i for i, h in enumerate(header)}
694
+
695
+ # Chinese to English column name mappings
696
+ chinese_mappings = {
697
+ '循环序号': 'Cycle Index',
698
+ '充电比容量/mAh/g': 'Chg. Spec. Cap.(mAh/g)',
699
+ '放电比容量/mAh/g': 'DChg. Spec. Cap.(mAh/g)',
700
+ '充电容量/mAh': 'Chg. Cap.(mAh)',
701
+ '放电容量/mAh': 'DChg. Cap.(mAh)',
702
+ '效率/%': 'Efficiency(%)',
703
+ '充电中压/V': 'Voltage(V)',
704
+ '放电中压/V': 'Voltage(V)',
705
+ '充电均压/V': 'Voltage(V)',
706
+ '放电均压/V': 'Voltage(V)',
707
+ }
708
+
709
+ # Add Chinese mappings to name_to_idx
710
+ for i, h in enumerate(header):
711
+ if h in chinese_mappings:
712
+ eng_name = chinese_mappings[h]
713
+ if eng_name not in name_to_idx:
714
+ name_to_idx[eng_name] = i
635
715
 
636
716
  def _find(name: str):
637
717
  return name_to_idx.get(name, None)
@@ -641,9 +721,7 @@ def read_ec_csv_file(fname: str, prefer_specific: bool = True) -> Tuple[np.ndarr
641
721
  i_idx = _find('Current(mA)')
642
722
  cyc_idx = _find('Cycle Index')
643
723
  step_type_idx = _find('Step Type') # Optional: explicitly indicates charge/discharge
644
- if v_idx is None or i_idx is None:
645
- raise ValueError("CSV missing required 'Voltage(V)' or 'Current(mA)' columns")
646
-
724
+
647
725
  # Capacity columns (absolute preferred unless prefer_specific True)
648
726
  cap_abs_idx = _find('Capacity(mAh)')
649
727
  cap_abs_chg_idx = _find('Chg. Cap.(mAh)')
@@ -651,6 +729,13 @@ def read_ec_csv_file(fname: str, prefer_specific: bool = True) -> Tuple[np.ndarr
651
729
  cap_spec_idx = _find('Spec. Cap.(mAh/g)')
652
730
  cap_spec_chg_idx = _find('Chg. Spec. Cap.(mAh/g)')
653
731
  cap_spec_dch_idx = _find('DChg. Spec. Cap.(mAh/g)')
732
+
733
+ # Check if this is a summary file (has capacity columns but no voltage/current)
734
+ has_capacity_cols = any([cap_abs_chg_idx, cap_abs_dch_idx, cap_spec_chg_idx, cap_spec_dch_idx])
735
+ is_summary_file = has_capacity_cols and (v_idx is None or i_idx is None)
736
+
737
+ if not is_summary_file and (v_idx is None or i_idx is None):
738
+ raise ValueError("CSV missing required 'Voltage(V)' or 'Current(mA)' columns")
654
739
 
655
740
  use_specific = False
656
741
  # Decide which flavor to use
@@ -664,16 +749,72 @@ def read_ec_csv_file(fname: str, prefer_specific: bool = True) -> Tuple[np.ndarr
664
749
 
665
750
  # Prepare arrays
666
751
  n = len(rows)
667
- voltage = np.empty(n, dtype=float)
668
- current = np.empty(n, dtype=float)
669
- cycles = np.ones(n, dtype=int)
670
- cap_x = np.full(n, np.nan, dtype=float)
671
-
752
+
672
753
  def _to_float(val: str) -> float:
673
754
  try:
674
755
  return float(val.strip()) if isinstance(val, str) else float(val)
675
756
  except Exception:
676
757
  return np.nan
758
+
759
+ # Special handling for summary files (charge/discharge capacities per cycle, no point-by-point data)
760
+ if is_summary_file:
761
+ # For summary files, create synthetic points: one charge point and one discharge point per cycle
762
+ voltage = []
763
+ current = []
764
+ cycles = []
765
+ cap_x = []
766
+ is_charge_list = []
767
+
768
+ for k, row in enumerate(rows):
769
+ if len(row) < len(header):
770
+ row = row + [''] * (len(header) - len(row))
771
+
772
+ # Get cycle number
773
+ cycle_num = 1
774
+ if cyc_idx is not None:
775
+ cval = _to_float(row[cyc_idx])
776
+ cycle_num = int(cval) if not np.isnan(cval) and cval > 0 else 1
777
+
778
+ # Get charge and discharge capacities
779
+ if use_specific:
780
+ cap_chg = _to_float(row[cap_spec_chg_idx]) if cap_spec_chg_idx is not None else 0
781
+ cap_dch = _to_float(row[cap_spec_dch_idx]) if cap_spec_dch_idx is not None else 0
782
+ else:
783
+ cap_chg = _to_float(row[cap_abs_chg_idx]) if cap_abs_chg_idx is not None else 0
784
+ cap_dch = _to_float(row[cap_abs_dch_idx]) if cap_abs_dch_idx is not None else 0
785
+
786
+ # Create charge point
787
+ if cap_chg > 0 and not np.isnan(cap_chg):
788
+ voltage.append(3.5) # Synthetic voltage
789
+ current.append(0.1) # Synthetic current
790
+ cycles.append(cycle_num)
791
+ cap_x.append(cap_chg)
792
+ is_charge_list.append(True)
793
+
794
+ # Create discharge point
795
+ if cap_dch > 0 and not np.isnan(cap_dch):
796
+ voltage.append(2.5) # Synthetic voltage
797
+ current.append(-0.1) # Synthetic current
798
+ cycles.append(cycle_num)
799
+ cap_x.append(cap_dch)
800
+ is_charge_list.append(False)
801
+
802
+ voltage = np.array(voltage, dtype=float)
803
+ current = np.array(current, dtype=float)
804
+ cycles = np.array(cycles, dtype=int)
805
+ cap_x = np.array(cap_x, dtype=float)
806
+ is_charge = np.array(is_charge_list, dtype=bool)
807
+
808
+ charge_mask = is_charge
809
+ discharge_mask = ~is_charge
810
+
811
+ return (cap_x, voltage, cycles, charge_mask, discharge_mask)
812
+
813
+ # Normal processing for point-by-point data
814
+ voltage = np.empty(n, dtype=float)
815
+ current = np.empty(n, dtype=float)
816
+ cycles = np.ones(n, dtype=int)
817
+ cap_x = np.full(n, np.nan, dtype=float)
677
818
 
678
819
  for k, row in enumerate(rows):
679
820
  # Ensure row has enough columns
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.4.1
3
+ Version: 1.4.3
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
@@ -72,7 +72,7 @@ pip install batplot
72
72
 
73
73
  ## Quick Start
74
74
 
75
- ### XRD / PDF / XAS
75
+ ### XRD / PDF / XAS and much more
76
76
 
77
77
  ```bash
78
78
  # Single diffraction pattern in 2theta
@@ -146,7 +146,7 @@ batplot --operando --interactive
146
146
 
147
147
  | Type | Formats |
148
148
  |------|---------|
149
- | **Electrochemistry** | `.csv` (Neware), `.mpt` (Biologic) |
149
+ | **Electrochemistry** | `.csv` (Neware), `.mpt` (Biologic), `.xlsx` (Landt/Lanhe) CPC only |
150
150
  | **XRD / PDF** | `.xye`, `.xy`, `.qye`, `.dat` |
151
151
  | **XAS** | `.nor`, `.chik`, `.chir` |
152
152
  | **Others** | `user defined` (plot first two columns as x and y) |
@@ -181,4 +181,4 @@ University of Oslo
181
181
 
182
182
  **GitHub**: https://github.com/tiandai-chem/batplot
183
183
 
184
- **Subscribe for Updates**: Join batplot-lab@kjemi.uio.no for updates, feature announcements, and community feedback
184
+ **Subscribe for Updates**: Join batplot-lab@kjemi.uio.no for updates, feature announcements, and community feedback. If you are not from UiO, send an email to sympa@kjemi.uio.no with the exact subject line with your name: "subscribe batplot-lab@kjemi.uio.no your-name"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "batplot"
7
- version = "1.4.1"
7
+ version = "1.4.3"
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