File Coverage

blib/lib/SVG/Timeline.pm
Criterion Covered Total %
statement 71 71 100.0
branch 5 8 62.5
condition 2 6 33.3
subroutine 20 20 100.0
pod 9 9 100.0
total 107 114 93.8


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             SVG::Timeline - Create SVG timeline charts
4              
5             =head1 SYNOPSIS
6              
7             use SVG::Timeline;
8              
9             my $tl = SVG::Timeline->new;
10              
11             $tl->add_event({
12             start => 1914,
13             end => 1918,
14             text => 'World War I',
15             });
16              
17             $tl->add_event({
18             start => '1939-09-01', # 1 Sep 1939
19             end => '1945-05-08', # 8 May 1945
20             text => 'World War II',
21             });
22              
23             print $tl->draw;
24              
25             =head1 DESCRIPTION
26              
27             This module allows you to easily create SVG documents that represent timelines.
28              
29             An SVG timeline is a diagram with a list of years across the top and a
30             number of bars below. Each bar represents a period of time.
31              
32             =head1 METHODS
33              
34             =head2 new(\%options)
35              
36             Creates and returns a new SVG::Timeline object.
37              
38             Takes an optional hash reference containing configuration options. You
39             probably don't need any of these, but the following options are supported:
40              
41             =over 4
42              
43             =cut
44              
45              
46             use 5.014;
47 2     2   205300  
  2         17  
48             our $VERSION = '0.1.3';
49              
50             use Moose;
51 2     2   1135 use Moose::Util::TypeConstraints;
  2         937681  
  2         13  
52 2     2   15390 use SVG;
  2         6  
  2         21  
53 2     2   6079 use List::Util qw[min max];
  2         33110  
  2         12  
54 2     2   2195 use Carp;
  2         4  
  2         141  
55 2     2   13  
  2         4  
  2         117  
56             use SVG::Timeline::Event;
57 2     2   976  
  2         6  
  2         2708  
