Graph.pm 22.2 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);
52
use Bio::EnsEMBL::Hive::Utils::GraphViz;
53
use Bio::EnsEMBL::Hive::Utils::Config;
54

55
56
use base ('Bio::EnsEMBL::Hive::Configurable');

57
58
59

=head2 new()

60
  Arg [1] : Bio::EnsEMBL::Hive::HivePipeline $pipeline;
61
62
63
64
65
              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.
66
67
68
69
70
71
72
  Returntype : Graph object
  Exceptions : If the parameters are not as required
  Status     : Beta
  
=cut

sub new {
73
    my $class       = shift @_;
74
    my $pipeline    = shift @_;
75

76
    my $self = bless({}, ref($class) || $class);
77

78
79
    $self->pipeline( $pipeline );

80
81
82
    my $config = Bio::EnsEMBL::Hive::Utils::Config->new( @_ );
    $self->config($config);
    $self->context( [ 'Graph' ] );
83

84
    return $self;
85
86
87
88
89
90
91
92
93
94
95
96
97
}


=head2 graph()

  Arg [1] : The GraphViz instance created by this module
  Returntype : GraphViz
  Exceptions : None
  Status     : Beta

=cut

sub graph {
98
99
    my ($self) = @_;

100
    if(! exists $self->{'_graph'}) {
101
        my $padding  = $self->config_get('Pad') || 0;
102
        $self->{'_graph'} = Bio::EnsEMBL::Hive::Utils::GraphViz->new( name => 'AnalysisWorkflow', ratio => qq{compress"; pad = "$padding}  ); # injection hack!
103
    }
104
    return $self->{'_graph'};
105
106
107
}


108
=head2 pipeline()
109

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

=cut

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

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

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


126
sub _analysis_node_name {
127
    my ($self, $analysis) = @_;
128

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

134

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

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

144

145
sub _midpoint_name {
146
    my ($df_rule) = @_;
147

148
149
150
151
152
    if(scalar($df_rule)=~/\((\w+)\)/) {     # a unique id of a df_rule assuming dbIDs are not available
        return 'dfr_'.$1.'_mp';
    } else {
        die "Wrong argument to _midpoint_name";
    }
153
154
}

155
156
157
158
159
160
161
162
163
164
165

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

168
    my $pipeline    = $self->pipeline;
169

170
    foreach my $df_rule ( $pipeline->collection_of('DataflowRule')->list ) {
171

172
173
        unless( $pipeline->collection_of('Analysis')->find_one_by('logic_name', $df_rule->to_analysis_url) ) {
            my $target_object = $df_rule->to_analysis
174
175
176
                or die "Could not fetch a target object for url='".$df_rule->to_analysis_url."', please check your database for consistency.\n";

            if( UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Analysis') ) { # dataflow target is a foreign Analysis
177
                $pipeline->collection_of('Analysis')->add_once( $target_object );  # add it to the collection
178
179
180
181
182
            } elsif( UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::NakedTable') ) {
            } elsif( UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Accumulator') ) {
            } else {
                warn "Do not know how to handle the type '".ref($target_object)."'";
            }
183
        }
184
185
    }

186
187
    foreach my $c_rule ( $pipeline->collection_of('AnalysisCtrlRule')->list ) {   # control rule's condition is a foreign Analysis
        unless( $pipeline->collection_of('Analysis')->find_one_by('logic_name', $c_rule->condition_analysis_url )) {
188
            my $condition_analysis = $c_rule->condition_analysis();
189
            $pipeline->collection_of('Analysis')->add_once( $condition_analysis ); # add it to the collection
190
191
192
        }
    }

193
194
195
196
197
198
199
200
201
    if( my $job_limit = $self->config_get('DisplayJobs') ) {
        foreach my $analysis ( $pipeline->collection_of('Analysis')->list ) {
            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 );
            }
        }
    }

202
203
        # NB: this is a very approximate algorithm with rough edges!
        # It will not find all start nodes in cyclic components!
