File Coverage

blib/lib/App/Fasops/Command/xlsx.pm
Criterion Covered Total %
statement 211 223 94.6
branch 66 78 84.6
condition 28 33 84.8
subroutine 15 15 100.0
pod 6 9 66.6
total 326 358 91.0


line stmt bran cond sub pod time code
1             package App::Fasops::Command::xlsx;
2 20     20   17252 use strict;
  20         56  
  20         694  
3 20     20   118 use warnings;
  20         49  
  20         642  
4 20     20   132 use autodie;
  20         57  
  20         159  
5              
6 20     20   110930 use Excel::Writer::XLSX;
  20         72  
  20         1146  
7              
8 20     20   132 use App::Fasops -command;
  20         42  
  20         196  
9 20     20   6906 use App::Fasops::Common;
  20         859  
  20         53798  
10              
11             sub abstract {
12 2     2 1 48 return 'paint substitutions and indels to an excel file';
13             }
14              
15             sub opt_spec {
16             return (
17 9     9 1 166 [ "outfile|o=s", "Output filename" ],
18             [ "length|l=i", "the threshold of alignment length", { default => 1 }, ],
19             [ 'wrap=i', 'wrap length', { default => 50 }, ],
20             [ 'spacing=i', 'wrapped line spacing', { default => 1 }, ],
21             [ 'colors=i', 'number of colors', { default => 15 }, ],
22             [ 'section=i', 'start section', { default => 1, hidden => 1 }, ],
23             [ 'outgroup', 'alignments have an outgroup', ],
24             [ 'noindel', 'omit indels', ],
25             [ 'nosingle', 'omit singleton SNPs and indels', ],
26             [ 'nocomplex', 'omit complex SNPs and indels', ],
27             [ 'min=f', 'minimal frequency', ],
28             [ 'max=f', 'maximal frequency', ],
29             { show_defaults => 1, }
30             );
31             }
32              
33             sub usage_desc {
34 9     9 1 846038 return "fasops xlsx [options] ";
35             }
36              
37             sub description {
38 1     1 1 2684 my $desc;
39 1         4 $desc .= ucfirst(abstract) . ".\n";
40 1         2 $desc .= <<'MARKDOWN';
41              
42             * are paths to axt files, .fas.gz is supported
43             * infile == stdin means reading from STDIN
44              
45             MARKDOWN
46              
47 1         4 return $desc;
48             }
49              
50             sub validate_args {
51 8     8 1 23258 my ( $self, $opt, $args ) = @_;
52              
53 8 100       21 if ( @{$args} != 1 ) {
  8         38  
54 1         3 my $message = "This command need one input file.\n\tIt found";
55 1         2 $message .= sprintf " [%s]", $_ for @{$args};
  1         4  
56 1         3 $message .= ".\n";
57 1         10 $self->usage_error($message);
58             }
59 7         18 for ( @{$args} ) {
  7         20  
60 7 50       33 next if lc $_ eq "stdin";
61 7 100       27 if ( !Path::Tiny::path($_)->is_file ) {
62 1         112 $self->usage_error("The input file [$_] doesn't exist.");
63             }
64             }
65              
66 6 50       435 if ( !exists $opt->{outfile} ) {
67 0         0 $opt->{outfile} = Path::Tiny::path( $args->[0] )->absolute . ".xlsx";
68             }
69              
70 6 50       26 if ( $opt->{colors} ) {
71 6         37 $opt->{colors} = List::Util::min( $opt->{colors}, 15 );
72             }
73             }
74              
75             sub execute {
76 6     6 1 43 my ( $self, $opt, $args ) = @_;
77              
78             #@type IO::Handle
79 6         12 my $in_fh;
80 6 50       24 if ( lc $args->[0] eq "stdin" ) {
81 0         0 $in_fh = *STDIN{IO};
82             }
83             else {
84 6         55 $in_fh = IO::Zlib->new( $args->[0], "rb" );
85             }
86              
87             # Create workbook and worksheet objects
88             #@type Excel::Writer::XLSX
89 6         10704 my $workbook = Excel::Writer::XLSX->new( $opt->{outfile} );
90              
91             #@type Excel::Writer::XLSX::Worksheet
92 6         3223 my $worksheet = $workbook->add_worksheet;
93              
94 6         1950 my $format_of = create_formats($workbook);
95 6         15 my $max_name_length = 1;
96              
97 6         15 my $content = ''; # content of one block
98 6         14 while (1) {
99 168 100 66     861 last if $in_fh->eof and $content eq '';
100 162         7635 my $line = '';
101 162 50       534 if ( !$in_fh->eof ) {
102 162         6513 $line = $in_fh->getline;
103             }
104 162 50       19838 next if substr( $line, 0, 1 ) eq "#";
105              
106 162 100 66     959 if ( ( $line eq '' or $line =~ /^\s+$/ ) and $content ne '' ) {
      66        
107 18         95 my $info_of = App::Fasops::Common::parse_block( $content, 1 );
108 18         42 $content = '';
109              
110 18         31 my @full_names;
111 18         27 my $seq_refs = [];
112              
113 18         32 for my $key ( keys %{$info_of} ) {
  18         72  
114 72         1146 push @full_names, $key;
115 72         100 push @{$seq_refs}, $info_of->{$key}{seq};
  72         211  
116             }
117              
118 18 50       246 if ( $opt->{length} ) {
119 18 100       65 next if length $info_of->{ $full_names[0] }{seq} < $opt->{length};
120             }
121              
122 17         309 print "Section [$opt->{section}]\n";
123 17         327 $max_name_length = List::Util::max( $max_name_length, map {length} @full_names );
  68         145  
124              
125             # including indels and snps
126 17         51 my $vars = get_vars( $seq_refs, $opt );
127 17         60 $opt->{section} = paint_vars( $worksheet, $format_of, $opt, $vars, \@full_names );
128              
129             }
130             else {
131 144         295 $content .= $line;
132             }
133             }
134              
135 6         1190 $in_fh->close;
136              
137             # format column
138 6         1258 $worksheet->set_column( 0, 0, $max_name_length + 1 );
139 6         608 $worksheet->set_column( 1, $opt->{wrap} + 3, 1.6 );
140              
141 6         1607 return;
142             }
143              
144             # Excel formats
145             sub create_formats {
146              
147             #@type Excel::Writer::XLSX
148 6     6 0 14 my $workbook = shift;
149              
150 6         16 my $format_of = {};
151              
152             # species name
153 6         33 $format_of->{name} = $workbook->add_format(
154             font => 'Courier New',
155             size => 10,
156             );
157              
158             # variation position
159 6         571 $format_of->{pos} = $workbook->add_format(
160             font => 'Courier New',
161             size => 8,
162             align => 'center',
163             valign => 'vcenter',
164             rotation => 90,
165             );
166              
167 6         1250 $format_of->{snp} = {};
168 6         18 $format_of->{indel} = {};
169              
170             # background
171 6         13 my $bg_of = {};
172              
173             # 15
174 6         28 my @colors = (
175             22, # Gray-25%, silver
176             43, # Light Yellow 0b001
177             42, # Light Green 0b010
178             27, # Lite Turquoise
179             44, # Pale Blue 0b100
180             46, # Lavender
181             47, # Tan
182             24, # Periwinkle
183             49, # Aqua
184             51, # Gold
185             45, # Rose
186             52, # Light Orange
187             26, # Ivory
188             29, # Coral
189             31, # Ice Blue
190              
191             # 30, # Ocean Blue
192             # 41, # Light Turquoise, again
193             # 48, # Light Blue
194             # 50, # Lime
195             # 54, # Blue-Gray
196             # 62, # Indigo
197             );
198              
199 6         27 for my $i ( 0 .. $#colors ) {
200 90         244 $bg_of->{$i}{bg_color} = $colors[$i];
201              
202             }
203 6         28 $bg_of->{unknown}{bg_color} = 9; # White
204              
205             # snp base
206 6         66 my $snp_fg_of = {
207             'A' => { color => 58, }, # Dark Green
208             'C' => { color => 18, }, # Dark Blue
209             'G' => { color => 28, }, # Dark Purple
210             'T' => { color => 16, }, # Dark Red
211             'N' => { color => 8, }, # Black
212             '-' => { color => 8, }, # Black
213             };
214              
215 6         13 for my $fg ( keys %{$snp_fg_of} ) {
  6         25  
216 36         6456 for my $bg ( keys %{$bg_of} ) {
  36         126  
217             $format_of->{snp}{"$fg$bg"} = $workbook->add_format(
218             font => 'Courier New',
219             size => 10,
220             align => 'center',
221             valign => 'vcenter',
222 576         1157 %{ $snp_fg_of->{$fg} },
223 576         116134 %{ $bg_of->{$bg} },
  576         1549  
224             );
225             }
226             }
227 6         1322 $format_of->{snp}{'-'} = $workbook->add_format(
228             font => 'Courier New',
229             size => 10,
230             align => 'center',
231             valign => 'vcenter',
232             );
233              
234 6         943 for my $bg ( keys %{$bg_of} ) {
  6         54  
235             $format_of->{indel}->{$bg} = $workbook->add_format(
236             font => 'Courier New',
237             size => 10,
238             bold => 1,
239             align => 'center',
240             valign => 'vcenter',
241 96         18031 %{ $bg_of->{$bg} },
  96         298  
242             );
243             }
244              
245 6         1288 return $format_of;
246             }
247              
248             # store all variations
249             sub get_vars {
250 17     17 0 32 my $seq_refs = shift;
251 17         30 my $opt = shift;
252              
253             # outgroup
254 17         23 my $out_seq;
255 17 100       50 if ( $opt->{outgroup} ) {
256 2         7 $out_seq = pop @{$seq_refs};
  2         5  
257             }
258              
259 17         32 my $seq_count = scalar @{$seq_refs};
  17         33  
260 17 50       61 if ( $seq_count < 2 ) {
261 0         0 Carp::confess "Too few sequences [$seq_count]\n";
262             }
263              
264 17         59 my $indel_sites = App::Fasops::Common::get_indels($seq_refs);
265 17 100       59 if ( $opt->{outgroup} ) {
266 2         10 App::Fasops::Common::polarize_indel( $indel_sites, $out_seq );
267             }
268              
269 17         60 my $snp_sites = App::Fasops::Common::get_snps($seq_refs);
270 17 100       60 if ( $opt->{outgroup} ) {
271 2         11 App::Fasops::Common::polarize_snp( $snp_sites, $out_seq );
272             }
273              
274 17         37 my %variations;
275 17         30 for my $site ( @{$indel_sites} ) {
  17         55  
276 33 100 100     83 if ( $opt->{nocomplex} and $site->{indel_freq} == -1 ) {
277 2         5 next;
278             }
279              
280 31 100 100     78 if ( $opt->{nosingle} and $site->{indel_freq} <= 1 ) {
281 4         8 next;
282             }
283              
284 27 100       46 if ( $opt->{noindel} ) {
285 6         12 next;
286             }
287              
288 21 100 100     64 if ( defined $opt->{min} and $site->{indel_freq} / $seq_count < $opt->{min} ) {
289 4         7 next;
290             }
291 17 50 66     48 if ( defined $opt->{max} and $site->{indel_freq} / $seq_count > $opt->{max} ) {
292 0         0 next;
293             }
294              
295 17         33 $site->{var_type} = 'indel';
296 17         39 $variations{ $site->{indel_start} } = $site;
297             }
298              
299 17         30 for my $site ( @{$snp_sites} ) {
  17         62  
300 448 100 100     906 if ( $opt->{nocomplex} and $site->{snp_freq} == -1 ) {
301 7         15 next;
302             }
303              
304 441 100 100     891 if ( $opt->{nosingle} and $site->{snp_freq} <= 1 ) {
305 63         99 next;
306             }
307              
308 378 100 100     805 if ( defined $opt->{min} and $site->{snp_freq} / $seq_count < $opt->{min} ) {
309 63         94 next;
310             }
311 315 50 66     613 if ( defined $opt->{max} and $site->{snp_freq} / $seq_count > $opt->{max} ) {
312 0         0 next;
313             }
314              
315 315         600 $site->{var_type} = 'snp';
316 315         559 $variations{ $site->{snp_pos} } = $site;
317             }
318              
319 17         180 return \%variations;
320             }
321              
322             # write excel
323             sub paint_vars {
324              
325             #@type Excel::Writer::XLSX::Worksheet
326 17     17 0 33 my $sheet = shift;
327 17         33 my $format_of = shift;
328 17         22 my $opt = shift;
329 17         26 my $vars = shift;
330 17         27 my $name_refs = shift;
331              
332 17         41 my $section_start = $opt->{section};
333 17         38 my $color_loop = $opt->{colors};
334              
335 17         34 my %variations = %{$vars};
  17         134  
336 17         50 my $section_cur = $section_start;
337 17         34 my $col_cursor = 1;
338 17         28 my $section_height = ( scalar( @{$name_refs} ) + 1 ) + $opt->{spacing};
  17         42  
339 17         27 my $seq_count = scalar @{$name_refs};
  17         26  
340 17 100       44 $seq_count-- if $opt->{outgroup};
341              
342 17         139 for my $pos ( sort { $a <=> $b } keys %variations ) {
  1263         1590  
343 332         567 my $var = $variations{$pos};
344 332         571 my $pos_row = $section_height * ( $section_cur - 1 );
345              
346             # write SNPs
347 332 100       717 if ( $var->{var_type} eq 'snp' ) {
348              
349             # write position
350 315         1019 $sheet->write( $pos_row, $col_cursor, $var->{snp_pos}, $format_of->{pos} );
351              
352 315         19052 for my $i ( 1 .. $seq_count ) {
353 1212         68464 my $base = substr $var->{snp_all_bases}, $i - 1, 1;
354              
355             my $occ
356             = $var->{snp_occured} eq "unknown"
357             ? 0
358 1212 100       2611 : substr( $var->{snp_occured}, $i - 1, 1 );
359              
360 1212 100       1940 if ( $occ eq "1" ) {
361 725         1564 my $bg_idx = oct( '0b' . $var->{snp_occured} ) % $color_loop;
362 725         1074 my $base_color = $base . $bg_idx;
363             $sheet->write( $pos_row + $i,
364 725         2035 $col_cursor, $base, $format_of->{snp}{$base_color} );
365             }
366             else {
367 487         824 my $base_color = $base . "unknown";
368             $sheet->write( $pos_row + $i,
369 487         1303 $col_cursor, $base, $format_of->{snp}{$base_color} );
370             }
371             }
372              
373             # outgroup bases with no background colors
374 315 100       23638 if ( $opt->{outgroup} ) {
375 48         114 my $base_color = $var->{snp_outgroup_base} . "unknown";
376             $sheet->write(
377             $pos_row + $seq_count + 1,
378             $col_cursor,
379             $var->{snp_outgroup_base},
380 48         138 $format_of->{snp}{$base_color}
381             );
382             }
383              
384             # increase column cursor
385 315         3930 $col_cursor++;
386             }
387              
388             # write indels
389 332 100       730 if ( $var->{var_type} eq 'indel' ) {
390              
391             # how many column does this indel take up
392 17         54 my $col_taken = List::Util::min( $var->{indel_length}, 3 );
393              
394             # if exceed the wrap limit, start a new section
395 17 50       42 if ( $col_cursor + $col_taken > $opt->{wrap} ) {
396 0         0 $col_cursor = 1;
397 0         0 $section_cur++;
398 0         0 $pos_row = $section_height * ( $section_cur - 1 );
399             }
400              
401 17         46 my $indel_string = "$var->{indel_type}$var->{indel_length}";
402              
403 17         28 my $bg_idx = 'unknown';
404 17 100       39 if ( $var->{indel_occured} ne 'unknown' ) {
405 14         35 $bg_idx = oct( '0b' . $var->{indel_occured} ) % $color_loop;
406             }
407              
408 17         41 for my $i ( 1 .. $seq_count ) {
409 65         3444 my $flag = 0;
410 65 100       119 if ( $var->{indel_occured} eq "unknown" ) {
411 11         20 $flag = 1;
412             }
413             else {
414 54         106 my $occ = substr $var->{indel_occured}, $i - 1, 1;
415 54 100       117 if ( $occ eq '1' ) {
416 32         48 $flag = 1;
417             }
418             }
419              
420 65 100       127 if ($flag) {
421 43 100       93 if ( $col_taken == 1 ) {
    50          
422              
423             # write position
424             $sheet->write( $pos_row, $col_cursor, $var->{indel_start},
425 37         121 $format_of->{pos} );
426              
427             # write in indel occured lineage
428             $sheet->write( $pos_row + $i,
429 37         2272 $col_cursor, $indel_string, $format_of->{indel}{$bg_idx} );
430             }
431             elsif ( $col_taken == 2 ) {
432              
433             # write indel_start position
434             $sheet->write( $pos_row, $col_cursor, $var->{indel_start},
435 6         22 $format_of->{pos} );
436              
437             # write indel_end position
438             $sheet->write( $pos_row, $col_cursor + 1,
439 6         363 $var->{indel_end}, $format_of->{pos} );
440              
441             # merge two indel position
442             $sheet->merge_range(
443             $pos_row + $i,
444             $col_cursor,
445             $pos_row + $i,
446             $col_cursor + 1,
447 6         349 $indel_string, $format_of->{indel}{$bg_idx},
448             );
449             }
450             else {
451              
452             # write indel_start position
453             $sheet->write( $pos_row, $col_cursor, $var->{indel_start},
454 0         0 $format_of->{pos} );
455              
456             # write middle sign
457 0         0 $sheet->write( $pos_row, $col_cursor + 1, '|', $format_of->{pos} );
458              
459             # write indel_end position
460             $sheet->write( $pos_row, $col_cursor + 2,
461 0         0 $var->{indel_end}, $format_of->{pos} );
462              
463             # merge two indel position
464             $sheet->merge_range(
465             $pos_row + $i,
466             $col_cursor,
467             $pos_row + $i,
468             $col_cursor + 2,
469 0         0 $indel_string, $format_of->{indel}{$bg_idx},
470             );
471             }
472             }
473             }
474              
475             # increase column cursor
476 17         639 $col_cursor += $col_taken;
477             }
478              
479 332 100       729 if ( $col_cursor > $opt->{wrap} ) {
480 2         6 $col_cursor = 1;
481 2         6 $section_cur++;
482             }
483             }
484              
485             # write names
486 17         74 for my $i ( $section_start .. $section_cur ) {
487 19         216 my $pos_row = $section_height * ( $i - 1 );
488              
489 19         38 for my $j ( 1 .. scalar @{$name_refs} ) {
  19         53  
490 76         4822 $sheet->write( $pos_row + $j, 0, $name_refs->[ $j - 1 ], $format_of->{name} );
491             }
492             }
493              
494 17         1329 $section_cur++;
495 17         793 return $section_cur;
496             }
497              
498             1;