File Coverage

blib/lib/Bio/Phylo/Treedrawer/Svg.pm
Criterion Covered Total %
statement 76 165 46.0
branch 12 40 30.0
condition 8 54 14.8
subroutine 16 21 76.1
pod n/a
total 112 280 40.0


line stmt bran cond sub pod time code
1             package Bio::Phylo::Treedrawer::Svg;
2 2     2   13 use strict;
  2         4  
  2         59  
3 2     2   9 use warnings;
  2         4  
  2         62  
4 2     2   9 use base 'Bio::Phylo::Treedrawer::Abstract';
  2         6  
  2         657  
5 2     2   20 use Bio::Phylo::Util::CONSTANT qw'looks_like_hash _PI_';
  2         4  
  2         130  
6 2     2   11 use Bio::Phylo::Util::Exceptions 'throw';
  2         4  
  2         71  
7 2     2   676 use Bio::Phylo::Util::Dependency 'SVG';
  2         6  
  2         32  
8 2     2   11 use Bio::Phylo::Util::Logger;
  2         4  
  2         1336  
9             SVG->import(
10             '-nocredits' => 1,
11             '-inline' => 1,
12             '-indent' => ' ',
13             );
14             my $logger = Bio::Phylo::Util::Logger->new;
15             my $PI = _PI_;
16             my %colors;
17              
18             =head1 NAME
19              
20             Bio::Phylo::Treedrawer::Svg - Graphics format writer used by treedrawer, no
21             serviceable parts inside
22              
23             =head1 DESCRIPTION
24              
25             This module creates a scalable vector graphic from a Bio::Phylo::Forest::DrawTree
26             object. It is called by the L<Bio::Phylo::Treedrawer> object, so look there to
27             learn how to create tree drawings.
28              
29             =begin comment
30              
31             Type : Constructor
32             Title : _new
33             Usage : my $svg = Bio::Phylo::Treedrawer::Svg->_new(%args);
34             Function: Initializes a Bio::Phylo::Treedrawer::Svg object.
35             Alias :
36             Returns : A Bio::Phylo::Treedrawer::Svg object.
37             Args : none.
38              
39             =end comment
40              
41             =cut
42              
43             sub _new {
44 2     2   4 my $class = shift;
45 2         9 my %opt = looks_like_hash @_;
46             my $self = $class->SUPER::_new(
47             %opt,
48             '-api' => SVG->new(
49             'width' => $opt{'-drawer'}->get_width,
50 2         13 'height' => $opt{'-drawer'}->get_height
51             )
52             );
53 2         11 $self->_api->tag( 'style', type => 'text/css' )
54             ->CDATA( "\n\tpolyline { fill: none; stroke: black; stroke-width: 1 }\n"
55             . "\tpath { fill: none; stroke: black; stroke-width: 1 }\n"
56             . "\tline { fill: none; stroke: black; stroke-width: 1 }\n"
57             . "\tcircle.node_circle {}\n"
58             . "\tcircle.taxon_circle {}\n"
59             . "\ttext.node_text {}\n"
60             . "\ttext.taxon_text {}\n"
61             . "\tline.scale_bar {}\n"
62             . "\ttext.scale_label {}\n"
63             . "\tline.scale_major {}\n"
64             . "\tline.scale_minor {}\n" );
65 2         106 return bless $self, $class;
66             }
67              
68             sub _finish {
69 2     2   5 my $self = shift;
70 2         5 undef %colors;
71 2         9 return $self->_api->render;
72             }
73              
74             =begin comment
75              
76             # required:
77             # -x1 => $x1,
78             # -y1 => $y1,
79             # -x2 => $x2,
80             # -y2 => $y2,
81             # -x3 => $x3,
82             # -y3 => $y3,
83              
84             # optional:
85             # -fill => $fill,
86             # -stroke => $stroke,
87             # -width => $width,
88             # -url => $url,
89             # -api => $api,
90              
91             =end comment
92              
93             =cut
94              
95             sub _draw_triangle {
96 0     0   0 my $self = shift;
97 0         0 my %args = @_;
98 0         0 my @coord = qw(-x1 -y1 -x2 -y2 -x3 -y3);
99 0         0 my ( $x1, $y1, $x2, $y2, $x3, $y3 ) = @args{@coord};
100 0         0 my @optional = qw(-fill -stroke -width -url -api);
101 0   0     0 my $fill = $args{'-fill'} || 'white';
102 0   0     0 my $stroke = $args{'-stroke'} || 'black';
103 0   0     0 my $width = $args{'-width'} || 1;
104 0   0     0 my $api = $args{'-api'} || $self->_api;
105 0 0       0 $api = $api->tag( 'a', 'xlink:href' => $args{'-url'} ) if $args{'-url'};
106 0         0 my $points = $self->_api->get_path(
107             'x' => [ int $x1, int $x2, int $x3, int $x1 ],
108             'y' => [ int $y1, int $y2, int $y3, int $y1 ],
109             '-type' => 'polygon',
110             );
111 0         0 delete @args{@coord};
112 0         0 delete @args{@optional};
113 0         0 return $api->polygon(
114             %$points,
115             'style' => {
116             'fill' => $fill,
117             'stroke' => $stroke,
118             'stroke-width' => $width,
119             },
120             %args
121             );
122             }
123              
124             =begin comment
125              
126             # required:
127             # -x => $x,
128             # -y => $y,
129             # -width => $width,
130             # -height => $height,
131              
132             # optional:
133             # -fill => $fill,
134             # -stroke => $stroke,
135             # -stroke_width => $stroke_width,
136             # -api => $api,
137              
138             =end comment
139              
140             =cut
141              
142             sub _draw_rectangle {
143 0     0   0 my $self = shift;
144 0         0 my %args = @_;
145 0         0 my @coord = qw(-x -y -width -height);
146 0         0 my ( $x, $y, $width, $height ) = @args{@coord};
147 0         0 my @optional = qw(-fill -stroke -stroke_width -api);
148 0   0     0 my $fill = $args{'-fill'} || 'white';
149 0   0     0 my $stroke = $args{'-stroke'} || 'black';
150 0   0     0 my $s_width = $args{'-stroke_width'} || 1;
151 0   0     0 my $api = $args{'-api'} || $self->_api;
152 0         0 delete @args{@coord};
153 0         0 delete @args{@optional};
154 0         0 return $api->rectangle(
155             'x' => $x,
156             'y' => $y,
157             'width' => $width,
158             'height' => $height,
159             'style' => {
160             'fill' => $fill,
161             'stroke' => $stroke,
162             'stroke-width' => $s_width,
163             },
164             );
165             }
166              
167             =begin comment
168              
169             # required:
170             # -x => $x,
171             # -y => $y,
172             # -text => $text,
173             #
174             # optional:
175             # -api => $api,
176             # -url => $url,
177             # -rotation => [ $rotation, $x, $y ]
178              
179             =end comment
180              
181             =cut
182              
183             sub _draw_text {
184 34     34   58 my $self = shift;
185 34         149 my %args = @_;
186 34         118 my ( $x, $y, $text ) = @args{qw(-x -y -text)};
187 34         59 my $url = $args{'-url'};
188 34   100     99 my $rotation = $args{'-rotation'} || [];
189 34   33     102 my $api = $args{'-api'} || $self->_api;
190 34         97 delete @args{qw(-x -y -text -api -url -rotation)};
191 34 50       66 if ( $url ) {
192 0         0 $api = $api->tag( 'a', 'xlink:href' => $url );
193             }
194 34         46 my @style;
195 34 50       66 if ( my $face = $args{'-font_face'} ) {
196 0         0 push @style, "font-family: ${face}";
197             }
198 34 50       59 if ( my $size = $args{'-font_size'} ) {
199 0         0 push @style, "font-size: ${size}"
200             }
201 34 50       61 if ( my $style = $args{'-font_style'} ) {
202 0         0 push @style, "font-style: ${style}";
203             }
204 34 50       65 if ( my $colour = $args{'-font_colour'} ) {
205 0         0 push @style, "fill: ${colour}";
206             }
207 34 50       64 if ( my $weight = $args{'-font_weight'} ) {
208 0         0 push @style, "font-weight: ${weight}";
209             }
210 2     2   14 no warnings 'uninitialized';
  2         3  
  2         2126  
211 34         251 return $api->tag(
212             'text',
213             'x' => $x,
214             'y' => $y,
215             'style' => join('; ',@style),
216             'transform' => "rotate(@${rotation})",
217             %args
218             )->cdata($text);
219             }
220              
221             =begin comment
222              
223             # -x => $x,
224             # -y => $y,
225             # -width => $width,
226             # -stroke => $color,
227             # -radius => $radius,
228             # -fill => $file,
229             # -api => $api,
230             # -url => $url,
231              
232             =end comment
233              
234             =cut
235              
236             sub _draw_circle {
237 0     0   0 my $self = shift;
238 0         0 my %args = @_;
239             my ( $x, $y, $radius, $width, $stroke, $fill, $api, $url ) =
240 0         0 @args{qw(-x -y -radius -width -stroke -fill -api -url)};
241 0 0       0 if ($radius) {
242 0   0     0 my $svg = $api || $self->_api;
243 0 0       0 if ($url) {
244 0         0 $svg = $svg->tag( 'a', 'xlink:href' => $url );
245             }
246 0   0     0 my %circle = (
      0        
      0        
247             'cx' => int $x,
248             'cy' => int $y,
249             'r' => int $radius,
250             'style' => {
251             'fill' => $fill || 'white',
252             'stroke' => $stroke || 'black',
253             'stroke-width' => $width || 1,
254             },
255             );
256 0         0 return $svg->tag( 'circle', %circle );
257             }
258             }
259              
260             =begin comment
261              
262             # -x1 => $x1,
263             # -x2 => $x2,
264             # -y1 => $y1,
265             # -y2 => $y2,
266             # -width => $width,
267             # -color => $color
268              
269             =end comment
270              
271             =cut
272              
273             sub _draw_curve {
274 8     8   18 my $self = shift;
275 8         43 my %args = @_;
276 8         28 my @keys = qw(-x1 -y1 -x2 -y2 -width -color);
277 8         29 my ( $x1, $y1, $x2, $y2, $width, $color ) = @args{@keys};
278 8         27 delete @args{@keys};
279 8         40 my $points = qq{M$x1,$y1 C$x1,$y2 $x2,$y2 $x2,$y2};
280 8   50     36 return $self->_api->path(
      50        
281             'd' => $points,
282             'style' => {
283             'stroke' => $color || 'black',
284             'stroke-width' => $width || 1,
285             }
286             );
287             }
288              
289             =begin comment
290              
291             # -x1 => $x1,
292             # -x2 => $x2,
293             # -y1 => $y1,
294             # -y2 => $y2,
295             # -radius => $radius
296             # -width => $width,
297             # -color => $color,
298             # -large => $large,
299             # -sweep => $sweep
300              
301             =end comment
302              
303             =cut
304              
305             sub _draw_arc {
306 0     0   0 my $self = shift;
307            
308             # process method arguments
309 0         0 my %args = @_;
310 0         0 my @keys = qw(-x1 -y1 -x2 -y2 -radius -width -color -linecap);
311 0         0 my ($x1, $y1, $x2, $y2, $radius, $width, $stroke, $linecap) = @args{@keys};
312 0 0       0 my $large = defined $args{'-large'} ? $args{'-large'} : 0; # default 0
313 0 0       0 my $sweep = defined $args{'-sweep'} ? $args{'-sweep'} : 1; # default 1
314            
315             # M = "moveto", i.e. the starting coordinates
316             # A = "elliptical Arc", The size and orientation of the ellipse are
317             # defined by two radii ($radius,$radius) and an x-axis-rotation
318             # (0.000), which indicates how the ellipse as a whole is rotated
319             # relative to the current coordinate system. The center of the
320             # ellipse is calculated automatically to satisfy the constraints
321             # imposed by the other parameters. large_arc_flag (0) and
322             # sweep-flag (1) contribute to the automatic calculations and
323             # help determine how the arc is drawn
324 0   0     0 $self->_api->path(
      0        
      0        
325            
326             # https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#Arcs
327             'd' => "M $x1 $y1 A $radius $radius, 0, $large, $sweep, $x2 $y2",
328             'style' => {
329             'fill' => 'none',
330             'stroke' => $stroke || 'black',
331             'stroke-width' => $width || 1,
332             'stroke-linecap' => $linecap || 'butt',
333             },
334             );
335             }
336              
337             =begin comment
338              
339             # -x1 => $x1,
340             # -x2 => $x2,
341             # -y1 => $y1,
342             # -y2 => $y2,
343             # -width => $width,
344             # -color => $color
345              
346             =end comment
347              
348             =cut
349              
350             sub _draw_multi {
351 0     0   0 my $self = shift;
352 0         0 my %args = @_;
353 0         0 my @keys = qw(-x1 -y1 -x2 -y2 -width -color);
354 0         0 my ( $x1, $y1, $x2, $y2, $width, $color ) = @args{@keys};
355 0         0 delete @args{@keys};
356 0         0 my $points = qq{$x1,$y1 $x1,$y2 $x2,$y2};
357 0   0     0 return $self->_api->polyline(
      0        
358             'points' => $points,
359             'style' => {
360             'stroke' => $color || 'black',
361             'stroke-width' => $width || 1,
362             },
363             %args,
364             );
365             }
366              
367             =begin comment
368              
369             # -x1 => $x1,
370             # -x2 => $x2,
371             # -y1 => $y1,
372             # -y2 => $y2,
373             # -width => $width,
374             # -color => $color
375              
376             =end comment
377              
378             =cut
379              
380             sub _draw_line {
381 126     126   149 my $self = shift;
382 126         331 my %args = @_;
383 126         241 my @keys = qw( -x1 -y1 -x2 -y2 -width -color -linecap );
384 126         245 my ( $x1, $y1, $x2, $y2, $width, $color, $linecap ) = @args{@keys};
385 126         270 delete @args{@keys};
386 126   50     220 return $self->_api->line(
      50        
      50        
387             'x1' => $x1,
388             'y1' => $y1,
389             'x2' => $x2,
390             'y2' => $y2,
391             'style' => {
392             'stroke' => $color || 'black',
393             'stroke-width' => $width || 1,
394             'stroke-linecap' => $linecap || 'butt',
395             },
396             %args
397             );
398             }
399              
400             =begin comment
401              
402             Type : Internal method.
403             Title : _draw_pies
404             Usage : $svg->_draw_pies();
405             Function: Draws likelihood pies
406             Returns :
407             Args : None.
408             Comments: Code cribbed from SVG::PieGraph
409              
410             =end comment
411              
412             =cut
413              
414             sub _draw_pies {
415 2     2   4 my $self = shift;
416            
417             # normally, the colors for the likelihood pie are generated on the fly, but if
418             # set_pie_colors has been set in the superclass then we use those instead
419 2         4 my %c = %{ $self->_drawer->get_pie_colors };
  2         10  
420 2 50       9 %colors = %c if scalar keys %c;
421            
422             $self->_tree->visit_level_order(
423             sub {
424 10     10   16 my $node = shift;
425 10 50       46 if ( not $node->get_collapsed ) {
426 10         42 my $cx = int $node->get_x;
427 10         42 my $cy = int $node->get_y;
428 10         18 my $r;
429 10 100       36 if ( $node->is_internal ) {
430 4         14 $r = int $self->_drawer->get_node_radius($node);
431             }
432             else {
433 6         20 $r = int $self->_drawer->get_tip_radius($node);
434             }
435 10 50       32 if ( $r ) {
436 0 0       0 if ( my $pievalues = $node->get_generic('pie') ) {
437 0         0 my @keys = keys %{$pievalues};
  0         0  
438 0         0 my $start = -90;
439 0         0 my $total;
440 0         0 $total += $pievalues->{$_} for @keys;
441 0         0 my $pie = $self->_api->tag(
442             'g',
443             'id' => 'pie_' . $node->get_id,
444             'transform' => "translate($cx,$cy)",
445             );
446 0         0 for my $i ( 0 .. $#keys ) {
447 0 0       0 next if not $pievalues->{ $keys[$i] };
448 0         0 my $slice = $pievalues->{ $keys[$i] } / $total * 360;
449 0         0 my $color = $colors{ $keys[$i] };
450 0 0       0 if ( not $color ) {
451 0         0 my $gray = int( ( $i / $#keys ) * 256 );
452 0         0 $colors{ $keys[$i] } = "rgb($gray,$gray,$gray)";
453             }
454 0         0 my $do_arc = 0;
455 0         0 my $radians = $slice * $PI / 180;
456 0 0       0 $do_arc++ if $slice > 180;
457 0         0 my $radius = $r - 2;
458 0         0 my $ry = $radius * sin($radians);
459 0         0 my $rx = $radius * cos($radians);
460 0         0 my $g =
461             $pie->tag( 'g', 'transform' => "rotate($start)" );
462 0         0 $g->path(
463             'style' =>
464             { 'fill' => "$color", 'stroke' => 'none' },
465             'd' => "M $radius,0 A $radius,$radius 0 $do_arc,1 $rx,$ry L 0,0 z"
466             );
467 0         0 $start += $slice;
468             }
469             }
470             }
471             }
472             }
473 2         8 );
474             }
475              
476             =begin comment
477              
478             Type : Internal method.
479             Title : _draw_legend
480             Usage : $svg->_draw_legend();
481             Function: Draws likelihood pie legend
482             Returns :
483             Args : None
484              
485             =end comment
486              
487             =cut
488              
489             sub _draw_legend {
490 2     2   6 my $self = shift;
491 2 50       10 if (%colors) {
492 0           my $svg = $self->_api;
493 0           my $tree = $self->_tree;
494 0           my $draw = $self->_drawer;
495 0           my @keys = keys %colors;
496 0           my $increment =
497             ( $tree->get_tallest_tip->get_x - $tree->get_root->get_x ) /
498             scalar @keys;
499 0           my $x = $tree->get_root->get_x + 5;
500 0           foreach my $key (@keys) {
501             $svg->rectangle(
502             'x' => $x,
503             'y' => ( $draw->get_height - 90 ),
504             'width' => ( $increment - 10 ),
505             'height' => 10,
506             'id' => 'legend_' . $key,
507             'style' => {
508 0           'fill' => $colors{$key},
509             'stroke' => 'black',
510             'stroke-width' => '1',
511             },
512             );
513 0   0       $self->_draw_text(
514             '-x' => $x,
515             '-y' => ( $draw->get_height - 60 ),
516             '-text' => $key || ' ',
517             'class' => 'legend_label'
518             );
519 0           $x += $increment;
520             }
521             $self->_draw_text(
522 0           '-x' =>
523             ( $tree->get_tallest_tip->get_x + $draw->get_text_horiz_offset ),
524             '-y' => ( $draw->get_height - 80 ),
525             '-text' => 'Node value legend',
526             'class' => 'legend_text',
527             );
528             }
529             }
530              
531             =head1 SEE ALSO
532              
533             There is a mailing list at L<https://groups.google.com/forum/#!forum/bio-phylo>
534             for any user or developer questions and discussions.
535              
536             =over
537              
538             =item L<Bio::Phylo::Treedrawer>
539              
540             The svg treedrawer is called by the L<Bio::Phylo::Treedrawer> object. Look there
541             to learn how to create tree drawings.
542              
543             =item L<Bio::Phylo::Manual>
544              
545             Also see the manual: L<Bio::Phylo::Manual> and L<http://rutgervos.blogspot.com>.
546              
547             =back
548              
549             =head1 CITATION
550              
551             If you use Bio::Phylo in published research, please cite it:
552              
553             B<Rutger A Vos>, B<Jason Caravas>, B<Klaas Hartmann>, B<Mark A Jensen>
554             and B<Chase Miller>, 2011. Bio::Phylo - phyloinformatic analysis using Perl.
555             I<BMC Bioinformatics> B<12>:63.
556             L<http://dx.doi.org/10.1186/1471-2105-12-63>
557              
558             =cut
559              
560             1;