204
    foreach my $source_analysis ( $pipeline->collection_of('Analysis')->list ) {
205
        my $is_foreign = $source_analysis->hive_pipeline != $pipeline;
206
        if( !$source_analysis->inflow_rules_count and !$is_foreign ) {    # if there is no dataflow into this analysis
207
                # run the recursion in each component that has a non-cyclic start:
208
            $self->_propagate_allocation( $source_analysis );
209
210
211
        }
    }

212
213
    if( $self->config_get('DisplayDetails') ) {
        $self->_add_pipeline_label( $pipeline->display_name );
214
    }
Leo Gordon's avatar
Leo Gordon committed
215

216
    foreach my $analysis ( $pipeline->collection_of('Analysis')->list ) {
217
        $self->_add_analysis_node($analysis);
218
219
    }

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

233
    if($self->config_get('DisplaySemaphoreBoxes') ) {
234
235
        my %cluster_2_nodes = ();

236
        foreach my $analysis ( $pipeline->collection_of('Analysis')->list ) {
237
            if(my $funnel = $analysis->{'_funnel_dfr'}) {
238
                push @{$cluster_2_nodes{ _midpoint_name( $funnel ) } }, $self->_analysis_node_name( $analysis );
239
            }
240
241

            foreach my $df_rule ( @{ $analysis->dataflow_rules_collection } ) {
242
                if( $df_rule->is_a_funnel_rule and ! $df_rule->{'_funnel_dfr'} ) {
243
244
245

                    push @{$cluster_2_nodes{ '' }}, _midpoint_name( $df_rule );     # top-level funnels define clusters (top-level "boxes")

246
                } elsif( UNIVERSAL::isa($df_rule->to_analysis, 'Bio::EnsEMBL::Hive::NakedTable') ) {
247
248
249
250
251
252
253
254
255

                    if(my $funnel = $df_rule->to_analysis->{'_funnel_dfr'}) {
                        push @{$cluster_2_nodes{ _midpoint_name( $funnel ) } }, $self->_table_node_name( $df_rule );    # table belongs to the same "box" as the dataflow source
                    }
                }

                if(my $funnel = $df_rule->{'_funnel_dfr'}) {
                    push @{$cluster_2_nodes{ _midpoint_name( $funnel ) } }, _midpoint_name( $df_rule ); # midpoints of rules that have a funnel live inside "boxes"
                }
256
257
258
259
            }
        }

        $self->graph->cluster_2_nodes( \%cluster_2_nodes );
260
261
        $self->graph->colour_scheme( $self->config_get('Box', 'ColourScheme') );
        $self->graph->colour_offset( $self->config_get('Box', 'ColourOffset') );
262
263
264
265
266
267
    }

    return $self->graph();
}


268
269
sub _propagate_allocation {
    my ($self, $source_analysis ) = @_;
270

Leo Gordon's avatar
Leo Gordon committed
271
    foreach my $df_rule ( @{ $source_analysis->dataflow_rules_collection } ) {
272
273
274
        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";

275
        my $target_node_name;
276

277
        if(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Analysis')) {
278
            $target_node_name = $self->_analysis_node_name( $target_object );
279
        } elsif(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::NakedTable')) {
280
            $target_node_name = $self->_table_node_name( $df_rule );
281
        } elsif(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Accumulator')) {
282
            next;
283
        } else {
284
            warn("Do not know how to handle the type '".ref($target_object)."'");
285
            next;
286
        }
287

288
289
        my $proposed_funnel_dfr;    # will depend on whether we start a new semaphore

290
        my $funnel_dataflow_rule  = $df_rule->funnel_dataflow_rule();
291
292
        if( $funnel_dataflow_rule ) {   # if there is a new semaphore, the dfrs involved (their midpoints) will also have to be allocated
            $proposed_funnel_dfr = $funnel_dataflow_rule;       # if we do start a new semaphore, report to the new funnel (based on common funnel rule's midpoint)
293

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

296
            $funnel_dataflow_rule->{'_funnel_dfr'} = $source_analysis->{'_funnel_dfr'}; # draw the funnel's midpoint outside of the box
297
        } else {
298
            $proposed_funnel_dfr = $source_analysis->{'_funnel_dfr'} || ''; # if we don't start a new semaphore, inherit the allocation of the source
299
        }
300

301
            # we allocate on first-come basis at the moment:
302
        if( exists $target_object->{'_funnel_dfr'} ) {  # node is already allocated?
303

304
305
306
307
            my $known_funnel_dfr = $target_object->{'_funnel_dfr'};

            if( $known_funnel_dfr eq $proposed_funnel_dfr) {
                # warn "analysis '$target_node_name' has already been allocated to the same '$known_funnel_dfr' by another branch";
308
            } else {
309
                # warn "analysis '$target_node_name' has already been allocated to '$known_funnel_dfr' however this branch would allocate it to '$proposed_funnel_dfr'";
310
311
            }

312
            if($funnel_dataflow_rule) {  # correction for multiple entries into the same box (probably needs re-thinking)
313
                $df_rule->{'_funnel_dfr'} = $target_object->{'_funnel_dfr'};
314
315
316
            }

        } else {
317
318
            # warn "allocating analysis '$target_node_name' to '$proposed_funnel_dfr'";
            $target_object->{'_funnel_dfr'} = $proposed_funnel_dfr;
319

320
            if(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Analysis')) {
321
                $self->_propagate_allocation( $target_object );
322
            }
323
324
        }
    }
325
326
}

