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.
Files changed (33) hide show
  1. {marsilea-0.4.6 → marsilea-0.4.7}/PKG-INFO +2 -2
  2. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/__init__.py +4 -2
  3. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/base.py +125 -9
  4. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/layout.py +352 -21
  5. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/_seaborn.py +2 -1
  6. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/arc.py +4 -2
  7. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/area.py +2 -1
  8. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/bar.py +6 -3
  9. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/bio.py +6 -3
  10. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/mesh.py +10 -5
  11. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/range.py +2 -1
  12. {marsilea-0.4.6 → marsilea-0.4.7}/.gitignore +0 -0
  13. {marsilea-0.4.6 → marsilea-0.4.7}/LICENSE +0 -0
  14. {marsilea-0.4.6 → marsilea-0.4.7}/README.md +0 -0
  15. {marsilea-0.4.6 → marsilea-0.4.7}/pyproject.toml +0 -0
  16. {marsilea-0.4.6 → marsilea-0.4.7}/setup.py +0 -0
  17. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/_api.py +0 -0
  18. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/_deform.py +0 -0
  19. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/dataset.py +0 -0
  20. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/dendrogram.py +0 -0
  21. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/exceptions.py +0 -0
  22. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/heatmap.py +0 -0
  23. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/layers.py +0 -0
  24. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/__init__.py +0 -0
  25. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/_utils.py +0 -0
  26. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/base.py +0 -0
  27. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/images.py +0 -0
  28. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/plotter/text.py +0 -0
  29. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/upset.py +0 -0
  30. {marsilea-0.4.6 → marsilea-0.4.7}/src/marsilea/utils.py +0 -0
  31. {marsilea-0.4.6 → marsilea-0.4.7}/src/oncoprinter/__init__.py +0 -0
  32. {marsilea-0.4.6 → marsilea-0.4.7}/src/oncoprinter/core.py +0 -0
  33. {marsilea-0.4.6 → marsilea-0.4.7}/src/oncoprinter/preset.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: marsilea
3
- Version: 0.4.6
3
+ Version: 0.4.7
4
4
  Dynamic: Summary
5
5
  Project-URL: Home, https://github.com/Marsilea-viz/marsilea
6
6
  Author: Zhihang Zheng
@@ -1,6 +1,6 @@
1
1
  """Declarative creation of composable visualization"""
2
2
 
3
- __version__ = "0.4.6"
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__(self, main_board: WhiteBoard):
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
- # self.main_board.remove_legends()
720
- self.layout = CompositeCrossLayout(self.main_board.layout)
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
- @staticmethod
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 rect, a rect can be recorded by a left-bottom anchor point
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 (w + self.get_margin_w(), h + self.get_margin_h())
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()) * scale
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=self.figsize)
590
+ figure = plt.figure(figsize=figsize)
584
591
  else:
585
592
  if not self.is_composite:
586
- figure.set_size_inches(*self.figsize)
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 = self._reset_layout(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
- adjust = "height" if side in ["left", "right"] else "width"
713
- other = self._reset_layout(other)
714
- adjust_size = getattr(self, f"main_cell_{adjust}")
715
- getattr(other, f"set_main_{adjust}").__call__(adjust_size)
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.invert_xaxis()
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.invert_yaxis()
234
+ if not ax.yaxis_inverted():
235
+ ax.invert_yaxis()
235
236
  if self.side == "left":
236
- ax.invert_xaxis()
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.invert_xaxis()
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.invert_xaxis()
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.invert_yaxis()
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.invert_xaxis()
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.invert_yaxis()
174
+ if not ax.yaxis_inverted():
175
+ ax.invert_yaxis()
175
176
  if self.side == "left":
176
- ax.invert_xaxis()
177
+ if not ax.xaxis_inverted():
178
+ ax.invert_xaxis()
177
179
  if self.side == "bottom":
178
- ax.invert_yaxis()
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.invert_yaxis()
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.invert_yaxis()
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.invert_yaxis()
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.invert_yaxis()
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.invert_yaxis()
813
+ if not ax.yaxis_inverted():
814
+ ax.invert_yaxis()
810
815
  if not self.frameon:
811
816
  ax.set_axis_off()
@@ -125,7 +125,8 @@ class Range(StatsBase):
125
125
  else:
126
126
  ax.set_xlim(0, len(data))
127
127
  if self.side == "left":
128
- ax.invert_xaxis()
128
+ if not ax.xaxis_inverted():
129
+ ax.invert_xaxis()
129
130
 
130
131
  def get_legends(self):
131
132
  return [
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes