File Coverage

blib/lib/SVG/Timeline.pm
Criterion Covered Total %
statement 74 74 100.0
branch 5 8 62.5
condition 2 6 33.3
subroutine 21 21 100.0
pod 9 9 100.0
total 111 118 94.0


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