Graph.pm 28.1 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-2016] 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 103 104 105 106 107

        $self->{'_graph'} = Bio::EnsEMBL::Hive::Utils::GraphViz->new(
            'name'          => 'AnalysisWorkflow',
            'concentrate'   => 'true',
            'pad'           => $self->config_get('Pad') || 0,
        );
108
    }
109
    return $self->{'_graph'};
110 111 112
}


113
=head2 pipeline()
114

115 116
  Arg [1] : The HivePipeline instance
  Returntype : HivePipeline
117 118 119

=cut

120
sub pipeline {
121 122 123
    my $self = shift @_;

    if(@_) {
124
        $self->{'_pipeline'} = shift @_;
125 126
    }

127
    return $self->{'_pipeline'};
128 129 130
}


131 132 133 134 135 136 137 138 139
sub _grouped_dataflow_rules {
    my ($self, $analysis) = @_;

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

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


140
sub _analysis_node_name {
141
    my ($self, $analysis) = @_;
142

143
    my $analysis_node_name = 'analysis_' . $analysis->relative_display_name( $self->pipeline );
144 145 146
    if($analysis_node_name=~s/\W/__/g) {
        $analysis_node_name = 'foreign_' . $analysis_node_name;
    }
147
    return $analysis_node_name;
148 149
}

150

151
sub _table_node_name {
152
    my ($self, $naked_table) = @_;
153

154
    my $table_node_name = 'table_' . $naked_table->relative_display_name( $self->pipeline );
155 156
    $table_node_name=~s/\W/__/g;
    return $table_node_name;
157 158
}

159

160 161 162 163 164 165 166
sub _accu_sink_node_name {
    my ($funnel_dfr) = @_;

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


167 168 169
sub _cluster_name {
    my ($df_rule) = @_;

170
    return UNIVERSAL::isa($df_rule, 'Bio::EnsEMBL::Hive::DataflowRule') ? _midpoint_name($df_rule) : ($df_rule || '');
171 172 173
}


174
sub _midpoint_name {
175
    my ($df_rule) = @_;
176

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

184 185 186 187 188 189 190 191 192 193 194

=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 {
195
    my ($self) = @_;
196

197
    my $main_pipeline    = $self->pipeline;
198

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

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

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

230
    my %cluster_2_nodes = ();
231

232 233 234
    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 );
235

236
            push @{$cluster_2_nodes{ $pipeline->hive_pipeline_name } }, $pipelabel_node_name;
237
        }
238
    }
239

240
    if($self->config_get('DisplaySemaphoreBoxes') ) {
241
        foreach my $analysis ( $main_pipeline->collection_of('Analysis')->list, values %{ $self->{'_foreign_analyses'} } ) {
242

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

245
            foreach my $group ( @{ $self->_grouped_dataflow_rules($analysis) } ) {
246

247
                my ($df_rule, $fan_dfrs, $df_targets) = @$group;
248

249 250 251
                my $choice      = (scalar(@$df_targets)!=1) || defined($df_targets->[0]->on_condition);

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

254 255
                    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"
256
                    }
257 258 259
                }

                foreach my $df_target (@$df_targets) {
260 261
                    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:
262

263 264 265 266
                        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:

267
                        push @{$cluster_2_nodes{ _cluster_name( $analysis->{'_funnel_dfr'} ) } }, _accu_sink_node_name( $analysis->{'_funnel_dfr'} );
268
                    }
269
                }
270
            } # /foreach group
271 272 273
        }

        $self->graph->cluster_2_nodes( \%cluster_2_nodes );
274
        $self->graph->main_pipeline_name( $main_pipeline->hive_pipeline_name );
275 276 277
        $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')] );
278 279 280 281 282 283
    }

    return $self->graph();
}


284
sub _propagate_allocation {
285
    my ($self, $source_object, $curr_allocation ) = @_;
286

287
    $curr_allocation ||= $source_object->hive_pipeline->hive_pipeline_name;
288

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

292
        if(UNIVERSAL::isa($source_object, 'Bio::EnsEMBL::Hive::Analysis')) {
293

294
            foreach my $group ( @{ $self->_grouped_dataflow_rules($source_object) } ) {
295

296
                my ($df_rule, $fan_dfrs, $df_targets) = @$group;
297

298 299
                $df_rule->{'_funnel_dfr'} = $curr_allocation;

300
                foreach my $df_target (@$df_targets) {
301
                    my $target_object       = $df_target->to_analysis;
302

303 304 305
                        #   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 : '' );
                }
306 307 308 309

                    # 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) {
310 311 312 313
                    $fan_dfr->{'_funnel_dfr'} = $curr_allocation;

                    foreach my $df_target (@{ $fan_dfr->get_my_targets }) {
                        my $fan_target_object = $df_target->to_analysis;
314

315 316
                        $self->_propagate_allocation( $fan_target_object, ($source_object->hive_pipeline == $fan_target_object->hive_pipeline) ? $df_rule : '' );
                    }
317
                }
318

319
            } # /foreach group
320
        } # if source_object isa Analysis
