File Coverage

blib/lib/SVG/Plot.pm
Criterion Covered Total %
statement 99 118 83.9
branch 33 54 61.1
condition 19 29 65.5
subroutine 9 9 100.0
pod 1 3 33.3
total 161 213 75.5


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             SVG::Plot - a simple module to take one or more sets of x,y points and plot them on a plane
4              
5             =head1 SYNOPSIS
6              
7             use SVG::Plot;
8             my $points = [ [0, 1, 'http://uri/'], [2, 3, '/uri/foo.png'] ];
9             my $plot = SVG::Plot->new(
10             points => $points,
11             debug => 0,
12             scale => 0.025,
13             max_width => 800,
14             max_height => 400,
15             point_size => 3,
16             point_style => {
17             fill => 'blue',
18             stroke => 'yellow',
19             },
20             line => 'follow',
21             margin => 6,
22             );
23              
24             # -- or --
25             $plot->points($points);
26             $plot->scale(4);
27              
28             print $plot->plot;
29              
30             =head1 DESCRIPTION
31              
32             a very simple module which allows you to give one or more sets of points [x co-ord, y co-ord and optional http uri]) and plot them in SVG.
33              
34             $plot->points($points) where $points is a reference to an array of array references.
35              
36             see B for a list of parameters you can give to the plot. (overriding the styles on the ponts; sizing a margin; setting a scale; optionally drawing a line ( line => 'follow' ) between the points in the order they are specified.
37              
38             =cut
39              
40             package SVG::Plot;
41              
42             our $VERSION = '0.06';
43 3     3   1888 use strict;
  3         4  
  3         130  
44 3     3   2855 use SVG;
  3         53540  
  3         21  
45 3     3   3214 use Carp qw( croak );
  3         6  
  3         198  
46 3     3   3353 use Algorithm::Points::MinimumDistance;
  3         2839  
  3         121  
47              
48 3     3   2873 use Class::MethodMaker new_hash_init => 'new', get_set => [ qw( debug grid scale points pointsets image point_style point_size min_point_size max_point_size margin line line_style max_width max_height svg_options) ];
  3         62332  
  3         37  
49              
50             =head1 METHODS
51              
52             =over 4
53              
54             =item B
55              
56             use SVG::Plot;
57              
58             # Simple use - single set of points, all in same style.
59             my $points = [ [0, 1, 'http://uri/'], [2, 3, '/uri/foo.png'] ];
60             my $plot = SVG::Plot->new(
61             points => \@points,
62             point_size => 3,
63             point_style => {
64             fill => 'blue',
65             stroke => 'yellow',
66             },
67             line => 'follow',
68             debug => 0,
69             scale => 0.025,
70             max_width => 800,
71             max_height => 400,
72             margin => 6,
73             );
74              
75             # Prepare to plot two sets of points, in distinct styles.
76             my $pubs = [
77             [ 522770, 179023, "http://example.com/?Andover_Arms" ],
78             [ 522909, 178232, "http://example.com/?Blue Anchor" ] ];
79             my $stations = [
80             [ 523474, 178483, "http://example.com/?Hammersmith" ] ];
81             my $pointsets = [ { points => $pubs,
82             point_size => 3,
83             point_style => { fill => "blue" }
84             },
85             { points => $stations,
86             point_size => 5,
87             point_style => { fill => "red" }
88             } ];
89             my $plot = SVG::Plot->new(
90             pointsets => $pointsets,
91             scale => 0.025,
92             max_width => 800,
93             max_height => 400,
94             );
95              
96             To pass options through to L, use the C parameter:
97              
98             SVG::Plot->new( points => $points,
99             svg_options => { -nocredits => 1 }
100             );
101              
102             You can define the boundaries of the plot:
103              
104             SVG::Plot->new(
105             grid => { min_x => 1,
106             min_y => 2,
107             max_x => 15,
108             max_y => 16 }
109             );
110              
111             or
112              
113             $plot->grid($grid)
114              
115             This is like a viewbox onto the plane of the plot. If it's not
116             specified, the module works out the viewbox from the highest and
117             lowest X and Y co-ordinates in the list(s) of points.
118              
119             Note that the actual margin will be half of the value set
120             in C, since half of it goes to each side.
121              
122             If C and/or C is set then C will be
123             reduced if necessary in order to keep the width down.
124              
125             If C is set to true then debugging information is emitted as
126             warnings.
127              
128             If C is set to C then
129             L will be used to make the point
130             circles as large as possible without overlapping, within the
131             constraints of C (which defaults to 1) and
132             C (which defaults to 10). Note that if you have multiple
133             pointsets then the point circle sizes will be worked out I.
134              
135             All arguments have get_set accessors like so:
136              
137             $plot->point_size(3);
138              
139             The C, C attributes of the SVG::Plot object
140             will be used as defaults for any pointsets that don't have their own
141             style set.
142              
143             =cut
144              
145             =item B
146              
147             print $plot->plot;
148              
149             C will croak if the object has a C or C
150             attribute that is smaller than its C attribute, since this is
151             impossible.
152              
153             =cut
154              
155             sub plot {
156 5     5 1 9928 my $self = shift;
157 5         157 my $points = $self->points;
158 5         180 my $pointsets = $self->pointsets;
159 5 50 33     49 croak "no points to plot!" unless ( $points or $pointsets );
160 5         150 my $grid = $self->grid;
161              
162 5 50       50 if (not $grid) {
163 5         17 $grid = $self->work_out_grid;
164             }
165              
166 5   100     147 my $scale = $self->scale || 10;
167 5   50     197 my $m = $self->margin || 10;
168            
169             # Reduce scale if necessary to fit to width constraint.
170 5 100       191 if ( $self->max_width ) {
171 1         37 my $max_plot_width = $self->max_width - $m; # Account for margin
172 1 50       11 croak "max_width must be larger than margin"
173             if $max_plot_width <= 0;
174 1         3 my $x_extent = $grid->{max_x} - $grid->{min_x};
175 1         3 my $max_width_scale = $max_plot_width / $x_extent;
176 1 50       6 $scale = $max_width_scale if $scale > $max_width_scale;
177             }
178              
179             # Reduce scale further if necessary to fit to height constraint.
180 5 50       175 if ( $self->max_height ) {
181 0         0 my $max_plot_height = $self->max_height - $m; # Account for margin
182 0 0       0 croak "max_height must be larger than margin"
183             if $max_plot_height <= 0;
184 0         0 my $y_extent = $grid->{max_y} - $grid->{min_y};
185 0         0 my $max_height_scale = $max_plot_height / $y_extent;
186 0 0       0 $scale = $max_height_scale if $scale > $max_height_scale;
187             }
188              
189 5         65 my $h = int(($grid->{max_y} - $grid->{min_y})*$scale);
190 5         14 my $w = int(($grid->{max_x} - $grid->{min_x})*$scale);
191            
192 5 100       147 my $svg = SVG->new(
193             width => $w + $m,
194             height => $h + $m,
195 5         11 %{ $self->svg_options || {} },
196             );
197              
198 5 50       1622 if (my $map = $self->image) {
199 0         0 my $img = $svg->image(
200             x=>0, y=>0,
201             '-href'=>$map, #may also embed SVG, e.g. "image.svg"
202             id=>'image_1'
203             );
204             }
205              
206             # Process each pointset.
207 5         47 my @pointset_data;
208              
209 5 50       142 if ( $self->points ) {
210 5         175 push @pointset_data, { points => $self->points,
211             point_size => $self->point_size,
212             min_point_size => $self->min_point_size,
213             max_point_size => $self->max_point_size,
214             point_style => $self->point_style,
215             line => $self->line,
216             line_style => $self->line_style };
217             }
218              
219 5 50       213 foreach my $pointset ( @{$self->pointsets || []} ) {
  5         141  
220 0         0 push @pointset_data, $pointset;
221             }
222              
223 5         191 my %defaults = ( point_size => $self->point_size,
224             min_point_size => $self->min_point_size,
225             max_point_size => $self->max_point_size,
226             point_style => $self->point_style );
227              
228 5         109 foreach my $dataset ( @pointset_data ) {
229 5         30 $self->_plot_pointset( svg => $svg,
230             margin => $m,
231             grid => $grid,
232             scale => $scale,
233             %defaults, # can be overridden by %$dataset
234             %$dataset );
235             }
236              
237 5         19 return $svg->xmlify;
238             }
239              
240             # Adds a pointset to the SVG plot - pass in args svg, margin, grid, scale,
241             # points, point_size, min_point_size, max_point_size, point_style,
242             # line, line_style.
243             sub _plot_pointset {
244 5     5   27 my ($self, %args) = @_;
245 5 50       58 my $points = $args{points} or croak "no points in pointset!";
246 5 50       16 scalar @$points or croak "no points in pointset!";
247 5 50       15 my $svg = $args{svg} or croak "no SVG object passed";
248 5 50       14 my $scale = $args{scale} or croak "no scale passed";
249 5   50     31 my $point_style = $args{point_style} || { stroke => 'red',
250             fill => 'white' };
251              
252 5         17 my $z = $svg->tag( 'g',
253             id => 'group_'.$self->random_id,
254             style => $point_style
255             );
256              
257 5         320 my $point_size = $args{point_size};
258 5 100 66     27 if ( $point_size && $point_size eq "AUTO" ) {
259 4   100     16 my $min_size = $args{min_point_size} || 1;
260 4   100     16 my $max_size = $args{max_point_size} || 10;
261             # Make sure we don't send URIs to A::P::MD
262 4         6 my @coords = map { [ $_->[0], $_->[1] ] } @$points;
  15         34  
263 4         25 my $boxsize = 1 + sprintf("%d", $max_size/$scale);
264 4         26 my $dists = Algorithm::Points::MinimumDistance->new(
265             points => \@coords,
266             boxsize => $boxsize );
267 4         3799 my $min_dist = $dists->min_distance;
268 4         70 my $auto_size = sprintf("%d", $scale*$min_dist/2);
269 4         14 $point_size = $auto_size;
270 4 100 66     24 if ( $min_size and $point_size < $min_size ) {
271 1         2 $point_size = $min_size;
272             }
273 4 100 66     58 if ( $max_size and $point_size > $max_size ) {
274 1         12 $point_size = $max_size;
275             }
276             }
277              
278 5   100     14 $point_size ||= 3;
279 5         7 my $plotted;
280              
281 5         17 foreach (@$points) {
282             # adding a margin ...
283 17         1696 my $halfm = $args{margin} / 2;
284              
285 17         31 my ($x,$y) = ($_->[0],$_->[1]);
286 17   66     58 my $href = $_->[2] || $self->random_id;
287            
288             # svg is upside-down
289 17         56 $x = int(($x - $args{grid}->{min_x})*$scale) + $halfm;
290 17         28 $y = int(($args{grid}->{max_y} - $y)*$scale) + $halfm;
291              
292 17         40 push @$plotted, [$x,$y,$href];
293 17         31 my $id = $self->random_id;
294 17 50       565 warn("anchor_$id") if $self->debug;;
295              
296 17         208 $z->anchor(id => "anchor_".$id,
297             -href => $href,
298             -target => 'new_window_0')->circle(
299             cx => $x, cy => $y,
300             r => $point_size,
301             id => 'dot_'.$id,
302             );
303             }
304              
305 5 50       749 if (my $line = $args{line}) {
306 0         0 my $style = $args{line_style};
307 0   0     0 $style ||= { 'stroke-width' => 2, stroke => 'blue' };
308              
309 0 0       0 if ($line eq 'follow') {
310 0         0 for my $n (0..($#{$plotted}-1)) {
  0         0  
311 0         0 my $p1 = $plotted->[$n];
312 0         0 my $p2 = $plotted->[$n+1];
313 0         0 my $tag = $svg->line(
314             id => $self->random_id,
315             x1 => $p1->[0], y1 => $p1->[1],
316             x2 => $p2->[0], y2 => $p2->[1],
317             style => $style
318             );
319             }
320             }
321             }
322             }
323              
324             sub work_out_grid {
325 5     5 0 8 my $self = shift;
326 5         140 my $all_points = $self->points;
327 5         169 my $pointsets = $self->pointsets;
328              
329 5 50       42 if ( $pointsets ) {
330 0         0 foreach my $pointset ( map { $_->{points} } @$pointsets ) {
  0         0  
331 0         0 foreach my $point ( @$pointset ) {
332 0         0 push @$all_points, $point;
333             }
334             }
335             }
336              
337 5         10 my $start = $all_points->[0];
338 5         8 my ($lx,$ly,$hx,$hy);
339 5         9 $lx = $start->[0];
340 5         7 $hx = $lx;
341 5         6 $ly = $start->[1];
342 5         6 $hy = $ly;
343              
344 5         11 foreach (@$all_points) {
345              
346 17 100       37 $lx = $_->[0] if ($_->[0] <= $lx);
347 17 100       33 $ly = $_->[1] if ($_->[1] <= $ly);
348 17 100       45 $hx = $_->[0] if ($_->[0] >= $hx);
349 17 100       41 $hy = $_->[1] if ($_->[1] >= $hy);
350             }
351             return {
352 5         31 min_x => $lx,
353             max_x => $hx,
354             min_y => $ly,
355             max_y => $hy
356             };
357             }
358              
359             sub random_id {
360 37     37 0 79 my @t = (0..9);
361 37         61 return '_:id'.join '', (map { @t[rand @t] } 0..6);
  259         726  
362             }
363              
364             =back
365              
366             =cut
367            
368             1;
369              
370             =head1 NOTES
371              
372             this is an early draft, released mostly so Kake can use it in OpenGuides without having non-CPAN dependencies.
373              
374             for an example of what one should be able to do with this, see http://space.frot.org/rdf/tubemap.svg ... a better way of making meta-information between the lines, some kind of matrix drawing; cf the grubstreet link below, different styles according to locales, sets, conceptual contexts...
375              
376             it would be fun to supply access to different plotting algorithms, not just for the cartesian plane; particularly the buckminster fuller dymaxion map; cf Geo::Dymaxion, when that gets released (http://iconocla.st/hacks/dymax/ )
377              
378             to see work in progress, http://un.earth.li/~kake/cgi-bin/plot2.cgi?cat=Pubs&cat=Restaurants&cat=Tube&colour_diff_this=loc&action=display
379              
380             =head1 BUGS
381              
382             possibly. this is alpha in terms of functionality, beta in terms of code; the API won't break backwards, though.
383              
384              
385             =head1 AUTHOR
386              
387             Jo Walsh ( jo@london.pm.org )
388             Kate L Pugh ( kake@earth.li )
389              
390             =cut
391              
392              
393