Graph.pm 27.2 KB
Newer Older
1
=pod
2 3 4

=head1 NAME

5
    Bio::EnsEMBL::Hive::Utils::Graph
6 7 8

=head1 SYNOPSIS

9
    my $g = Bio::EnsEMBL::Hive::Utils::Graph->new( $hive_pipeline );
10 11
    my $graphviz = $g->build();
    $graphviz->as_png('location.png');
12 13 14

=head1 DESCRIPTION

15 16 17 18 19 20 21
    This is a module for converting a hive database's flow of analyses, control 
    rules and dataflows into the GraphViz model language. This information can
    then be converted to an image or to the dot language for further manipulation
    in GraphViz.

=head1 LICENSE

22
    Copyright [1999-2015] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
23 24 25 26 27 28 29 30 31

    Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software distributed under the License
    is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and limitations under the License.
32

33
=head1 CONTACT
34

35
  Please subscribe to the Hive mailing list:  http://listserver.ebi.ac.uk/mailman/listinfo/ehive-users  to discuss Hive-related questions or to be notified of our updates
36 37 38 39 40

=head1 APPENDIX

    The rest of the documentation details each of the object methods.
    Internal methods are usually preceded with a _
41 42 43

=cut

44 45 46

package Bio::EnsEMBL::Hive::Utils::Graph;

47 48 49
use strict;
use warnings;

Leo Gordon's avatar
Leo Gordon committed
50
use Bio::EnsEMBL::Hive::Analysis;
51
use Bio::EnsEMBL::Hive::Utils qw(destringify throw);
52
use Bio::EnsEMBL::Hive::Utils::GraphViz;
53
use Bio::EnsEMBL::Hive::Utils::Config;
54
use Bio::EnsEMBL::Hive::TheApiary;
55

56 57
use base ('Bio::EnsEMBL::Hive::Configurable');

58 59 60

=head2 new()

61
  Arg [1] : Bio::EnsEMBL::Hive::HivePipeline $pipeline;
62 63 64 65 66
              The adaptor to get information from
  Arg [2] : (optional) string $config_file_name;
                  A JSON file name to initialize the Config object with.
                  If one is not given then we don't pass anything into Config's constructor,
                  which results in loading configuration from Config's standard locations.
67 68 69 70 71 72 73
  Returntype : Graph object
  Exceptions : If the parameters are not as required
  Status     : Beta
  
=cut

sub new {
74
    my $class       = shift @_;
75
    my $pipeline    = shift @_;
76

77
    my $self = bless({}, ref($class) || $class);
78

79 80
    $self->pipeline( $pipeline );

81 82 83
    my $config = Bio::EnsEMBL::Hive::Utils::Config->new( @_ );
    $self->config($config);
    $self->context( [ 'Graph' ] );
84

85
    return $self;
86 87 88 89 90 91 92 93 94 95 96 97 98
}


=head2 graph()

  Arg [1] : The GraphViz instance created by this module
  Returntype : GraphViz
  Exceptions : None
  Status     : Beta

=cut

sub graph {
99 100
    my ($self) = @_;

101
    if(! exists $self->{'_graph'}) {
102
        my $padding  = $self->config_get('Pad') || 0;
103
        $self->{'_graph'} = Bio::EnsEMBL::Hive::Utils::GraphViz->new( name => 'AnalysisWorkflow', concentrate => 'true', ratio => qq{compress"; pad = "$padding}  ); # injection hack!
104
    }
105
    return $self->{'_graph'};
106 107 108
}


109
=head2 pipeline()
110

111 112
  Arg [1] : The HivePipeline instance
  Returntype : HivePipeline
113 114 115

=cut

116
sub pipeline {
117 118 119
    my $self = shift @_;

    if(@_) {
120
        $self->{'_pipeline'} = shift @_;
121 122
    }

123
    return $self->{'_pipeline'};
124 125 126
}


127 128 129 130 131 132 133 134 135
sub _grouped_dataflow_rules {
    my ($self, $analysis) = @_;

    my $gdr = $self->{'_gdr'} ||= {};

    return $gdr->{$analysis} ||= $analysis->get_grouped_dataflow_rules;
}


