iplotx 0.12.0__py3-none-any.whl → 1.0.1__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.
- iplotx/artists.py +4 -2
- iplotx/ingest/providers/network/graph_tool.py +100 -0
- iplotx/{network.py → network/__init__.py} +8 -8
- iplotx/{groups.py → network/groups.py} +6 -6
- iplotx/plotting.py +2 -2
- iplotx/{tree.py → tree/__init__.py} +48 -8
- iplotx/{cascades.py → tree/cascades.py} +4 -5
- iplotx/tree/scalebar.py +327 -0
- iplotx/version.py +1 -1
- {iplotx-0.12.0.dist-info → iplotx-1.0.1.dist-info}/METADATA +7 -6
- {iplotx-0.12.0.dist-info → iplotx-1.0.1.dist-info}/RECORD +12 -10
- {iplotx-0.12.0.dist-info → iplotx-1.0.1.dist-info}/WHEEL +0 -0
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 .
|
|
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,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Optional,
|
|
3
|
+
Sequence,
|
|
4
|
+
)
|
|
5
|
+
from collections.abc import Hashable
|
|
6
|
+
import importlib
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from ....typing import (
|
|
11
|
+
LayoutType,
|
|
12
|
+
)
|
|
13
|
+
from ...heuristics import (
|
|
14
|
+
normalise_layout,
|
|
15
|
+
)
|
|
16
|
+
from ...typing import (
|
|
17
|
+
NetworkDataProvider,
|
|
18
|
+
NetworkData,
|
|
19
|
+
)
|
|
20
|
+
from ....utils.internal import (
|
|
21
|
+
_make_layout_columns,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GraphToolDataProvider(NetworkDataProvider):
|
|
26
|
+
def __call__(
|
|
27
|
+
self,
|
|
28
|
+
layout: Optional[LayoutType] = None,
|
|
29
|
+
vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
|
|
30
|
+
edge_labels: Optional[Sequence[str] | dict[str]] = None,
|
|
31
|
+
) -> NetworkData:
|
|
32
|
+
"""Create network data object for iplotx from an igraph object."""
|
|
33
|
+
|
|
34
|
+
# Get layout
|
|
35
|
+
vertex_df = normalise_layout(
|
|
36
|
+
layout,
|
|
37
|
+
network=self.network,
|
|
38
|
+
nvertices=self.number_of_vertices(),
|
|
39
|
+
)
|
|
40
|
+
ndim = vertex_df.shape[1]
|
|
41
|
+
vertex_df.columns = _make_layout_columns(ndim)
|
|
42
|
+
|
|
43
|
+
# Vertices are ordered integers, no gaps
|
|
44
|
+
|
|
45
|
+
# Vertex labels
|
|
46
|
+
# Recast vertex_labels=False as vertex_labels=None
|
|
47
|
+
if np.isscalar(vertex_labels) and (not vertex_labels):
|
|
48
|
+
vertex_labels = None
|
|
49
|
+
if vertex_labels is not None:
|
|
50
|
+
if np.isscalar(vertex_labels):
|
|
51
|
+
vertex_df["label"] = vertex_df.index.astype(str)
|
|
52
|
+
elif len(vertex_labels) != len(vertex_df):
|
|
53
|
+
raise ValueError("Vertex labels must be the same length as the number of vertices.")
|
|
54
|
+
else:
|
|
55
|
+
vertex_df["label"] = vertex_labels
|
|
56
|
+
|
|
57
|
+
# Edges are a list of tuples, because of multiedges
|
|
58
|
+
tmp = []
|
|
59
|
+
for edge in self.network.edges():
|
|
60
|
+
row = {"_ipx_source": edge.source(), "_ipx_target": edge.target()}
|
|
61
|
+
# TODO: add graph-tool edge attributes
|
|
62
|
+
# row.update(edge.attributes())
|
|
63
|
+
tmp.append(row)
|
|
64
|
+
if len(tmp):
|
|
65
|
+
edge_df = pd.DataFrame(tmp)
|
|
66
|
+
else:
|
|
67
|
+
edge_df = pd.DataFrame(columns=["_ipx_source", "_ipx_target"])
|
|
68
|
+
del tmp
|
|
69
|
+
|
|
70
|
+
# Edge labels
|
|
71
|
+
if edge_labels is not None:
|
|
72
|
+
if len(edge_labels) != len(edge_df):
|
|
73
|
+
raise ValueError("Edge labels must be the same length as the number of edges.")
|
|
74
|
+
edge_df["label"] = edge_labels
|
|
75
|
+
|
|
76
|
+
network_data = {
|
|
77
|
+
"vertex_df": vertex_df,
|
|
78
|
+
"edge_df": edge_df,
|
|
79
|
+
"directed": self.is_directed(),
|
|
80
|
+
"ndim": ndim,
|
|
81
|
+
}
|
|
82
|
+
return network_data
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def check_dependencies() -> bool:
|
|
86
|
+
return importlib.util.find_spec("graph_tool") is not None
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def graph_type():
|
|
90
|
+
import graph_tool.all as gt
|
|
91
|
+
|
|
92
|
+
return gt.Graph
|
|
93
|
+
|
|
94
|
+
def is_directed(self):
|
|
95
|
+
"""Whether the network is directed."""
|
|
96
|
+
return self.network.is_directed()
|
|
97
|
+
|
|
98
|
+
def number_of_vertices(self):
|
|
99
|
+
"""The number of vertices/nodes in the network."""
|
|
100
|
+
return self.network.num_vertices()
|
|
@@ -7,33 +7,33 @@ import numpy as np
|
|
|
7
7
|
import pandas as pd
|
|
8
8
|
import matplotlib as mpl
|
|
9
9
|
|
|
10
|
-
from
|
|
10
|
+
from ..typing import (
|
|
11
11
|
GraphType,
|
|
12
12
|
LayoutType,
|
|
13
13
|
)
|
|
14
|
-
from
|
|
14
|
+
from ..style import (
|
|
15
15
|
get_style,
|
|
16
16
|
rotate_style,
|
|
17
17
|
)
|
|
18
|
-
from
|
|
18
|
+
from ..utils.matplotlib import (
|
|
19
19
|
_stale_wrapper,
|
|
20
20
|
_forwarder,
|
|
21
21
|
_build_cmap_fun,
|
|
22
22
|
)
|
|
23
|
-
from
|
|
23
|
+
from ..ingest import (
|
|
24
24
|
ingest_network_data,
|
|
25
25
|
)
|
|
26
|
-
from
|
|
26
|
+
from ..vertex import (
|
|
27
27
|
VertexCollection,
|
|
28
28
|
)
|
|
29
|
-
from
|
|
29
|
+
from ..edge import (
|
|
30
30
|
EdgeCollection,
|
|
31
31
|
make_stub_patch as make_undirected_edge_patch,
|
|
32
32
|
)
|
|
33
|
-
from
|
|
33
|
+
from ..art3d.vertex import (
|
|
34
34
|
vertex_collection_2d_to_3d,
|
|
35
35
|
)
|
|
36
|
-
from
|
|
36
|
+
from ..art3d.edge import (
|
|
37
37
|
Edge3DCollection,
|
|
38
38
|
edge_collection_2d_to_3d,
|
|
39
39
|
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Module for vertex groupings code, especially the
|
|
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
|
|
12
|
+
from ..typing import (
|
|
13
13
|
GroupingType,
|
|
14
14
|
LayoutType,
|
|
15
15
|
)
|
|
16
|
-
from
|
|
16
|
+
from ..ingest.heuristics import (
|
|
17
17
|
normalise_layout,
|
|
18
18
|
normalise_grouping,
|
|
19
19
|
)
|
|
20
|
-
from
|
|
21
|
-
from
|
|
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
|
|
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
|
|
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 =
|
|
104
|
+
grpart = GroupingCollection(
|
|
105
105
|
grouping,
|
|
106
106
|
layout,
|
|
107
107
|
network=network,
|
|
@@ -10,38 +10,41 @@ import numpy as np
|
|
|
10
10
|
import pandas as pd
|
|
11
11
|
import matplotlib as mpl
|
|
12
12
|
|
|
13
|
-
from
|
|
13
|
+
from ..style import (
|
|
14
14
|
context,
|
|
15
15
|
get_style,
|
|
16
16
|
rotate_style,
|
|
17
17
|
merge_styles,
|
|
18
18
|
)
|
|
19
|
-
from
|
|
19
|
+
from ..utils.matplotlib import (
|
|
20
20
|
_stale_wrapper,
|
|
21
21
|
_forwarder,
|
|
22
22
|
_build_cmap_fun,
|
|
23
23
|
)
|
|
24
|
-
from
|
|
24
|
+
from ..ingest import (
|
|
25
25
|
ingest_tree_data,
|
|
26
26
|
data_providers,
|
|
27
27
|
)
|
|
28
|
-
from
|
|
28
|
+
from ..vertex import (
|
|
29
29
|
VertexCollection,
|
|
30
30
|
)
|
|
31
|
-
from
|
|
31
|
+
from ..edge import (
|
|
32
32
|
EdgeCollection,
|
|
33
33
|
make_stub_patch as make_undirected_edge_patch,
|
|
34
34
|
)
|
|
35
|
-
from
|
|
35
|
+
from ..edge.leaf import (
|
|
36
36
|
LeafEdgeCollection,
|
|
37
37
|
)
|
|
38
|
-
from
|
|
38
|
+
from ..label import (
|
|
39
39
|
LabelCollection,
|
|
40
40
|
)
|
|
41
41
|
from .cascades import (
|
|
42
42
|
CascadeCollection,
|
|
43
43
|
)
|
|
44
|
-
from .
|
|
44
|
+
from .scalebar import (
|
|
45
|
+
TreeScalebarArtist,
|
|
46
|
+
)
|
|
47
|
+
from ..network import (
|
|
45
48
|
_update_from_internal,
|
|
46
49
|
)
|
|
47
50
|
|
|
@@ -663,6 +666,43 @@ 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
|
+
label_format: str = ".2f",
|
|
673
|
+
**kwargs,
|
|
674
|
+
):
|
|
675
|
+
"""Create scalebar for the tree.
|
|
676
|
+
|
|
677
|
+
Parameters:
|
|
678
|
+
legth: Length of the scalebar in data units.
|
|
679
|
+
loc: Location of the scalebar. Same options as `matplotlib.legend`.
|
|
680
|
+
kwargs: Additional keyword arguments passed to `TreeScalebarArtist`. These are
|
|
681
|
+
generally the same options that you would pass to a legend, such as
|
|
682
|
+
bbox_to_anchor, bbox_transform, etc.
|
|
683
|
+
Returns:
|
|
684
|
+
The artist with the tree scale bar.
|
|
685
|
+
"""
|
|
686
|
+
if self.axes is None:
|
|
687
|
+
raise RuntimeError("Cannot add a scalebar if the artist is not in an Axes.")
|
|
688
|
+
|
|
689
|
+
scalebar = TreeScalebarArtist(
|
|
690
|
+
self,
|
|
691
|
+
layout=self.get_layout_name(),
|
|
692
|
+
loc=loc,
|
|
693
|
+
label_format=label_format,
|
|
694
|
+
**kwargs,
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
# Remove previous scalebars if any
|
|
698
|
+
for art in self.axes._children:
|
|
699
|
+
if isinstance(art, TreeScalebarArtist) and art._treeartist == self:
|
|
700
|
+
art.remove()
|
|
701
|
+
|
|
702
|
+
self.axes.add_artist(scalebar)
|
|
703
|
+
|
|
704
|
+
return scalebar
|
|
705
|
+
|
|
666
706
|
def style_subtree(
|
|
667
707
|
self,
|
|
668
708
|
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
|
|
10
|
+
from ..typing import (
|
|
10
11
|
TreeType,
|
|
11
12
|
)
|
|
12
|
-
from
|
|
13
|
+
from ..ingest.typing import (
|
|
13
14
|
TreeDataProvider,
|
|
14
15
|
)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
from .style import (
|
|
16
|
+
from ..style import (
|
|
18
17
|
copy_with_deep_values,
|
|
19
18
|
rotate_style,
|
|
20
19
|
)
|
iplotx/tree/scalebar.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
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: bool = False,
|
|
41
|
+
label_format: str = ".2f",
|
|
42
|
+
**kwargs,
|
|
43
|
+
):
|
|
44
|
+
handles = [treeartist.get_edges()]
|
|
45
|
+
labels = [""]
|
|
46
|
+
self._layout = layout
|
|
47
|
+
self._treeartist = treeartist
|
|
48
|
+
self._label_format = label_format
|
|
49
|
+
|
|
50
|
+
if layout == "vertical":
|
|
51
|
+
handler_kwargs = dict(xerr_size=0, yerr_size=1)
|
|
52
|
+
else:
|
|
53
|
+
handler_kwargs = dict(xerr_size=1)
|
|
54
|
+
handler = TreeLegendHandler(
|
|
55
|
+
update_func=_update_prop,
|
|
56
|
+
**handler_kwargs,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
super().__init__(
|
|
60
|
+
treeartist.axes,
|
|
61
|
+
handles,
|
|
62
|
+
labels,
|
|
63
|
+
handler_map={handles[0]: handler},
|
|
64
|
+
frameon=frameon,
|
|
65
|
+
**kwargs,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def _init_legend_box(self, handles, labels, markerfirst=True):
|
|
69
|
+
"""
|
|
70
|
+
Create the legend box.
|
|
71
|
+
|
|
72
|
+
This is a modified version of the original Legend._init_legend_box
|
|
73
|
+
method to accommodate a scale bar.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
fontsize = self._fontsize
|
|
77
|
+
|
|
78
|
+
# legend_box is a HPacker, horizontally packed with columns.
|
|
79
|
+
# Each column is a VPacker, vertically packed with legend items.
|
|
80
|
+
# Each legend item is a HPacker packed with:
|
|
81
|
+
# - handlebox: a DrawingArea which contains the legend handle.
|
|
82
|
+
# - labelbox: a TextArea which contains the legend text.
|
|
83
|
+
|
|
84
|
+
text_list = [] # the list of text instances
|
|
85
|
+
handle_list = [] # the list of handle instances
|
|
86
|
+
handles_and_labels = []
|
|
87
|
+
|
|
88
|
+
# The approximate height and descent of text. These values are
|
|
89
|
+
# only used for plotting the legend handle.
|
|
90
|
+
descent = 0.35 * fontsize * (self.handleheight - 0.7) # heuristic.
|
|
91
|
+
height = fontsize * self.handleheight - descent
|
|
92
|
+
# each handle needs to be drawn inside a box of (x, y, w, h) =
|
|
93
|
+
# (0, -descent, width, height). And their coordinates should
|
|
94
|
+
# be given in the display coordinates.
|
|
95
|
+
|
|
96
|
+
# The transformation of each handle will be automatically set
|
|
97
|
+
# to self.get_transform(). If the artist does not use its
|
|
98
|
+
# default transform (e.g., Collections), you need to
|
|
99
|
+
# manually set their transform to the self.get_transform().
|
|
100
|
+
legend_handler_map = self.get_legend_handler_map()
|
|
101
|
+
|
|
102
|
+
for orig_handle, label in zip(handles, labels):
|
|
103
|
+
handler = self.get_legend_handler(legend_handler_map, orig_handle)
|
|
104
|
+
if handler is None:
|
|
105
|
+
_api.warn_external(
|
|
106
|
+
"Legend does not support handles for "
|
|
107
|
+
f"{type(orig_handle).__name__} "
|
|
108
|
+
"instances.\nA proxy artist may be used "
|
|
109
|
+
"instead.\nSee: https://matplotlib.org/"
|
|
110
|
+
"stable/users/explain/axes/legend_guide.html"
|
|
111
|
+
"#controlling-the-legend-entries"
|
|
112
|
+
)
|
|
113
|
+
# No handle for this artist, so we just defer to None.
|
|
114
|
+
handle_list.append(None)
|
|
115
|
+
else:
|
|
116
|
+
handlebox = DrawingArea(
|
|
117
|
+
width=self.handlelength * fontsize,
|
|
118
|
+
height=height,
|
|
119
|
+
xdescent=0.0,
|
|
120
|
+
ydescent=descent,
|
|
121
|
+
)
|
|
122
|
+
# Create the artist for the legend which represents the
|
|
123
|
+
# original artist/handle.
|
|
124
|
+
handle_list.append(handler.legend_artist(self, orig_handle, fontsize, handlebox))
|
|
125
|
+
|
|
126
|
+
# The scale bar line is in this handle
|
|
127
|
+
bar_handle = handle_list[-1]
|
|
128
|
+
label = self._get_label_from_bar_handle(bar_handle)
|
|
129
|
+
|
|
130
|
+
textbox = TextArea(
|
|
131
|
+
label,
|
|
132
|
+
multilinebaseline=True,
|
|
133
|
+
textprops=dict(
|
|
134
|
+
verticalalignment="baseline",
|
|
135
|
+
horizontalalignment="left",
|
|
136
|
+
fontproperties=self.prop,
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
text_list.append(textbox._text)
|
|
140
|
+
|
|
141
|
+
handles_and_labels.append((handlebox, textbox))
|
|
142
|
+
|
|
143
|
+
columnbox = []
|
|
144
|
+
# array_split splits n handles_and_labels into ncols columns, with the
|
|
145
|
+
# first n%ncols columns having an extra entry. filter(len, ...)
|
|
146
|
+
# handles the case where n < ncols: the last ncols-n columns are empty
|
|
147
|
+
# and get filtered out.
|
|
148
|
+
for handles_and_labels_column in filter(
|
|
149
|
+
len, np.array_split(handles_and_labels, self._ncols)
|
|
150
|
+
):
|
|
151
|
+
# pack handlebox and labelbox into itembox
|
|
152
|
+
if self._layout == "vertical":
|
|
153
|
+
itempacker = HPacker
|
|
154
|
+
else:
|
|
155
|
+
itempacker = VPacker
|
|
156
|
+
itemboxes = [
|
|
157
|
+
itempacker(
|
|
158
|
+
pad=0,
|
|
159
|
+
sep=self.handletextpad * fontsize,
|
|
160
|
+
children=[h, t] if markerfirst else [t, h],
|
|
161
|
+
align="center",
|
|
162
|
+
)
|
|
163
|
+
for h, t in handles_and_labels_column
|
|
164
|
+
]
|
|
165
|
+
# pack columnbox
|
|
166
|
+
alignment = "baseline" if markerfirst else "right"
|
|
167
|
+
columnbox.append(
|
|
168
|
+
VPacker(
|
|
169
|
+
pad=0, sep=self.labelspacing * fontsize, align=alignment, children=itemboxes
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
mode = "expand" if self._mode == "expand" else "fixed"
|
|
174
|
+
sep = self.columnspacing * fontsize
|
|
175
|
+
self._legend_handle_box = HPacker(
|
|
176
|
+
pad=0, sep=sep, align="baseline", mode=mode, children=columnbox
|
|
177
|
+
)
|
|
178
|
+
self._legend_title_box = TextArea("")
|
|
179
|
+
self._legend_box = VPacker(
|
|
180
|
+
pad=self.borderpad * fontsize,
|
|
181
|
+
sep=self.labelspacing * fontsize,
|
|
182
|
+
align=self._alignment,
|
|
183
|
+
children=[self._legend_title_box, self._legend_handle_box],
|
|
184
|
+
)
|
|
185
|
+
self._legend_box.set_figure(self.get_figure(root=False))
|
|
186
|
+
self._legend_box.axes = self.axes
|
|
187
|
+
self.texts = text_list
|
|
188
|
+
self.legend_handles = handle_list
|
|
189
|
+
|
|
190
|
+
def _get_label_from_bar_handle(self, bar_handle: Any) -> str:
|
|
191
|
+
# Extract the x coordinates of the scale bar
|
|
192
|
+
p0, p1 = bar_handle.get_segments()[0]
|
|
193
|
+
|
|
194
|
+
bar_trans = bar_handle.get_transform()
|
|
195
|
+
data_trans = self.parent.transData
|
|
196
|
+
composite_trans = data_trans.inverted() + bar_trans
|
|
197
|
+
|
|
198
|
+
p0_data = composite_trans.transform(p0)
|
|
199
|
+
p1_data = composite_trans.transform(p1)
|
|
200
|
+
distance = np.linalg.norm(p1_data - p0_data)
|
|
201
|
+
label = format(distance, self._label_format)
|
|
202
|
+
return label
|
|
203
|
+
|
|
204
|
+
def draw(self, renderer):
|
|
205
|
+
bar_handle = self.legend_handles[0]
|
|
206
|
+
label = self._get_label_from_bar_handle(bar_handle)
|
|
207
|
+
|
|
208
|
+
text_handle = (
|
|
209
|
+
self._legend_box.get_children()[1].get_children()[0].get_children()[0].get_children()[1]
|
|
210
|
+
)
|
|
211
|
+
# Bypass stale=True (we are already redrawing)
|
|
212
|
+
text_handle.set_text(label)
|
|
213
|
+
|
|
214
|
+
super().draw(renderer)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class TreeLegendHandler(HandlerErrorbar):
|
|
218
|
+
def __init__(self, marker_size=6, **kw):
|
|
219
|
+
self.marker_size = marker_size
|
|
220
|
+
super().__init__(**kw)
|
|
221
|
+
|
|
222
|
+
def create_artists(
|
|
223
|
+
self,
|
|
224
|
+
legend,
|
|
225
|
+
orig_handle,
|
|
226
|
+
xdescent,
|
|
227
|
+
ydescent,
|
|
228
|
+
width,
|
|
229
|
+
height,
|
|
230
|
+
fontsize,
|
|
231
|
+
trans,
|
|
232
|
+
):
|
|
233
|
+
# docstring inherited
|
|
234
|
+
plotline = orig_handle
|
|
235
|
+
|
|
236
|
+
xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent, width, height, fontsize)
|
|
237
|
+
ydata = np.full_like(xdata, (height - ydescent) / 2)
|
|
238
|
+
|
|
239
|
+
xdata_marker = np.asarray(xdata_marker)
|
|
240
|
+
ydata_marker = np.asarray(ydata[: len(xdata_marker)])
|
|
241
|
+
|
|
242
|
+
xerr_size, yerr_size = self.get_err_size(
|
|
243
|
+
legend, xdescent, ydescent, width, height, fontsize
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
if legend._layout == "vertical":
|
|
247
|
+
xdata, ydata = np.array(
|
|
248
|
+
[
|
|
249
|
+
((x, y - yerr_size), (x, y + yerr_size))
|
|
250
|
+
for x, y in zip(xdata_marker, ydata_marker)
|
|
251
|
+
]
|
|
252
|
+
).T
|
|
253
|
+
|
|
254
|
+
legline = Line2D(xdata, ydata)
|
|
255
|
+
|
|
256
|
+
legline_marker = Line2D(xdata_marker, ydata_marker)
|
|
257
|
+
|
|
258
|
+
# when plotlines are None (only errorbars are drawn), we just
|
|
259
|
+
# make legline invisible.
|
|
260
|
+
if plotline is None:
|
|
261
|
+
legline.set_visible(False)
|
|
262
|
+
legline_marker.set_visible(False)
|
|
263
|
+
else:
|
|
264
|
+
self.update_prop(legline, plotline, legend)
|
|
265
|
+
|
|
266
|
+
legline.set_drawstyle("default")
|
|
267
|
+
legline.set_marker("none")
|
|
268
|
+
|
|
269
|
+
self.update_prop(legline_marker, plotline, legend)
|
|
270
|
+
legline_marker.set_linestyle("None")
|
|
271
|
+
|
|
272
|
+
if legend.markerscale != 1:
|
|
273
|
+
newsz = legline_marker.get_markersize() * legend.markerscale
|
|
274
|
+
legline_marker.set_markersize(newsz)
|
|
275
|
+
|
|
276
|
+
handle_barlinecols = []
|
|
277
|
+
handle_caplines = []
|
|
278
|
+
|
|
279
|
+
if legend._layout != "vertical":
|
|
280
|
+
verts = [
|
|
281
|
+
((x - xerr_size, y), (x + xerr_size, y)) for x, y in zip(xdata_marker, ydata_marker)
|
|
282
|
+
]
|
|
283
|
+
coll = mcoll.LineCollection(verts)
|
|
284
|
+
self.update_prop(coll, plotline, legend)
|
|
285
|
+
handle_barlinecols.append(coll)
|
|
286
|
+
|
|
287
|
+
# Always show the cap lines
|
|
288
|
+
if True:
|
|
289
|
+
capline_left = Line2D(xdata_marker - xerr_size, ydata_marker)
|
|
290
|
+
capline_right = Line2D(xdata_marker + xerr_size, ydata_marker)
|
|
291
|
+
self.update_prop(capline_left, plotline, legend)
|
|
292
|
+
self.update_prop(capline_right, plotline, legend)
|
|
293
|
+
capline_left.set_marker("|")
|
|
294
|
+
capline_right.set_marker("|")
|
|
295
|
+
|
|
296
|
+
handle_caplines.append(capline_left)
|
|
297
|
+
handle_caplines.append(capline_right)
|
|
298
|
+
|
|
299
|
+
else:
|
|
300
|
+
verts = [
|
|
301
|
+
((x, y - yerr_size), (x, y + yerr_size)) for x, y in zip(xdata_marker, ydata_marker)
|
|
302
|
+
]
|
|
303
|
+
coll = mcoll.LineCollection(verts)
|
|
304
|
+
self.update_prop(coll, plotline, legend)
|
|
305
|
+
handle_barlinecols.append(coll)
|
|
306
|
+
|
|
307
|
+
# Always show the cap lines
|
|
308
|
+
if True:
|
|
309
|
+
capline_left = Line2D(xdata_marker, ydata_marker - yerr_size)
|
|
310
|
+
capline_right = Line2D(xdata_marker, ydata_marker + yerr_size)
|
|
311
|
+
self.update_prop(capline_left, plotline, legend)
|
|
312
|
+
self.update_prop(capline_right, plotline, legend)
|
|
313
|
+
capline_left.set_marker("_")
|
|
314
|
+
capline_right.set_marker("_")
|
|
315
|
+
|
|
316
|
+
handle_caplines.append(capline_left)
|
|
317
|
+
handle_caplines.append(capline_right)
|
|
318
|
+
|
|
319
|
+
artists = [
|
|
320
|
+
*handle_barlinecols,
|
|
321
|
+
*handle_caplines,
|
|
322
|
+
legline,
|
|
323
|
+
legline_marker,
|
|
324
|
+
]
|
|
325
|
+
for artist in artists:
|
|
326
|
+
artist.set_transform(trans)
|
|
327
|
+
return artists
|
iplotx/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iplotx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.1
|
|
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
|
|
@@ -53,14 +53,15 @@ Supports:
|
|
|
53
53
|
- **networks**:
|
|
54
54
|
- [networkx](https://networkx.org/)
|
|
55
55
|
- [igraph](igraph.readthedocs.io/)
|
|
56
|
-
- [
|
|
56
|
+
- [graph-tool](https://graph-tool.skewed.de/)
|
|
57
|
+
- [zero-dependency](https://iplotx.readthedocs.io/en/latest/gallery/plot_simplenetworkdataprovider.html#sphx-glr-gallery-plot-simplenetworkdataprovider-py)
|
|
57
58
|
- **trees**:
|
|
58
59
|
- [ETE4](https://etetoolkit.github.io/ete/)
|
|
59
60
|
- [cogent3](https://cogent3.org/)
|
|
60
61
|
- [Biopython](https://biopython.org/)
|
|
61
62
|
- [scikit-bio](https://scikit.bio)
|
|
62
63
|
- [dendropy](https://jeetsukumaran.github.io/DendroPy/index.html)
|
|
63
|
-
- [
|
|
64
|
+
- [zero-dependency](https://iplotx.readthedocs.io/en/latest/gallery/tree/plot_simpletreedataprovider.html#sphx-glr-gallery-tree-plot-simpletreedataprovider-py)
|
|
64
65
|
|
|
65
66
|
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.
|
|
66
67
|
|
|
@@ -81,7 +82,7 @@ fig, ax = plt.subplots(figsize=(3, 3))
|
|
|
81
82
|
ipx.plot(g, ax=ax, layout=layout)
|
|
82
83
|
```
|
|
83
84
|
|
|
84
|
-

|
|
85
|
+

|
|
85
86
|
|
|
86
87
|
## Documentation
|
|
87
88
|
See [readthedocs](https://iplotx.readthedocs.io/en/latest/) for the full documentation.
|
|
@@ -90,11 +91,11 @@ See [readthedocs](https://iplotx.readthedocs.io/en/latest/) for the full documen
|
|
|
90
91
|
See [gallery](https://iplotx.readthedocs.io/en/latest/gallery/index.html).
|
|
91
92
|
|
|
92
93
|
## Features
|
|
93
|
-
- Plot networks from multiple libraries including networkx and
|
|
94
|
+
- Plot networks from multiple libraries including networkx, igraph and graph-tool, using Matplotlib. ✅
|
|
94
95
|
- Plot trees from multiple libraries such as cogent3, ETE4, skbio, biopython, and dendropy. ✅
|
|
95
96
|
- Flexible yet easy styling, including an internal library of styles ✅
|
|
96
97
|
- Interactive plotting, e.g. zooming and panning after the plot is created. ✅
|
|
97
|
-
- Store the plot to disk
|
|
98
|
+
- Store the plot to disk in many formats (SVG, PNG, PDF, GIF, etc.). ✅
|
|
98
99
|
- 3D network visualisation with depth shading. ✅
|
|
99
100
|
- Efficient plotting of large graphs (up to ~1 million nodes on a laptop). ✅
|
|
100
101
|
- Edit plotting elements after the plot is created, e.g. changing node colors, labels, etc. ✅
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
iplotx/__init__.py,sha256=RzSct91jO8abrxOIn33rKEnDUgYpu1oj4olbObgX_hs,489
|
|
2
|
-
iplotx/artists.py,sha256=
|
|
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/
|
|
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=
|
|
7
|
+
iplotx/version.py,sha256=V7W0rXi0DhtuwovCU4hLuXqTV7XgZp0IPed9QF9xUso,66
|
|
12
8
|
iplotx/vertex.py,sha256=_yYyvusn4vYvi6RBEW6CHa3vnbv43GnZylnMIaK4bG0,16040
|
|
13
9
|
iplotx/art3d/vertex.py,sha256=Xf8Um30X2doCd8KdNN7332F6BxC4k72Mb_GeRAuzQfQ,2545
|
|
14
10
|
iplotx/art3d/edge/__init__.py,sha256=uw1U_mMXqcZAvea-7JbU1PUKULQD1CMMrbwY02tiWRQ,8529
|
|
@@ -22,6 +18,7 @@ iplotx/edge/ports.py,sha256=BpkbiEhX4mPBBAhOv4jcKFG4Y8hxXz5GRtVLCC0jbtI,1235
|
|
|
22
18
|
iplotx/ingest/__init__.py,sha256=S0YfnXcFKseB7ZBQc4yRt0cNDsLlhqdom0TmSY3OY2E,4756
|
|
23
19
|
iplotx/ingest/heuristics.py,sha256=715VqgfKek5LOJnu1vTo7RqPgCl-Bb8Cf6o7_Tt57fA,5797
|
|
24
20
|
iplotx/ingest/typing.py,sha256=61LwNwrTHVh8eqqC778Gr81zPYcUKW61mDgGCCsuGSk,14181
|
|
21
|
+
iplotx/ingest/providers/network/graph_tool.py,sha256=iTCf4zHe4Zmdd8Tlz6j7Xfo_FwfsIiK5JkQfH3uq7TM,3028
|
|
25
22
|
iplotx/ingest/providers/network/igraph.py,sha256=WL9Yx2IF5QhUIoKMlozdyq5HWIZ-IJmNoeS8GOhL0KU,2945
|
|
26
23
|
iplotx/ingest/providers/network/networkx.py,sha256=ehCg4npL073HX-eAG-VoP6refLPsMb3lYG51xt_rNjA,4636
|
|
27
24
|
iplotx/ingest/providers/network/simple.py,sha256=e_aHhiHhN9DrMoNrt7tEMPURXGhQ1TYRPzsxDEptUlc,3766
|
|
@@ -31,13 +28,18 @@ iplotx/ingest/providers/tree/dendropy.py,sha256=uRMe46PfDPUTeNInUO2Gbp4pVr-WIFIZ
|
|
|
31
28
|
iplotx/ingest/providers/tree/ete4.py,sha256=D7usSq0MOjzrk3EoLi834IlaDGwv7_qG6Qt0ptfKqfI,928
|
|
32
29
|
iplotx/ingest/providers/tree/simple.py,sha256=aV9wGqBomJ5klM_aJQeuL_Q_J1pLCv6AFN98BPDiKUw,2593
|
|
33
30
|
iplotx/ingest/providers/tree/skbio.py,sha256=O1KUr8tYi28pZ3VVjapgO4Uj-YpMuix3GhOH5je8Lv4,822
|
|
31
|
+
iplotx/network/__init__.py,sha256=oEv6f8oFYrtcI_NKabr8a_oIWTc1jXXTl_yO1xox_rE,13575
|
|
32
|
+
iplotx/network/groups.py,sha256=E_eYVXRHjv1DcyA4RupTkMa-rRFrIKkt9Rxn_Elw9Nc,6796
|
|
34
33
|
iplotx/style/__init__.py,sha256=rf1GutrE8hHUhCoe4FGKYX-aNtHuu_U-kYQnqUxZNrY,10282
|
|
35
34
|
iplotx/style/leaf_info.py,sha256=3xBn7xv9Uy2KAqdhM9S6ew5ZBJrGRTXRL3xXb8atfLw,1018
|
|
36
35
|
iplotx/style/library.py,sha256=58Y8BlllGLsR4pQM7_PVCP5tH6_4GkchXZvJpqGHlcg,8534
|
|
36
|
+
iplotx/tree/__init__.py,sha256=mFCgXSusB1dSMc-xN_zOc5PojpEf_XyP8yR3xeZVQMY,30065
|
|
37
|
+
iplotx/tree/cascades.py,sha256=on5GyqbWasl1zgK7bYXYQE0LOSfHc1z-1hnm0GWd6aw,8184
|
|
38
|
+
iplotx/tree/scalebar.py,sha256=Yxt_kF8JdTwKGa8Jzqt3qVePPK5ZBG8P0EiONrsh3E8,11863
|
|
37
39
|
iplotx/utils/geometry.py,sha256=6RrC6qaB0-1vIk1LhGA4CfsiMd-9JNniSPyL_l9mshE,9245
|
|
38
40
|
iplotx/utils/internal.py,sha256=WWfcZDGK8Ut1y_tOHRGg9wSqY1bwSeLQO7dHM_8Tvwo,107
|
|
39
41
|
iplotx/utils/matplotlib.py,sha256=p_53Oamof0RI4mtV8HrdDtZbgVqUxeUZ_KDvLZSiBUQ,8604
|
|
40
42
|
iplotx/utils/style.py,sha256=vyNP80nDYVinqm6_9ltCJCtjK35ZcGlHvOskNv3eQBc,4225
|
|
41
|
-
iplotx-0.
|
|
42
|
-
iplotx-0.
|
|
43
|
-
iplotx-0.
|
|
43
|
+
iplotx-1.0.1.dist-info/METADATA,sha256=jxmMX1X7reQl_O0RJ1GtOS1kT0p8Nkkwoa7rTEwX3QQ,5053
|
|
44
|
+
iplotx-1.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
45
|
+
iplotx-1.0.1.dist-info/RECORD,,
|
|
File without changes
|