batplot 1.3.8__tar.gz → 1.3.10__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.8 → batplot-1.3.10}/PKG-INFO +1 -1
  2. {batplot-1.3.8 → batplot-1.3.10}/batplot/__init__.py +1 -1
  3. {batplot-1.3.8 → batplot-1.3.10}/batplot/batplot.py +99 -4
  4. {batplot-1.3.8 → batplot-1.3.10}/batplot/cpc_interactive.py +58 -5
  5. {batplot-1.3.8 → batplot-1.3.10}/batplot/electrochem_interactive.py +57 -5
  6. {batplot-1.3.8 → batplot-1.3.10}/batplot/interactive.py +231 -180
  7. {batplot-1.3.8 → batplot-1.3.10}/batplot/operando.py +4 -3
  8. {batplot-1.3.8 → batplot-1.3.10}/batplot/operando_ec_interactive.py +585 -297
  9. {batplot-1.3.8 → batplot-1.3.10}/batplot/session.py +228 -115
  10. {batplot-1.3.8 → batplot-1.3.10}/batplot/ui.py +171 -169
  11. {batplot-1.3.8 → batplot-1.3.10}/batplot.egg-info/PKG-INFO +1 -1
  12. {batplot-1.3.8 → batplot-1.3.10}/pyproject.toml +1 -1
  13. {batplot-1.3.8 → batplot-1.3.10}/LICENSE +0 -0
  14. {batplot-1.3.8 → batplot-1.3.10}/README.md +0 -0
  15. {batplot-1.3.8 → batplot-1.3.10}/batplot/args.py +0 -0
  16. {batplot-1.3.8 → batplot-1.3.10}/batplot/batch.py +0 -0
  17. {batplot-1.3.8 → batplot-1.3.10}/batplot/batplot_new.py +0 -0
  18. {batplot-1.3.8 → batplot-1.3.10}/batplot/cif.py +0 -0
  19. {batplot-1.3.8 → batplot-1.3.10}/batplot/cli.py +0 -0
  20. {batplot-1.3.8 → batplot-1.3.10}/batplot/converters.py +0 -0
  21. {batplot-1.3.8 → batplot-1.3.10}/batplot/modes.py +0 -0
  22. {batplot-1.3.8 → batplot-1.3.10}/batplot/plotting.py +0 -0
  23. {batplot-1.3.8 → batplot-1.3.10}/batplot/readers.py +0 -0
  24. {batplot-1.3.8 → batplot-1.3.10}/batplot/style.py +0 -0
  25. {batplot-1.3.8 → batplot-1.3.10}/batplot/utils.py +0 -0
  26. {batplot-1.3.8 → batplot-1.3.10}/batplot.egg-info/SOURCES.txt +0 -0
  27. {batplot-1.3.8 → batplot-1.3.10}/batplot.egg-info/dependency_links.txt +0 -0
  28. {batplot-1.3.8 → batplot-1.3.10}/batplot.egg-info/entry_points.txt +0 -0
  29. {batplot-1.3.8 → batplot-1.3.10}/batplot.egg-info/requires.txt +0 -0
  30. {batplot-1.3.8 → batplot-1.3.10}/batplot.egg-info/top_level.txt +0 -0
  31. {batplot-1.3.8 → batplot-1.3.10}/setup.cfg +0 -0
  32. {batplot-1.3.8 → batplot-1.3.10}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.3.8
3
+ Version: 1.3.10
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.8"
3
+ __version__ = "1.3.10"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -1409,6 +1409,29 @@ def batplot_main() -> int:
1409
1409
  spm = fig_cfg.get('subplot_margins')
1410
1410
  if spm and all(k in spm for k in ('left','right','bottom','top')):
1411
1411
  fig.subplots_adjust(left=spm['left'], right=spm['right'], bottom=spm['bottom'], top=spm['top'])
1412
+
1413
+ # Restore exact frame size if stored (for precision)
1414
+ frame_size = fig_cfg.get('frame_size')
1415
+ if frame_size and isinstance(frame_size, (list, tuple)) and len(frame_size) == 2:
1416
+ target_w_in, target_h_in = map(float, frame_size)
1417
+ # Get current canvas size
1418
+ canvas_w_in, canvas_h_in = fig.get_size_inches()
1419
+ # Calculate needed fractions to achieve exact frame size
1420
+ if canvas_w_in > 0 and canvas_h_in > 0:
1421
+ # Get current position to preserve centering
1422
+ bbox = ax.get_position()
1423
+ center_x = (bbox.x0 + bbox.x1) / 2.0
1424
+ center_y = (bbox.y0 + bbox.y1) / 2.0
1425
+ # Calculate new fractions
1426
+ new_w_frac = target_w_in / canvas_w_in
1427
+ new_h_frac = target_h_in / canvas_h_in
1428
+ # Reposition to maintain centering
1429
+ new_left = center_x - new_w_frac / 2.0
1430
+ new_right = center_x + new_w_frac / 2.0
1431
+ new_bottom = center_y - new_h_frac / 2.0
1432
+ new_top = center_y + new_h_frac / 2.0
1433
+ # Apply
1434
+ fig.subplots_adjust(left=new_left, right=new_right, bottom=new_bottom, top=new_top)
1412
1435
  except Exception:
