File Coverage

blib/lib/App/Fasops/Command/xlsx.pm
Criterion Covered Total %
statement 205 215 95.3
branch 60 70 85.7
condition 18 21 85.7
subroutine 15 15 100.0
pod 6 9 66.6
total 304 330 92.1


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