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.
Files changed (33) hide show
  1. {marsilea-0.4.6 → marsilea-0.4.8}/PKG-INFO +2 -2
  2. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/__init__.py +4 -2
  3. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/base.py +132 -9
  4. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/layout.py +352 -21
  5. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/_seaborn.py +2 -1
  6. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/bar.py +4 -2
  7. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/mesh.py +10 -5
  8. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/upset.py +1 -1
  9. {marsilea-0.4.6 → marsilea-0.4.8}/.gitignore +0 -0
  10. {marsilea-0.4.6 → marsilea-0.4.8}/LICENSE +0 -0
  11. {marsilea-0.4.6 → marsilea-0.4.8}/README.md +0 -0
  12. {marsilea-0.4.6 → marsilea-0.4.8}/pyproject.toml +0 -0
  13. {marsilea-0.4.6 → marsilea-0.4.8}/setup.py +0 -0
  14. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/_api.py +0 -0
  15. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/_deform.py +0 -0
  16. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/dataset.py +0 -0
  17. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/dendrogram.py +0 -0
  18. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/exceptions.py +0 -0
  19. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/heatmap.py +0 -0
  20. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/layers.py +0 -0
  21. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/__init__.py +0 -0
  22. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/_utils.py +0 -0
  23. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/arc.py +0 -0
  24. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/area.py +0 -0
  25. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/base.py +0 -0
  26. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/bio.py +0 -0
  27. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/images.py +0 -0
  28. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/range.py +0 -0
  29. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/plotter/text.py +0 -0
  30. {marsilea-0.4.6 → marsilea-0.4.8}/src/marsilea/utils.py +0 -0
  31. {marsilea-0.4.6 → marsilea-0.4.8}/src/oncoprinter/__init__.py +0 -0
  32. {marsilea-0.4.6 → marsilea-0.4.8}/src/oncoprinter/core.py +0 -0
  33. {marsilea-0.4.6 → marsilea-0.4.8}/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.8
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.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__(self, main_board: WhiteBoard):
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
- # self.main_board.remove_legends()
720
- self.layout = CompositeCrossLayout(self.main_board.layout)
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
- @staticmethod
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 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)
@@ -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)
@@ -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.invert_xaxis()
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.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()
@@ -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.figure = None
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