File Coverage

blib/lib/Excel/Writer/XLSX/CDF.pm
Criterion Covered Total %
statement 140 140 100.0
branch 48 56 85.7
condition 5 8 62.5
subroutine 17 17 100.0
pod 11 11 100.0
total 221 232 95.2


line stmt bran cond sub pod time code
1             use strict;
2 2     2   208895 use warnings;
  2         14  
  2         50  
3 2     2   7 use List::MoreUtils qw{first_index};
  2         4  
  2         55  
4 2     2   927  
  2         22135  
  2         15  
5             our $VERSION = '0.04';
6              
7             =head1 NAME
8              
9             Excel::Writer::XLSX::CDF - Generates Excel Document with Continuous Distribution Function Chart
10              
11             =head1 SYNOPSIS
12              
13             use Excel::Writer::XLSX::CDF;
14             my $writer = Excel::Writer::XLSX::CDF->new(
15             chart_title => "My Title",
16             group_names_sort => 1,
17             );
18             my @data = (
19             [group_name_A => 0.11], #group name is used to lable the chart series
20             [group_name_A => 0.21],
21             [group_name_A => 0.31],
22             [group_name_A => 0.41],
23             [group_name_B => 0.07],
24             [group_name_B => 0.13],
25             [group_name_Z => 0.10],
26             );
27             my $blob = $writer->generate(\@data); #returns Excel File in memory
28             my $filename = $writer->generate_file(\@data); #returns Excel File in tmp folder
29              
30             =head1 DESCRIPTION
31              
32             Generates Excel Document with Continuous Distribution Function Chart from the supplied data.
33              
34             =head1 CONSTRUCTOR
35              
36             =head2 new
37              
38             my $writer = Excel::Writer::XLSX::CDF->new(
39             chart_title => "Continuous Distribution Function (CDF)",
40             chart_y_label => "Probability",
41             chart_x_label => "",
42             chart_x_min => "auto", #defalut: undef => calculated by this package
43             chart_x_max => "auto", #default: undef => calculated by this package
44             group_names_sort => 0, #default: 0 => order of appearance in data
45             );
46              
47             =cut
48              
49             my $this = shift;
50             my $class = ref($this) ? ref($this) : $this;
51 7     7 1 7687 my $self = {@_};
52 7 50       28 bless $self, $class;
53 7         36 return $self;
54 7         14 }
55 7         19  
56             =head1 PROPERTIES
57              
58             =head2 chart_title
59              
60             Set and returns the title of the Excel chart
61              
62             Default: Continuous Distribution Function (CDF)
63              
64             =cut
65              
66             my $self = shift;
67             $self->{'chart_title'} = shift if @_;
68             $self->{'chart_title'} = 'Continuous Distribution Function (CDF)' unless defined $self->{'chart_title'};
69 9     9 1 4798 return $self->{'chart_title'};
70 9 100       28 }
71 9 100       31  
72 9         44 =head2 chart_y_label
73              
74             Set and returns the Y axis label of the Excel chart
75              
76             Default: Probability
77              
78             =cut
79              
80             my $self = shift;
81             $self->{'chart_y_label'} = shift if @_;
82             $self->{'chart_y_label'} = 'Probability' unless defined $self->{'chart_y_label'};
83             return $self->{'chart_y_label'};
84 9     9 1 21 }
85 9 100       26  
86 9 100       30 =head2 chart_x_label
87 9         39  
88             Set and returns the X axis max value of the Excel chart
89              
90             Default: ""
91              
92             =cut
93              
94             my $self = shift;
95             $self->{'chart_x_label'} = shift if @_;
96             $self->{'chart_x_label'} = '' unless defined $self->{'chart_x_label'};
97             return $self->{'chart_x_label'};
98             }
99 9     9 1 16  
100 9 100       27 =head2 chart_x_max
101 9 100       28  
102 9         36 Set and returns the X axis min value of the Excel chart
103              
104             Default: undef = calculate (currently max of all 90% or first 50% crossing)
105              
106             "auto" - set to auto in Excel
107             number - set set_x_axis max to number
108              
109             =cut
110              
111             my $self = shift;
112             $self->{'chart_x_max'} = shift if @_;
113             $self->{'chart_x_max'} = undef unless exists $self->{'chart_x_max'};
114             return $self->{'chart_x_max'};
115             }
116              
117 5     5 1 9 =head2 chart_x_min
118 5 50       15  
119 5 100       20 Set and returns the X axis label of the Excel chart
120 5         12  
121             Default: undef = calculate (currently using auto)
122              
123             "auto" - set to auto in Excel
124             number - set set_x_axis max to number
125              
126             =cut
127              
128             my $self = shift;
129             $self->{'chart_x_min'} = shift if @_;
130             $self->{'chart_x_min'} = undef unless exists $self->{'chart_x_min'};
131             return $self->{'chart_x_min'};
132             }
133              
134             =head2 chart_legend_display
135 5     5 1 7  
136 5 50       10 Set and returns the legend display property for the Excel chart
137 5 100       21  
138 5         10 Default: 1
139              
140             =cut
141              
142             my $self = shift;
143             $self->{'chart_legend_display'} = shift if @_;
144             $self->{'chart_legend_display'} = 1 unless defined $self->{'chart_legend_display'};
145             return $self->{'chart_legend_display'};
146             }
147              
148             =head2 chart_colors
149              
150 5     5 1 10 Set and returns an array reference of Excel color codes to use for each CDF in group order. The default color once all colors are used is black.
151 5 50       13  
152 5 50       13 Default: ['#FF0000', '#800000', '#FFFF00', '#808000', '#00FF00', '#008000', '#00FFFF', '#008080', '#0000FF', '#000080', '#FF00FF', '#800080']
153 5         17  
154             =cut
155              
156             my $self = shift;
157             $self->{'chart_colors'} = shift if @_;
158             $self->{'chart_colors'} = ['#FF0000', '#800000', '#FFFF00', '#808000',
159             '#00FF00', '#008000', '#00FFFF', '#008080',
160             '#0000FF', '#000080', '#FF00FF', '#800080'] unless $self->{'chart_colors'};
161             die('Error: chart_colors property must be and array reference') unless ref($self->{'chart_colors'}) eq 'ARRAY';
162             return $self->{'chart_colors'};
163             }
164              
165 9     9 1 19 =head2 group_names_sort
166 9 100       28  
167             Set and returns the alphabetical sort option for the group names. A true value Perl-wise will sort the group names before generating the Excel Workbook and a false value will use the order in which the groups were discovered in the data to generate the group names order.
168              
169 9 100       36 Default: 0
170 9 50       30  
171 9         34 =cut
172              
173             my $self = shift;
174             $self->{'group_names_sort'} = shift if @_;
175             $self->{'group_names_sort'} = 0 unless defined $self->{'group_names_sort'};
176             return $self->{'group_names_sort'};
177             }
178              
179             =head1 METHODS
180              
181             =head2 generate
182              
183 9     9 1 30 Generates an Excel Workbook in memory and returns the Workbook as a data blob stored in the returned scalar variable.
184 9 100       32  
185 9 100       36 my $blob = $writer->generate(\@data);
186 9         37  
187             =cut
188              
189             my $self = shift;
190             my $data = shift; #isa [[group=>value], [], [], ...]
191             my @groups = ();
192             my $series = {};
193              
194             foreach my $row (@$data) {
195             my $group = $row->[0];
196             my $value = $row->[1];
197             if (not exists $series->{$group}) {
198             push @groups, $group; #keep order
199             $series->{$group} = {count=>0, values=>[]};
200 5     5 1 11 }
201 5         9 $series->{$group}->{'count'}++;
202 5         8 push @{$series->{$group}->{'values'}}, $value;
203 5         11 }
204              
205 5         15 if ($self->group_names_sort) {
206 15021         16261 @groups = sort @groups;
207 15021         13981 }
208 15021 100       17992  
209 15         35 #Open string scalar reference as file handle for Excel::Writer::XLSX to write to
210 15         43 open my $fh, '>', \my $content or die("Error: Filehandle open error: $!");
211              
212 15021         14218 #Object for Excel Workbook
213 15021         13050 require Excel::Writer::XLSX;
  15021         19762  
