marsilea 0.4.6__tar.gz → 0.4.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.
- {marsilea-0.4.6 → marsilea-0.4.7}/PKG-INFO +2 -2
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/__init__.py +4 -2
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/base.py +125 -9
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/layout.py +352 -21
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/_seaborn.py +2 -1
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/arc.py +4 -2
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/area.py +2 -1
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/bar.py +6 -3
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/bio.py +6 -3
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/mesh.py +10 -5
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/range.py +2 -1
- {marsilea-0.4.6 → marsilea-0.4.7}/.gitignore +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/LICENSE +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/README.md +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/pyproject.toml +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/setup.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/_api.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/_deform.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/dataset.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/dendrogram.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/exceptions.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/heatmap.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/layers.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/__init__.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/_utils.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/base.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/images.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/text.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/upset.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/utils.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/src/oncoprinter/__init__.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/src/oncoprinter/core.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.7}/src/oncoprinter/preset.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Declarative creation of composable visualization"""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.4.
|
|
3
|
+
__version__ = "0.4.7"
|
|
4
4
|
|
|
5
5
|
import marsilea.plotter as plotter
|
|
6
6
|
from ._deform import Deformation
|
|
@@ -11,10 +11,12 @@ from .base import (
|
|
|
11
11
|
ZeroHeight,
|
|
12
12
|
ZeroWidthCluster,
|
|
13
13
|
ZeroHeightCluster,
|
|
14
|
+
CompositeBoard,
|
|
15
|
+
StackBoard,
|
|
14
16
|
)
|
|
15
17
|
from .dataset import load_data
|
|
16
18
|
from .dendrogram import Dendrogram, GroupDendrogram
|
|
17
19
|
from .heatmap import Heatmap, SizedHeatmap, CatHeatmap
|
|
18
20
|
from .layers import Piece, Layers
|
|
19
|
-
from .layout import CrossLayout, CompositeCrossLayout
|
|
21
|
+
from .layout import CrossLayout, CompositeCrossLayout, StackCrossLayout
|
|
20
22
|
from .upset import UpsetData, Upset
|
|
@@ -16,7 +16,7 @@ from matplotlib.figure import Figure
|
|
|
16
16
|
from ._deform import Deformation
|
|
17
17
|
from .dendrogram import Dendrogram
|
|
18
18
|
from .exceptions import SplitTwice, DuplicatePlotter
|
|
19
|
-
from .layout import CrossLayout, CompositeCrossLayout
|
|
19
|
+
from .layout import CrossLayout, CompositeCrossLayout, StackCrossLayout
|
|
20
20
|
from .plotter import RenderPlan, Title, SizedMesh
|
|
21
21
|
from .utils import pairwise, batched, get_plot_name, _check_side
|
|
22
22
|
|
|
@@ -47,7 +47,7 @@ def get_breakpoints(arr):
|
|
|
47
47
|
class LegendMaker:
|
|
48
48
|
"""The factory class to handle legends"""
|
|
49
49
|
|
|
50
|
-
layout: CrossLayout | CompositeCrossLayout
|
|
50
|
+
layout: CrossLayout | CompositeCrossLayout | StackCrossLayout
|
|
51
51
|
_legend_box: List[Artist] = None
|
|
52
52
|
_legend_name: str = None
|
|
53
53
|
|
|
@@ -711,20 +711,47 @@ class ZeroHeight(WhiteBoard):
|
|
|
711
711
|
|
|
712
712
|
|
|
713
713
|
class CompositeBoard(LegendMaker):
|
|
714
|
+
"""Layout multiple canvas
|
|
715
|
+
|
|
716
|
+
Parameters
|
|
717
|
+
----------
|
|
718
|
+
main_board : :class:`WhiteBoard` or :class:`ClusterBoard`
|
|
719
|
+
The main canvas
|
|
720
|
+
keep_legends : bool, default: False
|
|
721
|
+
Whether to keep the legends in each canvas
|
|
722
|
+
If False, you can group all legends with `.add_legends()`
|
|
723
|
+
align_main : bool, default: True
|
|
724
|
+
Whether to force the size of other canvas to align with the main canvas
|
|
725
|
+
margin : float, default: 0
|
|
726
|
+
The margin space reserved around the whole canvas
|
|
727
|
+
|
|
728
|
+
"""
|
|
729
|
+
|
|
714
730
|
layout: CompositeCrossLayout = None
|
|
715
731
|
figure: Figure = None
|
|
716
732
|
|
|
717
|
-
def __init__(
|
|
733
|
+
def __init__(
|
|
734
|
+
self,
|
|
735
|
+
main_board: WhiteBoard,
|
|
736
|
+
keep_legends=False,
|
|
737
|
+
align_main=True,
|
|
738
|
+
margin=0,
|
|
739
|
+
):
|
|
740
|
+
self.keep_legends = keep_legends
|
|
741
|
+
|
|
718
742
|
self.main_board = self.new_board(main_board)
|
|
719
|
-
|
|
720
|
-
|
|
743
|
+
if not keep_legends:
|
|
744
|
+
self.main_board.remove_legends()
|
|
745
|
+
self.layout = CompositeCrossLayout(
|
|
746
|
+
self.main_board.layout, align_main=align_main, margin=margin
|
|
747
|
+
)
|
|
721
748
|
self._board_list = [self.main_board]
|
|
749
|
+
|
|
722
750
|
super().__init__()
|
|
723
751
|
|
|
724
|
-
|
|
725
|
-
def new_board(board):
|
|
752
|
+
def new_board(self, board):
|
|
726
753
|
board = deepcopy(board)
|
|
727
|
-
if isinstance(board, LegendMaker):
|
|
754
|
+
if not self.keep_legends & isinstance(board, LegendMaker):
|
|
728
755
|
board.remove_legends()
|
|
729
756
|
return board
|
|
730
757
|
|
|
@@ -736,13 +763,16 @@ class CompositeBoard(LegendMaker):
|
|
|
736
763
|
"""Define behavior that vertical appends two grid"""
|
|
737
764
|
return self.append("bottom", other)
|
|
738
765
|
|
|
739
|
-
def append(self, side, other):
|
|
766
|
+
def append(self, side, other, pad=0):
|
|
740
767
|
if isinstance(other, Number):
|
|
741
768
|
self.layout.append(side, other)
|
|
742
769
|
else:
|
|
743
770
|
board = self.new_board(other)
|
|
744
771
|
self._board_list.append(board)
|
|
745
772
|
self.layout.append(side, board.layout)
|
|
773
|
+
|
|
774
|
+
if pad > 0:
|
|
775
|
+
self.layout.append(side, pad)
|
|
746
776
|
return self
|
|
747
777
|
|
|
748
778
|
def render(self, figure=None, scale=1):
|
|
@@ -777,6 +807,92 @@ class CompositeBoard(LegendMaker):
|
|
|
777
807
|
def get_ax(self, board_name, ax_name):
|
|
778
808
|
return self.layout.get_ax(board_name, ax_name)
|
|
779
809
|
|
|
810
|
+
def get_main_ax(self, name):
|
|
811
|
+
return self.layout.get_main_ax(name)
|
|
812
|
+
|
|
813
|
+
def set_margin(self, margin):
|
|
814
|
+
self.layout.set_margin(margin)
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
class StackBoard(LegendMaker):
|
|
818
|
+
"""Stack multiple boards
|
|
819
|
+
|
|
820
|
+
Parameters
|
|
821
|
+
----------
|
|
822
|
+
boards : list of :class:`WhiteBoard`, :class:`StackBoard`
|
|
823
|
+
|
|
824
|
+
"""
|
|
825
|
+
|
|
826
|
+
def __init__(
|
|
827
|
+
self,
|
|
828
|
+
boards: List[WhiteBoard, StackBoard],
|
|
829
|
+
direction="horizontal",
|
|
830
|
+
align="center",
|
|
831
|
+
spacing=0.2,
|
|
832
|
+
margin=0,
|
|
833
|
+
keep_legends=False,
|
|
834
|
+
):
|
|
835
|
+
self.keep_legends = keep_legends
|
|
836
|
+
board_list = []
|
|
837
|
+
layouts = []
|
|
838
|
+
for board in boards:
|
|
839
|
+
board = self.new_board(board)
|
|
840
|
+
board_list.append(board)
|
|
841
|
+
layouts.append(board.layout)
|
|
842
|
+
|
|
843
|
+
self.layout = StackCrossLayout(
|
|
844
|
+
layouts, margin=margin, direction=direction, align=align, spacing=spacing
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
self._board_list = board_list
|
|
848
|
+
super().__init__()
|
|
849
|
+
|
|
850
|
+
# To mimic the board API
|
|
851
|
+
def _freeze_flex_plots(self, figure):
|
|
852
|
+
for board in self._board_list:
|
|
853
|
+
board._freeze_flex_plots(figure)
|
|
854
|
+
|
|
855
|
+
def new_board(self, board):
|
|
856
|
+
board = deepcopy(board)
|
|
857
|
+
if not self.keep_legends & isinstance(board, LegendMaker):
|
|
858
|
+
board.remove_legends()
|
|
859
|
+
return board
|
|
860
|
+
|
|
861
|
+
def render(self, figure=None, scale=1):
|
|
862
|
+
if figure is None:
|
|
863
|
+
figure = plt.figure()
|
|
864
|
+
self._freeze_legend(figure)
|
|
865
|
+
for board in self._board_list:
|
|
866
|
+
board._freeze_flex_plots(figure)
|
|
867
|
+
self.layout.freeze(figure=figure, scale=scale)
|
|
868
|
+
self.figure = figure
|
|
869
|
+
for board in self._board_list:
|
|
870
|
+
board.render(figure=self.figure)
|
|
871
|
+
|
|
872
|
+
self._render_legend()
|
|
873
|
+
|
|
874
|
+
def save(self, fname, **kwargs):
|
|
875
|
+
if self.figure is not None:
|
|
876
|
+
save_options = dict(bbox_inches="tight")
|
|
877
|
+
save_options.update(kwargs)
|
|
878
|
+
self.figure.savefig(fname, **save_options)
|
|
879
|
+
else:
|
|
880
|
+
warnings.warn(
|
|
881
|
+
"Figure does not exist, " "please render it before saving as file."
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
def get_legends(self):
|
|
885
|
+
legends = {}
|
|
886
|
+
for m in self._board_list:
|
|
887
|
+
legends.update(m.get_legends())
|
|
888
|
+
return legends
|
|
889
|
+
|
|
890
|
+
def get_ax(self, board_name, ax_name):
|
|
891
|
+
return self.layout.get_ax(board_name, ax_name)
|
|
892
|
+
|
|
893
|
+
def get_main_ax(self, name):
|
|
894
|
+
return self.layout.get_main_ax(name)
|
|
895
|
+
|
|
780
896
|
def set_margin(self, margin):
|
|
781
897
|
self.layout.set_margin(margin)
|
|
782
898
|
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import numpy as np
|
|
4
3
|
from dataclasses import dataclass
|
|
5
|
-
from matplotlib import pyplot as plt
|
|
6
|
-
from matplotlib.axes import Axes
|
|
7
4
|
from numbers import Number
|
|
8
|
-
from typing import List, Dict
|
|
5
|
+
from typing import List, Dict, Literal
|
|
9
6
|
from uuid import uuid4
|
|
10
7
|
|
|
8
|
+
import numpy as np
|
|
9
|
+
from matplotlib import pyplot as plt
|
|
10
|
+
from matplotlib.axes import Axes
|
|
11
|
+
|
|
11
12
|
from .exceptions import AppendLayoutError, DuplicateName
|
|
12
13
|
from .utils import _check_side
|
|
13
14
|
|
|
@@ -16,7 +17,7 @@ from .utils import _check_side
|
|
|
16
17
|
# 1. Size is inch unit
|
|
17
18
|
# 2. Origin point is left-bottom
|
|
18
19
|
|
|
19
|
-
# Axes is a
|
|
20
|
+
# Axes is a rect, a rect can be recorded by a left-bottom anchor point
|
|
20
21
|
# and width and height. When other axes is added, the anchor point is aligned
|
|
21
22
|
# to either x-axis or y-axis, we could easily compute the final anchor point
|
|
22
23
|
# if we know the size of added axes
|
|
@@ -443,7 +444,7 @@ class CrossLayout(_MarginMixin):
|
|
|
443
444
|
if self.is_composite:
|
|
444
445
|
return self.get_bbox_size()
|
|
445
446
|
w, h = self.get_bbox_size()
|
|
446
|
-
return
|
|
447
|
+
return w + self.get_margin_w(), h + self.get_margin_h()
|
|
447
448
|
|
|
448
449
|
else:
|
|
449
450
|
ox, oy = self.anchor
|
|
@@ -514,6 +515,11 @@ class CrossLayout(_MarginMixin):
|
|
|
514
515
|
def set_anchor(self, anchor):
|
|
515
516
|
self.anchor = anchor
|
|
516
517
|
|
|
518
|
+
def set_bbox_anchor(self, anchor):
|
|
519
|
+
xoff = self.margin.left + self.get_side_size("left")
|
|
520
|
+
yoff = self.margin.bottom + self.get_side_size("bottom")
|
|
521
|
+
self.anchor = anchor[0] + xoff, anchor[1] + yoff
|
|
522
|
+
|
|
517
523
|
def set_figsize(self, figsize):
|
|
518
524
|
self.figsize = figsize
|
|
519
525
|
|
|
@@ -578,12 +584,13 @@ class CrossLayout(_MarginMixin):
|
|
|
578
584
|
"""
|
|
579
585
|
# If not composed, update the figsize
|
|
580
586
|
if not self.is_composite:
|
|
581
|
-
self.figsize = np.array(self.get_figure_size())
|
|
587
|
+
self.figsize = np.array(self.get_figure_size())
|
|
588
|
+
figsize = self.figsize * scale
|
|
582
589
|
if figure is None:
|
|
583
|
-
figure = plt.figure(figsize=
|
|
590
|
+
figure = plt.figure(figsize=figsize)
|
|
584
591
|
else:
|
|
585
592
|
if not self.is_composite:
|
|
586
|
-
figure.set_size_inches(*
|
|
593
|
+
figure.set_size_inches(*figsize)
|
|
587
594
|
|
|
588
595
|
main_anchor = self.get_main_anchor()
|
|
589
596
|
self.set_layout(main_anchor)
|
|
@@ -647,6 +654,12 @@ def close_ticks(ax):
|
|
|
647
654
|
)
|
|
648
655
|
|
|
649
656
|
|
|
657
|
+
def _reset_layout(layout: CrossLayout):
|
|
658
|
+
layout.set_anchor((0, 0))
|
|
659
|
+
layout.is_composite = True
|
|
660
|
+
return layout
|
|
661
|
+
|
|
662
|
+
|
|
650
663
|
@dataclass
|
|
651
664
|
class _LegendAxes:
|
|
652
665
|
side: str
|
|
@@ -673,8 +686,8 @@ class CompositeCrossLayout(_MarginMixin):
|
|
|
673
686
|
|
|
674
687
|
figure = None
|
|
675
688
|
|
|
676
|
-
def __init__(self, main_layout, margin=0) -> None:
|
|
677
|
-
self.main_layout =
|
|
689
|
+
def __init__(self, main_layout, margin=0, align_main=True) -> None:
|
|
690
|
+
self.main_layout = _reset_layout(main_layout)
|
|
678
691
|
self.main_cell_height = self.main_layout.get_main_height()
|
|
679
692
|
self.main_cell_width = self.main_layout.get_main_width()
|
|
680
693
|
self._side_layouts: Dict[str, List[CrossLayout]] = {
|
|
@@ -686,12 +699,7 @@ class CompositeCrossLayout(_MarginMixin):
|
|
|
686
699
|
self._legend_axes = None
|
|
687
700
|
self.layouts = {self.main_layout.main_cell.name: self.main_layout}
|
|
688
701
|
self.set_margin(margin)
|
|
689
|
-
|
|
690
|
-
@staticmethod
|
|
691
|
-
def _reset_layout(layout):
|
|
692
|
-
layout.set_anchor((0, 0))
|
|
693
|
-
layout.is_composite = True
|
|
694
|
-
return layout
|
|
702
|
+
self.align_main = align_main
|
|
695
703
|
|
|
696
704
|
def append(self, side, other):
|
|
697
705
|
_check_side(side)
|
|
@@ -709,10 +717,11 @@ class CompositeCrossLayout(_MarginMixin):
|
|
|
709
717
|
other.is_composite = True
|
|
710
718
|
self._side_layouts[side].append(other)
|
|
711
719
|
elif isinstance(other, CrossLayout):
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
720
|
+
other = _reset_layout(other)
|
|
721
|
+
if self.align_main:
|
|
722
|
+
adjust = "height" if side in ["left", "right"] else "width"
|
|
723
|
+
adjust_size = getattr(self, f"main_cell_{adjust}")
|
|
724
|
+
getattr(other, f"set_main_{adjust}").__call__(adjust_size)
|
|
716
725
|
self._side_layouts[side].append(other)
|
|
717
726
|
self.layouts[other.main_cell.name] = other
|
|
718
727
|
else:
|
|
@@ -920,3 +929,325 @@ class CompositeCrossLayout(_MarginMixin):
|
|
|
920
929
|
|
|
921
930
|
def get_ax(self, layout_name, ax_name):
|
|
922
931
|
return self.layouts[layout_name].get_ax(ax_name)
|
|
932
|
+
|
|
933
|
+
def get_main_ax(self, layout_name):
|
|
934
|
+
return self.layouts[layout_name].get_main_ax()
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
class StackCrossLayout(_MarginMixin):
|
|
938
|
+
"""A stack of cross layouts
|
|
939
|
+
|
|
940
|
+
This class allow users to stack multiple cross layouts
|
|
941
|
+
either horizontally or vertically
|
|
942
|
+
|
|
943
|
+
Multiple StackCrossLayout can also be stacked
|
|
944
|
+
|
|
945
|
+
.. warning::
|
|
946
|
+
This class are not supposed to be used directly by user
|
|
947
|
+
|
|
948
|
+
Parameters
|
|
949
|
+
----------
|
|
950
|
+
layouts : list of :class:`CrossLayout`, :class:`StackCrossLayout`
|
|
951
|
+
The layouts to be stacked
|
|
952
|
+
direction : {"horizontal", "vertical"}
|
|
953
|
+
The direction of the stack, horizontal will stack from left to right
|
|
954
|
+
vertical will stack from top to bottom
|
|
955
|
+
align : {"center", "bottom", "top", "left", "right"}
|
|
956
|
+
The alignment of the stack, the default is center
|
|
957
|
+
|
|
958
|
+
"""
|
|
959
|
+
|
|
960
|
+
_direction_align = {
|
|
961
|
+
"horizontal": {"center", "top", "bottom"},
|
|
962
|
+
"vertical": {"center", "left", "right"},
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
def __init__(
|
|
966
|
+
self,
|
|
967
|
+
layouts: List[CrossLayout | StackCrossLayout],
|
|
968
|
+
direction="horizontal",
|
|
969
|
+
align: Literal["center", "bottom", "top", "left", "right"] = "center",
|
|
970
|
+
spacing=0,
|
|
971
|
+
margin=0,
|
|
972
|
+
name=None,
|
|
973
|
+
):
|
|
974
|
+
# Check the direction and align
|
|
975
|
+
if direction not in self._direction_align:
|
|
976
|
+
raise ValueError(f"Invalid direction {direction}")
|
|
977
|
+
if align not in self._direction_align[direction]:
|
|
978
|
+
raise ValueError(
|
|
979
|
+
f"When setting direction={direction}, "
|
|
980
|
+
f"align must be one of {self._direction_align[direction]}"
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
self.layouts = []
|
|
984
|
+
self._layouts_mapper = {}
|
|
985
|
+
for layout in layouts:
|
|
986
|
+
layout = _reset_layout(layout)
|
|
987
|
+
self.layouts.append(layout)
|
|
988
|
+
if hasattr(layout, "name"):
|
|
989
|
+
self._layouts_mapper[layout.name] = layout
|
|
990
|
+
else:
|
|
991
|
+
self._layouts_mapper[layout.main_cell.name] = layout
|
|
992
|
+
self.direction = direction
|
|
993
|
+
self.align = align
|
|
994
|
+
self.spacing = spacing
|
|
995
|
+
if name is None:
|
|
996
|
+
name = uuid4().hex
|
|
997
|
+
self.name = name
|
|
998
|
+
self.set_margin(margin)
|
|
999
|
+
self.is_composite = False
|
|
1000
|
+
self.figure = None
|
|
1001
|
+
self.figsize = None
|
|
1002
|
+
# The left bottom anchor point of the layout
|
|
1003
|
+
self.anchor = None
|
|
1004
|
+
self._legend_axes = None
|
|
1005
|
+
|
|
1006
|
+
def remove_legend_ax(self):
|
|
1007
|
+
self._legend_axes = None
|
|
1008
|
+
for layout in self.layouts:
|
|
1009
|
+
layout.remove_legend_ax()
|
|
1010
|
+
|
|
1011
|
+
def _get_layout_widths(self):
|
|
1012
|
+
return [layout.get_bbox_width() for layout in self.layouts]
|
|
1013
|
+
|
|
1014
|
+
def _get_layout_heights(self):
|
|
1015
|
+
return [layout.get_bbox_height() for layout in self.layouts]
|
|
1016
|
+
|
|
1017
|
+
def _get_spacing_widths(self):
|
|
1018
|
+
return self.spacing * (len(self.layouts) - 1)
|
|
1019
|
+
|
|
1020
|
+
def _get_spacing_heights(self):
|
|
1021
|
+
return self.spacing * (len(self.layouts) - 1)
|
|
1022
|
+
|
|
1023
|
+
def get_bbox_width(self):
|
|
1024
|
+
ws = self._get_layout_widths()
|
|
1025
|
+
if self.direction == "horizontal":
|
|
1026
|
+
return np.sum(ws) + self._get_spacing_widths()
|
|
1027
|
+
else:
|
|
1028
|
+
if self.align == "center":
|
|
1029
|
+
return np.max(ws)
|
|
1030
|
+
elif self.align == "left":
|
|
1031
|
+
left_sides = np.asarray(
|
|
1032
|
+
[layout.get_side_size("left") for layout in self.layouts]
|
|
1033
|
+
)
|
|
1034
|
+
right_leftover = ws - left_sides
|
|
1035
|
+
return np.max(left_sides) + np.max(right_leftover)
|
|
1036
|
+
else:
|
|
1037
|
+
right_sides = np.asarray(
|
|
1038
|
+
[layout.get_side_size("right") for layout in self.layouts]
|
|
1039
|
+
)
|
|
1040
|
+
left_leftover = ws - right_sides
|
|
1041
|
+
return np.max(right_sides) + np.max(left_leftover)
|
|
1042
|
+
|
|
1043
|
+
def get_bbox_height(self):
|
|
1044
|
+
hs = self._get_layout_heights()
|
|
1045
|
+
if self.direction == "vertical":
|
|
1046
|
+
return np.sum(hs) + self._get_spacing_heights()
|
|
1047
|
+
else:
|
|
1048
|
+
if self.align == "center":
|
|
1049
|
+
return np.max(hs)
|
|
1050
|
+
elif self.align == "top":
|
|
1051
|
+
top_sides = np.asarray(
|
|
1052
|
+
[layout.get_side_size("top") for layout in self.layouts]
|
|
1053
|
+
)
|
|
1054
|
+
bottom_leftover = hs - top_sides
|
|
1055
|
+
return np.max(top_sides) + np.max(bottom_leftover)
|
|
1056
|
+
else:
|
|
1057
|
+
bottom_sides = np.asarray(
|
|
1058
|
+
[layout.get_side_size("bottom") for layout in self.layouts]
|
|
1059
|
+
)
|
|
1060
|
+
top_leftover = hs - bottom_sides
|
|
1061
|
+
return np.max(bottom_sides) + np.max(top_leftover)
|
|
1062
|
+
|
|
1063
|
+
def get_bbox_size(self):
|
|
1064
|
+
return self.get_bbox_width(), self.get_bbox_height()
|
|
1065
|
+
|
|
1066
|
+
def get_figure_size(self):
|
|
1067
|
+
fig_w = self.get_bbox_width() + self.get_margin_w()
|
|
1068
|
+
fig_h = self.get_bbox_height() + self.get_margin_h()
|
|
1069
|
+
return fig_w, fig_h
|
|
1070
|
+
|
|
1071
|
+
def set_figsize(self, figsize):
|
|
1072
|
+
self.figsize = figsize
|
|
1073
|
+
for layout in self.layouts:
|
|
1074
|
+
layout.set_figsize(figsize)
|
|
1075
|
+
|
|
1076
|
+
# Mimic the CrossLayoutAPI
|
|
1077
|
+
def get_side_size(self, side):
|
|
1078
|
+
legend_size = 0
|
|
1079
|
+
if self._legend_axes is not None:
|
|
1080
|
+
if self._legend_axes.side == side:
|
|
1081
|
+
legend_size = self._legend_axes.get_length()
|
|
1082
|
+
return self._get_layouts_offset(side) + legend_size
|
|
1083
|
+
|
|
1084
|
+
# Mimic the CrossLayoutAPI
|
|
1085
|
+
def get_main_height(self):
|
|
1086
|
+
return (
|
|
1087
|
+
self.get_bbox_height()
|
|
1088
|
+
- self._get_layouts_offset("bottom")
|
|
1089
|
+
- self._get_layouts_offset("top")
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
# Mimic the CrossLayoutAPI
|
|
1093
|
+
def get_main_width(self):
|
|
1094
|
+
return (
|
|
1095
|
+
self.get_bbox_width()
|
|
1096
|
+
- self._get_layouts_offset("left")
|
|
1097
|
+
- self._get_layouts_offset("right")
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
def _get_layouts_offset(self, side):
|
|
1101
|
+
if self.direction == "horizontal":
|
|
1102
|
+
if side in {"bottom", "top"}:
|
|
1103
|
+
return np.max([layout.get_side_size(side) for layout in self.layouts])
|
|
1104
|
+
elif side == "left":
|
|
1105
|
+
return self.layouts[0].get_side_size("left")
|
|
1106
|
+
else:
|
|
1107
|
+
return self.layouts[-1].get_side_size("right")
|
|
1108
|
+
else:
|
|
1109
|
+
if side in {"left", "right"}:
|
|
1110
|
+
return np.max([layout.get_side_size(side) for layout in self.layouts])
|
|
1111
|
+
elif side == "bottom":
|
|
1112
|
+
return self.layouts[-1].get_side_size("bottom")
|
|
1113
|
+
else:
|
|
1114
|
+
return self.layouts[0].get_side_size("top")
|
|
1115
|
+
|
|
1116
|
+
def get_layout_anchors(self):
|
|
1117
|
+
"""Get the anchor points of all layouts assume the layout is not composite"""
|
|
1118
|
+
base_x, base_y = self.margin.left, self.margin.bottom
|
|
1119
|
+
xs, ys = [], []
|
|
1120
|
+
if self.direction == "horizontal":
|
|
1121
|
+
# x is always the same regardless of the alignment
|
|
1122
|
+
for layout in self.layouts:
|
|
1123
|
+
base_x += layout.get_side_size("left")
|
|
1124
|
+
xs.append(base_x)
|
|
1125
|
+
base_x += (
|
|
1126
|
+
layout.get_main_width()
|
|
1127
|
+
+ layout.get_side_size("right")
|
|
1128
|
+
+ self.spacing
|
|
1129
|
+
)
|
|
1130
|
+
# only y is different
|
|
1131
|
+
if self.align == "bottom":
|
|
1132
|
+
base_y = self.margin.bottom + self._get_layouts_offset("bottom")
|
|
1133
|
+
ys = [base_y for _ in range(len(self.layouts))]
|
|
1134
|
+
elif self.align == "top":
|
|
1135
|
+
base_y = (
|
|
1136
|
+
self.margin.bottom
|
|
1137
|
+
+ self.get_bbox_height()
|
|
1138
|
+
- self._get_layouts_offset("top")
|
|
1139
|
+
)
|
|
1140
|
+
ys = [base_y - layout.get_main_height() for layout in self.layouts]
|
|
1141
|
+
else:
|
|
1142
|
+
center_y = self.margin.bottom + self.get_bbox_height() / 2
|
|
1143
|
+
ys = [
|
|
1144
|
+
center_y - layout.get_main_height() / 2 for layout in self.layouts
|
|
1145
|
+
]
|
|
1146
|
+
else:
|
|
1147
|
+
# y is always the same regardless of the alignment
|
|
1148
|
+
for layout in self.layouts[::-1]:
|
|
1149
|
+
base_y += layout.get_side_size("bottom")
|
|
1150
|
+
ys.append(base_y)
|
|
1151
|
+
base_y += (
|
|
1152
|
+
layout.get_main_height()
|
|
1153
|
+
+ layout.get_side_size("top")
|
|
1154
|
+
+ self.spacing
|
|
1155
|
+
)
|
|
1156
|
+
# only x is different
|
|
1157
|
+
if self.align == "left":
|
|
1158
|
+
base_x = self.margin.left + self._get_layouts_offset("left")
|
|
1159
|
+
xs = [base_x for _ in range(len(self.layouts))]
|
|
1160
|
+
elif self.align == "right":
|
|
1161
|
+
base_x = (
|
|
1162
|
+
self.margin.left
|
|
1163
|
+
+ self.get_bbox_width()
|
|
1164
|
+
- self._get_layouts_offset("right")
|
|
1165
|
+
)
|
|
1166
|
+
xs = [base_x - layout.get_main_width() for layout in self.layouts]
|
|
1167
|
+
else:
|
|
1168
|
+
center_x = self.margin.left + self.get_bbox_width() / 2
|
|
1169
|
+
xs = [center_x - layout.get_main_width() / 2 for layout in self.layouts]
|
|
1170
|
+
ys = ys[::-1]
|
|
1171
|
+
|
|
1172
|
+
return np.array(list(zip(xs, ys)))
|
|
1173
|
+
|
|
1174
|
+
def set_layout_anchors(self, anchors):
|
|
1175
|
+
for layout, a in zip(self.layouts, anchors):
|
|
1176
|
+
layout.set_anchor(a)
|
|
1177
|
+
|
|
1178
|
+
def set_anchor(self, anchor):
|
|
1179
|
+
self.anchor = anchor
|
|
1180
|
+
|
|
1181
|
+
def add_legend_ax(self, side, size, pad=0.0):
|
|
1182
|
+
"""Extend the layout
|
|
1183
|
+
|
|
1184
|
+
This is used to draw legends after concatenation
|
|
1185
|
+
|
|
1186
|
+
"""
|
|
1187
|
+
self._legend_axes = _LegendAxes(side=side, size=size, pad=pad)
|
|
1188
|
+
|
|
1189
|
+
def get_legend_ax(self):
|
|
1190
|
+
if self._legend_axes is not None:
|
|
1191
|
+
return self._legend_axes.ax
|
|
1192
|
+
|
|
1193
|
+
def set_legend_size(self, size):
|
|
1194
|
+
self._legend_axes.size = size
|
|
1195
|
+
|
|
1196
|
+
def freeze(self, figure=None, scale=1, _debug=False):
|
|
1197
|
+
# If not composed, update the figsize
|
|
1198
|
+
if not self.is_composite:
|
|
1199
|
+
self.figsize = np.array(self.get_figure_size())
|
|
1200
|
+
figsize = self.figsize * scale
|
|
1201
|
+
if figure is None:
|
|
1202
|
+
figure = plt.figure(figsize=figsize)
|
|
1203
|
+
else:
|
|
1204
|
+
if not self.is_composite:
|
|
1205
|
+
figure.set_size_inches(*figsize)
|
|
1206
|
+
|
|
1207
|
+
# Compute the anchor points for all sub layouts
|
|
1208
|
+
anchors = self.get_layout_anchors()
|
|
1209
|
+
# Offset the anchor point by the main anchor
|
|
1210
|
+
main_anchor = np.min(anchors, axis=0)
|
|
1211
|
+
if self.anchor is not None:
|
|
1212
|
+
xoff = self.anchor[0] - main_anchor[0]
|
|
1213
|
+
yoff = self.anchor[1] - main_anchor[1]
|
|
1214
|
+
anchors = [(x + xoff, y + yoff) for x, y in anchors]
|
|
1215
|
+
self.set_layout_anchors(anchors)
|
|
1216
|
+
|
|
1217
|
+
for layout in self.layouts:
|
|
1218
|
+
layout.set_figsize(figsize)
|
|
1219
|
+
layout.freeze(figure, _debug=_debug)
|
|
1220
|
+
|
|
1221
|
+
if self._legend_axes is not None:
|
|
1222
|
+
bbox_w, bbox_h = self.get_bbox_size()
|
|
1223
|
+
ax, ay = main_anchor
|
|
1224
|
+
|
|
1225
|
+
side = self._legend_axes.side
|
|
1226
|
+
size = self._legend_axes.size
|
|
1227
|
+
|
|
1228
|
+
if side == "right":
|
|
1229
|
+
cx, cy = ax + bbox_w, ay
|
|
1230
|
+
cw, ch = size, bbox_h
|
|
1231
|
+
elif side == "left":
|
|
1232
|
+
cx, cy = ax - size, ay
|
|
1233
|
+
cw, ch = size, bbox_h
|
|
1234
|
+
elif side == "top":
|
|
1235
|
+
cx, cy = ax, ay + bbox_h
|
|
1236
|
+
cw, ch = bbox_w, size
|
|
1237
|
+
elif side == "bottom":
|
|
1238
|
+
cx, cy = ax, ay - size
|
|
1239
|
+
cw, ch = bbox_w, size
|
|
1240
|
+
|
|
1241
|
+
rect = get_axes_rect((cx, cy, cw, ch), figsize)
|
|
1242
|
+
ax = figure.add_axes(rect)
|
|
1243
|
+
if _debug:
|
|
1244
|
+
_debug_ax(ax, side=side, text="Legend Axes")
|
|
1245
|
+
self._legend_axes.ax = ax
|
|
1246
|
+
|
|
1247
|
+
self.figure = figure
|
|
1248
|
+
|
|
1249
|
+
def get_ax(self, layout_name, ax_name):
|
|
1250
|
+
return self._layouts_mapper[layout_name].get_ax(ax_name)
|
|
1251
|
+
|
|
1252
|
+
def get_main_ax(self, layout_name):
|
|
1253
|
+
return self._layouts_mapper[layout_name].get_main_ax()
|
|
@@ -120,7 +120,8 @@ class _SeabornBase(StatsBase):
|
|
|
120
120
|
|
|
121
121
|
orient = self.get_orient()
|
|
122
122
|
if self.side == "left":
|
|
123
|
-
ax.
|
|
123
|
+
if not ax.xaxis_inverted():
|
|
124
|
+
ax.invert_xaxis()
|
|
124
125
|
# barplot(data=data, orient=orient, ax=ax, **self.kws)
|
|
125
126
|
plotter = getattr(seaborn, self._seaborn_plot)
|
|
126
127
|
plotter(data=pdata, orient=orient, ax=ax, **options)
|
|
@@ -231,9 +231,11 @@ class Arc(StatsBase):
|
|
|
231
231
|
ax.set_ylim(lim * 1.1, 0)
|
|
232
232
|
ax.set_xlim(0, 1)
|
|
233
233
|
if self.side == "top":
|
|
234
|
-
ax.
|
|
234
|
+
if not ax.yaxis_inverted():
|
|
235
|
+
ax.invert_yaxis()
|
|
235
236
|
if self.side == "left":
|
|
236
|
-
ax.
|
|
237
|
+
if not ax.xaxis_inverted():
|
|
238
|
+
ax.invert_xaxis()
|
|
237
239
|
ax.set_axis_off()
|
|
238
240
|
|
|
239
241
|
def get_legends(self):
|
|
@@ -90,7 +90,8 @@ class Area(StatsBase):
|
|
|
90
90
|
ax.plot(data, x, **line_options)
|
|
91
91
|
ax.set_ylim(-0.5, len(data) - 0.5)
|
|
92
92
|
if self.side == "left":
|
|
93
|
-
ax.
|
|
93
|
+
if not ax.xaxis_inverted():
|
|
94
|
+
ax.invert_xaxis()
|
|
94
95
|
else:
|
|
95
96
|
ax.fill_between(x, data, **fill_options)
|
|
96
97
|
if self.add_outline:
|
|
@@ -142,7 +142,8 @@ class Numbers(_BarBase):
|
|
|
142
142
|
ax.set_ylim(0, lim)
|
|
143
143
|
|
|
144
144
|
if self.side == "left":
|
|
145
|
-
ax.
|
|
145
|
+
if not ax.xaxis_inverted():
|
|
146
|
+
ax.invert_xaxis()
|
|
146
147
|
|
|
147
148
|
if self.show_value:
|
|
148
149
|
ax.bar_label(self.bars, fmt=self.fmt, padding=self.value_pad, **self.props)
|
|
@@ -269,7 +270,8 @@ class CenterBar(_BarBase):
|
|
|
269
270
|
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{np.abs(x):g}"))
|
|
270
271
|
|
|
271
272
|
if self.is_flank:
|
|
272
|
-
ax.
|
|
273
|
+
if not ax.yaxis_inverted():
|
|
274
|
+
ax.invert_yaxis()
|
|
273
275
|
|
|
274
276
|
if self.show_value:
|
|
275
277
|
left_label = _format_labels(left_bar, self.fmt)
|
|
@@ -412,7 +414,8 @@ class StackBar(_BarBase):
|
|
|
412
414
|
else:
|
|
413
415
|
ax.set_xlim(0, lim)
|
|
414
416
|
if self.side == "left":
|
|
415
|
-
ax.
|
|
417
|
+
if not ax.xaxis_inverted():
|
|
418
|
+
ax.invert_xaxis()
|
|
416
419
|
|
|
417
420
|
# Hanlde data
|
|
418
421
|
if orient == "h":
|
|
@@ -171,9 +171,12 @@ class SeqLogo(StatsBase):
|
|
|
171
171
|
ax.set_xlim(0, lim)
|
|
172
172
|
ax.set_ylim(0, data.shape[1])
|
|
173
173
|
if self.is_flank:
|
|
174
|
-
ax.
|
|
174
|
+
if not ax.yaxis_inverted():
|
|
175
|
+
ax.invert_yaxis()
|
|
175
176
|
if self.side == "left":
|
|
176
|
-
ax.
|
|
177
|
+
if not ax.xaxis_inverted():
|
|
178
|
+
ax.invert_xaxis()
|
|
177
179
|
if self.side == "bottom":
|
|
178
|
-
ax.
|
|
180
|
+
if not ax.yaxis_inverted():
|
|
181
|
+
ax.invert_yaxis()
|
|
179
182
|
ax.set_axis_off()
|
|
@@ -210,7 +210,8 @@ class ColorMesh(MeshBase):
|
|
|
210
210
|
self._annotate_text(ax, mesh, texts)
|
|
211
211
|
# set the mesh for legend
|
|
212
212
|
|
|
213
|
-
ax.
|
|
213
|
+
if not ax.yaxis_inverted():
|
|
214
|
+
ax.invert_yaxis()
|
|
214
215
|
ax.set_axis_off()
|
|
215
216
|
|
|
216
217
|
|
|
@@ -391,7 +392,8 @@ class Colors(MeshBase):
|
|
|
391
392
|
**self.kwargs,
|
|
392
393
|
)
|
|
393
394
|
ax.set_axis_off()
|
|
394
|
-
ax.
|
|
395
|
+
if not ax.yaxis_inverted():
|
|
396
|
+
ax.invert_yaxis()
|
|
395
397
|
|
|
396
398
|
|
|
397
399
|
class SizedMesh(MeshBase):
|
|
@@ -646,7 +648,8 @@ class SizedMesh(MeshBase):
|
|
|
646
648
|
ax.set_axis_off()
|
|
647
649
|
ax.set_xlim(0, xticks[-1] + 0.5)
|
|
648
650
|
ax.set_ylim(0, yticks[-1] + 0.5)
|
|
649
|
-
ax.
|
|
651
|
+
if not ax.yaxis_inverted():
|
|
652
|
+
ax.invert_yaxis()
|
|
650
653
|
|
|
651
654
|
|
|
652
655
|
# TODO: A patch mesh
|
|
@@ -740,7 +743,8 @@ class MarkerMesh(MeshBase):
|
|
|
740
743
|
close_ticks(ax)
|
|
741
744
|
ax.set_xlim(0, xticks[-1] + 0.5)
|
|
742
745
|
ax.set_ylim(0, yticks[-1] + 0.5)
|
|
743
|
-
ax.
|
|
746
|
+
if not ax.yaxis_inverted():
|
|
747
|
+
ax.invert_yaxis()
|
|
744
748
|
if not self.frameon:
|
|
745
749
|
ax.set_axis_off()
|
|
746
750
|
|
|
@@ -806,6 +810,7 @@ class TextMesh(MeshBase):
|
|
|
806
810
|
close_ticks(ax)
|
|
807
811
|
ax.set_xlim(0, xticks[-1] + 0.5)
|
|
808
812
|
ax.set_ylim(0, yticks[-1] + 0.5)
|
|
809
|
-
ax.
|
|
813
|
+
if not ax.yaxis_inverted():
|
|
814
|
+
ax.invert_yaxis()
|
|
810
815
|
if not self.frameon:
|
|
811
816
|
ax.set_axis_off()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|