Graph.pm 24.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, $naked_table) = @_;
138

139
    my $table_node_name = 'table_' . $naked_table->relative_display_name( $self->pipeline );
140
141
    $table_node_name=~s/\W/__/g;
    return $table_node_name;
142
143
}

144

145
146
147
148
149
150
151
sub _accu_sink_node_name {
    my ($funnel_dfr) = @_;

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


152
153
154
sub _cluster_name {
    my ($df_rule) = @_;

155
    return UNIVERSAL::isa($df_rule, 'Bio::EnsEMBL::Hive::DataflowRule') ? _midpoint_name($df_rule) : ($df_rule || '');
156
157
158
}


159
sub _midpoint_name {
160
    my ($df_rule) = @_;
161

162
    if($df_rule and scalar($df_rule)=~/\((\w+)\)/) {     # a unique id of a df_rule assuming dbIDs are not available
163
164
        return 'dfr_'.$1.'_mp';
    } else {
165
        throw("Wrong argument to _midpoint_name");
166
    }
167
168
}

169
170
171
172
173
174
175
176
177
178
179

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

182
    my $main_pipeline    = $self->pipeline;
183

184
    foreach my $source_analysis ( @{ $main_pipeline->get_source_analyses } ) {
185
186
            # run the recursion in each component that has a non-cyclic start:
        $self->_propagate_allocation( $source_analysis );
187
    }
188
    foreach my $cyclic_analysis ( $main_pipeline->collection_of( 'Analysis' )->list ) {
189
190
191
        next if(defined $cyclic_analysis->{'_funnel_dfr'});
        $self->_propagate_allocation( $cyclic_analysis );
    }
192

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

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

215
    my %cluster_2_nodes = ();
216

217
218
219
    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 );
220

221
            push @{$cluster_2_nodes{ $pipeline->hive_pipeline_name } }, $pipelabel_node_name;
222
        }
223
    }
224

225
    if($self->config_get('DisplaySemaphoreBoxes') ) {
226
        foreach my $analysis ( $main_pipeline->collection_of('Analysis')->list, values %{ $self->{'_foreign_analyses'} } ) {
227

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

230
            foreach my $group ( @{ $analysis->get_grouped_dataflow_rules } ) {
231

232
                my ($df_rule, $fan_dfrs) = @$group;
233

234
235
236
237
                my $df_targets  = $df_rule->get_my_targets;
                my $choice      = (scalar(@$df_targets)!=1) || defined($df_targets->[0]->on_condition);

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

240
241
                    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"
242
                    }
243
244
245
                }

                foreach my $df_target (@$df_targets) {
246
247
                    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:
248

249
250
251
252
253
                        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'} );
254
                    }
255
                }
256
            } # /foreach group
257
258
259
        }

        $self->graph->cluster_2_nodes( \%cluster_2_nodes );
260
        $self->graph->main_pipeline_name( $main_pipeline->hive_pipeline_name );
261
262
263
        $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')] );
264
265
266
267
268
269
    }

    return $self->graph();
}


270
sub _propagate_allocation {
271
    my ($self, $source_object, $curr_allocation ) = @_;
272

273
    $curr_allocation ||= $source_object->hive_pipeline->hive_pipeline_name;
274

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

278
        if(UNIVERSAL::isa($source_object, 'Bio::EnsEMBL::Hive::Analysis')) {
279

280
            foreach my $group ( @{ $source_object->get_grouped_dataflow_rules } ) {
281

282
                my ($df_rule, $fan_dfrs) = @$group;
283

284
285
286
287
                $df_rule->{'_funnel_dfr'} = $curr_allocation;

                foreach my $df_target (@{ $df_rule->get_my_targets }) {
                    my $target_object       = $df_target->to_analysis;
288

289
290
291
                        #   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 : '' );
                }
292
293
294
295

                    # 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) {
296
297
298
299
300
301
                    $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 : '' );
                    }
302
                }
303

304
            } # /foreach group
305
        } # if source_object isa Analysis
306
    }
307
308
}

