Graph.pm 22.7 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

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->relative_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->relative_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
    if($df_rule and scalar($df_rule)=~/\((\w+)\)/) {     # a unique id of a df_rule assuming dbIDs are not available
149
150
        return 'dfr_'.$1.'_mp';
    } else {
151
        throw("Wrong argument to _midpoint_name");
152
    }
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
171
172
173
174
175
        # FIXME: using this approach we will never reach cyclic components that lack zero-inflow source nodes!
        #        But we have to start somewhere...
        #
    foreach my $source_analysis ( @{ $pipeline->get_source_analyses } ) {
            # run the recursion in each component that has a non-cyclic start:
        $self->_propagate_allocation( $source_analysis );
176
177
    }

178
179
    if( $self->config_get('DisplayDetails') ) {
        $self->_add_pipeline_label( $pipeline->display_name );
180
    }
Leo Gordon's avatar
Leo Gordon committed
181

182
    foreach my $analysis ( $pipeline->collection_of('Analysis')->list ) {
183
        $self->_add_analysis_node($analysis);
184
    }
185
186
187
    foreach my $foreign_analysis ( values %{ $self->{'_foreign_analyses'}} ) {
        $self->_add_analysis_node($foreign_analysis);
    }
188

189
    if($self->config_get('DisplayStretched') ) {    # put each analysis before its' funnel midpoint
190
        foreach my $analysis ( $pipeline->collection_of('Analysis')->list ) {
191
            if($analysis->{'_funnel_dfr'}) {    # this should only affect analyses that have a funnel
192
                my $from = $self->_analysis_node_name( $analysis );
193
                my $to   = _midpoint_name( $analysis->{'_funnel_dfr'} );
194
                $self->graph->add_edge( $from => $to,
195
196
197
198
199
200
201
                    color     => 'black',
                    style     => 'invis',   # toggle visibility by changing 'invis' to 'dashed'
                );
            }
        }
    }

202
    if($self->config_get('DisplaySemaphoreBoxes') ) {
203
204
        my %cluster_2_nodes = ();

205
        foreach my $analysis ( $pipeline->collection_of('Analysis')->list ) {
206
            if(my $funnel = $analysis->{'_funnel_dfr'}) {
207
                push @{$cluster_2_nodes{ _midpoint_name( $funnel ) } }, $self->_analysis_node_name( $analysis );
208
            }
209
210

            foreach my $df_rule ( @{ $analysis->dataflow_rules_collection } ) {
211
                if( $df_rule->is_a_funnel_rule and ! $df_rule->{'_funnel_dfr'} ) {
212
213
214

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

215
                } elsif( UNIVERSAL::isa($df_rule->to_analysis, 'Bio::EnsEMBL::Hive::NakedTable') ) {
216
217
218
219
220
221
222
223
224

                    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"
                }
225
226
227
228
            }
        }

        $self->graph->cluster_2_nodes( \%cluster_2_nodes );
229
230
        $self->graph->colour_scheme( $self->config_get('Box', 'ColourScheme') );
        $self->graph->colour_offset( $self->config_get('Box', 'ColourOffset') );
231
232
233
234
235
236
    }

    return $self->graph();
}