1413
1436
  pass
1414
1437
  # Font
@@ -1431,13 +1454,13 @@ def batplot_main() -> int:
1431
1454
  try:
1432
1455
  tw = sess.get('tick_widths', {})
1433
1456
  if tw.get('x_major') is not None:
1434
- ax.tick_params(axis='x', which='major', width=tw['x_major'])
1457
+ ax.tick_params(axis='x', which='major', width=float(tw['x_major']))
1435
1458
  if tw.get('x_minor') is not None:
1436
- ax.tick_params(axis='x', which='minor', width=tw['x_minor'])
1459
+ ax.tick_params(axis='x', which='minor', width=float(tw['x_minor']))
1437
1460
  if tw.get('y_major') is not None:
1438
- ax.tick_params(axis='y', which='major', width=tw['y_major'])
1461
+ ax.tick_params(axis='y', which='major', width=float(tw['y_major']))
1439
1462
  if tw.get('y_minor') is not None:
1440
- ax.tick_params(axis='y', which='minor', width=tw['y_minor'])
1463
+ ax.tick_params(axis='y', which='minor', width=float(tw['y_minor']))
1441
1464
  except Exception:
1442
1465
  pass
1443
1466
  # Tick lengths restore
@@ -1457,6 +1480,78 @@ def batplot_main() -> int:
1457
1480
  fig._tick_lengths['minor'] = minor_len
1458
1481
  except Exception:
1459
1482
  pass
1483
+
1484
+ # Restore WASD state (spine, ticks, labels, title visibility for all 4 sides)
1485
+ try:
1486
+ wasd = sess.get('wasd_state', {})
1487
+ if wasd:
1488
+ # Store the xlabel/ylabel before applying WASD (to restore hidden titles later if needed)
1489
+ stored_xlabel = ax.get_xlabel()
1490
+ stored_ylabel = ax.get_ylabel()
1491
+
1492
+ # Apply spine visibility
1493
+ for side in ('top', 'bottom', 'left', 'right'):
1494
+ state = wasd.get(side, {})
1495
+ sp = ax.spines.get(side)
1496
+ if sp and 'spine' in state:
1497
+ sp.set_visible(bool(state['spine']))
1498
+
1499
+ # Apply tick and label visibility
1500
+ for side in ('top', 'bottom', 'left', 'right'):
1501
+ state = wasd.get(side, {})
1502
+ if side in ('top', 'bottom'):
1503
+ # X-axis ticks
1504
+ tick_key = 'tick1On' if side == 'top' else 'tick2On'
1505
+ label_key = 'label1On' if side == 'top' else 'label2On'
1506
+ if 'ticks' in state:
1507
+ ax.tick_params(axis='x', which='major', **{tick_key: bool(state['ticks'])})
1508
+ if 'labels' in state:
1509
+ ax.tick_params(axis='x', which='major', **{label_key: bool(state['labels'])})
1510
+ if 'minor' in state:
1511
+ ax.tick_params(axis='x', which='minor', **{tick_key: bool(state['minor'])})
1512
+ else:
1513
+ # Y-axis ticks
1514
+ tick_key = 'tick1On' if side == 'left' else 'tick2On'
1515
+ label_key = 'label1On' if side == 'left' else 'label2On'
1516
+ if 'ticks' in state:
1517
+ ax.tick_params(axis='y', which='major', **{tick_key: bool(state['ticks'])})
1518
+ if 'labels' in state:
1519
+ ax.tick_params(axis='y', which='major', **{label_key: bool(state['labels'])})
1520
+ if 'minor' in state:
1521
+ ax.tick_params(axis='y', which='minor', **{tick_key: bool(state['minor'])})
1522
+
1523
+ # Apply title visibility - CRITICAL: Check title state before restoring labels
1524
+ # Bottom xlabel
1525
+ bottom_title_on = wasd.get('bottom', {}).get('title', True)
1526
+ if bottom_title_on:
1527
+ ax.set_xlabel(stored_xlabel)
1528
+ else:
1529
+ ax.set_xlabel('') # Hidden by user via s5
1530
+ # Store the hidden label for later restoration
1531
+ if stored_xlabel:
1532
+ setattr(ax, '_stored_xlabel', stored_xlabel)
1533
+
1534
+ # Left ylabel
1535
+ left_title_on = wasd.get('left', {}).get('title', True)
1536
+ if left_title_on:
1537
+ ax.set_ylabel(stored_ylabel)
1538
+ else:
1539
+ ax.set_ylabel('') # Hidden by user via a5
1540
+ # Store the hidden label for later restoration
1541
+ if stored_ylabel:
1542
+ setattr(ax, '_stored_ylabel', stored_ylabel)
1543
+
1544
+ # Top xlabel (if exists)
1545
+ top_title_on = wasd.get('top', {}).get('title', False)
1546
+ setattr(ax, '_top_xlabel_on', top_title_on)
1547
+
1548
+ # Right ylabel (if exists)
1549
+ right_title_on = wasd.get('right', {}).get('title', False)
1550
+ setattr(ax, '_right_ylabel_on', right_title_on)
1551
+ except Exception as e:
1552
+ # Don't fail session load if WASD restoration fails
1553
+ print(f"Warning: Could not fully restore WASD state: {e}")
1554
+
1460
1555
  # Rebuild label texts
