yabplot 0.1.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.
- yabplot/__init__.py +9 -0
- yabplot/data/__init__.py +371 -0
- yabplot/plotting.py +614 -0
- yabplot/scene.py +124 -0
- yabplot/utils.py +170 -0
- yabplot-0.1.0.dist-info/METADATA +97 -0
- yabplot-0.1.0.dist-info/RECORD +10 -0
- yabplot-0.1.0.dist-info/WHEEL +5 -0
- yabplot-0.1.0.dist-info/licenses/LICENSE +19 -0
- yabplot-0.1.0.dist-info/top_level.txt +1 -0
yabplot/scene.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pyvista as pv
|
|
4
|
+
|
|
5
|
+
def get_shading_preset(style_name):
|
|
6
|
+
"""
|
|
7
|
+
Returns a dictionary of lighting parameters for pyvista.add_mesh.
|
|
8
|
+
|
|
9
|
+
Styles:
|
|
10
|
+
- 'default': Balanced, no shine.
|
|
11
|
+
- 'matte': (Soft) High ambient, low contrast. Good for reading atlas colors.
|
|
12
|
+
- 'sculpted':(Hard) Stronger shadows, higher contrast. Good for showing anatomy.
|
|
13
|
+
- 'glossy': (Shiny) Wet/Plastic look with specular highlights.
|
|
14
|
+
"""
|
|
15
|
+
presets = {
|
|
16
|
+
'default': {
|
|
17
|
+
'lighting': True,
|
|
18
|
+
'specular': 0.0,
|
|
19
|
+
'ambient': 0.65,
|
|
20
|
+
'diffuse': 0.4,
|
|
21
|
+
'specular_power': 15
|
|
22
|
+
},
|
|
23
|
+
# very bright shadows
|
|
24
|
+
'matte': {
|
|
25
|
+
'lighting': True,
|
|
26
|
+
'specular': 0.0,
|
|
27
|
+
'ambient': 0.75,
|
|
28
|
+
'diffuse': 0.2,
|
|
29
|
+
'specular_power': 0
|
|
30
|
+
},
|
|
31
|
+
# slight shime, dark shadows, strong directional light
|
|
32
|
+
'sculpted': {
|
|
33
|
+
'lighting': True,
|
|
34
|
+
'specular': 0.05,
|
|
35
|
+
'ambient': 0.4,
|
|
36
|
+
'diffuse': 0.6,
|
|
37
|
+
'specular_power': 10
|
|
38
|
+
},
|
|
39
|
+
# strong shine, sharp highlights
|
|
40
|
+
'glossy': {
|
|
41
|
+
'lighting': True,
|
|
42
|
+
'specular': 0.3,
|
|
43
|
+
'ambient': 0.4,
|
|
44
|
+
'diffuse': 0.6,
|
|
45
|
+
'specular_power': 30
|
|
46
|
+
},
|
|
47
|
+
# flat 2D
|
|
48
|
+
'flat': {
|
|
49
|
+
'lighting': False,
|
|
50
|
+
'ambient': 1.0,
|
|
51
|
+
'diffuse': 0.0,
|
|
52
|
+
'specular': 0.0
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if style_name not in presets:
|
|
57
|
+
print(f"Warning: Style '{style_name}' not found. Using 'default'. Options: {list(presets.keys())}")
|
|
58
|
+
return presets['default']
|
|
59
|
+
|
|
60
|
+
return presets[style_name]
|
|
61
|
+
|
|
62
|
+
def get_view_configs(view_names):
|
|
63
|
+
all_views = {
|
|
64
|
+
'left_lateral': {'pos': (-1, 0, 0), 'up': (0, 0, 1), 'side': 'L'},
|
|
65
|
+
'right_lateral': {'pos': (1, 0, 0), 'up': (0, 0, 1), 'side': 'R'},
|
|
66
|
+
'left_medial': {'pos': (1, 0, 0), 'up': (0, 0, 1), 'side': 'L'},
|
|
67
|
+
'right_medial': {'pos': (-1, 0, 0), 'up': (0, 0, 1), 'side': 'R'},
|
|
68
|
+
'superior': {'pos': (0, 0, 1), 'up': (0, 1, 0), 'side': 'both'},
|
|
69
|
+
'inferior': {'pos': (0, 0, -1), 'up': (0, 1, 0), 'side': 'both'},
|
|
70
|
+
'anterior': {'pos': (0, 1, 0), 'up': (0, 0, 1), 'side': 'both'},
|
|
71
|
+
'posterior': {'pos': (0, -1, 0), 'up': (0, 0, 1), 'side': 'both'}
|
|
72
|
+
}
|
|
73
|
+
if view_names is None: return all_views
|
|
74
|
+
return {k: all_views[k] for k in view_names if k in all_views}
|
|
75
|
+
|
|
76
|
+
def setup_plotter(sel_views, layout, figsize, display_type, needs_bottom_row=True):
|
|
77
|
+
n = len(sel_views)
|
|
78
|
+
if layout is None:
|
|
79
|
+
if n <= 4: base_layout = (1, n)
|
|
80
|
+
elif n <= 6: base_layout = (2, 3)
|
|
81
|
+
else: base_layout = (int(np.ceil(n/4)), 4)
|
|
82
|
+
else: base_layout = layout
|
|
83
|
+
|
|
84
|
+
if needs_bottom_row:
|
|
85
|
+
nrows, ncols = base_layout[0] + 1, base_layout[1]
|
|
86
|
+
groups = [(nrows - 1, slice(0, ncols))]
|
|
87
|
+
row_weights = [1.0]*base_layout[0] + [0.2]
|
|
88
|
+
else:
|
|
89
|
+
nrows, ncols = base_layout[0], base_layout[1]
|
|
90
|
+
groups = None
|
|
91
|
+
row_weights = None
|
|
92
|
+
|
|
93
|
+
plotter = pv.Plotter(shape=(nrows, ncols), groups=groups, row_weights=row_weights,
|
|
94
|
+
off_screen=(display_type=='none'), window_size=figsize, border=False)
|
|
95
|
+
plotter.set_background('white')
|
|
96
|
+
return plotter, ncols, nrows
|
|
97
|
+
|
|
98
|
+
def add_context_to_view(plotter, bmesh, view_side, alpha, color, **kwargs):
|
|
99
|
+
"""
|
|
100
|
+
Adds context mesh. Lighting parameters are passed via **kwargs.
|
|
101
|
+
"""
|
|
102
|
+
if not bmesh: return
|
|
103
|
+
for h, mesh in bmesh.items():
|
|
104
|
+
if (view_side == 'L' and h == 'rh') or (view_side == 'R' and h == 'lh'): continue
|
|
105
|
+
plotter.add_mesh(mesh, color=color, opacity=alpha,
|
|
106
|
+
smooth_shading=True, show_edges=False,
|
|
107
|
+
**kwargs)
|
|
108
|
+
|
|
109
|
+
def set_camera(plotter, view_cfg, zoom=1.0, distance=200):
|
|
110
|
+
plotter.camera.position = tuple(p * distance for p in view_cfg['pos'])
|
|
111
|
+
plotter.camera.focal_point = (0, 0, 0)
|
|
112
|
+
plotter.camera.up = view_cfg['up']
|
|
113
|
+
plotter.camera.parallel_projection = True
|
|
114
|
+
plotter.reset_camera()
|
|
115
|
+
plotter.camera.zoom(zoom)
|
|
116
|
+
|
|
117
|
+
def finalize_plot(plotter, export_path, display_type):
|
|
118
|
+
if export_path: plotter.screenshot(export_path, transparent_background=True)
|
|
119
|
+
|
|
120
|
+
if display_type == 'static': plotter.show(jupyter_backend='static')
|
|
121
|
+
elif display_type == 'interactive': plotter.show(jupyter_backend='trame')
|
|
122
|
+
elif display_type == 'none': plotter.close()
|
|
123
|
+
return plotter
|
|
124
|
+
|
yabplot/utils.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import nibabel as nib
|
|
5
|
+
import pyvista as pv
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
from importlib.resources import files
|
|
8
|
+
|
|
9
|
+
def load_gii(gii_path):
|
|
10
|
+
"""Load GIfTI geometry (vertices, faces)."""
|
|
11
|
+
mesh = nib.load(gii_path)
|
|
12
|
+
verts = mesh.darrays[0].data
|
|
13
|
+
faces = mesh.darrays[1].data
|
|
14
|
+
return verts, faces
|
|
15
|
+
|
|
16
|
+
def load_gii2pv(gii_path, smooth_i=0, smooth_f=0.1):
|
|
17
|
+
"""
|
|
18
|
+
Load GIfTI and convert to PyVista format with optional smoothing.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
smooth_i : int
|
|
23
|
+
Number of smoothing iterations (e.g. 15).
|
|
24
|
+
smooth_f : float
|
|
25
|
+
Relaxation factor (0.0 to 1.0, e.g. 0.6).
|
|
26
|
+
"""
|
|
27
|
+
verts, faces = load_gii(gii_path)
|
|
28
|
+
|
|
29
|
+
# create pyvista mesh
|
|
30
|
+
faces_pv = np.hstack([np.full((faces.shape[0], 1), 3), faces]).flatten().astype(int)
|
|
31
|
+
mesh = pv.PolyData(verts, faces_pv)
|
|
32
|
+
|
|
33
|
+
# apply smoothing
|
|
34
|
+
if smooth_i > 0:
|
|
35
|
+
# use Laplacian smoothing (standard vtkSmoothPolyDataFilter)
|
|
36
|
+
# note: higher relaxation factors can shrink the mesh significantly
|
|
37
|
+
# if shrinkage is an issue, could consider mesh.smooth_taubin() instead
|
|
38
|
+
mesh = mesh.smooth(n_iter=smooth_i, relaxation_factor=smooth_f)
|
|
39
|
+
|
|
40
|
+
return mesh
|
|
41
|
+
|
|
42
|
+
def make_cortical_mesh(verts, faces, scalars):
|
|
43
|
+
"""Helper to create a PyVista mesh from raw buffers."""
|
|
44
|
+
faces_pv = np.hstack([np.full((faces.shape[0], 1), 3), faces]).flatten().astype(int)
|
|
45
|
+
mesh = pv.PolyData(verts, faces_pv)
|
|
46
|
+
mesh['Data'] = scalars
|
|
47
|
+
return mesh
|
|
48
|
+
|
|
49
|
+
def prep_data(data, regions, atlas, category):
|
|
50
|
+
"""Standardize input data to dictionary."""
|
|
51
|
+
if isinstance(data, pd.DataFrame):
|
|
52
|
+
if data.shape[1] >= 2:
|
|
53
|
+
return dict(zip(data.iloc[:, 0], data.iloc[:, 1]))
|
|
54
|
+
elif isinstance(data, pd.Series):
|
|
55
|
+
return data.to_dict()
|
|
56
|
+
elif isinstance(data, dict):
|
|
57
|
+
return data
|
|
58
|
+
elif isinstance(data, (list, np.ndarray, tuple)):
|
|
59
|
+
if len(data) != len(regions):
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"Data length mismatch! Atlas '{atlas}' has {len(regions)} regions, "
|
|
62
|
+
f"but input data has {len(data)}. "
|
|
63
|
+
f"For partial data, use a dictionary, pd.Series, or pd.DataFrame. "
|
|
64
|
+
f"Use `yabplot.get_atlas_regions('{atlas}', '{category}')` to see expected order."
|
|
65
|
+
)
|
|
66
|
+
# map strictly by order
|
|
67
|
+
return dict(zip(regions, data))
|
|
68
|
+
|
|
69
|
+
return data
|
|
70
|
+
|
|
71
|
+
def generate_distinct_colors(n_colors, seed=42):
|
|
72
|
+
"""Generate visually distinct colors using Golden Ratio."""
|
|
73
|
+
np.random.seed(seed)
|
|
74
|
+
colors = []
|
|
75
|
+
hue = np.random.rand()
|
|
76
|
+
for _ in range(n_colors):
|
|
77
|
+
hue = (hue + 0.618033988749895) % 1.0
|
|
78
|
+
colors.append(plt.cm.hsv(hue)[:3])
|
|
79
|
+
return colors
|
|
80
|
+
|
|
81
|
+
def parse_lut(lut_path):
|
|
82
|
+
"""parses LUT to color array and name list."""
|
|
83
|
+
|
|
84
|
+
# load and sort by ID to ensure strict order (1..N)
|
|
85
|
+
df = pd.read_csv(lut_path, sep=r'\s+', header=None)
|
|
86
|
+
df = df.sort_values(by=0)
|
|
87
|
+
|
|
88
|
+
ids = df[0].values
|
|
89
|
+
names = df[1].tolist()
|
|
90
|
+
rgb = df.iloc[:, 2:5].values / 255.0
|
|
91
|
+
|
|
92
|
+
max_id = ids.max()
|
|
93
|
+
|
|
94
|
+
lut_colors = np.full((max_id + 1, 3), 0.5)
|
|
95
|
+
lut_names_list = ["Unknown"] * (max_id + 1)
|
|
96
|
+
|
|
97
|
+
lut_colors[ids] = rgb
|
|
98
|
+
for idx, name in zip(ids, names):
|
|
99
|
+
lut_names_list[idx] = name
|
|
100
|
+
|
|
101
|
+
return ids, lut_colors, lut_names_list, max_id
|
|
102
|
+
|
|
103
|
+
def map_values_to_surface(data, target_labels, lut_ids, dense_lut_names):
|
|
104
|
+
"""maps data to vertices."""
|
|
105
|
+
# filter valid regions
|
|
106
|
+
valid_ids_list = []
|
|
107
|
+
valid_names_list = []
|
|
108
|
+
|
|
109
|
+
for rid in lut_ids:
|
|
110
|
+
if rid < len(dense_lut_names):
|
|
111
|
+
valid_ids_list.append(rid)
|
|
112
|
+
valid_names_list.append(dense_lut_names[rid])
|
|
113
|
+
|
|
114
|
+
valid_ids = np.array(valid_ids_list)
|
|
115
|
+
n_regions = len(valid_ids)
|
|
116
|
+
|
|
117
|
+
# atlas visualization without data
|
|
118
|
+
if data is None:
|
|
119
|
+
return target_labels
|
|
120
|
+
|
|
121
|
+
# data mapping
|
|
122
|
+
max_id = max(target_labels.max(), lut_ids.max())
|
|
123
|
+
lookup_table = np.full(max_id + 1, np.nan)
|
|
124
|
+
source_values = np.full(n_regions, np.nan)
|
|
125
|
+
|
|
126
|
+
if isinstance(data, dict):
|
|
127
|
+
for i, name in enumerate(valid_names_list):
|
|
128
|
+
if name in data:
|
|
129
|
+
source_values[i] = data[name]
|
|
130
|
+
elif isinstance(data, (np.ndarray, list, tuple)):
|
|
131
|
+
# map by order
|
|
132
|
+
if len(data) != n_regions:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"Data length mismatch! The atlas LUT defines {n_regions} regions, "
|
|
135
|
+
f"but input data has {len(data)}.\n"
|
|
136
|
+
f"Expected order starts with: {valid_names_list[0:3]}...\n"
|
|
137
|
+
f"Solution: Use a dictionary for partial data, or check `yabplot.get_atlas_regions`."
|
|
138
|
+
)
|
|
139
|
+
source_values = np.array(data)
|
|
140
|
+
else:
|
|
141
|
+
raise ValueError("Data must be dict, list, or numpy array.")
|
|
142
|
+
|
|
143
|
+
lookup_table[valid_ids] = source_values
|
|
144
|
+
return lookup_table[target_labels]
|
|
145
|
+
|
|
146
|
+
def lines_from_streamlines(streamlines):
|
|
147
|
+
if len(streamlines) == 0: return np.array([]), np.array([]), np.array([])
|
|
148
|
+
|
|
149
|
+
points = np.vstack(streamlines)
|
|
150
|
+
n_points = [len(s) for s in streamlines]
|
|
151
|
+
offsets = np.insert(np.cumsum(n_points), 0, 0)[:-1]
|
|
152
|
+
|
|
153
|
+
cells = []
|
|
154
|
+
for length, offset in zip(n_points, offsets):
|
|
155
|
+
cells.append(np.hstack([[length], np.arange(offset, offset + length)]))
|
|
156
|
+
lines = np.hstack(cells)
|
|
157
|
+
|
|
158
|
+
# Calculate tangents
|
|
159
|
+
tangents = []
|
|
160
|
+
for s in streamlines:
|
|
161
|
+
if len(s) < 2:
|
|
162
|
+
tangents.append(np.array([[0,0,0]]))
|
|
163
|
+
continue
|
|
164
|
+
vecs = np.diff(s, axis=0)
|
|
165
|
+
vecs = np.vstack([vecs, vecs[-1:]])
|
|
166
|
+
norms = np.linalg.norm(vecs, axis=1, keepdims=True)
|
|
167
|
+
norms[norms == 0] = 1
|
|
168
|
+
tangents.append(vecs / norms)
|
|
169
|
+
|
|
170
|
+
return points, lines, np.vstack(tangents)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yabplot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: yet another brain plot
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: ipywidgets>=8.1.8
|
|
9
|
+
Requires-Dist: nibabel>=5.3.2
|
|
10
|
+
Requires-Dist: pandas>=2.3.3
|
|
11
|
+
Requires-Dist: pooch>=1.8.2
|
|
12
|
+
Requires-Dist: pyvista>=0.46.4
|
|
13
|
+
Requires-Dist: scikit-image>=0.25.2
|
|
14
|
+
Requires-Dist: trame>=3.12.0
|
|
15
|
+
Requires-Dist: trame-vtk>=2.10.0
|
|
16
|
+
Requires-Dist: trame-vuetify>=3.1.0
|
|
17
|
+
Provides-Extra: docs
|
|
18
|
+
Requires-Dist: mkdocs; extra == "docs"
|
|
19
|
+
Requires-Dist: mkdocs-jupyter>=0.25.1; extra == "docs"
|
|
20
|
+
Requires-Dist: mkdocs-material; extra == "docs"
|
|
21
|
+
Requires-Dist: mkdocstrings[python]; extra == "docs"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# yabplot: yet another brain plot
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/yabplot/)
|
|
29
|
+
[](https://teanijarv.github.io/yabplot/)
|
|
30
|
+
[](https://github.com/teanijarv/yabplot/actions/workflows/tests.yml)
|
|
31
|
+
<!-- [](https://doi.org/10.5281/zenodo.XXXXXX) -->
|
|
32
|
+
|
|
33
|
+
**yabplot** is a Python library for creating beautiful, publication-quality 3D brain visualizations. it supports plotting cortical regions, subcortical structures, and white matter bundles.
|
|
34
|
+
|
|
35
|
+
the idea is simple. while there are already amazing visualization tools available, they often focus on specific domains—using one tool for white matter tracts and another for cortical surfaces inevitably leads to inconsistent styles. i wanted a unified, simple-to-use tool that enables me (and hopefully others) to perform most brain visualizations in a single place. recognizing that neuroscience evolves daily, i designed **yabplot** to be modular: it supports standard pre-packaged atlases out of the box, but easily accepts any custom parcellation or tractography dataset you might need.
|
|
36
|
+
|
|
37
|
+
## features
|
|
38
|
+
|
|
39
|
+
* **pre-existing atlases:** access many commonly used atlases (schaefer2018, brainnetome, aparc, aseg, musus100, xtract, etc) on demand.
|
|
40
|
+
* **simple to use:** plug-n-play functions for cortex, subcortex, and tracts with a unified API.
|
|
41
|
+
* **custom atlases:** easily use your own parcellations, segmentations (.nii/.gii), or tractograms (.trk).
|
|
42
|
+
* **flexible inputs:** accepts data as dictionaries (for partial mapping) or arrays (for strict mapping).
|
|
43
|
+
|
|
44
|
+
## installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv add yabplot
|
|
48
|
+
```
|
|
49
|
+
or
|
|
50
|
+
```bash
|
|
51
|
+
pip install yabplot
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
dependencies: python 3.11 with ipywidgets, nibabel, pandas, pooch, pyvista, scikit-image, trame, trame-vtk, trame-vuetify
|
|
55
|
+
|
|
56
|
+
## quick start
|
|
57
|
+
|
|
58
|
+
please refer to the [documentation](https://teanijarv.github.io/yabplot/) for more comprehensive guides.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import yabplot as yab
|
|
62
|
+
import numpy as np
|
|
63
|
+
|
|
64
|
+
# see available cortical atlases
|
|
65
|
+
atlases = yab.get_available_resources(category='cortical')
|
|
66
|
+
|
|
67
|
+
# see the region names within the aseg atlas
|
|
68
|
+
regions = yab.get_atlas_regions(atlas='aseg', category='subcortical')
|
|
69
|
+
|
|
70
|
+
# plot data on cortical regions
|
|
71
|
+
data = np.arange(0, 1, 0.001)
|
|
72
|
+
yab.plot_cortical(data=data, atlas='schaefer_1000', figsize=(600, 300),
|
|
73
|
+
cmap='viridis', vminmax=[0, 1], style='default',
|
|
74
|
+
views=['left_lateral', 'superior', 'right_lateral'])
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# plot values for specific subcortical regions
|
|
78
|
+
data = {'Left_Amygdala': 0.8, 'Right_Hippocampus': 0.5,
|
|
79
|
+
'Right_Thalamus': -0.5, 'Left_Putamen': -1}
|
|
80
|
+
yab.plot_subcortical(data=data, atlas='aseg', figsize=(600, 450), layout=(2, 2),
|
|
81
|
+
views=['superior', 'anterior', 'left_lateral', 'right_lateral'],
|
|
82
|
+
cmap='coolwarm', vminmax=[-1, 1], style='matte')
|
|
83
|
+
|
|
84
|
+
# plot data on white matter bundles
|
|
85
|
+
regions = yab.get_atlas_regions(atlas='xtract_tiny', category='tracts')
|
|
86
|
+
data = np.arange(0, len(regions))
|
|
87
|
+
yab.plot_tracts(data=data, atlas='xtract_tiny', figsize=(600, 300),
|
|
88
|
+
views=['superior', 'anterior', 'left_lateral'], nan_color='#cccccc',
|
|
89
|
+
bmesh_type='fsaverage', style='default', cmap='plasma')
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+

|
|
94
|
+
|
|
95
|
+
## acknowledgements
|
|
96
|
+
|
|
97
|
+
yabplot relies on the extensive work of the neuroimaging community. if you use these atlases in your work, please cite the original authors.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
yabplot/__init__.py,sha256=qWDPpGuPgMJhol88FJ1BlPgnu1kMQQWj-iLUAniUVJQ,308
|
|
2
|
+
yabplot/plotting.py,sha256=UI2fOqNeHIB0_Exj0bltnyUrEhpeNMl7cEJkS9Uzdws,25967
|
|
3
|
+
yabplot/scene.py,sha256=vQ4QxOdQ6uhoMGtz9dxQi5G0E5gip5DR3v3sbjMviaU,4522
|
|
4
|
+
yabplot/utils.py,sha256=ROtBqmTqmhNrdPBUCqp4hWmRv363i-R7xH1SZGHTNNk,5737
|
|
5
|
+
yabplot/data/__init__.py,sha256=jYR5iyJeEk0qRt8jYcaHGy6q6uJQVHFUasj78SH5X64,12047
|
|
6
|
+
yabplot-0.1.0.dist-info/licenses/LICENSE,sha256=bz__yccnNr-Tsjp1iTjk2KBb1EgW4mdkdZCLAU-Yw24,1063
|
|
7
|
+
yabplot-0.1.0.dist-info/METADATA,sha256=NmCvg1vkedOTV-fRdt6hFe7ZjNBsWhTkaOmn5srinS0,4331
|
|
8
|
+
yabplot-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
yabplot-0.1.0.dist-info/top_level.txt,sha256=hrhVrEs-yKkYrOJfuc84JCkuOxJMN2AxEU5jxA1tZco,8
|
|
10
|
+
yabplot-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 Toomas Erik Anijärv
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
14
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
15
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
16
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
17
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
18
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
|
19
|
+
OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
yabplot
|