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.
- toucan_plot/__main__.py +4 -0
- toucan_plot/assets/adjustments-dark.svg +1 -0
- toucan_plot/assets/adjustments-light.svg +1 -0
- toucan_plot/assets/arrow-autofit-height-dark.svg +1 -0
- toucan_plot/assets/arrow-autofit-height-light.svg +1 -0
- toucan_plot/assets/arrow-narrow-left-dark.svg +1 -0
- toucan_plot/assets/arrow-narrow-left-light.svg +1 -0
- toucan_plot/assets/arrow-narrow-right-dark.svg +1 -0
- toucan_plot/assets/arrow-narrow-right-light.svg +1 -0
- toucan_plot/assets/device-floppy-dark.svg +1 -0
- toucan_plot/assets/device-floppy-light.svg +1 -0
- toucan_plot/assets/hand-stop-dark.svg +1 -0
- toucan_plot/assets/hand-stop-light.svg +1 -0
- toucan_plot/assets/home-dark.svg +1 -0
- toucan_plot/assets/home-light.svg +1 -0
- toucan_plot/assets/ico/toucan-plot.ico +0 -0
- toucan_plot/assets/layout-grid-add-dark.svg +1 -0
- toucan_plot/assets/layout-grid-add-light.svg +1 -0
- toucan_plot/assets/math-function-dark.svg +1 -0
- toucan_plot/assets/math-function-light.svg +1 -0
- toucan_plot/assets/ruler-measure-dark.svg +1 -0
- toucan_plot/assets/ruler-measure-light.svg +1 -0
- toucan_plot/assets/zoom-dark.svg +1 -0
- toucan_plot/assets/zoom-light.svg +1 -0
- toucan_plot/main.py +2047 -0
- toucan_plot/utils/__init__.py +14 -0
- toucan_plot/utils/loaders.py +219 -0
- toucan_plot/utils/styles.py +55 -0
- toucan_plot-0.2.0.dist-info/METADATA +16 -0
- toucan_plot-0.2.0.dist-info/RECORD +34 -0
- toucan_plot-0.2.0.dist-info/WHEEL +5 -0
- toucan_plot-0.2.0.dist-info/entry_points.txt +2 -0
- toucan_plot-0.2.0.dist-info/licenses/LICENSE +21 -0
- toucan_plot-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -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,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
|