The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#$Id: Plotter.pm 516 2006-09-28 14:59:28Z schroeer $

package Lab::Data::Plotter;

use strict;
use Lab::Data::Meta;
use Data::Dumper;
use Time::HiRes qw/gettimeofday tv_interval/;

our $VERSION = sprintf("1.%04d", q$Revision: 516 $ =~ / (\d+) /);

sub new {
    my $proto = shift;
    my $class = ref($proto) || $proto;
    my ($main_meta,$options,@other)=@_;
    my @other_metas;
    for my $meta ($main_meta,@other) {
        unless (ref $meta eq 'Lab::Data::Meta') {
            for ("",qw/.META META .meta meta/) {
                if (-e $meta.$_) {
                    $meta=$meta.$_;
                    last;
                }
            }
            die "Metafile $meta does not exist!" unless (-e $meta);
            $meta=Lab::Data::Meta->new_from_file($meta);
        }
        push @other_metas,$meta;
    }
    
    $main_meta=shift @other_metas;
    
    my $self = bless {
        meta    => $main_meta,
        options => $options,
        other_metas => \@other_metas,
    }, $class;
    
    return $self;
}

sub start_live_plot {
    my ($self,$plot,$interval)=@_;
    $self->{live_plot}->{pipe}=$self->_start_plot($plot);
    $self->{live_plot}->{plot}=$plot;
    $self->{live_plot}->{refresh}=$interval;
    $self->{live_plot}->{last}=[gettimeofday];
}

sub update_live_plot {
    my $self=shift;
    return unless (defined $self->{live_plot});
    
    return if (($self->{live_plot}->{refresh}) && 
               (tv_interval($self->{live_plot}->{last},[gettimeofday]) < 
               ($self->{live_plot}->{refresh})));
               
    $self->{live_plot}->{last}=[gettimeofday];

    $self->_plot($self->{live_plot}->{pipe},$self->{live_plot}->{plot});
}        

sub stop_live_plot {
    my $self=shift;
    return unless (defined $self->{live_plot});

    close $self->{live_plot}->{pipe};
    undef $self->{live_plot};
}

sub plot {
    my ($self,$plot)=@_;
        
    die "Plot what?" unless ($self->{meta} && $plot);
    
    my $gpipe=$self->_start_plot($plot);
    $self->_plot($gpipe,$plot);
    
    return $gpipe;
}

