File Coverage

blib/lib/WWW/Analytics/MultiTouch/Tabular.pm
Criterion Covered Total %
statement 132 327 40.3
branch 16 96 16.6
condition 3 82 3.6
subroutine 28 40 70.0
pod 7 7 100.0
total 186 552 33.7


line stmt bran cond sub pod time code
1             package WWW::Analytics::MultiTouch::Tabular;
2              
3 3     3   112392 use warnings;
  3         7  
  3         115  
4 3     3   17 use strict;
  3         7  
  3         94  
5              
6 3     3   14 use IO::File;
  3         6  
  3         519  
7 3     3   17 use strict;
  3         5  
  3         90  
8 3     3   18 use warnings;
  3         4  
  3         86  
9 3     3   3103 use Text::Table;
  3         99749  
  3         117  
10 3     3   5430 use Text::CSV_XS;
  3         57331  
  3         249  
11 3     3   10856 use Spreadsheet::WriteExcel;
  3         315256  
  3         170  
12 3     3   3627 use Spreadsheet::WriteExcel::Utility;
  3         13033  
  3         597  
13 3     3   41 use Digest::MD5 qw/md5_hex/;
  3         6  
  3         527  
14 3     3   1167 use Params::Validate qw(:all);
  3         25429  
  3         812  
15 3     3   27 use List::Util qw/max/;
  3         6  
  3         369  
16 3     3   1529 use Storable qw/dclone/;
  3         4990  
  3         280  
17 3     3   1113 use Hash::Merge qw/merge/;
  3         9308  
  3         193  
18 3     3   38 use POSIX qw/floor/;
  3         6  
  3         38  
19 3     3   5376 use LWP::UserAgent;
  3         52788  
  3         123  
20 3     3   1774 use File::Temp;
  3         15691  
  3         391  
21 3     3   2241 use Encode;
  3         25632  
  3         679  
