clean up common value range calculation
[barcat.git] / barcat
diff --git a/barcat b/barcat
index 15a2269c9829079d81e0b86368e9a398e3d566f4..b6d2f4da87a4e39fb2dcf813ae40c75bca217a30 100755 (executable)
--- a/barcat
+++ b/barcat
@@ -1,10 +1,9 @@
 #!/usr/bin/perl -CA
 #!/usr/bin/perl -CA
-use 5.018;
+use 5.014;
 use warnings;
 use utf8;
 use List::Util qw( min max sum );
 use open qw( :std :utf8 );
 use warnings;
 use utf8;
 use List::Util qw( min max sum );
 use open qw( :std :utf8 );
-use experimental qw( lexical_subs );
 
 our $VERSION = '1.06';
 
 
 our $VERSION = '1.06';
 
@@ -35,7 +34,7 @@ GetOptions(\%opt,
        'hidemax=i',
        'minval=f',
        'maxval=f',
        'hidemax=i',
        'minval=f',
        'maxval=f',
-       'limit|L=s' => sub {
+       'limit|L:s' => sub {
                my ($optname, $optval) = @_;
                $optval ||= 0;
                ($opt{hidemin}, $opt{hidemax}) =
                my ($optname, $optval) = @_;
                $optval ||= 0;
                ($opt{hidemin}, $opt{hidemax}) =
@@ -50,10 +49,24 @@ GetOptions(\%opt,
                $opt{'graph-format'} = substr $_[1], 0, 1;
        },
        'spark:s' => sub {
                $opt{'graph-format'} = substr $_[1], 0, 1;
        },
        'spark:s' => sub {
-               $opt{spark} = [split //, $_[1] || '▁▂▃▄▅▆▇█'];
+               $opt{spark} = [split //, $_[1] || ' ▁▂▃▄▅▆▇█'];
        },
        },
-       'palette:s' => sub {
-               $opt{palette} = [ split /\s/, $_[1] ];
+       'palette=s' => sub {
+               $opt{palette} = {
+                       fire   => [qw( 90 31 91 33 93 97 96 )],
+                       fire88 => [map {"38;5;$_"} qw(
+                               80  32 48 64  68 72 76  77 78 79  47
+                       )],
+                       fire256=> [map {"38;5;$_"} qw(
+                               235  52 88 124 160 196
+                               202 208 214 220 226  227 228 229 230 231  159
+                       )],
+                       ramp88 => [map {"38;5;$_"} qw(
+                               64 65 66 67 51 35 39 23 22 26 25 28
+                       )],
+                       whites => [qw( 1;30 0;37 1;37 )],
+                       greys  => [map {"38;5;$_"} 52, 235..255, 47],
+               }->{$_[1]} // [ split /[^0-9;]/, $_[1] ];
        },
        'stat|s!',
        'signal-stat=s',
        },
        'stat|s!',
        'signal-stat=s',
@@ -87,7 +100,7 @@ GetOptions(\%opt,
        },
 ) or exit 64;  # EX_USAGE
 
        },
 ) or exit 64;  # EX_USAGE
 
-$opt{width} ||= $ENV{COLUMNS} || 80;
+$opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
 $opt{color} //= -t *STDOUT;  # enable on tty
 $opt{'graph-format'} //= '-';
 $opt{trim}   *= $opt{width} / 100 if $opt{trimpct};
 $opt{color} //= -t *STDOUT;  # enable on tty
 $opt{'graph-format'} //= '-';
 $opt{trim}   *= $opt{width} / 100 if $opt{trimpct};
@@ -98,6 +111,8 @@ $opt{'value-length'} = 1 if $opt{unmodified};
 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
 $opt{palette} //= $opt{color} && [31, 90, 32];
 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
 $opt{palette} //= $opt{color} && [31, 90, 32];
+$opt{input} = @ARGV && $ARGV[0] =~ m/\A[-0-9]/ ? \@ARGV : undef
+       and undef $opt{interval};
 
 my (@lines, @values, @order);
 
 
 my (@lines, @values, @order);
 
@@ -118,8 +133,10 @@ if (defined $opt{interval}) {
        } or warn $@, "Expect slowdown with large datasets!\n";
 }
 
        } or warn $@, "Expect slowdown with large datasets!\n";
 }
 
-my $valmatch = qr/$opt{anchor} ( \h* -? [0-9]* \.? [0-9]+ (?: e[+-]?[0-9]+ )? |)/x;
-while (readline) {
+my $valmatch = qr<
+       $opt{anchor} ( \h* -? [0-9]* \.? [0-9]+ (?: e[+-]?[0-9]+ )? |)
+>x;
+while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
        s/\r?\n\z//;
        s/^\h*// unless $opt{unmodified};
        push @values, s/$valmatch/\n/ && $1;
        s/\r?\n\z//;
        s/^\h*// unless $opt{unmodified};
        push @values, s/$valmatch/\n/ && $1;
@@ -139,6 +156,10 @@ while (readline) {
                and $. % $opt{interval} == 0;
 }
 
                and $. % $opt{interval} == 0;
 }
 