321
    }
322 323
}

324

325
sub _add_pipeline_label {
326
    my ($self, $pipeline) = @_;
327

328 329 330 331 332
    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,
333
        shape     => 'plaintext',
334 335
        fontname  => $node_fontname,
        label     => $pipeline_label,
336
    );
337 338

    return $pipelabel_node_name;
339 340
}

341

342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
sub _protect_for_display {
    my ($string, $length_limit, $drop_framing_curlies) = @_;

    if($drop_framing_curlies) {
        $string=~s/^\{//;       # drop leading curly
        $string=~s/\}$//;       # drop trailing curly
    }

    if(defined( $length_limit )) {
        $string=~s{^(.{$length_limit}).+}{$1 \.\.\.};   # shorten down to $length_limit characters
    }

    $string=~s{&}{&}g;      # Since we are in HTML context now, ampersands should be escaped (first thing after trimming)
    $string=~s{"}{"}g;     # should fix a string display bug for pre-2.16 GraphViz'es
    $string=~s{<}{&lt;}g;
    $string=~s{>}{&gt;}g;

    return $string;
}


363
sub _add_analysis_node {
364
    my ($self, $analysis) = @_;
365

366 367 368 369
    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

370
    my $analysis_stats = $analysis->stats();
371

372 373 374
    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');
375 376
    my $analysis_shape                                    = $self->config_get('Node', 'AnalysisStatus', 'Shape');
    my $analysis_style                                    = $analysis->can_be_empty() ? 'dashed, filled' : 'filled' ;
377 378
    my $node_fontname                                     = $self->config_get('Node', 'AnalysisStatus', $analysis_status, 'Font');
    my $display_stats                                     = $self->config_get('DisplayStats');
379
    my $hive_pipeline                                     = $self->pipeline;
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397

    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;
398
    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>';
399 400 401 402 403 404 405 406 407
    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>};
        }
    }

408
    if( my $job_limit = $self->config_get('DisplayJobs') ) {
409 410
        my $display_job_length = $self->config_get('DisplayJobLength');

411 412 413 414 415
        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 );
        }

416
        my @jobs = @{ $analysis->jobs_collection };
417 418 419 420 421 422

        my $hit_limit;
        if(scalar(@jobs)>$job_limit) {
            pop @jobs;
            $hit_limit = 1;
        }
423 424 425

        $analysis_label    .= '<tr><td colspan="'.$colspan.'"> </td></tr>';
        foreach my $job (@jobs) {
426
            my $input_id = _protect_for_display( $job->input_id, $display_job_length, 1 );
427
            my $status   = $job->status;
428
            my $job_id   = $job->dbID || 'unstored';
429

Leo Gordon's avatar
Leo Gordon committed
430
            $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>};
431
        }
432 433

        if($hit_limit) {
434
            $analysis_label    .= qq{<tr><td colspan="$colspan">[ + }.($total_job_count-$job_limit).qq{ more jobs ]</td></tr>};
435
        }
436 437
    }
    $analysis_label    .= '</table>>';
438
  
439
    $self->graph->add_node( $this_analysis_node_name,
440
        shape       => 'record',
441 442
        comment     => qq{new_shape:$analysis_shape},
        style       => $analysis_style,
443
        fillcolor   => $analysis_status_colour,
444 445
        fontname    => $node_fontname,
        label       => $analysis_label,
446
    );
Leo Gordon's avatar
Leo Gordon committed
447 448

    $self->_add_control_rules( $analysis->control_rules_collection );
449
    $self->_add_dataflow_rules( $analysis );
450 451

    return $this_analysis_node_name;
452 453 454
}


