/
Gnuplot.pm
1666 lines (1253 loc) · 52.1 KB
/
Gnuplot.pm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package PDL::Graphics::Gnuplot;
use strict;
use warnings;
use PDL;
use List::Util qw(first);
use Storable qw(dclone);
use IPC::Open3;
use IPC::Run;
use IO::Select;
use Symbol qw(gensym);
use Time::HiRes qw(gettimeofday tv_interval);
use base 'Exporter';
our @EXPORT_OK = qw(plot plot3d plotlines plotpoints);
# when testing plots with ASCII i/o, this is the unit of test data
my $testdataunit_ascii = "10 ";
# if I call plot() as a global function I create a new PDL::Graphics::Gnuplot
# object. I would like the gnuplot process to persist to keep the plot
# interactive at least while the perl program is running. This global variable
# keeps the new object referenced so that it does not get deleted. Once can
# create their own PDL::Graphics::Gnuplot objects, but there's one free global
# one available
my $globalPlot;
# I make a list of all the options. I can use this list to determine if an
# options hash I encounter is for the plot, or for a curve
my @allPlotOptions = qw(3d dump binary log
extracmds nogrid square square_xy title
hardcopy terminal output
globalwith
xlabel xmax xmin
y2label y2max y2min
ylabel ymax ymin
zlabel zmax zmin
cbmin cbmax);
my %plotOptionsSet;
foreach(@allPlotOptions) { $plotOptionsSet{$_} = 1; }
my @allCurveOptions = qw(legend y2 with tuplesize);
my %curveOptionsSet;
foreach(@allCurveOptions) { $curveOptionsSet{$_} = 1; }
# get a list of all the -- options that this gnuplot supports
my %gnuplotFeatures = _getGnuplotFeatures();
sub new
{
my $classname = shift;
my %plotoptions = ();
if(@_)
{
if(ref $_[0])
{
if(@_ != 1)
{
barf "PDL::Graphics::Gnuplot->new() got a ref as a first argument and has OTHER arguments. Don't know what to do";
}
%plotoptions = %{$_[0]};
}
else
{ %plotoptions = @_; }
}
if( my @badKeys = grep {!defined $plotOptionsSet{$_}} keys %plotoptions )
{
barf "PDL::Graphics::Gnuplot->new() got option(s) that were NOT a plot option: (@badKeys)";
}
my $pipes = startGnuplot( $plotoptions{dump} );
my $this = {%$pipes, # %$this is built on top of %$pipes
options => \%plotoptions,
t0 => [gettimeofday]};
bless($this, $classname);
_logEvent($this, "startGnuplot() finished");
# the plot options affect all the plots made by this object, so I can set them
# now
_safelyWriteToPipe($this, parseOptions(\%plotoptions));
return $this;
sub startGnuplot
{
my $dump = shift;
return {in => \*STDOUT} if($dump);
my @options = $gnuplotFeatures{persist} ? qw(--persist) : ();
my $in = gensym();
my $err = gensym();
my $pid =
open3($in, undef, $err, 'gnuplot', @options)
or die "Couldn't run the 'gnuplot' backend";
return {in => $in,
err => $err,
errSelector => IO::Select->new($err),
pid => $pid};
}
sub parseOptions
{
my $options = shift;
# set some defaults
# plot with lines and points by default
$options->{globalwith} = 'linespoints' unless defined $options->{globalwith};
# make sure I'm not passed invalid combinations of options
{
if ( $options->{'3d'} )
{
if ( defined $options->{y2min} || defined $options->{y2max} )
{ barf "'3d' does not make sense with 'y2'...\n"; }
if ( !$gnuplotFeatures{equal_3d} && (defined $options->{square_xy} || defined $options->{square} ) )
{
warn "Your gnuplot doesn't support square aspect ratios for 3D plots, so I'm ignoring that";
delete $options->{square_xy};
delete $options->{square};
}
}
else
{
if ( defined $options->{square_xy} )
{ barf "'square'_xy only makes sense with '3d'\n"; }
}
}
my $cmd = '';
# grid on by default
if( !$options->{nogrid} )
{ $cmd .= "set grid\n"; }
# set the plot bounds
{
# If a bound isn't given I want to set it to the empty string, so I can communicate it simply
# to gnuplot
$options->{xmin} = '' unless defined $options->{xmin};
$options->{xmax} = '' unless defined $options->{xmax};
$options->{ymin} = '' unless defined $options->{ymin};
$options->{ymax} = '' unless defined $options->{ymax};
$options->{y2min} = '' unless defined $options->{y2min};
$options->{y2max} = '' unless defined $options->{y2max};
$options->{zmin} = '' unless defined $options->{zmin};
$options->{zmax} = '' unless defined $options->{zmax};
$options->{cbmin} = '' unless defined $options->{cbmin};
$options->{cbmax} = '' unless defined $options->{cbmax};
# if any of the ranges are given, set the range
$cmd .= "set xrange [$options->{xmin} :$options->{xmax} ]\n" if length( $options->{xmin} . $options->{xmax} );
$cmd .= "set yrange [$options->{ymin} :$options->{ymax} ]\n" if length( $options->{ymin} . $options->{ymax} );
$cmd .= "set zrange [$options->{zmin} :$options->{zmax} ]\n" if length( $options->{zmin} . $options->{zmax} );
$cmd .= "set cbrange [$options->{cbmin}:$options->{cbmax}]\n" if length( $options->{cbmin} . $options->{cbmax} );
$cmd .= "set y2range [$options->{y2min}:$options->{y2max}]\n" if length( $options->{y2min} . $options->{y2max} );
}
# set the curve labels, titles
{
$cmd .= "set xlabel \"$options->{xlabel }\"\n" if defined $options->{xlabel};
$cmd .= "set ylabel \"$options->{ylabel }\"\n" if defined $options->{ylabel};
$cmd .= "set zlabel \"$options->{zlabel }\"\n" if defined $options->{zlabel};
$cmd .= "set y2label \"$options->{y2label}\"\n" if defined $options->{y2label};
$cmd .= "set title \"$options->{title }\"\n" if defined $options->{title};
}
# handle a requested square aspect ratio
{
# set a square aspect ratio. Gnuplot does this differently for 2D and 3D plots
if ( $options->{'3d'})
{
if ($options->{square}) { $cmd .= "set view equal xyz\n"; }
elsif ($options->{square_xy}) { $cmd .= "set view equal xy\n" ; }
}
else
{
if( $options->{square} ) { $cmd .= "set size ratio -1\n"; }
}
}
# handle 'hardcopy'. This simply ties in to 'output' and 'terminal', handled
# later
{
if ( defined $options->{hardcopy})
{
# 'hardcopy' is simply a shorthand for 'terminal' and 'output', so they
# can't exist together
if(defined $options->{terminal} || defined $options->{output} )
{
barf <<EOM;
The 'hardcopy' option can't coexist with either 'terminal' or 'output'. If the
defaults are acceptable, use 'hardcopy' only, otherwise use 'terminal' and
'output' to get more control.
EOM
}
my $outputfile = $options->{hardcopy};
my ($outputfileType) = $outputfile =~ /\.(eps|ps|pdf|png)$/;
if (!$outputfileType)
{ barf "Only .eps, .ps, .pdf and .png hardcopy output supported\n"; }
my %terminalOpts =
( eps => 'postscript solid color enhanced eps',
ps => 'postscript solid color landscape 10',
pdf => 'pdf solid color font ",10" size 11in,8.5in',
png => 'png size 1280,1024' );
$options->{terminal} = $terminalOpts{$outputfileType};
$options->{output} = $outputfile;
}
if( defined $options->{terminal} && !defined $options->{output} )
{
print STDERR <<EOM;
Warning: defined gnuplot terminal, but NOT an output file. Is this REALLY what you want?
EOM
}
}
# add the extra global options
{
if($options->{extracmds})
{
# if there's a single extracmds option, put it into a 1-element list to
# make the processing work
if(!ref $options->{extracmds} )
{ $options->{extracmds} = [$options->{extracmds}]; }
foreach (@{$options->{extracmds}})
{ $cmd .= "$_\n"; }
}
}
return $cmd;
}
}
sub DESTROY
{
my $this = shift;
# if we're stuck on a checkpoint, "exit" won't work, so I just kill the
# child gnuplot process
if( defined $this->{pid})
{
if( $this->{checkpoint_stuck} )
{
kill 'TERM', $this->{pid};
}
else
{
_printGnuplotPipe( $this, "exit\n" );
}
waitpid( $this->{pid}, 0 ) ;
}
}
# the main API function to generate a plot. Input arguments are a bunch of
# piddles optionally preceded by a bunch of options for each curve. See the POD
# for details
sub plot
{
barf( "Plot called with no arguments") unless @_;
my $this;
if(defined ref $_[0] && ref $_[0] eq 'PDL::Graphics::Gnuplot')
{
# I called this as an object-oriented method. First argument is the
# object. I already got the plot options in the constructor, so I don't need
# to get them again.
$this = shift;
}
else
{
# plot() called as a global function, NOT as a method. The initial arguments
# can be the plot options (hashrefs or inline). I keep trying to parse the
# initial arguments as plot options until I run out
my $plotOptions = {};
while(1)
{
if (defined ref $_[0] && ref $_[0] eq 'HASH')
{
# arg is a hash. Is it plot options or curve options?
my $NmatchedPlotOptions = grep {defined $plotOptionsSet{$_}} keys %{$_[0]};
last if $NmatchedPlotOptions == 0; # not plot options, so done scanning
if( $NmatchedPlotOptions != scalar keys %{$_[0]} )
{ barf "Plot option hash has some non-plot options"; }
# grab all the plot options
my $newPlotOptions = shift;
foreach my $key (keys %$newPlotOptions)
{ $plotOptions->{$key} = $newPlotOptions->{$key}; }
}
else
{
# arg is NOT a hashref. It could be an inline hash. I grab a hash pair
# if it's plot options
last unless @_ >= 2 && $plotOptionsSet{$_[0]};
my $key = shift;
my $val = shift;
$plotOptions->{$key} = $val;
}
}
$this = $globalPlot = PDL::Graphics::Gnuplot->new($plotOptions);
}
my $plotOptions = $this->{options};
# I split my data-to-plot into similarly-styled chunks
# pieces of data we're plotting. Each chunk has a similar style
my ($chunks, $Ncurves) = parseArgs($plotOptions->{'3d'}, @_);
if( scalar @$chunks == 0)
{ barf "plot() was not given any data"; }
# I'm now ready to send the plot command. If the plot command fails, I'll get
# an error message; if it succeeds, gnuplot will sit there waiting for data. I
# don't want to have a timeout waiting for the error message, so I try to run
# the plot command to see if it works. I make a dummy plot into the 'dumb'
# terminal, and then _checkpoint() for errors. To make this quick, the test
# plot command contains the minimum number of data points
my ($plotcmd, $testplotcmd, $testplotdata) =
plotcmd( $chunks, $plotOptions );
testPlotcmd($this, $testplotcmd, $testplotdata);
# tests ok. Now set the terminal and actually make the plot!
if(defined $this->{options}{terminal})
{ _safelyWriteToPipe($this, "set terminal $this->{options}{terminal}\n", 'terminal'); }
if(defined $this->{options}{output})
{ _safelyWriteToPipe($this, "set output \"$this->{options}{output}\"\n", 'output'); }
# all done. make the plot
_printGnuplotPipe( $this, "$plotcmd\n");
foreach my $chunk(@$chunks)
{
# In order for the PDL threading to work, I need at least one dimension. Add
# it where needed. pdl(5) has 0 dimensions, for instance. I really want
# something like "plot(5, pdl(3,4,5,3,4))" to work; It doesn't right
# now. This map() makes "plot(pdl(3), pdl(5))" work. This is good for
# completeness, but not really all that interesting
my @data = map {$_->ndims == 0 ? $_->dummy(0) : $_} @{$chunk->{data}};
my $tuplesize = scalar @data;
eval( "_writedata_$tuplesize" . '(@data, $this, $plotOptions->{binary})');
}
# read and report any warnings that happened during the plot
_checkpoint($this, 'printwarnings');
# generates the gnuplot command to generate the plot. The curve options are parsed here
sub plotcmd
{
my ($chunks, $plotOptions) = @_;
my $basecmd = '';
# if anything is to be plotted on the y2 axis, set it up
if( grep {my $chunk = $_; grep {$_->{y2}} @{$chunk->{options}}} @$chunks)
{
if ( $plotOptions->{'3d'} )
{ barf "3d plots don't have a y2 axis"; }
$basecmd .= "set ytics nomirror\n";
$basecmd .= "set y2tics\n";
}
if($plotOptions->{'3d'} ) { $basecmd .= 'splot '; }
else { $basecmd .= 'plot ' ; }
my @plotChunkCmd;
my @plotChunkCmdMinimal; # same as above, but with a single data point per plot only
my $testData = ''; # data to make a minimal plot
foreach my $chunk (@$chunks)
{
my @optionCmds =
map { optioncmd($_, $plotOptions->{globalwith}) } @{$chunk->{options}};
if( $plotOptions->{binary} )
{
# I get 2 formats: one real, and another to test the plot cmd, in case it
# fails. The test command is the same, but with a minimal point count. I
# also get the number of bytes in a single data point here
my ($format, $formatMinimal) = binaryFormatcmd($chunk);
my $Ntestbytes_here = getNbytes_tuple($chunk);
push @plotChunkCmd, map { "'-' $format $_" } @optionCmds;
push @plotChunkCmdMinimal, map { "'-' $formatMinimal $_" } @optionCmds;
# If there was an error, these whitespace commands will simply do
# nothing. If there was no error, these are data that will be plotted in
# some manner. I'm not actually looking at this plot so I don't care
# what it is. Note that I'm not making assumptions about how long a
# newline is (perl docs say it could be 0 bytes). I'm printing as many
# spaces as the number of bytes that I need, so I'm potentially doubling
# or even tripling the amount of needed data. This is OK, since gnuplot
# will simply ignore the tail.
$testData .= " \n" x ($Ntestbytes_here * scalar @optionCmds);
}
else
{
# I'm using ascii to talk to gnuplot, so the minimal and "normal" plot
# commands are the same (point count is not in the plot command)
push @plotChunkCmd, map { "'-' $_" } @optionCmds;
my $testData_curve = $testdataunit_ascii x $chunk->{tuplesize} . "\n" . "e\n";
$testData .= $testData_curve x scalar @optionCmds;
}
}
# the command to make the plot and to test the plot
my $cmd = $basecmd . join(',', @plotChunkCmd);
my $cmdMinimal = @plotChunkCmdMinimal ?
$basecmd . join(',', @plotChunkCmdMinimal) :
$cmd;
return ($cmd, $cmdMinimal, $testData);
# parses a curve option
sub optioncmd
{
my $option = shift;
my $globalwith = shift;
my $cmd = '';
if( defined $option->{legend} )
{ $cmd .= "title \"$option->{legend}\" "; }
else
{ $cmd .= "notitle "; }
# use the given per-curve 'with' style if there is one. Otherwise fall
# back on the global
my $with = $option->{with} || $globalwith;
$cmd .= "with $with " if $with;
$cmd .= "axes x1y2 " if $option->{y2};
return $cmd;
}
sub binaryFormatcmd
{
# I make 2 formats: one real, and another to test the plot cmd, in case it
# fails
my $chunk = shift;
my $tuplesize = $chunk->{tuplesize};
my $recordSize = $chunk->{data}[0]->dim(0);
my $format = "binary record=$recordSize format=\"";
$format .= '%double' x $tuplesize;
$format .= '"';
# When plotting in binary, gnuplot gets confused if I don't explicitly
# tell it the tuplesize. It's got its own implicit-tuples logic that I
# don't want kicking in. As an example, the following simple plot doesn't
# work in binary without this extra line:
# plot3d(binary => 1,
# with => 'image', sequence(5,5));
$format .= ' using ' . join(':', 1..$tuplesize);
# to test the plot I plot a single record
my $formatTest = $format;
$formatTest =~ s/record=\d+/record=1/;
return ($format, $formatTest);
}
sub getNbytes_tuple
{
my $chunk = shift;
# assuming sizeof(double)==8
return 8 * $chunk->{tuplesize};
}
}
sub parseArgs
{
# Here I parse the plot() arguments. Each chunk of data to plot appears in
# the argument list as plot(options, options, ..., data, data, ....). The
# options are a hashref, an inline hash or can be absent entirely. THE
# OPTIONS ARE ALWAYS CUMULATIVELY DEFINED ON TOP OF THE PREVIOUS SET OF
# OPTIONS (except the legend)
# The data arguments are one-argument-per-tuple-element.
my $is3d = shift;
my @args = @_;
# options are cumulative except the legend (don't want multiple plots named
# the same). This is a hashref that contains the accumulator
my $lastOptions = {};
my @chunks;
my $Ncurves = 0;
my $argIndex = 0;
while($argIndex <= $#args)
{
# First, I find and parse the options in this chunk
my $nextDataIdx = first {ref $args[$_] && ref $args[$_] eq 'PDL'} $argIndex..$#args;
last if !defined $nextDataIdx; # no more data. done.
# I do not reuse the curve legend, since this would result it multiple
# curves with the same name
delete $lastOptions->{legend};
my %chunk;
if( $nextDataIdx > $argIndex )
{
$chunk{options} = parseOptionsArgs($lastOptions, @args[$argIndex..$nextDataIdx-1]);
# make sure I know what to do with all the options
foreach my $option (@{$chunk{options}})
{
if (my @badKeys = grep {!defined $curveOptionsSet{$_}} keys %$option)
{
barf "plot() got some unknown curve options: (@badKeys)";
}
}
}
else
{
# No options given for this chunk, so use the last ones
$chunk{options} = [ dclone $lastOptions ];
}
# I now have the options for this chunk. Let's grab the data
$argIndex = $nextDataIdx;
my $nextOptionIdx = first {!ref $args[$_] || ref $args[$_] ne 'PDL'} $argIndex..$#args;
$nextOptionIdx = @args unless defined $nextOptionIdx;
my $tuplesize = getTupleSize($is3d, $chunk{options});
my $NdataPiddles = $nextOptionIdx - $argIndex;
# If I have more data piddles that I need, use only what I need now, and
# use the rest for the next curve
if($NdataPiddles > $tuplesize)
{
$nextOptionIdx = $argIndex + $tuplesize;
$NdataPiddles = $tuplesize;
}
my @dataPiddles = @args[$argIndex..$nextOptionIdx-1];
if($NdataPiddles < $tuplesize)
{
# I got fewer data elements than I expected
if(!$is3d && $NdataPiddles+1 == $tuplesize)
{
# A 2D plot is one data element short. Fill in a sequential domain
# 0,1,2,...
unshift @dataPiddles, sequence($dataPiddles[0]->dim(0));
}
elsif($is3d && $NdataPiddles+2 == $tuplesize)
{
# a 3D plot is 2 elements short. Use a grid as a domain
my @dims = $dataPiddles[0]->dims();
if(@dims < 1)
{ barf "plot() tried to build a 2D implicit domain, but the first data piddle is too small"; }
# grab the first 2 dimensions to build the x-y domain
splice @dims, 2;
my $x = zeros(@dims)->xvals->clump(2);
my $y = zeros(@dims)->yvals->clump(2);
unshift @dataPiddles, $x, $y;
# un-grid the data-to plot to match the new domain
foreach my $data(@dataPiddles)
{ $data = $data->clump(2); }
}
else
{ barf "plot() needed $tuplesize data piddles, but only got $NdataPiddles"; }
}
$chunk{data} = \@dataPiddles;
$chunk{tuplesize} = $tuplesize;
$chunk{Ncurves} = countCurvesAndValidate(\%chunk);
$Ncurves += $chunk{Ncurves};
push @chunks, \%chunk;
$argIndex = $nextOptionIdx;
}
return (\@chunks, $Ncurves);
sub parseOptionsArgs
{
# my options are cumulative, except the legend. This variable contains the accumulator
my $options = shift;
# I now have my options arguments. Each curve is described by a hash
# (reference or inline). To have separate options for each curve, I use an
# ref to an array of hashrefs
my @optionsArgs = @_;
# the options for each curve go here
my @curveOptions = ();
my $optionArgIdx = 0;
while ($optionArgIdx < @optionsArgs)
{
my $optionArg = $optionsArgs[$optionArgIdx];
if (ref $optionArg)
{
if (ref $optionArg eq 'HASH')
{
# add this hashref to the options
@{$options}{keys %$optionArg} = values %$optionArg;
push @curveOptions, dclone($options);
# I do not reuse the curve legend, since this would result it multiple
# curves with the same name
delete $options->{legend};
}
else
{
barf "plot() got a reference to a " . ref( $optionArg) . ". I can only deal with HASHes and ARRAYs";
}
$optionArgIdx++;
}
else
{
my %unrefedOptions;
do
{
$optionArg = $optionsArgs[$optionArgIdx];
# this is a scalar. I interpret a pair as key/value
if ($optionArgIdx+1 == @optionsArgs)
{ barf "plot() got a lone scalar argument $optionArg, where a key/value was expected"; }
$options->{$optionArg} = $optionsArgs[++$optionArgIdx];
$optionArgIdx++;
} while($optionArgIdx < @optionsArgs && !ref $optionsArgs[$optionArgIdx]);
push @curveOptions, dclone($options);
# I do not reuse the curve legend, since this would result it multiple
# curves with the same name
delete $options->{legend};
}
}
return \@curveOptions;
}
sub countCurvesAndValidate
{
my $chunk = shift;
# Make sure the domain and ranges describe the same number of data points
my $data = $chunk->{data};
foreach (1..$#$data)
{
my $dim0 = $data->[$_ ]->dim(0);
my $dim1 = $data->[$_-1]->dim(0);
if( $dim0 != $dim1 )
{ barf "plot() was given mismatched tuples to plot. $dim0 vs $dim1"; }
}
# I now make sure I have exactly one set of curve options per curve
my $Ncurves = countCurves($data);
my $Noptions = scalar @{$chunk->{options}};
if($Noptions > $Ncurves)
{ barf "plot() got $Noptions options but only $Ncurves curves. Not enough curves"; }
elsif($Noptions < $Ncurves)
{
# I have more curves then options. I pad the option list with the last
# option, removing the legend
my $lastOption = dclone $chunk->{options}[-1];
delete $lastOption->{legend};
push @{$chunk->{options}}, ($lastOption) x ($Ncurves - $Noptions);
}
return $Ncurves;
sub countCurves
{
# compute how many curves have been passed in, assuming things thread
my $data = shift;
my $N = 1;
# I need to look through every dimension to check that things can thread
# and then to compute how many threads there will be. I skip the first
# dimension since that's the data points, NOT separate curves
my $maxNdims = List::Util::max map {$_->ndims} @$data;
foreach my $dimidx (1..$maxNdims-1)
{
# in a particular dimension, there can be at most 1 non-1 unique
# dimension. Otherwise threading won't work.
my $nonDegenerateDim;
foreach (@$data)
{
my $dim = $_->dim($dimidx);
if($dim != 1)
{
if(defined $nonDegenerateDim && $nonDegenerateDim != $dim)
{
barf "plot() was given non-threadable arguments. Got a dim of size $dim, when I already saw size $nonDegenerateDim";
}
else
{
$nonDegenerateDim = $dim;
}
}
}
# this dimension checks out. Count up the curve contribution
$N *= $nonDegenerateDim if $nonDegenerateDim;
}
return $N;
}
}
sub getTupleSize
{
my $is3d = shift;
my $options = shift;
# I have a list of options for a set of curves in a chunk. Inside a chunk
# the tuple set MUST be the same. I.e. I can have 2d data in one chunk and
# 3d data in another, but inside a chunk it MUST be consistent
my $size;
foreach my $option (@$options)
{
my $sizehere;
if ($option->{tuplesize})
{
# if we have a given tuple size, just use it
$sizehere = $option->{tuplesize};
}
else
{
$sizehere = $is3d ? 3 : 2; # given nothing else, use ONLY the geometrical plotting
}
if(!defined $size)
{ $size = $sizehere;}
else
{
if($size != $sizehere)
{
barf "plot() tried to change tuplesize in a chunk: $size vs $sizehere";
}
}
}
return $size;
}
}
sub testPlotcmd
{
# I test the plot command by making a dummy plot with the test command.
my ($this, $testplotcmd, $testplotdata) = @_;
_printGnuplotPipe( $this, "set terminal push\n" );
_printGnuplotPipe( $this, "set output\n" );
_printGnuplotPipe( $this, "set terminal dumb\n" );
# I send a test plot command. Gnuplot implicitly uses && if multiple
# commands are present on the same line. Thus if I see the post-plot print
# in the output, I know the plot command succeeded
my $postTestplotCheckpoint = 'xxxxxxx Plot succeeded xxxxxxx';
my $print_postTestCheckpoint = "; print \"$postTestplotCheckpoint\"";
_printGnuplotPipe( $this, "$testplotcmd$print_postTestCheckpoint\n" );
_printGnuplotPipe( $this, $testplotdata );
my $checkpointMessage = _checkpoint($this, 'ignore_invalidcommand');
if(defined $checkpointMessage && $checkpointMessage !~ /^$postTestplotCheckpoint/m)
{
# don't actually print out the checkpoint message
$checkpointMessage =~ s/$print_postTestCheckpoint//;
# The checkpoint message does not contain the post-plot checkpoint. This
# means gnuplot decided that the plot command failed.
barf "Gnuplot error: \"\n$checkpointMessage\n\" while sending plotcmd \"$testplotcmd\"";
}
_printGnuplotPipe( $this, "set terminal pop\n" );
}
# syncronizes the child and parent processes. After _checkpoint() returns, I
# know that I've read all the data from the child. Extra data that represents
# errors is returned. Warnings are explicitly stripped out
sub _checkpoint
{
my $this = shift;
my $pipeerr = $this->{err};
# string containing various options to this function
my $flags = shift;
# I have no way of knowing if the child process has sent its error data
# yet. It may be that an error has already occurred, but the message hasn't
# yet arrived. I thus print out a checkpoint message and keep reading the
# child's STDERR pipe until I get that message back. Any errors would have
# been printed before this
my $checkpoint = "xxxxxxx Syncronizing gnuplot i/o xxxxxxx";
_printGnuplotPipe( $this, "print \"$checkpoint\"\n" );
# if no error pipe exists, we can't check for errors, so we're done. Usually
# happens if($dump)
return unless defined $pipeerr;
my $fromerr = '';
do
{
# if no data received in 5 seconds, the gnuplot process is stuck. This
# usually happens if the gnuplot process is not in a command mode, but in
# a data-receiving mode. I'm careful to avoid this situation, but bugs in
# this module and/or in gnuplot itself can make this happen
_logEvent($this, "Trying to read from gnuplot");
if( $this->{errSelector}->can_read(5) )
{
# read a byte into the tail of $fromerr. I'd like to read "as many bytes
# as are available", but I don't know how to this in a very portable way
# (I just know there will be windows users complaining if I simply do a
# non-blocking read). Very little data will be coming in anyway, so
# doing this a byte at a time is an irrelevant inefficiency
my $byte;
sysread $pipeerr, $byte, 1;
$fromerr .= $byte;
_logEvent($this, "Read byte '$byte' (0x" . unpack("H2", $byte) . ") from gnuplot child process");
}
else
{
_logEvent($this, "Gnuplot read timed out");
$this->{checkpoint_stuck} = 1;
barf <<EOM;
Gnuplot process no longer responding. This is likely a bug in PDL::Graphics::Gnuplot
and/or gnuplot itself. Please report this as a PDL::Graphics::Gnuplot bug.
EOM
}
} until $fromerr =~ /\s*(.*?)\s*$checkpoint.*$/ms;
$fromerr = $1;
my $warningre = qr{^(?:Warning:\s*(.*?)\s*$)\n?}m;
if(defined $flags && $flags =~ /printwarnings/)
{
while($fromerr =~ m/$warningre/gm)
{ print STDERR "Gnuplot warning: $1\n"; }
}
# I've now read all the data up-to the checkpoint. Strip out all the warnings
$fromerr =~ s/$warningre//gm;
# if asked, get rid of all the "invalid command" errors. This is useful if
# I'm testing a plot command and I want to ignore the errors caused by the
# test data bein sent to gnuplot as a command. The plot command itself will
# never be invalid, so this doesn't actually mask out any errors
if(defined $flags && $flags =~ /ignore_invalidcommand/)
{
$fromerr =~ s/^gnuplot>\s*(?:$testdataunit_ascii|e\b).*$ # report of the actual invalid command
\n^\s+\^\s*$ # ^ mark pointing to where the error happened
\n^.*invalid\s+command.*$//xmg; # actual 'invalid command' complaint
}
# strip out all the leading/trailing whitespace
$fromerr =~ s/^\s*//;
$fromerr =~ s/\s*$//;
return $fromerr;
}
}
# these are convenience wrappers for plot()
sub plot3d
{
plot('3d' => 1, @_);
}
sub plotlines
{
plot(globalwith => 'lines', @_);
}
sub plotpoints
{
plot(globalwith => 'points', @_);
}
# subroutine to write the columns of some piddles into a gnuplot stream. This
# assumes the last argument is a file handle. Generally you should NOT be using
# this directly at all; it's just used to define the threading-aware routines
sub _wcols_gnuplot
{
my $isbinary = pop @_;
my $this = pop @_;
if( $isbinary)
{
# this is not efficient right now. I should do this in C so that I don't
# have to physical-ize the piddles and so that I can keep the original type
# instead of converting to double
_printGnuplotPipe( $this, ${ cat(@_)->transpose->double->get_dataref } );
}
else
{
_wcolsGnuplotPipe( $this, @_ );
_printGnuplotPipe( $this, "e\n" );
}
};
sub _printGnuplotPipe
{
my $this = shift;
my $string = shift;
my $pipein = $this->{in};
print $pipein $string;
my $len = length $string;
_logEvent($this,
"Sent to child process $len bytes ==========\n" . $string . "\n=========================" );
}
sub _wcolsGnuplotPipe
{
my $this = shift;
my $pipein = $this->{in};
wcols @_, $pipein;
if( $this->{options}{log} )
{
my $string;
open FH, '>', \$string or barf "Couldn't open filehandle into string";
wcols @_, *FH;
close FH;
_logEvent($this,
"Sent to child process ==========\n" . $string . "\n=========================" );
}
}