136
sub _analysis_node_name {
137
    my ($self, $analysis) = @_;
138

139
    my $analysis_node_name = 'analysis_' . $analysis->relative_display_name( $self->pipeline );
140 141 142
    if($analysis_node_name=~s/\W/__/g) {
        $analysis_node_name = 'foreign_' . $analysis_node_name;
    }
143
    return $analysis_node_name;
144 145
}

146

147
sub _table_node_name {
148
    my ($self, $naked_table) = @_;
149

150
    my $table_node_name = 'table_' . $naked_table->relative_display_name( $self->pipeline );
151 152
    $table_node_name=~s/\W/__/g;
    return $table_node_name;
153 154
}

155

156 157 158 159 160 161 162
sub _accu_sink_node_name {
    my ($funnel_dfr) = @_;

    return 'sink_'.(UNIVERSAL::isa($funnel_dfr, 'Bio::EnsEMBL::Hive::DataflowRule') ? _midpoint_name($funnel_dfr) : ($funnel_dfr || ''));
}


163 164 165
sub _cluster_name {
    my ($df_rule) = @_;

166
    return UNIVERSAL::isa($df_rule, 'Bio::EnsEMBL::Hive::DataflowRule') ? _midpoint_name($df_rule) : ($df_rule || '');
167 168 169
}


170
sub _midpoint_name {
171
    my ($df_rule) = @_;
172

173
    if($df_rule and scalar($df_rule)=~/\((\w+)\)/) {     # a unique id of a df_rule assuming dbIDs are not available
174 175
        return 'dfr_'.$1.'_mp';
    } else {
176
        throw("Wrong argument to _midpoint_name");
177
    }
178 179
}

180 181 182 183 184 185 186 187 188 189 190

=head2 build()

  Returntype : The GraphViz object built & populated
  Exceptions : Raised if there are issues with accessing the database
  Description : Builds the graph object and returns it.
  Status     : Beta

=cut

sub build {
191
    my ($self) = @_;
192

193
    my $main_pipeline    = $self->pipeline;
194

195
    foreach my $source_analysis ( @{ $main_pipeline->get_source_analyses } ) {
196 197
            # run the recursion in each component that has a non-cyclic start:
        $self->_propagate_allocation( $source_analysis );
198
    }
199
    foreach my $cyclic_analysis ( $main_pipeline->collection_of( 'Analysis' )->list ) {
200 201 202
        next if(defined $cyclic_analysis->{'_funnel_dfr'});
        $self->_propagate_allocation( $cyclic_analysis );
    }
203

204
    foreach my $source_analysis ( @{ $main_pipeline->get_source_analyses } ) {
205 206
            # run the recursion in each component that has a non-cyclic start:
        $self->_add_analysis_node( $source_analysis );
207
    }
208
    foreach my $cyclic_analysis ( $main_pipeline->collection_of( 'Analysis' )->list ) {
209 210
        next if($self->{'_created_analysis'}{ $cyclic_analysis });
        $self->_add_analysis_node( $cyclic_analysis );
211
    }
212

213
    if($self->config_get('DisplayStretched') ) {    # put each analysis before its' funnel midpoint
214
        foreach my $analysis ( $main_pipeline->collection_of('Analysis')->list ) {
215
            if(ref($analysis->{'_funnel_dfr'})) {    # this should only affect analyses that have a funnel
216
                my $from = $self->_analysis_node_name( $analysis );
217
                my $to   = _midpoint_name( $analysis->{'_funnel_dfr'} );
218
                $self->graph->add_edge( $from => $to,
219
                    style     => 'invis',   # toggle visibility by changing 'invis' to 'dashed'
220
                    color     => 'black',
221 222 223 224 225
                );
            }
        }
    }

226
    my %cluster_2_nodes = ();
227

228 229 230
    if( $self->config_get('DisplayDetails') ) {
        foreach my $pipeline ( $main_pipeline, values %{Bio::EnsEMBL::Hive::TheApiary->pipelines_collection} ) {
            my $pipelabel_node_name = $self->_add_pipeline_label( $pipeline );
231

232
            push @{$cluster_2_nodes{ $pipeline->hive_pipeline_name } }, $pipelabel_node_name;
233
        }
234
    }
235

236
    if($self->config_get('DisplaySemaphoreBoxes') ) {
237
        foreach my $analysis ( $main_pipeline->collection_of('Analysis')->list, values %{ $self->{'_foreign_analyses'} } ) {
238

239
            push @{$cluster_2_nodes{ _cluster_name( $analysis->{'_funnel_dfr'} ) } }, $self->_analysis_node_name( $analysis );
240

241
            foreach my $group ( @{ $self->_grouped_dataflow_rules($analysis) } ) {
242

243
                my ($df_rule, $fan_dfrs, $df_targets) = @$group;
244

245 246 247
                my $choice      = (scalar(@$df_targets)!=1) || defined($df_targets->[0]->on_condition);

                if(@$fan_dfrs or $choice) {
248
                    push @{$cluster_2_nodes{ _cluster_name( $df_rule->{'_funnel_dfr'} ) }}, _midpoint_name( $df_rule ); # top-level funnels define clusters (top-level "boxes")
249

250 251
                    foreach my $fan_dfr (@$fan_dfrs) {
                        push @{$cluster_2_nodes{ _cluster_name( $fan_dfr->{'_funnel_dfr'} ) } }, _midpoint_name( $fan_dfr ); # midpoints of rules that have a funnel live inside "boxes"
252
                    }
253 254 255
                }

                foreach my $df_target (@$df_targets) {
256 257
                    my $target_object = $df_target->to_analysis;
                    if( UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::NakedTable') ) {        # put the table into the same "box" as the dataflow source:
258

259 260 261 262 263
                        push @{$cluster_2_nodes{ _cluster_name( $target_object->{'_funnel_dfr'} ) } }, $self->_table_node_name( $target_object );

                    } elsif( UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Accumulator') ) {  # put the accu sink into the same "box" as the dataflow source:

                        push @{$cluster_2_nodes{ _cluster_name( $target_object->{'_funnel_dfr'} ) } }, _accu_sink_node_name( $target_object->{'_funnel_dfr'} );
264
                    }
265
                }
266
            } # /foreach group
267 268 269
        }

        $self->graph->cluster_2_nodes( \%cluster_2_nodes );
270
        $self->graph->main_pipeline_name( $main_pipeline->hive_pipeline_name );
271 272 273
        $self->graph->semaphore_bgcolour(       [$self->config_get('Box', 'Semaphore', 'ColourScheme'),     $self->config_get('Box', 'Semaphore', 'ColourOffset')] );
        $self->graph->main_pipeline_bgcolour(   [$self->config_get('Box', 'MainPipeline', 'ColourScheme'),  $self->config_get('Box', 'MainPipeline', 'ColourOffset')] );
        $self->graph->other_pipeline_bgcolour(  [$self->config_get('Box', 'OtherPipeline', 'ColourScheme'), $self->config_get('Box', 'OtherPipeline', 'ColourOffset')] );
274 275 276 277 278 279
    }

    return $self->graph();
}


