package Chart::GGPlot::Backend::Plotly::Util; # ABSTRACT: Utilities used by Chart::GGPlot::Backend::Plotly use Chart::GGPlot::Setup qw(:base :pdl); our $VERSION = '0.0010_01'; # TRIAL VERSION use Data::Frame; use Data::Munge qw(elem); use List::AllUtils qw(all min max pairmap pairwise reduce); use PDL::Primitive qw(which); use Types::PDL qw(Piddle); use Types::Standard qw(Str); use parent qw(Exporter::Tiny); use Chart::GGPlot::Util::Scales qw( csshex_to_rgb255 colorname_to_csshex ); use constant br => '<br />'; our @EXPORT_OK = qw( pt_to_px cex_to_px br to_rgb group_to_NA pdl_to_plotly ribbon ); our %EXPORT_TAGS = ( all => \@EXPORT_OK ); my $dpi = 96; fun pt_to_px ($x) { $x / 72 * $dpi } # This is approximately similar to the size in ggplot2. # Default R fontsize is 12pt. And R scales many symbols by 0.75. # 0.3 is a magic number from my guess. fun cex_to_px ($x) { pt_to_px( 12 * $x * 0.75 * 0.3 ) } sub _rgb { my ($c) = @_; return 'transparent' if $c eq 'BAD'; return $c =~ /^#/ ? $c : colorname_to_csshex($c); } sub _rgba { my ($c, $a) = @_; return 'transparent' if $c eq 'BAD'; $c = $c =~ /^#/ ? $c : colorname_to_csshex($c); return $c if $a == 1; if ($c =~ /^#/) { return sprintf( "rgba(%s,%s,%s,%s)", csshex_to_rgb255($c), 0+sprintf("%.2f", $a) # 0+ for removing trailing zeros ); } return $c; } # plotly does not understands some non-rgb colors like "grey35" fun to_rgb ($color, $alpha=pdl(1)) { state $check = Type::Params::compile((Piddle | Str), Piddle); ($color, $alpha) = $check->($color, $alpha); if ( !ref($color) ) { return _rgba($color, $alpha->at(0)); } else { if ($alpha->length != $color->length and $alpha->length != 1) { die "alpha must be of length 1 or the same length as x"; } $alpha = $alpha->setbadtoval(1); $alpha->where($alpha > 1) .= 1; $alpha->where($alpha < 0) .= 0; my @color = $color->flatten; my @rgba; if ($alpha->uniq->length == 1 and $alpha->at(0) == 1) { @rgba = map { _rgb($_) } @color; } else { my @alpha = $alpha->flatten; @rgba = pairwise { _rgba($a, $b) } @color, @alpha; } return PDL::SV->new(\@rgba); } } fun group_to_NA ($df, :$group_vars=['group'], :$nested=[], :$ordered=[], :$retrace_first=false) { return $df if ( $df->nrow == 0 ); my $df_names = $df->names; $group_vars = $group_vars->intersect($df_names); $nested = $nested->intersect($df_names); $ordered = $ordered->intersect($df_names); # if group does not exist, just order the rows and exit unless ( $group_vars->length ) { my @key_vars = ( @$nested, @$ordered ); return ( @key_vars ? $df->sort( \@key_vars ) : $df ); } if ( $df->nrow == 1 ) { return ( $retrace_first ? $df->append( $df->select_rows(0) ) : $df ); } # ordered the rows $df = $df->sort( [ @$nested, @$group_vars, @$ordered ] ); #inserting NAs to ensure each "group" my $changes_group = ( $df->select_columns($group_vars)->id->diff != 0 ); my $to_insert = $changes_group; if ( $nested->length > 0 ) { my $changes_nested = ( $df->select_columns($nested)->id->diff == 0 ); $to_insert = ( $to_insert & $changes_nested ); } my $idx_to_insert = which($to_insert); # insert after the indices # prepare row indices, each item has start row, stop row, # and places to retrace. state $split_range = sub { my ( $upper, $after ) = @_; return ( pairmap { [ $a .. $b ] } ( 0, ( map { ( $_, $_ + 1 ) } grep { $_ < $upper } $after->flatten ), $upper ) ); }; my @group_rows = $split_range->( $df->nrow - 1, $idx_to_insert ); my @splitted = map { $df->select_rows($_) } @group_rows; if ($retrace_first) { my $to_retrace = $changes_group->glue( 0, pdl( [1] ) ); my @retrace_at = map { my $rindices = pdl($_); which( $to_retrace->slice($rindices) )->unpdl; } @group_rows; @splitted = map { my $d = $splitted[$_]; my @retrace_rows = $split_range->( $d->nrow - 1, $retrace_at[$_] ); my @splitted_for_retrace = map { my $x = $d->select_rows($_); $x->append( $x->select_rows( [0] ) ) } @retrace_rows; reduce { $a->append($b); } ( shift @splitted_for_retrace ), @splitted_for_retrace; } ( 0 .. $#splitted ); } my @key_vars = ( @$nested, @$group_vars ); my @vars_to_na = grep { !elem( $_, \@key_vars ) } $df->names->flatten; return ( reduce { # copy last row and make it a NA row my $na = $a->select_rows( [ $a->nrow - 1 ] )->copy; for my $var (@vars_to_na) { $na->at($var)->setbadat(0); } $a->append($na)->append($b); } ( shift @splitted ), @splitted ); } # prepare from piddle to aref or value, to be send to Chart::Plotly fun pdl_to_plotly ($p, $allow_collapse=false) { return [] if $p->length == 0; if ( $p->badflag ) { return $p->unpdl; } if ($allow_collapse) { return $p->at(0) if $p->length == 1; if ( $p->$_DOES('PDL::SV') ) { my @lst = $p->flatten; my $elem = shift @lst; if ( all { $_ eq $elem } @lst ) { return $elem; } } else { my $elem = $p->at(0); if ( ( $p == $elem )->all ) { return $elem; } } } return $p->unpdl; } # Transform geom_smooth prediction confidence intervals into format plotly # likes fun ribbon ($data) { my $n = $data->nrow; my $tmp = $data->sort( ['x'] ); my $tmp2 = $data->sort( ['x'], false ); my $not_used = $data->names->setdiff( [qw(x ymin ymax y)] ); # top-half of ribbon my @others = map { $_ => $tmp->at($_) } @$not_used; my $data1 = Data::Frame->new( columns => [ x => $tmp->at('x'), y => $tmp->at('ymax'), @others, ] ); # bottom-half of ribbon my @others2 = map { $_ => $tmp2->at($_) } @$not_used; my $data2 = Data::Frame->new( columns => [ x => $tmp2->at('x'), y => $tmp2->at('ymin'), @others2, ] ); return $data1->rbind($data2); } 1; __END__ =pod =encoding UTF-8 =head1 NAME Chart::GGPlot::Backend::Plotly::Util - Utilities used by Chart::GGPlot::Backend::Plotly =head1 VERSION version 0.0010_01 =head1 FUNCTIONS =head2 group_to_NA group_to_NA($df, :$group_vars=['group'], :$nested=[], :$ordered=[], :$retrace_first=false) If a group of scatter traces share the same non-positional characteristics (i.e., color, fill, etc), it is more efficient to draw them as a single trace with missing values that separate the groups (instead of multiple traces) In this case, one should also take care to make sure L<connectgaps|https://plot.ly/r/reference/#scatter-connectgaps> is set to false. Returns a data frame with rows ordered by C<$nested> then C<$group_vars> then C<$ordered>. As long as C<$group_vars> contains valid variable names, new rows will be inserted to separate the groups, at places where group changes in each chunk of same C<$nested> values. =head1 AUTHOR Stephan Loyd <sloyd@cpan.org> =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2019 by Stephan Loyd. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut