root/veekun/trunk/lib/Vee/Utils/SVG.pm

Revision 387, 8.7 KB (checked in by latiass, 2 years ago)

Jesus Christ. Fixed a bug with SVG.pm that would cause the graph to be empty when 1 field had all of the data allocated to it. (part of #207, might be all SVG.pm needed)

Line 
1package Vee::Utils::SVG;
2
3use strict;
4use warnings;
5use constant PI => 3.14159265359;
6use List::Util qw/sum/;
7use Exporter 'import';
8our @EXPORT = qw/piechart histogram/;
9
10=head1 NAME
11
12Vee::Controller::Utils::SVG - Catalyst Controller
13
14=head1 DESCRIPTION
15
16Catalyst Controller.
17
18=head1 METHODS
19
20=cut
21
22=head2 piechart
23
24THIS IS NOT A METHOD.
25
26Returns an SVG pie chart, given a list of fields and a list of data points.
27Eventually I'd like to split this off into its own module.
28
29=cut
30
31my $default_svg_pie_css = <<END_CSS;
32circle#pie {
33    stroke: black;
34    stroke-width: 2px;
35    fill: white;
36}
37
38path.slice {
39    fill-opacity: 0.67;
40    fill: white;
41    stroke: black;
42    stroke-width: 1px;
43}
44
45rect.legend-frame {
46    fill: white;
47    fill-opacity: 0.0;
48    stroke: transparent;
49    stroke-width: 1px;
50}
51
52text.legend-label {
53    text-anchor: start;
54    dominant-baseline: middle;
55}
56
57.field:hover .slice {
58    fill-opacity: 0.33;
59}
60.field:hover .legend-frame {
61    fill-opacity: 0.75;
62    stroke: #c0c0c0;
63}
64
65#field1  .slice, #field1  .legend-box { fill: #ff0000; }
66#field2  .slice, #field2  .legend-box { fill: #ff8000; }
67#field3  .slice, #field3  .legend-box { fill: #ffff00; }
68#field4  .slice, #field4  .legend-box { fill: #80ff00; }
69#field5  .slice, #field5  .legend-box { fill: #00ff00; }
70#field6  .slice, #field6  .legend-box { fill: #00ff80; }
71#field7  .slice, #field7  .legend-box { fill: #00ffff; }
72#field8  .slice, #field8  .legend-box { fill: #0080ff; }
73#field9  .slice, #field9  .legend-box { fill: #0000ff; }
74#field10 .slice, #field10 .legend-box { fill: #8000ff; }
75#field11 .slice, #field11 .legend-box { fill: #ff00ff; }
76#field12 .slice, #field12 .legend-box { fill: #ff0080; }
77END_CSS
78
79sub piechart {
80    my ($fields, $data, $opts) = @_;
81    my @fields = @$fields;
82    my @data   = @$data;
83   
84    my $total = sum @data;
85   
86    # some default values that should be overridden!
87    my $scale = $opts->{scale} || 1;
88    my $pie_radius = 100 * $scale;
89    my $pie_padding = 10;
90   
91    # calculated now for convenience
92    my $pie_diameter = $pie_radius * 2;
93    my $width = $pie_diameter + $pie_padding * 2;
94    my $height = $pie_diameter + $pie_padding * 2;
95    $width *= 2;
96    # scaling makes things dumb
97    $width *= 2 if ($scale < 1);
98    my ($center_x, $center_y) = ($pie_radius + $pie_padding) x 2;
99   
100    my $svg = qq[<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">\n];
101    $svg   .= qq[<svg width="$width" height="$height" viewBox="0 0 $width $height" xmlns="http://www.w3.org/2000/svg">\n];
102    $svg   .= qq[<style type="text/css">\n];
103    $svg   .= $default_svg_pie_css;
104    $svg   .= qq[</style>\n];
105   
106    # if there are no fields and/or data, this won't work!
107    if (!@fields || !@data || $total < 1) {
108        $svg   .= qq[<g class="field">];
109        $svg   .= qq[ <text x="18" y="12" class="legend-label">There is no data to display.</text>\n</g>\n</svg>];
110        return $svg;
111    }
112   
113    my $legend_left = $pie_diameter + $pie_padding * 3;
114    my $legend_row_height = $pie_diameter / @fields;
115    my $legend_box_size = $legend_row_height * 2 / 3;
116
117    $svg   .= qq[<circle cx="$center_x" cy="$center_y" r="$pie_radius" id="pie"/>\n];
118
119    my $angle = 0;
120    my $last_x = sprintf '%.3f', $center_x + $pie_radius * cos $angle;
121    my $last_y = sprintf '%.3f', $center_y + $pie_radius * sin $angle;
122    for my $i (0 .. $#data) {
123        my $field = $fields[$i];
124        my $datum = $data[$i];
125        my $ratio = $datum / $total;
126       
127        $angle += $ratio * 2 * PI;
128        $angle /=2 if ($ratio == 1); #if angle is 360, make it 180 (see hack below)
129        my $to_x = sprintf '%.3f', $center_x + $pie_radius * cos $angle;
130        my $to_y = sprintf '%.3f', $center_y + $pie_radius * sin $angle;
131       
132        $svg .= qq[<g class="field" id="field@{[ $i + 1 ]}" title="@{[ $datum . sprintf(" (%.0f%%)", ($ratio * 100)) ]}">\n];
133       
134        # slice itself
135        $svg .= qq[    <path d="];
136        $svg .= qq[ M$last_x $last_y ];
137        # kind of hacky: if the angle is 360, draw 180 degrees of the circle, then draw the other 180 degrees
138        # breaks in firefox otherwise
139        if ($ratio == 1) {
140            $svg .= qq[ A$pie_radius $pie_radius 0 1,1 $to_x $to_y ];
141            $svg .= qq[ A$pie_radius $pie_radius 0 1,1 $last_x $last_y ];
142        } else {
143            $svg .= qq[ A$pie_radius $pie_radius 0 @{[ $ratio > 0.5 ? 1 : 0 ]},1 $to_x $to_y ];
144        }
145        $svg .= qq[ L$center_x $center_y ] unless ($ratio == 0 || $ratio == 1);
146        $svg .= qq[ Z ];
147        $svg .= qq[" class="slice"/>\n];
148       
149        # legend
150        my $legend_row_y   = $pie_padding + $legend_row_height * $i;
151        my $legend_box_y   = $legend_row_y + $legend_row_height * 0.1667;   # top of box
152        my $legend_box_x   = $legend_left  + $legend_row_height * 0.1667;   # left of box
153        my $legend_label_x = $legend_left + $legend_row_height;
154        my $legend_label_y = $legend_row_y + $legend_row_height * 0.5;      # middle of text
155        $svg .= qq[    <rect x="$legend_left" y="$legend_row_y" width="@{[ $width - $legend_left ]}" height="$legend_row_height" class="legend-frame"/>\n];
156        $svg .= qq[    <rect x="$legend_box_x" y="$legend_box_y" width="$legend_box_size" height="$legend_box_size" rx="2" class="legend-box"/>\n];
157        $svg .= qq[    <text x="$legend_label_x" y="$legend_label_y" class="legend-label">$field ];
158        $svg .= qq[</text>\n];
159
160        $svg .= qq[</g>\n];
161
162        $last_x = $to_x;
163        $last_y = $to_y;
164    }
165
166    $svg .= qq[ </svg> ];
167
168    return $svg;
169}
170
171=head2 histogram
172
173THIS IS NOT A METHOD.
174
175Returns an SVG histogram, given a list of data points.
176Eventually I'd like to split this off into its own module.
177
178=cut
179
180my $default_svg_histogram_css = <<END_CSS;
181circle#pie {
182    stroke: black;
183    stroke-width: 2px;
184    fill: white;
185}
186
187path.slice {
188    fill-opacity: 0.7;
189    fill: white;
190    stroke: black;
191    stroke-width: 1px;
192}
193
194#field1  .slice { fill: #ff0000; }
195#field2  .slice { fill: #ff8000; }
196#field3  .slice { fill: #ffff00; }
197#field4  .slice { fill: #80ff00; }
198#field5  .slice { fill: #00ff00; }
199#field6  .slice { fill: #00ff80; }
200#field7  .slice { fill: #00ffff; }
201#field8  .slice { fill: #0080ff; }
202#field9  .slice { fill: #0000ff; }
203#field10 .slice { fill: #8000ff; }
204#field11 .slice { fill: #ff00ff; }
205#field12 .slice { fill: #ff0080; }
206END_CSS
207
208sub histogram {
209    my ($data, %opts) = @_;
210    my @data   = @$data;
211    my @fields;
212
213    my $total = sum @data;
214
215    # some default values that should be overridden!
216    my $pie_radius = 100;
217    my $pie_padding = 10;
218    my $pie_diameter = $pie_radius * 2;
219    my $width  = $pie_diameter + $pie_padding * 2;
220    my $height = $pie_diameter + $pie_padding * 2;
221
222    my ($center_x, $center_y) = ($pie_radius + $pie_padding) x 2;
223
224    my $svg = qq[<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">\n];
225    $svg   .= qq[<svg width="$width" height="$height" viewBox="0 0 $width $height" xmlns="http://www.w3.org/2000/svg">\n];
226    $svg   .= qq[<style type="text/css">\n];
227    $svg   .= $default_svg_pie_css;
228    $svg   .= qq[</style>\n];
229    $svg   .= qq[<circle cx="$center_x" cy="$center_y" r="$pie_radius" id="pie"/>\n];
230
231    my $angle = 0;
232    my $last_x = sprintf '%.3f', $center_x + $pie_radius * cos $angle;
233    my $last_y = sprintf '%.3f', $center_y + $pie_radius * sin $angle;
234    for my $i (0 .. $#data) {
235        my $field = $fields[$i];
236        my $datum = $data[$i];
237        my $ratio = $datum / $total;
238
239        $angle += $ratio * 2 * PI;
240        $angle /=2 if ($ratio == 1); #if angle is 360, make it 180 (see hack below)
241        my $to_x = sprintf '%.3f', $center_x + $pie_radius * cos $angle;
242        my $to_y = sprintf '%.3f', $center_y + $pie_radius * sin $angle;
243
244        $svg .= qq[<g class="field" id="field@{[ $i + 1 ]}">\n];
245           
246        $svg .= qq[    <path d="];
247        $svg .= qq[ M$last_x $last_y ];
248        # kind of hacky: if the angle is 360, draw 180 degrees of the circle, then draw the other 180 degrees
249        # breaks in firefox otherwise
250        if ($ratio == 1) {
251            $svg .= qq[ A$pie_radius $pie_radius 0 1,1 $to_x $to_y ];
252            $svg .= qq[ A$pie_radius $pie_radius 0 1,1 $last_x $last_y ];
253        } else {
254            $svg .= qq[ A$pie_radius $pie_radius 0 @{[ $ratio > 0.5 ? 1 : 0 ]},1 $to_x $to_y ];
255        }
256        $svg .= qq[ L$center_x $center_y ];
257        $svg .= qq[ Z ];
258        $svg .= qq[" class="slice"/>\n];
259        $svg .= qq[</g>\n];
260
261        $last_x = $to_x;
262        $last_y = $to_y;
263    }
264
265    $svg .= qq[ </svg> ];
266
267    return $svg;
268}
269
270=head1 AUTHOR
271
272Maintainer: Alex "Eevee" Munroe (C<veekun@veekun.com>)
273
274See the included F<AUTHORS> file for a full list of contributers.
275
276=head1 LICENSE
277
278See the included F<LICENSE> file.
279
280=cut
281
2821;
Note: See TracBrowser for help on using the browser.