280
sub _propagate_allocation {
281
    my ($self, $source_object, $curr_allocation ) = @_;
282

283
    $curr_allocation ||= $source_object->hive_pipeline->hive_pipeline_name;
284

285 286
    if(!exists $source_object->{'_funnel_dfr'} ) {     # only allocate on the first-come basis:
        $source_object->{'_funnel_dfr'} = $curr_allocation;
287

288
        if(UNIVERSAL::isa($source_object, 'Bio::EnsEMBL::Hive::Analysis')) {
289

290
            foreach my $group ( @{ $self->_grouped_dataflow_rules($source_object) } ) {
291

292
                my ($df_rule, $fan_dfrs, $df_targets) = @$group;
293

294 295
                $df_rule->{'_funnel_dfr'} = $curr_allocation;

296
                foreach my $df_target (@$df_targets) {
297
                    my $target_object       = $df_target->to_analysis;
298

299 300 301
                        #   In case we have crossed pipeline borders, let the next call decide its own allocation by resetting it.
                    $self->_propagate_allocation( $target_object, ($source_object->hive_pipeline == $target_object->hive_pipeline) ? $curr_allocation : '' );
                }
302 303 304 305

                    # all fan members point to the funnel.
                    #   Request midpoint's allocation since we definitely have a funnel to link to.
                foreach my $fan_dfr (@$fan_dfrs) {
306 307 308 309 310 311
                    $fan_dfr->{'_funnel_dfr'} = $curr_allocation;

                    foreach my $df_target (@{ $fan_dfr->get_my_targets }) {
                        my $fan_target_object = $df_target->to_analysis;
                        $self->_propagate_allocation( $fan_target_object, ($source_object->hive_pipeline == $fan_target_object->hive_pipeline) ? $df_rule : '' );
                    }
312
                }
313

314
            } # /foreach group
315
        } # if source_object isa Analysis
316
    }
317 318
}

319

320
sub _add_pipeline_label {
321
    my ($self, $pipeline) = @_;
322

323 324 325 326 327
    my $node_fontname       = $self->config_get('Node', 'Details', 'Font');
    my $pipeline_label      = $pipeline->display_name;
    my $pipelabel_node_name = 'pipelabel_'.$pipeline->hive_pipeline_name;

    $self->graph()->add_node( $pipelabel_node_name,
328
        shape     => 'plaintext',
329 330
        fontname  => $node_fontname,
        label     => $pipeline_label,
331
    );
332 333

    return $pipelabel_node_name;
334 335
}

336

