1 %# BEGIN BPS TAGGED BLOCK {{{
5 %# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
6 %# <sales@bestpractical.com>
8 %# (Except where explicitly superseded by other copyright notices)
13 %# This work is made available to you under the terms of Version 2 of
14 %# the GNU General Public License. A copy of that license should have
15 %# been provided with this software, but in any event can be snarfed
18 %# This work is distributed in the hope that it will be useful, but
19 %# WITHOUT ANY WARRANTY; without even the implied warranty of
20 %# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 %# General Public License for more details.
23 %# You should have received a copy of the GNU General Public License
24 %# along with this program; if not, write to the Free Software
25 %# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 %# 02110-1301 or visit their web page on the internet at
27 %# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 %# CONTRIBUTION SUBMISSION POLICY:
32 %# (The following paragraph is not intended to limit the rights granted
33 %# to you to modify and distribute this software under the terms of
34 %# the GNU General Public License and is only of importance to you if
35 %# you choose to contribute your changes and enhancements to the
36 %# community by submitting them to Best Practical Solutions, LLC.)
38 %# By intentionally submitting any modifications, corrections or
39 %# derivatives to this work, or any other work intended for use with
40 %# Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 %# you are the copyright holder for those contributions and you grant
42 %# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 %# royalty-free, perpetual, license to use, copy, create derivative
44 %# works based on those contributions, and sublicense and distribute
45 %# those contributions and any derivatives thereof.
47 %# END BPS TAGGED BLOCK }}}
52 $ChartStyle => 'bar+table+sql'
53 @ChartFunction => 'COUNT'
61 my %font_config = RT->Config->Get('ChartFont');
62 my $font = $font_config{ $session{CurrentUser}->UserObj->Lang || '' }
63 || $font_config{'others'};
65 s/\D//g for grep defined, $Width, $Height;
67 $Height ||= ($ChartStyle =~ /\bpie\b/ ? $Width : 400);
68 $Height = $Width if $ChartStyle =~ /\bpie\b/;
70 my $plot_error = sub {
74 my $create_plot = sub {
75 my ($width, $height) = @_;
77 my $plot = GD::Image->new($width => $height);
78 $plot->colorAllocate(255, 255, 255); # background
79 my $black = $plot->colorAllocate(0, 0, 0);
81 require GD::Text::Wrap;
82 my $error = GD::Text::Wrap->new($plot,
89 $error->set_font( $font, 16 );
90 return ($plot, $error);
93 ($plot, $error) = $create_plot->($Width, $Height);
94 my $text_height = ($error->get_bounds(0, 0))[3];
96 # GD requires us to replot it all with the new height
97 ($plot, $error) = $create_plot->($Width, $text_height + 20);
100 $m->comp( 'SELF:Plot', plot => $plot, %ARGS );
103 use RT::Report::Tickets;
104 my $report = RT::Report::Tickets->new( $session{'CurrentUser'} );
107 if ( $Cache and my $data = delete $session{'charts_cache'}{ $Cache } ) {
108 %columns = %{ $data->{'columns'} };
109 $report->Deserialize( $data->{'report'} );
112 %columns = $report->SetupGroupings(
114 GroupBy => \@GroupBy,
115 Function => \@ChartFunction,
118 $report->SortEntries;
124 my $max_key_length = 0;
125 while ( my $entry = $report->Next ) {
126 push @{ $data[0] }, [ map $entry->LabelValue( $_ ), @{ $columns{'Groups'} } ];
129 foreach my $column ( @{ $columns{'Functions'} } ) {
130 my $v = $entry->RawValue( $column );
136 my @subs = $report->FindImplementationCode(
137 $report->ColumnInfo( $column )->{'META'}{'SubValues'}
139 push @values, map $v->{$_}, @subs;
143 push @{ $data[++$i] }, $_ foreach @values;
145 foreach my $v ( @values ) {
146 $max_value = $v if $max_value < $v;
147 $min_value = $v if !defined $min_value || $min_value > $v;
151 $ChartStyle =~ s/\bpie\b/bar/ if @data > 2;
154 if ($ChartStyle =~ /\bpie\b/) {
155 require GD::Graph::pie;
156 $chart_class = "GD::Graph::pie";
158 require GD::Graph::bars;
159 $chart_class = "GD::Graph::bars";
162 # Pie charts don't like having no input, so we show a special image
163 # that indicates an error message. Because this is used in an <img>
164 # context, it can't be a simple error message. Without this check,
165 # the chart will just be a non-loading image.
166 unless ( $report->Count ) {
167 return $plot_error->(loc("No tickets found."));
170 my $chart = $chart_class->new( $Width => $Height );
173 if ($chart_class eq "GD::Graph::bars") {
174 my $count = @{ $data[0] };
175 $chart_options{'bar_spacing'} =
181 if ( my $code = $report->LabelValueCode( $columns{'Functions'}[0] ) ) {
182 my %info = %{ $report->ColumnInfo( $columns{'Functions'}[0] ) };
183 $chart_options{'values_format'} = $chart_options{'y_number_format'} = sub {
184 return $code->($report, %info, VALUE => shift );
187 $report->GotoFirstItem;
189 # normalize min/max values to graph boundaries
192 $integer = 0 for grep $_ ne int $_, $min_value, $max_value;
194 $max_value *= $max_value > 0 ? 1.1 : 0.9
196 $min_value *= $min_value > 0 ? 0.9 : 1.1
200 $max_value = int($max_value + ($max_value > 0? 1 : 0) );
201 $min_value = int($min_value + ($min_value < 0? -1 : 0) );
203 my $span = abs($max_value - $min_value);
204 $max_value += 5 - ($span % 5);
206 $chart_options{'y_label_skip'} = 2;
207 $chart_options{'y_tick_number'} = 10;
209 my $text_size = sub {
210 my ($size, $text) = (@_);
211 my $font_handle = GD::Text::Align->new(
212 $chart->get('graph'), valign => 'top', 'halign' => 'center',
214 $font_handle->set_font($font, $size);
215 $font_handle->set_text($text);
222 foreach my $font_size ( @{$args{'sizes'}} ) {
223 my $line_height = $text_size->($font_size, 'Q')->get('height');
225 my $keyset_height = $line_height;
226 if ( ref $args{data}->[0] ) {
227 $keyset_height = $text_size->($font_size, join "\n", ('Q')x scalar @{ $args{data}->[0] })
232 foreach my $e ( @{ $args{data} } ) {
233 $status = $args{'cb'}->(
236 line_height => $line_height,
237 keyset_height => $keyset_height,
248 # try to fit in labels on X axis values, aka key
250 # we have several labels layouts:
251 # 1) horizontal, one line per label
252 # 2) horizontal, multi-line - doesn't work, GD::Chart bug
253 # 3) vertical, one line
254 # 4) vertical, multi-line
256 'horizontal, one line' => 1,
257 'vertical, one line' => 1,
258 'vertical, multi line' => @{$data[0][0]} > 1,
261 my $x_space_for_label = $Width*0.8/($count+1.5);
262 my $y_space_for_label = $Height*0.4;
264 my $found_solution = $fitter->(
270 # if horizontal space doesn't allow us to fit one vertical line,
271 # then we need smaller font
272 return 0 if $args{'line_height'} > $x_space_for_label;
274 my $width = $text_size->( $args{'size'}, join ' - ', @{ $args{'element'} } )
277 if ( $width > $x_space_for_label ) {
278 $can{'horizontal, one line'} = 0;
280 if ( $width > $y_space_for_label ) {
281 $can{'vertical, one line'} = 0;
283 if ( $args{'keyset_height'} >= $x_space_for_label ) {
284 $can{'vertical, multi line'} = 0;
286 if ( $can{'vertical, multi line'} ) {
287 my $width = $text_size->( $args{'size'}, join "\n", @{ $args{'element'} } )
289 if ( $width > $y_space_for_label ) {
290 $can{'vertical, multi line'} = 0;
293 return 0 unless grep $_, values %can;
297 if ( $found_solution ) {
298 $chart_options{'x_axis_font'} = [$font, $found_solution];
300 if ( $can{'horizontal, one line'} ) {
301 $chart_options{'x_labels_vertical'} = 0;
302 $_ = join ' - ', @$_ foreach @{$data[0]};
304 elsif ( $can{'vertical, multi line'} ) {
305 $chart_options{'x_labels_vertical'} = 1;
306 $_ = join "\n", @$_ foreach @{$data[0]};
309 $chart_options{'x_labels_vertical'} = 1;
310 $_ = join " - ", @$_ foreach @{$data[0]};
314 my $font_handle = $text_size->(10, 'Q');
315 my $line_height = $font_handle->get('height');
316 if ( $line_height > $x_space_for_label ) {
317 $Width *= $line_height/$x_space_for_label;
318 $Width = int( $Width+1 );
321 $_ = join " - ", @$_ foreach @{$data[0]};
323 my $max_text_width = 0;
324 foreach (@{$data[0]}) {
325 $font_handle->set_text($_);
326 my $width = $font_handle->get('width');
327 $max_text_width = $width if $width > $max_text_width;
329 if ( $max_text_width > $Height*0.4 ) {
330 $Height = int($max_text_width / 0.4 + 1);
333 $chart_options{'x_labels_vertical'} = 1;
334 $chart_options{'x_axis_font'} = [$font, 10];
338 # use the same size for y axis labels
340 $chart_options{'y_axis_font'} = $chart_options{'x_axis_font'};
343 # try to fit in values above bars
345 # 0.8 is guess, labels for ticks on Y axis can be wider
346 # 1.5 for paddings around bars that GD::Graph adds
347 my $x_space_for_label = $Width*0.8/($count*(@data - 1)+1.5);
350 'horizontal, one line' => 1,
351 'vertical, one line' => 1,
355 my $found_solution = $fitter->(
356 sizes => [ grep $_ <= $chart_options{'x_axis_font'}[1], 12, 11, 10, 9 ],
357 data => [ map {@$_} @data[1..(@data-1)] ],
361 # if horizontal space doesn't allow us to fit one vertical line,
362 # then we need smaller font
363 return 0 if $args{'line_height'} > $x_space_for_label;
365 my $value = $args{'element'};
366 $value = $chart_options{'values_format'}->($value)
367 if $chart_options{'values_format'};
368 return 1 if $seen{$value}++;
370 my $width = $text_size->( $args{'size'}, $value )->get('width');
371 if ( $width > $x_space_for_label ) {
372 $can{'horizontal, one line'} = 0;
374 my $y_space_for_label = $Height * 0.6
375 *( 1 - ($args{'element'}-$min_value)/($max_value-$min_value) );
376 if ( $width > $y_space_for_label ) {
377 $can{'vertical, one line'} = 0;
379 return 0 unless grep $_, values %can;
383 $chart_options{'show_values'} = 1;
384 $chart_options{'hide_overlapping_values'} = 1;
385 if ( $found_solution ) {
386 $chart_options{'values_font'} = [ $font, $found_solution ],
387 $chart_options{'values_space'} = 2;
388 $chart_options{'values_vertical'} =
389 $can{'horizontal, one line'} ? 0 : 1;
391 $chart_options{'values_font'} = [ $font, 9 ],
392 $chart_options{'values_space'} = 1;
393 $chart_options{'values_vertical'} = 1;
399 x_label => join( ' - ', map $report->Label( $_ ), @{ $columns{'Groups'} } ),
400 x_label_position => 0.6,
401 y_label => $report->Label( $columns{'Functions'}[0] ),
402 y_label_position => 0.6,
403 # use a top margin enough to display values over the top line if needed
405 # the following line to make sure there's enough space for values to show
406 y_max_value => $max_value,
407 y_min_value => $min_value,
408 # if there're too many bars or at least one key is too long, use vertical
409 bargroup_spacing => $chart_options{'bar_spacing'}*5,
414 while ( my $entry = $report->Next ) {
415 push @{ $data[0][$i++] }, $entry->LabelValue( $columns{'Functions'}[0] );
417 $_ = join ' - ', @$_ foreach @{$data[0]};
420 if ($chart->get('width') != $Width || $chart->get('height') != $Height ) {
421 $chart = $chart_class->new( $Width => $Height );
426 title_font => [ $font, 16 ],
427 legend_font => [ $font, 16 ],
428 x_label_font => [ $font, 14 ],
429 y_label_font => [ $font, 14 ],
430 label_font => [ $font, 14 ],
431 y_axis_font => [ $font, 12 ],
432 values_font => [ $font, 12 ],
433 value_font => [ $font, 12 ],
437 foreach my $opt ( grep /_font$/, keys %chart_options ) {
438 my $v = delete $chart_options{$opt};
439 next unless my $can = $chart->can("set_$opt");
443 $chart->set(%chart_options) if keys %chart_options;
445 $chart->{dclrs} = [ RT->Config->Get("ChartColors") ];
448 no warnings 'redefine';
449 *GD::Graph::pick_data_clr = sub {
451 my $color_hex = $self->{dclrs}[ $_[0] % @{ $self->{dclrs} } - 1 ];
452 return map { hex } ( $color_hex =~ /(..)(..)(..)/ );
456 if (my $plot = eval { $chart->plot( \@data ) }) {
457 $m->comp( 'SELF:Plot', plot => $plot, %ARGS );
459 my $error = join "\n", grep defined && length, $chart->error, $@;
460 $plot_error->(loc("Error plotting chart: [_1]", $error));
469 my @types = ('png', 'gif');
470 for my $type (@types) {
474 $r->content_type("image/$type");
475 $m->out( $plot->$type );
479 die "Your GD library appears to support none of the following image types: " . join(', ', @types);