sub _start_plot {
    my ($self,$plot)=@_;
    die "plot \"$plot\" undefined" unless (defined($self->{meta}->plot($plot)));

    my $gpipe;
    if ($self->{options}->{dump}) {
        open $gpipe,">".$self->{options}->{dump} or die "cannot open gnuplot dump file ".$self->{options}->{dump};
    } else {
        $gpipe=$self->get_gnuplot_pipe();
    }
    
    my $gp="";
    $gp.="# Encoding of this file\n";
    $gp.="set encoding iso_8859_1\n";
    
    if ($self->{options}->{eps}) {
        $gp.="#\n# Output to file\n";
        $gp.="set terminal postscript color enhanced 10\n";
        $gp.=qq(set output ").$self->{options}->{eps}.qq("\n);
    }
    
    if ($self->{meta}->plot_type($plot) eq 'pm3d') {
        $gp.="#\n# Set color plot\n";
        $gp.="set pm3d map corners2color c1\n";
        $gp.="set view map\n";
        if ($self->{meta}->plot_palette($plot)) {
            $gp.="set palette ".$self->{meta}->plot_palette($plot)."\n";
        }
    }
    
    #Quatsch. Aussen über Files loopen, dann über Konstanten
    if ($self->{meta}->constant()) {
        $gp.="#\n# Constants\n" ;
        for (@{$self->{meta}->constant()}) {
            unless ($self->{options}->{multiple}) {
                $gp.=($_->{name})."=".($_->{value})."\n";
            } else {
                my $num=0;
                for my $meta ($self->{meta},@{$self->{othermetas}}) {
                    $gp.=($_->{name})."$num++=".($_->{value})."\n";
                }
            }
        }
    }
    
    my $xaxis=$self->{meta}->plot_xaxis($plot);
    my $yaxis=$self->{meta}->plot_yaxis($plot);
    my $zaxis=$self->{meta}->plot_zaxis($plot);
    my $cbaxis=$self->{meta}->plot_cbaxis($plot);
    
    $gp.="#\n# Axis labels\n";
    for my $i (qw/x y z cb/) {
        my $axisname="plot_".$i."axis";
        my $metaaxis=$self->{meta}->$axisname($plot);
        if (defined $metaaxis) {
            my $label='"'.
                      ($self->{meta}->axis_label($metaaxis)).
                      ' ('.($self->{meta}->axis_unit($metaaxis)).")".
                      ($self->{options}->{fulllabels} ?
                        ('\n'.$self->{meta}->axis_description($metaaxis)) :
                        ''
                      )."\"\n";
            $gp.="set ".$i."label ".$label;
        }
    }

    if (defined $self->{meta}->plot_grid($plot)) {
        $gp.="#\n# Grid\n";
        $gp.="set grid ".($self->{meta}->plot_grid($plot))."\n";
    }

    if ($self->{meta}->plot_time($plot)) {
        $gp.="#\n# Time axes\n";
        for (qw/x y z cb/) {
            if ($self->{meta}->plot_time($plot) =~ /$_/) { $gp.="set ".$_."data time\n" }
        }
        $gp.=qq(set timefmt "%s"\n);
    }
    
    my $gp_help;
    for (qw/x y z cb/) {
        my $name="plot_".$_."format";
        if ($self->{meta}->$name($plot)) {
            $gp_help.=qq(set format $_ ").($self->{meta}->$name($plot)).qq("\n);
        }
    }
    $gp.="#\n# Axis format\n".$gp_help if ($gp_help);
    
    unless ($self->{options}->{multiple}) {
        $gp.="#\n# Ranges\n";
        my $xmin=(defined $self->{meta}->axis_min($xaxis)) ? $self->{meta}->axis_min($xaxis) : "*";
        my $xmax=(defined $self->{meta}->axis_max($xaxis)) ? $self->{meta}->axis_max($xaxis) : "*";
        my $ymin=(defined $self->{meta}->axis_min($yaxis)) ? $self->{meta}->axis_min($yaxis) : "*";
        my $ymax=(defined $self->{meta}->axis_max($yaxis)) ? $self->{meta}->axis_max($yaxis) : "*";
        $gp.="set xrange [$xmin:$xmax]\n";
        $gp.="set yrange [$ymin:$ymax]\n";
        if ($zaxis) {
            my $zmin=(defined $self->{meta}->axis_min($zaxis)) ? $self->{meta}->axis_min($zaxis) : "*";
            my $zmax=(defined $self->{meta}->axis_max($zaxis)) ? $self->{meta}->axis_max($zaxis) : "*";
            $gp.="set zrange [$zmin:$zmax]\n";
        }
        if ($cbaxis) {
            my $cbmin=(defined $self->{meta}->axis_min($cbaxis)) ? $self->{meta}->axis_min($cbaxis) : "*";
            my $cbmax=(defined $self->{meta}->axis_max($cbaxis)) ? $self->{meta}->axis_max($cbaxis) : "*";
            $gp.="set cbrange [$cbmin:$cbmax]\n";
            $gp.="set zrange [$cbmin:$cbmax]\n" if ($self->{meta}->plot_logscale($plot));
        }
    }
    
    if ($self->{meta}->plot_logscale($plot)) {
        $gp.="#\n# Axes with logscale\n";
        $gp.="set logscale ".$self->{meta}->plot_logscale($plot)."\n";
    }

    $gp.="#\n# Title and labels\n";
    $gp.=qq(set title ").$self->{meta}->dataset_title()." (".$self->{meta}->sample().")\"\n";
    if ($self->{options}->{fulllabels}) {
        my $h=0.95;my $screen=0.99;
        my @lines=split "\n",$self->{meta}->dataset_description();
        for (@lines) {
            if ($self->{meta}->plot_type($plot) eq 'pm3d') {
                $gp.=qq(set label "$_" at screen 0.01, screen $screen\n);
            } else {
                $gp.=qq(set label "$_" at graph 0.02, graph $h\n);
            }
            $h-=0.04;
            $screen-=0.025;$screen=0.13 if (abs($screen-0.865)<0.001);
        }
    
    }
    if ($self->{meta}->plot_label($plot)) {
        my @labels=$self->{meta}->plot_label($plot);
        for (@labels) {
            my $text=$_->{text};
            my $x=$_->{x};
            my $y=$_->{y};
            $gp.=qq(set label "$text" at $x,$y center front\n);
        }
    }
    
    print $gpipe $gp;
    return $gpipe;
}

sub _plot {
    my ($self,$gpipe,$plot)=@_;

    my $xaxis=$self->{meta}->plot_xaxis($plot);
    my $yaxis=$self->{meta}->plot_yaxis($plot);
    my $zaxis=$self->{meta}->plot_zaxis($plot);
    my $cbaxis=$self->{meta}->plot_cbaxis($plot);

    my $xexp=$self->_flatten_exp($xaxis);
    my $yexp=$self->_flatten_exp($yaxis);
    my $zexp=$self->_flatten_exp($zaxis) if ($zaxis);
    my $cbexp=$self->_flatten_exp($cbaxis) if ($cbaxis);

    my $datafile=$self->{meta}->get_abs_path().$self->{meta}->data_file();
    
    my $pp;
    if ($self->{meta}->plot_type($plot) eq 'pm3d') {
        $pp=qq(splot "$datafile" using ($xexp):($yexp):($cbexp) title "$plot"\n);
    } else {
        if ($self->{options}->{live_latest}) {
            my %blocks=$self->{meta}->block();
            my @keys=sort(keys %blocks);
            @keys=splice @keys,-$self->{options}->{live_latest};
            $pp="plot ";
            for (@keys) {
                $pp.=qq("$datafile" using ($xexp):($yexp) every :::$_::$_ title "$blocks{label}" with lines, );
            }
            $pp=substr $pp,0,(length $pp) -2;
        } else {
            $pp=qq(plot "$datafile" using ($xexp):($yexp) title "$plot" with lines\n);
        }
    }
    print $gpipe $pp;
    
}

sub _plot_multiple {
    my ($self,$gpipe,$plot)=@_;

    my $xaxis=$self->{meta}->plot_xaxis($plot);
    my $yaxis=$self->{meta}->plot_yaxis($plot);
    my $zaxis=$self->{meta}->plot_zaxis($plot);
    my $cbaxis=$self->{meta}->plot_cbaxis($plot);

    my $xexp=$self->_flatten_exp($xaxis);
    my $yexp=$self->_flatten_exp($yaxis);
    my $zexp=$self->_flatten_exp($zaxis) if ($zaxis);
    my $cbexp=$self->_flatten_exp($cbaxis) if ($cbaxis);

    my $datafile=$self->{meta}->get_abs_path().$self->{meta}->data_file();
    
    my $pp="#\n# Plot\n";
    if ($self->{meta}->plot_type($plot) eq 'pm3d') {
        $pp.=qq(splot "$datafile" using ($xexp):($yexp):($cbexp) title "$plot"\n);
    } else {
        if ($self->{options}->{last_live}) {
            my %blocks=$self->{meta}->block();
            my @keys=sort(keys %blocks);
            @keys=splice @keys,-$self->{options}->{last_live};
            $pp.="plot ";
            for (@keys) {
                $pp.=qq("$datafile" using ($xexp):($yexp) every :::$_::$_ title "$blocks{label}" with lines, );
            }
            $pp=substr $pp,0,(length $pp) -2;
        } else {
            $pp.=qq(plot "$datafile" using ($xexp):($yexp) title "$plot"\n);
        }
    }
    print $gpipe $pp;
    
}

sub _flatten_exp {
    my ($self,$axis)=@_;
    $_=$self->{meta}->axis_expression($axis);
    while (/\$A\d+/) {
        s/\$A(\d+)/($self->{meta}->axis_expression($1))/e;
    }
    while (/\$C\d+/) {
        s/\$C(\d+)/'$'.($1+1)/e;
    }
    $_;
}

sub available_plots {
    my $self=shift;
    
    my %plots=$self->{meta}->plot();
    my @names=(keys %plots);
    
    for (@names) {
        my $xlabel=$self->{meta}->axis_label($plots{$_}->{xaxis});
        my $ylabel=$self->{meta}->axis_label($plots{$_}->{yaxis});
        
        $plots{$_}="$ylabel vs. $xlabel";
    }
    return %plots;
}

sub get_gnuplot_pipe {
    my $self=shift;
    my $gpname;
    if ($^O =~ /MSWin32/) {
        $gpname="pgnuplot";
    } else {
        $gpname="gnuplot -noraise";
    }
    if (open my $GP,"| $gpname") {
        my $oldfh = select($GP);
        $| = 1;
        select($oldfh);
        return $GP;
    }
    return undef;
}

1;

__END__

=head1 NAME

Lab::Data::Plotter - Plot data with Gnuplot

=head1 SYNOPSIS

  use Lab::Data::Plotter;
  
  my $plotter=new Lab::Data::Plotter($metafile);
  
  my %plots=$plotter->available_plots();
  my @names=keys %plots;
  
  $plotter->plot($names[0]);

=head1 DESCRIPTION

This module can plot data with GnuPlot. It plots data from C<.DATA> files
and takes into account the data information in the corresponding C<.META> file.

The module also offers the possibility to plot data live, while it is
being aquired.

=head1 CONSTRUCTOR

=head2 new

  $plotter=new Lab::Data::Plotter($meta,\%options);

Creates a Plotter object. C<$meta> is either an object of type
L<Lab::Data::Meta|Lab::Data::Meta> or a filename that points to a C<.META> file.

Available options are

=over 2

=item dump

=item eps

=item fulllabels

=item last_live

=back

=head1 METHODS

=head2 available_plots

  my %plots=$plotter->available_plots();

=head2 plot

  $plotter->plot($plot);

=head2 start_live_plot

  $plotter->start_live_plot($plot);

=head2 update_live_plot

  $plotter->update_live_plot();

=head2 stop_live_plot

  $plotter->stop_live_plot();

=head1 AUTHOR/COPYRIGHT

This is $Id: Plotter.pm 516 2006-09-28 14:59:28Z schroeer $

Copyright 2004-2006 Daniel Schröer (L<http://www.danielschroeer.de>)

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

=cut