Graph.pm 23.3 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', 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
sub _analysis_node_name {
128
    my ($self, $analysis) = @_;
129

130
    my $analysis_node_name = 'analysis_' . $analysis->relative_display_name( $self->pipeline );
131 132
    $analysis_node_name=~s/\W/__/g;
    return $analysis_node_name;
133 134
}

135

136
sub _table_node_name {
137
    my ($self, $df_rule) = @_;
138

139
    my $table_node_name = 'table_' . $df_rule->to_analysis->relative_display_name( $self->pipeline ) .
140
                ($self->config_get('DuplicateTables') ?  '_'.$df_rule->from_analysis->logic_name : '');
141 142
    $table_node_name=~s/\W/__/g;
    return $table_node_name;
143 144
}

145

146 147 148
sub _cluster_name {
    my ($df_rule) = @_;

149
    return UNIVERSAL::isa($df_rule, 'Bio::EnsEMBL::Hive::DataflowRule') ? _midpoint_name($df_rule) : ($df_rule || '');
150 151 152
}


153
sub _midpoint_name {
154
    my ($df_rule) = @_;
155

156
    if($df_rule and scalar($df_rule)=~/\((\w+)\)/) {     # a unique id of a df_rule assuming dbIDs are not available
157 158
        return 'dfr_'.$1.'_mp';
    } else {
159
        throw("Wrong argument to _midpoint_name");
160
    }
161 162
}

163 164 165 166 167 168 169 170 171 172 173

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

176
    my $main_pipeline    = $self->pipeline;
177

178
    foreach my $source_analysis ( @{ $main_pipeline->get_source_analyses } ) {
179 180
            # run the recursion in each component that has a non-cyclic start:
        $self->_propagate_allocation( $source_analysis );
181
    }
182
    foreach my $cyclic_analysis ( $main_pipeline->collection_of( 'Analysis' )->list ) {
183 184 185
        next if(defined $cyclic_analysis->{'_funnel_dfr'});
        $self->_propagate_allocation( $cyclic_analysis );
    }
186

187
    foreach my $source_analysis ( @{ $main_pipeline->get_source_analyses } ) {
188 189
            # run the recursion in each component that has a non-cyclic start:
        $self->_add_analysis_node( $source_analysis );
190
    }
191
    foreach my $cyclic_analysis ( $main_pipeline->collection_of( 'Analysis' )->list ) {
192 193
        next if($self->{'_created_analysis'}{ $cyclic_analysis });
        $self->_add_analysis_node( $cyclic_analysis );
194
    }
195

196
    if($self->config_get('DisplayStretched') ) {    # put each analysis before its' funnel midpoint
197
        foreach my $analysis ( $main_pipeline->collection_of('Analysis')->list ) {
198
            if($analysis->{'_funnel_dfr'}) {    # this should only affect analyses that have a funnel
199
                my $from = $self->_analysis_node_name( $analysis );
200
                my $to   = _midpoint_name( $analysis->{'_funnel_dfr'} );
201
                $self->graph->add_edge( $from => $to,
202 203 204 205 206 207 208
                    color     => 'black',
                    style     => 'invis',   # toggle visibility by changing 'invis' to 'dashed'
                );
            }
        }
    }

209
    my %cluster_2_nodes = ();
210

211 212 213
    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 );
214

215
            push @{$cluster_2_nodes{ $pipeline->hive_pipeline_name } }, $pipelabel_node_name;
216
        }
217
    }
218

