File Coverage

blib/lib/Chart/Weather/Forecast/Temperature.pm
Criterion Covered Total %
statement 14 16 87.5
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 20 22 90.9


line stmt bran cond sub pod time code
1 1     1   1507 use strictures 1;
  1         971  
  1         35  
2             package Chart::Weather::Forecast::Temperature;
3             BEGIN {
4 1     1   100 $Chart::Weather::Forecast::Temperature::VERSION = '0.04';
5             }
6 1     1   1138 use Moose;
  1         791785  
  1         11  
7 1     1   10564 use namespace::autoclean;
  1         1892  
  1         5  
8 1     1   1420 use MooseX::Types::Path::Class;
  1         222259  
  1         11  
9              
10 1     1   2038 use Chart::Clicker;
  0            
  0            
11             use Chart::Clicker::Data::Range;
12             use Chart::Clicker::Data::Series;
13             use Chart::Clicker::Data::DataSet;
14             use Chart::Clicker::Drawing::ColorAllocator;
15             use Geometry::Primitive::Circle;
16             use Graphics::Primitive::Font;
17             use Graphics::Color::RGB;
18             use Number::Format;
19             use List::Util qw/ min max /;
20             use Path::Class qw/ file /;
21              
22             use Data::Dumper::Concise;
23              
24             =head1 Synopsis
25              
26             my $highs = [37, 28, 17, 22, 28];
27             my $lows = [18, 14, 5, 10, 18];
28            
29             my $forecast = Chart::Weather::Forecast::Temperature->new(
30             highs => $highs,
31             lows => $lows,
32             chart_temperature_file => '/tmp/temperature_forecast.png',
33             );
34             $forecast->create_chart;
35              
36             =head1 Attributes
37              
38             =head2 highs
39              
40             ArrayRef[Num] of high temperatures
41              
42             Required at construction (new): yes
43              
44             =head2 lows
45              
46             ArrayRef[Num] of low temperatures
47              
48             Required at construction (new): yes
49              
50             =head2 chart_temperature_file
51              
52             Where you want to write out the chart image.
53              
54             Default: /tmp/temperature-forecast.png' on *nix
55            
56             NOTE: The chart_temperature_file attribute isa 'Path::Class::File'
57             so if you want to specifiy an output file then do so like:
58              
59             chart_temperature_file => Path::Class::File->new( $your_dir, $your_file_name);
60             chart_temperature_file => Path::Class::File->new( '/tmp/', 'forecast_temps.png');
61              
62             =head2 chart_width
63              
64             Chart dimension in pixels
65              
66             Default: 240
67              
68             =head2 chart_height
69              
70             Chart dimension in pixels
71              
72             Default: 160
73              
74             =head2 chart_format
75              
76             Format of the chart image
77              
78             Default: png
79            
80             =head2 title_text
81              
82             The text to title the chart with.
83              
84             Default: Temperature Forecast
85              
86             =cut
87              
88             has 'highs' => (
89             is => 'rw',
90             isa => 'ArrayRef[Num]',
91             required => 1,
92             );
93             has 'lows' => (
94             is => 'rw',
95             isa => 'ArrayRef[Num]',
96             required => 1,
97             );
98             has 'chart_temperature_file' => (
99             is => 'ro',
100             isa => 'Path::Class::File',
101             required => 1,
102             coerce => 1,
103             'default' => sub { Path::Class::File->new(File::Spec->tmpdir, 'temperature-forecast.png') },
104             );
105             has 'chart_format' => (
106             is => 'ro',
107             isa => 'Str',
108             default => sub { 'png' },
109             );
110              
111             has 'chart_width' => (
112             is => 'ro',
113             isa => 'Int',
114             default => 240,
115             );
116             has 'chart_height' => (
117             is => 'ro',
118             isa => 'Int',
119             default => 160,
120             );
121              
122             has 'title_text' => (
123             is => 'rw',
124             isa => 'Str',
125             'default' => sub {
126             my $self = shift;
127             return $self->number_of_datum . '-Day Temperature Forecast';
128             },
129             );
130             has 'title_font' => (
131             is => 'rw',
132             isa => 'Graphics::Primitive::Font',
133             'default' => sub {
134             Graphics::Primitive::Font->new(
135             {
136             family => 'Trebuchet',
137             size => 11,
138             antialias_mode => 'subpixel',
139             hint_style => 'medium',
140              
141             }
142             );
143             },
144             );
145             has 'tick_font' => (
146             is => 'rw',
147             isa => 'Graphics::Primitive::Font',
148             'default' => sub {
149             Graphics::Primitive::Font->new(
150             {
151             family => 'Trebuchet',
152             size => 11,
153             antialias_mode => 'subpixel',
154             hint_style => 'medium',
155              
156             }
157             );
158             },
159             );
160              
161             has 'number_formatter' => (
162             is => 'ro',
163             isa => 'Number::Format',
164             'default' => sub { Number::Format->new },
165             );
166              
167             has 'freezing_line' => (
168             is => 'ro',
169             isa => 'Chart::Clicker::Data::Series',
170             lazy => 1,
171             'default' => sub {
172             my $self = shift;
173             Chart::Clicker::Data::Series->new(
174             keys => $self->x_values,
175             values => [ (32) x $self->number_of_datum ],
176             );
177             },
178             );
179             has 'zero_line' => (
180             is => 'ro',
181             isa => 'Chart::Clicker::Data::Series',
182             lazy => 1,
183             'default' => sub {
184             my $self = shift;
185             Chart::Clicker::Data::Series->new(
186             keys => $self->x_values,
187             values => [ (0) x $self->number_of_datum ],
188             );
189             },
190             );
191             has 'high_series' => (
192             is => 'ro',
193             isa => 'Chart::Clicker::Data::Series',
194             lazy => 1,
195             'default' => sub {
196             my $self = shift;
197             Chart::Clicker::Data::Series->new(
198             keys => $self->x_values,
199             values => $self->highs,
200             );
201             },
202             );
203             has 'low_series' => (
204             is => 'ro',
205             isa => 'Chart::Clicker::Data::Series',
206             lazy => 1,
207             'default' => sub {
208             my $self = shift;
209             Chart::Clicker::Data::Series->new(
210             keys => $self->x_values,
211             values => $self->lows,
212             );
213             },
214             );
215             has 'chart' => (
216             is => 'ro',
217             isa => 'Chart::Clicker',
218             lazy_build => 1,
219             );
220             has 'default_ctx' => (
221             is => 'ro',
222             isa => 'Chart::Clicker::Context',
223             lazy_build => 1,
224             );
225             has 'colors' => (
226             is => 'ro',
227             isa => 'HashRef[Graphics::Color::RGB]',
228             lazy_build => 1,
229             );
230             has 'dataset' => (
231             is => 'ro',
232             isa => 'Chart::Clicker::Data::DataSet',
233             'default' => sub {
234             Chart::Clicker::Data::DataSet->new;
235             },
236             );
237             has 'color_allocator' => (
238             is => 'ro',
239             isa => 'Chart::Clicker::Drawing::ColorAllocator',
240             'default' => sub {
241             Chart::Clicker::Drawing::ColorAllocator->new;
242             },
243             );
244             has 'min_range' => (
245             is => 'rw',
246             isa => 'Num',
247             lazy => 1,
248             builder => '_build_y_range',
249             );
250             has 'max_range' => (
251             is => 'rw',
252             isa => 'Num',
253             lazy => 1,
254             builder => '_build_y_range',
255             );
256             has 'min_range_padded' => (
257             is => 'rw',
258             isa => 'Num',
259             lazy => 1,
260             builder => '_build_y_range_padded',
261             );
262             has 'max_range_padded' => (
263             is => 'rw',
264             isa => 'Num',
265             lazy => 1,
266             builder => '_build_y_range_padded',
267             );
268             has 'range_ticks' => (
269             is => 'ro',
270             isa => 'ArrayRef[Int]',
271             lazy_build => 1,
272             );
273             has 'domain' => (
274             is => 'ro',
275             isa => 'Chart::Clicker::Data::Range',
276             lazy_build => 1,
277             );
278             has 'range' => (
279             is => 'ro',
280             isa => 'Chart::Clicker::Data::Range',
281             lazy_build => 1,
282             );
283             has 'number_of_datum' => (
284             is => 'ro',
285             isa => 'Int',
286             lazy_build => 1,
287             );
288             has 'x_values' => (
289             is => 'ro',
290             isa => 'ArrayRef[Int]',
291             lazy_build => 1,
292             );
293              
294              
295              
296             =head1 Methods
297              
298             =head2 create_chart
299              
300             This is the main method to call on an object to create a chart.
301              
302             =cut
303              
304             sub create_chart {
305             my $self = shift;
306              
307             # Add high series data set and color it red
308             $self->dataset->add_to_series( $self->high_series );
309             $self->color_allocator->add_to_colors( $self->colors->{red} );
310            
311             # Add low series data set and color it blue
312             $self->dataset->add_to_series( $self->low_series );
313             $self->color_allocator->add_to_colors( $self->colors->{blue} );
314              
315             # Add freezing line when appropriate.
316             if ( $self->min_range_padded <= 32 ) {
317             $self->dataset->add_to_series( $self->freezing_line );
318             $self->color_allocator->add_to_colors( $self->colors->{light_blue} );
319             }
320              
321             # Add zero line when appropriate.
322             if ( $self->min_range_padded <= 0 ) {
323             $self->dataset->add_to_series( $self->zero_line );
324             $self->color_allocator->add_to_colors( $self->colors->{light_blue} );
325             }
326              
327             # add the dataset to the chart
328             $self->chart->add_to_datasets( $self->dataset );
329              
330             # assign the color allocator to the chart
331             $self->chart->color_allocator( $self->color_allocator );
332              
333             # write the chart to a file
334             $self->chart->write_output( $self->chart_temperature_file );
335              
336             }
337              
338             # Compute the max and min values for the y-axis (range).
339              
340             sub _compute_range {
341             my $self = shift;
342              
343             my $min_temperature = min @{ $self->lows };
344             my $max_temperature = max @{ $self->highs };
345              
346             # Find nearest factor of 10 above and below
347             $max_temperature += 10 - ( $max_temperature % 10 );
348             $min_temperature -= ( $min_temperature % 10 );
349              
350             return ( $min_temperature, $max_temperature );
351             }
352              
353             sub _build_domain {
354             my $self = shift;
355              
356             my $fudge_factor = 0.25;
357             return Chart::Clicker::Data::Range->new(
358             {
359             lower => (1 - $fudge_factor),
360             upper => ($self->number_of_datum + $fudge_factor),
361             }
362             );
363             }
364              
365             sub _build_range {
366             my $self = shift;
367              
368             return Chart::Clicker::Data::Range->new(
369             {
370             lower => $self->min_range_padded,
371             upper => $self->max_range_padded,
372             }
373             );
374             }
375              
376             sub _build_x_values {
377             my $self = shift;
378            
379             return [1..$self->number_of_datum];
380             }
381              
382             # Add just a touch of padding in case a value is right on the computed range.
383             # This keeps data from being cropped off in the graph.
384              
385             sub _pad_range {
386             my ( $self, $padding ) = @_;
387             $padding ||= 2;
388              
389             return ( ( $self->min_range - $padding ), ( $self->max_range + $padding ) );
390             }
391              
392              
393             # Determine where the ticks for the y-axis will be based on the high and low temperatures.
394             # We coerce the ticks into integers for readability.
395              
396             sub _build_range_ticks {
397             my ($self) = @_;
398              
399             my $delta = $self->max_range - $self->min_range;
400             my $tens = int( $delta / 10 );
401             my @ticks = ( int $self->min_range );
402             for my $factor ( 1 .. $tens ) {
403             push @ticks, ( ( int $self->min_range ) + ( $factor * 10 ) );
404             }
405             return \@ticks;
406             }
407              
408             sub _build_y_range {
409             my $self = shift;
410              
411             my ( $min_range, $max_range ) = $self->_compute_range;
412             $self->min_range($min_range);
413             $self->max_range($max_range);
414              
415             return;
416             }
417              
418             sub _build_y_range_padded {
419             my $self = shift;
420              
421             my ( $min_range_padded, $max_range_padded ) = $self->_pad_range;
422             $self->min_range_padded($min_range_padded);
423             $self->max_range_padded($max_range_padded);
424              
425             return;
426             }
427              
428             sub _build_number_of_datum {
429             my $self = shift;
430              
431             my $nbr_of_lows = scalar @{$self->lows};
432             my $nbr_of_highs = scalar @{$self->highs};
433            
434             if ( $nbr_of_lows != $nbr_of_highs ) {
435             die "ERROR: You need to have the same number of high and low values";
436             }
437             else {
438             return $nbr_of_highs;
439             }
440             }
441              
442             sub _build_colors {
443             my $self = shift;
444              
445             {
446             red => Graphics::Color::RGB->new(
447             {
448             red => .75,
449             green => 0,
450             blue => 0,
451             alpha => .8
452             }
453             ),
454             blue => Graphics::Color::RGB->new(
455             {
456             red => 0,
457             green => 0,
458             blue => .75,
459             alpha => .8
460             }
461             ),
462             light_blue => Graphics::Color::RGB->new(
463             {
464             red => 0,
465             green => 0,
466             blue => .95,
467             alpha => .16
468             }
469             ),
470             };
471             }
472              
473             ##-- Builders
474             sub _build_chart {
475             my $self = shift;
476              
477             # Create the chart canvas
478             my $chart = Chart::Clicker->new(
479             width => $self->chart_width,
480             height => $self->chart_height,
481             format => $self->chart_format,
482             );
483              
484             # Title
485             $chart->title->text( $self->title_text );
486             $chart->title->font( $self->title_font );
487              
488             # Tufte influenced customizations (maximize data-to-ink)
489             $chart->grid_over(1);
490             $chart->plot->grid->show_range(0);
491             $chart->plot->grid->show_domain(0);
492             $chart->legend->visible(0);
493             $chart->border->width(0);
494              
495             return $chart;
496             }
497              
498             sub _build_default_ctx {
499             my $self = shift;
500              
501             my $default_ctx = $self->chart->get_context('default');
502              
503             # Set number format of axis
504             $default_ctx->domain_axis->format(
505             sub { return $self->number_formatter->format_number(shift); } );
506             $default_ctx->range_axis->format(
507             sub { return $self->number_formatter->format_number(shift); } );
508            
509             # Set font of ticks
510             $default_ctx->domain_axis->tick_font( $self->tick_font );
511             $default_ctx->range_axis->tick_font( $self->tick_font );
512            
513             # The chart type is a "connect the dots" (line segments between data circles)
514             $default_ctx->renderer( Chart::Clicker::Renderer::Line->new );
515             $default_ctx->renderer->shape(
516             Geometry::Primitive::Circle->new( { radius => 3, } ) );
517             $default_ctx->renderer->brush->width(1);
518            
519             # Set ticks values for each axis
520             $default_ctx->domain_axis->tick_values( $self->x_values );
521             $default_ctx->range_axis->tick_values( $self->range_ticks );
522              
523             # Set max and min values for each axis.
524             $default_ctx->domain_axis->range($self->domain);
525             $default_ctx->range_axis->range($self->range);
526              
527             return $default_ctx;
528             }
529              
530             =head2 BUILD
531              
532             Here we do some initialization just after the object has been constructed.
533             Calling these builders here helped me defeat undef occuring from lazy dependencies.
534              
535             =cut
536              
537             sub BUILD {
538             my $self = shift;
539            
540             $self->_build_y_range;
541             $self->_build_y_range_padded;
542             $self->_build_default_ctx;
543             }
544              
545             __PACKAGE__->meta->make_immutable;
546             1
547              
548             __END__
549              
550             =head1 Authors
551              
552             Mateu Hunter C<hunter@missoula.org>
553              
554             =head1 Copyright
555              
556             Copyright 2010, Mateu Hunter
557              
558             =head1 License
559              
560             You may distribute this code under the same terms as Perl itself.
561              
562             =cut