455 456 457
sub _add_accu_sink_node {
    my ($self, $funnel_dfr) = @_;

458 459 460 461 462 463
    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');

464 465 466
    my $accu_sink_node_name = _accu_sink_node_name( $funnel_dfr );

    $self->graph->add_node( $accu_sink_node_name,
467 468 469 470 471 472
        style       => $accusink_style,
        shape       => $accusink_shape,
        fillcolor   => $accusink_colour,
        fontname    => $accusink_font,
        fontcolor   => $accusink_fontcolour,
        label       => 'Accu',
473 474 475 476 477 478
    );

    return $accu_sink_node_name;
}


479
sub _add_control_rules {
480 481 482 483
    my ($self, $ctrl_rules) = @_;

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

485 486 487 488
        #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;
489

490 491
        my $ctrled_is_local     = $ctrled_analysis->is_local_to( $self->pipeline );
        my $condition_is_local  = $condition_analysis->is_local_to( $self->pipeline );
492

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

497
        next unless( $ctrled_is_local or $condition_is_local or $self->{'_foreign_analyses'}{ $condition_analysis->relative_display_name($self->pipeline) } );
498 499 500 501 502 503 504 505 506

        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',
        );
    }
507 508
}

509

510
sub _last_part_arrow {
511
    my ($self, $from_analysis, $source_node_name, $label_prefix, $df_target, $extras) = @_;
512 513 514 515 516 517 518 519 520 521

    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 )
522
        :   UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Accumulator')   ? $self->_add_accu_sink_node( $from_analysis->{'_funnel_dfr'} )
523
        :   die "Unknown node type";
524

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

527 528 529 530 531 532 533 534 535 536 537
        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;
538 539 540 541
    my $extend_param_stack  = $df_target->extend_param_stack;
    my $multistring_template= ($extend_param_stack ? "INPUT_PLUS " : '')
                             .($input_id_template  ?  '{'.join(",\n", sort keys( %{destringify($input_id_template)} )).'}'
                                                   : ($extend_param_stack ? '' : '') );
542 543

    $graph->add_edge( $source_node_name => $target_node_name,
544
        @$extras,
545 546 547 548 549 550 551 552 553 554 555 556 557 558 559
        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,
    );
560 561 562 563
}


sub _twopart_arrow {
564
    my ($self, $df_rule, $df_targets) = @_;
565 566 567

    my $graph               = $self->graph();
    my $df_edge_fontname    = $self->config_get('Edge', 'Data', 'Font');
568 569 570 571 572
    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');
573
    my $display_cond_length = $self->config_get('DisplayConditionLength');
574

575 576
    my $from_analysis       = $df_rule->from_analysis;
    my $from_node_name      = $self->_analysis_node_name( $from_analysis );
577
    my $midpoint_name       = _midpoint_name( $df_rule );
578 579

       $df_targets        ||= $df_rule->get_my_targets;
580
    my $choice              = (scalar(@$df_targets)!=1) || defined($df_targets->[0]->on_condition);
581 582
    my $tablabel            = qq{<<table border="0" cellborder="0" cellspacing="0" cellpadding="1">i<tr><td></td></tr>};

Leo Gordon's avatar
Leo Gordon committed
583 584 585 586 587 588
    my $targets_grouped_by_condition = $df_rule->get_my_targets_grouped_by_condition( $df_targets );

    foreach my $i (0..scalar(@$targets_grouped_by_condition)-1) {

        my $condition = $targets_grouped_by_condition->[$i]->[0];

589 590 591 592
        if(defined($condition)) {
            $condition = $display_cond_length
                       ? _protect_for_display( $condition, $display_cond_length )   # trim and protect it
                       : 'condition_'.$i;                                           # override it completely with a numbered label
593
        }
594 595 596
        $tablabel .= qq{<tr><td port="cond_$i">}.($condition ? "WHEN $condition" : $choice ? 'ELSE' : '')."</td></tr>";
    }
    $tablabel .= '</table>>';
597 598

    $graph->add_node( $midpoint_name,   # midpoint itself
599
        $choice ? (
600 601
            shape       => 'record',
            comment     => qq{new_shape:$switch_shape},
602 603 604 605
            style       => $switch_style,
            fillcolor   => $switch_colour,
            fontname    => $switch_font,
            fontcolor   => $switch_fontcolour,
606
            label       => $tablabel,
607 608 609 610 611 612
        ) : (
            shape       => 'point',
            fixedsize   => 1,
            width       => 0.01,
            height      => 0.01,
        ),
613 614
    );
    $graph->add_edge( $from_node_name => $midpoint_name, # first half of the two-part arrow
615 616
        color       => 'black',
        fontcolor   => 'black',
617
        fontname    => $df_edge_fontname,
618
        label       => '#'.$df_rule->branch_code,
619
        headport    => 'n',
620 621 622 623 624
        $choice ? (
            arrowhead   => 'normal',
        ) : (
            arrowhead   => 'none',
        ),
625 626
    );

Leo Gordon's avatar
Leo Gordon committed
627 628 629 630 631 632 633
    foreach my $i (0..scalar(@$targets_grouped_by_condition)-1) {

        my $target_group = $targets_grouped_by_condition->[$i]->[1];

        foreach my $df_target (@$target_group) {
            $self->_last_part_arrow($from_analysis, $midpoint_name, '', $df_target, $choice ? [ tailport => "cond_$i" ] : [ tailport => 's' ]);
        }
634 635
    }

636 637 638 639
    return $midpoint_name;
}