237
238
sub _propagate_allocation {
    my ($self, $source_analysis ) = @_;
239

Leo Gordon's avatar
Leo Gordon committed
240
    foreach my $df_rule ( @{ $source_analysis->dataflow_rules_collection } ) {
241
242
243
        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";

244
        my $target_node_name;
245

246
        if(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Analysis')) {
247
            $target_node_name = $self->_analysis_node_name( $target_object );
248
        } elsif(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::NakedTable')) {
249
            $target_node_name = $self->_table_node_name( $df_rule );
250
        } elsif(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Accumulator')) {
251
            next;
252
        } else {
253
            warn("Do not know how to handle the type '".ref($target_object)."'");
254
            next;
255
        }
256

257
258
        my $proposed_funnel_dfr;    # will depend on whether we start a new semaphore

259
        # --------------- first assign the rules (their midpoints if applicable) --------------------
260

261
262
        my $funnel_dataflow_rule;
        if( $funnel_dataflow_rule = $df_rule->funnel_dataflow_rule ) {   # if there is a new semaphore, the dfrs involved (their midpoints) will also have to be allocated
263
            $funnel_dataflow_rule->{'_funnel_dfr'} = $source_analysis->{'_funnel_dfr'}; # draw the funnel's midpoint outside of the box
264
265

            $proposed_funnel_dfr = $df_rule->{'_funnel_dfr'} = $funnel_dataflow_rule;       # if we do start a new semaphore, report to the new funnel (based on common funnel rule's midpoint)
266
        } else {
267
            $proposed_funnel_dfr = $source_analysis->{'_funnel_dfr'} || ''; # if we don't start a new semaphore, inherit the allocation of the source
268
        }
269

270
271
        # --------------- then assign the target_objects --------------------------------------------

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

275
276
277
278
            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";
279
            } else {
280
                # warn "analysis '$target_node_name' has already been allocated to '$known_funnel_dfr' however this branch would allocate it to '$proposed_funnel_dfr'";
281
282
            }

283
            if($funnel_dataflow_rule) {  # correction for multiple entries into the same box (probably needs re-thinking)
284
                $df_rule->{'_funnel_dfr'} = $target_object->{'_funnel_dfr'};
285
286
287
            }

        } else {
288
289
            # warn "allocating analysis '$target_node_name' to '$proposed_funnel_dfr'";
            $target_object->{'_funnel_dfr'} = $proposed_funnel_dfr;
290

291
            if(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Analysis')) {
292
                $self->_propagate_allocation( $target_object );
293
            }
294
295
        }
    }
296
297
}

298

299
300
sub _add_pipeline_label {
    my ($self, $pipeline_label) = @_;
301

302
    my $node_fontname  = $self->config_get('Node', 'Details', 'Font');
303
    $self->graph()->add_node( 'Details',
304
        label     => $pipeline_label,
305
306
        fontname  => $node_fontname,
        shape     => 'plaintext',
307
308
309
    );
}

310

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

314
    my $analysis_stats = $analysis->stats();
315

316
317
318
319
320
321
    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');
322
    my $hive_pipeline                                     = $self->pipeline;
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340

    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;
341
    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>';
342
343
344
345
346
347
348
349
350
    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>};
        }
    }

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

357
        my @jobs = @{ $analysis->jobs_collection };
358
359
360
361
362
363

        my $hit_limit;
        if(scalar(@jobs)>$job_limit) {
            pop @jobs;
            $hit_limit = 1;
        }
364
365
366
367
368

        $analysis_label    .= '<tr><td colspan="'.$colspan.'"> </td></tr>';
        foreach my $job (@jobs) {
            my $input_id = $job->input_id;
            my $status   = $job->status;
369
            my $job_id   = $job->dbID || 'unstored';
370
371
372
373
374
            $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>};
        }
375
376

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

    $self->_add_control_rules( $analysis->control_rules_collection );
391
    $self->_add_dataflow_rules( $analysis );
392
393
394
}


395
sub _add_control_rules {
396
397
398
399
    my ($self, $ctrl_rules) = @_;

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

401
402
403
404
        #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;
405

406
407
        my $ctrled_is_local     = $ctrled_analysis->is_local_to( $self->pipeline );
        my $condition_is_local  = $condition_analysis->is_local_to( $self->pipeline );
408

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

413
        next unless( $ctrled_is_local or $condition_is_local or $self->{'_foreign_analyses'}{ $condition_analysis->relative_display_name($self->pipeline) } );
414
415
416
417
418
419
420
421
422

        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',
        );
    }
423
424
}

