package SVGGraph;
use strict;
use warnings;
use utf8;
our $VERSION = '0.07';
sub new()
{
my $self = shift;
return bless {}, $self;
}
sub CreateGraph()
{
### First element of @_ is a reference to the element that called this subroutine
my $self = shift;
### Second is a reference to a hash with options
my $options = shift;
### The options passed in the anonymous hash are optional so create a default value first
my $horiUnitDistance = 20;
if ($$options{'horiunitdistance'})
{
$horiUnitDistance = $$options{'horiunitdistance'};
}
my $graphType = 'spline';
if ($$options{'graphtype'})
{
$graphType = $$options{'graphtype'};
}
### The rest are references to arrays with references to arrays with x and y values
my @xyArrayRefs = @_;
### Check if the color ($xyArrayRefs[$i]->[3]) is provided. If not, choose black
for (my $i = 0; $i < @xyArrayRefs; $i++)
{
unless ($xyArrayRefs[$i]->[3])
{
$xyArrayRefs[$i]->[3] = '#000000';
}
}
### Declare the $minX as the lowest value of x in the arrays, same for $minY, $maxX and $maxY
my $minX = $xyArrayRefs[0]->[0]->[0]; ### Equivalent to ${${$xyArrayRefs[0]}[0]}[0];
my $minY = $xyArrayRefs[0]->[1]->[0];
my $maxX = $minX;
my $maxY = $minY;
### Then really search for the lowest and highest value of x and y
for (my $i = 0; $i < @xyArrayRefs; $i++)
{
for (my $j = 0; $j < @{$xyArrayRefs[$i]->[0]}; $j++)
{
if ($xyArrayRefs[$i]->[0]->[$j] > $maxX)
{
$maxX = $xyArrayRefs[$i]->[0]->[$j];
}
if ($xyArrayRefs[$i]->[0]->[$j] < $minX)
{
$minX = $xyArrayRefs[$i]->[0]->[$j];
}
if ($xyArrayRefs[$i]->[1]->[$j] > $maxY)
{
$maxY = $xyArrayRefs[$i]->[1]->[$j];
}
if ($xyArrayRefs[$i]->[1]->[$j] < $minY)
{
$minY = $xyArrayRefs[$i]->[1]->[$j];
}
}
}
### If max equals min, change them artificially
if ($maxX == $minX)
{
$maxX += 1;
}
if ($maxY == $minY)
{
$maxY += 1;
}
### Calculate all dimensions neccessary to create the Graph
### Height of the total svg image in pixels:
my $imageHeight = 400;
if ($$options{'imageheight'})
{
$imageHeight = $$options{'imageheight'};
}
### Width of the verticabar or dots in the graph
my $barWidth = 3;
if ($$options{'barwidth'})
{
$barWidth = $$options{'barwidth'};
}
### Distance between the sides of the gris and the sides of the image:
my $cornerDistance = 50;
### Since svg counts from the top left corner of the image, we translate all coordinates vertically in pixels:
my $vertTranslate = $imageHeight - $cornerDistance;
### The width of the grid in pixels:
my $gridWidth = $horiUnitDistance * ($maxX - $minX);
### The height of the grid in pixels:
my $gridHeight = $imageHeight - 2 * $cornerDistance;
### The width of the whole svg image:
my $imageWidth = $gridWidth + (4 * $cornerDistance);
### The horizontal space between vertical gridlines in pixels:
my $xGridDistance = 20;
### The vertical space between horizontal gridlines in pixels:
my $yGridDistance = 30;
### Now initiate the svg graph by declaring some general stuff.
my $svg .= <<" EOF";
\n";
return $svg;
}
### CreateDot is a subroutine that creates the svg code for different
### kinds of dots used in the spline graph type: circles, squares, triangles and more.
sub CreateDot($$$$$)
{
my $self = shift;
my $x = shift;
my $y = shift;
my $r = shift;
my $color = shift;
$color = $self->DarkenHexRGB($color);
my $dotNumber = shift;
my $d = 2 * $r;
my $negr = -1 * $r;
my $svg;
### Circle
if ($dotNumber == 0)
{
$svg = "\n";
}
### Stars
else
{
$svg .= "\n";
}
return $svg;
}
### NaturalRound is a subroutine that rounds a number to 1, 2, 5 or 10 times its order
### So 110.34 becomes 100
### 3.1234 becomes 2
### 40 becomes 50
sub NaturalRound($)
{
my $self = shift;
my $numberToRound = shift;
my $rounded;
my $order = int (log ($numberToRound) / log (10));
my $remainder = $numberToRound / 10**$order;
if ($remainder < 1.4)
{
$rounded = 10**$order;
}
elsif ($remainder < 3.2)
{
$rounded = 2 * 10**$order;
}
elsif ($remainder < 7.1)
{
$rounded = 5 * 10**$order;
}
else
{
$rounded = 10 * 10**$order;
}
}
### DarkenHexRGB is a subroutine that makes a rgb color value darker
sub DarkenHexRGB($)
{
my $self = shift;
my $hexString = shift;
my $darkHexString;
if ($hexString =~ m/^\#/)
{
$darkHexString = '#';
}
if ($hexString =~ m/^\#?[0-9a-f]{6}$/i)
{
while ($hexString =~ m/([0-9a-f][0-9a-f])/ig)
{
$darkHexString .= sprintf "%02lx", int(hex($1)/2);
}
return $darkHexString;
}
else
{
return $hexString;
}
}
sub NegateHexadecimalRGB($)
{
my $self = shift;
my $hexString = shift;
my $negHexString;
if ($hexString =~ m/^\#/)
{
$negHexString = '#';
}
while ($hexString =~ m/([0-9a-f]{2})/ig)
{
$negHexString .= sprintf "%02lx", (255 - hex($1));
}
return $negHexString;
}
### XMLEscape is a subroutine that converts special XML characters to their xml encoding character.
sub XMLEscape($)
{
my $self = shift;
my $string = shift;
unless (defined ($string))
{
$string = '';
}
$string =~ s/\&/&/g;
$string =~ s/>/>/g;
$string =~ s/</g;
$string =~ s/\"/"/g;
$string =~ s/\'/'/g;
#$string =~ s/([\x00-\x1f])/sprintf('%02X;', ord($1))/ge;
$string =~ s/([\x{80}-\x{ffff}])/sprintf('%04X;', ord($1))/ge;
return $string;
}
1;
__END__
=head1 NAME
SVGGraph - Perl extension for creating SVG Graphs / Diagrams / Charts / Plots.
=head1 SYNOPSIS
use SVGGraph;
my @a = (1, 2, 3, 4);
my @b = (3, 4, 3.5, 6.33);
print "Content-type: image/svg-xml\n\n";
my $SVGGraph = new SVGGraph;
print SVGGraph->CreateGraph(
{'title' => 'Financial Results Q1 2002'},
[\@a, \@b, 'Staplers', 'red']
);
=head1 DESCRIPTION
This module converts sets of arrays with coordinates into
graphs, much like GNUplot would. It creates the graphs in the
SVG (Scalable Vector Graphics) format. It has two styles,
verticalbars and spline. It is designed to be light-weight.
If your internet browser cannot display SVG, try downloading
a plugin at adobe.com.
=head1 EXAMPLES
For examples see: http://pearlshed.nl/svggraph/1.png
and http://pearlshed.nl/svggraph/2.png
Long code example:
#!/usr/bin/perl -w -I.
use strict;
use SVGGraph;
### Array with x-values
my @a = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
### Arrays with y-values
my @b = (-5, 2, 1, 5, 8, 8, 9, 5, 4, 10, 2, 1, 5, 8, 8, 9, 5, 4, 10, 5);
my @c = (6, -4, 2, 1, 5, 8, 8, 9, 5, 4, 10, 2, 1, 5, 8, 8, 9, 5, 4, 10);
my @d = (1, 2, 3, 4, 9, 8, 7, 6, 5, 12, 30, 23, 12, 17, 13, 23, 12, 10, 20, 11);
my @e = (3, 1, 2, -3, -4, -9, -8, -7, 6, 5, 12, 30, 23, 12, 17, 13, 23, 12, 10, 20);
### Initialise
my $SVGGraph = new SVGGraph;
### Print the elusive content-type so the browser knows what mime type to expect
print "Content-type: image/svg-xml\n\n";
### Print the graph
print $SVGGraph->CreateGraph( {
'graphtype' => 'verticalbars', ### verticalbars or spline
'imageheight' => 300, ### The total height of the whole svg image
'barwidth' => 8, ### Width of the bar or dot in pixels
'horiunitdistance' => 20, ### This is the distance in pixels between 1 x-unit
'title' => 'Financial Results Q1 2002',
'titlestyle' => 'font-size:24;fill:#FF0000;',
'xlabel' => 'Week',
'xlabelstyle' => 'font-size:16;fill:darkblue',
'ylabel' => 'Revenue (x1000 USD)',
'ylabelstyle' => 'font-size:16;fill:brown',
'legendoffset' => '10, 10' ### In pixels from top left corner
},
[\@a, \@b, 'Bananas', '#FF0000'],
[\@a, \@c, 'Apples', '#006699'],
[\@a, \@d, 'Strawberries', '#FF9933'],
[\@a, \@e, 'Melons', 'green']
);
=head1 AUTHOR
Teun van Eijsden, teun@chello.nl
=head1 SEE ALSO
http://perldoc.com/
For SVG styling: http://www.w3.org/TR/SVG/styling.html
=cut