batplot 1.7.28__py3-none-any.whl → 1.8.1__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 +3 -3
- batplot/cpc_interactive.py +89 -3
- batplot/electrochem_interactive.py +118 -55
- batplot/interactive.py +100 -63
- batplot/modes.py +12 -12
- batplot/operando.py +2 -0
- batplot/operando_ec_interactive.py +260 -89
- batplot/session.py +18 -1
- batplot/utils.py +40 -0
- batplot/version_check.py +85 -6
- {batplot-1.7.28.dist-info → batplot-1.8.1.dist-info}/METADATA +1 -1
- {batplot-1.7.28.dist-info → batplot-1.8.1.dist-info}/RECORD +17 -17
- {batplot-1.7.28.dist-info → batplot-1.8.1.dist-info}/WHEEL +0 -0
- {batplot-1.7.28.dist-info → batplot-1.8.1.dist-info}/entry_points.txt +0 -0
- {batplot-1.7.28.dist-info → batplot-1.8.1.dist-info}/licenses/LICENSE +0 -0
- {batplot-1.7.28.dist-info → batplot-1.8.1.dist-info}/top_level.txt +0 -0
batplot/__init__.py
CHANGED
batplot/args.py
CHANGED
|
@@ -169,7 +169,7 @@ def _print_general_help() -> None:
|
|
|
169
169
|
" batplot --cv FILE.txt # EC CV (cyclic voltammetry) from .txt\n"
|
|
170
170
|
" batplot --cv --all # Batch: all .mpt/.txt in directory (CV mode)\n\n"
|
|
171
171
|
" [Operando]\n"
|
|
172
|
-
" batplot --operando --i [FOLDER] # Operando contour (with or without .mpt file)
|
|
172
|
+
" batplot --operando --i [FOLDER] # Operando contour (with or without .mpt file)\n\n"
|
|
173
173
|
"Features:\n"
|
|
174
174
|
" • Quick plotting with sensible defaults, no config files needed\n"
|
|
175
175
|
" • Supports many common file formats (see -h xy/ec/op)\n"
|
|
@@ -183,9 +183,9 @@ def _print_general_help() -> None:
|
|
|
183
183
|
" batplot -h xy # XY file plotting guide\n"
|
|
184
184
|
" batplot -h ec # Electrochemistry (GC/dQdV/CV/CPC) guide\n"
|
|
185
185
|
" batplot -h op # Operando guide\n"
|
|
186
|
-
" batplot -m # Open the illustrated txt manual with highlights\n"
|
|
186
|
+
" batplot -m # Open the illustrated txt manual with highlights\n\n"
|
|
187
187
|
|
|
188
|
-
"Contact & Updates:\n
|
|
188
|
+
"Contact & Updates:\n"
|
|
189
189
|
" Subscribe to batplot-lab@kjemi.uio.no for updates\n"
|
|
190
190
|
" (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
191
|
" Kindly cite the pypi package page (https://pypi.org/project/batplot/) if the plot is used for publication\n"
|
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
|
|
@@ -3141,6 +3218,9 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3141
3218
|
_print_menu(); continue
|
|
3142
3219
|
elif key == 't':
|
|
3143
3220
|
# Unified WASD toggles for spines/ticks/minor/labels/title per side
|
|
3221
|
+
# Import UI positioning functions locally to ensure they're accessible in nested functions
|
|
3222
|
+
from .ui import position_top_xlabel as _ui_position_top_xlabel, position_bottom_xlabel as _ui_position_bottom_xlabel, position_left_ylabel as _ui_position_left_ylabel, position_right_ylabel as _ui_position_right_ylabel
|
|
3223
|
+
|
|
3144
3224
|
try:
|
|
3145
3225
|
# Local WASD state stored on figure to persist across openings
|
|
3146
3226
|
wasd = getattr(fig, '_cpc_wasd_state', None)
|
|
@@ -3735,6 +3815,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3735
3815
|
print("Tip: Use LaTeX/mathtext for special characters:")
|
|
3736
3816
|
print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
|
|
3737
3817
|
print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
|
|
3818
|
+
print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
|
|
3738
3819
|
while True:
|
|
3739
3820
|
print("Rename: x=x-axis, ly=left y-axis, ry=right y-axis, l=legend labels, q=back")
|
|
3740
3821
|
sub = _safe_input("Rename> ").strip().lower()
|
|
@@ -3785,6 +3866,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3785
3866
|
print(f"Current file name in legend: '{base_name}'")
|
|
3786
3867
|
new_name = _safe_input("Enter new file name (q=cancel): ").strip()
|
|
3787
3868
|
if new_name and new_name.lower() != 'q':
|
|
3869
|
+
new_name = convert_label_shortcuts(new_name)
|
|
3788
3870
|
try:
|
|
3789
3871
|
push_state("rename-legend")
|
|
3790
3872
|
|
|
@@ -3892,6 +3974,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3892
3974
|
print(f"Current file name in legend: '{base_name}'")
|
|
3893
3975
|
new_name = _safe_input("Enter new file name (q=cancel): ").strip()
|
|
3894
3976
|
if new_name and new_name.lower() != 'q':
|
|
3977
|
+
new_name = convert_label_shortcuts(new_name)
|
|
3895
3978
|
try:
|
|
3896
3979
|
push_state("rename-legend")
|
|
3897
3980
|
|
|
@@ -3963,6 +4046,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3963
4046
|
print(f"Current x-axis title: '{current}'")
|
|
3964
4047
|
new_title = _safe_input("Enter new x-axis title (q=cancel): ")
|
|
3965
4048
|
if new_title and new_title.lower() != 'q':
|
|
4049
|
+
new_title = convert_label_shortcuts(new_title)
|
|
3966
4050
|
try:
|
|
3967
4051
|
push_state("rename-x")
|
|
3968
4052
|
ax.set_xlabel(new_title)
|
|
@@ -3982,6 +4066,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3982
4066
|
print(f"Current left y-axis title: '{current}'")
|
|
3983
4067
|
new_title = _safe_input("Enter new left y-axis title (q=cancel): ")
|
|
3984
4068
|
if new_title and new_title.lower() != 'q':
|
|
4069
|
+
new_title = convert_label_shortcuts(new_title)
|
|
3985
4070
|
try:
|
|
3986
4071
|
push_state("rename-ly")
|
|
3987
4072
|
ax.set_ylabel(new_title)
|
|
@@ -3996,6 +4081,7 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
3996
4081
|
print(f"Current right y-axis title: '{current}'")
|
|
3997
4082
|
new_title = _safe_input("Enter new right y-axis title (q=cancel): ")
|
|
3998
4083
|
if new_title and new_title.lower() != 'q':
|
|
4084
|
+
new_title = convert_label_shortcuts(new_title)
|
|
3999
4085
|
try:
|
|
4000
4086
|
push_state("rename-ry")
|
|
4001
4087
|
ax2.set_ylabel(new_title)
|