5 include('elements/report.html',
7 'title' => 'Page title',
9 'data' => [ \@item1 \@item2 ... ],
11 #these run parallel to items, and can be given as hashes
12 'row_labels' => \@row_labels, #required
13 'colors' => \@colors, #required
14 'graph_labels' => \@graph_labels, #defaults to row_labels
16 'links' => \@links, #optional
18 #these run parallel to the elements of each @item
19 'col_labels' => \@col_labels, #required
20 'axis_labels' => \@axis_labels, #defaults to col_labels
24 'graph_type' => 'LinesPoints',
26 'sprintf' => '%u', #sprintf format, overrides default %.2f
30 About @links: Each element must be an arrayref, corresponding to an element of
31 @items. Within the array, the first element is a URL prefix, and the rest
32 are suffixes corresponding to data elements. These will be joined without
33 any delimiter and linked from the elements in @data.
36 % if ( $cgi->param('_type') =~ /^(csv)$/ ) {
38 % #http_header('Content-Type' => 'text/comma-separated-values' ); #IE chokes
39 % #http_header('Content-Type' => 'text/plain' );
40 % http_header('Content-Type' => 'text/csv');
41 % http_header('Content-Disposition' => "attachment;filename=$filename.csv");
43 % my $csv = new Text::CSV_XS { 'always_quote' => 1,
44 % 'eol' => "\n", #"\015\012", #"\012"
47 % $csv->combine('', @col_labels, $opt{'nototal'} ? () : 'Total');
51 % my @bottom_total = ();
52 % foreach ( @items ) {
57 % shift( @row_labels ),
58 % map { $total += $_; $bottom_total[$col++] += $_; sprintf($sprintf, $_); }
59 % ( @{ shift( @data ) } ),
60 % ( $opt{'nototal'} ? () : sprintf($sprintf, $total) ),
62 % unless ( $opt{'nototal'} ) {
63 % $bottom_total[$col++] += $total;
69 % if ( $opt{'bottom_total'} ) {
72 % map { sprintf($sprintf, $_) } @bottom_total,
79 % } elsif ( $cgi->param('_type') =~ /(xls)$/ ) {
81 % #http_header('Content-Type' => 'application/excel' ); #eww
82 % http_header('Content-Type' => 'application/vnd.ms-excel' );
83 % #http_header('Content-Type' => 'application/msexcel' ); #alas
84 % http_header('Content-Disposition' => "attachment;filename=$filename.xls");
87 % my $XLS = new IO::Scalar \$output;
88 % my $workbook = Spreadsheet::WriteExcel->new($XLS)
89 % or die "Error opening .xls file: $!";
91 % my $worksheet = $workbook->add_worksheet(substr($opt{'title'},0,31));
95 % foreach ('', @col_labels, ($opt{'nototal'} ? () : 'Total') ) {
97 % $worksheet->write($r, $c++, $header)
100 % my @bottom_total = ();
101 % foreach ( @items ) {
105 % $worksheet->write( $r, $c++, shift( @row_labels ) );
106 % foreach ( @{ shift( @data ) } ) {
108 % $bottom_total[$c-1] += $_;
109 % $worksheet->write($r, $c++, sprintf($sprintf, $_) );
111 % unless ( $opt{'nototal'} ) {
112 % $bottom_total[$c-1] += $total;
113 % $worksheet->write($r, $c++, sprintf($sprintf, $total) );
118 % if ( $opt{'bottom_total'} ) {
120 % $worksheet->write($r, $c++, 'Total');
121 % $worksheet->write($r, $c++, sprintf($sprintf, $_)) foreach @bottom_total;
124 % $workbook->close();# or die "Error creating .xls file: $!";
126 % http_header('Content-Length' => length($output) );
129 % } elsif ( $cgi->param('_type') eq 'png' ) {
131 % my $graph_type = 'LinesPoints';
132 % if ( $opt{'graph_type'} =~ /^(LinesPoints|Mountain|Bars)$/ ) {
135 % my $class = "Chart::$graph_type";
137 % my $chart = $class->new(976,384);
138 % # the chart area itself is 900 pixels wide, and the date labels are ~60 each.
139 % # staggered, we can fit about 28 of them.
140 % # they're about 12 pixels high, so vertically, we can fit about 60 (allowing
141 % # space for them to be readable).
142 % # after that we have to start skipping labels. also remove the dots, since
143 % # they're just a blob at that point.
144 % my $num_labels = scalar(@{ $opt{axis_labels} });
145 % my %chart_opt = %{ $opt{chart_options} || {} };
146 % if ( $num_labels > 28 ) {
147 % $chart_opt{x_ticks} = 'vertical';
148 % if ( $num_labels > 60 ) {
149 % $chart_opt{skip_x_ticks} = int($num_labels / 60) + 1;
150 % $chart_opt{pt_size} = 1;
156 % 'legend' => 'bottom',
158 % map { my $color = $_;
160 % [ map hex($_), unpack 'a2a2a2', $color ]
162 % @{ $opt{'colors'} }
164 % 'grey_background' => 'white',
165 % 'background' => [ 0xe8, 0xe8, 0xe8 ], #grey
167 % 'legend_labels' => $opt{'graph_labels'},
172 % http_header('Content-Type' => 'image/png' );
173 % http_header('Cache-Control' => 'no-cache' );
175 % $chart->_set_colors();
177 <% $chart->scalar_png([ $opt{'axis_labels'}, @data ]) %>
180 % # image and download links should use the cached data
181 % # just directly reference this component
182 % my $myself = $p.'graph/elements/report.html?session='.$session;
184 <% include('/elements/header.html', $opt{'title'} ) %>
185 % unless ( $opt{'graph_type'} eq 'none' ) {
187 <IMG SRC="<% "$myself;_type=png" %>" WIDTH="976" HEIGHT="384"
188 STYLE="page-break-after:always;">
190 <P ALIGN="right" CLASS="noprint">
192 % unless ( $opt{'disable_download'} ) {
193 Download full results<BR>
194 as <A HREF="<% "$myself;_type=xls" %>">Excel spreadsheet</A><BR>
195 as <A HREF="<% "$myself;_type=csv" %>">CSV file</A></P>
199 %# indexed by item, then by entry (the element indices of @{$data[$i]}).
202 % my $num_entries = scalar(@col_labels);
203 % my $num_items = scalar(@items);
204 % $cell[0] = ['']; #top left corner
205 % foreach my $column ( @col_labels ) {
206 % $column =~ s/ /\<BR\>/;
207 % push @{$cell[0]}, $column;
209 % if ( ! $opt{'nototal'} ) {
211 % push @{$cell[0]}, emt('Total');
214 % # i for item, e for entry
216 % foreach my $row ( @items ) {
218 % my $color = shift @{ $opt{'colors'} };
219 % push @styles, ".i$i { text-align: right; color: #$color; }";
220 % #create the data row
221 % my $links = shift @{$opt{'links'}} || [''];
222 % my $link_prefix = shift @$links;
223 % $link_prefix = '<A CLASS="cell" HREF="'.$link_prefix if $link_prefix;
224 % my $label = shift @row_labels;
225 % $cell[$i] = [ $label ];
227 % my $data_row = $data[$i-1];
228 %# my $data_row = shift @data;
229 % if ( ! $opt{'nototal'} ) {
230 % push @$data_row, sum(@$data_row);
232 % foreach ( @$data_row ) {
234 % $entry = $money_char . sprintf($sprintf, $entry);
235 % $entry = $link_prefix . shift(@$links) . "\">$entry</A>" if $link_prefix;
236 % push @{$cell[$i]}, $entry;
240 % if ( $opt{'bottom_total'} ) {
241 % # it's an extra item
243 % push @styles, ".i$i { text-align: right; background-color: #f5f6be; }";
244 % my $links = $opt{'bottom_link'} || [];
245 % my $link_prefix = shift @$links;
246 % $link_prefix = '<A CLASS="cell" HREF="'.$link_prefix if $link_prefix;
247 % $cell[$i] = [ emt('Total') ];
248 % for (my $e = 0; $e < $num_entries + 1; $e++) {
249 % my $entry = sum(map { $_->[$e] } @data);
250 % $entry = $money_char . sprintf($sprintf, $entry);
251 % $entry = $link_prefix . shift(@$links) . "\">$entry</A>" if $link_prefix;
252 % push @{$cell[$i]}, $entry;
256 <STYLE type="text/css">
258 color: inherit !important;
263 <% join("\n", @styles) %>
270 % if ( ! $opt{'nototal'} ) {
271 .e<% $num_entries %> {
273 background-color: #f5f6be;
283 <% include('/elements/table.html', 'f8f8f8') %>
284 % if ( $opt{'transpose'} ) {
285 % for ( my $e = 0; $e < $num_entries + 1; $e++ ) {
287 % for ( my $i = 0; $i < $num_items + 1; $i++ ) {
288 <TD CLASS="<%"cell i$i e$e"%>"><% $cell[$i][$e] %></TD>
293 % } else { #!transpose
295 % for (my $i = 0; $i < $num_items + 1; $i++) {
297 % for (my $e = 0; $e < $num_entries + 1; $e++) {
298 <TD CLASS="<%"cell i$i e$e"%>"><% $cell[$i][$e] %></TD>
305 <% include('/elements/footer.html') %>
314 # load from cache if possible, to avoid recalculating
315 if ( $cgi->param('session') =~ /^(\d+)$/ ) {
317 %opt = %{ $m->cache->get($session) };
320 $session = sprintf("%010d%06d", time, int(rand(1000000)));
321 $m->cache->set($session, \%opt, '1h');
324 my $sprintf = $opt{'sprintf'} || '%.2f';
326 my $conf = new FS::Conf;
327 my $money_char = $opt{'disable_money'} ? '' : $conf->config('money_char');
329 my @items = @{ $opt{'items'} };
331 foreach my $other (qw( col_labels row_labels graph_labels axis_labels colors links )) {
332 if ( ref($opt{$other}) eq 'HASH' ) {
333 $opt{$other} = [ map $opt{$other}{$_}, @items ];
337 my @col_labels = @{$opt{'col_labels'}};
338 my @row_labels = @{$opt{'row_labels'}};
339 my @data = @{$opt{'data'}};
341 $opt{'axis_labels'} ||= $opt{'col_labels'};
342 $opt{'graph_labels'} ||= $opt{'row_labels'};
344 my $filename = $cgi->url(-relative => 1);
345 $filename =~ s/\.(cgi|html)$//;