214             my $workbook = Excel::Writer::XLSX->new($fh);
215              
216 5 100       29 #Add a worksheet chart as first tab so it shows when document is opened
217 4         22 my $chart = $workbook->add_chart(type=>'scatter', subtype=>'straight');
218              
219             #Add worksheet for chart legend groups
220             my $worksheet_groups = $workbook->add_worksheet('groups');
221 5 50   1   110 $worksheet_groups->write_string(0, 0, 'Group');
  1         7  
  1         2  
  1         11  
222             $worksheet_groups->write_string(0, 1, 'Index');
223             $worksheet_groups->write_string(0, 2, 'Count');
224 5         1754 my $group_index = 0;
225 5         144318  
226             #Colors for data series lines and legend
227             my @colors = @{$self->chart_colors};
228 5         1654  
229             #foreach group add worksheet, data and chart series
230             my @stats_groups = ();
231 5         5881  
232 5         1016 foreach my $group (@groups) {
233 5         370  
234 5         265 #Add series label for legend
235 5         226 $group_index++;
236             $worksheet_groups->write_string($group_index, 0, $group);
237             $worksheet_groups->write_number($group_index, 1, $group_index);
238 5         7  
  5         16  
239             #Add worksheet
240             my $worksheet = $workbook->add_worksheet("group_$group_index");
241 5         7  
242             #Add data to worksheet
243 5         12 my @values = sort {$a <=> $b} @{$series->{$group}->{'values'}};
244             my $values_count = scalar(@values);
245             $worksheet_groups->write_number($group_index, 2, $values_count);
246 15         3171 my @cdf = ();
247 15         60 my $loop = 0;
248 15         938 foreach my $value (@values) {
249             $loop++;
250             push @cdf, $loop/$values_count;
251 15         518 }
252             $worksheet->write_row(A1 => [$group, 'Probability'] );
253             $worksheet->write_row(A2 => [\@values, \@cdf] );
254 15         3709  
  124093         115651  
  15         329  
255 15         43 my $stat = {};
256 15         128 $stat->{'min'} = $values[0];
257 15         708 $stat->{'max'} = $values[-1];
258 15         24 my $p50_group_index = first_index {$_ >= 0.5} @cdf;
259 15         35 $stat->{'p50'} = $values[$p50_group_index];
260 15021         13007 my $p90_group_index = first_index {$_ >= 0.9} @cdf;
261 15021         16134 $stat->{'p90'} = $values[$p90_group_index];
262              
263 15         118 push @stats_groups, $stat;
264 15         3103  
265             #Add data references to chart
266 15         1241075 my $color = shift @colors || 'black';
267 15         47 $chart->add_series(
268 15         39 line => {color => $color},
269 15     7511   285 name => sprintf('=%s!$A$%s', 'groups', $group_index + 1), #groups header row is 0
  7511         6530  
270 15         63 categories => sprintf('=%s!$A$2:$A$%s', $worksheet->get_name, $values_count + 1),
271 15     13519   121 values => sprintf('=%s!$B$2:$B$%s', $worksheet->get_name, $values_count + 1),
  13519         11739  
272 15         57 );
273              
274 15         30 } #foreach group
275              
276             my $maxset = $self->chart_x_max;
277 15   50     53 if (!defined($maxset)) {
278 15         158 if (@stats_groups) {
279             require List::Util;
280             require Math::Round::SignificantFigures;
281              
282             my $max = List::Util::max(map {$_->{'max'}} @stats_groups);
283             my $p50 = Math::Round::SignificantFigures::ceilsigfigs(List::Util::max(map {$_->{'p50'}} @stats_groups) * 1.5, 2);
284             my $p90 = Math::Round::SignificantFigures::ceilsigfigs(List::Util::min(map {$_->{'p90'}} @stats_groups) * 1.5, 2);
285             $maxset = List::Util::max($p50, $p90);
286             $maxset = undef if $maxset > $max;
287 5         1722 }
288 5 100 66     29 } elsif (defined($maxset) and $maxset =~ m/\Aauto\Z/i) {
    100          
289 3 50       7 $maxset = undef;
290 3         25 }
291 3         454  
292             my $minset = $self->chart_x_min;
293 3         487 if (!defined($minset)) {
  9         24  
294 3         7 $minset = undef; #allow for calculation in the future
  9         23  
295 3         84 } elsif (defined($minset) and $minset =~ m/\Aauto\Z/i) {
  9         17  
296 3         44 $minset = undef;
297 3 100       10 }
298              
299             #Configure chart
300 1         4 $chart->set_title( name => $self->chart_title );
301             $chart->set_y_axis(name => $self->chart_y_label, min => 0 , max => 1 );
302             $chart->set_x_axis(name => $self->chart_x_label, min => $minset, max => $maxset);
303 5         14 $chart->set_legend(none => 1) unless $self->chart_legend_display;
304 5 100 66     21  
    100          