309

310
sub _add_pipeline_label {
311
    my ($self, $pipeline) = @_;
312

313
314
315
316
317
    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,
318
        label     => $pipeline_label,
319
320
        fontname  => $node_fontname,
        shape     => 'plaintext',
321
    );
322
323

    return $pipelabel_node_name;
324
325
}

326

327
sub _add_analysis_node {
328
    my ($self, $analysis) = @_;
329

330
331
332
333
    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

334
    my $analysis_stats = $analysis->stats();
335

336
337
338
339
340
341
    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');
342
    my $hive_pipeline                                     = $self->pipeline;
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360

    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;
361
    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>';
362
363
364
365
366
367
368
369
370
    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>};
        }
    }

371
    if( my $job_limit = $self->config_get('DisplayJobs') ) {
372
373
374
375
376
        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 );
        }

377
        my @jobs = @{ $analysis->jobs_collection };
378
379
380
381
382
383

        my $hit_limit;
        if(scalar(@jobs)>$job_limit) {
            pop @jobs;
            $hit_limit = 1;
        }
384
385
386
387
388

        $analysis_label    .= '<tr><td colspan="'.$colspan.'"> </td></tr>';
        foreach my $job (@jobs) {
            my $input_id = $job->input_id;
            my $status   = $job->status;
389
            my $job_id   = $job->dbID || 'unstored';
390
391
392
393
394
            $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>};
        }
395
396

        if($hit_limit) {
397
            $analysis_label    .= qq{<tr><td colspan="$colspan">[ + }.($total_job_count-$job_limit).qq{ more jobs ]</td></tr>};
398
        }
399
400
    }
    $analysis_label    .= '</table>>';
401
  
402
    $self->graph->add_node( $this_analysis_node_name,
403
404
405
406
407
408
        label       => $analysis_label,
        shape       => 'record',
        fontname    => $node_fontname,
        style       => $style,
        fillcolor   => $analysis_status_colour,
    );
Leo Gordon's avatar
Leo Gordon committed
409
410

    $self->_add_control_rules( $analysis->control_rules_collection );
411
    $self->_add_dataflow_rules( $analysis );
412
413

    return $this_analysis_node_name;
414
415
416
}


417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
sub _add_accu_sink_node {
    my ($self, $funnel_dfr) = @_;

    my $accu_sink_node_name = _accu_sink_node_name( $funnel_dfr );

    $self->graph->add_node( $accu_sink_node_name,
#        shape       => 'noverhang',
#        shape       => 'invtriangle',
        shape       => 'invhouse',
        label       => '',
        style       => 'filled',
        fillcolor   => 'darkgreen',
    );

    return $accu_sink_node_name;
}


435
sub _add_control_rules {
436
437
438
439
    my ($self, $ctrl_rules) = @_;

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

441
442
443
444
        #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;
445

446
447
        my $ctrled_is_local     = $ctrled_analysis->is_local_to( $self->pipeline );
        my $condition_is_local  = $condition_analysis->is_local_to( $self->pipeline );
448

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

453
        next unless( $ctrled_is_local or $condition_is_local or $self->{'_foreign_analyses'}{ $condition_analysis->relative_display_name($self->pipeline) } );
454
455
456
457
458
459
460
461
462

        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',
        );
    }
463
464
}

465

466
467
468
469
470
471
472
473
474
475
476
477
sub _last_part_arrow {
    my ($self, $from_analysis, $source_node_name, $label_prefix, $df_target) = @_;

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

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

483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
        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,
        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,
    );
512
513
514
515
516
517
518
519
520
}


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

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

521
522
    my $from_analysis       = $df_rule->from_analysis;
    my $from_node_name      = $self->_analysis_node_name( $from_analysis );
523
    my $midpoint_name       = _midpoint_name( $df_rule );
524
525
    my $df_targets          = $df_rule->get_my_targets;
    my $choice              = (scalar(@$df_targets)!=1) || defined($df_targets->[0]->on_condition);