1461
1556
  for i, lab in enumerate(labels_list):
1462
1557
  txt = ax.text(1.0, 1.0, f"{i+1}: {lab}", ha='right', va='top', transform=ax.transAxes,
@@ -296,6 +296,11 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
296
296
  'right': {'linewidth': ax2.spines.get('right').get_linewidth() if ax2.spines.get('right') else None,
297
297
  'visible': ax2.spines.get('right').get_visible() if ax2.spines.get('right') else None},
298
298
  },
299
+ 'labelpads': {
300
+ 'x': getattr(ax.xaxis, 'labelpad', None),
301
+ 'ly': getattr(ax.yaxis, 'labelpad', None), # left y-axis (capacity)
302
+ 'ry': getattr(ax2.yaxis, 'labelpad', None), # right y-axis (efficiency)
303
+ },
299
304
  'series': {
300
305
  'charge': {
301
306
  'color': _color_of(sc_charge),
@@ -628,6 +633,18 @@ def _apply_style(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, cfg: Dict, file_
628
633
  except Exception: pass
629
634
  except Exception:
630
635
  pass
636
+ # Restore labelpads
637
+ try:
638
+ pads = cfg.get('labelpads', {})
639
+ if pads:
640
+ if pads.get('x') is not None:
641
+ ax.xaxis.labelpad = pads['x']
642
+ if pads.get('ly') is not None:
643
+ ax.yaxis.labelpad = pads['ly']
644
+ if pads.get('ry') is not None:
645
+ ax2.yaxis.labelpad = pads['ry']
646
+ except Exception:
647
+ pass
631
648
  try:
632
649
  fig.canvas.draw_idle()
633
650
  except Exception:
@@ -1187,13 +1204,46 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
1187
1204
  continue
1188
1205
  elif key == 'e':
1189
1206
  try:
1190
- fname = input("Export filename (default .svg if no extension, q=cancel): ").strip()
1207
+ # List existing figure files
1208
+ folder = os.getcwd()
1209
+ fig_extensions = ('.svg', '.png', '.jpg', '.jpeg', '.pdf', '.eps', '.tif', '.tiff')
1210
+ files = []
1211
+ try:
1212
+ files = sorted([f for f in os.listdir(folder) if f.lower().endswith(fig_extensions)])
1213
+ except Exception:
1214
+ files = []
1215
+ if files:
1216
+ print("Existing figure files:")
1217
+ for i, f in enumerate(files, 1):
1218
+ print(f" {i}: {f}")
1219
+
1220
+ fname = input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
1191
1221
  if not fname or fname.lower() == 'q':
1192
1222
  _print_menu(); continue
1193
- root, ext = os.path.splitext(fname)
1194
- if ext == '':
1195
- fname = fname + '.svg'
1196
- target = _confirm_overwrite(fname)
1223
+
1224
+ # Check if user selected a number
1225
+ already_confirmed = False
1226
+ if fname.isdigit() and files:
1227
+ idx = int(fname)
1228
+ if 1 <= idx <= len(files):
1229
+ name = files[idx-1]
1230
+ yn = input(f"Overwrite '{name}'? (y/n): ").strip().lower()
1231
+ if yn != 'y':
1232
+ _print_menu(); continue
1233
+ fname = name
1234
+ already_confirmed = True
1235
+ else:
1236
+ print("Invalid number.")
1237
+ _print_menu(); continue
1238
+ else:
1239
+ root, ext = os.path.splitext(fname)
1240
+ if ext == '':
1241
+ fname = fname + '.svg'
1242
+
1243
+ if already_confirmed:
1244
+ target = fname
1245
+ else:
1246
+ target = _confirm_overwrite(fname)
1197
1247
  if target:
1198
1248
  # Remove numbering from legend labels before export
1199
1249
  original_labels = {}
@@ -2230,6 +2280,9 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
2230
2280
  _print_menu(); continue
2231
2281
  elif key == 'r':
2232
2282
  # Rename axis titles
2283
+ print("Tip: Use LaTeX/mathtext for special characters:")
2284
+ print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
2285
+ print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
2233
2286
  while True:
2234
2287
  print("Rename titles: x=x-axis, ly=left y-axis, ry=right y-axis, q=back")
2235
2288
  sub = input("Rename> ").strip().lower()
@@ -649,6 +649,10 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
649
649
  'wasd_state': dict(getattr(fig, '_ec_wasd_state', {})) if hasattr(fig, '_ec_wasd_state') else {},
650
650
  'fig_size': list(fig.get_size_inches()),
651
651
  'rotation_angle': getattr(fig, '_ec_rotation_angle', 0),
652
+ 'labelpads': {
653
+ 'x': getattr(ax.xaxis, 'labelpad', None),
654
+ 'y': getattr(ax.yaxis, 'labelpad', None),
655
+ },
652
656
  'spines': {name: {
653
657
  'lw': (ax.spines.get(name).get_linewidth() if ax.spines.get(name) else None),
654
658
  'visible': (ax.spines.get(name).get_visible() if ax.spines.get(name) else None)
@@ -776,6 +780,16 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
776
780
  _position_top_xlabel(); _position_right_ylabel()
777
781
  except Exception:
778
782
  pass
783
+ # Restore labelpads (for title positioning)
784
+ try:
785
+ pads = snap.get('labelpads', {})
786
+ if pads:
787
+ if pads.get('x') is not None:
788
+ ax.xaxis.labelpad = pads['x']
789
+ if pads.get('y') is not None:
790
+ ax.yaxis.labelpad = pads['y']
791
+ except Exception:
792
+ pass
779
793
  # Lines (by index)
780
794
  try:
781
795
  if len(ax.lines) == len(snap.get('lines', [])):
@@ -828,15 +842,50 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
828
842
  elif key == 'e':
829
843
  # Export current figure to a file; default extension .svg if missing
830
844
  try:
831
- fname = input("Export filename (default .svg if no extension, q=cancel): ").strip()
845
+ # List existing figure files
846
+ folder = os.getcwd()
847
+ fig_extensions = ('.svg', '.png', '.jpg', '.jpeg', '.pdf', '.eps', '.tif', '.tiff')
848
+ files = []
849
+ try:
850
+ files = sorted([f for f in os.listdir(folder) if f.lower().endswith(fig_extensions)])
851
+ except Exception:
852
+ files = []
853
+ if files:
854
+ print("Existing figure files:")
855
+ for i, f in enumerate(files, 1):
856
+ print(f" {i}: {f}")
857
+
858
+ fname = input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
832
859
  if not fname or fname.lower() == 'q':
833
860
  _print_menu(len(all_cycles), is_dqdv)
834
861
  continue
835
- root, ext = os.path.splitext(fname)
836
- if ext == '':
837
- fname = fname + '.svg'
862
+
863
+ # Check if user selected a number
864
+ already_confirmed = False
865
+ if fname.isdigit() and files:
866
+ idx = int(fname)
867
+ if 1 <= idx <= len(files):
868
+ name = files[idx-1]
869
+ yn = input(f"Overwrite '{name}'? (y/n): ").strip().lower()
870
+ if yn != 'y':
871
+ _print_menu(len(all_cycles), is_dqdv)
872
+ continue
873
+ fname = name
874
+ already_confirmed = True
875
+ else:
876
+ print("Invalid number.")
877
+ _print_menu(len(all_cycles), is_dqdv)
878
+ continue
879
+ else:
880
+ root, ext = os.path.splitext(fname)
881
+ if ext == '':
882
+ fname = fname + '.svg'
883
+
838
884
  try:
839
- target = _confirm_overwrite(fname)
885
+ if already_confirmed:
886
+ target = fname
887
+ else:
888
+ target = _confirm_overwrite(fname)
840
889
  if target:
841
890
  # If exporting SVG, make background transparent for PowerPoint
842
891
  _, ext2 = os.path.splitext(target)
@@ -1471,6 +1520,9 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1471
1520
  elif key == 'r':
1472
1521
  # Rename axis labels
1473
1522
  try:
1523
+ print("Tip: Use LaTeX/mathtext for special characters:")
1524
+ print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
1525
+ print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
1474
1526
  while True:
1475
1527
  print("Rename axis: x, y, both, q=back")
1476
1528
  sub = input("Rename> ").strip().lower()