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 # Imager doesn't have a arc boundary function, and the obvious code
39 # either leaves gaps between the circle and the fill, or has some of the
40 # fill outside the outline. These fudge factors produced good results
41 # for the test images <sigh>
42 use constant CIRCLE_FUDGE_X => 0.4;
43 use constant CIRCLE_FUDGE_Y => 0.4;
44 use constant CIRCLE_RADIUS_FUDGE => 0.2;
46 =item $graph->draw(...)
48 Draws a pie graph onto a new image and returns the image.
50 You must at least supply a C<data> parameter and should probably supply a C<labels> parameter.
52 The C<data> parameter should be a reference to an array containing the
53 data the pie graph should present.
55 The C<labels> parameter is a reference to an array of labels,
56 corresponding to the values in C<data>.
62 As described in L<Imager::Graph> you can enable extra features for
63 your graph. The features you can use with pie graphs are:
69 adds a legend to your graph. Requires the labels parameter
73 labels each segment of the graph. If the label doesn't fit inside the
74 segment it is presented as a callout.
78 adds the percentage of the pie to each label.
82 the segments are labels with their percentages only.
86 all labels are presented as callouts
90 the segments are blurred, as a substitute for anti-aliased arcs
94 the pie segments are outlined.
98 the pie is given a drop shadow.
106 # from the Netcraft September 2001 web survey
107 # http://www.netcraft.com/survey/
108 my @data = qw(17874757 8146372 1321544 811406 );
109 my @labels = qw(Apache Microsoft iPlanet Zeus );
111 my $pie = Imager::Graph::Pie->new;
113 First a simple graph, normal size, no labels:
115 my $img = $pie->draw(data=>\@data)
120 # error handling omitted for brevity from now on
121 $img = $pie->draw(data=>\@data, labels=>\@labels, features=>'labels');
123 just percentages in the segments:
125 $img = $pie->draw(data=>\@data, features=>'labelspconly');
127 add a legend as well:
129 $img = $pie->draw(data=>\@data, labels=>\@labels,
130 features=>[ 'labelspconly', 'legend' ]);
132 and a title, but move the legend down, and add a dropshadow:
134 $img = $pie->draw(data=>\@data, labels=>\@labels,
135 title=>'Netcraft Web Survey',
136 legend=>{ valign=>'bottom' },
137 features=>[ qw/labelspconly legend dropshadow/ ]);
139 something a bit prettier:
141 # requires Imager > 0.38
142 $img = $pie->draw(data=>\@data, labels=>\@labels,
143 style=>'fount_lin', features=>'legend');
145 suitable for monochrome output:
147 # requires Imager > 0.38
148 $img = $pie->draw(data=>\@data, labels=>\@labels,
149 style=>'mono', features=>'legend');
153 # this function is too long
155 my ($self, %opts) = @_;
158 or return $self->_error("No data parameter supplied");
159 my @data = @{$opts{data}};
161 @labels = @{$opts{labels}} if $opts{labels};
163 $self->_style_setup(\%opts);
165 my $style = $self->{_style};
167 my $img = $self->_make_img()
171 for my $item (@data) {
175 my @chart_box = ( 0, 0, $img->getwidth-1, $img->getheight-1 );
176 if ($style->{title}{text}) {
177 $self->_draw_title($img, \@chart_box)
181 # consolidate any segments that are too small to display
182 $self->_consolidate_segments(\@data, \@labels, $total);
184 if ($style->{features}{legend} && $opts{labels}) {
185 $self->_draw_legend($img, \@labels, \@chart_box)
189 # the following code is fairly ugly
190 # it attempts to work out a good layout for the components of the chart
194 my @ebox = (0, 0, 0, 0);
195 defined(my $callout_outside = $self->_get_number('callout.outside'))
197 defined(my $callout_leadlen = $self->_get_number('callout.leadlen'))
199 defined(my $callout_gap = $self->_get_number('callout.gap'))
201 defined(my $label_vpad = $self->_get_number('label.vpad'))
203 defined(my $label_hpad = $self->_get_number('label.hpad'))
206 int($self->_small_extent(\@chart_box) * $style->{pie}{guessfactor} * 0.5);
207 for my $data (@data) {
208 my $item = { data=>$data, index=>$index };
209 my $size = 2 * PI * $data / $total;
210 $item->{begin} = $pos;
214 $item->{text} = $labels[$index];
216 if ($style->{features}{labelspconly}) {
218 $style->{label}{pconlyformat}->($data/$total * 100);
221 if ($style->{features}{labelspc}) {
223 $style->{label}{pcformat}->($item->{text}, $data/$total * 100);
226 elsif ($style->{features}{labelspconly}) {
228 $style->{label}{pconlyformat}->($data/$total * 100);
231 elsif ($style->{features}{labels}) {
234 $item->{lbox} = [ $self->_text_bbox($item->{text}, 'label') ];
235 if ($item->{label}) {
236 unless ($self->_fit_text(0, 0, 'label', $item->{text}, $guessradius,
237 $item->{begin}, $item->{end})) {
238 $item->{callout} = 1;
241 $item->{callout} = 1 if $style->{features}{allcallouts};
242 if ($item->{callout}) {
244 $item->{cbox} = [ $self->_text_bbox($item->{text}, 'callout') ];
245 $item->{cangle} = ($item->{begin} + $item->{end}) / 2;
246 my $dist = cos($item->{cangle}) * ($guessradius+
248 my $co_size = $callout_leadlen + $callout_gap + $item->{cbox}[2];
250 $dist -= $co_size - $guessradius;
251 $dist < $ebox[0] and $ebox[0] = $dist;
254 $dist += $co_size - $guessradius;
255 $dist > $ebox[2] and $ebox[2] = $dist;
264 int($self->_small_extent(\@chart_box) * $style->{pie}{size} * 0.5);
265 my $max_width = $chart_box[2] - $chart_box[0] + $ebox[0] - $ebox[2];
266 if ($radius > $max_width / 2) {
267 $radius = $max_width / 2;
269 $chart_box[0] -= $ebox[0];
270 $chart_box[2] -= $ebox[2];
271 my $cx = int(($chart_box[0] + $chart_box[2]) / 2);
272 my $cy = int(($chart_box[1] + $chart_box[3]) / 2);
273 if ($style->{features}{dropshadow}) {
274 my @shadow_fill = $self->_get_fill('dropshadow.fill')
276 my $offx = $self->_get_number('dropshadow.offx')
278 my $offy = $self->_get_number('dropshadow.offy');
279 for my $item (@info) {
280 $img->arc(x=>$cx+$offx, 'y'=>$cy+$offy, r=>$radius+1,
281 d1=>180/PI * $item->{begin}, d2=>180/PI * $item->{end},
284 $self->_filter_region($img,
285 $cx+$offx-$radius-10, $cy+$offy-$radius-10,
286 $cx+$offx+$radius+10, $cy+$offy+$radius+10,
288 if $style->{dropshadow}{filter};
290 my @fill_box = ( $cx-$radius, $cy-$radius, $cx+$radius, $cy+$radius );
291 for my $item (@info) {
292 my @fill = $self->_data_fill($item->{index}, \@fill_box)
294 $img->arc(x=>$cx, 'y'=>$cy, r=>$radius,
295 d1=>180/PI * $item->{begin}, d2=>180/PI * $item->{end},
298 if ($style->{features}{pieblur}) {
299 $self->_pieblur($img, $cx, $cy, $radius);
301 if ($style->{features}{outline}) {
302 my $outcolor = $self->_get_color('outline.line');
303 for my $item (@info) {
304 my $px = int($cx + CIRCLE_FUDGE_X +
305 ($radius+CIRCLE_RADIUS_FUDGE) * cos($item->{begin}));
306 my $py = int($cy + CIRCLE_FUDGE_Y +
307 ($radius+CIRCLE_RADIUS_FUDGE) * sin($item->{begin}));
308 $img->line(x1=>$cx, y1=>$cy, x2=>$px, y2=>$py, color=>$outcolor);
309 for (my $i = $item->{begin}; $i < $item->{end}; $i += PI/180) {
310 my $stroke_end = $i + PI/180;
311 $stroke_end = $item->{end} if $stroke_end > $item->{end};
312 my $nx = int($cx + CIRCLE_FUDGE_X +
313 ($radius+CIRCLE_RADIUS_FUDGE) * cos($stroke_end));
314 my $ny = int($cy + CIRCLE_FUDGE_Y +
315 ($radius+CIRCLE_RADIUS_FUDGE) * sin($stroke_end));
316 $img->line(x1=>$px, y1=>$py, x2=>$nx, y2=>$ny, color=>$outcolor,
318 ($px, $py) = ($nx, $ny);
323 my $callout_inside = $radius - $self->_get_number('callout.inside');
324 $callout_outside += $radius;
325 my %callout_text = $self->_text_style('callout');
326 my %label_text = $self->_text_style('label');
327 for my $label (@info) {
328 if ($label->{label}) {
329 my @loc = $self->_fit_text($cx, $cy, 'label', $label->{text}, $radius,
330 $label->{begin}, $label->{end});
332 my $tcx = ($loc[0]+$loc[2])/2;
333 my $tcy = ($loc[1]+$loc[3])/2;
334 #$img->box(xmin=>$loc[0], ymin=>$loc[1], xmax=>$loc[2], ymax=>$loc[3],
335 # color=>Imager::Color->new(0,0,0));
336 $img->string(%label_text, x=>$tcx-$label->{lbox}[2]/2,
337 'y'=>$tcy+$label->{lbox}[3]/2+$label->{lbox}[1],
338 text=>$label->{text});
341 $label->{callout} = 1;
342 $label->{cbox} = [ $self->_text_bbox($label->{text}, 'callout') ];
343 $label->{cangle} = ($label->{begin} + $label->{end}) / 2;
346 if ($label->{callout}) {
347 my $ix = floor(0.5 + $cx + $callout_inside * cos($label->{cangle}));
348 my $iy = floor(0.5 + $cy + $callout_inside * sin($label->{cangle}));
349 my $ox = floor(0.5 + $cx + $callout_outside * cos($label->{cangle}));
350 my $oy = floor(0.5 + $cy + $callout_outside * sin($label->{cangle}));
351 my $lx = ($ox < $cx) ? $ox - $callout_leadlen : $ox + $callout_leadlen;
352 $img->line(x1=>$ix, y1=>$iy, x2=>$ox, y2=>$oy, antialias=>1,
353 color=>$self->_get_color('callout.color'));
354 $img->line(x1=>$ox, y1=>$oy, x2=>$lx, y2=>$oy, antialias=>1,
355 color=>$self->_get_color('callout.color'));
356 #my $tx = $lx + $callout_gap;
357 my $ty = $oy + $label->{cbox}[3]/2+$label->{cbox}[1];
359 $img->string(%callout_text, x=>$lx-$callout_gap-$label->{cbox}[2],
360 'y'=>$ty, text=>$label->{text});
363 $img->string(%callout_text, x=>$lx+$callout_gap, 'y'=>$ty,
364 text=>$label->{text});
372 =head1 INTERNAL FUNCTIONS
374 These are used in the implementation of Imager::Graph, and are
375 documented for debuggers and developers.
379 =item _consolidate_segments($data, $labels, $total)
381 Consolidate segments that are too small into an 'others' segment.
385 sub _consolidate_segments {
386 my ($self, $data, $labels, $total) = @_;
390 for my $item (@$data) {
391 if ($item / $total < $self->{_style}{pie}{maxsegment}) {
392 push(@others, $index);
398 for my $index (reverse @others) {
399 $others += $data->[$index];
400 splice(@$labels, $index, 1);
401 splice(@$data, $index, 1);
403 push(@$labels, $self->{_style}{otherlabel}) if @$labels;
404 push(@$data, $others);
408 =item _pieblur($img, $cx, $cy, $radius)
410 Blurs the pie as a substitute for anti-aliased segments.
415 my ($self, $img, $cx, $cy, $radius) = @_;
417 my $left = $cx - $radius - 2;
418 $left > 1 or $left = 2;
419 my $right = $cx + $radius + 2;
420 my $top = $cy - $radius - 2;
421 $top > 1 or $top = 2;
422 my $bottom = $cy + $radius + 2;
424 my $filter = $self->_get_thing("pie.blur")
427 # newer versions of Imager let you work on just part of an image
428 if ($img->can('masked') && !$self->{_style}{features}{_debugblur}) {
429 # the mask prevents the blur from leaking over the edges
430 my $mask = Imager->new(xsize=>$right-$left, ysize=>$bottom-$top,
432 $mask->arc(x=>$cx-$left, 'y'=>$cy-$top, r=>$radius);
433 my $masked = $img->masked(mask=>$mask,
434 left=>$left, top=>$top,
435 right=>$right, bottom=>$bottom);
436 $masked->filter(%{$self->{_style}{pie}{blur}});
439 # for older versions of Imager
440 my $subset = $img->crop(left=>$left, top=>$top,
441 right=>$right, bottom=>$bottom);
442 $subset->filter(%{$self->{_style}{pie}{blur}});
443 $img->paste(left=>$left, top=>$top, img=>$subset);
449 my ($x, $y, @l) = @_;
451 my $res = $l[0]*$x + $l[1] * $y + $l[2];
452 print "test ", (abs($res) < 0.000001) ? "success\n" : "failure $res\n";
455 =item _fit_text($cx, $cy, $name, $text, $radius, $begin, $end)
457 Attempts to fit text into a pie segment with its center at ($cx, $cy)
458 with the given radius, covering the angles $begin through $end.
460 Returns a list defining the bounding box of the text if it does fit.
465 my ($self, $cx, $cy, $name, $text, $radius, $begin, $end) = @_;
467 #print "fit: $cx, $cy '$text' $radius $begin $end\n";
468 my @tbox = $self->_text_bbox($text, $name);
469 my $tcx = floor(0.5+$cx + cos(($begin+$end)/2) * $radius *3/5);
470 my $tcy = floor(0.5+$cy + sin(($begin+$end)/2) * $radius *3/5);
471 my $topy = $tcy - $tbox[3]/2;
472 my $boty = $topy + $tbox[3];
474 for my $y ($topy, $boty) {
475 my %entry = ( 'y'=>$y );
476 $entry{line} = [ line_from_points($tcx, $y, $tcx+1, $y) ];
477 $entry{left} = -$radius;
478 $entry{right} = $radius;
479 for my $angle ($begin, $end) {
480 my $ex = $cx + cos($angle)*$radius;
481 my $ey = $cy + sin($angle)*$radius;
482 my @line = line_from_points($cx, $cy, $ex, $ey);
483 #_test_line($cx, $cy, @line);
484 #_test_line($ex, $ey, @line);
485 my $goodsign = $line[0] * $tcx + $line[1] * $tcy + $line[2];
486 for my $pos (@entry{qw/left right/}) {
487 my $sign = $line[0] * ($pos+$tcx) + $line[1] * $y + $line[2];
488 if ($goodsign * $sign < 0) {
489 if (my @p = intersect_lines(@line, @{$entry{line}})) {
490 # die "$goodsign $sign ($pos, $tcx) no intersect (@line) (@{$entry{line}})" ; # this would be wierd
491 #_test_line(@p, @line);
492 #_test_line(@p, @{$entry{line}});
502 my $dist2 = ($pos+$tcx-$cx) * ($pos+$tcx-$cx)
503 + ($y - $cy) * ($y - $cy);
504 if ($dist2 > $radius * $radius) {
506 intersect_line_and_circle(@{$entry{line}}, $cx, $cy, $radius);
508 my @p = splice(@points, 0, 2);
509 if ($p[0] < $cx && $tcx+$pos < $p[0]) {
512 elsif ($p[0] > $cx && $tcx+$pos > $p[0]) {
519 push(@lines, \%entry);
521 my $left = $lines[0]{left} > $lines[1]{left} ? $lines[0]{left} : $lines[1]{left};
522 my $right = $lines[0]{right} < $lines[1]{right} ? $lines[0]{right} : $lines[1]{right};
523 return if $right - $left < $tbox[2];
525 return ($tcx+$left, $topy, $tcx+$right, $boty);
529 ( 'pie', $_[0]->SUPER::_composite() );
535 my %work = %{$self->SUPER::_style_defs()};
536 $work{otherlabel} = "(others)";
537 $work{features}{pieblur} = 1;
542 coef=>[0.05, 0.1, 0.3, 1, 0.3, 0.1, 0.05]
557 Tony Cook <tony@develop-help.com>
561 Imager::Graph(3), Imager(3), perl(1)