iplotx 0.1.0__py3-none-any.whl → 0.2.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.
@@ -1,5 +1,6 @@
1
1
  from functools import wraps, partial
2
2
  from math import atan2
3
+ import numpy as np
3
4
  import matplotlib as mpl
4
5
 
5
6
  from .geometry import (
@@ -29,6 +30,7 @@ def _forwarder(forwards, cls=None):
29
30
 
30
31
  def make_forward(name):
31
32
  def method(self, *args, **kwargs):
33
+ """Each decorated method is called on the decorated class and then, nonrecursively, on all children."""
32
34
  ret = getattr(cls.mro()[1], name)(self, *args, **kwargs)
33
35
  for c in self.get_children():
34
36
  getattr(c, name)(*args, **kwargs)
@@ -46,7 +48,13 @@ def _forwarder(forwards, cls=None):
46
48
 
47
49
 
48
50
  def _additional_set_methods(attributes, cls=None):
49
- """Decorator to add specific set methods for children properties."""
51
+ """Decorator to add specific set methods for children properties.
52
+
53
+ This is useful to autogenerate methods a la set_<key>(value), for
54
+ instance set_alpha(value). It works by delegating to set(alpha=value).
55
+
56
+ Overall, this is a minor tweak compared to the previous decorator.
57
+ """
50
58
  if cls is None:
51
59
  return partial(_additional_set_methods, attributes)
52
60
 
@@ -73,64 +81,76 @@ def _additional_set_methods(attributes, cls=None):
73
81
  # many use cases.
74
82
  def _get_label_width_height(text, hpadding=18, vpadding=12, **kwargs):
75
83
  """Get the bounding box size for a text with certain properties."""
76
- forbidden_props = ["horizontalalignment", "verticalalignment", "ha", "va"]
84
+ forbidden_props = [
85
+ "horizontalalignment",
86
+ "verticalalignment",
87
+ "ha",
88
+ "va",
89
+ "color",
90
+ "edgecolor",
91
+ "facecolor",
92
+ ]
77
93
  for prop in forbidden_props:
78
94
  if prop in kwargs:
79
95
  del kwargs[prop]
80
96
 
81
97
  path = mpl.textpath.TextPath((0, 0), text, **kwargs)
82
98
  boundingbox = path.get_extents()
83
- width = boundingbox.width + hpadding
84
- height = boundingbox.height + vpadding
99
+ width = boundingbox.width
100
+ height = boundingbox.height
101
+
102
+ # Scaling with font size appears broken... try to patch it up linearly here, even though we know it don't work well
103
+ width *= kwargs.get("size", 12) / 12.0
104
+ height *= kwargs.get("size", 12) / 12.0
105
+
106
+ width += hpadding
107
+ height += vpadding
85
108
  return (width, height)
86
109
 
87
110
 
88
- def _compute_mid_coord(path):
111
+ def _compute_mid_coord_and_rot(path, trans):
89
112
  """Compute mid point of an edge, straight or curved."""
90
113
  # Distinguish between straight and curved paths
91
114
  if path.codes[-1] == mpl.path.Path.LINETO:
92
- return path.vertices.mean(axis=0)
115
+ coord = path.vertices.mean(axis=0)
116
+ vtr = trans(path.vertices)
117
+ rot = atan2(
118
+ vtr[-1, 1] - vtr[0, 1],
119
+ vtr[-1, 0] - vtr[0, 0],
120
+ )
93
121
 
94
122
  # Cubic Bezier
95
- if path.codes[-1] == mpl.path.Path.CURVE4:
96
- return _evaluate_cubic_bezier(path.vertices, 0.5)
123
+ elif path.codes[-1] == mpl.path.Path.CURVE4:
124
+ coord = _evaluate_cubic_bezier(path.vertices, 0.5)
125
+ # TODO:
126
+ rot = 0
97
127
 
98
128
  # Square Bezier
99
- if path.codes[-1] == mpl.path.Path.CURVE3:
100
- return _evaluate_squared_bezier(path.vertices, 0.5)
101
-
102
- raise ValueError(
103
- "Curve type not straight and not squared/cubic Bezier, cannot compute mid point."
104
- )
129
+ elif path.codes[-1] == mpl.path.Path.CURVE3:
130
+ coord = _evaluate_squared_bezier(path.vertices, 0.5)
131
+ # TODO:
132
+ rot = 0
105
133
 
134
+ else:
135
+ raise ValueError(
136
+ "Curve type not straight and not squared/cubic Bezier, cannot compute mid point."
137
+ )
106
138
 
107
- def _compute_group_path_with_vertex_padding(
108
- points,
109
- transform,
110
- vertexpadding=10,
111
- ):
112
- """Offset path for a group based on vertex padding.
139
+ return coord, rot
113
140
 
114
- At the input, the structure is [v1, v1, v1, v2, v2, v2, ...]
115
- """
116
141
 
117
- # Transform into figure coordinates
118
- trans = transform.transform
119
- trans_inv = transform.inverted().transform
120
- points = trans(points)
142
+ def _build_cmap_fun(values, cmap):
143
+ """Map colormap on top of numerical values."""
144
+ cmap = mpl.cm._ensure_cmap(cmap)
121
145
 
122
- npoints = len(points) // 3
123
- vprev = points[-1]
124
- mprev = atan2(points[0, 1] - vprev[1], points[0, 0] - vprev[0])
125
- for i, vcur in enumerate(points[::3]):
126
- vnext = points[(i + 1) * 3]
127
- mnext = atan2(vnext[1] - vcur[1], vnext[0] - vcur[0])
146
+ if np.isscalar(values):
147
+ values = [values]
128
148
 
129
- mprev_orth = -1 / mprev
130
- points[i * 3] = vcur + vertexpadding * mprev_orth
149
+ if isinstance(values, dict):
150
+ values = np.array(list(values.values()))
131
151
 
132
- vprev = vcur
133
- mprev = mnext
152
+ vmin = np.nanmin(values)
153
+ vmax = np.nanmax(values)
154
+ norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
134
155
 
135
- points = trans_inv(points)
136
- return points
156
+ return lambda x: cmap(norm(x))
iplotx/utils/style.py ADDED
@@ -0,0 +1 @@
1
+ from copy import copy
iplotx/version.py CHANGED
@@ -1 +1,5 @@
1
- __version__ = "0.1.0"
1
+ """
2
+ iplotx version information module.
3
+ """
4
+
5
+ __version__ = "0.2.0"
iplotx/vertex.py CHANGED
@@ -1,51 +1,129 @@
1
+ """
2
+ Module containing code to manipulate vertex visualisations, especially the VertexCollection class.
3
+ """
4
+
5
+ from typing import (
6
+ Optional,
7
+ Sequence,
8
+ Any,
9
+ Never,
10
+ )
11
+ import warnings
1
12
  import numpy as np
2
- from matplotlib.transforms import IdentityTransform
13
+ import pandas as pd
14
+ import matplotlib as mpl
3
15
  from matplotlib.collections import PatchCollection
4
16
  from matplotlib.patches import (
17
+ Patch,
5
18
  Ellipse,
6
19
  Circle,
7
20
  RegularPolygon,
8
21
  Rectangle,
9
22
  )
10
23
 
24
+ from .style import (
25
+ get_style,
26
+ rotate_style,
27
+ copy_with_deep_values,
28
+ )
29
+ from .utils.matplotlib import (
30
+ _get_label_width_height,
31
+ _build_cmap_fun,
32
+ _forwarder,
33
+ )
34
+ from .label import LabelCollection
35
+
11
36
 
37
+ @_forwarder(
38
+ (
39
+ "set_clip_path",
40
+ "set_clip_box",
41
+ "set_snap",
42
+ "set_sketch_params",
43
+ "set_animated",
44
+ "set_picker",
45
+ )
46
+ )
12
47
  class VertexCollection(PatchCollection):
13
- """Collection of vertex patches for plotting.
48
+ """Collection of vertex patches for plotting."""
14
49
 
15
- This class takes additional keyword arguments compared to PatchCollection:
50
+ _factor = 1.0
16
51
 
17
- @param vertex_builder: A list of vertex builders to construct the visual
18
- vertices. This is updated if the size of the vertices is changed.
19
- @param size_callback: A function to be triggered after vertex sizes are
20
- changed. Typically this redraws the edges.
21
- """
52
+ def __init__(
53
+ self,
54
+ layout: pd.DataFrame,
55
+ *args,
56
+ layout_coordinate_system: str = "cartesian",
57
+ style: Optional[dict[str, Any]] = None,
58
+ labels: Optional[Sequence[str]] = None,
59
+ **kwargs,
60
+ ):
61
+ """Initialise the VertexCollection.
22
62
 
23
- def __init__(self, *args, **kwargs):
24
- super().__init__(*args, **kwargs)
63
+ Parameters:
64
+ layout: The vertex layout.
65
+ layout_coordinate_system: The coordinate system for the layout, usually "cartesian").
66
+ style: The vertex style (subdictionary "vertex") to apply.
67
+ labels: The vertex labels, if present.
68
+ """
25
69
 
26
- def get_sizes(self):
27
- """Same as get_size."""
28
- return self.get_size()
70
+ self._index = layout.index
71
+ self._style = style
72
+ self._labels = labels
73
+
74
+ # Create patches from structured data
75
+ patches, offsets, sizes, kwargs2 = self._init_vertex_patches(
76
+ layout,
77
+ layout_coordinate_system=layout_coordinate_system,
78
+ )
79
+
80
+ kwargs.update(kwargs2)
81
+ kwargs["offsets"] = offsets
82
+ kwargs["match_original"] = True
29
83
 
30
- def get_size(self):
31
- """Get vertex sizes.
84
+ # Pass to PatchCollection constructor
85
+ super().__init__(patches, *args, **kwargs)
32
86
 
33
- If width and height are unequal, get the largest of the two.
87
+ # Compute _transforms like in _CollectionWithScales for dpi issues
88
+ self.set_sizes(sizes)
34
89
 
35
- @return: An array of vertex sizes.
90
+ if self._labels is not None:
91
+ self._compute_label_collection()
92
+
93
+ def get_children(self) -> tuple[mpl.artist.Artist]:
94
+ """Get the children artists.
95
+
96
+ This can include the labels as a LabelCollection.
36
97
  """
37
- import numpy as np
98
+ children = []
99
+ if hasattr(self, "_label_collection"):
100
+ children.append(self._label_collection)
101
+ return tuple(children)
38
102
 
39
- sizes = []
40
- for path in self.get_paths():
41
- bbox = path.get_extents()
42
- mins, maxs = bbox.min, bbox.max
43
- width, height = maxs - mins
44
- size = max(width, height)
45
- sizes.append(size)
46
- return np.array(sizes)
103
+ def set_figure(self, fig) -> Never:
104
+ """Set the figure for this artist and all children."""
105
+ super().set_figure(fig)
106
+ self.set_sizes(self._sizes, self.get_figure(root=True).dpi)
107
+ for child in self.get_children():
108
+ child.set_figure(fig)
109
+
110
+ def get_index(self):
111
+ """Get the VertexCollection index."""
112
+ return self._index
113
+
114
+ def get_vertex_id(self, index):
115
+ """Get the id of a single vertex at a positional index."""
116
+ return self._index[index]
47
117
 
48
- def set_size(self, sizes):
118
+ def get_sizes(self):
119
+ """Get vertex sizes (max of width and height), not scaled by dpi."""
120
+ return self._sizes
121
+
122
+ def get_sizes_dpi(self):
123
+ """Get vertex sizes (max of width and height), scaled by dpi."""
124
+ return self._transforms[:, 0, 0]
125
+
126
+ def set_sizes(self, sizes, dpi=72.0):
49
127
  """Set vertex sizes.
50
128
 
51
129
  This rescales the current vertex symbol/path linearly, using this
@@ -53,26 +131,109 @@ class VertexCollection(PatchCollection):
53
131
 
54
132
  @param sizes: A sequence of vertex sizes or a single size.
55
133
  """
56
- paths = self._paths
57
- try:
58
- iter(sizes)
59
- except TypeError:
60
- sizes = [sizes] * len(paths)
61
-
62
- sizes = list(sizes)
63
- current_sizes = self.get_sizes()
64
- for path, cursize in zip(paths, current_sizes):
65
- # Circular use of sizes
66
- size = sizes.pop(0)
134
+ if sizes is None:
135
+ self._sizes = np.array([])
136
+ self._transforms = np.empty((0, 3, 3))
137
+ else:
138
+ self._sizes = np.asarray(sizes)
139
+ self._transforms = np.zeros((len(self._sizes), 3, 3))
140
+ scale = self._sizes * dpi / 72.0 * self._factor
141
+ self._transforms[:, 0, 0] = scale
142
+ self._transforms[:, 1, 1] = scale
143
+ self._transforms[:, 2, 2] = 1.0
144
+ self.stale = True
145
+
146
+ get_size = get_sizes
147
+ set_size = set_sizes
148
+
149
+ def _init_vertex_patches(
150
+ self, vertex_layout_df, layout_coordinate_system="cartesian"
151
+ ):
152
+ style = self._style or {}
153
+ if "cmap" in style:
154
+ cmap_fun = _build_cmap_fun(
155
+ style["facecolor"],
156
+ style["cmap"],
157
+ )
158
+ else:
159
+ cmap_fun = None
160
+
161
+ if style.get("size", 20) == "label":
162
+ if self._labels is None:
163
+ warnings.warn(
164
+ "No labels found, cannot resize vertices based on labels."
165
+ )
166
+ style["size"] = get_style("default.vertex")["size"]
167
+ else:
168
+ vertex_labels = self._labels
169
+
170
+ if "cmap" in style:
171
+ colorarray = []
172
+ patches = []
173
+ offsets = []
174
+ sizes = []
175
+ for i, (vid, row) in enumerate(vertex_layout_df.iterrows()):
176
+ # Centre of the vertex
177
+ offset = list(row.values)
178
+
179
+ # Transform to cartesian coordinates if needed
180
+ if layout_coordinate_system == "polar":
181
+ r, theta = offset
182
+ offset = [r * np.cos(theta), r * np.sin(theta)]
183
+
184
+ offsets.append(offset)
185
+
186
+ if style.get("size") == "label":
187
+ # NOTE: it's ok to overwrite the dict here
188
+ style["size"] = _get_label_width_height(
189
+ str(vertex_labels[vid]), **style.get("label", {})
190
+ )
191
+
192
+ stylei = rotate_style(style, index=i, key=vid)
193
+ if cmap_fun is not None:
194
+ colorarray.append(style["facecolor"])
195
+ stylei["facecolor"] = cmap_fun(stylei["facecolor"])
196
+
197
+ # Shape of the vertex (Patch)
198
+ art, size = make_patch(**stylei)
199
+ patches.append(art)
67
200
  sizes.append(size)
68
- # Rescale the path for this vertex
69
- path.vertices *= size / cursize
70
201
 
71
- self.stale = True
202
+ kwargs = {}
203
+ if "cmap" in style:
204
+ vmin = np.min(colorarray)
205
+ vmax = np.max(colorarray)
206
+ norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
207
+ kwargs["cmap"] = style["cmap"]
208
+ kwargs["norm"] = norm
209
+
210
+ return patches, offsets, sizes, kwargs
72
211
 
73
- def set_sizes(self, sizes):
74
- """Same as set_size."""
75
- self.set_size(sizes)
212
+ def _compute_label_collection(self):
213
+ transform = self.get_offset_transform()
214
+
215
+ style = (
216
+ copy_with_deep_values(self._style.get("label", None))
217
+ if self._style is not None
218
+ else {}
219
+ )
220
+ forbidden_props = ["hpadding", "vpadding"]
221
+ for prop in forbidden_props:
222
+ if prop in style:
223
+ del style[prop]
224
+
225
+ self._label_collection = LabelCollection(
226
+ self._labels,
227
+ style=style,
228
+ offsets=self._offsets,
229
+ transform=transform,
230
+ )
231
+
232
+ def get_labels(self):
233
+ if hasattr(self, "_label_collection"):
234
+ return self._label_collection
235
+ else:
236
+ return None
76
237
 
77
238
  @property
78
239
  def stale(self):
@@ -84,29 +245,63 @@ class VertexCollection(PatchCollection):
84
245
  if val and hasattr(self, "stale_callback_post"):
85
246
  self.stale_callback_post(self)
86
247
 
248
+ @mpl.artist.allow_rasterization
249
+ def draw(self, renderer):
250
+ if not self.get_visible():
251
+ return
252
+
253
+ # null graph, no need to draw anything
254
+ # NOTE: I would expect this to be already a clause in the superclass by oh well
255
+ if len(self.get_paths()) == 0:
256
+ return
87
257
 
88
- def make_patch(marker: str, size, **kwargs):
258
+ self.set_sizes(self._sizes, self.get_figure(root=True).dpi)
259
+
260
+ # NOTE: This draws the vertices first, then the labels.
261
+ # The correct order would be vertex1->label1->vertex2->label2, etc.
262
+ # We might fix if we manage to find a way to do it.
263
+ super().draw(renderer)
264
+ for child in self.get_children():
265
+ child.draw(renderer)
266
+
267
+
268
+ def make_patch(
269
+ marker: str, size: float | Sequence[float], **kwargs
270
+ ) -> tuple[Patch, float]:
89
271
  """Make a patch of the given marker shape and size."""
90
- forbidden_props = ["label"]
272
+ forbidden_props = ["label", "cmap", "norm"]
91
273
  for prop in forbidden_props:
92
274
  if prop in kwargs:
93
275
  kwargs.pop(prop)
94
276
 
95
- if isinstance(size, (int, float)):
277
+ if np.isscalar(size):
278
+ size = float(size)
96
279
  size = (size, size)
97
280
 
98
- if marker in ("o", "circle"):
99
- return Circle((0, 0), size[0] / 2, **kwargs)
281
+ # Size of vertices is determined in self._transforms, which scales with dpi, rather than here,
282
+ # so normalise by the average dimension (btw x and y) to keep the ratio of the marker.
283
+ # If you check in get_sizes, you will see that rescaling also happens with the max of width and height.
284
+ size = np.asarray(size, dtype=float)
285
+ size_max = size.max()
286
+ if size_max > 0:
287
+ size /= size_max
288
+
289
+ art: Patch
290
+ if marker in ("o", "c", "circle"):
291
+ art = Circle((0, 0), size[0] / 2, **kwargs)
100
292
  elif marker in ("s", "square", "r", "rectangle"):
101
- return Rectangle((-size[0] / 2, -size[1] / 2), size[0], size[1], **kwargs)
293
+ art = Rectangle((-size[0] / 2, -size[1] / 2), size[0], size[1], **kwargs)
102
294
  elif marker in ("^", "triangle"):
103
- return RegularPolygon((0, 0), numVertices=3, radius=size[0] / 2, **kwargs)
295
+ art = RegularPolygon((0, 0), numVertices=3, radius=size[0] / 2, **kwargs)
104
296
  elif marker in ("d", "diamond"):
105
- return make_patch("s", size[0], angle=45, **kwargs)
297
+ art, _ = make_patch("s", size[0], angle=45, **kwargs)
106
298
  elif marker in ("v", "triangle_down"):
107
- return RegularPolygon(
299
+ art = RegularPolygon(
108
300
  (0, 0), numVertices=3, radius=size[0] / 2, orientation=np.pi, **kwargs
109
301
  )
110
302
  elif marker in ("e", "ellipse"):
111
- return Ellipse((0, 0), size[0] / 2, size[1] / 2, **kwargs)
112
- raise KeyError(f"Unknown marker: {marker}")
303
+ art = Ellipse((0, 0), size[0] / 2, size[1] / 2, **kwargs)
304
+ else:
305
+ raise KeyError(f"Unknown marker: {marker}")
306
+
307
+ return (art, size_max)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iplotx
3
- Version: 0.1.0
3
+ Version: 0.2.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
@@ -14,13 +14,14 @@ Keywords: graph,network,plotting,visualisation
14
14
  Classifier: License :: OSI Approved :: MIT License
15
15
  Classifier: Operating System :: OS Independent
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.8
18
- Classifier: Programming Language :: Python :: 3.9
19
- Classifier: Programming Language :: Python :: 3.10
20
17
  Classifier: Programming Language :: Python :: 3.11
21
- Requires-Python: >=3.10
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Requires-Python: >=3.11
22
21
  Requires-Dist: matplotlib>=2.0.0
22
+ Requires-Dist: numpy>=2.0.0
23
23
  Requires-Dist: pandas>=2.0.0
24
+ Requires-Dist: pylint>=3.3.7
24
25
  Provides-Extra: igraph
25
26
  Requires-Dist: igraph>=0.11.0; extra == 'igraph'
26
27
  Provides-Extra: networkx
@@ -29,17 +30,45 @@ Description-Content-Type: text/markdown
29
30
 
30
31
  ![Github Actions](https://github.com/fabilab/iplotx/actions/workflows/test.yml/badge.svg)
31
32
  ![PyPI - Version](https://img.shields.io/pypi/v/iplotx)
33
+ ![RTD](https://readthedocs.org/projects/iplotx/badge/?version=latest)
32
34
 
33
35
  # iplotx
34
36
  Plotting networks from igraph and networkx.
35
37
 
36
- **NOTE**: This is currently pre-alpha quality software. The API and functionality will break constantly, so use at your own risk. That said, if you have things you would like to see improved, please open a GitHub issue.
38
+ **NOTE**: This is currently alpha quality software. The API and functionality will break constantly, so use at your own risk. That said, if you have things you would like to see improved, please open a GitHub issue.
39
+
40
+ ## Installation
41
+ ```bash
42
+ pip install iplotx
43
+ ```
44
+
45
+ ## Quick Start
46
+ ```python
47
+ import networkx as nx
48
+ import matplotlib.pyplot as plt
49
+ import iplotx as ipx
50
+
51
+ g = nx.Graph([(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)])
52
+ layout = nx.layout.circular_layout(g)
53
+ fig, ax = plt.subplots(figsize=(3, 3))
54
+ ipx.plot(g, ax=ax, layout=layout)
55
+ ```
56
+
57
+ ![Quick start image](docs/source/_static/graph_basic.png)
58
+
59
+ ## Documentation
60
+ See [readthedocs](https://iplotx.readthedocs.io/en/latest/) for the full documentation.
61
+
62
+ ## Gallery
63
+ See [gallery](https://iplotx.readthedocs.io/en/latest/gallery/index.html).
37
64
 
38
65
  ## Roadmap
39
66
  - Plot networks from igraph and networkx interchangeably, using matplotlib as a backend. ✅
40
67
  - Support interactive plotting, e.g. zooming and panning after the plot is created. ✅
41
- - Support storing the plot to disk thanks to the many matplotlib backends (SVG, PNG, PDF, etc.).
42
- - Efficient plotting of large graphs using matplotlib's collection functionality. ✅ (partially)
68
+ - Support storing the plot to disk thanks to the many matplotlib backends (SVG, PNG, PDF, etc.).
69
+ - Support flexible yet easy styling. ✅
70
+ - Efficient plotting of large graphs using matplotlib's collection functionality. ✅
71
+ - Support trees from special libraries such as ete3, biopython, etc. This will need a dedicated function and layouting. ✅
43
72
  - Support animations, e.g. showing the evolution of a network over time. 🏗️
44
73
  - Support uni- and bi-directional communication between graph object and plot object.🏗️
45
74
 
@@ -0,0 +1,30 @@
1
+ iplotx/__init__.py,sha256=DIOWUaEo9vgmz8B6hBlurV95BEYmjmUe5BU-jRWrtG4,418
2
+ iplotx/groups.py,sha256=QZ4iounIcoPQfxRIJamGJljpdKTsVJGM0DCfBPJKBm0,6106
3
+ iplotx/label.py,sha256=zS53rzA2j4yjo150CfjqbYfBTFWNG5tt9AkfdNWZlZ0,3884
4
+ iplotx/layout.py,sha256=IDj66gXUvAN8q1uBIhmob-ZOQFWO8GAlW-4gGprN7H0,3963
5
+ iplotx/network.py,sha256=VcrKdq0PzbXfufIAir-nyMgrpfGvvXpdqK5H8kPDf3I,9739
6
+ iplotx/plotting.py,sha256=1C6wenDeUohoYVl_vAcTs2aR-5LcRHFOcNb4ghIbkLI,7152
7
+ iplotx/style.py,sha256=m-sWbC4qjfdPr4fUns_vCPKl035-uJXLy6F4J-egQjM,11058
8
+ iplotx/tree.py,sha256=7o9BUltkl37wy9Gn8RepPoNlcbzZYALC1tZVLDJ0CVo,9372
9
+ iplotx/typing.py,sha256=dfiCkn_7plbQZ1tkBAdK8vCap8r9ZB6Skw2W4RYKVbo,1078
10
+ iplotx/version.py,sha256=yw3yV4Zl2O7TK5Iy1aB2-9BhAM-EcoMbba7u7G2n1TE,66
11
+ iplotx/vertex.py,sha256=h0B5pTDxT2dTd73fN8sXYsJdzanwaI38T12ZYpX_EgI,9505
12
+ iplotx/edge/__init__.py,sha256=1BE4qCIXJwcN6A1AQVY1PFxje9WNYR1PEMk36vTALkk,30285
13
+ iplotx/edge/arrow.py,sha256=dTlXMi1PsQDbpIktjRCGtubMGF72iOg1Gw9vC0FMOYo,11217
14
+ iplotx/edge/ports.py,sha256=UdK3ylEWOyaZkKAKqN-reL0euF6QBWHLTJG7Ts7uhz4,1126
15
+ iplotx/ingest/__init__.py,sha256=75Pml7X65tP8b2G3qaeZUdnDgwP6dclcCEEFl0BYSdo,4707
16
+ iplotx/ingest/heuristics.py,sha256=32AZ8iidM_uooaBKe2EMjNU_nJDbhA_28ADc0dpQy5A,6636
17
+ iplotx/ingest/typing.py,sha256=QEgCpLyfp-0v9czn6OaJ0_nuCo1AXv3lGS3aD6w-Ezw,3134
18
+ iplotx/ingest/providers/network/igraph.py,sha256=_J7lH-jrT0_1oSfwgT_mQMRhxNoFwHo8dyBLCbgqETQ,2766
19
+ iplotx/ingest/providers/network/networkx.py,sha256=u7NegapWZ0gWUj5n1PUVD-zZ92lKUiv6BLNTNIrXlRk,4233
20
+ iplotx/ingest/providers/tree/biopython.py,sha256=7ZVD_WwIaBOSl-at4r_Y4d2qHQq6BcvlV-yDxMVCWnw,3456
21
+ iplotx/ingest/providers/tree/cogent3.py,sha256=5O92zkdA43LWQ7h6r-uG_5X76EPHo-Yx9ODLb0hd_qc,3636
22
+ iplotx/ingest/providers/tree/ete4.py,sha256=sGm2363yLJWsWsrRn2iFx8qRiq57h_5dQWBD41xJmhY,3660
23
+ iplotx/ingest/providers/tree/skbio.py,sha256=iNZhY_TNLpzd55cK2nybjiftUfakeOrDDJ9dMBna_io,3629
24
+ iplotx/utils/geometry.py,sha256=K5ZBYPmz4-KNm64pDh2p0L6PF5-u57SCVaEd2eWeRv0,8956
25
+ iplotx/utils/internal.py,sha256=WWfcZDGK8Ut1y_tOHRGg9wSqY1bwSeLQO7dHM_8Tvwo,107
26
+ iplotx/utils/matplotlib.py,sha256=T37SMwKNSA-dRKgNkVw5H4fGr5NACtxvBPebDJGdhjk,4516
27
+ iplotx/utils/style.py,sha256=fEi14nc37HQqHxZTPeQaTnFFwbSneY1oDCyv2UbWfKk,22
28
+ iplotx-0.2.0.dist-info/METADATA,sha256=5Xey_j3r4NJ78C9f-cMvfyhZc9rPb6E6xwdBXbVjRUI,3055
29
+ iplotx-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
+ iplotx-0.2.0.dist-info/RECORD,,
iplotx/edge/common.py DELETED
@@ -1,47 +0,0 @@
1
- from math import pi
2
- import numpy as np
3
-
4
-
5
- def _compute_loops_per_angle(nloops, angles):
6
- if len(angles) == 0:
7
- return [(0, 2 * pi, nloops)]
8
-
9
- angles_sorted_closed = list(sorted(angles))
10
- angles_sorted_closed.append(angles_sorted_closed[0] + 2 * pi)
11
- deltas = np.diff(angles_sorted_closed)
12
-
13
- # Now we have the deltas and the total number of loops
14
- # 1. Assign all loops to the largest wedge
15
- idx_dmax = deltas.argmax()
16
- if nloops == 1:
17
- return [
18
- (angles_sorted_closed[idx_dmax], angles_sorted_closed[idx_dmax + 1], nloops)
19
- ]
20
-
21
- # 2. Check if any other wedges are larger than this
22
- # If not, we are done (this is the algo in igraph)
23
- dsplit = deltas[idx_dmax] / nloops
24
- if (deltas > dsplit).sum() < 2:
25
- return [
26
- (angles_sorted_closed[idx_dmax], angles_sorted_closed[idx_dmax + 1], nloops)
27
- ]
28
-
29
- # 3. Check how small the second-largest wedge would become
30
- idx_dsort = np.argsort(deltas)
31
- return [
32
- (
33
- angles_sorted_closed[idx_dmax],
34
- angles_sorted_closed[idx_dmax + 1],
35
- nloops - 1,
36
- ),
37
- (
38
- angles_sorted_closed[idx_dsort[-2]],
39
- angles_sorted_closed[idx_dsort[-2] + 1],
40
- 1,
41
- ),
42
- ]
43
-
44
- ## TODO: we should greedily iterate from this
45
- ## TODO: finish this
46
- # dsplit_new = dsplit * nloops / (nloops - 1)
47
- # dsplit2_new = deltas[idx_dsort[-2]]