python-igraph manual

For using igraph from Python

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

Source Code for Module igraph.drawing.graph

  1  """ 
  2  Drawing routines to draw graphs. 
  3   
  4  This module contains routines to draw graphs on: 
  5   
  6    - Cairo surfaces (L{DefaultGraphDrawer}) 
  7    - UbiGraph displays (L{UbiGraphDrawer}, see U{http://ubietylab.net/ubigraph}) 
  8   
  9  It also contains routines to send an igraph graph directly to 
 10  (U{Cytoscape<http://www.cytoscape.org>}) using the 
 11  (U{CytoscapeRPC plugin<http://gforge.nbic.nl/projects/cytoscaperpc/>}), see 
 12  L{CytoscapeGraphDrawer}. L{CytoscapeGraphDrawer} can also fetch the current 
 13  network from Cytoscape and convert it to igraph format. 
 14  """ 
 15   
 16  from collections import defaultdict 
 17  from itertools import izip 
 18  from math import atan2, cos, pi, sin, tan 
 19  from warnings import warn 
 20   
 21  from igraph._igraph import convex_hull, VertexSeq 
 22  from igraph.compat import property 
 23  from igraph.configuration import Configuration 
 24  from igraph.drawing.baseclasses import AbstractDrawer, AbstractCairoDrawer, \ 
 25                                         AbstractXMLRPCDrawer 
 26  from igraph.drawing.colors import color_to_html_format, color_name_to_rgb 
 27  from igraph.drawing.edge import ArrowEdgeDrawer 
 28  from igraph.drawing.text import TextAlignment, TextDrawer 
 29  from igraph.drawing.metamagic import AttributeCollectorBase 
 30  from igraph.drawing.shapes import PolygonDrawer 
 31  from igraph.drawing.utils import find_cairo, Point 
 32  from igraph.drawing.vertex import DefaultVertexDrawer 
 33  from igraph.layout import Layout 
 34   
 35  __all__ = ["DefaultGraphDrawer", "UbiGraphDrawer", "CytoscapeGraphDrawer"] 
 36  __license__ = "GPL" 
 37   
 38  cairo = find_cairo() 