+if ($opt{'zero-missing'}) {
+       push @values, (0) x 10;
+}
+
 $SIG{INT} = 'DEFAULT';
 
 sub color {
 $SIG{INT} = 'DEFAULT';
 
 sub color {
@@ -155,14 +176,18 @@ state $nr = $opt{hidemin} ? $opt{hidemin} - 1 : 0;
 @lines > $nr or return unless $opt{hidemin};
 
 @order = sort { $b <=> $a } @order unless tied @order;
 @lines > $nr or return unless $opt{hidemin};
 
 @order = sort { $b <=> $a } @order unless tied @order;
-my $maxval = $opt{maxval} // ($opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] : $order[0]) // 0;
+my $maxval = $opt{maxval} // (
+       $opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] :
+       $order[0]
+) // 0;
 my $minval = $opt{minval} // min $order[-1] // (), 0;
 my $minval = $opt{minval} // min $order[-1] // (), 0;
+my $range = $maxval - $minval;
 my $lenval = $opt{'value-length'} // max map { length } @order;
 my $len    = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
        max map { length $values[$_] && length $lines[$_] }
                0 .. min $#lines, $opt{hidemax} || ();  # left padding
 my $lenval = $opt{'value-length'} // max map { length } @order;
 my $len    = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
        max map { length $values[$_] && length $lines[$_] }
                0 .. min $#lines, $opt{hidemax} || ();  # left padding
