marsilea 0.3.1__py3-none-any.whl → 0.3.3__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.
- marsilea/__init__.py +3 -2
- marsilea/_api.py +3 -2
- marsilea/_deform.py +44 -38
- marsilea/base.py +702 -118
- marsilea/dataset.py +49 -23
- marsilea/dendrogram.py +119 -84
- marsilea/exceptions.py +5 -6
- marsilea/heatmap.py +85 -25
- marsilea/layers.py +77 -69
- marsilea/layout.py +132 -102
- marsilea/plotter/__init__.py +29 -0
- marsilea/plotter/_images.py +37 -7
- marsilea/plotter/_seaborn.py +35 -24
- marsilea/plotter/_utils.py +3 -2
- marsilea/plotter/arc.py +29 -19
- marsilea/plotter/area.py +80 -0
- marsilea/plotter/bar.py +119 -92
- marsilea/plotter/base.py +76 -49
- marsilea/plotter/bio.py +40 -30
- marsilea/plotter/mesh.py +184 -88
- marsilea/plotter/text.py +303 -194
- marsilea/upset.py +229 -151
- marsilea/utils.py +23 -10
- {marsilea-0.3.1.dist-info → marsilea-0.3.3.dist-info}/LICENSE +1 -1
- marsilea-0.3.3.dist-info/METADATA +74 -0
- marsilea-0.3.3.dist-info/RECORD +27 -0
- marsilea-0.3.1.dist-info/METADATA +0 -50
- marsilea-0.3.1.dist-info/RECORD +0 -26
- {marsilea-0.3.1.dist-info → marsilea-0.3.3.dist-info}/WHEEL +0 -0
marsilea/layout.py
CHANGED
|
@@ -24,7 +24,8 @@ from .utils import _check_side
|
|
|
24
24
|
# It's unlikely to return axes right after being added, the split operation
|
|
25
25
|
# is unknown, this will create more than one axes.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
|
|
28
|
+
def _split(chunk_ratios, spacing=0.05, group_ratios=None):
|
|
28
29
|
"""Return the relative anchor point, in ratio"""
|
|
29
30
|
|
|
30
31
|
ratios = np.asarray(chunk_ratios) / np.sum(chunk_ratios)
|
|
@@ -53,13 +54,13 @@ def _split(chunk_ratios, spacing=.05, group_ratios=None):
|
|
|
53
54
|
if g == 1:
|
|
54
55
|
gratio = ratios[start_ix]
|
|
55
56
|
else:
|
|
56
|
-
mr = ratios[start_ix:start_ix+g].sum()
|
|
57
|
-
ms = spacing[start_ix:start_ix+g-1].sum()
|
|
57
|
+
mr = ratios[start_ix : start_ix + g].sum()
|
|
58
|
+
ms = spacing[start_ix : start_ix + g - 1].sum()
|
|
58
59
|
gratio = mr + ms
|
|
59
60
|
|
|
60
61
|
if i < last_ix:
|
|
61
|
-
gspacing = spacing[start_ix+g-1]
|
|
62
|
-
start_anchor +=
|
|
62
|
+
gspacing = spacing[start_ix + g - 1]
|
|
63
|
+
start_anchor += gratio + gspacing
|
|
63
64
|
anchors.append(start_anchor)
|
|
64
65
|
else:
|
|
65
66
|
start_anchor += gratio
|
|
@@ -116,7 +117,7 @@ class BaseCell:
|
|
|
116
117
|
|
|
117
118
|
return cx, cy, cw, ch
|
|
118
119
|
|
|
119
|
-
def hsplit(self, chunk_ratios, spacing
|
|
120
|
+
def hsplit(self, chunk_ratios, spacing=0.05, group_ratios=None):
|
|
120
121
|
"""
|
|
121
122
|
Parameters
|
|
122
123
|
----------
|
|
@@ -128,12 +129,13 @@ class BaseCell:
|
|
|
128
129
|
Regroup the split chunks
|
|
129
130
|
|
|
130
131
|
"""
|
|
131
|
-
ratios, anchors = _split(
|
|
132
|
-
|
|
132
|
+
ratios, anchors = _split(
|
|
133
|
+
chunk_ratios[::-1], spacing=spacing, group_ratios=group_ratios
|
|
134
|
+
)
|
|
133
135
|
self.h_ratios = ratios
|
|
134
136
|
self.h_anchors = anchors
|
|
135
137
|
|
|
136
|
-
def vsplit(self, chunk_ratios, spacing
|
|
138
|
+
def vsplit(self, chunk_ratios, spacing=0.05, group_ratios=None):
|
|
137
139
|
"""
|
|
138
140
|
Parameters
|
|
139
141
|
----------
|
|
@@ -145,8 +147,9 @@ class BaseCell:
|
|
|
145
147
|
Regroup the split chunks
|
|
146
148
|
|
|
147
149
|
"""
|
|
148
|
-
ratios, anchors = _split(
|
|
149
|
-
|
|
150
|
+
ratios, anchors = _split(
|
|
151
|
+
chunk_ratios, spacing=spacing, group_ratios=group_ratios
|
|
152
|
+
)
|
|
150
153
|
self.v_ratios = ratios
|
|
151
154
|
self.v_anchors = anchors
|
|
152
155
|
|
|
@@ -204,7 +207,7 @@ class GridCell(BaseCell):
|
|
|
204
207
|
|
|
205
208
|
def get_cell_size(self):
|
|
206
209
|
"""Get width, height of a cell"""
|
|
207
|
-
if self.side in [
|
|
210
|
+
if self.side in ["top", "bottom"]:
|
|
208
211
|
return self.attach.width, self.size
|
|
209
212
|
else:
|
|
210
213
|
return self.size, self.attach.height
|
|
@@ -228,8 +231,10 @@ class _MarginMixin:
|
|
|
228
231
|
if len(margin) == 4:
|
|
229
232
|
self.margin = Margin(*margin)
|
|
230
233
|
else:
|
|
231
|
-
msg =
|
|
232
|
-
|
|
234
|
+
msg = (
|
|
235
|
+
"margin must be one number or a tuple with 4 numbers"
|
|
236
|
+
"(top, right, bottom, left)"
|
|
237
|
+
)
|
|
233
238
|
raise ValueError(msg)
|
|
234
239
|
|
|
235
240
|
def get_margin_w(self):
|
|
@@ -269,14 +274,14 @@ class CrossLayout(_MarginMixin):
|
|
|
269
274
|
|
|
270
275
|
"""
|
|
271
276
|
|
|
272
|
-
def __init__(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
277
|
+
def __init__(
|
|
278
|
+
self, name, width, height, init_main=True, projection=None, margin=0.2
|
|
279
|
+
):
|
|
276
280
|
self._legend_ax_name = None
|
|
277
|
-
self.main_cell = MainCell(
|
|
278
|
-
|
|
279
|
-
|
|
281
|
+
self.main_cell = MainCell(
|
|
282
|
+
name, width, height, is_canvas=init_main, projection=projection
|
|
283
|
+
)
|
|
284
|
+
self._side_cells = {"top": [], "bottom": [], "left": [], "right": []}
|
|
280
285
|
self.cells: Dict[str, BaseCell] = {name: self.main_cell}
|
|
281
286
|
self._pads = {}
|
|
282
287
|
|
|
@@ -294,7 +299,7 @@ class CrossLayout(_MarginMixin):
|
|
|
294
299
|
raise ValueError(f"Axes with name {name} not exist")
|
|
295
300
|
return cell
|
|
296
301
|
|
|
297
|
-
def add_ax(self, side, name, size, pad=0
|
|
302
|
+
def add_ax(self, side, name, size, pad=0.0, projection=None):
|
|
298
303
|
"""Add an axes to the layout
|
|
299
304
|
|
|
300
305
|
Parameters
|
|
@@ -313,11 +318,15 @@ class CrossLayout(_MarginMixin):
|
|
|
313
318
|
if self.cells.get(name) is not None:
|
|
314
319
|
raise DuplicateName(name)
|
|
315
320
|
|
|
316
|
-
new_cell = GridCell(
|
|
317
|
-
|
|
318
|
-
|
|
321
|
+
new_cell = GridCell(
|
|
322
|
+
name=name,
|
|
323
|
+
side=side,
|
|
324
|
+
size=size,
|
|
325
|
+
attach=self.main_cell,
|
|
326
|
+
projection=projection,
|
|
327
|
+
)
|
|
319
328
|
# Add pad before add canvas
|
|
320
|
-
if pad > 0
|
|
329
|
+
if pad > 0.0:
|
|
321
330
|
self.add_pad(side, pad)
|
|
322
331
|
self._side_cells[side].append(new_cell)
|
|
323
332
|
self.cells[name] = new_cell
|
|
@@ -335,8 +344,13 @@ class CrossLayout(_MarginMixin):
|
|
|
335
344
|
|
|
336
345
|
"""
|
|
337
346
|
_check_side(side)
|
|
338
|
-
new_pad = GridCell(
|
|
339
|
-
|
|
347
|
+
new_pad = GridCell(
|
|
348
|
+
name=uuid4().hex,
|
|
349
|
+
side=side,
|
|
350
|
+
size=size,
|
|
351
|
+
is_canvas=False,
|
|
352
|
+
attach=self.main_cell,
|
|
353
|
+
)
|
|
340
354
|
self._side_cells[side].append(new_pad)
|
|
341
355
|
|
|
342
356
|
def remove_ax(self, name):
|
|
@@ -356,7 +370,7 @@ class CrossLayout(_MarginMixin):
|
|
|
356
370
|
def get_main_ax(self):
|
|
357
371
|
return self.main_cell.ax
|
|
358
372
|
|
|
359
|
-
def add_legend_ax(self, side, size, pad=0.):
|
|
373
|
+
def add_legend_ax(self, side, size, pad=0.0):
|
|
360
374
|
"""Add a legend axes
|
|
361
375
|
|
|
362
376
|
Parameters
|
|
@@ -370,8 +384,7 @@ class CrossLayout(_MarginMixin):
|
|
|
370
384
|
|
|
371
385
|
"""
|
|
372
386
|
# TODO: Ensure the legend axes always add at last
|
|
373
|
-
self._legend_ax_name = "-".join(
|
|
374
|
-
[self.main_cell.name, "legend", uuid4().hex])
|
|
387
|
+
self._legend_ax_name = "-".join([self.main_cell.name, "legend", uuid4().hex])
|
|
375
388
|
self.add_ax(side, self._legend_ax_name, size, pad=pad)
|
|
376
389
|
|
|
377
390
|
def get_legend_ax(self):
|
|
@@ -385,19 +398,15 @@ class CrossLayout(_MarginMixin):
|
|
|
385
398
|
legend_cell = self._get_cell(self._legend_ax_name)
|
|
386
399
|
legend_cell.size = size
|
|
387
400
|
|
|
388
|
-
def vsplit(self, name, chunk_ratios,
|
|
389
|
-
spacing=.05, group_ratios=None):
|
|
401
|
+
def vsplit(self, name, chunk_ratios, spacing=0.05, group_ratios=None):
|
|
390
402
|
cell = self._get_cell(name)
|
|
391
403
|
cell.is_split = True
|
|
392
|
-
cell.vsplit(chunk_ratios, spacing=spacing,
|
|
393
|
-
group_ratios=group_ratios)
|
|
404
|
+
cell.vsplit(chunk_ratios, spacing=spacing, group_ratios=group_ratios)
|
|
394
405
|
|
|
395
|
-
def hsplit(self, name, chunk_ratios,
|
|
396
|
-
spacing=.05, group_ratios=None):
|
|
406
|
+
def hsplit(self, name, chunk_ratios, spacing=0.05, group_ratios=None):
|
|
397
407
|
cell = self._get_cell(name)
|
|
398
408
|
cell.is_split = True
|
|
399
|
-
cell.hsplit(chunk_ratios, spacing=spacing,
|
|
400
|
-
group_ratios=group_ratios)
|
|
409
|
+
cell.hsplit(chunk_ratios, spacing=spacing, group_ratios=group_ratios)
|
|
401
410
|
|
|
402
411
|
def is_split(self, name):
|
|
403
412
|
"""Query if a cell is split"""
|
|
@@ -408,14 +417,20 @@ class CrossLayout(_MarginMixin):
|
|
|
408
417
|
|
|
409
418
|
def get_bbox_width(self):
|
|
410
419
|
"""Get the bbox width in inches"""
|
|
411
|
-
box_w =
|
|
412
|
-
|
|
420
|
+
box_w = (
|
|
421
|
+
self.main_cell.width
|
|
422
|
+
+ self.get_side_size("left")
|
|
423
|
+
+ self.get_side_size("right")
|
|
424
|
+
)
|
|
413
425
|
return box_w
|
|
414
426
|
|
|
415
427
|
def get_bbox_height(self):
|
|
416
428
|
"""Get the bbox height in inches"""
|
|
417
|
-
box_h =
|
|
418
|
-
|
|
429
|
+
box_h = (
|
|
430
|
+
self.main_cell.height
|
|
431
|
+
+ self.get_side_size("top")
|
|
432
|
+
+ self.get_side_size("bottom")
|
|
433
|
+
)
|
|
419
434
|
return box_h
|
|
420
435
|
|
|
421
436
|
def get_bbox_size(self):
|
|
@@ -428,13 +443,12 @@ class CrossLayout(_MarginMixin):
|
|
|
428
443
|
if self.is_composite:
|
|
429
444
|
return self.get_bbox_size()
|
|
430
445
|
w, h = self.get_bbox_size()
|
|
431
|
-
return (w + self.get_margin_w(),
|
|
432
|
-
h + self.get_margin_h())
|
|
446
|
+
return (w + self.get_margin_w(), h + self.get_margin_h())
|
|
433
447
|
|
|
434
448
|
else:
|
|
435
449
|
ox, oy = self.anchor
|
|
436
|
-
fig_w = ox + self.get_main_width() + self.get_side_size(
|
|
437
|
-
fig_h = ox + self.get_main_height() + self.get_side_size(
|
|
450
|
+
fig_w = ox + self.get_main_width() + self.get_side_size("right")
|
|
451
|
+
fig_h = ox + self.get_main_height() + self.get_side_size("top")
|
|
438
452
|
if self.is_composite:
|
|
439
453
|
return fig_w, fig_h
|
|
440
454
|
return fig_w + self.get_margin_w(), fig_h + self.get_margin_h()
|
|
@@ -442,8 +456,8 @@ class CrossLayout(_MarginMixin):
|
|
|
442
456
|
def get_main_anchor(self):
|
|
443
457
|
"""Get the main anchor point"""
|
|
444
458
|
if self.anchor is None:
|
|
445
|
-
x = self.get_side_size(
|
|
446
|
-
y = self.get_side_size(
|
|
459
|
+
x = self.get_side_size("left")
|
|
460
|
+
y = self.get_side_size("bottom")
|
|
447
461
|
if self.is_composite:
|
|
448
462
|
return x, y
|
|
449
463
|
x += self.margin.bottom
|
|
@@ -534,10 +548,14 @@ class CrossLayout(_MarginMixin):
|
|
|
534
548
|
ax = figure.add_axes(ax_rect, projection=c.projection)
|
|
535
549
|
c.set_ax(ax)
|
|
536
550
|
if _debug:
|
|
537
|
-
_debug_ax(ax, side=c.side,
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
551
|
+
_debug_ax(ax, side=c.side, text=f"{c.name}{c.get_cell_size()}")
|
|
552
|
+
|
|
553
|
+
def freeze(
|
|
554
|
+
self,
|
|
555
|
+
figure=None,
|
|
556
|
+
scale=1,
|
|
557
|
+
_debug=False,
|
|
558
|
+
):
|
|
541
559
|
"""Freeze the current layout and draw on figure
|
|
542
560
|
|
|
543
561
|
Freeze is safe to be called multiple times, it will update
|
|
@@ -598,12 +616,12 @@ def _debug_ax(ax, side, text=None):
|
|
|
598
616
|
close_ticks(ax)
|
|
599
617
|
options = dict(transform=ax.transAxes, va="center", ha="center")
|
|
600
618
|
if side == "left":
|
|
601
|
-
options[
|
|
619
|
+
options["rotation"] = 90
|
|
602
620
|
elif side == "right":
|
|
603
|
-
options[
|
|
621
|
+
options["rotation"] = -90
|
|
604
622
|
|
|
605
623
|
if text is not None:
|
|
606
|
-
ax.text(.5, .5, text, **options)
|
|
624
|
+
ax.text(0.5, 0.5, text, **options)
|
|
607
625
|
|
|
608
626
|
|
|
609
627
|
def _remove_axes(ax: Axes | List[Axes]):
|
|
@@ -617,10 +635,16 @@ def _remove_axes(ax: Axes | List[Axes]):
|
|
|
617
635
|
|
|
618
636
|
|
|
619
637
|
def close_ticks(ax):
|
|
620
|
-
ax.tick_params(
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
638
|
+
ax.tick_params(
|
|
639
|
+
bottom=False,
|
|
640
|
+
top=False,
|
|
641
|
+
left=False,
|
|
642
|
+
right=False,
|
|
643
|
+
labelbottom=False,
|
|
644
|
+
labeltop=False,
|
|
645
|
+
labelleft=False,
|
|
646
|
+
labelright=False,
|
|
647
|
+
)
|
|
624
648
|
|
|
625
649
|
|
|
626
650
|
@dataclass
|
|
@@ -646,14 +670,19 @@ class CompositeCrossLayout(_MarginMixin):
|
|
|
646
670
|
The main cross layout
|
|
647
671
|
|
|
648
672
|
"""
|
|
673
|
+
|
|
649
674
|
figure = None
|
|
650
675
|
|
|
651
676
|
def __init__(self, main_layout, margin=0) -> None:
|
|
652
677
|
self.main_layout = self._reset_layout(main_layout)
|
|
653
678
|
self.main_cell_height = self.main_layout.get_main_height()
|
|
654
679
|
self.main_cell_width = self.main_layout.get_main_width()
|
|
655
|
-
self._side_layouts: Dict[str, List[CrossLayout]] =
|
|
656
|
-
|
|
680
|
+
self._side_layouts: Dict[str, List[CrossLayout]] = {
|
|
681
|
+
"top": [],
|
|
682
|
+
"bottom": [],
|
|
683
|
+
"right": [],
|
|
684
|
+
"left": [],
|
|
685
|
+
}
|
|
657
686
|
self._legend_axes = None
|
|
658
687
|
self.layouts = {self.main_layout.main_cell.name: self.main_layout}
|
|
659
688
|
self.set_margin(margin)
|
|
@@ -674,10 +703,9 @@ class CompositeCrossLayout(_MarginMixin):
|
|
|
674
703
|
width, height = other, self.main_cell_height
|
|
675
704
|
else:
|
|
676
705
|
width, height = self.main_cell_height, other
|
|
677
|
-
other = CrossLayout(
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
init_main=False)
|
|
706
|
+
other = CrossLayout(
|
|
707
|
+
name=uuid4().hex, width=width, height=height, init_main=False
|
|
708
|
+
)
|
|
681
709
|
other.is_composite = True
|
|
682
710
|
self._side_layouts[side].append(other)
|
|
683
711
|
elif isinstance(other, CrossLayout):
|
|
@@ -691,12 +719,12 @@ class CompositeCrossLayout(_MarginMixin):
|
|
|
691
719
|
raise TypeError(f"Cannot append object type of {type(other)}")
|
|
692
720
|
|
|
693
721
|
def __truediv__(self, other: CrossLayout):
|
|
694
|
-
self.append(
|
|
722
|
+
self.append("bottom", other)
|
|
695
723
|
|
|
696
724
|
def __add__(self, other: CrossLayout):
|
|
697
|
-
self.append(
|
|
725
|
+
self.append("right", other)
|
|
698
726
|
|
|
699
|
-
def add_legend_ax(self, side, size, pad=0.):
|
|
727
|
+
def add_legend_ax(self, side, size, pad=0.0):
|
|
700
728
|
"""Extend the layout
|
|
701
729
|
|
|
702
730
|
This is used to draw legends after concatenation
|
|
@@ -722,18 +750,16 @@ class CompositeCrossLayout(_MarginMixin):
|
|
|
722
750
|
other_size = []
|
|
723
751
|
size += self.main_layout.get_side_size(side)
|
|
724
752
|
if side in ["left", "right"]:
|
|
725
|
-
|
|
726
753
|
for g in self._side_layouts.get(side):
|
|
727
754
|
size += g.get_bbox_width()
|
|
728
755
|
|
|
729
|
-
for g in self._side_layouts[
|
|
756
|
+
for g in self._side_layouts["top"] + self._side_layouts["bottom"]:
|
|
730
757
|
other_size.append(g.get_side_size(side))
|
|
731
758
|
else:
|
|
732
|
-
|
|
733
759
|
for g in self._side_layouts.get(side):
|
|
734
760
|
size += g.get_bbox_height()
|
|
735
761
|
|
|
736
|
-
for g in self._side_layouts[
|
|
762
|
+
for g in self._side_layouts["left"] + self._side_layouts["right"]:
|
|
737
763
|
other_size.append(g.get_side_size(side))
|
|
738
764
|
|
|
739
765
|
other_size = np.max(other_size, initial=0)
|
|
@@ -744,12 +770,18 @@ class CompositeCrossLayout(_MarginMixin):
|
|
|
744
770
|
return max(size, other_size) + legend_size
|
|
745
771
|
|
|
746
772
|
def get_bbox_width(self):
|
|
747
|
-
return
|
|
748
|
-
self.
|
|
773
|
+
return (
|
|
774
|
+
self.main_cell_width
|
|
775
|
+
+ self.get_side_size("left")
|
|
776
|
+
+ self.get_side_size("right")
|
|
777
|
+
)
|
|
749
778
|
|
|
750
779
|
def get_bbox_height(self):
|
|
751
|
-
return
|
|
752
|
-
self.
|
|
780
|
+
return (
|
|
781
|
+
self.main_cell_height
|
|
782
|
+
+ self.get_side_size("top")
|
|
783
|
+
+ self.get_side_size("bottom")
|
|
784
|
+
)
|
|
753
785
|
|
|
754
786
|
def get_bbox_size(self):
|
|
755
787
|
"""Get the minimum figsize that fits all layouts inside"""
|
|
@@ -762,8 +794,8 @@ class CompositeCrossLayout(_MarginMixin):
|
|
|
762
794
|
return fig_w, fig_h
|
|
763
795
|
|
|
764
796
|
def get_main_anchor(self):
|
|
765
|
-
x = self.get_side_size(
|
|
766
|
-
y = self.get_side_size(
|
|
797
|
+
x = self.get_side_size("left") + self.margin.left
|
|
798
|
+
y = self.get_side_size("bottom") + self.margin.bottom
|
|
767
799
|
return x, y
|
|
768
800
|
|
|
769
801
|
def set_anchor(self, anchor):
|
|
@@ -790,39 +822,37 @@ class CompositeCrossLayout(_MarginMixin):
|
|
|
790
822
|
|
|
791
823
|
# The left and right share the y
|
|
792
824
|
offset_x = mx - self.main_layout.get_side_size("left")
|
|
793
|
-
for g in self._side_layouts[
|
|
794
|
-
offset_x -=
|
|
825
|
+
for g in self._side_layouts["left"]:
|
|
826
|
+
offset_x -= g.get_side_size("right") + g.get_main_width()
|
|
795
827
|
g.set_anchor((offset_x, my))
|
|
796
828
|
g.set_figsize(figsize)
|
|
797
829
|
g.freeze(figure, _debug=_debug)
|
|
798
|
-
offset_x -= g.get_side_size(
|
|
830
|
+
offset_x -= g.get_side_size("left")
|
|
799
831
|
|
|
800
|
-
offset_x =
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
offset_x += g.get_side_size('left')
|
|
832
|
+
offset_x = mx + self.main_cell_width + self.main_layout.get_side_size("right")
|
|
833
|
+
for g in self._side_layouts["right"]:
|
|
834
|
+
offset_x += g.get_side_size("left")
|
|
804
835
|
g.set_anchor((offset_x, my))
|
|
805
836
|
g.set_figsize(figsize)
|
|
806
837
|
g.freeze(figure, _debug=_debug)
|
|
807
|
-
offset_x +=
|
|
838
|
+
offset_x += g.get_main_width() + g.get_side_size("right")
|
|
808
839
|
|
|
809
840
|
# The top and bottom share the x
|
|
810
|
-
offset_y = my - self.main_layout.get_side_size(
|
|
811
|
-
for g in self._side_layouts[
|
|
812
|
-
offset_y -=
|
|
841
|
+
offset_y = my - self.main_layout.get_side_size("bottom")
|
|
842
|
+
for g in self._side_layouts["bottom"]:
|
|
843
|
+
offset_y -= g.get_side_size("top") + g.get_main_height()
|
|
813
844
|
g.set_anchor((mx, offset_y))
|
|
814
845
|
g.set_figsize(figsize)
|
|
815
846
|
g.freeze(figure, _debug=_debug)
|
|
816
|
-
offset_y -= g.get_side_size(
|
|
847
|
+
offset_y -= g.get_side_size("bottom")
|
|
817
848
|
|
|
818
|
-
offset_y =
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
offset_y += g.get_side_size('bottom')
|
|
849
|
+
offset_y = my + self.main_cell_height + self.main_layout.get_side_size("top")
|
|
850
|
+
for g in self._side_layouts["top"]:
|
|
851
|
+
offset_y += g.get_side_size("bottom")
|
|
822
852
|
g.set_anchor((mx, offset_y))
|
|
823
853
|
g.set_figsize(figsize)
|
|
824
854
|
g.freeze(figure, _debug=_debug)
|
|
825
|
-
offset_y +=
|
|
855
|
+
offset_y += g.get_main_height() + g.get_side_size("top")
|
|
826
856
|
|
|
827
857
|
# Add legend axes
|
|
828
858
|
# The range is limited to the main_cell in each layout
|
|
@@ -834,10 +864,10 @@ class CompositeCrossLayout(_MarginMixin):
|
|
|
834
864
|
side = self._legend_axes.side
|
|
835
865
|
size = self._legend_axes.size
|
|
836
866
|
|
|
837
|
-
if side ==
|
|
867
|
+
if side == "right":
|
|
838
868
|
cx, cy = fig_w - size - self.margin.right, ymin
|
|
839
869
|
cw, ch = size, legend_h
|
|
840
|
-
elif side ==
|
|
870
|
+
elif side == "left":
|
|
841
871
|
cx, cy = self.margin.left, ymin
|
|
842
872
|
cw, ch = size, legend_h
|
|
843
873
|
elif side == "top":
|
|
@@ -857,26 +887,26 @@ class CompositeCrossLayout(_MarginMixin):
|
|
|
857
887
|
|
|
858
888
|
def _edge_main_point(self):
|
|
859
889
|
"""This will return the edge point of all the main cell"""
|
|
860
|
-
left_side = self._side_layouts[
|
|
890
|
+
left_side = self._side_layouts["left"]
|
|
861
891
|
if len(left_side) > 0:
|
|
862
892
|
xmin = left_side[-1].main_cell.anchor[0]
|
|
863
893
|
else:
|
|
864
894
|
xmin = self.main_layout.main_cell.anchor[0]
|
|
865
895
|
|
|
866
|
-
right_side = self._side_layouts[
|
|
896
|
+
right_side = self._side_layouts["right"]
|
|
867
897
|
if len(right_side) > 0:
|
|
868
898
|
cell = right_side[-1].main_cell
|
|
869
899
|
else:
|
|
870
900
|
cell = self.main_layout.main_cell
|
|
871
901
|
xmax = cell.anchor[0] + cell.width
|
|
872
902
|
|
|
873
|
-
bottom_side = self._side_layouts[
|
|
903
|
+
bottom_side = self._side_layouts["bottom"]
|
|
874
904
|
if len(bottom_side) > 0:
|
|
875
905
|
ymin = bottom_side[-1].main_cell.anchor[1]
|
|
876
906
|
else:
|
|
877
907
|
ymin = self.main_layout.main_cell.anchor[1]
|
|
878
908
|
|
|
879
|
-
top_side = self._side_layouts[
|
|
909
|
+
top_side = self._side_layouts["top"]
|
|
880
910
|
if len(top_side) > 0:
|
|
881
911
|
cell = top_side[-1].main_cell
|
|
882
912
|
else:
|
marsilea/plotter/__init__.py
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"Bar",
|
|
3
|
+
"Box",
|
|
4
|
+
"Boxen",
|
|
5
|
+
"Violin",
|
|
6
|
+
"Point",
|
|
7
|
+
"Strip",
|
|
8
|
+
"Swarm",
|
|
9
|
+
"Arc",
|
|
10
|
+
"Numbers",
|
|
11
|
+
"StackBar",
|
|
12
|
+
"CenterBar",
|
|
13
|
+
"RenderPlan",
|
|
14
|
+
"SeqLogo",
|
|
15
|
+
"Colors",
|
|
16
|
+
"ColorMesh",
|
|
17
|
+
"SizedMesh",
|
|
18
|
+
"MarkerMesh",
|
|
19
|
+
"TextMesh",
|
|
20
|
+
"Labels",
|
|
21
|
+
"AnnoLabels",
|
|
22
|
+
"Title",
|
|
23
|
+
"Chunk",
|
|
24
|
+
"FixedChunk",
|
|
25
|
+
"Area",
|
|
26
|
+
]
|
|
27
|
+
|
|
1
28
|
from ._seaborn import Bar, Box, Boxen, Violin, Point, Strip, Swarm
|
|
2
29
|
from .arc import Arc
|
|
3
30
|
from .bar import Numbers, StackBar, CenterBar
|
|
@@ -5,3 +32,5 @@ from .base import RenderPlan
|
|
|
5
32
|
from .bio import SeqLogo
|
|
6
33
|
from .mesh import Colors, ColorMesh, SizedMesh, MarkerMesh, TextMesh
|
|
7
34
|
from .text import Labels, AnnoLabels, Title, Chunk, FixedChunk
|
|
35
|
+
# from ._images import Emoji
|
|
36
|
+
from .area import Area
|
marsilea/plotter/_images.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# The implementation of Image in matplotlib may suffer from compatibility
|
|
2
2
|
# issues across different rendering backend at different DPI. Currently
|
|
3
3
|
# not a public API.
|
|
4
|
+
from functools import partial
|
|
4
5
|
|
|
5
6
|
import numpy as np
|
|
6
7
|
from matplotlib.image import imread, BboxImage
|
|
@@ -27,7 +28,6 @@ def _cache_remote(url, cache=True):
|
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
class Emoji(RenderPlan):
|
|
30
|
-
|
|
31
31
|
def __init__(self, images, lang="en", scale=1, mode="filled"):
|
|
32
32
|
try:
|
|
33
33
|
import emoji
|
|
@@ -40,7 +40,8 @@ class Emoji(RenderPlan):
|
|
|
40
40
|
if not emoji.is_emoji(i):
|
|
41
41
|
raise ValueError(f"{i} is not a valid emoji")
|
|
42
42
|
codes.append(f"{ord(i):X}".lower())
|
|
43
|
-
|
|
43
|
+
|
|
44
|
+
self.set_data(np.asarray(codes))
|
|
44
45
|
self.emoji_caches = {}
|
|
45
46
|
for c in codes:
|
|
46
47
|
cache_image = _cache_remote(f"{TWEMOJI_CDN}{c}.png")
|
|
@@ -49,19 +50,48 @@ class Emoji(RenderPlan):
|
|
|
49
50
|
self.scale = scale
|
|
50
51
|
self.mode = mode
|
|
51
52
|
|
|
53
|
+
def _get_images_bbox(self, figure, imgs):
|
|
54
|
+
|
|
55
|
+
for img in imgs:
|
|
56
|
+
width, height = img.shape[:2]
|
|
57
|
+
|
|
58
|
+
return Bbox.from_bounds(0, 0, width, height)
|
|
59
|
+
|
|
60
|
+
|
|
52
61
|
def render_ax(self, spec):
|
|
53
62
|
ax = spec.ax
|
|
54
63
|
data = spec.data
|
|
55
64
|
|
|
56
|
-
|
|
65
|
+
# TODO: Does not work for orient = "v"
|
|
66
|
+
locs = np.linspace(0, 1, len(data) + 1)
|
|
57
67
|
for loc, d in zip(locs, data):
|
|
58
68
|
img = self.emoji_caches[d]
|
|
59
69
|
width, height = img.shape[:2]
|
|
60
70
|
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
xmin, ymin = ax.transAxes.transform((0, 0))
|
|
72
|
+
xmax, ymax = ax.transAxes.transform((1, 1))
|
|
73
|
+
|
|
74
|
+
ax_width = xmax - xmin
|
|
75
|
+
ax_height = ymax - ymin
|
|
76
|
+
|
|
77
|
+
fit_width = ax_width / len(data)
|
|
78
|
+
fit_height = height / width * fit_width
|
|
79
|
+
|
|
80
|
+
fit_scale_width = fit_width * self.scale
|
|
81
|
+
fit_scale_height = fit_height * self.scale
|
|
82
|
+
|
|
83
|
+
offset = (fit_width - fit_scale_width) / 2 / ax_width
|
|
84
|
+
loc += offset
|
|
85
|
+
|
|
86
|
+
loc_y = 0.5 - fit_scale_height / 2 / ax_height
|
|
87
|
+
|
|
88
|
+
def get_emoji_bbox(renderer, loc, loc_y, width, height):
|
|
89
|
+
x0, y0 = ax.transData.transform((loc, loc_y))
|
|
63
90
|
return Bbox.from_bounds(x0, y0, width, height)
|
|
64
91
|
|
|
65
|
-
|
|
66
|
-
|
|
92
|
+
partial_get_emoji_bbox = partial(get_emoji_bbox, loc=loc, loc_y=loc_y,
|
|
93
|
+
width=fit_scale_width,
|
|
94
|
+
height=fit_scale_height)
|
|
95
|
+
|
|
96
|
+
i1 = BboxImage(partial_get_emoji_bbox, data=img)
|
|
67
97
|
ax.add_artist(i1)
|