Creating Scalable Vector Graphics with Perl
by Kip Hampton
|
Pages: 1, 2
Generating Dynamic SVG From Non-XML Data
There are many examples on the Web that illustrate how to generate SVG images from other data sources. Most, however, presume that the data in question is already marked up as XML and, in the real world, this is often not the case. Although it may be trivial (using Perl) to create XML documents based on the data at hand, unless those document are useful elsewhere this would constitute a needless extra step in our applications. As usual, Perl lets us cut right to the chase.
For our second and final example we will parse a Web host's user agent log and, based on the data extracted, generate an SVG pie chart that illustrates the browser preference among visitors to that site. In order to make the chart more meaningful, the generated image will also feature a color-coded legend detailing the names and number of requests made by each type of browser.
Our script begins by passing the sole command line argument, the
location of the user agent log we wish to analyze, to Akira Hangai's
Apache::ParseLog. We then extract a hash containing the
browser data using the browser() method, and then we
calculate the total number of browsers reported using Perl's built-in
map function.
use strict;
use XML::Writer;
use Apache::ParseLog;
my $ua_log = $ARGV[0];
my $log = Apache::ParseLog->new();
$log = $log->config(agentlog => $ua_log);
my $log_data = $log->getAgentLog;
my %browsers = $log_data->browser();
my $total_browsers = 0;
map {$total_browsers += $_ } values (%browsers);
We declare a few global variables that will help us create our
graphic. We use globals here rather than hardcoding the values later
in order to make the script a bit more flexible. For example, changing
the value for $pie_radius also alters the overall height
of the image and the placement of the chart's legend. Also, since we
do not know the how many individual types and versions of browsers
will be found while parsing the log, we will also adjust the image's
height based on the number of keys in the %browsers hash
to ensure that it is neither too long nor too short.
my $pi = '3.14159256';
my $pie_radius = '90';
my $pie_center_x = '150';
my $pie_center_y = '125';
my $wedge_rotation = '0';
my $legend_start = $pie_center_y + $pie_radius + 10;
my $image_height = $legend_start + (20 * scalar (keys (%browsers)));
# generate random hex colors for the wedges
my @colors = map {join "", map { sprintf "%02x", rand(255) } (0..2) } (0..63);
With the initialization out of the way we can get down to the
business of generating our image. We will start by creating a new
XML::Writer object, setting the XML encoding
pseudo-attribute to UTF-8, and adding the standard SVG Document Type
Definition,
my $writer = XML::Writer->new();
$writer->xmlDecl('UTF-8');
$writer->doctype('svg', '-//W3C//DTD SVG 20001102//EN',
'http://www.w3.org/TR/2000/CR-SVG-20001102/DTD/svg-20001102.dtd');
All SVG images must have an <svg> element as the root
element. The SVG specification provides developers with many
attributes for this root element that can be used to configure how the
image is rendered, but since our needs are modest we only set the
height and width. While the width attribute is hardcoded, the height
attribute is set to the value of $image_height, which we
calculated earlier.
$writer->startTag('svg',
height => $image_height,
width => '300');
Next we add a simple heading to our image using SVG's <text> element. SVG shares many of the styling rules of Cascading Style Sheets, which is convenient for those familiar with CSS already.
$writer->startTag('text',
x => '20',
y => '20',
style => 'font-size:14;font-weight:bold;fill:#000000');
$writer->characters('Browser Stats - ' . localtime(time));
$writer->endTag('text');
The last step before creating the wedges for our pie chart is to
define a <g> (group) element to use as a wrapper for pie wedges
themselves. While this element is optional, it will greatly simplify
the task of creating the individual wedges since all child elements in
a group inherit the properties of that enclosing group (unless
explicitly overridden). That means that each wedge of our chart will
begin at the coordinates defined by $pie_center_x and
$pie_center_y through the use of the transform attribute.
$writer->startTag('g',
id => 'pieChart',
transform => "translate($pie_center_x,$pie_center_y)");
To create the pie chart for our sample image all we need to do is
loop over the elements in the %browsers hash, select a
random hex color from the @colors array, and draw the
wedge that represents each browser's fractional percentage of the
total number of hits. In the interest of clarity we will not descend
into a detailed description of how the size of each wedge is
calculated, nor will we look at the SVG <path> element's
somewhat esoteric syntax for drawing free-form shapes. For our
purposes it is enough to understand that each wedge is created in the
context of the x and y coordinates inherited from the parent <g>
element and is rotated into it's proper place based on the value of
the $wedge_rotation variable. For a more detailed
discussion of the <path> element and its properties, please see
the relevant parts
of the SVG specification.
my $i = 0;
my %color_lookup = ();
foreach my $browser (sortHashByValue(%browsers)) {
my $do_arc = '0';
my $wedge_color = '#' . $colors[$i];
my $wedge = $browsers{$browser} / $total_browsers * 360;
my $radians = $wedge * $pi / 180;
my $ry = 0 - int($pie_radius * sin($radians));
my $rx = int($pie_radius * cos($radians));
$do_arc++ if $wedge > 180;
$writer->startTag('g',
id => "wedge_$browser",
transform => "rotate(-$wedge_rotation)");
$writer->emptyTag('path',
style => "fill:$wedge_color;",
d => "M $pie_radius,0
A $pie_radius, $pie_radius 0 $do_arc 0 $rx,$ry
L 0,0
z");
$writer->endTag('g');
$wedge_rotation += $wedge;
$color_lookup{$browser} = $wedge_color;
$i++;
}
$writer->endTag('g');
Next we create the legend for our chart. Once again we create a
top-level <g> context element and then loop through the elements
of the %browser hash. This time through we create a
nested group element containing a small rectangle (the <rect>
element) filled with corresponding color for each browser, and a
<text> element containing the browser's name and number of
requests. These legend items are rendered from top to bottom in a
single column 20 pixels apart by incrementing the
$legend_item_xoffset variable and passing its value to
the item via the transform attribute's translate function.
my $legend_item_xoffset = 0;
$writer->startTag('g',
id => 'legendGroup',
style => 'font-size:10;fill:#000000',
transform => "translate(25, $legend_start)");
foreach my $browser (sortHashByValue(%browsers)) {
$writer->startTag('g',
id => "legend_item_$browser",
transform => "translate(0, $legend_item_xoffset)");
$writer->emptyTag('rect',
width => '10',
height => '10',
style => "fill:$color_lookup{$browser};");
$writer->startTag('text',
transform => 'translate(15, 10)');
$writer->characters("$browser ($browsers{$browser})");
$writer->endTag('text');
$writer->endTag('g');
$legend_item_xoffset += 20;
}
Finally we close the legend's group element and the root
<svg> element, and we call XML::Writer's
close method which, by default, prints the document to
STDOUT.
$writer->endTag('g');
$writer->endTag('svg');
$writer->end();
Passing the location of a sample referrer log to our script yields the following image.
This script is far from perfect. For example, it is mathematically possible for two or more browsers to be assigned the same random color. Also, in real world applications, it is often desirable to exclude Web spiders and other automated user agents from browser reports. The goal here, however, has been to illustrate just how easy it is to generate eye-catching images dynamically using nothing more than Perl and a few of its modules. Other features are left as an exercise for the reader.
Conclusions
While I rarely offer subjective value judgments about the technologies I discuss in this column, I'm going to make an exception in this case. SVG is astonishingly cool. The examples above barely scratch the surface of SVG's flexibility and communicative power. To brush it aside as just another way to make "pretty pictures" is to miss the point completely. The combination of Perl's XML tools and SVG provides a range of creative options that would be difficult to achieve by other means. I hope that you have been inspired to continue to investigate both SVG and Perl's ability to generate it.
- PenDraw
2005-08-19 11:08:31 AndrewMain
2001-10-15 21:48:39 Nguyen Thai Ha- Use SVG.pm and do not worry about the underlying XML
2001-10-12 01:08:55 Ronan Oger - SVG and IRC Bots
2001-08-07 15:11:23 Thomas Rathbone - SVG and IRC Bots...
2001-08-08 12:42:50 Thomas Rathbone - SVG and IRC Bots...
2004-04-28 17:50:55 Thomas Rathbone