[Rt-commit] rt branch, 4.4/stacked-bar-chart, created. rt-4.4.4-204-g6d44d7e7ab

? sunnavy sunnavy at bestpractical.com
Fri Jan 22 11:09:03 EST 2021


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

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

    Add 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 e5cb765bbe..f9ee53cc98 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 44fb74a473..e4e04ea001 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,15 @@ $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');
+        jQuery("input.stacked-group-checkbox").prop('disabled', false);
+    }
+    else {
+        jQuery("span.stacked-group").addClass('hidden');
+        jQuery("input.stacked-group-checkbox").prop('disabled', true);
+    }
+
     if ( val != 'table' && jQuery(".chart-picture [name=ChartStyleIncludeTable]").is(':checked') ) {
         val += '+table';
     }
@@ -215,6 +227,50 @@ 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( function() {
+    jQuery("select[name=ChartFunction-Groups]").change( function() {
+        var allow_stacked = jQuery(".chart-picture [name=ChartType]").val() == 'bar';
+
+        var value_count;
+        jQuery("select[name=ChartFunction-Groups]").each( function() {
+            if ( jQuery(this).val() ) {
+                value_count++;
+                if ( value_count > 1 || !jQuery(this).val().match(/count/i) ) {
+                    allow_stacked = 0;
+                }
+            }
+        } );
+
+        if ( allow_stacked ) {
+            jQuery("span.stacked-group").removeClass('hidden');
+            jQuery("input.stacked-group-checkbox").prop('disabled', false);
+        }
+        else {
+            jQuery("span.stacked-group").addClass('hidden');
+            jQuery("input.stacked-group-checkbox").prop('disabled', true);
+        }
+    }).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 95b5ee1fb8..0e2de5cf20 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 938968cc9f63d64047552ded541c9fad5ab55bf7
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 f9ee53cc98..555e51baf6 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 6d44d7e7ab939ed88c49b5bcdc3e5f5131967dcb
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Jan 26 01:26:40 2018 +0800

    Silence "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, perl warns "isn't
    numeric" in this case.
    
    The other related warning is "Use of uninitialized value $_ in numeric",
    which happens if there are absent stacks(i.e. 0 tickets in some groups).

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index 555e51baf6..41002bd07c 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -533,6 +533,29 @@ $chart->{dclrs} = [ RT->Config->Get("ChartColors") ];
         my $color_hex = $self->{dclrs}[ $_[0] % @{ $self->{dclrs} } - 1 ];
         return map { hex } ( $color_hex =~ /(..)(..)(..)/ );
     };
+
+    if ( $chart_options{cumulate} ) {
+        # Avoid "numeric" warnings caused by labels like "2 open" or absent
+        # stacks.
+        no strict 'refs';
+        my @warning_subs = ();
+        for my $pkg ( 'GD::Graph::Data::', 'GD::Graph::axestype::', 'GD::Graph::bars' ) {
+            push @warning_subs, map { $pkg . $_ } grep { /^[a-z_]+$/ } keys %$pkg;
+        }
+
+        if ( !$RT::HandledGDGraphNumericWarnings ) {
+            for my $warning_sub ( @warning_subs ) {
+                my ( $package, $sub ) = $warning_sub =~ /(.+)::(\w+)/;
+                if ( my $orig = $package->can($sub) ) {
+                    *$warning_sub = sub {
+                        local $SIG{__WARN__} = sub { warn $_[0] unless $_[0] =~ /numeric/ };
+                        $orig->( @_ );
+                    };
+                }
+            }
+            $RT::HandledGDGraphNumericWarnings = 1;
+        }
+    }
 }
 
 if (my $plot = eval { $chart->plot( \@data ) }) {

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


More information about the rt-commit mailing list