python-igraph manual

For using igraph from Python

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

Source Code for Module igraph.drawing.shapes

  1  # vim:ts=4:sw=4:sts=4:et 
  2  # -*- coding: utf-8 -*- 
  3  """ 
  4  Shape drawing classes for igraph 
  5   
  6  Vertex shapes in igraph are usually referred to by short names like 
  7  C{"rect"} or C{"circle"}. This module contains the classes that 
  8  implement the actual drawing routines for these shapes, and a 
  9  resolver class that determines the appropriate shape drawer class 
 10  given the short name. 
 11   
 12  Classes that are derived from L{ShapeDrawer} in this module are 
 13  automatically registered by L{ShapeDrawerDirectory}. If you 
 14  implement a custom shape drawer, you must register it in 
 15  L{ShapeDrawerDirectory} manually if you wish to refer to it by a 
 16  name in the C{shape} attribute of vertices. 
 17  """ 
 18   
 19  from __future__ import division 
 20   
 21  __all__ = ["ShapeDrawerDirectory"] 
 22   
 23  __license__ = u"""\ 
 24  Copyright (C) 2006-2012  Tamás Nepusz <ntamas@gmail.com> 
 25  Pázmány Péter sétány 1/a, 1117 Budapest, Hungary 
 26   
 27  This program is free software; you can redistribute it and/or modify 
 28  it under the terms of the GNU General Public License as published by 
 29  the Free Software Foundation; either version 2 of the License, or 
 30  (at your option) any later version. 
 31   
 32  This program is distributed in the hope that it will be useful, 
 33  but WITHOUT ANY WARRANTY; without even the implied warranty of 
 34  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 35  GNU General Public License for more details. 
 36   
 37  You should have received a copy of the GNU General Public License 
 38  along with this program; if not, write to the Free Software 
 39  Foundation, Inc.,  51 Franklin Street, Fifth Floor, Boston, MA  
 40  02110-1301 USA 
 41  """ 
 42   
 43  from math import atan2, copysign, cos, pi, sin 
 44  import sys 
 45   
 46  from igraph.drawing.baseclasses import AbstractCairoDrawer 
 47  from igraph.drawing.utils import Point 
 48  from igraph.utils import consecutive_pairs 