219
    if($self->config_get('DisplaySemaphoreBoxes') ) {
220
        foreach my $analysis ( $main_pipeline->collection_of('Analysis')->list, values %{ $self->{'_foreign_analyses'} } ) {
221
            push @{$cluster_2_nodes{ _cluster_name( $analysis->{'_funnel_dfr'} ) } }, $self->_analysis_node_name( $analysis );
222

223
            foreach my $group ( @{ $analysis->get_grouped_dataflow_rules } ) {
224

225
                my ($df_rule, $fan_dfrs) = @$group;
226

227 228
                if(@$fan_dfrs) {
                    push @{$cluster_2_nodes{ _cluster_name( $df_rule->{'_funnel_dfr'} ) }}, _midpoint_name( $df_rule ); # top-level funnels define clusters (top-level "boxes")
229

230 231 232
                    foreach my $fan_dfr (@$fan_dfrs) {
                        print "Alternative entry into a box detected.\n" unless($fan_dfr->{'_funnel_dfr'} == $df_rule);
                        push @{$cluster_2_nodes{ _cluster_name( $fan_dfr->{'_funnel_dfr'} ) } }, _midpoint_name( $fan_dfr ); # midpoints of rules that have a funnel live inside "boxes"
233
                    }
234
                } elsif( UNIVERSAL::isa($df_rule->to_analysis, 'Bio::EnsEMBL::Hive::NakedTable') ) {    # table belongs to the same "box" as the dataflow source:
235

236
                    push @{$cluster_2_nodes{ _cluster_name( $df_rule->to_analysis->{'_funnel_dfr'} ) } }, $self->_table_node_name( $df_rule );
237
                }
238
            } # /foreach group
239 240 241
        }

        $self->graph->cluster_2_nodes( \%cluster_2_nodes );
242
        $self->graph->main_pipeline_name( $main_pipeline->hive_pipeline_name );
243 244 245
        $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')] );
246 247 248 249 250 251
    }

    return $self->graph();
}


252
sub _propagate_allocation {
253
    my ($self, $source_object, $source_rule, $curr_allocation ) = @_;
254

255
    $curr_allocation ||= $source_object->hive_pipeline->hive_pipeline_name;
256

257 258 259
    if(!exists $source_object->{'_funnel_dfr'} ) {     # only allocate on the first-come basis:
        $source_object->{'_funnel_dfr'} = $curr_allocation;
        if($source_rule) { $source_rule->{'_funnel_dfr'} = $curr_allocation; }
260

261
        if(UNIVERSAL::isa($source_object, 'Bio::EnsEMBL::Hive::Analysis')) {
262

263
            foreach my $group ( @{ $source_object->get_grouped_dataflow_rules } ) {
264

265
                my ($df_rule, $fan_dfrs) = @$group;
266

267 268
                my $target_object       = $df_rule->to_analysis
                    or die "Could not fetch a target object for url='".$df_rule->to_analysis_url."', please check your database for consistency.\n";
269

270
                unless(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Accumulator')) {
271

272 273 274 275
                    if($self->pipeline!=$target_object->hive_pipeline) {    # clear the allocation so it can be reset by the next recursive invocation
                        $curr_allocation = '';
                    }

276 277
                        # the funnel itself points at the factory analysis, to draw the funnel's midpoint outside of the box:
                    $self->_propagate_allocation( $target_object, @$fan_dfrs && $df_rule, $curr_allocation );
278

279 280 281 282 283 284 285
                        # all fan members point to the funnel:
                    foreach my $fan_dfr (@$fan_dfrs) {
                        $self->_propagate_allocation( $fan_dfr->to_analysis, $fan_dfr, $df_rule );
                    }
                } # /unless
            } # /foreach group
        } # if Analysis
286

287 288
    } elsif($source_rule) {
        $source_rule->{'_funnel_dfr'} = $source_object->{'_funnel_dfr'};    # correction for multiple entries into the same box (probably needs re-thinking)
289
    }
290 291
}

292

293
sub _add_pipeline_label {
294
    my ($self, $pipeline) = @_;
295

296 297 298 299 300
    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,
301
        label     => $pipeline_label,
302 303
        fontname  => $node_fontname,
        shape     => 'plaintext',
304
    );
305 306

    return $pipelabel_node_name;
307 308
}

309

310
sub _add_analysis_node {
311
    my ($self, $analysis) = @_;
312

313 314 315 316
    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

317
    my $analysis_stats = $analysis->stats();
318

319 320 321 322 323 324
    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');
    my $style                                             = $analysis->can_be_empty() ? 'dashed, filled' : 'filled' ;
    my $node_fontname                                     = $self->config_get('Node', 'AnalysisStatus', $analysis_status, 'Font');
    my $display_stats                                     = $self->config_get('DisplayStats');
325
    my $hive_pipeline                                     = $self->pipeline;
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343

    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;
344
    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>';
345 346 347 348 349 350 351 352 353
    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>};
        }
    }

354
    if( my $job_limit = $self->config_get('DisplayJobs') ) {
355 356 357 358 359
        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 );
        }

360
        my @jobs = @{ $analysis->jobs_collection };
361 362 363 364 365 366

        my $hit_limit;
        if(scalar(@jobs)>$job_limit) {
            pop @jobs;
            $hit_limit = 1;
        }