22              
23             my %legal_name = map { $_ => 1 } qw/font size color bold italic underline font_strikeout font_script font_outline font_shadow num_format locked hidden align valign rotation text_wrap test_justlast center_across indent shrink pattern bg_color fg_color border bottom top left right border_color bottom_color top_color left_color right_color/;
24              
25             my %chart_methods = ( title => [ qw/name name_formula/ ],
26             x_axis => [ qw/name name_formula/ ],
27             y_axis => [ qw/name name_formula/ ],
28             legend => [ qw/position/ ],
29             chartarea => [ qw/color line_color line_pattern line_weight/ ],
30             plotarea => [ qw/visible color line_color line_pattern line_weight/ ],
31             );
32              
33              
34             sub new
35             {
36 1     1 1 60 my $class = shift;
37              
38 1         41 my %params = validate(@_, { format => 0,
39             filename => 0,
40             header_layout => 0,
41             footer_layout => 0,
42             });
43              
44 1         8 for my $method (qw/format filename filehandle/) {
45 3     3   61 no strict 'refs';
  3         6  
  3         13681  
46 3         20 *{"${class}::$method"} = sub {
47 22     22   683 my $self = shift;
48 22         46 my $ret = $self->{$method};
49 22 100       50 $self->{$method} = $_[0] if defined $_[0];
50 22         86 return $ret;
51             }
52 3         20 }
53              
54 1   33     9 my $self = bless \%params, ref $class || $class;
55              
56 1         5 $self->open;
57              
58 1         3 return $self;
59             }
60              
61              
62             sub print
63             {
64 2     2 1 8608 my ($self, $data) = @_;
65              
66 2         10 local $_ = $self->format;
67             SWITCH: {
68 2 100       6 m/csv/ && do { $self->csv($data), last SWITCH };
  2         16  
  1         6  
69 1 50       5 m/xls/ && do { $self->xls($data), last SWITCH };
  0         0  
70 1         5 $self->txt($data);
71             };
72             }
73              
74             sub txt
75             {
76 1     1 1 2 my ($self, $datasets) = @_;
77 1         5 my $handle = $self->filehandle;
78 1         13 binmode $handle, ':utf8';
79 1         5 foreach my $data (@$datasets) {
80 2         6 my $tab = Text::Table->new(map { _text_of($_) } @{$data->{'headings'}});
  6         13  
  2         7  
81              
82 2         2467 $tab->load(map { my $row = $_; [ map { _text_of($_) } @$row ] } @{$data->{'data'}});
  6         9  
  6         13  
  18         31  
  2         8  
83 2 50       458 $handle->print($data->{'sheetname'} . "\n") if $data->{'sheetname'};
84 2         23 $handle->print(_text_of($data->{'title'}) . "\n" . $tab->table);
85 2         14415 $handle->print("\n\n");
86 2 50 33     54 if ($data->{'notes'} && @{$data->{'notes'}}) {
  0         0  
87 0         0 $handle->print(_text_of($_) . "\n") for @{$data->{'notes'}};
  0         0  
88 0         0 $handle->print("\n\n");
89             }
90             }
91             }
92              
93              
94             sub csv
95             {
96 1     1 1 3 my ($self, $datasets) = @_;
97              
98 1         4 my $handle = $self->filehandle;
99 1         8 binmode $handle, ':utf8';
100              
101 1         11 my $csv = Text::CSV_XS->new( { 'binary'=>1 } );
102 1         134 foreach my $data (@$datasets) {
103 2 50       8 if ($data->{'sheetname'}) {
104 2     1   92 $csv->print($handle, [ $data->{'sheetname'} ]);
  1         12  
  1         2  
  1         7  
105 2         22 $handle->print("\n");
106             }
107 2         15 $csv->print($handle, [ _text_of($data->{'title'}) ]);
108 2         16 $handle->print("\n");
109 2         9 $csv->print($handle, [ map { _text_of($_) } @{$data->{'headings'}}] );
  6         10  
  2         6  
110 2         17 $handle->print("\n");
111 6         26 map { my $row = $_;
  2         5  
112 6         12 $csv->print($handle, [ map { _text_of($_) } @$row ]);
  18         29  
113 6         52 $handle->print("\n");
114 2         11 } @{$data->{'data'}};
115 2         15 $handle->print("\n\n\n");
116 2 50 33     35 if ($data->{'notes'} && @{$data->{'notes'}}) {
  0         0  
117 0         0 $handle->print(_text_of($_) . "\n") for @{$data->{'notes'}};
  0         0  
118 0         0 $handle->print("\n\n\n");
119             }
120             }
121             }
122              
123             sub xls
124             {
125 0     0 1 0 my ($self, $worksheets) = @_;
126              
127 0         0 my $handle = $self->filehandle;
128 0         0 binmode $handle, ':raw';
129 0         0 my $xls = Spreadsheet::WriteExcel->new($handle);
130              
131 0         0 my $bold = $xls->add_format();
132              
133 0         0 $bold->set_bold();
134              
135             # Collate formats
136             # fmts stores colour data, and format data (indexed by md5 of format hash)
137 0         0 my %fmts = ( colours => {},
138             colour_indexes => [ 19, 21, 24 .. 32, 34 .. 52, 53 .. 63 ],
139             last_colour_index => 0,
140             _tempfiles => {},
141             );
142 0         0 for my $tab (@$worksheets) {
143 0         0 _collate_formats($xls, \%fmts, $tab->{title});
144 0 0       0 _collate_formats($xls, \%fmts, $_) for @{$tab->{headings} || []};
  0         0  
145 0         0 for my $row (@{$tab->{'data'}}) {
  0         0  
146 0         0 for my $cell (@$row) {
147 0         0 _collate_formats($xls, \%fmts, $cell);
148             }
149             }
150             }
151              
152 0         0 my $tabcount = 0;
153 0         0 foreach my $tab (@$worksheets) {
154 0         0 $tabcount++;
155 0   0     0 my $name = $tab->{'sheetname'} || "Sheet $tabcount";
156 0 0       0 $name = substr($name, 0, 31) if length($name) > 31;
157 0         0 $name = Encode::encode_utf8($name);
158 0         0 my $worksheet = $xls->add_worksheet($name);
159 0         0 $name = $worksheet->get_name(); # in case characters get altered
160 0   0     0 my $row = $self->{layout}->{start_row} || 0;
161 0         0 my $col = 0;
162 0 0       0 my @cols = map { length(ref($_) eq 'ARRAY' ? $_->[0] : $_); } @{$tab->{'headings'}};
  0         0  
  0         0  
163              
164 0         0 _write_layout($worksheet, $xls, \%fmts, $tab,
165             merge($self->{header_layout}, $tab->{header_layout}),
166             0,
167             \$row, \@cols);
168              
169 0         0 $row++;
170 0         0 _write_cell($worksheet, \%fmts, $row++, 0, $tab->{'title'}, $bold);
171 0         0 for my $header (@{$tab->{headings}}) {
  0         0  
172 0         0 _write_cell($worksheet, \%fmts, $row, $col++, $header, $bold);
173             }
174 0         0 $row++;
175              
176 0         0 my $start_row = $row; # keep data start row for chart references
177 0         0 foreach my $line (@{$tab->{'data'}}) {
  0         0  
178 0         0 $col = 0;
179 0         0 for my $cell (@{$line}) {
  0         0  
180 0         0 _write_cell($worksheet, \%fmts, $row, $col, $cell);
181 0         0 $col++;
182             }
183 0         0 $row++;
184              
185 0         0 for my $i (0..@$line) {
186 0 0 0     0 $cols[$i] = length(_text_of($line->[$i])) if ($line->[$i] && length(_text_of($line->[$i])) > ($cols[$i] || 0));
      0        
187             }
188             }
189              
190 0         0 foreach my $chart (@{$tab->{chart}}) {
  0         0  
191 0   0     0 my $obj = $xls->add_chart( type => ($chart->{type} || 'line'), embedded => 1 );
192 0         0 for my $series (@{$chart->{series}}) {
  0         0  
193 0         0 $obj->add_series(categories => xl_range_formula($name,
194             $series->{categories}[0] + $start_row,
195             $series->{categories}[1] + $start_row,
196             $series->{categories}[2],
197             $series->{categories}[3]),
198             values => xl_range_formula($name,
199             $series->{values}[0] + $start_row,
200             $series->{values}[1] + $start_row,
201             $series->{values}[2],
202             $series->{values}[3]),
203             name_formula => _to_formula($name, $start_row, $series->{name_formula}),
204             name => $series->{name},
205             );
206             }
207              
208 0         0 for my $method (keys %chart_methods) {
209 0 0       0 next unless exists $chart->{$method};
210            
211 0         0 my %props = map { $_ => $chart->{$method}{$_} }
  0         0  
212 0         0 grep { exists $chart->{$method}{$_} } @{$chart_methods{$method}};
  0         0  
213 0         0 $method = 'set_' . $method;
214 0 0       0 if (scalar keys %props > 0) {
215 0 0       0 if (exists $props{name_formula}) {
216 0         0 $props{name_formula} = _to_formula($name, $start_row, $props{name_formula});
217             }
218 0         0 _add_custom_colour($xls, \%fmts, \%props);
219 0         0 $obj->$method(%props);
220             }
221             }
222 0         0 my $ins_row = ++$row;
223 0 0       0 if (defined $chart->{abs_row}) {
224 0         0 $ins_row = $chart->{abs_row} + $start_row;
225             }
226             else {
227 0   0     0 $row += ($chart->{row} || 0) + floor(20 * ($chart->{y_scale} || 1));
      0        
228             }
229 0   0     0 $worksheet->insert_chart($ins_row + ($chart->{row} || 0),
      0        
      0        
      0        
230             $chart->{abs_col} || $chart->{col} || 0,
231             $obj, 0, 0, $chart->{x_scale} || 1, $chart->{y_scale} || 1);
232             }
233 0         0 $row++;
234              
235 0         0 _write_layout($worksheet, $xls, \%fmts, $tab,
236             merge($self->{footer_layout}, $tab->{footer_layout}),
237             $row,
238             \$row, \@cols);
239              
240 0         0 for my $i (0..@cols) {
241 0         0 $cols[$i] += 2;
242 0 0       0 $cols[$i] = 6 if ($cols[$i] < 6);
243 0 0       0 $cols[$i] = 60 if ($cols[$i] > 60);
244 0         0 $worksheet->set_column($i, $i, $cols[$i]);
245             }
246             }
247              
248 0         0 $xls->close();
249 0         0 unlink $_ for values %{$fmts{_tempfiles}};
  0         0  
250             }
251              
252             sub _to_formula {
253 0     0   0 my ($name, $start_row, $name_formula) = @_;
254 0         0 return "='$name'!" . xl_rowcol_to_cell($name_formula->[0] + $start_row,
255             $name_formula->[1], 1, 1);
256             }
257              
258             sub _get_sub_text {
259 0     0   0 my $subst = shift;
260 0   0     0 return (ref($subst) eq 'ARRAY' ? $subst->[0] : $subst) || '';
261             }
262              
263             sub _substitute_text {
264 0     0   0 my $tab = shift;
265 0         0 my $texts = shift;
266              
267 0 0       0 $texts = ref($texts) eq 'ARRAY' ? dclone($texts) : [ $texts ];
268 0         0 for (@$texts) {
269 0         0 s{(?{$1}) }eg;
  0         0  
270 0         0 s/\\@/@/g;
271             }
272 0         0 return $texts;
273             }
274              
275              
276             sub _row_of {
277 0     0   0 my $img = shift;
278 0         0 my $baserow = shift;
279              
280 0 0       0 if (defined $img->{row}) {
281 0         0 $_[0] = $img->{row};
282 0         0 return $baserow + $img->{row};
283             }
284 0         0 return $baserow + ++$_[0];
285             }
286              
287             sub _text_of {
288 52     52   71 my $cell = shift;
289              
290 52 50       109 $cell = $cell->[0] if ref($cell) eq 'ARRAY';
291 52 50       237 return defined $cell ? $cell : '';
292             }
293              
294             sub _cache_image_file {
295 0     0   0 my ($fmts, $img) = @_;
296              
297 0 0       0 return $img->{filename} unless $img->{filename} =~ m{^https?://};
298              
299 0 0 0     0 if (exists($fmts->{_tempfiles}{$img->{filename}})
300             && -f $fmts->{_tempfiles}{$img->{filename}}) {
301 0         0 return $img->{filename} = $fmts->{_tempfiles}{$img->{filename}};
302             }
303 0         0 my $ua = LWP::UserAgent->new;
304 0         0 my $response = $ua->get($img->{filename});
305 0 0       0 if (! $response->is_success) {
306 0         0 warn "Failed to download $img->{filename}\n";
307 0         0 return;
308             }
309 0 0       0 if (my $dst = File::Temp->new(UNLINK => 0)) {
310 0         0 print $dst $response->decoded_content;
311 0         0 close $dst;
312 0         0 return $img->{filename} = $fmts->{_tempfiles}{$img->{filename}} = $dst->filename;
313             }
314 0         0 warn "Failed to store $img->{filename}\n";
315 0         0 return;
316             }
317              
318             sub _write_layout {
319 0     0   0 my ($worksheet, $xls, $fmts, $tab, $layout, $baserow, $rowref, $colsref) = @_;
320              
321 0         0 my $current_row = 0;
322 0         0 for my $method (keys %$layout) {
323 0         0 my $params = $layout->{$method};
324 0 0 0     0 if ($method eq 'image') {
    0          
    0          
325 0 0       0 $params = [ $params ] unless ref($params) eq 'ARRAY';
326 0         0 for my $img (@{$params}) {
  0         0  
327 0 0       0 _cache_image_file($fmts, $img) or next;
328 0   0     0 $worksheet->insert_image(_row_of($img, $baserow, $current_row),
      0        
      0        
      0        
      0        
329             $img->{col} || 0,
330             $img->{filename},
331             $img->{x_offset} || 0,
332             $img->{y_offset} || 0,
333             $img->{x_scale} || 1,
334             $img->{y_scale} || 1,
335             );
336             }
337             }
338             elsif ($method eq 'header' || $method eq 'footer') {
339 0 0       0 $params = [ $params ] unless ref($params) eq 'ARRAY';
340 0         0 for my $hdr (@{$params}) {
  0         0  
341 0         0 my $texts = _substitute_text($tab, $hdr->{text});
342 0 0 0     0 if ($hdr->{colspan} || $hdr->{rowspan}) {
343 0   0     0 my $format = _collate_formats($xls, $fmts, [ undef, $hdr->{cell_format} ||= { bold => 1 }], 1);
344 0         0 my $i = 0;
345 0         0 for my $text (@$texts) {
346 0         0 my $r = _row_of($hdr, $baserow + $i, $current_row);
347 0   0     0 my $c = $hdr->{col} || 0;
348 0   0     0 $worksheet->merge_range($r,
      0        
349             $c,
350             $r + ($hdr->{rowspan} || 0),
351             $c + ($hdr->{colspan} || 0),
352             $text,
353             $format);
354 0         0 $i++;
355 0   0     0 $$rowref = max($$rowref, $r + ($hdr->{rowspan} || 0));
356 0 0 0     0 if (! $hdr->{colspan} && defined $colsref) {
357 0         0 $colsref->[$hdr->{col}] = max($colsref->[$hdr->{col}], length($text));
358             }
359             }
360             }
361             else {
362 0         0 _collate_formats($xls, $fmts, [ undef, $hdr->{cell_format} ]);
363 0         0 my $i = 0;
364 0         0 for my $text (@$texts) {
365 0   0     0 $text ||= '';
366 0         0 my $r = _row_of($hdr, $baserow + $i, $current_row);
367 0   0     0 my $c = $hdr->{col} || 0;
368 0         0 _write_cell($worksheet, $fmts, $r, $c, [ $text, $hdr->{cell_format} ]);
369 0         0 $i++;
370 0         0 $$rowref = max($$rowref, $r);
371 0 0 0     0 $colsref->[$c] = max($colsref->[$c] || 0, length($text)) if defined $colsref;
372             }
373             }
374             }
375             }
376             elsif ($worksheet->can($method)) {
377 0 0       0 $worksheet->$method(ref($params) eq 'ARRAY' ? @$params : $params);
378             }
379              
380             }
381             }
382              
383             sub _to_legal_format {
384 0     0   0 my $fmt = shift;
385              
386 0 0       0 my %vals = map { $_ => $fmt->{$_} }
  0         0  
387 0         0 grep { exists($legal_name{$_}) && defined($fmt->{$_}) }
388 0         0 keys %{$fmt};
389 0 0       0 if (scalar keys %vals != scalar keys %$fmt) {
390 0         0 warn "Invalid format key found: " . join(' ', grep { ! exists($legal_name{$_}) } keys %{$fmt}) . "\n";
  0         0  
  0         0  
391             }
392              
393 0         0 return \%vals;
394             }
395              
396             # Convert format to unique string
397             # Not required to be reversible
398             sub _to_format_key {
399 0     0   0 my $fmt = shift;
400 0         0 my $key = '';
401            
402 0         0 for my $k (sort keys %$fmt) {
403 0         0 my $v = $fmt->{$k};
404 0         0 $key .= "$k=$v&";
405             }
406              
407 0         0 return md5_hex($key);
408             }
409              
410             sub _add_custom_colour {
411 0     0   0 my ($xls, $fmts, $fmt) = @_;
412              
413             # Create custom colours for RGB triples #RRGGBB
414 0         0 for my $colourkey (grep { m/color/ } keys %$fmt) {
  0         0  
415 0         0 my $colourval = $fmt->{$colourkey};
416 0 0       0 next unless $colourval =~ m/^#/;
417 0 0       0 if (! exists($fmts->{colours}{$colourval})) {
418 0         0 $fmts->{colours}{$colourval} = $xls->set_custom_color($fmts->{colour_indexes}[$fmts->{last_colour_index}++], $colourval);
419             }
420 0         0 $fmt->{$colourkey} = $fmts->{colours}{$colourval};
421             }
422             }
423              
424             sub _collate_formats {
425 0     0   0 my ($xls, $fmts, $cell, $merged) = @_;
426 0 0 0     0 if (ref($cell) eq 'ARRAY' && @$cell == 2 && ref($cell->[1]) eq 'HASH') {
      0        
427 0         0 my $fmt = _to_legal_format($cell->[1]);
428 0 0       0 my $fmt_key = _to_format_key($merged ? { %$fmt, merged => 1 } : $fmt);
429 0         0 _add_custom_colour($xls, $fmts, $fmt);
430 0 0       0 unless (exists $fmts->{$fmt_key}) {
431 0         0 $fmts->{$fmt_key} = $xls->add_format(%$fmt);
432             }
433 0         0 return $fmts->{$fmt_key}
434             }
435 0         0 return;
436             }
437              
438             sub _write_cell {
439 0     0   0 my ($worksheet, $fmts, $row, $col, $cell, $def_fmt) = @_;
440              
441 0 0       0 if (ref($cell) eq 'ARRAY') {
442 0   0     0 $worksheet->write($row, $col, $cell->[0], $fmts->{_to_format_key(_to_legal_format($cell->[1]))} || $def_fmt);
443             }
444             else {
445 0         0 $worksheet->write($row, $col, $cell, $def_fmt);
446             }
447             }
448              
449              
450             sub open
451             {
452 2     2 1 5 my ($self, $format, $filename) = @_;
453 2         4 my $filehandle;
454              
455 2 50       7 $self->format($format) if $format;
456 2 50       7 $self->filename($filename) if $filename;
457              
458 2 50       5 if ($self->filename) {
459 2 50       6 $filehandle = IO::File->new(">" . $self->filename) or die "Failed to open " . $self->filename . ": $!";
460             }
461             else {
462 0         0 $filehandle = \*STDOUT;
463             }
464              
465 2         345 $self->filehandle($filehandle);
466             }
467              
468              
469             sub close
470             {
471 2     2 1 10 my $self = shift;
472              
473 2 50       9 if ($self->filehandle) {
474 2         7 $self->filehandle->close();
475 2         165 $self->filehandle(undef);
476 2         6 $self->filename(undef);
477             }
478             }
479              
480             =head1 NAME
481              
482             WWW::Analytics::MultiTouch::Tabular - Provides various output formats for writing tabular reports
483              
484             =head1 SYNOPSIS
485              
486             # Simple usage
487              
488             use WWW::Analytics::MultiTouch::Tabular;
489              
490             my @data = ( [ 1, 2, 3 ],
491             [ 4, 5, 6 ],
492             [ 7, 8, 9 ],
493             );
494             my @reports = (
495             {
496             title => "Number of Results in Top 10, by Site",
497             sheetname => "Top Positions",
498             headings => [ "Site", "Engine", "Top 10 Results", "Unique URLs", "Top 10 Previous Week", "Unique URLs Previous Week" ],
499             data => \@data,
500             },
501             ...
502             );
503             my $output = WWW::Analytics::MultiTouch::Tabular->new({format => 'txt', outfile => $file});
504             $output->print(\@reports);
505             $output->close();
506              
507             # With formatting
508              
509             my @data = ( [ [ 1, { color => 'red' } ], 2, 3 ],
510             [ [ 4, { color => '#123456' ], 5, 6 ],
511             [ [ 7, { bold => 1 } ], 8, 9 ],
512             );
513              
514             =head1 DESCRIPTION
515              
516             Takes a list of reports and outputs them in the specified format (text, csv, or Excel).
517              
518             For Excel, supports extended formatting including headers, footers, colours, fonts, images, charts.
519              
520             =head1 METHODS
521              
522             =head2 new
523              
524             $output = WWW::Analytics::MultiTouch::Tabular->new({format => 'txt', filename => $file});
525              
526             Creates a new WWW::Analytics::MultiTouch::Tabular object. Options are as follows:
527              
528             =over 4
529              
530             =item * format
531              
532             txt, csv or xls.
533              
534             =item * filename
535              
536             Name of output file
537              
538             =item * header_layout, footer_layout
539              
540             See L.
541              
542             =back
543              
544             =head2 print
545              
546             $output->print(\@reports);
547              
548             Prints given data in txt, csv, or xls format.
549              
550             Each item in @reports is a hash containing the following elements:
551              
552             =over 4
553              
554             =item * title
555              
556             Report title
557              
558             =item * sheetname
559              
560             Sheet name, where applicable (as in spreadsheet output).
561              
562             =item * headings
563              
564             Array of column headings. Each heading entry may be a scalar (used as is) or a
565             two-element array, in which case the first element is the data and the second
566             element is the cell format. See L for cell formatting details.
567              
568             =item * data
569              
570             Array of data; each row is a row in the output, with columns corresponding to
571             the column headings given. Each data point may be a scalar (used as is) or a
572             two-element array, in which case the first element is the data and the second
573             element is the cell format. See L for cell formatting details.
574              
575             Examples:
576              
577             Simple data array, no formatting:
578              
579             data => [ [ 1, 2, 3 ],
580             [ 4, 5, 6 ],
581             [ 7, 8, 9 ],
582             ]
583              
584             Data array with first entry in each row formatted:
585              
586             data => [ [ [ 1, { color => 'red' } ], 2, 3 ],
587             [ [ 4, { color => '#123456' ], 5, 6 ],
588             [ [ 7, { bold => 1 } ], 8, 9 ],
589             ]
590              
591              
592             =item * header_layout, footer_layout
593              
594             See L.
595              
596             =item * chart
597              
598             Insert one or more charts. Example:
599              
600             chart => [ { type => 'column',
601             x_scale => 1.5,
602             y_scale => 1.5,
603             series => [ map {
604             { categories => [ -1, -1, 1, scalar @{$data[0]} ],
605             values => [ $_, $_, 1, scalar @{$data[0]} ],
606             name_formula => [$_, 0],
607             name => $data[$_][0],
608             } } (0 .. @data - 1) ]
609             } ],
610              
611             Options are:
612              
613             =over 4
614              
615             =item * type
616              
617             May be 'area', 'bar', 'column', 'line', 'pie', 'scatter', 'stock'. See
618             L for more details on the available types.
619              
620             =item * series
621              
622             This is the array of data series for the chart. 'categories', 'values' and
623             'name_formula' are given in terms of cell ranges referenced to the start of the
624             spreadsheet data. The heading row may be referenced as -1 relative to the start
625             of the data. See L for more details.
626              
627             =item * row, abs_row
628              
629             Optional row offset or absolute row position. Will be placed at the current row if not specified.
630              
631             =item * col, abs_col
632              
633             Optional column number. Will be placed at column 0 if not specified.
634              
635             =item * x_scale, y_scale
636              
637             Optional chart scaling factors.
638              
639             =item * title, x_axis, y_axis, legend, chartarea, plotarea
640              
641             See the corresponding set_* method in L for more details.
642              
643             =back
644              
645             =back
646              
647             =head2 txt
648              
649             $output->txt(\@reports);
650              
651             Generate output in plain text format.
652              
653             =head2 csv
654              
655             $output->csv(\@reports);
656              
657             Generate output in CSV format.
658              
659             =head2 xls
660              
661             $output->xls(\@reports);
662              
663             Generate output in Excel spreadsheet format.
664              
665            
666             =head2 open
667              
668             $output->format('csv');
669             $output->filename("$dir/csv-test.csv");
670             $output->open;
671              
672             $output->open("xls", "$dir/xls-test.xls");
673              
674             'open' opens a file for writing. It is usually not necessary to call 'open' as it
675             is implicit in 'new'. However, if you wish to re-use the object created with
676             'new' to output a different format or to a different file, then you need to call
677             open with the new format/file arguments, or after setting the new format and
678             output file with the format and outfile methods.
679              
680              
681             =head2 format
682              
683             $output->format('xls');
684              
685             Set/get format to be used in L. L must be called for the format change to take effect.
686              
687             =head2 filename
688              
689             Set/get filename to be used in L. L must be called for the filename change to take effect.
690              
691             If no filename is provided as an argument or previously set, STDOUT will be used.
692              
693             =head2 outfile
694              
695             'outfile' is equivalent to 'filename', provided for backward compatibility.
696              
697             =head2 filehandle
698              
699             $output->filehandle(\*STDOUT);
700              
701             As an alternative to 'open', you can set the file handle explicitly using filehandle().
702              
703             =head2 close
704              
705             Close file
706              
707             =head1 HEADERS AND FOOTERS
708              
709             A set of images, rows of text and/or spreadsheet operations to create page headers and footers. 'header_layout' is used prior to placing any data on the page, and 'footer_layout' afterwards. Example:
710              
711             'header_layout' => {
712             'hide_gridlines' => '2',
713             'image' => [
714             {
715             'filename' => 'http://www.multitouchanalytics.com/images/multitouch-analytics-header.jpg',
716             'col' => 0,
717             'row' => 1,
718             'x_scale' => 0.7,
719             'y_scale' => 0.7
720             },
721             ],
722             'header' => [
723             {
724             'colspan' => '5',
725             'cell_format' => {
726             'color' => 'white',
727             'align' => 'center',
728             'bold' => 1,
729             'bg_color' => 'blue',
730             'size' => '16'
731             },
732             'text' => 'Multi Touch Reporting',
733             'col' => 0,
734             'row' => '5'
735             },
736             {
737             'cell_format' => {
738             'align' => 'right',
739             'bold' => 1
740             },
741             'text' => [
742             'Generation Date:',
743             'Report Type:',
744             'Date Range:',
745             'Analysis Window:'
746             ],
747             'col' => 0,
748             'row' => '7'
749             },
750             {
751             'text' => [
752             '@generation_date',
753             '@title',
754             '@start_date - @end_date',
755             '@window_length days'
756             ],
757             'col' => 1,
758             'row' => '7'
759             }
760             ],
761             'start_row' => '10'
762             }
763             }
764              
765             Options are as follows:
766              
767             =over 4
768              
769             =item * image
770              
771             Specifies an image (PNG or JPEG) or images to be placed into the spreadsheet.
772             Multiple images may be inserted by specifying an array of option hashes,
773             comprising the following keys:
774              
775             =over 4
776              
777             =item * row
778              
779             Optional row number. Will be placed at the current row if not specified.
780              
781             =item * col
782              
783             Optional column number. Will be placed at column 0 if not specified.
784              
785             =item * filename
786              
787             Filename or URL of the image.
788              
789             =item * x_offset, y_offset
790              
791             Optional pixel offsets of image from top-left of cell.
792              
793             =item * x_scale, y_scale
794              
795             Optional image scaling factors.
796              
797             =back
798              
799             =item * header, footer
800              
801             Specifies formatted rows of text. 'header' is intended to be used for
802             'header_layout' and 'footer' for 'footer_layout', but it doesn't actually matter
803             if they are used the other way around. Multiple rows may be inserted by
804             specifying an array of option hashes, comprising the following keys:
805              
806             =over 4
807              
808             =item * cell_format
809              
810             See L.
811              
812             =item * row
813              
814             Optional row number. Will be placed at the current row if not specified.
815              
816             =item * col
817              
818             Optional column number. Will be placed at column 0 if not specified.
819              
820             =item * rowspan, colspan
821              
822             Optional row and column spans for merged cells.
823              
824             =item * text
825              
826             Lines of text. If given as an array, each line will be inserted on subsequent
827             rows, keeping the same formatting options.
828              
829             Variables may be specified in the text as @variable_name, and will be
830             substituted. Valid variable names are the top level keys passed in the report
831             hash to print().
832              
833             =back
834              
835             =item * Any worksheet or page setup method from L
836              
837             e.g. keep_leading_zeros, show_comments, set_first_sheet, set_tab_color, hide_gridlines, set_zoom, etc.
838              
839             Any valid Spreadsheet::WriteExcel worksheet method will be invoked with the given values, i.e.
840              
841             hide_gridlines => 1
842             print_area => [ 1, 2, 3, 4 ]
843              
844             invokes $worksheet->hide_gridlines(1) and $worksheet->print_area(1, 2, 3, 4).
845              
846             =back
847              
848             =head1 CELL FORMAT
849              
850             Cell formatting options are defined through a hashref of any text formatting
851             options from L; specifically, any of 'font', 'size',
852             'color', 'bold', 'italic', 'underline', 'font_strikeout', 'font_script',
853             'font_outline', 'font_shadow', 'num_format', 'locked', 'hidden', 'align',
854             'valign', 'rotation', 'text_wrap', 'test_justlast', 'center_across', 'indent',
855             'shrink', 'pattern', 'bg_color', 'fg_color', 'border', 'bottom', 'top', 'left',
856             'right', 'border_color', 'bottom_color', 'top_color', 'left_color',
857             'right_color'.
858              
859             =head1 AUTHOR
860              
861             Jon Schutz, C<< >>
862              
863              
864             =head1 BUGS
865              
866             Please report any bugs or feature requests to C, or through
867             the web interface at L. I will be notified, and then you'll
868             automatically be notified of progress on your bug as I make changes.
869              
870              
871             =head1 SUPPORT
872              
873             You can find documentation for this module with the perldoc command.
874              
875             perldoc WWW::Analytics::MultiTouch
876              
877              
878             You can also look for information at:
879              
880             =over 4
881              
882             =item * RT: CPAN's request tracker
883              
884             L
885              
886             =item * AnnoCPAN: Annotated CPAN documentation
887              
888             L
889              
890             =item * CPAN Ratings
891              
892             L
893              
894             =item * Search CPAN
895              
896             L
897              
898             =back
899              
900              
901             =head1 COPYRIGHT & LICENSE
902              
903             Copyright 2010 YourAmigo Ltd.
904              
905             Permission is hereby granted, free of charge, to any person obtaining a copy
906             of this software and associated documentation files (the "Software"), to deal
907             in the Software without restriction, including without limitation the rights
908             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
909             copies of the Software, and to permit persons to whom the Software is
910             furnished to do so, subject to the following conditions:
911              
912             The above copyright notice and this permission notice shall be included in
913             all copies or substantial portions of the Software.
914              
915             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
916             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
917             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
918             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
919             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
920             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
921             THE SOFTWARE.
922              
923             =cut
924              
925             1;