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(
14 data => [ $first_amount, $second_amount ],
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. If you supply a C<labels> parameter, you must supply a C<font> 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.
90 =head1 PIE CHART STYLES
92 The following style values are specific to pie charts:
94 Controlling callouts, the C<callout> option:
100 color - the color of the callout line and the callout text.
104 font, size - font and size of the callout text
108 outside - the distance the radial callout line goes outside the pie
112 leadlen - the length of the horizontal callout line from the end of
117 gap - the distance between the end of the horizontal callout line and
122 inside - the length of the radial callout line within the pie.
126 The outline, line option controls the color of the pie segment
127 outlines, if enabled with the C<outline> feature.
135 maxsegment - any segment below this fraction of the total of the
136 segments will be put into the "others" segment. Default: 0.01
140 The top level C<otherlabel> setting controls the label for the
141 "others" segment, default "(others)".
147 # from the Netcraft September 2001 web survey
148 # http://www.netcraft.com/survey/
149 my @data = qw(17874757 8146372 1321544 811406 );
150 my @labels = qw(Apache Microsoft iPlanet Zeus );
152 my $pie = Imager::Graph::Pie->new;
154 First a simple graph, normal size, no labels:
156 my $img = $pie->draw(data=>\@data)
161 # error handling omitted for brevity from now on
162 $img = $pie->draw(data=>\@data, labels=>\@labels, features=>'labels');
164 just percentages in the segments:
166 $img = $pie->draw(data=>\@data, features=>'labelspconly');
168 add a legend as well:
170 $img = $pie->draw(data=>\@data, labels=>\@labels,
171 features=>[ 'labelspconly', 'legend' ]);
173 and a title, but move the legend down, and add a dropshadow:
175 $img = $pie->draw(data=>\@data, labels=>\@labels,
176 title=>'Netcraft Web Survey',
177 legend=>{ valign=>'bottom' },
178 features=>[ qw/labelspconly legend dropshadow/ ]);
180 something a bit prettier:
182 $img = $pie->draw(data=>\@data, labels=>\@labels,
183 style=>'fount_lin', features=>'legend');
185 suitable for monochrome output:
187 $img = $pie->draw(data=>\@data, labels=>\@labels,
188 style=>'mono', features=>'legend');
192 # this function is too long
194 my ($self, %opts) = @_;
196 my $data_series = $self->_getDataSeries(\%opts);
198 $self->_validInput($data_series)
201 my @data = @{$data_series->[0]->{'data'}};
203 my @labels = @{$self->_getLabels(\%opts) || []};
205 $self->_style_setup(\%opts);
207 my $style = $self->{_style};
209 my $img = $self->_make_img()
212 my @chart_box = ( 0, 0, $img->getwidth-1, $img->getheight-1 );
213 if ($style->{title}{text}) {
214 $self->_draw_title($img, \@chart_box)
219 for my $item (@data) {
223 # consolidate any segments that are too small to display
224 $self->_consolidate_segments(\@data, \@labels, $total);
226 if ($style->{features}{legend} && (scalar @labels)) {
227 $self->_draw_legend($img, \@labels, \@chart_box)
231 # the following code is fairly ugly
232 # it attempts to work out a good layout for the components of the chart
236 my @ebox = (0, 0, 0, 0);
237 defined(my $callout_outside = $self->_get_number('callout.outside'))
239 defined(my $callout_leadlen = $self->_get_number('callout.leadlen'))
241 defined(my $callout_gap = $self->_get_number('callout.gap'))
243 defined(my $label_vpad = $self->_get_number('label.vpad'))
245 defined(my $label_hpad = $self->_get_number('label.hpad'))
248 int($self->_small_extent(\@chart_box) * $style->{pie}{guessfactor} * 0.5);
249 for my $data (@data) {
250 my $item = { data=>$data, index=>$index };
251 my $size = 2 * PI * $data / $total;
252 $item->{begin} = $pos;
255 if (scalar @labels) {
256 $item->{text} = $labels[$index];
258 if ($style->{features}{labelspconly}) {
260 $style->{label}{pconlyformat}->($data/$total * 100);
263 if ($style->{features}{labelspc}) {
265 $style->{label}{pcformat}->($item->{text}, $data/$total * 100);
268 elsif ($style->{features}{labelspconly}) {
270 $style->{label}{pconlyformat}->($data/$total * 100);
273 elsif ($style->{features}{labels}) {
276 $item->{callout} = 1 if $style->{features}{allcallouts};
277 if (!$item->{callout}) {
278 my @lbox = $self->_text_bbox($item->{text}, 'label')
280 $item->{lbox} = \@lbox;
281 if ($item->{label}) {
282 unless ($self->_fit_text(0, 0, 'label', $item->{text}, $guessradius,
283 $item->{begin}, $item->{end})) {
284 $item->{callout} = 1;
288 if ($item->{callout}) {
290 my @cbox = $self->_text_bbox($item->{text}, 'callout')
292 $item->{cbox} = \@cbox;
293 $item->{cangle} = ($item->{begin} + $item->{end}) / 2;
294 my $dist = cos($item->{cangle}) * ($guessradius+
296 my $co_size = $callout_leadlen + $callout_gap + $item->{cbox}[2];
298 $dist -= $co_size - $guessradius;
299 $dist < $ebox[0] and $ebox[0] = $dist;
302 $dist += $co_size - $guessradius;
303 $dist > $ebox[2] and $ebox[2] = $dist;
312 int($self->_small_extent(\@chart_box) * $style->{pie}{size} * 0.5);
313 my $max_width = $chart_box[2] - $chart_box[0] + $ebox[0] - $ebox[2];
314 if ($radius > $max_width / 2) {
315 $radius = int($max_width / 2);
317 $chart_box[0] -= $ebox[0];
318 $chart_box[2] -= $ebox[2];
319 my $cx = int(($chart_box[0] + $chart_box[2]) / 2);
320 my $cy = int(($chart_box[1] + $chart_box[3]) / 2);
321 if ($style->{features}{dropshadow}) {
322 my @shadow_fill = $self->_get_fill('dropshadow.fill')
324 my $offx = $self->_get_number('dropshadow.offx')
326 my $offy = $self->_get_number('dropshadow.offy');
327 for my $item (@info) {
328 $img->arc(x=>$cx+$offx, 'y'=>$cy+$offy, r=>$radius+1, aa => 1,
329 d1=>180/PI * $item->{begin}, d2=>180/PI * $item->{end},
332 $self->_filter_region($img,
333 $cx+$offx-$radius-10, $cy+$offy-$radius-10,
334 $cx+$offx+$radius+10, $cy+$offy+$radius+10,
336 if $style->{dropshadow}{filter};
339 my @fill_box = ( $cx-$radius, $cy-$radius, $cx+$radius, $cy+$radius );
340 for my $item (@info) {
341 $item->{begin} < $item->{end}
343 my @fill = $self->_data_fill($item->{index}, \@fill_box)
345 $img->arc(x=>$cx, 'y'=>$cy, r=>$radius, aa => 1,
346 d1=>180/PI * $item->{begin}, d2=>180/PI * $item->{end},
349 if ($style->{features}{outline}) {
350 my $outcolor = $self->_get_color('outline.line');
351 for my $item (@info) {
352 my $px = int($cx + $radius * cos($item->{begin}));
353 my $py = int($cy + $radius * sin($item->{begin}));
354 $item->{begin} < $item->{end}
356 $img->line(x1=>$cx, y1=>$cy, x2=>$px, y2=>$py, color=>$outcolor);
357 for (my $i = $item->{begin}; $i < $item->{end}; $i += PI/180) {
358 my $stroke_end = $i + PI/180;
359 $stroke_end = $item->{end} if $stroke_end > $item->{end};
360 my $nx = int($cx + $radius * cos($stroke_end));
361 my $ny = int($cy + $radius * sin($stroke_end));
362 $img->line(x1=>$px, y1=>$py, x2=>$nx, y2=>$ny, color=>$outcolor,
364 ($px, $py) = ($nx, $ny);
369 my $callout_inside = $radius - $self->_get_number('callout.inside');
370 $callout_outside += $radius;
373 for my $label (@info) {
374 if ($label->{label} && !$label->{callout}) {
375 # at this point we know we need the label font, to calculate
376 # whether the label will fit if anything else
377 unless (%label_text) {
378 %label_text = $self->_text_style('label')
381 my @loc = $self->_fit_text($cx, $cy, 'label', $label->{text}, $radius,
382 $label->{begin}, $label->{end});
384 my $tcx = ($loc[0]+$loc[2])/2;
385 my $tcy = ($loc[1]+$loc[3])/2;
386 #$img->box(xmin=>$loc[0], ymin=>$loc[1], xmax=>$loc[2], ymax=>$loc[3],
387 # color=>Imager::Color->new(0,0,0));
388 $img->string(%label_text, x=>$tcx-$label->{lbox}[2]/2,
389 'y'=>$tcy+$label->{lbox}[3]/2+$label->{lbox}[1],
390 text=>$label->{text});
393 $label->{callout} = 1;
394 my @cbox = $self->_text_bbox($label->{text}, 'callout')
396 $label->{cbox} = \@cbox;
397 $label->{cangle} = ($label->{begin} + $label->{end}) / 2;
400 if ($label->{callout}) {
401 unless (%callout_text) {
402 %callout_text = $self->_text_style('callout')
405 my $ix = floor(0.5 + $cx + $callout_inside * cos($label->{cangle}));
406 my $iy = floor(0.5 + $cy + $callout_inside * sin($label->{cangle}));
407 my $ox = floor(0.5 + $cx + $callout_outside * cos($label->{cangle}));
408 my $oy = floor(0.5 + $cy + $callout_outside * sin($label->{cangle}));
409 my $lx = ($ox < $cx) ? $ox - $callout_leadlen : $ox + $callout_leadlen;
410 $img->line(x1=>$ix, y1=>$iy, x2=>$ox, y2=>$oy, antialias=>1,
411 color=>$self->_get_color('callout.color'));
412 $img->line(x1=>$ox, y1=>$oy, x2=>$lx, y2=>$oy, antialias=>1,
413 color=>$self->_get_color('callout.color'));
414 #my $tx = $lx + $callout_gap;
415 my $ty = $oy + $label->{cbox}[3]/2+$label->{cbox}[1];
417 $img->string(%callout_text, x=>$lx-$callout_gap-$label->{cbox}[2],
418 'y'=>$ty, text=>$label->{text});
421 $img->string(%callout_text, x=>$lx+$callout_gap, 'y'=>$ty,
422 text=>$label->{text});
431 my ($self, $data_series) = @_;
433 if (!defined $data_series || !scalar @$data_series) {
434 return $self->_error("No data supplied");
438 or return $self->_error("Pie charts only allow one data series");
440 my $data = $data_series->[0]{data};
442 if (!scalar @$data) {
443 return $self->_error("No values in data series");
449 for my $item (@$data) {
451 and return $self->_error("Data index $index is less than zero");
459 and return $self->_error("Sum of all data values is zero");
464 =head1 INTERNAL FUNCTIONS
466 These are used in the implementation of Imager::Graph, and are
467 documented for debuggers and developers.
471 =item _consolidate_segments($data, $labels, $total)
473 Consolidate segments that are too small into an 'others' segment.
477 sub _consolidate_segments {
478 my ($self, $data, $labels, $total) = @_;
482 for my $item (@$data) {
483 if ($item / $total < $self->{_style}{pie}{maxsegment}) {
484 push(@others, $index);
490 for my $index (reverse @others) {
491 $others += $data->[$index];
492 splice(@$labels, $index, 1);
493 splice(@$data, $index, 1);
495 push(@$labels, $self->{_style}{otherlabel}) if @$labels;
496 push(@$data, $others);
502 my ($x, $y, @l) = @_;
504 my $res = $l[0]*$x + $l[1] * $y + $l[2];
505 print "test ", (abs($res) < 0.000001) ? "success\n" : "failure $res\n";
508 =item _fit_text($cx, $cy, $name, $text, $radius, $begin, $end)
510 Attempts to fit text into a pie segment with its center at ($cx, $cy)
511 with the given radius, covering the angles $begin through $end.
513 Returns a list defining the bounding box of the text if it does fit.
518 my ($self, $cx, $cy, $name, $text, $radius, $begin, $end) = @_;
520 #print "fit: $cx, $cy '$text' $radius $begin $end\n";
521 my @tbox = $self->_text_bbox($text, $name)
523 my $tcx = floor(0.5+$cx + cos(($begin+$end)/2) * $radius *3/5);
524 my $tcy = floor(0.5+$cy + sin(($begin+$end)/2) * $radius *3/5);
525 my $topy = $tcy - $tbox[3]/2;
526 my $boty = $topy + $tbox[3];
528 for my $y ($topy, $boty) {
529 my %entry = ( 'y'=>$y );
530 $entry{line} = [ line_from_points($tcx, $y, $tcx+1, $y) ];
531 $entry{left} = -$radius;
532 $entry{right} = $radius;
533 for my $angle ($begin, $end) {
534 my $ex = $cx + cos($angle)*$radius;
535 my $ey = $cy + sin($angle)*$radius;
536 my @line = line_from_points($cx, $cy, $ex, $ey);
537 #_test_line($cx, $cy, @line);
538 #_test_line($ex, $ey, @line);
539 my $goodsign = $line[0] * $tcx + $line[1] * $tcy + $line[2];
540 for my $pos (@entry{qw/left right/}) {
541 my $sign = $line[0] * ($pos+$tcx) + $line[1] * $y + $line[2];
542 if ($goodsign * $sign < 0) {
543 if (my @p = intersect_lines(@line, @{$entry{line}})) {
544 # die "$goodsign $sign ($pos, $tcx) no intersect (@line) (@{$entry{line}})" ; # this would be wierd
545 #_test_line(@p, @line);
546 #_test_line(@p, @{$entry{line}});
556 my $dist2 = ($pos+$tcx-$cx) * ($pos+$tcx-$cx)
557 + ($y - $cy) * ($y - $cy);
558 if ($dist2 > $radius * $radius) {
560 intersect_line_and_circle(@{$entry{line}}, $cx, $cy, $radius);
562 my @p = splice(@points, 0, 2);
563 if ($p[0] < $cx && $tcx+$pos < $p[0]) {
566 elsif ($p[0] > $cx && $tcx+$pos > $p[0]) {
573 push(@lines, \%entry);
575 my $left = $lines[0]{left} > $lines[1]{left} ? $lines[0]{left} : $lines[1]{left};
576 my $right = $lines[0]{right} < $lines[1]{right} ? $lines[0]{right} : $lines[1]{right};
577 return if $right - $left < $tbox[2];
579 return ($tcx+$left, $topy, $tcx+$right, $boty);
583 ( 'pie', $_[0]->SUPER::_composite() );
589 my %work = %{$self->SUPER::_style_defs()};
590 $work{otherlabel} = "(others)";
608 Tony Cook <tony@develop-help.com>
612 Imager::Graph(3), Imager(3), perl(1)