python-igraph manual

For using igraph from Python

   Home       Trees       Indices       Help   
Package igraph :: Package drawing
[hide private]

Source Code for Package igraph.drawing

  1  """ 
  2  Drawing and plotting routines for IGraph. 
  3   
  4  Plotting is dependent on the C{pycairo} library which provides Python bindings 
  5  to the popular U{Cairo library<http://www.cairographics.org>}. This means that 
  6  if you don't have U{pycairo<http://www.cairographics.org/pycairo>} installed, 
  7  you won't be able to use the plotting capabilities. However, you can still use 
  8  L{Graph.write_svg} to save the graph to an SVG file and view it from 
  9  U{Mozilla Firefox<http://www.mozilla.org/firefox>} (free) or edit it in 
 10  U{Inkscape<http://www.inkscape.org>} (free), U{Skencil<http://www.skencil.org>} 
 11  (formerly known as Sketch, also free) or Adobe Illustrator (not free, therefore 
 12  I'm not linking to it :)). 
 13  """ 
 14   
 15  from __future__ import with_statement 
 16   
 17  from cStringIO import StringIO 
 18  from warnings import warn 
 19   
 20  import os 
 21  import platform 
 22  import time 
 23   
 24  from igraph.compat import property, BytesIO 
 25  from igraph.configuration import Configuration 
 26  from igraph.drawing.colors import Palette, palettes 
 27  from igraph.drawing.graph import DefaultGraphDrawer 
 28  from igraph.drawing.utils import BoundingBox, Point, Rectangle, find_cairo 
 29  from igraph.utils import _is_running_in_ipython, named_temporary_file 
 30   
 31  __all__ = ["BoundingBox", "DefaultGraphDrawer", "Plot", "Point", "Rectangle", "plot"] 
 32   
 33  __license__ = "GPL" 
 34   
 35  cairo = find_cairo() 
