toucan-plot 0.2.7__tar.gz → 0.4.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.2.7/src/toucan_plot.egg-info → toucan_plot-0.4.1}/PKG-INFO +19 -19
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/README.md +1 -1
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/pyproject.toml +10 -3
- toucan_plot-0.4.1/src/toucan_plot/__init__.py +0 -0
- toucan_plot-0.4.1/src/toucan_plot/assets/ico/toucan-plot.png +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/main.py +171 -10
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/utils/__init__.py +2 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/utils/loaders.py +38 -0
- toucan_plot-0.2.7/LICENSE +0 -21
- toucan_plot-0.2.7/PKG-INFO +0 -130
- toucan_plot-0.2.7/setup.cfg +0 -4
- toucan_plot-0.2.7/src/toucan_plot.egg-info/SOURCES.txt +0 -37
- toucan_plot-0.2.7/src/toucan_plot.egg-info/dependency_links.txt +0 -1
- toucan_plot-0.2.7/src/toucan_plot.egg-info/entry_points.txt +0 -2
- toucan_plot-0.2.7/src/toucan_plot.egg-info/requires.txt +0 -8
- toucan_plot-0.2.7/src/toucan_plot.egg-info/top_level.txt +0 -1
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/__main__.py +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/adjustments-dark.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/adjustments-light.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/arrow-autofit-height-dark.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/arrow-autofit-height-light.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/arrow-narrow-left-dark.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/arrow-narrow-left-light.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/arrow-narrow-right-dark.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/arrow-narrow-right-light.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/device-floppy-dark.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/device-floppy-light.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/hand-stop-dark.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/hand-stop-light.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/home-dark.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/home-light.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/ico/toucan-plot.ico +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/layout-grid-add-dark.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/layout-grid-add-light.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/math-function-dark.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/math-function-light.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/ruler-measure-dark.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/ruler-measure-light.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/zoom-dark.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/zoom-light.svg +0 -0
- {toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/utils/styles.py +0 -0
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: toucan-plot
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: A versatile PyQt6 and Matplotlib-based plotting tool for CSV and SMV data files.
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
Requires-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Requires-Dist:
|
|
10
|
-
Requires-Dist:
|
|
11
|
-
Requires-Dist:
|
|
12
|
-
Requires-Dist:
|
|
13
|
-
Requires-Dist:
|
|
14
|
-
Requires-Dist:
|
|
15
|
-
Requires-Dist:
|
|
16
|
-
Requires-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
#
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: toucan-plot
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: A versatile PyQt6 and Matplotlib-based plotting tool for CSV and SMV data files.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Dist: pyqt6>=6.0
|
|
7
|
+
Requires-Dist: matplotlib>=3.0
|
|
8
|
+
Requires-Dist: numpy>=1.20
|
|
9
|
+
Requires-Dist: pyqtdarktheme>=2.1.0
|
|
10
|
+
Requires-Dist: scienceplots>=2.2.1
|
|
11
|
+
Requires-Dist: python-can>=4.0
|
|
12
|
+
Requires-Dist: cantools>=39.0
|
|
13
|
+
Requires-Dist: asammdf>=7.0
|
|
14
|
+
Requires-Dist: pyarrow>=23.0.1
|
|
15
|
+
Requires-Dist: chardet>=7.4.3
|
|
16
|
+
Requires-Python: >=3.13
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# TouCAN-Plot
|
|
20
20
|
|
|
21
21
|
An interactive PyQt6 + Matplotlib plotting tool for CSV, SMV, and CAN bus log files.
|
|
22
22
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "uv_build"
|
|
3
|
+
requires = ["uv-build>=0.11.14,<0.12"]
|
|
4
|
+
|
|
1
5
|
[project]
|
|
2
6
|
name = "toucan-plot"
|
|
3
|
-
version = "0.
|
|
7
|
+
version = "0.4.1"
|
|
8
|
+
requires-python = ">=3.13"
|
|
4
9
|
description = "A versatile PyQt6 and Matplotlib-based plotting tool for CSV and SMV data files."
|
|
5
10
|
license = "MIT"
|
|
6
11
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
7
|
-
requires-python = ">=3.11"
|
|
8
12
|
dependencies = [
|
|
9
13
|
"pyqt6>=6.0",
|
|
10
14
|
"matplotlib>=3.0",
|
|
@@ -14,6 +18,8 @@ dependencies = [
|
|
|
14
18
|
"python-can>=4.0",
|
|
15
19
|
"cantools>=39.0",
|
|
16
20
|
"asammdf>=7.0",
|
|
21
|
+
"pyarrow>=23.0.1",
|
|
22
|
+
"chardet>=7.4.3",
|
|
17
23
|
]
|
|
18
24
|
|
|
19
25
|
[project.scripts]
|
|
@@ -24,7 +30,8 @@ toucan_plot = ["assets/**/*.svg", "assets/**/*.ico"]
|
|
|
24
30
|
|
|
25
31
|
[dependency-groups]
|
|
26
32
|
dev = [
|
|
27
|
-
"nuitka>=4.
|
|
33
|
+
"nuitka[all]>=4.1",
|
|
34
|
+
"uv-build>=0.11.14",
|
|
28
35
|
"zstandard>=0.25.0",
|
|
29
36
|
]
|
|
30
37
|
lint = [
|
|
File without changes
|
|
Binary file
|
|
@@ -9,7 +9,7 @@ from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
|
|
9
9
|
from matplotlib.backends.backend_qt import NavigationToolbar2QT as NavigationToolbar
|
|
10
10
|
import qdarktheme
|
|
11
11
|
|
|
12
|
-
from toucan_plot.utils import styles, csv_load_worker, can_log_load_worker, mf4_load_worker
|
|
12
|
+
from toucan_plot.utils import styles, csv_load_worker, can_log_load_worker, mf4_load_worker, feather_load_worker
|
|
13
13
|
|
|
14
14
|
LEGEND_POSITIONS = [
|
|
15
15
|
'best', 'upper right', 'upper left', 'lower left', 'lower right',
|
|
@@ -21,7 +21,7 @@ LEGEND_POSITIONS = [
|
|
|
21
21
|
class MainWindow(QtWidgets.QMainWindow):
|
|
22
22
|
def __init__(self):
|
|
23
23
|
super().__init__()
|
|
24
|
-
self.setWindowTitle('
|
|
24
|
+
self.setWindowTitle('TouCAN-Plot')
|
|
25
25
|
self.setWindowIcon(QtGui.QIcon(os.path.join(os.path.dirname(__file__), 'assets', 'ico', 'toucan-plot.ico')))
|
|
26
26
|
self.resize(900, 600)
|
|
27
27
|
|
|
@@ -687,6 +687,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
|
687
687
|
dbc_files = [p for p in paths if os.path.splitext(p)[1].lower() == '.dbc']
|
|
688
688
|
csv_files = [p for p in paths if os.path.splitext(p)[1].lower() in ('.csv', '.smv')]
|
|
689
689
|
mf4_files = [p for p in paths if os.path.splitext(p)[1].lower() in ('.mf4', '.mf4z')]
|
|
690
|
+
feather_files = [p for p in paths if os.path.splitext(p)[1].lower() == '.feather']
|
|
690
691
|
|
|
691
692
|
first_loaded = False
|
|
692
693
|
|
|
@@ -715,19 +716,27 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
|
715
716
|
self._merged_mode = True
|
|
716
717
|
self._merge_load_mf4(path, os.path.basename(path))
|
|
717
718
|
|
|
719
|
+
for path in feather_files:
|
|
720
|
+
if not first_loaded:
|
|
721
|
+
self._load_feather(path)
|
|
722
|
+
first_loaded = True
|
|
723
|
+
else:
|
|
724
|
+
self._merged_mode = True
|
|
725
|
+
self._merge_load_feather(path, os.path.basename(path))
|
|
726
|
+
|
|
718
727
|
if self._merged_mode and self.series_list:
|
|
719
728
|
file_names = [f['name'] for f in self._loaded_files]
|
|
720
729
|
self.update_plot()
|
|
721
|
-
self.setWindowTitle(f'
|
|
730
|
+
self.setWindowTitle(f'TouCAN-Plot — Merged: {", ".join(file_names)}')
|
|
722
731
|
|
|
723
732
|
def open_file(self):
|
|
724
|
-
file_filter = "All supported files (*.csv *.smv *.blf *.trc *.asc *.dbc *.mf4 *.mf4z);;CSV files (*.csv
|
|
733
|
+
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 (*)"
|
|
725
734
|
paths, _ = QtWidgets.QFileDialog.getOpenFileNames(self, 'Open File', '', file_filter)
|
|
726
735
|
self.open_files(paths)
|
|
727
736
|
|
|
728
737
|
def merge_file(self):
|
|
729
738
|
"""Open additional files and append their series to the existing plot session."""
|
|
730
|
-
file_filter = "All supported files (*.csv *.smv *.blf *.trc *.asc *.dbc *.mf4 *.mf4z);;CSV files (*.csv
|
|
739
|
+
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 (*)"
|
|
731
740
|
paths, _ = QtWidgets.QFileDialog.getOpenFileNames(self, 'Merge Files', '', file_filter)
|
|
732
741
|
if not paths:
|
|
733
742
|
return
|
|
@@ -738,6 +747,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
|
738
747
|
dbc_files = [p for p in paths if os.path.splitext(p)[1].lower() == '.dbc']
|
|
739
748
|
csv_files = [p for p in paths if os.path.splitext(p)[1].lower() in ('.csv', '.smv')]
|
|
740
749
|
mf4_files = [p for p in paths if os.path.splitext(p)[1].lower() in ('.mf4', '.mf4z')]
|
|
750
|
+
feather_files = [p for p in paths if os.path.splitext(p)[1].lower() == '.feather']
|
|
741
751
|
|
|
742
752
|
for path in csv_files:
|
|
743
753
|
basename = os.path.basename(path)
|
|
@@ -752,10 +762,14 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
|
752
762
|
basename = os.path.basename(path)
|
|
753
763
|
self._merge_load_mf4(path, basename)
|
|
754
764
|
|
|
765
|
+
for path in feather_files:
|
|
766
|
+
basename = os.path.basename(path)
|
|
767
|
+
self._merge_load_feather(path, basename)
|
|
768
|
+
|
|
755
769
|
if self.series_list:
|
|
756
770
|
file_names = [f['name'] for f in self._loaded_files]
|
|
757
771
|
self.update_plot()
|
|
758
|
-
self.setWindowTitle(f'
|
|
772
|
+
self.setWindowTitle(f'TouCAN-Plot — Merged: {", ".join(file_names)}')
|
|
759
773
|
self.show_series_selector()
|
|
760
774
|
|
|
761
775
|
def _merge_load_csv(self, path, prefix):
|
|
@@ -972,7 +986,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
|
972
986
|
self._merge_action.setEnabled(True)
|
|
973
987
|
|
|
974
988
|
self.update_plot()
|
|
975
|
-
self.setWindowTitle(f'
|
|
989
|
+
self.setWindowTitle(f'TouCAN-Plot — {basename}')
|
|
976
990
|
self.show_series_selector()
|
|
977
991
|
|
|
978
992
|
def _load_blf(self, blf_path, dbc_paths):
|
|
@@ -1053,7 +1067,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
|
1053
1067
|
self._merge_action.setEnabled(True)
|
|
1054
1068
|
|
|
1055
1069
|
self.update_plot()
|
|
1056
|
-
self.setWindowTitle(f'
|
|
1070
|
+
self.setWindowTitle(f'TouCAN-Plot — {basename}')
|
|
1057
1071
|
self.show_series_selector()
|
|
1058
1072
|
|
|
1059
1073
|
def _load_mf4(self, path):
|
|
@@ -1134,7 +1148,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
|
1134
1148
|
self._merge_action.setEnabled(True)
|
|
1135
1149
|
|
|
1136
1150
|
self.update_plot()
|
|
1137
|
-
self.setWindowTitle(f'
|
|
1151
|
+
self.setWindowTitle(f'TouCAN-Plot — {basename}')
|
|
1138
1152
|
self.show_series_selector()
|
|
1139
1153
|
|
|
1140
1154
|
def _merge_load_mf4(self, path, prefix):
|
|
@@ -1203,6 +1217,153 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
|
1203
1217
|
file_entry['series_indices'].append(idx)
|
|
1204
1218
|
self._loaded_files.append(file_entry)
|
|
1205
1219
|
|
|
1220
|
+
def _load_feather(self, path):
|
|
1221
|
+
"""Load a Feather file using a worker process with live progress."""
|
|
1222
|
+
queue = multiprocessing.Queue()
|
|
1223
|
+
proc = multiprocessing.Process(target=feather_load_worker, args=(path, queue))
|
|
1224
|
+
|
|
1225
|
+
progress = QtWidgets.QProgressDialog(f'Loading {os.path.basename(path)}...', None, 0, 100, self)
|
|
1226
|
+
progress.setWindowTitle('Loading')
|
|
1227
|
+
progress.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
|
1228
|
+
progress.setMinimumDuration(0)
|
|
1229
|
+
progress.setValue(0)
|
|
1230
|
+
|
|
1231
|
+
result_data = {}
|
|
1232
|
+
|
|
1233
|
+
def poll():
|
|
1234
|
+
try:
|
|
1235
|
+
while not queue.empty():
|
|
1236
|
+
msg = queue.get_nowait()
|
|
1237
|
+
if msg[0] == 'progress':
|
|
1238
|
+
pct = msg[1]
|
|
1239
|
+
label = msg[2] if len(msg) > 2 else ''
|
|
1240
|
+
if pct >= 0:
|
|
1241
|
+
progress.setValue(pct)
|
|
1242
|
+
if label:
|
|
1243
|
+
progress.setLabelText(label)
|
|
1244
|
+
elif msg[0] == 'result':
|
|
1245
|
+
result_data['data'] = msg[1]
|
|
1246
|
+
progress.setValue(100)
|
|
1247
|
+
progress.close()
|
|
1248
|
+
elif msg[0] == 'error':
|
|
1249
|
+
result_data['error'] = msg[1]
|
|
1250
|
+
progress.close()
|
|
1251
|
+
except Exception:
|
|
1252
|
+
pass
|
|
1253
|
+
|
|
1254
|
+
timer = QtCore.QTimer(progress)
|
|
1255
|
+
timer.timeout.connect(poll)
|
|
1256
|
+
timer.start(50)
|
|
1257
|
+
proc.start()
|
|
1258
|
+
progress.exec()
|
|
1259
|
+
timer.stop()
|
|
1260
|
+
proc.join(timeout=10)
|
|
1261
|
+
|
|
1262
|
+
if 'error' in result_data:
|
|
1263
|
+
QtWidgets.QMessageBox.warning(self, 'Error', result_data['error'])
|
|
1264
|
+
return
|
|
1265
|
+
if 'data' not in result_data:
|
|
1266
|
+
return
|
|
1267
|
+
|
|
1268
|
+
x_col, all_columns = result_data['data']
|
|
1269
|
+
x_array = all_columns[x_col]
|
|
1270
|
+
|
|
1271
|
+
self._x = x_array
|
|
1272
|
+
self._all_columns = all_columns
|
|
1273
|
+
self._x_col_name = x_col
|
|
1274
|
+
self._merged_mode = False
|
|
1275
|
+
|
|
1276
|
+
# Clear existing series and subplots
|
|
1277
|
+
self.series_list.clear()
|
|
1278
|
+
self.subplot_series.clear()
|
|
1279
|
+
self.subplot_series_props.clear()
|
|
1280
|
+
self.subplot_axes_labels.clear()
|
|
1281
|
+
self.subplot_config.clear()
|
|
1282
|
+
self._loaded_files.clear()
|
|
1283
|
+
|
|
1284
|
+
# Build series_list from all_columns
|
|
1285
|
+
basename = os.path.basename(path)
|
|
1286
|
+
file_entry = {'name': basename, 'series_indices': []}
|
|
1287
|
+
for name in all_columns:
|
|
1288
|
+
if name == x_col:
|
|
1289
|
+
continue
|
|
1290
|
+
data = all_columns[name]
|
|
1291
|
+
idx = len(self.series_list)
|
|
1292
|
+
self.series_list.append((name, lambda x, f, _d=data: _d, x_array))
|
|
1293
|
+
file_entry['series_indices'].append(idx)
|
|
1294
|
+
self._loaded_files.append(file_entry)
|
|
1295
|
+
self._merge_action.setEnabled(True)
|
|
1296
|
+
|
|
1297
|
+
self.update_plot()
|
|
1298
|
+
self.setWindowTitle(f'TouCAN-Plot — {basename}')
|
|
1299
|
+
self.show_series_selector()
|
|
1300
|
+
|
|
1301
|
+
def _merge_load_feather(self, path, prefix):
|
|
1302
|
+
"""Load a Feather file and append its series (prefixed) to the existing series_list."""
|
|
1303
|
+
queue = multiprocessing.Queue()
|
|
1304
|
+
proc = multiprocessing.Process(target=feather_load_worker, args=(path, queue))
|
|
1305
|
+
|
|
1306
|
+
progress = QtWidgets.QProgressDialog(f'Loading {os.path.basename(path)}...', None, 0, 100, self)
|
|
1307
|
+
progress.setWindowTitle('Loading')
|
|
1308
|
+
progress.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
|
1309
|
+
progress.setMinimumDuration(0)
|
|
1310
|
+
progress.setValue(0)
|
|
1311
|
+
|
|
1312
|
+
result_data = {}
|
|
1313
|
+
|
|
1314
|
+
def poll():
|
|
1315
|
+
try:
|
|
1316
|
+
while not queue.empty():
|
|
1317
|
+
msg = queue.get_nowait()
|
|
1318
|
+
if msg[0] == 'progress':
|
|
1319
|
+
pct = msg[1]
|
|
1320
|
+
label = msg[2] if len(msg) > 2 else ''
|
|
1321
|
+
if pct >= 0:
|
|
1322
|
+
progress.setValue(pct)
|
|
1323
|
+
if label:
|
|
1324
|
+
progress.setLabelText(label)
|
|
1325
|
+
elif msg[0] == 'result':
|
|
1326
|
+
result_data['data'] = msg[1]
|
|
1327
|
+
progress.setValue(100)
|
|
1328
|
+
progress.close()
|
|
1329
|
+
elif msg[0] == 'error':
|
|
1330
|
+
result_data['error'] = msg[1]
|
|
1331
|
+
progress.close()
|
|
1332
|
+
except Exception:
|
|
1333
|
+
pass
|
|
1334
|
+
|
|
1335
|
+
timer = QtCore.QTimer(progress)
|
|
1336
|
+
timer.timeout.connect(poll)
|
|
1337
|
+
timer.start(50)
|
|
1338
|
+
proc.start()
|
|
1339
|
+
progress.exec()
|
|
1340
|
+
timer.stop()
|
|
1341
|
+
proc.join(timeout=10)
|
|
1342
|
+
|
|
1343
|
+
if 'error' in result_data:
|
|
1344
|
+
QtWidgets.QMessageBox.warning(self, 'Error', result_data['error'])
|
|
1345
|
+
return
|
|
1346
|
+
if 'data' not in result_data:
|
|
1347
|
+
return
|
|
1348
|
+
|
|
1349
|
+
x_col, all_columns = result_data['data']
|
|
1350
|
+
x_array = all_columns[x_col]
|
|
1351
|
+
|
|
1352
|
+
if len(self.series_list) == 0:
|
|
1353
|
+
self._x = x_array
|
|
1354
|
+
self._x_col_name = x_col
|
|
1355
|
+
|
|
1356
|
+
file_entry = {'name': prefix, 'series_indices': []}
|
|
1357
|
+
for name in all_columns:
|
|
1358
|
+
if name == x_col:
|
|
1359
|
+
continue
|
|
1360
|
+
data = all_columns[name]
|
|
1361
|
+
prefixed_name = f'{prefix}: {name}'
|
|
1362
|
+
idx = len(self.series_list)
|
|
1363
|
+
self.series_list.append((prefixed_name, lambda x, f, _d=data: _d, x_array))
|
|
1364
|
+
file_entry['series_indices'].append(idx)
|
|
1365
|
+
self._loaded_files.append(file_entry)
|
|
1366
|
+
|
|
1206
1367
|
# ---- Measure cursors ----
|
|
1207
1368
|
|
|
1208
1369
|
def _toggle_measure(self, checked: bool):
|
|
@@ -2016,7 +2177,7 @@ def main():
|
|
|
2016
2177
|
import argparse
|
|
2017
2178
|
parser = argparse.ArgumentParser(
|
|
2018
2179
|
prog='toucan-plot',
|
|
2019
|
-
description='
|
|
2180
|
+
description='TouCAN-Plot — Interactive plotting tool for CSV, SMV, and CAN log (BLF/TRC/ASC) files.',
|
|
2020
2181
|
epilog='BLF, TRC, and ASC files require at least one DBC file for signal decoding.\n'
|
|
2021
2182
|
'Example: toucan-plot log.blf signals.dbc',
|
|
2022
2183
|
)
|
|
@@ -4,6 +4,7 @@ from .loaders import (
|
|
|
4
4
|
csv_load_worker,
|
|
5
5
|
can_log_load_worker,
|
|
6
6
|
mf4_load_worker,
|
|
7
|
+
feather_load_worker,
|
|
7
8
|
)
|
|
8
9
|
|
|
9
10
|
__all__ = [
|
|
@@ -11,4 +12,5 @@ __all__ = [
|
|
|
11
12
|
"csv_load_worker",
|
|
12
13
|
"can_log_load_worker",
|
|
13
14
|
"mf4_load_worker",
|
|
15
|
+
"feather_load_worker",
|
|
14
16
|
]
|
|
@@ -217,3 +217,41 @@ def mf4_load_worker(path, queue):
|
|
|
217
217
|
queue.put(('result', (x_col, all_columns)))
|
|
218
218
|
except Exception as e:
|
|
219
219
|
queue.put(('error', str(e)))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def feather_load_worker(path, queue):
|
|
223
|
+
"""Multiprocessing worker for Feather loading via pandas. Sends progress via queue."""
|
|
224
|
+
try:
|
|
225
|
+
import pandas as pd
|
|
226
|
+
|
|
227
|
+
queue.put(('progress', 5, 'Reading Feather file...'))
|
|
228
|
+
df = pd.read_feather(path)
|
|
229
|
+
|
|
230
|
+
if df.empty:
|
|
231
|
+
queue.put(('error', 'Feather file is empty or has no data rows.'))
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
queue.put(('progress', 30, 'Building columns...'))
|
|
235
|
+
all_columns = {}
|
|
236
|
+
num_cols = len(df.columns)
|
|
237
|
+
|
|
238
|
+
x_col = None
|
|
239
|
+
for candidate in ('Time', 'timestamp', 'time'):
|
|
240
|
+
if candidate in df.columns:
|
|
241
|
+
x_col = candidate
|
|
242
|
+
break
|
|
243
|
+
if x_col is None:
|
|
244
|
+
x_col = df.columns[0]
|
|
245
|
+
|
|
246
|
+
for col_idx, col_name in enumerate(df.columns):
|
|
247
|
+
try:
|
|
248
|
+
all_columns[col_name] = df[col_name].to_numpy(dtype=float, na_value=np.nan)
|
|
249
|
+
except (ValueError, TypeError):
|
|
250
|
+
continue
|
|
251
|
+
pct = 30 + int(65 * (col_idx + 1) / num_cols)
|
|
252
|
+
queue.put(('progress', pct, f'Processing column {col_idx + 1}/{num_cols}...'))
|
|
253
|
+
|
|
254
|
+
queue.put(('progress', 98, 'Finalizing...'))
|
|
255
|
+
queue.put(('result', (x_col, all_columns)))
|
|
256
|
+
except Exception as e:
|
|
257
|
+
queue.put(('error', str(e)))
|
toucan_plot-0.2.7/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Claudio Emanoel Barbosa Lima
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
toucan_plot-0.2.7/PKG-INFO
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: toucan-plot
|
|
3
|
-
Version: 0.2.7
|
|
4
|
-
Summary: A versatile PyQt6 and Matplotlib-based plotting tool for CSV and SMV data files.
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
Requires-Python: >=3.11
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
License-File: LICENSE
|
|
9
|
-
Requires-Dist: pyqt6>=6.0
|
|
10
|
-
Requires-Dist: matplotlib>=3.0
|
|
11
|
-
Requires-Dist: numpy>=1.20
|
|
12
|
-
Requires-Dist: pyqtdarktheme>=2.1.0
|
|
13
|
-
Requires-Dist: scienceplots>=2.2.1
|
|
14
|
-
Requires-Dist: python-can>=4.0
|
|
15
|
-
Requires-Dist: cantools>=39.0
|
|
16
|
-
Requires-Dist: asammdf>=7.0
|
|
17
|
-
Dynamic: license-file
|
|
18
|
-
|
|
19
|
-
# Toucan-Plot
|
|
20
|
-
|
|
21
|
-
An interactive PyQt6 + Matplotlib plotting tool for CSV, SMV, and CAN bus log files.
|
|
22
|
-
|
|
23
|
-

|
|
24
|
-

|
|
25
|
-
|
|
26
|
-
## Features
|
|
27
|
-
|
|
28
|
-
### Supported File Formats
|
|
29
|
-
|
|
30
|
-
| Format | Description |
|
|
31
|
-
|------------------|--------------------------------------------------------------|
|
|
32
|
-
| `.csv` / `.smv` | Comma or semicolon delimited data (auto-detected) |
|
|
33
|
-
| `.mf4` / `.mf4z` | ASAM MDF v4 measurement data (via asammdf) |
|
|
34
|
-
| `.blf` | CAN bus binary log |
|
|
35
|
-
| `.trc` | CAN bus trace log |
|
|
36
|
-
| `.asc` | CAN bus ASCII log |
|
|
37
|
-
| `.dbc` | CAN database for signal decoding (required with BLF/TRC/ASC) |
|
|
38
|
-
|
|
39
|
-
- Auto-detects `Time` / `timestamp` columns as the default X axis
|
|
40
|
-
- CAN signals are forward-fill interpolated and named as `Message.Signal`
|
|
41
|
-
|
|
42
|
-
### Plot Interaction
|
|
43
|
-
|
|
44
|
-
- **Shared X axis** across all subplots with linked panning and zooming
|
|
45
|
-
- **Secondary Y axis** support per series
|
|
46
|
-
- **Double-click** a subplot to edit its series, or **right-click** for a context menu
|
|
47
|
-
- **Measure cursors** — two draggable vertical cursors with a live delta table
|
|
48
|
-
- **Expression evaluator** — create computed series from math expressions (e.g. `sin("series_A") + "series_B" * 2`)
|
|
49
|
-
- **Multi-file merge** — overlay series from different files, each in its own tab
|
|
50
|
-
- **X axis switching** — reassign the X column without losing subplot configuration
|
|
51
|
-
- **Per-series properties** (label, color, linewidth, linestyle, marker) persisted across all redraws
|
|
52
|
-
- Live mouse coordinates in the status bar
|
|
53
|
-
|
|
54
|
-
### Toolbar
|
|
55
|
-
|
|
56
|
-
| Button | Action |
|
|
57
|
-
|----------------|-----------------------------------------|
|
|
58
|
-
| Home | Auto-fit all axes |
|
|
59
|
-
| Back / Forward | Navigate view history |
|
|
60
|
-
| Pan | Pan mode |
|
|
61
|
-
| Zoom | Zoom rectangle mode |
|
|
62
|
-
| Customize | Matplotlib per-curve property editor |
|
|
63
|
-
| Save | Export figure to file |
|
|
64
|
-
| Add subplot | Open the series selector |
|
|
65
|
-
| Fit Y | Auto-fit Y axis keeping current X range |
|
|
66
|
-
| Measure | Toggle measure cursors |
|
|
67
|
-
| X axis | Select X axis column |
|
|
68
|
-
|
|
69
|
-
### Menus
|
|
70
|
-
|
|
71
|
-
- **File → Open** (`Ctrl+O`) — open one or more files (multi-select)
|
|
72
|
-
- **File → Merge** (`Ctrl+M`) — append files to the current session
|
|
73
|
-
- **Style → Customize plot style** — preset, line mode, marker, text size, grid, legend
|
|
74
|
-
- **Style → Theme** — Dark / Light (icons swap automatically)
|
|
75
|
-
|
|
76
|
-
### Right-Click Context Menu
|
|
77
|
-
|
|
78
|
-
- Add subplot above / below
|
|
79
|
-
- Delete subplot
|
|
80
|
-
- Legend: show/hide, position (14 options), orientation, text size
|
|
81
|
-
- Line style: plot / step
|
|
82
|
-
|
|
83
|
-
### Style Presets
|
|
84
|
-
|
|
85
|
-
Four built-in presets: **Default**, **Style 1**, **IEEE**, **Other**.
|
|
86
|
-
|
|
87
|
-
## Installation
|
|
88
|
-
|
|
89
|
-
### From PyPI (recommended)
|
|
90
|
-
|
|
91
|
-
Install the released package from PyPI:
|
|
92
|
-
|
|
93
|
-
```shell
|
|
94
|
-
pip install toucan-plot
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## Usage
|
|
98
|
-
|
|
99
|
-
```shell
|
|
100
|
-
# Open a CSV file
|
|
101
|
-
toucan-plot data.csv
|
|
102
|
-
|
|
103
|
-
# Open an MF4 measurement file
|
|
104
|
-
toucan-plot recording.mf4
|
|
105
|
-
|
|
106
|
-
# Open a CAN log with DBC decoding
|
|
107
|
-
toucan-plot recording.blf signals.dbc
|
|
108
|
-
|
|
109
|
-
# Open multiple files (merge mode)
|
|
110
|
-
toucan-plot file1.csv file2.csv
|
|
111
|
-
|
|
112
|
-
# Launch without files
|
|
113
|
-
toucan-plot
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## Requirements
|
|
117
|
-
|
|
118
|
-
- Python ≥ 3.11
|
|
119
|
-
- PyQt6
|
|
120
|
-
- Matplotlib
|
|
121
|
-
- NumPy
|
|
122
|
-
- pyqtdarktheme
|
|
123
|
-
- SciencePlots
|
|
124
|
-
- python-can (for CAN log files)
|
|
125
|
-
- cantools (for DBC decoding)
|
|
126
|
-
- asammdf (for MF4 files)
|
|
127
|
-
|
|
128
|
-
## License
|
|
129
|
-
|
|
130
|
-
MIT
|
toucan_plot-0.2.7/setup.cfg
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
LICENSE
|
|
2
|
-
README.md
|
|
3
|
-
pyproject.toml
|
|
4
|
-
src/toucan_plot/__main__.py
|
|
5
|
-
src/toucan_plot/main.py
|
|
6
|
-
src/toucan_plot.egg-info/PKG-INFO
|
|
7
|
-
src/toucan_plot.egg-info/SOURCES.txt
|
|
8
|
-
src/toucan_plot.egg-info/dependency_links.txt
|
|
9
|
-
src/toucan_plot.egg-info/entry_points.txt
|
|
10
|
-
src/toucan_plot.egg-info/requires.txt
|
|
11
|
-
src/toucan_plot.egg-info/top_level.txt
|
|
12
|
-
src/toucan_plot/assets/adjustments-dark.svg
|
|
13
|
-
src/toucan_plot/assets/adjustments-light.svg
|
|
14
|
-
src/toucan_plot/assets/arrow-autofit-height-dark.svg
|
|
15
|
-
src/toucan_plot/assets/arrow-autofit-height-light.svg
|
|
16
|
-
src/toucan_plot/assets/arrow-narrow-left-dark.svg
|
|
17
|
-
src/toucan_plot/assets/arrow-narrow-left-light.svg
|
|
18
|
-
src/toucan_plot/assets/arrow-narrow-right-dark.svg
|
|
19
|
-
src/toucan_plot/assets/arrow-narrow-right-light.svg
|
|
20
|
-
src/toucan_plot/assets/device-floppy-dark.svg
|
|
21
|
-
src/toucan_plot/assets/device-floppy-light.svg
|
|
22
|
-
src/toucan_plot/assets/hand-stop-dark.svg
|
|
23
|
-
src/toucan_plot/assets/hand-stop-light.svg
|
|
24
|
-
src/toucan_plot/assets/home-dark.svg
|
|
25
|
-
src/toucan_plot/assets/home-light.svg
|
|
26
|
-
src/toucan_plot/assets/layout-grid-add-dark.svg
|
|
27
|
-
src/toucan_plot/assets/layout-grid-add-light.svg
|
|
28
|
-
src/toucan_plot/assets/math-function-dark.svg
|
|
29
|
-
src/toucan_plot/assets/math-function-light.svg
|
|
30
|
-
src/toucan_plot/assets/ruler-measure-dark.svg
|
|
31
|
-
src/toucan_plot/assets/ruler-measure-light.svg
|
|
32
|
-
src/toucan_plot/assets/zoom-dark.svg
|
|
33
|
-
src/toucan_plot/assets/zoom-light.svg
|
|
34
|
-
src/toucan_plot/assets/ico/toucan-plot.ico
|
|
35
|
-
src/toucan_plot/utils/__init__.py
|
|
36
|
-
src/toucan_plot/utils/loaders.py
|
|
37
|
-
src/toucan_plot/utils/styles.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
toucan_plot
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{toucan_plot-0.2.7 → toucan_plot-0.4.1}/src/toucan_plot/assets/arrow-autofit-height-dark.svg
RENAMED
|
File without changes
|
{toucan_plot-0.2.7 → toucan_plot-0.4.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
|