File Coverage

blib/lib/Graph/Timeline/DiagonalGD.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Graph::Timeline::DiagonalGD;
2              
3 1     1   982 use strict;
  1         3  
  1         43  
4 1     1   7 use warnings;
  1         3  
  1         39  
5              
6 1     1   485 use GD;
  0            
  0            
7             use GD::Text::Wrap;
8              
9             use base 'Graph::Timeline';
10              
11             our $VERSION = '1.5';
12              
13             sub render {
14             die "Timeline::DiagonalGD->render() expected HASH as parameter" unless scalar(@_) % 2 == 1;
15              
16             my ( $self, %data ) = @_;
17              
18             %data = $self->_lowercase_keys(%data);
19             $self->_valid_keys( 'render', \%data, (qw/border graph-width label-width colours/) );
20             $data{border} = 0 unless $data{border};
21              
22             # Validate the parameters
23              
24             my $counter = 0;
25             foreach my $key ( 'graph-width', 'label-width' ) {
26             $counter++ if $data{$key};
27             }
28              
29             if ( $counter != 2 ) {
30             die "Timeline::DiagonalGD->render() 'graph-width' and 'label-width' must be defined";
31             }
32              
33             # Get the data to render
34              
35             my @pool = sort { $a->{start_start} cmp $b->{start_start} } $self->data();
36              
37             die "Timeline::DiagonalGD->render() there is not enough data to render" if scalar(@pool) < 2;
38              
39             my $number_of_rows = scalar @pool;
40             my $start_graph = 0;
41             my $end_graph = 0;
42              
43             my $image_width = $data{'label-width'} + $data{'graph-width'} + ( $data{'border'} * 2 );
44             my $image_height = ( $number_of_rows * 20 ) + ( $data{'border'} * 2 ) + 40;
45              
46             my $image = GD::Image->new( $image_width, $image_height );
47              
48             my $white = $image->colorAllocate( 255, 255, 255 );
49             my $black = $image->colorAllocate( 0, 0, 0 );
50              
51             my $start_label = $pool[0]->{start_start};
52             my $end_label = '';
53             foreach my $x (@pool) {
54             my $start = $self->_calc_seconds( $pool[0]->{start_start}, $x->{start_start} );
55             my $end = $self->_calc_seconds( $pool[0]->{end_end}, $x->{end_end} );
56              
57             $end_graph = $end if $end > $end_graph;
58              
59             $x->{graph_start} = $start;
60             $x->{graph_end} = $end;
61              
62             $end_label = $x->{end_end} if $end_label lt $x->{end_end};
63             }
64              
65             ##
66             ## Setting up the title for the page
67             ##
68              
69             my $wrapbox = GD::Text::Wrap->new(
70             $image,
71             width => $data{'graph-width'},
72             height => 20,
73             color => $black,
74             text => $self->_title_line( $start_label, $end_label ),
75             align => 'center',
76             );
77              
78             $wrapbox->set_font(gdSmallFont);
79             $wrapbox->draw( $data{border} + $data{'label-width'}, $data{border} + 2 );
80              
81             $wrapbox = GD::Text::Wrap->new(
82             $image,
83             width => $data{'graph-width'},
84             height => 20,
85             color => $black,
86             text => ( split( 'T', $start_label ) )[1],
87             align => 'left',
88             );
89              
90             $wrapbox->set_font(gdSmallFont);
91             $wrapbox->draw( $data{border} + $data{'label-width'}, $data{border} + 22 );
92              
93             $wrapbox = GD::Text::Wrap->new(
94             $image,
95             width => $data{'graph-width'},
96             height => 20,
97             color => $black,
98             text => ( split( 'T', $end_label ) )[1],
99             align => 'right',
100             );
101              
102             $wrapbox->set_font(gdSmallFont);
103             $wrapbox->draw( $data{border} + $data{'label-width'}, $data{border} + 22 );
104              
105             my $pos = $data{border} + 40;
106             my $offset = $data{border} + $data{'label-width'};
107              
108             my %colours;
109              
110             ##
111             ## Rather than calculate this twice lets store the values as we go along
112             ##
113              
114             my @map_line;
115             my @map_box;
116              
117             foreach my $x (@pool) {
118             next if $x->{type} eq 'marker';
119              
120             my $element_start = int( ( $x->{graph_start} / $end_graph ) * $data{'graph-width'} ) + $offset;
121             my $element_end = int( ( $x->{graph_end} / $end_graph ) * $data{'graph-width'} ) + $offset;
122              
123             $element_end = $element_start + 1 if $element_end <= $element_start;
124              
125             ##
126             ## Set up the map elements
127             ##
128              
129             if ( $x->{url} ) {
130             push @map_line, $self->_map_line( $x->{url}, $data{border}, $pos, ( $image_width - $data{border} ), $pos + 19 );
131             push @map_box, $self->_map_box( $x->{url}, $element_start, $pos, $element_end, $pos + 19, $image_width );
132             }
133              
134             ##
135             ## Select the colour to use
136             ##
137              
138             unless ( defined( $colours{ $x->{label} } ) ) {
139             if ( defined( $data{colours}{ $x->{label} } ) ) {
140             $colours{ $x->{label} } = $image->colorAllocate( @{ $data{colours}{ $x->{label} } } );
141             }
142             else {
143             $colours{ $x->{label} } = $black;
144             }
145             }
146             my $colour = $colours{ $x->{label} };
147              
148             ##
149             ## Draw the line
150             ##
151              
152             $image->line( $offset, $pos, $element_start, $pos, $colour );
153             $image->line( $offset, $pos + 20, $element_start, $pos + 20, $colour );
154              
155             ##
156             ## Draw the box
157             ##
158              
159             $image->filledRectangle( $element_start, $pos, $element_end, $pos + 20, $colour );
160              
161             ##
162             ## Draw the label
163             ##
164              
165             my $wrapbox = GD::Text::Wrap->new(
166             $image,
167             width => $data{'label-width'},
168             height => 20,
169             color => $colour,
170             text => $x->{id},
171             align => 'left',
172             );
173              
174             $wrapbox->set_font(gdSmallFont);
175             $wrapbox->draw( $data{border}, $pos + 2 );
176              
177             $pos += 20;
178             }
179              
180             ##
181             ## Store the maps for the later call
182             ##
183              
184             $self->{map_line} = [@map_line];
185             $self->{map_box} = [@map_box];
186              
187             return $image->png;
188             }
189              
190             sub map {
191             my ( $self, $style, $name ) = @_;
192              
193             die "Timeline::DiagonalGD->map() The map requires a name" unless $name;
194              
195             my $text = "\n";
196              
197             if ( $style eq 'line' ) {
198             foreach my $line ( @{ $self->{map_line} } ) {
199             $text .= " $line\n";
200             }
201             }
202             elsif ( $style eq 'box' ) {
203             foreach my $line ( @{ $self->{map_box} } ) {
204             $text .= " $line\n";
205             }
206             }
207             else {
208             die "Timeline::DiagonalGD->map() Unknown map style, use 'line' or 'box'";
209             }
210              
211             $text .= "\n";
212              
213             return $text;
214             }
215              
216             sub _map_line {
217             my ( $self, $url, $x1, $y1, $x2, $y2 ) = @_;
218              
219             return " ";
220             }
221              
222             sub _map_box {
223             my ( $self, $url, $x1, $y1, $x2, $y2, $max_width ) = @_;
224              
225             my $new_x1 = $x1 - 5;
226             my $new_x2 = $x2 + 5;
227              
228             $new_x2 = $max_width if $new_x2 > $max_width;
229              
230             return $self->_map_line( $url, $new_x1, $y1, $new_x2, $y2 );
231             }
232              
233             sub _title_line {
234             my ( $self, $start, $end ) = @_;
235              
236             my $start_date = ( split( 'T', $start ) )[0];
237             my $end_date = ( split( 'T', $end ) )[0];
238              
239             if ( $start_date eq $end_date ) {
240             return $start_date;
241             }
242             else {
243             return "$start_date to $end_date";
244             }
245             }
246              
247             sub _calc_seconds {
248             my ( $self, $base, $date ) = @_;
249              
250             my ( $date_date, $date_time ) = split( 'T', $date );
251             my ( $base_date, $base_time ) = split( 'T', $base );
252              
253             my ( $dyear, $dmonth, $dday ) = split( '[\/-]', $date_date );
254             my ( $dhour, $dminute, $dsecond ) = split( ':', $date_time );
255             my ( $byear, $bmonth, $bday ) = split( '[\/-]', $base_date );
256             my ( $bhour, $bminute, $bsecond ) = split( ':', $base_time );
257              
258             my ( $D_y, $D_m, $D_d, $Dh, $Dm, $Ds ) = Date::Calc::Delta_YMDHMS( $byear, $bmonth, $bday, $bhour, $bminute, $bsecond, $dyear, $dmonth, $dday, $dhour, $dminute, $dsecond );
259              
260             if ( $D_y or $D_m ) {
261             die "Timeline::DiagonalGD->render() Date range spans into months or years. No can do";
262             }
263              
264             my $total = $Ds + ( 60 * $Dm ) + ( 60 * 60 * $Dh ) + ( 60 * 60 * 24 * $D_d );
265              
266             return $total;
267             }
268              
269             1;
270              
271             =head1 NAME
272              
273             Graph::Timeline::DiagonalGD - Render timeline data with GD
274              
275             =head1 VERSION
276              
277             This document refers to verion 1.5 of Graph::Timeline::DiagonalGD, September 29, 2009
278              
279             =head1 SYNOPSIS
280              
281             This class is used to clear charts where earliest starting event is at the top of the page
282             and the next event to start follows it (and so on). For each event a box is drawn relative
283             to the length of the event. You get something like this:
284              
285             first event : XX
286             second event : XXXXX
287             third event : XX
288             fourth event: : XXXXXX
289              
290             Optionally a client side imagemap can be generated for the events that have a url defined.
291              
292             An example of usage follows. Note that the labels down the left hand side are based on the
293             id attribute and the colour of the event box on the label.
294              
295             #!/usr/bin/perl
296              
297             use strict;
298             use warnings;
299              
300             use Graph::Timeline::DiagonalGD;
301              
302             my $x = Graph::Timeline::DiagonalGD->new();
303              
304             while ( my $line = <> ) {
305             chomp($line);
306              
307             next if $line =~ m/^\s*$/;
308             next if $line =~ m/^\s*#/;
309            
310             my ( $id, $label, $start, $end, $url ) = split( ',', $line );
311             $x->add_interval( label => $label, start => $start, end => $end, id => $id, url => $url );
312             }
313              
314             my %render = (
315             'graph-width' => 400,
316             'label-width' => 150,
317             'border' => 10,
318             'colours' => {
319             'Ended_Successfully' => [ 128, 128, 128 ],
320             'Failed' => [ 255, 0, 0 ]
321             }
322             );
323              
324             open( FILE, '>test_diagonal1.png' );
325             binmode(FILE);
326             print FILE $x->render(%render);
327             close(FILE);
328              
329             open( FILE, '>test_diagonal1.map' );
330             print FILE $x->map( 'box', 'image1' );
331             close(FILE);
332              
333             =head1 DESCRIPTION
334              
335             Render a diagonal event graph based on the input data.
336              
337             =head2 Overview
338              
339             The render method controls the display. This is inturn controlled by the parameters
340             that are passed in to it.
341              
342             =head2 Constructors and initialisation
343              
344             =over 4
345              
346             =item new( )
347              
348             Inherited from Graph::Timeline
349              
350             =back
351              
352             =head2 Public methods
353              
354             =over 4
355              
356             =item render( HASH )
357              
358             The hight of the image created will be 20 pixels per event reported plus 40 pixels for the title, plus an
359             additional 2 * border. The width of the image will be 2 * border + label-width + graph-width.
360              
361             =over 4
362              
363             =item border
364              
365             The number of pixels to use as a border around the graph. If omitted will be set to 0.
366              
367             =item label-width
368              
369             The number of pixels used to display the id of the event.
370              
371             =item graph-width
372              
373             The number of pixels within which the events will be drawn.
374              
375             =item colours
376              
377             When an event is rendered the label is used as a key to this hash to return a list of values to use
378             for the colour for that event:
379              
380             'colours' => {
381             'Ended_Successfully' => [ 128, 128, 128 ],
382             'Failed' => [ 255, 0, 0 ]
383             }
384              
385             The values are for the RGB triplet, if no value is supplied for a label the event will be draw
386             in black.
387              
388             =back
389              
390             =item map( style, name )
391              
392             Produce a client side imagemap for the data that has a url defined.
393              
394             =over 4
395              
396             =item style
397              
398             There are two styles available. 'line' or 'box'. For line the clickable area is the whole line
399             that the event occurs on. For box the clickable area is the box drawn for the event plus 5 pixels
400             to the left and right.
401              
402             =item name
403              
404             This is the name that will be used for the imagemap
405              
406             =back
407              
408             =back
409              
410             =head2 Private methods
411              
412             =over 4
413              
414             =item _calc_seconds
415              
416             A method to calculate the duration of an event in seconds.
417              
418             =item _title_line
419              
420             The the events are within one day return just a day to use as the title, if they span more than
421             one day return a string 'start TO end' to display as the title.
422              
423             =back
424              
425             =head1 ENVIRONMENT
426              
427             None
428              
429             =head1 DIAGNOSTICS
430              
431             =over 4
432              
433             =item Timeline->new() takes no arguments
434              
435             When the constructor is initialised it requires no arguments. This message is given if
436             some arguments were supplied.
437              
438             =item Timeline::DiagonalGD->render() expected HASH as parameter
439              
440             Render expects a hash and did not get one
441              
442             =item Timeline::DiagonalGD->render() 'graph-width' and 'label-width' must be defined
443              
444             Both of these parameters must be defined.
445              
446             =item Timeline::DiagonalGD->render() there is not enough data to render
447              
448             None of the input data got passed through the call to window()
449              
450             =item Timeline::DiagonalGD->render() Date range spans into months or years. No can do
451              
452             It is assumed that the data will span, at best, a few days. More than that and we can't
453             realy draw this graph.
454              
455             =item Timeline::DiagonalGD->map() Unknown map style, use 'line' or 'box'
456              
457             Maps come in type styles, 'line' or 'box'. You tried to use something else
458              
459             =item Timeline::DiagonalGD->map() The map requires a name
460              
461             You must supply a name for the map
462              
463             =back
464              
465             =head1 BUGS
466              
467             None
468              
469             =head1 FILES
470              
471             See the diagonal script in the examples directory
472              
473             =head1 SEE ALSO
474              
475             Graph::Timeline - The core timeline class
476              
477             =head1 AUTHORS
478              
479             Peter Hickman (peterhi@ntlworld.com)
480              
481             =head1 COPYRIGHT
482              
483             Copyright (c) 2007, Peter Hickman. All rights reserved.
484              
485             This module is free software. It may be used, redistributed and/or
486             modified under the same terms as Perl itself.