File Coverage

blib/lib/GD/3DBarGrapher.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package GD::3DBarGrapher;
2            
3             # -----------------------------------------------------------------------------
4             #
5             # "3DBarGrapher"
6             #
7             # http://www.creationfactor.net/software.htm
8             #
9             # Copyright (c) 2009 S.I.Warhurst
10             #
11             # See DOCUMENTATION at end of file
12             #
13             # -----------------------------------------------------------------------------
14             # INITIALISATION
15             # -----------------------------------------------------------------------------
16            
17 1     1   697 use strict;
  1         1  
  1         29  
18 1     1   2724 use GD;
  0            
  0            
19            
20             require Exporter;
21             @GD::3DBarGrapher::ISA = qw(Exporter);
22             @GD::3DBarGrapher::EXPORT_OK = qw(creategraph);
23             $GD::3DBarGrapher::VERSION = '0.9.6';
24            
25             our $image;
26            
27             # -----------------------------------------------------------------------------
28             # MAIN FUNCTION
29             # -----------------------------------------------------------------------------
30            
31             sub creategraph {
32            
33             my($arrayref,$options) = @_;
34            
35             # --- get default config & update with customisations --- #
36            
37             my(%conf) = config();
38             foreach my $k (keys %{$options}){
39             $conf{lc($k)} = $$options{$k};
40             }
41            
42             # --- get data --- #
43            
44             my(@data) = @$arrayref;
45            
46             # --- get dimensions of objects --- #
47            
48             my(%dims) = getdimensions(\@data,\%conf);
49            
50             # --- create graph --- #
51            
52             # adjust overall image dimensions if necessary
53             $conf{imgw} = $dims{minwidth} if $dims{minwidth} > $conf{imgw};
54             $conf{imgh} = $dims{minheight} if $dims{minheight} > $conf{imgh};
55             $image = GD::Image->newTrueColor($conf{imgw},$conf{imgh});
56            
57             # fill image background colour
58             my $col = $image->colorAllocate($conf{$conf{ibgcol}}{R},$conf{$conf{ibgcol}}{G},$conf{$conf{ibgcol}}{B});
59             $image->fill(10,10,$col);
60            
61             # draw graph border if necessary
62             if($conf{iborder} ne ""){
63             my $col = $image->colorAllocate($conf{$conf{iborder}}{R},$conf{$conf{iborder}}{G},$conf{$conf{iborder}}{B});
64             $image->rectangle(0,0,$conf{imgw}-1,$conf{imgh}-1,$col);
65             }
66            
67             # draw title
68             if($conf{ttext} ne ''){
69             my $col = $image->colorAllocate($conf{$conf{tfontcol}}{R},$conf{$conf{tfontcol}}{G},$conf{$conf{tfontcol}}{B});
70             if($conf{tfont} eq ''){
71             my $x = ($conf{imgw}/2)-($dims{titlew}/2);
72             my $y = $conf{ipadding};
73             $image->string(gdGiantFont,$x,$y,$conf{ttext},$col);
74             }
75             else{
76             my $x = ($conf{imgw}/2)-($dims{titlew}/2);
77             my $y = $conf{ipadding} + $dims{titleh};
78             $image->stringFT($col,$conf{tfont},$conf{tsize},0,$x,$y,$conf{ttext});
79             }
80             }
81            
82             # draw y label text
83             if($conf{yltext} ne ''){
84             my $col = $image->colorAllocate($conf{$conf{lfontcol}}{R},$conf{$conf{lfontcol}}{G},$conf{$conf{lfontcol}}{B});
85             if($conf{lfont} eq ''){
86             my $x = $conf{ipadding};
87             my $temp = 0;
88             $temp = ($conf{ipadding} + $dims{titleh}) if $dims{titleh} > 0;
89             my $y = ((($dims{floor} + $dims{plotheight})/2) + ($dims{ylabelheight}/2)) + $temp + $conf{ipadding};
90             $image->stringUp(gdLargeFont,$x,$y,$conf{yltext},$col);
91             }
92             else{
93             my $x = $conf{ipadding} + $dims{ylabelwidth};
94             my $temp = 0;
95             $temp = ($conf{ipadding} + $dims{titleh}) if $dims{titleh} > 0;
96             my $y = ((($dims{floor} + $dims{plotheight})/2) + ($dims{ylabelheight}/2)) + $temp + $conf{ipadding};
97             $image->stringFT($col,$conf{lfont},$conf{lsize},90/57.2958,$x,$y,$conf{yltext});
98             }
99             }
100            
101             # draw x label text
102             if($conf{xltext} ne ''){
103             my $col = $image->colorAllocate($conf{$conf{lfontcol}}{R},$conf{$conf{lfontcol}}{G},$conf{$conf{lfontcol}}{B});
104             if($conf{lfont} eq ''){
105             my $x = $conf{imgw} - ($conf{ipadding} + (($dims{floor} + $dims{plotwidth})/2) + ($dims{xlabelwidth}/2));
106             my $y = $conf{imgh} - $conf{ipadding} - $dims{xlabelheight};
107             $image->string(gdLargeFont,$x,$y,$conf{xltext},$col);
108             }
109             else{
110             my $x = $conf{imgw} - ($conf{ipadding} + (($dims{floor} + $dims{plotwidth})/2) + ($dims{xlabelwidth}/2));
111             my $y = $conf{imgh} - $conf{ipadding};
112             $image->stringFT($col,$conf{lfont},$conf{lsize},0,$x,$y,$conf{xltext});
113             }
114             }
115            
116             # draw main plot box
117             my $col = $image->colorAllocate($conf{$conf{plinecol}}{R},$conf{$conf{plinecol}}{G},$conf{$conf{plinecol}}{B});
118             my $ypos = $conf{ipadding};
119             $ypos += $conf{ipadding} + $dims{titleh} if $conf{ttext} ne '';
120             my $plotleftedge = $conf{imgw}-$conf{ipadding}-$dims{plotwidth};
121             $image->rectangle($conf{imgw}-$conf{ipadding},$ypos,$plotleftedge,$ypos+$dims{plotheight},$col);
122            
123             # draw side & floor
124             $image->line($plotleftedge,$ypos,$plotleftedge-$dims{floor},$ypos+$dims{floor},$col);
125             $image->line($plotleftedge-$dims{floor},$ypos+$dims{floor},$plotleftedge-$dims{floor},$ypos+$dims{plotheight}+$dims{floor},$col);
126             $image->line($plotleftedge-$dims{floor},$ypos+$dims{plotheight}+$dims{floor},$plotleftedge,$ypos+$dims{plotheight},$col);
127             $image->line($plotleftedge-$dims{floor},$ypos+$dims{plotheight}+$dims{floor},$conf{imgw}-$conf{ipadding}-$dims{floor},$ypos+$dims{plotheight}+$dims{floor},$col);
128             $image->line($conf{imgw}-$conf{ipadding}-$dims{floor},$ypos+$dims{plotheight}+$dims{floor},$conf{imgw}-$conf{ipadding},$ypos+$dims{plotheight},$col);
129            
130             # fill plot box, side and floor
131             my $flr = $image->colorAllocate($conf{$conf{pflcol}}{R},$conf{$conf{pflcol}}{G},$conf{$conf{pflcol}}{B});
132             my $bg = $image->colorAllocate($conf{$conf{pbgcol}}{R},$conf{$conf{pbgcol}}{G},$conf{$conf{pbgcol}}{B});
133             $image->fill($plotleftedge,$ypos+$dims{plotheight}+2,$flr);
134             if($conf{pbgfill} eq "gradient"){
135             gradientfill($bg,$plotleftedge+1,$ypos+1,$plotleftedge+$dims{plotwidth}-1,$ypos+1,$dims{plotheight}-1,'',$conf{imgh});
136             gradientfill($bg,($plotleftedge-$dims{floor})+1,($ypos+$dims{floor}),$plotleftedge-1,$ypos+2,$dims{plotheight}-1,'',$conf{imgh});
137             }
138             else{
139             $image->fill($conf{imgw}-$conf{ipadding}-2,$ypos+2,$bg);
140             $image->fill($plotleftedge-2,$ypos+$dims{floor}+2,$bg);
141             }
142            
143             # draw div lines and y vals
144             my ($x1,$x2,$x3) = ($conf{imgw}-$conf{ipadding}-$dims{plotwidth}-$dims{floor},$conf{imgw}-$conf{ipadding}-$dims{plotwidth},$conf{imgw}-$conf{ipadding});
145             my ($y1,$y2) = ($ypos+$dims{plotheight}+$dims{floor},$ypos+$dims{plotheight});
146             my $divspacing = $dims{plotheight}/$dims{numdivs};
147             my $txtcol = $image->colorAllocate($conf{$conf{vfontcol}}{R},$conf{$conf{vfontcol}}{G},$conf{$conf{vfontcol}}{B});
148             if($conf{vfont} ne ''){
149             my($w,$h) = getstringsize($conf{vfont},"0",$conf{vsize},0);
150             $image->stringFT($txtcol,$conf{vfont},$conf{vsize},0,$x1-$conf{iplotpad}-$w,$y1+($h/2),"0");
151             }
152             else{
153             my($w,$h) = getstringsize("gdSmallFont","0");
154             $image->string(gdSmallFont,$x1-$conf{iplotpad}-$w,$y1-($h/2),"0",$txtcol);
155             }
156             for(my $d = 1; $d <= $dims{numdivs}; $d++){
157             $image->line($x1,$y1-($d*$divspacing),$x2,$y2-($d*$divspacing),$col);
158             $image->line($x2,$y2-($d*$divspacing),$x3,$y2-($d*$divspacing),$col);
159             if($conf{vfont} ne ''){
160             my($w,$h) = getstringsize($conf{vfont},($dims{range}/$dims{numdivs})*$d,$conf{vsize},0);
161             $image->stringFT($txtcol,$conf{vfont},$conf{vsize},0,$x1-$conf{iplotpad}-$w,($y1-($d*$divspacing))+($h/2),($dims{range}/$dims{numdivs})*$d);
162             }
163             else{
164             my($w,$h) = getstringsize("gdSmallFont",($dims{range}/$dims{numdivs})*$d);
165             $image->string(gdSmallFont,$x1-$conf{iplotpad}-$w,($y1-($d*$divspacing))-($h/2),($dims{range}/$dims{numdivs})*$d,$txtcol);
166             }
167             }
168            
169             # get imagemap html ready
170             my($imgtag, $maptag, $areatag) = imagemaphtml();
171             my ($imagemap,$shapes);
172             $imagemap = $imgtag . $maptag;
173             my ($filename) = $conf{file} =~ /([^\/]+)$/;
174             $imagemap =~ s/%imagename%/$filename/;
175             $imagemap =~ s/%width%/$conf{imgw}/;
176             $imagemap =~ s/%height%/$conf{imgh}/;
177             $filename =~ s/(\W+|_|\-)//g; # attempt to give map
178             $filename .= time; # unique name!
179             $imagemap =~ s/%mapname%/$filename/g;
180            
181             # draw columns or bars
182             my ($colbar,%shades);
183             if($conf{bfacecol} ne "random"){
184             $colbar = $image->colorAllocate($conf{$conf{bfacecol}}{R},$conf{$conf{bfacecol}}{G},$conf{$conf{bfacecol}}{B});
185             (%shades) = getshades($conf{$conf{bfacecol}}{R},$conf{$conf{bfacecol}}{G},$conf{$conf{bfacecol}}{B},\%conf);
186             }
187             else {
188             my @rgb = ($conf{$conf{pflcol}}{R},$conf{$conf{pflcol}}{G},$conf{$conf{pflcol}}{B});
189             my (%colour) = randomcolour();
190             $colbar = $image->colorAllocate($colour{R},$colour{G},$colour{B});
191             (%shades) = getshades($colour{R},$colour{G},$colour{B},\%conf);
192             }
193             my $shadetop = $image->colorAllocate($shades{top}{R},$shades{top}{G},$shades{top}{B});
194             my $shadeside = $image->colorAllocate($shades{side}{R},$shades{side}{G},$shades{side}{B});
195             my $xtxt = $image->colorAllocate($conf{$conf{vfontcol}}{R},$conf{$conf{vfontcol}}{G},$conf{$conf{vfontcol}}{B});
196             my $keyn = scalar @data;
197             my $spacing = ($dims{plotwidth} - $conf{iplotpad} - $conf{iplotpad} - $dims{floor} - ($keyn * $conf{bwidth})) / ($keyn-1);
198             my $barpos = $plotleftedge + $conf{iplotpad};
199             my ($bwidby2,$bwidby3,$bwidby4) = (
200             int($conf{bwidth}/2),
201             int($conf{bwidth}/3),
202             int($conf{bwidth}/4)
203             );
204             my $floordepth = sprintf("%.0f",sqrt(($bwidby2*$bwidby2)/2));
205             foreach my $d(@data){
206             # draw x axis text
207             if($conf{vfont} ne ''){
208             my($w,$h,$x) = getstringsize($conf{vfont},$d->[0],$conf{vsize},45);
209             $image->stringFT($xtxt,$conf{vfont},$conf{vsize},45/57.2958,($barpos-$w)+$x+$bwidby3,$ypos+$dims{plotheight}+$dims{floor}+$conf{iplotpad}+$h,$d->[0]);
210             }
211             else{
212             my($h,$w) = getstringsize("gdSmallFont",$d->[0]);
213             $image->stringUp(gdSmallFont,$barpos+($bwidby2-($w/2)),$ypos+$dims{plotheight}+$dims{floor}+$conf{iplotpad}+$h,$d->[0],$xtxt);
214             }
215             my $coords;
216             # draw columns
217             if($conf{bstyle} eq "column"){
218             # draw bottom arc
219             $image->filledArc($barpos+$bwidby2,$ypos+$dims{plotheight}+$bwidby4,$conf{bwidth},$bwidby2,0,180,$colbar);
220             # draw bar
221             my $centretopy = $ypos + ($dims{plotheight} - (($dims{plotheight}/$dims{range})*$d->[1])) + $bwidby4;
222             $image->filledRectangle($barpos,$centretopy,$barpos+$conf{bwidth}-1,$ypos+$dims{plotheight}+$bwidby4,$colbar);
223             if($conf{bcolumnfill} eq "gradient"){
224             gradientfill($colbar,$centretopy,$barpos+$conf{bwidth}-1,$ypos+$dims{plotheight}+$bwidby4,$barpos+$conf{bwidth}-1,$conf{bwidth},'column',$conf{imgh});
225             }
226             # draw top ellipse
227             $image->filledEllipse($barpos+$bwidby2,$centretopy,$conf{bwidth},$bwidby2,$shadetop);
228             $coords = int($barpos) . "," . int($centretopy-$bwidby4) . "," . int($barpos+$conf{bwidth}) . "," . int($ypos+$dims{plotheight}+$bwidby4);
229             }
230             # draw bars
231             else {
232             # draw main bar face
233             my $centretopy = $ypos + ($dims{plotheight} - (($dims{plotheight}/$dims{range})*$d->[1])) + $floordepth;
234             $image->filledRectangle($barpos,$centretopy,$barpos+$conf{bwidth},$ypos+$dims{plotheight}+$floordepth,$colbar);
235             # draw top and side sections
236             my $poly = new GD::Polygon;
237             $poly->addPt($barpos,$centretopy);
238             $poly->addPt($barpos+$floordepth,$centretopy-$floordepth);
239             $poly->addPt($barpos+$floordepth+$conf{bwidth},$centretopy-$floordepth);
240             $poly->addPt($barpos+$conf{bwidth},$centretopy);
241             $image->filledPolygon($poly,$shadetop);
242             my $poly = new GD::Polygon;
243             $poly->addPt($barpos+$floordepth+$conf{bwidth},$centretopy-$floordepth);
244             $poly->addPt($barpos+$floordepth+$conf{bwidth},($ypos+$dims{plotheight}));
245             $poly->addPt($barpos+$conf{bwidth},$ypos+$dims{plotheight}+$floordepth);
246             $poly->addPt($barpos+$conf{bwidth},$centretopy);
247             $image->filledPolygon($poly,$shadeside);
248             $coords = int($barpos) . "," . int($centretopy-$floordepth) . "," . int($barpos+$conf{bwidth}+$spacing) . "," . int($ypos+$dims{plotheight}+$floordepth);
249             }
250             # create imagemap shape
251             $shapes .= $areatag;
252             $shapes =~ s/%coords%/$coords/;
253             $shapes =~ s/%title%/$d->[0]: $d->[1]/;
254             # increment xpos for next bar
255             $barpos += ($conf{bwidth} + $spacing);
256             }
257            
258             # finish imagemap html
259             $imagemap =~ s/%shapes%/$shapes/g;
260            
261             # --- create image file --- #
262            
263             my $writedata;
264             if($conf{file} =~ /\.gif$/i){
265             $writedata = $image->gif();
266             }
267             elsif($conf{file} =~ /\.png$/i){
268             my $q = 10-$conf{quality};
269             $writedata = $image->png($q);
270             }
271             else{
272             my $q = $conf{quality}*10;
273             $writedata = $image->jpeg($q);
274             }
275             open IMG,">$conf{file}";
276             binmode IMG;
277             print IMG $writedata;
278             close IMG;
279            
280             return $imagemap;
281            
282             }
283            
284             # -----------------------------------------------------------------------------
285             # SUBROUTINES
286             # -----------------------------------------------------------------------------
287            
288             sub config {
289            
290             my %conf = (
291            
292             # colours
293            
294             black => { R => 0, G => 0, B => 0 },
295             white => { R => 255, G => 255, B => 255 },
296             vltgrey => { R => 245, G => 245, B => 245 },
297             ltgrey => { R => 230, G => 230, B => 230 },
298             midgrey => { R => 180, G => 180, B => 180 },
299             midblue => { R => 54, G => 100, B => 170 },
300            
301             # file output details
302            
303             file => '', # file path and name; file extension can be .jpg|gif|png
304             quality => '9', # image file quality: 1 (worst) - 10 (best)
305            
306             # main image properties
307            
308             imgw => 400, # preferred width - maybe more depending on bar properties and number of x-axis values specified
309             imgh => 320, # preferred height - maybe more depending on bar properties and number of y-axis values specified
310             ipadding => 14, # padding between items, eg: between top of image and title
311             iplotpad => 8, # padding between axis vals and plot area
312             ibgcol => 'white', # background colour
313             iborder => '', # defaults to no border
314            
315             # plot area properties
316            
317             plinecol => 'midgrey', # line colour
318             pflcol => 'vltgrey', # floor colour
319             pbgcol => 'ltgrey', # background colour
320             pbgfill => 'gradient', # 'gradient' or 'solid' for fill type
321             plnspace => 25, # minimum spacing between divisions
322             pnumdivs => 6, # maximum number of divisions
323            
324             # bar properties
325             bstyle => 'bar', # can be 'column' or 'bar'
326             bcolumnfill => 'gradient', # 'gradient' or 'solid' for columns
327             bminspace => 18, # minimum spacing between bars
328             bwidth => 18, # width
329             bfacecol => 'midblue', # colour of column/bar face, or 'random' for random colour
330            
331             # graph title
332            
333             ttext => '', # title text
334             tfont => '', # specify path/truetype font otherwise defaults to gdGiantFont
335             tsize => 11, # font size
336             tfontcol => 'black', # font colour
337            
338             # axis labels
339            
340             xltext => '', # x label text
341             yltext => '', # y label text
342             lfont => '', # specify path/truetype font otherwise defaults to gdLargeFont
343             lsize => 10, # font size
344             lfontcol => 'midblue', # font colour
345            
346             # axis values
347            
348             vfont => '', # specify path/truetype font otherwise defaults to gdSmallFont
349             vsize => 8, # font size
350             vfontcol => 'black', # font colour
351            
352             );
353            
354             return %conf;
355             }
356            
357             sub imagemaphtml {
358            
359             my $imgtag = qq[\n];
360             my $maptag = qq[\n%shapes%];
361             my $areatag = qq[\n];
362             return ($imgtag, $maptag, $areatag);
363             }
364            
365             sub getstringsize {
366            
367             my ($font,$string,$size,$angle) = @_;
368            
369             if($font =~ /^gd\w+Font$/){
370             my %gdfonts = (
371             'gdTinyFont' => { 'w' => 5, 'h' => 8 },
372             'gdSmallFont' => { 'w' => 6, 'h' => 12 },
373             'gdMediumBoldFont' => { 'w' => 7, 'h' => 13 },
374             'gdLargeFont' => { 'w' => 8, 'h' => 16 },
375             'gdGiantFont' => { 'w' => 9, 'h' => 15 }
376             );
377             return ($gdfonts{$font}{w}*length($string),$gdfonts{$font}{h});
378             }
379             else {
380             my ($wid,$hgt,$x);
381             my $tst = new GD::Image(1000,1000,1);
382             my $tmp = $tst->colorAllocate(0,0,0);
383             my $radangle = $angle / 57.2958;
384             my @bounds = GD::Image->stringFT($tmp,$font,$size,$radangle,50,950,$string);
385             if ($angle == 0) {
386             $wid = $bounds[4]-$bounds[6];
387             $hgt = $bounds[1]-$bounds[7];
388             }
389             elsif ($angle == 45) {
390             $wid = $bounds[2]-$bounds[6];
391             $hgt = $bounds[1]-$bounds[5];
392             $x = $bounds[0]-$bounds[6];
393             }
394             else {
395             $wid = $bounds[0]-$bounds[6];
396             $hgt = $bounds[1]-$bounds[3];
397             }
398             #print "LL=$bounds[0],$bounds[1] LR=$bounds[2],$bounds[3] UR=$bounds[4],$bounds[5] UL=$bounds[6],$bounds[7]" if $string eq "Number sold";
399             return ($wid,$hgt,$x);
400             }
401             }
402            
403             sub getdimensions {
404            
405             my @data = @{$_[0]};
406             my %conf = %{$_[1]};
407            
408             my %dims = (
409             minwidth => 0, # min overall graph width
410             minheight => 0, # min overall graph height
411             titlew => 0, # title width
412             titleh => 0, # title text height
413             ylabelwidth => 0, # y axis label width
414             ylabelheight => 0, # y axis label height
415             xlabelwidth => 0, # x axis label width
416             xlabelheight => 0, # x axis label height
417             xvalheight => 0, # largest x axis value height
418             xhorheight => 0, # largest x axis value height
419             yvalwidth => 0, # largest y axis value width
420             floor => 0, # width/height of 3D floor/sides
421             plotwidth => 0, # overall plot area width
422             plotheight => 0, # overall plot area height
423             numdivs => 6, # number of divisions in plot area
424             range => 6000000 # upper range value
425             );
426            
427             # --- calculate y axis ranges --- #
428            
429             # find highest number
430             my $high = 0;
431             foreach my $d(@data){
432             $high = $d->[1] if $d->[1] > $high;
433             }
434            
435             # find best number of divs and upper range number
436             my @divs = (1,2,5,10,20,50,100,200,500,1000,2000,5000,10000,20000,50000,100000,200000,500000,1000000);
437             foreach my $n(6,5,4){
438             foreach my $d(@divs){
439             if(($n*$d) > $high and (($n*$d)-$high) < ($dims{range}-$high)){
440             $dims{numdivs} = $n;
441             $dims{range} = $n*$d;
442             last;
443             }
444             }
445             }
446            
447             # --- calculate heights --- #
448            
449             # top padding
450             $dims{minheight} += $conf{ipadding};
451            
452             # title height
453             if($conf{ttext} ne ''){
454             if($conf{tfont} eq ''){
455             ($dims{titlew},$dims{titleh}) = getstringsize("gdGiantFont",$conf{ttext});
456             }
457             else{
458             ($dims{titlew},$dims{titleh}) = getstringsize($conf{tfont},$conf{ttext},$conf{tsize},0);
459             }
460             $dims{minheight} += ($dims{titleh} + $conf{ipadding}); # add title height & padding below to minheight
461             }
462            
463             # padding between x vals and plot area
464             $dims{minheight} += $conf{iplotpad};
465            
466             # largest x val height - angled and horizontal
467             foreach my $d(@data){
468             if($conf{vfont} eq ''){
469             my($h,$w) = getstringsize("gdSmallFont",$d->[0]);
470             $dims{xvalheight} = $h if $h > $dims{xvalheight};
471             my($w2,$h2) = getstringsize("gdSmallFont",$d->[0]);
472             $dims{xhorheight} = $h2 if $h2 > $dims{xhorheight};
473             }
474             else{
475             my($w,$h) = getstringsize($conf{vfont},$d->[0],$conf{vsize},45);
476             $dims{xvalheight} = $h if $h > $dims{xvalheight};
477             my($w2,$h2) = getstringsize($conf{vfont},$d->[0],$conf{vsize},0);
478             $dims{xhorheight} = $h2 if $h2 > $dims{xhorheight};
479             }
480             }
481             $dims{minheight} += $dims{xvalheight};
482            
483             # bottom padding
484             $dims{minheight} += $conf{ipadding};
485            
486             # x axis label height & extra padding
487             if($conf{xltext} ne ''){
488             if($conf{lfont} eq ''){
489             ($dims{xlabelwidth},$dims{xlabelheight}) = getstringsize("gdMediumBoldFont",$conf{xltext});
490             }
491             else{
492             ($dims{xlabelwidth},$dims{xlabelheight}) = getstringsize($conf{lfont},$conf{xltext},$conf{lsize},0);
493             }
494             $dims{minheight} += ($dims{xlabelheight} + $conf{ipadding});
495             }
496            
497             # --- calculate widths --- #
498            
499             # left padding
500             $dims{minwidth} += $conf{ipadding};
501            
502             # y label width
503             if($conf{yltext} ne ''){
504             if($conf{lfont} eq ''){
505             ($dims{ylabelheight},$dims{ylabelwidth}) = getstringsize("gdMediumBoldFont",$conf{yltext});
506             }
507             else{
508             ($dims{ylabelwidth},$dims{ylabelheight}) = getstringsize($conf{lfont},$conf{yltext},$conf{lsize},90);
509             }
510             $dims{minwidth} += ($dims{ylabelwidth} + $conf{ipadding});
511             }
512            
513             # largest y val width (ie: of upper range)
514             if($conf{vfont} eq ''){
515             ($dims{yvalwidth},$dims{yvalheight}) = getstringsize("gdSmallFont",$dims{range});
516             }
517             else{
518             ($dims{yvalwidth},$dims{yvalheight}) = getstringsize($conf{vfont},$dims{range},$conf{vsize},0);
519             }
520             $dims{minwidth} += $dims{yvalwidth};
521            
522             # padding between y vals and plot area
523             $dims{minwidth} += $conf{iplotpad};
524            
525             # right padding
526             $dims{minwidth} += $conf{ipadding};
527            
528             # --- calculate plot area and make final adjustments to min width/height --- #
529            
530             # force practical minimum bar/column widths
531             $conf{bwidth} = 10 if $conf{bwidth} < 10;
532             $conf{bwidth} += 1 if $conf{bwidth} =~ /[02468]$/ and $conf{bstyle} eq "column";
533            
534             # floor/side sizes
535             my $floorwidth = $conf{bwidth}*1.25;
536             $dims{floor} = sprintf("%.0f",sqrt(($floorwidth*$floorwidth)/2));
537             $dims{minheight} += $dims{floor};
538             $dims{minwidth} += $dims{floor};
539            
540             # plot width
541             $conf{bminspace} = $dims{xhorheight} if $conf{bminspace} < $dims{xhorheight}; # ensure min bar spacing !<= x val height
542             my $keyn = scalar @data;
543             $dims{plotwidth} = $conf{iplotpad} + ($keyn * $conf{bwidth}) + (($keyn-1) * $conf{bminspace}) + $conf{iplotpad} + $dims{floor};
544             $dims{plotwidth} = $conf{imgw} - $dims{minwidth} if $dims{plotwidth} < $conf{imgw} - $dims{minwidth};
545             $dims{minwidth} += $dims{plotwidth};
546            
547             # plot height
548             $conf{plnspace} = $dims{yvalheight} if $conf{plnspace} < $dims{yvalheight}; # ensure min line spacing !<= y val height
549             $dims{plotheight} = $dims{numdivs}*$conf{plnspace};
550             $dims{plotheight} = $conf{imgh} - $dims{minheight} if $dims{plotheight} < $conf{imgh} - $dims{minheight};
551             $dims{minheight} += $dims{plotheight};
552            
553             return %dims;
554             }
555            
556             sub getshades {
557            
558             my @rgb = ($_[0],$_[1],$_[2]);
559             my %conf = %{$_[3]};
560            
561             # make sure 2 or more colour values can accommodate darkening by 70
562             my ($ctr,$darker) = (0,0);
563             foreach my $c(@rgb){
564             $ctr++ if $c >= 70;
565             }
566             $darker = 1 if $ctr >= 2;
567             # create shades
568             my %shades;
569             my $ctr = 0;
570             foreach my $s(qw/R G B/){
571             # shades darker than face colour
572             if($darker == 1){
573             $conf{bcolumnfill} eq "gradient" and $conf{bstyle} eq "column" ? ($shades{top}{$s} = $rgb[$ctr] - 50) : ($shades{top}{$s} = $rgb[$ctr] - 70);
574             $shades{side}{$s} = $rgb[$ctr] - 40;
575             $shades{top}{$s} = 0 if $shades{top}{$s} < 0;
576             $shades{side}{$s} = 0 if $shades{side}{$s} < 0;
577             }
578             # shades lighter than face colour
579             else{
580             $conf{bcolumnfill} eq "gradient" and $conf{bstyle} eq "column" ? ($shades{top}{$s} = $rgb[$ctr] + 40) : ($shades{top}{$s} = $rgb[$ctr] + 70);
581             $shades{side}{$s} = $rgb[$ctr] + 50;
582             $shades{top}{$s} = 255 if $shades{top}{$s} > 255;
583             $shades{side}{$s} = 255 if $shades{side}{$s} > 255;
584             }
585             $ctr++;
586             }
587             return %shades;
588             }
589            
590             sub randomcolour {
591            
592             my %colour;
593             # generate random colour numbers but make sure not too close to floor colour
594             for my $c(qw/R G B/){
595             $colour{$c} = int(rand(256));
596             }
597             return %colour;
598             }
599            
600             sub gradientfill
601             {
602             # get params
603             my ($clr,$fromx,$fromy,$tox,$toy,$height,$column,$conf_imgheight) = @_;
604            
605             # colour hash for passed colour
606             my @n = $image->rgb($clr);
607             my %c2 = (
608             R => $n[0],
609             G => $n[1],
610             B => $n[2]
611             );
612            
613             # work out darkness of colour and set offset accordingly
614             my ($offset,$ctr) = (50,0);
615             foreach my $i(qw/R G B/){
616             $ctr++ if $c2{$i} > 150;
617             }
618             $offset += 35 if $ctr < 2 and $column eq '';
619            
620             # set up colour hash for lighter shade
621             my %c1;
622             foreach my $i(qw/R G B/){
623             $c1{$i} = $c2{$i} + $offset;
624             $c1{$i} = 255 if $c1{$i} > 255;
625             }
626            
627             # initiate dynamic vars
628             my $pixposf = $fromy; # current from x position
629             my $pixpost = $toy; # current to x position
630             my %clrs;
631             my $rgb = 0;
632             foreach ( keys %c1 ) { $clrs{$_}{clr} = $c1{$_}; }
633            
634             # add {adj} & {pix} & {pxctr} subhashes to %clrs
635             foreach $rgb (qw/R G B/) {
636             if ($c1{$rgb} > $c2{$rgb} and $height > ($c1{$rgb}-$c2{$rgb})) {
637             $clrs{$rgb}{adj} = -1;
638             $clrs{$rgb}{pix} = ($height-1)/($c1{$rgb}-$c2{$rgb});
639             }
640             elsif ($c1{$rgb} > $c2{$rgb} and $height < ($c1{$rgb}-$c2{$rgb})) {
641             $clrs{$rgb}{adj} = -(($c1{$rgb}-$c2{$rgb})/($height-1));
642             $clrs{$rgb}{pix} = 1;
643             }
644             elsif ($c2{$rgb} > $c1{$rgb} and $height > ($c2{$rgb}-$c1{$rgb})) {
645             $clrs{$rgb}{adj} = 1;
646             $clrs{$rgb}{pix} = ($height-1)/($c2{$rgb}-$c1{$rgb});
647             }
648             elsif ($c2{$rgb} > $c1{$rgb} and $height < ($c2{$rgb}-$c1{$rgb})) {
649             $clrs{$rgb}{adj} = ($c2{$rgb}-$c1{$rgb})/($height-1);
650             $clrs{$rgb}{pix} = 1;
651             }
652             $clrs{$rgb}{pxctr} = $clrs{$rgb}{pix};
653             }
654            
655             # do gradient fill
656             while ($column ne '' ? ($pixposf > $fromy-$height) : ($pixposf < $fromy+$height)) {
657             # round to nearest integer and make sure within 0-255 range
658             my %colour;
659             foreach $rgb (qw/R G B/) {
660             $colour{$rgb} = sprintf("%.0f",$clrs{$rgb}{clr});
661             if ($colour{$rgb} > 255) {
662             $colour{$rgb} = 255;
663             }
664             elsif ($colour{$rgb} < 0) {
665             $colour{$rgb} = 0;
666             }
667             }
668             # set line colour
669             my $temp = $image->colorAllocate($colour{R},$colour{G},$colour{B});
670            
671             # draw line
672             if($column ne ''){
673             my $ind = $image->getPixel($pixposf,$tox);
674             my $toytemp = $tox;
675             while ($ind eq $clr and $toytemp < $conf_imgheight){
676             $toytemp++;
677             $ind = $image->getPixel($pixposf,$toytemp);
678             }
679             $image->line($pixposf,$fromx,$pixposf,$toytemp,$temp);
680             $pixposf--;
681             }
682             else{
683             $image->line($fromx,$pixposf,$tox,$pixpost,$temp);
684             $pixposf++;
685             $pixpost++;
686             }
687            
688             # adjust RGB values
689             foreach $rgb (qw/R G B/) {
690             if($column ne ''){
691             if ($pixposf == ($fromy-$height)) {
692             $clrs{$rgb}{clr} = $c2{$rgb};
693             }
694             elsif ( $fromy-$pixposf >= $clrs{$rgb}{pxctr} ) {
695             $clrs{$rgb}{pxctr} += $clrs{$rgb}{pix};
696             $clrs{$rgb}{clr} += $clrs{$rgb}{adj};
697             }
698             }
699             else{
700             if ($pixposf == ($fromy+$height)-1) {
701             $clrs{$rgb}{clr} = $c2{$rgb};
702             }
703             elsif ( $pixposf-$fromy >= $clrs{$rgb}{pxctr} ) {
704             $clrs{$rgb}{pxctr} += $clrs{$rgb}{pix};
705             $clrs{$rgb}{clr} += $clrs{$rgb}{adj};
706             }
707             }
708             }
709             }
710            
711             }
712            
713             1;
714            
715             # -----------------------------------------------------------------------------
716             # DOCUMENTATION
717             # -----------------------------------------------------------------------------
718            
719             =head1 NAME
720            
721             GD::3DBarGrapher - Create 3D bar graphs using GD
722            
723             =head1 SYNOPSIS
724            
725             use GD::3DBarGrapher qw(creategraph);
726            
727             my @data = (
728             ['Apples', 28],
729             ['Pears', 43],
730             ...etc
731             );
732            
733             my %options = (
734             'file' => '/webroot/images/mygraph.jpg',
735             );
736            
737             my $imagemap = creategraph(\@data, \%options);
738            
739             =head1 DESCRIPTION
740            
741             There is only one function in the 3dBarGrapher module and that is creategraph
742             which will return image map XHTML for use in a web page displaying the graph.
743            
744             The data to graph must be passed in a multidimensional array where column 0
745             is the x-axis name of the item to graph and column 1 is it's associated
746             numerical value.
747            
748             Graph options are passed in a hash and override the defaults listed below. At
749             minimum the 'file' option must be included and specify the full path and
750             filename of the graph to create.
751            
752             =head1 Options
753            
754             my %options = (
755            
756             # colours
757            
758             black => { R => 0, G => 0, B => 0 },
759             white => { R => 255, G => 255, B => 255 },
760             vltgrey => { R => 245, G => 245, B => 245 },
761             ltgrey => { R => 230, G => 230, B => 230 },
762             midgrey => { R => 180, G => 180, B => 180 },
763             midblue => { R => 54, G => 100, B => 170 },
764            
765             # file output details
766            
767             file => '', # file path and name; file extension
768             # can be .jpg|gif|png
769             quality => 9, # image quality: 1 (worst) - 10 (best)
770             # Only applies to jpg and png
771            
772             # main image properties
773            
774             imgw => 400, # preferred width in pixels
775             imgh => 320, # preferred height in pixels
776             iplotpad => 8, # padding between axis vals & plot area
777             ipadding => 14, # padding between other items
778             ibgcol => 'white', # COLOUR NAME; background colour
779             iborder => '', # COLOUR NAME; border, if any
780            
781             # plot area properties
782            
783             plinecol => 'midgrey', # COLOUR NAME; line colour
784             pflcol => 'vltgrey', # COLOUR NAME; floor colour
785             pbgcol => 'ltgrey', # COLOUR NAME; back/side colour
786             pbgfill => 'gradient', # 'gradient' or 'solid'; back/side fill
787             plnspace => 25, # minimum pixel spacing between divisions
788             pnumdivs => 6, # maximum number of y-axis divisions
789            
790             # bar properties
791            
792             bstyle => 'bar', # 'bar' or 'column' style
793             bcolumnfill => 'gradient', # 'gradient' or 'solid' for columns
794             bminspace => 18, # minimum spacing between bars
795             bwidth => 18, # width of bar
796             bfacecol => 'midblue', # COLOUR NAME or 'random'; bar face,
797             # 'random' for random bar face colour
798             # graph title
799            
800             ttext => '', # title text
801             tfont => '', # uses gdGiantFont unless a true type
802             # font is specified
803             tsize => 11, # font point size
804             tfontcol => 'black', # COLOUR NAME; font colour
805            
806             # axis labels
807            
808             xltext => '', # x-axis label text
809             yltext => '', # y-axis label text
810             lfont => '', # uses gdLargeFont unless a true type
811             # font is specified
812             lsize => 10, # font point size
813             lfontcol => 'midblue', # COLOUR NAME; font colour
814            
815             # axis values
816            
817             vfont => '', # uses gdSmallFont unless a true type
818             # font is specified
819             vsize => 8, # font point size
820             vfontcol => 'black', # COLOUR NAME; font colour
821            
822             );
823            
824             Notes on options:
825            
826             =over 5
827            
828             =item 1.
829             Options commented with "COLOUR NAME" expect the name of one of the default
830             colours above, or you can define your own colours by adding new lines in the
831             same format
832            
833             =item 2.
834             Overall graph width and height can exceed the preferred values, depending on
835             number of items to graph and the values specified for various settings like
836             bwidth, bminspace, etc
837            
838             =item 3.
839             For better text quality it is recommended to specify true type fonts for
840             options tfont, lfont & vfont. the full path and font file name must be
841             included, eg: 'c:/windows/fonts/verdana.ttf'
842            
843             =item 4.
844             Only options that default to empty can be defined as empty
845            
846             =head1 Image Map
847            
848             The creategraph function returns XHTML code for the image and an associated
849             image map, something like this:
850            
851            
852            
853            
854            
855             ...etc
856            
857            
858             =head1 Bugs
859            
860             There aren't any known ones but feel free to report any you find and I may
861             (or may not) fix them! Contact swarhurst _at_ cpan.org
862            
863             =head1 AUTHOR
864            
865             3DBarGrapher is copyright (c) 2009 S.I.Warhurst and is distributed under the
866             same terms and conditions as Perl itself. See the Perl Artistic license:
867            
868             http://www.perl.com/language/misc/Artistic.html
869            
870             =head1 SEE ALSO
871            
872             L
873            
874             =cut