python-igraph manual

For using igraph from Python

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

Source Code for Module igraph.layout

  1  # vim:ts=4:sw=4:sts=4:et 
  2  # -*- coding: utf-8 -*- 
  3  """ 
  4  Layout-related code in the IGraph library. 
  5   
  6  This package contains the implementation of the L{Layout} object. 
  7  """ 
  8   
  9  from itertools import izip 
 10  from math import sin, cos, pi 
 11   
 12  from igraph.drawing.utils import BoundingBox 
 13  from igraph.statistics import RunningMean 
 14   
 15  __license__ = u"""\ 
 16  Copyright (C) 2006-2012  Tamás Nepusz <ntamas@gmail.com> 
 17  Pázmány Péter sétány 1/a, 1117 Budapest, Hungary 
 18   
 19  This program is free software; you can redistribute it and/or modify 
 20  it under the terms of the GNU General Public License as published by 
 21  the Free Software Foundation; either version 2 of the License, or 
 22  (at your option) any later version. 
 23   
 24  This program is distributed in the hope that it will be useful, 
 25  but WITHOUT ANY WARRANTY; without even the implied warranty of 
 26  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 27  GNU General Public License for more details. 
 28   
 29  You should have received a copy of the GNU General Public License 
 30  along with this program; if not, write to the Free Software 
 31  Foundation, Inc.,  51 Franklin Street, Fifth Floor, Boston, MA  
 32  02110-1301 USA 
 33  """ 