305 3         4 #Write Excel output to filehandle
306             $workbook->close;
307 1         2  
308             return $content;
309              
310             }
311 5         17  
312 5         273 =head2 generate_file
313 5         655  
314 5 100       446 Returns Excel file name in temp folder
315              
316             use File::Copy qw{move};
317 5         105 my $filename = $writer->generate_file(\@data);
318             move $filename, '.';
319 5         4029355  
320             =cut
321              
322             require DateTime;
323             require File::Temp;
324              
325             my $self = shift;
326             my $ymd = DateTime->now->ymd;
327             my ($fh, $filename) = File::Temp::tempfile("excel-cdf-$ymd-XXXXXX", SUFFIX => '.xlsx', DIR => File::Temp::tempdir());
328             binmode($fh);
329             my $blob = $self->generate(@_);
330             print $fh $blob;
331             close($fh);
332             return $filename;
333             }
334 5     5 1 1094  
335 5         388194 =head1 SEE ALSO
336              
337 5         10 L<Excel::Writer::XLSX>
338 5         28  
339 5         1701 =head1 AUTHOR
340 5         2909  
341 5         23 Michael R. Davis
342 5         23067  
343 5         105 =head1 COPYRIGHT AND LICENSE
344 5         39  
345             MIT License
346              
347             Copyright (c) 2022 Michael R. Davis
348              
349             Permission is hereby granted, free of charge, to any person obtaining a copy
350             of this software and associated documentation files (the "Software"), to deal
351             in the Software without restriction, including without limitation the rights
352             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
353             copies of the Software, and to permit persons to whom the Software is
354             furnished to do so, subject to the following conditions:
355              
356             The above copyright notice and this permission notice shall be included in all
357             copies or substantial portions of the Software.
358              
359             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
360             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
361             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
362             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
363             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
364             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
365             SOFTWARE.
366              
367             =cut
368              
369             1;