File Coverage

blib/lib/PDL/Graphics/Limits.pm
Criterion Covered Total %
statement 339 370 91.6
branch 157 210 74.7
condition 42 65 64.6
subroutine 32 33 96.9
pod 1 12 8.3
total 571 690 82.7


line stmt bran cond sub pod time code
1             package PDL::Graphics::Limits;
2              
3 8     8   2281670 use strict;
  8         58  
  8         323  
4 8     8   41 use warnings;
  8         13  
  8         1343  
5              
6             require Exporter;
7              
8             our @ISA = qw(Exporter);
9              
10             our %EXPORT_TAGS = ( 'all' => [ qw(
11             limits
12             ) ] );
13              
14             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
15              
16             our @EXPORT = qw(
17             limits
18             );
19              
20             our $VERSION = '0.03';
21             $VERSION = eval $VERSION;
22              
23             # Preloaded methods go here.
24              
25 8     8   1034 use PDL::Core qw( cat pdl );
  8         39838  
  8         60  
26 8     8   2125 use PDL::Primitive qw( append );
  8         32696  
  8         97  
27 8     8   4582 use PDL::Fit::Polynomial;
  8         20487  
  8         69  
28 8     8   996 use PDL::Options;
  8         13  
  8         459  
29 8     8   776 use PDL::Bad;
  8         3977  
  8         70  
30 8     8   1427 use Carp;
  8         17  
  8         509  
31 8     8   4004 use POSIX qw( log10 );
  8         54204  
  8         78  
32              
33 8     8   13099 use strict;
  8         16  
  8         202  
34 8     8   30 use warnings;
  8         16  
  8         1262  
