]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/reporter/clark-kent.pl
pile of reporting updates
[working/Evergreen.git] / Open-ILS / src / reporter / clark-kent.pl
1 #!/usr/bin/perl -w
2
3 use strict;
4 use diagnostics;
5 use DBI;
6 use FileHandle;
7 use XML::LibXML;
8 use Getopt::Long;
9 use DateTime;
10 use DateTime::Format::ISO8601;
11 use JSON;
12 use Data::Dumper;
13 use Text::CSV_XS;
14 use Spreadsheet::WriteExcel::Big;
15 use OpenSRF::EX qw/:try/;
16 use OpenSRF::Utils qw/:daemon/;
17 #use OpenSRF::Utils::Logger qw/:level/;
18 use OpenSRF::System;
19 use OpenSRF::AppSession;
20 use OpenSRF::Utils::SettingsClient;
21 use OpenILS::Reporter::SQLBuilder;
22 use POSIX;
23 use GD::Graph::pie;
24 use GD::Graph::bars3d;
25 use GD::Graph::lines3d;
26 use Tie::IxHash;
27 use Email::Send;
28
29 use open ':utf8';
30
31
32 my ($count, $config, $sleep_interval, $lockfile, $daemon) = (1, '/openils/conf/bootstrap.conf', 10, '/tmp/reporter-LOCK');
33
34 GetOptions(
35         "daemon"        => \$daemon,
36         "sleep"         => \$sleep_interval,
37         "concurrency=i" => \$count,
38         "boostrap=s"    => \$config,
39         "lockfile=s"    => \$lockfile,
40 );
41
42 if (-e $lockfile) {
43         die "I seem to be running already. If not remove $lockfile, try again\n";
44 }
45
46 OpenSRF::System->bootstrap_client( config_file => $config );
47
48 # XXX Get this stuff from the settings server
49 my $sc = OpenSRF::Utils::SettingsClient->new;
50 my $db_driver = $sc->config_value( reporter => setup => database => 'driver' );
51 my $db_host = $sc->config_value( reporter => setup => database => 'host' );
52 my $db_port = $sc->config_value( reporter => setup => database => 'port' );
53 my $db_name = $sc->config_value( reporter => setup => database => 'name' );
54 my $db_user = $sc->config_value( reporter => setup => database => 'user' );
55 my $db_pw = $sc->config_value( reporter => setup => database => 'password' );
56
57 my $email_server = $sc->config_value( email_notify => 'smtp_server' );
58 my $email_sender = $sc->config_value( email_notify => 'sender_address' );
59 my $success_template = $sc->config_value( reporter => setup => files => 'success_template' );
60 my $fail_template = $sc->config_value( reporter => setup => files => 'fail_template' );
61
62 my $output_base = $sc->config_value( reporter => setup => files => 'output_base' );
63
64 my $base_uri = $sc->config_value( reporter => setup => 'base_uri' );
65
66 my $dsn = "dbi:" . $db_driver . ":dbname=" . $db_name .';host=' . $db_host . ';port=' . $db_port;
67
68 my ($dbh,$running,$sth,@reports,$run, $current_time);
69
70 if ($daemon) {
71         open(F, ">$lockfile");
72         print F $$;
73         close F;
74         daemonize("Clark Kent, waiting for trouble");
75 }
76
77
78 DAEMON:
79
80 $dbh = DBI->connect($dsn,$db_user,$db_pw, {pg_enable_utf8 => 1, RaiseError => 1});
81
82 $current_time = DateTime->from_epoch( epoch => time() )->strftime('%FT%T%z');
83
84 # make sure we're not already running $count reports
85 ($running) = $dbh->selectrow_array(<<SQL);
86 SELECT  count(*)
87   FROM  reporter.schedule
88   WHERE start_time IS NOT NULL AND complete_time IS NULL;
89 SQL
90
91 if ($count <= $running) {
92         if ($daemon) {
93                 $dbh->disconnect;
94                 sleep 1;
95                 POSIX::waitpid( -1, POSIX::WNOHANG );
96                 sleep $sleep_interval;
97                 goto DAEMON;
98         }
99         print "Already running maximum ($running) concurrent reports\n";
100         exit 1;
101 }
102
103 # if we have some open slots then generate the sql
104 $run = $count - $running;
105
106 $sth = $dbh->prepare(<<SQL);
107 SELECT  *
108   FROM  reporter.schedule
109   WHERE start_time IS NULL AND run_time < NOW()
110   ORDER BY run_time
111   LIMIT $run;
112 SQL
113
114 $sth->execute;
115
116 @reports = ();
117 while (my $r = $sth->fetchrow_hashref) {
118         my $s3 = $dbh->selectrow_hashref(<<"    SQL", {}, $r->{report});
119                 SELECT * FROM reporter.report WHERE id = ?;
120         SQL
121
122         my $s2 = $dbh->selectrow_hashref(<<"    SQL", {}, $s3->{template});
123                 SELECT * FROM reporter.template WHERE id = ?;
124         SQL
125
126         $s3->{template} = $s2;
127         $r->{report} = $s3;
128
129         my $b = OpenILS::Reporter::SQLBuilder->new;
130         $b->register_params( JSON->JSON2perl( $r->{report}->{data} ) );
131
132         $r->{resultset} = $b->parse_report( JSON->JSON2perl( $r->{report}->{template}->{data} ) );
133         $r->{resultset}->relative_time($r->{run_time});
134         push @reports, $r;
135 }
136
137 $sth->finish;
138
139 $dbh->disconnect;
140
141 # Now we spaun the report runners
142
143 for my $r ( @reports ) {
144         next if (safe_fork());
145
146         # This is the child (runner) process;
147         daemonize("Clark Kent reporting: $r->{report}->{name}");
148
149         $dbh = DBI->connect($dsn,$db_user,$db_pw, {pg_enable_utf8 => 1, RaiseError => 1});
150
151         try {
152                 $dbh->do(<<'            SQL',{}, $r->{id});
153                         UPDATE  reporter.schedule
154                           SET   start_time = now()
155                           WHERE id = ?;
156                 SQL
157
158                 $sth = $dbh->prepare($r->{resultset}->toSQL);
159
160                 $sth->execute;
161                 $r->{data} = $sth->fetchall_arrayref;
162
163                 $r->{column_labels} = [$r->{resultset}->column_label_list];
164
165                 if ($r->{resultset}->pivot_data && $r->{resultset}->pivot_label) {
166                         my @labels = $r->{resultset}->column_label_list;
167                         my $newdata = pivot_data(
168                                 { columns => $r->{column_labels}, data => $r->{data}},
169                                 $r->{resultset}->pivot_label,
170                                 $r->{resultset}->pivot_data,
171                                 $r->{resultset}->pivot_default
172                         );
173
174                         $r->{column_labels} = $newdata->{columns};
175                         $r->{data} = $newdata->{data};
176                         $r->{group_by_list} = $newdata->{group_by_list};
177                 } else {
178                         $r->{group_by_list} = [$r->{resultset}->group_by_list(0)];
179                 }
180
181                 my $s2 = $r->{report}->{template}->{id};
182                 my $s3 = $r->{report}->{id};
183                 my $output = $r->{id};
184
185                 mkdir($output_base);
186                 mkdir("$output_base/$s2");
187                 mkdir("$output_base/$s2/$s3");
188                 mkdir("$output_base/$s2/$s3/$output");
189         
190                 my $output_dir = "$output_base/$s2/$s3/$output";
191
192                 if ( $r->{csv_format} ) {
193                         build_csv("$output_dir/report-data.csv", $r);
194                 }
195
196                 if ( $r->{excel_format} ) {
197                         build_excel("$output_dir/report-data.xls", $r);
198                 }
199
200                 build_html("$output_dir/report-data.html", $r);
201
202                 $dbh->begin_work;
203
204                 if ($r->{report}->{recur} ) {
205                         my $sql = <<'                   SQL';
206                                 INSERT INTO reporter.schedule (
207                                                 report,
208                                                 folder,
209                                                 runner,
210                                                 run_time,
211                                                 email,
212                                                 csv_format,
213                                                 excel_format,
214                                                 html_format,
215                                                 chart_pie,
216                                                 chart_bar,
217                                                 chart_line )
218                                         VALUES ( ?, ?, ?, ?::TIMESTAMPTZ + ?, ?, ?, ?, ?, ?, ?, ? );
219                         SQL
220
221                         $dbh->do(
222                                 $sql,
223                                 {},
224                                 $r->{report}->{id},
225                                 $r->{folder},
226                                 $r->{runner},
227                                 $r->{run_time},
228                                 $r->{report}->{recurance},
229                                 $r->{email},
230                                 $r->{csv_format},
231                                 $r->{excel_format},
232                                 $r->{html_format},
233                                 $r->{chart_pie},
234                                 $r->{chart_bar},
235                                 $r->{chart_line},
236                         );
237                 }
238
239                 $dbh->do(<<'            SQL',{}, $r->{id});
240                         UPDATE  reporter.schedule
241                           SET   complete_time = now()
242                           WHERE id = ?;
243                 SQL
244
245                 $dbh->commit;
246
247                 my $new_r = $dbh->selectrow_hashref(<<"         SQL", {}, $r->{id});
248                         SELECT * FROM reporter.schedule WHERE id = ?;
249                 SQL
250
251                 $r->{start_time} = $new_r->{start_time};
252                 $r->{complete_time} = $new_r->{complete_time};
253
254                 if ($r->{email}) {
255                         send_success($r);
256                 }
257
258         } otherwise {
259                 my $e = shift;
260                 $r->{error_text} = ''.$e;
261                 $dbh->rollback;
262                 $dbh->do(<<'            SQL',{}, $e, $r->{id});
263                         UPDATE  reporter.schedule
264                           SET   error_text = ?,
265                                 complete_time = now(),
266                                 error_code = 1
267                           WHERE id = ?;
268                 SQL
269
270                 my $new_r = $dbh->selectrow_hashref(<<"         SQL", {}, $r->{id});
271                         SELECT * FROM reporter.schedule WHERE id = ?;
272                 SQL
273
274                 $r->{error_text} = $new_r->{error_text};
275                 $r->{complete_time} = $new_r->{complete_time};
276
277                 if ($r->{email}) {
278                         send_fail($r);
279                 }
280
281         };
282
283         $dbh->disconnect;
284
285         exit; # leave the child
286 }
287
288 if ($daemon) {
289         sleep 1;
290         POSIX::waitpid( -1, POSIX::WNOHANG );
291         sleep $sleep_interval;
292         goto DAEMON;
293 }
294
295 #-------------------------------------------------------------------
296
297 sub send_success {
298         my $r = shift;
299         open F, $success_template;
300         my $tmpl = join('',<F>);
301         close F;
302
303         my $url = $base_uri . '/' .
304                 $r->{report}->{template}->{id} . '/' .
305                 $r->{report}->{id} . '/' .
306                 $r->{id} . '/report-data.html';
307
308         $tmpl =~ s/{TO}/$r->{email}/smog;
309         $tmpl =~ s/{FROM}/$email_sender/smog;
310         $tmpl =~ s/{REPLY_TO}/$email_sender/smog;
311         $tmpl =~ s/{REPORT_NAME}/$r->{report}->{template}->{name} -- $r->{report}->{name}/smog;
312         $tmpl =~ s/{RUN_TIME}/$r->{run_time}/smog;
313         $tmpl =~ s/{COMPLETE_TIME}/$r->{complete_time}/smog;
314         $tmpl =~ s/{OUTPUT_URL}/$url/smog;
315
316         my $sender = Email::Send->new({mailer => 'SMTP'});
317         $sender->mailer_args([Host => $email_server]);
318         $sender->send($tmpl);
319 }
320
321 sub send_fail {
322         my $r = shift;
323         open F, $fail_template;
324         my $tmpl = join('',<F>);
325         close F;
326
327         my $sql = $r->{resultset}->toSQL;
328
329         $tmpl =~ s/{TO}/$r->{email}/smog;
330         $tmpl =~ s/{FROM}/$email_sender/smog;
331         $tmpl =~ s/{REPLY_TO}/$email_sender/smog;
332         $tmpl =~ s/{REPORT_NAME}/$r->{report}->{template}->{name} -- $r->{report}->{name}/smog;
333         $tmpl =~ s/{RUN_TIME}/$r->{run_time}/smog;
334         $tmpl =~ s/{ERROR_TEXT}/$r->{error_text}/smog;
335         $tmpl =~ s/{SQL}/$sql/smog;
336
337         my $sender = Email::Send->new({mailer => 'SMTP'});
338         $sender->mailer_args([Host => $email_server]);
339         $sender->send($tmpl);
340 }
341
342 sub build_csv {
343         my $file = shift;
344         my $r = shift;
345
346         my $csv = Text::CSV_XS->new({ always_quote => 1, eol => "\015\012" });
347
348         return unless ($csv);
349         
350         my $f = new FileHandle (">$file");
351
352         $csv->print($f, $r->{column_labels});
353         $csv->print($f, $_) for (@{$r->{data}});
354
355         $f->close;
356 }
357 sub build_excel {
358         my $file = shift;
359         my $r = shift;
360         my $xls = Spreadsheet::WriteExcel::Big->new($file);
361
362         my $sheetname = substr($r->{report}->{name},1,31);
363         $sheetname =~ s/\W/_/gos;
364         
365         my $sheet = $xls->add_worksheet($sheetname);
366
367         $sheet->write_row('A1', $r->{column_labels});
368
369         $sheet->write_col('A2', $r->{data});
370
371         $xls->close;
372 }
373
374 sub build_html {
375         my $file = shift;
376         my $r = shift;
377
378         my $index = new FileHandle (">$file");
379         
380         # index header
381         print $index <<"        HEADER";
382 <html>
383         <head>
384                 <title>$$r{report}{name}</title>
385                 <style>
386                         table { border-collapse: collapse; }
387                         th { background-color: lightgray; }
388                         td,th { border: solid black 1px; }
389                         * { font-family: sans-serif; font-size: 10px; }
390                 </style>
391         </head>
392         <body>
393                 <center>
394                 <h2><u>$$r{report}{name}</u></h2>
395                 $$r{report}{description}<br/><br/><br/>
396         HEADER
397
398         my @links;
399
400         # add a link to the raw output html
401         push @links, "<a href='report-data.html.raw.html'>Tabular Output</a>" if ($r->{html_format});
402
403         # add a link to the CSV output
404         push @links, "<a href='report-data.xls'>Excel Output</a>" if ($r->{excel_format});
405
406         # add a link to the CSV output
407         push @links, "<a href='report-data.csv'>CSV Output</a>" if ($r->{csv_format});
408
409         print $index join(' -- ', @links);
410         print $index "<br/><br/><br/><br/></center>";
411
412         if ($r->{html_format}) {
413                 # create the raw output html file
414                 my $raw = new FileHandle (">$file.raw.html");
415                 print $raw "<html><head><title>$$r{report}{name}</title>";
416
417                 print $raw <<'          CSS';
418                         <style>
419                                 table { border-collapse: collapse; }
420                                 th { background-color: lightgray; }
421                                 td,th { border: solid black 1px; }
422                                 * { font-family: sans-serif; font-size: 10px; }
423                         </style>
424                 CSS
425
426                 print $raw "</head><body><table>";
427
428                 {       no warnings;
429                         print $raw "<tr><th>".join('</th><th>',@{$r->{column_labels}}).'</th></tr>';
430                         print $raw "<tr><td>".join('</td><td>',@$_                   ).'</td></tr>' for (@{$r->{data}});
431                 }
432
433                 print $raw '</table></body></html>';
434         
435                 $raw->close;
436         }
437
438         # Time for a pie chart
439         if ($r->{chart_pie}) {
440                 my $pics = draw_pie($r, $file);
441                 for my $pic (@$pics) {
442                         print $index "<img src='report-data.html.$pic->{file}' alt='$pic->{name}'/><br/><br/><br/><br/>";
443                 }
444         }
445
446         print $index '<br/><br/><br/><br/>';
447         # Time for a bar chart
448         if ($r->{chart_bar}) {
449                 my $pics = draw_bars($r, $file);
450                 for my $pic (@$pics) {
451                         print $index "<img src='report-data.html.$pic->{file}' alt='$pic->{name}'/><br/><br/><br/><br/>";
452                 }
453         }
454
455         print $index '<br/><br/><br/><br/>';
456         # Time for a bar chart
457         if ($r->{chart_line}) {
458                 my $pics = draw_lines($r, $file);
459                 for my $pic (@$pics) {
460                         print $index "<img src='report-data.html.$pic->{file}' alt='$pic->{name}'/><br/><br/><br/><br/>";
461                 }
462         }
463
464         # and that's it!
465         print $index '</body></html>';
466         
467         $index->close;
468 }
469
470 sub draw_pie {
471         my $r = shift;
472         my $file = shift;
473
474         my $data = $r->{data};
475
476         my @groups = @{ $r->{group_by_list} };
477         
478         my @values = (0 .. (scalar(@{$r->{column_labels}}) - 1));
479         delete @values[@groups];
480
481         #my $logo = $doc->findvalue('/reporter/setup/files/chart_logo');
482         
483         my @pics;
484         for my $vcol (@values) {
485                 next unless (defined $vcol);
486
487                 my @pic_data = ([],[]);
488                 for my $row (@$data) {
489                         next if (!defined($$row[$vcol]) || $$row[$vcol] == 0);
490                         my $val = $$row[$vcol];
491                         push @{$pic_data[0]}, join(" -- ", @$row[@groups])." ($val)";
492                         push @{$pic_data[1]}, $val;
493                 }
494
495                 next unless (@{$pic_data[0]});
496
497                 my $size = 300;
498                 my $split = int(scalar(@{$pic_data[0]}) / $size);
499                 my $last = scalar(@{$pic_data[0]}) % $size;
500
501                 for my $sub_graph (0 .. $split) {
502                         
503                         if ($sub_graph == $split) {
504                                 $size = $last;
505                         }
506
507                         my @sub_data;
508                         for my $set (@pic_data) {
509                                 push @sub_data, [ splice(@$set,0,$size) ];
510                         }
511
512                         my $pic = new GD::Graph::pie;
513
514                         $pic->set(
515                                 label                   => $r->{column_labels}->[$vcol],
516                                 start_angle             => 180,
517                                 legend_placement        => 'R',
518                                 #logo                   => $logo,
519                                 #logo_position          => 'TL',
520                                 #logo_resize            => 0.5,
521                                 show_values             => 1,
522                         );
523
524                         my $format = $pic->export_format;
525
526                         open(IMG, ">$file.pie.$vcol.$sub_graph.$format");
527                         binmode IMG;
528
529                         my $forgetit = 0;
530                         try {
531                                 $pic->plot(\@sub_data) or die $pic->error;
532                                 print IMG $pic->gd->$format;
533                         } otherwise {
534                                 my $e = shift;
535                                 warn "Couldn't draw $file.pie.$vcol.$sub_graph.$format : $e";
536                                 $forgetit = 1;
537                         };
538
539                         close IMG;
540
541
542                         push @pics,
543                                 { file => "pie.$vcol.$sub_graph.$format",
544                                   name => $r->{column_labels}->[$vcol].' (Pie)',
545                                 } unless ($forgetit);
546
547                         last if ($sub_graph == $split);
548                 }
549
550         }
551         
552         return \@pics;
553 }
554
555 sub draw_bars {
556         my $r = shift;
557         my $file = shift;
558         my $data = $r->{data};
559
560         #my $logo = $doc->findvalue('/reporter/setup/files/chart_logo');
561
562         my @groups = @{ $r->{group_by_list} };
563
564         
565         my @values = (0 .. (scalar(@{$r->{column_labels}}) - 1));
566         splice(@values,$_,1) for (reverse @groups);
567
568         my @pic_data;
569         {       no warnings;
570                 for my $row (@$data) {
571                         push @{$pic_data[0]}, join(' -- ', @$row[@groups]);
572                 }
573         }
574
575         my @leg;
576         my $set = 1;
577
578         my %trim_candidates;
579
580         my $max_y = 0;
581         for my $vcol (@values) {
582                 next unless (defined $vcol);
583                 $pic_data[$set] ||= [];
584
585                 my $pos = 0;
586                 for my $row (@$data) {
587                         my $val = $$row[$vcol] ? $$row[$vcol] : 0;
588                         push @{$pic_data[$set]}, $val;
589                         $max_y = $val if ($val > $max_y);
590                         $trim_candidates{$pos}++ if ($val == 0);
591                         $pos++;
592                 }
593
594                 $set++;
595         }
596         my $set_count = scalar(@pic_data) - 1;
597         my @trim_cols = grep { $trim_candidates{$_} == $set_count } keys %trim_candidates;
598
599         my @new_data;
600         my @use_me;
601         my @no_use;
602         my $set_index = 0;
603         for my $dataset (@pic_data) {
604                 splice(@$dataset,$_,1) for (reverse sort @trim_cols);
605
606                 if (grep { $_ } @$dataset) {
607                         push @new_data, $dataset;
608                         push @use_me, $set_index if ($set_index > 0);
609                 } else {
610                         push @no_use, $set_index;
611                 }
612                 $set_index++;
613                 
614         }
615
616         return [] unless ($new_data[0] && @{$new_data[0]});
617
618         for my $col (@use_me) {
619                 push @leg, $r->{column_labels}->[$values[$col - 1]];
620         }
621
622         my $w = 100 + 10 * scalar(@{$new_data[0]});
623         $w = 400 if ($w < 400);
624
625         my $h = 10 * (scalar(@new_data) / 2);
626
627         $h = 0 if ($h < 0);
628
629         my $pic = new GD::Graph::bars3d ($w + 250, $h + 500);
630
631         $pic->set(
632                 title                   => $r->{report}{name},
633                 x_labels_vertical       => 1,
634                 shading                 => 1,
635                 bar_depth               => 5,
636                 bar_spacing             => 2,
637                 y_max_value             => $max_y,
638                 legend_placement        => 'TR',
639                 boxclr                  => 'lgray',
640                 #logo                   => $logo,
641                 #logo_position          => 'R',
642                 #logo_resize            => 0.5,
643                 show_values             => 1,
644                 overwrite               => 1,
645         );
646         $pic->set_legend(@leg);
647
648         my $format = $pic->export_format;
649
650         open(IMG, ">$file.bar.$format");
651         binmode IMG;
652
653         try {
654                 $pic->plot(\@new_data) or die $pic->error;
655                 print IMG $pic->gd->$format;
656         } otherwise {
657                 my $e = shift;
658                 warn "Couldn't draw $file.bar.$format : $e";
659         };
660
661         close IMG;
662
663         return [{ file => "bar.$format",
664                   name => $r->{report}{name}.' (Bar)',
665                 }];
666
667 }
668
669 sub draw_lines {
670         my $r = shift;
671         my $file = shift;
672         my $data = $r->{data};
673
674         #my $logo = $doc->findvalue('/reporter/setup/files/chart_logo');
675
676         my @groups = @{ $r->{group_by_list} };
677         
678         my @values = (0 .. (scalar(@{$r->{column_labels}}) - 1));
679         splice(@values,$_,1) for (reverse @groups);
680
681         my @pic_data;
682         {       no warnings;
683                 for my $row (@$data) {
684                         push @{$pic_data[0]}, join(' -- ', @$row[@groups]);
685                 }
686         }
687
688         my @leg;
689         my $set = 1;
690
691         my $max_y = 0;
692         for my $vcol (@values) {
693                 next unless (defined $vcol);
694                 $pic_data[$set] ||= [];
695
696
697                 for my $row (@$data) {
698                         my $val = $$row[$vcol] ? $$row[$vcol] : 0;
699                         push @{$pic_data[$set]}, $val;
700                         $max_y = $val if ($val > $max_y);
701                 }
702
703                 $set++;
704         }
705         my $set_count = scalar(@pic_data) - 1;
706
707         my @new_data;
708         my @use_me;
709         my @no_use;
710         my $set_index = 0;
711         for my $dataset (@pic_data) {
712
713                 if (grep { $_ } @$dataset) {
714                         push @new_data, $dataset;
715                         push @use_me, $set_index if ($set_index > 0);
716                 } else {
717                         push @no_use, $set_index;
718                 }
719                 $set_index++;
720                 
721         }
722
723         for my $col (@use_me) {
724                 push @leg, $r->{column_labels}->[$values[$col - 1]];
725         }
726
727         my $w = 100 + 10 * scalar(@{$new_data[0]});
728         $w = 400 if ($w < 400);
729
730         my $h = 10 * (scalar(@new_data) / 2);
731
732         $h = 0 if ($h < 0);
733
734         my $pic = new GD::Graph::lines3d ($w + 250, $h + 500);
735
736         $pic->set(
737                 title                   => $r->{report}{name},
738                 x_labels_vertical       => 1,
739                 shading                 => 1,
740                 line_depth              => 5,
741                 y_max_value             => $max_y,
742                 legend_placement        => 'TR',
743                 boxclr                  => 'lgray',
744                 #logo                   => $logo,
745                 #logo_position          => 'R',
746                 #logo_resize            => 0.5,
747                 show_values             => 1,
748                 overwrite               => 1,
749         );
750         $pic->set_legend(@leg);
751
752         my $format = $pic->export_format;
753
754         open(IMG, ">$file.line.$format");
755         binmode IMG;
756
757         try {
758                 $pic->plot(\@new_data) or die $pic->error;
759                 print IMG $pic->gd->$format;
760         } otherwise {
761                 my $e = shift;
762                 warn "Couldn't draw $file.line.$format : $e";
763         };
764
765         close IMG;
766
767         return [{ file => "line.$format",
768                   name => $r->{report}{name}.' (Bar)',
769                 }];
770
771 }
772
773
774 sub pivot_data {
775         my $blob = shift;
776         my $pivot_label = shift;
777         my $pivot_data = shift;
778         my $default = shift;
779         $default = 0 unless (defined $default);
780
781         my $data = $$blob{data};
782         my $cols = $$blob{columns};
783
784         my @keep_labels =  @$cols;
785         splice(@keep_labels, $_ - 1, 1) for (reverse sort ($pivot_label, $pivot_data));
786
787         my @keep_cols = (0 .. @$cols - 1);
788         splice(@keep_cols, $_ - 1, 1) for (reverse sort ($pivot_label, $pivot_data));
789
790         my @gb = ( 0 .. @keep_cols - 1);
791
792         #first, find the unique list of pivot values
793         my %tmp;
794         for my $row (@$data) {
795                 $tmp{ $$row[$pivot_label - 1] } = 1;
796         }
797         my @new_cols = sort keys %tmp;
798
799         tie my %split_data, 'Tie::IxHash';
800         for my $row (@$data) {
801
802                 my $row_fp = ''. join('', map { defined($$row[$_]) ? $$row[$_] : '' } @keep_cols);
803                 $split_data{$row_fp} ||= [];
804
805                 push @{ $split_data{$row_fp} }, $row;
806         }
807
808
809         #now loop over the data, building a new result set
810         tie my %new_data, 'Tie::IxHash';
811
812         for my $fp ( keys %split_data ) {
813
814                 $new_data{$fp} = [];
815
816                 for my $col (@keep_cols) {
817                         push @{ $new_data{$fp} }, $split_data{$fp}[0][$col];
818                 }
819
820                 for my $col (@new_cols) {
821
822                         my ($datum) = map { $_->[$pivot_data - 1] } grep { $_->[$pivot_label - 1] eq $col } @{ $split_data{$fp} };
823                         $datum ||= $default;
824                         push @{ $new_data{$fp} }, $datum;
825                 }
826         }
827
828         push @keep_labels, @new_cols;
829
830         return { columns => \@keep_labels, data => [ values %new_data ], group_by_list => \@gb };
831 }
832
833