58             subtype 'ArrayOfEvents', as 'ArrayRef[SVG::Timeline::Event]';
59              
60             coerce 'ArrayOfEvents',
61             from 'HashRef',
62             via { [ SVG::Timeline::Event->new($_) ] },
63             from 'ArrayRef[HashRef]',
64             via { [ map { SVG::Timeline::Event->new($_) } @$_ ] };
65              
66             =item * events - a reference to an array containing events. Events are hash
67             references. See L<add_event> below for the format of events.
68              
69             =cut
70              
71             has events => (
72             traits => ['Array'],
73             isa => 'ArrayOfEvents',
74             is => 'rw',
75             coerce => 1,
76             default => sub { [] },
77             handles => {
78             all_events => 'elements',
79             add_event => 'push',
80             count_events => 'count',
81             has_events => 'count',
82             },
83             );
84              
85             around 'add_event' => sub {
86             my $orig = shift;
87             my $self = shift;
88              
89             $self->_clear_viewbox;
90             $self->_clear_svg;
91              
92             my $index = $self->count_events + 1;
93             $_[0]->{index} = $index;
94              
95             $self->$orig(@_);
96             };
97              
98             =item * width - the width of the output in any format used by SVG. The default
99             is 100%.
100              
101             =cut
102              
103             has width => (
104             is => 'ro',
105             isa => 'Str',
106             default => '100%',
107             );
108              
109             =item * height - the height of the output in any format used by SVG. The
110             default is 100%.
111              
112             =cut
113              
114             has height => (
115             is => 'ro',
116             isa => 'Str',
117             default => '100%',
118             );
119              
120             =item * aspect_ratio - the default is 16/9.
121              
122             =cut
123              
124             has aspect_ratio => (
125             is => 'ro',
126             isa => 'Num',
127             default => 16/9,
128             );
129              
130             =item * viewport - a viewport definition (which is a space separated list of
131             four integers. Unless you know what you're doing, it's probably best to leave
132             the class to work this out for you.
133              
134             =cut
135              
136             has viewbox => (
137             is => 'ro',
138             isa => 'Str',
139             lazy_build => 1,
140             clearer => '_clear_viewbox',
141             );
142              
143             my $self = shift;
144             return join ' ',
145 2     2   4 $self->min_year * $self->units_per_year,
146 2         7 0,
147             $self->years * $self->units_per_year,
148             ($self->bar_height * $self->events_in_timeline) + $self->bar_height
149             + (($self->count_events - 1) * $self->bar_height * $self->bar_spacing);
150             }
151              
152             =item * svg - an instance of the SVG class that is used to generate the final
153             SVG output. Unless you're using a subclass of this class for some reason,
154             there is no reason to set this manually.
155              
156             =cut
157              
158             has svg => (
159             is => 'ro',
160             isa => 'SVG',
161             lazy_build => 1,
162             clearer => '_clear_svg',
163             handles => [qw[xmlify line text rect cdata]],
164             );
165              
166             my $self = shift;
167              
168             $_->{end} //= (localtime)[5] + 1900 foreach $self->all_events;
169 2     2   5  
170             return SVG->new(
171 2   33     124 width => $self->width,
172             height => $self->height,
173 2         55 viewBox => $self->viewbox,
174             );
175             }
176              
177             =item * default_colour - the colour that is used to fill the timeline
178             blocks. This should be defined in the RGB format used by SVG. For example,
179             red would be 'RGB(255,0,0)'.
180              
181             =cut
182              
183             has default_colour => (
184             is => 'ro',
185             isa => 'Str',
186             lazy_build => 1,
187             );
188              
189             return 'rgb(255,127,127)';
190             }
191              
192             =item * years_per_grid - the number of years between vertical grid lines
193 1     1   37 in the output. The default of 10 should be fine unless your timeline covers
194             a really long timespan.
195              
196             =cut
197              
198             # The number of years between vertical grid lines
199             has years_per_grid => (
200             is => 'ro',
201             isa => 'Int',
202             default => 10, # One decade by default
203             );
204              
205             =item * bar_height - the height of an individual timeline bar.
206              
207             =cut
208              
209             has bar_height => (
210             is => 'ro',
211             isa => 'Int',
212             default => 50,
213             );
214              
215             =item * bar_spacing - the height if the vertical space between bars (expresssed
216             as a decimal fraction of the bar height).
217              
218             =cut
219              
220             has bar_spacing => (
221             is => 'ro',
222             isa => 'Num',
223             default => 0.25,
224             );
225              
226             =item * decade_line_colour - the colour of the grid lines.
227              
228             =cut
229              
230             has decade_line_colour => (
231             is => 'ro',
232             isa => 'Str',
233             default => 'rgb(127,127,127)',
234             );
235              
236             =item * bar_outline_colour - the colour that is used for the outline of the
237             timeline bars.
238              
239             =cut
240              
241             has bar_outline_colour => (
242             is => 'ro',
243             isa => 'Str',
244             default => 'rgb(0,0,0)',
245             );
246              
247             =back
248              
249             =head2 events_in_timeline
250              
251             The number of events that we need to make space for in the timeline. This
252             is generally just the number of events that we have added to the timeline, but
253             this method is here in case subclasses want t odo something different.
254              
255             =cut
256              
257             return $_[0]->count_events;
258             }
259              
260             =head2 add_event
261              
262 34     34 1 1099 Takes a hash reference with event details and adds an L<SVG::Timeline::Event>
263             to the timeline. The following details are supported:
264              
265             =over 4
266              
267             =item * text - the name of the event that is displayed on the bar. This is required.
268              
269             =item * start - the start year of the event. It is a string of format C<YYYY-MM-DD>.
270             For example, C<2017-07-02> is the 2nd of July 2017. The month and day can be omitted
271             (in which case they are replaced with '01').
272              
273             =item * end - the end year of the event, requirements are the same as that of C<start>.
274            
275             =item * colour - the colour that is used to fill the timeline block. This should be
276             defined in the RGB format used by SVG. For example, red would be 'RGB(255,0,0)'.
277             This is optional. If not provided, the C<default_color> is used.
278              
279             =back
280              
281             =head2 calculated_height
282              
283             The height of the timeline in "calculated units".
284              
285             =cut
286              
287             my $self = shift;
288              
289             # Number of events ...
290             my $calulated_height = $self->events_in_timeline;
291             # ... plus one for the header ...
292             $calulated_height++;
293 15     15 1 24 # ... multiplied by the bar height...
294             $calulated_height *= $self->bar_height;
295             # .. add spacing.
296 15         22 $calulated_height += $self->bar_height * $self->bar_spacing *
297             ($self->events_in_timeline - 1);
298 15         27  
299             return $calulated_height;
300 15         373 }
301              
302 15         356 =head2 calculated_width
303              
304             The width in "calulated units".
305 15         397  
306             =cut
307              
308             my $self = shift;
309              
310             return $self->calculated_height * $self->aspect_ratio;
311             }
312              
313             =head2 units_per_year
314              
315 11     11 1 18 The number of horizontal units that each year should take up.
316              
317 11         17 =cut
318              
319             my $self = shift;
320              
321             return $self->calculated_width / $self->years;
322             }
323              
324             =head2 draw_grid
325              
326             Method to draw the underlying grid.
327 11     11 1 20  
328             =cut
329 11         20  
330             my $self = shift;
331              
332             my $curr_year = $self->min_year;
333             my $units_per_year = $self->units_per_year;
334              
335             # Draw the grid lines
336             while ( $curr_year <= $self->max_year ) {
337             unless ( $curr_year % $self->years_per_grid ) {
338             $self->line(
339 1     1 1 3 x1 => $curr_year * $units_per_year,
340             y1 => 0,
341 1         2 x2 => $curr_year * $units_per_year,
342 1         3 y2 => $self->calculated_height,
343             stroke => $self->decade_line_colour,
344             stroke_width => 1
345 1         4 );
346 36 100       943 $self->text(
347 4         14 x => ($curr_year + 1) * $units_per_year,
348             y => 20,
349             'font-size' => $self->bar_height / 2
350             )->cdata($curr_year);
351             }
352             $curr_year++;
353             }
354              
355 4         435 $self->rect(
356             x => $self->min_year * $units_per_year,
357             y => 0,
358             width => $self->years * $units_per_year,
359             height => ($self->bar_height * ($self->events_in_timeline + 1))
360             + ($self->bar_height * $self->bar_spacing
361 36         467 * ($self->events_in_timeline - 1)),
362             stroke => $self->bar_outline_colour,
363             'stroke-width' => 1,
364             fill => 'none',
365 1         14 );
366              
367             return $self;
368             }
369              
370             =head2 draw
371              
372             Method to draw the timeline.
373              
374             =cut
375              
376 1         105 my $self = shift;
377             my %args = @_;
378              
379             croak "Can't draw a timeline with no events"
380             unless $self->has_events;
381              
382             $self->draw_grid;
383              
384             my $curr_event_idx = 1;
385             foreach ($self->all_events) {
386 1     1 1 702  
387 1         6 $_->draw_on($self);
388              
389 1 50       42 $curr_event_idx++;
390             }
391              
392 1         5 return $self->xmlify;
393             }
394 1         2  
395 1         39 =head2 min_year
396              
397 3         14 Returns the minimum year from all the events in the timeline.
398              
399 3         30 =cut
400              
401             my $self = shift;
402 1         11 return unless $self->has_events;
403             my @years = map { int ($_->start) } $self->all_events;
404             return min(@years);
405             }
406              
407             =head2 max_year
408              
409             Returns the maximum year from all the events in the timeline.
410              
411             =cut
412 18     18 1 26  
413 18 50       575 my $self = shift;
414 18         581 return unless $self->has_events;
  50         1268  
415 18         250 my @years = map { int($_->end) // localtime->year } $self->all_events;
416             return max(@years);
417             }
418              
419             =head2 years
420              
421             The number of years that all the events in the timeline span.
422              
423             =cut
424              
425 51     51 1 69 my $self = shift;
426 51 50       1664 return $self->max_year - $self->min_year;
427 51   33     1725 }
  150         3782  
428 51         179  
429             no Moose;
430             __PACKAGE__->meta->make_immutable;
431              
432             =head1 AUTHOR
433              
434             Dave Cross <dave@perlhacks.com>
435              
436             =head1 COPYRIGHT AND LICENCE
437              
438 14     14 1 25 Copyright (c) 2017, Magnum Solutions Ltd. All Rights Reserved.
439 14         25  
440             This library is free software; you can redistribute it and/or modify it
441             under the same terms as Perl itself.
442 2     2   33  
  2         6  
  2         13  
443             =cut
444              
445             1;