[Rt-commit] rt branch, 4.4/stacked-bar-chart, created. rt-4.4.2-67-g5ae593a4c

? sunnavy sunnavy at bestpractical.com
Sat Jan 27 06:39:15 EST 2018


The branch, 4.4/stacked-bar-chart has been created
        at  5ae593a4cfb08166a705ea07e7a6c03b3075725d (commit)

- Log -----------------------------------------------------------------
commit 1c64006267fad049125e702901c3f29d84bdf67f
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Jan 25 22:19:21 2018 +0800

    basic stacked bar chart support
    
    for now, only count chart function is supported.

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index f51898b0f..34034ecb2 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -49,6 +49,7 @@
 $Cache => undef
 $Query => "id > 0"
 @GroupBy => ()
+$StackedGroupBy => undef
 $ChartStyle => 'bar+table+sql'
 @ChartFunction => 'COUNT'
 $Width  => undef
@@ -171,6 +172,47 @@ my $chart = $chart_class->new( $Width => $Height );
 
 my %chart_options;
 if ($chart_class eq "GD::Graph::bars") {
+
+    # $ChartStyle could be pie even if $chart_class is ::bars
+    if ( $StackedGroupBy && $ChartStyle =~ /\bbar\b/ ) {
+        if ( scalar @data > 2 ) {
+            RT->Logger->warning( "Invalid stack option: it can't apply to multiple data rows" );
+        }
+        else {
+
+            my $labels = $data[0];
+
+            # find the stacked group index
+            require List::MoreUtils;
+            my $stacked_index = List::MoreUtils::first_index { $_ eq $StackedGroupBy } @GroupBy;
+            if ( $stacked_index >= 0 ) {
+                $chart_options{cumulate} = 1;
+                my @new_labels;
+                my %rows;
+                my $i = 0;
+
+                for my $label ( @$labels ) {
+                    my @new_label = @$label;
+                    splice @new_label, $stacked_index, 1; # remove the stacked group
+                    my $key = join ';;;', @new_label;
+                    push @new_labels, \@new_label unless $rows{$key};
+                    push @{$rows{$key}}, $data[1][$i] . ' ' . $label->[$stacked_index];
+                    $i++;
+                }
+
+                @data = \@new_labels;
+
+                my $ea = List::MoreUtils::each_arrayref( map { $rows{join ';;;', @$_} } @new_labels );
+                while ( my ( @list ) = $ea->() ) {
+                    push @data, [ map { $_ || '' } @list ];
+                }
+            }
+            else {
+                RT->Logger->warning("Invalid StackedGroupBy: $StackedGroupBy");
+            }
+        }
+    }
+
     my $count = @{ $data[0] };
     $chart_options{'bar_spacing'} =
         $count > 30 ? 1
@@ -341,7 +383,14 @@ if ($chart_class eq "GD::Graph::bars") {
     }
 
     # try to fit in values above bars
-    {
+    if ( $chart_options{cumulate} ) {
+        $chart_options{'show_values'}             = 1;
+        $chart_options{'values_vertical'}         = 0;
+        $chart_options{'values_space'}            = -25;
+        $chart_options{'values_font'}             = [ $font, 12 ];
+        $chart_options{'hide_overlapping_values'} = 1;
+    }
+    else {
         # 0.8 is guess, labels for ticks on Y axis can be wider
         # 1.5 for paddings around bars that GD::Graph adds
         my $x_space_for_label = $Width*0.8/($count*(@data - 1)+1.5);
@@ -388,15 +437,16 @@ if ($chart_class eq "GD::Graph::bars") {
         );
         $chart_options{'show_values'} = 1;
         $chart_options{'hide_overlapping_values'} = 1;
+
         if ( $found_solution ) {
             $chart_options{'values_font'} = [ $font, $found_solution ],
-            $chart_options{'values_space'} = 2;
-            $chart_options{'values_vertical'} =
+            $chart_options{'values_space'} ||= 2;
+            $chart_options{'values_vertical'} //=
                 $can{'horizontal, one line'} ? 0 : 1;
         } else {
             $chart_options{'values_font'} = [ $font, 9 ],
-            $chart_options{'values_space'} = 1;
-            $chart_options{'values_vertical'} = 1;
+            $chart_options{'values_space'} ||= 1;
+            $chart_options{'values_vertical'} //= 1;
         }
     }
 
diff --git a/share/html/Search/Chart.html b/share/html/Search/Chart.html
index 1a6768a51..e5889d52c 100644
--- a/share/html/Search/Chart.html
+++ b/share/html/Search/Chart.html
@@ -57,7 +57,7 @@ $m->callback( ARGSRef => \%ARGS, CallbackName => 'Initial' );
 
 my $title = loc( "Grouped search results");
 
-my @search_fields = qw(Query GroupBy ChartStyle ChartFunction Width Height);
+my @search_fields = qw(Query GroupBy StackedGroupBy ChartStyle ChartFunction Width Height);
 my $saved_search = $m->comp( '/Widgets/SavedSearch:new',
     SearchType   => 'Chart',
     SearchFields => [@search_fields],
@@ -143,6 +143,7 @@ $m->callback( ARGSRef => \%ARGS, QueryArgsRef => \%query );
     Name => 'GroupBy',
     Query => $query{Query},
     Default => $query{'GroupBy'}[0],
+    Stacked => $query{'GroupBy'}[0] eq ($query{StackedGroupBy} // '') ? 1 : 0,
     &>
 </fieldset>
 <fieldset><legend><% loc('and then') %></legend>
@@ -151,6 +152,7 @@ $m->callback( ARGSRef => \%ARGS, QueryArgsRef => \%query );
     Query => $query{Query},
     Default => $query{'GroupBy'}[1] // q{},
     ShowEmpty => 1,
+    Stacked => $query{'GroupBy'}[1] && ($query{'GroupBy'}[1] eq $query{StackedGroupBy} // '') ? 1 : 0,
     &>
 </fieldset>
 <fieldset><legend><% loc('and then') %></legend>
@@ -159,6 +161,7 @@ $m->callback( ARGSRef => \%ARGS, QueryArgsRef => \%query );
     Query => $query{Query},
     Default => $query{'GroupBy'}[2] // q{},
     ShowEmpty => 1,
+    Stacked => $query{'GroupBy'}[2] && ($query{'GroupBy'}[2] eq ($query{StackedGroupBy}) // '') ? 1 : 0,
     &>
 </fieldset>
 </&>
@@ -197,6 +200,13 @@ $m->callback( ARGSRef => \%ARGS, QueryArgsRef => \%query );
 <script type="text/javascript">
 var updateChartStyle = function() {
     var val = jQuery(".chart-picture [name=ChartType]").val();
+    if ( val == 'bar' ) {
+        jQuery("span.stacked-group").removeClass('hidden');
+    }
+    else {
+        jQuery("span.stacked-group").addClass('hidden');
+    }
+
     if ( val != 'table' && jQuery(".chart-picture [name=ChartStyleIncludeTable]").is(':checked') ) {
         val += '+table';
     }
@@ -215,6 +225,35 @@ jQuery(".chart-picture [name=ChartType]").change(function(){
 
 jQuery(".chart-picture [name=ChartStyleIncludeTable]").change( updateChartStyle );
 jQuery(".chart-picture [name=ChartStyleIncludeSQL]").change( updateChartStyle );
+
+jQuery("input.stacked-group-checkbox").change( function() {
+    if ( jQuery(this).is(':checked') ) {
+        jQuery("input.stacked-group-checkbox").not(this).prop('checked', false);
+    }
+});
+
+jQuery("select[name=GroupBy]").change( function() {
+    jQuery(this).closest('fieldset').find('input.stacked-group-checkbox').val(jQuery(this).val());
+});
+
+jQuery("select[name=ChartFunction]:first").change( function() {
+    if ( jQuery(this).val() && jQuery(this).val().match(/count/i) ) {
+        jQuery("input.stacked-group-checkbox").prop('disabled', false);
+    }
+    else {
+        jQuery("input.stacked-group-checkbox").prop('disabled', true).prop('checked', false);
+    }
+});
+
+jQuery("select[name=ChartFunction]:not(:first)").change( function() {
+    if ( jQuery(this).val() ) {
+        jQuery("input.stacked-group-checkbox").prop('disabled', true).prop('checked', false);
+    }
+    else {
+        jQuery("select[name=ChartFunction]:first").change();
+    }
+});
+
 </script>
 
 <& /Elements/Submit, Label => loc('Update Chart'), Name => 'Update' &>
diff --git a/share/html/Search/Elements/SelectGroupBy b/share/html/Search/Elements/SelectGroupBy
index f9dd9aa51..dd1eb349a 100644
--- a/share/html/Search/Elements/SelectGroupBy
+++ b/share/html/Search/Elements/SelectGroupBy
@@ -50,6 +50,7 @@ $Name => 'GroupBy'
 $Default => 'Status'
 $Query   => ''
 $ShowEmpty => 0
+$Stacked => 0
 </%args>
 <select name="<% $Name %>" class="cascade-by-optgroup">
 % if ( $ShowEmpty ) {
@@ -74,6 +75,11 @@ while ( my ($label, $value) = splice @options, 0, 2 ) {
   </optgroup>
 % }
 </select>
+
+<span class="stacked-group">
+     <% loc('Stacked?') %> <input name="Stacked<% $Name %>" type="checkbox" class="stacked-group-checkbox" value="<% $Default %>" <% $Stacked ? 'checked="checked"' : '' |n %>/>
+</span>
+
 <%init>
 use RT::Report::Tickets;
 my $report = RT::Report::Tickets->new( $session{'CurrentUser'} );

commit 955f950a2f39ff0356211db7e1bd1bebfc41201d
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Jan 26 01:16:39 2018 +0800

    increase chart width/height for better stacked bars rendering

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index 34034ecb2..6db049e2d 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -172,6 +172,15 @@ my $chart = $chart_class->new( $Width => $Height );
 
 my %chart_options;
 if ($chart_class eq "GD::Graph::bars") {
+    my $text_size = sub {
+        my ($size, $text) = (@_);
+        my $font_handle = GD::Text::Align->new(
+            $chart->get('graph'), valign => 'top', 'halign' => 'center',
+        );
+        $font_handle->set_font($font, $size);
+        $font_handle->set_text($text);
+        return $font_handle;
+    };
 
     # $ChartStyle could be pie even if $chart_class is ::bars
     if ( $StackedGroupBy && $ChartStyle =~ /\bbar\b/ ) {
@@ -200,6 +209,32 @@ if ($chart_class eq "GD::Graph::bars") {
                     $i++;
                 }
 
+                # increase $Width and $Height if necessary
+                require List::Util;
+                my ( $max_sum, $min_value, $max_width );
+                for my $vertical_values ( map { $rows{join ';;;', @$_} } @new_labels ) {
+                    my $sum = List::Util::sum( map { defined $_ && /^(\d+)/ ? $1 : () } @$vertical_values );
+                    my $min_v = List::Util::min( map { defined $_ && /^(\d+)/ ? $1 : () } @$vertical_values );
+                    my $max_w = List::Util::max( map { defined $_ ? $text_size->(12, $_)->get('width') : () } @$vertical_values );
+                    $max_sum = $sum if !$max_sum || $max_sum < $sum;
+                    $min_value = $min_v if !$min_value || ( $min_v > 0 && $min_value > $min_v );
+                    $max_width = $max_w if !$max_width || $max_width < $max_w;
+                }
+
+                $chart_options{y_max_value} = int( $max_sum * 1.1 );
+                $chart_options{y_max_value} += 5 - $chart_options{y_max_value} % 5;
+                if ( $min_value ) {
+                    my $pixels = $min_value * $Height / $chart_options{y_max_value};
+                    if ( $pixels < 30 ) {
+                        $Height = int( $Height * 30 / $pixels );
+                    }
+                    $Height = 200 if $Height < 200;
+                }
+
+                my $value_width = ( $max_width + 25 ) * scalar @new_labels;
+                $Width = $value_width * 2 if $Width < $value_width * 2;
+                $Width = 200 if $Width < 200;
+
                 @data = \@new_labels;
 
                 my $ea = List::MoreUtils::each_arrayref( map { $rows{join ';;;', @$_} } @new_labels );
@@ -248,15 +283,6 @@ if ($chart_class eq "GD::Graph::bars") {
         $chart_options{'y_label_skip'} = 2;
         $chart_options{'y_tick_number'} = 10;
     }
-    my $text_size = sub {
-        my ($size, $text) = (@_);
-        my $font_handle = GD::Text::Align->new(
-            $chart->get('graph'), valign => 'top', 'halign' => 'center',
-        );
-        $font_handle->set_font($font, $size);
-        $font_handle->set_text($text);
-        return $font_handle;
-    };
 
     my $fitter = sub {
         my %args = @_;
@@ -459,7 +485,7 @@ if ($chart_class eq "GD::Graph::bars") {
 # use a top margin enough to display values over the top line if needed
         t_margin => 18,
 # the following line to make sure there's enough space for values to show
-        y_max_value => $max_value,
+        ( $chart_options{y_max_value} || 0 ) < $max_value ? ( y_max_value => $max_value ) : (),
         y_min_value => $min_value,
 # if there're too many bars or at least one key is too long, use vertical
         bargroup_spacing => $chart_options{'bar_spacing'}*5,

commit 5ae593a4cfb08166a705ea07e7a6c03b3075725d
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Jan 26 01:26:40 2018 +0800

    silence "isn't numeric" warnings in GD::Graph for stacked bar charts
    
    we want to show extra info such as status or queue names in bar values,
    but values are supposed to be numeric. though strings like "2 open"
    could be automatically interpreted to numbers, "isn't numeric" warnings
    will be emitted too. it's ok to silence them since it's expected.

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index 6db049e2d..79846cb48 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -533,6 +533,23 @@ $chart->{dclrs} = [ RT->Config->Get("ChartColors") ];
         my $color_hex = $self->{dclrs}[ $_[0] % @{ $self->{dclrs} } - 1 ];
         return map { hex } ( $color_hex =~ /(..)(..)(..)/ );
     };
+
+    if ( $chart_options{cumulate} ) {
+        my @warning_subs = (
+            'GD::Graph::Data::cumulate',
+            'GD::Graph::Data::get_y_cumulative',
+            'GD::Graph::axestype::val_to_pixel',
+            'GD::Graph::axestype::_correct_y_min_max',
+        );
+        no strict 'refs';
+        for my $warning_sub ( @warning_subs ) {
+            my $orig = \&$warning_sub;
+            *$warning_sub = sub {
+                local $SIG{__WARN__} = sub { warn $_[0] unless $_[0] =~ /isn't numeric/ };
+                $orig->( @_ );
+            };
+        }
+    }
 }
 
 if (my $plot = eval { $chart->plot( \@data ) }) {

-----------------------------------------------------------------------


More information about the rt-commit mailing list