package SGX::Graph; use strict; use warnings; use base qw/SGX::Strategy::Base/; use List::Util qw/max min/; use SGX::Util qw/car label_format/; #=== CLASS METHOD ============================================================ # CLASS: Graph # METHOD: default_head # PARAMETERS: ???? # RETURNS: ???? # DESCRIPTION: # THROWS: no exceptions # COMMENTS: none # SEE ALSO: n/a #=============================================================================== sub default_head { my $self = shift; my ( $dbh, $q, $s ) = @$self{qw/_dbh _cgi _UserSession/}; my $reporter = car $q->param('rid'); my $transform = car $q->param('trans'); my $curr_proj = car $q->param('proj'); return unless defined $reporter; $self->{_reporter_name} = $q->param('reporter'); my $sql_trans; my $sql_cutoff; my $y_start; my $ytitle_text; my $middle_label; if ( $transform eq 'Log Ratio' ) { $sql_trans = 'if(foldchange>0, log2(foldchange), log2(-1/foldchange))'; $sql_cutoff = 'log2(def_f_cutoff)'; $ytitle_text = 'Log2 of Normalized Ratio'; $y_start = 0; $middle_label = '0'; } else { # default: $transform eq 'fold' $sql_trans = 'if(foldchange>0,foldchange-1,foldchange+1)'; $sql_cutoff = '(def_f_cutoff-1)'; $ytitle_text = 'Fold Change (Normalized Ratio)'; $y_start = 1; $middle_label = '±1'; } $self->{_meta} = [ $ytitle_text, $y_start, $middle_label ]; #################################################################### my $sql_join_clause = ''; my $sql_where_clause = ''; my @exec_array_title = ($reporter); # check if proj parameter is numeric if ( defined($curr_proj) and $curr_proj =~ m/^\d+$/ ) { $sql_join_clause = 'INNER JOIN study USING(pid) INNER JOIN ProjectStudy USING(stid)'; $sql_where_clause = 'AND prid=?'; push @exec_array_title, $curr_proj; } # Get the sequence name for the title my $sql1 = <<"END_SQL1"; SELECT group_concat(if(gtype=1, gsymbol, NULL) separator ', ') AS 'Gene Symb.', $sql_cutoff AS cutoff, def_p_cutoff AS cutoff_p FROM probe INNER JOIN platform USING(pid) $sql_join_clause LEFT JOIN (ProbeGene NATURAL JOIN gene) USING(rid) WHERE rid=? $sql_where_clause GROUP BY probe.rid END_SQL1 my $sth1 = $dbh->prepare($sql1); my $rowcount = $sth1->execute(@exec_array_title); $self->{_scc} = $sth1->fetchrow_arrayref; $sth1->finish; #################################################################### # Get the data my $xtitle_text = 'Experiment'; my $sql_project_clause = 'WHERE response.rid=?'; my @exec_array = ($reporter); if ( defined($curr_proj) and $curr_proj =~ m/^\d+$/ ) { $sql_project_clause = 'NATURAL JOIN ProjectStudy WHERE response.rid=? AND ProjectStudy.prid=?'; push @exec_array, $curr_proj; } my $sql2 = <<"END_SQL2"; SELECT experiment.eid, CONCAT(GROUP_CONCAT(study.description SEPARATOR ', '), ': ', experiment.sample2, '/', experiment.sample1) AS label, $sql_trans as y, pvalue1 FROM response NATURAL JOIN experiment NATURAL JOIN StudyExperiment NATURAL JOIN study $sql_project_clause GROUP BY experiment.eid ORDER BY experiment.eid ASC END_SQL2 my $sth2 = $dbh->prepare($sql2); $rowcount = $sth2->execute(@exec_array); return if $rowcount < 1; my @exp_ids; my @labels; my @y; my @pvalues; while ( my $row = $sth2->fetchrow_arrayref ) { push @exp_ids, $row->[0]; push @labels, $row->[1]; push @y, $row->[2]; push @pvalues, $row->[3]; } $self->{_data} = [ \@exp_ids, \@labels, \@y, \@pvalues ]; $sth2->finish; # this is a hack (temporary until we put content wrapping into # Strategy::Base): call body to send data to the client but do not do it # normal way (do not return true value). normally we would just return 1 # and let the default_body() be called by the main controller. $s->commit(); print $q->header( -type => 'image/svg+xml', -cookie => $s->cookie_array() ), $self->default_body(); exit; # do not show body } #=== CLASS METHOD ============================================================ # CLASS: Graph # METHOD: default_body # PARAMETERS: ???? # RETURNS: ???? # DESCRIPTION: # THROWS: no exceptions # COMMENTS: none # SEE ALSO: n/a #=============================================================================== sub default_body { my $self = shift; my $reporter_name = $self->{_reporter_name}; my ( $exp_ids, $labels, $y, $pvalues ) = @{ $self->{_data} }; my ( $ytitle_text, $y_start, $middle_label ) = @{ $self->{_meta} }; my ( $seqname, $cutoff, $cutoff_p ) = @{ $self->{_scc} }; $seqname = '' if not defined $seqname; my $title_text = "$seqname Differential Expression: Probe $reporter_name"; #Set particulars for graph my $xl = 55; my $yl = 24; my $body_width = 500; my $body_height = 300; my $body_height_extended = 400; my $longest_xlabel = 7 * max( map { length($_) } @$labels ); my $text_breath = 6; # pixels my $text_fudge = 4; # fudge factor my $text_fudge_inv = 10; # inverse fudge factor #my $total_width = $xl + $body_width my $total_width = $xl + $body_width + $longest_xlabel; my $label_shift = $yl + $body_height + $text_breath + $text_fudge; # space between bars is wider than the bars by golden ratio my $golden_ratio = 1.61803399; my $bar_width = $body_width / ( @$y * ( 1 + $golden_ratio ) + $golden_ratio ); my $min_data = min( grep { defined } @$y ) || 0; my $max_data = max( grep { defined } @$y ) || 0; $max_data = $cutoff if $max_data < $cutoff; $min_data = -$cutoff if $min_data > -$cutoff; my $spread = $max_data - $min_data; my $body_prop = 0.9; my $scale = $body_prop * $body_height / $spread; my $wspace = ( 1 - $body_prop ) / 2 * $body_height; my $yupper = $wspace + $max_data * $scale; my $ylower = $body_height - $yupper; my $xaxisy = $yupper + $yl; my $left_pos = $xl - $text_breath; my $xlabels = ''; my $legend = ''; my $vguides = ''; my $datapoints = ''; my $rw = $golden_ratio * $bar_width; my $wrw = $bar_width + $rw; my $left_off = $xl + $rw; my $vguides_shift = $left_off + $bar_width / 2; my $text_left = $vguides_shift + $text_fudge; # legend my $text_height = $text_fudge_inv + 2; my $legend_left = $xl + $body_width + $text_fudge_inv; my $legend_top = $yl + $text_fudge_inv; for ( my $i = 0 ; $i < @$y ; $i++ ) { my ( $yvalue, $lab_class, $leg_class ); if ( defined $y->[$i] ) { $lab_class = 'xAxisLabel'; $leg_class = 'legendLabel'; $yvalue = $y->[$i]; } else { $lab_class = 'xAxisLabel xNALabel'; $leg_class = 'legendLabel legendNALabel'; $yvalue = 0; } my $top = $xaxisy - $scale * $yvalue; #$xlabels .= "".$labels[$i]."\n"; $xlabels .= "" . $exp_ids->[$i] . "\n"; $legend .= "" . $exp_ids->[$i] . '. ' . $labels->[$i] . "\n"; $vguides .= "\n"; my $fill_class = ( defined $pvalues->[$i] ) ? ( $pvalues->[$i] < $cutoff_p ? 'fill2' : 'fill1' ) : 'fill3'; $datapoints .= "\n"; $text_left += $wrw; $left_off += $wrw; $legend_top += $text_height; $vguides_shift += $wrw; } my $total_height = max( $label_shift, $legend_top ) + 50; my $hguides = ''; my $ylabels = ''; # Make sure we have at least around 4 labels. Round to one significant # figure which then becomes either 1, 2, 5, or 10 my $num_sep = label_format( $spread / 4 ); my $split = 0; if ( $body_height / ( $scale * $num_sep ) < 6 ) { # make sure we have at least around 6 gridlines $num_sep /= 2; $split = 1; } my $ysep = $scale * $num_sep; #--------------------------------------------------------------------------- for ( my ( $put_label, $offset, $ylabel ) = ( 0, $ysep, $y_start + $num_sep ) ; $offset <= $yupper ; $offset += $ysep, $ylabel += $num_sep ) { my $real_offset = $xaxisy - $offset; my $text_offset = $real_offset + $text_fudge; if ($split) { if ($put_label) { $ylabels .= "$ylabel\n"; $put_label = 0; # skip next time } else { $put_label = 1; # do it next time } } else { $ylabels .= "$ylabel\n"; } $hguides .= "\n"; } #--------------------------------------------------------------------------- for ( my ( $put_label, $offset, $ylabel ) = ( 0, $ysep, -$y_start - $num_sep ) ; $offset <= $ylower ; $offset += $ysep, $ylabel -= $num_sep ) { my $real_offset = $xaxisy + $offset; my $text_offset = $real_offset + $text_fudge; if ($split) { if ($put_label) { $ylabels .= "$ylabel\n"; $put_label = 0; # skip next time } else { $put_label = 1; # do it next time } } else { $ylabels .= "$ylabel\n"; } $hguides .= "\n"; } #--------------------------------------------------------------------------- $cutoff = $scale * $cutoff; $hguides .= "\n" if $cutoff <= $yupper; $hguides .= "\n" if $cutoff <= $ylower; my $text_offset = $xaxisy + $text_fudge; $ylabels .= "$middle_label\n"; my $titlex = $xl; my $titley = $yl / 2; my $ytitlex = $text_fudge_inv; # + $text_breath; my $ytitley = $yl + $body_height / 2; #--------------------------------------------------------------------------- # Including CSS into SVG so that one could save and use the SVG as a # stand-alone file. # # not drawing the Y-axis anymore -- if needed, the SVG code to show it was: # # also not showing label for the X-axis. The SVG code was: # $xtitle_text # and the Perl code to find xy coords was: # my $xtitlex = $xl + $body_width / 2; # my $xtitley = $total_height - $yl / 3; #--------------------------------------------------------------------------- return <<"END_SVG"; $xlabels $ylabels $legend $vguides $hguides $datapoints $ytitle_text $title_text END_SVG } 1; __END__ #=============================================================================== # # FILE: Graph.pm # # DESCRIPTION: # # FILES: --- # BUGS: --- # NOTES: --- # AUTHOR: Eugene Scherba (es), escherba@gmail.com # COMPANY: Boston University # VERSION: 1.0 # CREATED: 10/13/2011 14:57:32 # REVISION: --- #===============================================================================