425

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

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

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


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

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

    my $midpoint_name       = _midpoint_name( $df_rule );

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

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

    return $midpoint_name;
}


471
sub _add_dataflow_rules {
472
    my ($self, $from_analysis) = @_;
473

474
    my $graph               = $self->graph();
475
476
477
478
    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');
479

480
    foreach my $group ( @{ $from_analysis->get_grouped_dataflow_rules } ) {
481

482
        my ($df_rule, $fan_dfrs) = @$group;
483

484
485
        my $from_node_name  = $self->_analysis_node_name( $from_analysis );
        my $target_object   = $df_rule->to_analysis
486
487
            or die "Could not fetch a target object for url='".$df_rule->to_analysis_url."', please check your database for consistency.\n";

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

490
            my $from_is_local   = $df_rule->from_analysis->is_local_to( $self->pipeline );
491
            my $target_is_local = $target_object->is_local_to( $self->pipeline );
492

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

497
            next unless( $from_is_local or $target_is_local or $self->{'_foreign_analyses'}{ $target_object->relative_display_name($self->pipeline) } );
498
        }
499

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

502
            my $funnel_midpoint_name = $self->_twopart_arrow( $df_rule );
503

504
505
            foreach my $fan_dfr (@$fan_dfrs) {
                my $fan_midpoint_name = $self->_twopart_arrow( $fan_dfr );
506

507
508
                    # add a semaphore inter-rule blocking arc:
                $graph->add_edge( $fan_midpoint_name => $funnel_midpoint_name,
509
                    color     => $semablock_colour,
510
511
512
513
514
515
                    style     => 'dashed',
                    arrowhead => 'tee',
                    dir       => 'both',
                    arrowtail => 'crow',
                );
            }
516

517
        } elsif(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Accumulator')) {
518
519
520
521

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

522
                # one-part dashed arrow:
523
            $graph->add_edge( $from_node_name => _midpoint_name( $funnel_analysis ),
524
525
                color       => $accu_colour,
                style       => 'dashed',
526
                label       => '#'.$df_rule->branch_code.":\n".$target_object->relative_display_name( $self->pipeline ),
527
528
529
530
531
                fontname    => $df_edge_fontname,
                fontcolor   => $accu_colour,
                dir         => 'both',
                arrowtail   => 'crow',
            );
532

533
        } else {
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550

            my $target_node_name;

            if(UNIVERSAL::isa($target_object, 'Bio::EnsEMBL::Hive::Analysis')) {

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

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

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

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

551
                # one-part solid arrow:
552
            $graph->add_edge( $from_node_name => $target_node_name,
553
                color       => $dataflow_colour,
554
                fontname    => $df_edge_fontname,
555
                fontcolor   => $dataflow_colour,
556
                label       => $self->_branch_and_template( $df_rule ),
557
            );
558
        } # /if
559

560
    } # /foreach my $group
561
562
}

563

564
sub _add_table_node {
565
    my ($self, $table_node_name, $naked_table) = @_;
566

567
    my $node_fontname   = $self->config_get('Node', 'Table', 'Font');
568
    my (@column_names, $columns, $table_data, $data_limit, $hit_limit);
569

570
    my $hive_pipeline   = $self->pipeline;
571

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

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

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

584
    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>';
585

586
    if( $self->config_get('DisplayData') and $columns) {
587
588
589
590
591
        $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>';
        }
592
593
594
        if($hit_limit) {
            $table_label  .= qq{<tr><td colspan="$columns">[ more data ]</td></tr>};
        }
595
596
597
    }
    $table_label .= '</table>>';

598
    $self->graph()->add_node( $table_node_name, 
599
600
601
602
603
        label => $table_label,
        shape => 'record',
        fontname => $node_fontname,
        color => $self->config_get('Node', 'Table', 'Colour'),
    );
604
605
}

Leo Gordon's avatar
Leo Gordon committed
606
1;