[Rt-commit] rt branch, 4.4/stacked-bar-chart, created. rt-4.4.2-67-gc6e41c53b
? sunnavy
sunnavy at bestpractical.com
Wed Feb 7 14:24:45 EST 2018
The branch, 4.4/stacked-bar-chart has been created
at c6e41c53bce35acb1b91779cd6b277c40e8c142a (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 c6e41c53bce35acb1b91779cd6b277c40e8c142a
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..878571ce2 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -533,6 +533,32 @@ $chart->{dclrs} = [ RT->Config->Get("ChartColors") ];
my $color_hex = $self->{dclrs}[ $_[0] % @{ $self->{dclrs} } - 1 ];
return map { hex } ( $color_hex =~ /(..)(..)(..)/ );
};
+
+ if ( $chart_options{cumulate} ) {
+ # 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.
+
+ no strict 'refs';
+ my @warning_subs = ();
+ for my $pkg ( 'GD::Graph::Data::', 'GD::Graph::axestype::' ) {
+ 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] =~ /isn't numeric/ };
+ $orig->( @_ );
+ };
+ }
+ }
+ $RT::HandledGDGraphNumericWarnings = 1;
+ }
+ }
}
if (my $plot = eval { $chart->plot( \@data ) }) {
-----------------------------------------------------------------------
More information about the rt-commit
mailing list