Graph.pm 27.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', concentrate => 'true', ratio => qq{compress"; pad = "$padding}  ); # injection hack!
104
    }
105
    return $self->{'_graph'};
106
107
108
}


109
=head2 pipeline()
110

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

=cut

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

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

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


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

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

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


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

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

146

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

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

155

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

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


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

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


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

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

180
181
182
183
184
185
186
187
188
189
190

=head2 build()

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

=cut

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

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

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

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

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

226
    my %cluster_2_nodes = ();
227

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return $self->graph();
}


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

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

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

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

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

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

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

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

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

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

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

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

319

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

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

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

    return $pipelabel_node_name;
334
335
}

336

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return $this_analysis_node_name;
426
427
428
}


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

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

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

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

    return $accu_sink_node_name;
}


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

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

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

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

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

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

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

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

483

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

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

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

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

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

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

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

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

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


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

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

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

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

    foreach my $i (0..scalar(@$df_targets)-1) {
        my $df_target = $df_targets->[$i];
        my $condition = $df_target->on_condition;
558
559
        if($display_cond_length) {
            if(defined($condition)) {
560
561
562
                $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)
563
564
565
566
567
568
569
                $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;
        }
570
571
572
        $tablabel .= qq{<tr><td port="cond_$i">}.($condition ? "WHEN $condition" : $choice ? 'ELSE' : '')."</td></tr>";
    }
    $tablabel .= '</table>>';
573
574

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

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

607
608
609
610
    return $midpoint_name;
}


611
sub _add_dataflow_rules {
612
    my ($self, $from_analysis) = @_;
613

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

617
    foreach my $group ( @{ $self->_grouped_dataflow_rules($from_analysis) } ) {
618

619
        my ($df_rule, $fan_dfrs, $df_targets) = @$group;
620

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

623
            my $funnel_midpoint_name = $self->_twopart_arrow( $df_rule, $df_targets );
624

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

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

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

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

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

651
    } # /foreach my $group
652
653
}

654

655
sub _add_table_node {
656
    my ($self, $naked_table) = @_;
657

658
659
    my $table_shape             = $self->config_get('Node', 'Table', 'Shape');
    my $table_style             = $self->config_get('Node', 'Table', 'Style');
660
661
662
663
664
    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');

665
    my $hive_pipeline           = $self->pipeline;
666
    my $this_table_node_name    = $self->_table_node_name( $naked_table );
667

668
    my (@column_names, $columns, $table_data, $data_limit, $hit_limit);
669

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

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

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

682
    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>';
683

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

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

    return $this_table_node_name;
707
708
}

Leo Gordon's avatar
Leo Gordon committed
709
1;