35              
36             ################################################################################
37             # figure out what's good in an ndarray after a possible transformation which could
38             # generate Infs or NaN's.
39             sub set_mask
40             {
41 188     188 0 358 my ( $mask, $data ) = @_;
42 188         553 my $badflag = $data->badflag();
43 188         528 $data->badflag(1);
44 188         4149 $mask .= ( $data->isfinite & ! $data->isbad );
45 188         8271 $data->badflag($badflag);
46             }
47              
48              
49              
50             {
51             package PDL::Graphics::Limits::DSet;
52              
53 8     8   49 use PDL::Core qw( cat pdl );
  8         13  
  8         84  
54              
55             *set_mask = \*PDL::Graphics::Limits::set_mask;
56              
57             sub new
58             {
59 124     124   208 my $class = shift;
60 124         262 my $self = bless {}, $class;
61              
62 124         453 my ( $min, $max ) = splice( @_, 0, 2 );
63              
64 124         366 $self->{Vectors} = [ @_ ];
65 124         217 $self->{MinMax} = [ map{ [ $min, $max ] } 1..@{$self->{Vectors}} ];
  245         695  
  124         288  
66              
67 124         423 $self;
68             }
69              
70 219     219   282 sub ndim { scalar @{$_[0]->{Vectors}} }
  219         752  
71              
72             sub validate
73             {
74 120     120   268 my ( $self, $attr) = @_;
75              
76 120         159 my $ivec = 0;
77 120         189 my $n;
78 120         151 foreach my $vec ( @{$self->{Vectors}} )
  120         256  
79             {
80             die( 'vector ', $ivec+1, ": no data?\n" )
81 239 50       557 unless defined $vec->{data};
82              
83 239 100       797 $n = $vec->{data}->nelem unless defined $n;
84              
85             # if a data set vector has no transformation function, use the
86             # default in $attr{Trans}
87             $vec->{trans} = $attr->{Trans}[$ivec]
88 239 100 100     876 if ! exists $vec->{trans} && exists $attr->{Trans}[$ivec];
89              
90             # remove explicitly undefined trans
91             delete $vec->{trans}
92 239 100 100     535 if exists $vec->{trans} && ! defined $vec->{trans};
93              
94             # ensure that data and errors have the same length.
95             die( 'vector ', $ivec+1, ": attribute $_: ",
96             "inconsistent number of elements",
97             "expected $n, got ", $vec->{$_}->nelem, "\n" )
98 239         396 foreach
99             grep { exists $vec->{$_} &&
100             defined $vec->{$_} &&
101 717 100 66     2361 $vec->{$_}->nelem != $n }
102             qw( data en ep );
103             }
104             continue
105             {
106 239         491 $ivec++;
107             }
108              
109             }
110              
111             sub vector
112             {
113 13     13   58 $_[0]->{Vectors}[$_[1]];
114             }
115              
116             sub set_minmax
117             {
118 358     358   938 my ( $dset, $min, $max, $axis ) = @_;
119              
120 358         561 my $mm = $dset->{MinMax}[$axis];
121              
122 358 100       751 $mm->[0] = $min if defined $min;
123 358 100       1351 $mm->[1] = $max if defined $max;
124             }
125              
126             sub upd_minmax
127             {
128 0     0   0 my ( $dset, $min, $max, $axis ) = @_;
129              
130 0         0 my $mm = $dset->{MinMax}[$axis];
131              
132 0 0       0 $mm->[0] = $min if $mm->[0] > $min;
133 0 0       0 $mm->[1] = $max if $mm->[1] < $max;
134             }
135              
136             sub get_minmax
137             {
138 96     96   11202 my ( $dset ) = @_;
139 96         299 cat( map { pdl( $dset->{MinMax}[$_] ) } 0..$dset->ndim-1 );
  191         4299  
140             }
141              
142             sub calc_minmax
143             {
144 154     154   228 my $dset = shift;
145              
146 154 50       453 my @axes = @_ ? ( $_[0] ) : ( 0 ..$dset->ndims-1 );
147              
148 154         409 $dset->calc_minmax_axis( $_ ) foreach @axes;
149             }
150              
151             #####################################################################
152             # determine the limits for a dataset.
153             sub calc_minmax_axis
154             {
155 154     154   249 my ( $dset, $axis ) = @_;
156              
157 154         277 my $vec = $dset->{Vectors}[$axis];
158 154         245 my $data = $vec->{data};
159              
160 154         267 my $xfrm = defined $vec->{trans};
161              
162             # we need the transformed data point min max in case
163             # a transformed data + error is out of range of the transform
164             # function (e.g. log(0)).
165              
166 154         350 my @minmax;
167              
168             # reuse these as much as possible to reduce memory hit
169             my $tmp;
170 154         397 my $mask = PDL::null;
171              
172             # i know of no way of determining whether a function can be applied inplace.
173             # assume not.
174              
175             # if xfrm is true, $tmp will be an independent ndarray, else its an alias for data
176             # no need to create a new ndarray unless necessary.
177 154 100       1673 $tmp = $xfrm ? $vec->{trans}->($data) : $data;
178 154         866 set_mask( $mask, $tmp );
179 154         554 push @minmax, $tmp->where($mask)->minmax;
180              
181 154 100       21518 if ( defined $vec->{errn} )
182             {
183             # worry about not overwriting the original data!
184 9 100       27 if ( $xfrm ) { $tmp .= $vec->{trans}->($data - $vec->{errn}) }
  3         13  
185 6         31 else { $tmp = $data - $vec->{errn} }
186 9         253 set_mask( $mask, $tmp );
187 9         34 push @minmax, $tmp->where($mask)->minmax;
188             }
189              
190 154 100       1478 if ( defined $vec->{errp} )
191             {
192             # worry about not overwriting the original data!
193 12 100       30 if ( $xfrm ) { $tmp .= $vec->{trans}->($data + $vec->{errp}) }
  4         14  
194 8         36 else { $tmp = $data + $vec->{errp} }
195 12         371 set_mask( $mask, $tmp );
196 12         63 push @minmax, $tmp->where($mask)->minmax;
197             }
198              
199 154         1935 my ( $min, $max ) = PDL::Core::pdl( @minmax )->minmax;
200              
201 154         10650 $dset->set_minmax( $min, $max, $axis );
202             }
203              
204             }
205              
206             #####################################################################
207              
208             sub range_frac
209             {
210 2     2 0 4 my ( $axis, $frac, $zerofix ) = @_;
211              
212 2         8 my $expand = $frac * ( $axis->[1] - $axis->[0] );
213 2         4 my $min = $axis->[0] - $expand;
214 2         3 my $max = $axis->[1] + $expand;
215              
216 2 50       4 if ( $zerofix )
217             {
218 0 0 0     0 $min = 0.0
219             if $min < 0 && $axis->[0] >= 0.0;
220              
221 0 0 0     0 $max = 0.0
222             if $max > 0 && $axis->[1] <= 0.0;
223             }
224              
225 2         3 @{$axis} = ( $min, $max );
  2         6  
226             }
227              
228             #####################################################################
229              
230             # routine to find the closest "round" number to X, a "round" number
231             # being 1, 2 or 5 times a power of 10.
232              
233             # If X is negative, round_pow(X) = -round_pow(abs(X)).
234             # If X is zero, the value returned is zero.
235              
236             # round_pow( direction, $x )
237             # where direction is up, down, or both i.e.
238             # $ub = round ( up => $x );
239             # $lb = round ( down => $x );
240              
241             our @nice = ( 1, 2, 5, 10 );
242             our %flip = ( 'up' => 'down', 'down' => 'up' );
243             sub round_pow
244             {
245 78     78 0 198247 my ( $what, $x ) = @_;
246              
247 78 50       206 croak( "incorrect number of arguments" )
248             unless 2 == @_;
249              
250 78 100       218 if ( $x != 0.0 )
251             {
252 76         149 my $xx = abs($x);
253 76         244 my $xlog = log10($xx);
254 76         129 my $ilog = int($xlog);
255              
256 76 100       166 $what = $flip{$what} if $x < 0 ;
257              
258 76 100 100     465 $ilog--
      100        
      100        
      100        
      100        
259             if ( $xlog <= 0 && ( 'down' eq $what || $xlog != $ilog ) )
260             ||
261             ( $xlog > 0 && 'down' eq $what && $xlog == $ilog ) ;
262              
263 76         142 my $pwr = 10 ** $ilog;
264 76         116 my $frac = $xx / $pwr;
265              
266 76         95 my $i;
267 76 100       175 if ( 'up' eq $what )
    50          
268             {
269 38         51 $i = 3;
270 38 100       70 $i = 2 if $frac < $nice[2];
271 38 100       77 $i = 1 if $frac < $nice[1];
272 38 50       64 $i = 0 if $frac < $nice[0];
273 38 100       87 my $t = ( $x < 0 ? -1 : 1 ) * $pwr * $nice[$i];
274 38 50       96 if(abs($t - $x) < 0.0000001) {$i++}
  0         0  
275             }
276              
277             elsif ( 'down' eq $what )
278             {
279 38         51 $i = 0;
280 38 100       77 $i = 1 if $frac > $nice[1];
281 38 100       70 $i = 2 if $frac > $nice[2];
282 38 50       64 $i = 3 if $frac > $nice[3];
283             }
284              
285 76 100       169 $x = ( $x < 0 ? -1 : 1 ) * $pwr * $nice[$i];
286             }
287              
288 78         261 $x;
289             }
290              
291             #####################################################################
292              
293             sub setup_multi
294             {
295 2     2 0 5 my ( $common, $dim, $keys ) = @_;
296              
297 2         3 my @arr;
298              
299 2 50       7 if ( 'ARRAY' eq ref $common )
    50          
300             {
301 0         0 return $common;
302             }
303              
304             elsif ( 'HASH' eq ref $common )
305             {
306 0         0 @arr[ 0..($dim-1)] = map { $common->{$_->{data}} } @{$keys};
  0         0  
  0         0  
307             }
308              
309             else
310             {
311 2         3 my $value = $common;
312 2         4 @arr = ($value) x $dim;
313             }
314              
315 2         3 \@arr;
316             }
317              
318             #####################################################################
319             # normalize_dsets
320             #
321             # transform the user's heterogeneous list of data sets into a regular
322             # list of data sets, each with the form
323             # { Vectors => \@vectors }
324             # where each vector is a hashref with the following keys:
325             # { data => $data,
326             # en => $err_n,
327             # ep => $err_p,
328             # trans => $trans }
329              
330             sub normalize_dsets
331             {
332 70     70 0 52680 my ( $attr, @udsets ) = @_;
333 70         125 my @dsets;
334              
335 70         212 while ( @udsets )
336             {
337 109         177 my $ds = shift @udsets;
338 109         225 my $ref = ref $ds;
339              
340             # peek inside the array to see what's there. we can have the following
341             # [ scalar|ndarray, scalar|ndarray, ... ] -> a zero dimensional data set
342             # [ \@a, \@b, \@c, \%d, ... ] -> a bunch of data sets
343             # [ \%h, @keys ] -> a hash with its keys
344              
345             # scalar or ndarray, turn it into its own data set
346 109 100 66     764 if ( ! $ref || UNIVERSAL::isa($ds, 'PDL') )
    50          
347             {
348             push @dsets,
349             PDL::Graphics::Limits::DSet->new( $attr->{Min}, $attr->{Max},
350 2         13 { data => PDL::Core::topdl( $ds ) } );
351             }
352              
353             elsif ( 'ARRAY' eq $ref )
354             {
355 107         315 normalize_array( \@dsets, $attr, $ds );
356             }
357              
358             else
359             {
360 0         0 die( "data set: ", scalar @dsets + 1,
361             "illegal type in data set list: not an arrayref, scalar, or ndarray\n" );
362             }
363              
364             }
365              
366             # ensure data sets have the same dimensions
367 69         114 my %dim;
368 69         248 $dim{$_->ndim}++ foreach @dsets;
369              
370             # whoops. only one allowed
371 69 100       188 die( "data sets do not all have the same dimensionality\n" )
372             if keys %dim > 1;
373              
374 68         229 ( $attr->{dims} ) = keys %dim;
375              
376             # clean up datasets.
377 68         147 my $idset = -1;
378 68         120 foreach my $dset ( @dsets )
379             {
380 120         191 $idset++;
381              
382 120         186 eval { $dset->validate( $attr ) };
  120         276  
383 120 50       282 if ( $@ )
384             {
385 0         0 chomp $@;
386 0         0 die( "data set $idset: $@\n" );
387             }
388             }
389              
390 68         268 @dsets;
391             }
392              
393             #####################################################################
394              
395             # array refs in data set lists may be just a plain ol' data set, or
396             # it may contain a bunch of other stuff. here we deal with a single
397             # array ref. we tear it apart and (re)build data sets.
398             sub normalize_array
399             {
400 107     107 0 316 my ( $dsets, $attr, $aref ) = @_;
401              
402             # if the first element is a hash, it's either a hash based data set
403             # with a bunch of attributes specific to that hash:
404             # [ \%h, @keys ] -> a hash with its keys
405             # in which case the rest of the elements are scalars, or its
406             # all hashes.
407              
408             eval
409 107         180 {
410 107 100       303 if ( 'HASH' eq ref $aref->[0] )
411             {
412              
413             # all hashes?
414 27 100 33     68 if ( @$aref == grep { 'HASH' eq ref $_ } @$aref )
  56 50       197  
415             {
416             # can't do anything unless we've been told which hash keys
417             # we should use, as this format doesn't allow local specification
418             die( "must specify hash keys for hash based data set spec\n" )
419 20 50 50     112 unless defined $attr->{KeySpec} && scalar @{$attr->{KeySpec}};
  20         78  
420              
421 20         40 foreach ( @{$aref} )
  20         45  
422             {
423 36         62 push @$dsets, normalize_hash_dset($attr, $_, @{$attr->{Keys}} );
  36         170  
424             }
425             }
426              
427             # hash + scalars?
428 20         53 elsif ( @$aref > 1 && 1 == grep { ref $_ } @$aref )
429             {
430 7         15 push @$dsets, normalize_hash_dset( $attr, @{$aref} )
  7         25  
431             }
432              
433             # something wrong
434             else
435             {
436 0         0 die( "hash based data specification has an unexpected element" );
437             }
438              
439             }
440              
441             # must be a list of vectors as either scalars, ndarrays, or array
442             # refs (vectors with attributes)
443             else
444             {
445             # for array based data sets, we have to accumulate vectors as we iterate
446             # through the array. they are stored here
447 80         123 my @vecs;
448              
449 80         149 for my $vec ( @$aref )
450             {
451 159         296 my $ref = ref $vec;
452              
453             eval
454 159         231 {
455             # naked scalar or ndarray: data vector with no attributes
456 159 100 100     689 if ( ! $ref || UNIVERSAL::isa($vec, 'PDL') )
    100          
457             {
458 139         348 push @vecs, { data => PDL::Core::topdl( $vec ) };
459             }
460              
461             # array: data vector with attributes
462             elsif ( 'ARRAY' eq $ref )
463             {
464 19         78 push @vecs, normalize_array_vec( $vec );
465             }
466              
467             else
468             {
469 1         40 die( 'vector ', @vecs+1, ": unexpected data type ($ref) in list of data sets\n" );
470             }
471             };
472              
473 159 100       974 if ( $@ )
474             {
475 1         3 chomp $@;
476 1         6 die( 'vector ', @vecs+1, ": $@\n" );
477             }
478             }
479              
480             push @$dsets,
481 79 50       461 PDL::Graphics::Limits::DSet->new( $attr->{Min}, $attr->{Max}, @vecs )
482             if @vecs;
483             }
484             };
485              
486 107 100       390 if ( $@ )
487             {
488 1         3 chomp $@;
489 1         12 die( 'data set ', @$dsets+1, ": $@\n" );
490             }
491             }
492              
493             #####################################################################
494              
495             # parse an array based vector
496             sub normalize_array_vec
497             {
498 19     19 0 47 my ( $vec ) = @_;
499              
500             # we should have
501             # [ $data, [ $err | $err_n, $err_p ], [ \&func ] ]
502              
503 19         48 my @el = @$vec;
504              
505 19 50 33     83 die( "too few or too many entries in array based data set spec\n" )
506             if @el < 1 || @el > 4;
507              
508 19         46 my %vec;
509 19         96 $vec{data} = PDL::Core::topdl( shift @el);
510              
511             # if last value is CODE, it's a trans
512 19 100       166 $vec{trans} = pop @el if 'CODE' eq ref $el[-1];
513              
514 19 100       74 if ( exists $el[2] )
515             {
516             # if we have 3 elements and the last isn't undef, it's an error.
517             # it can't be CODE as we'd have stripped it off in the last statement
518 2 50       8 die( "illegal value for trans func: $el[2]\n" )
519             if defined $el[2];
520              
521             # we need to turn off trans for this element
522 2         5 $vec{trans} = undef;
523 2         6 pop @el;
524             }
525              
526             # two values? asymmetric errors
527 19 100       97 if ( @el == 2 )
    100          
528             {
529 8 100       53 $vec{errn} = PDL::Core::topdl($el[0]) if defined $el[0];
530 8 100       38 $vec{errp} = PDL::Core::topdl($el[1]) if defined $el[1];
531             }
532              
533             # one value? symmetric errors
534             elsif ( @el == 1 )
535             {
536 6 50       37 $vec{errn} = PDL::Core::topdl($el[0]) if defined $el[0];
537 6 50       40 $vec{errp} = $vec{errn} if defined $vec{errn};
538             }
539              
540 19         72 \%vec;
541             }
542              
543             #####################################################################
544              
545             # this takes a hash and a hash key spec and generates a regularized
546             # data set array of the form
547             # [ { data => $data, ep => ..., en => ..., trans => }, ... ]
548             sub normalize_hash_dset
549             {
550 43     43 0 134 my ( $attr, $ds, @keys ) = @_;
551              
552 43         86 my $KeySpec = $attr->{KeySpec};
553              
554 43         62 my @dset;
555              
556             die( "too many local VecKeys (", scalar @keys,
557 0         0 ") and global VecKeys (", scalar @{$KeySpec}, ")\n" )
558 43 50 100     107 if @keys && @{$KeySpec} && @{$KeySpec} <= @keys;
  7   66     30  
  1         5  
559              
560 43         52 my @spec;
561              
562             # handle local keys
563 43 100       83 if ( @keys )
564             {
565 7         16 my $nvec = 0;
566 7         19 for my $key ( @keys )
567             {
568 13         23 my %spec;
569              
570              
571             # parse the specs for this vector
572 13         24 eval { %spec = parse_vecspec( $key ) };
  13         32  
573 13 50       33 do { chomp $@; die( "vector $nvec: $@" ) }
  0         0  
  0         0  
574             if $@;
575              
576              
577             # now, merge it with the global KeySpecs
578              
579 13 100       22 if ( @{$KeySpec} )
  13         32  
580             {
581 1         3 my $Spec = $KeySpec->[$nvec];
582              
583 1         2 foreach ( keys %{$Spec} )
  1         4  
584             {
585             # only copy from Spec if not present in spec
586 1 50       6 $spec{$_} = $Spec->{$_} if ! exists $spec{$_};
587             }
588             }
589              
590 13         30 push @spec, \%spec;
591             }
592             continue
593             {
594 13         30 $nvec++;
595             }
596              
597             # handle case where local VecKeys are a subst of global VecKeys
598 7         13 while ( @{$KeySpec} > @spec )
  8         30  
599             {
600 1         4 push @spec, $KeySpec->[$nvec++];
601             }
602             }
603              
604             # no local keys; use global KeySpec
605             else
606             {
607 36         46 @spec = @{$KeySpec};
  36         79  
608             }
609              
610 43         68 my $nvec = 0;
611 43         95 for my $spec ( @spec )
612             {
613 86         112 $nvec++;
614 86         111 my %vec;
615              
616             die( "vector $nvec: no data spec?\n" )
617 86 50       161 unless exists $spec->{data};
618              
619 86         135 for my $el ( qw( data errn errp trans ) )
620             {
621 344 100       657 if ( exists $spec->{$el} )
622             {
623              
624             # if not defined, don't bother looking for it in the data set
625 108 100 33     292 unless ( defined $spec->{$el} )
    100          
626             {
627             # trans is different from the others in that we need to pass
628             # it as undef if $spec->{trans} is undef (as full handling of
629             # trans is done elsewhere.
630 1 50       6 $vec{trans} = undef if 'trans' eq $el;
631             }
632              
633 0         0 elsif ( exists $ds->{$spec->{$el}} )
634             {
635 101         218 $vec{$el} = $ds->{$spec->{$el}};
636             }
637              
638             elsif ( $attr->{KeyCroak} )
639             {
640             die( "vector $nvec: missing key in data set hash: ", $spec->{$el}, "\n" )
641             }
642             }
643              
644             }
645              
646             # missing data; certainly a fatal error.
647             die( "vector $nvec: no data for key $spec->{data}\n" )
648 86 50       288 unless defined $vec{data};
649              
650 86         169 push @dset, \%vec;
651             }
652              
653 43         199 PDL::Graphics::Limits::DSet->new( $attr->{Min}, $attr->{Max}, @dset );
654             }
655              
656             #####################################################################
657             # parse specifications for a hash based data set. These are the elements
658             # in the VecKeys attribute. See the docs for more details.
659             # Returns a hashref with keys data, en, ep, trans
660              
661             my $colre = qr/[^&<>=]/;
662              
663             # these are the different specs available.
664             my %keyre = ( data => qr/^($colre+)/,
665             errn => qr/<($colre*)/,
666             errp => qr/>($colre*)/,
667             err => qr/=($colre*)/,
668             trans => qr/\&($colre*)/
669             );
670              
671             my %vecspeckeys = ( data => 1,
672             err => 1,
673             errn => 1,
674             errp => 1,
675             trans => 1 );
676              
677             sub parse_vecspec
678             {
679 55     55 0 258404 my ( $ukeys ) = @_;
680              
681 55         147 my %k;
682              
683             # do we get a hash?
684 55 50       169 if ( 'HASH' eq ref $ukeys )
685             {
686             # complain about keys we don't use
687 0         0 my @badkeys = grep { ! defined $vecspeckeys{$_} } keys %$ukeys;
  0         0  
688 0 0       0 die( "illegal keys: ", join(' ,', sort @badkeys), "\n" )
689             if @badkeys;
690              
691             # copy keys we need
692 0 0       0 do { $k{$_} = $ukeys->{$_} if exists $ukeys->{$_} }
693 0         0 foreach keys %vecspeckeys;
694              
695             }
696              
697             # parse the string.
698             else
699             {
700              
701             # make a local copy, as we modify it in place.
702 55         116 my $keys = $ukeys;
703              
704             # deal with a "default" spec
705 55 100       119 if ( ! defined $keys )
706             {
707 1         3 $keys = '';
708             }
709             else
710             {
711             # spaces and commas are there for human use only
712 54         196 $keys =~ s/[\s,]//g;
713             }
714              
715              
716             # extract the known specs.
717 55         132 my ( $what, $re );
718 55   66     1670 $keys =~ s/$re// and $k{$what} = $1 while( ($what, $re) = each %keyre);
719              
720             # if there's anything left, it's bogus
721 55 100       197 die( "illegal key specification: $ukeys\n" )
722             unless $keys eq '';
723              
724             }
725              
726             # check for consistent error bar specs
727             die( "can't specify `=' with `<' or `>'\n" )
728 54 50 33     202 if exists $k{err} && ( exists $k{errn} || exists $k{errp} );
      66        
729              
730             # error bars are always specified as positive and negative; turn a symmetric
731             # spec into that
732 54 100       128 $k{errn} = $k{errp} = $k{err} if exists $k{err};
733 54         85 delete $k{err};
734              
735             # set empty values to undefined ones
736 54 100       133 do { $k{$_} = undef if $k{$_} eq '' } foreach keys %k;
  83         211  
737              
738 54         300 %k;
739             }
740              
741             sub parse_vecspecs
742             {
743 50     50 0 2698 my $keys = shift;
744 50         82 my @specs;
745              
746             push @specs, { parse_vecspec($_) }
747 50         174 foreach @$keys;
748              
749 50         138 \@specs;
750             }
751              
752             #####################################################################
753             # normalize user supplied limits
754              
755             sub parse_limits
756             {
757 49     49 0 112 my ( $ndim, $spec, $KeySpec ) = @_;
758              
759 49 50       106 $spec = [] unless defined $spec;
760              
761 49         118 my @limits;
762              
763             # array containing limits (as arrays or scalars)
764 49 100       146 if ( 'ARRAY' eq ref $spec )
    50          
765             {
766             # no limits; just move on
767 41 50 0     214 unless ( @$spec )
    100          
768             {
769             }
770              
771             # multi-dimensional data sets
772 0         0 elsif ( 'ARRAY' eq ref $spec->[0] )
773             {
774 16         33 my $ilim = 0;
775 16         47 for my $vlim ( @$spec )
776             {
777 25         43 $ilim++;
778 25 50       61 die( "Limit spec element $ilim: expected array ref\n" )
779             if 'ARRAY' ne ref $vlim;
780              
781 25 50       54 die( "Limit spec element $ilim: too many values\n" )
782             if @$vlim > 2;
783              
784             die( "Limit spec element $vlim: values must be scalars\n" )
785 25 50       73 if grep { ref $_ } @$vlim;
  28         66  
786              
787 25         57 my @lims = @$vlim;
788 25 100       79 $lims[0] = undef unless defined $lims[0];
789 25 100       55 $lims[1] = undef unless defined $lims[1];
790              
791 25         150 push @limits, \@lims;
792             }
793             }
794              
795             # one-dimensional data sets
796             elsif ( ! ref $spec->[0] )
797             {
798             die( "unexpected non-scalar element in Limits spec\n" )
799             if grep { ref $_ } @$spec;
800              
801             my @lims = @$spec;
802             $lims[0] = undef unless defined $lims[0];
803             $lims[1] = undef unless defined $lims[1];
804              
805             push @limits, \@lims;
806             }
807              
808 41         211 push @limits, [ undef, undef ]
809             while ( @limits != $ndim );
810              
811             }
812              
813             # hash containing vector names and limits
814             elsif ( 'HASH' eq ref $spec )
815             {
816             # first ensure that VecKeys has been specified
817 8 50       18 die( "cannot use Limits without VecKeys\n" )
818             unless @$KeySpec;
819              
820             # make sure that we've got common keys.
821 8         16 my %vecs = map { ( $_->{data} => 1) } @$KeySpec;
  16         43  
822              
823             # identify unknown vectors
824 8         20 my @badvecs = grep { ! defined $vecs{$_} } keys %$spec;
  8         15  
825 8 50       19 die( 'unknown vector(s): ', join(', ', @badvecs), "\n" )
826             if @badvecs;
827              
828             # work our way through the KeySpec's, filling in values from
829             # $spec as appropriate.
830 8         12 for my $kspec ( @$KeySpec )
831             {
832 16         31 my @lims = ( undef, undef );
833 16 100       51 if ( exists $spec->{$kspec->{data}} )
834             {
835 8         14 my $lspec = $spec->{$kspec->{data}};
836 8 100       23 $lims[0] = $lspec->{min} if exists $lspec->{min};
837 8 100       15 $lims[1] = $lspec->{max} if exists $lspec->{max};
838             }
839 16         33 push @limits, \@lims;
840             }
841             }
842              
843             # say what?
844             else
845             {
846 0         0 die( "Limits attribute value must be a hashref or arrayref\n" );
847             }
848              
849 49         138 map { { calc => scalar ( grep { !defined $_ } @{$_} ), range => $_ } } @limits;
  97         166  
  194         557  
  97         184  
850             }
851              
852              
853              
854             #####################################################################
855              
856             sub limits
857             {
858 49 50   49 1 1121657 my $attr = 'HASH' eq ref $_[-1] ? pop @_ : {};
859              
860 49         177 my @udsets = @_;
861              
862 49         642 my %attr = iparse( {
863             Min => -1.8e308,
864             Max => +1.8e308,
865             Bounds => 'minmax',
866             Clean => 'RangeFrac',
867             RangeFrac => 0.05,
868             ZeroFix => 0,
869             VecKeys => [],
870             KeyCroak => 1,
871             Limits => [],
872             Trans => [],
873             }, $attr );
874              
875             # turn Trans and VecKeys into arrays if necessary; may be scalars for 1D
876             # data sets
877             $attr{$_} = [ $attr{$_} ]
878 49 50       28662 foreach grep { defined $attr{$_} && 'ARRAY' ne ref $attr{$_} }
  98         550  
879             qw( VecKeys Trans );
880              
881             # parse vector key specs
882 49         171 $attr{KeySpec} = parse_vecspecs( $attr{VecKeys} );
883              
884             # normalize data sets to make life easier later. also
885             # counts up the number of dimensions and sets $attr{dims}
886 49         192 my @dsets = normalize_dsets( \%attr, @udsets );
887              
888             # set up the Limits
889 49         202 my @limits = parse_limits( $attr{dims}, $attr{Limits}, $attr{KeySpec} );
890              
891 49 100       164 if ( 'minmax' eq lc $attr{Bounds} )
    50          
892             {
893 40         219 for my $dim ( 0..$attr{dims}-1 )
894             {
895             # only calculate minmax values for those dims which need them.
896 79         137 my $limits = $limits[$dim];
897              
898 79         133 foreach ( @dsets )
899             {
900             # calculate min & max
901             $_->calc_minmax( $dim )
902 157 100       493 if $limits->{calc};
903              
904             # make sure we pay attention to user specified limits
905 157         253 $_->set_minmax( @{$limits->{range}}, $dim );
  157         432  
906             }
907             }
908             }
909              
910             elsif ( 'zscale' eq lc $attr{Bounds} )
911             {
912             croak( "zscale only good for dim = 2\n" )
913 9 50       34 unless $attr{dims} == 2;
914              
915 9         18 foreach my $dset ( @dsets )
916             {
917             $dset->calc_minmax( 0 )
918 17 100       72 if $limits[0]{calc};
919              
920              
921 17 100       44 if ( $limits[1]{calc} )
922             {
923 13         38 my $y = $dset->vector(1)->{data};
924              
925             # this is a waste, as we don't care about the evaluated
926             # fit values, just the min and max values. since we
927             # get them all anyway, we'll use them.
928              
929 13         29 my $mask = PDL::null;
930 13         130 set_mask( $mask, $y );
931              
932 13         38 my $fit = fitpoly1d( $y->where($mask)->qsort, 2 );
933 13         40549 $dset->set_minmax( $fit->minmax, 1 );
934             }
935              
936 17         52 $dset->set_minmax( @{$limits[$_]{range}}, $_ ) for 0,1;
  34         84  
937             }
938             }
939             else
940             {
941 0         0 die( "unknown Bounds type: $attr{Bounds}\n" );
942             }
943              
944             # derive union of minmax limits from data sets
945 49         153 my $minmax = PDL::Core::null;
946 49         609 $minmax = append( $minmax, $_->get_minmax ) foreach @dsets;
947              
948             # get overall minmax limits
949 49         11605 $minmax = cat(($minmax->minmaximum)[0,1])->transpose;
950              
951 49         5153 my @minmax = map{ [ $minmax->slice(":,$_")->list ] } 0..$attr{dims}-1;
  97         2180  
952              
953 49 100       1676 if ( 'rangefrac' eq lc $attr{Clean} )
    100          
    50          
954             {
955             my $RangeFrac =
956 1         9 setup_multi( $attr{RangeFrac}, $attr{dims}, $attr{KeySpec} );
957              
958             my $ZeroFix =
959 1         3 setup_multi( $attr{ZeroFix}, $attr{dims}, $attr{KeySpec} );
960              
961             range_frac( $minmax[$_], $RangeFrac->[$_], $ZeroFix->[$_] )
962 1         21 for 0..$attr{dims}-1;
963             }
964              
965             elsif ( 'roundpow' eq lc $attr{Clean} )
966             {
967             $_ = [ round_pow( down => $_->[0] ),
968             round_pow( up => $_->[1] ) ]
969 1         7 foreach @minmax;
970             }
971              
972             elsif ( 'none' eq lc $attr{Clean} )
973             {
974             # do nothing
975             }
976              
977             else
978             {
979 0         0 die( "unknown Clean type: $attr{Clean}\n" );
980             }
981              
982 49 100       142 if ( wantarray )
983             {
984 48         93 return map { ( @{$_} ) } @minmax;
  95         154  
  95         1111  
985             }
986             else
987             {
988 1         2 my @key;
989 1 50       2 if ( @{$attr{KeySpec}} )
  1         3  
990             {
991 0         0 @key = map { $_->{data} } @{$attr{KeySpec}};
  0         0  
  0         0  
992             }
993             else
994             {
995 1         4 @key = map { 'q' . $_ } ( 1 .. $attr{dims} );
  2         6  
996             }
997              
998 1         4 return { map { ( $key[$_] => { min => $minmax[$_][0],
  2         35  
999             max => $minmax[$_][1] } ) }
1000             0.. ( @minmax - 1 ) };
1001             }
1002             }
1003              
1004             1;
1005              
1006              
1007             __END__