337
sub _add_analysis_node {
338
    my ($self, $analysis) = @_;
339

340 341 342 343
    my $this_analysis_node_name                           = $self->_analysis_node_name( $analysis );

    return $this_analysis_node_name if($self->{'_created_analysis'}{ $analysis }++);   # making sure every Analysis node gets created no more than once

344
    my $analysis_stats = $analysis->stats();
345

346 347 348
    my ($breakout_label, $total_job_count, $count_hash)   = $analysis_stats->job_count_breakout();
    my $analysis_status                                   = $analysis_stats->status;
    my $analysis_status_colour                            = $self->config_get('Node', 'AnalysisStatus', $analysis_status, 'Colour');
349 350
    my $analysis_shape                                    = $self->config_get('Node', 'AnalysisStatus', 'Shape');
    my $analysis_style                                    = $analysis->can_be_empty() ? 'dashed, filled' : 'filled' ;
351 352
    my $node_fontname                                     = $self->config_get('Node', 'AnalysisStatus', $analysis_status, 'Font');
    my $display_stats                                     = $self->config_get('DisplayStats');
353
    my $hive_pipeline                                     = $self->pipeline;
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371

    my $colspan = 0;
    my $bar_chart = '';

    if( $display_stats eq 'barchart' ) {
        foreach my $count_method (qw(SEMAPHORED READY INPROGRESS DONE FAILED)) {
            if(my $count=$count_hash->{lc($count_method).'_job_count'}) {
                $bar_chart .= '<td bgcolor="'.$self->config_get('Node', 'JobStatus', $count_method, 'Colour').'" width="'.int(100*$count/$total_job_count).'%">'.$count.lc(substr($count_method,0,1)).'</td>';
                ++$colspan;
            }
        }
        if($colspan != 1) {
            $bar_chart .= '<td>='.$total_job_count.'</td>';
            ++$colspan;
        }
    }

    $colspan ||= 1;
372
    my $analysis_label  = '<<table border="0" cellborder="0" cellspacing="0" cellpadding="1"><tr><td colspan="'.$colspan.'">'.$analysis->relative_display_name( $hive_pipeline ).' ('.($analysis->dbID || 'unstored').')</td></tr>';
373 374 375 376 377 378 379 380 381
    if( $display_stats ) {
        $analysis_label    .= qq{<tr><td colspan="$colspan"> </td></tr>};
        if( $display_stats eq 'barchart') {
            $analysis_label    .= qq{<tr>$bar_chart</tr>};
        } elsif( $display_stats eq 'text') {
            $analysis_label    .= qq{<tr><td colspan="$colspan">$breakout_label</td></tr>};
        }
    }

382
    if( my $job_limit = $self->config_get('DisplayJobs') ) {
383 384 385 386 387
        if(my $job_adaptor = $analysis->adaptor && $analysis->adaptor->db->get_AnalysisJobAdaptor) {
            my @jobs = sort {$a->dbID <=> $b->dbID} @{ $job_adaptor->fetch_some_by_analysis_id_limit( $analysis->dbID, $job_limit+1 )};
            $analysis->jobs_collection( \@jobs );
        }

388
        my @jobs = @{ $analysis->jobs_collection };
389 390 391 392 393 394

        my $hit_limit;
        if(scalar(@jobs)>$job_limit) {
            pop @jobs;
            $hit_limit = 1;
        }
395 396 397 398 399

        $analysis_label    .= '<tr><td colspan="'.$colspan.'"> </td></tr>';
        foreach my $job (@jobs) {
            my $input_id = $job->input_id;
            my $status   = $job->status;
400
            my $job_id   = $job->dbID || 'unstored';
401 402 403
            $input_id=~s/\>/&gt;/g;
            $input_id=~s/\</&lt;/g;
            $input_id=~s/\{|\}//g;
Leo Gordon's avatar
Leo Gordon committed
404
            $analysis_label    .= qq{<tr><td align="left" colspan="$colspan" bgcolor="}.$self->config_get('Node', 'JobStatus', $status, 'Colour').qq{">$job_id [$status]: $input_id</td></tr>};
405
        }
406 407

        if($hit_limit) {
408
            $analysis_label    .= qq{<tr><td colspan="$colspan">[ + }.($total_job_count-$job_limit).qq{ more jobs ]</td></tr>};
409
        }
410 411
    }
    $analysis_label    .= '</table>>';
412
  
413
    $self->graph->add_node( $this_analysis_node_name,
414
        shape       => 'record',
415 416
        comment     => qq{new_shape:$analysis_shape},
        style       => $analysis_style,
417
        fillcolor   => $analysis_status_colour,
418 419
        fontname    => $node_fontname,
        label       => $analysis_label,
420
    );
Leo Gordon's avatar
Leo Gordon committed
421 422

    $self->_add_control_rules( $analysis->control_rules_collection );
423
    $self->_add_dataflow_rules( $analysis );
424 425

    return $this_analysis_node_name;
426 427 428
}


429 430 431
sub _add_accu_sink_node {
    my ($self, $funnel_dfr) = @_;

432 433 434 435 436 437
    my $accusink_shape      = $self->config_get('Node', 'AccuSink', 'Shape');
    my $accusink_style      = $self->config_get('Node', 'AccuSink', 'Style');
    my $accusink_colour     = $self->config_get('Node', 'AccuSink', 'Colour');
    my $accusink_font       = $self->config_get('Node', 'AccuSink', 'Font');
    my $accusink_fontcolour = $self->config_get('Node', 'AccuSink', 'FontColour');

438 439 440
    my $accu_sink_node_name = _accu_sink_node_name( $funnel_dfr );

    $self->graph->add_node( $accu_sink_node_name,
441 442 443 444 445 446
        style       => $accusink_style,
        shape       => $accusink_shape,
        fillcolor   => $accusink_colour,
        fontname    => $accusink_font,
        fontcolor   => $accusink_fontcolour,
        label       => 'Accu',
447 448 449 450 451 452
    );

    return $accu_sink_node_name;
}


