toucan-plot 0.2.0__py3-none-any.whl

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 (34) hide show
  1. toucan_plot/__main__.py +4 -0
  2. toucan_plot/assets/adjustments-dark.svg +1 -0
  3. toucan_plot/assets/adjustments-light.svg +1 -0
  4. toucan_plot/assets/arrow-autofit-height-dark.svg +1 -0
  5. toucan_plot/assets/arrow-autofit-height-light.svg +1 -0
  6. toucan_plot/assets/arrow-narrow-left-dark.svg +1 -0
  7. toucan_plot/assets/arrow-narrow-left-light.svg +1 -0
  8. toucan_plot/assets/arrow-narrow-right-dark.svg +1 -0
  9. toucan_plot/assets/arrow-narrow-right-light.svg +1 -0
  10. toucan_plot/assets/device-floppy-dark.svg +1 -0
  11. toucan_plot/assets/device-floppy-light.svg +1 -0
  12. toucan_plot/assets/hand-stop-dark.svg +1 -0
  13. toucan_plot/assets/hand-stop-light.svg +1 -0
  14. toucan_plot/assets/home-dark.svg +1 -0
  15. toucan_plot/assets/home-light.svg +1 -0
  16. toucan_plot/assets/ico/toucan-plot.ico +0 -0
  17. toucan_plot/assets/layout-grid-add-dark.svg +1 -0
  18. toucan_plot/assets/layout-grid-add-light.svg +1 -0
  19. toucan_plot/assets/math-function-dark.svg +1 -0
  20. toucan_plot/assets/math-function-light.svg +1 -0
  21. toucan_plot/assets/ruler-measure-dark.svg +1 -0
  22. toucan_plot/assets/ruler-measure-light.svg +1 -0
  23. toucan_plot/assets/zoom-dark.svg +1 -0
  24. toucan_plot/assets/zoom-light.svg +1 -0
  25. toucan_plot/main.py +2047 -0
  26. toucan_plot/utils/__init__.py +14 -0
  27. toucan_plot/utils/loaders.py +219 -0
  28. toucan_plot/utils/styles.py +55 -0
  29. toucan_plot-0.2.0.dist-info/METADATA +16 -0
  30. toucan_plot-0.2.0.dist-info/RECORD +34 -0
  31. toucan_plot-0.2.0.dist-info/WHEEL +5 -0
  32. toucan_plot-0.2.0.dist-info/entry_points.txt +2 -0
  33. toucan_plot-0.2.0.dist-info/licenses/LICENSE +21 -0
  34. toucan_plot-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,14 @@
