###################################################################### ###################################################################### ###################################################################### ### ### ### Gnuplot backend for PDL::Graphics:Simple ### ### See the PDL::Graphics::Simple docs for details ### ## # package PDL::Graphics::Simple::Gnuplot; use strict; use warnings; use File::Temp qw/tempfile/; use PDL::Options q/iparse/; use PDL; use PDL::ImageND; # for polylines our $required_PGG_version = 1.5; our $mod = { shortname => 'gnuplot', module=>'PDL::Graphics::Simple::Gnuplot', engine => 'PDL::Graphics::Gnuplot', synopsis=> 'Gnuplot 2D/3D (versatile; beautiful output)', pgs_api_version=> '1.012', }; PDL::Graphics::Simple::register( $mod ); our $filetypes = { ps => ['pscairo','postscript'], dxf => 'dxf', png => ['pngcairo','png'], pdf => ['pdfcairo','pdf'], txt => 'dumb', jpg => 'jpeg', svg => 'svg', gif => 'gif' }; our @disp_terms = qw/ qt wxt x11 aqua windows /; our $disp_opts = { wxt=>{persist=>1}, x11=>{persist=>1}, aqua=>{persist=>0}, windows=>{persist=>0} }; ########## # PDL::Graphics::Simple::Gnuplot::check # Checker sub check { my $force = shift; $force = 0 unless(defined($force)); return $mod->{ok} unless( $force or !defined($mod->{ok}) ); # Eval PDL::Graphics::Gnuplot. Require relatively recent version. # We don't specify the version in the 'use', so we can issue a # warning on an older version. eval { require PDL::Graphics::Gnuplot; PDL::Graphics::Gnuplot->import; }; if ($@) { $mod->{ok} = 0; $mod->{msg} = $@; return 0; } if ($PDL::Graphics::Gnuplot::VERSION < $required_PGG_version) { $mod->{msg} = sprintf("PDL::Graphics::Gnuplot was found, but is too old (v%s < v%s). Ignoring it.\n", $PDL::Graphics::Gnuplot::VERSION, $required_PGG_version ); $mod->{ok} = 0; return 0; } my $gpw = eval { gpwin() }; if ($@) { $mod->{ok} = 0; $mod->{msg} = $@; die "PDL::Graphics::Simple: PDL::Graphics::Gnuplot didn't construct properly.\n\t$@"; } $mod->{valid_terms} = $gpw->{valid_terms}; my $okterm = undef; if ($ENV{PDL_SIMPLE_DEVICE}) { $okterm = 1; } else { for my $term (@disp_terms) { if ($mod->{valid_terms}{$term}) { $okterm = $term; last; } } } unless ( defined $okterm ) { $mod->{ok} = 0; my $s = "Gnuplot doesn't seem to support any of the known display terminals:\n they are: (".join(",",@disp_terms).")\n"; $mod->{msg} = $s; die "PDL::Graphics::Simple: $s"; } $mod->{gp_version} = $PDL::Graphics::Gnuplot::gp_version; $mod->{ok} = 1; return 1; } ########## # PDL::Graphics::Simple::Gnuplot::new # Constructor our $new_defaults = { size => [6,4.5,'in'], type => '', output => '', multi=>undef }; sub new { my $class = shift; my $opt_in = shift; $opt_in = {} unless(defined($opt_in)); my $opt = { iparse( $new_defaults, $opt_in ) }; my $gpw; # Force a recheck on failure, in case the user fixed gnuplot. # Also loads PDL::Graphics::Gnuplot. unless(check()) { die "$mod->{shortname} appears nonfunctional: $mod->{msg}\n" unless(check(1)); } # Generate the @params array to feed to gnuplot my @params = (); push( @params, "size" => $opt->{size} ); # tempfile gets set if we need to write to a temporary file for image conversion my $conv_tempfile = ''; # Do different things for interactive and file types if ($opt->{type} =~ m/^i/i) { push(@params, title=>$opt->{output}) if defined $opt->{output}; # Interactive - try known terminals unless PDL_SIMPLE_DEVICE given push @params, font=>"=16", dashed=>1; if (my $try = $mod->{itype}) { $gpw = gpwin($mod->{itype}, @params, ($disp_opts->{$try} // {})->{persist} ? (persist=>0) : () ); } else { if (my $try = $ENV{PDL_SIMPLE_DEVICE}) { $gpw = gpwin($try, @params, ($disp_opts->{$try} // {})->{persist} ? (persist=>0) : () ); } else { attempt:for my $try( @disp_terms ) { eval { $gpw = gpwin($try, @params, ($disp_opts->{$try} // {})->{persist} ? (persist=>0) : () ); }; last attempt if $gpw; } } die "Couldn't start a gnuplot interactive window" unless($gpw); $mod->{itype} = $gpw->{terminal}; } } else { # File output - parse out file type, and then see if we support it. # (Maybe the parsing part could be pushed into a utility routine...) # Filename extension -- 2-4 characters my $ext; if ($opt->{output} =~ m/\.(\w{2,4})$/) { $ext = $1; } else { $ext = '.png'; print STDERR "PDL::Graphics::Simple::Gnuplot: Warning - defaulting to .png type for file '$opt->{output}'\n"; } $opt->{ext} = $ext; ########## # Scan through the supported file types. Gnuplot has several drivers for some # of the types, so we search until we find a valid one. # At the end, $ft has either a valid terminal name from the table (at top), # or undef. my $ft = $filetypes->{$ext}; if (ref $ft eq 'ARRAY') { try:for my $try (@$ft) { if ($mod->{valid_terms}{$try}) { $ft = $try; last try; } } if (ref($ft)) { $ft = undef; } } elsif (!defined($mod->{valid_terms}{$ft})) { $ft = undef; } # Now $ext has the file type - check if its a supported type. If not, make a # tempfilename to hold gnuplot's output. unless ( defined($ft) ) { unless ($mod->{valid_terms}{pscairo} or $mod->{valid_terms}{postscript}) { die "PDL::Graphics::Simple: $ext isn't a valid output file type for your gnuplot,\n\tand it doesn't support .ps either. Sorry, I give up.\n"; } # Term is invalid but png is supported - set up a tempfile for conversion. my($fh); ($fh,$conv_tempfile) = tempfile('pgs_gnuplot_XXXX'); close $fh; unlink($conv_tempfile); # just to be sure; $conv_tempfile .= ".ps"; $ft = $mod->{valid_terms}{pscairo} ? 'pscairo' : 'postscript'; } push @params, output => ($conv_tempfile || $opt->{output}); push @params, color => 1 if $PDL::Graphics::Gnuplot::termTab->{$ft}{color}; push @params, dashed => 1 if $PDL::Graphics::Gnuplot::termTab->{$ft}{dashed}; $gpw = gpwin( $ft, @params ); } my $me = { opt => $opt, conv_fn => $conv_tempfile, obj=>$gpw }; # Deal with multiplot setup... if (defined($opt->{multi})) { $me->{nplots} = $opt->{multi}[0] * $opt->{multi}[1]; $me->{plot_no} = 0; } else { $me->{nplots} = 0; } return bless($me, 'PDL::Graphics::Simple::Gnuplot'); } ############################## # PDL::Graphics::Simple::Gnuplot::plot # Most of the curve types are implemented by passing them on to gnuplot -- circles is an # exception, since the gnuplot "circles" curve type doesn't scale the circles in scientific # coordinates (they are always rendered as circular on the screen), and we want to match # the scaling behavior of the other engines. our $curve_types = { points => 'points', lines => 'lines', bins => 'histeps', errorbars => 'yerrorbars', limitbars => 'yerrorbars', image => 'image', circles => sub { my($me, $po, $co, @data) = @_; my $ang = PDL->xvals(362)*3.14159/180; my $c = $ang->cos; my $s = $ang->sin; $s->slice("361") .= $c->slice("361") .= PDL->pdl(1.1)->acos; # NaN my $dr = $data[2]->flat; my $dx = ($data[0]->flat->slice("*1") + $dr->slice("*1") * $c)->flat; my $dy = ($data[1]->flat->slice("*1") + $dr->slice("*1") * $s)->flat; $co->{with} = "lines"; return [ $co, $dx, $dy ]; }, contours => sub { my ($me, $po, $co, $vals, $cvals) = @_; $co->{with} = "lines"; $co->{style} //= 6; # so all contour parts have same style, blue somewhat visible against sepia my @out; for my $thresh ($cvals->list) { my ($pi, $p) = contour_polylines($thresh, $vals, $vals->ndcoords); next if $pi->at(0) < 0; push @out, map [ $co, $_->dog ], path_segs($pi, $p->mv(0,-1)); } @out; }, polylines => sub { my ($me, $po, $co, $xy, $pen) = @_; $co->{with} = "lines"; $co->{style} //= 6; # so all polylines have same style, blue somewhat visible against sepia my $pi = $pen->eq(0)->which; map [ $co, $_->dog ], path_segs($pi, $xy->mv(0,-1)); }, fits => 'fits', labels => sub { my($me, $po, $co, @data) = @_; my $label_list = ($po->{label} or []); for my $i(0..$data[0]->dim(0)-1) { my $j = ""; my $s = $data[2]->[$i]; if ( $s =~ s/^([\<\>\| ])// ) { $j = $1; } my @spec = ("$s", at=>[$data[0]->at($i), $data[1]->at($i)]); push @spec,"left" if $j eq '<'; push @spec,"center" if $j eq '|'; push @spec,"right" if $j eq '>'; push @{$label_list}, \@spec; } $po->{label} = $label_list; $co->{with} = "labels"; return [ $co, [$po->{xrange}[0]], [$po->{yrange}[0]], [""] ]; }, }; sub plot { my $me = shift; my $ipo = shift; my $po = { title => $ipo->{title}, xlab => $ipo->{xlabel}, ylab => $ipo->{ylabel}, key => $ipo->{key}, xrange => $ipo->{xrange}, yrange => $ipo->{yrange}, cbrange => $ipo->{crange}, colorbox => $ipo->{wedge}, justify => $ipo->{justify}>0 ? $ipo->{justify} : undef, clut => 'sepia', }; if ( defined($ipo->{legend}) ) { my $legend = ""; if ( $ipo->{legend} =~ m/l/i ) { $legend .= ' left '; } elsif ($ipo->{legend} =~ m/r/i) { $legend .= ' right '; } else { $legend .= ' center '; } if ( $ipo->{legend} =~ m/t/i) { $legend .= ' top '; } elsif ( $ipo->{legend} =~ m/b/i) { $legend .= ' bottom '; } else { $legend .= ' center '; } $po->{key} = $legend; } $po->{logscale} = [$ipo->{logaxis}] if $ipo->{logaxis}; unless ($ipo->{oplot}) { $me->{curvestyle} = 0; } my @arglist = $po; for my $block (@_) { die "PDL::Graphics::Simple::Gnuplot: undefined curve type $block->[0]{with}" unless my $ct = $curve_types->{ $block->[0]{with} }; my @blocks = ref($ct) eq 'CODE' ? $ct->($me, $po, @$block) : [{%{$block->[0]}, with=>$ct}, @$block[1..$#$block]]; # Now parse out curve options and deal with line styles... for my $b (@blocks) { my ($co, @rest) = @$b; my $gco = { with => $co->{with} }; unless($co->{with} eq 'labels') { $me->{curvestyle} = $co->{style} // ($me->{curvestyle}//0)+1; $gco->{dashtype} = $gco->{linetype} = $me->{curvestyle}; if ( $co->{width} ) { $gco->{pointsize} = $co->{width} if $co->{with} =~ m/^points/; $gco->{linewidth} = $co->{width}; } } $gco->{legend} = $co->{key} if defined $co->{key}; push @arglist, $gco, @rest; } } if ($me->{nplots}) { unless($me->{plot_no}) { $me->{obj}->multiplot( layout=>[@{$me->{opt}{multi}}[0,1]] ); } } if ($ipo->{oplot}) { delete @$po{qw(logaxis xrange yrange cbrange justify)}; $me->{obj}->replot(@arglist); } else { $me->{obj}->plot(@arglist); } if ($me->{nplots}) { $me->{plot_no}++; if ($me->{plot_no} >= $me->{nplots}) { $me->{obj}->end_multi; $me->{plot_no} = 0; $me->{obj}->close if $me->{opt}{type} =~ m/^f/i; } } else { $me->{obj}->close if $me->{opt}{type} =~ m/^f/i; } if ($me->{opt}{type} =~ m/^f/i and $me->{conv_fn}) { print "converting $me->{conv_fn} to $me->{opt}{output}..."; $a = rim($me->{conv_fn}); wim($a->slice('-1:0:-1')->mv(1,0), $me->{opt}{output}); unlink($me->{conv_fn}); } } 1;