453
sub _add_control_rules {
454 455 456 457
    my ($self, $ctrl_rules) = @_;

    my $control_colour = $self->config_get('Edge', 'Control', 'Colour');
    my $graph = $self->graph();
458

459 460 461 462
        #The control rules are always from and to an analysis so no need to search for odd cases here
    foreach my $c_rule ( @$ctrl_rules ) {
        my $condition_analysis  = $c_rule->condition_analysis;
        my $ctrled_analysis     = $c_rule->ctrled_analysis;
463

464 465
        my $ctrled_is_local     = $ctrled_analysis->is_local_to( $self->pipeline );
        my $condition_is_local  = $condition_analysis->is_local_to( $self->pipeline );
466

467
        if($ctrled_is_local and !$condition_is_local) {     # register a new "near neighbour" node if it's reachable by following one rule "out":
468
            $self->{'_foreign_analyses'}{ $condition_analysis->relative_display_name($self->pipeline) } = $condition_analysis;
469 470
        }

471
        next unless( $ctrled_is_local or $condition_is_local or $self->{'_foreign_analyses'}{ $condition_analysis->relative_display_name($self->pipeline) } );
472 473 474 475 476 477 478 479 480

        my $from_node_name      = $self->_analysis_node_name( $condition_analysis );
        my $to_node_name        = $self->_analysis_node_name( $ctrled_analysis );

        $graph->add_edge( $from_node_name => $to_node_name,
            color => $control_colour,
            arrowhead => 'tee',
        );
    }
481 482
}

483

484
sub _last_part_arrow {
485
    my ($self, $from_analysis, $source_node_name, $label_prefix, $df_target, $extras) = @_;
486 487 488 489 490 491 492 493 494 495

    my $graph               = $self->graph();
    my $dataflow_colour     = $self->config_get('Edge', 'Data', 'Colour');
    my $accu_colour         = $self->config_get('Edge', 'Accu', 'Colour');
    my $df_edge_fontname    = $self->config_get('Edge', 'Data', 'Font');

    my $target_object       = $df_target->to_analysis;
    my $target_node_name    =
            UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Analysis')      ? $self->_add_analysis_node( $target_object )
        :   UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::NakedTable')    ? $self->_add_table_node( $target_object )
496
        :   UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Accumulator')   ? $self->_add_accu_sink_node( $from_analysis->{'_funnel_dfr'} )
497
        :   die "Unknown node type";
498

499
    if(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Analysis')) {    # skip some *really* foreign dataflow rules:
500

501 502 503 504 505 506 507 508 509 510 511 512 513 514
        my $from_is_local   = $from_analysis->is_local_to( $self->pipeline );
        my $target_is_local = $target_object->is_local_to( $self->pipeline );

        if($from_is_local and !$target_is_local) {  # register a new "near neighbour" node if it's reachable by following one rule "out":
            $self->{'_foreign_analyses'}{ $target_object->relative_display_name($self->pipeline) } = $target_object;
        }

        return unless( $from_is_local or $target_is_local or $self->{'_foreign_analyses'}{ $target_object->relative_display_name($self->pipeline) } );
    }

    my $input_id_template   = $self->config_get('DisplayInputIDTemplate') ? $df_target->input_id_template : undef;
    my $multistring_template= $input_id_template ? ": {".join(",\n", sort keys( %{destringify($input_id_template)} )).'}' : '';

    $graph->add_edge( $source_node_name => $target_node_name,
515
        @$extras,
516 517 518 519 520 521 522 523 524 525 526 527 528 529 530
        UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Accumulator')
            ? (
                color       => $accu_colour,
                fontcolor   => $accu_colour,
                style       => 'dashed',
                dir         => 'both',
                arrowtail   => 'crow',
                label       => $label_prefix."\n=> ".$target_object->relative_display_name( $self->pipeline ),
            ) : (
                color       => $dataflow_colour,
                fontcolor   => $dataflow_colour,
                label       => $label_prefix."\n".$multistring_template,
            ),
        fontname    => $df_edge_fontname,
    );
531 532 533 534
}