367 368 369 370 371

        $analysis_label    .= '<tr><td colspan="'.$colspan.'"> </td></tr>';
        foreach my $job (@jobs) {
            my $input_id = $job->input_id;
            my $status   = $job->status;
372
            my $job_id   = $job->dbID || 'unstored';
373 374 375 376 377
            $input_id=~s/\>/&gt;/g;
            $input_id=~s/\</&lt;/g;
            $input_id=~s/\{|\}//g;
            $analysis_label    .= qq{<tr><td colspan="$colspan" bgcolor="}.$self->config_get('Node', 'JobStatus', $status, 'Colour').qq{">$job_id [$status]: $input_id</td></tr>};
        }
378 379

        if($hit_limit) {
380
            $analysis_label    .= qq{<tr><td colspan="$colspan">[ + }.($total_job_count-$job_limit).qq{ more jobs ]</td></tr>};
381
        }
382 383
    }
    $analysis_label    .= '</table>>';
384
  
385
    $self->graph->add_node( $this_analysis_node_name,
386 387 388 389 390 391
        label       => $analysis_label,
        shape       => 'record',
        fontname    => $node_fontname,
        style       => $style,
        fillcolor   => $analysis_status_colour,
    );
Leo Gordon's avatar
Leo Gordon committed
392 393

    $self->_add_control_rules( $analysis->control_rules_collection );
394
    $self->_add_dataflow_rules( $analysis );
395 396

    return $this_analysis_node_name;
397 398 399
}


400
sub _add_control_rules {
401 402 403 404
    my ($self, $ctrl_rules) = @_;

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

406 407 408 409
        #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;
410

411 412
        my $ctrled_is_local     = $ctrled_analysis->is_local_to( $self->pipeline );
        my $condition_is_local  = $condition_analysis->is_local_to( $self->pipeline );
413

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

418
        next unless( $ctrled_is_local or $condition_is_local or $self->{'_foreign_analyses'}{ $condition_analysis->relative_display_name($self->pipeline) } );
419 420 421 422 423 424 425 426 427

        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',
        );
    }
428 429
}

430

431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475
sub _branch_and_template {
    my ($self, $df_rule) = @_;

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

    return '#'.$df_rule->branch_code.($input_id_template ? ":\n".$input_id_template : '');
}


sub _twopart_arrow {
    my ($self, $df_rule) = @_;

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

    my $midpoint_name       = _midpoint_name( $df_rule );

    my $from_node_name      = $self->_analysis_node_name( $df_rule->from_analysis );
    my $target_node_name    = $self->_analysis_node_name( $df_rule->to_analysis );

    $graph->add_node( $midpoint_name,   # midpoint itself
        color       => $dataflow_colour,
        label       => '',
        shape       => 'point',
        fixedsize   => 1,
        width       => 0.01,
        height      => 0.01,
    );
    $graph->add_edge( $from_node_name => $midpoint_name, # first half of the two-part arrow
        color       => $dataflow_colour,
        arrowhead   => 'none',
        fontname    => $df_edge_fontname,
        fontcolor   => $dataflow_colour,
        label       => $self->_branch_and_template( $df_rule ),
    );
    $graph->add_edge( $midpoint_name => $target_node_name,   # second half of the two-part arrow
        color     => $dataflow_colour,
    );

    return $midpoint_name;
}


