package PostScript::Graph::XY; our $VERSION = 0.04; use strict; use warnings; use Text::CSV_XS; use PostScript::File 1.00 qw(check_file array_as_string); use PostScript::Graph::Key 1.00; use PostScript::Graph::Paper 1.00; use PostScript::Graph::Style 1.00; =head1 NAME PostScript::Graph::XY - graph lines and points =head1 SYNOPSIS =head2 Simplest Draw a graph from data in the CSV file 'results.csv', and saves it as 'results.ps': use PostScript::Graph::XY; my $xy = new PostScript::Graph::XY(); $xy->build_chart("results.csv"); $xy->output("results"); =head2 Typical With more direct control: use PostScript::Graph::XY; use PostScript::Graph::Style; my $seq = PostScript::Graph::Sequence; $seq->setup('color', [ [ 1, 1, 0 ], # yellow [ 0, 1, 0 ], # green [ 0, 1, 1 ], ], # cyan ); my $xy = new PostScript::Graph::XY( file => { errors => 1, eps => 0, landscape => 1, paper => 'Letter', }, layout => { dots_per_inch => 72, heading => "Example", background => [ 0.9, 0.9, 1 ], heavy_color => [ 0, 0.2, 0.8 ], mid_color => [ 0, 0.5, 1 ], light_color => [ 0.7, 0.8, 1 ], }, x_axis => { smallest => 4, title => "Control variable", font => "Courier", }, y_axis => { smallest => 3, title => "Dependent variable", font => "Courier", }, style => { auto => [qw(color dashes)], color => 0, line => { inner_width => 2, outer_width => 2.5, outer_dashes => [], }, point => { shape => "circle", size => 8, color => [ 1, 0, 0 ], }, }, key => { background => 0.9, }, ); $xy->line_from_array( [ [ qw(Control First Second Third Fourth), qw(Fifth Sixth Seventh Eighth Nineth)], [ 1, 0, 1, 2, 3, 4, 5, 6, 7, 8 ], [ 2, 1, 2, 3, 4, 5, 6, 7, 8, 9 ], [ 3, 2, 3, 4, 5, 6, 7, 8, 9,10 ], [ 4, 3, 4, 5, 6, 7, 8, 9,10,11 ], ] ); $xy->build_chart(); $xy->output("controlled"); =head2 All options $xy = new PostScript::Graph::XY( file => { # see PostScript::File }, layout => { # see PostScript::Graph::Paper }, x_axis => { # see PostScript::Graph::Paper }, y_axis => { # see PostScript::Graph::Paper }, style => { # see PostScript::Graph::Style }, key => { # see PostScript::Graph::Key }, chart => { # see 'new' below }, ); =head1 DESCRIPTION A graph is drawn on a PostScript file from one or more sets of numeric data. Scales are automatically adjusted for each data set and the style of lines and points varies between them. A title, axis labels and a key are also provided. =head1 CONSTRUCTOR =cut sub new { my $class = shift; my $opt = {}; if (@_ == 1) { $opt = $_[0]; } else { %$opt = @_; } my $o = {}; bless( $o, $class ); $o->{opt} = $opt; $o->{opt}{file} = {} unless (defined $o->{opt}{file}); $o->{opt}{layout} = {} unless (defined $o->{opt}{layout}); $o->{opt}{x_axis} = {} unless (defined $o->{opt}{x_axis}); $o->{opt}{y_axis} = {} unless (defined $o->{opt}{y_axis}); $o->{opt}{style} = {} unless (defined $o->{opt}{style}); $o->{opt}{key} = {} unless (defined $o->{opt}{key}); $o->{opt}{chart} = {} unless (defined $o->{opt}{chart}); my $ch = $opt->{chart}; $o->{points} = defined($ch->{show_points}) ? $ch->{show_points} : 1; $o->{lines} = defined($ch->{show_lines}) ? $ch->{show_lines} : 1; $o->{key} = defined($ch->{show_key}) ? $ch->{show_key} : 1; $o->{data} = defined($ch->{data}) ? $ch->{data} : undef; $o->{opt}{style}{sequence} = new PostScript::Graph::Sequence() unless (defined $o->{opt}{style}{sequence}); $o->build_chart($o->{data}, $opt->{style}) if ($o->{data}); return $o; } =head2 new( [options] ) C may be either a list of hash keys and values or a hash reference. Either way, the hash should have the same structure - made up of keys to several sub-hashes. Only one (chart) holds options for this module. The other sections are passed on to the appropriate module as it is created. Hash Key Module ======== ====== file PostScript::File layout PostScript::Graph::Paper x_axis PostScript::Graph::Paper y_axis PostScript::Graph::Paper style PostScript::Graph::Style key PostScript::Graph::Key chart this one, see below =head3 data This may be either an array or the name of a CSV file. See B or B for details. If data is given here, the chart is built automatically. There is no opportunity to add extra lines (they should be included in this data) but there is no need to call B explicitly as the chart is ready for output. =head3 show_key Set to 0 if key panel is not required. (Default: 1) =head3 show_lines Set to 0 to hide lines and make a scatter graph. (Default: 1) =head3 show_points Set to 0 to hide points. (Default: 1) All the settings are optional and the defaults work reasonably well. See the other PostScript manpages for details of their options. =head1 OBJECT METHODS =cut sub line_from_array { my $o = shift; my ($data, $style, $opts, $label); foreach my $arg (@_) { $_ = ref($arg); CASE: { if (/ARRAY/) { $data = $arg; last CASE; } if (/HASH/) { $opts = $arg; last CASE; } if (/PostScript::Graph::Style/) { $style = $arg; last CASE; } $label = $arg; } } die "add_line() requires an array\nStopped" unless (defined $data); $o->{ylabel} = $label unless (defined $o->{ylabel}); ## create style object $opts = $o->{opt}{style} unless (defined $opts); $opts->{line} = {} unless (defined $opts->{line}); $opts->{point} = {} unless (defined $opts->{point}); $style = new PostScript::Graph::Style($opts) unless (defined $style); ## split multi-columns into seperate lines my $name = $o->{default}++; my ($first, @rest) = split_data($data); foreach my $column (@rest) { $o->line_from_array($column, $opts); } ## identify axis titles $o->{line}{$name}{xtitle} = ""; my $line = $o->{line}{$name}; $line->{ytitle} = $label || ""; $line->{style} = $style; my $number = qr/^\s*[-+]?[0-9.]+(?:[Ee][-+]?[0-9.]+)?\s*$/; unless ($first->[0][1] =~ $number) { my $row = shift(@$first); $line->{xtitle} = $$row[0]; $line->{ytitle} = $$row[1]; } $o->{ylabel} = $line->{ytitle} unless (defined $o->{ylabel}); ## find min and max for each axis my @coords; my ($xmin, $ymin, $xmax, $ymax); foreach my $row (@$first) { my ($x, $y) = @$row; if ($x =~ $number) { $xmin = $x if (not defined($xmin) or $x < $xmin); $xmax = $x if (not defined($xmax) or $x > $xmax); } if ($y =~ $number) { $ymin = $y if (not defined($ymin) or $y < $ymin); $ymax = $y if (not defined($ymax) or $y > $ymax); } } $line->{data} = $first; $line->{last} = 2 * ($#$first + 1) - 1; $line->{xmin} = $xmin; $line->{xmax} = $xmax; $line->{ymin} = $ymin; $line->{ymax} = $ymax; } =head2 line_from_array( data [, label | opts | style ]... ) =over 8 =item data An array reference pointing to a list of positions. =item label A string to represent this line in the Key. =item opts This should be a hash reference containing keys and values suitable for a PostScript::Graph::Style object. If present, the object is created with the options specified. =item style It is also acceptable to create a PostScript::Graph::Style object independently and pass that in here. =back One or more lines of data is added to the chart. This may be called many times before the chart is finalized with B. Each position is the data array contains an x value and one or more y values. For example, the following points will be plotted on an x axis from 2 to 4 a y axis including from 49 to 57. [ [ 2, 49.7 ], [ 3, 53.4 ], [ 4. 56.1 ], ] This will plot three lines with 6 points each. [ ["X", "Y", "Yb", "Yc"], [x0, y0, yb0, yc0], [x1, y1, yb1, yc1], [x2, y2, yb2, yc2], [x3, y3, yb3, yc3], [x4, y4, yb4, yc4], [x5, y5, yb5, yc5], ] The first line is made up of (x0,y0), (x1,y1)... and these must be there. The second line comes from (x0,yb0), (x1,yp1)... and so on. Optionally, the first row of data in the array may be labels for the X and Y axis, and then for each line. Where multiple lines are given, it is best to specify C