batplot 1.8.41__tar.gz → 1.8.42__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 (124) hide show
  1. {batplot-1.8.41/batplot.egg-info → batplot-1.8.42}/PKG-INFO +1 -1
  2. {batplot-1.8.41 → batplot-1.8.42}/batplot/__init__.py +1 -1
  3. {batplot-1.8.41 → batplot-1.8.42}/batplot/color_utils.py +72 -0
  4. {batplot-1.8.41 → batplot-1.8.42}/batplot/data/CHANGELOG.md +4 -0
  5. {batplot-1.8.41 → batplot-1.8.42}/batplot/ec_common.py +7 -4
  6. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/cpc/routing.py +4 -4
  7. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/electrochem/actions.py +10 -0
  8. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/electrochem/dqdv_2d.py +5 -2
  9. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/electrochem/interactive.py +23 -1
  10. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/electrochem/style.py +4 -0
  11. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/operando/actions.py +6 -0
  12. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/operando/style.py +1 -1
  13. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/cif.py +2 -1
  14. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/colors.py +3 -24
  15. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/interactive.py +54 -14
  16. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/style.py +118 -135
  17. {batplot-1.8.41 → batplot-1.8.42}/batplot/session.py +61 -0
  18. {batplot-1.8.41 → batplot-1.8.42}/batplot/version_check.py +1 -2
  19. {batplot-1.8.41 → batplot-1.8.42/batplot.egg-info}/PKG-INFO +1 -1
  20. {batplot-1.8.41 → batplot-1.8.42}/batplot.egg-info/SOURCES.txt +1 -0
  21. {batplot-1.8.41 → batplot-1.8.42}/pyproject.toml +1 -1
  22. batplot-1.8.42/tests/test_color_utils.py +31 -0
  23. {batplot-1.8.41 → batplot-1.8.42}/tests/test_contracts.py +10 -13
  24. {batplot-1.8.41 → batplot-1.8.42}/tests/test_xy_modules.py +152 -2
  25. {batplot-1.8.41 → batplot-1.8.42}/LICENSE +0 -0
  26. {batplot-1.8.41 → batplot-1.8.42}/MANIFEST.in +0 -0
  27. {batplot-1.8.41 → batplot-1.8.42}/NOTICE +0 -0
  28. {batplot-1.8.41 → batplot-1.8.42}/README.md +0 -0
  29. {batplot-1.8.41 → batplot-1.8.42}/batplot/args.py +0 -0
  30. {batplot-1.8.41 → batplot-1.8.42}/batplot/batch.py +0 -0
  31. {batplot-1.8.41 → batplot-1.8.42}/batplot/batplot.py +0 -0
  32. {batplot-1.8.41 → batplot-1.8.42}/batplot/canvas_interactive.py +0 -0
  33. {batplot-1.8.41 → batplot-1.8.42}/batplot/cif.py +0 -0
  34. {batplot-1.8.41 → batplot-1.8.42}/batplot/cli.py +0 -0
  35. {batplot-1.8.41 → batplot-1.8.42}/batplot/config.py +0 -0
  36. {batplot-1.8.41 → batplot-1.8.42}/batplot/converters.py +0 -0
  37. {batplot-1.8.41 → batplot-1.8.42}/batplot/dev_upgrade.py +0 -0
  38. {batplot-1.8.41 → batplot-1.8.42}/batplot/modes.py +0 -0
  39. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/__init__.py +0 -0
  40. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/common/__init__.py +0 -0
  41. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/common/axis_state.py +0 -0
  42. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/common/files.py +0 -0
  43. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/common/fonts.py +0 -0
  44. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/common/interactive_state.py +0 -0
  45. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/common/menu_rendering.py +0 -0
  46. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/common/menus.py +0 -0
  47. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/common/palettes.py +0 -0
  48. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/common/smoothing.py +0 -0
  49. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/common/sources.py +0 -0
  50. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/common/spines.py +0 -0
  51. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/common/terminal.py +0 -0
  52. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/common/title_offsets.py +0 -0
  53. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/cpc/__init__.py +0 -0
  54. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/cpc/actions.py +0 -0
  55. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/cpc/colors.py +0 -0
  56. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/cpc/interactive.py +0 -0
  57. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/cpc/labels.py +0 -0
  58. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/cpc/legend.py +0 -0
  59. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/cpc/menu.py +0 -0
  60. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/cpc/session.py +0 -0
  61. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/cpc/snapshots.py +0 -0
  62. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/electrochem/__init__.py +0 -0
  63. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/electrochem/colors.py +0 -0
  64. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/electrochem/export.py +0 -0
  65. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/electrochem/labels.py +0 -0
  66. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/electrochem/legend.py +0 -0
  67. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/electrochem/legend_order.py +0 -0
  68. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/electrochem/line_style.py +0 -0
  69. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/electrochem/menu.py +0 -0
  70. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/electrochem/routing.py +0 -0
  71. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/electrochem/session.py +0 -0
  72. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/electrochem/spine_colors.py +0 -0
  73. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/operando/__init__.py +0 -0
  74. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/operando/colors.py +0 -0
  75. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/operando/grid.py +0 -0
  76. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/operando/interactive.py +0 -0
  77. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/operando/ions_axis.py +0 -0
  78. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/operando/labels.py +0 -0
  79. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/operando/layout.py +0 -0
  80. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/operando/line_style.py +0 -0
  81. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/operando/menu.py +0 -0
  82. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/operando/peaks.py +0 -0
  83. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/operando/plot.py +0 -0
  84. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/operando/routing.py +0 -0
  85. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/operando/session.py +0 -0
  86. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/operando/visibility.py +0 -0
  87. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/session_routing.py +0 -0
  88. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/__init__.py +0 -0
  89. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/actions.py +0 -0
  90. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/arrange.py +0 -0
  91. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/axis_range.py +0 -0
  92. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/data_ops.py +0 -0
  93. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/derivative.py +0 -0
  94. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/game.py +0 -0
  95. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/labels.py +0 -0
  96. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/line_style.py +0 -0
  97. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/menu.py +0 -0
  98. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/peaks.py +0 -0
  99. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/pipeline.py +0 -0
  100. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/session.py +0 -0
  101. {batplot-1.8.41 → batplot-1.8.42}/batplot/plot_modes/xy/smoothing.py +0 -0
  102. {batplot-1.8.41 → batplot-1.8.42}/batplot/plotting.py +0 -0
  103. {batplot-1.8.41 → batplot-1.8.42}/batplot/readers.py +0 -0
  104. {batplot-1.8.41 → batplot-1.8.42}/batplot/showcol.py +0 -0
  105. {batplot-1.8.41 → batplot-1.8.42}/batplot/style.py +0 -0
  106. {batplot-1.8.41 → batplot-1.8.42}/batplot/ui.py +0 -0
  107. {batplot-1.8.41 → batplot-1.8.42}/batplot/utils.py +0 -0
  108. {batplot-1.8.41 → batplot-1.8.42}/batplot.egg-info/dependency_links.txt +0 -0
  109. {batplot-1.8.41 → batplot-1.8.42}/batplot.egg-info/entry_points.txt +0 -0
  110. {batplot-1.8.41 → batplot-1.8.42}/batplot.egg-info/requires.txt +0 -0
  111. {batplot-1.8.41 → batplot-1.8.42}/batplot.egg-info/top_level.txt +0 -0
  112. {batplot-1.8.41 → batplot-1.8.42}/setup.cfg +0 -0
  113. {batplot-1.8.41 → batplot-1.8.42}/setup.py +0 -0
  114. {batplot-1.8.41 → batplot-1.8.42}/tests/test_cli_smoke.py +0 -0
  115. {batplot-1.8.41 → batplot-1.8.42}/tests/test_common_files.py +0 -0
  116. {batplot-1.8.41 → batplot-1.8.42}/tests/test_common_palettes.py +0 -0
  117. {batplot-1.8.41 → batplot-1.8.42}/tests/test_cpc_roundtrip.py +0 -0
  118. {batplot-1.8.41 → batplot-1.8.42}/tests/test_csv_readers.py +0 -0
  119. {batplot-1.8.41 → batplot-1.8.42}/tests/test_dev_upgrade.py +0 -0
  120. {batplot-1.8.41 → batplot-1.8.42}/tests/test_ec_roundtrip.py +0 -0
  121. {batplot-1.8.41 → batplot-1.8.42}/tests/test_interactive_menu_smoke.py +0 -0
  122. {batplot-1.8.41 → batplot-1.8.42}/tests/test_interactive_state.py +0 -0
  123. {batplot-1.8.41 → batplot-1.8.42}/tests/test_operando_roundtrip.py +0 -0
  124. {batplot-1.8.41 → batplot-1.8.42}/tests/test_xy_roundtrip.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.8.41
