File Coverage

blib/lib/URI/GoogleChart.pm
Criterion Covered Total %
statement 175 182 96.1
branch 96 116 82.7
condition 51 73 69.8
subroutine 20 20 100.0
pod 1 2 50.0
total 343 393 87.2


line stmt bran cond sub pod time code
1             package URI::GoogleChart;
2              
3 2     2   13842 use strict;
  2         5  
  2         108  
4              
5             our $VERSION = "1.02";
6              
7 2     2   1946 use URI;
  2         23009  
  2         80  
8 2     2   21 use Carp qw(croak carp);
  2         9  
  2         13388  
9              
10             my $BASE = "http://chart.apis.google.com/chart";
11              
12             our %TYPE_ALIAS = (
13             "lines" => "lc",
14             "sparklines" => "ls",
15             "xy-lines" => "lxy",
16              
17             "horizontal-stacked-bars" => "bhs",
18             "vertical-stacked-bars" => "bvs",
19             "horizontal-grouped-bars" => "bhg",
20             "vertical-grouped-bars" => "bvg",
21              
22             "pie" => "p",
23             "pie-3d" => "p3",
24             "3d-pie" => "p3",
25             "concentric-pie" => "pc",
26              
27             "venn" => "v",
28             "scatter-plot" => "s",
29             "radar" => "r",
30             "radar-splines" => "rs",
31             "google-o-meter" => "gom",
32              
33             "africa" => "t",
34             "asia" => "t",
35             "europe" => "t",
36             "middle_east" => "t",
37             "south_america" => "t",
38             "usa" => "t",
39             "world" => "t",
40             );
41              
42             our %COLOR_ALIAS = (
43             "red" => "FF0000",
44             "lime" => "00FF00",
45             "blue" => "0000FF",
46              
47             "green" => "008000",
48             "navy" => "000080",
49              
50             "yellow" => "FFFF00",
51             "aqua" => "00FFFF",
52             "fuchsia" => "FF00FF",
53             "maroon" => "800000",
54             "purple" => "800080",
55             "olive" => "808000",
56             "teal" => "008080",
57              
58             "white" => "FFFFFF",
59             "silver" => "C0C0C0",
60             "gray" => "808080",
61             "black" => "000000",
62              
63             "transparent" => "00000000",
64             );
65              
66             our %AXIS_ALIAS = (
67             "left" => "y",
68             "right" => "r",
69             "top" => "t",
70             "bottom" => "x",
71             );
72              
73             our %ENCODING_ALIAS = (
74             "text" => "t",
75             "simple" => "s",
76             "extended" => "e",
77             );
78              
79             # constants for data encoding
80             my @C = ("A" .. "Z", "a" .. "z", 0 .. 9, "-", ".");
81             my $STR_s = join("", @C[0 .. 61]);
82             my $STR_e = do {
83             my @v;
84             for my $x (@C) {
85             for my $y (@C) {
86             push(@v, "$x$y");
87             }
88             }
89             join("", @v);
90             };
91             die unless length($STR_s) == 62;
92             die unless length($STR_e) == 4096 * 2;
93              
94              
95             sub new {
96 22     22 1 4542 my($class, $type, $width, $height, %opt) = @_;
97              
98 22 50       67 croak("Chart type not provided") unless $type;
99 22 50 33     108 croak("Chart size not provided") unless $width && $height;
100              
101 22   66     136 my %param = (
102             cht => $TYPE_ALIAS{$type} || $type,
103             chs => join("x", $width, $height),
104             );
105 22 100 66     87 $param{chtm} = $type if $param{cht} eq "t" && $type ne "t"; # maps
106              
107             my %handle = (
108             data => \&_data,
109             range => 1,
110             min => 1,
111             max => 1,
112             range_round => 1,
113             range_show => 1,
114             encoding => 1,
115              
116             color => sub {
117 8     8   11 my $v = shift;
118 8 100       22 $v = [$v] unless ref($v);
119 8         21 $param{chco} = join(",", map _color($_), @$v);
120             },
121             background => sub {
122 3     3   7 $param{chf} = "bg,s," . _color(shift);
123             },
124             title => sub {
125 4     4   7 my $title = shift;
126 4 50       87 ($title, my($color, $size)) = @$title if ref($title) eq "ARRAY";
127 4         46 $title =~ s/\n+\z//;
128 4         9 $title =~ s/\n/|/g;
129 4         8 $param{chtt} = $title;
130 4 50 33     30 if (defined($color) || defined($size)) {
131 0 0       0 $color = defined($color) ? _color($color) : "";
132 0 0       0 $size = "" unless defined $size;
133 0         0 $param{chts} = "$color,$size";
134             }
135             },
136             label => sub {
137 7     7   11 my $lab = shift;
138 7 100       21 $lab = [$lab] unless ref($lab) eq "ARRAY";
139 7 100       32 my $k = $param{cht} =~ /^p|^gom$/ ? "chl" : "chdl";
140 7         35 $param{$k} = join("|", @$lab);
141             },
142             rotate => sub {
143 2     2   3 my $p = shift;
144 2         8 $p += 360 while $p < 0;
145 2         6 $p /= 180 / 3.1416; # convert to radians
146 2         34 $param{chp} = sprintf "%.2f", $p;
147             },
148             margin => sub {
149 6     6   8 my $m = shift;
150 6 100       23 $m = [($m) x 4] unless ref($m);
151 6         37 $param{chma} = join(",", @$m);
152             }
153 22         472 );
154              
155 22         60 my $data = delete $opt{data}; # need to be processed last
156 22         68 for my $k (keys %opt) {
157 52 100       111 if (my $h = $handle{$k}) {
158 45 100       166 $h->($opt{$k}, \%param, \%opt) if ref($h) eq "CODE";
159             }
160             else {
161 7         16 $param{$k} = $opt{$k};
162 7 50       33 carp("Unrecognized parameter '$k' embedded in GoogleChart URI")
163             unless $k =~ /^ch/;
164             }
165             }
166 22 100       86 _data($data, \%param, \%opt) if $data;
167              
168             # generate URI
169 22         107 my $uri = URI->new($BASE);
170 22         27558 $uri->query_form(map { $_ => $param{$_} } _sort_chart_keys(keys %param));
  116         290  
171 22         4103 for ($uri->query) {
172 22         289 s/%3A/:/g;
173 22         89 s/%2C/,/g;
174 22         55 s/%7C/|/g; # XXX doesn't work (it ends up encoded anyways)
175 22         66 $uri->query($_);
176             }
177 22         1132 return $uri;
178             }
179              
180             sub _color {
181 19     19   31 local $_ = shift;
182 19   66     144 return $COLOR_ALIAS{$_} ||
183             (/^[\da-fA-F]{3}\z/ ? join("", map "$_$_", split(//, $_)) : $_);
184             }
185              
186             sub _sort_chart_keys {
187 22     22   81 my %o = ( cht => 1, chtm => 2, chs => 3, chd => 100 );
188 22 50 100     71 return sort { ($o{$a}||=99) <=> ($o{$b}||=99) || $a cmp $b } @_;
  198   100     877  
189             }
190              
191             sub _default_minmax {
192 25     25   27 my $param = shift;
193 25         38 my $t = $param->{cht};
194 25 100       65 return 0, undef if $t =~ /^p/; # pie chart
195 22 100       46 return 0, undef if $t eq "v"; # venn
196 21 50       57 return 0, undef if $t =~ /^r/; # radar chart
197 21 100       60 return 0, undef if $t =~ /^b/; # bar chart
198 15 100       31 return 0, 100 if $t eq "gom"; # meter
199 14         29 return;
200             }
201              
202             sub _data {
203 19     19   31 my($data, $param, $opt) = @_;
204              
205             # various shortcuts
206 19         39 $data = _deep_copy($data); # want to modify it
207 19 100       53 if (ref($data) eq "ARRAY") {
    50          
208 18 100       59 $data = [$data] unless ref($data->[0]);
209             }
210             elsif (ref($data) eq "HASH") {
211 0         0 $data = [$data];
212             }
213             else {
214 1         3 $data = [[$data]];
215             }
216              
217 19         83 my $range = _deep_copy($opt->{range});
218 19         49 for (qw(min max range_round range_show)) {
219 76         172 (my $r = $_) =~ s/^range_//;
220 76 100       234 $range->{""}{$r} = $opt->{$_} if exists $opt->{$_};
221             }
222              
223 19         28 my $vcount = 0;
224 19         36 for my $set (@$data) {
225 25 100       90 $set = { v => $set } if ref($set) eq "ARRAY";
226 25         65 my $v = $set->{v};
227 25   100     110 my $r = $set->{range} ||= "";
228 25   100     99 my $rh = $range->{$r} ||= {};
229              
230 25         49 my($min, $max) = _default_minmax($param);
231 25         35 my $i = 0;
232 25         46 for (@$v) {
233 136 100       253 next unless defined;
234 135 100 100     469 $min = $_ if !defined($min) || $_ < $min;
235 135 100 100     449 $max = $_ if !defined($max) || $_ > $max;
236 135 100       338 if ($param->{cht} =~ /^b.s\z/) {
237             # stacked stuff
238 22   50     93 $rh->{stacked}{min}[$i] ||= 0;
239 22   100     70 $rh->{stacked}{max}[$i] ||= 0;
240 22 100       60 $rh->{stacked}{$_ < 0 ? "min" : "max"}[$i] += $_;
241             }
242             }
243             continue {
244 136         191 $i++;
245             }
246 25         45 $vcount += @$v;
247              
248 25 100       59 if ($rh->{stacked}) {
249             # XXX we really only need to this after we have processed
250             # the last dataset, the other rounds it's wasted effort
251 4         6 ($min, $max) = (0, 0);
252 4         8 for (qw(min max)) {
253 8         9 for my $v (@{$rh->{stacked}{$_}}) {
  8         18  
254 44 50       75 next unless defined $v;
255 44 100       67 if ($_ eq "min") {
256 22 100       53 $min = $v if $v < $min;
257             }
258             else {
259 22 100       53 $max = $v if $v > $max;
260             }
261             }
262             }
263             }
264              
265 25 100       51 if (defined $min) {
266 24         66 my %h = (min => $min, max => $max);
267 24         53 for my $k (keys %h) {
268 48 50       99 if (defined $set->{$k}) {
269 0         0 $h{$k} = $set->{$k};
270             }
271             else {
272 48         83 $set->{$k} = $h{$k};
273             }
274              
275 48         62 my $rv = $rh->{$k};
276 48 100 100     181 if (!defined($rv) ||
      66        
      100        
      66        
277             ($k eq "min" && $h{$k} < $rv) ||
278             ($k eq "max" && $h{$k} > $rv)
279             )
280             {
281 40         152 $rh->{$k} = $h{$k};
282             }
283             }
284             }
285             }
286              
287             # should we round any of the ranges
288 19         50 for my $r (values %$range) {
289 20 100       61 next unless $r->{round};
290              
291 2     2   2393 use POSIX qw(floor ceil);
  2         43726  
  2         17  
292 4     4 0 26 sub log10 { log(shift) / log(10) }
293              
294 4         11 my($min, $max) = @$r{"min", "max"};
295 4         6 my $range = $max - $min;
296 4 50       10 next if $range == 0;
297 4 50       34 die "Assert" if $range < 0;
298              
299 4         12 my $step = 10 ** int(log10($range));
300 4 50       14 $step /= 10 if $step / $range >= 0.1;
301 4 100       11 $step *= 5 if $step / $range < 0.05;
302              
303 4         24 $min = floor($min / $step - 0.2) * $step;
304 4         12 $max = ceil($max / $step + 0.2) * $step;
305              
306             # zero based minimum is usually a good thing so make it more likely
307 4 100 66     19 $min = 0 if $min > 0 && $min/$range < 0.4;
308              
309 4         16 @$r{"min", "max"} = ($min, $max);
310             }
311              
312             #use Data::Dump; dd $data;
313             #use Data::Dump; dd $range;
314              
315             # encode data
316 19   66     150 my $e = $ENCODING_ALIAS{$opt->{encoding} || ""} || $opt->{encoding};
317 19 100       39 unless ($e) {
318             # try to me a little smart about selecting a suitable encoding based
319             # on the number of data points we're plotting and the resolution of
320             # the generated image
321 15         98 my @s = ($param->{chs} =~ /(\d+)/g);
322 15         40 my $res = $s[0] * $s[1];
323 15 100 33     43 if ($vcount < 20) {
    50          
324 13         27 $e = "t";
325             }
326             elsif ($vcount > 256 || $res < 300*200) {
327 2         6 $e = "s";
328             }
329             else {
330 0         0 $e = "e";
331             }
332             }
333              
334             my %enc = (
335             t => {
336             null => -1,
337             sep1 => ",",
338             sep2 => "|",
339             fmt => sub {
340 78     78   111 my $v = 100 * shift;
341 78 100       372 $v = sprintf "%.1f", $v if $v ne int($v);
342 78         209 $v;
343             },
344             },
345             s => {
346             null => "_",
347             sep1 => "",
348             sep2 => ",",
349             fmt => sub {
350 54     54   172 return substr($STR_s, $_[0] * length($STR_s) - 0.5, 1);
351             },
352             },
353             e => {
354             null => "__",
355             sep1 => "",
356             sep2 => ",",
357             fmt => sub {
358 3     3   15 return substr($STR_e, int($_[0] * length($STR_e) / 2 - 0.5) * 2, 2);
359             },
360             }
361 19         315 );
362 19   33     71 my $enc = $enc{$e} || croak("unsupported encoding $e");
363 19         22 my @res;
364 19         31 for my $set (@$data) {
365 25         30 my($min, $max) = @{$range->{$set->{range}}}{"min", "max"};
  25         66  
366 25         40 my $v = $set->{v};
367 25         39 for (@$v) {
368 136 100 66     956 if (defined($_) && $_ >= $min && $_ <= $max && $min != $max) {
      66        
      33        
369 135         336 $_ = $enc->{fmt}(($_ - $min) / ($max - $min));
370             }
371             else {
372 1         4 $_ = $enc->{null};
373             }
374             }
375 25         110 push(@res, join($enc->{sep1}, @$v));
376             }
377 19         74 $param->{chd} = "$e:" . join($enc->{sep2}, @res);
378              
379             # handle bar chart zero line if we charted negative data
380 19 100       58 if ($param->{cht} =~ /^b/) {
381 4         6 my($min, $max) = @{$range->{""}}{"min", "max"};
  4         10  
382 4 100       10 if ($min < 0) {
383 2 50       16 $param->{chp} = $max < 0 ? 1 : sprintf "%.2f", -$min / ($max - $min);
384             }
385             }
386              
387             # enable axis labels?
388 19         62 for (sort keys %$range) {
389 20         34 my $r = $range->{$_};
390 20   100     98 my @chxt = split(/,/, $param->{chxt} || "");
391 20   100     76 my @chxr = split(/\|/, $param->{chxr} || "");
392 20 100       55 if (my $rshow = $r->{show}) {
393 7         16 my($min, $max) = @$r{"min", "max"};
394 7         26 for ($min, $max) {
395 14         55 $_ = sprintf "%.2g", $_;
396             }
397 7   33     26 push(@chxt, $AXIS_ALIAS{$rshow} || $rshow);
398 7         10 my $i = $#chxt;
399 7         31 push(@chxr, "$i,$min,$max");
400             }
401 20 100       218 if (@chxt) {
402 7         17 $param->{chxt} = join(",", @chxt);
403 7         106 $param->{chxr} = join("|", @chxr);
404             }
405             }
406             }
407              
408             sub _deep_copy {
409 193     193   222 my $o = shift;
410 193 100       547 return $o unless ref($o);
411 33 100       115 return [map _deep_copy($_), @$o] if ref($o) eq "ARRAY";
412 5 50       20 return {map { $_ => _deep_copy($o->{$_}) } keys %$o} if ref($o) eq "HASH";
  10         19  
413 0           die "Can't copy " . ref($o);
414             }
415              
416             1;
417              
418             __END__