batplot 1.8.0__py3-none-any.whl → 1.8.2__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.
Potentially problematic release.
This version of batplot might be problematic. Click here for more details.
- batplot/__init__.py +1 -1
- batplot/args.py +5 -3
- batplot/batplot.py +44 -4
- batplot/cpc_interactive.py +96 -3
- batplot/electrochem_interactive.py +28 -0
- batplot/interactive.py +18 -2
- batplot/modes.py +12 -12
- batplot/operando.py +2 -0
- batplot/operando_ec_interactive.py +112 -11
- batplot/session.py +35 -1
- batplot/utils.py +40 -0
- batplot/version_check.py +85 -6
- {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/METADATA +1 -1
- batplot-1.8.2.dist-info/RECORD +75 -0
- {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/top_level.txt +1 -0
- batplot_backup_20251221_101150/__init__.py +5 -0
- batplot_backup_20251221_101150/args.py +625 -0
- batplot_backup_20251221_101150/batch.py +1176 -0
- batplot_backup_20251221_101150/batplot.py +3589 -0
- batplot_backup_20251221_101150/cif.py +823 -0
- batplot_backup_20251221_101150/cli.py +149 -0
- batplot_backup_20251221_101150/color_utils.py +547 -0
- batplot_backup_20251221_101150/config.py +198 -0
- batplot_backup_20251221_101150/converters.py +204 -0
- batplot_backup_20251221_101150/cpc_interactive.py +4409 -0
- batplot_backup_20251221_101150/electrochem_interactive.py +4520 -0
- batplot_backup_20251221_101150/interactive.py +3894 -0
- batplot_backup_20251221_101150/manual.py +323 -0
- batplot_backup_20251221_101150/modes.py +799 -0
- batplot_backup_20251221_101150/operando.py +603 -0
- batplot_backup_20251221_101150/operando_ec_interactive.py +5487 -0
- batplot_backup_20251221_101150/plotting.py +228 -0
- batplot_backup_20251221_101150/readers.py +2607 -0
- batplot_backup_20251221_101150/session.py +2951 -0
- batplot_backup_20251221_101150/style.py +1441 -0
- batplot_backup_20251221_101150/ui.py +790 -0
- batplot_backup_20251221_101150/utils.py +1046 -0
- batplot_backup_20251221_101150/version_check.py +253 -0
- batplot-1.8.0.dist-info/RECORD +0 -52
- {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/WHEEL +0 -0
- {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/entry_points.txt +0 -0
- {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/licenses/LICENSE +0 -0
batplot/__init__.py
CHANGED
batplot/args.py
CHANGED
|
@@ -152,6 +152,8 @@ def _print_general_help() -> None:
|
|
|
152
152
|
" batplot --all style.bps # Batch with style: apply style.bps to all files\n"
|
|
153
153
|
" batplot --all ./Style/style.bps # Batch with style: use relative path to style file\n"
|
|
154
154
|
" batplot --all config.bpsg # Batch with style+geom: apply to all XY files\n"
|
|
155
|
+
" batplot file1.xy:1.54 file2.qye --stack # Stack mode: stack all files vertically\n"
|
|
156
|
+
" batplot file1.xy:1.54 file2.qye structure.cif --stack --i # Stack mode: stack all files vertically with cif ticks\n"
|
|
155
157
|
" batplot file1.qye file2.qye style.bps # Apply style to multiple files and export\n"
|
|
156
158
|
" batplot file1.xy file2.xye ./Style/style.bps # Apply style from relative path\n\n"
|
|
157
159
|
" [Electrochemistry]\n"
|
|
@@ -169,7 +171,7 @@ def _print_general_help() -> None:
|
|
|
169
171
|
" batplot --cv FILE.txt # EC CV (cyclic voltammetry) from .txt\n"
|
|
170
172
|
" batplot --cv --all # Batch: all .mpt/.txt in directory (CV mode)\n\n"
|
|
171
173
|
" [Operando]\n"
|
|
172
|
-
" batplot --operando --i [FOLDER] # Operando contour (with or without .mpt file)
|
|
174
|
+
" batplot --operando --i [FOLDER] # Operando contour (with or without .mpt file)\n\n"
|
|
173
175
|
"Features:\n"
|
|
174
176
|
" • Quick plotting with sensible defaults, no config files needed\n"
|
|
175
177
|
" • Supports many common file formats (see -h xy/ec/op)\n"
|
|
@@ -183,9 +185,9 @@ def _print_general_help() -> None:
|
|
|
183
185
|
" batplot -h xy # XY file plotting guide\n"
|
|
184
186
|
" batplot -h ec # Electrochemistry (GC/dQdV/CV/CPC) guide\n"
|
|
185
187
|
" batplot -h op # Operando guide\n"
|
|
186
|
-
" batplot -m # Open the illustrated txt manual with highlights\n"
|
|
188
|
+
" batplot -m # Open the illustrated txt manual with highlights\n\n"
|
|
187
189
|
|
|
188
|
-
"Contact & Updates:\n
|
|
190
|
+
"Contact & Updates:\n"
|
|
189
191
|
" Subscribe to batplot-lab@kjemi.uio.no for updates\n"
|
|
190
192
|
" (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"
|
|
191
193
|
" Kindly cite the pypi package page (https://pypi.org/project/batplot/) if the plot is used for publication\n"
|
batplot/batplot.py
CHANGED
|
@@ -1890,10 +1890,31 @@ def batplot_main() -> int:
|
|
|
1890
1890
|
x_full_list = []
|
|
1891
1891
|
raw_y_full_list = []
|
|
1892
1892
|
offsets_list = []
|
|
1893
|
-
tick_state
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1893
|
+
# Load tick_state from wasd_state if available (version 2+), otherwise use defaults
|
|
1894
|
+
wasd_loaded = sess.get('wasd_state')
|
|
1895
|
+
if wasd_loaded and isinstance(wasd_loaded, dict):
|
|
1896
|
+
# Convert wasd_state to tick_state format
|
|
1897
|
+
tick_state = {}
|
|
1898
|
+
for side_key, prefix in [('top', 't'), ('bottom', 'b'), ('left', 'l'), ('right', 'r')]:
|
|
1899
|
+
s = wasd_loaded.get(side_key, {})
|
|
1900
|
+
tick_state[f'{prefix}_ticks'] = bool(s.get('ticks', side_key in ('bottom', 'left')))
|
|
1901
|
+
tick_state[f'{prefix}_labels'] = bool(s.get('labels', side_key in ('bottom', 'left')))
|
|
1902
|
+
tick_state[f'm{prefix}x' if prefix in 'tb' else f'm{prefix}y'] = bool(s.get('minor', False))
|
|
1903
|
+
# Legacy keys for backward compatibility
|
|
1904
|
+
tick_state['bx'] = tick_state.get('b_ticks', True)
|
|
1905
|
+
tick_state['tx'] = tick_state.get('t_ticks', False)
|
|
1906
|
+
tick_state['ly'] = tick_state.get('l_ticks', True)
|
|
1907
|
+
tick_state['ry'] = tick_state.get('r_ticks', False)
|
|
1908
|
+
tick_state['mbx'] = tick_state.get('mbx', False)
|
|
1909
|
+
tick_state['mtx'] = tick_state.get('mtx', False)
|
|
1910
|
+
tick_state['mly'] = tick_state.get('mly', False)
|
|
1911
|
+
tick_state['mry'] = tick_state.get('mry', False)
|
|
1912
|
+
else:
|
|
1913
|
+
# Fallback to legacy tick_state or defaults
|
|
1914
|
+
tick_state = sess.get('tick_state', {
|
|
1915
|
+
'bx': True,'tx': False,'ly': True,'ry': False,
|
|
1916
|
+
'mbx': False,'mtx': False,'mly': False,'mry': False
|
|
1917
|
+
})
|
|
1897
1918
|
saved_stack = bool(sess.get('args_subset', {}).get('stack', False))
|
|
1898
1919
|
# Pull data
|
|
1899
1920
|
# --- Robust reconstruction of stored curves ---
|
|
@@ -1954,8 +1975,27 @@ def batplot_main() -> int:
|
|
|
1954
1975
|
pass
|
|
1955
1976
|
labels_list[:] = sess.get('labels', [f"Curve {i+1}" for i in range(len(y_data_list))])
|
|
1956
1977
|
delta = sess.get('delta', 0.0)
|
|
1978
|
+
# Apply tick state (labels visibility) BEFORE setting axis labels
|
|
1979
|
+
try:
|
|
1980
|
+
ax.tick_params(axis='x',
|
|
1981
|
+
bottom=tick_state.get('b_ticks', tick_state.get('bx', True)),
|
|
1982
|
+
labelbottom=tick_state.get('b_labels', tick_state.get('bx', True)),
|
|
1983
|
+
top=tick_state.get('t_ticks', tick_state.get('tx', False)),
|
|
1984
|
+
labeltop=tick_state.get('t_labels', tick_state.get('tx', False)))
|
|
1985
|
+
ax.tick_params(axis='y',
|
|
1986
|
+
left=tick_state.get('l_ticks', tick_state.get('ly', True)),
|
|
1987
|
+
labelleft=tick_state.get('l_labels', tick_state.get('ly', True)),
|
|
1988
|
+
right=tick_state.get('r_ticks', tick_state.get('ry', False)),
|
|
1989
|
+
labelright=tick_state.get('r_labels', tick_state.get('ry', False)))
|
|
1990
|
+
except Exception:
|
|
1991
|
+
pass
|
|
1957
1992
|
ax.set_xlabel(sess.get('axis', {}).get('xlabel', 'X'))
|
|
1958
1993
|
ax.set_ylabel(sess.get('axis', {}).get('ylabel', 'Intensity'))
|
|
1994
|
+
# Store tick_state on axes for interactive menu
|
|
1995
|
+
try:
|
|
1996
|
+
ax._saved_tick_state = dict(tick_state)
|
|
1997
|
+
except Exception:
|
|
1998
|
+
pass
|
|
1959
1999
|
|
|
1960
2000
|
# Restore normalization ranges (if saved)
|
|
1961
2001
|
axis_cfg = sess.get('axis', {})
|
batplot/cpc_interactive.py
CHANGED
|
@@ -86,6 +86,7 @@ from .ui import (
|
|
|
86
86
|
from .utils import (
|
|
87
87
|
_confirm_overwrite,
|
|
88
88
|
choose_save_path,
|
|
89
|
+
convert_label_shortcuts,
|
|
89
90
|
choose_style_file,
|
|
90
91
|
list_files_in_subdirectory,
|
|
91
92
|
get_organized_path,
|
|
@@ -335,13 +336,36 @@ def _rebuild_legend(ax, ax2, file_data, preserve_position=True):
|
|
|
335
336
|
"""
|
|
336
337
|
try:
|
|
337
338
|
fig = ax.figure
|
|
338
|
-
# Get stored position before rebuilding
|
|
339
|
+
# Get stored position before rebuilding. If none is stored yet, try to
|
|
340
|
+
# capture the current on-canvas position once so subsequent rebuilds
|
|
341
|
+
# (e.g., after renaming) do not jump to a new "best" location.
|
|
339
342
|
xy_in = None
|
|
340
343
|
if preserve_position:
|
|
341
344
|
try:
|
|
342
345
|
xy_in = getattr(fig, '_cpc_legend_xy_in', None)
|
|
343
346
|
except Exception:
|
|
344
|
-
|
|
347
|
+
xy_in = None
|
|
348
|
+
if xy_in is None:
|
|
349
|
+
try:
|
|
350
|
+
leg0 = ax.get_legend()
|
|
351
|
+
if leg0 is not None and leg0.get_visible():
|
|
352
|
+
try:
|
|
353
|
+
renderer = fig.canvas.get_renderer()
|
|
354
|
+
except Exception:
|
|
355
|
+
fig.canvas.draw()
|
|
356
|
+
renderer = fig.canvas.get_renderer()
|
|
357
|
+
bb = leg0.get_window_extent(renderer=renderer)
|
|
358
|
+
cx = 0.5 * (bb.x0 + bb.x1)
|
|
359
|
+
cy = 0.5 * (bb.y0 + bb.y1)
|
|
360
|
+
fx, fy = fig.transFigure.inverted().transform((cx, cy))
|
|
361
|
+
fw, fh = fig.get_size_inches()
|
|
362
|
+
offset = ((fx - 0.5) * fw, (fy - 0.5) * fh)
|
|
363
|
+
offset = _sanitize_legend_offset(offset)
|
|
364
|
+
if offset is not None:
|
|
365
|
+
fig._cpc_legend_xy_in = offset
|
|
366
|
+
xy_in = offset
|
|
367
|
+
except Exception:
|
|
368
|
+
pass
|
|
345
369
|
|
|
346
370
|
h1, l1 = ax.get_legend_handles_labels()
|
|
347
371
|
h2, l2 = ax2.get_legend_handles_labels()
|
|
@@ -1643,6 +1667,31 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1643
1667
|
else:
|
|
1644
1668
|
# Single file mode: toggle efficiency
|
|
1645
1669
|
push_state("visibility-eff")
|
|
1670
|
+
# Capture current legend position BEFORE toggling visibility
|
|
1671
|
+
try:
|
|
1672
|
+
if not hasattr(fig, '_cpc_legend_xy_in') or getattr(fig, '_cpc_legend_xy_in') is None:
|
|
1673
|
+
leg0 = ax.get_legend()
|
|
1674
|
+
if leg0 is not None and leg0.get_visible():
|
|
1675
|
+
try:
|
|
1676
|
+
# Ensure renderer exists
|
|
1677
|
+
try:
|
|
1678
|
+
renderer = fig.canvas.get_renderer()
|
|
1679
|
+
except Exception:
|
|
1680
|
+
fig.canvas.draw()
|
|
1681
|
+
renderer = fig.canvas.get_renderer()
|
|
1682
|
+
bb = leg0.get_window_extent(renderer=renderer)
|
|
1683
|
+
cx = 0.5 * (bb.x0 + bb.x1)
|
|
1684
|
+
cy = 0.5 * (bb.y0 + bb.y1)
|
|
1685
|
+
fx, fy = fig.transFigure.inverted().transform((cx, cy))
|
|
1686
|
+
fw, fh = fig.get_size_inches()
|
|
1687
|
+
offset = ((fx - 0.5) * fw, (fy - 0.5) * fh)
|
|
1688
|
+
offset = _sanitize_legend_offset(offset)
|
|
1689
|
+
if offset is not None:
|
|
1690
|
+
fig._cpc_legend_xy_in = offset
|
|
1691
|
+
except Exception:
|
|
1692
|
+
pass
|
|
1693
|
+
except Exception:
|
|
1694
|
+
pass
|
|
1646
1695
|
vis = sc_eff.get_visible()
|
|
1647
1696
|
sc_eff.set_visible(not vis)
|
|
1648
1697
|
try:
|
|
@@ -1650,7 +1699,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1650
1699
|
except Exception:
|
|
1651
1700
|
pass
|
|
1652
1701
|
|
|
1653
|
-
_rebuild_legend(ax, ax2, file_data)
|
|
1702
|
+
_rebuild_legend(ax, ax2, file_data, preserve_position=True)
|
|
1654
1703
|
fig.canvas.draw_idle()
|
|
1655
1704
|
except ValueError:
|
|
1656
1705
|
print("Invalid input.")
|
|
@@ -1676,6 +1725,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
1676
1725
|
elif key == 'c':
|
|
1677
1726
|
# Colors submenu: ly (left Y series) and ry (right Y efficiency), with user colors and palettes
|
|
1678
1727
|
try:
|
|
1728
|
+
# Note: Individual series may use different colors, so we can't show a single "current" palette
|
|
1679
1729
|
# Use same palettes as EC interactive
|
|
1680
1730
|
palette_opts = ['tab10', 'Set2', 'Dark2', 'viridis', 'plasma']
|
|
1681
1731
|
def _palette_color(name, idx=0, total=1, default_val=0.4):
|
|
@@ -2668,6 +2718,33 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2668
2718
|
try:
|
|
2669
2719
|
push_state("toggle-eff")
|
|
2670
2720
|
|
|
2721
|
+
# Capture current legend position BEFORE toggling visibility
|
|
2722
|
+
# This ensures the position is preserved when legend is rebuilt
|
|
2723
|
+
try:
|
|
2724
|
+
if not hasattr(fig, '_cpc_legend_xy_in') or getattr(fig, '_cpc_legend_xy_in') is None:
|
|
2725
|
+
leg0 = ax.get_legend()
|
|
2726
|
+
if leg0 is not None and leg0.get_visible():
|
|
2727
|
+
try:
|
|
2728
|
+
# Ensure renderer exists
|
|
2729
|
+
try:
|
|
2730
|
+
renderer = fig.canvas.get_renderer()
|
|
2731
|
+
except Exception:
|
|
2732
|
+
fig.canvas.draw()
|
|
2733
|
+
renderer = fig.canvas.get_renderer()
|
|
2734
|
+
bb = leg0.get_window_extent(renderer=renderer)
|
|
2735
|
+
cx = 0.5 * (bb.x0 + bb.x1)
|
|
2736
|
+
cy = 0.5 * (bb.y0 + bb.y1)
|
|
2737
|
+
fx, fy = fig.transFigure.inverted().transform((cx, cy))
|
|
2738
|
+
fw, fh = fig.get_size_inches()
|
|
2739
|
+
offset = ((fx - 0.5) * fw, (fy - 0.5) * fh)
|
|
2740
|
+
offset = _sanitize_legend_offset(offset)
|
|
2741
|
+
if offset is not None:
|
|
2742
|
+
fig._cpc_legend_xy_in = offset
|
|
2743
|
+
except Exception:
|
|
2744
|
+
pass
|
|
2745
|
+
except Exception:
|
|
2746
|
+
pass
|
|
2747
|
+
|
|
2671
2748
|
# Determine current visibility state (check if any efficiency is visible)
|
|
2672
2749
|
if is_multi_file:
|
|
2673
2750
|
# In multi-file mode, check if any efficiency is visible
|
|
@@ -3381,6 +3458,11 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3381
3458
|
if not cmd:
|
|
3382
3459
|
continue
|
|
3383
3460
|
if cmd == 'q':
|
|
3461
|
+
# Update ax._saved_tick_state before exiting so changes are persisted
|
|
3462
|
+
try:
|
|
3463
|
+
ax._saved_tick_state = dict(tick_state)
|
|
3464
|
+
except Exception:
|
|
3465
|
+
pass
|
|
3384
3466
|
break
|
|
3385
3467
|
if cmd == 'i':
|
|
3386
3468
|
# Invert tick direction (toggle between 'out' and 'in')
|
|
@@ -3702,6 +3784,11 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3702
3784
|
tick_state['mry'] = bool(wasd['right']['minor'])
|
|
3703
3785
|
if changed:
|
|
3704
3786
|
push_state("wasd-toggle")
|
|
3787
|
+
# Update ax._saved_tick_state so dump_session can read it
|
|
3788
|
+
try:
|
|
3789
|
+
ax._saved_tick_state = dict(tick_state)
|
|
3790
|
+
except Exception:
|
|
3791
|
+
pass
|
|
3705
3792
|
_apply_wasd(changed_sides if changed_sides else None)
|
|
3706
3793
|
# Single draw at the end after all positioning is complete
|
|
3707
3794
|
try:
|
|
@@ -3738,6 +3825,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3738
3825
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
3739
3826
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
3740
3827
|
print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
3828
|
+
print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
|
|
3741
3829
|
while True:
|
|
3742
3830
|
print("Rename: x=x-axis, ly=left y-axis, ry=right y-axis, l=legend labels, q=back")
|
|
3743
3831
|
sub = _safe_input("Rename> ").strip().lower()
|
|
@@ -3788,6 +3876,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3788
3876
|
print(f"Current file name in legend: '{base_name}'")
|
|
3789
3877
|
new_name = _safe_input("Enter new file name (q=cancel): ").strip()
|
|
3790
3878
|
if new_name and new_name.lower() != 'q':
|
|
3879
|
+
new_name = convert_label_shortcuts(new_name)
|
|
3791
3880
|
try:
|
|
3792
3881
|
push_state("rename-legend")
|
|
3793
3882
|
|
|
@@ -3895,6 +3984,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3895
3984
|
print(f"Current file name in legend: '{base_name}'")
|
|
3896
3985
|
new_name = _safe_input("Enter new file name (q=cancel): ").strip()
|
|
3897
3986
|
if new_name and new_name.lower() != 'q':
|
|
3987
|
+
new_name = convert_label_shortcuts(new_name)
|
|
3898
3988
|
try:
|
|
3899
3989
|
push_state("rename-legend")
|
|
3900
3990
|
|
|
@@ -3966,6 +4056,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3966
4056
|
print(f"Current x-axis title: '{current}'")
|
|
3967
4057
|
new_title = _safe_input("Enter new x-axis title (q=cancel): ")
|
|
3968
4058
|
if new_title and new_title.lower() != 'q':
|
|
4059
|
+
new_title = convert_label_shortcuts(new_title)
|
|
3969
4060
|
try:
|
|
3970
4061
|
push_state("rename-x")
|
|
3971
4062
|
ax.set_xlabel(new_title)
|
|
@@ -3985,6 +4076,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3985
4076
|
print(f"Current left y-axis title: '{current}'")
|
|
3986
4077
|
new_title = _safe_input("Enter new left y-axis title (q=cancel): ")
|
|
3987
4078
|
if new_title and new_title.lower() != 'q':
|
|
4079
|
+
new_title = convert_label_shortcuts(new_title)
|
|
3988
4080
|
try:
|
|
3989
4081
|
push_state("rename-ly")
|
|
3990
4082
|
ax.set_ylabel(new_title)
|
|
@@ -3999,6 +4091,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3999
4091
|
print(f"Current right y-axis title: '{current}'")
|
|
4000
4092
|
new_title = _safe_input("Enter new right y-axis title (q=cancel): ")
|
|
4001
4093
|
if new_title and new_title.lower() != 'q':
|
|
4094
|
+
new_title = convert_label_shortcuts(new_title)
|
|
4002
4095
|
try:
|
|
4003
4096
|
push_state("rename-ry")
|
|
4004
4097
|
ax2.set_ylabel(new_title)
|
|
@@ -31,6 +31,7 @@ from .utils import (
|
|
|
31
31
|
choose_style_file,
|
|
32
32
|
list_files_in_subdirectory,
|
|
33
33
|
get_organized_path,
|
|
34
|
+
convert_label_shortcuts,
|
|
34
35
|
)
|
|
35
36
|
import time
|
|
36
37
|
from .color_utils import (
|
|
@@ -502,6 +503,28 @@ def _rebuild_legend(ax):
|
|
|
502
503
|
fig = ax.figure
|
|
503
504
|
# Capture existing title before any rebuild so it isn't lost
|
|
504
505
|
_store_legend_title(fig, ax)
|
|
506
|
+
# If no stored position yet, try to capture the current legend location once
|
|
507
|
+
# so rebuilds (e.g., after renaming) don't jump to a new "best" spot.
|
|
508
|
+
try:
|
|
509
|
+
if getattr(fig, '_ec_legend_xy_in', None) is None:
|
|
510
|
+
leg0 = ax.get_legend()
|
|
511
|
+
if leg0 is not None and leg0.get_visible():
|
|
512
|
+
try:
|
|
513
|
+
renderer = fig.canvas.get_renderer()
|
|
514
|
+
except Exception:
|
|
515
|
+
fig.canvas.draw()
|
|
516
|
+
renderer = fig.canvas.get_renderer()
|
|
517
|
+
bb = leg0.get_window_extent(renderer=renderer)
|
|
518
|
+
cx = 0.5 * (bb.x0 + bb.x1)
|
|
519
|
+
cy = 0.5 * (bb.y0 + bb.y1)
|
|
520
|
+
fx, fy = fig.transFigure.inverted().transform((cx, cy))
|
|
521
|
+
fw, fh = fig.get_size_inches()
|
|
522
|
+
offset = ((fx - 0.5) * fw, (fy - 0.5) * fh)
|
|
523
|
+
offset = _sanitize_legend_offset(fig, offset)
|
|
524
|
+
if offset is not None:
|
|
525
|
+
fig._ec_legend_xy_in = offset
|
|
526
|
+
except Exception:
|
|
527
|
+
pass
|
|
505
528
|
if not _get_legend_user_pref(fig):
|
|
506
529
|
leg = ax.get_legend()
|
|
507
530
|
if leg is not None:
|
|
@@ -2697,6 +2720,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2697
2720
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
2698
2721
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
2699
2722
|
print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
2723
|
+
print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
|
|
2700
2724
|
while True:
|
|
2701
2725
|
print("Rename axis: x, y, both, q=back")
|
|
2702
2726
|
sub = _safe_input("Rename> ").strip().lower()
|
|
@@ -2707,6 +2731,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2707
2731
|
if sub in ('x','both'):
|
|
2708
2732
|
txt = _safe_input("New X-axis label (blank=cancel): ")
|
|
2709
2733
|
if txt:
|
|
2734
|
+
txt = convert_label_shortcuts(txt)
|
|
2710
2735
|
push_state("rename-x")
|
|
2711
2736
|
try:
|
|
2712
2737
|
# Freeze layout and preserve existing pad for one-shot restore
|
|
@@ -2730,6 +2755,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
2730
2755
|
if sub in ('y','both'):
|
|
2731
2756
|
txt = _safe_input("New Y-axis label (blank=cancel): ")
|
|
2732
2757
|
if txt:
|
|
2758
|
+
txt = convert_label_shortcuts(txt)
|
|
2733
2759
|
push_state("rename-y")
|
|
2734
2760
|
base_ylabel = txt
|
|
2735
2761
|
try:
|
|
@@ -3027,6 +3053,8 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
3027
3053
|
_print_menu(len(all_cycles), is_dqdv)
|
|
3028
3054
|
continue
|
|
3029
3055
|
elif key == 'c':
|
|
3056
|
+
# Show current palette if one is applied (this is informational only)
|
|
3057
|
+
# Note: Individual cycles may use different colors, so we can't show a single "current" palette
|
|
3030
3058
|
print(f"Total cycles: {len(all_cycles)}")
|
|
3031
3059
|
print("Enter one of:")
|
|
3032
3060
|
print(_colorize_inline_commands(" - numbers: e.g. 1 5 10"))
|
batplot/interactive.py
CHANGED
|
@@ -25,6 +25,7 @@ from .utils import (
|
|
|
25
25
|
choose_save_path,
|
|
26
26
|
choose_style_file,
|
|
27
27
|
list_files_in_subdirectory,
|
|
28
|
+
convert_label_shortcuts,
|
|
28
29
|
get_organized_path,
|
|
29
30
|
)
|
|
30
31
|
import time
|
|
@@ -1955,12 +1956,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
1955
1956
|
ax._cif_draw_func()
|
|
1956
1957
|
fig.canvas.draw()
|
|
1957
1958
|
elif sub == 'p':
|
|
1959
|
+
# Show current palette if one is applied
|
|
1958
1960
|
history = getattr(fig, '_curve_palette_history', [])
|
|
1959
1961
|
current_palette = history[-1]['palette'] if history else None
|
|
1960
1962
|
if current_palette:
|
|
1961
1963
|
print(f"Current palette: {current_palette}")
|
|
1962
|
-
else:
|
|
1963
|
-
print("Current palette: manual/custom")
|
|
1964
1964
|
base_palettes = ['viridis', 'cividis', 'plasma', 'inferno', 'magma', 'batlow']
|
|
1965
1965
|
extras = []
|
|
1966
1966
|
def _palette_available(name: str) -> bool:
|
|
@@ -2211,6 +2211,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2211
2211
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
2212
2212
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
2213
2213
|
print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
2214
|
+
print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
|
|
2214
2215
|
idx_in = _safe_input("Curve number to rename (q=cancel): ").strip()
|
|
2215
2216
|
if not idx_in or idx_in.lower() == 'q':
|
|
2216
2217
|
print("Canceled.")
|
|
@@ -2227,6 +2228,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2227
2228
|
if not new_label or new_label.lower() == 'q':
|
|
2228
2229
|
print("Canceled.")
|
|
2229
2230
|
continue
|
|
2231
|
+
new_label = convert_label_shortcuts(new_label)
|
|
2230
2232
|
push_state("rename-curve")
|
|
2231
2233
|
labels[idx] = new_label
|
|
2232
2234
|
label_text_objects[idx].set_text(f"{idx+1}: {new_label}")
|
|
@@ -2250,9 +2252,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2250
2252
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
2251
2253
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
2252
2254
|
print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
2255
|
+
print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
|
|
2253
2256
|
new_name = _safe_input("New CIF tick label (q=cancel): ")
|
|
2254
2257
|
if not new_name or new_name.lower()=='q':
|
|
2255
2258
|
print("Canceled."); continue
|
|
2259
|
+
new_name = convert_label_shortcuts(new_name)
|
|
2256
2260
|
lab,fname,peaksQ,wl,qmax_sim,color = cts[idx]
|
|
2257
2261
|
# Suspend extension while updating label
|
|
2258
2262
|
if _bp is not None:
|
|
@@ -2278,10 +2282,12 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2278
2282
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
2279
2283
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
2280
2284
|
print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
2285
|
+
print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
|
|
2281
2286
|
new_axis = _safe_input("New axis label: ")
|
|
2282
2287
|
if not new_axis or new_axis.lower() == 'q':
|
|
2283
2288
|
print("Canceled.")
|
|
2284
2289
|
continue
|
|
2290
|
+
new_axis = convert_label_shortcuts(new_axis)
|
|
2285
2291
|
new_axis = normalize_label_text(new_axis)
|
|
2286
2292
|
push_state("rename-axis")
|
|
2287
2293
|
# Freeze layout and preserve current pad via one-shot pending to avoid drift
|
|
@@ -3307,6 +3313,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
3307
3313
|
if not cmd:
|
|
3308
3314
|
continue
|
|
3309
3315
|
if cmd == 'q':
|
|
3316
|
+
# Update ax._saved_tick_state before exiting so changes are persisted
|
|
3317
|
+
try:
|
|
3318
|
+
ax._saved_tick_state = dict(tick_state)
|
|
3319
|
+
except Exception:
|
|
3320
|
+
pass
|
|
3310
3321
|
break
|
|
3311
3322
|
if cmd == 'i':
|
|
3312
3323
|
# Invert tick direction (toggle between 'out' and 'in')
|
|
@@ -3578,6 +3589,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
3578
3589
|
continue
|
|
3579
3590
|
# Unknown code
|
|
3580
3591
|
print(f"Unknown code: {p}")
|
|
3592
|
+
# After tick toggles, update ax._saved_tick_state so dump_session can read it
|
|
3593
|
+
try:
|
|
3594
|
+
ax._saved_tick_state = dict(tick_state)
|
|
3595
|
+
except Exception:
|
|
3596
|
+
pass
|
|
3581
3597
|
# After tick toggles, update visibility and reposition ALL axis labels for independence
|
|
3582
3598
|
update_tick_visibility()
|
|
3583
3599
|
update_labels(ax, y_data_list, label_text_objects, args.stack, getattr(fig, '_stack_label_at_bottom', False))
|
batplot/modes.py
CHANGED
|
@@ -280,8 +280,8 @@ def handle_cv_mode(args) -> int:
|
|
|
280
280
|
ax.set_xlabel('Current (mA)', labelpad=8.0)
|
|
281
281
|
ax.set_ylabel('Voltage (V)', labelpad=8.0)
|
|
282
282
|
else:
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
ax.set_xlabel('Voltage (V)', labelpad=8.0)
|
|
284
|
+
ax.set_ylabel('Current (mA)', labelpad=8.0)
|
|
285
285
|
legend = ax.legend(title='Cycle')
|
|
286
286
|
legend.get_title().set_fontsize('medium')
|
|
287
287
|
# Adjust margins to prevent label clipping
|
|
@@ -642,8 +642,8 @@ def handle_gc_mode(args) -> int:
|
|
|
642
642
|
ln_c, = ax.plot(y_b, x_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
|
|
643
643
|
linewidth=2.0, label=str(cyc), alpha=0.8)
|
|
644
644
|
else:
|
|
645
|
-
|
|
646
|
-
|
|
645
|
+
ln_c, = ax.plot(x_b, y_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
|
|
646
|
+
linewidth=2.0, label=str(cyc), alpha=0.8)
|
|
647
647
|
else:
|
|
648
648
|
ln_c = None
|
|
649
649
|
mask_d = (cyc_int == cyc) & discharge_mask
|
|
@@ -656,8 +656,8 @@ def handle_gc_mode(args) -> int:
|
|
|
656
656
|
ln_d, = ax.plot(yd_b, xd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
|
|
657
657
|
linewidth=2.0, label=lbl, alpha=0.8)
|
|
658
658
|
else:
|
|
659
|
-
|
|
660
|
-
|
|
659
|
+
ln_d, = ax.plot(xd_b, yd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
|
|
660
|
+
linewidth=2.0, label=lbl, alpha=0.8)
|
|
661
661
|
else:
|
|
662
662
|
ln_d = None
|
|
663
663
|
cycle_lines[cyc] = {"charge": ln_c, "discharge": ln_d}
|
|
@@ -677,8 +677,8 @@ def handle_gc_mode(args) -> int:
|
|
|
677
677
|
ln_c, = ax.plot(y_b, x_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
|
|
678
678
|
linewidth=2.0, label=str(cyc), alpha=0.8)
|
|
679
679
|
else:
|
|
680
|
-
|
|
681
|
-
|
|
680
|
+
ln_c, = ax.plot(x_b, y_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
|
|
681
|
+
linewidth=2.0, label=str(cyc), alpha=0.8)
|
|
682
682
|
ln_d = None
|
|
683
683
|
if i < len(dch_blocks):
|
|
684
684
|
a, b = dch_blocks[i]
|
|
@@ -690,8 +690,8 @@ def handle_gc_mode(args) -> int:
|
|
|
690
690
|
ln_d, = ax.plot(yd_b, xd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
|
|
691
691
|
linewidth=2.0, label=lbl, alpha=0.8)
|
|
692
692
|
else:
|
|
693
|
-
|
|
694
|
-
|
|
693
|
+
ln_d, = ax.plot(xd_b, yd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
|
|
694
|
+
linewidth=2.0, label=lbl, alpha=0.8)
|
|
695
695
|
cycle_lines[cyc] = {"charge": ln_c, "discharge": ln_d}
|
|
696
696
|
|
|
697
697
|
# Swap x and y if --ro flag is set
|
|
@@ -699,8 +699,8 @@ def handle_gc_mode(args) -> int:
|
|
|
699
699
|
ax.set_xlabel('Voltage (V)', labelpad=8.0)
|
|
700
700
|
ax.set_ylabel(x_label_gc, labelpad=8.0)
|
|
701
701
|
else:
|
|
702
|
-
|
|
703
|
-
|
|
702
|
+
ax.set_xlabel(x_label_gc, labelpad=8.0)
|
|
703
|
+
ax.set_ylabel('Voltage (V)', labelpad=8.0)
|
|
704
704
|
legend = ax.legend(title='Cycle')
|
|
705
705
|
legend.get_title().set_fontsize('medium')
|
|
706
706
|
fig.subplots_adjust(left=0.12, right=0.95, top=0.88, bottom=0.15)
|
batplot/operando.py
CHANGED
|
@@ -315,6 +315,8 @@ def plot_operando_folder(folder: str, args) -> Tuple[plt.Figure, plt.Axes, Dict[
|
|
|
315
315
|
extent = (grid_x.min(), grid_x.max(), 0, Zm.shape[0]-1)
|
|
316
316
|
# Bottom-to-top visual order (scan 0 at bottom) to match EC time progression -> origin='lower'
|
|
317
317
|
im = ax.imshow(Zm, aspect='auto', origin='lower', extent=extent, cmap='viridis', interpolation='nearest')
|
|
318
|
+
# Store the colormap name explicitly so it can be retrieved reliably when saving
|
|
319
|
+
setattr(im, '_operando_cmap_name', 'viridis')
|
|
318
320
|
# Create custom colorbar axes on the left (will be positioned by layout function)
|
|
319
321
|
# Create a dummy axes that will be replaced by the custom colorbar in interactive menu
|
|
320
322
|
cbar_ax = fig.add_axes([0.0, 0.0, 0.01, 0.01]) # Temporary position, will be repositioned
|