package TimeSeries; use File::Temp qw(tempfile); use Time::Local; use Data::Dumper; use HTTP::Date qw(parse_date); use Time::Local qw(timegm_nocheck); $VERSION = do { my @r=(q$Revision: 1.7 $=~/\d+/g);sprintf "%d."."%02d"x$#r,@r}; sub new { my ($class, %opts) = @_; my $self = {}; bless ($self, $class); $self->{data} = []; $self->{style} = "lines"; $self->{output_format} = "png"; return $self; } sub add { my ($self, $timestamp, @data) = @_; push(@{$self->{data}}, [ $timestamp, [ @data ] ]); #print Dumper($self); } sub add_timestring { my ($self, $timestring, @data) = @_; my ($year, $mon, $day, $hour, $min, $sec, $zone) = parse_date($timestring); # print STDERR "date = ($year, $mon, $day, $hour, $min, $sec, $zone)\n"; my $timestamp; if (defined ($zone)) { # adjust for timezone my ($zs, $zh, $zm) = $zone =~ /([+-])(\d\d)(\d\d)/; $min -= ($zs eq '-' ? -1 : +1) * ($zh * 60 + $zm); $timestamp = timegm_nocheck($sec, $min, $hour, $day, $mon-1, $year); } else { $timestamp = timelocal($sec, $min, $hour, $day, $mon-1, $year); } # print STDERR "\$timestamp = $timestamp\n"; $self->add($timestamp, @data); } sub legend { my ($self, @legend) = @_; my $oldlegend = $self->{legend}; $self->{legend} = [@legend] if (@legend); return $oldlegend ? @$oldlegend : (); } sub style { my ($self, $style) = @_; my $oldstyle = $self->{style}; $self->{style} = $style if ($style); return $oldstyle; } sub log_x { my ($self, $log_x) = @_; my $oldlog_x = $self->{log_x}; $self->{log_x} = $log_x if ($log_x); return $oldlog_x; } sub log_y { my ($self, $log_y) = @_; my $oldlog_y = $self->{log_y}; $self->{log_y} = $log_y if ($log_y); return $oldlog_y; } sub output_format { my ($self, $output_format) = @_; my $oldoutput_format = $self->{output_format}; $self->{output_format} = $output_format if ($output_format); return $oldoutput_format; } =head2 dstcorr $time [, $period] corrects for time shifts caused by DST switches by aligning the time to the given period in local time. Example: 1048989600 is 2003-03-30 00:00:00 CET. 4 hours (14400 seconds) later, the time is 2003-03-30 05:00:00 CEST. To get back to a 4 hour period starting at midnight, 1 hour needs to be subtracted, so C returns 1048989600, which is 2003-03-30 04:00:00 CEST. =cut sub dstcorr { my ($time, $period) = @_; $period = 24 * 3600 unless ($period); ($sec,$min,$hour,$mday,$mon,$year) = localtime($time); my $toff = ($hour * 3600 + $min * 60 * $sec) % $period; if ($toff != 0) { if ($toff > $period/2) { $toff -= $period; } print STDERR "correcting time by $toff seconds "; printf STDERR "from %04d-%02d-%02d %02d:%02d:%02d ", $year+1900, $mon+1, $mday, $hour, $min, $sec; $time -= $toff; ($sec,$min,$hour,$mday,$mon,$year) = localtime($time); printf STDERR "to %04d-%02d-%02d %02d:%02d:%02d\n", $year+1900, $mon+1, $mday, $hour, $min, $sec; } return $time; } sub plot { my ($self) = @_; #print Dumper($self); my ($datafh, $datafn) = tempfile(); for my $i (@{$self->{data}}) { my $time = $i->[0]; my $data = $i->[1]; print $datafh $time; for my $j (@$data) { print $datafh "\t", $j + 0; } print $datafh "\n"; } close($datafh); my ($ctlfh, $ctlfn) = tempfile(); my ($psfh, $psfn) = tempfile(); # generic settings print $ctlfh "set term postscript color\n"; print $ctlfh "set output '$psfn'\n"; print $ctlfh "set data style $self->{style}\n"; print $ctlfh "set grid\n"; print $ctlfh "set log x\n" if ($self->{log_x}); print $ctlfh "set log y\n" if ($self->{log_y}); # compute ticks # The spacing of the ticks a bit tricky: They should be related to # common time units (1 hour, 1 day, 1 week, ...), which are # irregular and not even of constant length (a day can be 23, 24 or # 25 hours, a month 28 to 31 days, a year 365 or 366 days). Also the # spacing shouldn't be too tight or too sparse. So there's quite a # bit of special-case code below (but also much code duplication # which should be cleaned up). my $firsttime = $self->{data}[0][0]; my $lasttime = $self->{data}[$#{$self->{data}}][0]; if ($lasttime - $firsttime > 3 * 365 * 24 * 3600) { # more than 3 years: 1 tick/year my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($firsttime); $sec = $min = $hour = 0; $mday = 1; $mon = int($mon/3) * 3; $firsttime = timelocal($sec,$min,$hour,$mday,$mon,$year); print $ctlfh "set xtics rotate ("; my $comma = 0; my $time; for (;;) { $time = timelocal($sec,$min,$hour,$mday,$mon,$year); if ($comma) { print $ctlfh ", "; } else { $comma = 1; } printf $ctlfh qq|"%04d-%02d-%02d" %d|, $year+1900, $mon+1, $mday, $time; $mon += 3; if ($mon >= 12) { $mon -= 12; $year++; } if ($time > $lasttime) {last} } $lasttime = $time; print $ctlfh ")\n"; } elsif ($lasttime - $firsttime > 3 * 30 * 24 * 3600) { # 3 to 36 months: 1 tick/month my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($firsttime); $sec = $min = $hour = 0; $mday = 1; $firsttime = timelocal($sec,$min,$hour,$mday,$mon,$year); print $ctlfh "set xtics rotate ("; my $comma = 0; my $time; for (;;) { $time = timelocal($sec,$min,$hour,$mday,$mon,$year); if ($comma) { print $ctlfh ", "; } else { $comma = 1; } printf $ctlfh qq|"%04d-%02d-%02d" %d|, $year+1900, $mon+1, $mday, $time; if (++$mon >= 12) { $mon = 0; $year++; } if ($time > $lasttime) {last} } $lasttime = $time; print $ctlfh ")\n"; } elsif ($lasttime - $firsttime > 30 * 24 * 3600) { # 30 ... 90 days: 1 tick/week. my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($firsttime); $firsttime -= 86400 * $wday; ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($firsttime); $sec = $min = $hour = 0; my $time = $firsttime = timelocal($sec,$min,$hour,$mday,$mon,$year); print $ctlfh "set xtics rotate ("; my $comma = 0; for (;;) { ($sec,$min,$hour,$mday,$mon,$year) = localtime($time); if ($comma) { print $ctlfh ", "; } else { $comma = 1; } printf $ctlfh qq|"%04d-%02d-%02d" %d|, $year+1900, $mon+1, $mday, $time; if ($time > $lasttime) {last} $time += 7 * 24 * 3600; $time = dstcorr($time); } $lasttime = $time; print $ctlfh ")\n"; } elsif ($lasttime - $firsttime > 10 * 24 * 3600) { # 10 .. 30 days: 1 tick per day. my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($firsttime); $sec = $min = $hour = 0; my $time = $firsttime = timelocal($sec,$min,$hour,$mday,$mon,$year); print $ctlfh "set xtics rotate ("; my $comma = 0; for (;;) { ($sec,$min,$hour,$mday,$mon,$year) = localtime($time); if ($comma) { print $ctlfh ", "; } else { $comma = 1; } printf $ctlfh qq|"%04d-%02d-%02d" %d|, $year+1900, $mon+1, $mday, $time; if ($time > $lasttime) {last} $time += 24 * 3600; $time = dstcorr($time); } $lasttime = $time; print $ctlfh ")\n"; } elsif ($lasttime - $firsttime > 2 * 24 * 3600) { # 2 .. 10 days: 1 tick/4 hours my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($firsttime); $sec = $min = $hour = 0; my $time = $firsttime = timelocal($sec,$min,$hour,$mday,$mon,$year); print $ctlfh "set xtics rotate ("; my $comma = 0; for (;;) { ($sec,$min,$hour,$mday,$mon,$year) = localtime($time); if ($comma) { print $ctlfh ", "; } else { $comma = 1; } printf $ctlfh qq|"%04d-%02d-%02d %02d:%02d" %d|, $year+1900, $mon+1, $mday, $hour, $min, $time; if ($time > $lasttime) {last} $time += 4 * 3600; $time = dstcorr($time, 4 * 3600); } $lasttime = $time; print $ctlfh ")\n"; } else { # less than 2 days: 1 tick per hour. my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($firsttime); $sec = $min = 0; my $time = $firsttime = timelocal($sec,$min,$hour,$mday,$mon,$year); print $ctlfh "set xtics rotate ("; my $comma = 0; for (;;) { ($sec,$min,$hour,$mday,$mon,$year) = localtime($time); if ($comma) { print $ctlfh ", "; } else { $comma = 1; } printf $ctlfh qq|"%04d-%02d-%02d %02d:%02d" %d|, $year+1900, $mon+1, $mday, $hour, $min, $time; if ($time > $lasttime) {last} $time += 3600; } $lasttime = $time; print $ctlfh ")\n"; } # what to plot print $ctlfh "plot "; $comma = 0; $col = 2; for $i (@{$self->{legend}}) { if ($comma) { print $ctlfh ", "; } else { $comma = 1; } print $ctlfh "'$datafn' using 1:", $col++, " title '$i'"; } print $ctlfh "\n"; close ($ctlfh); my $rc = system("gnuplot", $ctlfn); #print STDERR "system returned $rc\n"; my $pipe; if ($self->{output_format} eq "ps") { $pipe = "< $psfn"; } else { $pipe = "gs -sDEVICE=ppmraw -r150 -dBATCH -sOutputFile=- -q - < $psfn |" . "pnmscale 0.5 |" . "pnmflip -cw |" . "pnmcrop 2> /dev/null |"; } if ($self->{output_format} eq "png") { $pipe .= "pnmtopng |"; } if ($self->{output_format} eq "gif") { # the ppm tools are noisy. Shut them up. $pipe .= "ppmquant 256 2> /dev/null |" . "ppmtogif 2> /dev/null |"; } if ($self->{output_format} eq "jpeg") { $pipe .= "cjpeg -sample 1x1,1x1,1x1 |"; } open(PNG, $pipe); my $graph; { local $/ = undef; $graph = ; } close(PNG); return $graph; } 1;