3
+ Version: 1.8.42
4
4
  Summary: Interactive plotting tool for material science (1D plot) and electrochemistry (GC, CV, dQ/dV, CPC, operando) with batch processing
5
5
  Author-email: Tian Dai <tianda@uio.no>
6
6
  License: MIT License
@@ -7,7 +7,7 @@ import importlib.abc
7
7
  import importlib.machinery
8
8
  import sys
9
9
 
10
- __version__ = "1.8.41"
10
+ __version__ = "1.8.42"
11
11
 
12
12
 
13
13
  _LEGACY_MODULE_ALIASES = {
@@ -177,6 +177,76 @@ def ensure_colormap(name: Optional[str]) -> bool:
177
177
  return False
178
178
 
179
179
 
180
+ def get_colormap(name: Optional[str]) -> Optional[Colormap]:
181
+ """Return a Colormap by name across matplotlib versions and custom maps.
182
+
183
+ Prefer this over ``matplotlib.cm.get_cmap`` — that API was removed in
184
+ matplotlib 3.11, which broke palette commands such as ``all viridis`` on
185
+ Windows installs with newer matplotlib.
186
+ """
187
+ if not name:
188
+ return None
189
+
190
+ ensure_colormap(name)
191
+
192
+ candidates: List[str] = []
193
+ for candidate in (name, name.lower()):
194
+ if candidate and candidate not in candidates:
195
+ candidates.append(candidate)
196
+
197
+ try:
198
+ from matplotlib import colormaps as mpl_colormaps
199
+
200
+ registry_get = getattr(mpl_colormaps, "get_cmap", None)
201
+ if callable(registry_get):
202
+ for candidate in candidates:
203
+ try:
204
+ return registry_get(candidate)
205
+ except Exception:
206
+ pass
207
+ for candidate in candidates:
208
+ try:
209
+ return mpl_colormaps[candidate]
210
+ except Exception:
211
+ pass
212
+ except Exception:
213
+ pass
214
+
215
+ for candidate in candidates:
216
+ try:
217
+ return plt.get_cmap(candidate)
218
+ except Exception:
219
+ pass
220
+
221
+ reversed_flag = name.lower().endswith("_r")
222
+ base = name[:-2] if reversed_flag else name
223
+ base_lower = base.lower()
224
+
225
+ custom = _CUSTOM_CMAPS.get(base_lower)
226
+ if custom:
227
+ try:
228
+ cmap_obj = LinearSegmentedColormap.from_list(base_lower, custom, N=256)
229
+ if reversed_flag:
230
+ cmap_obj = cmap_obj.reversed()
231
+ return cmap_obj
232
+ except Exception:
233
+ pass
234
+
235
+ if base_lower.startswith("batlow"):
236
+ try:
237
+ import cmcrameri.cm as cmc # type: ignore[import]
238
+
239
+ cmap_obj = getattr(cmc, base_lower, None) or getattr(cmc, "batlow", None)
240
+ if cmap_obj is not None:
241
+ if reversed_flag and hasattr(cmap_obj, "reversed"):
242
+ return cmap_obj.reversed()
243
+ return cmap_obj
244
+ except Exception:
245
+ pass
246
+
247
+ return None
248
+
249
+
180
250
  def _ansi_color_block_from_rgba(rgba) -> str:
181
251
  """Return a two-space block with the given RGBA color."""
182
252
  try:
@@ -570,6 +640,8 @@ __all__ = [
570
640
  'clear_user_colors',
571
641
  'color_bar',
572
642
  'color_block',
643
+ 'ensure_colormap',
644
+ 'get_colormap',
573
645
  'manage_user_colors',
574
646
  'palette_preview',
575
647
  'print_user_colors',
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.8.42] - 2026-06-12
4
+ - Bug fixes
5
+
6
+
3
7
  ## [1.8.41] - 2026-06-12
4
8
  - Bug fixes
5
9
  - Complete update of the codebase
@@ -20,7 +20,8 @@ except ImportError:
20
20
 
21
21
  _EC_DEFAULT_FIGSIZE = (10.0, 6.0)
22
22
  _EC_DEFAULT_LAYOUT = {'left': 0.12, 'right': 0.95, 'top': 0.88, 'bottom': 0.15}
23
- _CPC_DEFAULT_LAYOUT = {'left': 0.12, 'right': 0.88, 'top': 0.88, 'bottom': 0.15}
23
+ # CPC shares the same canvas and plot-frame defaults as GC/CV/dQ/dV.
24
+ _CPC_DEFAULT_LAYOUT = _EC_DEFAULT_LAYOUT
24
25
  _EC_DEFAULT_FRAME_SIZE = (
25
26
  _EC_DEFAULT_FIGSIZE[0] * (_EC_DEFAULT_LAYOUT['right'] - _EC_DEFAULT_LAYOUT['left']),
26
27
  _EC_DEFAULT_FIGSIZE[1] * (_EC_DEFAULT_LAYOUT['top'] - _EC_DEFAULT_LAYOUT['bottom']),
@@ -39,12 +40,14 @@ def _default_ec_figsize() -> tuple[float, float]:
39
40
 
40
41
 
41
42
  def _default_cpc_figsize() -> tuple[float, float]:
42
- return _figsize_for_frame(_CPC_DEFAULT_LAYOUT)
43
+ """Alias for :func:`_default_ec_figsize` (GC/CV/dQ/dV/CPC share one default)."""
44
+ return _default_ec_figsize()
43
45
 
44
46
 
45
47
  def _apply_default_ec_layout(fig, *, cpc: bool = False) -> None:
46
- """Apply the default layout while keeping the plotted frame size identical."""
47
- fig.subplots_adjust(**(_CPC_DEFAULT_LAYOUT if cpc else _EC_DEFAULT_LAYOUT))
48
+ """Apply the default electrochem layout (same for GC, CV, dQ/dV, and CPC)."""
49
+ del cpc # kept for backward compatibility; CPC uses the same layout as other EC modes
50
+ fig.subplots_adjust(**_EC_DEFAULT_LAYOUT)
48
51
 
49
52
 
50
53
  def _resolve_mass(mass_arg, file_idx: int = 0):
@@ -23,7 +23,7 @@ import matplotlib.colors as mcolors # type: ignore[import-untyped]
23
23
 
24
24
  from ...ec_common import (
25
25
  _resolve_mass,
26
- _default_cpc_figsize,
26
+ _default_ec_figsize,
27
27
  _apply_default_ec_layout,
28
28
  )
29
29
  from ...batch import _apply_ec_style
@@ -340,8 +340,8 @@ def handle_cpc_mode(args) -> int:
340
340
  print("No valid CPC data files to plot.")
341
341
  exit(1)
342
342
 
343
- # Plot (same figsize as GC)
344
- fig, ax = plt.subplots(figsize=_default_cpc_figsize())
343
+ # Plot (same canvas and frame size as GC/CV/dQ/dV)
344
+ fig, ax = plt.subplots(figsize=_default_ec_figsize())
345
345
  ax.set_xlabel('Cycle number', labelpad=8.0)
346
346
  if is_epc:
347
347
  ax.set_ylabel(r'Specific Energy (mWh g$^{-1}$)', labelpad=8.0)
@@ -439,7 +439,7 @@ def handle_cpc_mode(args) -> int:
439
439
  print(f"Warning: Could not create CPC legend: {e}")
440
440
 
441
441
  # Adjust layout to ensure top and bottom labels/titles are visible
442
- _apply_default_ec_layout(fig, cpc=True)
442
+ _apply_default_ec_layout(fig)
443
443
 
444
444
  # Check for style file in file list
445
445
  style_file_path = None
@@ -540,6 +540,16 @@ def handle_import_style_command(ctx: ElectrochemActionContext) -> None: # pyrig
540
540
  plt.rcParams['mathtext.fontset'] = font_cfg['mathtext_fontset']
541
541
  except Exception:
542
542
  pass
543
+ axis_label_colors = cfg.get('axis_label_colors') or {}
544
+ try:
545
+ if axis_label_colors.get('x'):
546
+ ax.xaxis.label.set_color(axis_label_colors['x'])
547
+ ax._stored_xlabel_color = axis_label_colors['x']
548
+ if axis_label_colors.get('y'):
549
+ ax.yaxis.label.set_color(axis_label_colors['y'])
550
+ ax._stored_ylabel_color = axis_label_colors['y']
551
+ except Exception:
552
+ pass
543
553
  except Exception as e:
544
554
  print(f"Warning: Could not apply figure/font settings: {e}")
545
555
 
@@ -10,6 +10,7 @@ from matplotlib.ticker import FuncFormatter, NullFormatter # type: ignore[impor
10
10
 
11
11
  from ...ui import capture_axes_tick_locators, restore_axes_tick_locators
12
12
  from ...utils import natural_sort_key
13
+ from ...ec_common import _default_ec_figsize
13
14
 
14
15
 
15
16
  def _dqdv_2d_row_tick_indices(n_rows: int, max_ticks: int = 24) -> np.ndarray:
@@ -536,11 +537,13 @@ def restore_dqdv_2d_companion_figure(blob: Dict[str, Any]) -> Optional[Tuple[Any
536
537
  row_labels = [str(x) for x in (blob.get("row_labels") or [])]
537
538
  zlab = str(blob.get("zlabel") or "dQ/dV")
538
539
  cmap = str(blob.get("cmap") or "viridis")
539
- figsize = blob.get("figsize") or [8.0, 6.0]
540
+ figsize = blob.get("figsize")
541
+ if not figsize:
542
+ figsize = list(_default_ec_figsize())
540
543
  try:
541
544
  cfig, cax = plt.subplots(figsize=(float(figsize[0]), float(figsize[1])))
542
545
  except Exception:
543
- cfig, cax = plt.subplots(figsize=(8, 6))
546
+ cfig, cax = plt.subplots(figsize=_default_ec_figsize())
544
547
  Zm = np.ma.masked_invalid(Z)
545
548
  extent = (0.0, float(2 * dv), -0.5, float(Zm.shape[0] - 0.5))
546
549
  im = cax.imshow(
@@ -32,6 +32,7 @@ from matplotlib.ticker import ( # type: ignore[import-untyped]
32
32
  AutoLocator,
33
33
  )
34
34
  from ...plotting import update_labels as _update_labels
35
+ from ...ec_common import _default_ec_figsize
35
36
  import matplotlib as mpl # type: ignore[import-untyped]
36
37
  from ...color_utils import (
37
38
  color_block,
@@ -981,6 +982,11 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Optional[Dict[int, Dict[s
981
982
  'font_size': plt.rcParams.get('font.size'),
982
983
  'font_family': plt.rcParams.get('font.family'),
983
984
  'font_sans_serif': list(plt.rcParams.get('font.sans-serif', [])),
985
+ 'mathtext_fontset': plt.rcParams.get('mathtext.fontset'),
986
+ 'axis_label_colors': {
987
+ 'x': getattr(ax, '_stored_xlabel_color', None) or ax.xaxis.label.get_color(),
988
+ 'y': getattr(ax, '_stored_ylabel_color', None) or ax.yaxis.label.get_color(),
989
+ },
984
990
  'titles': {
985
991
  'top_x': bool(getattr(ax, '_top_xlabel_on', False)),
986
992
  'right_y': bool(getattr(ax, '_right_ylabel_on', False))
@@ -1210,6 +1216,22 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Optional[Dict[int, Dict[s
1210
1216
  _rebuild_legend(ax)
1211
1217
  except Exception:
1212
1218
  pass
1219
+ try:
1220
+ mathtext_fontset = snap.get('mathtext_fontset')
1221
+ if mathtext_fontset:
1222
+ mpl.rcParams['mathtext.fontset'] = mathtext_fontset
1223
+ except Exception:
1224
+ pass
1225
+ try:
1226
+ axis_label_colors = snap.get('axis_label_colors') or {}
1227
+ if axis_label_colors.get('x') is not None:
1228
+ ax.xaxis.label.set_color(axis_label_colors['x'])
1229
+ ax._stored_xlabel_color = axis_label_colors['x']
1230
+ if axis_label_colors.get('y') is not None:
1231
+ ax.yaxis.label.set_color(axis_label_colors['y'])
1232
+ ax._stored_ylabel_color = axis_label_colors['y']
1233
+ except Exception:
1234
+ pass
1213
1235
  # Title offsets - all four titles
1214
1236
  try:
1215
1237
  offsets = snap.get('title_offsets', {})
@@ -1998,7 +2020,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Optional[Dict[int, Dict[s
1998
2020
  im = None
1999
2021
  dv = float(v_hi - v_lo)
2000
2022
  try:
2001
- cfig, cax = plt.subplots(figsize=(8, 6))
2023
+ cfig, cax = plt.subplots(figsize=_default_ec_figsize())
2002
2024
  Zm = np.ma.masked_invalid(Z)
2003
2025
  extent = (0.0, float(2 * dv), -0.5, float(Zm.shape[0] - 0.5))
2004
2026
  im = cax.imshow(
@@ -321,6 +321,10 @@ def _get_style_snapshot(fig, ax, cycle_lines: Dict, tick_state: Dict, file_data:
321
321
  'size': font_size,
322
322
  'mathtext_fontset': plt.rcParams.get('mathtext.fontset'),
323
323
  },
324
+ 'axis_label_colors': {
325
+ 'x': mcolors.to_hex(getattr(ax, '_stored_xlabel_color', None) or ax.xaxis.label.get_color()),
326
+ 'y': mcolors.to_hex(getattr(ax, '_stored_ylabel_color', None) or ax.yaxis.label.get_color()),
327
+ },
324
328
  'legend': {
325
329
  'visible': legend_visible,
326
330
  'position_inches': legend_xy_in,
@@ -855,6 +855,12 @@ def handle_import_style(ctx: OperandoActionContext) -> None: # pyright: ignore[
855
855
  font = cfg.get('font', {})
856
856
  fam = font.get('family')
857
857
  size = font.get('size')
858
+ mathtext_fs = font.get('mathtext_fontset')
859
+ if mathtext_fs:
860
+ try:
861
+ plt.rcParams['mathtext.fontset'] = mathtext_fs
862
+ except Exception:
863
+ pass
858
864
  if fam or size is not None:
859
865
  try:
860
866
  set_fonts(family=fam if fam else None, size=size if size is not None else None)
@@ -367,7 +367,7 @@ def build_operando_ec_style_config_v2(fig, ax, im, cbar, ec_ax, exp_choice: str)
367
367
  "title_offsets": op_title_offsets,
368
368
  },
369
369
  "ec": ec_payload,
370
- "font": {"family": fam, "size": fsize},
370
+ "font": {"family": fam, "size": fsize, "mathtext_fontset": plt.rcParams.get("mathtext.fontset")},
371
371
  "colorbar": {"label": cb_label_text, "mode": cb_label_mode, "visible": cb_vis},
372
372
  }
373
373
  default_ext = ".bps" if exp_choice == "ps" else ".bpsg"
@@ -24,6 +24,7 @@ from ...utils import (
24
24
  from ...color_utils import (
25
25
  color_block,
26
26
  ensure_colormap,
27
+ get_colormap,
27
28
  resolve_color_token,
28
29
  _CUSTOM_CMAPS,
29
30
  )
@@ -352,7 +353,7 @@ def run_cif_ticks_menu(
352
353
  print("No valid indices parsed.")
353
354
  continue
354
355
  try:
355
- cmap = ensure_colormap(palette_name) or plt.get_cmap(palette_name)
356
+ cmap = get_colormap(palette_name)
356
357
  except Exception:
357
358
  cmap = None
358
359
  if cmap is None:
@@ -9,13 +9,10 @@ so undo and CIF redraw behavior are unchanged.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
- import importlib as _il
13
12
  from typing import Any, Callable, List, Optional, Sequence
14
13
 
15
14
  import matplotlib.pyplot as plt # type: ignore[import]
16
- import matplotlib.cm as _cm # type: ignore[import]
17
15
  from matplotlib import colors as mcolors # type: ignore[import]
18
- from matplotlib.colors import LinearSegmentedColormap # type: ignore[import]
19
16
 
20
17
  from ...plotting import apply_curve_color, update_labels
21
18
  from ...ui import set_spine_side_color as _ui_set_spine_side_color
@@ -27,6 +24,7 @@ from ...color_utils import (
27
24
  get_user_color_list,
28
25
  resolve_color_token,
29
26
  ensure_colormap,
27
+ get_colormap,
30
28
  _CUSTOM_CMAPS,
31
29
  )
32
30
  from ..common.palettes import (
@@ -81,26 +79,7 @@ def run_xy_color_menu(
81
79
  return resolve_palette_token(token, _palette_index)
82
80
 
83
81
  def _apply_palette_to_lines(palette_name, indices):
84
- ensure_colormap(palette_name)
85
- cmap = None
86
- try:
87
- cmap = _cm.get_cmap(palette_name)
88
- except Exception:
89
- pass
90
- if cmap is None and palette_name.lower().startswith('batlow'):
91
- try:
92
- _cmc = _il.import_module('cmcrameri.cm')
93
- _attr = palette_name.lower()
94
- cmap = getattr(_cmc, _attr, None) or getattr(_cmc, 'batlow', None)
95
- except Exception:
96
- pass
97
- if cmap is None:
98
- _base = palette_name.lower().rstrip('_r')
99
- _cc = _CUSTOM_CMAPS.get(_base)
100
- if _cc:
101
- cmap = LinearSegmentedColormap.from_list(_base, _cc, N=256)
102
- if palette_name.lower().endswith('_r'):
103
- cmap = cmap.reversed()
82
+ cmap = get_colormap(palette_name)
104
83
  if cmap is None:
105
84
  print(f"Unknown palette '{palette_name}'.")
106
85
  return
@@ -234,7 +213,7 @@ def run_xy_color_menu(
234
213
  print("No valid indices parsed.")
235
214
  continue
236
215
  try:
237
- cmap = ensure_colormap(pal_name) or plt.get_cmap(pal_name)
216
+ cmap = get_colormap(pal_name)
238
217
  except Exception:
239
218
  cmap = None
240
219
  if cmap is None:
@@ -18,7 +18,7 @@ from matplotlib.ticker import ( # type: ignore[import]
18
18
  NullLocator,
19
19
  )
20
20
 
21
- from ...plotting import update_labels
21
+ from ...plotting import apply_curve_color, update_labels
22
22
  from ...utils import (
23
23
  normalize_label_text,
24
24
  )
@@ -39,6 +39,8 @@ from ...ui import (
39
39
  from .style import (
40
40
  print_style_info as _bp_print_style_info,
41
41
  apply_style_config as _bp_apply_style_config,
42
+ capture_xy_axis_style,
43
+ apply_xy_axis_style,
42
44
  )
43
45
  from ...config import load_config, save_config
44
46
  from .style import export_style_config as _export_style_config
@@ -1262,7 +1264,9 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1262
1264
  "rotation_angle": getattr(ax, '_rotation_angle', 0),
1263
1265
  "stack_label_at_bottom": getattr(fig, '_stack_label_at_bottom', False),
1264
1266
  "label_anchor_left": getattr(fig, '_label_anchor_left', False),
1265
- "grid": any(line.get_visible() for line in ax.get_xgridlines() + ax.get_ygridlines())
1267
+ "grid": any(line.get_visible() for line in ax.get_xgridlines() + ax.get_ygridlines()),
1268
+ "curve_palettes": list(getattr(fig, '_curve_palette_history', []) or []),
1269
+ "axis_style": capture_xy_axis_style(ax),
1266
1270
  }
1267
1271
  # Optional per-set CIF visibility state for 1D mode
1268
1272
  try:
@@ -1494,6 +1498,20 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1494
1498
  except Exception:
1495
1499
  pass
1496
1500
 
1501
+ # Tick/label colors and labelpads
1502
+ try:
1503
+ axis_style = snap.get("axis_style")
1504
+ if axis_style:
1505
+ spine_specs = {
1506
+ name: {"color": spec.get("color")}
1507
+ for name, spec in snap.get("spines", {}).items()
1508
+ }
1509
+ apply_xy_axis_style(ax, axis_style, fig=fig, spines_cfg=spine_specs)
1510
+ _ui_position_bottom_xlabel(ax, fig, tick_state)
1511
+ _ui_position_left_ylabel(ax, fig, tick_state)
1512
+ except Exception:
1513
+ pass
1514
+
1497
1515
  # Labels list
1498
1516
  labels[:] = snap["labels"]
1499
1517
 
@@ -1503,22 +1521,30 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1503
1521
  i = item["index"]
1504
1522
  ln = _line(i)
1505
1523
  ln.set_data(item["x"], item["y"])
1506
- ln.set_color(item["color"])
1507
- ln.set_linewidth(item["lw"])
1508
- ln.set_linestyle(item["ls"])
1524
+ ln.set_linewidth(item["lw"])
1525
+ ln.set_linestyle(item["ls"])
1509
1526
  if item["marker"] is not None:
1510
- ln.set_marker(item["marker"])
1527
+ ln.set_marker(item["marker"])
1511
1528
  if item.get("markersize") is not None:
1512
- try: ln.set_markersize(item["markersize"])
1513
- except Exception: pass
1529
+ try:
1530
+ ln.set_markersize(item["markersize"])
1531
+ except Exception:
1532
+ pass
1533
+ if item["alpha"] is not None:
1534
+ ln.set_alpha(item["alpha"])
1535
+ apply_curve_color(ln, item["color"])
1514
1536
  if item.get("mfc") is not None:
1515
- try: ln.set_markerfacecolor(item["mfc"])
1516
- except Exception: pass
1537
+ try:
1538
+ if str(item["mfc"]).lower() == "none":
1539
+ ln.set_markerfacecolor("none")
1540
+ except Exception:
1541
+ pass
1517
1542
  if item.get("mec") is not None:
1518
- try: ln.set_markeredgecolor(item["mec"])
1519
- except Exception: pass
1520
- if item["alpha"] is not None:
1521
- ln.set_alpha(item["alpha"])
1543
+ try:
1544
+ if str(item["mec"]).lower() == "none":
1545
+ ln.set_markeredgecolor("none")
1546
+ except Exception:
1547
+ pass
1522
1548
 
1523
1549
  # Replace lists
1524
1550
  x_data_list[:] = [np.array(a, copy=True) for a in snap["x_data_list"]]
@@ -1602,6 +1628,20 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1602
1628
  if 'label_anchor_left' in snap:
1603
1629
  fig._label_anchor_left = bool(snap['label_anchor_left'])
1604
1630
 
1631
+ if snap.get("curve_palettes"):
1632
+ fig._curve_palette_history = [
1633
+ {
1634
+ 'palette': rec.get('palette'),
1635
+ 'indices': list(rec.get('indices', [])),
1636
+ 'low_clip': float(rec.get('low_clip', 0.08)),
1637
+ 'high_clip': float(rec.get('high_clip', 0.85)),
1638
+ }
1639
+ for rec in snap["curve_palettes"]
1640
+ if rec.get('palette') and rec.get('indices')
1641
+ ]
1642
+ elif hasattr(fig, '_curve_palette_history'):
1643
+ delattr(fig, '_curve_palette_history')
1644
+
1605
1645
  # Restore grid state
1606
1646
  if 'grid' in snap:
1607
1647
  try: