iplotx 0.11.1__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,13 +2,26 @@
2
2
  Module containing code to manipulate edge visualisations in 3D, especially the Edge3DCollection class.
3
3
  """
4
4
 
5
- from mpl_toolkits.mplot3d import Axes3D
5
+ import numpy as np
6
+ from matplotlib import (
7
+ colors as mcolors,
8
+ )
9
+ from matplotlib.collections import (
10
+ LineCollection,
11
+ )
12
+ from mpl_toolkits.mplot3d import (
13
+ Axes3D,
14
+ )
6
15
  from mpl_toolkits.mplot3d.art3d import (
7
16
  Line3DCollection,
17
+ _viewlim_mask,
8
18
  )
9
19
 
10
20
  from ...utils.matplotlib import (
11
21
  _forwarder,
22
+ _proj_transform_vectors,
23
+ _zalpha,
24
+ _get_data_scale,
12
25
  )
13
26
  from ...edge import (
14
27
  EdgeCollection,
@@ -73,11 +86,113 @@ class Edge3DCollection(Line3DCollection):
73
86
  segments3d.append(segment)
74
87
  self.set_segments(segments3d)
75
88
 
89
+ def do_3d_projection(self):
90
+ """
91
+ Project the points according to renderer matrix.
92
+ """
93
+ segments = np.asanyarray(self._segments3d)
94
+
95
+ mask = False
96
+ if np.ma.isMA(segments):
97
+ mask = segments.mask
98
+
99
+ if self._axlim_clip:
100
+ viewlim_mask = _viewlim_mask(
101
+ segments[..., 0], segments[..., 1], segments[..., 2], self.axes
102
+ )
103
+ if np.any(viewlim_mask):
104
+ # broadcast mask to 3D
105
+ viewlim_mask = np.broadcast_to(
106
+ viewlim_mask[..., np.newaxis], (*viewlim_mask.shape, 3)
107
+ )
108
+ mask = mask | viewlim_mask
109
+ xyzs = np.ma.array(_proj_transform_vectors(segments, self.axes.M), mask=mask)
110
+ segments_2d = xyzs[..., 0:2]
111
+ LineCollection.set_segments(self, segments_2d)
112
+
113
+ # Use the average projected z value of each line for depthshade
114
+ xyzs_mean = xyzs.mean(axis=1)
115
+ self._data_scale = _get_data_scale(*(xyzs_mean.T))
116
+ self._vzs = xyzs_mean[..., 2]
117
+
118
+ # FIXME
119
+ if len(xyzs) > 0:
120
+ minz = min(xyzs[..., 2].min(), 1e9)
121
+ else:
122
+ minz = np.nan
123
+ return minz
124
+
125
+ def _maybe_depth_shade_and_sort_colors(self, color_array):
126
+ color_array = (
127
+ _zalpha(
128
+ color_array,
129
+ self._vzs,
130
+ min_alpha=self._depthshade_minalpha,
131
+ _data_scale=self._data_scale,
132
+ )
133
+ if self._vzs is not None and self._depthshade
134
+ else color_array
135
+ )
136
+ return mcolors.to_rgba_array(color_array, self._alpha)
137
+
138
+ def set_edgecolor(self, color):
139
+ """Set the edge color of the collection."""
140
+ self._edgecolors = mcolors.to_rgba_array(color, self._alpha)
141
+ self._edgecolors_noshade = self._edgecolors.copy()
142
+
143
+ def get_edgecolor(self):
144
+ """Set the edge color of the collection, including depth shading."""
145
+ # We need this check here to make sure we do not double-apply the depth
146
+ # based alpha shading when the edge color is "face" which means the
147
+ # edge colour should be identical to the face colour.
148
+ if not hasattr(self, "_edgecolors_noshade"):
149
+ self._edgecolors_noshade = self._edgecolors.copy()
150
+ return self._maybe_depth_shade_and_sort_colors(self._edgecolors_noshade)
151
+
152
+ set_edgecolors = set_edgecolor
153
+ get_edgecolors = get_edgecolor
154
+
155
+ def get_depthshade(self):
156
+ """Get whether depth shading is performed on collection members."""
157
+ return self._depthshade
158
+
159
+ def get_depthshade_minalpha(self):
160
+ """The minimum alpha value used by depth-shading."""
161
+ return self._depthshade_minalpha
162
+
163
+ def set_depthshade(
164
+ self,
165
+ depthshade,
166
+ depthshade_minalpha=0.1,
167
+ ):
168
+ """
169
+ Set whether depth shading is performed on collection members.
170
+
171
+ Parameters
172
+ ----------
173
+ depthshade : bool
174
+ Whether to shade the patches in order to give the appearance of
175
+ depth.
176
+ depthshade_minalpha : float
177
+ Sets the minimum alpha value used by depth-shading.
178
+
179
+ .. versionadded:: 3.11
180
+ """
181
+ self._depthshade = depthshade
182
+ self._depthshade_minalpha = depthshade_minalpha
183
+ self.stale = True
184
+
76
185
  def _update_before_draw(self) -> None:
77
186
  """Update the collection before drawing."""
78
187
  if isinstance(self.axes, Axes3D) and hasattr(self, "do_3d_projection"):
79
188
  self.do_3d_projection()
80
189
 
190
+ if not hasattr(self, "_edgecolors_noshade"):
191
+ self._edgecolors_noshade = self._edgecolors.copy()
192
+ self._edgecolors = self._maybe_depth_shade_and_sort_colors(
193
+ self._edgecolors_noshade,
194
+ )
195
+
81
196
  # TODO: Here's where we would shorten the edges to fit the vertex
82
197
  # projections from 3D onto 2D, if we wanted to do that. Because edges
83
198
  # in 3D are chains of segments rathen than splines, the shortening
@@ -111,6 +226,7 @@ def edge_collection_2d_to_3d(
111
226
  col: EdgeCollection,
112
227
  zdir: str = "z",
113
228
  axlim_clip: bool = False,
229
+ depthshade: bool = True,
114
230
  ):
115
231
  """Convert a 2D EdgeCollection to a 3D Edge3DCollection.
116
232
 
@@ -127,6 +243,9 @@ def edge_collection_2d_to_3d(
127
243
  # NOTE: after this line, none of the EdgeCollection methods will work
128
244
  # It's become a static drawer now. It uses segments instead of paths.
129
245
  col.__class__ = Edge3DCollection
246
+ col._depthshade = depthshade
247
+ col._depthshade_minalpha = 0.1
248
+
130
249
  col._compute_edge_segments()
131
250
 
132
251
  col._axlim_clip = axlim_clip
iplotx/artists.py CHANGED
@@ -3,19 +3,21 @@ All artists defined in iplotx.
3
3
  """
4
4
 
5
5
  from .network import NetworkArtist
6
- from .tree import TreeArtist
6
+ from .network.groups import GroupingCollection
7
7
  from .vertex import VertexCollection
8
8
  from .edge import EdgeCollection
9
9
  from .label import LabelCollection
10
10
  from .edge.arrow import EdgeArrowCollection
11
11
  from .edge.leaf import LeafEdgeCollection
12
- from .cascades import CascadeCollection
13
12
  from .art3d.vertex import Vertex3DCollection
14
13
  from .art3d.edge import Edge3DCollection
14
+ from .tree import TreeArtist
15
+ from .tree.cascades import CascadeCollection
15
16
 
16
17
 
17
18
  ___all__ = (
18
19
  NetworkArtist,
20
+ GroupingCollection,
19
21
  TreeArtist,
20
22
  VertexCollection,
21
23
  EdgeCollection,
iplotx/edge/__init__.py CHANGED
@@ -705,6 +705,7 @@ def make_stub_patch(**kwargs):
705
705
  "norm",
706
706
  "split",
707
707
  "shrink",
708
+ "depthshade",
708
709
  # DEPRECATED
709
710
  "padding",
710
711
  ]
@@ -0,0 +1,59 @@
1
+ from typing import (
2
+ Any,
3
+ Optional,
4
+ Sequence,
5
+ )
6
+ import importlib
7
+
8
+ from ...typing import (
9
+ TreeDataProvider,
10
+ )
11
+
12
+
13
+ class DendropyDataProvider(TreeDataProvider):
14
+ def is_rooted(self) -> bool:
15
+ return True
16
+
17
+ def get_root(self) -> Any:
18
+ """Get the root of the tree."""
19
+ return next(self.preorder())
20
+
21
+ def preorder(self) -> Any:
22
+ """Preorder traversal of the tree.
23
+
24
+ NOTE: This will work on both entire Trees and Nodes (which means a subtree including self).
25
+ """
26
+ if hasattr(self.tree, "preorder_node_iter"):
27
+ return self.tree.preorder_node_iter()
28
+ return self.tree.preorder_iter()
29
+
30
+ def postorder(self) -> Any:
31
+ """Preorder traversal of the tree.
32
+
33
+ NOTE: This will work on both entire Trees and Nodes (which means a subtree including self).
34
+ """
35
+ if hasattr(self.tree, "postorder_node_iter"):
36
+ return self.tree.postorder_node_iter()
37
+ return self.tree.postorder_iter()
38
+
39
+ def get_leaves(self) -> Sequence[Any]:
40
+ """Get a list of leaves."""
41
+ return self.tree.leaf_nodes()
42
+
43
+ @staticmethod
44
+ def get_children(node: Any) -> Sequence[Any]:
45
+ return node.child_nodes()
46
+
47
+ @staticmethod
48
+ def get_branch_length(node: Any) -> Optional[float]:
49
+ return node.edge.length
50
+
51
+ @staticmethod
52
+ def check_dependencies() -> bool:
53
+ return importlib.util.find_spec("dendropy") is not None
54
+
55
+ @staticmethod
56
+ def tree_type():
57
+ import dendropy
58
+
59
+ return dendropy.Tree
@@ -7,33 +7,33 @@ import numpy as np
7
7
  import pandas as pd
8
8
  import matplotlib as mpl
9
9
 
10
- from .typing import (
10
+ from ..typing import (
11
11
  GraphType,
12
12
  LayoutType,
13
13
  )
14
- from .style import (
14
+ from ..style import (
15
15
  get_style,
16
16
  rotate_style,
17
17
  )
18
- from .utils.matplotlib import (
18
+ from ..utils.matplotlib import (
19
19
  _stale_wrapper,
20
20
  _forwarder,
21
21
  _build_cmap_fun,
22
22
  )
23
- from .ingest import (
23
+ from ..ingest import (
24
24
  ingest_network_data,
25
25
  )
26
- from .vertex import (
26
+ from ..vertex import (
27
27
  VertexCollection,
28
28
  )
29
- from .edge import (
29
+ from ..edge import (
30
30
  EdgeCollection,
31
31
  make_stub_patch as make_undirected_edge_patch,
32
32
  )
33
- from .art3d.vertex import (
33
+ from ..art3d.vertex import (
34
34
  vertex_collection_2d_to_3d,
35
35
  )
36
- from .art3d.edge import (
36
+ from ..art3d.edge import (
37
37
  Edge3DCollection,
38
38
  edge_collection_2d_to_3d,
39
39
  )
@@ -279,10 +279,11 @@ class NetworkArtist(mpl.artist.Artist):
279
279
  )
280
280
 
281
281
  if self.get_ndim() == 3:
282
+ depthshade = get_style(".vertex").get("depthshade", True)
282
283
  vertex_collection_2d_to_3d(
283
284
  self._vertices,
284
285
  zs=self.get_layout().iloc[:, 2].values,
285
- depthshade=False,
286
+ depthshade=depthshade,
286
287
  )
287
288
 
288
289
  def _add_edges(self):
@@ -367,8 +368,10 @@ class NetworkArtist(mpl.artist.Artist):
367
368
  self._edges.set_array(colorarray)
368
369
 
369
370
  if self.get_ndim() == 3:
371
+ depthshade = get_style(".edge").get("depthshade", True)
370
372
  edge_collection_2d_to_3d(
371
373
  self._edges,
374
+ depthshade=depthshade,
372
375
  )
373
376
 
374
377
  @_stale_wrapper
@@ -1,5 +1,5 @@
1
1
  """
2
- Module for vertex groupings code, especially the GroupingArtist class.
2
+ Module for vertex groupings code, especially the GroupingCollection class.
3
3
  """
4
4
 
5
5
  from typing import Union
@@ -9,22 +9,22 @@ import matplotlib as mpl
9
9
  from matplotlib.collections import PatchCollection
10
10
 
11
11
 
12
- from .typing import (
12
+ from ..typing import (
13
13
  GroupingType,
14
14
  LayoutType,
15
15
  )
16
- from .ingest.heuristics import (
16
+ from ..ingest.heuristics import (
17
17
  normalise_layout,
18
18
  normalise_grouping,
19
19
  )
20
- from .style import get_style, rotate_style
21
- from .utils.geometry import (
20
+ from ..style import get_style, rotate_style
21
+ from ..utils.geometry import (
22
22
  convex_hull,
23
23
  _compute_group_path_with_vertex_padding,
24
24
  )
25
25
 
26
26
 
27
- class GroupingArtist(PatchCollection):
27
+ class GroupingCollection(PatchCollection):
28
28
  """Matplotlib artist for a vertex grouping (clustering/cover).
29
29
 
30
30
  This class is used to plot patches surrounding groups of vertices in a network.
iplotx/plotting.py CHANGED
@@ -13,7 +13,7 @@ from .typing import (
13
13
  TreeType,
14
14
  )
15
15
  from .network import NetworkArtist
16
- from .groups import GroupingArtist
16
+ from .network.groups import GroupingCollection
17
17
  from .tree import TreeArtist
18
18
  from .style import context
19
19
 
@@ -101,7 +101,7 @@ def network(
101
101
  nwkart = None
102
102
 
103
103
  if grouping is not None:
104
- grpart = GroupingArtist(
104
+ grpart = GroupingCollection(
105
105
  grouping,
106
106
  layout,
107
107
  network=network,
iplotx/style/leaf_info.py CHANGED
@@ -42,6 +42,7 @@ nonrotating_leaves = (
42
42
  "angular",
43
43
  "curved",
44
44
  "capstyle",
45
+ "depthshade",
45
46
  )
46
47
 
47
48
  # Union of all style leaves (rotating and nonrotating)
@@ -10,38 +10,41 @@ import numpy as np
10
10
  import pandas as pd
11
11
  import matplotlib as mpl
12
12
 
13
- from .style import (
13
+ from ..style import (
14
14
  context,
15
15
  get_style,
16
16
  rotate_style,
17
17
  merge_styles,
18
18
  )
19
- from .utils.matplotlib import (
19
+ from ..utils.matplotlib import (
20
20
  _stale_wrapper,
21
21
  _forwarder,
22
22
  _build_cmap_fun,
23
23
  )
24
- from .ingest import (
24
+ from ..ingest import (
25
25
  ingest_tree_data,
26
26
  data_providers,
27
27
  )
28
- from .vertex import (
28
+ from ..vertex import (
29
29
  VertexCollection,
30
30
  )
31
- from .edge import (
31
+ from ..edge import (
32
32
  EdgeCollection,
33
33
  make_stub_patch as make_undirected_edge_patch,
34
34
  )
35
- from .edge.leaf import (
35
+ from ..edge.leaf import (
36
36
  LeafEdgeCollection,
37
37
  )
38
- from .label import (
38
+ from ..label import (
39
39
  LabelCollection,
40
40
  )
41
41
  from .cascades import (
42
42
  CascadeCollection,
43
43
  )
44
- from .network import (
44
+ from .scalebar import (
45
+ TreeScalebarArtist,
46
+ )
47
+ from ..network import (
45
48
  _update_from_internal,
46
49
  )
47
50
 
@@ -663,6 +666,40 @@ class TreeArtist(mpl.artist.Artist):
663
666
  """Get the orientation of the tree layout."""
664
667
  return self._ipx_internal_data.get("orientation", None)
665
668
 
669
+ def scalebar(
670
+ self,
671
+ loc: str = "upper left",
672
+ **kwargs,
673
+ ):
674
+ """Create scalebar for the tree.
675
+
676
+ Parameters:
677
+ legth: Length of the scalebar in data units.
678
+ loc: Location of the scalebar. Same options as `matplotlib.legend`.
679
+ kwargs: Additional keyword arguments passed to `TreeScalebarArtist`. These are
680
+ generally the same options that you would pass to a legend, such as
681
+ bbox_to_anchor, bbox_transform, etc.
682
+ Returns:
683
+ The artist with the tree scale bar.
684
+ """
685
+ if self.axes is None:
686
+ raise RuntimeError("Cannot add a scalebar if the artist is not in an Axes.")
687
+
688
+ scalebar = TreeScalebarArtist(
689
+ self,
690
+ layout=self.get_layout_name(),
691
+ loc=loc,
692
+ )
693
+
694
+ # Remove previous scalebars if any
695
+ for art in self.axes._children:
696
+ if isinstance(art, TreeScalebarArtist) and art._treeartist == self:
697
+ art.remove()
698
+
699
+ self.axes.add_artist(scalebar)
700
+
701
+ return scalebar
702
+
666
703
  def style_subtree(
667
704
  self,
668
705
  nodes: Sequence[Hashable],
@@ -5,16 +5,15 @@ from typing import (
5
5
  import warnings
6
6
  import numpy as np
7
7
  import pandas as pd
8
+ import matplotlib as mpl
8
9
 
9
- from .typing import (
10
+ from ..typing import (
10
11
  TreeType,
11
12
  )
12
- from .ingest.typing import (
13
+ from ..ingest.typing import (
13
14
  TreeDataProvider,
14
15
  )
15
- import matplotlib as mpl
16
-
17
- from .style import (
16
+ from ..style import (
18
17
  copy_with_deep_values,
19
18
  rotate_style,
20
19
  )
@@ -0,0 +1,326 @@
1
+ from typing import (
2
+ Any,
3
+ )
4
+ import numpy as np
5
+ from matplotlib import (
6
+ _api,
7
+ )
8
+ from matplotlib import collections as mcoll
9
+ from matplotlib.legend import Legend
10
+ from matplotlib.legend_handler import HandlerErrorbar
11
+ from matplotlib.lines import Line2D
12
+ from matplotlib.offsetbox import (
13
+ HPacker,
14
+ VPacker,
15
+ DrawingArea,
16
+ TextArea,
17
+ )
18
+
19
+
20
+ def _update_prop(legend_artist, orig_handle):
21
+ # NOTE: This is de facto a bug in mpl, because Line2D.set_linestyle()
22
+ # does two things: it reformats tuple-style dashing, and it sets the
23
+ # artist as stale. We want to do the former only here, so we reset
24
+ # the artist as the original stale state after calling it.
25
+ stale_orig = legend_artist.stale
26
+ legend_artist.set_linestyle(orig_handle.get_linestyle()[0])
27
+ legend_artist.stale = stale_orig
28
+
29
+ # These other properties can be set directly.
30
+ legend_artist._linewidth = orig_handle.get_linewidth()[0]
31
+ legend_artist._color = orig_handle.get_edgecolor()[0]
32
+ legend_artist._gapcolor = orig_handle._gapcolor
33
+
34
+
35
+ class TreeScalebarArtist(Legend):
36
+ def __init__(
37
+ self,
38
+ treeartist,
39
+ layout: str = "horizontal",
40
+ frameon=False,
41
+ **kwargs,
42
+ ):
43
+ handles = [treeartist.get_edges()]
44
+ labels = [""]
45
+ self._layout = layout
46
+ self._treeartist = treeartist
47
+
48
+ if layout == "vertical":
49
+ handler_kwargs = dict(xerr_size=0, yerr_size=1)
50
+ else:
51
+ handler_kwargs = dict(xerr_size=1)
52
+ handler = TreeLegendHandler(
53
+ update_func=_update_prop,
54
+ **handler_kwargs,
55
+ )
56
+
57
+ super().__init__(
58
+ treeartist.axes,
59
+ handles,
60
+ labels,
61
+ handler_map={handles[0]: handler},
62
+ frameon=frameon,
63
+ **kwargs,
64
+ )
65
+
66
+ def _init_legend_box(self, handles, labels, markerfirst=True):
67
+ """
68
+ Create the legend box.
69
+
70
+ This is a modified version of the original Legend._init_legend_box
71
+ method to accommodate a scale bar.
72
+ """
73
+
74
+ fontsize = self._fontsize
75
+
76
+ # legend_box is a HPacker, horizontally packed with columns.
77
+ # Each column is a VPacker, vertically packed with legend items.
78
+ # Each legend item is a HPacker packed with:
79
+ # - handlebox: a DrawingArea which contains the legend handle.
80
+ # - labelbox: a TextArea which contains the legend text.
81
+
82
+ text_list = [] # the list of text instances
83
+ handle_list = [] # the list of handle instances
84
+ handles_and_labels = []
85
+
86
+ # The approximate height and descent of text. These values are
87
+ # only used for plotting the legend handle.
88
+ descent = 0.35 * fontsize * (self.handleheight - 0.7) # heuristic.
89
+ height = fontsize * self.handleheight - descent
90
+ # each handle needs to be drawn inside a box of (x, y, w, h) =
91
+ # (0, -descent, width, height). And their coordinates should
92
+ # be given in the display coordinates.
93
+
94
+ # The transformation of each handle will be automatically set
95
+ # to self.get_transform(). If the artist does not use its
96
+ # default transform (e.g., Collections), you need to
97
+ # manually set their transform to the self.get_transform().
98
+ legend_handler_map = self.get_legend_handler_map()
99
+
100
+ for orig_handle, label in zip(handles, labels):
101
+ handler = self.get_legend_handler(legend_handler_map, orig_handle)
102
+ if handler is None:
103
+ _api.warn_external(
104
+ "Legend does not support handles for "
105
+ f"{type(orig_handle).__name__} "
106
+ "instances.\nA proxy artist may be used "
107
+ "instead.\nSee: https://matplotlib.org/"
108
+ "stable/users/explain/axes/legend_guide.html"
109
+ "#controlling-the-legend-entries"
110
+ )
111
+ # No handle for this artist, so we just defer to None.
112
+ handle_list.append(None)
113
+ else:
114
+ handlebox = DrawingArea(
115
+ width=self.handlelength * fontsize,
116
+ height=height,
117
+ xdescent=0.0,
118
+ ydescent=descent,
119
+ )
120
+ # Create the artist for the legend which represents the
121
+ # original artist/handle.
122
+ handle_list.append(handler.legend_artist(self, orig_handle, fontsize, handlebox))
123
+
124
+ # The scale bar line is in this handle
125
+ bar_handle = handle_list[-1]
126
+ label = self._get_label_from_bar_handle(bar_handle)
127
+
128
+ textbox = TextArea(
129
+ label,
130
+ multilinebaseline=True,
131
+ textprops=dict(
132
+ verticalalignment="baseline",
133
+ horizontalalignment="left",
134
+ fontproperties=self.prop,
135
+ ),
136
+ )
137
+ text_list.append(textbox._text)
138
+
139
+ handles_and_labels.append((handlebox, textbox))
140
+
141
+ columnbox = []
142
+ # array_split splits n handles_and_labels into ncols columns, with the
143
+ # first n%ncols columns having an extra entry. filter(len, ...)
144
+ # handles the case where n < ncols: the last ncols-n columns are empty
145
+ # and get filtered out.
146
+ for handles_and_labels_column in filter(
147
+ len, np.array_split(handles_and_labels, self._ncols)
148
+ ):
149
+ # pack handlebox and labelbox into itembox
150
+ if self._layout == "vertical":
151
+ itempacker = HPacker
152
+ else:
153
+ itempacker = VPacker
154
+ itemboxes = [
155
+ itempacker(
156
+ pad=0,
157
+ sep=self.handletextpad * fontsize,
158
+ children=[h, t] if markerfirst else [t, h],
159
+ align="center",
160
+ )
161
+ for h, t in handles_and_labels_column
162
+ ]
163
+ # pack columnbox
164
+ alignment = "baseline" if markerfirst else "right"
165
+ columnbox.append(
166
+ VPacker(
167
+ pad=0, sep=self.labelspacing * fontsize, align=alignment, children=itemboxes
168
+ )
169
+ )
170
+
171
+ mode = "expand" if self._mode == "expand" else "fixed"
172
+ sep = self.columnspacing * fontsize
173
+ self._legend_handle_box = HPacker(
174
+ pad=0, sep=sep, align="baseline", mode=mode, children=columnbox
175
+ )
176
+ self._legend_title_box = TextArea("")
177
+ self._legend_box = VPacker(
178
+ pad=self.borderpad * fontsize,
179
+ sep=self.labelspacing * fontsize,
180
+ align=self._alignment,
181
+ children=[self._legend_title_box, self._legend_handle_box],
182
+ )
183
+ self._legend_box.set_figure(self.get_figure(root=False))
184
+ self._legend_box.axes = self.axes
185
+ self.texts = text_list
186
+ self.legend_handles = handle_list
187
+
188
+ def _get_label_from_bar_handle(self, bar_handle: Any) -> str:
189
+ # Extract the x coordinates of the scale bar
190
+ p0, p1 = bar_handle.get_segments()[0]
191
+
192
+ bar_trans = bar_handle.get_transform()
193
+ data_trans = self.parent.transData
194
+ # FIXME: this is off, probably because of some Packer anchor additional transform
195
+ composite_trans = data_trans.inverted() + bar_trans
196
+
197
+ p0_data = composite_trans.transform(p0)
198
+ p1_data = composite_trans.transform(p1)
199
+ distance = np.linalg.norm(p1_data - p0_data)
200
+ label = f"{distance:.2f}"
201
+ return label
202
+
203
+ def draw(self, renderer):
204
+ bar_handle = self.legend_handles[0]
205
+ label = self._get_label_from_bar_handle(bar_handle)
206
+
207
+ text_handle = (
208
+ self._legend_box.get_children()[1].get_children()[0].get_children()[0].get_children()[1]
209
+ )
210
+ # Bypass stale=True (we are already redrawing)
211
+ text_handle.set_text(label)
212
+
213
+ super().draw(renderer)
214
+
215
+
216
+ class TreeLegendHandler(HandlerErrorbar):
217
+ def __init__(self, marker_size=6, **kw):
218
+ self.marker_size = marker_size
219
+ super().__init__(**kw)
220
+
221
+ def create_artists(
222
+ self,
223
+ legend,
224
+ orig_handle,
225
+ xdescent,
226
+ ydescent,
227
+ width,
228
+ height,
229
+ fontsize,
230
+ trans,
231
+ ):
232
+ # docstring inherited
233
+ plotline = orig_handle
234
+
235
+ xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent, width, height, fontsize)
236
+ ydata = np.full_like(xdata, (height - ydescent) / 2)
237
+
238
+ xdata_marker = np.asarray(xdata_marker)
239
+ ydata_marker = np.asarray(ydata[: len(xdata_marker)])
240
+
241
+ xerr_size, yerr_size = self.get_err_size(
242
+ legend, xdescent, ydescent, width, height, fontsize
243
+ )
244
+
245
+ if legend._layout == "vertical":
246
+ xdata, ydata = np.array(
247
+ [
248
+ ((x, y - yerr_size), (x, y + yerr_size))
249
+ for x, y in zip(xdata_marker, ydata_marker)
250
+ ]
251
+ ).T
252
+
253
+ legline = Line2D(xdata, ydata)
254
+
255
+ legline_marker = Line2D(xdata_marker, ydata_marker)
256
+
257
+ # when plotlines are None (only errorbars are drawn), we just
258
+ # make legline invisible.
259
+ if plotline is None:
260
+ legline.set_visible(False)
261
+ legline_marker.set_visible(False)
262
+ else:
263
+ self.update_prop(legline, plotline, legend)
264
+
265
+ legline.set_drawstyle("default")
266
+ legline.set_marker("none")
267
+
268
+ self.update_prop(legline_marker, plotline, legend)
269
+ legline_marker.set_linestyle("None")
270
+
271
+ if legend.markerscale != 1:
272
+ newsz = legline_marker.get_markersize() * legend.markerscale
273
+ legline_marker.set_markersize(newsz)
274
+
275
+ handle_barlinecols = []
276
+ handle_caplines = []
277
+
278
+ if legend._layout != "vertical":
279
+ verts = [
280
+ ((x - xerr_size, y), (x + xerr_size, y)) for x, y in zip(xdata_marker, ydata_marker)
281
+ ]
282
+ coll = mcoll.LineCollection(verts)
283
+ self.update_prop(coll, plotline, legend)
284
+ handle_barlinecols.append(coll)
285
+
286
+ # Always show the cap lines
287
+ if True:
288
+ capline_left = Line2D(xdata_marker - xerr_size, ydata_marker)
289
+ capline_right = Line2D(xdata_marker + xerr_size, ydata_marker)
290
+ self.update_prop(capline_left, plotline, legend)
291
+ self.update_prop(capline_right, plotline, legend)
292
+ capline_left.set_marker("|")
293
+ capline_right.set_marker("|")
294
+
295
+ handle_caplines.append(capline_left)
296
+ handle_caplines.append(capline_right)
297
+
298
+ else:
299
+ verts = [
300
+ ((x, y - yerr_size), (x, y + yerr_size)) for x, y in zip(xdata_marker, ydata_marker)
301
+ ]
302
+ coll = mcoll.LineCollection(verts)
303
+ self.update_prop(coll, plotline, legend)
304
+ handle_barlinecols.append(coll)
305
+
306
+ # Always show the cap lines
307
+ if True:
308
+ capline_left = Line2D(xdata_marker, ydata_marker - yerr_size)
309
+ capline_right = Line2D(xdata_marker, ydata_marker + yerr_size)
310
+ self.update_prop(capline_left, plotline, legend)
311
+ self.update_prop(capline_right, plotline, legend)
312
+ capline_left.set_marker("_")
313
+ capline_right.set_marker("_")
314
+
315
+ handle_caplines.append(capline_left)
316
+ handle_caplines.append(capline_right)
317
+
318
+ artists = [
319
+ *handle_barlinecols,
320
+ *handle_caplines,
321
+ legline,
322
+ legline_marker,
323
+ ]
324
+ for artist in artists:
325
+ artist.set_transform(trans)
326
+ return artists
@@ -4,6 +4,7 @@ from math import atan2
4
4
  import numpy as np
5
5
  import pandas as pd
6
6
  import matplotlib as mpl
7
+ import matplotlib.colors as mcolors
7
8
 
8
9
  from .geometry import (
9
10
  _evaluate_squared_bezier,
@@ -204,3 +205,80 @@ def _build_cmap_fun(
204
205
  norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
205
206
 
206
207
  return lambda x: cmap(norm(x))
208
+
209
+
210
+ # NOTE: this is polyfill from future matplotlib versions
211
+ # LICENSED UNDER THE MPL LICENSE from:
212
+ # https://github.com/matplotlib/matplotlib/blob/main/lib/mpl_toolkits/mplot3d/proj3d.py
213
+ def _proj_transform_vectors(vecs, M):
214
+ """
215
+ Vectorized version of ``_proj_transform_vec``.
216
+
217
+ Parameters
218
+ ----------
219
+ vecs : ... x 3 np.ndarray
220
+ Input vectors
221
+ M : 4 x 4 np.ndarray
222
+ Projection matrix
223
+ """
224
+ vecs_shape = vecs.shape
225
+ vecs = vecs.reshape(-1, 3).T
226
+
227
+ vecs_pad = np.empty((vecs.shape[0] + 1,) + vecs.shape[1:])
228
+ vecs_pad[:-1] = vecs
229
+ vecs_pad[-1] = 1
230
+ product = np.dot(M, vecs_pad)
231
+ tvecs = product[:3] / product[3]
232
+
233
+ return tvecs.T.reshape(vecs_shape)
234
+
235
+
236
+ def _zalpha(
237
+ colors,
238
+ zs,
239
+ min_alpha=0.3,
240
+ _data_scale=None,
241
+ ):
242
+ """Modify the alpha values of the color list according to z-depth."""
243
+
244
+ if len(colors) == 0 or len(zs) == 0:
245
+ return np.zeros((0, 4))
246
+
247
+ # Alpha values beyond the range 0-1 inclusive make no sense, so clip them
248
+ min_alpha = np.clip(min_alpha, 0, 1)
249
+
250
+ if _data_scale is None or _data_scale == 0:
251
+ # Don't scale the alpha values since we have no valid data scale for reference
252
+ sats = np.ones_like(zs)
253
+
254
+ else:
255
+ # Deeper points have an increasingly transparent appearance
256
+ sats = np.clip(1 - (zs - np.min(zs)) / _data_scale, min_alpha, 1)
257
+
258
+ rgba = np.broadcast_to(mcolors.to_rgba_array(colors), (len(zs), 4))
259
+
260
+ # Change the alpha values of the colors using the generated alpha multipliers
261
+ return np.column_stack([rgba[:, :3], rgba[:, 3] * sats])
262
+
263
+
264
+ def _get_data_scale(X, Y, Z):
265
+ """
266
+ Estimate the scale of the 3D data for use in depth shading
267
+
268
+ Parameters
269
+ ----------
270
+ X, Y, Z : masked arrays
271
+ The data to estimate the scale of.
272
+ """
273
+ # Account for empty datasets. Assume that X Y and Z have the same number
274
+ # of elements.
275
+ if not np.ma.count(X):
276
+ return 0
277
+
278
+ # Estimate the scale using the RSS of the ranges of the dimensions
279
+ # Note that we don't use np.ma.ptp() because we otherwise get a build
280
+ # warning about handing empty arrays.
281
+ ptp_x = X.max() - X.min()
282
+ ptp_y = Y.max() - Y.min()
283
+ ptp_z = Z.max() - Z.min()
284
+ return np.sqrt(ptp_x**2 + ptp_y**2 + ptp_z**2)
iplotx/version.py CHANGED
@@ -2,4 +2,4 @@
2
2
  iplotx version information module.
3
3
  """
4
4
 
5
- __version__ = "0.11.1"
5
+ __version__ = "1.0.0"
iplotx/vertex.py CHANGED
@@ -398,7 +398,7 @@ def make_patch(
398
398
  **kwargs,
399
399
  ) -> tuple[Patch, float]:
400
400
  """Make a patch of the given marker shape and size."""
401
- forbidden_props = ["label", "cmap", "norm", "cascade", "deep"]
401
+ forbidden_props = ["label", "cmap", "norm", "cascade", "deep", "depthshade"]
402
402
  for prop in forbidden_props:
403
403
  if prop in kwargs:
404
404
  kwargs.pop(prop)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iplotx
3
- Version: 0.11.1
3
+ Version: 1.0.0
4
4
  Summary: Plot networkx from igraph and networkx.
5
5
  Project-URL: Homepage, https://github.com/fabilab/iplotx
6
6
  Project-URL: Documentation, https://readthedocs.org/iplotx
@@ -10,8 +10,9 @@ Project-URL: Changelog, https://github.com/fabilab/iplotx/blob/main/CHANGELOG.md
10
10
  Author-email: Fabio Zanini <fabio.zanini@unsw.edu.au>
11
11
  Maintainer-email: Fabio Zanini <fabio.zanini@unsw.edu.au>
12
12
  License: MIT
13
- Keywords: graph,network,plotting,visualisation
13
+ Keywords: graph,network,phylogeny,plotting,tree,visualisation
14
14
  Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Framework :: Matplotlib
15
16
  Classifier: Intended Audience :: Developers
16
17
  Classifier: Intended Audience :: Education
17
18
  Classifier: Intended Audience :: Science/Research
@@ -52,13 +53,14 @@ Supports:
52
53
  - **networks**:
53
54
  - [networkx](https://networkx.org/)
54
55
  - [igraph](igraph.readthedocs.io/)
55
- - [minimal network data structure](https://iplotx.readthedocs.io/en/latest/gallery/plot_simplenetworkdataprovider.html#sphx-glr-gallery-plot-simplenetworkdataprovider-py) (for educational purposes)
56
+ - [minimal network data structure](https://iplotx.readthedocs.io/en/latest/gallery/plot_simplenetworkdataprovider.html#sphx-glr-gallery-plot-simplenetworkdataprovider-py) (zero dependency)
56
57
  - **trees**:
57
58
  - [ETE4](https://etetoolkit.github.io/ete/)
58
59
  - [cogent3](https://cogent3.org/)
59
60
  - [Biopython](https://biopython.org/)
60
61
  - [scikit-bio](https://scikit.bio)
61
- - [minimal tree data structure](https://iplotx.readthedocs.io/en/latest/gallery/tree/plot_simpletreedataprovider.html#sphx-glr-gallery-tree-plot-simpletreedataprovider-py) (for educational purposes)
62
+ - [dendropy](https://jeetsukumaran.github.io/DendroPy/index.html)
63
+ - [minimal tree data structure](https://iplotx.readthedocs.io/en/latest/gallery/tree/plot_simpletreedataprovider.html#sphx-glr-gallery-tree-plot-simpletreedataprovider-py) (zero dependency)
62
64
 
63
65
  In addition to the above, *any* network or tree analysis library can register an [entry point](https://iplotx.readthedocs.io/en/latest/providers.html#creating-a-custom-data-provider) to gain compatibility with `iplotx` with no intervention from our side.
64
66
 
@@ -89,15 +91,17 @@ See [gallery](https://iplotx.readthedocs.io/en/latest/gallery/index.html).
89
91
 
90
92
  ## Features
91
93
  - Plot networks from multiple libraries including networkx and igraph, using matplotlib as a backend. ✅
92
- - Plot trees from multiple libraries such as cogent3, ETE4, skbio, and biopython. ✅
94
+ - Plot trees from multiple libraries such as cogent3, ETE4, skbio, biopython, and dendropy. ✅
93
95
  - Flexible yet easy styling, including an internal library of styles ✅
94
96
  - Interactive plotting, e.g. zooming and panning after the plot is created. ✅
95
97
  - Store the plot to disk thanks to the many matplotlib backends (SVG, PNG, PDF, etc.). ✅
96
- - Efficient plotting of large graphs using matplotlib's collection functionality. ✅
98
+ - 3D network visualisation with depth shading. ✅
99
+ - Efficient plotting of large graphs (up to ~1 million nodes on a laptop). ✅
97
100
  - Edit plotting elements after the plot is created, e.g. changing node colors, labels, etc. ✅
98
101
  - Animations, e.g. showing the evolution of a network over time. ✅
99
102
  - Mouse and keyboard interaction, e.g. hovering over nodes/edges to get information about them. ✅
100
103
  - Node clustering and covers, e.g. showing communities in a network. ✅
104
+ - Edge tension, edge waypoints, and edge ports. ✅
101
105
  - Choice of tree layouts and orientations. ✅
102
106
  - Tree-specific options: cascades, subtree styling, split edges, etc. ✅
103
107
  - (WIP) Support uni- and bi-directional communication between graph object and plot object.🏗️
@@ -1,20 +1,16 @@
1
1
  iplotx/__init__.py,sha256=RzSct91jO8abrxOIn33rKEnDUgYpu1oj4olbObgX_hs,489
2
- iplotx/artists.py,sha256=XNtRwuvQdKkZCAejILydLD3J5B87sg5xPXuZFv_Gkk8,654
3
- iplotx/cascades.py,sha256=OPqF7Huls-HFmDA5MCF6DEZlUeRVaXsbQcHBoKAgNJs,8182
4
- iplotx/groups.py,sha256=g6ahm61BSBmd2weIjr40MvPi_GcNRgvNb9YklQsiza4,6784
2
+ iplotx/artists.py,sha256=2dBDT240zGwKb6tIc_y9pXeyU3LuYeF9wjj2tvi4KJo,730
5
3
  iplotx/label.py,sha256=7eS8ByadrhdIFOZz19U4VrS-oXY_ndFYNB-D4RZbFqI,9573
6
4
  iplotx/layout.py,sha256=KxmRLqjo8AYCBAmXez8rIiLU2sM34qhb6ox9AHYwRyE,4839
7
- iplotx/network.py,sha256=ae5rZwzWxmcBQXx1Y0q24jaXcM1hT1kip-JKsyk11QY,13385
8
- iplotx/plotting.py,sha256=icEefWJnS2lEGLp4t1LhDSP40JuvNKgOie3FDLOnTMk,13195
9
- iplotx/tree.py,sha256=TxbNoBHS0CfswrcMIWCNtnOl_3e4-PwCrVo0goywC0U,28807
5
+ iplotx/plotting.py,sha256=FvV33DCuEjJwO9ytiYJuQmfOywgF-cDANd6nEE5s8R0,13211
10
6
  iplotx/typing.py,sha256=QLdzV358IiD1CFe88MVp0D77FSx5sSAVUmM_2WPPE8I,1463
11
- iplotx/version.py,sha256=BVfqBj50ae0jogJGXsQXi_fML0WjuC4GLFRHZQWawYY,67
12
- iplotx/vertex.py,sha256=bjvAy9UciPWkA1J-SroWF9ZaTXRzNKtDZXBlZ80VM60,16026
7
+ iplotx/version.py,sha256=Zw6LAvjlzbItG1QBPJb1Tuqkb2PVUYsDwCVRYEJusgc,66
8
+ iplotx/vertex.py,sha256=_yYyvusn4vYvi6RBEW6CHa3vnbv43GnZylnMIaK4bG0,16040
13
9
  iplotx/art3d/vertex.py,sha256=Xf8Um30X2doCd8KdNN7332F6BxC4k72Mb_GeRAuzQfQ,2545
14
- iplotx/art3d/edge/__init__.py,sha256=EzzW06YEeyIu52gXormkGIobae-etwKevZ_PDBr-S9c,4624
10
+ iplotx/art3d/edge/__init__.py,sha256=uw1U_mMXqcZAvea-7JbU1PUKULQD1CMMrbwY02tiWRQ,8529
15
11
  iplotx/art3d/edge/arrow.py,sha256=14BFXY9kDOUGPZl2fMD9gRVGyaaN5kyd-l6ikBg6WHU,3601
16
12
  iplotx/art3d/edge/geometry.py,sha256=76VUmpPG-4Mls7x_994dMwdDPrWWnjT7nHJsHfwK_hA,2467
17
- iplotx/edge/__init__.py,sha256=wMKXD1h5SBaUv6HmebIc5wc9k8AuukaXzAOBu7epaqA,26341
13
+ iplotx/edge/__init__.py,sha256=lkrMkQFx9PNzorKc9trQ8MggC-nZSyALhvP78DMvhN4,26363
18
14
  iplotx/edge/arrow.py,sha256=U7vvBo7IMwo1qiyU9cyUEwraOaBcJLgdu9oU2OyoHL4,17453
19
15
  iplotx/edge/geometry.py,sha256=jkTMvQC5425GjB_fmGLIPJeSDAr_7NZF8zZDLTrSj34,15541
20
16
  iplotx/edge/leaf.py,sha256=SyGMv2PIOoH0pey8-aMVaZheK3hNe1Qz_okcyWbc4E4,4268
@@ -27,16 +23,22 @@ iplotx/ingest/providers/network/networkx.py,sha256=ehCg4npL073HX-eAG-VoP6refLPsM
27
23
  iplotx/ingest/providers/network/simple.py,sha256=e_aHhiHhN9DrMoNrt7tEMPURXGhQ1TYRPzsxDEptUlc,3766
28
24
  iplotx/ingest/providers/tree/biopython.py,sha256=4N_54cVyHHPcASJZGr6pHKE2p5R3i8Cm307SLlSLHLA,1480
29
25
  iplotx/ingest/providers/tree/cogent3.py,sha256=JmELbDK7LyybiJzFNbmeqZ4ySJoDajvFfJebpNfFKWo,1073
26
+ iplotx/ingest/providers/tree/dendropy.py,sha256=uRMe46PfDPUTeNInUO2Gbp4pVr-WIFIZQvrND2tovsg,1548
30
27
  iplotx/ingest/providers/tree/ete4.py,sha256=D7usSq0MOjzrk3EoLi834IlaDGwv7_qG6Qt0ptfKqfI,928
31
28
  iplotx/ingest/providers/tree/simple.py,sha256=aV9wGqBomJ5klM_aJQeuL_Q_J1pLCv6AFN98BPDiKUw,2593
32
29
  iplotx/ingest/providers/tree/skbio.py,sha256=O1KUr8tYi28pZ3VVjapgO4Uj-YpMuix3GhOH5je8Lv4,822
30
+ iplotx/network/__init__.py,sha256=oEv6f8oFYrtcI_NKabr8a_oIWTc1jXXTl_yO1xox_rE,13575
31
+ iplotx/network/groups.py,sha256=E_eYVXRHjv1DcyA4RupTkMa-rRFrIKkt9Rxn_Elw9Nc,6796
33
32
  iplotx/style/__init__.py,sha256=rf1GutrE8hHUhCoe4FGKYX-aNtHuu_U-kYQnqUxZNrY,10282
34
- iplotx/style/leaf_info.py,sha256=mcd6ewZl3jC0CPshmbeUkNp2geoihJW9515roGy2T8o,1000
33
+ iplotx/style/leaf_info.py,sha256=3xBn7xv9Uy2KAqdhM9S6ew5ZBJrGRTXRL3xXb8atfLw,1018
35
34
  iplotx/style/library.py,sha256=58Y8BlllGLsR4pQM7_PVCP5tH6_4GkchXZvJpqGHlcg,8534
35
+ iplotx/tree/__init__.py,sha256=6a8cbTd-OS-x8GEZJeRo3vneVjI98AiQW_gIo1H7h3Y,29969
36
+ iplotx/tree/cascades.py,sha256=on5GyqbWasl1zgK7bYXYQE0LOSfHc1z-1hnm0GWd6aw,8184
37
+ iplotx/tree/scalebar.py,sha256=QC2l-Nx39g8fkO1tHHvbrtzLncfQc9L57BQniSk2h5Q,11849
36
38
  iplotx/utils/geometry.py,sha256=6RrC6qaB0-1vIk1LhGA4CfsiMd-9JNniSPyL_l9mshE,9245
37
39
  iplotx/utils/internal.py,sha256=WWfcZDGK8Ut1y_tOHRGg9wSqY1bwSeLQO7dHM_8Tvwo,107
38
- iplotx/utils/matplotlib.py,sha256=wELE73quQv10-1w9uA5eDTgkZkylJvjg7pd3K5tZPOo,6294
40
+ iplotx/utils/matplotlib.py,sha256=p_53Oamof0RI4mtV8HrdDtZbgVqUxeUZ_KDvLZSiBUQ,8604
39
41
  iplotx/utils/style.py,sha256=vyNP80nDYVinqm6_9ltCJCtjK35ZcGlHvOskNv3eQBc,4225
40
- iplotx-0.11.1.dist-info/METADATA,sha256=1F0wH64PAw1_Bx-Qv7RSTgz6nwoQfcbRDJjmlR3KYfc,4880
41
- iplotx-0.11.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
42
- iplotx-0.11.1.dist-info/RECORD,,
42
+ iplotx-1.0.0.dist-info/METADATA,sha256=VsUs2_FiPrGNcfAfnL-T0fB9RObJCdAUvjaEUO46vFg,5086
43
+ iplotx-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
44
+ iplotx-1.0.0.dist-info/RECORD,,