File Coverage

blib/lib/Graph/Layouter/Spring.pm
Criterion Covered Total %
statement 18 84 21.4
branch 0 26 0.0
condition 0 9 0.0
subroutine 6 10 60.0
pod 1 1 100.0
total 25 130 19.2


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Graph::Layouter::Spring - spring graph drawing algorithm implementation
4              
5             =cut
6              
7              
8             package Graph::Layouter::Spring;
9              
10 1     1   4082 use strict;
  1         3  
  1         157  
11 1     1   5 use Carp qw (croak);
  1         2  
  1         77  
12              
13 1     1   5 use vars qw ($VERSION @ISA @EXPORT_OK);
  1         2  
  1         105  
14              
15             # $Id: Spring.pm,v 1.8 2006/02/11 17:11:39 pasky Exp $
16             $VERSION = 0.03;
17              
18              
19             =head1 SYNOPSIS
20              
21             use Graph::Layouter::Spring;
22             Graph::Layouter::Spring::layout($graph);
23              
24             =cut
25              
26              
27 1     1   7 use base qw (Graph::Layouter);
  1         2  
  1         145  
28              
29             require Exporter;
30             push @ISA, 'Exporter';
31              
32             @EXPORT_OK = qw (layout);
33              
34              
35             =head1 DESCRIPTION
36              
37             This module provides the famous spring graph drawing algorithm implementation.
38             See the C class documentation for usage description.
39              
40             =cut
41              
42              
43 1     1   7 use Graph;
  1         3  
  1         18  
44 1     1   5 use Graph::Layouter;
  1         2  
  1         3480  
