toucan-plot 0.5.0__tar.gz → 0.5.1__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 (33) hide show
  1. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/PKG-INFO +5 -5
  2. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/README.md +4 -4
  3. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/pyproject.toml +1 -1
  4. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/main.py +318 -6
  5. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/__init__.py +0 -0
  6. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/__main__.py +0 -0
  7. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/adjustments-dark.svg +0 -0
  8. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/adjustments-light.svg +0 -0
  9. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/arrow-autofit-height-dark.svg +0 -0
  10. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/arrow-autofit-height-light.svg +0 -0
  11. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/arrow-narrow-left-dark.svg +0 -0
  12. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/arrow-narrow-left-light.svg +0 -0
  13. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/arrow-narrow-right-dark.svg +0 -0
  14. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/arrow-narrow-right-light.svg +0 -0
  15. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/device-floppy-dark.svg +0 -0
  16. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/device-floppy-light.svg +0 -0
  17. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/hand-stop-dark.svg +0 -0
  18. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/hand-stop-light.svg +0 -0
  19. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/home-dark.svg +0 -0
  20. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/home-light.svg +0 -0
  21. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/ico/toucan-plot.ico +0 -0
  22. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/ico/toucan-plot.png +0 -0
  23. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/layout-grid-add-dark.svg +0 -0
  24. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/layout-grid-add-light.svg +0 -0
  25. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/math-function-dark.svg +0 -0
  26. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/math-function-light.svg +0 -0
  27. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/ruler-measure-dark.svg +0 -0
  28. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/ruler-measure-light.svg +0 -0
  29. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/zoom-dark.svg +0 -0
  30. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/assets/zoom-light.svg +0 -0
  31. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/utils/__init__.py +0 -0
  32. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/utils/loaders.py +0 -0
  33. {toucan_plot-0.5.0 → toucan_plot-0.5.1}/src/toucan_plot/utils/styles.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: toucan-plot
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: A versatile PySide6 and Matplotlib-based plotting tool for CSV and SMV data files.
5
5
  License-Expression: MIT
6
6
  Requires-Dist: pyside6>=6.0
@@ -18,9 +18,9 @@ Description-Content-Type: text/markdown
18
18
 
19
19
  # TouCAN-Plot
20
20
 
21
- An interactive PyQt6 + Matplotlib plotting tool for CSV, SMV, and CAN bus log files.
21
+ An interactive PySide6 + Matplotlib plotting tool for CSV, SMV, and CAN bus log files.
22
22
 
