Matplotlib-Simple
view release on metacpan or search on metacpan
lib/Matplotlib/Simple.pm view on Meta::CPAN
say { $args->{fh} } 'ys = []';
my ( $min_x, $max_x ) = ( 'inf', '-inf' );
foreach my $run ( 0 .. scalar @{ $plot->{data} } - 1 ) {
$min_x = min( $min_x, @{ $plot->{data}[$run][0] } );
$max_x = max( $max_x, @{ $plot->{data}[$run][0] } );
}
say { $args->{fh} } "base_y = np.linspace($max_x, $min_x, 101)";
foreach my $run ( 0 .. scalar @{ $plot->{data} } - 1 ) {
say { $args->{fh} } 'x = ['
. join( ',', @{ $plot->{data}[$run][0] } ) . ']';
say { $args->{fh} } 'y = ['
. join( ',', @{ $plot->{data}[$run][1] } ) . ']';
say { $args->{fh} } "ax$ax.plot(x, y, '$color', alpha=0.15)";
say { $args->{fh} } 'y = np.interp(base_y, x, y)';
say { $args->{fh} } 'ys.append(y)';
}
say { $args->{fh} } 'ys = np.array(ys)';
say { $args->{fh} } 'mean_ys = ys.mean(axis=0)';
say { $args->{fh} } 'std = ys.std(axis=0)';
say { $args->{fh} } 'ys_upper = mean_ys + std';
say { $args->{fh} } 'ys_lower = mean_ys - std';
say { $args->{fh} } "ax$ax.plot(base_y, mean_ys, '$color')";
say { $args->{fh} }
"ax$ax.fill_between(base_y, ys_lower, ys_upper, color='$color', alpha=0.3)";
} else {
die "$current_sub cannot take ref type \"$ref_type\" for \"data\"";
}
}
sub print_type {
my $str = shift;
my $type = 'no quotes';
if (looks_like_number($str)) { # numbers (e.g. 0.8) must not be quoted
return 'no quotes';
}
if ($str =~ m/\w+\h*=\h*["'\(]/) {
return 'no quotes';
}
if ($str =~ m/^\w+$/) {
return 'single quotes';
} elsif ($str =~ m/^\[\h*\-?\d.+\d\h*\]$/) {
return 'no quotes';
} elsif ($str =~ m/[!@#\$\%^&*\(\)\{\}\[\]\<\>,\/\-\h:;\+=\w]+$/) {
return 'single quotes';
} elsif (($str =~ m/,/) && ($str !~ m/[\]\[]/)) {
return 'single quotes';
}
return $type;
}
my @all_opt;
foreach my $type (keys %opt) {
push @all_opt, @{ $opt{$type} };
}
sub normalise_p {
# "p" is a flat list of subplots: ONE array element == ONE subplot.
# p => [ \%h, \%h, ... ] each hash is its own subplot
# p => [ \%h, [ \%h, \%h ], \%h, ... ] an inner array of hashes is a
# single subplot holding several
# plots on the same axes (the 1st
# hash is the base plot, the rest
# are "add"itions/overlays)
# The plain-hash and inner-array forms may be mixed freely in one "p".
# Using "p" means top-level "plot.type"/"data" are not used. If no grid is
# given, the subplots are laid out on an automatically-sized near-square
# grid; pass nrow(s)/ncol(s) to override.
my ( $args, $current_sub ) = @_;
$current_sub = $current_sub // 'plt';
my $p = delete $args->{p};
if ( ref $p ne 'ARRAY' ) {
die "$current_sub: \"p\" must be an ARRAY reference (one element per subplot)";
}
if ( scalar @{$p} == 0 ) {
die "$current_sub: \"p\" is empty";
}
# "p" supersedes the older interface; forbid mixing to avoid ambiguity
foreach my $clash ( grep { defined $args->{$_} } ( 'plot.type', 'data', 'plots', 'add' ) ) {
die "$current_sub: \"$clash\" cannot be combined with \"p\"";
}
my @plots;
my $i = 0;
foreach my $subplot ( @{$p} ) {
my $ref = ref $subplot;
if ( $ref eq 'HASH' ) { # one subplot, one plot
push @plots, { %{$subplot} }; # shallow copy; don't mutate caller
} elsif ( $ref eq 'ARRAY' ) { # one subplot, several overlaid plots
if ( scalar @{$subplot} == 0 ) {
die "$current_sub: subplot $i in \"p\" is an empty array";
}
my @g = @{$subplot};
my $main = shift @g; # 1st hash is the base plot
foreach my $plot ( $main, @g ) {
if ( ref $plot ne 'HASH' ) {
die "$current_sub: subplot $i in \"p\": every plot must be a HASH reference";
}
}
my %plot = %{$main}; # shallow copy of the base plot
push @{ $plot{add} }, @g if scalar @g; # the rest overlay on the same axes
push @plots, \%plot;
} else {
die "$current_sub: subplot $i in \"p\" must be a HASH reference (one plot) or an ARRAY of HASH references (overlaid plots), not a \"$ref\" reference";
}
$i++;
}
$args->{plots} = \@plots;
# convenience: choose a subplot grid. With neither dimension given, use a
# near-square grid; with exactly one given (and positive), derive the other
# so e.g. "ncols => 1" stacks the subplots in a single column. Empty
# trailing cells are pruned later by the engine.
my $n = scalar @plots;
my $ncols = $args->{ncols} // $args->{ncol};
my $nrows = $args->{nrows} // $args->{nrow};
if ( ( not defined $ncols ) && ( not defined $nrows ) ) {
$ncols = 1;
$ncols++ while ( $ncols * $ncols ) < $n; # ceil(sqrt $n), no floats
$args->{ncols} = $ncols;
$args->{nrows} = int( ( $n + $ncols - 1 ) / $ncols ); # ceil($n / $ncols)
} elsif ( ( not defined $ncols ) && ( $nrows > 0 ) ) { # only rows given
$args->{ncols} = int( ( $n + $nrows - 1 ) / $nrows );
} elsif ( ( not defined $nrows ) && ( $ncols > 0 ) ) { # only cols given
$args->{nrows} = int( ( $n + $ncols - 1 ) / $ncols );
}
return $args;
}
sub plt {
my $current_sub = ( split( /::/, ( caller(0) )[3] ) )[-1]
; # https://stackoverflow.com/questions/2559792/how-can-i-get-the-name-of-the-current-subroutine-in-perl
# Accept BOTH calling conventions:
# new style: plt( key => value, ... )
# back-compatible: plt({ key => value, ... })
my $args;
if ( ( scalar @_ == 1 ) && ( ref $_[0] eq 'HASH' ) ) {
$args = $_[0]; # plt({ ... })
} elsif ( ( scalar @_ % 2 ) == 0 ) {
$args = { @_ }; # plt( ... )
} else {
die "$current_sub: odd number of arguments; call as $current_sub( key => value, ... ) or $current_sub({ key => value, ... })";
}
# fold the "p" interface into the engine's internal plot model
normalise_p( $args, $current_sub ) if defined $args->{p};
if ((scalar grep {$args->{$_}} ('output.file', 'show')) == 0) {
p $args;
die 'either "show" or "output.file" must be defined';
}
my @reqd_args = $args->{show} ? () : ('output.file'); # e.g. "my_image.svg"; optional if "show" is set
my $single_example = 'plt({
\'output.file\' => \'/tmp/gospel.word.counts.svg\',
\'plot.type\' => \'bar\',
data => {
\'Matthew\' => 18345,
\'Mark\' => 11304,
\'Luke\' => 19482,
\'John\' => 15635,
}
});';
my $multi_example = 'plt({
\'output.file\' => \'svg/pie.svg\',
plots => [
lib/Matplotlib/Simple.pm view on Meta::CPAN
=head1 Multiple Plots
Having a C<plots> argument as an array lets the module know to create subplots:
use Matplotlib::Simple 'plt';
plt(
'output.file' => 'svg/pies.png',
plots => [
{
data => {
Russian => 106_000_000, # Primarily European Russia
German => 95_000_000, # Germany, Austria, Switzerland, etc.
},
'plot.type' => 'pie',
title => 'Top Languages in Europe',
suptitle => 'Pie in subplots',
},
{
data => {
Russian => 106_000_000, # Primarily European Russia
German => 95_000_000, # Germany, Austria, Switzerland, etc.
},
'plot.type' => 'pie',
title => 'Top Languages in Europe',
},
],
ncols => 2,
);
which produces the following subplots image:
=for html
<p>
<img width="651" height="424" alt="pies" src="https://github.com/user-attachments/assets/49d3e28b-f897-4b01-9e72-38afa12fa538" />
<p>
C<bar>, C<barh>, C<boxplot>, C<hexbin>, C<hist>, C<hist2d>, C<imshow>, C<pie>, C<plot>, C<scatter>, and C<violinplot> all match the methods in matplotlib itself.
=head2 The C<p> argument
C<p> is a single, uniform way to describe one I<or> many subplots, so you no
longer need a top-level C<plot.type> (or the older C<plots> array). Each plot is a
hash, exactly like a single-plot call, and C<p> collects the subplots into one
array.
The rule is simple: B<< one element of C<p> is one subplot. >>
=over
=item * A B<hash> element is a subplot containing a single plot.
=item * An B<array of hashes> element is a single subplot whose plots are drawn on
the B<same> axes: the first hash is the base plot and the rest are
I<additions> (overlays), exactly like C<add>.
=back
The two forms may be B<mixed freely> in the same C<p>. The first (or only) hash
of a subplot supplies that subplot's axes-level options (C<title>, C<xlabel>,
C<ylabel>, C<legend>, â¦).
If you don't give a grid, the subplots are laid out automatically on a
near-square grid. Give C<ncol>/C<nrow> (aliases for C<ncols>/C<nrows>) to control
it; supplying only one dimension derives the other (so C<< ncols =E<gt> 1 >> stacks the
subplots in a single column), and supplying both is honored as given.
C<p> cannot be combined with C<plot.type>, C<data>, C<plots>, or C<add>.
> Arguments are now passed as a plain list â C<plt( ... )> â though the older
> C<plt({ ... })> form still works.
=head3 One subplot, several plots overlaid
Wrap the plots in an inner array and they all land on a single subplot (the
first is the base plot, the rest are additions):
plt(
p => [
[
{
data => {
E => [ 55, @{$x}, 160 ],
B => [ @{$y}, 140 ],
},
'plot.type' => 'boxplot',
title => 'Single Box Plot: Specified Colors',
colors => { E => 'yellow', B => 'purple' },
},
{
data => {
A => [ 55, @{$z} ],
E => [ @{$y} ],
B => [ 122, @{$z} ],
},
'plot.type' => 'violinplot',
title => 'Single Violin Plot: Specified Colors',
colors => { E => 'yellow', B => 'purple', A => 'green' },
},
],
],
'output.file' => '1plot.svg', # note: no C<plot.type> needed
);
=head3 Multiple subplots
Give each subplot as its own element. A bare hash is a one-plot subplot, so two
hashes make two subplots; with C<< ncol =E<gt> 2 >> they sit side by side:
plt(
p => [
{
data => {
E => [ 55, @{$x}, 160 ],
B => [ @{$y}, 140 ],
},
'plot.type' => 'boxplot',
title => 'Box Plot: Specified Colors',
colors => { E => 'yellow', B => 'purple' },
},
{
data => {
A => [ 55, @{$z} ],
E => [ @{$y} ],
B => [ 122, @{$z} ],
},
'plot.type' => 'violinplot',
title => 'Violin Plot: Specified Colors',
colors => { E => 'yellow', B => 'purple', A => 'green' },
},
],
ncol => 2,
'output.file' => '2plots.svg',
);
To overlay extra plots on any one subplot, make that element an array of hashes
instead of a bare hash (the first is the plot, the rest are additions). Bare
hashes and inner arrays may be intermixed in the same C<p>, for example
C<< p =E<gt> [ \%single, [ \%base, \%overlay ], \%another ] >>.
=head2 Options
C<sharex> and C<sharey> are both implemented at the plot, rather than subplot, level. See Matplotlib's documentation for more clarity.
=head1 Color Bars (colorbars)
Colarbar args attempt to match matplotlib closely
=for html
<table>
<tbody>
<tr><td>Option</td><td>Description</td><td>Example</td></tr>
<tr><td>--------</td><td>-------</td><td>------- </td></tr>
<tr><td><code>cbdrawedges</code></td><td>Whether to draw lines at color boundaries</td><td><code>cbdrawedges => 1</code></td></tr>
<tr><td><code>cblabel</code></td><td>The label on the colorbar's long axis</td><td><code>cblabel => 1</code></td></tr>
<tr><td><code>cblocation</code></td><td>of the colorbar None or {'left', 'right', 'top', 'bottom'}</td><td></td></tr>
<tr><td><code>cborientation</code></td><td># None or {<code>vertical</code>, <code>horizontal</code>}</td><td></td></tr>
<tr><td><code>cbpad</code></td><td>pad : float, default: 0.05 if vertical, 0.15 if horizontal; Fraction of original Axes between colorbar and new image Axes</td><td></td></tr>
<tr><td><code>cb_logscale</code></td><td>Perl true (anything but 0) or false (0)</td><td></td></tr>
<tr><td><code>shared.colorbar</code></td><td>share colorbar between different plots: specify plot indices</td><td><code>'shared.colorbar' => [0,1]</code></td></tr>
</tbody>
</table>
=head1 Size/Dimensions of output file
=for html
<table>
<tbody>
<tr><td>Option</td><td>Description</td><td>Example</td></tr>
<tr><td>--------</td><td>-------</td><td>-------</td></tr>
<tr><td><code>scale</code></td><td>scale/multiply the size of the output figure</td><td><code>scale => 2.4</code></td></tr>
<tr><td><code>scalex</code></td><td>scale/multiply the x-axis only</td><td><code>scalex => 2.4</code></td></tr>
<tr><td><code>scaley</code></td><td>scale/multiply the y-axis only</td><td><code>scalex => 1.4</code></td></tr>
</tbody>
</table>
=head1 Examples/Plot Types
Consider the following helper subroutines to generate data to plot:
sub linspace { # mostly written by Grok
my ($start, $stop, $num, $endpoint) = @_; # endpoint means include $stop
$num = defined $num ? int($num) : 50; # Default to 50 points
$endpoint = defined $endpoint ? $endpoint : 1; # Default to include endpoint
return () if $num < 0; # Return empty array for invalid num
return ($start) if $num == 1; # Return single value if num is 1
my (@result, $step);
if ($endpoint) {
$step = ($stop - $start) / ($num - 1) if $num > 1;
for my $i (0 .. $num - 1) {
$result[$i] = $start + $i * $step;
}
} else {
$step = ($stop - $start) / $num;
for my $i (0 .. $num - 1) {
$result[$i] = $start + $i * $step;
}
}
lib/Matplotlib/Simple.pm view on Meta::CPAN
{
'plot.type' => 'plot',
data => {
A => [ [5..9], [5..9] ],
B => [ [5..9], [1..5] ],
},
'set.options' => {
A => 'color = "red"',
B => 'color = "blue", marker = "o"',
},
}
Note the pairing rule: a scalar C<set.options> goes with B<any> data shape; an
B<array> C<set.options> goes with B<array> data; a B<hash> C<set.options> goes
with B<hash> data. Mismatches (for example a hash of options with array data)
are rejected with an explanatory error.
=head3 Other options
=over
=item * C<show.legend> â on by default (C<1>); set to C<0> to suppress labels. Only the
hash form produces labels in the first place.
=item * C<key.order> â array of keys (hash form) fixing the draw/legend order; defaults
to the keys sorted alphabetically.
=item * C<logscale> â array of axes to put on a log scale, e.g. C<[ 'x', 'y' ]>.
=item * C<twinx> â draw selected series against a secondary y-axis.
=over
=item * hash data: a single key, or a hash whose keys are the series to twin;
=item * array data: an integer index, or an array of indices.
=back
=item * C<twinx.args> â a hash keyed by data key (hash form) or index (array form);
each value is a hash of axis options (e.g. C<ylabel>, C<set_ylim>) applied to
that twin axis.
=back
Common axes options such as C<title>, C<xlabel>, C<ylabel>, and C<legend> are
accepted here too, exactly as for the other plot types.
A C<plot> spec is an ordinary plot hash, so it can be dropped straight into the
L<#the-p-argument> argument â on its own for a single subplot, or alongside
other hashes to overlay or to fill a grid of subplots.
=head3 single, simple
data can be given as a hash, where the hash key is the label:
plt(
fh => $fh,
execute => 0,
'output.file' => 'output.images/plot.single.png',
data => {
'sin(x)' => [
[@x], # x
[ map { sin($_) } @x ] # y
],
'cos(x)' => [
[@x], # x
[ map { cos($_) } @x ] # y
],
},
'plot.type' => 'plot',
title => 'simple plot',
set_xticks =>
"[-2 * $pi, -3 * $pi / 2, -$pi, -$pi / 2, 0, $pi / 2, $pi, 3 * $pi / 2, 2 * $pi"
. '], [r\'$-2\pi$\', r\'$-3\pi/2$\', r\'$-\pi$\', r\'$-\pi/2$\', r\'$0$\', r\'$\pi/2$\', r\'$\pi$\', r\'$3\pi/2$\', r\'$2\pi$\']',
'set.options' => { # set options overrides global settings
'sin(x)' => 'color="blue", linewidth=2',
'cos(x)' => 'color="red", linewidth=2'
}
);
or as an array of arrays:
plt(
fh => $fh,
execute => 0,
'output.file' => 'output.images/plot.single.arr.png',
data => [
[
[@x], # x
[ map { sin($_) } @x ] # y
],
[
[@x], # x
[ map { cos($_) } @x ] # y
],
],
'plot.type' => 'plot',
title => 'simple plot',
set_xticks =>
"[-2 * $pi, -3 * $pi / 2, -$pi, -$pi / 2, 0, $pi / 2, $pi, 3 * $pi / 2, 2 * $pi"
. '], [r\'$-2\pi$\', r\'$-3\pi/2$\', r\'$-\pi$\', r\'$-\pi/2$\', r\'$0$\', r\'$\pi/2$\', r\'$\pi$\', r\'$3\pi/2$\', r\'$2\pi$\']',
'set.options' => [ # set options overrides global settings; indices match data array
'color="blue", linewidth=2, label = "sin(x)"', # labels aren't added automatically when using array here
'color="red", linewidth=2, label = "cos(x)"'
],
);
both of which make the following "plot" plot:
lib/Matplotlib/Simple.pm view on Meta::CPAN
Better warning when color key isn't defined for C<scatter>
When giving two hash of hashes for a barplot, if one second key is defined in one subplot, but not the other, that subkey is initialized to 0.
=head3 Cross-platform support
The module now should run on Windows in addition to Linux and macOS.
The generated Python script is written to the system temporary directory (via C<< File::Spec-E<gt>tmpdir() >>) instead of a hard-coded C</tmp>, which does not exist on Windows.
The Python interpreter is now discovered automatically by probing, in order, C<python3>, C<python>, and the Windows C<py> launcher, accepting the first that reports Python 3. This fixes Windows, where the interpreter is typically named C<python> (not...
The Python script is now executed with the list form of C<system> rather than a single shell string, so script paths containing spaces (common on Windows, e.g. C<C:\Users\First Last\AppData\Local\Temp>) no longer break execution.
The C<Creator> metadata embedded in the output file is now passed through C<write_data> (base64), so Windows paths containing backslashes no longer produce invalid escape sequences (e.g. C<\U> in C<C:\Users>) in the generated Python string literal.
On Windows, C<Win32::Console::ANSI> is loaded if available (it is optional, not a hard dependency) so colored status messages render on legacy consoles.
=head3 Crashes / generated-code fixes
C<violinplot> is now a callable wrapper; it was exported and dispatched but never defined, so calling it died with "Undefined subroutine".
C<hist> with an array of C<bins> no longer emits a stray double-quote (e.g. C<[0,2,4"]>) that caused a Python C<SyntaxError>.
C<hexbin> and C<hist2d> no longer pass C<cblabel> twice (once inside the option string and again as C<< label =E<gt> ... >>), which previously caused a duplicate-keyword C<SyntaxError>.
C<scatter> with a scalar C<set.options> no longer emits a doubled comma (C<scatter(x, y, , ...)>), which was a C<SyntaxError>.
Stacked C<barh> now uses the C<left> keyword for stacking instead of C<bottom>, which collided with C<barh>'s own C<bottom> (y-position) parameter and raised "got multiple values for keyword argument 'bottom'".
C<colored_table> with C<cb_logscale> together with C<cb_min>/C<cb_max> no longer emits C<LogNorm(, vmin=...)> with a leading comma (a C<SyntaxError>).
C<plot> with a hash of data and a scalar C<set.options> no longer crashes by dereferencing a string as a hash under C<strict refs>.
C<plot> with a hash of data now accepts a scalar C<twinx> naming a data key (e.g. C<< twinx =E<gt> 'pressure' >>); previously the value was wrongly required to be a digit string, making key-named C<twinx> impossible.
Grouped bar plots with a single scalar C<color> (e.g. C<< color =E<gt> 'green' >>) no longer crash trying to dereference the string as an array; the color is applied to all series.
=head3 Incorrect-output fixes
C<colored_table> no longer clobbers asymmetric data: filling undefined cells with C<np.nan> previously also overwrote the mirror cell, destroying defined values (if C<< A-E<gt>B >> was defined but C<< B-E<gt>A >> was not, both became C<NaN>).
C<colored_table> now honors C<cb_min> and C<cb_max>; they were read from the wrong hash (C<$args> instead of the plot options) and so were silently ignored.
C<colored_table> now honors the C<cmap> option; the color map and C<set_bad> color were hard-coded to C<gist_rainbow> regardless of the C<cmap> given. The colormap is copied before calling C<set_bad>, as registered colormaps are immutable in current ...
C<colored_table> default row labels now mirror the column labels, matching the matrix that is actually built; with asymmetric data the old default could produce a row-label count mismatch ("'rowLabels' must be of length N").
C<scatter> (single set, three keys) now honors the C<cmap> option instead of always using C<gist_rainbow>.
C<scatter> now validates undefined values in I<both> coordinate keys; the undefined-data check previously inspected only the first key.
Grouped, non-stacked bar widths are now divided by the number of bar series (plus one), not by a constant; the old divisor came from a hash that always held exactly one key, so groups with more than a few series overlapped their neighbors.
The C<wide> plot no longer clamps the upper standard-deviation band at C<1>; that clamp assumed data in the range C<[0, 1]> and clipped ordinary data (the documented example reaches roughly C<1.9>).
Numeric arguments to C<plt> methods (e.g. C<< margins =E<gt> 0.2 >>) are no longer quoted into strings; C<print_type> now recognizes numbers.
C<plt.show()> is now emitted after C<plt.savefig()> (and only once), so using C<show> no longer writes the file only after the interactive window is closed; C<output.file> is no longer required when C<show> is requested.
The C<add> overlay's C<plot.type> now correctly falls back to the parent plot's type when omitted, in both single- and multi-plot calls; the fallback was previously unreachable dead code, and an undefined type could be dispatched on.
=head3 Cleanups
Removed corrupted entries from the method whitelists (C<'set_mouseover( '> and a leading-space C<' FixedFormatter'>) that made those options unusable.
Removed a stray default applied to the wrong hash in C<violin>, two empty dead C<if> blocks, and a duplicated C<die>.
=head2 0.27
Better warnings for undefined data in C<scatter>
C<color_key> didn't work properly for multiple sets of data in C<scatter>, which has now been fixed
=head2 0.26
C<ncol> & C<nrow> are synonymous with C<ncols> and C<nrows> respectively; testing now reflects these two specifically numeric options
no longer exports Data::Printer and Devel::Confess with the module, but is still used inside the module
'show.legend' option added to "hist", which is automatically turned off if there is only 1 group
"add" group is no longer deleted
"boxplot", "hist", and "violin" can take a single array, simplifying calls without requiring useless single keys when calling a single distribution
C<cb_min> and C<cb_max> now work for colored_table
"write_data" is no longer used in hist, as it prints numbers as strings (python3's types are a headache)
Instead, all values are checked in hist for being numeric before being sent to "write_data"
re-use undefined error array in hist_helper (slightly less RAM use)
=head2 0.25
re-used error array in scatter_helper
better warnings for undefined values in multiple-set scatterplots
fixed bug in scatterplot, where different sets would have the same label
"logscale" now available with "boxplot, "hist", "plot", "scatter"
$VERSION now prints with metadata for SVG output files, which required minor changes to testing
slightly better warnings in plot_helper
removed duplicate check from hist2d_helper
better warnings if wrong data types are given to "add"
Fixed bug in scatterplot, where color key could repeat on axes
=head2 colorbar can now be in logscale for colored_table
=head2 ## 0.24
Newlines are now possible in key names for barplot and pie; other characters may be fixed too
@prop_cycle is only now taking RAM/valid where it's needed
( run in 0.680 second using v1.01-cache-2.11-cpan-7fcb06a456a )