python-igraph manual

For using igraph from Python

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

Source Code for Module igraph.drawing.edge

  1  """ 
  2  Drawers for various edge styles in graph plots. 
  3  """ 
  4   
  5  __all__ = ["AbstractEdgeDrawer", "AlphaVaryingEdgeDrawer", 
  6             "ArrowEdgeDrawer", "DarkToLightEdgeDrawer", 
  7             "LightToDarkEdgeDrawer", "TaperedEdgeDrawer"] 
  8   
  9  __license__ = "GPL" 
 10   
 11  from igraph.drawing.colors import clamp 
 12  from igraph.drawing.metamagic import AttributeCollectorBase 
 13  from igraph.drawing.text import TextAlignment 
 14  from igraph.drawing.utils import find_cairo 
 15  from math import atan2, cos, pi, sin, sqrt 
 16   
 17  cairo = find_cairo() 
18 19 -class AbstractEdgeDrawer(object):
20 """Abstract edge drawer object from which all concrete edge drawer 21 implementations are derived.""" 22
23 - def __init__(self, context, palette):
24 """Constructs the edge drawer. 25 26 @param context: a Cairo context on which the edges will be drawn. 27 @param palette: the palette that can be used to map integer 28 color indices to colors when drawing edges 29 """ 30 self.context = context 31 self.palette = palette 32 self.VisualEdgeBuilder = self._construct_visual_edge_builder() 33 34 @staticmethod
35 - def _curvature_to_float(value):
36 """Converts values given to the 'curved' edge style argument 37 in plotting calls to floating point values.""" 38 if value is None or value is False: 39 return 0.0 40 if value is True: 41 return 0.5 42 return float(value) 43
45 """Construct the visual edge builder that will collect the visual 46 attributes of an edge when it is being drawn.""" 47 class VisualEdgeBuilder(AttributeCollectorBase): 48 """Builder that collects some visual properties of an edge for 49 drawing""" 50 _kwds_prefix = "edge_" 51 arrow_size = 1.0 52 arrow_width = 1.0 53 color = ("#444", self.palette.get) 54 curved = (0.0, self._curvature_to_float) 55 label = None 56 label_color = ("black", self.palette.get) 57 label_size = 12.0 58 width = 1.0 59 return VisualEdgeBuilder 60
61 - def draw_directed_edge(self, edge, src_vertex, dest_vertex):
62 """Draws a directed edge. 63 64 @param edge: the edge to be drawn. Visual properties of the edge 65 are defined by the attributes of this object. 66 @param src_vertex: the source vertex. Visual properties are given 67 again as attributes. 68 @param dest_vertex: the target vertex. Visual properties are given 69 again as attributes. 70 """ 71 raise NotImplementedError() 72
73 - def draw_loop_edge(self, edge, vertex):
74 """Draws a loop edge. 75 76 The default implementation draws a small circle. 77 78 @param edge: the edge to be drawn. Visual properties of the edge 79 are defined by the attributes of this object. 80 @param vertex: the vertex to which the edge is attached. Visual 81 properties are given again as attributes. 82 """ 83 ctx = self.context 84 ctx.set_source_rgba(*edge.color) 85 ctx.set_line_width(edge.width) 86 radius = vertex.size * 1.5 87 center_x = vertex.position[0] + cos(pi/4) * radius / 2. 88 center_y = vertex.position[1] - sin(pi/4) * radius / 2. 89 ctx.arc(center_x, center_y, radius/2., 0, pi * 2) 90 ctx.stroke() 91
92 - def draw_undirected_edge(self, edge, src_vertex, dest_vertex):
93 """Draws an undirected edge. 94 95 The default implementation of this method draws undirected edges 96 as straight lines. Loop edges are drawn as small circles. 97 98 @param edge: the edge to be drawn. Visual properties of the edge 99 are defined by the attributes of this object. 100 @param src_vertex: the source vertex. Visual properties are given 101 again as attributes. 102 @param dest_vertex: the target vertex. Visual properties are given 103 again as attributes. 104 """ 105 if src_vertex == dest_vertex: # TODO 106 return self.draw_loop_edge(edge, src_vertex) 107 108 ctx = self.context 109 ctx.set_source_rgba(*edge.color) 110 ctx.set_line_width(edge.width) 111 ctx.move_to(*src_vertex.position) 112 113 if edge.curved: 114 (x1, y1), (x2, y2) = src_vertex.position, dest_vertex.position 115 aux1 = (2*x1+x2) / 3.0 - edge.curved * 0.5 * (y2-y1), \ 116 (2*y1+y2) / 3.0 + edge.curved * 0.5 * (x2-x1) 117 aux2 = (x1+2*x2) / 3.0 - edge.curved * 0.5 * (y2-y1), \ 118 (y1+2*y2) / 3.0 + edge.curved * 0.5 * (x2-x1) 119 ctx.curve_to(aux1[0], aux1[1], aux2[0], aux2[1], *dest_vertex.position) 120 else: 121 ctx.line_to(*dest_vertex.position) 122 123 ctx.stroke() 124
125 - def get_label_position(self, edge, src_vertex, dest_vertex):
126 """Returns the position where the label of an edge should be drawn. The 127 default implementation returns the midpoint of the edge and an alignment 128 that tries to avoid overlapping the label with the edge. 129 130 @param edge: the edge to be drawn. Visual properties of the edge 131 are defined by the attributes of this object. 132 @param src_vertex: the source vertex. Visual properties are given 133 again as attributes. 134 @param dest_vertex: the target vertex. Visual properties are given 135 again as attributes. 136 @return: a tuple containing two more tuples: the desired position of the 137 label and the desired alignment of the label, where the position is 138 given as C{(x, y)} and the alignment is given as C{(horizontal, vertical)}. 139 Members of the alignment tuple are taken from constants in the 140 L{TextAlignment} class. 141 """ 142 # Determine the angle of the line 143 dx = dest_vertex.position[0] - src_vertex.position[0] 144 dy = dest_vertex.position[1] - src_vertex.position[1] 145 if dx != 0 or dy != 0: 146 # Note that we use -dy because the Y axis points downwards 147 angle = atan2(-dy, dx) % (2*pi) 148 else: 149 angle = None 150 151 # Determine the midpoint 152 pos = ((src_vertex.position[0] + dest_vertex.position[0]) / 2., \ 153 (src_vertex.position[1] + dest_vertex.position[1]) / 2) 154 155 # Determine the alignment based on the angle 156 pi4 = pi / 4 157 if angle is None: 158 halign, valign = TextAlignment.CENTER, TextAlignment.CENTER 159 else: 160 index = int((angle / pi4) % 8) 161 halign = [TextAlignment.RIGHT, TextAlignment.RIGHT, 162 TextAlignment.RIGHT, TextAlignment.RIGHT, 163 TextAlignment.LEFT, TextAlignment.LEFT, 164 TextAlignment.LEFT, TextAlignment.LEFT][index] 165 valign = [TextAlignment.BOTTOM, TextAlignment.CENTER, 166 TextAlignment.CENTER, TextAlignment.TOP, 167 TextAlignment.TOP, TextAlignment.CENTER, 168 TextAlignment.CENTER, TextAlignment.BOTTOM][index] 169 170 return pos, (halign, valign)
171
172 173 -class ArrowEdgeDrawer(AbstractEdgeDrawer):
174 """Edge drawer implementation that draws undirected edges as 175 straight lines and directed edges as arrows. 176 """ 177
178 - def draw_directed_edge(self, edge, src_vertex, dest_vertex):
179 if src_vertex == dest_vertex: # TODO 180 return self.draw_loop_edge(edge, src_vertex) 181 182 ctx = self.context 183 (x1, y1), (x2, y2) = src_vertex.position, dest_vertex.position 184 (x_src, y_src), (x_dest, y_dest) = src_vertex.position, dest_vertex.position 185 186 187 def bezier_cubic(x0,y0, x1,y1, x2,y2, x3,y3, t): 188 """ Computes the Bezier curve from point (x0,y0) to (x3,y3) 189 via control points (x1,y1) and (x2,y2) with parameter t. 190 """ 191 xt = (1.0 - t) ** 3 * x0 + 3. *t * (1.0 - t) ** 2 * x1 + 3. * t**2 * (1. - t) * x2 + t**3 * x3 192 yt = (1.0 - t) ** 3 * y0 + 3. *t * (1.0 - t) ** 2 * y1 + 3. * t**2 * (1. - t) * y2 + t**3 * y3 193 return xt,yt 194 195 def euclidean_distance(x1,y1,x2,y2): 196 """ Computes the Euclidean distance between points (x1,y1) and (x2,y2). 197 """ 198 return sqrt( (1.0*x1-x2) **2 + (1.0*y1-y2) **2 ) 199 200 def intersect_bezier_circle(x0,y0, x1,y1, x2,y2, x3,y3, radius): 201 """ Binary search solver for finding the intersection of a Bezier curve 202 and a circle centered at the curve's end point. 203 Returns the x,y of the intersection point. 204 TODO: implement safeguard to ensure convergence in ALL possible cases. 205 """ 206 precision = radius / 20.0 207 source_target_distance = euclidean_distance(x0,y0,x3,y3) 208 radius = float(radius) 209 t0 = 1.0 210 t1 = 1.0 - radius / source_target_distance 211 212 xt0, yt0 = x3, y3 213 xt1, yt1 = bezier_cubic(x0,y0, x1,y1, x2,y2, x3,y3, t1) 214 215 distance_t0 = 0 216 distance_t1 = euclidean_distance(x3,y3, xt1,yt1) 217 counter = 0 218 while abs(distance_t1 - radius) > precision: 219 if ((distance_t1-radius) > 0) != ((distance_t0-radius) > 0): 220 t_new = (t0 + t1)/2.0 221 else: 222 if (abs(distance_t1 - radius) < abs(distance_t0 - radius)): 223 # If t1 gets us closer to the circumference step in the same direction 224 t_new = t1 + (t1 - t0)/ 2.0 225 else: 226 t_new = t1 - (t1 - t0) 227 t_new = 1 if t_new > 1 else (0 if t_new < 0 else t_new) 228 t0,t1 = t1,t_new 229 distance_t0 = distance_t1 230 xt1, yt1 = bezier_cubic(x0,y0, x1,y1, x2,y2, x3,y3, t1) 231 distance_t1 = euclidean_distance(x3,y3, xt1,yt1) 232 counter += 1 233 return bezier_cubic(x0,y0, x1,y1, x2,y2, x3,y3, t1) 234 235 236 237 # Draw the edge 238 ctx.set_source_rgba(*edge.color) 239 ctx.set_line_width(edge.width) 240 ctx.move_to(x1, y1) 241 242 if edge.curved: 243 # Calculate the curve 244 aux1 = (2*x1+x2) / 3.0 - edge.curved * 0.5 * (y2-y1), \ 245 (2*y1+y2) / 3.0 + edge.curved * 0.5 * (x2-x1) 246 aux2 = (x1+2*x2) / 3.0 - edge.curved * 0.5 * (y2-y1), \ 247 (y1+2*y2) / 3.0 + edge.curved * 0.5 * (x2-x1) 248 249 # Coordinates of the control points of the Bezier curve 250 xc1, yc1 = aux1 251 xc2, yc2 = aux2 252 253 # Determine where the edge intersects the circumference of the 254 # vertex shape: Tip of the arrow 255 x2, y2 = intersect_bezier_circle(x_src,y_src, xc1,yc1, xc2,yc2, x_dest,y_dest, dest_vertex.size/2.0) 256 257 # Calculate the arrow head coordinates 258 angle = atan2(y_dest - y2, x_dest - x2) # navid 259 arrow_size = 15. * edge.arrow_size 260 arrow_width = 10. / edge.arrow_width 261 aux_points = [ 262 (x2 - arrow_size * cos(angle - pi/arrow_width), 263 y2 - arrow_size * sin(angle - pi/arrow_width)), 264 (x2 - arrow_size * cos(angle + pi/arrow_width), 265 y2 - arrow_size * sin(angle + pi/arrow_width)), 266 ] 267 268 # Midpoint of the base of the arrow triangle 269 x_arrow_mid , y_arrow_mid = (aux_points [0][0] + aux_points [1][0]) / 2.0, (aux_points [0][1] + aux_points [1][1]) / 2.0 270 271 # Vector representing the base of the arrow triangle 272 x_arrow_base_vec, y_arrow_base_vec = (aux_points [0][0] - aux_points [1][0]) , (aux_points [0][1] - aux_points [1][1]) 273 274 # Recalculate the curve such that it lands on the base of the arrow triangle 275 aux1 = (2*x_src+x_arrow_mid) / 3.0 - edge.curved * 0.5 * (y_arrow_mid-y_src), \ 276 (2*y_src+y_arrow_mid) / 3.0 + edge.curved * 0.5 * (x_arrow_mid-x_src) 277 aux2 = (x_src+2*x_arrow_mid) / 3.0 - edge.curved * 0.5 * (y_arrow_mid-y_src), \ 278 (y_src+2*y_arrow_mid) / 3.0 + edge.curved * 0.5 * (x_arrow_mid-x_src) 279 280 # Offset the second control point (aux2) such that it falls precisely on the normal to the arrow base vector 281 # Strictly speaking, offset_length is the offset length divided by the length of the arrow base vector. 282 offset_length = (x_arrow_mid - aux2[0]) * x_arrow_base_vec + (y_arrow_mid - aux2[1]) * y_arrow_base_vec 283 offset_length /= euclidean_distance(0,0, x_arrow_base_vec, y_arrow_base_vec) ** 2 284 285 aux2 = aux2[0] + x_arrow_base_vec * offset_length, \ 286 aux2[1] + y_arrow_base_vec * offset_length 287 288 # Draw tthe curve from the first vertex to the midpoint of the base of the arrow head 289 ctx.curve_to(aux1[0], aux1[1], aux2[0], aux2[1], x_arrow_mid, y_arrow_mid) 290 else: 291 # Determine where the edge intersects the circumference of the 292 # vertex shape. 293 x2, y2 = dest_vertex.shape.intersection_point( 294 x2, y2, x1, y1, dest_vertex.size) 295 296 # Draw the arrowhead 297 angle = atan2(y_dest - y2, x_dest - x2) 298 arrow_size = 15. * edge.arrow_size 299 arrow_width = 10. / edge.arrow_width 300 aux_points = [ 301 (x2 - arrow_size * cos(angle - pi/arrow_width), 302 y2 - arrow_size * sin(angle - pi/arrow_width)), 303 (x2 - arrow_size * cos(angle + pi/arrow_width), 304 y2 - arrow_size * sin(angle + pi/arrow_width)), 305 ] 306 307 # Midpoint of the base of the arrow triangle 308 x_arrow_mid , y_arrow_mid = (aux_points [0][0] + aux_points [1][0]) / 2.0, (aux_points [0][1] + aux_points [1][1]) / 2.0 309 # Draw the line 310 ctx.line_to(x_arrow_mid, y_arrow_mid) 311 312 # Draw the edge 313 ctx.stroke() 314 315 316 # Draw the arrow head 317 ctx.move_to(x2, y2) 318 ctx.line_to(*aux_points[0]) 319 ctx.line_to(*aux_points[1]) 320 ctx.line_to(x2, y2) 321 ctx.fill()
322
323 324 325 -class TaperedEdgeDrawer(AbstractEdgeDrawer):
326 """Edge drawer implementation that draws undirected edges as 327 straight lines and directed edges as tapered lines that are 328 wider at the source and narrow at the destination. 329 """ 330
331 - def draw_directed_edge(self, edge, src_vertex, dest_vertex):
332 if src_vertex == dest_vertex: # TODO 333 return self.draw_loop_edge(edge, src_vertex) 334 335 # Determine where the edge intersects the circumference of the 336 # destination vertex. 337 src_pos, dest_pos = src_vertex.position, dest_vertex.position 338 dest_pos = dest_vertex.shape.intersection_point( 339 dest_pos[0], dest_pos[1], src_pos[0], src_pos[1], 340 dest_vertex.size 341 ) 342 343 ctx = self.context 344 345 # Draw the edge 346 ctx.set_source_rgba(*edge.color) 347 ctx.set_line_width(edge.width) 348 angle = atan2(dest_pos[1]-src_pos[1], dest_pos[0]-src_pos[0]) 349 arrow_size = src_vertex.size / 4. 350 aux_points = [ 351 (src_pos[0] + arrow_size * cos(angle + pi/2), 352 src_pos[1] + arrow_size * sin(angle + pi/2)), 353 (src_pos[0] + arrow_size * cos(angle - pi/2), 354 src_pos[1] + arrow_size * sin(angle - pi/2)) 355 ] 356 ctx.move_to(*dest_pos) 357 ctx.line_to(*aux_points[0]) 358 ctx.line_to(*aux_points[1]) 359 ctx.line_to(*dest_pos) 360 ctx.fill()
361
362 363 -class AlphaVaryingEdgeDrawer(AbstractEdgeDrawer):
364 """Edge drawer implementation that draws undirected edges as 365 straight lines and directed edges by varying the alpha value 366 of the specified edge color between the source and the destination. 367 """ 368
369 - def __init__(self, context, alpha_at_src, alpha_at_dest):
370 super(AlphaVaryingEdgeDrawer, self).__init__(context) 371 self.alpha_at_src = (clamp(float(alpha_at_src), 0., 1.), ) 372 self.alpha_at_dest = (clamp(float(alpha_at_dest), 0., 1.), ) 373
374 - def draw_directed_edge(self, edge, src_vertex, dest_vertex):
375 if src_vertex == dest_vertex: # TODO 376 return self.draw_loop_edge(edge, src_vertex) 377 378 src_pos, dest_pos = src_vertex.position, dest_vertex.position 379 ctx = self.context 380 381 # Set up the gradient 382 lg = cairo.LinearGradient(src_pos[0], src_pos[1], dest_pos[0], dest_pos[1]) 383 edge_color = edge.color[:3] + self.alpha_at_src 384 edge_color_end = edge_color[:3] + self.alpha_at_dest 385 lg.add_color_stop_rgba(0, *edge_color) 386 lg.add_color_stop_rgba(1, *edge_color_end) 387 388 # Draw the edge 389 ctx.set_source(lg) 390 ctx.set_line_width(edge.width) 391 ctx.move_to(*src_pos) 392 ctx.line_to(*dest_pos) 393 ctx.stroke()
394
395 396 -class LightToDarkEdgeDrawer(AlphaVaryingEdgeDrawer):
397 """Edge drawer implementation that draws undirected edges as 398 straight lines and directed edges by using an alpha value of 399 zero (total transparency) at the source and an alpha value of 400 one (full opacity) at the destination. The alpha value is 401 interpolated in-between. 402 """ 403
404 - def __init__(self, context):
405 super(LightToDarkEdgeDrawer, self).__init__(context, 0.0, 1.0)
406
407 408 -class DarkToLightEdgeDrawer(AlphaVaryingEdgeDrawer):
409 """Edge drawer implementation that draws undirected edges as 410 straight lines and directed edges by using an alpha value of 411 one (full opacity) at the source and an alpha value of zero 412 (total transparency) at the destination. The alpha value is 413 interpolated in-between. 414 """ 415
416 - def __init__(self, context):
417 super(DarkToLightEdgeDrawer, self).__init__(context, 1.0, 0.0)
418

   Home       Trees       Indices       Help