Graph.pm 27.8 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
267
                        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'} );
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
314
315
                    $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 : '' );
                    }
316
                }
317

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

323

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

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

    return $pipelabel_node_name;
338
339
}

340

341
sub _add_analysis_node {
342
    my ($self, $analysis) = @_;
343

344
345
346
347
    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

348
    my $analysis_stats = $analysis->stats();
349

350
351
352
    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');
353
354
    my $analysis_shape                                    = $self->config_get('Node', 'AnalysisStatus', 'Shape');
    my $analysis_style                                    = $analysis->can_be_empty() ? 'dashed, filled' : 'filled' ;
355
356
    my $node_fontname                                     = $self->config_get('Node', 'AnalysisStatus', $analysis_status, 'Font');
    my $display_stats                                     = $self->config_get('DisplayStats');
357
    my $hive_pipeline                                     = $self->pipeline;
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375

    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;
376
    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>';
377
378
379
380
381
382
383
384
385
    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>};
        }
    }

386
    if( my $job_limit = $self->config_get('DisplayJobs') ) {
387
388
389
390
391
        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 );
        }

392
        my @jobs = @{ $analysis->jobs_collection };
393
394
395
396
397
398

        my $hit_limit;
        if(scalar(@jobs)>$job_limit) {
            pop @jobs;
            $hit_limit = 1;
        }
399
400
401
402
403

        $analysis_label    .= '<tr><td colspan="'.$colspan.'"> </td></tr>';
        foreach my $job (@jobs) {
            my $input_id = $job->input_id;
            my $status   = $job->status;
404
            my $job_id   = $job->dbID || 'unstored';
405
406
407
            $input_id=~s/\>/&gt;/g;
            $input_id=~s/\</&lt;/g;
            $input_id=~s/\{|\}//g;
Leo Gordon's avatar
Leo Gordon committed
408
            $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>};
409
        }
410
411

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

    $self->_add_control_rules( $analysis->control_rules_collection );
427
    $self->_add_dataflow_rules( $analysis );
428
429

    return $this_analysis_node_name;
430
431
432
}


433
434
435
sub _add_accu_sink_node {
    my ($self, $funnel_dfr) = @_;

436
437
438
439
440
441
    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');

442
443
444
    my $accu_sink_node_name = _accu_sink_node_name( $funnel_dfr );

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

    return $accu_sink_node_name;
}


457
sub _add_control_rules {
458
459
460
461
    my ($self, $ctrl_rules) = @_;

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

463
464
465
466
        #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;
467

468
469
        my $ctrled_is_local     = $ctrled_analysis->is_local_to( $self->pipeline );
        my $condition_is_local  = $condition_analysis->is_local_to( $self->pipeline );
470

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

475
        next unless( $ctrled_is_local or $condition_is_local or $self->{'_foreign_analyses'}{ $condition_analysis->relative_display_name($self->pipeline) } );
476
477
478
479
480
481
482
483
484

        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',
        );
    }
485
486
}

487

488
sub _last_part_arrow {
489
    my ($self, $from_analysis, $source_node_name, $label_prefix, $df_target, $extras) = @_;
490
491
492
493
494
495
496
497
498
499

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

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

505
506
507
508
509
510
511
512
513
514
515
        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;
516
517
518
519
    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 ? '' : '') );
520
521

    $graph->add_edge( $source_node_name => $target_node_name,
522
        @$extras,
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
        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,
    );
538
539
540
541
}


sub _twopart_arrow {
542
    my ($self, $df_rule, $df_targets) = @_;
543
544
545

    my $graph               = $self->graph();
    my $df_edge_fontname    = $self->config_get('Edge', 'Data', 'Font');
546
547
548
549
550
    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');
551
    my $display_cond_length = $self->config_get('DisplayConditionLength');
552

553
554
    my $from_analysis       = $df_rule->from_analysis;
    my $from_node_name      = $self->_analysis_node_name( $from_analysis );
555
    my $midpoint_name       = _midpoint_name( $df_rule );
556
557

       $df_targets        ||= $df_rule->get_my_targets;
558
    my $choice              = (scalar(@$df_targets)!=1) || defined($df_targets->[0]->on_condition);
559
560
561
    #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>};

Leo Gordon's avatar
Leo Gordon committed
562
563
564
565
566
567
    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];

568
569
        if($display_cond_length) {
            if(defined($condition)) {
570
571
572
                $condition=~s{^(.{$display_cond_length}).+}{$1 \.\.\.};     # shorten down to $display_cond_length characters

                $condition=~s{&}{&amp;}g;   # Since we are in HTML context now, ampersands should be escaped (first thing after trimming)
573
574
575
576
577
578
579
                $condition=~s{"}{&quot;}g;  # should fix a string display bug for pre-2.16 GraphViz'es
                $condition=~s{<}{&lt;}g;
                $condition=~s{>}{&gt;}g;
            }
        } else {
            $condition &&= 'condition_'.$i;
        }
580
581
582
        $tablabel .= qq{<tr><td port="cond_$i">}.($condition ? "WHEN $condition" : $choice ? 'ELSE' : '')."</td></tr>";
    }
    $tablabel .= '</table>>';