-my $size   = ($maxval - $minval) &&
-       ($opt{width} - $lenval - $len) / ($maxval - $minval);  # bar multiplication
+my $size   = $range &&
+       ($opt{width} - $lenval - $len) / $range;  # bar multiplication
 
 my @barmark;
 if ($opt{markers} and $size > 0) {
 
 my @barmark;
 if ($opt{markers} and $size > 0) {
@@ -192,7 +217,7 @@ if ($opt{markers} and $size > 0) {
                        ($lastmax - $minval) * $size + .5,
                        '-' x (($values[$nr - 1] - $minval) * $size);
                print color(92);
                        ($lastmax - $minval) * $size + .5,
                        '-' x (($values[$nr - 1] - $minval) * $size);
                print color(92);
-               say '+' x (($maxval - $lastmax - $minval) * $size + .5);
+               say '+' x (($range - $lastmax) * $size + .5);
                print color(0);
                $lastmax = $maxval;
        }
                print color(0);
                $lastmax = $maxval;
        }
@@ -213,7 +238,7 @@ sub sival {
 say(
        color(31), sprintf('%*s', $lenval, $minval),
        color(90), '-', color(36), '+',
 say(
        color(31), sprintf('%*s', $lenval, $minval),
        color(90), '-', color(36), '+',
-       color(32), sprintf('%*s', $size * ($maxval - $minval) - 3, $maxval),
+       color(32), sprintf('%*s', $size * $range - 3, $maxval),
        color(90), '-', color(36), '+',
        color(0),
 ) if $opt{header};
        color(90), '-', color(36), '+',
        color(0),
 ) if $opt{header};
@@ -221,25 +246,32 @@ say(
 while ($nr <= $#lines) {
        $nr >= $opt{hidemax} and last if defined $opt{hidemax};
        my $val = $values[$nr];
 while ($nr <= $#lines) {
        $nr >= $opt{hidemax} and last if defined $opt{hidemax};
        my $val = $values[$nr];
-       my $rel = length $val && ($val - $minval) / ($maxval - $minval);
+       my $rel = length $val && $range && ($val - $minval) / $range;
+       my $color = !length $val || !$opt{palette} ? undef :
+               $val == $order[0] ? $opt{palette}->[-1] : # max
+               $val == $order[-1] ? $opt{palette}->[0] : # min
+               $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
 
        if ($opt{spark}) {
 
        if ($opt{spark}) {
-               print color($opt{palette}->[ $rel * $#{$opt{palette}} ]) if $opt{palette};
-               print $opt{spark}->[ $rel * $#{$opt{spark}} ];
+               say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
+               print color($color), $opt{spark}->[
+                       !$val ? 0 : # blank
+                       $val == $order[0] ? -1 : # max
+                       $val == $order[-1] ? 1 : # min
+                       $#{$opt{spark}} < 3 ? 1 :
+                       $rel * ($#{$opt{spark}} - 3) + 2.5
+               ];
                next;
        }
 
        if (length $val) {
                next;
        }
 
        if (length $val) {
-               my $color = !$opt{palette} ? undef :
-                       $val == $order[0] ? $opt{palette}->[-1] : # max
-                       $val == $order[-1] ? $opt{palette}->[0] : # min
-                       $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
                $val = $opt{units} ? sival($val) : sprintf "%*s", $lenval, $val;
                color($color) for $val;
        }
        my $line = $lines[$nr] =~ s/\n/$val/r;
        printf '%-*s', $len + length($val), $line;
                $val = $opt{units} ? sival($val) : sprintf "%*s", $lenval, $val;
                color($color) for $val;
        }
        my $line = $lines[$nr] =~ s/\n/$val/r;
        printf '%-*s', $len + length($val), $line;
-       print $barmark[$_] // $opt{'graph-format'} for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
+       print $barmark[$_] // $opt{'graph-format'}
+               for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
        say '';
 }
 continue {
        say '';
 }
 continue {
@@ -253,12 +285,13 @@ sub show_stat {
        if ($opt{hidemin} or $opt{hidemax}) {
                $opt{hidemin} ||= 1;
                $opt{hidemax} ||= @lines;
        if ($opt{hidemin} or $opt{hidemax}) {
                $opt{hidemin} ||= 1;
                $opt{hidemax} ||= @lines;
-               printf '%s of ', sum(@values[$opt{hidemin} - 1 .. $opt{hidemax} - 1]) // 0;
+               printf '%s of ', sum(grep {length} @values[$opt{hidemin} - 1 .. $opt{hidemax} - 1]) // 0;
        }
        if (@order) {
                my $total = sum @order;
        }
        if (@order) {
                my $total = sum @order;
-               printf '%s total', color(1) . $total . color(0);
-               printf ' in %d values', scalar @values;
+               printf '%s total', color(1) . sprintf('%.8g', $total) . color(0);
+               printf ' in %d values', scalar @order;
+               printf ' over %d lines', scalar @lines if @order != @lines;
                printf(' (%s min, %s avg, %s max)',
                        color(31) . $order[-1] . color(0),
                        color(36) . (sprintf '%*.*f', 0, 2, $total / @order) . color(0),
                printf(' (%s min, %s avg, %s max)',
                        color(31) . $order[-1] . color(0),
                        color(36) . (sprintf '%*.*f', 0, 2, $total / @order) . color(0),
@@ -286,11 +319,12 @@ barcat - graph to visualize input values
 
 =head1 SYNOPSIS
 
 
 =head1 SYNOPSIS
 
-B<barcat> [<options>] [<input>]
+B<barcat> [<options>] [<file>... | <numbers>]
 
 =head1 DESCRIPTION
 
 
 =head1 DESCRIPTION
 
-Visualizes relative sizes of values read from input (file(s) or STDIN).
+Visualizes relative sizes of values read from input
+(parameters, file(s) or STDIN).
 Contents are concatenated similar to I<cat>,
 but numbers are reformatted and a bar graph is appended to each line.
 
 Contents are concatenated similar to I<cat>,
 but numbers are reformatted and a bar graph is appended to each line.
 
@@ -348,7 +382,7 @@ unless C<--length=0>.
 Prepend a dash (i.e. make negative) to enforce padding
 regardless of encountered contents.
 
 Prepend a dash (i.e. make negative) to enforce padding
 regardless of encountered contents.
 
-=item -L, --limit=(<count>|<start>-[<end>])
+=item -L, --limit[=(<count> | <start>-[<end>])]
 
 Stop output after a number of lines.
 All input is still counted and analyzed for statistics,
 
 Stop output after a number of lines.
 All input is still counted and analyzed for statistics,
@@ -396,7 +430,7 @@ Bars extend from 0 or the minimum value if lower,
 to the largest value encountered.
 These options can be set to customize this range.
 
 to the largest value encountered.
 These options can be set to customize this range.
 
-=item --palette=<color>...
+=item --palette=(<preset> | <color>...)
 
 Override colors of parsed numbers.
 Can be any CSI escape, such as I<90> for default dark grey,
 
 Override colors of parsed numbers.
 Can be any CSI escape, such as I<90> for default dark grey,
@@ -471,7 +505,7 @@ Monitor network latency from prefixed results:
 
 Commonly used after counting, for example users on the current server:
 
 
 Commonly used after counting, for example users on the current server:
 
-    users | sed 's/ /\n/g' | sort | uniq -c | barcat
+    users | tr ' ' '\n' | sort | uniq -c | barcat
 
 Letter frequencies in text files:
 
 
 Letter frequencies in text files:
 
@@ -512,8 +546,7 @@ USD/EUR exchange rate from CSV provided by the ECB:
          -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
     grep '^[12]' | barcat -f',\K' --value-length=7
 
          -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
     grep '^[12]' | barcat -f',\K' --value-length=7
 
-Total population history from the World Bank dataset (XML):
-External datasets, like total population in XML from the World Bank:
+Total population history in XML from the World Bank:
 
     curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
     xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
 
     curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
     xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
@@ -527,7 +560,11 @@ Or the top 3 most frequent authors with statistics over all:
 
     git shortlog -sn | barcat -L3 -s
 
 
     git shortlog -sn | barcat -L3 -s
 
-Activity of the last days (substitute date C<-v-{}d> on BSD):
+Sparkline graphics of simple input given as inline parameters:
+
+       barcat --spark= 3 1 4 1 5 0 9 2 4
+
+Activity graph of the last days (substitute date C<-v-{}d> on BSD):
 
     ( git log --pretty=%ci --since=30day | cut -b-10
       seq 0 30 | xargs -i date +%F -d-{}day ) |
 
     ( git log --pretty=%ci --since=30day | cut -b-10
       seq 0 30 | xargs -i date +%F -d-{}day ) |