marsilea 0.4.6__tar.gz → 0.4.8__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.8}/PKG-INFO +2 -2
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/__init__.py +4 -2
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/base.py +132 -9
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/layout.py +352 -21
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/_seaborn.py +2 -1
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/bar.py +4 -2
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/mesh.py +10 -5
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/upset.py +1 -1
- {marsilea-0.4.6 → marsilea-0.4.8}/.gitignore +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/LICENSE +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/README.md +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/pyproject.toml +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/setup.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/_api.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/_deform.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/dataset.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/dendrogram.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/exceptions.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/heatmap.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/layers.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/__init__.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/_utils.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/arc.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/area.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/base.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/bio.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/images.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/range.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/text.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/utils.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/oncoprinter/__init__.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/src/oncoprinter/core.py +0 -0
- {marsilea-0.4.6 → marsilea-0.4.8}/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.8"
|
|
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
|
|
|
@@ -169,7 +169,14 @@ class LegendMaker:
|
|
|
169
169
|
for _, legs in legends.items():
|
|
170
170
|
for leg in legs:
|
|
171
171
|
try:
|
|
172
|
+
# Try to detach legend from figure
|
|
172
173
|
leg.remove()
|
|
174
|
+
# For matplotlib >= 3.10.0
|
|
175
|
+
if hasattr(leg, "_parent_figure"):
|
|
176
|
+
setattr(leg, "_parent_figure", None)
|
|
177
|
+
# For matplotlib < 3.10.0
|
|
178
|
+
if hasattr(leg, "figure"):
|
|
179
|
+
setattr(leg, "figure", None)
|
|
173
180
|
except Exception:
|
|
174
181
|
pass
|
|
175
182
|
|
|
@@ -711,20 +718,47 @@ class ZeroHeight(WhiteBoard):
|
|
|
711
718
|
|
|
712
719
|
|
|
713
720
|
class CompositeBoard(LegendMaker):
|
|
721
|
+
"""Layout multiple canvas
|
|
722
|
+
|
|
723
|
+
Parameters
|
|
724
|
+
----------
|
|
725
|
+
main_board : :class:`WhiteBoard` or :class:`ClusterBoard`
|
|
726
|
+
The main canvas
|
|
727
|
+
keep_legends : bool, default: False
|
|
728
|
+
Whether to keep the legends in each canvas
|
|
729
|
+
If False, you can group all legends with `.add_legends()`
|
|
730
|
+
align_main : bool, default: True
|
|
731
|
+
Whether to force the size of other canvas to align with the main canvas
|
|
732
|
+
margin : float, default: 0
|
|
733
|
+
The margin space reserved around the whole canvas
|
|
734
|
+
|
|
735
|
+
"""
|
|
736
|
+
|
|
714
737
|
layout: CompositeCrossLayout = None
|
|
715
738
|
figure: Figure = None
|
|
716
739
|
|
|
717
|
-
def __init__(
|
|
740
|
+
def __init__(
|
|
741
|
+
self,
|
|
742
|
+
main_board: WhiteBoard,
|
|
743
|
+
keep_legends=False,
|
|
744
|
+
align_main=True,
|
|
745
|
+
margin=0,
|
|
746
|
+
):
|
|
747
|
+
self.keep_legends = keep_legends
|
|
748
|
+
|
|
718
749
|
self.main_board = self.new_board(main_board)
|
|
719
|
-
|
|
720
|
-
|
|
750
|
+
if not keep_legends:
|
|
751
|
+
self.main_board.remove_legends()
|
|
752
|
+
self.layout = CompositeCrossLayout(
|
|
753
|
+
self.main_board.layout, align_main=align_main, margin=margin
|
|
754
|
+
)
|
|
721
755
|
self._board_list = [self.main_board]
|
|
756
|
+
|
|
722
757
|
super().__init__()
|
|
723
758
|
|
|
724
|
-
|
|
725
|
-
def new_board(board):
|
|
759
|
+
def new_board(self, board):
|
|
726
760
|
board = deepcopy(board)
|
|
727
|
-
if isinstance(board, LegendMaker):
|
|
761
|
+
if not self.keep_legends & isinstance(board, LegendMaker):
|
|
728
762
|
board.remove_legends()
|
|
729
763
|
return board
|
|
730
764
|
|
|
@@ -736,13 +770,16 @@ class CompositeBoard(LegendMaker):
|
|
|
736
770
|
"""Define behavior that vertical appends two grid"""
|
|
737
771
|
return self.append("bottom", other)
|
|
738
772
|
|
|
739
|
-
def append(self, side, other):
|
|
773
|
+
def append(self, side, other, pad=0):
|
|
740
774
|
if isinstance(other, Number):
|
|
741
775
|
self.layout.append(side, other)
|
|
742
776
|
else:
|
|
743
777
|
board = self.new_board(other)
|
|
744
778
|
self._board_list.append(board)
|
|
745
779
|
self.layout.append(side, board.layout)
|
|
780
|
+
|
|
781
|
+
if pad > 0:
|
|
782
|
+
self.layout.append(side, pad)
|
|
746
783
|
return self
|
|
747
784
|
|
|
748
785
|
def render(self, figure=None, scale=1):
|
|
@@ -777,6 +814,92 @@ class CompositeBoard(LegendMaker):
|
|
|
777
814
|
def get_ax(self, board_name, ax_name):
|
|
778
815
|
return self.layout.get_ax(board_name, ax_name)
|
|
779
816
|
|
|
817
|
+
def get_main_ax(self, name):
|
|
818
|
+
return self.layout.get_main_ax(name)
|
|
819
|
+
|
|
820
|
+
def set_margin(self, margin):
|
|
821
|
+
self.layout.set_margin(margin)
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
class StackBoard(LegendMaker):
|
|
825
|
+
"""Stack multiple boards
|
|
826
|
+
|
|
827
|
+
Parameters
|
|
828
|
+
----------
|
|
829
|
+
boards : list of :class:`WhiteBoard`, :class:`StackBoard`
|
|
830
|
+
|
|
831
|
+
"""
|
|
832
|
+
|
|
833
|
+
def __init__(
|
|
834
|
+
self,
|
|
835
|
+
boards: List[WhiteBoard, StackBoard],
|
|
836
|
+
direction="horizontal",
|
|
837
|
+
align="center",
|
|
838
|
+
spacing=0.2,
|
|
839
|
+
margin=0,
|
|
840
|
+
keep_legends=False,
|
|
841
|
+
):
|
|
842
|
+
self.keep_legends = keep_legends
|
|
843
|
+
board_list = []
|
|
844
|
+
layouts = []
|
|
845
|
+
for board in boards:
|
|
846
|
+
board = self.new_board(board)
|
|
847
|
+
board_list.append(board)
|
|
848
|
+
layouts.append(board.layout)
|
|
849
|
+
|
|
850
|
+
self.layout = StackCrossLayout(
|
|
851
|
+
layouts, margin=margin, direction=direction, align=align, spacing=spacing
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
self._board_list = board_list
|
|
855
|
+
super().__init__()
|
|
856
|
+
|
|
857
|
+
# To mimic the board API
|
|
858
|
+
def _freeze_flex_plots(self, figure):
|
|
859
|
+
for board in self._board_list:
|
|
860
|
+
board._freeze_flex_plots(figure)
|
|
861
|
+
|
|
862
|
+
def new_board(self, board):
|
|
863
|
+
board = deepcopy(board)
|
|
864
|
+
if not self.keep_legends & isinstance(board, LegendMaker):
|
|
865
|
+
board.remove_legends()
|
|
866
|
+
return board
|
|
867
|
+
|
|
868
|
+
def render(self, figure=None, scale=1):
|
|
869
|
+
if figure is None:
|
|
870
|
+
figure = plt.figure()
|
|
871
|
+
self._freeze_legend(figure)
|
|
872
|
+
for board in self._board_list:
|
|
873
|
+
board._freeze_flex_plots(figure)
|
|
874
|
+
self.layout.freeze(figure=figure, scale=scale)
|
|
875
|
+
self.figure = figure
|
|
876
|
+
for board in self._board_list:
|
|
877
|
+
board.render(figure=self.figure)
|
|
878
|
+
|
|
879
|
+
self._render_legend()
|
|
880
|
+
|
|
881
|
+
def save(self, fname, **kwargs):
|
|
882
|
+
if self.figure is not None:
|
|
883
|
+
save_options = dict(bbox_inches="tight")
|
|
884
|
+
save_options.update(kwargs)
|
|
885
|
+
self.figure.savefig(fname, **save_options)
|
|
886
|
+
else:
|
|
887
|
+
warnings.warn(
|
|
888
|
+
"Figure does not exist, " "please render it before saving as file."
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
def get_legends(self):
|
|
892
|
+
legends = {}
|
|
893
|
+
for m in self._board_list:
|
|
894
|
+
legends.update(m.get_legends())
|
|
895
|
+
return legends
|
|
896
|
+
|
|
897
|
+
def get_ax(self, board_name, ax_name):
|
|
898
|
+
return self.layout.get_ax(board_name, ax_name)
|
|
899
|
+
|
|
900
|
+
def get_main_ax(self, name):
|
|
901
|
+
return self.layout.get_main_ax(name)
|
|
902
|
+
|
|
780
903
|
def set_margin(self, margin):
|
|
781
904
|
self.layout.set_margin(margin)
|
|
782
905
|
|
|
@@ -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)
|
|
@@ -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)
|
|
@@ -412,7 +413,8 @@ class StackBar(_BarBase):
|
|
|
412
413
|
else:
|
|
413
414
|
ax.set_xlim(0, lim)
|
|
414
415
|
if self.side == "left":
|
|
415
|
-
ax.
|
|
416
|
+
if not ax.xaxis_inverted():
|
|
417
|
+
ax.invert_xaxis()
|
|
416
418
|
|
|
417
419
|
# Hanlde data
|
|
418
420
|
if orient == "h":
|
|
@@ -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()
|
|
@@ -947,7 +947,7 @@ class Upset(WhiteBoard):
|
|
|
947
947
|
def _extra_legends(self):
|
|
948
948
|
handles = [Patch(**entry) for entry in self._legend_entries]
|
|
949
949
|
highlight_legend = ListLegend(handles=handles, handlelength=2)
|
|
950
|
-
highlight_legend.
|
|
950
|
+
# highlight_legend.set_figure(None)
|
|
951
951
|
return {"highlight_subsets": [highlight_legend]}
|
|
952
952
|
|
|
953
953
|
def render(self, figure=None, scale=1):
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|