583
584

    $graph->add_node( $midpoint_name,   # midpoint itself
585
        $choice ? (
586
587
            shape       => 'record',
            comment     => qq{new_shape:$switch_shape},
588
589
590
591
            style       => $switch_style,
            fillcolor   => $switch_colour,
            fontname    => $switch_font,
            fontcolor   => $switch_fontcolour,
592
            label       => $tablabel,
593
594
595
596
597
598
        ) : (
            shape       => 'point',
            fixedsize   => 1,
            width       => 0.01,
            height      => 0.01,
        ),
599
600
    );
    $graph->add_edge( $from_node_name => $midpoint_name, # first half of the two-part arrow
601
602
        color       => 'black',
        fontcolor   => 'black',
603
        fontname    => $df_edge_fontname,
604
        label       => '#'.$df_rule->branch_code,
605
        headport    => 'n',
606
607
608
609
610
        $choice ? (
            arrowhead   => 'normal',
        ) : (
            arrowhead   => 'none',
        ),
611
612
    );

Leo Gordon's avatar
Leo Gordon committed
613
614
615
616
617
618
619
    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' ]);
        }
620
621
    }

622
623
624
625
    return $midpoint_name;
}


626
sub _add_dataflow_rules {
627
    my ($self, $from_analysis) = @_;
628

629
    my $graph               = $self->graph();
630
    my $semablock_colour    = $self->config_get('Edge', 'Semablock', 'Colour');
631

632
    foreach my $group ( @{ $self->_grouped_dataflow_rules($from_analysis) } ) {
633

634
        my ($df_rule, $fan_dfrs, $df_targets) = @$group;
635

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

638
            my $funnel_midpoint_name = $self->_twopart_arrow( $df_rule, $df_targets );
639

640
641
            foreach my $fan_dfr (@$fan_dfrs) {
                my $fan_midpoint_name = $self->_twopart_arrow( $fan_dfr );
642

643
644
                    # add a semaphore inter-rule blocking arc:
                $graph->add_edge( $fan_midpoint_name => $funnel_midpoint_name,
645
                    color     => $semablock_colour,
646
647
                    style     => 'dashed',
                    dir       => 'both',
648
                    arrowhead => 'tee',
649
650
651
                    arrowtail => 'crow',
                );
            }
652

653
654
655
656
        } else {
            my $choice      = (scalar(@$df_targets)!=1) || defined($df_targets->[0]->on_condition);

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

662
                $self->_last_part_arrow($from_analysis, $from_node_name, '#'.$df_rule->branch_code, $df_target, []);
663
            }
664
        }
665

666
    } # /foreach my $group
667
668
}

669

670
sub _add_table_node {
671
    my ($self, $naked_table) = @_;
672

673
674
    my $table_shape             = $self->config_get('Node', 'Table', 'Shape');
    my $table_style             = $self->config_get('Node', 'Table', 'Style');
675
676
677
678
679
    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');

680
    my $hive_pipeline           = $self->pipeline;
681
    my $this_table_node_name    = $self->_table_node_name( $naked_table );
682

683
    my (@column_names, $columns, $table_data, $data_limit, $hit_limit);
684

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

687
        @column_names = sort keys %{$naked_table_adaptor->column_set};
688
        $columns = scalar(@column_names);
689
        $table_data = $naked_table_adaptor->fetch_all( 'LIMIT '.($data_limit+1) );
690
691
692
693
694

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

697
    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>';
698

699
    if( $self->config_get('DisplayData') and $columns) {
700
        $table_label .= '<tr><td colspan="'.$columns.'"> </td></tr>';
701
        $table_label .= '<tr>'.join('', map { qq{<td bgcolor="$table_header_colour" border="1">$_</td>} } @column_names).'</tr>';
702
703
704
        foreach my $row (@$table_data) {
            $table_label .= '<tr>'.join('', map { qq{<td>$_</td>} } @{$row}{@column_names}).'</tr>';
        }
705
706
707
        if($hit_limit) {
            $table_label  .= qq{<tr><td colspan="$columns">[ more data ]</td></tr>};
        }
708
709
710
    }
    $table_label .= '</table>>';

711
    $self->graph()->add_node( $this_table_node_name,
712
        shape       => 'record',
713
714
        comment     => qq{new_shape:$table_shape},
        style       => $table_style,
715
716
717
718
        fillcolor   => $table_colour,
        fontname    => $table_fontname,
        fontcolor   => $table_fontcolour,
        label       => $table_label,
719
    );
720
721

    return $this_table_node_name;
722
723
}

Leo Gordon's avatar
Leo Gordon committed
724
1;