File Coverage

blib/lib/SVGGraph.pm
Criterion Covered Total %
statement 9 195 4.6
branch 0 76 0.0
condition 0 3 0.0
subroutine 3 10 30.0
pod 0 7 0.0
total 12 291 4.1


line stmt bran cond sub pod time code
1             package SVGGraph;
2              
3 1     1   11409 use strict;
  1         2  
  1         37  
4 1     1   6 use warnings;
  1         3  
  1         33  
5 1     1   1045 use utf8;
  1         14  
  1         6  
6             our $VERSION = '0.07';
7              
8             sub new()
9             {
10 0     0 0   my $self = shift;
11 0           return bless {}, $self;
12             }
13              
14             sub CreateGraph()
15             {
16             ### First element of @_ is a reference to the element that called this subroutine
17 0     0 0   my $self = shift;
18             ### Second is a reference to a hash with options
19 0           my $options = shift;
20             ### The options passed in the anonymous hash are optional so create a default value first
21 0           my $horiUnitDistance = 20;
22 0 0         if ($$options{'horiunitdistance'})
23             {
24 0           $horiUnitDistance = $$options{'horiunitdistance'};
25             }
26 0           my $graphType = 'spline';
27 0 0         if ($$options{'graphtype'})
28             {
29 0           $graphType = $$options{'graphtype'};
30             }
31             ### The rest are references to arrays with references to arrays with x and y values
32 0           my @xyArrayRefs = @_;
33             ### Check if the color ($xyArrayRefs[$i]->[3]) is provided. If not, choose black
34 0           for (my $i = 0; $i < @xyArrayRefs; $i++)
35             {
36 0 0         unless ($xyArrayRefs[$i]->[3])
37             {
38 0           $xyArrayRefs[$i]->[3] = '#000000';
39             }
40             }
41             ### Declare the $minX as the lowest value of x in the arrays, same for $minY, $maxX and $maxY
42 0           my $minX = $xyArrayRefs[0]->[0]->[0]; ### Equivalent to ${${$xyArrayRefs[0]}[0]}[0];
43 0           my $minY = $xyArrayRefs[0]->[1]->[0];
44 0           my $maxX = $minX;
45 0           my $maxY = $minY;
46             ### Then really search for the lowest and highest value of x and y
47 0           for (my $i = 0; $i < @xyArrayRefs; $i++)
48             {
49 0           for (my $j = 0; $j < @{$xyArrayRefs[$i]->[0]}; $j++)
  0            
50             {
51 0 0         if ($xyArrayRefs[$i]->[0]->[$j] > $maxX)
52             {
53 0           $maxX = $xyArrayRefs[$i]->[0]->[$j];
54             }
55 0 0         if ($xyArrayRefs[$i]->[0]->[$j] < $minX)
56             {
57 0           $minX = $xyArrayRefs[$i]->[0]->[$j];
58             }
59 0 0         if ($xyArrayRefs[$i]->[1]->[$j] > $maxY)
60             {
61 0           $maxY = $xyArrayRefs[$i]->[1]->[$j];
62             }
63 0 0         if ($xyArrayRefs[$i]->[1]->[$j] < $minY)
64             {
65 0           $minY = $xyArrayRefs[$i]->[1]->[$j];
66             }
67             }
68             }
69             ### If max equals min, change them artificially
70 0 0         if ($maxX == $minX)
71             {
72 0           $maxX += 1;
73             }
74 0 0         if ($maxY == $minY)
75             {
76 0           $maxY += 1;
77             }
78             ### Calculate all dimensions neccessary to create the Graph
79             ### Height of the total svg image in pixels:
80 0           my $imageHeight = 400;
81 0 0         if ($$options{'imageheight'})
82             {
83 0           $imageHeight = $$options{'imageheight'};
84             }
85             ### Width of the verticabar or dots in the graph
86 0           my $barWidth = 3;
87 0 0         if ($$options{'barwidth'})
88             {
89 0           $barWidth = $$options{'barwidth'};
90             }
91             ### Distance between the sides of the gris and the sides of the image:
92 0           my $cornerDistance = 50;
93             ### Since svg counts from the top left corner of the image, we translate all coordinates vertically in pixels:
94 0           my $vertTranslate = $imageHeight - $cornerDistance;
95             ### The width of the grid in pixels:
96 0           my $gridWidth = $horiUnitDistance * ($maxX - $minX);
97             ### The height of the grid in pixels:
98 0           my $gridHeight = $imageHeight - 2 * $cornerDistance;
99             ### The width of the whole svg image:
100 0           my $imageWidth = $gridWidth + (4 * $cornerDistance);
101             ### The horizontal space between vertical gridlines in pixels:
102 0           my $xGridDistance = 20;
103             ### The vertical space between horizontal gridlines in pixels:
104 0           my $yGridDistance = 30;
105              
106             ### Now initiate the svg graph by declaring some general stuff.
107 0           my $svg .= <<" EOF";
108            
109            
110            
111            
112             EOF
113 0 0         if ($graphType eq 'spline')
114             {
115 0           for (my $i = 0; $i < @xyArrayRefs; $i++)
116             {
117 0           $svg .= $self->CreateDot(0, 0, $barWidth, $xyArrayRefs[$i]->[3], $i);
118             }
119             }
120 0           $svg .= <<" EOF";
121            
140            
141            
142             EOF
143              
144             ### make x- and y axes
145 0           $svg .= "\n";
146              
147             ### print numbers on y axis and horizontal gridlines
148             ### First calculate the width between the gridlines in y-units, not in pixels
149 0           my $deltaYUnits = $self->NaturalRound ($yGridDistance * ($maxY - $minY) / $gridHeight);
150             ### Adjust $minX and $maxX so the gridlines and numbers startand end in a whole and nice number.
151 0           $minY = int ($minY / $deltaYUnits - 0.999999999999) * $deltaYUnits;
152 0           $maxY = int ($maxY / $deltaYUnits + 0.999999999999) * $deltaYUnits;
153             ### Calculate the number of pixels each units stands for.
154 0           my $yPixelsPerUnit = ($gridHeight / ($maxY - $minY));
155 0           my $deltaYPixels = $deltaYUnits * $yPixelsPerUnit;
156             ### Calculate the amount of gridlines and therefore the amount of numbers on the y-axis
157 0           my $yNumberOfNumbers = int ($gridHeight / $deltaYPixels) + 1;
158             ### Draw the numbers and the gridlines
159 0           for (my $i = 0; $i < $yNumberOfNumbers; $i++)
160             {
161 0           my $YValue = sprintf ("%1.2f", (-1 * $i * $deltaYPixels)) + 0;
162             ### numbers
163 0           $svg .= "" . ($minY + $i * $deltaYUnits) . "\n";
164             ### gridline
165 0 0         if ($i != 0)
166             {
167 0           $svg .= "\n";
168             }
169             }
170              
171             ### print numbers on x axis and vertical gridlines
172 0           my $deltaXUnits = $self->NaturalRound ($xGridDistance * ($maxX - $minX) / $gridWidth);
173 0           my $xPixelsPerUnit = ($gridWidth / ($maxX - $minX));
174 0           my $deltaXPixels = $deltaXUnits * $xPixelsPerUnit;
175 0           my $xNumberOfNumbers = int ($gridWidth / $deltaXPixels) + 1;
176 0           for (my $i = 0; $i < $xNumberOfNumbers; $i++)
177             {
178 0           my $XValue = sprintf ("%1.2f", ($i * $deltaXPixels)) + 0;
179             ### numbers
180 0           $svg .= "" . ($minX + $i * $deltaXUnits) . "\n";
181             ### gridline
182 0 0         if ($i != 0)
183             {
184 0           $svg .= "\n";
185             }
186             }
187              
188             ### print measurepoints (dots) (data) (coordinates)
189             ### Spline
190 0 0         if ($graphType eq 'spline')
    0          
191             {
192 0           for (my $i = 0; $i < @xyArrayRefs; $i++)
193             {
194 0           my $dots;
195 0           for (my $dotNumber = 0; $dotNumber < @{$xyArrayRefs[$i]->[0]}; $dotNumber++)
  0            
196             {
197 0           my $dotX = $horiUnitDistance * ($xyArrayRefs[$i]->[0]->[$dotNumber] - $minX);
198 0           my $dotY = sprintf ("%1.2f", -1 * $yPixelsPerUnit * ($xyArrayRefs[$i]->[1]->[$dotNumber] - $minY)) + 0;
199 0           $dots .= "\n";
200 0 0         if ($dotNumber == 0)
201             {
202 0           $svg .= "
203             }
204             else
205             {
206 0           $svg .= " L$dotX $dotY";
207             }
208             }
209 0           $svg .= "\" style=\"fill: none; stroke: " . $xyArrayRefs[$i]->[3] . "; stroke-width:2\"/>\n$dots";
210             }
211             }
212             ### Vertical Bars
213             elsif ($graphType eq 'verticalbars')
214             {
215 0           for (my $dotNumber = 0; $dotNumber < @{$xyArrayRefs[0]->[0]}; $dotNumber++)
  0            
216             {
217             ### The longest bars must be drawn first, so that the shorter bars are drwan on top of the longer.
218             ### So we sort $i (the number of the graph) to the length of the bar for each point.
219 0           foreach my $i (sort {$xyArrayRefs[$b]->[1]->[$dotNumber] <=> $xyArrayRefs[$a]->[1]->[$dotNumber]} (0 .. $#xyArrayRefs))
  0            
220             {
221 0           my $lineX = $horiUnitDistance * ($xyArrayRefs[$i]->[0]->[$dotNumber] - $minX);
222 0           my $lineY1 = 0;
223 0 0 0       if (($minY < 0) && ($maxY > 0))
    0          
224             {
225 0           $lineY1 = $yPixelsPerUnit * $minY;
226             }
227             elsif ($maxY < 0)
228             {
229 0           $lineY1 = -1 * 1;
230             }
231 0           my $lineY2 = sprintf ("%1.2f", -1 * $yPixelsPerUnit * ($xyArrayRefs[$i]->[1]->[$dotNumber] - $minY)) + 0;
232 0           $svg .= "[3] . ";stroke-width:$barWidth;\"/>\n";
233             }
234             }
235             }
236              
237             ### print Title, Labels and Legend
238             ### Title
239 0 0         if ($$options{'title'})
240             {
241 0           my $titleStyle = 'font-size:24;';
242 0 0         if ($$options{'titlestyle'})
243             {
244 0           $titleStyle = $self->XMLEscape($$options{'titlestyle'});
245             }
246 0           $svg .= "" . $self->XMLEscape($$options{'title'}) . "\n";
247             }
248             ### x-axis label
249 0 0         if ($$options{'xlabel'})
250             {
251 0           my $xLabelStyle = 'font-size:16;';
252 0 0         if ($$options{'xlabelstyle'})
253             {
254 0           $xLabelStyle = $self->XMLEscape($$options{'xlabelstyle'});
255             }
256 0           $svg .= "" . $self->XMLEscape($$options{'xlabel'}) . "\n";
257             }
258             ### y-axis label
259 0 0         if ($$options{'ylabel'})
260             {
261 0           my $yLabelStyle = 'font-size:16;';
262 0 0         if ($$options{'ylabelstyle'})
263             {
264 0           $yLabelStyle = $self->XMLEscape($$options{'ylabelstyle'});
265             }
266 0           $svg .= "" . $self->XMLEscape($$options{'ylabel'}) . "\n";
267             }
268             ### Legend
269 0           my $legendOffset = ($cornerDistance + $gridWidth + 10) . ", $cornerDistance";
270 0 0         if ($$options{'legendoffset'})
271             {
272 0           $legendOffset = $self->XMLEscape($$options{'legendoffset'});
273             }
274 0           $svg .= "\n\n";
275 0           for (my $i = 0; $i < @xyArrayRefs; $i++)
276             {
277 0 0         if ($xyArrayRefs[$i]->[2])
278             {
279 0           my $y = 12 * $i;
280 0 0         if ($graphType eq 'spline')
281             {
282             ### The line
283 0           $svg .= "[3] . "\"/>\n";
284             ### The dot
285 0           $svg .= $self->CreateDot(8, $y, 3, $xyArrayRefs[$i]->[3], $i);
286             }
287             ### The text
288 0           $svg .= "[3] . "\">" . $xyArrayRefs[$i]->[2] . "\n";
289             }
290             }
291 0           $svg .= "\n\n";
292 0           return $svg;
293             }
294              
295             ### CreateDot is a subroutine that creates the svg code for different
296             ### kinds of dots used in the spline graph type: circles, squares, triangles and more.
297             sub CreateDot($$$$$)
298             {
299 0     0 0   my $self = shift;
300 0           my $x = shift;
301 0           my $y = shift;
302 0           my $r = shift;
303 0           my $color = shift;
304 0           $color = $self->DarkenHexRGB($color);
305 0           my $dotNumber = shift;
306 0           my $d = 2 * $r;
307 0           my $negr = -1 * $r;
308 0           my $svg;
309             ### Circle
310 0 0         if ($dotNumber == 0)
311             {
312 0           $svg = "\n";
313             }
314             ### Stars
315             else
316             {
317 0           $svg .= "
318 0           for (my $i = 1; $i <= (2*$dotNumber+2); $i++)
319             {
320 0 0         my $radius = ($i % 2) ? $r*1.5 : $r/2;
321 0           my $pi = atan2(1,1) * 4;
322 0           my $alpha = $i * ($pi / ($dotNumber + 1));
323 0           my $xi = $x + $radius * cos($alpha);
324 0           my $yi = $y + $radius * sin($alpha);
325 0 0         $svg .= ($i == 1) ? "M" : "L";
326 0           $svg .= sprintf (" %1.3f ", $xi) . (sprintf (" %1.3f ", $yi) + 0);
327             }
328 0           $svg .= "z\" style=\"fill: $color; stroke: $color;\"/>\n";
329             }
330 0           return $svg;
331             }
332              
333             ### NaturalRound is a subroutine that rounds a number to 1, 2, 5 or 10 times its order
334             ### So 110.34 becomes 100
335             ### 3.1234 becomes 2
336             ### 40 becomes 50
337              
338             sub NaturalRound($)
339             {
340 0     0 0   my $self = shift;
341 0           my $numberToRound = shift;
342 0           my $rounded;
343 0           my $order = int (log ($numberToRound) / log (10));
344 0           my $remainder = $numberToRound / 10**$order;
345 0 0         if ($remainder < 1.4)
    0          
    0          
346             {
347 0           $rounded = 10**$order;
348             }
349             elsif ($remainder < 3.2)
350             {
351 0           $rounded = 2 * 10**$order;
352             }
353             elsif ($remainder < 7.1)
354             {
355 0           $rounded = 5 * 10**$order;
356             }
357             else
358             {
359 0           $rounded = 10 * 10**$order;
360             }
361             }
362              
363             ### DarkenHexRGB is a subroutine that makes a rgb color value darker
364              
365             sub DarkenHexRGB($)
366             {
367 0     0 0   my $self = shift;
368 0           my $hexString = shift;
369 0           my $darkHexString;
370 0 0         if ($hexString =~ m/^\#/)
371             {
372 0           $darkHexString = '#';
373             }
374 0 0         if ($hexString =~ m/^\#?[0-9a-f]{6}$/i)
375             {
376 0           while ($hexString =~ m/([0-9a-f][0-9a-f])/ig)
377             {
378 0           $darkHexString .= sprintf "%02lx", int(hex($1)/2);
379             }
380 0           return $darkHexString;
381             }
382             else
383             {
384 0           return $hexString;
385             }
386             }
387              
388             sub NegateHexadecimalRGB($)
389             {
390 0     0 0   my $self = shift;
391 0           my $hexString = shift;
392 0           my $negHexString;
393 0 0         if ($hexString =~ m/^\#/)
394             {
395 0           $negHexString = '#';
396             }
397 0           while ($hexString =~ m/([0-9a-f]{2})/ig)
398             {
399 0           $negHexString .= sprintf "%02lx", (255 - hex($1));
400             }
401 0           return $negHexString;
402             }
403              
404             ### XMLEscape is a subroutine that converts special XML characters to their xml encoding character.
405              
406             sub XMLEscape($)
407             {
408 0     0 0   my $self = shift;
409 0           my $string = shift;
410 0 0         unless (defined ($string))
411             {
412 0           $string = '';
413             }
414 0           $string =~ s/\&/&/g;
415 0           $string =~ s/>/>/g;
416 0           $string =~ s/
417 0           $string =~ s/\"/"/g;
418 0           $string =~ s/\'/'/g;
419             #$string =~ s/([\x00-\x1f])/sprintf('&#x%02X;', ord($1))/ge;
420 0           $string =~ s/([\x{80}-\x{ffff}])/sprintf('&#x%04X;', ord($1))/ge;
  0            
421 0           return $string;
422             }
423              
424             1;
425              
426             __END__