36 37 ##################################################################### 38 39 -class Plot(object):
40 """Class representing an arbitrary plot 41 42 Every plot has an associated surface object where the plotting is done. The 43 surface is an instance of C{cairo.Surface}, a member of the C{pycairo} 44 library. The surface itself provides a unified API to various plotting 45 targets like SVG files, X11 windows, PostScript files, PNG files and so on. 46 C{igraph} usually does not know on which surface it is plotting right now, 47 since C{pycairo} takes care of the actual drawing. Everything that's supported 48 by C{pycairo} should be supported by this class as well. 49 50 Current Cairo surfaces that I'm aware of are: 51 52 - C{cairo.GlitzSurface} -- OpenGL accelerated surface for the X11 53 Window System. 54 55 - C{cairo.ImageSurface} -- memory buffer surface. Can be written to a 56 C{PNG} image file. 57 58 - C{cairo.PDFSurface} -- PDF document surface. 59 60 - C{cairo.PSSurface} -- PostScript document surface. 61 62 - C{cairo.SVGSurface} -- SVG (Scalable Vector Graphics) document surface. 63 64 - C{cairo.Win32Surface} -- Microsoft Windows screen rendering. 65 66 - C{cairo.XlibSurface} -- X11 Window System screen rendering. 67 68 If you create a C{Plot} object with a string given as the target surface, 69 the string will be treated as a filename, and its extension will decide 70 which surface class will be used. Please note that not all surfaces might 71 be available, depending on your C{pycairo} installation. 72 73 A C{Plot} has an assigned default palette (see L{igraph.drawing.colors.Palette}) 74 which is used for plotting objects. 75 76 A C{Plot} object also has a list of objects to be plotted with their 77 respective bounding boxes, palettes and opacities. Palettes assigned 78 to an object override the default palette of the plot. Objects can be 79 added by the L{Plot.add} method and removed by the L{Plot.remove} method. 80 """ 81 82 # pylint: disable-msg=E1101 83 # E1101: Module 'cairo' has no 'foo' member - of course it has! :)
84 - def __init__(self, target=None, bbox=None, palette=None, background=None):
85 """Creates a new plot. 86 87 @param target: the target surface to write to. It can be one of the 88 following types: 89 90 - C{None} -- an appropriate surface will be created and the object 91 will be plotted there. 92 93 - C{cairo.Surface} -- the given Cairo surface will be used. 94 95 - C{string} -- a file with the given name will be created and an 96 appropriate Cairo surface will be attached to it. 97 98 @param bbox: the bounding box of the surface. It is interpreted 99 differently with different surfaces: PDF and PS surfaces will 100 treat it as points (1 point = 1/72 inch). Image surfaces will 101 treat it as pixels. SVG surfaces will treat it as an abstract 102 unit, but it will mostly be interpreted as pixels when viewing 103 the SVG file in Firefox. 104 105 @param palette: the palette primarily used on the plot if the 106 added objects do not specify a private palette. Must be either 107 an L{igraph.drawing.colors.Palette} object or a string referring 108 to a valid key of C{igraph.drawing.colors.palettes} (see module 109 L{igraph.drawing.colors}) or C{None}. In the latter case, the default 110 palette given by the configuration key C{plotting.palette} is used. 111 112 @param background: the background color. If C{None}, the background 113 will be transparent. You can use any color specification here that 114 is understood by L{igraph.drawing.colors.color_name_to_rgba}. 115 """ 116 self._filename = None 117 self._surface_was_created = not isinstance(target, cairo.Surface) 118 self._need_tmpfile = False 119 120 # Several Windows-specific hacks will be used from now on, thanks 121 # to Dale Hunscher for debugging and fixing all that stuff 122 self._windows_hacks = "Windows" in platform.platform() 123 124 if bbox is None: 125 self.bbox = BoundingBox(600, 600) 126 elif isinstance(bbox, tuple) or isinstance(bbox, list): 127 self.bbox = BoundingBox(bbox) 128 else: 129 self.bbox = bbox 130 131 if palette is None: 132 config = Configuration.instance() 133 palette = config["plotting.palette"] 134 if not isinstance(palette, Palette): 135 palette = palettes[palette] 136 self._palette = palette 137 138 if target is None: 139 self._need_tmpfile = True 140 self._surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, \ 141 int(self.bbox.width), int(self.bbox.height)) 142 elif isinstance(target, cairo.Surface): 143 self._surface = target 144 else: 145 self._filename = target 146 _, ext = os.path.splitext(target) 147 ext = ext.lower() 148 if ext == ".pdf": 149 self._surface = cairo.PDFSurface(target, self.bbox.width, \ 150 self.bbox.height) 151 elif ext == ".ps" or ext == ".eps": 152 self._surface = cairo.PSSurface(target, self.bbox.width, \ 153 self.bbox.height) 154 elif ext == ".png": 155 self._surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, \ 156 int(self.bbox.width), int(self.bbox.height)) 157 elif ext == ".svg": 158 self._surface = cairo.SVGSurface(target, self.bbox.width, \ 159 self.bbox.height) 160 else: 161 raise ValueError("image format not handled by Cairo: %s" % ext) 162 163 self._ctx = cairo.Context(self._surface) 164 self._objects = [] 165 self._is_dirty = False 166 167 self.background = background 168
169 - def add(self, obj, bbox=None, palette=None, opacity=1.0, *args, **kwds):
170 """Adds an object to the plot. 171 172 Arguments not specified here are stored and passed to the object's 173 plotting function when necessary. Since you are most likely interested 174 in the arguments acceptable by graphs, see L{Graph.__plot__} for more 175 details. 176 177 @param obj: the object to be added 178 @param bbox: the bounding box of the object. If C{None}, the object 179 will fill the entire area of the plot. 180 @param palette: the color palette used for drawing the object. If the 181 object tries to get a color assigned to a positive integer, it 182 will use this palette. If C{None}, defaults to the global palette 183 of the plot. 184 @param opacity: the opacity of the object being plotted, in the range 185 0.0-1.0 186 187 @see: Graph.__plot__ 188 """ 189 if opacity < 0.0 or opacity > 1.0: 190 raise ValueError("opacity must be between 0.0 and 1.0") 191 if bbox is None: 192 bbox = self.bbox 193 if not isinstance(bbox, BoundingBox): 194 bbox = BoundingBox(bbox) 195 self._objects.append((obj, bbox, palette, opacity, args, kwds)) 196 self.mark_dirty() 197 198 @property
199 - def background(self):
200 """Returns the background color of the plot. C{None} means a 201 transparent background. 202 """ 203 return self._background 204 205 @background.setter
206 - def background(self, color):
207 """Sets the background color of the plot. C{None} means a 208 transparent background. You can use any color specification here 209 that is understood by the C{get} method of the current palette 210 or by L{igraph.colors.color_name_to_rgb}. 211 """ 212 if color is None: 213 self._background = None 214 else: 215 self._background = self._palette.get(color) 216
217 - def remove(self, obj, bbox=None, idx=1):
218 """Removes an object from the plot. 219 220 If the object has been added multiple times and no bounding box 221 was specified, it removes the instance which occurs M{idx}th 222 in the list of identical instances of the object. 223 224 @param obj: the object to be removed 225 @param bbox: optional bounding box specification for the object. 226 If given, only objects with exactly this bounding box will be 227 considered. 228 @param idx: if multiple objects match the specification given by 229 M{obj} and M{bbox}, only the M{idx}th occurrence will be removed. 230 @return: C{True} if the object has been removed successfully, 231 C{False} if the object was not on the plot at all or M{idx} 232 was larger than the count of occurrences 233 """ 234 for i in xrange(len(self._objects)): 235 current_obj, current_bbox = self._objects[i][0:2] 236 if current_obj is obj and (bbox is None or current_bbox == bbox): 237 idx -= 1 238 if idx == 0: 239 self._objects[i:(i+1)] = [] 240 self.mark_dirty() 241 return True 242 return False 243
244 - def mark_dirty(self):
245 """Marks the plot as dirty (should be redrawn)""" 246 self._is_dirty = True 247 248 # pylint: disable-msg=W0142 249 # W0142: used * or ** magic
250 - def redraw(self, context=None):
251 """Redraws the plot""" 252 ctx = context or self._ctx 253 if self._background is not None: 254 ctx.set_source_rgba(*self._background) 255 ctx.rectangle(0, 0, self.bbox.width, self.bbox.height) 256 ctx.fill() 257 258 for obj, bbox, palette, opacity, args, kwds in self._objects: 259 if palette is None: 260 palette = getattr(obj, "_default_palette", self._palette) 261 plotter = getattr(obj, "__plot__", None) 262 if plotter is None: 263 warn("%s does not support plotting" % obj) 264 else: 265 if opacity < 1.0: 266 ctx.push_group() 267 else: 268 ctx.save() 269 plotter(ctx, bbox, palette, *args, **kwds) 270 if opacity < 1.0: 271 ctx.pop_group_to_source() 272 ctx.paint_with_alpha(opacity) 273 else: 274 ctx.restore() 275 276 self._is_dirty = False 277
278 - def save(self, fname=None):
279 """Saves the plot. 280 281 @param fname: the filename to save to. It is ignored if the surface 282 of the plot is not an C{ImageSurface}. 283 """ 284 if self._is_dirty: 285 self.redraw() 286 if isinstance(self._surface, cairo.ImageSurface): 287 if fname is None and self._need_tmpfile: 288 with named_temporary_file(prefix="igraph", suffix=".png") as fname: 289 self._surface.write_to_png(fname) 290 return None 291 292 fname = fname or self._filename 293 if fname is None: 294 raise ValueError("no file name is known for the surface " + \ 295 "and none given") 296 return self._surface.write_to_png(fname) 297 298 if fname is not None: 299 warn("filename is ignored for surfaces other than ImageSurface") 300 301 self._ctx.show_page() 302 self._surface.finish() 303 304
305 - def show(self):
306 """Saves the plot to a temporary file and shows it.""" 307 if not isinstance(self._surface, cairo.ImageSurface): 308 sur = cairo.ImageSurface(cairo.FORMAT_ARGB32, 309 int(self.bbox.width), int(self.bbox.height)) 310 ctx = cairo.Context(sur) 311 self.redraw(ctx) 312 else: 313 sur = self._surface 314 ctx = self._ctx 315 if self._is_dirty: 316 self.redraw(ctx) 317 318 with named_temporary_file(prefix="igraph", suffix=".png") as tmpfile: 319 sur.write_to_png(tmpfile) 320 config = Configuration.instance() 321 imgviewer = config["apps.image_viewer"] 322 if not imgviewer: 323 # No image viewer was given and none was detected. This 324 # should only happen on unknown platforms. 325 plat = platform.system() 326 raise NotImplementedError("showing plots is not implemented " + \ 327 "on this platform: %s" % plat) 328 else: 329 os.system("%s %s" % (imgviewer, tmpfile)) 330 if platform.system() == "Darwin" or self._windows_hacks: 331 # On Mac OS X and Windows, launched applications are likely to 332 # fork and give control back to Python immediately. 333 # Chances are that the temporary image file gets removed 334 # before the image viewer has a chance to open it, so 335 # we wait here a little bit. Yes, this is quite hackish :( 336 time.sleep(5) 337
338 - def _repr_svg_(self):
339 """Returns an SVG representation of this plot as a string. 340 341 This method is used by IPython to display this plot inline. 342 """ 343 io = BytesIO() 344 # Create a new SVG surface and use that to get the SVG representation, 345 # which will end up in io 346 surface = cairo.SVGSurface(io, self.bbox.width, self.bbox.height) 347 context = cairo.Context(surface) 348 # Plot the graph on this context 349 self.redraw(context) 350 # No idea why this is needed but python crashes without 351 context.show_page() 352 surface.finish() 353 # Return the raw SVG representation 354 return io.getvalue().encode("utf-8") 355 356 @property
357 - def bounding_box(self):
358 """Returns the bounding box of the Cairo surface as a 359 L{BoundingBox} object""" 360 return BoundingBox(self.bbox) 361 362 @property
363 - def height(self):
364 """Returns the height of the Cairo surface on which the plot 365 is drawn""" 366 return self.bbox.height 367 368 @property
369 - def surface(self):
370 """Returns the Cairo surface on which the plot is drawn""" 371 return self._surface 372 373 @property
374 - def width(self):
375 """Returns the width of the Cairo surface on which the plot 376 is drawn""" 377 return self.bbox.width
378
379 ##################################################################### 380 381 -def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds):
382 """Plots the given object to the given target. 383 384 Positional and keyword arguments not explicitly mentioned here will be 385 passed down to the C{__plot__} method of the object being plotted. 386 Since you are most likely interested in the keyword arguments available 387 for graph plots, see L{Graph.__plot__} as well. 388 389 @param obj: the object to be plotted 390 @param target: the target where the object should be plotted. It can be one 391 of the following types: 392 393 - C{None} -- an appropriate surface will be created and the object will 394 be plotted there. 395 396 - C{cairo.Surface} -- the given Cairo surface will be used. This can 397 refer to a PNG image, an arbitrary window, an SVG file, anything that 398 Cairo can handle. 399 400 - C{string} -- a file with the given name will be created and an 401 appropriate Cairo surface will be attached to it. The supported image 402 formats are: PNG, PDF, SVG and PostScript. 403 404 @param bbox: the bounding box of the plot. It must be a tuple with either 405 two or four integers, or a L{BoundingBox} object. If this is a tuple 406 with two integers, it is interpreted as the width and height of the plot 407 (in pixels for PNG images and on-screen plots, or in points for PDF, 408 SVG and PostScript plots, where 72 pt = 1 inch = 2.54 cm). If this is 409 a tuple with four integers, the first two denotes the X and Y coordinates 410 of a corner and the latter two denoting the X and Y coordinates of the 411 opposite corner. 412 413 @keyword opacity: the opacity of the object being plotted. It can be 414 used to overlap several plots of the same graph if you use the same 415 layout for them -- for instance, you might plot a graph with opacity 416 0.5 and then plot its spanning tree over it with opacity 0.1. To 417 achieve this, you'll need to modify the L{Plot} object returned with 418 L{Plot.add}. 419 420 @keyword palette: the palette primarily used on the plot if the 421 added objects do not specify a private palette. Must be either 422 an L{igraph.drawing.colors.Palette} object or a string referring 423 to a valid key of C{igraph.drawing.colors.palettes} (see module 424 L{igraph.drawing.colors}) or C{None}. In the latter case, the default 425 palette given by the configuration key C{plotting.palette} is used. 426 427 @keyword margin: the top, right, bottom, left margins as a 4-tuple. 428 If it has less than 4 elements or is a single float, the elements 429 will be re-used until the length is at least 4. The default margin 430 is 20 on each side. 431 432 @keyword inline: whether to try and show the plot object inline in the 433 current IPython notebook. Passing ``None`` here or omitting this keyword 434 argument will look up the preferred behaviour from the 435 C{shell.ipython.inlining.Plot} configuration key. Note that this keyword 436 argument has an effect only if igraph is run inside IPython and C{target} 437 is C{None}. 438 439 @return: an appropriate L{Plot} object. 440 441 @see: Graph.__plot__ 442 """ 443 if not isinstance(bbox, BoundingBox): 444 bbox = BoundingBox(bbox) 445 446 result = Plot(target, bbox, background=kwds.get("background", "white")) 447 448 if "margin" in kwds: 449 bbox = bbox.contract(kwds["margin"]) 450 del kwds["margin"] 451 else: 452 bbox = bbox.contract(20) 453 result.add(obj, bbox, *args, **kwds) 454 455 if target is None and _is_running_in_ipython(): 456 # Get the default value of the `inline` argument from the configuration if 457 # needed 458 inline = kwds.get("inline") 459 if inline is None: 460 config = Configuration.instance() 461 inline = config["shell.ipython.inlining.Plot"] 462 463 # If we requested an inline plot, just return the result and IPython will 464 # call its _repr_svg_ method. If we requested a non-inline plot, show the 465 # plot in a separate window and return nothing 466 if inline: 467 return result 468 else: 469 result.show() 470 return 471 472 # We are either not in IPython or the user specified an explicit plot target, 473 # so just show or save the result 474 if target is None: 475 result.show() 476 elif isinstance(target, basestring): 477 result.save() 478 479 # Also return the plot itself 480 return result 481 482 ##################################################################### 483

   Home       Trees       Indices       Help