################## # FancyBarGraph: a bar graphing module with emphasis on aesthetic flexibility ################## # # # Version: 0.2 (slowly crawling toward stability ;-) # # See post- __END__ for pod # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # # New in 0.2: # # * transparent gif's supported now - $gif->transparent(1) turns it on, $gif->transparent turns it off # * support for canned colour schemes # ################# package FancyBarGraph; use GD; use strict; sub new { my $self = {}; my $class = shift; my ($type,$width,$height) = @_ or die "Usage: FancyBarGraph->new(width,height)"; bless($self,$class); $self->{type} = $type; $self->{width} = $width; $self->{height} = $height; $self->init; return $self; } sub init #set some default values.. { my $self = shift; $self->{image} = new GD::Image($self->{width},$self->{height}); $self->{styles} = { show_values => 1, #show values on bars margin => 35, tick_len => 5, axes_width => 3, margin_offset => 1.3, char_width => 7, rgb_offset => 40, bar_width => 15, bar_spacer => 2, y_start => 0, y_end => 100, x_label => 'Default', y_label => 'Default', title => 'No title is set - set {styles}{title}', border => [0,0,0], # r, g, b, axes_color => [0,0,0], bgcolor => [255,255,255], axes_shader => [150,150,150], label_color => [0,0,0], boxes_color1 => [0,0,255], boxes_color2 => [255,0,0], }; $self->define_canned_styles; } sub transparent { my $self = shift; my $set_trans = shift; if($set_trans) { $self->{transparent} = 1 } else { delete $self->{transparent}} } sub set_data { my $self = shift; my (@datarefs) = @_; if($self->{type} eq 'single' and !@datarefs) { die "set_data requires a ref to a data array" } #patch in other cases here as req'd $self->{datarefs} = \@datarefs; } sub set_labels { my $self = shift; my $ar_ref = shift; $self->{labels} = $ar_ref; } sub draw_graph { my $self = shift; $self->allocate_colors; #background and border $self->{image}->filledRectangle(0,0,$self->{width}-1,$self->{height}-1,$self->{bgcolor}); $self->{image}->rectangle(0,0,$self->{width}-1,$self->{height}-1,$self->{border}); #xaxis $self->{image}->line( $self->{styles}{margin}, $self->{styles}{margin}, $self->{styles}{margin}, $self->{height}-($self->{styles}{margin}*$self->{styles}{margin_offset}), $self->{axes_color} ); #yaxis $self->{image}->line( $self->{styles}{margin}, $self->{height}-($self->{styles}{margin} * $self->{styles}{margin_offset}), $self->{width} - ($self->{styles}{margin} * $self->{styles}{margin_offset}), $self->{height}-($self->{styles}{margin} * $self->{styles}{margin_offset}), $self->{axes_color} ); if($self->{type} eq 'single') { $self->draw_single_graph } #patch in additional types here later.. } # plot a 'single' dataset graph sub draw_single_graph { my $self = shift; my @zero = ($self->{styles}{margin},$self->{height} - ($self->{styles}{margin} * $self->{styles}{margin_offset})); my @yend = ($self->{styles}{margin},$self->{styles}{margin}); my @xend = ($self->{width}-($self->{styles}{margin}* $self->{styles}{margin_offset}),$self->{height} - ($self->{styles}{margin} * $self->{styles}{margin_offset})); my $xlen = sprintf("%0d",$xend[0] - $zero[0]); my $ylen = sprintf("%0d",$zero[1] - $self->{styles}{margin}); #tickify y-axis for(my $i =1 ;$i <= 4; ++$i) #just 4 ticks for now { my $xoffset = sprintf("%0d",($xlen /4 ) * $i); my $yoffset = sprintf("%0d",($ylen /4 ) * $i); #ytick $self->{image}->line ( $self->{styles}{margin}, $zero[1]-$yoffset, $self->{styles}{margin} - $self->{styles}{tick_len}, $zero[1]-$yoffset, $self->{axes_color} ); } #mark x-axis start $self->{image}->line ( $zero[0], $xend[1], $zero[0], $xend[1]+$self->{styles}{tick_len}, $self->{axes_color} ); #mark y-axis start $self->{image}->line ( $zero[0], $zero[1], $zero[0]- $self->{styles}{tick_len}, $zero[1], $self->{axes_color} ); #mark x-axis end $self->{image}->line ( $self->{width} - ($self->{styles}{margin} * $self->{styles}{margin_offset}), $xend[1], $self->{width} - ($self->{styles}{margin} * $self->{styles}{margin_offset}), $xend[1]+$self->{styles}{tick_len},$self->{axes_color} ); my $barcount; for(@{${$self->{datarefs}}[0]}) { ++$barcount; #sprintfs make sure we only get whole nums... my $x1 = sprintf("%0d",1+ $zero[0] + ( ($barcount -1) * ("%0d",($xlen-$self->{styles}{bar_width}*2) / ($#{${$self->{datarefs}}[0]})))); my $y1 = sprintf("%0d",$zero[1] - (($_ /$self->{styles}{y_end}) * $ylen )); my $x2 = sprintf("%0d",1 + $x1 + $self->{styles}{bar_width}); my $y2 = $zero[1]; $self->{image}->filledRectangle($x1,$y1-1,$x2,$y2-1,$self->{boxes_color1}); #now the '3d' lines (left side, top side) $self->{image}->line($x1,$y1,$x1,$y2-1,$self->{box1_shade_up}); $self->{image}->line($x1,$y1-1,$x1+$self->{styles}{bar_width},$y1-1,$self->{box1_shade_up}); #(right side, bottom) $self->{image}->line($x1+$self->{styles}{bar_width}+1,$y1,$x2,$y2-1,$self->{box1_shade_dn}); $self->{image}->line($x1,$y2-1,$x2,$y2-1,$self->{box1_shade_dn}); if($self->{styles}{show_values}) { $self->{image}->string(gdTinyFont,$x1+1,$y1-10,$_,$self->{label_color}); } if($self->{labels}) { $self->{image}->string(gdTinyFont,$x1+1,$y2+5,${$self->{labels}}[$barcount-1],$self->{label_color}); } } #label y-axis values my $numlength = length($self->{styles}{y_start}); $self->{image}->string(gdTinyFont,$zero[0]-($numlength+5)-$self->{styles}{tick_len},$zero[1]-2,$self->{styles}{y_start},$self->{label_color}); $numlength = ($self->{styles}{margin}/3) + length($self->{styles}{y_end}); $self->{image}->string(gdTinyFont,$zero[0]-($numlength)-$self->{styles}{tick_len},$yend[1]-2,$self->{styles}{y_end},$self->{label_color}); $numlength = length($self->{styles}{y_end}); $self->{image}->string(gdTinyFont,$zero[0]-$self->{styles}{tick_len}-($numlength * 3+2) -$self->{styles}{tick_len},$zero[1]-($ylen /2)-3,$self->{styles}{y_end}/2,$self->{label_color}); #now add titles and label my $title_len = length($self->{styles}{title}); $self->{image}->string( gdMediumBoldFont, $self->{width}/2 - (3.5*$title_len), #Font seems to be about 3 pixels per char wide :/ $self->{styles}{margin}/3, $self->{styles}{title}, $self->{label_color} ); my $x_label_len = length($self->{styles}{x_label}); $self->{image}->string( gdSmallFont, $self->{width}/2 - (3*$x_label_len), $self->{height}- ($self->{styles}{margin}/1.5), $self->{styles}{x_label}, $self->{label_color} ); my $y_label_len = length($self->{styles}{y_label}); $self->{image}->stringUp( gdSmallFont, $self->{styles}{margin} /4, $self->{height}/2 + (3*$y_label_len), $self->{styles}{y_label}, $self->{label_color} ); } sub allocate_colors { my $self = shift; $self->{bgcolor} = $self->{image}->colorAllocate(@{$self->{styles}{bgcolor}}); if($self->{transparent}) { $self->{image}->transparent($self->{bgcolor}) } $self->{border} = $self->{image}->colorAllocate(@{$self->{styles}{border}}); $self->{axes_color} = $self->{image}->colorAllocate(@{$self->{styles}{axes_color}}); $self->{axes_shader} = $self->{image}->colorAllocate(@{$self->{styles}{axes_shader}}); $self->{label_color} = $self->{image}->colorAllocate(@{$self->{styles}{label_color}}); $self->{boxes_color1} = $self->{image}->colorAllocate(@{$self->{styles}{boxes_color1}}); $self->{boxes_color2} = $self->{image}->colorAllocate(@{$self->{styles}{boxes_color2}}); #'light' 3d color (r + rgb_offset,g + rgb_offset, b + rgb_offset) $self->{box1_shade_dn} = $self->{image}->colorAllocate( ${$self->{styles}{boxes_color1}}[0] + $self->{styles}{rgb_offset}, ${$self->{styles}{boxes_color1}}[1] + $self->{styles}{rgb_offset}, ${$self->{styles}{boxes_color1}}[2] + $self->{styles}{rgb_offset}, ); # 'dark' 3d color (r - rgb_offset,g - rgb_offset, b - rgb_offset) $self->{box1_shade_up} = $self->{image}->colorAllocate( ${$self->{styles}{boxes_color1}}[0] - $self->{styles}{rgb_offset}, ${$self->{styles}{boxes_color1}}[1] - $self->{styles}{rgb_offset}, ${$self->{styles}{boxes_color1}}[2] - $self->{styles}{rgb_offset}, ); } sub set_style { my $self = shift; my $hashref = shift; for my $key ( sort keys %$hashref) { $self->{styles}{$key} = $$hashref{$key} } return; } sub output { my $self = shift; $self->draw_graph; binmode STDOUT; print "Content-type: image/gif \n\n"; print STDOUT $self->{image}->gif; } sub use_canned_style { my $self = shift; my $style_name = shift or die "Usage: FancyBarGraph::use_canned_style('stylename')}"; if($self->{canned}{$style_name}) { $self->set_style($self->{canned}{$style_name}); } } sub define_canned_styles #set some default colour schemes for the lazy { my $self = shift; $self->{canned}{greyscale} = { rgb_offset => -40, border => [255,255,255], # r, g, b, axes_color => [150,150,150], bgcolor => [20,20,20], axes_shader => [150,150,150], label_color => [200,200,200], boxes_color1 => [40,40,40], }; $self->{canned}{corporate_blues} = { rgb_offset => 50, border => [0,0,0], # r, g, b, axes_color => [255,255,255], bgcolor => [91,101,130], axes_shader => [150,150,150], label_color => [255,255,255], boxes_color1 => [7,9,49], }; $self->{canned}{jim_rockford} = { rgb_offset => -30, border => [0,0,0], # r, g, b, axes_color => [207,202,165], bgcolor => [52,46,8], axes_shader => [150,150,150], label_color => [239,238,138], boxes_color1 => [105,91,37], }; $self->{canned}{green_on_black} = { rgb_offset => -30, border => [0,0,0], # r, g, b, axes_color => [43,192,31], bgcolor => [0,0,0], axes_shader => [150,150,150], label_color => [45,227,32], boxes_color1 => [65,153,59], }; } #debugging routine sub dump_data { my $self = shift; my $refcount; for(@{$self->{datarefs}}) { ++$refcount; for(@$_) { print "Data from array $refcount: $_\n" } } } 1; __END__; =head1 NAME FancyBarGraph a bar graphing module with emphasis on aesthetic flexibilty, not paranoia =head2 VERSION 0.2 (this is probably a little rough around the edges just yet) New in 0.2 : * transparency support * support for canned colour schemes =head1 SYNOPSIS use FancyBarGraph; $graph = new FancyBarGraph('single',640,480); @some_data = qw(10 20 30 40 50 60 70 80 90 1000 100 900 400 300 200 100 230); %style_info = ( x_label => "Here is the x-axis label", y_label => "And a y-axis label too", title => "Profile of some interesting data you might want to graph" ); $graph->set_data(\@single_data); $graph->set_style(\%style_test); $graph->output; =head2 ABSTRACT FancyBarGraph was created because although GifGraph is very groovy, it doesn't really give me the aesthetic flexibility I was looking for. FancyBarGraph lets you set various color and layout values to give you much tighter control over the appearance of your graph. This version currently supports "single" datasets (a single set of data) but support for multiple datasets (plot set A next to set B) will be forthcoming As mentioned above, this module is not at all paranoid. Some would argue that this is a bad thing: I disagree. Since you are the one controlling your data, and the one who knows how you want your data to be represented, you should be taking care of this before the graphing stage. I thought it better to allow for "bad" graphs (out-of-range data, too much data to fit, etc) than to burden the process with lots of logic checks. So, this module will let you make bad graphs if you want to. =head2 INSTALLATION Just copy FancyBarGraph.pm to your lib directory =head1 DESCRIPTION =head2 METHODS =over 4 =item new FancyBarGraph::new(type,width,height) constructor. Currently, only type 'single' is supported (plot a single data set). Other types will be supported eventually =item set_data FancyBarGraph::set_data(\@data_to_plot) This sets the dataset for the graph. Values are plotted left to right =item set_style FancyBarGraph::set_style(\%style_info) This method lets you set any or all of the style parameters en mass. I suppose you could set them one at a time also, if you wanted to. Here are all the things you can set through set_style: show_values => show x-axis values at the top of each bar (default: 1) margin => margin, in pixels (default: 35) tick_len => axes tick length, in pixels (default: 5) margin_offset => margin offset [used for fancy bottom spacing](default: 1.3) rgb_offset => a multiplier used to shade the 3d parts of the bars. you may need to set this to a negative value for some colour combinations (default 40) bar_width => width of each histogram bar, in pixels (default: 15) y_end => what is the biggest value on the y-axis? (default: 100) x_label => what text do you want to see on the x-axis? (default: 'Default') y_label => what text do you want to see on the y-axix? (default: 'Default') title => what text do you want as the title? (default: 'No title is set - set {styles}{title}') border => what color [r,g,b] for the border? (default: [0,0,0] ) axes_color => what color [r,g,b] for the axes? (default: [0,0,0] ) bgcolor => what color for the background? (default: [255,255,255]) label_color => what color for the labels? (default: [0,0,0]) boxes_color1 => what color for the histogram bars? (default: [0,0,255]) =item transparent FancyBarGraph::transparent(1); sets the colour defined as the background to be transparent =item use_canned_style FancyBarGraph::use_canned_style('stylename'); Use a built-in canned colour scheme. Available schemes include: * greyscale * corporate_blues * jim_rockford (for that great 70's brown feel ;-) * green_on_black =back =head2 Author: Steve McNabb, s.mcnabb@sympatico.ca =head2 Copyright: =item © 1999 Training Alternatives Inc http://www.trainingalternatives.com. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. =cut