-
Notifications
You must be signed in to change notification settings - Fork 266
Expand file tree
/
Copy path__init__.py
More file actions
322 lines (267 loc) · 12.5 KB
/
__init__.py
File metadata and controls
322 lines (267 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
"""
Drawing and plotting routines for igraph.
igraph has two stable plotting backends at the moment: Cairo and Matplotlib.
It also has experimental support for plotly.
The Cairo backend is dependent on the C{pycairo} or C{cairocffi} libraries that
provide Python bindings to the popular U{Cairo library<https://www.cairographics.org>}.
This means that if you don't have U{pycairo<https://pycairo.readthedocs.io/>}
or U{cairocffi<https://doc.courtbouillon.org/cairocffi/>} installed, you won't be able
to use the Cairo plotting backend. Whenever the documentation refers to the
C{pycairo} library, you can safely replace it with C{cairocffi} as the two are
API-compatible.
The Matplotlib backend uses the U{Matplotlib library<https://matplotlib.org>}.
You will need to install it from PyPI if you want to use the Matplotlib
plotting backend. Many of our gallery examples use the matplotlib backend.
The plotly backend uses the U{plotly library <https://plotly.com/python/>} and,
like matplotlib, requires installation from PyPI.
If you do not want to (or cannot) install any of the dependencies outlined
above, you can still save the graph to an SVG file and view it from
U{Mozilla Firefox<https://www.mozilla.org/firefox>} (free) or edit it in
U{Inkscape<https://www.inkscape.org>} (free), U{Skencil<https://www.skencil.org>}
(formerly known as Sketch, also free) or Adobe Illustrator.
"""
from pathlib import Path
from warnings import warn
from igraph.configuration import Configuration
from igraph.drawing.cairo.utils import find_cairo
from igraph.drawing.matplotlib.utils import find_matplotlib
from igraph.drawing.plotly.utils import find_plotly
from igraph.drawing.cairo.plot import CairoPlot
from igraph.drawing.colors import Palette, palettes
from igraph.drawing.cairo.graph import CairoGraphDrawer
from igraph.drawing.cairo.matrix import CairoMatrixDrawer
from igraph.drawing.cairo.histogram import CairoHistogramDrawer
from igraph.drawing.cairo.palette import CairoPaletteDrawer
from igraph.drawing.matplotlib.graph import MatplotlibGraphDrawer
from igraph.drawing.matplotlib.matrix import MatplotlibMatrixDrawer
from igraph.drawing.matplotlib.histogram import MatplotlibHistogramDrawer
from igraph.drawing.matplotlib.palette import MatplotlibPaletteDrawer
from igraph.drawing.plotly.graph import PlotlyGraphDrawer
from igraph.drawing.utils import BoundingBox, Point, Rectangle
from igraph.utils import _is_running_in_ipython
__all__ = (
"BoundingBox",
"CairoGraphDrawer",
"MatplotlibGraphDrawer",
"DefaultGraphDrawer",
"Plot",
"Point",
"Rectangle",
"plot",
"DrawerDirectory",
)
# TODO: deprecate
Plot = CairoPlot
# TODO: deprecate
DefaultGraphDrawer = CairoGraphDrawer
class DrawerDirectory:
"""Static class that finds the object/backend drawer
This directory is used by the __plot__ functions.
"""
valid_backends = ("cairo", "matplotlib")
valid_objects = (
"Graph",
"Matrix",
"Histogram",
"Palette",
)
known_drawers = {
"cairo": {
"Graph": CairoGraphDrawer,
"Matrix": CairoMatrixDrawer,
"Histogram": CairoHistogramDrawer,
"Palette": CairoPaletteDrawer,
},
"matplotlib": {
"Graph": MatplotlibGraphDrawer,
"Matrix": MatplotlibMatrixDrawer,
"Histogram": MatplotlibHistogramDrawer,
"Palette": MatplotlibPaletteDrawer,
},
"plotly": {
"Graph": PlotlyGraphDrawer,
},
}
@classmethod
def resolve(cls, obj, backend):
"""Given a shape name, returns the corresponding shape drawer class
@param cls: the class to resolve
@param obj: an instance of the object to plot
@param backend: the name of the backend
@return: the corresponding shape drawer class
@raise ValueError: if no drawer is available for this backend/object
"""
object_name = str(obj.__class__).split(".")[-1].strip("<'>")
try:
return cls.known_drawers[backend][object_name]
except KeyError:
raise ValueError(
f"unknown drawer for {object_name} and backend {backend}",
) from None
def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds):
"""Plots the given object to the given target.
Positional and keyword arguments not explicitly mentioned here will be
passed down to the C{__plot__} method of the object being plotted.
Since you are most likely interested in the keyword arguments available
for graph plots, see L{Graph.__plot__} as well.
@param obj: the object to be plotted
@param target: the target where the object should be plotted. It can be one
of the following types:
- C{matplotib.axes.Axes} -- a matplotlib/pyplot axes in which the
graph will be plotted. Drawing is delegated to the chosen matplotlib
backend, and you can use interactive backends and matplotlib
functions to save to file as well.
- C{string} -- a file with the given name will be created and the plot
will be stored there. If you are using the Cairo backend, an
appropriate Cairo surface will be attached to the file. If you are
using the matplotlib backend, the Figure will be saved to that file
using Figure.savefig with default parameters. The supported image
formats for Cairo are: PNG, PDF, SVG and PostScript; matplotlib might
support additional formats.
- C{cairo.Surface} -- the given Cairo surface will be used. This can
refer to a PNG image, an arbitrary window, an SVG file, anything that
Cairo can handle.
- C{None} -- If you are using the Cairo backend, no plotting will be
performed; igraph simply returns a CairoPlot_ object that you can use
to manipulate the plot and save it to a file later. If you are using
the matplotlib backend, a Figure objet and an Axes are created and
the Axes is returned so you can manipulate it further. Similarly, if
you are using the plotly backend, a Figure object is returned.
@param bbox: the bounding box of the plot. It must be a tuple with either
two or four integers, or a L{BoundingBox} object. If this is a tuple
with two integers, it is interpreted as the width and height of the plot
(in pixels for PNG images and on-screen plots, or in points for PDF,
SVG and PostScript plots, where 72 pt = 1 inch = 2.54 cm). If this is
a tuple with four integers, the first two denotes the X and Y coordinates
of a corner and the latter two denoting the X and Y coordinates of the
opposite corner. Ignored for Matplotlib plots.
@keyword opacity: the opacity of the object being plotted. It can be
used to overlap several plots of the same graph if you use the same
layout for them -- for instance, you might plot a graph with opacity
0.5 and then plot its spanning tree over it with opacity 0.1. To
achieve this, you'll need to modify the L{Plot} object returned with
L{Plot.add}. Ignored for Matplotlib plots.
@keyword palette: the palette primarily used on the plot if the
added objects do not specify a private palette. Must be either
an L{igraph.drawing.colors.Palette} object or a string referring
to a valid key of C{igraph.drawing.colors.palettes} (see module
L{igraph.drawing.colors}) or C{None}. In the latter case, the default
palette given by the configuration key C{plotting.palette} is used.
@keyword margin: the top, right, bottom, left margins as a 4-tuple.
If it has less than 4 elements or is a single float, the elements
will be re-used until the length is at least 4. The default margin
is 20 units on each side. Ignored for Matplotlib plots.
@keyword inline: whether to try and show the plot object inline in the
current IPython notebook. Passing C{None} here or omitting this keyword
argument will look up the preferred behaviour from the
C{shell.ipython.inlining.Plot} configuration key. Note that this keyword
argument has an effect only if igraph is run inside IPython and C{target}
is C{None}.
@keyword backend: the plotting backend to use; one of C{"cairo"},
C{"matplotlib"} or C{"plotly"}. C{None} means to try to decide the backend
from the plotting target and the default igraph configuration object.
@return: an appropriate L{CairoPlot} object for the Cairo backend, the
Matplotlib C{Axes} object for the Matplotlib backend, and the C{Figure}
object for the plotly backend.
@see: Graph.__plot__
"""
VALID_BACKENDS = ("cairo", "matplotlib", "plotly")
_, plt = find_matplotlib()
cairo = find_cairo()
plotly = find_plotly()
backend = kwds.pop("backend", None)
# Switch backend based on target (first) and config (second) if it was not
# selected explicitly
if backend is not None:
pass
elif hasattr(plt, "Axes") and isinstance(target, plt.Axes):
backend = "matplotlib"
elif hasattr(plotly, "graph_objects") and isinstance(
target, plotly.graph_objects.Figure
):
backend = "plotly"
elif hasattr(cairo, "Surface") and isinstance(target, cairo.Surface):
backend = "cairo"
else:
backend = Configuration.instance()["plotting.backend"]
if backend not in VALID_BACKENDS:
raise ValueError(f"unknown plotting backend: {backend!r}")
if backend in ("matplotlib", "plotly"):
# Choose palette
# If explicit, use it. If not or None, ask the object: None is an
# acceptable response from the object (e.g. for clusterings), it means
# the palette is handled internally. If no response, default to config.
palette = kwds.pop("palette", None)
if palette is None:
palette = getattr(
obj,
"_default_palette",
Configuration.instance()["plotting.palette"],
)
if palette is not None and not isinstance(palette, Palette):
palette = palettes[palette]
if isinstance(target, (str, Path)):
save_path = str(target)
target = None
else:
save_path = None
if target is None:
if backend == "matplotlib":
# Use get current axes, customary in these cases
target = plt.gca()
elif backend == "plotly":
# Create a new figure if needed
target = plotly.graph_objects.Figure()
# Get the plotting function from the object
plotter = getattr(obj, "__plot__", None)
if plotter is None:
warn("%s does not support plotting" % (obj,), stacklevel=1)
return
else:
result = plotter(
backend,
target,
palette=palette,
*args, # noqa: B026
**kwds,
)
if save_path is not None:
if backend == "matplotlib":
target.figure.savefig(save_path)
elif backend == "plotly":
target.write_image(save_path)
# For matplotlib, return the container artist, which makes it easier
# to manipulate post-facto. The user can always get the artist with
# plt.gca() - as we do, in fact.
if backend == "matplotlib":
return result
return target
# Cairo backend
inline = False
if target is None and _is_running_in_ipython():
inline = kwds.get("inline")
if inline is None:
inline = Configuration.instance()["shell.ipython.inlining.Plot"]
palette = kwds.pop("palette", None)
background = kwds.pop("background", "white")
margin = float(kwds.pop("margin", 20))
result = CairoPlot(
target=target,
bbox=bbox,
palette=palette,
background=background,
)
item_bbox = result.bbox.contract(margin)
result.add(obj, item_bbox, *args, **kwds)
# If we requested an inline plot, just return the result and IPython will
# call its _repr_svg_ method. If we requested a non-inline plot, show the
# plot in a separate window and return nothing
if inline:
return result
# We are either not in IPython or the user specified an explicit plot target,
# so just show or save the result
if isinstance(target, (str, Path)):
# save
result.save()
# Also return the plot itself
return result