45              
46             =head2 How does it work
47              
48             The algorithm is principially simple, simulating a space of electrically
49             charged particles. Basically, each node is thought of as a particle with the
50             same charge, therefore they all try to get as far of each other as possible. On
51             the other hand, though, there are the edges, which keep nodes together; higher
52             weight the edges have, stronger are they in pulling nodes near each other.
53              
54             So to recapitulate, we have I pushing nodes from each other
55             and I pushing connected nodes near each other. We then just
56             apply the repulsive force between each two nodes and the attractive force
57             between each two connected nodes; each node will have a resulting movement
58             force, which we will apply to the node's position (initially randomzero-zero)
59             after the forces calculation is finished.
60              
61             However, we need to let this repeat for several times in order for the
62             positions to stabilize. In fact, a lot of iterations is needed; higher the
63             better, but also higher the slower, you can very easily get to tens of seconds
64             here so beware. Currently, the number of iterations is hardcoded to 500, but
65             this is expected to get configurable soon.
66              
67             =cut
68              
69             # TODO : _This_ should be all adjustable!
70              
71             my $iterations = 100; # undef for no iterations limit
72             my $max_wait = 10; # (in seconds) undef for no time limit
73             my $max_repulsive_force_distance = 6;
74             my $k = 2;
75             my $c = 0.01;
76             my $max_vertex_movement = 0.5;
77              
78             sub layout {
79 0     0 1   my $graph = shift;
80              
81 0           Graph::Layouter::_layout_prepare($graph);
82              
83             # Cache
84 0           my @vertices = $graph->vertices;
85              
86             # Bound execution based on time
87 0           my $end;
88 0 0         $end = time + $max_wait if defined $max_wait;
89              
90 0 0 0       unless (defined $end or defined $iterations) {
91 0           croak "You did not bound the layouting loop by either time or iterations count!";
92             }
93              
94 0 0 0       for (my $i = 0;
    0          
95             (defined $iterations ? $i < $iterations : 1)
96             and (defined $end ? time <= $end : 1);
97             $i++) {
98 0           _layout_iteration($graph, \@vertices);
99             }
100              
101 0           Graph::Layouter::_layout_calc_bounds($graph);
102             }
103              
104              
105             sub _layout_repulsive($$$) {
106 0     0     my ($graph, $vertex1, $vertex2) = @_;
107              
108 0           my $dx = $graph->get_vertex_attribute($vertex2, 'layout_pos1') -
109             $graph->get_vertex_attribute($vertex1, 'layout_pos1');
110 0           my $dy = $graph->get_vertex_attribute($vertex2, 'layout_pos2') -
111             $graph->get_vertex_attribute($vertex1, 'layout_pos2');
112              
113 0           my $d2 = $dx * $dx + $dy * $dy;
114 0 0         if ($d2 < 0.01) {
115 0           $dx = rand (0.1) + 0.1;
116 0           $dy = rand (0.1) + 0.1;
117 0           $d2 = $dx * $dx + $dy * $dy;
118             }
119              
120 0           my $d = sqrt $d2;
121 0 0         if ($d < $max_repulsive_force_distance) {
122 0           my $repulsive_force = $k * $k / $d;
123              
124             # Now, how simple and clear would this be without the silly
125             # encapsulation games...
126              
127 0           $graph->set_vertex_attribute($vertex2, 'layout_force1',
128             $graph->get_vertex_attribute($vertex2, 'layout_force1')
129             + $repulsive_force * $dx / $d);
130 0           $graph->set_vertex_attribute($vertex2, 'layout_force2',
131             $graph->get_vertex_attribute($vertex2, 'layout_force2')
132             + $repulsive_force * $dy / $d);
133              
134 0           $graph->set_vertex_attribute($vertex1, 'layout_force1',
135             $graph->get_vertex_attribute($vertex1, 'layout_force1')
136             - $repulsive_force * $dx / $d);
137 0           $graph->set_vertex_attribute($vertex1, 'layout_force2',
138             $graph->get_vertex_attribute($vertex1, 'layout_force2')
139             - $repulsive_force * $dy / $d);
140             }
141             }
142              
143             sub _layout_attractive($@) {
144 0     0     my ($graph, $vertex1, $vertex2) = @_;
145              
146 0           my $dx = $graph->get_vertex_attribute($vertex2, 'layout_pos1') -
147             $graph->get_vertex_attribute($vertex1, 'layout_pos1');
148 0           my $dy = $graph->get_vertex_attribute($vertex2, 'layout_pos2') -
149             $graph->get_vertex_attribute($vertex1, 'layout_pos2');
150              
151 0           my $d2 = $dx * $dx + $dy * $dy;
152 0 0         if ($d2 < 0.01) {
153 0           $dx = rand (0.1) + 0.1;
154 0           $dy = rand (0.1) + 0.1;
155 0           $d2 = $dx * $dx + $dy * $dy;
156             }
157              
158 0           my $d = sqrt $d2;
159 0 0         if ($d > $max_repulsive_force_distance) {
160 0           $d = $max_repulsive_force_distance;
161 0           $d2 = $d * $d;
162             }
163              
164 0           my $attractive_force = ($d2 - $k * $k) / $k;
165 0           my $weight = $graph->get_edge_attribute($vertex1, $vertex2, 'weight');
166 0 0 0       $weight = 1 if not $weight or $weight < 1;
167 0           $attractive_force *= log($weight) * 0.5 + 1;
168              
169 0           $graph->set_vertex_attribute($vertex2, 'layout_force1',
170             $graph->get_vertex_attribute($vertex2, 'layout_force1')
171             - $attractive_force * $dx / $d);
172 0           $graph->set_vertex_attribute($vertex2, 'layout_force2',
173             $graph->get_vertex_attribute($vertex2, 'layout_force2')
174             - $attractive_force * $dy / $d);
175              
176 0           $graph->set_vertex_attribute($vertex1, 'layout_force1',
177             $graph->get_vertex_attribute($vertex1, 'layout_force1')
178             + $attractive_force * $dx / $d);
179 0           $graph->set_vertex_attribute($vertex1, 'layout_force2',
180             $graph->get_vertex_attribute($vertex1, 'layout_force2')
181             + $attractive_force * $dy / $d);
182             }
183              
184             sub _layout_iteration($$) {
185 0     0     my ($graph, $vertices) = @_;
186              
187             # Welcome to the time-critical zone
188              
189             # Forces on vertices due to vertex-vertex repulsions
190              
191 0           foreach my $n1 (0 .. $#$vertices) {
192 0           my $vertex1 = $vertices->[$n1];
193 0           foreach my $n2 ($n1 + 1 .. $#$vertices) {
194 0           my $vertex2 = $vertices->[$n2];
195              
196 0           _layout_repulsive($graph, $vertex1, $vertex2);
197             }
198             }
199              
200             # Forces on vertices due to edge attractions
201              
202 0           my @edges = $graph->edges;
203 0           foreach my $edge (@edges) {
204 0           _layout_attractive($graph, @$edge);
205             }
206              
207             # Move by the given force
208              
209 0           foreach my $vertex (@$vertices) {
210 0           my $xmove = $c * $graph->get_vertex_attribute($vertex, 'layout_force1');
211 0           my $ymove = $c * $graph->get_vertex_attribute($vertex, 'layout_force2');
212              
213 0           my $max = $max_vertex_movement;
214 0 0         $xmove = $max if $xmove > $max;
215 0 0         $xmove = -$max if $xmove < -$max;
216 0 0         $ymove = $max if $ymove > $max;
217 0 0         $ymove = -$max if $ymove < -$max;
218              
219 0           $graph->set_vertex_attribute($vertex, 'layout_pos1',
220             $graph->get_vertex_attribute($vertex, 'layout_pos1')
221             + $xmove);
222 0           $graph->set_vertex_attribute($vertex, 'layout_pos2',
223             $graph->get_vertex_attribute($vertex, 'layout_pos2')
224             + $ymove);
225              
226 0           $graph->set_vertex_attribute($vertex, 'layout_force1', 0);
227 0           $graph->set_vertex_attribute($vertex, 'layout_force2', 0);
228             }
229             }
230              
231              
232             =head1 SEE ALSO
233              
234             C, C, C
235              
236              
237             =head1 BUGS
238              
239             The object-oriented interface is missing as well as some more universal layout
240             calling interface (hash parameters).
241              
242             It should all be configurable.
243              
244              
245             =head1 COPYRIGHT
246              
247             Copyright 2004 by Petr Baudis Epasky@ucw.czE.
248              
249             This code is distributed under the same copyright terms as
250             Perl itself.
251              
252              
253             The algorithm is based on a spring-style layouter of a Java-based social
254             network tracker PieSpy written by Paul Mutton Epaul@jibble.orgE.
255              
256              
257             =head1 VERSION
258              
259             Version 0.03
260              
261             $Id: Spring.pm,v 1.8 2006/02/11 17:11:39 pasky Exp $
262              
263             =cut
264              
265             1;