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

? sunnavy sunnavy at bestpractical.com
Sat Feb 3 13:51:35 EST 2018


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

- Log -----------------------------------------------------------------
commit 7b87228f2aff4d6a65cb561f572d6f318f221da9
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..aaed98732 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,44 @@ 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() {
+    // "GroupBy-Groups" could be triggered because the they are fully cloned from "GroupBy"
+    if ( jQuery(this).attr('name') == 'GroupBy-Groups' ) {
+        var elem = jQuery(this).next('select[name=GroupBy]');
+        setTimeout( function () {
+            elem.change();
+        }, 100 ); // give it a moment to prepare "GroupBy" options
+    }
+    else {
+        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..776aa027b 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" <% $Stacked ? 'checked="checked"' : '' |n %>/>
+</span>
+
 <%init>
 use RT::Report::Tickets;
 my $report = RT::Report::Tickets->new( $session{'CurrentUser'} );

commit 642cae88551c5ad54bc8190e212032eceb119d41
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 d7386abb95803eb8e7d0ac7df0f813e33eba05e8
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