476
sub _add_dataflow_rules {
477
    my ($self, $from_analysis) = @_;
478

479
    my $graph               = $self->graph();
480 481 482 483
    my $dataflow_colour     = $self->config_get('Edge', 'Data', 'Colour');
    my $semablock_colour    = $self->config_get('Edge', 'Semablock', 'Colour');
    my $accu_colour         = $self->config_get('Edge', 'Accu', 'Colour');
    my $df_edge_fontname    = $self->config_get('Edge', 'Data', 'Font');
484

485
    foreach my $group ( @{ $from_analysis->get_grouped_dataflow_rules } ) {
486

487
        my ($df_rule, $fan_dfrs) = @$group;
488

489
        my $from_node_name  = $self->_analysis_node_name( $from_analysis );
490
        my $target_node_name;
491
        my $target_object   = $df_rule->to_analysis
492 493
            or die "Could not fetch a target object for url='".$df_rule->to_analysis_url."', please check your database for consistency.\n";

494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511
        if(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Accumulator')) {

            my $funnel_analysis = $from_analysis->{'_funnel_dfr'}
                or die "Could not find funnel analysis for the ".$target_object->toString."\n";

                # one-part dashed arrow:
            $graph->add_edge( $from_node_name => _midpoint_name( $funnel_analysis ),
                color       => $accu_colour,
                style       => 'dashed',
                label       => '#'.$df_rule->branch_code.":\n".$target_object->relative_display_name( $self->pipeline ),
                fontname    => $df_edge_fontname,
                fontcolor   => $accu_colour,
                dir         => 'both',
                arrowtail   => 'crow',
            );
            next;

        } elsif(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Analysis')) {    # skip some *really* foreign dataflow rules:
512

513
            my $from_is_local   = $df_rule->from_analysis->is_local_to( $self->pipeline );
514
            my $target_is_local = $target_object->is_local_to( $self->pipeline );
515

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

520
            next unless( $from_is_local or $target_is_local or $self->{'_foreign_analyses'}{ $target_object->relative_display_name($self->pipeline) } );
521 522 523 524 525 526

            $target_node_name = $self->_add_analysis_node( $target_object );

        } elsif(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::NakedTable')) {

            $target_node_name = $self->_add_table_node( $df_rule );
527
        }
528

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

531
            my $funnel_midpoint_name = $self->_twopart_arrow( $df_rule );
532

533
            foreach my $fan_dfr (@$fan_dfrs) {
534 535 536 537 538 539

                my $fan_target_object = $fan_dfr->to_analysis;
                die "All semaphored fan rules must be wired to Analyses" unless(UNIVERSAL::isa($fan_target_object, 'Bio::EnsEMBL::Hive::Analysis'));

                $self->_add_analysis_node( $fan_target_object );

540
                my $fan_midpoint_name = $self->_twopart_arrow( $fan_dfr );
541

542 543
                    # add a semaphore inter-rule blocking arc:
                $graph->add_edge( $fan_midpoint_name => $funnel_midpoint_name,
544
                    color     => $semablock_colour,
545 546 547 548 549 550
                    style     => 'dashed',
                    arrowhead => 'tee',
                    dir       => 'both',
                    arrowtail => 'crow',
                );
            }
551

552
        } else {    # one-part solid arrow either to an analysis or to a table:
553
            $graph->add_edge( $from_node_name => $target_node_name,
554
                color       => $dataflow_colour,
555
                fontname    => $df_edge_fontname,
556
                fontcolor   => $dataflow_colour,
557
                label       => $self->_branch_and_template( $df_rule ),
558
            );
559
        }
560

561
    } # /foreach my $group
562 563
}

564

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

568 569 570 571
    my $node_fontname           = $self->config_get('Node', 'Table', 'Font');
    my $hive_pipeline           = $self->pipeline;
    my $naked_table             = $df_rule->to_analysis;
    my $this_table_node_name    = $self->_table_node_name( $df_rule );
572

573
    my (@column_names, $columns, $table_data, $data_limit, $hit_limit);
574

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

577
        @column_names = sort keys %{$naked_table_adaptor->column_set};
578
        $columns = scalar(@column_names);
579
        $table_data = $naked_table_adaptor->fetch_all( 'LIMIT '.($data_limit+1) );
580 581 582 583 584

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

587
    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>';
588

589
    if( $self->config_get('DisplayData') and $columns) {
590 591 592 593 594
        $table_label .= '<tr><td colspan="'.$columns.'"> </td></tr>';
        $table_label .= '<tr>'.join('', map { qq{<td bgcolor="lightblue" border="1">$_</td>} } @column_names).'</tr>';
        foreach my $row (@$table_data) {
            $table_label .= '<tr>'.join('', map { qq{<td>$_</td>} } @{$row}{@column_names}).'</tr>';
        }
595 596 597
        if($hit_limit) {
            $table_label  .= qq{<tr><td colspan="$columns">[ more data ]</td></tr>};
        }
598 599 600
    }
    $table_label .= '</table>>';

601
    $self->graph()->add_node( $this_table_node_name,
602 603 604 605 606
        label       => $table_label,
        shape       => 'record',
        fontname    => $node_fontname,
        style       => 'filled',
        fillcolor   => 'white',
607
    );
608 609

    return $this_table_node_name;
610 611
}

Leo Gordon's avatar
Leo Gordon committed
612
1;