toucan-plot 0.2.7__tar.gz → 0.3.0__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 (39) hide show
  1. {toucan_plot-0.2.7/src/toucan_plot.egg-info → toucan_plot-0.3.0}/PKG-INFO +2 -1
  2. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/pyproject.toml +2 -1
  3. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/main.py +164 -3
  4. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/utils/__init__.py +2 -0
  5. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/utils/loaders.py +38 -0
  6. {toucan_plot-0.2.7 → toucan_plot-0.3.0/src/toucan_plot.egg-info}/PKG-INFO +2 -1
  7. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot.egg-info/requires.txt +1 -0
  8. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/LICENSE +0 -0
  9. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/README.md +0 -0
  10. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/setup.cfg +0 -0
  11. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/__main__.py +0 -0
  12. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/adjustments-dark.svg +0 -0
  13. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/adjustments-light.svg +0 -0
  14. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/arrow-autofit-height-dark.svg +0 -0
  15. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/arrow-autofit-height-light.svg +0 -0
  16. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/arrow-narrow-left-dark.svg +0 -0
  17. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/arrow-narrow-left-light.svg +0 -0
  18. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/arrow-narrow-right-dark.svg +0 -0
  19. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/arrow-narrow-right-light.svg +0 -0
  20. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/device-floppy-dark.svg +0 -0
  21. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/device-floppy-light.svg +0 -0
  22. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/hand-stop-dark.svg +0 -0
  23. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/hand-stop-light.svg +0 -0
  24. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/home-dark.svg +0 -0
  25. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/home-light.svg +0 -0
  26. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/ico/toucan-plot.ico +0 -0
  27. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/layout-grid-add-dark.svg +0 -0
  28. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/layout-grid-add-light.svg +0 -0
  29. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/math-function-dark.svg +0 -0
  30. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/math-function-light.svg +0 -0
  31. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/ruler-measure-dark.svg +0 -0
  32. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/ruler-measure-light.svg +0 -0
  33. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/zoom-dark.svg +0 -0
  34. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/assets/zoom-light.svg +0 -0
  35. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot/utils/styles.py +0 -0
  36. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot.egg-info/SOURCES.txt +0 -0
  37. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot.egg-info/dependency_links.txt +0 -0
  38. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot.egg-info/entry_points.txt +0 -0
  39. {toucan_plot-0.2.7 → toucan_plot-0.3.0}/src/toucan_plot.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: toucan-plot
3
- Version: 0.2.7
3
+ Version: 0.3.0
4
4
  Summary: A versatile PyQt6 and Matplotlib-based plotting tool for CSV and SMV data files.
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -14,6 +14,7 @@ Requires-Dist: scienceplots>=2.2.1
14
14
  Requires-Dist: python-can>=4.0
15
15
  Requires-Dist: cantools>=39.0
16
16
  Requires-Dist: asammdf>=7.0
17
+ Requires-Dist: pyarrow>=23.0.1
17
18
  Dynamic: license-file
18
19
 
19
20
  # Toucan-Plot
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "toucan-plot"
3
- version = "0.2.7"
3
+ version = "0.3.0"
4
4
  description = "A versatile PyQt6 and Matplotlib-based plotting tool for CSV and SMV data files."
5
5
  license = "MIT"
6
6
  readme = { file = "README.md", content-type = "text/markdown" }
@@ -14,6 +14,7 @@ dependencies = [
14
14
  "python-can>=4.0",
15
15
  "cantools>=39.0",
16
16
  "asammdf>=7.0",
17
+ "pyarrow>=23.0.1",
17
18
  ]
18
19
 
19
20
  [project.scripts]
@@ -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',
@@ -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
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);;SMV files (*.smv);;MF4 files (*.mf4 *.mf4z);;CAN files (*.blf *.trc *.asc *.dbc);;All files (*)"
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);;SMV files (*.smv);;MF4 files (*.mf4 *.mf4z);;CAN files (*.blf *.trc *.asc *.dbc);;All files (*)"
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,6 +762,10 @@ 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()
@@ -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):
@@ -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)))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: toucan-plot
3
- Version: 0.2.7
3
+ Version: 0.3.0
4
4
  Summary: A versatile PyQt6 and Matplotlib-based plotting tool for CSV and SMV data files.
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -14,6 +14,7 @@ Requires-Dist: scienceplots>=2.2.1
14
14
  Requires-Dist: python-can>=4.0
15
15
  Requires-Dist: cantools>=39.0
16
16
  Requires-Dist: asammdf>=7.0
17
+ Requires-Dist: pyarrow>=23.0.1
17
18
  Dynamic: license-file
18
19
 
19
20
  # Toucan-Plot
@@ -6,3 +6,4 @@ scienceplots>=2.2.1
6
6
  python-can>=4.0
7
7
  cantools>=39.0
8
8
  asammdf>=7.0
9
+ pyarrow>=23.0.1
File without changes
File without changes
File without changes