39 40 ##################################################################### 41 42 # pylint: disable-msg=R0903 43 # R0903: too few public methods 44 -class AbstractGraphDrawer(AbstractDrawer):
45 """Abstract class that serves as a base class for anything that 46 draws an igraph.Graph.""" 47 48 # pylint: disable-msg=W0221 49 # W0221: argument number differs from overridden method 50 # E1101: Module 'cairo' has no 'foo' member - of course it does :)
51 - def draw(self, graph, *args, **kwds):
52 """Abstract method, must be implemented in derived classes.""" 53 raise NotImplementedError("abstract class") 54
55 - def ensure_layout(self, layout, graph = None):
56 """Helper method that ensures that I{layout} is an instance 57 of L{Layout}. If it is not, the method will try to convert 58 it to a L{Layout} according to the following rules: 59 60 - If I{layout} is a string, it is assumed to be a name 61 of an igraph layout, and it will be passed on to the 62 C{layout} method of the given I{graph} if I{graph} is 63 not C{None}. 64 65 - If I{layout} is C{None}, the C{layout} method of 66 I{graph} will be invoked with no parameters, which 67 will call the default layout algorithm. 68 69 - Otherwise, I{layout} will be passed on to the constructor 70 of L{Layout}. This handles lists of lists, lists of tuples 71 and such. 72 73 If I{layout} is already a L{Layout} instance, it will still 74 be copied and a copy will be returned. This is because graph 75 drawers are allowed to transform the layout for their purposes, 76 and we don't want the transformation to propagate back to the 77 caller. 78 """ 79 if isinstance(layout, Layout): 80 layout = Layout(layout.coords) 81 elif isinstance(layout, str) or layout is None: 82 layout = graph.layout(layout) 83 else: 84 layout = Layout(layout) 85 return layout
86
87 ##################################################################### 88 89 -class AbstractCairoGraphDrawer(AbstractGraphDrawer, AbstractCairoDrawer):
90 """Abstract base class for graph drawers that draw on a Cairo canvas. 91 """ 92
93 - def __init__(self, context, bbox):
94 """Constructs the graph drawer and associates it to the given 95 Cairo context and the given L{BoundingBox}. 96 97 @param context: the context on which we will draw 98 @param bbox: the bounding box within which we will draw. 99 Can be anything accepted by the constructor 100 of L{BoundingBox} (i.e., a 2-tuple, a 4-tuple 101 or a L{BoundingBox} object). 102 """ 103 AbstractCairoDrawer.__init__(self, context, bbox) 104 AbstractGraphDrawer.__init__(self)
105
106 ##################################################################### 107 108 -class DefaultGraphDrawer(AbstractCairoGraphDrawer):
109 """Class implementing the default visualisation of a graph. 110 111 The default visualisation of a graph draws the nodes on a 2D plane 112 according to a given L{Layout}, then draws a straight or curved 113 edge between nodes connected by edges. This is the visualisation 114 used when one invokes the L{plot()} function on a L{Graph} object. 115 116 See L{Graph.__plot__()} for the keyword arguments understood by 117 this drawer.""" 118
119 - def __init__(self, context, bbox, \ 120 vertex_drawer_factory = DefaultVertexDrawer, 121 edge_drawer_factory = ArrowEdgeDrawer, 122 label_drawer_factory = TextDrawer):
123 """Constructs the graph drawer and associates it to the given 124 Cairo context and the given L{BoundingBox}. 125 126 @param context: the context on which we will draw 127 @param bbox: the bounding box within which we will draw. 128 Can be anything accepted by the constructor 129 of L{BoundingBox} (i.e., a 2-tuple, a 4-tuple 130 or a L{BoundingBox} object). 131 @param vertex_drawer_factory: a factory method that returns an 132 L{AbstractCairoVertexDrawer} instance bound to a 133 given Cairo context. The factory method must take 134 three parameters: the Cairo context, the bounding 135 box of the drawing area and the palette to be 136 used for drawing colored vertices. The default 137 vertex drawer is L{DefaultVertexDrawer}. 138 @param edge_drawer_factory: a factory method that returns an 139 L{AbstractEdgeDrawer} instance bound to a 140 given Cairo context. The factory method must take 141 two parameters: the Cairo context and the palette 142 to be used for drawing colored edges. You can use 143 any of the actual L{AbstractEdgeDrawer} 144 implementations here to control the style of 145 edges drawn by igraph. The default edge drawer is 146 L{ArrowEdgeDrawer}. 147 @param label_drawer_factory: a factory method that returns a 148 L{TextDrawer} instance bound to a given Cairo 149 context. The method must take one parameter: the 150 Cairo context. The default label drawer is 151 L{TextDrawer}. 152 """ 153 AbstractCairoGraphDrawer.__init__(self, context, bbox) 154 self.vertex_drawer_factory = vertex_drawer_factory 155 self.edge_drawer_factory = edge_drawer_factory 156 self.label_drawer_factory = label_drawer_factory 157
158 - def _determine_edge_order(self, graph, kwds):
159 """Returns the order in which the edge of the given graph have to be 160 drawn, assuming that the relevant keyword arguments (C{edge_order} and 161 C{edge_order_by}) are given in C{kwds} as a dictionary. If neither 162 C{edge_order} nor C{edge_order_by} is present in C{kwds}, this 163 function returns C{None} to indicate that the graph drawer is free to 164 choose the most convenient edge ordering.""" 165 if "edge_order" in kwds: 166 # Edge order specified explicitly 167 return kwds["edge_order"] 168 169 if kwds.get("edge_order_by") is None: 170 # No edge order specified 171 return None 172 173 # Order edges by the value of some attribute 174 edge_order_by = kwds["edge_order_by"] 175 reverse = False 176 if isinstance(edge_order_by, tuple): 177 edge_order_by, reverse = edge_order_by 178 if isinstance(reverse, basestring): 179 reverse = reverse.lower().startswith("desc") 180 attrs = graph.es[edge_order_by] 181 edge_order = sorted(range(len(attrs)), key=attrs.__getitem__, 182 reverse=bool(reverse)) 183 184 return edge_order 185
186 - def _determine_vertex_order(self, graph, kwds):
187 """Returns the order in which the vertices of the given graph have to be 188 drawn, assuming that the relevant keyword arguments (C{vertex_order} and 189 C{vertex_order_by}) are given in C{kwds} as a dictionary. If neither 190 C{vertex_order} nor C{vertex_order_by} is present in C{kwds}, this 191 function returns C{None} to indicate that the graph drawer is free to 192 choose the most convenient vertex ordering.""" 193 if "vertex_order" in kwds: 194 # Vertex order specified explicitly 195 return kwds["vertex_order"] 196 197 if kwds.get("vertex_order_by") is None: 198 # No vertex order specified 199 return None 200 201 # Order vertices by the value of some attribute 202 vertex_order_by = kwds["vertex_order_by"] 203 reverse = False 204 if isinstance(vertex_order_by, tuple): 205 vertex_order_by, reverse = vertex_order_by 206 if isinstance(reverse, basestring): 207 reverse = reverse.lower().startswith("desc") 208 attrs = graph.vs[vertex_order_by] 209 vertex_order = sorted(range(len(attrs)), key=attrs.__getitem__, 210 reverse=bool(reverse)) 211 212 return vertex_order 213 214 # pylint: disable-msg=W0142,W0221,E1101 215 # W0142: Used * or ** magic 216 # W0221: argument number differs from overridden method 217 # E1101: Module 'cairo' has no 'foo' member - of course it does :)
218 - def draw(self, graph, palette, *args, **kwds):
219 # Some abbreviations for sake of simplicity 220 directed = graph.is_directed() 221 context = self.context 222 223 # Calculate/get the layout of the graph 224 layout = self.ensure_layout(kwds.get("layout", None), graph) 225 226 # Determine the size of the margin on each side 227 margin = kwds.get("margin", 0) 228 try: 229 margin = list(margin) 230 except TypeError: 231 margin = [margin] 232 while len(margin)<4: 233 margin.extend(margin) 234 235 # Contract the drawing area by the margin and fit the layout 236 bbox = self.bbox.contract(margin) 237 layout.fit_into(bbox, keep_aspect_ratio=kwds.get("keep_aspect_ratio", False)) 238 239 # Decide whether we need to calculate the curvature of edges 240 # automatically -- and calculate them if needed. 241 autocurve = kwds.get("autocurve", None) 242 if autocurve or (autocurve is None and \ 243 "edge_curved" not in kwds and "curved" not in graph.edge_attributes() \ 244 and graph.ecount() < 10000): 245 from igraph import autocurve 246 default = kwds.get("edge_curved", 0) 247 if default is True: 248 default = 0.5 249 default = float(default) 250 kwds["edge_curved"] = autocurve(graph, attribute=None, default=default) 251 252 # Construct the vertex, edge and label drawers 253 vertex_drawer = self.vertex_drawer_factory(context, bbox, palette, layout) 254 edge_drawer = self.edge_drawer_factory(context, palette) 255 label_drawer = self.label_drawer_factory(context) 256 257 # Construct the visual vertex/edge builders based on the specifications 258 # provided by the vertex_drawer and the edge_drawer 259 vertex_builder = vertex_drawer.VisualVertexBuilder(graph.vs, kwds) 260 edge_builder = edge_drawer.VisualEdgeBuilder(graph.es, kwds) 261 262 # Determine the order in which we will draw the vertices and edges 263 vertex_order = self._determine_vertex_order(graph, kwds) 264 edge_order = self._determine_edge_order(graph, kwds) 265 266 # Draw the highlighted groups (if any) 267 if "mark_groups" in kwds: 268 mark_groups = kwds["mark_groups"] 269 270 # Figure out what to do with mark_groups in order to be able to 271 # iterate over it and get memberlist-color pairs 272 if isinstance(mark_groups, dict): 273 group_iter = mark_groups.iteritems() 274 elif hasattr(mark_groups, "__iter__"): 275 # Lists, tuples, iterators etc 276 group_iter = iter(mark_groups) 277 else: 278 # False 279 group_iter = {}.iteritems() 280 281 # We will need a polygon drawer to draw the convex hulls 282 polygon_drawer = PolygonDrawer(context, bbox) 283 284 # Iterate over color-memberlist pairs 285 for group, color_id in group_iter: 286 if not group or color_id is None: 287 continue 288 289 color = palette.get(color_id) 290 291 if isinstance(group, VertexSeq): 292 group = [vertex.index for vertex in group] 293 if not hasattr(group, "__iter__"): 294 raise TypeError("group membership list must be iterable") 295 296 # Get the vertex indices that constitute the convex hull 297 hull = [group[i] for i in convex_hull([layout[idx] for idx in group])] 298 299 # Calculate the preferred rounding radius for the corners 300 corner_radius = 1.25 * max(vertex_builder[idx].size for idx in hull) 301 302 # Construct the polygon 303 polygon = [layout[idx] for idx in hull] 304 305 if len(polygon) == 2: 306 # Expand the polygon (which is a flat line otherwise) 307 a, b = Point(*polygon[0]), Point(*polygon[1]) 308 c = corner_radius * (a-b).normalized() 309 n = Point(-c[1], c[0]) 310 polygon = [a + n, b + n, b - c, b - n, a - n, a + c] 311 else: 312 # Expand the polygon around its center of mass 313 center = Point(*[sum(coords) / float(len(coords)) 314 for coords in zip(*polygon)]) 315 polygon = [Point(*point).towards(center, -corner_radius) 316 for point in polygon] 317 318 # Draw the hull 319 context.set_source_rgba(color[0], color[1], color[2], 320 color[3]*0.25) 321 polygon_drawer.draw_path(polygon, corner_radius=corner_radius) 322 context.fill_preserve() 323 context.set_source_rgba(*color) 324 context.stroke() 325 326 # Construct the iterator that we will use to draw the edges 327 es = graph.es 328 if edge_order is None: 329 # Default edge order 330 edge_coord_iter = izip(es, edge_builder) 331 else: 332 # Specified edge order 333 edge_coord_iter = ((es[i], edge_builder[i]) for i in edge_order) 334 335 # Draw the edges 336 if directed: 337 drawer_method = edge_drawer.draw_directed_edge 338 else: 339 drawer_method = edge_drawer.draw_undirected_edge 340 for edge, visual_edge in edge_coord_iter: 341 src, dest = edge.tuple 342 src_vertex, dest_vertex = vertex_builder[src], vertex_builder[dest] 343 drawer_method(visual_edge, src_vertex, dest_vertex) 344 345 # Construct the iterator that we will use to draw the vertices 346 vs = graph.vs 347 if vertex_order is None: 348 # Default vertex order 349 vertex_coord_iter = izip(vs, vertex_builder, layout) 350 else: 351 # Specified vertex order 352 vertex_coord_iter = ((vs[i], vertex_builder[i], layout[i]) 353 for i in vertex_order) 354 355 # Draw the vertices 356 drawer_method = vertex_drawer.draw 357 context.set_line_width(1) 358 for vertex, visual_vertex, coords in vertex_coord_iter: 359 drawer_method(visual_vertex, vertex, coords) 360 361 # Set the font we will use to draw the labels 362 context.select_font_face("sans-serif", cairo.FONT_SLANT_NORMAL, \ 363 cairo.FONT_WEIGHT_NORMAL) 364 365 # Decide whether the labels have to be wrapped 366 wrap = kwds.get("wrap_labels") 367 if wrap is None: 368 wrap = Configuration.instance()["plotting.wrap_labels"] 369 wrap = bool(wrap) 370 371 # Construct the iterator that we will use to draw the vertex labels 372 if vertex_order is None: 373 # Default vertex order 374 vertex_coord_iter = izip(vertex_builder, layout) 375 else: 376 # Specified vertex order 377 vertex_coord_iter = ((vertex_builder[i], layout[i]) 378 for i in vertex_order) 379 380 # Draw the vertex labels 381 for vertex, coords in vertex_coord_iter: 382 if vertex.label is None: 383 continue 384 385 context.set_font_size(vertex.label_size) 386 context.set_source_rgba(*vertex.label_color) 387 label_drawer.text = vertex.label 388 389 if vertex.label_dist: 390 # Label is displaced from the center of the vertex. 391 _, yb, w, h, _, _ = label_drawer.text_extents() 392 w, h = w/2.0, h/2.0 393 radius = vertex.label_dist * vertex.size / 2. 394 # First we find the reference point that is at distance `radius' 395 # from the vertex in the direction given by `label_angle'. 396 # Then we place the label in a way that the line connecting the 397 # center of the bounding box of the label with the center of the 398 # vertex goes through the reference point and the reference 399 # point lies exactly on the bounding box of the vertex. 400 alpha = vertex.label_angle % (2*pi) 401 cx = coords[0] + radius * cos(alpha) 402 cy = coords[1] - radius * sin(alpha) 403 # Now we have the reference point. We have to decide which side 404 # of the label box will intersect with the line that connects 405 # the center of the label with the center of the vertex. 406 if w > 0: 407 beta = atan2(h, w) % (2*pi) 408 else: 409 beta = pi/2. 410 gamma = pi - beta 411 if alpha > 2*pi-beta or alpha <= beta: 412 # Intersection at left edge of label 413 cx += w 414 cy -= tan(alpha) * w 415 elif alpha > beta and alpha <= gamma: 416 # Intersection at bottom edge of label 417 try: 418 cx += h / tan(alpha) 419 except: 420 pass # tan(alpha) == inf 421 cy -= h 422 elif alpha > gamma and alpha <= gamma + 2*beta: 423 # Intersection at right edge of label 424 cx -= w 425 cy += tan(alpha) * w 426 else: 427 # Intersection at top edge of label 428 try: 429 cx -= h / tan(alpha) 430 except: 431 pass # tan(alpha) == inf 432 cy += h 433 # Draw the label 434 label_drawer.draw_at(cx-w, cy-h-yb, wrap=wrap) 435 else: 436 # Label is exactly in the center of the vertex 437 cx, cy = coords 438 half_size = vertex.size / 2. 439 label_drawer.bbox = (cx - half_size, cy - half_size, 440 cx + half_size, cy + half_size) 441 label_drawer.draw(wrap=wrap) 442 443 # Construct the iterator that we will use to draw the edge labels 444 es = graph.es 445 if edge_order is None: 446 # Default edge order 447 edge_coord_iter = izip(es, edge_builder) 448 else: 449 # Specified edge order 450 edge_coord_iter = ((es[i], edge_builder[i]) for i in edge_order) 451 452 # Draw the edge labels 453 for edge, visual_edge in edge_coord_iter: 454 if visual_edge.label is None: 455 continue 456 457 # Set the font size, color and text 458 context.set_font_size(visual_edge.label_size) 459 context.set_source_rgba(*visual_edge.label_color) 460 label_drawer.text = visual_edge.label 461 462 # Ask the edge drawer to propose an anchor point for the label 463 src, dest = edge.tuple 464 src_vertex, dest_vertex = vertex_builder[src], vertex_builder[dest] 465 (x, y), (halign, valign) = \ 466 edge_drawer.get_label_position(edge, src_vertex, dest_vertex) 467 468 # Measure the text 469 _, yb, w, h, _, _ = label_drawer.text_extents() 470 w /= 2.0 471 h /= 2.0 472 473 # Place the text relative to the edge 474 if halign == TextAlignment.RIGHT: 475 x -= w 476 elif halign == TextAlignment.LEFT: 477 x += w 478 if valign == TextAlignment.BOTTOM: 479 y -= h - yb / 2.0 480 elif valign == TextAlignment.TOP: 481 y += h 482 483 # Draw the edge label 484 label_drawer.halign = halign 485 label_drawer.valign = valign 486 label_drawer.bbox = (x-w, y-h, x+w, y+h) 487 label_drawer.draw(wrap=wrap)
488
489 490 ##################################################################### 491 492 -class UbiGraphDrawer(AbstractXMLRPCDrawer, AbstractGraphDrawer):
493 """Graph drawer that draws a given graph on an UbiGraph display 494 using the XML-RPC API of UbiGraph. 495 496 The following vertex attributes are supported: C{color}, C{label}, 497 C{shape}, C{size}. See the Ubigraph documentation for supported shape 498 names. Sizes are relative to the default Ubigraph size. 499 500 The following edge attributes are supported: C{color}, C{label}, 501 C{width}. Edge widths are relative to the default Ubigraph width. 502 503 All color specifications supported by igraph (e.g., color names, 504 palette indices, RGB triplets, RGBA quadruplets, HTML format) 505 are understood by the Ubigraph graph drawer. 506 507 The drawer also has two attributes, C{vertex_defaults} and 508 C{edge_defaults}. These are dictionaries that can be used to 509 set default values for the vertex/edge attributes in Ubigraph. 510 """ 511
512 - def __init__(self, url="http://localhost:20738/RPC2"):
513 """Constructs an UbiGraph drawer using the display at the given 514 URL.""" 515 super(UbiGraphDrawer, self).__init__(url, "ubigraph") 516 self.vertex_defaults = dict( 517 color="#ff0000", 518 shape="cube", 519 size=1.0 520 ) 521 self.edge_defaults = dict( 522 color="#ffffff", 523 width=1.0 524 ) 525
526 - def draw(self, graph, *args, **kwds):
527 """Draws the given graph on an UbiGraph display. 528 529 @keyword clear: whether to clear the current UbiGraph display before 530 plotting. Default: C{True}.""" 531 display = self.service 532 533 # Clear the display and set the default visual attributes 534 if kwds.get("clear", True): 535 display.clear() 536 537 for k, v in self.vertex_defaults.iteritems(): 538 display.set_vertex_style_attribute(0, k, str(v)) 539 for k, v in self.edge_defaults.iteritems(): 540 display.set_edge_style_attribute(0, k, str(v)) 541 542 # Custom color converter function 543 def color_conv(color): 544 return color_to_html_format(color_name_to_rgb(color)) 545 546 # Construct the visual vertex/edge builders 547 class VisualVertexBuilder(AttributeCollectorBase): 548 """Collects some visual properties of a vertex for drawing""" 549 _kwds_prefix = "vertex_" 550 color = (str(self.vertex_defaults["color"]), color_conv) 551 label = None 552 shape = str(self.vertex_defaults["shape"]) 553 size = float(self.vertex_defaults["size"]) 554 555 class VisualEdgeBuilder(AttributeCollectorBase): 556 """Collects some visual properties of an edge for drawing""" 557 _kwds_prefix = "edge_" 558 color = (str(self.edge_defaults["color"]), color_conv) 559 label = None 560 width = float(self.edge_defaults["width"]) 561 562 vertex_builder = VisualVertexBuilder(graph.vs, kwds) 563 edge_builder = VisualEdgeBuilder(graph.es, kwds) 564 565 # Add the vertices 566 n = graph.vcount() 567 new_vertex = display.new_vertex 568 vertex_ids = [new_vertex() for _ in xrange(n)] 569 570 # Add the edges 571 new_edge = display.new_edge 572 eids = [new_edge(vertex_ids[edge.source], vertex_ids[edge.target]) \ 573 for edge in graph.es] 574 575 # Add arrowheads if needed 576 if graph.is_directed(): 577 display.set_edge_style_attribute(0, "arrow", "true") 578 579 # Set the vertex attributes 580 set_attr = display.set_vertex_attribute 581 vertex_defaults = self.vertex_defaults 582 for vertex_id, vertex in izip(vertex_ids, vertex_builder): 583 if vertex.color != vertex_defaults["color"]: 584 set_attr(vertex_id, "color", vertex.color) 585 if vertex.label: 586 set_attr(vertex_id, "label", str(vertex.label)) 587 if vertex.shape != vertex_defaults["shape"]: 588 set_attr(vertex_id, "shape", vertex.shape) 589 if vertex.size != vertex_defaults["size"]: 590 set_attr(vertex_id, "size", str(vertex.size)) 591 592 # Set the edge attributes 593 set_attr = display.set_edge_attribute 594 edge_defaults = self.edge_defaults 595 for edge_id, edge in izip(eids, edge_builder): 596 if edge.color != edge_defaults["color"]: 597 set_attr(edge_id, "color", edge.color) 598 if edge.label: 599 set_attr(edge_id, "label", edge.label) 600 if edge.width != edge_defaults["width"]: 601 set_attr(edge_id, "width", str(edge.width))
602
603 ##################################################################### 604 605 -class CytoscapeGraphDrawer(AbstractXMLRPCDrawer, AbstractGraphDrawer):
606 """Graph drawer that sends/receives graphs to/from Cytoscape using 607 CytoscapeRPC. 608 609 This graph drawer cooperates with U{Cytoscape<http://www.cytoscape.org>} 610 using U{CytoscapeRPC<http://wiki.nbic.nl/index.php/CytoscapeRPC>}. 611 You need to install the CytoscapeRPC plugin first and start the 612 XML-RPC server on a given port (port 9000 by default) from the 613 appropriate Plugins submenu in Cytoscape. 614 615 Graph, vertex and edge attributes are transferred to Cytoscape whenever 616 possible (i.e. when a suitable mapping exists between a Python type 617 and a Cytoscape type). If there is no suitable Cytoscape type for a 618 Python type, the drawer will use a string attribute on the Cytoscape 619 side and invoke C{str()} on the Python attributes. 620 621 If an attribute to be created on the Cytoscape side already exists with 622 a different type, an underscore will be appended to the attribute name 623 to resolve the type conflict. 624 625 You can use the C{network_id} attribute of this class to figure out the 626 network ID of the last graph drawn with this drawer. 627 """ 628
629 - def __init__(self, url="http://localhost:9000/Cytoscape"):
630 """Constructs a Cytoscape graph drawer using the XML-RPC interface 631 of Cytoscape at the given URL.""" 632 super(CytoscapeGraphDrawer, self).__init__(url, "Cytoscape") 633 self.network_id = None 634
635 - def draw(self, graph, name="Network from igraph", create_view=True, 636 *args, **kwds):
637 """Sends the given graph to Cytoscape as a new network. 638 639 @param name: the name of the network in Cytoscape. 640 @param create_view: whether to create a view for the network 641 in Cytoscape.The default is C{True}. 642 @keyword node_ids: specifies the identifiers of the nodes to 643 be used in Cytoscape. This must either be the name of a 644 vertex attribute or a list specifying the identifiers, one 645 for each node in the graph. The default is C{None}, which 646 simply uses the vertex index for each vertex.""" 647 from xmlrpclib import Fault 648 649 cy = self.service 650 651 # Create the network 652 if not create_view: 653 try: 654 network_id = cy.createNetwork(name, False) 655 except Fault: 656 warn("CytoscapeRPC too old, cannot create network without view." 657 " Consider upgrading CytoscapeRPC to use this feature.") 658 network_id = cy.createNetwork(name) 659 else: 660 network_id = cy.createNetwork(name) 661 self.network_id = network_id 662 663 # Create the nodes 664 if "node_ids" in kwds: 665 node_ids = kwds["node_ids"] 666 if isinstance(node_ids, basestring): 667 node_ids = graph.vs[node_ids] 668 else: 669 node_ids = xrange(graph.vcount()) 670 node_ids = [str(identifier) for identifier in node_ids] 671 cy.createNodes(network_id, node_ids) 672 673 # Create the edges 674 edgelists = [[], []] 675 for v1, v2 in graph.get_edgelist(): 676 edgelists[0].append(node_ids[v1]) 677 edgelists[1].append(node_ids[v2]) 678 edge_ids = cy.createEdges(network_id, 679 edgelists[0], edgelists[1], 680 ["unknown"] * graph.ecount(), 681 [graph.is_directed()] * graph.ecount(), 682 False 683 ) 684 685 if "layout" in kwds: 686 # Calculate/get the layout of the graph 687 layout = self.ensure_layout(kwds["layout"], graph) 688 size = 100 * graph.vcount() ** 0.5 689 layout.fit_into((size, size), keep_aspect_ratio=True) 690 layout.translate(-size/2., -size/2.) 691 cy.setNodesPositions(network_id, 692 node_ids, *zip(*list(layout))) 693 else: 694 # Ask Cytoscape to perform the default layout so the user can 695 # at least see something in Cytoscape while the attributes are 696 # being transferred 697 cy.performDefaultLayout(network_id) 698 699 # Send the network attributes 700 attr_names = set(cy.getNetworkAttributeNames()) 701 for attr in graph.attributes(): 702 cy_type, value = self.infer_cytoscape_type([graph[attr]]) 703 value = value[0] 704 if value is None: 705 continue 706 707 # Resolve type conflicts (if any) 708 try: 709 while attr in attr_names and \ 710 cy.getNetworkAttributeType(attr) != cy_type: 711 attr += "_" 712 except Fault: 713 # getNetworkAttributeType is not available in some older versions 714 # so we simply pass here 715 pass 716 cy.addNetworkAttributes(attr, cy_type, {network_id: value}) 717 718 # Send the node attributes 719 attr_names = set(cy.getNodeAttributeNames()) 720 for attr in graph.vertex_attributes(): 721 cy_type, values = self.infer_cytoscape_type(graph.vs[attr]) 722 values = dict(pair for pair in izip(node_ids, values) 723 if pair[1] is not None) 724 # Resolve type conflicts (if any) 725 while attr in attr_names and \ 726 cy.getNodeAttributeType(attr) != cy_type: 727 attr += "_" 728 # Send the attribute values 729 cy.addNodeAttributes(attr, cy_type, values, True) 730 731 # Send the edge attributes 732 attr_names = set(cy.getEdgeAttributeNames()) 733 for attr in graph.edge_attributes(): 734 cy_type, values = self.infer_cytoscape_type(graph.es[attr]) 735 values = dict(pair for pair in izip(edge_ids, values) 736 if pair[1] is not None) 737 # Resolve type conflicts (if any) 738 while attr in attr_names and \ 739 cy.getEdgeAttributeType(attr) != cy_type: 740 attr += "_" 741 # Send the attribute values 742 cy.addEdgeAttributes(attr, cy_type, values) 743
744 - def fetch(self, name = None, directed = False, keep_canonical_names = False):
745 """Fetches the network with the given name from Cytoscape. 746 747 When fetching networks from Cytoscape, the C{canonicalName} attributes 748 of vertices and edges are not converted by default. Use the 749 C{keep_canonical_names} parameter to retrieve these attributes as well. 750 751 @param name: the name of the network in Cytoscape. 752 @param directed: whether the network is directed. 753 @param keep_canonical_names: whether to keep the C{canonicalName} 754 vertex/edge attributes that are added automatically by Cytoscape 755 @return: an appropriately constructed igraph L{Graph}.""" 756 from igraph import Graph 757 758 cy = self.service 759 760 # Check the version number. Anything older than 1.3 is bad. 761 version = cy.version() 762 if " " in version: 763 version = version.split(" ")[0] 764 version = tuple(map(int, version.split(".")[:2])) 765 if version < (1, 3): 766 raise NotImplementedError("CytoscapeGraphDrawer requires " 767 "Cytoscape-RPC 1.3 or newer") 768 769 # Find out the ID of the network we are interested in 770 if name is None: 771 network_id = cy.getNetworkID() 772 else: 773 network_id = [k for k, v in cy.getNetworkList().iteritems() 774 if v == name] 775 if not network_id: 776 raise ValueError("no such network: %r" % name) 777 elif len(network_id) > 1: 778 raise ValueError("more than one network exists with name: %r" % name) 779 network_id = network_id[0] 780 781 # Fetch the list of all the nodes and edges 782 vertices = cy.getNodes(network_id) 783 edges = cy.getEdges(network_id) 784 n, m = len(vertices), len(edges) 785 786 # Fetch the graph attributes 787 graph_attrs = cy.getNetworkAttributes(network_id) 788 789 # Fetch the vertex attributes 790 vertex_attr_names = cy.getNodeAttributeNames() 791 vertex_attrs = {} 792 for attr_name in vertex_attr_names: 793 if attr_name == "canonicalName" and not keep_canonical_names: 794 continue 795 has_attr = cy.nodesHaveAttribute(attr_name, vertices) 796 filtered = [idx for idx, ok in enumerate(has_attr) if ok] 797 values = cy.getNodesAttributes(attr_name, 798 [name for name, ok in izip(vertices, has_attr) if ok] 799 ) 800 attrs = [None] * n 801 for idx, value in izip(filtered, values): 802 attrs[idx] = value 803 vertex_attrs[attr_name] = attrs 804 805 # Fetch the edge attributes 806 edge_attr_names = cy.getEdgeAttributeNames() 807 edge_attrs = {} 808 for attr_name in edge_attr_names: 809 if attr_name == "canonicalName" and not keep_canonical_names: 810 continue 811 has_attr = cy.edgesHaveAttribute(attr_name, edges) 812 filtered = [idx for idx, ok in enumerate(has_attr) if ok] 813 values = cy.getEdgesAttributes(attr_name, 814 [name for name, ok in izip(edges, has_attr) if ok] 815 ) 816 attrs = [None] * m 817 for idx, value in izip(filtered, values): 818 attrs[idx] = value 819 edge_attrs[attr_name] = attrs 820 821 # Create a vertex name index 822 vertex_name_index = dict((v, k) for k, v in enumerate(vertices)) 823 del vertices 824 825 # Remap the edges list to numeric IDs 826 edge_list = [] 827 for edge in edges: 828 parts = edge.split() 829 edge_list.append((vertex_name_index[parts[0]], vertex_name_index[parts[2]])) 830 del edges 831 832 return Graph(n, edge_list, directed=directed, 833 graph_attrs=graph_attrs, vertex_attrs=vertex_attrs, 834 edge_attrs=edge_attrs) 835 836 @staticmethod
837 - def infer_cytoscape_type(values):
838 """Returns a Cytoscape type that can be used to represent all the 839 values in `values` and an appropriately converted copy of `values` that 840 is suitable for an XML-RPC call. Note that the string type in 841 Cytoscape is used as a catch-all type; if no other type fits, attribute 842 values will be converted to string and then posted to Cytoscape. 843 844 ``None`` entries are allowed in `values`, they will be ignored on the 845 Cytoscape side. 846 """ 847 types = [type(value) for value in values if value is not None] 848 if all(t == bool for t in types): 849 return "BOOLEAN", values 850 if all(issubclass(t, (int, long)) for t in types): 851 return "INTEGER", values 852 if all(issubclass(t, float) for t in types): 853 return "FLOATING", values 854 return "STRING", [ 855 str(value) if not isinstance(value, basestring) else value 856 for value in values 857 ]
858
859 ##################################################################### 860 861 -class GephiGraphStreamingDrawer(AbstractGraphDrawer):
862 """Graph drawer that sends a graph to a file-like object (e.g., socket, URL 863 connection, file) using the Gephi graph streaming format. 864 865 The Gephi graph streaming format is a simple JSON-based format that can be used 866 to post mutations to a graph (i.e. node and edge additions, removals and updates) 867 to a remote component. For instance, one can open up Gephi (U{http://www.gephi.org}), 868 install the Gephi graph streaming plugin and then send a graph from igraph 869 straight into the Gephi window by using C{GephiGraphStreamingDrawer} with the 870 appropriate URL where Gephi is listening. 871 872 The C{connection} property exposes the L{GephiConnection} that the drawer 873 uses. The drawer also has a property called C{streamer} which exposes the underlying 874 L{GephiGraphStreamer} that is responsible for generating the JSON objects, 875 encoding them and writing them to a file-like object. If you want to customize 876 the encoding process, this is the object where you can tweak things to your taste. 877 """ 878
879 - def __init__(self, conn=None, *args, **kwds):
880 """Constructs a Gephi graph streaming drawer that will post graphs to the 881 given Gephi connection. If C{conn} is C{None}, the remaining arguments of 882 the constructor are forwarded intact to the constructor of 883 L{GephiConnection} in order to create a connection. This means that any of 884 the following are valid: 885 886 - C{GephiGraphStreamingDrawer()} will construct a drawer that connects to 887 workspace 0 of the local Gephi instance on port 8080. 888 889 - C{GephiGraphStreamingDrawer(workspace=2)} will connect to workspace 2 890 of the local Gephi instance on port 8080. 891 892 - C{GephiGraphStreamingDrawer(port=1234)} will connect to workspace 0 893 of the local Gephi instance on port 1234. 894 895 - C{GephiGraphStreamingDrawer(host="remote", port=1234, workspace=7)} 896 will connect to workspace 7 of the Gephi instance on host C{remote}, 897 port 1234. 898 899 - C{GephiGraphStreamingDrawer(url="http://remote:1234/workspace7)} is 900 the same as above, but with an explicit URL. 901 """ 902 super(GephiGraphStreamingDrawer, self).__init__() 903 904 from igraph.remote.gephi import GephiGraphStreamer, GephiConnection 905 self.connection = conn or GephiConnection(*args, **kwds) 906 self.streamer = GephiGraphStreamer() 907
908 - def draw(self, graph, *args, **kwds):
909 """Draws (i.e. sends) the given graph to the destination of the drawer using 910 the Gephi graph streaming API. 911 912 The following keyword arguments are allowed: 913 914 - ``encoder`` lets one specify an instance of ``json.JSONEncoder`` that 915 will be used to encode the JSON objects. 916 """ 917 self.streamer.post(graph, self.connection, encoder=kwds.get("encoder"))
918

   Home       Trees       Indices       Help