640
sub _add_dataflow_rules {
641
    my ($self, $from_analysis) = @_;
642

643
    my $graph               = $self->graph();
644
    my $semablock_colour    = $self->config_get('Edge', 'Semablock', 'Colour');
645

646
    foreach my $group ( @{ $self->_grouped_dataflow_rules($from_analysis) } ) {
647

648
        my ($df_rule, $fan_dfrs, $df_targets) = @$group;
649

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

652
            my $funnel_midpoint_name = $self->_twopart_arrow( $df_rule, $df_targets );
653

654 655
            foreach my $fan_dfr (@$fan_dfrs) {
                my $fan_midpoint_name = $self->_twopart_arrow( $fan_dfr );
656

657 658
                    # add a semaphore inter-rule blocking arc:
                $graph->add_edge( $fan_midpoint_name => $funnel_midpoint_name,
659
                    color     => $semablock_colour,
660 661
                    style     => 'dashed',
                    dir       => 'both',
662
                    arrowhead => 'tee',
663 664 665
                    arrowtail => 'crow',
                );
            }
666

667 668 669 670
        } else {
            my $choice      = (scalar(@$df_targets)!=1) || defined($df_targets->[0]->on_condition);

            if($choice) {
671
                $self->_twopart_arrow( $df_rule, $df_targets );
672 673 674 675
            } else {
                my $from_node_name  = $self->_analysis_node_name( $from_analysis );
                my $df_target       = $df_targets->[0];

676
                $self->_last_part_arrow($from_analysis, $from_node_name, '#'.$df_rule->branch_code, $df_target, []);
677
            }
678
        }
679

680
    } # /foreach my $group
681 682
}

683

684
sub _add_table_node {
685
    my ($self, $naked_table) = @_;
686

687 688
    my $table_shape             = $self->config_get('Node', 'Table', 'Shape');
    my $table_style             = $self->config_get('Node', 'Table', 'Style');
689 690 691 692 693
    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');

694
    my $hive_pipeline           = $self->pipeline;
695
    my $this_table_node_name    = $self->_table_node_name( $naked_table );
696

697
    my (@column_names, $columns, $table_data, $data_limit, $hit_limit);
698

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

701
        @column_names = sort keys %{$naked_table_adaptor->column_set};
702
        $columns = scalar(@column_names);
703
        $table_data = $naked_table_adaptor->fetch_all( 'LIMIT '.($data_limit+1) );
704 705 706 707 708

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

711
    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>';
712

713
    if( $self->config_get('DisplayData') and $columns) {
714
        $table_label .= '<tr><td colspan="'.$columns.'"> </td></tr>';
715
        $table_label .= '<tr>'.join('', map { qq{<td bgcolor="$table_header_colour" border="1">$_</td>} } @column_names).'</tr>';
716 717 718
        foreach my $row (@$table_data) {
            $table_label .= '<tr>'.join('', map { qq{<td>$_</td>} } @{$row}{@column_names}).'</tr>';
        }
719 720 721
        if($hit_limit) {
            $table_label  .= qq{<tr><td colspan="$columns">[ more data ]</td></tr>};
        }
722 723 724
    }
    $table_label .= '</table>>';

725
    $self->graph()->add_node( $this_table_node_name,
726
        shape       => 'record',
727 728
        comment     => qq{new_shape:$table_shape},
        style       => $table_style,
729 730 731 732
        fillcolor   => $table_colour,
        fontname    => $table_fontname,
        fontcolor   => $table_fontcolour,
        label       => $table_label,
733
    );
734 735

    return $this_table_node_name;
736 737
}

Leo Gordon's avatar
Leo Gordon committed
738
1;