#========================================================================== # Copyright (c) 1995-2000 Martien Verbruggen #-------------------------------------------------------------------------- # # Name: # GD::Graph::Data.pm # # $Id: Data.pm,v 1.15 2000/10/07 05:52:41 mgjv Exp $ # #========================================================================== package GD::Graph::Data; $GD::Graph::Data::VERSION = '$Revision: 1.15 $' =~ /\s([\d.]+)/; use strict; use GD::Graph::Error; @GD::Graph::Data::ISA = qw( GD::Graph::Error ); =head1 NAME GD::Graph::Data - Data set encapsulation for GD::Graph =head1 SYNOPSIS use GD::Graph::Data; =head1 DESCRIPTION This module encapsulates the data structure that is needed for GD::Graph and friends. An object of this class contains a list of X values, and a number of lists of corresponding Y values. This only really makes sense if the Y values are numerical, but you can basically store anything. Undefined values have a special meaning to GD::Graph, so they are treated with care when stored. Many of the methods of this module are intended for internal use by GD::Graph and the module itself, and will most likely not be useful to you. Many won't even I useful to you... =head1 EXAMPLES use GD::Graph::Data; use GD::Graph::bars; my $data = GD::Graph::Data->new(); $data->read(file => '/data/sales.dat', delimiter => ','); $data = $data->copy(wanted => [2, 4, 5]); # Add the newer figures from the database use DBI; # do DBI things, like connecting to the database, statement # preparation and execution while (@row = $sth->fetchrow_array) { $data->add_point(@row); } my $chart = GD::Graph::bars->new(); my $gd = $chart->plot($data); or for quick changes to legacy code # Legacy code builds array like this @data = ( [qw(Jan Feb Mar)], [1, 2, 3], [5, 4, 3], [6, 3, 7] ); # And we quickly need to do some manipulations on that my $data = GD::Graph::Data->new(); $data->copy_from(\@data); # And now do all the new stuff that's wanted. while (@foo = bar_baz()) { $data->add_point(@foo); } =head1 METHODS =head2 $data = GD::Graph::Data->new() Create a new GD::Graph::Data object. =cut # Error constants use constant ERR_ILL_DATASET => 'Illegal dataset number'; use constant ERR_ILL_POINT => 'Illegal point number'; use constant ERR_NO_DATASET => 'No data sets set'; use constant ERR_ARGS_NO_HASH => 'Arguments must be given as a hash list'; sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = []; bless $self => $class; $self->copy_from(@_) or return $self->_move_errors if (@_); return $self; } sub DESTROY { my $self = shift; $self->clear_errors(); } sub _set_value { my $self = shift; my ($nd, $np, $val) = @_; # Make sure we have empty arrays in between if ($nd > $self->num_sets) { # XXX maybe do this with splice for ($self->num_sets .. $nd - 1) { push @{$self}, []; } } $self->[$nd][$np] = $val; return $self; } =head2 $data->set_x($np, $value); Set the X value of point I<$np> to I<$value>. Points are numbered starting with 0. You probably will never need this. Returns undef on failure. =cut sub set_x { my $self = shift; $self->_set_value(0, @_); } =head2 $data->get_x($np) Get the X value of point I<$np>. See L<"set_x">. =cut sub get_x { my $self = shift; my $np = shift; return $self->_set_error(ERR_ILL_POINT) unless defined $np && $np >= 0; $self->[0][$np]; } =head2 $data->set_y($nd, $np, $value); Set the Y value of point I<$np> in data set I<$nd> to I<$value>. Points are numbered starting with 0, data sets are numbered starting with 1. You probably will never need this. Returns undef on failure. =cut sub set_y { my $self = shift; return $self->_set_error(ERR_ILL_DATASET) unless defined $_[0] && $_[0] >= 1; $self->_set_value(@_); } =head2 $data->get_y($nd, $np) Get the Y value of point I<$np> in data set I<$nd>. See L<"set_y">. This will return undef on an error, but the fact that it returns undef does not mean there was an error (since undefined values can be stored, and therefore returned). =cut sub get_y { my $self = shift; my ($nd, $np) = @_; return $self->_set_error(ERR_ILL_DATASET) unless defined $nd && $nd >= 1 && $nd <= $self->num_sets; return $self->_set_error(ERR_ILL_POINT) unless defined $np && $np >= 0; $self->[$nd][$np]; } =head2 $data->get_y_cumulative($nd, $np) Get the cumulative value of point I<$np> in data set<$nd>. The cumulative value is obtained by adding all the values of the points I<$np> in the data sets 1 to I<$nd>. =cut sub get_y_cumulative { my $self = shift; my ($nd, $np) = @_; return $self->_set_error(ERR_ILL_DATASET) unless defined $nd && $nd >= 1 && $nd <= $self->num_sets; return $self->_set_error(ERR_ILL_POINT) unless defined $np && $np >= 0; my $value; for (my $i = 1; $i <= $nd; $i++) { $value += $self->[$i][$np] || 0; } return $value; } sub _get_min_max { my $self = shift; my $nd = shift; my ($min, $max); for my $val (@{$self->[$nd]}) { next unless defined $val; $min = $val if !defined $min || $val < $min; $max = $val if !defined $max || $val > $max; } return $self->_set_error("No (defined) values in " . ($nd == 0 ? "X list" : "dataset $nd")) unless defined $min && defined $max; return ($min, $max); } =head2 $data->get_min_max_x Returns a list of the minimum and maximum x value or the empty list on failure. =cut sub get_min_max_x { my $self = shift; $self->_get_min_max(0); } =head2 $data->get_min_max_y($nd) Returns a list of the minimum and maximum y value in data set $nd or the empty list on failure. =cut sub get_min_max_y { my $self = shift; my $nd = shift; return $self->_set_error(ERR_ILL_DATASET) unless defined $nd && $nd >= 1 && $nd <= $self->num_sets; $self->_get_min_max($nd); } =head2 $data->get_min_max_y_all() Returns a list of the minimum and maximum y value in all data sets or the empty list on failure. =cut sub get_min_max_y_all { my $self = shift; my ($min, $max); for (my $ds = 1; $ds <= $self->num_sets; $ds++) { my ($ds_min, $ds_max) = $self->get_min_max_y($ds); next unless defined $ds_min; $min = $ds_min if !defined $min || $ds_min < $min; $max = $ds_max if !defined $max || $ds_max > $max; } return $self->_set_error('No (defined) values in any data set') unless defined $min && defined $max; return ($min, $max); } # Undocumented, not part of interface right now. Might expose at later # point in time. sub set_point { my $self = shift; my $np = shift; return $self->_set_error(ERR_ILL_POINT) unless defined $np && $np >= 0; for (my $ds = 0; $ds < @_; $ds++) { $self->_set_value($ds, $np, $_[$ds]); } return $self; } =head2 $data->add_point($X, $Y1, $Y2 ...) Adds a point to the data set. The base for the addition is the current number of X values. This means that if you have a data set with the contents (X1, X2) (Y11, Y12) (Y21) (Y31, Y32, Y33, Y34) a $data->add_point(Xx, Y1x, Y2x, Y3x, Y4x) will result in (X1, X2, Xx ) (Y11, Y12, Y1x) (Y21, undef, Y2x) (Y31, Y32, Y3x, Y34) (undef, undef, Y4x) In other words: beware how you use this. As long as you make sure that all data sets are of equal length, this method is safe to use. =cut sub add_point { my $self = shift; $self->set_point(scalar $self->num_points, @_); } =head2 $data->num_sets() Returns the number of data sets. =cut sub num_sets { my $self = shift; @{$self} - 1; } =head2 $data->num_points() In list context, returns a list with its first element the number of X values, and the subsequent elements the number of respective Y values for each data set. In scalar context returns the number of points that have an X value set, i.e. the number of data sets that would result from a call to C. =cut sub num_points { my $self = shift; return (0) unless @{$self}; wantarray ? map { scalar @{$_} } @{$self} : scalar @{$self->[0]} } =head2 $data->x_values() Return a list of all the X values. =cut sub x_values { my $self = shift; return $self->_set_error(ERR_NO_DATASET) unless @{$self}; @{$self->[0]}; } =head2 $data->y_values($nd) Return a list of the Y values for data set I<$nd>. Data sets are numbered from 1. Returns the empty list if $nd is out of range, or if the data set at $nd is empty. =cut sub y_values { my $self = shift; my $nd = shift; return $self->_set_error(ERR_ILL_DATASET) unless defined $nd && $nd >= 1 && $nd <= $self->num_sets; return $self->_set_error(ERR_NO_DATASET) unless @{$self}; @{$self->[$nd]}; } =head2 $data->reset() OR GD::Graph::Data->reset() As an object method: Reset the data container, get rid of all data and error messages. As a class method: get rid of accumulated error messages and possible other crud. =cut sub reset { my $self = shift; @{$self} = () if ref($self); $self->clear_errors(); return $self; } =head2 $data->make_strict() Make all data set lists the same length as the X list by truncating data sets that are too long, and filling data sets that are too short with undef values. always returns a true value. =cut sub make_strict { my $self = shift; for my $ds (1 .. $self->num_sets) { my $data_set = $self->[$ds]; my $short = $self->num_points - @{$data_set}; next if $short == 0; if ($short > 0) { my @fill = (undef) x $short; push @{$data_set}, @fill; } else { splice @{$data_set}, $short; } } return $self; } =head2 $data->cumulate(preserve_undef => boolean) The B parameter will summarise the Y value sets as follows: the first Y value list will be unchanged, the second will contain a sum of the first and second, the third will contain the sum of first, second and third, and so on. Returns undef on failure. if the argument I is set to a true value, then the sum of exclusively undefined values will be preserved as an undefined value. If it is not present or a false value, undef will be treated as zero. Note that this still will leave undefined values in the first data set alone. Note: Any non-numerical defined Y values will be treated as 0, but you really shouldn't be using this to store that sort of Y data. =cut sub cumulate { my $self = shift; return $self->_set_error(ERR_ARGS_NO_HASH) if (@_ && @_ % 2); my %args = @_; # For all the sets, starting at the last one, ending just # before the first for (my $ds = $self->num_sets; $ds > 1; $ds--) { # For each point in the set for my $point (0 .. $#{$self->[$ds]}) { # Add the value for each point in lower sets to this one for my $i (1 .. $ds - 1) { # If neither are defined, we want to preserve the # undefinedness of this point. If we don't do this, then # the mathematical operation will force undef to be a 0. next if $args{preserve_undef} && ! defined $self->[$ds][$point] && ! defined $self->[$i][$point]; $self->[$ds][$point] += $self->[$i][$point] || 0; } } } return $self; } =head2 $data->wanted(indexes) Removes all data sets except the ones in the argument list. It will also reorder the data sets in the order given. Returns undef on failure. To remove all data sets except the first, sixth and second, in that order: $data->wanted(1, 6, 2) or die $data->error; =cut sub wanted { my $self = shift; for my $wanted (@_) { return $self->_set_error("Wanted index $wanted out of range 1-" . $self->num_sets) if $wanted < 1 || $wanted > $self->num_sets; } @{$self} = @{$self}[0, @_]; return $self; } =head2 $data->reverse Reverse the order of the data sets. =cut sub reverse { my $self = shift; @{$self} = ($self->[0], reverse @{$self}[1..$#{$self}]); return $self; } =head2 $data->copy_from($data_ref) Copy an 'old' style GD::Graph data structure or another GD::Graph::Data object into this object. This will remove the current data. Returns undef on failure. =cut sub copy_from { my $self = shift; my $data = shift; return $self->_set_error('Not a valid source data structure') unless defined $data && ( ref($data) eq 'ARRAY' || ref($data) eq __PACKAGE__); $self->reset; my $i = 0; for my $data_set (@{$data}) { return $self->_set_error("Invalid data set: $i") unless ref($data_set) eq 'ARRAY'; push @{$self}, [@{$data_set}]; $i++; } return $self; } =head2 $data->copy() Returns a copy of the object, or undef on failure. =cut sub copy { my $self = shift; my $new = $self->new(); $new->copy_from($self); return $new; } =head2 $data->read(I) Read a data set from a file. This will remove the current data. returns undef on failure. This method uses the standard module Text::ParseWords to parse lines. If you don't have this for some odd reason, don't use this method, or your program will die. B: The default data file format is tab separated data (which can be changed with the delimiter argument). Comment lines are any lines that start with a #. In the following example I have replaced literal tabs with for clarity # This is a comment, and will be ignored Jan1224 Feb1337 # March is missing Mar Apr918 Valid arguments are: I, mandatory. The file name of the file to read from. $data->read(file => '/data/foo.dat') or die $data->error; I, optional. Give this a true value if you don't want lines with an initial # to be skipped. $data->read(file => '/data/foo.dat', no_comment => 1); I, optional. A regular expression that will become the delimiter instead of a single tab. $data->read(file => '/data/foo.dat', delimiter => '\s+'); $data->read(file => '/data/foo.dat', delimiter => qr/\s+/); =cut sub read { my $self = shift; local(*DATA, $_); return $self->_set_error(ERR_ARGS_NO_HASH) if (@_ && @_ % 2); my %args = @_; return $self->_set_error('Missing required argument: file') unless $args{file}; my $delim = $args{delimiter} || "\t"; # The following will die if these modules are not present, as # documented. require Text::ParseWords; $self->reset; open(DATA, $args{file}) or return $self->_set_error("open ($args{file}): $!"); while () { chomp; next if /^#/ && !$args{no_comment}; my @fields = Text::ParseWords::parse_line($delim, 1, $_); next unless @fields; $self->add_point(@fields); } return $self; } =head2 $data->error() OR GD::Graph::Data->error() Returns a list of all the errors that the current object has accumulated. In scalar context, returns the last error. If called as a class method it works at a class level. This method is inherited, see L for more information. =cut =head2 $data->has_error() OR GD::Graph::Data->has_error() Returns true if the object (or class) has errors pending, false if not. In some cases (see L<"copy">) this is the best way to check for errors. This method is inherited, see L for more information. =cut =head1 NOTES As with all Modules for Perl: Please stick to using the interface. If you try to fiddle too much with knowledge of the internals of this module, you could get burned. I may change them at any time. Specifically, I probably won't always keep this implemented as an array reference. =head1 AUTHOR Martien Verbruggen Emgjv@tradingpost.com.auE =head2 Copyright Copyright (c) 2000 Martien Verbruggen. All rights reserved. This package is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 SEE ALSO L, L =cut "Just another true value";