327

328
329
sub _add_pipeline_label {
    my ($self, $pipeline_label) = @_;
330

331
    my $node_fontname  = $self->config_get('Node', 'Details', 'Font');
332
    $self->graph()->add_node( 'Details',
333
        label     => $pipeline_label,
334
335
        fontname  => $node_fontname,
        shape     => 'plaintext',
336
337
338
    );
}

339

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

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

345
346
347
348
349
350
    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');
351
    my $hive_pipeline                                     = $self->pipeline;
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369

    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;
370
    my $analysis_label  = '<<table border="0" cellborder="0" cellspacing="0" cellpadding="1"><tr><td colspan="'.$colspan.'">'.$analysis->display_name( $hive_pipeline ).' ('.($analysis->dbID || 'unstored').')</td></tr>';
371
372
373
374
375
376
377
378
379
    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>};
        }
    }

380
    if( my $job_limit = $self->config_get('DisplayJobs') ) {
381
        my @jobs = @{ $analysis->jobs_collection };
382
383
384
385
386
387

        my $hit_limit;
        if(scalar(@jobs)>$job_limit) {
            pop @jobs;
            $hit_limit = 1;
        }
388
389
390
391
392

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

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

    $self->_add_control_rules( $analysis->control_rules_collection );
    $self->_add_dataflow_rules( $analysis->dataflow_rules_collection );
416
417
418
}


419
sub _add_control_rules {
420
  my ($self, $ctrl_rules) = @_;
421
  
422
  my $control_colour = $self->config_get('Edge', 'Control', 'Colour');
423
424
  my $graph = $self->graph();

425
      #The control rules are always from and to an analysis so no need to search for odd cases here
426
  foreach my $c_rule ( @$ctrl_rules ) {
427
428
    my $from_node_name = $self->_analysis_node_name( $c_rule->condition_analysis );
    my $to_node_name   = $self->_analysis_node_name( $c_rule->ctrled_analysis );
429
430

    $graph->add_edge( $from_node_name => $to_node_name,
431
      color => $control_colour,
432
      arrowhead => 'tee',
433
434
    );
  }
435
436
}

437

