toucan-plot 0.4.1__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.
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/PKG-INFO +7 -7
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/README.md +4 -4
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/pyproject.toml +3 -3
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/main.py +319 -7
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/__init__.py +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/__main__.py +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/adjustments-dark.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/adjustments-light.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/arrow-autofit-height-dark.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/arrow-autofit-height-light.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/arrow-narrow-left-dark.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/arrow-narrow-left-light.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/arrow-narrow-right-dark.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/arrow-narrow-right-light.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/device-floppy-dark.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/device-floppy-light.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/hand-stop-dark.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/hand-stop-light.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/home-dark.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/home-light.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/ico/toucan-plot.ico +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/ico/toucan-plot.png +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/layout-grid-add-dark.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/layout-grid-add-light.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/math-function-dark.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/math-function-light.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/ruler-measure-dark.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/ruler-measure-light.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/zoom-dark.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/zoom-light.svg +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/utils/__init__.py +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/utils/loaders.py +0 -0
- {toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/utils/styles.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: toucan-plot
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: A versatile
|
|
3
|
+
Version: 0.5.1
|
|
4
|
+
Summary: A versatile PySide6 and Matplotlib-based plotting tool for CSV and SMV data files.
|
|
5
5
|
License-Expression: MIT
|
|
6
|
-
Requires-Dist:
|
|
6
|
+
Requires-Dist: pyside6>=6.0
|
|
7
7
|
Requires-Dist: matplotlib>=3.0
|
|
8
8
|
Requires-Dist: numpy>=1.20
|
|
9
9
|
Requires-Dist: pyqtdarktheme>=2.1.0
|
|
@@ -18,9 +18,9 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
|
|
19
19
|
# TouCAN-Plot
|
|
20
20
|
|
|
21
|
-
An interactive
|
|
21
|
+
An interactive PySide6 + Matplotlib plotting tool for CSV, SMV, and CAN bus log files.
|
|
22
22
|
|
|
23
|
-

|
|
24
24
|

|
|
25
25
|
|
|
26
26
|
## Features
|
|
@@ -115,8 +115,8 @@ toucan-plot
|
|
|
115
115
|
|
|
116
116
|
## Requirements
|
|
117
117
|
|
|
118
|
-
- Python ≥ 3.
|
|
119
|
-
-
|
|
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
|
|
3
|
+
An interactive PySide6 + Matplotlib plotting tool for CSV, SMV, and CAN bus log files.
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|

|
|
7
7
|
|
|
8
8
|
## Features
|
|
@@ -97,8 +97,8 @@ toucan-plot
|
|
|
97
97
|
|
|
98
98
|
## Requirements
|
|
99
99
|
|
|
100
|
-
- Python ≥ 3.
|
|
101
|
-
-
|
|
100
|
+
- Python ≥ 3.13
|
|
101
|
+
- PySide6
|
|
102
102
|
- Matplotlib
|
|
103
103
|
- NumPy
|
|
104
104
|
- pyqtdarktheme
|
|
@@ -4,13 +4,13 @@ requires = ["uv-build>=0.11.14,<0.12"]
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "toucan-plot"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.1"
|
|
8
8
|
requires-python = ">=3.13"
|
|
9
|
-
description = "A versatile
|
|
9
|
+
description = "A versatile PySide6 and Matplotlib-based plotting tool for CSV and SMV data files."
|
|
10
10
|
license = "MIT"
|
|
11
11
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
12
12
|
dependencies = [
|
|
13
|
-
"
|
|
13
|
+
"pyside6>=6.0",
|
|
14
14
|
"matplotlib>=3.0",
|
|
15
15
|
"numpy>=1.20",
|
|
16
16
|
"pyqtdarktheme>=2.1.0",
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
3
|
import multiprocessing
|
|
4
|
+
import ast
|
|
4
5
|
import numpy as np
|
|
5
|
-
|
|
6
|
+
import xml.etree.ElementTree as ET
|
|
7
|
+
from PySide6 import QtWidgets, QtCore, QtGui
|
|
6
8
|
import matplotlib as mpl
|
|
7
9
|
from matplotlib.figure import Figure
|
|
8
10
|
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
|
@@ -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
|
-
|
|
558
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/arrow-autofit-height-dark.svg
RENAMED
|
File without changes
|
{toucan_plot-0.4.1 → toucan_plot-0.5.1}/src/toucan_plot/assets/arrow-autofit-height-light.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|