File Coverage

blib/lib/PostScript/Graph/XY.pm
Criterion Covered Total %
statement 214 240 89.1
branch 99 150 66.0
condition 28 38 73.6
subroutine 14 21 66.6
pod 12 14 85.7
total 367 463 79.2


line stmt bran cond sub pod time code
1             package PostScript::Graph::XY;
2             our $VERSION = 0.04;
3 3     3   107319 use strict;
  3         8  
  3         128  
4 3     3   19 use warnings;
  3         7  
  3         101  
5 3     3   4447 use Text::CSV_XS;
  3         57274  
  3         227  
6 3     3   37 use PostScript::File 1.00 qw(check_file array_as_string);
  3         110  
  3         174  
7 3     3   4314 use PostScript::Graph::Key 1.00;
  3         76  
  3         225  
8 3     3   36 use PostScript::Graph::Paper 1.00;
  3         46  
  3         73  
9 3     3   1655 use PostScript::Graph::Style 1.00;
  3         88  
  3         9562  
10              
11             =head1 NAME
12              
13             PostScript::Graph::XY - graph lines and points
14              
15             =head1 SYNOPSIS
16              
17             =head2 Simplest
18              
19             Draw a graph from data in the CSV file 'results.csv', and saves it as 'results.ps':
20            
21             use PostScript::Graph::XY;
22              
23             my $xy = new PostScript::Graph::XY();
24             $xy->build_chart("results.csv");
25             $xy->output("results");
26            
27             =head2 Typical
28            
29             With more direct control:
30              
31             use PostScript::Graph::XY;
32             use PostScript::Graph::Style;
33              
34             my $seq = PostScript::Graph::Sequence;
35             $seq->setup('color',
36             [ [ 1, 1, 0 ], # yellow
37             [ 0, 1, 0 ], # green
38             [ 0, 1, 1 ], ], # cyan
39             );
40            
41             my $xy = new PostScript::Graph::XY(
42             file => {
43             errors => 1,
44             eps => 0,
45             landscape => 1,
46             paper => 'Letter',
47             },
48            
49             layout => {
50             dots_per_inch => 72,
51             heading => "Example",
52             background => [ 0.9, 0.9, 1 ],
53             heavy_color => [ 0, 0.2, 0.8 ],
54             mid_color => [ 0, 0.5, 1 ],
55             light_color => [ 0.7, 0.8, 1 ],
56             },
57            
58             x_axis => {
59             smallest => 4,
60             title => "Control variable",
61             font => "Courier",
62             },
63             y_axis => {
64             smallest => 3,
65             title => "Dependent variable",
66             font => "Courier",
67             },
68            
69             style => {
70             auto => [qw(color dashes)],
71             color => 0,
72             line => {
73             inner_width => 2,
74             outer_width => 2.5,
75             outer_dashes => [],
76             },
77             point => {
78             shape => "circle",
79             size => 8,
80             color => [ 1, 0, 0 ],
81             },
82             },
83            
84             key => {
85             background => 0.9,
86             },
87             );
88              
89             $xy->line_from_array(
90             [ [ qw(Control First Second Third Fourth),
91             qw(Fifth Sixth Seventh Eighth Nineth)],
92             [ 1, 0, 1, 2, 3, 4, 5, 6, 7, 8 ],
93             [ 2, 1, 2, 3, 4, 5, 6, 7, 8, 9 ],
94             [ 3, 2, 3, 4, 5, 6, 7, 8, 9,10 ],
95             [ 4, 3, 4, 5, 6, 7, 8, 9,10,11 ], ]
96             );
97             $xy->build_chart();
98             $xy->output("controlled");
99            
100             =head2 All options
101            
102             $xy = new PostScript::Graph::XY(
103             file => {
104             # see PostScript::File
105             },
106             layout => {
107             # see PostScript::Graph::Paper
108             },
109             x_axis => {
110             # see PostScript::Graph::Paper
111             },
112             y_axis => {
113             # see PostScript::Graph::Paper
114             },
115             style => {
116             # see PostScript::Graph::Style
117             },
118             key => {
119             # see PostScript::Graph::Key
120             },
121             chart => {
122             # see 'new' below
123             },
124             );
125            
126             =head1 DESCRIPTION
127              
128             A graph is drawn on a PostScript file from one or more sets of numeric data. Scales are automatically adjusted
129             for each data set and the style of lines and points varies between them. A title, axis labels and a key are also
130             provided.
131              
132             =head1 CONSTRUCTOR
133              
134             =cut
135              
136             sub new {
137 3     3 1 1311 my $class = shift;
138 3         9 my $opt = {};
139 3 50       18 if (@_ == 1) { $opt = $_[0]; } else { %$opt = @_; }
  0         0  
  3         20  
140            
141 3         7 my $o = {};
142 3         10 bless( $o, $class );
143 3         26 $o->{opt} = $opt;
144            
145 3 100       21 $o->{opt}{file} = {} unless (defined $o->{opt}{file});
146 3 100       19 $o->{opt}{layout} = {} unless (defined $o->{opt}{layout});
147 3 100       25 $o->{opt}{x_axis} = {} unless (defined $o->{opt}{x_axis});
148 3 100       28 $o->{opt}{y_axis} = {} unless (defined $o->{opt}{y_axis});
149 3 100       18 $o->{opt}{style} = {} unless (defined $o->{opt}{style});
150 3 50       18 $o->{opt}{key} = {} unless (defined $o->{opt}{key});
151 3 50       23 $o->{opt}{chart} = {} unless (defined $o->{opt}{chart});
152              
153 3         11 my $ch = $opt->{chart};
154 3 50       41 $o->{points} = defined($ch->{show_points}) ? $ch->{show_points} : 1;
155 3 50       13 $o->{lines} = defined($ch->{show_lines}) ? $ch->{show_lines} : 1;
156 3 50       21 $o->{key} = defined($ch->{show_key}) ? $ch->{show_key} : 1;
157 3 50       17 $o->{data} = defined($ch->{data}) ? $ch->{data} : undef;
158              
159 3 50       39 $o->{opt}{style}{sequence} = new PostScript::Graph::Sequence() unless (defined $o->{opt}{style}{sequence});
160 3 50       15 $o->build_chart($o->{data}, $opt->{style}) if ($o->{data});
161              
162 3         12 return $o;
163             }
164              
165             =head2 new( [options] )
166              
167             C may be either a list of hash keys and values or a hash reference. Either way, the hash should have the
168             same structure - made up of keys to several sub-hashes. Only one (chart) holds options for this module. The
169             other sections are passed on to the appropriate module as it is created.
170              
171             Hash Key Module
172             ======== ======
173             file PostScript::File
174             layout PostScript::Graph::Paper
175             x_axis PostScript::Graph::Paper
176             y_axis PostScript::Graph::Paper
177             style PostScript::Graph::Style
178             key PostScript::Graph::Key
179             chart this one, see below
180              
181             =head3 data
182              
183             This may be either an array or the name of a CSV file. See B or B for details.
184             If data is given here, the chart is built automatically. There is no opportunity to add extra lines (they should
185             be included in this data) but there is no need to call B explicitly as the chart is ready for output.
186              
187             =head3 show_key
188              
189             Set to 0 if key panel is not required. (Default: 1)
190              
191             =head3 show_lines
192              
193             Set to 0 to hide lines and make a scatter graph. (Default: 1)
194              
195             =head3 show_points
196              
197             Set to 0 to hide points. (Default: 1)
198              
199             All the settings are optional and the defaults work reasonably well. See the other PostScript manpages for
200             details of their options.
201              
202             =head1 OBJECT METHODS
203              
204             =cut
205              
206             sub line_from_array {
207 16     16 1 960 my $o = shift;
208 16         113 my ($data, $style, $opts, $label);
209 16         35 foreach my $arg (@_) {
210 31         62 $_ = ref($arg);
211             CASE: {
212 31 100       33 if (/ARRAY/) { $data = $arg; last CASE; }
  31         98  
  16         26  
  16         35  
213 15 100       70 if (/HASH/) { $opts = $arg; last CASE; }
  14         20  
  14         33  
214 1 50       4 if (/PostScript::Graph::Style/) { $style = $arg; last CASE; }
  0         0  
  0         0  
215 1         3 $label = $arg;
216             }
217             }
218 16 50       55 die "add_line() requires an array\nStopped" unless (defined $data);
219 16 100       100 $o->{ylabel} = $label unless (defined $o->{ylabel});
220            
221             ## create style object
222 16 100       80 $opts = $o->{opt}{style} unless (defined $opts);
223 16 100       149 $opts->{line} = {} unless (defined $opts->{line});
224 16 100       42 $opts->{point} = {} unless (defined $opts->{point});
225 16 50       100 $style = new PostScript::Graph::Style($opts) unless (defined $style);
226            
227             ## split multi-columns into seperate lines
228 16         37 my $name = $o->{default}++;
229 16         40 my ($first, @rest) = split_data($data);
230 16         31 foreach my $column (@rest) {
231 12         77 $o->line_from_array($column, $opts);
232             }
233            
234             ## identify axis titles
235 16         76 $o->{line}{$name}{xtitle} = "";
236 16         35 my $line = $o->{line}{$name};
237 16   100     127 $line->{ytitle} = $label || "";
238 16         34 $line->{style} = $style;
239            
240 16         83 my $number = qr/^\s*[-+]?[0-9.]+(?:[Ee][-+]?[0-9.]+)?\s*$/;
241 16 100       212 unless ($first->[0][1] =~ $number) {
242 13         21 my $row = shift(@$first);
243 13         30 $line->{xtitle} = $$row[0];
244 13         46 $line->{ytitle} = $$row[1];
245             }
246 16 100       46 $o->{ylabel} = $line->{ytitle} unless (defined $o->{ylabel});
247            
248             ## find min and max for each axis
249 16         28 my @coords;
250 16         36 my ($xmin, $ymin, $xmax, $ymax);
251 16         29 foreach my $row (@$first) {
252 70         120 my ($x, $y) = @$row;
253 70 50       461 if ($x =~ $number) {
254 70 100 66     352 $xmin = $x if (not defined($xmin) or $x < $xmin);
255 70 50 66     441 $xmax = $x if (not defined($xmax) or $x > $xmax);
256             }
257 70 50       453 if ($y =~ $number) {
258 70 100 100     280 $ymin = $y if (not defined($ymin) or $y < $ymin);
259 70 100 100     343 $ymax = $y if (not defined($ymax) or $y > $ymax);
260             }
261             }
262 16         41 $line->{data} = $first;
263 16         55 $line->{last} = 2 * ($#$first + 1) - 1;
264 16         37 $line->{xmin} = $xmin;
265 16         33 $line->{xmax} = $xmax;
266 16         54 $line->{ymin} = $ymin;
267 16         200 $line->{ymax} = $ymax;
268             }
269              
270             =head2 line_from_array( data [, label | opts | style ]... )
271              
272             =over 8
273              
274             =item data
275              
276             An array reference pointing to a list of positions.
277              
278             =item label
279              
280             A string to represent this line in the Key.
281              
282             =item opts
283              
284             This should be a hash reference containing keys and values suitable for a PostScript::Graph::Style object. If present,
285             the object is created with the options specified.
286              
287             =item style
288              
289             It is also acceptable to create a PostScript::Graph::Style object independently and pass that in here.
290              
291             =back
292              
293             One or more lines of data is added to the chart. This may be called many times before the chart is finalized with
294             B.
295            
296             Each position is the data array contains an x value and one or more y values. For example, the following points
297             will be plotted on an x axis from 2 to 4 a y axis including from 49 to 57.
298              
299             [ [ 2, 49.7 ],
300             [ 3, 53.4 ],
301             [ 4. 56.1 ], ]
302              
303             This will plot three lines with 6 points each.
304              
305             [ ["X", "Y", "Yb", "Yc"],
306             [x0, y0, yb0, yc0],
307             [x1, y1, yb1, yc1],
308             [x2, y2, yb2, yc2],
309             [x3, y3, yb3, yc3],
310             [x4, y4, yb4, yc4],
311             [x5, y5, yb5, yc5], ]
312              
313             The first line is made up of (x0,y0), (x1,y1)... and these must be there. The second line comes from (x0,yb0),
314             (x1,yp1)... and so on. Optionally, the first row of data in the array may be labels for the X and Y axis, and
315             then for each line.
316              
317             Where multiple lines are given, it is best to specify C
318             name of the first line - rarely what you want. Of course this is ignored if the B option 'y_axis => title'
319             was given.
320              
321             =cut
322              
323             sub line_from_file {
324 1     1 1 197 my ($o, $file, $style) = @_;
325 1         8 my $filename = check_file($file);
326 1         265 my @data;
327 1         10 my $csv = new Text::CSV_XS;
328 1 50       151 open(INFILE, "<", $filename) or die "Unable to open \'$filename\': $!\nStopped";
329 1         30 while () {
330 6         10 chomp;
331 6         19 my $ok = $csv->parse($_);
332 6 100       140 if ($ok) {
333 5         17 my @row = $csv->fields();
334 5 50       63 push @data, [ @row ] if (@row);
335             }
336             }
337 1         11 close INFILE;
338              
339 1         5 $o->line_from_array( \@data, $style );
340             }
341              
342             =head2 line_from_file( file [, label|opts|style ]... )
343              
344             =over 4
345              
346             =item C
347              
348             The name of a CSV file.
349              
350             =item C
351              
352             A string to represent this line in the Key.
353              
354             =item C
355              
356             This should be a hash reference containing keys and values suitable for a PostScript::Graph::Style object. If present,
357             the object is created with the options specified.
358              
359             =item C