49 50 -class ShapeDrawer(object):
51 """Static class, the ancestor of all vertex shape drawer classes. 52 53 Custom shapes must implement at least the C{draw_path} method of the class. 54 The method I{must not} stroke or fill, it should just set up the current 55 Cairo path appropriately.""" 56 57 @staticmethod
58 - def draw_path(ctx, center_x, center_y, width, height=None):
59 """Draws the path of the shape on the given Cairo context, without 60 stroking or filling it. 61 62 This method must be overridden in derived classes implementing custom shapes 63 and declared as a static method using C{staticmethod(...)}. 64 65 @param ctx: the context to draw on 66 @param center_x: the X coordinate of the center of the object 67 @param center_y: the Y coordinate of the center of the object 68 @param width: the width of the object 69 @param height: the height of the object. If C{None}, equals to the width. 70 """ 71 raise NotImplementedError("abstract class") 72 73 # pylint: disable-msg=W0613 74 @staticmethod
75 - def intersection_point(center_x, center_y, source_x, source_y, \ 76 width, height=None):
77 """Determines where the shape centered at (center_x, center_y) 78 intersects with a line drawn from (source_x, source_y) to 79 (center_x, center_y). 80 81 Can be overridden in derived classes. Must always be defined as a static 82 method using C{staticmethod(...)} 83 84 @param width: the width of the shape 85 @param height: the height of the shape. If C{None}, defaults to the width 86 @return: the intersection point (the closest to (source_x, source_y) if 87 there are more than one) or (center_x, center_y) if there is no 88 intersection 89 """ 90 return center_x, center_y
91
92 93 -class NullDrawer(ShapeDrawer):
94 """Static drawer class which draws nothing. 95 96 This class is used for graph vertices with unknown shapes""" 97 names = ["null", "none", "empty", "hidden", ""] 98 99 @staticmethod
100 - def draw_path(ctx, center_x, center_y, width, height=None):
101 """Draws nothing.""" 102 pass
103
104 105 -class RectangleDrawer(ShapeDrawer):
106 """Static class which draws rectangular vertices""" 107 names = "rectangle rect rectangular square box" 108 109 @staticmethod
110 - def draw_path(ctx, center_x, center_y, width, height=None):
111 """Draws a rectangle-shaped path on the Cairo context without stroking 112 or filling it. 113 @see: ShapeDrawer.draw_path""" 114 height = height or width 115 ctx.rectangle(center_x - width/2, center_y - height/2, 116 width, height) 117 118 # pylint: disable-msg=C0103, R0911 119 # R0911: too many return statements 120 @staticmethod
121 - def intersection_point(center_x, center_y, source_x, source_y, \ 122 width, height=None):
123 """Determines where the rectangle centered at (center_x, center_y) 124 having the given width and height intersects with a line drawn from 125 (source_x, source_y) to (center_x, center_y). 126 127 @see: ShapeDrawer.intersection_point""" 128 height = height or width 129 delta_x, delta_y = center_x-source_x, center_y-source_y 130 131 if delta_x == 0 and delta_y == 0: 132 return center_x, center_y 133 134 if delta_y > 0 and delta_x <= delta_y and delta_x >= -delta_y: 135 # this is the top edge 136 ry = center_y - height/2 137 ratio = (height/2) / delta_y 138 return center_x-ratio*delta_x, ry 139 140 if delta_y < 0 and delta_x <= -delta_y and delta_x >= delta_y: 141 # this is the bottom edge 142 ry = center_y + height/2 143 ratio = (height/2) / -delta_y 144 return center_x-ratio*delta_x, ry 145 146 if delta_x > 0 and delta_y <= delta_x and delta_y >= -delta_x: 147 # this is the left edge 148 rx = center_x - width/2 149 ratio = (width/2) / delta_x 150 return rx, center_y-ratio*delta_y 151 152 if delta_x < 0 and delta_y <= -delta_x and delta_y >= delta_x: 153 # this is the right edge 154 rx = center_x + width/2 155 ratio = (width/2) / -delta_x 156 return rx, center_y-ratio*delta_y 157 158 if delta_x == 0: 159 if delta_y > 0: 160 return center_x, center_y - height/2 161 return center_x, center_y + height/2 162 163 if delta_y == 0: 164 if delta_x > 0: 165 return center_x - width/2, center_y 166 return center_x + width/2, center_y
167
168 169 -class CircleDrawer(ShapeDrawer):
170 """Static class which draws circular vertices""" 171 names = "circle circular" 172 173 @staticmethod
174 - def draw_path(ctx, center_x, center_y, width, height=None):
175 """Draws a circular path on the Cairo context without stroking or 176 filling it. 177 178 Height is ignored, it is the width that determines the diameter of the circle. 179 180 @see: ShapeDrawer.draw_path""" 181 ctx.arc(center_x, center_y, width/2, 0, 2*pi) 182 183 @staticmethod
184 - def intersection_point(center_x, center_y, source_x, source_y, \ 185 width, height=None):
186 """Determines where the circle centered at (center_x, center_y) 187 intersects with a line drawn from (source_x, source_y) to 188 (center_x, center_y). 189 190 @see: ShapeDrawer.intersection_point""" 191 height = height or width 192 angle = atan2(center_y-source_y, center_x-source_x) 193 return center_x-width/2 * cos(angle), \ 194 center_y-height/2* sin(angle)
195
196 197 -class UpTriangleDrawer(ShapeDrawer):
198 """Static class which draws upright triangles""" 199 names = "triangle triangle-up up-triangle arrow arrow-up up-arrow" 200 201 @staticmethod
202 - def draw_path(ctx, center_x, center_y, width, height=None):
203 """Draws an upright triangle on the Cairo context without stroking or 204 filling it. 205 206 @see: ShapeDrawer.draw_path""" 207 height = height or width 208 ctx.move_to(center_x-width/2, center_y+height/2) 209 ctx.line_to(center_x, center_y-height/2) 210 ctx.line_to(center_x+width/2, center_y+height/2) 211 ctx.close_path() 212 213 @staticmethod
214 - def intersection_point(center_x, center_y, source_x, source_y, \ 215 width, height=None):
216 """Determines where the triangle centered at (center_x, center_y) 217 intersects with a line drawn from (source_x, source_y) to 218 (center_x, center_y). 219 220 @see: ShapeDrawer.intersection_point""" 221 # TODO: finish it properly 222 height = height or width 223 return center_x, center_y
224
225 -class DownTriangleDrawer(ShapeDrawer):
226 """Static class which draws triangles pointing down""" 227 names = "down-triangle triangle-down arrow-down down-arrow" 228 229 @staticmethod
230 - def draw_path(ctx, center_x, center_y, width, height=None):
231 """Draws a triangle on the Cairo context without stroking or 232 filling it. 233 234 @see: ShapeDrawer.draw_path""" 235 height = height or width 236 ctx.move_to(center_x-width/2, center_y-height/2) 237 ctx.line_to(center_x, center_y+height/2) 238 ctx.line_to(center_x+width/2, center_y-height/2) 239 ctx.close_path() 240 241 @staticmethod
242 - def intersection_point(center_x, center_y, source_x, source_y, \ 243 width, height=None):
244 """Determines where the triangle centered at (center_x, center_y) 245 intersects with a line drawn from (source_x, source_y) to 246 (center_x, center_y). 247 248 @see: ShapeDrawer.intersection_point""" 249 # TODO: finish it properly 250 height = height or width 251 return center_x, center_y
252
253 -class DiamondDrawer(ShapeDrawer):
254 """Static class which draws diamonds (i.e. rhombuses)""" 255 names = "diamond rhombus" 256 257 @staticmethod
258 - def draw_path(ctx, center_x, center_y, width, height=None):
259 """Draws a rhombus on the Cairo context without stroking or 260 filling it. 261 262 @see: ShapeDrawer.draw_path""" 263 height = height or width 264 ctx.move_to(center_x-width/2, center_y) 265 ctx.line_to(center_x, center_y+height/2) 266 ctx.line_to(center_x+width/2, center_y) 267 ctx.line_to(center_x, center_y-height/2) 268 ctx.close_path() 269 270 @staticmethod
271 - def intersection_point(center_x, center_y, source_x, source_y, \ 272 width, height=None):
273 """Determines where the rhombus centered at (center_x, center_y) 274 intersects with a line drawn from (source_x, source_y) to 275 (center_x, center_y). 276 277 @see: ShapeDrawer.intersection_point""" 278 height = height or width 279 280 if height == 0 and width == 0: 281 return center_x, center_y 282 283 delta_x, delta_y = source_x - center_x, source_y - center_y 284 285 # Treat edge case when delta_x = 0 286 if delta_x == 0: 287 if delta_y == 0: 288 return center_x, center_y 289 else: 290 return center_x, center_y + copysign(height / 2, delta_y) 291 292 width = copysign(width, delta_x) 293 height = copysign(height, delta_y) 294 295 f = height / (height + width * delta_y / delta_x) 296 return center_x + f * width / 2, center_y + (1-f) * height / 2
297
298 ##################################################################### 299 300 -class PolygonDrawer(AbstractCairoDrawer):
301 """Class that is used to draw polygons. 302 303 The corner points of the polygon can be set by the C{points} 304 property of the drawer, or passed at construction time. Most 305 drawing methods in this class also have an extra C{points} 306 argument that can be used to override the set of points in the 307 C{points} property.""" 308
309 - def __init__(self, context, bbox=(1, 1), points = []):
310 """Constructs a new polygon drawer that draws on the given 311 Cairo context. 312 313 @param context: the Cairo context to draw on 314 @param bbox: ignored, leave it at its default value 315 @param points: the list of corner points 316 """ 317 super(PolygonDrawer, self).__init__(context, bbox) 318 self.points = points 319
320 - def draw_path(self, points=None, corner_radius=0):
321 """Sets up a Cairo path for the outline of a polygon on the given 322 Cairo context. 323 324 @param points: the coordinates of the corners of the polygon, 325 in clockwise or counter-clockwise order, or C{None} if we are 326 about to use the C{points} property of the class. 327 @param corner_radius: if zero, an ordinary polygon will be drawn. 328 If positive, the corners of the polygon will be rounded with 329 the given radius. 330 """ 331 if points is None: 332 points = self.points 333 334 self.context.new_path() 335 336 if len(points) < 2: 337 # Well, a polygon must have at least two corner points 338 return 339 340 ctx = self.context 341 if corner_radius <= 0: 342 # No rounded corners, this is simple 343 ctx.move_to(*points[-1]) 344 for point in points: 345 ctx.line_to(*point) 346 return 347 348 # Rounded corners. First, we will take each side of the 349 # polygon and find what the corner radius should be on 350 # each corner. If the side is longer than 2r (where r is 351 # equal to corner_radius), the radius allowed by that side 352 # is r; if the side is shorter, the radius is the length 353 # of the side / 2. For each corner, the final corner radius 354 # is the smaller of the radii on the two sides adjacent to 355 # the corner. 356 points = [Point(*point) for point in points] 357 side_vecs = [v-u for u, v in consecutive_pairs(points, circular=True)] 358 half_side_lengths = [side.length() / 2 for side in side_vecs] 359 corner_radii = [corner_radius] * len(points) 360 for idx in xrange(len(corner_radii)): 361 prev_idx = -1 if idx == 0 else idx - 1 362 radii = [corner_radius, half_side_lengths[prev_idx], 363 half_side_lengths[idx]] 364 corner_radii[idx] = min(radii) 365 366 # Okay, move to the last corner, adjusted by corner_radii[-1] 367 # towards the first corner 368 ctx.move_to(*(points[-1].towards(points[0], corner_radii[-1]))) 369 # Now, for each point in points, draw a line towards the 370 # corner, stopping before it in a distance of corner_radii[idx], 371 # then draw the corner 372 u = points[-1] 373 for idx, (v, w) in enumerate(consecutive_pairs(points, True)): 374 radius = corner_radii[idx] 375 ctx.line_to(*v.towards(u, radius)) 376 aux1 = v.towards(u, radius / 2) 377 aux2 = v.towards(w, radius / 2) 378 ctx.curve_to(aux1.x, aux1.y, aux2.x, aux2.y, 379 *v.towards(w, corner_radii[idx])) 380 u = v 381
382 - def draw(self, points=None):
383 """Draws the polygon using the current stroke of the Cairo context. 384 385 @param points: the coordinates of the corners of the polygon, 386 in clockwise or counter-clockwise order, or C{None} if we are 387 about to use the C{points} property of the class. 388 """ 389 self.draw_path(points) 390 self.context.stroke()
391
392 ##################################################################### 393 394 -class ShapeDrawerDirectory(object):
395 """Static class that resolves shape names to their corresponding 396 shape drawer classes. 397 398 Classes that are derived from L{ShapeDrawer} in this module are 399 automatically registered by L{ShapeDrawerDirectory} when the module 400 is loaded for the first time. 401 """ 402 403 known_shapes = {} 404 405 @classmethod
406 - def register(cls, drawer_class):
407 """Registers the given shape drawer class under the given names. 408 409 @param drawer_class: the shape drawer class to be registered 410 """ 411 names = drawer_class.names 412 if isinstance(names, (str, unicode)): 413 names = names.split() 414 415 for name in names: 416 cls.known_shapes[name] = drawer_class 417 418 @classmethod
419 - def register_namespace(cls, namespace):
420 """Registers all L{ShapeDrawer} classes in the given namespace 421 422 @param namespace: a Python dict mapping names to Python objects.""" 423 for name, value in namespace.iteritems(): 424 if name.startswith("__"): 425 continue 426 if isinstance(value, type): 427 if issubclass(value, ShapeDrawer) and value != ShapeDrawer: 428 cls.register(value) 429 430 @classmethod
431 - def resolve(cls, shape):
432 """Given a shape name, returns the corresponding shape drawer class 433 434 @param shape: the name of the shape 435 @return: the corresponding shape drawer class 436 437 @raise ValueError: if the shape is unknown 438 """ 439 try: 440 return cls.known_shapes[shape] 441 except KeyError: 442 raise ValueError("unknown shape: %s" % shape) 443 444 @classmethod
445 - def resolve_default(cls, shape, default=NullDrawer):
446 """Given a shape name, returns the corresponding shape drawer class 447 or the given default shape drawer if the shape name is unknown. 448 449 @param shape: the name of the shape 450 @param default: the default shape drawer to return when the shape 451 is unknown 452 @return: the shape drawer class corresponding to the given name or 453 the default shape drawer class if the name is unknown 454 """ 455 return cls.known_shapes.get(shape, default)
456 457 ShapeDrawerDirectory.register_namespace(sys.modules[__name__].__dict__) 458

   Home       Trees       Indices       Help