526
527

    $graph->add_node( $midpoint_name,   # midpoint itself
528
529
530
531
532
533
534
535
536
537
        color       => 'black',
        $choice ? (
            shape   => 'diamond',
            label   => scalar(@$df_targets)==1 ? 'Filter' : 'Switch',
        ) : (
            shape       => 'point',
            fixedsize   => 1,
            width       => 0.01,
            height      => 0.01,
        ),
538
539
    );
    $graph->add_edge( $from_node_name => $midpoint_name, # first half of the two-part arrow
540
541
        color       => 'black',
        fontcolor   => 'black',
542
        fontname    => $df_edge_fontname,
543
544
545
546
547
548
549
        label       => '#'.$df_rule->branch_code,
        $choice ? (
            headport    => 'n',
            arrowhead   => 'normal',
        ) : (
            arrowhead   => 'none',
        ),
550
551
    );

552
553
554
555
556
    foreach my $df_target (@$df_targets) {
        my $condition = $df_target->on_condition;
        $self->_last_part_arrow($from_analysis, $midpoint_name, $condition ? "IF $condition" : $choice ? 'ELSE' : '', $df_target);
    }

557
558
559
560
    return $midpoint_name;
}


561
sub _add_dataflow_rules {
562
    my ($self, $from_analysis) = @_;
563

564
    my $graph               = $self->graph();
565
    my $semablock_colour    = $self->config_get('Edge', 'Semablock', 'Colour');
566

567
    foreach my $group ( @{ $from_analysis->get_grouped_dataflow_rules } ) {
568

569
        my ($df_rule, $fan_dfrs) = @$group;
570

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

573
            my $funnel_midpoint_name = $self->_twopart_arrow( $df_rule );
574

575
576
            foreach my $fan_dfr (@$fan_dfrs) {
                my $fan_midpoint_name = $self->_twopart_arrow( $fan_dfr );
577

578
579
                    # add a semaphore inter-rule blocking arc:
                $graph->add_edge( $fan_midpoint_name => $funnel_midpoint_name,
580
                    color     => $semablock_colour,
581
582
                    style     => 'dashed',
                    dir       => 'both',
583
                    arrowhead => 'tee',
584
585
586
                    arrowtail => 'crow',
                );
            }
587

588
589
590
591
592
593
594
595
596
597
598
599
        } else {
            my $df_targets  = $df_rule->get_my_targets;
            my $choice      = (scalar(@$df_targets)!=1) || defined($df_targets->[0]->on_condition);

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

                $self->_last_part_arrow($from_analysis, $from_node_name, '#'.$df_rule->branch_code, $df_target);
            }
600
        }
601

602
    } # /foreach my $group
603
604
}

605

606
sub _add_table_node {
607
    my ($self, $naked_table) = @_;
608

609
610
    my $node_fontname           = $self->config_get('Node', 'Table', 'Font');
    my $hive_pipeline           = $self->pipeline;
611
    my $this_table_node_name    = $self->_table_node_name( $naked_table );
612

613
    my (@column_names, $columns, $table_data, $data_limit, $hit_limit);
614

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

617
        @column_names = sort keys %{$naked_table_adaptor->column_set};
618
        $columns = scalar(@column_names);
619
        $table_data = $naked_table_adaptor->fetch_all( 'LIMIT '.($data_limit+1) );
620
621
622
623
624

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

627
    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>';
628

629
    if( $self->config_get('DisplayData') and $columns) {
630
631
632
633
634
        $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>';
        }
635
636
637
        if($hit_limit) {
            $table_label  .= qq{<tr><td colspan="$columns">[ more data ]</td></tr>};
        }
638
639
640
    }
    $table_label .= '</table>>';

641
    $self->graph()->add_node( $this_table_node_name,
642
643
644
645
646
        label       => $table_label,
        shape       => 'record',
        fontname    => $node_fontname,
        style       => 'filled',
        fillcolor   => 'white',
647
    );
648
649

    return $this_table_node_name;
650
651
}

Leo Gordon's avatar
Leo Gordon committed
652
1;