Forms and CGI programs
make it easier to conduct surveys and polls on the Web. Let's look
at an application that tabulates poll data and dynamically creates
a pie graph illustrating the results.
But since we are passing a filename, it seems more logical
to pass the information as an extra path. If we were passing the
information as a query string, we would have had to encode some
of the characters.[1] Let's look at the format of the data
file:
As you can see, the string "::" separates each entity throughout
the file. A unique separator should be used whenever you are dealing
with data to ensure that it does not get mixed up with the data.
The first line contains all of the selections within the poll.
The second line contains the actual data (initially, all values
should be zero). And the last line represents the colors to be used
to graph the options. In other words, red is used to draw the slice
representing Vanilla in the pie graph. The range of colors is limited
to the ones defined in the CGI pie graphics program, as you will
see.
The CGI program (ice_cream.pl)
decodes the form information, tabulates it, and adds it to the data
file. The program does not contain the form.
The program
begins as follows:
#!/usr/local/bin/perl
$webmaster = "shishir\@bu\.edu";
$document_root = "/usr/local/bin/httpd_1.4.2/public";
$ice_cream_file = "/ice_cream.dat";
$full_path = $document_root . $ice_cream_file;
$exclusive_lock = 2;
$unlock = 8;
&parse_form_data(*poll);
$user_selection = $poll{'ice_cream'};
The form information
is placed in the poll associative array. The
parse_form_data subroutine is the same one
we used previously. Since parse_form_data decodes
both GET and POST submissions,
users can submit their favorite flavor either with a GET
query or through a form. The ice_cream field,
which represents the user's selection, is stored in the user_selection
variable.
if ( open (POLL, "<" . $full_path) ) {
flock (POLL, $exclusive_lock);
for ($loop=0; $loop < 3; $loop++) {
$line[$loop] = <POLL>;
$line[$loop] =~ s/\n$//;
}
The data file
is opened in read mode, and exclusively locked. The loop retrieves
the first three lines from the file and stores it in the line
array. Newline characters at the end of each line are removed. We
use a regular expression to remove the last character rather than
using the chop operator, because the third
line may or may not have a newline character initially, and chop
would automatically remove the last character, creating a potential
problem.
@options = split ("::", $line[0]);
@data = split ("::", $line[1]);
$colors = $line[2];
flock (POLL, $unlock);
close (POLL);
The first line of the file
is split on the "::" delimiter and stored in the options
array. Each element in this array represents a separate decision
(or flavor) within the poll. The same process is repeated for the
second line of the data file as well. The main reason for doing
this is to find and increment the user-selected flavor, and write
the information back to the file. However, the third line, which
contains the color information, is not modified in any way.
$item_no = 3;
for ($loop=0; $loop <= $#options; $loop++) {
if ($options[$loop] eq $user_selection) {
$item_no = $loop;
last;
}
}
The loop iterates through each flavor and compares it to the
user selection. If there is a match, the item_no
variable will point to the flavor in the array. If there is no match,
item_no will have the default value of three,
in which case, it equals "Other." The only reason it might not match
is if the user accessed the script through a GET
query and passed a flavor which is not included in the survey.
The
data that represents the flavor is incremented.
if ( open (POLL, ">" . $full_path) ) {
flock (POLL, $exclusive_lock);
The
file is opened in write, and not append, mode. As a result, the
file will be overwritten.
print POLL join ("::", @options), "\n";
print POLL join ("::", @data), "\n";
print POLL $colors, "\n";
Each element within
the options and data arrays are joined with the "::" separator and
written to the file. The color information is also written to the
file.
flock (POLL, $unlock);
close (POLL);
print "Content-type: text/html", "\n\n";
print <<End_of_Thanks;
<HTML>
<HEAD><TITLE>Thank You!</TITLE></HEAD>
<BODY>
<H1>Thank You!</H1>
<HR>
Thanks for participating in the Ice Cream survey. If you would like to see the
current results, click <A HREF="/cgi-bin/pie.pl${ice_cream_file}">here </A>.
</BODY></HTML>
End_of_Thanks
The file is unlocked and closed. A thank-you message, along
with a link to the CGI program that graphs the data, is displayed.
} else {
&return_error (500, "Ice Cream Poll File Error",
"Cannot write to the poll data file [$full_path].");
}
} else {
&return_error (500, "Ice Cream Poll File Error",
"Cannot read from the poll data file [$full_path].");
}
exit (0);
If the file could not be opened successfully,
error messages are sent to the client. Since both subroutines used
by the ice_cream.pl program (return_error
and parse_form_data) should be familiar to
you by now, we won't bother to show them.
The pie.pl
program reads the poll data file and outputs the results, as either
a pie graph, or a simple text table, depending on the browser capabilities.
The program can be accessed with the following URL:
https://your.machine/cgi-bin/pie.pl/ice_cream.dat
where we use extra path information to specify ice_cream.dat
as the data file, located in the document root directory. On a graphic
browser such as Netscape Navigator, the pie graph will look like
Figure 7.4.
The program begins as follows:
#!/usr/local/bin/perl5
use GD;
$webmaster = "shishir\@bu\.edu";
$document_root = "/usr/local/bin/httpd_1.4.2/public";
&read_data_file (*slices, *slices_color, *slices_message);
$no_slices = &remove_empty_slices();
The
gd
graphics library is used to create the pie graph. The read_data_file
subroutine reads the information from the data file and places the
corresponding values in slices, slices_color,
and slices_message arrays. The remove_empty_slices
subroutine checks these three arrays for any zero values within
the data, and returns the number of non-zero data values into the
no_slices variable.
if ($no_slices == -1) {
&no_data ();
When all of the values in the data file
are zeros, the remove_empty_slices subroutine
returns a value of -1. If a -1 is returned into the no_slices
variable, the no_data subroutine is called
to output a message explaining that there are no results in the
data file.
} else {
$nongraphic_browsers = 'Lynx|CERN-LineMode';
$client_browser = $ENV{'HTTP_USER_AGENT'};
if ($client_browser =~ /$nongraphic_browsers/) {
&text_results();
} else {
&draw_pie ();
}
}
exit(0);
If the client browser supports graphics, the draw_pie
subroutine is called to display a pie graph. Otherwise, the text_results
subroutine is called to display the results as text.
That's
it for the main body of the program. The subroutines that do all
the work follow.
The no_data subroutine
displays a simple message explaining that there is no information
in the data file.
sub no_data
{
print "Content-type: text/html", "\n\n";
print <<End_of_Message;
<HTML>
<HEAD><TITLE>Results</TITLE></HEAD>
<BODY>
<H1>No Results Available</H1>
<HR>
Sorry, no one has participated in this survey up to this point.
As a result, there is no data available. Try back later.
<HR>
</BODY></HTML>
End_of_Message
}
The draw_pie subroutine is responsible
for drawing the actual pie graph.
sub draw_pie
{
local ( $legend_rect_size, $legend_rect, $max_length, $max_height,
$pie_indent, $pie_length, $pie_height, $radius, @origin,
$legend_indent, $legend_rect_to_text, $deg_to_rad, $image,
$white, $black, $red, $yellow, $green, $blue, $orange,
$percent, $loop, $degrees, $x, $y, $legend_x, $legend_y,
$legend_rect_y, $text, $message);
The
pie graph consists of various colored slices representing the different
choices, and a legend that points out the color that represents
each choice. All of the local variables needed to create the graph
are defined.
$legend_rect_size = 10;
$legend_rect = $legend_rect_size * 2;
The legend_rect_size
variable represents the length and height of each rectangle (actually
a square) in the legend. legend_rect is simply
the number of pixels from one rectangle to another, taking into
account the spacing between adjacent rectangles.
$max_length = 450;
if ($no_slices > 8) {
$max_height = 200 + ( ($no_slices - 8) * $legend_rect );
} else {
$max_height = 200;
}
The length of the image is set to 450 pixels. However, the
height of the image is based on the number of options (or flavors)
within a poll. This is because the legend rectangles are drawn vertically.
If there are eight options or less, the height is set to 200 pixels.
On the other hand, if the number of options is greater than eight,
the excess amount is multiplied by legend_rect
and added to 200 to determine the height of the image.
$pie_indent = 10;
$pie_length = $pie_height = 200;
$radius = $pie_height / 2;
The process of actually drawing the pie is very similar to
drawing a clock (see Chapter 6, Hypermedia Documents).
The pie is indented from the left and top edges by the value stored
in pie_indent. The length and height of the
pie graph is 200 pixels, and is constant. The radius of the pie
is the diameter of the circle--represented by pie_length
and pie_height --divided by two.
@origin = ($radius + $pie_indent, $max_height / 2);
$legend_indent = $pie_length + 40;
$legend_rect_to_text = 25;
$deg_to_rad = (atan2 (1, 1) * 4) / 180;
The origin is defined to
be the center of the pie graph. The legend is spaced 40 pixels from
the right edge of the graph. The legend_rect_to_text
variable determines the amount of pixels from a legend rectangle
to the start of the explanatory text.
$image = new GD::Image ($max_length, $max_height);
$white = $image->colorAllocate (255, 255, 255);
$black = $image->colorAllocate(0, 0, 0);
$red = $image->colorAllocate (255, 0, 0);
$yellow = $image->colorAllocate (255, 255, 0);
$green = $image->colorAllocate(0, 255, 0);
$blue = $image->colorAllocate(0, 0, 255);
$orange = $image->colorAllocate(255, 165, 0);
A new image is created, and some
colors are allocated. As mentioned earlier, the colors that are
specified in the data file are limited to the ones defined in the
preceding code.
grep ($_ = eval("\$$_"), @slices_color);
This is a new construct you have not seen before. It takes
each element within the slices_color array,
evaluates it at run-time, and stores the corresponding RGB index
back in the index. It is equivalent to the following code:
for ($loop=0; $loop <= $no_slices; $loop++) {
$temp_color = $slices_color[$loop];
$slices_color[$loop] = eval("\$$temp_color");
}
As
you can clearly see, the grep equivalent is
so much more compact. The slices_color array
contains the colors specified in the data file. And the colors above
are also defined with English names. As a result, we can take a
color from the data file, such as "yellow," and determine the RGB
index by evaluating $yellow. This is exactly
what the eval statement does.
$image->arc (@origin, $pie_length, $pie_height, 0, 360, $black);
A black circle is drawn from
the origin, i.e., the center of the pie graph.
$percent = 0;
for ($loop=0; $loop <= $no_slices; $loop++) {
$percent += $slices[$loop];
$degrees = int ($percent * 360) * $deg_to_rad;
$image->line ( $origin[0],
$origin[1],
$origin[0] + ($radius * cos ($degrees)),
$origin[1] + ($radius * sin ($degrees)),
$slices_color[$loop] );
}
The read_data_file
subroutine, called at the beginning of the program, also calculates
percentages for each option and stores them in the slices
array. The proportion of votes that go to each flavor is called
the "percentage" here, although it's actually a fraction of 1, not
100. For example, if there were a total of five votes cast with
two votes for "Vanilla," the value for "Vanilla" would be 0.4.
The loop iterates through each percentage value and draws
a line from the origin to the outer edge of the circle. Initially,
the first percentage value is multiplied by 360 degrees to determine
the angle at which the first line should be drawn. On each successive
iteration through the loop, the percentage value represents the
sum of all the percentage values up to that point. Then, this percentage
value is used to draw the next line, until the sum of the total
percentage values equal 100%.
$percent = 0;
for ($loop=0; $loop <= $no_slices; $loop++) {
$percent += $slices[$loop];
$degrees = int (($percent * 360) - 1) * $deg_to_rad;
$x = $origin[0] + ( ($radius - 10) * cos ($degrees) );
$y = $origin[1] + ( ($radius - 10) * sin ($degrees) );
$image->fill ($x, $y, $slices_color[$loop]);
}
This fills the areas represented by the
various colored lines produced by the previous loop. The fill
function in the gd library works in the same
manner as the "paint bucket" operation in most drawing programs.
It colors an area pixel by pixel until it reaches a pixel that contains
a different color than that of the starting pixel. That is the reason
why this loop and the previous one cannot be combined, as different
colored lines must be drawn first. The starting pixel is calculated
so that its angle-from the origin-is slightly less than that of
the previously drawn line. As a result, when the fill
function is called, the area between two differently colored lines
is flooded with color.
$legend_x = $legend_indent;
$legend_y = ( $max_height - ($no_slices * $legend_rect) -
($legend_rect * 0.75) ) / 2;
The
legend's x coordinate is simply defined by the legend_indent
variable. However, the y coordinate is calculated in such a way
that the legend will be centered with respect to the pie graph.
for ($loop=0; $loop <= $no_slices; $loop++) {
$legend_rect_y = $legend_y + ($loop * $legend_rect);
$text = pack ("A18", $slices_message[$loop]);
This loop draws the rectangles and the corresponding text.
The y coordinate is incremented each time through the loop. The
text variable reserves 18 characters for the
explanatory text. If the text exceeds this limit, it is truncated.
Otherwise, it is padded to the limit with spaces.
$message = sprintf ("%s (%4.2f%%)", $text, $slices[$loop] * 100);
The message
variable is formatted to display the text and the corresponding
percentage value.
$image->filledRectangle ( $legend_x,
$legend_rect_y,
$legend_x + $legend_rect_size,
$legend_rect_y + $legend_rect_size,
$slices_color[$loop] );
$image->string ( gdSmallFont,
$legend_x + $legend_rect_to_text,
$legend_rect_y,
$message,
$black );
}
The rectangle
is drawn, and the text is displayed.
$image->transparent($white);
$| = 1;
print "Content-type: image/gif", "\n\n";
print $image->gif;
}
Finally,
white is chosen as the transparent color to create a transparent
image.
The draw_pie subroutine
ends by printing the Content-type header (using
a content type of image/gif) and then the image
itself.
For non-graphic browsers, we want to be able
to generate the results in text format. The text_results
subroutine does just that.
sub text_results
{
local ($text, $message, $loop);
print "Content-type: text/html", "\n\n";
print <<End_of_Results;
<HTML>
<HEAD><TITLE>Results</TITLE></HEAD>
<BODY>
<H1>Results</H1>
<HR>
<PRE>
End_of_Results
for ($loop=0; $loop <= $no_slices; $loop++) {
$text = pack ("A18", $slices_message[$loop]);
$message = sprintf ("%s (%4.2f%%)", $text, $slices[$loop] * 100);
print $message, "\n";
}
print "</PRE><HR>", "\n";
print "</BODY></HTML>", "\n";
}
The data is formatted using the sprintf
function and displayed. The string representing the flavor is limited
to 18 characters.
The read_data_file
subroutine opens and reads the ice_cream.dat
file and returns the results.
sub read_data_file
{
local (*slices, *slices_color, *slices_message) = @_;
local (@line, $total_votes, $poll_file, $loop, $exclusive_lock, $unlock);
$exclusive_lock = 2;
$unlock = 8;
if ($ENV{'PATH_INFO'}) {
$poll_file = $document_root . $ENV{'PATH_INFO'};
} else {
&return_error (500, "Poll Data File Error",
"A poll data file has to be specified.");
}
The environment variable PATH_INFO
is checked to see if it contains any information. If a null string
is returned, an error message is output. If a filename is specified,
the server root directory is concatenated to the data file. Unlike
a query, the leading "/" is returned as part of the variable.
if ( open (POLL, "<" . $poll_file) ) {
flock (POLL, $exclusive_lock);
The
data file is opened in read mode. If the file cannot be opened,
an error message is returned.
for ($loop=0; $loop < 3; $loop++) {
$line[$loop] = <POLL>;
$line[$loop] =~ s/\n$//;
}
@slices_message = split ("::", $line[0]);
@slices = split ("::", $line[1]);
@slices_color = split ("::", $line[2]);
flock (POLL, $unlock);
close (POLL);
Three lines are read
from the data file. The lines are split on the "::" character and
stored in arrays. The file is unlocked and closed.
$total_votes = 0;
for ($loop=0; $loop <= $#slices; $loop++) {
$total_votes += $slices[$loop];
}
The total number of votes is determined by adding each element
of the slices array.
if ($total_votes > 0) {
grep ($_ = ($_ / $total_votes), @slices);
}
Each element of the slices
array is modified to contain the percentage value, instead of the
number of votes. You should always check to see that the divisor
is greater than zero, as Perl will return an "Illegal
division by zero" error.
} else {
&return_error (500, "Poll Data File Error",
"Cannot read from the poll data file [$poll_file].");
}
}
If the program cannot open
the data file, an error message is displayed.
The final
subroutine in pie.pl is remove_empty_slices.
sub remove_empty_slices
{
local ($loop) = 0;
while (defined ($slices[$loop])) {
if ($slices[$loop] <= 0.0) {
splice(@slices, $loop, 1);
splice(@slices_color, $loop, 1);
splice(@slices_message, $loop, 1);
} else {
$loop++;
}
}
return ($#slices);
}
In order to save the program from processing choices (or flavors)
that have zero votes, those elements and their corresponding colors
and text are removed. The splice function removes
an element from the array.