23
- ![Python](https://img.shields.io/badge/python-3.11+-blue)
23
+ ![Python](https://img.shields.io/badge/python-3.13+-blue)
24
24
  ![License](https://img.shields.io/badge/license-MIT-green)
25
25
 
26
26
  ## Features
@@ -115,8 +115,8 @@ toucan-plot
115
115
 
116
116
  ## Requirements
117
117
 
118
- - Python ≥ 3.11
119
- - PyQt6
118
+ - Python ≥ 3.13
119
+ - PySide6
120
120
  - Matplotlib
121
121
  - NumPy
122
122
  - pyqtdarktheme
@@ -1,8 +1,8 @@
1
1
  # TouCAN-Plot
2
2
 
3
- An interactive PyQt6 + Matplotlib plotting tool for CSV, SMV, and CAN bus log files.
3
+ An interactive PySide6 + Matplotlib plotting tool for CSV, SMV, and CAN bus log files.
4
4
 
5
- ![Python](https://img.shields.io/badge/python-3.11+-blue)
5
+ ![Python](https://img.shields.io/badge/python-3.13+-blue)
6
6
  ![License](https://img.shields.io/badge/license-MIT-green)
7
7
 
8
8
  ## Features
@@ -97,8 +97,8 @@ toucan-plot
97
97
 
98
98
  ## Requirements
99
99
 
100
- - Python ≥ 3.11
101
- - PyQt6
100
+ - Python ≥ 3.13
101
+ - PySide6
102
102
  - Matplotlib
103
103
  - NumPy
104
104
  - pyqtdarktheme
@@ -4,7 +4,7 @@ requires = ["uv-build>=0.11.14,<0.12"]
4
4
 
5
5
  [project]
6
6
  name = "toucan-plot"
7
- version = "0.5.0"
7
+ version = "0.5.1"
8
8
  requires-python = ">=3.13"
9
9
  description = "A versatile PySide6 and Matplotlib-based plotting tool for CSV and SMV data files."
10
10
  license = "MIT"
@@ -1,7 +1,9 @@
1
1
  import os
2
2
  import sys
3
3
  import multiprocessing
4
+ import ast
4
5
  import numpy as np
6
+ import xml.etree.ElementTree as ET
5
7
  from PySide6 import QtWidgets, QtCore, QtGui
6
8
  import matplotlib as mpl
7
9
  from matplotlib.figure import Figure
@@ -55,6 +57,12 @@ class MainWindow(QtWidgets.QMainWindow):
55
57
  file_menu.addSeparator()
56
58
  file_menu.addAction(exit_action)
57
59
 
60
+ options_menu = menubar.addMenu('Options')
61
+ save_plot_settings_action = QtGui.QAction('Save plot settings', self)
62
+ save_plot_settings_action.triggered.connect(self.save_plot_settings)
63
+ if options_menu is not None:
64
+ options_menu.addAction(save_plot_settings_action)
65
+
58
66
  style_menu = menubar.addMenu('Style')
59
67
  style_action = QtGui.QAction('Customize plot style', self)
60
68
  style_action.triggered.connect(self.show_plot_style_dialog)
@@ -216,6 +224,7 @@ class MainWindow(QtWidgets.QMainWindow):
216
224
  self._merged_mode = False # True when multiple files are merged
217
225
  # Tracks loaded files: list of {'name': str, 'series_indices': [int, ...]}
218
226
  self._loaded_files = []
227
+ self._main_opened_file_path = ''
219
228
  # Each series is a callable that accepts x and freq and returns y
220
229
  self.series_list = []
221
230
  # Keep track of series per subplot: a list of lists. Each sublist contains indices into series_list.
@@ -554,8 +563,40 @@ class MainWindow(QtWidgets.QMainWindow):
554
563
  """Fit Y data to axis while preserving the current X range."""
555
564
  for ax in getattr(self, 'current_axes', []):
556
565
  xlim = ax.get_xlim()
557
- ax.relim()
558
- ax.autoscale(enable=True, axis='y', tight=False)
566
+ visible_y = []
567
+ # Consider only points inside the currently visible X window.
568
+ for line in ax.get_lines():
569
+ x_data = np.asarray(line.get_xdata())
570
+ y_data = np.asarray(line.get_ydata())
571
+ if x_data.size == 0 or y_data.size == 0:
572
+ continue
573
+ mask = (x_data >= xlim[0]) & (x_data <= xlim[1])
574
+ if not np.any(mask):
575
+ continue
576
+ y_visible = y_data[mask]
577
+ if y_visible.size == 0:
578
+ continue
579
+ y_visible = y_visible[np.isfinite(y_visible)]
580
+ if y_visible.size:
581
+ visible_y.append(y_visible)
582
+
583
+ if visible_y:
584
+ all_visible_y = np.concatenate(visible_y)
585
+ y_min = float(np.min(all_visible_y))
586
+ y_max = float(np.max(all_visible_y))
587
+ if np.isclose(y_min, y_max):
588
+ pad = 1.0 if y_min == 0.0 else abs(y_min) * 0.05
589
+ y_min -= pad
590
+ y_max += pad
591
+ else:
592
+ pad = (y_max - y_min) * 0.05
593
+ y_min -= pad
594
+ y_max += pad
595
+ ax.set_ylim(y_min, y_max)
596
+ else:
597
+ # Fallback when no visible points are present.
598
+ ax.relim()
599
+ ax.autoscale(enable=True, axis='y', tight=False)
559
600
  ax.set_xlim(xlim)
560
601
  self.canvas.draw_idle()
561
602
 
@@ -734,6 +775,265 @@ class MainWindow(QtWidgets.QMainWindow):
734
775
  paths, _ = QtWidgets.QFileDialog.getOpenFileNames(self, 'Open File', '', file_filter)
735
776
  self.open_files(paths)
736
777
 
778
+ def _to_bool_str(self, value):
779
+ return 'true' if bool(value) else 'false'
780
+
781
+ def _parse_bool(self, text, default=False):
782
+ if text is None:
783
+ return default
784
+ return str(text).strip().lower() in ('1', 'true', 'yes', 'on')
785
+
786
+ def _parse_float(self, text, default):
787
+ try:
788
+ return float(text)
789
+ except (TypeError, ValueError):
790
+ return default
791
+
792
+ def _parse_color_value(self, text):
793
+ if text is None:
794
+ return ''
795
+ raw = str(text).strip()
796
+ if not raw:
797
+ return ''
798
+ # Backward compatibility: old settings may store RGBA tuples as strings.
799
+ if raw.startswith(('(', '[')):
800
+ try:
801
+ parsed = ast.literal_eval(raw)
802
+ if isinstance(parsed, (tuple, list)):
803
+ return tuple(parsed)
804
+ except (ValueError, SyntaxError):
805
+ return ''
806
+ return raw
807
+
808
+ def _get_settings_path_for_data_file(self, data_file_path):
809
+ if not data_file_path:
810
+ return ''
811
+ folder = os.path.dirname(data_file_path)
812
+ base_name = os.path.splitext(os.path.basename(data_file_path))[0]
813
+ return os.path.join(folder, f'{base_name}.xml')
814
+
815
+ def _capture_plot_runtime_state(self):
816
+ """Capture runtime labels and line style properties into persistent state lists."""
817
+ axes = getattr(self, 'current_axes', [])
818
+ for i, ax in enumerate(axes):
819
+ if i < len(self.subplot_axes_labels):
820
+ xlabel = ax.get_xlabel()
821
+ ylabel = ax.get_ylabel()
822
+ if xlabel and xlabel != self._x_col_name and not self._skip_xlabel_capture:
823
+ self.subplot_axes_labels[i]['xlabel'] = xlabel
824
+ if ylabel:
825
+ self.subplot_axes_labels[i]['ylabel_primary'] = ylabel
826
+
827
+ if (i < len(self._plotted_line_artists)
828
+ and i < len(self._plotted_series_snapshot)
829
+ and i < len(self.subplot_series)
830
+ and self._plotted_series_snapshot[i] == self.subplot_series[i]
831
+ and i < len(self.subplot_series_props)):
832
+ line_artists = self._plotted_line_artists[i]
833
+ for j, line in enumerate(line_artists):
834
+ if j < len(self.subplot_series_props[i]):
835
+ label = line.get_label()
836
+ if label and not label.startswith('_'):
837
+ self.subplot_series_props[i][j]['label'] = label
838
+ color = line.get_color()
839
+ if color:
840
+ self.subplot_series_props[i][j]['color'] = color
841
+ self.subplot_series_props[i][j]['linewidth'] = line.get_linewidth()
842
+ self.subplot_series_props[i][j]['linestyle'] = line.get_linestyle()
843
+ marker = line.get_marker()
844
+ self.subplot_series_props[i][j]['marker'] = marker if marker and marker != 'None' else ''
845
+
846
+ def save_plot_settings(self):
847
+ if not self._main_opened_file_path:
848
+ QtWidgets.QMessageBox.information(self, 'Save plot settings', 'Open a data file first.')
849
+ return
850
+ if self._merged_mode:
851
+ QtWidgets.QMessageBox.warning(self, 'Save plot settings', 'Saving settings is only available for a single opened file (non-merged mode).')
852
+ return
853
+
854
+ self._capture_plot_runtime_state()
855
+ settings_path = self._get_settings_path_for_data_file(self._main_opened_file_path)
856
+ if not settings_path:
857
+ QtWidgets.QMessageBox.warning(self, 'Save plot settings', 'Failed to determine settings file path.')
858
+ return
859
+
860
+ try:
861
+ root = ET.Element('toucan_plot_settings', version='1')
862
+
863
+ defaults_el = ET.SubElement(root, 'default_plot_style')
864
+ for key in ('legend_show', 'legend_pos', 'legend_orient', 'legend_fontsize', 'plot_mode', 'grid_show', 'marker'):
865
+ defaults_el.set(key, str(self.default_plot_style.get(key, '')))
866
+
867
+ root.set('x_axis_column', str(self._x_col_name or ''))
868
+
869
+ subplots_el = ET.SubElement(root, 'subplots')
870
+ axes = getattr(self, 'current_axes', [])
871
+ for i, series_indices in enumerate(self.subplot_series):
872
+ subplot_el = ET.SubElement(subplots_el, 'subplot', index=str(i))
873
+
874
+ cfg = self.subplot_config[i] if i < len(self.subplot_config) else self._make_default_subplot_config()
875
+ cfg_el = ET.SubElement(subplot_el, 'config')
876
+ cfg_el.set('legend_show', self._to_bool_str(cfg.get('legend_show', True)))
877
+ cfg_el.set('legend_pos', str(cfg.get('legend_pos', 'best')))
878
+ cfg_el.set('legend_orient', str(cfg.get('legend_orient', 'vertical')))
879
+ cfg_el.set('legend_fontsize', str(cfg.get('legend_fontsize', 12)))
880
+ cfg_el.set('plot_mode', str(cfg.get('plot_mode', 'step')))
881
+ cfg_el.set('grid_show', self._to_bool_str(cfg.get('grid_show', True)))
882
+ cfg_el.set('marker', str(cfg.get('marker', '')))
883
+
884
+ labels = self.subplot_axes_labels[i] if i < len(self.subplot_axes_labels) else {}
885
+ labels_el = ET.SubElement(subplot_el, 'labels')
886
+ labels_el.set('xlabel', str(labels.get('xlabel', '')))
887
+ labels_el.set('ylabel_primary', str(labels.get('ylabel_primary', '')))
888
+ labels_el.set('ylabel_secondary', str(labels.get('ylabel_secondary', '')))
889
+
890
+ if i < len(axes):
891
+ xlim = axes[i].get_xlim()
892
+ ylim = axes[i].get_ylim()
893
+ limits_el = ET.SubElement(subplot_el, 'limits')
894
+ limits_el.set('x_min', repr(float(xlim[0])))
895
+ limits_el.set('x_max', repr(float(xlim[1])))
896
+ limits_el.set('y_min', repr(float(ylim[0])))
897
+ limits_el.set('y_max', repr(float(ylim[1])))
898
+
899
+ series_el = ET.SubElement(subplot_el, 'series')
900
+ props_list = self.subplot_series_props[i] if i < len(self.subplot_series_props) else []
901
+ for j, sidx in enumerate(series_indices):
902
+ if sidx < 0 or sidx >= len(self.series_list):
903
+ continue
904
+ name = self.series_list[sidx][0]
905
+ p = props_list[j] if j < len(props_list) else {}
906
+ item_el = ET.SubElement(series_el, 'item')
907
+ item_el.set('name', str(name))
908
+ item_el.set('label', str(p.get('label', name)))
909
+ item_el.set('color', str(p.get('color', '')))
910
+ item_el.set('linewidth', str(p.get('linewidth', 1.5)))
911
+ item_el.set('linestyle', str(p.get('linestyle', '')))
912
+ item_el.set('marker', str(p.get('marker', '')))
913
+ item_el.set('yaxis', str(p.get('yaxis', 1)))
914
+
915
+ tree = ET.ElementTree(root)
916
+ ET.indent(tree, space=' ')
917
+ tree.write(settings_path, encoding='utf-8', xml_declaration=True)
918
+ QtWidgets.QMessageBox.information(self, 'Save plot settings', f'Settings saved to:\n{settings_path}')
919
+ except Exception as exc:
920
+ QtWidgets.QMessageBox.warning(self, 'Save plot settings', f'Failed to save settings:\n{exc}')
921
+
922
+ def _try_load_plot_settings(self, data_file_path):
923
+ if not data_file_path or self._merged_mode:
924
+ return False
925
+ settings_path = self._get_settings_path_for_data_file(data_file_path)
926
+ if not settings_path or not os.path.exists(settings_path):
927
+ return False
928
+
929
+ try:
930
+ tree = ET.parse(settings_path)
931
+ root = tree.getroot()
932
+ if root.tag != 'toucan_plot_settings':
933
+ return False
934
+
935
+ defaults_el = root.find('default_plot_style')
936
+ if defaults_el is not None:
937
+ self.default_plot_style['legend_show'] = self._parse_bool(defaults_el.get('legend_show'), self.default_plot_style.get('legend_show', True))
938
+ self.default_plot_style['legend_pos'] = defaults_el.get('legend_pos', self.default_plot_style.get('legend_pos', 'best'))
939
+ self.default_plot_style['legend_orient'] = defaults_el.get('legend_orient', self.default_plot_style.get('legend_orient', 'vertical'))
940
+ self.default_plot_style['legend_fontsize'] = int(self._parse_float(defaults_el.get('legend_fontsize'), self.default_plot_style.get('legend_fontsize', 12)))
941
+ self.default_plot_style['plot_mode'] = defaults_el.get('plot_mode', self.default_plot_style.get('plot_mode', 'step'))
942
+ self.default_plot_style['grid_show'] = self._parse_bool(defaults_el.get('grid_show'), self.default_plot_style.get('grid_show', True))
943
+ self.default_plot_style['marker'] = defaults_el.get('marker', self.default_plot_style.get('marker', ''))
944
+
945
+ x_axis_column = root.get('x_axis_column', '')
946
+ if x_axis_column and x_axis_column in self._all_columns and x_axis_column != self._x_col_name:
947
+ self._change_x_axis(x_axis_column)
948
+
949
+ name_to_index = {name: i for i, (name, *_rest) in enumerate(self.series_list)}
950
+
951
+ new_subplot_series = []
952
+ new_subplot_props = []
953
+ new_subplot_axes_labels = []
954
+ new_subplot_config = []
955
+ pending_limits = []
956
+
957
+ subplots_el = root.find('subplots')
958
+ if subplots_el is not None:
959
+ for subplot_el in subplots_el.findall('subplot'):
960
+ cfg = self._make_default_subplot_config()
961
+ cfg_el = subplot_el.find('config')
962
+ if cfg_el is not None:
963
+ cfg['legend_show'] = self._parse_bool(cfg_el.get('legend_show'), cfg.get('legend_show', True))
964
+ cfg['legend_pos'] = cfg_el.get('legend_pos', cfg.get('legend_pos', 'best'))
965
+ cfg['legend_orient'] = cfg_el.get('legend_orient', cfg.get('legend_orient', 'vertical'))
966
+ cfg['legend_fontsize'] = int(self._parse_float(cfg_el.get('legend_fontsize'), cfg.get('legend_fontsize', 12)))
967
+ cfg['plot_mode'] = cfg_el.get('plot_mode', cfg.get('plot_mode', 'step'))
968
+ cfg['grid_show'] = self._parse_bool(cfg_el.get('grid_show'), cfg.get('grid_show', True))
969
+ cfg['marker'] = cfg_el.get('marker', cfg.get('marker', ''))
970
+
971
+ labels = {'xlabel': '', 'ylabel_primary': '', 'ylabel_secondary': ''}
972
+ labels_el = subplot_el.find('labels')
973
+ if labels_el is not None:
974
+ labels['xlabel'] = labels_el.get('xlabel', '')
975
+ labels['ylabel_primary'] = labels_el.get('ylabel_primary', '')
976
+ labels['ylabel_secondary'] = labels_el.get('ylabel_secondary', '')
977
+
978
+ xlim = None
979
+ ylim = None
980
+ limits_el = subplot_el.find('limits')
981
+ if limits_el is not None:
982
+ x_min = self._parse_float(limits_el.get('x_min'), None)
983
+ x_max = self._parse_float(limits_el.get('x_max'), None)
984
+ y_min = self._parse_float(limits_el.get('y_min'), None)
985
+ y_max = self._parse_float(limits_el.get('y_max'), None)
986
+ if x_min is not None and x_max is not None:
987
+ xlim = (x_min, x_max)
988
+ if y_min is not None and y_max is not None:
989
+ ylim = (y_min, y_max)
990
+
991
+ indices = []
992
+ props = []
993
+ series_el = subplot_el.find('series')
994
+ if series_el is not None:
995
+ for item_el in series_el.findall('item'):
996
+ s_name = item_el.get('name', '')
997
+ if s_name not in name_to_index:
998
+ continue
999
+ sidx = name_to_index[s_name]
1000
+ indices.append(sidx)
1001
+ props.append({
1002
+ 'label': item_el.get('label', s_name),
1003
+ 'color': self._parse_color_value(item_el.get('color', '')),
1004
+ 'linewidth': self._parse_float(item_el.get('linewidth'), 1.5),
1005
+ 'linestyle': item_el.get('linestyle', ''),
1006
+ 'marker': item_el.get('marker', ''),
1007
+ 'yaxis': int(self._parse_float(item_el.get('yaxis'), 1)),
1008
+ })
1009
+
1010
+ if indices:
1011
+ new_subplot_series.append(indices)
1012
+ new_subplot_props.append(props)
1013
+ new_subplot_axes_labels.append(labels)
1014
+ new_subplot_config.append(cfg)
1015
+ pending_limits.append((xlim, ylim))
1016
+
1017
+ self.subplot_series = new_subplot_series
1018
+ self.subplot_series_props = new_subplot_props
1019
+ self.subplot_axes_labels = new_subplot_axes_labels
1020
+ self.subplot_config = new_subplot_config
1021
+
1022
+ self.update_plot()
1023
+ axes = getattr(self, 'current_axes', [])
1024
+ for i, limits in enumerate(pending_limits):
1025
+ if i >= len(axes):
1026
+ break
1027
+ xlim, ylim = limits
1028
+ if xlim is not None:
1029
+ axes[i].set_xlim(xlim)
1030
+ if ylim is not None:
1031
+ axes[i].set_ylim(ylim)
1032
+ self.canvas.draw_idle()
1033
+ return True
1034
+ except Exception:
1035
+ return False
1036
+
737
1037
  def merge_file(self):
738
1038
  """Open additional files and append their series to the existing plot session."""
739
1039
  file_filter = "All supported files (*.csv *.smv *.blf *.trc *.asc *.dbc *.mf4 *.mf4z *.feather);;CSV files (*.csv *.smv);;MF4 files (*.mf4 *.mf4z);;Feather files (*.feather);;CAN files (*.blf *.trc *.asc *.dbc);;All files (*)"
@@ -963,6 +1263,7 @@ class MainWindow(QtWidgets.QMainWindow):
963
1263
  self._all_columns = all_columns
964
1264
  self._x_col_name = x_col
965
1265
  self._merged_mode = False
1266
+ self._main_opened_file_path = path
966
1267
 
967
1268
  # Clear existing series and subplots
968
1269
  self.series_list.clear()
@@ -987,7 +1288,9 @@ class MainWindow(QtWidgets.QMainWindow):
987
1288
 
988
1289
  self.update_plot()
989
1290
  self.setWindowTitle(f'TouCAN-Plot — {basename}')
990
- self.show_series_selector()
1291
+ loaded = self._try_load_plot_settings(path)
1292
+ if not loaded:
1293
+ self.show_series_selector()
991
1294
 
992
1295
  def _load_blf(self, blf_path, dbc_paths):
993
1296
  """Load a BLF file using a worker process with live progress."""
@@ -1044,6 +1347,7 @@ class MainWindow(QtWidgets.QMainWindow):
1044
1347
  self._all_columns = all_columns
1045
1348
  self._x_col_name = x_col
1046
1349
  self._merged_mode = False
1350
+ self._main_opened_file_path = blf_path
1047
1351
 
1048
1352
  # Clear existing series and subplots
1049
1353
  self.series_list.clear()
@@ -1068,7 +1372,9 @@ class MainWindow(QtWidgets.QMainWindow):
1068
1372
 
1069
1373
  self.update_plot()
1070
1374
  self.setWindowTitle(f'TouCAN-Plot — {basename}')
1071
- self.show_series_selector()
1375
+ loaded = self._try_load_plot_settings(blf_path)
1376
+ if not loaded:
1377
+ self.show_series_selector()
1072
1378
 
1073
1379
  def _load_mf4(self, path):
1074
1380
  """Load an MF4 file using a worker process with live progress."""
@@ -1125,6 +1431,7 @@ class MainWindow(QtWidgets.QMainWindow):
1125
1431
  self._all_columns = all_columns
1126
1432
  self._x_col_name = x_col
1127
1433
  self._merged_mode = False
1434
+ self._main_opened_file_path = path
1128
1435
 
1129
1436
  # Clear existing series and subplots
1130
1437
  self.series_list.clear()
@@ -1149,7 +1456,9 @@ class MainWindow(QtWidgets.QMainWindow):
1149
1456
 
1150
1457
  self.update_plot()
1151
1458
  self.setWindowTitle(f'TouCAN-Plot — {basename}')
1152
- self.show_series_selector()
1459
+ loaded = self._try_load_plot_settings(path)
1460
+ if not loaded:
1461
+ self.show_series_selector()
1153
1462
 
1154
1463
  def _merge_load_mf4(self, path, prefix):
1155
1464
  """Load an MF4 file and append its series (prefixed) to the existing series_list."""
@@ -1272,6 +1581,7 @@ class MainWindow(QtWidgets.QMainWindow):
1272
1581
  self._all_columns = all_columns
1273
1582
  self._x_col_name = x_col
1274
1583
  self._merged_mode = False
1584
+ self._main_opened_file_path = path
1275
1585
 
1276
1586
  # Clear existing series and subplots
1277
1587
  self.series_list.clear()
@@ -1296,7 +1606,9 @@ class MainWindow(QtWidgets.QMainWindow):
1296
1606
 
1297
1607
  self.update_plot()
1298
1608
  self.setWindowTitle(f'TouCAN-Plot — {basename}')
1299
- self.show_series_selector()
1609
+ loaded = self._try_load_plot_settings(path)
1610
+ if not loaded:
1611
+ self.show_series_selector()
1300
1612
 
1301
1613
  def _merge_load_feather(self, path, prefix):
1302
1614
  """Load a Feather file and append its series (prefixed) to the existing series_list."""