File Coverage

blib/lib/Graph/Timeline/GD.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::GD;
2              
3 3     3   23852 use strict;
  3         7  
  3         114  
4 3     3   14 use warnings;
  3         8  
  3         88  
5              
6 3     3   2727 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::GD->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 pixelsperday pixelspermonth pixelsperyear/) );
20             $data{border} = 0 unless $data{border};
21              
22             # Validate the parameters
23              
24             my $counter = 0;
25             $counter++ if $data{pixelsperday};
26             $counter++ if $data{pixelspermonth};
27             $counter++ if $data{pixelsperyear};
28              
29             if ( $counter == 0 ) {
30             die "Timeline::GD->render() one of 'pixelsperday', 'pixelspermonth' or 'pixelsperyear' must be defined";
31             }
32             elsif ( $counter > 1 ) {
33             die "Timeline::GD->render() only one of 'pixelsperday', 'pixelspermonth' or 'pixelsperyear' can be defined";
34             }
35              
36             # Get the data to render
37              
38             my @pool = $self->data();
39              
40             die "Timeline::GD->render() there is no data to render" if scalar(@pool) == 2;
41              
42             my ( $start, $end ) = $self->_get_start_and_end(@pool);
43              
44             # Work out the width of a year in pixels
45              
46             my %years;
47             my $image_width = 0;
48              
49             foreach my $year ( $start .. $end ) {
50             $years{$year}->{year} = $year;
51             $years{$year}->{days_in_year} = Date::Calc::Days_in_Year( $year, 12 );
52             if ( $data{pixelsperday} ) {
53             $years{$year}->{pixels_in_year} = $years{$year}->{days_in_year} * $data{pixelsperday};
54             }
55             elsif ( $data{pixelspermonth} ) {
56             $years{$year}->{pixels_in_year} = 12 * $data{pixelspermonth};
57             }
58             else {
59             $years{$year}->{pixels_in_year} = $data{pixelsperyear};
60             }
61              
62             $image_width += $years{$year}->{pixels_in_year};
63              
64             $self->render_year( $years{$year} );
65             }
66              
67             # Now we should build up the streams for the points and intervals
68              
69             my %intervals;
70             my $sequence = 1;
71              
72             foreach my $record (@pool) {
73             if ( $record->{type} eq 'interval' ) {
74             my $done = 0;
75              
76             my $group = $record->{group};
77              
78             $record->{width} = $self->_calculate_width( $record, 'start_start', 'end_end', %years );
79             $record->{width_pre} = $self->_calculate_width( $record, 'start_start', 'start_end', %years );
80             $record->{width_post} = $self->_calculate_width( $record, 'end_start', 'end_end', %years );
81             $self->render_interval($record);
82              
83             foreach my $stream ( @{ $intervals{$group} } ) {
84             if ( $stream->[-1]->{end_end} lt $record->{start_start} ) {
85             push( @{$stream}, $record );
86             $done = 1;
87             last;
88             }
89             }
90              
91             push( @{ $intervals{$group} }, [$record] ) unless $done;
92             }
93             else {
94             $record->{sequence} = $sequence++;
95             $self->render_point($record);
96             push( @{ $intervals{'--points--'}[0] }, $record );
97             }
98             }
99              
100             # Work out the full height of the image
101              
102             my $image_height = 0;
103              
104             # First the years
105              
106             my $max = 0;
107             foreach my $year ( $start .. $end ) {
108             die "Timeline::GD->render() key 'height' is not defined from render_year()" unless $years{$year}->{height};
109             $max = $years{$year}->{height} if $years{$year}->{height} > $max;
110             }
111             $image_height += $max;
112              
113             # Then the intervals
114              
115             foreach my $group ( keys %intervals ) {
116             foreach my $stream ( @{ $intervals{$group} } ) {
117             $max = 0;
118             foreach my $entry ( @{$stream} ) {
119             die "Timeline::GD->render() key 'height' is not defined from render_" . $entry->{type} . "()" unless $entry->{height};
120             $max = $entry->{height} if $entry->{height} > $max;
121             }
122             $image_height += $max;
123             }
124             }
125              
126             my $im = GD::Image->new( $image_width + ( 2 * $data{border} ), $image_height + ( 2 * $data{border} ) );
127             my $white = $im->colorAllocate( 255, 255, 255 );
128              
129             my $ypointer = $data{border};
130              
131             # Render the big image, points first
132              
133             my $xpointer = $data{border};
134              
135             $max = 0;
136              
137             foreach my $entry ( @{ $intervals{'--points--'}[0] } ) {
138             $max = $entry->{height};
139             $xpointer = $data{border} + $self->_calc_start_x( $start, $entry->{start}, %years );
140             $im->copy( $entry->{data}, $xpointer, $ypointer, 0, 0, $entry->{width}, $entry->{height} );
141             }
142              
143             $ypointer += $max;
144              
145             # Render the big image, years next
146              
147             $xpointer = $data{border};
148              
149             foreach my $year ( $start .. $end ) {
150             $im->copy( $years{$year}->{data}, $xpointer, $ypointer, 0, 0, $years{$year}->{pixels_in_year}, $years{$year}->{height} );
151             $xpointer += $years{$year}->{pixels_in_year};
152             }
153              
154             $ypointer += $years{$start}->{height};
155              
156             # Render the big image, intervals last
157              
158             foreach my $group ( sort keys %intervals ) {
159             if ( $group ne '--points--' ) {
160             foreach my $stream ( @{ $intervals{$group} } ) {
161             $max = 0;
162             foreach my $entry ( @{$stream} ) {
163             $max = $entry->{height} if $entry->{height} > $max;
164             $xpointer = $data{border} + $self->_calc_start_x( $start, $entry->{start_start}, %years );
165             $im->copy( $entry->{data}, $xpointer, $ypointer, 0, 0, $entry->{width}, $entry->{height} );
166             }
167             $ypointer += $max;
168             }
169             }
170             }
171              
172             # Return the data
173              
174             return $im->png;
175             }
176              
177             sub render_year {
178             my ( $self, $year ) = @_;
179              
180             # height of a year
181              
182             $year->{height} = 15;
183              
184             # Create a year line
185              
186             my $im = GD::Image->new( $year->{pixels_in_year}, $year->{height} );
187             my $base;
188              
189             if ( $year->{year} % 2 == 0 ) {
190             $base = $im->colorAllocate( 255, 0, 0 );
191             }
192             else {
193             $base = $im->colorAllocate( 0, 255, 0 );
194             }
195              
196             my $ink = $im->colorAllocate( 255, 255, 255 );
197              
198             my $wrapbox = GD::Text::Wrap->new(
199             $im,
200             line_space => 4,
201             color => $ink,
202             text => $year->{year},
203             align => 'center',
204             );
205              
206             $wrapbox->set_font(gdSmallFont);
207              
208             $wrapbox->draw( 0, 0 );
209              
210             $year->{data} = $im;
211             }
212              
213             sub render_interval {
214             my ( $self, $record ) = @_;
215              
216             # height of a year
217              
218             my $height = 30;
219              
220             # Create a year line
221              
222             my $im = GD::Image->new( $record->{width}, $height );
223             my $base = $im->colorAllocate( 127, 127, 127 );
224             my $ink = $im->colorAllocate( 255, 255, 255 );
225             my $edge = $im->colorAllocate( 180, 180, 180 );
226              
227             if ( $record->{width_pre} ) {
228             $im->filledRectangle( 0, 0, $record->{width_pre} - 1, $height, $edge );
229             }
230              
231             if ( $record->{width_post} ) {
232             $im->filledRectangle( $record->{width} - $record->{width_post}, 0, $record->{width}, $height, $edge );
233             }
234              
235             my $wrapbox = GD::Text::Wrap->new(
236             $im,
237             line_space => 4,
238             color => $ink,
239             text => $record->{label},
240             align => 'center',
241             );
242              
243             $wrapbox->set_font(gdSmallFont);
244              
245             $wrapbox->draw( 0, 0 );
246              
247             $record->{data} = $im;
248             $record->{height} = $height;
249             }
250              
251             sub render_point {
252             my ( $self, $record ) = @_;
253              
254             # height and width of a point
255              
256             my $height = 30;
257             my $width = 100;
258              
259             my $im = GD::Image->new( $width, $height );
260             my $base = $im->colorAllocate( 255, 255, 255 );
261             my $ink = $im->colorAllocate( 0, 0, 0 );
262              
263             $im->transparent($base);
264              
265             my $wrapbox = GD::Text::Wrap->new(
266             $im,
267             width => ( $width - 2 ),
268             height => ( $height / 2 ),
269             line_space => 4,
270             color => $ink,
271             text => $record->{label},
272             align => 'left',
273             );
274              
275             $wrapbox->set_font(gdSmallFont);
276              
277             if ( $record->{sequence} % 2 == 1 ) {
278             $wrapbox->draw( 2, 0 );
279             $im->line( 0, 0, 0, $height, $ink );
280             }
281             else {
282             $wrapbox->draw( 2, ( $height / 2 ) );
283             $im->line( 0, ( $height / 2 ), 0, $height, $ink );
284             }
285              
286             $record->{data} = $im;
287             $record->{height} = $height;
288             $record->{width} = $width;
289             }
290              
291             sub _calculate_width {
292             my ( $self, $record, $start, $end, %years ) = @_;
293              
294             return 0 if $record->{$start} eq $record->{$end};
295              
296             my ( $first_year, $first_month, $first_day ) = split( '[\/-]', ( split( 'T', $record->{$start} ) )[0] );
297             my ( $last_year, $last_month, $last_day ) = split( '[\/-]', ( split( 'T', $record->{$end} ) )[0] );
298              
299             # Calculate pixel width
300              
301             my $width = 0;
302              
303             if ( $first_year eq $last_year ) {
304             $width += ( $years{$first_year}->{pixels_in_year} / $years{$first_year}->{days_in_year} ) * ( Date::Calc::Delta_Days( $first_year, $first_month, $first_day, $last_year, $last_month, $last_day ) + 1 );
305             }
306             else {
307             foreach my $year ( $first_year .. $last_year ) {
308             if ( $year == $first_year ) {
309             $width += ( $years{$year}->{pixels_in_year} / $years{$year}->{days_in_year} ) * ( Date::Calc::Delta_Days( $first_year, $first_month, $first_day, $first_year, '12', '31' ) + 1 );
310             }
311             elsif ( $year == $last_year ) {
312             $width += ( $years{$year}->{pixels_in_year} / $years{$year}->{days_in_year} ) * ( Date::Calc::Delta_Days( $last_year, 1, 1, $last_year, $last_month, $last_day ) + 1 );
313             }
314             else {
315             $width += $years{$year}->{pixels_in_year};
316             }
317             }
318             }
319              
320             return int($width);
321             }
322              
323             sub _calc_start_x {
324             my ( $self, $start_graph, $start_interval, %years ) = @_;
325              
326             my ( $first_year, $first_month, $first_day ) = split( '[\/-]', ( split( 'T', $start_graph ) )[0] );
327             my ( $last_year, $last_month, $last_day ) = split( '[\/-]', ( split( 'T', $start_interval ) )[0] );
328              
329             my $x = 0;
330              
331             foreach my $year ( $first_year .. $last_year ) {
332             if ( $year != $last_year ) {
333             $x += $years{$year}->{pixels_in_year};
334             }
335             else {
336             $x += ( $years{$year}->{pixels_in_year} / $years{$year}->{days_in_year} ) * ( Date::Calc::Delta_Days( $last_year, 1, 1, $last_year, $last_month, $last_day ) + 1 );
337             }
338             }
339              
340             return $x;
341             }
342              
343             sub _get_start_and_end {
344             my ( $self, @pool ) = @_;
345              
346             my $start = $pool[0]->{start};
347             my $end = $pool[0]->{end};
348              
349             foreach my $record (@pool) {
350             $end = $record->{end} if $record->{end} gt $end;
351             }
352              
353             $start = ( split( '[\/-]', $start ) )[0];
354             $end = ( split( '[\/-]', $end ) )[0];
355              
356             return $start, $end;
357             }
358              
359             1;
360              
361             =head1 NAME
362              
363             Graph::Timeline::GD - Render timeline data with GD
364              
365             =head1 VERSION
366              
367             This document refers to verion 1.5 of Graph::Timeline::GD, September 29, 2009
368              
369             =head1 SYNOPSIS
370              
371             This subclass produces the GD object of the timeline. The user has to subclass from this class if they want
372             a GD rendering of the timeline data. By overriding the render_year( ), render_point( ) and render_interval( )
373             methods the user can supply a less garish and more pleasing display.
374              
375             use Graph::Timeline::GD;
376              
377             my $x = Graph::Timeline::GD->new();
378              
379             while ( my $line = <> ) {
380             chomp($line);
381              
382             my ( $label, $start, $end, $group ) = split ( ',', $line );
383             if($end) {
384             $x->add_interval( label => $label, start => $start, end => $end, group => $group );
385             }
386             else {
387             $x->add_point( label => $label, start => $start, group => $group );
388             }
389             }
390              
391             $x->window(start=>'1900/01/01', end=>'1999/12/31');
392              
393             open(FILE, '>test.png');
394             binmode(FILE);
395             print FILE $x->render( border => 2, pixelsperyear => 35 );
396             close(FILE);
397              
398             All the user needs to do is create a package that subclasses Graph::Timeline::GD
399              
400             package MyTimeLine;
401              
402             use base Graph::Timeline::GD;
403              
404             sub render_year { ... }
405              
406             sub render_interval { ... }
407              
408             sub render point { ... }
409              
410             1;
411              
412             The default methods in Graph::Timeline::GD will show you how to write your own methods and the timeline
413             script in the examples directory will show you how read in data, set up the timeline and draw various
414             graphs with it.
415              
416             =head1 DESCRIPTION
417              
418             =head2 Overview
419              
420             Only three methods need to be overridden to create your own GD image of the data.
421              
422             =over 4
423              
424             =item render_year( YEAR )
425              
426             The years that form the axis of the graph are rendered by render_year( ). A scalar pointing to the data
427             for the year to be rendered is passed to the method. All you have to do is create an image of the correct
428             size and decorate it.
429              
430             =item render_interval( RECORD )
431              
432             To render an interval this method takes the record of the interval. RECORD is a pointer to a hash that
433             contains the all the data you should require, the important ones are:
434              
435             =over 4
436              
437             =item width
438              
439             This the width of the required image.
440              
441             =item label
442              
443             The label that came from the data.
444              
445             =item group
446              
447             The group that the interval belongs to.
448              
449             =back
450              
451             Additionally the following are also defined but you may have no need for them
452              
453             =over 4
454              
455             =item end, end_start, end_end, width_post
456              
457             End is the end date as defined in the data, end_start and end_end define a subinterval that the end of the data occured in.
458             For example if the end date is 1980/12/15 then end, end_start and end_end will be the same and width_post will be 0. However
459             should the end date be an interval like 1980/12 (something during December 1980) then end_start will be 1980/12/01 and
460             end_end will be 1980/12/31. Width_post will contain the number of pixels that represent the width of the subinterval.
461              
462             =item start, start_start, start_end, width_pre
463              
464             The same subinterval messing about for the start date as for the end date (defined above).
465              
466             =back
467              
468             =item render_point( RECORD )
469              
470             Just the same as render_interval but with the addition of the sequence data, as points are rendered they
471             are numbered sequentualy from 1.
472              
473             =back
474              
475             =head2 Constructors and initialisation
476              
477             =over 4
478              
479             =item new( )
480              
481             Inherited from Graph::Timeline
482              
483             =back
484              
485             =head2 Public methods
486              
487             =over 4
488              
489             =item render( HASH )
490              
491             The method called to return the rendered image. This takes a hash of configuration options however only
492             one of the pixelsper* keys can be supplied (being as they are mutually exclusive) and border is optional.
493              
494             =over 4
495              
496             =item border
497              
498             The number of pixels to use as a border around the graph.
499              
500             =item pixelsperyear
501              
502             The number of pixels the year will be rendered in
503              
504             =item pixelspermonth
505              
506             The number of pixels to render a month in, the number of pixels a year will be this value times twelve
507              
508             =item pixelsperday
509              
510             The number of pixels to render a day in, the number of pixels in a year will be calculated from this
511              
512             =back
513              
514             =item render_year( SCALAR )
515              
516             Override this method to render a year.
517              
518             =item render_interval( SCALAR )
519              
520             Override this method to render an interval.
521              
522             =item render_point( SCALAR )
523              
524             Override this method to render a point.
525              
526             =back
527              
528             =head2 Private methods
529              
530             =over 4
531              
532             =item _calculate_width
533              
534             A method to calculate the width in pixels of an interval
535              
536             =item _calc_start_x
537              
538             A method to calculate at what offset a year, interval or point should be placed in the final image
539              
540             =item _get_start_and_end
541              
542             A method to find the first and last date
543              
544             =back
545              
546             =head1 ENVIRONMENT
547              
548             None
549              
550             =head1 DIAGNOSTICS
551              
552             =over 4
553              
554             =item Timeline->new() takes no arguments
555              
556             When the constructor is initialised it requires no arguments. This message is given if
557             some arguments were supplied.
558              
559             =item Timeline::GD->render() expected HASH as parameter
560              
561             Render expects a hash and did not get one
562              
563             =item Timeline::GD->render() one of 'pixelsperday', 'pixelspermonth' or 'pixelsperyear' must be defined
564              
565             One of the required parameters needs to be defined
566              
567             =item Timeline::GD->render() only one of 'pixelsperday', 'pixelspermonth' or 'pixelsperyear' can be defined
568              
569             Only on parameter can be defined
570              
571             =item Timeline::GD->render() key 'height' is not defined from render_year()
572              
573             The method that renders the year has not set the height key, this is required
574              
575             =item Timeline::GD->render() key 'height' is not defined from render_interval()
576              
577             The method that renders an interval has not set the height key, this is required
578              
579             =item Timeline::GD->render() there is no data to render
580              
581             None of the input data got passed through the call to window()
582              
583             =back
584              
585             =head1 BUGS
586              
587             None
588              
589             =head1 FILES
590              
591             See the timeline script in the examples directory
592              
593             =head1 SEE ALSO
594              
595             Graph::Timeline - The core timeline class
596              
597             =head1 AUTHORS
598              
599             Peter Hickman (peterhi@ntlworld.com)
600              
601             =head1 COPYRIGHT
602              
603             Copyright (c) 2003, Peter Hickman. All rights reserved.
604              
605             This module is free software. It may be used, redistributed and/or
606             modified under the same terms as Perl itself.