python-igraph manual

For using igraph from Python

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

Source Code for Module igraph.drawing.utils

  1  """ 
  2  Utility classes for drawing routines. 
  3  """ 
  4   
  5  from igraph.compat import property 
  6  from itertools import izip 
  7  from math import atan2, cos, sin 
  8  from operator import itemgetter 
  9   
 10  __all__ = ["BoundingBox", "FakeModule", "Point", "Rectangle"] 
 11  __license__ = "GPL" 
12 13 ##################################################################### 14 15 -class Rectangle(object):
16 """Class representing a rectangle.""" 17 18 __slots__ = ("_left", "_top", "_right", "_bottom") 19
20 - def __init__(self, *args):
21 """Creates a rectangle. 22 23 The corners of the rectangle can be specified by either a tuple 24 (four items, two for each corner, respectively), four separate numbers 25 (X and Y coordinates for each corner) or two separate numbers (width 26 and height, the upper left corner is assumed to be at (0,0))""" 27 coords = None 28 if len(args) == 1: 29 if isinstance(args[0], Rectangle): 30 coords = args[0].coords 31 elif len(args[0]) >= 4: 32 coords = tuple(args[0])[0:4] 33 elif len(args[0]) == 2: 34 coords = (0, 0, args[0][0], args[0][1]) 35 elif len(args) == 4: 36 coords = tuple(args) 37 elif len(args) == 2: 38 coords = (0, 0, args[0], args[1]) 39 if coords is None: 40 raise ValueError("invalid coordinate format") 41 42 try: 43 coords = tuple(float(coord) for coord in coords) 44 except ValueError: 45 raise ValueError("invalid coordinate format, numbers expected") 46 47 self.coords = coords 48 49 @property
50 - def coords(self):
51 """The coordinates of the corners. 52 53 The coordinates are returned as a 4-tuple in the following order: 54 left edge, top edge, right edge, bottom edge. 55 """ 56 return self._left, self._top, self._right, self._bottom 57 58 @coords.setter
59 - def coords(self, coords):
60 """Sets the coordinates of the corners. 61 62 @param coords: a 4-tuple with the coordinates of the corners 63 """ 64 self._left, self._top, self._right, self._bottom = coords 65 if self._left > self._right: 66 self._left, self._right = self._right, self._left 67 if self._top > self._bottom: 68 self._bottom, self._top = self._top, self._bottom 69 70 @property
71 - def width(self):
72 """The width of the rectangle""" 73 return self._right - self._left 74 75 @width.setter
76 - def width(self, value):
77 """Sets the width of the rectangle by adjusting the right edge.""" 78 self._right = self._left + value 79 80 @property
81 - def height(self):
82 """The height of the rectangle""" 83 return self._bottom - self._top 84 85 @height.setter
86 - def height(self, value):
87 """Sets the height of the rectangle by adjusting the bottom edge.""" 88 self._bottom = self._top + value 89 90 @property
91 - def left(self):
92 """The X coordinate of the left side of the box""" 93 return self._left 94 95 @left.setter
96 - def left(self, value):
97 """Sets the X coordinate of the left side of the box""" 98 self._left = float(value) 99 self._right = max(self._left, self._right) 100 101 @property
102 - def right(self):
103 """The X coordinate of the right side of the box""" 104 return self._right 105 106 @right.setter
107 - def right(self, value):
108 """Sets the X coordinate of the right side of the box""" 109 self._right = float(value) 110 self._left = min(self._left, self._right) 111 112 @property
113 - def top(self):
114 """The Y coordinate of the top edge of the box""" 115 return self._top 116 117 @top.setter
118 - def top(self, value):
119 """Sets the Y coordinate of the top edge of the box""" 120 self._top = value 121 self._bottom = max(self._bottom, self._top) 122 123 @property
124 - def bottom(self):
125 """The Y coordinate of the bottom edge of the box""" 126 return self._bottom 127 128 @bottom.setter
129 - def bottom(self, value):
130 """Sets the Y coordinate of the bottom edge of the box""" 131 self._bottom = value 132 self._top = min(self._bottom, self._top) 133 134 @property
135 - def midx(self):
136 """The X coordinate of the center of the box""" 137 return (self._left + self._right) / 2.0 138 139 @midx.setter
140 - def midx(self, value):
141 """Moves the center of the box to the given X coordinate""" 142 dx = value - (self._left + self._right) / 2.0 143 self._left += dx 144 self._right += dx 145 146 @property
147 - def midy(self):
148 """The Y coordinate of the center of the box""" 149 return (self._top + self._bottom) / 2.0 150 151 @midy.setter
152 - def midy(self, value):
153 """Moves the center of the box to the given Y coordinate""" 154 dy = value - (self._top + self._bottom) / 2.0 155 self._top += dy 156 self._bottom += dy 157 158 @property
159 - def shape(self):
160 """The shape of the rectangle (width, height)""" 161 return self._right - self._left, self._bottom - self._top 162 163 @shape.setter
164 - def shape(self, shape):
165 """Sets the shape of the rectangle (width, height).""" 166 self.width, self.height = shape 167
168 - def contract(self, margins):
169 """Contracts the rectangle by the given margins. 170 171 @return: a new L{Rectangle} object. 172 """ 173 if isinstance(margins, int) or isinstance(margins, float): 174 margins = [float(margins)] * 4 175 if len(margins) != 4: 176 raise ValueError("margins must be a 4-tuple or a single number") 177 nx1, ny1 = self._left+margins[0], self._top+margins[1] 178 nx2, ny2 = self._right-margins[2], self._bottom-margins[3] 179 if nx1 > nx2: 180 nx1 = (nx1+nx2)/2. 181 nx2 = nx1 182 if ny1 > ny2: 183 ny1 = (ny1+ny2)/2. 184 ny2 = ny1 185 return self.__class__(nx1, ny1, nx2, ny2) 186
187 - def expand(self, margins):
188 """Expands the rectangle by the given margins. 189 190 @return: a new L{Rectangle} object. 191 """ 192 if isinstance(margins, int) or isinstance(margins, float): 193 return self.contract(-float(margins)) 194 return self.contract([-float(margin) for margin in margins]) 195
196 - def isdisjoint(self, other):
197 """Returns ``True`` if the two rectangles have no intersection. 198 199 Example:: 200 201 >>> r1 = Rectangle(10, 10, 30, 30) 202 >>> r2 = Rectangle(20, 20, 50, 50) 203 >>> r3 = Rectangle(70, 70, 90, 90) 204 >>> r1.isdisjoint(r2) 205 False 206 >>> r2.isdisjoint(r1) 207 False 208 >>> r1.isdisjoint(r3) 209 True 210 >>> r3.isdisjoint(r1) 211 True 212 """ 213 return self._left > other._right or self._right < other._left \ 214 or self._top > other._bottom or self._bottom < other._top 215
216 - def isempty(self):
217 """Returns ``True`` if the rectangle is empty (i.e. it has zero 218 width and height). 219 220 Example:: 221 222 >>> r1 = Rectangle(10, 10, 30, 30) 223 >>> r2 = Rectangle(70, 70, 90, 90) 224 >>> r1.isempty() 225 False 226 >>> r2.isempty() 227 False 228 >>> r1.intersection(r2).isempty() 229 True 230 """ 231 return self._left == self._right and self._top == self._bottom 232
233 - def intersection(self, other):
234 """Returns the intersection of this rectangle with another. 235 236 Example:: 237 238 >>> r1 = Rectangle(10, 10, 30, 30) 239 >>> r2 = Rectangle(20, 20, 50, 50) 240 >>> r3 = Rectangle(70, 70, 90, 90) 241 >>> r1.intersection(r2) 242 Rectangle(20.0, 20.0, 30.0, 30.0) 243 >>> r2 & r1 244 Rectangle(20.0, 20.0, 30.0, 30.0) 245 >>> r2.intersection(r1) == r1.intersection(r2) 246 True 247 >>> r1.intersection(r3) 248 Rectangle(0.0, 0.0, 0.0, 0.0) 249 """ 250 if self.isdisjoint(other): 251 return Rectangle(0, 0, 0, 0) 252 return Rectangle(max(self._left, other._left), 253 max(self._top, other._top), 254 min(self._right, other._right), 255 min(self._bottom, other._bottom)) 256 __and__ = intersection 257
258 - def translate(self, dx, dy):
259 """Translates the rectangle in-place. 260 261 Example: 262 263 >>> r = Rectangle(10, 20, 50, 70) 264 >>> r.translate(30, -10) 265 >>> r 266 Rectangle(40.0, 10.0, 80.0, 60.0) 267 268 @param dx: the X coordinate of the translation vector 269 @param dy: the Y coordinate of the translation vector 270 """ 271 self._left += dx 272 self._right += dx 273 self._top += dy 274 self._bottom += dy 275
276 - def union(self, other):
277 """Returns the union of this rectangle with another. 278 279 The resulting rectangle is the smallest rectangle that contains both 280 rectangles. 281 282 Example:: 283 284 >>> r1 = Rectangle(10, 10, 30, 30) 285 >>> r2 = Rectangle(20, 20, 50, 50) 286 >>> r3 = Rectangle(70, 70, 90, 90) 287 >>> r1.union(r2) 288 Rectangle(10.0, 10.0, 50.0, 50.0) 289 >>> r2 | r1 290 Rectangle(10.0, 10.0, 50.0, 50.0) 291 >>> r2.union(r1) == r1.union(r2) 292 True 293 >>> r1.union(r3) 294 Rectangle(10.0, 10.0, 90.0, 90.0) 295 """ 296 return Rectangle(min(self._left, other._left), 297 min(self._top, other._top), 298 max(self._right, other._right), 299 max(self._bottom, other._bottom)) 300 __or__ = union 301
302 - def __ior__(self, other):
303 """Expands this rectangle to include itself and another completely while 304 still being as small as possible. 305 306 Example:: 307 308 >>> r1 = Rectangle(10, 10, 30, 30) 309 >>> r2 = Rectangle(20, 20, 50, 50) 310 >>> r3 = Rectangle(70, 70, 90, 90) 311 >>> r1 |= r2 312 >>> r1 313 Rectangle(10.0, 10.0, 50.0, 50.0) 314 >>> r1 |= r3 315 >>> r1 316 Rectangle(10.0, 10.0, 90.0, 90.0) 317 """ 318 self._left = min(self._left, other._left) 319 self._top = min(self._top, other._top) 320 self._right = max(self._right, other._right) 321 self._bottom = max(self._bottom, other._bottom) 322 return self 323
324 - def __repr__(self):
325 return "%s(%s, %s, %s, %s)" % (self.__class__.__name__, \ 326 self._left, self._top, self._right, self._bottom) 327
328 - def __eq__(self, other):
329 return self.coords == other.coords 330
331 - def __ne__(self, other):
332 return self.coords != other.coords 333
334 - def __bool__(self):
335 return self._left != self._right or self._top != self._bottom 336
337 - def __nonzero__(self):
338 return self._left != self._right or self._top != self._bottom 339
340 - def __hash__(self):
341 return hash(self.coords)
342
343 ##################################################################### 344 345 -class BoundingBox(Rectangle):
346 """Class representing a bounding box (a rectangular area) that 347 encloses some objects.""" 348
349 - def __ior__(self, other):
350 """Replaces this bounding box with the union of itself and 351 another. 352 353 Example:: 354 355 >>> box1 = BoundingBox(10, 20, 50, 60) 356 >>> box2 = BoundingBox(70, 40, 100, 90) 357 >>> box1 |= box2 358 >>> print(box1) 359 BoundingBox(10.0, 20.0, 100.0, 90.0) 360 """ 361 self._left = min(self._left, other._left) 362 self._top = min(self._top, other._top) 363 self._right = max(self._right, other._right) 364 self._bottom = max(self._bottom, other._bottom) 365 return self 366
367 - def __or__(self, other):
368 """Takes the union of this bounding box with another. 369 370 The result is a bounding box which encloses both bounding 371 boxes. 372 373 Example:: 374 375 >>> box1 = BoundingBox(10, 20, 50, 60) 376 >>> box2 = BoundingBox(70, 40, 100, 90) 377 >>> box1 | box2 378 BoundingBox(10.0, 20.0, 100.0, 90.0) 379 """ 380 return self.__class__( 381 min(self._left, other._left), 382 min(self._top, other._top), 383 max(self._right, other._right), 384 max(self._bottom, other._bottom) 385 )
386
387 388 ##################################################################### 389 390 # pylint: disable-msg=R0903 391 # R0903: too few public methods 392 -class FakeModule(object):
393 """Fake module that raises an exception for everything""" 394
395 - def __getattr__(self, _):
396 raise TypeError("plotting not available")
397 - def __call__(self, _):
398 raise TypeError("plotting not available")
399 - def __setattr__(self, key, value):
400 raise TypeError("plotting not available")
401
402 ##################################################################### 403 404 -def find_cairo():
405 """Tries to import the ``cairo`` Python module if it is installed, 406 also trying ``cairocffi`` (a drop-in replacement of ``cairo``). 407 Returns a fake module if everything fails. 408 """ 409 module_names = ["cairo", "cairocffi"] 410 module = FakeModule() 411 for module_name in module_names: 412 try: 413 module = __import__(module_name) 414 break 415 except ImportError: 416 pass 417 return module 418
419 ##################################################################### 420 421 -class Point(tuple):
422 """Class representing a point on the 2D plane.""" 423 __slots__ = () 424 _fields = ('x', 'y') 425
426 - def __new__(cls, x, y):
427 """Creates a new point with the given coordinates""" 428 return tuple.__new__(cls, (x, y)) 429 430 # pylint: disable-msg=W0622 431 # W0622: redefining built-in 'len' 432 @classmethod
433 - def _make(cls, iterable, new = tuple.__new__, len = len):
434 """Creates a new point from a sequence or iterable""" 435 result = new(cls, iterable) 436 if len(result) != 2: 437 raise TypeError('Expected 2 arguments, got %d' % len(result)) 438 return result 439
440 - def __repr__(self):
441 """Returns a nicely formatted representation of the point""" 442 return 'Point(x=%r, y=%r)' % self 443
444 - def _asdict(self):
445 """Returns a new dict which maps field names to their values""" 446 return dict(zip(self._fields, self)) 447 448 # pylint: disable-msg=W0141 449 # W0141: used builtin function 'map'
450 - def _replace(self, **kwds):
451 """Returns a new point object replacing specified fields with new 452 values""" 453 result = self._make(map(kwds.pop, ('x', 'y'), self)) 454 if kwds: 455 raise ValueError('Got unexpected field names: %r' % kwds.keys()) 456 return result 457
458 - def __getnewargs__(self):
459 """Return self as a plain tuple. Used by copy and pickle.""" 460 return tuple(self) 461 462 x = property(itemgetter(0), doc="Alias for field number 0") 463 y = property(itemgetter(1), doc="Alias for field number 1") 464
465 - def __add__(self, other):
466 """Adds the coordinates of a point to another one""" 467 return self.__class__(x = self.x + other.x, y = self.y + other.y) 468
469 - def __sub__(self, other):
470 """Subtracts the coordinates of a point to another one""" 471 return self.__class__(x = self.x - other.x, y = self.y - other.y) 472
473 - def __mul__(self, scalar):
474 """Multiplies the coordinates by a scalar""" 475 return self.__class__(x = self.x * scalar, y = self.y * scalar) 476 __rmul__ = __mul__ 477
478 - def __div__(self, scalar):
479 """Divides the coordinates by a scalar""" 480 return self.__class__(x = self.x / scalar, y = self.y / scalar) 481
482 - def as_polar(self):
483 """Returns the polar coordinate representation of the point. 484 485 @return: the radius and the angle in a tuple. 486 """ 487 return len(self), atan2(self.y, self.x) 488
489 - def distance(self, other):
490 """Returns the distance of the point from another one. 491 492 Example: 493 494 >>> p1 = Point(5, 7) 495 >>> p2 = Point(8, 3) 496 >>> p1.distance(p2) 497 5.0 498 """ 499 dx, dy = self.x - other.x, self.y - other.y 500 return (dx * dx + dy * dy) ** 0.5 501
502 - def interpolate(self, other, ratio = 0.5):
503 """Linearly interpolates between the coordinates of this point and 504 another one. 505 506 @param other: the other point 507 @param ratio: the interpolation ratio between 0 and 1. Zero will 508 return this point, 1 will return the other point. 509 """ 510 ratio = float(ratio) 511 return Point(x = self.x * (1.0 - ratio) + other.x * ratio, \ 512 y = self.y * (1.0 - ratio) + other.y * ratio) 513
514 - def length(self):
515 """Returns the length of the vector pointing from the origin to this 516 point.""" 517 return (self.x ** 2 + self.y ** 2) ** 0.5 518
519 - def normalized(self):
520 """Normalizes the coordinates of the point s.t. its length will be 1 521 after normalization. Returns the normalized point.""" 522 len = self.length() 523 if len == 0: 524 return Point(x = self.x, y = self.y) 525 return Point(x = self.x / len, y = self.y / len) 526
527 - def sq_length(self):
528 """Returns the squared length of the vector pointing from the origin 529 to this point.""" 530 return (self.x ** 2 + self.y ** 2) 531
532 - def towards(self, other, distance = 0):
533 """Returns the point that is at a given distance from this point 534 towards another one.""" 535 if not distance: 536 return self 537 538 angle = atan2(other.y - self.y, other.x - self.x) 539 return Point(self.x + distance * cos(angle), 540 self.y + distance * sin(angle)) 541 542 @classmethod
543 - def FromPolar(cls, radius, angle):
544 """Constructs a point from polar coordinates. 545 546 `radius` is the distance of the point from the origin; `angle` is the 547 angle between the X axis and the vector pointing to the point from 548 the origin. 549 """ 550 return cls(radius * cos(angle), radius * sin(angle))
551

   Home       Trees       Indices       Help