438
sub _add_dataflow_rules {
439
    my ($self, $dataflow_rules) = @_;
440

441
    my $graph = $self->graph();
442
443
444
445
    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');
446

447
    foreach my $df_rule ( @$dataflow_rules ) {
448
    
449
450
451
        my ($from_analysis, $branch_code, $funnel_dataflow_rule) =
            ($df_rule->from_analysis, $df_rule->branch_code, $df_rule->funnel_dataflow_rule);

452
453
454
        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;

455
456
457
        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";

458
        my $from_node_name = $self->_analysis_node_name( $from_analysis );
459
        my $target_node_name;
460
    
461
            # Different treatment for analyses and tables:
462
        if(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Analysis')) {
463

464
            $target_node_name = $self->_analysis_node_name( $target_object );
465

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

468
            $target_node_name = $self->_table_node_name( $df_rule );
469
            $self->_add_table_node($target_node_name, $target_object);
470

471
        } elsif(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Accumulator')) {
472
473

            $target_node_name = _midpoint_name( $from_analysis->{'_funnel_dfr'} );
474

475
        } else {
476
            warn("Do not know how to handle the type '".ref($target_object)."'");
477
478
479
            next;
        }

480
            # a rule needs a midpoint either if it HAS a funnel or if it IS a funnel
481
        if( $funnel_dataflow_rule or $df_rule->is_a_funnel_rule ) {
482
            my $midpoint_name = _midpoint_name( $df_rule );
483

484
            $graph->add_node( $midpoint_name,   # midpoint itself
485
                color       => $dataflow_colour,
486
487
                label       => '',
                shape       => 'point',
488
489
490
                fixedsize   => 1,
                width       => 0.01,
                height      => 0.01,
491
            );
492
            $graph->add_edge( $from_node_name => $midpoint_name, # first half of the two-part arrow
493
                color       => $dataflow_colour,
494
                arrowhead   => 'none',
495
                fontname    => $df_edge_fontname,
496
                fontcolor   => $dataflow_colour,
497
                label       => '#'.$branch_code.($input_id_template ? ":\n".$input_id_template : ''),
498
            );
499
            $graph->add_edge( $midpoint_name => $target_node_name,   # second half of the two-part arrow
500
                color     => $dataflow_colour,
501
            );
502
            if($funnel_dataflow_rule) {
503
                $graph->add_edge( $midpoint_name => _midpoint_name( $funnel_dataflow_rule ),   # semaphore inter-rule link
504
                    color     => $semablock_colour,
505
506
507
508
509
510
                    style     => 'dashed',
                    arrowhead => 'tee',
                    dir       => 'both',
                    arrowtail => 'crow',
                );
            }
511
        } elsif(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Accumulator')) {
512
                # one-part dashed arrow:
513
            $graph->add_edge( $from_node_name => $target_node_name,
514
515
                color       => $accu_colour,
                style       => 'dashed',
516
                label       => '#'.$branch_code.":\n".$target_object->display_name( $self->pipeline ),
517
518
519
520
521
                fontname    => $df_edge_fontname,
                fontcolor   => $accu_colour,
                dir         => 'both',
                arrowtail   => 'crow',
            );
522
        } else {
523
                # one-part solid arrow:
524
            $graph->add_edge( $from_node_name => $target_node_name,
525
                color       => $dataflow_colour,
526
                fontname    => $df_edge_fontname,
527
                fontcolor   => $dataflow_colour,
528
                label       => '#'.$branch_code.($input_id_template ? ":\n".$input_id_template : ''),
529
            );
530
        } # /if( "$df_rule needs a midpoint" )
531
    } # /foreach my $df_rule (@$dataflow_rules)
532

533
534
}

535

536
sub _add_table_node {
537
    my ($self, $table_node_name, $naked_table) = @_;
538

539
    my $node_fontname   = $self->config_get('Node', 'Table', 'Font');
540
    my (@column_names, $columns, $table_data, $data_limit, $hit_limit);
541

542
    my $hive_pipeline   = $self->pipeline;
543

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

546
        @column_names = sort keys %{$naked_table_adaptor->column_set};
547
        $columns = scalar(@column_names);
548
        $table_data = $naked_table_adaptor->fetch_all( 'LIMIT '.($data_limit+1) );
549
550
551
552
553

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

556
    my $table_label = '<<table border="0" cellborder="0" cellspacing="0" cellpadding="1"><tr><td colspan="'.($columns||1).'">'. $naked_table->display_name( $hive_pipeline ) .'</td></tr>';
557

558
    if( $self->config_get('DisplayData') and $columns) {
559
560
561
562
563
        $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>';
        }
564
565
566
        if($hit_limit) {
            $table_label  .= qq{<tr><td colspan="$columns">[ more data ]</td></tr>};
        }
567
568
569
    }
    $table_label .= '</table>>';

570
    $self->graph()->add_node( $table_node_name, 
571
572
573
574
575
        label => $table_label,
        shape => 'record',
        fontname => $node_fontname,
        color => $self->config_get('Node', 'Table', 'Colour'),
    );
576
577
}

Leo Gordon's avatar
Leo Gordon committed
578
1;