1 package Imager::Graph::Pie;
5 Imager::Graph::Pie - a tool for drawing pie charts on Imager images
9 use Imager::Graph::Pie;
11 my $chart = Imager::Graph::Pie->new;
12 # see Imager::Graph for options
13 my $img = $chart->draw(labels=>['first segment', 'second segment'],
14 data=>[ $first_amount, $second_amount ],
15 size=>[$width, $height])
19 Imager::Graph::Pie is intender to make it simple to use L<Imager> to
20 create good looking pie graphs.
22 Most of the basic layout and color selection is handed off to
32 @ISA = qw(Imager::Graph);
33 use Imager::Graph::Util;
36 use constant PI => 3.1415926535;
38 =item $graph->draw(...)
40 Draws a pie graph onto a new image and returns the image.
42 You must at least supply a C<data> parameter and should probably supply a C<labels> parameter.
44 The C<data> parameter should be a reference to an array containing the
45 data the pie graph should present.
47 The C<labels> parameter is a reference to an array of labels,
48 corresponding to the values in C<data>.
54 As described in L<Imager::Graph> you can enable extra features for
55 your graph. The features you can use with pie graphs are:
61 adds a legend to your graph. Requires the labels parameter
65 labels each segment of the graph. If the label doesn't fit inside the
66 segment it is presented as a callout.
70 adds the percentage of the pie to each label.
74 the segments are labels with their percentages only.
78 all labels are presented as callouts
82 the pie segments are outlined.
86 the pie is given a drop shadow.
94 # from the Netcraft September 2001 web survey
95 # http://www.netcraft.com/survey/
96 my @data = qw(17874757 8146372 1321544 811406 );
97 my @labels = qw(Apache Microsoft iPlanet Zeus );
99 my $pie = Imager::Graph::Pie->new;
101 First a simple graph, normal size, no labels:
103 my $img = $pie->draw(data=>\@data)
108 # error handling omitted for brevity from now on
109 $img = $pie->draw(data=>\@data, labels=>\@labels, features=>'labels');
111 just percentages in the segments:
113 $img = $pie->draw(data=>\@data, features=>'labelspconly');
115 add a legend as well:
117 $img = $pie->draw(data=>\@data, labels=>\@labels,
118 features=>[ 'labelspconly', 'legend' ]);
120 and a title, but move the legend down, and add a dropshadow:
122 $img = $pie->draw(data=>\@data, labels=>\@labels,
123 title=>'Netcraft Web Survey',
124 legend=>{ valign=>'bottom' },
125 features=>[ qw/labelspconly legend dropshadow/ ]);
127 something a bit prettier:
129 # requires Imager > 0.38
130 $img = $pie->draw(data=>\@data, labels=>\@labels,
131 style=>'fount_lin', features=>'legend');
133 suitable for monochrome output:
135 # requires Imager > 0.38
136 $img = $pie->draw(data=>\@data, labels=>\@labels,
137 style=>'mono', features=>'legend');
141 # this function is too long
143 my ($self, %opts) = @_;
146 or return $self->_error("No data parameter supplied");
147 my @data = @{$opts{data}};
149 @labels = @{$opts{labels}} if $opts{labels};
151 $self->_style_setup(\%opts);
153 my $style = $self->{_style};
155 my $img = $self->_make_img()
159 for my $item (@data) {
163 my @chart_box = ( 0, 0, $img->getwidth-1, $img->getheight-1 );
164 if ($style->{title}{text}) {
165 $self->_draw_title($img, \@chart_box)
169 # consolidate any segments that are too small to display
170 $self->_consolidate_segments(\@data, \@labels, $total);
172 if ($style->{features}{legend} && $opts{labels}) {
173 $self->_draw_legend($img, \@labels, \@chart_box)
177 # the following code is fairly ugly
178 # it attempts to work out a good layout for the components of the chart
182 my @ebox = (0, 0, 0, 0);
183 defined(my $callout_outside = $self->_get_number('callout.outside'))
185 defined(my $callout_leadlen = $self->_get_number('callout.leadlen'))
187 defined(my $callout_gap = $self->_get_number('callout.gap'))
189 defined(my $label_vpad = $self->_get_number('label.vpad'))
191 defined(my $label_hpad = $self->_get_number('label.hpad'))
194 int($self->_small_extent(\@chart_box) * $style->{pie}{guessfactor} * 0.5);
195 for my $data (@data) {
196 my $item = { data=>$data, index=>$index };
197 my $size = 2 * PI * $data / $total;
198 $item->{begin} = $pos;
202 $item->{text} = $labels[$index];
204 if ($style->{features}{labelspconly}) {
206 $style->{label}{pconlyformat}->($data/$total * 100);
209 if ($style->{features}{labelspc}) {
211 $style->{label}{pcformat}->($item->{text}, $data/$total * 100);
214 elsif ($style->{features}{labelspconly}) {
216 $style->{label}{pconlyformat}->($data/$total * 100);
219 elsif ($style->{features}{labels}) {
222 $item->{lbox} = [ $self->_text_bbox($item->{text}, 'label') ];
223 if ($item->{label}) {
224 unless ($self->_fit_text(0, 0, 'label', $item->{text}, $guessradius,
225 $item->{begin}, $item->{end})) {
226 $item->{callout} = 1;
229 $item->{callout} = 1 if $style->{features}{allcallouts};
230 if ($item->{callout}) {
232 $item->{cbox} = [ $self->_text_bbox($item->{text}, 'callout') ];
233 $item->{cangle} = ($item->{begin} + $item->{end}) / 2;
234 my $dist = cos($item->{cangle}) * ($guessradius+
236 my $co_size = $callout_leadlen + $callout_gap + $item->{cbox}[2];
238 $dist -= $co_size - $guessradius;
239 $dist < $ebox[0] and $ebox[0] = $dist;
242 $dist += $co_size - $guessradius;
243 $dist > $ebox[2] and $ebox[2] = $dist;
252 int($self->_small_extent(\@chart_box) * $style->{pie}{size} * 0.5);
253 my $max_width = $chart_box[2] - $chart_box[0] + $ebox[0] - $ebox[2];
254 if ($radius > $max_width / 2) {
255 $radius = $max_width / 2;
257 $chart_box[0] -= $ebox[0];
258 $chart_box[2] -= $ebox[2];
259 my $cx = int(($chart_box[0] + $chart_box[2]) / 2);
260 my $cy = int(($chart_box[1] + $chart_box[3]) / 2);
261 if ($style->{features}{dropshadow}) {
262 my @shadow_fill = $self->_get_fill('dropshadow.fill')
264 my $offx = $self->_get_number('dropshadow.offx')
266 my $offy = $self->_get_number('dropshadow.offy');
267 for my $item (@info) {
268 $img->arc(x=>$cx+$offx, 'y'=>$cy+$offy, r=>$radius+1, aa => 1,
269 d1=>180/PI * $item->{begin}, d2=>180/PI * $item->{end},
272 $self->_filter_region($img,
273 $cx+$offx-$radius-10, $cy+$offy-$radius-10,
274 $cx+$offx+$radius+10, $cy+$offy+$radius+10,
276 if $style->{dropshadow}{filter};
279 my @fill_box = ( $cx-$radius, $cy-$radius, $cx+$radius, $cy+$radius );
280 for my $item (@info) {
281 $item->{begin} < $item->{end}
283 my @fill = $self->_data_fill($item->{index}, \@fill_box)
285 $img->arc(x=>$cx, 'y'=>$cy, r=>$radius, aa => 1,
286 d1=>180/PI * $item->{begin}, d2=>180/PI * $item->{end},
289 if ($style->{features}{outline}) {
290 my $outcolor = $self->_get_color('outline.line');
291 for my $item (@info) {
292 my $px = int($cx + $radius * cos($item->{begin}));
293 my $py = int($cy + $radius * sin($item->{begin}));
294 $item->{begin} < $item->{end}
296 $img->line(x1=>$cx, y1=>$cy, x2=>$px, y2=>$py, color=>$outcolor);
297 for (my $i = $item->{begin}; $i < $item->{end}; $i += PI/180) {
298 my $stroke_end = $i + PI/180;
299 $stroke_end = $item->{end} if $stroke_end > $item->{end};
300 my $nx = int($cx + $radius * cos($stroke_end));
301 my $ny = int($cy + $radius * sin($stroke_end));
302 $img->line(x1=>$px, y1=>$py, x2=>$nx, y2=>$ny, color=>$outcolor,
304 ($px, $py) = ($nx, $ny);
309 my $callout_inside = $radius - $self->_get_number('callout.inside');
310 $callout_outside += $radius;
311 my %callout_text = $self->_text_style('callout');
312 my %label_text = $self->_text_style('label');
313 for my $label (@info) {
314 if ($label->{label}) {
315 my @loc = $self->_fit_text($cx, $cy, 'label', $label->{text}, $radius,
316 $label->{begin}, $label->{end});
318 my $tcx = ($loc[0]+$loc[2])/2;
319 my $tcy = ($loc[1]+$loc[3])/2;
320 #$img->box(xmin=>$loc[0], ymin=>$loc[1], xmax=>$loc[2], ymax=>$loc[3],
321 # color=>Imager::Color->new(0,0,0));
322 $img->string(%label_text, x=>$tcx-$label->{lbox}[2]/2,
323 'y'=>$tcy+$label->{lbox}[3]/2+$label->{lbox}[1],
324 text=>$label->{text});
327 $label->{callout} = 1;
328 $label->{cbox} = [ $self->_text_bbox($label->{text}, 'callout') ];
329 $label->{cangle} = ($label->{begin} + $label->{end}) / 2;
332 if ($label->{callout}) {
333 my $ix = floor(0.5 + $cx + $callout_inside * cos($label->{cangle}));
334 my $iy = floor(0.5 + $cy + $callout_inside * sin($label->{cangle}));
335 my $ox = floor(0.5 + $cx + $callout_outside * cos($label->{cangle}));
336 my $oy = floor(0.5 + $cy + $callout_outside * sin($label->{cangle}));
337 my $lx = ($ox < $cx) ? $ox - $callout_leadlen : $ox + $callout_leadlen;
338 $img->line(x1=>$ix, y1=>$iy, x2=>$ox, y2=>$oy, antialias=>1,
339 color=>$self->_get_color('callout.color'));
340 $img->line(x1=>$ox, y1=>$oy, x2=>$lx, y2=>$oy, antialias=>1,
341 color=>$self->_get_color('callout.color'));
342 #my $tx = $lx + $callout_gap;
343 my $ty = $oy + $label->{cbox}[3]/2+$label->{cbox}[1];
345 $img->string(%callout_text, x=>$lx-$callout_gap-$label->{cbox}[2],
346 'y'=>$ty, text=>$label->{text});
349 $img->string(%callout_text, x=>$lx+$callout_gap, 'y'=>$ty,
350 text=>$label->{text});
358 =head1 INTERNAL FUNCTIONS
360 These are used in the implementation of Imager::Graph, and are
361 documented for debuggers and developers.
365 =item _consolidate_segments($data, $labels, $total)
367 Consolidate segments that are too small into an 'others' segment.
371 sub _consolidate_segments {
372 my ($self, $data, $labels, $total) = @_;
376 for my $item (@$data) {
377 if ($item / $total < $self->{_style}{pie}{maxsegment}) {
378 push(@others, $index);
384 for my $index (reverse @others) {
385 $others += $data->[$index];
386 splice(@$labels, $index, 1);
387 splice(@$data, $index, 1);
389 push(@$labels, $self->{_style}{otherlabel}) if @$labels;
390 push(@$data, $others);
396 my ($x, $y, @l) = @_;
398 my $res = $l[0]*$x + $l[1] * $y + $l[2];
399 print "test ", (abs($res) < 0.000001) ? "success\n" : "failure $res\n";
402 =item _fit_text($cx, $cy, $name, $text, $radius, $begin, $end)
404 Attempts to fit text into a pie segment with its center at ($cx, $cy)
405 with the given radius, covering the angles $begin through $end.
407 Returns a list defining the bounding box of the text if it does fit.
412 my ($self, $cx, $cy, $name, $text, $radius, $begin, $end) = @_;
414 #print "fit: $cx, $cy '$text' $radius $begin $end\n";
415 my @tbox = $self->_text_bbox($text, $name);
416 my $tcx = floor(0.5+$cx + cos(($begin+$end)/2) * $radius *3/5);
417 my $tcy = floor(0.5+$cy + sin(($begin+$end)/2) * $radius *3/5);
418 my $topy = $tcy - $tbox[3]/2;
419 my $boty = $topy + $tbox[3];
421 for my $y ($topy, $boty) {
422 my %entry = ( 'y'=>$y );
423 $entry{line} = [ line_from_points($tcx, $y, $tcx+1, $y) ];
424 $entry{left} = -$radius;
425 $entry{right} = $radius;
426 for my $angle ($begin, $end) {
427 my $ex = $cx + cos($angle)*$radius;
428 my $ey = $cy + sin($angle)*$radius;
429 my @line = line_from_points($cx, $cy, $ex, $ey);
430 #_test_line($cx, $cy, @line);
431 #_test_line($ex, $ey, @line);
432 my $goodsign = $line[0] * $tcx + $line[1] * $tcy + $line[2];
433 for my $pos (@entry{qw/left right/}) {
434 my $sign = $line[0] * ($pos+$tcx) + $line[1] * $y + $line[2];
435 if ($goodsign * $sign < 0) {
436 if (my @p = intersect_lines(@line, @{$entry{line}})) {
437 # die "$goodsign $sign ($pos, $tcx) no intersect (@line) (@{$entry{line}})" ; # this would be wierd
438 #_test_line(@p, @line);
439 #_test_line(@p, @{$entry{line}});
449 my $dist2 = ($pos+$tcx-$cx) * ($pos+$tcx-$cx)
450 + ($y - $cy) * ($y - $cy);
451 if ($dist2 > $radius * $radius) {
453 intersect_line_and_circle(@{$entry{line}}, $cx, $cy, $radius);
455 my @p = splice(@points, 0, 2);
456 if ($p[0] < $cx && $tcx+$pos < $p[0]) {
459 elsif ($p[0] > $cx && $tcx+$pos > $p[0]) {
466 push(@lines, \%entry);
468 my $left = $lines[0]{left} > $lines[1]{left} ? $lines[0]{left} : $lines[1]{left};
469 my $right = $lines[0]{right} < $lines[1]{right} ? $lines[0]{right} : $lines[1]{right};
470 return if $right - $left < $tbox[2];
472 return ($tcx+$left, $topy, $tcx+$right, $boty);
476 ( 'pie', $_[0]->SUPER::_composite() );
482 my %work = %{$self->SUPER::_style_defs()};
483 $work{otherlabel} = "(others)";
484 $work{features}{pieblur} = 1;
489 coef=>[0.05, 0.1, 0.3, 1, 0.3, 0.1, 0.05]
506 Tony Cook <tony@develop-help.com>
510 Imager::Graph(3), Imager(3), perl(1)