1
+ from . import styles
2
+
3
+ from .loaders import (
4
+ csv_load_worker,
5
+ can_log_load_worker,
6
+ mf4_load_worker,
7
+ )
8
+
9
+ __all__ = [
10
+ "styles",
11
+ "csv_load_worker",
12
+ "can_log_load_worker",
13
+ "mf4_load_worker",
14
+ ]
@@ -0,0 +1,219 @@
1
+ import os
2
+ import csv
3
+ import numpy as np
4
+ import can
5
+ import cantools
6
+ from asammdf import MDF
7
+
8
+ def detect_delimiter(path):
9
+ with open(path, 'r') as f:
10
+ header = f.readline()
11
+ if header.find(";") != -1:
12
+ return ";"
13
+ if header.find(",") != -1:
14
+ return ","
15
+ return ";"
16
+
17
+
18
+ def csv_load_worker(path, queue):
19
+ """Multiprocessing worker for CSV loading. Sends progress via queue."""
20
+ try:
21
+ queue.put(('progress', 5, 'Reading file...'))
22
+ with open(path, 'r', newline='') as f:
23
+ reader = csv.reader(f, delimiter=detect_delimiter(path))
24
+ headers = next(reader)
25
+ headers = [h.strip() for h in headers]
26
+ rows = list(reader)
27
+
28
+ if not headers or not rows:
29
+ queue.put(('error', 'File is empty or has no data rows.'))
30
+ return
31
+
32
+ queue.put(('progress', 20, 'Building columns...'))
33
+ num_cols = len(headers)
34
+ columns = {}
35
+ for col_idx in range(num_cols):
36
+ values = []
37
+ for row in rows:
38
+ if col_idx < len(row):
39
+ try:
40
+ values.append(float(row[col_idx]))
41
+ except ValueError:
42
+ values.append(float('nan'))
43
+ else:
44
+ values.append(float('nan'))
45
+ columns[headers[col_idx]] = np.array(values)
46
+ pct = 20 + int(70 * (col_idx + 1) / num_cols)
47
+ queue.put(('progress', pct, f'Processing column {col_idx + 1}/{num_cols}...'))
48
+
49
+ queue.put(('progress', 95, 'Finalizing...'))
50
+ x_col = None
51
+ for candidate in ('Time', 'timestamp'):
52
+ if candidate in columns:
53
+ x_col = candidate
54
+ break
55
+ if x_col is None:
56
+ x_col = headers[0]
57
+
58
+ queue.put(('result', (x_col, columns)))
59
+ except Exception as e:
60
+ queue.put(('error', str(e)))
61
+
62
+
63
+ def _can_reader(path):
64
+ """Return the appropriate python-can reader for the file extension."""
65
+ ext = os.path.splitext(path)[1].lower()
66
+ if ext == '.trc':
67
+ return can.TRCReader(path)
68
+ if ext == '.asc':
69
+ return can.ASCReader(path)
70
+ return can.BLFReader(path)
71
+
72
+
73
+ def can_log_load_worker(blf_path, dbc_paths, queue):
74
+ """Multiprocessing worker for CAN log loading (BLF/TRC). Sends progress via queue."""
75
+ try:
76
+ queue.put(('progress', 5, 'Loading DBC files...'))
77
+ db = cantools.database.Database()
78
+ for dbc_path in dbc_paths:
79
+ db.add_dbc_file(dbc_path)
80
+
81
+ queue.put(('progress', 10, 'Reading CAN messages...'))
82
+ signal_data = {}
83
+ msg_count = 0
84
+
85
+ with _can_reader(blf_path) as reader:
86
+ for msg in reader:
87
+ try:
88
+ db_msg = db.get_message_by_frame_id(msg.arbitration_id)
89
+ except KeyError:
90
+ msg_count += 1
91
+ continue
92
+ try:
93
+ decoded = db_msg.decode(msg.data, decode_choices=False)
94
+ except Exception:
95
+ msg_count += 1
96
+ continue
97
+ t = msg.timestamp
98
+ for signal_name, value in decoded.items():
99
+ full_name = f"{db_msg.name}.{signal_name}"
100
+ if full_name not in signal_data:
101
+ signal_data[full_name] = []
102
+ signal_data[full_name].append((t, float(value)))
103
+ msg_count += 1
104
+ if msg_count % 5000 == 0:
105
+ queue.put(('progress', -1, f'Reading messages... ({msg_count} processed)'))
106
+
107
+ if not signal_data:
108
+ queue.put(('error', 'No CAN messages could be decoded with the provided DBC file(s).'))
109
+ return
110
+
111
+ queue.put(('progress', 60, f'Building time index ({msg_count} messages)...'))
112
+ all_timestamps = set()
113
+ for pairs in signal_data.values():
114
+ for t, _ in pairs:
115
+ all_timestamps.add(t)
116
+ all_timestamps = sorted(all_timestamps)
117
+
118
+ t0 = all_timestamps[0] if all_timestamps else 0.0
119
+ x_array = np.array([t - t0 for t in all_timestamps])
120
+ time_index = {t: i for i, t in enumerate(all_timestamps)}
121
+
122
+ queue.put(('progress', 70, 'Building signal arrays...'))
123
+ all_columns = {'Time': x_array}
124
+ sorted_names = sorted(signal_data.keys())
125
+ total_signals = len(sorted_names)
126
+ for sig_idx, full_name in enumerate(sorted_names):
127
+ pairs = signal_data[full_name]
128
+ data = np.full(len(x_array), np.nan)
129
+ for t, val in pairs:
130
+ data[time_index[t]] = val
131
+ # Forward-fill NaNs with last known value
132
+ last = np.nan
133
+ for i in range(len(data)):
134
+ if np.isnan(data[i]):
135
+ data[i] = last
136
+ else:
137
+ last = data[i]
138
+ all_columns[full_name] = data
139
+ pct = 70 + int(25 * (sig_idx + 1) / total_signals)
140
+ if (sig_idx + 1) % 10 == 0 or sig_idx == total_signals - 1:
141
+ queue.put(('progress', pct, f'Processing signal {sig_idx + 1}/{total_signals}...'))
142
+
143
+ queue.put(('result', ('Time', all_columns)))
144
+ except Exception as e:
145
+ queue.put(('error', str(e)))
146
+
147
+
148
+ def mf4_load_worker(path, queue):
149
+ """Multiprocessing worker for MF4 loading via asammdf. Sends progress via queue."""
150
+ try:
151
+ queue.put(('progress', 5, 'Opening MF4 file...'))
152
+ mdf = MDF(path)
153
+
154
+ queue.put(('progress', 15, 'Extracting channels...'))
155
+ # Collect all non-empty numeric channels
156
+ channel_names = []
157
+ for i, group in enumerate(mdf.groups):
158
+ for j, channel in enumerate(group.channels):
159
+ name = channel.name
160
+ if name and name != 't':
161
+ channel_names.append((name, i, j))
162
+
163
+ if not channel_names:
164
+ queue.put(('error', 'No channels found in MF4 file.'))
165
+ mdf.close()
166
+ return
167
+
168
+ queue.put(('progress', 25, f'Processing {len(channel_names)} channels...'))
169
+ all_columns = {}
170
+ x_col = None
171
+ total = len(channel_names)
172
+
173
+ for idx, (name, group_idx, ch_idx) in enumerate(channel_names):
174
+ try:
175
+ sig = mdf.get(name, group=group_idx, index=ch_idx)
176
+ except Exception:
177
+ continue
178
+
179
+ # Skip non-numeric or empty signals
180
+ if sig.samples.size == 0:
181
+ continue
182
+ if sig.samples.ndim != 1:
183
+ continue
184
+ if not np.issubdtype(sig.samples.dtype, np.number):
185
+ continue
186
+
187
+ timestamps = sig.timestamps
188
+ try:
189
+ samples = sig.samples.astype(float)
190
+ except (ValueError, TypeError):
191
+ continue
192
+
193
+ # Use the first valid channel's timestamps as x axis
194
+ if x_col is None:
195
+ x_col = 'Time'
196
+ all_columns['Time'] = timestamps
197
+
198
+ # If this channel's timestamps match the common time axis, use directly
199
+ common_t = all_columns['Time']
200
+ if len(timestamps) == len(common_t) and np.allclose(timestamps, common_t):
201
+ all_columns[name] = samples
202
+ else:
203
+ # Interpolate onto the common time axis
204
+ all_columns[name] = np.interp(common_t, timestamps, samples)
205
+
206
+ pct = 25 + int(70 * (idx + 1) / total)
207
+ if (idx + 1) % 20 == 0 or idx == total - 1:
208
+ queue.put(('progress', pct, f'Processing channel {idx + 1}/{total}...'))
209
+
210
+ mdf.close()
211
+
212
+ if x_col is None or len(all_columns) <= 1:
213
+ queue.put(('error', 'No numeric channels could be extracted from the MF4 file.'))
214
+ return
215
+
216
+ queue.put(('progress', 98, 'Finalizing...'))
217
+ queue.put(('result', (x_col, all_columns)))
218
+ except Exception as e:
219
+ queue.put(('error', str(e)))
@@ -0,0 +1,55 @@
1
+ import matplotlib.pyplot as plt
2
+ from cycler import cycler
3
+ import scienceplots
4
+
5
+ def set_default_style() -> None:
6
+ """Configure matplotlib for screen viewing."""
7
+ plt.style.use(['default','no-latex'])
8
+ plt.rcParams.update(
9
+ {
10
+ "axes.prop_cycle": cycler("color", ["blue", "orange", "green", "red", "purple", "brown"]),
11
+ "lines.linewidth": 1,
12
+ "axes.grid": True,
13
+ "text.usetex": False,
14
+ "axes3d.grid": True,
15
+ "figure.constrained_layout.use": True,
16
+ }
17
+ )
18
+
19
+ def set_style1_style() -> None:
20
+ """Configure matplotlib for screen viewing."""
21
+ plt.style.use(['default','no-latex'])
22
+ plt.rcParams.update(
23
+ {
24
+ "axes.prop_cycle": cycler("color", ["b", "r", "m", "c"]),
25
+ "lines.linewidth": 1,
26
+ "axes.grid": True,
27
+ "text.usetex": False,
28
+ "axes3d.grid": True,
29
+ "figure.constrained_layout.use": True,
30
+ }
31
+ )
32
+
33
+ def set_style2_style() -> None:
34
+ """Configure matplotlib for Style 2."""
35
+ plt.style.use(['default','no-latex'])
36
+ plt.rcParams.update(
37
+ {
38
+ "axes.prop_cycle": cycler("color", ["#2962FF", "r", "#AA00FF", "#00B8D4", "#FF6D00", "#00C853"]),
39
+ "lines.linewidth": 1,
40
+ "axes.grid": True,
41
+ "axes3d.grid": True,
42
+ "figure.constrained_layout.use": True,
43
+ }
44
+ )
45
+
46
+ def set_ieee_style() -> None:
47
+ """Configure matplotlib for IEEE style."""
48
+ plt.style.use(['science','ieee','grid','no-latex'])
49
+ plt.rcParams.update(
50
+ {
51
+ "axes3d.grid": True,
52
+ "figure.dpi": 150,
53
+ "figure.constrained_layout.use": True,
54
+ }
55
+ )
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: toucan-plot
3
+ Version: 0.2.0
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
+ License-File: LICENSE
8
+ Requires-Dist: pyqt6>=6.0
9
+ Requires-Dist: matplotlib>=3.0
10
+ Requires-Dist: numpy>=1.20
11
+ Requires-Dist: pyqtdarktheme>=2.1.0
12
+ Requires-Dist: scienceplots>=2.2.1
13
+ Requires-Dist: python-can>=4.0
14
+ Requires-Dist: cantools>=39.0
15
+ Requires-Dist: asammdf>=7.0
16
+ Dynamic: license-file
@@ -0,0 +1,34 @@
1
+ toucan_plot/__main__.py,sha256=GyGkAx0WXg25bWW-FqX3yVHGkiOl9wjvrXPZGSpOHA4,75
2
+ toucan_plot/main.py,sha256=CnuM6IczvVnTuRaBHnyh8fZRbL5xp-snRi2hbl1ONNw,93245
3
+ toucan_plot/assets/adjustments-dark.svg,sha256=2pJd9UEOONi8LOvb052rBvAKZdZ85ri9Xo7H4OeW0dw,568
4
+ toucan_plot/assets/adjustments-light.svg,sha256=BvbEEEKHzSAbys96JMZFt7AuNZ20qOjEVoEeJP4WJKU,568
5
+ toucan_plot/assets/arrow-autofit-height-dark.svg,sha256=Osd1UNJ5mzrw1zmBxE4fnqzWSIjcoSYsE1Cv9_XvXlw,468
6
+ toucan_plot/assets/arrow-autofit-height-light.svg,sha256=a8K9kwJuoe7n5dx83PPtVG23RZllfbek2n8LEml2RiY,468
7
+ toucan_plot/assets/arrow-narrow-left-dark.svg,sha256=Gk4YxEBOyGO8Odpq_GszQwVPd9NfheYeJF8JDhtuLXE,378
8
+ toucan_plot/assets/arrow-narrow-left-light.svg,sha256=ipQ2oJuQqLyOnDBoJVIRuzplrJKzJe1mxy3mbYuLeEc,378
9
+ toucan_plot/assets/arrow-narrow-right-dark.svg,sha256=NhCv2etMYOulRsmSB-ldzKnhEt2MMtOcdslczOuxqxs,380
10
+ toucan_plot/assets/arrow-narrow-right-light.svg,sha256=XYMe0SIpLwsEYDRMkyj8jS__2DqoxcHkKKXfTLpf_ic,380
11
+ toucan_plot/assets/device-floppy-dark.svg,sha256=e5r2oT-FIIVyHHWJ_bwYkp_Yi2ACuq1u9Zizb9vkEDM,487
12
+ toucan_plot/assets/device-floppy-light.svg,sha256=Shl-N6sAWhE5mO4ZDO4HgGMF3Lf7Ntx52DqcF0siE4E,487
13
+ toucan_plot/assets/hand-stop-dark.svg,sha256=w3h2ZtVG2hMCN0T0tA642o0CeV1xytw46KFG_keH1zo,652
14
+ toucan_plot/assets/hand-stop-light.svg,sha256=V7iRtr3lL1yg1Wet4EanYnWqcb5lV0P_E-M2eEi5hMA,652
15
+ toucan_plot/assets/home-dark.svg,sha256=JMmXu6C1eMJy9l75LCqlGZ9rj7NOUxKZ4EnbSGbcqZ8,443
16
+ toucan_plot/assets/home-light.svg,sha256=9oi05ncSwTN5jraYkcBclxHJOOn3FAVFmxmN1phnrhs,443
17
+ toucan_plot/assets/layout-grid-add-dark.svg,sha256=_KFoic-KvgFpPkyzxW7TdpDGZBombWuhV8tKsJQJxsg,606
18
+ toucan_plot/assets/layout-grid-add-light.svg,sha256=qKu8zLUsBrEdThqVBgAXjGxhf3ABed0hnh-0GJr54yU,606
19
+ toucan_plot/assets/math-function-dark.svg,sha256=ql8lWpUY_5PFgRj1Kq8i7rFB5KRT8Xqm34a_5bVzZNg,443
20
+ toucan_plot/assets/math-function-light.svg,sha256=A1hcYzYJxjqfH3_eht1ikflduFrAuamOr4WcWwG5a6o,443
21
+ toucan_plot/assets/ruler-measure-dark.svg,sha256=Ujqsex7bqHRXiU1DWCxVVHf-iiNz7QY123TpSFAioT4,629
22
+ toucan_plot/assets/ruler-measure-light.svg,sha256=Uz7AkVpifwUVJAWrA3iNb2iOIBtqNUDbHkF7GK2LgVE,629
23
+ toucan_plot/assets/zoom-dark.svg,sha256=c4uZt1NIb3lwoLgfoqH3Zfj018vDHC57x_MEzOrTYU0,371
24
+ toucan_plot/assets/zoom-light.svg,sha256=RolfeW-FvrmVA0ERV7iNQ7_RQl7KaOyOY-ZEbnCjfpg,371
25
+ toucan_plot/assets/ico/toucan-plot.ico,sha256=m5ZVE4A7G0MkZU9dIRrT-HVrjiT7jsFgxi1jaUetx_4,751300
26
+ toucan_plot/utils/__init__.py,sha256=MZ1gO96ov04y4QHheZG3tCzHgslYNEOG2qeY7as-uv0,228
27
+ toucan_plot/utils/loaders.py,sha256=mRrM0arJQ7IsY9y4y22A2FtplGBrh2_marZtHmAwqWM,8214
28
+ toucan_plot/utils/styles.py,sha256=liC5yb_7uR0h5EoFh8P_wFui62WBa1XH-wWssO6uu_w,1754
29
+ toucan_plot-0.2.0.dist-info/licenses/LICENSE,sha256=ZvZfVEqq1wmh-7AWxXbLxI2z2iKLpt85hdexHL_B3RM,1106
30
+ toucan_plot-0.2.0.dist-info/METADATA,sha256=1mLoRh6pclTiA8g-3E7h02hSVndEhvFZaaWXf7BOB-0,497
31
+ toucan_plot-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
32
+ toucan_plot-0.2.0.dist-info/entry_points.txt,sha256=FaV5CRr3WQ6i68t72m8bfvIYWBxrv0f_XrZPkjDkqPY,54
33
+ toucan_plot-0.2.0.dist-info/top_level.txt,sha256=4zDACL7KXhtQYEFlRrzXMpTwQiG-sTJOR_mLlVXLnkc,12
34
+ toucan_plot-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ toucan-plot = toucan_plot.main:main
@@ -0,0 +1,21 @@
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.
@@ -0,0 +1 @@
1
+ toucan_plot