34 35 -class Layout(object):
36 """Represents the layout of a graph. 37 38 A layout is practically a list of coordinates in an n-dimensional 39 space. This class is generic in the sense that it can store coordinates 40 in any n-dimensional space. 41 42 Layout objects are not associated directly with a graph. This is deliberate: 43 there were times when I worked with almost identical copies of the same 44 graph, the only difference was that they had different colors assigned to 45 the vertices. It was particularly convenient for me to use the same layout 46 for all of them, especially when I made figures for a paper. However, 47 C{igraph} will of course refuse to draw a graph with a layout that has 48 less coordinates than the node count of the graph. 49 50 Layouts behave exactly like lists when they are accessed using the item 51 index operator (C{[...]}). They can even be iterated through. Items 52 returned by the index operator are only copies of the coordinates, 53 but the stored coordinates can be modified by directly assigning to 54 an index. 55 56 >>> layout = Layout([(0, 1), (0, 2)]) 57 >>> coords = layout[1] 58 >>> print coords 59 [0, 2] 60 >>> coords = (0, 3) 61 >>> print layout[1] 62 [0, 2] 63 >>> layout[1] = coords 64 >>> print layout[1] 65 [0, 3] 66 """ 67
68 - def __init__(self, coords=None, dim=None):
69 """Constructor. 70 71 @param coords: the coordinates to be stored in the layout. 72 @param dim: the number of dimensions. If C{None}, the number of 73 dimensions is determined automatically from the length of the first 74 item of the coordinate list. If there are no entries in the coordinate 75 list, the default will be 2. Generally, this should be given if the 76 length of the coordinate list is zero, otherwise it should be left as 77 is. 78 """ 79 if coords: 80 self._coords = [list(coord) for coord in coords] 81 else: 82 self._coords = [] 83 84 if dim is None: 85 if len(self._coords) == 0: 86 self._dim = 2 87 else: 88 self._dim = len(self._coords[0]) 89 else: 90 self._dim = int(dim) 91 for row in self._coords: 92 if len(row) != self._dim: 93 raise ValueError("all items in the coordinate list "+ 94 "must have a length of %d" % self._dim) 95
96 - def __len__(self):
97 return len(self._coords) 98
99 - def __getitem__(self, idx):
100 return self._coords[idx] 101
102 - def __setitem__(self, idx, value):
103 if len(value) != self._dim: 104 raise ValueError("assigned item must have %d elements" % self._dim) 105 self._coords[idx] = list(value) 106
107 - def __delitem__(self, idx):
108 del self._coords[idx] 109
110 - def __copy__(self):
111 return self.__class__(self.coords, self.dim) 112
113 - def __repr__(self):
114 if not self.coords: 115 vertex_count = "no vertices" 116 elif len(self.coords) == 1: 117 vertex_count = "1 vertex" 118 else: 119 vertex_count = "%d vertices" % len(self.coords) 120 if self.dim == 1: 121 dim_count = "1 dimension" 122 else: 123 dim_count = "%d dimensions" % self.dim 124 return "<%s with %s and %s>" % (self.__class__.__name__, 125 vertex_count, dim_count) 126 127 @property
128 - def dim(self):
129 """Returns the number of dimensions""" 130 return self._dim 131 132 @property
133 - def coords(self):
134 """The coordinates as a list of lists""" 135 return [row[:] for row in self._coords] 136
137 - def append(self, value):
138 """Appends a new point to the layout""" 139 if len(value) < self._dim: 140 raise ValueError("appended item must have %d elements" % self._dim) 141 self._coords.append([float(coord) for coord in value[0:self._dim]]) 142
143 - def mirror(self, dim):
144 """Mirrors the layout along the given dimension(s) 145 146 @param dim: the list of dimensions or a single dimension 147 """ 148 if isinstance(dim, int): 149 dim = [dim] 150 else: 151 dim = [int(x) for x in dim] 152 153 for current_dim in dim: 154 for row in self._coords: 155 row[current_dim] *= -1 156 157
158 - def rotate(self, angle, dim1=0, dim2=1, **kwds):
159 """Rotates the layout by the given degrees on the plane defined by 160 the given two dimensions. 161 162 @param angle: the angle of the rotation, specified in degrees. 163 @param dim1: the first axis of the plane of the rotation. 164 @param dim2: the second axis of the plane of the rotation. 165 @keyword origin: the origin of the rotation. If not specified, the 166 origin will be the origin of the coordinate system. 167 """ 168 169 origin = list(kwds.get("origin", [0.]*self._dim)) 170 if len(origin) != self._dim: 171 raise ValueError("origin must have %d dimensions" % self._dim) 172 173 radian = angle * pi / 180. 174 cos_alpha, sin_alpha = cos(radian), sin(radian) 175 176 for idx, row in enumerate(self._coords): 177 x, y = row[dim1] - origin[dim1], row[dim2] - origin[dim2] 178 row[dim1] = cos_alpha*x - sin_alpha*y + origin[dim1] 179 row[dim2] = sin_alpha*x + cos_alpha*y + origin[dim2] 180 181
182 - def scale(self, *args, **kwds):
183 """Scales the layout. 184 185 Scaling parameters can be provided either through the C{scale} keyword 186 argument or through plain unnamed arguments. If a single integer or 187 float is given, it is interpreted as a uniform multiplier to be applied 188 on all dimensions. If it is a list or tuple, its length must be equal to 189 the number of dimensions in the layout, and each element must be an 190 integer or float describing the scaling coefficient in one of the 191 dimensions. 192 193 @keyword scale: scaling coefficients (integer, float, list or tuple) 194 @keyword origin: the origin of scaling (this point will stay in place). 195 Optional, defaults to the origin of the coordinate system being used. 196 """ 197 origin = list(kwds.get("origin", [0.]*self._dim)) 198 if len(origin) != self._dim: 199 raise ValueError("origin must have %d dimensions" % self._dim) 200 201 scaling = kwds.get("scale") or args 202 if isinstance(scaling, (int, float)): 203 scaling = [scaling] 204 if len(scaling) == 0: 205 raise ValueError("scaling factor must be given") 206 elif len(scaling) == 1: 207 if type(scaling[0]) == int or type(scaling[0]) == float: 208 scaling = scaling*self._dim 209 else: 210 scaling = scaling[0] 211 if len(scaling) != self._dim: 212 raise ValueError("scaling factor list must have %d elements" \ 213 % self._dim) 214 215 for idx, row in enumerate(self._coords): 216 self._coords[idx] = [(row[d]-origin[d])*scaling[d]+origin[d] \ 217 for d in xrange(self._dim)] 218
219 - def translate(self, *args, **kwds):
220 """Translates the layout. 221 222 The translation vector can be provided either through the C{v} keyword 223 argument or through plain unnamed arguments. If unnamed arguments are 224 used, the vector can be supplied as a single list (or tuple) or just as 225 a series of arguments. In all cases, the translation vector must have 226 the same number of dimensions as the layout. 227 228 @keyword v: the translation vector 229 """ 230 v = kwds.get("v") or args 231 if len(v) == 0: 232 raise ValueError("translation vector must be given") 233 elif len(v) == 1 and type(v[0]) != int and type(v[0]) != float: 234 v = v[0] 235 if len(v) != self._dim: 236 raise ValueError("translation vector must have %d dimensions" \ 237 % self._dim) 238 239 for idx, row in enumerate(self._coords): 240 self._coords[idx] = [row[d]+v[d] for d in xrange(self._dim)] 241 242
243 - def to_radial(self, min_angle = 100, max_angle = 80, \ 244 min_radius=0.0, max_radius=1.0):
245 """Converts a planar layout to a radial one 246 247 This method applies only to 2D layouts. The X coordinate of the 248 layout is transformed to an angle, with min(x) corresponding to 249 the parameter called I{min_angle} and max(y) corresponding to 250 I{max_angle}. Angles are given in degrees, zero degree corresponds 251 to the direction pointing upwards. The Y coordinate is 252 interpreted as a radius, with min(y) belonging to the minimum and 253 max(y) to the maximum radius given in the arguments. 254 255 This is not a fully generic polar coordinate transformation, but 256 it is fairly useful in creating radial tree layouts from ordinary 257 top-down ones (that's why the Y coordinate belongs to the radius). 258 It can also be used in conjunction with the Fruchterman-Reingold 259 layout algorithm via its I{miny} and I{maxy} parameters (see 260 L{Graph.layout_fruchterman_reingold}) to produce radial layouts 261 where the radius belongs to some property of the vertices. 262 263 @param min_angle: the angle corresponding to the minimum X value 264 @param max_angle: the angle corresponding to the maximum X value 265 @param min_radius: the radius corresponding to the minimum Y value 266 @param max_radius: the radius corresponding to the maximum Y value 267 """ 268 if self._dim != 2: 269 raise TypeError("implemented only for 2D layouts") 270 bbox = self.bounding_box() 271 272 while min_angle > max_angle: 273 max_angle += 360 274 while min_angle > 360: 275 min_angle -= 360 276 max_angle -= 360 277 while min_angle < 0: 278 min_angle += 360 279 max_angle += 360 280 281 ratio_x = (max_angle - min_angle) / bbox.width 282 ratio_x *= pi / 180. 283 min_angle *= pi / 180. 284 ratio_y = (max_radius - min_radius) / bbox.height 285 for idx, (x, y) in enumerate(self._coords): 286 alpha = (x-bbox.left) * ratio_x + min_angle 287 radius = (y-bbox.top) * ratio_y + min_radius 288 self._coords[idx] = [cos(alpha)*radius, -sin(alpha)*radius] 289 290
291 - def transform(self, function, *args, **kwds):
292 """Performs an arbitrary transformation on the layout 293 294 Additional positional and keyword arguments are passed intact to 295 the given function. 296 297 @param function: a function which receives the coordinates as a 298 tuple and returns the transformed tuple. 299 """ 300 self._coords = [list(function(tuple(row), *args, **kwds)) \ 301 for row in self._coords] 302 303
304 - def centroid(self):
305 """Returns the centroid of the layout. 306 307 The centroid of the layout is the arithmetic mean of the points in 308 the layout. 309 310 @return: the centroid as a list of floats""" 311 centroid = [RunningMean() for _ in xrange(self._dim)] 312 for row in self._coords: 313 for dim in xrange(self._dim): 314 centroid[dim].add(row[dim]) 315 return [rm.mean for rm in centroid] 316
317 - def boundaries(self, border=0):
318 """Returns the boundaries of the layout. 319 320 The boundaries are the minimum and maximum coordinates along all 321 dimensions. 322 323 @param border: this value gets subtracted from the minimum bounds 324 and gets added to the maximum bounds before returning the coordinates 325 of the box. Defaults to zero. 326 @return: the minimum and maximum coordinates along all dimensions, 327 in a tuple containing two lists, one for the minimum coordinates, 328 the other one for the maximum. 329 @raises ValueError: if the layout contains no layout items 330 """ 331 if not self._coords: 332 raise ValueError("layout contains no layout items") 333 334 mins, maxs = [], [] 335 for dim in xrange(self._dim): 336 col = [row[dim] for row in self._coords] 337 mins.append(min(col)-border) 338 maxs.append(max(col)+border) 339 return mins, maxs 340
341 - def bounding_box(self, border=0):
342 """Returns the bounding box of the layout. 343 344 The bounding box of the layout is the smallest box enclosing all the 345 points in the layout. 346 347 @param border: this value gets subtracted from the minimum bounds 348 and gets added to the maximum bounds before returning the coordinates 349 of the box. Defaults to zero. 350 @return: the coordinates of the lower left and the upper right corner 351 of the box. "Lower left" means the minimum coordinates and "upper right" 352 means the maximum. These are encapsulated in a L{BoundingBox} object. 353 """ 354 if self._dim != 2: 355 raise ValueError("Layout.boundary_box() supports 2D layouts only") 356 357 try: 358 (x0, y0), (x1, y1) = self.boundaries(border) 359 return BoundingBox(x0, y0, x1, y1) 360 except ValueError: 361 return BoundingBox(0, 0, 0, 0) 362 363
364 - def center(self, *args, **kwds):
365 """Centers the layout around the given point. 366 367 The point itself can be supplied as multiple unnamed arguments, as a 368 simple unnamed list or as a keyword argument. This operation moves 369 the centroid of the layout to the given point. If no point is supplied, 370 defaults to the origin of the coordinate system. 371 372 @keyword p: the point where the centroid of the layout will be after 373 the operation.""" 374 center = kwds.get("p") or args 375 if len(center) == 0: 376 center = [0.] * self._dim 377 elif len(center) == 1 and type(center[0]) != int \ 378 and type(center[0]) != float: 379 center = center[0] 380 if len(center) != self._dim: 381 raise ValueError("the given point must have %d dimensions" \ 382 % self._dim) 383 centroid = self.centroid() 384 vec = [center[d]-centroid[d] for d in xrange(self._dim)] 385 self.translate(vec) 386 387
388 - def copy(self):
389 """Creates an exact copy of the layout.""" 390 return self.__copy__() 391
392 - def fit_into(self, bbox, keep_aspect_ratio=True):
393 """Fits the layout into the given bounding box. 394 395 The layout will be modified in-place. 396 397 @param bbox: the bounding box in which to fit the layout. If the 398 dimension of the layout is d, it can either be a d-tuple (defining 399 the sizes of the box), a 2d-tuple (defining the coordinates of the 400 top left and the bottom right point of the box), or a L{BoundingBox} 401 object (for 2D layouts only). 402 @param keep_aspect_ratio: whether to keep the aspect ratio of the current 403 layout. If C{False}, the layout will be rescaled to fit exactly into 404 the bounding box. If C{True}, the original aspect ratio of the layout 405 will be kept and it will be centered within the bounding box. 406 """ 407 if isinstance(bbox, BoundingBox): 408 if self._dim != 2: 409 raise TypeError("bounding boxes work for 2D layouts only") 410 corner, target_sizes = [bbox.left, bbox.top], [bbox.width, bbox.height] 411 elif len(bbox) == self._dim: 412 corner, target_sizes = [0.] * self._dim, list(bbox) 413 elif len(bbox) == 2 * self._dim: 414 corner, opposite_corner = list(bbox[0:self._dim]), list(bbox[self._dim:]) 415 for i in xrange(self._dim): 416 if corner[i] > opposite_corner[i]: 417 corner[i], opposite_corner[i] = opposite_corner[i], corner[i] 418 target_sizes = [max_val-min_val \ 419 for min_val, max_val in izip(corner, opposite_corner)] 420 421 try: 422 mins, maxs = self.boundaries() 423 except ValueError: 424 mins, maxs = [0.0] * self._dim, [0.0] * self._dim 425 sizes = [max_val - min_val for min_val, max_val in izip(mins, maxs)] 426 427 for i, size in enumerate(sizes): 428 if size == 0: 429 sizes[i] = 2 430 mins[i] -= 1 431 maxs[i] += 1 432 433 ratios = [float(target_size) / current_size \ 434 for current_size, target_size in izip(sizes, target_sizes)] 435 if keep_aspect_ratio: 436 min_ratio = min(ratios) 437 ratios = [min_ratio] * self._dim 438 439 translations = [] 440 for i in xrange(self._dim): 441 trans = (target_sizes[i] - ratios[i] * sizes[i]) / 2. 442 trans -= mins[i] * ratios[i] - corner[i] 443 translations.append(trans) 444 445 self.scale(*ratios) 446 self.translate(*translations)
447

   Home       Trees       Indices       Help