sub _twopart_arrow {
535
    my ($self, $df_rule, $df_targets) = @_;
536 537 538

    my $graph               = $self->graph();
    my $df_edge_fontname    = $self->config_get('Edge', 'Data', 'Font');
539 540 541 542 543
    my $switch_shape        = $self->config_get('Node', 'Switch', 'Shape');
    my $switch_style        = $self->config_get('Node', 'Switch', 'Style');
    my $switch_colour       = $self->config_get('Node', 'Switch', 'Colour');
    my $switch_font         = $self->config_get('Node', 'Switch', 'Font');
    my $switch_fontcolour   = $self->config_get('Node', 'Switch', 'FontColour');
544
    my $display_cond_length = $self->config_get('DisplayConditionLength');
545

546 547
    my $from_analysis       = $df_rule->from_analysis;
    my $from_node_name      = $self->_analysis_node_name( $from_analysis );
548
    my $midpoint_name       = _midpoint_name( $df_rule );
549 550

       $df_targets        ||= $df_rule->get_my_targets;
551
    my $choice              = (scalar(@$df_targets)!=1) || defined($df_targets->[0]->on_condition);
552 553 554 555 556 557
    #my $label               = scalar(@$df_targets)==1 ? 'Filter' : 'Switch';
    my $tablabel            = qq{<<table border="0" cellborder="0" cellspacing="0" cellpadding="1">i<tr><td></td></tr>};

    foreach my $i (0..scalar(@$df_targets)-1) {
        my $df_target = $df_targets->[$i];
        my $condition = $df_target->on_condition;
558 559 560 561 562 563 564 565 566 567
        if($display_cond_length) {
            if(defined($condition)) {
                $condition=~s{"}{&quot;}g;  # should fix a string display bug for pre-2.16 GraphViz'es
                $condition=~s{<}{&lt;}g;
                $condition=~s{>}{&gt;}g;
                $condition=~s{^(.{$display_cond_length}).+}{$1 \.\.\.};     # shorten down to $display_cond_length characters
            }
        } else {
            $condition &&= 'condition_'.$i;
        }
568 569 570
        $tablabel .= qq{<tr><td port="cond_$i">}.($condition ? "WHEN $condition" : $choice ? 'ELSE' : '')."</td></tr>";
    }
    $tablabel .= '</table>>';
571 572

    $graph->add_node( $midpoint_name,   # midpoint itself
573
        $choice ? (
574 575
            shape       => 'record',
            comment     => qq{new_shape:$switch_shape},
576 577 578 579
            style       => $switch_style,
            fillcolor   => $switch_colour,
            fontname    => $switch_font,
            fontcolor   => $switch_fontcolour,
580
            label       => $tablabel,
581 582 583 584 585 586
        ) : (
            shape       => 'point',
            fixedsize   => 1,
            width       => 0.01,
            height      => 0.01,
        ),
587 588
    );
    $graph->add_edge( $from_node_name => $midpoint_name, # first half of the two-part arrow
589 590
        color       => 'black',
        fontcolor   => 'black',
591
        fontname    => $df_edge_fontname,
592
        label       => '#'.$df_rule->branch_code,
593
        headport    => 'n',
594 595 596 597 598
        $choice ? (
            arrowhead   => 'normal',
        ) : (
            arrowhead   => 'none',
        ),
599 600
    );

601
    foreach my $i (0..scalar(@$df_targets)-1) {
602
        $self->_last_part_arrow($from_analysis, $midpoint_name, '', $df_targets->[$i], $choice ? [ tailport => "cond_$i" ] : [ tailport => 's' ]);
603 604
    }

605 606 607 608
    return $midpoint_name;
}


609
sub _add_dataflow_rules {
610
    my ($self, $from_analysis) = @_;
611

612
    my $graph               = $self->graph();
613
    my $semablock_colour    = $self->config_get('Edge', 'Semablock', 'Colour');
614

615
    foreach my $group ( @{ $self->_grouped_dataflow_rules($from_analysis) } ) {
616

617
        my ($df_rule, $fan_dfrs, $df_targets) = @$group;
618

619
        if(@$fan_dfrs) {    # semaphored funnel case => all rules have an Analysis target and have two parts:
620

621
            my $funnel_midpoint_name = $self->_twopart_arrow( $df_rule, $df_targets );
622

623 624
            foreach my $fan_dfr (@$fan_dfrs) {
                my $fan_midpoint_name = $self->_twopart_arrow( $fan_dfr );
625

626 627
                    # add a semaphore inter-rule blocking arc:
                $graph->add_edge( $fan_midpoint_name => $funnel_midpoint_name,
628
                    color     => $semablock_colour,
629 630
                    style     => 'dashed',
                    dir       => 'both',
631
                    arrowhead => 'tee',
632 633 634
                    arrowtail => 'crow',
                );
            }
635

636 637 638 639
        } else {
            my $choice      = (scalar(@$df_targets)!=1) || defined($df_targets->[0]->on_condition);

            if($choice) {
640
                $self->_twopart_arrow( $df_rule, $df_targets );
641 642 643 644
            } else {
                my $from_node_name  = $self->_analysis_node_name( $from_analysis );
                my $df_target       = $df_targets->[0];

645
                $self->_last_part_arrow($from_analysis, $from_node_name, '#'.$df_rule->branch_code, $df_target, []);
646
            }
647
        }
648

649
    } # /foreach my $group
650 651
}

652

653
sub _add_table_node {
654
    my ($self, $naked_table) = @_;
655

656 657
    my $table_shape             = $self->config_get('Node', 'Table', 'Shape');
    my $table_style             = $self->config_get('Node', 'Table', 'Style');
658 659 660 661 662
    my $table_colour            = $self->config_get('Node', 'Table', 'Colour');
    my $table_header_colour     = $self->config_get('Node', 'Table', 'HeaderColour');
    my $table_fontcolour        = $self->config_get('Node', 'Table', 'FontColour');
    my $table_fontname          = $self->config_get('Node', 'Table', 'Font');

663
    my $hive_pipeline           = $self->pipeline;
664
    my $this_table_node_name    = $self->_table_node_name( $naked_table );
665

666
    my (@column_names, $columns, $table_data, $data_limit, $hit_limit);
667

668
    if( $data_limit = $self->config_get('DisplayData') and my $naked_table_adaptor = $naked_table->adaptor ) {
669

670
        @column_names = sort keys %{$naked_table_adaptor->column_set};
671
        $columns = scalar(@column_names);
672
        $table_data = $naked_table_adaptor->fetch_all( 'LIMIT '.($data_limit+1) );
673 674 675 676 677

        if(scalar(@$table_data)>$data_limit) {
            pop @$table_data;
            $hit_limit = 1;
        }
678 679
    }

680
    my $table_label = '<<table border="0" cellborder="0" cellspacing="0" cellpadding="1"><tr><td colspan="'.($columns||1).'">'. $naked_table->relative_display_name( $hive_pipeline ) .'</td></tr>';
681

682
    if( $self->config_get('DisplayData') and $columns) {
683
        $table_label .= '<tr><td colspan="'.$columns.'"> </td></tr>';
684
        $table_label .= '<tr>'.join('', map { qq{<td bgcolor="$table_header_colour" border="1">$_</td>} } @column_names).'</tr>';
685 686 687
        foreach my $row (@$table_data) {
            $table_label .= '<tr>'.join('', map { qq{<td>$_</td>} } @{$row}{@column_names}).'</tr>';
        }
688 689 690
        if($hit_limit) {
            $table_label  .= qq{<tr><td colspan="$columns">[ more data ]</td></tr>};
        }
691 692 693
    }
    $table_label .= '</table>>';

694
    $self->graph()->add_node( $this_table_node_name,
695
        shape       => 'record',
696 697
        comment     => qq{new_shape:$table_shape},
        style       => $table_style,
698 699 700 701
        fillcolor   => $table_colour,
        fontname    => $table_fontname,
        fontcolor   => $table_fontcolour,
        label       => $table_label,
702
    );
703 704

    return $this_table_node_name;
705 706
}

Leo Gordon's avatar
Leo Gordon committed
707
1;