polyptich 0.0.7__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.
- polyptich-0.0.7/.gitignore +5 -0
- polyptich-0.0.7/PKG-INFO +21 -0
- polyptich-0.0.7/README.md +1 -0
- polyptich-0.0.7/dist/eyck-0.0.2-py3-none-any.whl +0 -0
- polyptich-0.0.7/dist/eyck-0.0.2.dev1+g32d3f6b-py3-none-any.whl +0 -0
- polyptich-0.0.7/dist/eyck-0.0.2.dev1+g32d3f6b.tar.gz +0 -0
- polyptich-0.0.7/dist/eyck-0.0.2.tar.gz +0 -0
- polyptich-0.0.7/dist/genomeplot-0.0.1-py3-none-any.whl +0 -0
- polyptich-0.0.7/dist/genomeplot-0.0.1.tar.gz +0 -0
- polyptich-0.0.7/dist.sh +23 -0
- polyptich-0.0.7/pyproject.toml +61 -0
- polyptich-0.0.7/setup.cfg +4 -0
- polyptich-0.0.7/src/polyptich/__init__.py +4 -0
- polyptich-0.0.7/src/polyptich/grid/__init__.py +2 -0
- polyptich-0.0.7/src/polyptich/grid/broken.py +159 -0
- polyptich-0.0.7/src/polyptich/grid/grid.py +631 -0
- polyptich-0.0.7/src/polyptich/utils.py +8 -0
- polyptich-0.0.7/src/polyptich.egg-info/PKG-INFO +21 -0
- polyptich-0.0.7/src/polyptich.egg-info/SOURCES.txt +20 -0
- polyptich-0.0.7/src/polyptich.egg-info/dependency_links.txt +1 -0
- polyptich-0.0.7/src/polyptich.egg-info/requires.txt +9 -0
- polyptich-0.0.7/src/polyptich.egg-info/top_level.txt +1 -0
polyptich-0.0.7/PKG-INFO
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: polyptich
|
|
3
|
+
Version: 0.0.7
|
|
4
|
+
Summary: Extra visualization functions
|
|
5
|
+
Author-email: Wouter Saelens <wouter.saelens@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/probabilistic-cell/polyptich
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/probabilistic-cell/polyptich/issues
|
|
9
|
+
Keywords: visualization
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: matplotlib
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest; extra == "dev"
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest; extra == "test"
|
|
19
|
+
Requires-Dist: ruff; extra == "test"
|
|
20
|
+
|
|
21
|
+
# polyptich
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# polyptich
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
polyptich-0.0.7/dist.sh
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
python -m setuptools_git_versioning
|
|
2
|
+
|
|
3
|
+
version="0.0.7"
|
|
4
|
+
|
|
5
|
+
git add .
|
|
6
|
+
git commit -m "version v${version}"
|
|
7
|
+
|
|
8
|
+
git tag -a v${version} -m "v${version}"
|
|
9
|
+
|
|
10
|
+
python -m build
|
|
11
|
+
|
|
12
|
+
# optional: upload test
|
|
13
|
+
# twine upload --repository testpypi dist/polyptich-${version}.tar.gz --verbose
|
|
14
|
+
|
|
15
|
+
git push --tags
|
|
16
|
+
|
|
17
|
+
# conda install gh --channel conda-forge
|
|
18
|
+
gh release create v${version} -t "v${version}" -n "v${version}" dist/polyptich-${version}.tar.gz
|
|
19
|
+
|
|
20
|
+
# pip install twine
|
|
21
|
+
twine upload dist/polyptich-${version}.tar.gz --verbose
|
|
22
|
+
|
|
23
|
+
python -m build --wheel
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=41", "wheel", "setuptools_scm[toml]>=6.2", "numpy"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.setuptools-git-versioning]
|
|
6
|
+
enabled = true
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "polyptich"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "Wouter Saelens", email = "wouter.saelens@gmail.com"},
|
|
12
|
+
]
|
|
13
|
+
description = "Extra visualization functions"
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
keywords = ["visualization"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"matplotlib",
|
|
21
|
+
"numpy",
|
|
22
|
+
]
|
|
23
|
+
dynamic = ["version", "readme"]
|
|
24
|
+
license = {text = "MIT"}
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
"Homepage" = "https://github.com/probabilistic-cell/polyptich"
|
|
28
|
+
"Bug Tracker" = "https://github.com/probabilistic-cell/polyptich/issues"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.dynamic]
|
|
31
|
+
readme = {file = "README.md", content-type = "text/markdown"}
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest",
|
|
36
|
+
]
|
|
37
|
+
test = [
|
|
38
|
+
"pytest",
|
|
39
|
+
"ruff",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[tool.setuptools_scm]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
filterwarnings = [
|
|
46
|
+
"ignore",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[tool.pylint.'MESSAGES CONTROL']
|
|
50
|
+
max-line-length = 120
|
|
51
|
+
disable = [
|
|
52
|
+
"too-many-arguments",
|
|
53
|
+
"not-callable",
|
|
54
|
+
"redefined-builtin",
|
|
55
|
+
"redefined-outer-name",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
line-length = 100
|
|
60
|
+
include = ['src/**/*.py']
|
|
61
|
+
exclude = ['scripts/*']
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from polyptich.grid.grid import Grid, Panel
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import dataclasses
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclasses.dataclass
|
|
8
|
+
class Breaking:
|
|
9
|
+
regions: pd.DataFrame
|
|
10
|
+
gap: int = 0.05
|
|
11
|
+
resolution: int = 2500
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def width(self):
|
|
15
|
+
return (self.regions["length"] / self.resolution).sum() + self.gap * (len(self.regions) - 1)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Broken(Grid):
|
|
19
|
+
"""
|
|
20
|
+
A grid build from distinct regions that are using the same coordinate space
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, breaking, height=0.5, margin_height=0.0, *args, **kwargs):
|
|
24
|
+
super().__init__(padding_width=breaking.gap, margin_height=margin_height, *args, **kwargs)
|
|
25
|
+
|
|
26
|
+
regions = breaking.regions
|
|
27
|
+
|
|
28
|
+
regions["width"] = regions["end"] - regions["start"]
|
|
29
|
+
regions["ix"] = np.arange(len(regions))
|
|
30
|
+
|
|
31
|
+
for i, (region, region_info) in enumerate(regions.iterrows()):
|
|
32
|
+
if "resolution" in region_info.index:
|
|
33
|
+
resolution = region_info["resolution"]
|
|
34
|
+
else:
|
|
35
|
+
resolution = breaking.resolution
|
|
36
|
+
subpanel_width = region_info["width"] / resolution
|
|
37
|
+
panel, ax = self.add_right(
|
|
38
|
+
Panel((subpanel_width, height + 1e-4)),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
ax.set_xlim(region_info["start"], region_info["end"])
|
|
42
|
+
ax.set_xticks([])
|
|
43
|
+
ax.set_ylim(0, 1)
|
|
44
|
+
if i != 0:
|
|
45
|
+
ax.set_yticks([])
|
|
46
|
+
if region_info["ix"] != 0:
|
|
47
|
+
ax.spines.left.set_visible(False)
|
|
48
|
+
if region_info["ix"] != len(regions) - 1:
|
|
49
|
+
ax.spines.right.set_visible(False)
|
|
50
|
+
ax.spines.top.set_visible(False)
|
|
51
|
+
ax.set_facecolor("none")
|
|
52
|
+
|
|
53
|
+
# ax.plot([0, 0], [0, 1], transform=ax.transAxes, color="k", lw=1, clip_on=False)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BrokenGrid(Grid):
|
|
57
|
+
"""
|
|
58
|
+
A grid build from distinct regions that are using the same coordinate space
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self, breaking, height=0.5, padding_height=0.05, margin_height=0.0, *args, **kwargs
|
|
63
|
+
):
|
|
64
|
+
super().__init__(padding_width=breaking.gap, margin_height=margin_height, *args, **kwargs)
|
|
65
|
+
|
|
66
|
+
regions = breaking.regions
|
|
67
|
+
|
|
68
|
+
regions["width"] = regions["end"] - regions["start"]
|
|
69
|
+
regions["ix"] = np.arange(len(regions))
|
|
70
|
+
|
|
71
|
+
regions["panel_width"] = regions["width"] / breaking.resolution
|
|
72
|
+
|
|
73
|
+
self.panel_widths = regions["panel_width"].values
|
|
74
|
+
|
|
75
|
+
for i, (region, region_info) in enumerate(regions.iterrows()):
|
|
76
|
+
_ = self.add_right(
|
|
77
|
+
Grid(padding_height=padding_height, margin_height=0.0),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def add_slanted_x(ax1, ax2, size=4, **kwargs):
|
|
82
|
+
d = 1.0 # proportion of vertical to horizontal extent of the slanted line
|
|
83
|
+
kwargs = dict(
|
|
84
|
+
marker=[(-1, -d), (1, d)],
|
|
85
|
+
markersize=size,
|
|
86
|
+
linestyle="none",
|
|
87
|
+
mew=1,
|
|
88
|
+
clip_on=False,
|
|
89
|
+
**{"color": "k", "mec": "k", **kwargs},
|
|
90
|
+
)
|
|
91
|
+
ax1.plot([1, 1], [0, 1], transform=ax1.transAxes, **kwargs)
|
|
92
|
+
ax2.plot([0, 0], [0, 1], transform=ax2.transAxes, **kwargs)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TransformBroken:
|
|
96
|
+
def __init__(self, breaking):
|
|
97
|
+
"""
|
|
98
|
+
Transforms from data coordinates to (broken) data coordinates
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
breaking : Breaking
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
regions = breaking.regions
|
|
106
|
+
|
|
107
|
+
regions["width"] = regions["end"] - regions["start"]
|
|
108
|
+
regions["ix"] = np.arange(len(regions))
|
|
109
|
+
|
|
110
|
+
regions["cumstart"] = (np.pad(np.cumsum(regions["width"])[:-1], (1, 0))) + regions[
|
|
111
|
+
"ix"
|
|
112
|
+
] * breaking.gap * breaking.resolution
|
|
113
|
+
regions["cumend"] = (
|
|
114
|
+
np.cumsum(regions["width"]) + regions["ix"] * breaking.gap / breaking.resolution
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
self.regions = regions
|
|
118
|
+
self.resolution = breaking.resolution
|
|
119
|
+
self.gap = breaking.gap
|
|
120
|
+
|
|
121
|
+
def __call__(self, x):
|
|
122
|
+
"""
|
|
123
|
+
Transform from data coordinates to (broken) data coordinates
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
x : float
|
|
128
|
+
Position in data coordinates
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
float
|
|
133
|
+
Position in (broken) data coordinates
|
|
134
|
+
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
assert isinstance(x, (int, float, np.ndarray, np.float64, np.int64))
|
|
138
|
+
|
|
139
|
+
if isinstance(x, (int, float, np.float64, np.int64)):
|
|
140
|
+
x = np.array([x])
|
|
141
|
+
|
|
142
|
+
match = (x[:, None] >= self.regions["start"].values) & (
|
|
143
|
+
x[:, None] <= self.regions["end"].values
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
argmax = np.argmax(
|
|
147
|
+
match,
|
|
148
|
+
axis=1,
|
|
149
|
+
)
|
|
150
|
+
allzero = (match == False).all(axis=1)
|
|
151
|
+
|
|
152
|
+
# argmax[allzero] = np.nan
|
|
153
|
+
|
|
154
|
+
y = self.regions.iloc[argmax]["cumstart"].values + (
|
|
155
|
+
x - self.regions.iloc[argmax]["start"].values
|
|
156
|
+
)
|
|
157
|
+
y[allzero] = np.nan
|
|
158
|
+
|
|
159
|
+
return y
|
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
import matplotlib as mpl
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
import numpy as np
|
|
4
|
+
from typing import List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
active_fig = None
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Element:
|
|
10
|
+
"""
|
|
11
|
+
A basic element in a figure with a (top-left) position and dimensions
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
pos = None
|
|
15
|
+
dim = None
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def width(self):
|
|
19
|
+
return self.dim[0]
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def height(self):
|
|
23
|
+
return self.dim[1]
|
|
24
|
+
|
|
25
|
+
def initialize(self, fig):
|
|
26
|
+
self.fig = fig
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
TITLE_HEIGHT = 0.3
|
|
30
|
+
AXIS_WIDTH = AXIS_HEIGHT = 0.0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Ax(Element):
|
|
34
|
+
"""
|
|
35
|
+
A panel with an axis
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
ax2 = None
|
|
43
|
+
insets = None
|
|
44
|
+
fig = None
|
|
45
|
+
|
|
46
|
+
def __init__(self, dim:tuple=None, pos:tuple=(0.0, 0.0), fig=None):
|
|
47
|
+
self.dim = dim
|
|
48
|
+
self.pos = pos
|
|
49
|
+
self.ax = mpl.figure.Axes.__new__(mpl.figure.Axes)
|
|
50
|
+
|
|
51
|
+
if fig is None:
|
|
52
|
+
global active_fig
|
|
53
|
+
if active_fig is not None:
|
|
54
|
+
fig = active_fig
|
|
55
|
+
else:
|
|
56
|
+
fig = plt.gcf()
|
|
57
|
+
|
|
58
|
+
self.fig = fig
|
|
59
|
+
self.ax.__init__(fig, [0, 0, 1, 1])
|
|
60
|
+
|
|
61
|
+
def initialize(self, fig):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def dim(self):
|
|
66
|
+
return self._dim
|
|
67
|
+
|
|
68
|
+
@dim.setter
|
|
69
|
+
def dim(self, value):
|
|
70
|
+
if len(value) != 2:
|
|
71
|
+
raise ValueError("dim must be a tuple of length 2")
|
|
72
|
+
if value[0] <= 0 or value[1] <= 0:
|
|
73
|
+
raise ValueError("dim must be positive")
|
|
74
|
+
self._dim = value
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def height(self):
|
|
78
|
+
h = self.dim[1]
|
|
79
|
+
|
|
80
|
+
# add some extra height if we have a title
|
|
81
|
+
if self.ax.get_title() != "":
|
|
82
|
+
h += TITLE_HEIGHT
|
|
83
|
+
if self.ax.axison:
|
|
84
|
+
h += AXIS_HEIGHT
|
|
85
|
+
return h
|
|
86
|
+
|
|
87
|
+
@height.setter
|
|
88
|
+
def height(self, value):
|
|
89
|
+
self.dim = (self.dim[0], value)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def width(self):
|
|
93
|
+
w = self.dim[0]
|
|
94
|
+
if self.ax.axison:
|
|
95
|
+
w += AXIS_WIDTH
|
|
96
|
+
return w
|
|
97
|
+
|
|
98
|
+
@width.setter
|
|
99
|
+
def width(self, value):
|
|
100
|
+
self.dim = (value, self.dim[1])
|
|
101
|
+
|
|
102
|
+
def align(self):
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
def position(self, fig, pos=(0, 0)):
|
|
106
|
+
fig_width, fig_height = fig.get_size_inches()
|
|
107
|
+
width, height = self.dim
|
|
108
|
+
x, y = self.pos[0] + pos[0], self.pos[1] + pos[1]
|
|
109
|
+
|
|
110
|
+
axes = [self.ax]
|
|
111
|
+
if self.ax2 is not None:
|
|
112
|
+
axes.append(self.ax2)
|
|
113
|
+
|
|
114
|
+
for ax in axes:
|
|
115
|
+
ax.set_position(
|
|
116
|
+
[
|
|
117
|
+
x / fig_width,
|
|
118
|
+
(fig_height - y - height) / fig_height,
|
|
119
|
+
width / fig_width,
|
|
120
|
+
height / fig_height,
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
fig.add_axes(ax)
|
|
125
|
+
|
|
126
|
+
for inset, inset_position, inset_offset, inset_anchor in self.insets or []:
|
|
127
|
+
inset.position(
|
|
128
|
+
fig,
|
|
129
|
+
pos=(
|
|
130
|
+
x
|
|
131
|
+
+ (width - inset.dim[0]) * inset_anchor[0]
|
|
132
|
+
+ (width) * inset_position[0]
|
|
133
|
+
+ inset_offset[0],
|
|
134
|
+
y
|
|
135
|
+
+ (height - inset.dim[1]) * inset_anchor[1]
|
|
136
|
+
+ (height) * inset_position[1]
|
|
137
|
+
+ inset_offset[1],
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def add_twinx(self):
|
|
142
|
+
global active_fig
|
|
143
|
+
self.ax2 = mpl.figure.Axes(active_fig, [0, 0, 1, 1])
|
|
144
|
+
self.ax2.xaxis.set_visible(False)
|
|
145
|
+
self.ax2.patch.set_visible(False)
|
|
146
|
+
self.ax2.yaxis.tick_right()
|
|
147
|
+
self.ax2.yaxis.set_label_position("right")
|
|
148
|
+
self.ax2.yaxis.set_offset_position("right")
|
|
149
|
+
self.ax.yaxis.tick_left()
|
|
150
|
+
return self.ax2
|
|
151
|
+
|
|
152
|
+
def add_inset(self, inset, pos=(0, 0), offset=(0, 0), anchor=(0, 0)):
|
|
153
|
+
if self.insets is None:
|
|
154
|
+
self.insets = []
|
|
155
|
+
self.insets.append([inset, pos, offset, anchor])
|
|
156
|
+
return inset
|
|
157
|
+
|
|
158
|
+
def __iter__(self):
|
|
159
|
+
yield self
|
|
160
|
+
yield self.ax
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class Panel(Ax):
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class Title(Panel):
|
|
168
|
+
def __init__(self, label, dim=None):
|
|
169
|
+
if dim is None:
|
|
170
|
+
dim = (1, TITLE_HEIGHT)
|
|
171
|
+
super().__init__(dim=dim)
|
|
172
|
+
self.label = label
|
|
173
|
+
self.ax.set_axis_off()
|
|
174
|
+
self.ax.text(0.5, 0.5, label, ha="center", va="center", size="large")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class Wrap(Element):
|
|
178
|
+
"""
|
|
179
|
+
Grid-like layout with a fixed number of columns that will automatically wrap panels in the next row
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
The number of columns in the grid. Defaults to 6.
|
|
184
|
+
padding_width : float, optional
|
|
185
|
+
The width padding between elements in the grid. Defaults to 0.5.
|
|
186
|
+
padding_height : float, optional
|
|
187
|
+
The height padding between elements in the grid. If not provided, it defaults to the value of padding_width. Defaults to None.
|
|
188
|
+
margin_height : float, optional
|
|
189
|
+
The height margin around the grid. Defaults to 0.5.
|
|
190
|
+
margin_width : float, optional
|
|
191
|
+
The width margin around the grid. Defaults to 0.5.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
title = None
|
|
195
|
+
fig = None
|
|
196
|
+
|
|
197
|
+
def __init__(
|
|
198
|
+
self,
|
|
199
|
+
ncol: int = 6,
|
|
200
|
+
padding_width: float = 0.5,
|
|
201
|
+
padding_height: Optional[float] = None,
|
|
202
|
+
margin_height: float = 0.5,
|
|
203
|
+
margin_width: float = 0.5,
|
|
204
|
+
):
|
|
205
|
+
self.ncol: int = ncol
|
|
206
|
+
self.padding_width: float = padding_width
|
|
207
|
+
self.padding_height: Optional[float] = (
|
|
208
|
+
padding_height if padding_height is not None else padding_width
|
|
209
|
+
)
|
|
210
|
+
self.margin_width: float = margin_width
|
|
211
|
+
self.margin_height: float = margin_height
|
|
212
|
+
self.elements: List[Element] = []
|
|
213
|
+
self.pos: Tuple[int, int] = (0, 0)
|
|
214
|
+
|
|
215
|
+
def add(self, element: Element):
|
|
216
|
+
"""
|
|
217
|
+
Add an element to the grid
|
|
218
|
+
"""
|
|
219
|
+
self.elements.append(element)
|
|
220
|
+
element.initialize(self.fig)
|
|
221
|
+
return element
|
|
222
|
+
|
|
223
|
+
def align(self):
|
|
224
|
+
width = 0
|
|
225
|
+
height = 0
|
|
226
|
+
nrow = 1
|
|
227
|
+
x = 0
|
|
228
|
+
y = 0
|
|
229
|
+
next_y = 0
|
|
230
|
+
|
|
231
|
+
if self.title is not None:
|
|
232
|
+
y += self.title.height
|
|
233
|
+
|
|
234
|
+
for i, el in enumerate(self.elements):
|
|
235
|
+
el.align()
|
|
236
|
+
|
|
237
|
+
el.pos = (x, y)
|
|
238
|
+
|
|
239
|
+
next_y = max(next_y, y + el.height + self.padding_height)
|
|
240
|
+
height = max(height, next_y)
|
|
241
|
+
|
|
242
|
+
width = max(width, x + el.width)
|
|
243
|
+
|
|
244
|
+
if (self.ncol > 1) and ((i == 0) or (((i + 1) % (self.ncol)) != 0)):
|
|
245
|
+
x += el.width + self.padding_width
|
|
246
|
+
else:
|
|
247
|
+
nrow += 1
|
|
248
|
+
x = 0
|
|
249
|
+
y = next_y
|
|
250
|
+
|
|
251
|
+
if self.title is not None:
|
|
252
|
+
self.title.dim = (width, self.title.dim[1])
|
|
253
|
+
|
|
254
|
+
self.dim = (width, height)
|
|
255
|
+
|
|
256
|
+
def set_title(self, label):
|
|
257
|
+
if self.title is not None:
|
|
258
|
+
try:
|
|
259
|
+
self.title.ax.remove()
|
|
260
|
+
except KeyError:
|
|
261
|
+
pass
|
|
262
|
+
except AttributeError:
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
self.title = Title(label)
|
|
266
|
+
|
|
267
|
+
def position(self, fig, pos=(0, 0)):
|
|
268
|
+
pos = self.pos[0] + pos[0], self.pos[1] + pos[1]
|
|
269
|
+
if self.title is not None:
|
|
270
|
+
self.title.position(fig, pos)
|
|
271
|
+
for el in self.elements:
|
|
272
|
+
el.position(fig, pos)
|
|
273
|
+
|
|
274
|
+
def __getitem__(self, key):
|
|
275
|
+
return list(self.elements)[key]
|
|
276
|
+
|
|
277
|
+
def get_bottom_left_corner(self):
|
|
278
|
+
nrow = (len(self.elements) - 1) // self.ncol
|
|
279
|
+
ix = (nrow) * self.ncol
|
|
280
|
+
return self.elements[ix]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class WrapAutobreak(Wrap):
|
|
284
|
+
"""
|
|
285
|
+
Wraps panels if the size exeeds a maximum width
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
title = None
|
|
289
|
+
|
|
290
|
+
def __init__(
|
|
291
|
+
self,
|
|
292
|
+
max_width,
|
|
293
|
+
max_n_row=-1,
|
|
294
|
+
padding_width=0.5,
|
|
295
|
+
padding_height=None,
|
|
296
|
+
margin_height=0.5,
|
|
297
|
+
margin_width=0.5,
|
|
298
|
+
):
|
|
299
|
+
self.max_width = max_width
|
|
300
|
+
self.max_n_row = max_n_row
|
|
301
|
+
super().__init__(
|
|
302
|
+
ncol=1,
|
|
303
|
+
padding_width=padding_width,
|
|
304
|
+
padding_height=padding_height,
|
|
305
|
+
margin_height=margin_height,
|
|
306
|
+
margin_width=margin_width,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def align(self):
|
|
310
|
+
width = 0
|
|
311
|
+
height = 0
|
|
312
|
+
self.nrow = 1
|
|
313
|
+
x = 0
|
|
314
|
+
y = 0
|
|
315
|
+
next_y = 0
|
|
316
|
+
|
|
317
|
+
if self.title is not None:
|
|
318
|
+
y += self.title.height
|
|
319
|
+
for i, el in enumerate(self.elements):
|
|
320
|
+
el.align()
|
|
321
|
+
|
|
322
|
+
el.pos = (x, y)
|
|
323
|
+
|
|
324
|
+
next_y = max(next_y, y + el.height + self.padding_height)
|
|
325
|
+
height = max(height, next_y)
|
|
326
|
+
|
|
327
|
+
width = max(width, x + el.width)
|
|
328
|
+
|
|
329
|
+
x += el.width + self.padding_width
|
|
330
|
+
|
|
331
|
+
if x > self.max_width:
|
|
332
|
+
self.nrow += 1
|
|
333
|
+
x = 0
|
|
334
|
+
y = next_y
|
|
335
|
+
|
|
336
|
+
if self.title is not None:
|
|
337
|
+
self.title.dim = (width, self.title.dim[1])
|
|
338
|
+
|
|
339
|
+
self.dim = (width, height)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class Grid(Element):
|
|
343
|
+
"""
|
|
344
|
+
Grid layout with a fixed number of columns and rows
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
title = None
|
|
348
|
+
|
|
349
|
+
def __init__(
|
|
350
|
+
self,
|
|
351
|
+
nrow: int = 1,
|
|
352
|
+
ncol: int = 1,
|
|
353
|
+
padding_width: float = 0.5,
|
|
354
|
+
padding_height: Optional[float] = None,
|
|
355
|
+
margin_height: float = 0.5,
|
|
356
|
+
margin_width: float = 0.5,
|
|
357
|
+
) -> None:
|
|
358
|
+
self.padding_width = padding_width
|
|
359
|
+
self.padding_height = padding_height if padding_height is not None else padding_width
|
|
360
|
+
self.margin_width = margin_width
|
|
361
|
+
self.margin_height = margin_height
|
|
362
|
+
self.elements: List[List[Optional[Element]]] = [
|
|
363
|
+
[None for _ in range(ncol)] for _ in range(nrow)
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
self.pos: Tuple[int, int] = (0, 0)
|
|
367
|
+
|
|
368
|
+
self.nrow: int = nrow
|
|
369
|
+
self.ncol: int = ncol
|
|
370
|
+
|
|
371
|
+
self.paddings_height: List[Optional[float]] = [None] * (nrow)
|
|
372
|
+
self.paddings_width: List[Optional[float]] = [None] * (ncol)
|
|
373
|
+
|
|
374
|
+
def align(self):
|
|
375
|
+
width = 0
|
|
376
|
+
height = 0
|
|
377
|
+
x = 0
|
|
378
|
+
y = 0
|
|
379
|
+
next_y = 0
|
|
380
|
+
|
|
381
|
+
if self.title is not None:
|
|
382
|
+
y += self.title.height
|
|
383
|
+
|
|
384
|
+
widths = [0] * self.ncol
|
|
385
|
+
heights = [0] * self.nrow
|
|
386
|
+
|
|
387
|
+
assert len(self.paddings_height) == self.nrow, (
|
|
388
|
+
len(self.paddings_height),
|
|
389
|
+
self.nrow,
|
|
390
|
+
)
|
|
391
|
+
assert len(self.paddings_width) == self.ncol, (
|
|
392
|
+
len(self.paddings_width),
|
|
393
|
+
self.ncol,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
for row, row_elements in enumerate(self.elements):
|
|
397
|
+
for col, el in enumerate(row_elements):
|
|
398
|
+
if el is not None:
|
|
399
|
+
el.align()
|
|
400
|
+
if el.width > widths[col]:
|
|
401
|
+
widths[col] = el.width
|
|
402
|
+
if el.height > heights[row]:
|
|
403
|
+
heights[row] = el.height
|
|
404
|
+
|
|
405
|
+
for row, (row_elements, el_height) in enumerate(zip(self.elements, heights)):
|
|
406
|
+
padding_height = self.paddings_height[min(row + 1, self.nrow - 1)]
|
|
407
|
+
if padding_height is None:
|
|
408
|
+
padding_height = self.padding_height
|
|
409
|
+
|
|
410
|
+
x = 0
|
|
411
|
+
for col, (el, el_width) in enumerate(zip(row_elements, widths)):
|
|
412
|
+
if el is not None:
|
|
413
|
+
el.pos = (x, y)
|
|
414
|
+
|
|
415
|
+
next_y = max(next_y, y + el.height + padding_height)
|
|
416
|
+
height = max(height, next_y)
|
|
417
|
+
|
|
418
|
+
width = max(width, x + el.width)
|
|
419
|
+
|
|
420
|
+
padding_width = self.paddings_width[min(col + 1, self.ncol - 1)]
|
|
421
|
+
if padding_width is None:
|
|
422
|
+
padding_width = self.padding_width
|
|
423
|
+
|
|
424
|
+
x += el_width + padding_width
|
|
425
|
+
y += el_height + padding_height
|
|
426
|
+
|
|
427
|
+
if self.title is not None:
|
|
428
|
+
self.title.dim = (width, self.title.dim[1])
|
|
429
|
+
|
|
430
|
+
self.dim = (width, height)
|
|
431
|
+
|
|
432
|
+
def set_title(self, label):
|
|
433
|
+
self.title = Title(label)
|
|
434
|
+
|
|
435
|
+
def position(self, fig, pos=(0, 0)):
|
|
436
|
+
pos = self.pos[0] + pos[0], self.pos[1] + pos[1]
|
|
437
|
+
if self.title is not None:
|
|
438
|
+
self.title.position(fig, pos)
|
|
439
|
+
|
|
440
|
+
for row_elements in self.elements:
|
|
441
|
+
for el in row_elements:
|
|
442
|
+
if el is not None:
|
|
443
|
+
el.position(fig, pos)
|
|
444
|
+
|
|
445
|
+
def __getitem__(self, index):
|
|
446
|
+
if not isinstance(index, tuple):
|
|
447
|
+
raise TypeError("index must be a tuple, not " + str(index))
|
|
448
|
+
return self.elements[index[0]][index[1]]
|
|
449
|
+
|
|
450
|
+
def __setitem__(self, index, v):
|
|
451
|
+
row = index[0]
|
|
452
|
+
col = index[1]
|
|
453
|
+
|
|
454
|
+
if not isinstance(row, int) or not isinstance(col, int):
|
|
455
|
+
raise TypeError("row and col must be integers")
|
|
456
|
+
|
|
457
|
+
if row >= (self.nrow):
|
|
458
|
+
# add new row(s)
|
|
459
|
+
for i in range(self.nrow, row + 1):
|
|
460
|
+
self.elements.append([None for _ in range(self.ncol)])
|
|
461
|
+
self.nrow = row + 1
|
|
462
|
+
self.paddings_height.append(None)
|
|
463
|
+
|
|
464
|
+
if col >= (self.ncol):
|
|
465
|
+
# add new col(s)
|
|
466
|
+
for i in range(self.ncol, col + 1):
|
|
467
|
+
for row_ in self.elements:
|
|
468
|
+
row_.append(None)
|
|
469
|
+
self.ncol = col + 1
|
|
470
|
+
self.paddings_width.append(None)
|
|
471
|
+
|
|
472
|
+
self.elements[row][col] = v
|
|
473
|
+
|
|
474
|
+
def add(self, el, row=0, column=0, padding_height=None, padding_width=None):
|
|
475
|
+
self[row, column] = el
|
|
476
|
+
if padding_height is not None:
|
|
477
|
+
self.paddings_height[row] = padding_height
|
|
478
|
+
if padding_width is not None:
|
|
479
|
+
self.paddings_width[column] = padding_width
|
|
480
|
+
return el
|
|
481
|
+
|
|
482
|
+
def add_under(self, el, column=0, padding=None):
|
|
483
|
+
if (self.nrow == 1) and self[0, 0] is None:
|
|
484
|
+
row = 0
|
|
485
|
+
else:
|
|
486
|
+
row = self.nrow
|
|
487
|
+
|
|
488
|
+
# get column index if column is a panel
|
|
489
|
+
if "grid.Element" in column.__class__.__mro__.__repr__():
|
|
490
|
+
try:
|
|
491
|
+
print(row)
|
|
492
|
+
column = np.array(self.elements).flatten().tolist().index(column) % self.ncol
|
|
493
|
+
except ValueError as e:
|
|
494
|
+
raise ValueError("The panel specified as column was not found in the grid") from e
|
|
495
|
+
if not isinstance(column, int):
|
|
496
|
+
raise TypeError("column must be an integer, not " + str(column))
|
|
497
|
+
self[row, column] = el
|
|
498
|
+
if padding is not None:
|
|
499
|
+
self.paddings_height[row] = padding
|
|
500
|
+
return el
|
|
501
|
+
|
|
502
|
+
def add_right(self, el, row=0, padding=None):
|
|
503
|
+
if (self.ncol == 1) and (self[0, 0] is None):
|
|
504
|
+
column = 0
|
|
505
|
+
else:
|
|
506
|
+
if row < self.nrow:
|
|
507
|
+
# get first empty element
|
|
508
|
+
for i, el_ in enumerate(self.elements[row]):
|
|
509
|
+
if el_ is None:
|
|
510
|
+
column = i
|
|
511
|
+
break
|
|
512
|
+
else:
|
|
513
|
+
column = self.ncol
|
|
514
|
+
else:
|
|
515
|
+
# if the row does not exist => col is just 0
|
|
516
|
+
column = 0
|
|
517
|
+
|
|
518
|
+
# get column index if row is a panel
|
|
519
|
+
if "grid.Element" in row.__class__.__mro__.__repr__():
|
|
520
|
+
try:
|
|
521
|
+
row = np.array(self.elements).flatten().tolist().index(row) // self.ncol
|
|
522
|
+
except ValueError as e:
|
|
523
|
+
raise ValueError("The panel specified as row was not found in the grid") from e
|
|
524
|
+
|
|
525
|
+
self[row, column] = el
|
|
526
|
+
if padding is not None:
|
|
527
|
+
self.paddings_width[column] = padding
|
|
528
|
+
return el
|
|
529
|
+
|
|
530
|
+
def get_panel_position(self, panel):
|
|
531
|
+
for row, row_elements in enumerate(self.elements):
|
|
532
|
+
for col, el in enumerate(row_elements):
|
|
533
|
+
if el is panel:
|
|
534
|
+
return row, col
|
|
535
|
+
|
|
536
|
+
def __iter__(self):
|
|
537
|
+
for row in self.elements:
|
|
538
|
+
for el in row:
|
|
539
|
+
if el is not None:
|
|
540
|
+
yield el
|
|
541
|
+
|
|
542
|
+
def get_bottom_left_corner(self):
|
|
543
|
+
return self.elements[self.nrow - 1][0]
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
class _Figure(mpl.figure.Figure):
|
|
547
|
+
"""
|
|
548
|
+
Figure but with panel support
|
|
549
|
+
"""
|
|
550
|
+
|
|
551
|
+
main: Panel
|
|
552
|
+
|
|
553
|
+
def __init__(self, main: Panel, *args, **kwargs):
|
|
554
|
+
self.main = main
|
|
555
|
+
global active_fig
|
|
556
|
+
active_fig = self
|
|
557
|
+
self.plot_hooks = []
|
|
558
|
+
super().__init__(*args, **kwargs)
|
|
559
|
+
main.initialize(self)
|
|
560
|
+
|
|
561
|
+
def plot(self):
|
|
562
|
+
"""
|
|
563
|
+
Align and position all elements in the figure
|
|
564
|
+
"""
|
|
565
|
+
|
|
566
|
+
self.main.align()
|
|
567
|
+
self.set_size_inches(*self.main.dim)
|
|
568
|
+
self.main.position(self)
|
|
569
|
+
for hook in self.plot_hooks:
|
|
570
|
+
hook()
|
|
571
|
+
return self
|
|
572
|
+
|
|
573
|
+
def set_tight_bounds(self):
|
|
574
|
+
"""
|
|
575
|
+
Sets the bounds of the figure so that all elements are visible
|
|
576
|
+
"""
|
|
577
|
+
new_bounds = self.get_tightbbox().extents
|
|
578
|
+
current_size = self.get_size_inches()
|
|
579
|
+
new_bounds[2] - new_bounds[0], new_bounds[3] - new_bounds[1]
|
|
580
|
+
|
|
581
|
+
self.set_figwidth(new_bounds[2] - new_bounds[0])
|
|
582
|
+
self.set_figheight(new_bounds[3] - new_bounds[1])
|
|
583
|
+
|
|
584
|
+
for ax in self.axes:
|
|
585
|
+
new_bbox = ax.get_position()
|
|
586
|
+
current_axis_bounds = ax.get_position().extents
|
|
587
|
+
new_bbox = mpl.figure.Bbox(
|
|
588
|
+
np.array(
|
|
589
|
+
[
|
|
590
|
+
(current_axis_bounds[0] - (new_bounds[0] / current_size[0]))
|
|
591
|
+
/ ((new_bounds[2] - new_bounds[0]) / current_size[0]),
|
|
592
|
+
(current_axis_bounds[1] - (new_bounds[1] / current_size[1]))
|
|
593
|
+
/ ((new_bounds[3] - new_bounds[1]) / current_size[1]),
|
|
594
|
+
(current_axis_bounds[2] - (new_bounds[0] / current_size[0]))
|
|
595
|
+
/ ((new_bounds[2] - new_bounds[0]) / current_size[0]),
|
|
596
|
+
(current_axis_bounds[3] - (new_bounds[1] / current_size[1]))
|
|
597
|
+
/ ((new_bounds[3] - new_bounds[1]) / current_size[1]),
|
|
598
|
+
]
|
|
599
|
+
).reshape((2, 2))
|
|
600
|
+
)
|
|
601
|
+
ax.set_position(new_bbox)
|
|
602
|
+
|
|
603
|
+
def savefig(self, *args, dpi=300, bbox_inches="tight", display=True, **kwargs):
|
|
604
|
+
self.plot()
|
|
605
|
+
|
|
606
|
+
plt.close()
|
|
607
|
+
|
|
608
|
+
super().savefig(*args, dpi=dpi, bbox_inches=bbox_inches, **kwargs)
|
|
609
|
+
|
|
610
|
+
import IPython
|
|
611
|
+
|
|
612
|
+
if IPython.get_ipython() is not None and display and not str(args[0]).endswith(".pdf"):
|
|
613
|
+
IPython.display.display(IPython.display.Image(args[0], retina=True))
|
|
614
|
+
|
|
615
|
+
def display(self):
|
|
616
|
+
import tempfile
|
|
617
|
+
|
|
618
|
+
file = tempfile.NamedTemporaryFile(suffix=".png")
|
|
619
|
+
self.savefig(file.name, display=True)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def Figure(main: Element, *args, **kwargs):
|
|
623
|
+
"""
|
|
624
|
+
Create a figure with panel support
|
|
625
|
+
|
|
626
|
+
Parameters
|
|
627
|
+
----------
|
|
628
|
+
main : Element
|
|
629
|
+
The main panel of the figure. All other panels are a child of this panel
|
|
630
|
+
"""
|
|
631
|
+
return plt.figure(*args, main=main, **kwargs, FigureClass=_Figure)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def case_when(default="other", **kwargs):
|
|
5
|
+
y = np.zeros(len(kwargs[list(kwargs.keys())[0]]), dtype=int) + len(kwargs)
|
|
6
|
+
for i, (key, value) in enumerate({k: kwargs[k] for k in list(kwargs.keys())[::-1]}.items()):
|
|
7
|
+
y[value] = i
|
|
8
|
+
return np.array([*kwargs.keys(), default])[y]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: polyptich
|
|
3
|
+
Version: 0.0.7
|
|
4
|
+
Summary: Extra visualization functions
|
|
5
|
+
Author-email: Wouter Saelens <wouter.saelens@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/probabilistic-cell/polyptich
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/probabilistic-cell/polyptich/issues
|
|
9
|
+
Keywords: visualization
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: matplotlib
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest; extra == "dev"
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest; extra == "test"
|
|
19
|
+
Requires-Dist: ruff; extra == "test"
|
|
20
|
+
|
|
21
|
+
# polyptich
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.gitignore
|
|
2
|
+
README.md
|
|
3
|
+
dist.sh
|
|
4
|
+
pyproject.toml
|
|
5
|
+
dist/eyck-0.0.2-py3-none-any.whl
|
|
6
|
+
dist/eyck-0.0.2.dev1+g32d3f6b-py3-none-any.whl
|
|
7
|
+
dist/eyck-0.0.2.dev1+g32d3f6b.tar.gz
|
|
8
|
+
dist/eyck-0.0.2.tar.gz
|
|
9
|
+
dist/genomeplot-0.0.1-py3-none-any.whl
|
|
10
|
+
dist/genomeplot-0.0.1.tar.gz
|
|
11
|
+
src/polyptich/__init__.py
|
|
12
|
+
src/polyptich/utils.py
|
|
13
|
+
src/polyptich.egg-info/PKG-INFO
|
|
14
|
+
src/polyptich.egg-info/SOURCES.txt
|
|
15
|
+
src/polyptich.egg-info/dependency_links.txt
|
|
16
|
+
src/polyptich.egg-info/requires.txt
|
|
17
|
+
src/polyptich.egg-info/top_level.txt
|
|
18
|
+
src/polyptich/grid/__init__.py
|
|
19
|
+
src/polyptich/grid/broken.py
|
|
20
|
+
src/polyptich/grid/grid.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
polyptich
|