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.
@@ -0,0 +1,5 @@
1
+ __pycache__
2
+ *.egg-info
3
+ *.whl
4
+ *.tar.gz
5
+ build
@@ -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
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ from . import grid
2
+ from .utils import case_when
3
+
4
+ __all__ = ["grid", "case_when"]
@@ -0,0 +1,2 @@
1
+ from .grid import Grid, Figure, Panel, Wrap, Ax
2
+ from .broken import Broken, BrokenGrid, Breaking
@@ -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,9 @@
1
+ matplotlib
2
+ numpy
3
+
4
+ [dev]
5
+ pytest
6
+
7
+ [test]
8
+ pytest
9
+ ruff
@@ -0,0 +1 @@
1
+ polyptich