File Coverage

blib/lib/GD/Graph/radar.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package GD::Graph::radar;
2             # ABSTRACT: Make radial bar charts
3              
4 2     2   52090 use strict;
  2         6  
  2         81  
5 2     2   10 use warnings;
  2         4  
  2         105  
6              
7             our $VERSION = '0.1101';
8 2     2   12 use base qw(GD::Graph);
  2         14  
  2         1002  
9 2     2   693 use GD;
  0            
  0            
10             use GD::Graph::colour qw(:colours :lists);
11             use GD::Graph::utils qw(:all);
12             use GD::Text::Align;
13              
14             use constant PI => 4 * atan2(1, 1);
15             use constant ANGLE_OFFSET => 90;
16              
17             =head1 NAME
18              
19             GD::Graph::radar - Make radial bar charts
20              
21             =head1 SYNOPSIS
22              
23             use GD::Graph::radar;
24             my $radar = GD::Graph::radar->new(400, 400);
25             my $image = $radar->plot([
26             [qw( a b c d e f g h i )],
27             [qw( 3.2 9 4.4 3.9 4.1 4.3 7 6.1 5 )]
28             ]);
29             print $image->png; #or ->gif, or ->jpeg, or...
30              
31             =head1 DESCRIPTION
32              
33             This module is based on C with the exception of
34             changes to the default settings, the C method, and
35             elimination of the pie specific code.
36              
37             =head1 PUBLIC METHODS
38              
39             =head2 new()
40              
41             $img = GD::Graph::radar->new();
42              
43             Create a new C object.
44              
45             =cut
46              
47             my %Defaults = (
48             # The angle at which to start the first data set 0 is pointing straight down
49             start_angle => 0,
50              
51             # and some public attributes without defaults
52             label => undef,
53              
54             # Absolute graphs always start at zero and cannot have negative
55             # values. non-absolute graphs start at minimum data set value
56             absolute => 1,
57              
58             # number of scale markers to draw
59             nmarkers => 6,
60              
61             # if set, draw a polygon connecting the apices of each line
62             polygon => 1,
63              
64             # if defined, fill the polygon with the colour specified as
65             # a hex rgb string. Note that if one of the data
66             # elements is zero, then the polygon will not fill as we fill
67             # from the origin
68             poly_fill => '#e4e4e4',
69             );
70              
71             sub _has_default {
72             my $self = shift;
73             my $attr = shift || return;
74             exists $Defaults{$attr} || $self->SUPER::_has_default($attr);
75             }
76              
77             =head2 plot()
78              
79             Create the image.
80              
81             =cut
82              
83             sub plot {
84             my $self = shift;
85             my $data = shift;
86              
87             $self->check_data($data) or return;
88             $self->init_graph() or return;
89             $self->setup_text() or return;
90             $self->setup_coords() or return;
91             $self->draw_text() or return;
92             $self->draw_data() or return;
93              
94             return $self->{graph};
95             }
96              
97             =head1 PRIVATE METHODS
98              
99             =head2 initialise()
100              
101             Setup defaults.
102              
103             =cut
104              
105             sub initialise {
106             my $self = shift;
107              
108             $self->SUPER::initialise();
109              
110             while (my ($key, $val) = each %Defaults) {
111             $self->{$key} = $val;
112             }
113              
114             $self->set_value_font(gdTinyFont);
115             $self->set_label_font(gdSmallFont);
116             }
117              
118             =head2 set_label_font()
119              
120             Set the font-name of the label.
121              
122             =cut
123              
124             sub set_label_font {
125             my $self = shift;
126             $self->_set_font('gdta_label', @_) or return;
127             $self->{gdta_label}->set_align('bottom', 'center');
128             }
129              
130             =head2 set_value_font()
131              
132             Set the font-name of the value.
133              
134             =cut
135              
136             sub set_value_font {
137             my $self = shift;
138             $self->_set_font('gdta_value', @_) or return;
139             $self->{gdta_value}->set_align('center', 'center');
140             }
141              
142             =head2 setup_coords()
143              
144             Setup the coordinate system and colours. Calculate the relative axis
145             coordinates with respect to the canvas size.
146              
147             =cut
148              
149             sub setup_coords() {
150             # Inherit defaults() from GD::Graph
151             # Inherit checkdata from GD::Graph
152             my $self = shift;
153              
154             # Make sure we're not reserving space we don't need.
155             my $tfh = $self->{title} ? $self->{gdta_title}->get('height') : 0;
156             my $lfh = $self->{label} ? $self->{gdta_label}->get('height') : 0;
157              
158             # Calculate the bounding box for the graph, and
159             # some width, height, and centre parameters
160             $self->{bottom} =
161             $self->{height} - $self->{b_margin} -
162             ( $lfh ? $lfh + $self->{text_space} : 0 );
163             $self->{top} =
164             $self->{t_margin} + ( $tfh ? $tfh + $self->{text_space} : 0 );
165              
166             return $self->_set_error('Vertical size too small')
167             if $self->{bottom} - $self->{top} <= 0;
168              
169             $self->{left} = $self->{l_margin};
170             $self->{right} = $self->{width} - $self->{r_margin};
171              
172             return $self->_set_error('Horizontal size too small')
173             if $self->{right} - $self->{left} <= 0;
174              
175             $self->{w} = $self->{right} - $self->{left};
176             $self->{h} = $self->{bottom} - $self->{top};
177              
178             $self->{xc} = ($self->{right} + $self->{left}) / 2;
179             $self->{yc} = ($self->{bottom} + $self->{top}) / 2;
180              
181             return $self;
182             }
183              
184             =head2 setup_text()
185              
186             Setup the parameters for the text elements.
187              
188             =cut
189              
190             sub setup_text {
191             # Inherit open_graph from GD::Graph
192             my $self = shift;
193              
194             if ($self->{title}) {
195             #print "'$s->{title}' at ($s->{xc},$s->{t_margin})\n";
196             $self->{gdta_title}->set(colour => $self->{tci});
197             $self->{gdta_title}->set_text($self->{title});
198             }
199              
200             if ($self->{label}) {
201             $self->{gdta_label}->set(colour => $self->{lci});
202             $self->{gdta_label}->set_text($self->{label});
203             }
204              
205             $self->{gdta_value}->set(colour => $self->{alci});
206              
207             return $self;
208             }
209              
210             =head2 draw_text()
211              
212             Put the text on the canvas.
213              
214             =cut
215              
216             sub draw_text {
217             my $self = shift;
218              
219             $self->{gdta_title}->draw($self->{xc}, $self->{t_margin})
220             if $self->{title};
221             $self->{gdta_label}->draw($self->{xc}, $self->{height} - $self->{b_margin})
222             if $self->{label};
223            
224             return $self;
225             }
226              
227             =head2 draw_data()
228              
229             Draw the data lines and the polygon.
230              
231             =cut
232              
233             sub draw_data {
234             my $self = shift;
235              
236             my $max_val = 0;
237             my @values = $self->{_data}->y_values(1); # for now, only one
238             my $min_val = $values[0];
239             my $scale = 1;
240              
241             for (@values) {
242             if ($_ > $max_val) { $max_val = $_; }
243             if ($_ < $min_val) { $min_val = $_; }
244             }
245              
246             $scale = $self->{absolute}
247             ? ($self->{w} / 2) / $max_val
248             : ($self->{w} / 2) / ($max_val - $min_val);
249            
250             my $ac = $self->{acci}; # Accent colour
251             my $pb = $self->{start_angle};
252              
253             my $poly = new GD::Polygon;
254             my @vertices = ();
255              
256             for (my $i = 0; $i < @values; $i++) {
257             # Set the angles of each arm
258             # Angle 0 faces down, positive angles are clockwise
259             # from there.
260             # ---
261             # / \
262             # | |
263             # \ | /
264             # ---
265             # 0
266             # $pa/$pb include the start_angle (so if start_angle
267             # is 90, there will be no pa/pb < 90.
268             my $pa = $pb;
269             $pb += my $slice_angle = 360 / @values;
270              
271             # Calculate the end points of the lines at the boundaries of
272             # the pie slice
273             my $radius = $values[$i] * $scale;
274              
275             $radius = 0 if $radius < 0 && $self->{absolute};
276              
277             my ($xe, $ye) = cartesian(
278             $radius,
279             $pa,
280             $self->{xc}, $self->{yc},
281             $self->{h} / $self->{w}
282             );
283              
284             $poly->addPt($xe, $ye) if $self->{polygon};
285              
286             push @vertices, [$xe, $ye];
287             }
288              
289             # draw the apex polygon
290             $self->{graph}->polygon($poly, $ac);
291              
292             if (defined $self->{poly_fill}) {
293             my ($r, $g, $b) = GD::Graph::colour::hex2rgb($self->{poly_fill});
294              
295             my $fc = $self->{graph}->colorAllocate($r, $g, $b);
296              
297             $self->{graph}->fill($self->{xc}, $self->{yc}, $fc);
298             }
299              
300             # draw markers
301             my $mark_incr = 1;
302             $mark_incr = $self->{absolute}
303             ? int ($max_val / $self->{nmarkers})
304             : int (($max_val - $min_val) / $self->{nmarkers});
305              
306             for (1 .. $self->{nmarkers}) {
307             my $width = 2 * $_ * $mark_incr * $scale;
308              
309             $self->{graph}->arc(
310             $self->{xc}, $self->{yc},
311             $width,
312             $width * ($self->{h} / $self->{w}),
313             0, 360,
314             $ac,
315             );
316             }
317              
318             # draw radar value bars
319             my $dc = $self->{graph}->colorAllocate(0, 0, 0);
320              
321             for (@vertices) {
322             $self->{graph}->line(
323             $self->{xc}, $self->{yc},
324             $_->[0], $_->[1],
325             $dc
326             );
327             }
328              
329             # draw labels
330             $pb = $self->{start_angle};
331              
332             for (my $i = 0; $i < @values; $i++) {
333             next unless $values[$i];
334              
335             my $pa = $pb;
336             $pb += my $slice_angle = 360 / @values;
337              
338             next if $self->{suppress_angle} && $slice_angle <= $self->{suppress_angle};
339              
340             my ($xe, $ye) = cartesian(
341             3 * $self->{w} / 8, $pa,
342             $self->{xc}, $self->{yc},
343             $self->{h} / $self->{w}
344             );
345              
346             $self->put_slice_label($xe, $ye, $self->{_data}->get_x($i));
347             }
348            
349             return $self;
350             }
351              
352             =head2 put_slice_label()
353              
354             Put the slice label on the pie.
355              
356             =cut
357              
358             sub put_slice_label {
359             my $self = shift;
360             my ($x, $y, $label) = @_;
361              
362             return unless defined $label;
363              
364             $self->{gdta_value}->set_text($label);
365             $self->{gdta_value}->draw($x, $y);
366             }
367              
368             =head2 cartesian()
369              
370             Return x, y coordinates, radius, angle, center x and y, and a scaling
371             factor (height/width).
372              
373             =cut
374              
375             sub cartesian {
376             # $ANGLE_OFFSET is used to define where 0 is meant to be
377             my ($r, $phi, $xi, $yi, $cr) = @_;
378             return (
379             $xi + $r * cos (PI * ($phi + ANGLE_OFFSET) / 180),
380             $yi + $cr * $r * sin (PI * ($phi + ANGLE_OFFSET) / 180)
381             )
382             }
383              
384             1;
385             __END__