distribute palette parameters over value range
[barcat.git] / barcat
diff --git a/barcat b/barcat
index 1114656f74c0cab9a0a69eb91f3599ca78ffea26..15a2269c9829079d81e0b86368e9a398e3d566f4 100755 (executable)
--- a/barcat
+++ b/barcat
@@ -1,4 +1,4 @@
-#!/usr/bin/env perl
+#!/usr/bin/perl -CA
 use 5.018;
 use warnings;
 use utf8;
 use 5.018;
 use warnings;
 use utf8;
@@ -6,13 +6,9 @@ use List::Util qw( min max sum );
 use open qw( :std :utf8 );
 use experimental qw( lexical_subs );
 
 use open qw( :std :utf8 );
 use experimental qw( lexical_subs );
 
-our $VERSION = '1.03';
+our $VERSION = '1.06';
 
 use Getopt::Long '2.33', qw( :config gnu_getopt );
 
 use Getopt::Long '2.33', qw( :config gnu_getopt );
-sub podexit {
-       require Pod::Usage;
-       Pod::Usage::pod2usage(-exitval => 0, -perldocopt => '-oman', @_);
-}
 my %opt;
 GetOptions(\%opt,
        'color|c!',
 my %opt;
 GetOptions(\%opt,
        'color|c!',
@@ -37,6 +33,8 @@ GetOptions(\%opt,
        'value-length=i',
        'hidemin=i',
        'hidemax=i',
        'value-length=i',
        'hidemin=i',
        'hidemax=i',
+       'minval=f',
+       'maxval=f',
        'limit|L=s' => sub {
                my ($optname, $optval) = @_;
                $optval ||= 0;
        'limit|L=s' => sub {
                my ($optname, $optval) = @_;
                $optval ||= 0;
@@ -46,53 +44,109 @@ GetOptions(\%opt,
                        " (range expected)\n"
                );
        },
                        " (range expected)\n"
                );
        },
+       'header!',
        'markers|m=s',
        'markers|m=s',
+       'graph-format=s' => sub {
+               $opt{'graph-format'} = substr $_[1], 0, 1;
+       },
+       'spark:s' => sub {
+               $opt{spark} = [split //, $_[1] || '▁▂▃▄▅▆▇█'];
+       },
+       'palette:s' => sub {
+               $opt{palette} = [ split /\s/, $_[1] ];
+       },
        'stat|s!',
        'stat|s!',
+       'signal-stat=s',
        'unmodified|u!',
        'width|w=i',
        'unmodified|u!',
        'width|w=i',
-       'usage|h' => sub { podexit() },
-       'help'    => sub { podexit(-verbose => 2) },
+       'version' => sub {
+               say "barcat version $VERSION";
+               exit;
+       },
+       'usage|h' => sub {
+               local $/;
+               my $pod = readline *DATA;
+               $pod =~ s/^=over\K/ 22/m;  # indent options list
+               $pod =~ s/^=item \N*\n\n\N*\n\K(?:(?:^=over.*?^=back\n)?(?!=)\N*\n)*/\n/msg;
+
+               require Pod::Usage;
+               my $parser = Pod::Usage->new;
+               $parser->select('SYNOPSIS', 'OPTIONS');
+               $parser->output_string(\my $contents);
+               $parser->parse_string_document($pod);
+
+               $contents =~ s/\n(?=\n\h)//msg;  # strip space between items
+               print $contents;
+               exit;
+       },
+       'help|?'  => sub {
+               require Pod::Usage;
+               Pod::Usage::pod2usage(
+                       -exitval => 0, -perldocopt => '-oman', -verbose => 2,
+               );
+       },
 ) or exit 64;  # EX_USAGE
 
 $opt{width} ||= $ENV{COLUMNS} || 80;
 $opt{color} //= -t *STDOUT;  # enable on tty
 ) or exit 64;  # EX_USAGE
 
 $opt{width} ||= $ENV{COLUMNS} || 80;
 $opt{color} //= -t *STDOUT;  # enable on tty
+$opt{'graph-format'} //= '-';
 $opt{trim}   *= $opt{width} / 100 if $opt{trimpct};
 $opt{units}   = [split //, ' kMGTPEZYyzafpnμm'] if $opt{'human-readable'};
 $opt{anchor} //= qr/\A/;
 $opt{'value-length'} = 6 if $opt{units};
 $opt{trim}   *= $opt{width} / 100 if $opt{trimpct};
 $opt{units}   = [split //, ' kMGTPEZYyzafpnμm'] if $opt{'human-readable'};
 $opt{anchor} //= qr/\A/;
 $opt{'value-length'} = 6 if $opt{units};
+$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];
+
+my (@lines, @values, @order);
+
+$SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
+$SIG{ALRM} = sub {
+       show_lines();
+       alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
+};
+$SIG{INT} = \&show_exit;
 
 if (defined $opt{interval}) {
        $opt{interval} ||= 1;
 
 if (defined $opt{interval}) {
        $opt{interval} ||= 1;
-       $SIG{ALRM} = sub {
-               show_lines();
-               alarm $opt{interval};
-       };
-       alarm $opt{interval};
-}
+       alarm $opt{interval} if $opt{interval} > 0;
 
 
-$SIG{INT} = 'IGNORE';  # continue after assumed eof
+       eval {
+               require Tie::Array::Sorted;
+               tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
+       } or warn $@, "Expect slowdown with large datasets!\n";
+}
 
 
-my (@lines, @values, @order);
 my $valmatch = qr/$opt{anchor} ( \h* -? [0-9]* \.? [0-9]+ (?: e[+-]?[0-9]+ )? |)/x;
 while (readline) {
        s/\r?\n\z//;
        s/^\h*// unless $opt{unmodified};
        push @values, s/$valmatch/\n/ && $1;
        push @order, $1 if length $1;
 my $valmatch = qr/$opt{anchor} ( \h* -? [0-9]* \.? [0-9]+ (?: e[+-]?[0-9]+ )? |)/x;
 while (readline) {
        s/\r?\n\z//;
        s/^\h*// unless $opt{unmodified};
        push @values, s/$valmatch/\n/ && $1;
        push @order, $1 if length $1;
-       if (defined $opt{trim}) {
+       if (defined $opt{trim} and defined $1) {
                my $trimpos = abs $opt{trim};
                my $trimpos = abs $opt{trim};
+               $trimpos -= length $1 if $opt{unmodified};
                if ($trimpos <= 1) {
                if ($trimpos <= 1) {
-                       $_ = substr $_, 0, 1;
+                       $_ = substr $_, 0, 2;
                }
                elsif (length > $trimpos) {
                        substr($_, $trimpos - 1) = '…';
                }
        }
        push @lines, $_;
                }
                elsif (length > $trimpos) {
                        substr($_, $trimpos - 1) = '…';
                }
        }
        push @lines, $_;
+       show_lines() if defined $opt{interval} and $opt{interval} < 0
+               and $. % $opt{interval} == 0;
 }
 
 $SIG{INT} = 'DEFAULT';
 
 }
 
 $SIG{INT} = 'DEFAULT';
 
+sub color {
+       $opt{color} and defined $_[0] or return '';
+       return "\e[$_[0]m" if defined wantarray;
+       $_ = color(@_) . $_ . color(0) if defined;
+}
+
 sub show_lines {
 
 state $nr = $opt{hidemin} ? $opt{hidemin} - 1 : 0;
 sub show_lines {
 
 state $nr = $opt{hidemin} ? $opt{hidemin} - 1 : 0;
@@ -100,9 +154,9 @@ state $nr = $opt{hidemin} ? $opt{hidemin} - 1 : 0;
 @lines or return;
 @lines > $nr or return unless $opt{hidemin};
 
 @lines or return;
 @lines > $nr or return unless $opt{hidemin};
 
-@order = sort { $b <=> $a } @order;
-my $maxval = ($opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] : $order[0]) // 0;
-my $minval = min $order[-1] // (), 0;
+@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 $minval = $opt{minval} // min $order[-1] // (), 0;
 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[$_] }
 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[$_] }
@@ -111,25 +165,35 @@ my $size   = ($maxval - $minval) &&
        ($opt{width} - $lenval - $len) / ($maxval - $minval);  # bar multiplication
 
 my @barmark;
        ($opt{width} - $lenval - $len) / ($maxval - $minval);  # bar multiplication
 
 my @barmark;
-if ($opt{markers} // 1 and $size > 0) {
-       my sub orderpos { (($order[$_[0]] + $order[$_[0] + .5]) / 2 - $minval) * $size }
-       $barmark[ (sum(@order) / @order - $minval) * $size ] = '=';  # average
-       $barmark[ orderpos($#order * .31731) ] = '>';
-       $barmark[ orderpos($#order * .68269) ] = '<';
-       $barmark[ orderpos($#order / 2) ] = '+';  # mean
-       $barmark[ -$minval * $size ] = '|' if $minval < 0;  # zero
-       defined and $opt{color} and $_ = "\e[36m$_\e[0m" for @barmark;
+if ($opt{markers} and $size > 0) {
+       for my $markspec (split /\h/, $opt{markers}) {
+               my ($char, $func) = split //, $markspec, 2;
+               my $pos = eval {
+                       if ($func eq 'avg') {
+                               return sum(@order) / @order;
+                       }
+                       elsif ($func =~ /\A([0-9.]+)v\z/) {
+                               my $index = $#order * $1 / 100;
+                               return ($order[$index] + $order[$index + .5]) / 2;
+                       }
+                       else {
+                               return $func;
+                       }
+               } - $minval;
+               $pos >= 0 or next;
+               color(36) for $barmark[$pos * $size] = $char;
+       }
 
        state $lastmax = $maxval;
        if ($maxval > $lastmax) {
                print ' ' x ($lenval + $len);
 
        state $lastmax = $maxval;
        if ($maxval > $lastmax) {
                print ' ' x ($lenval + $len);
-               printf "\e[90m" if $opt{color};
+               printf color(90);
                printf '%-*s',
                        ($lastmax - $minval) * $size + .5,
                        '-' x (($values[$nr - 1] - $minval) * $size);
                printf '%-*s',
                        ($lastmax - $minval) * $size + .5,
                        '-' x (($values[$nr - 1] - $minval) * $size);
-               print "\e[92m" if $opt{color};
+               print color(92);
                say '+' x (($maxval - $lastmax - $minval) * $size + .5);
                say '+' x (($maxval - $lastmax - $minval) * $size + .5);
-               print "\e[0m" if $opt{color};
+               print color(0);
                $lastmax = $maxval;
        }
 }
                $lastmax = $maxval;
        }
 }
@@ -137,53 +201,84 @@ if ($opt{markers} // 1 and $size > 0) {
 @lines > $nr or return if $opt{hidemin};
 
 sub sival {
 @lines > $nr or return if $opt{hidemin};
 
 sub sival {
-       my $unit = int(log($_[0]) / log(1000) - ($_[0] < 1));
-       my $float = $_[0] !~ /^ (?: 0*\.)? [0-9]{1,3} $/x;
-       sprintf('%*.*f%*s',
-               $float ? 5 : 3, $float,  # length and tenths
-               $_[0] / 1000 ** $unit,   # number
-               $float ? 0 : 3,          # unit size
-               $#{$opt{units}} >> 1 < abs $unit ? "e$unit" : $opt{units}->[$unit]
+       my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
+       my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
+       sprintf('%3.*f%1s',
+               $float && ($unit % 3) == ($unit < 0),  # tenths
+               $_[0] / 1000 ** int($unit/3),   # number
+               $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
        );
 }
 
        );
 }
 
+say(
+       color(31), sprintf('%*s', $lenval, $minval),
+       color(90), '-', color(36), '+',
+       color(32), sprintf('%*s', $size * ($maxval - $minval) - 3, $maxval),
+       color(90), '-', color(36), '+',
+       color(0),
+) if $opt{header};
+
 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);
+
+       if ($opt{spark}) {
+               print color($opt{palette}->[ $rel * $#{$opt{palette}} ]) if $opt{palette};
+               print $opt{spark}->[ $rel * $#{$opt{spark}} ];
+               next;
+       }
+
        if (length $val) {
        if (length $val) {
-               my $color = !$opt{color} ? 0 :
-                       $val == $order[0] ? 32 : # max
-                       $val == $order[-1] ? 31 : # min
-                       90;
+               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;
                $val = $opt{units} ? sival($val) : sprintf "%*s", $lenval, $val;
-               $val = "\e[${color}m$val\e[0m" if $color;
+               color($color) for $val;
        }
        my $line = $lines[$nr] =~ s/\n/$val/r;
        printf '%-*s', $len + length($val), $line;
        }
        my $line = $lines[$nr] =~ s/\n/$val/r;
        printf '%-*s', $len + length($val), $line;
-       print $barmark[$_] // '-' for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
+       print $barmark[$_] // $opt{'graph-format'} for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
        say '';
        say '';
-
+}
+continue {
        $nr++;
 }
        $nr++;
 }
+say $opt{palette} ? color(0) : '' if $opt{spark};
 
 }
 
 }
-show_lines();
 
 
-if ($opt{stat}) {
+sub show_stat {
        if ($opt{hidemin} or $opt{hidemax}) {
                $opt{hidemin} ||= 1;
                $opt{hidemax} ||= @lines;
                printf '%s of ', sum(@values[$opt{hidemin} - 1 .. $opt{hidemax} - 1]) // 0;
        }
        if ($opt{hidemin} or $opt{hidemax}) {
                $opt{hidemin} ||= 1;
                $opt{hidemax} ||= @lines;
                printf '%s of ', sum(@values[$opt{hidemin} - 1 .. $opt{hidemax} - 1]) // 0;
        }
-       my $total = sum @order;
-       printf '%s total', $total;
-       printf ' in %d values', scalar @values;
-       printf ' (%s min, %*.*f avg, %s max)',
-               $order[-1], 0, 2, $total / @order, $order[0];
+       if (@order) {
+               my $total = sum @order;
+               printf '%s total', color(1) . $total . color(0);
+               printf ' in %d values', scalar @values;
+               printf(' (%s min, %s avg, %s max)',
+                       color(31) . $order[-1] . color(0),
+                       color(36) . (sprintf '%*.*f', 0, 2, $total / @order) . color(0),
+                       color(32) . $order[0] . color(0),
+               );
+       }
        say '';
 }
 
        say '';
 }
 
+sub show_exit {
+       show_lines();
+       show_stat() if $opt{stat};
+       exit 130 if @_;  # 0x80+signo
+       exit;
+}
+
+show_exit();
+
 __END__
 __END__
+=encoding utf8
 
 =head1 NAME
 
 
 =head1 NAME
 
@@ -199,6 +294,12 @@ Visualizes relative sizes of values read from input (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.
 
+Don't worry, barcat does not drink and divide.
+It can has various options for input and output (re)formatting,
+but remains limited to one-dimensional charts.
+For more complex graphing needs
+you'll need a larger animal like I<gnuplot>.
+
 =head1 OPTIONS
 
 =over
 =head1 OPTIONS
 
 =over
@@ -221,6 +322,10 @@ A string can indicate the starting position of a value
 or capture the numbers itself,
 for example I<-f'(\d+)'> for the first digits anywhere.
 
 or capture the numbers itself,
 for example I<-f'(\d+)'> for the first digits anywhere.
 
+=item --header
+
+Prepend a chart axis with minimum and maximum values labeled.
+
 =item -H, --human-readable
 
 Format values using SI unit prefixes,
 =item -H, --human-readable
 
 Format values using SI unit prefixes,
@@ -228,9 +333,10 @@ turning long numbers like I<12356789> into I<12.4M>.
 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
 Short integers are aligned but kept without decimal point.
 
 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
 Short integers are aligned but kept without decimal point.
 
-=item -t, --interval[=<seconds>]
+=item -t, --interval[=(<seconds>|-<lines>)]
 
 
-Interval time to output partial progress.
+Output partial progress every given number of seconds or input lines.
+An update can also be forced by sending a I<SIGALRM> alarm signal.
 
 =item -l, --length=[-]<size>[%]
 
 
 =item -l, --length=[-]<size>[%]
 
@@ -248,38 +354,69 @@ Stop output after a number of lines.
 All input is still counted and analyzed for statistics,
 but disregarded for padding and bar size.
 
 All input is still counted and analyzed for statistics,
 but disregarded for padding and bar size.
 
-=item -m, --markers=
+=item --graph-format=<character>
 
 
-Statistical positions to indicate on bars.
-Cannot be customized yet,
-only disabled by providing an empty argument.
+Glyph to repeat for the graph line.
+Defaults to a dash C<->.
 
 
-Any value enables all marker characters:
+=item -m, --markers=<format>
+
+Statistical positions to indicate on bars.
+A single indicator glyph precedes each position:
 
 =over 2
 
 
 =over 2
 
-=item B<=>
+=item <number>
 
 
-Average:
-the sum of all values divided by the number of counted lines.
+Exact value to match on the axis.
+A vertical bar at the zero crossing is displayed by I<|0>
+for negative values.
+For example I<:3.14> would show a colon at pi.
 
 
-=item B<+>
+=item <percentage>I<v>
 
 
-Mean, median:
+Ranked value at the given percentile.
+The default shows I<+> at I<50v> for the mean or median;
 the middle value or average between middle values.
 the middle value or average between middle values.
+One standard deviation right of the mean is at about I<68.3v>.
+The default includes I<< >31.73v <68.27v >>
+to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
 
 
-=item B<<>
+=item I<avg>
 
 
-Standard deviation left of the mean.
-Only 16% of all values are lower.
+Matches the average;
+the sum of all values divided by the number of counted lines.
+Indicated by default as I<=>.
+
+=back
 
 
-=item B<< > >>
+=item --min=<number>, --max=<number>
 
 
-Standard deviation right of the mean.
-The part between B<< <--> >> encompass all I<normal> results,
-or 68% of all entries.
+Bars extend from 0 or the minimum value if lower,
+to the largest value encountered.
+These options can be set to customize this range.
 
 
-=back
+=item --palette=<color>...
+
+Override colors of parsed numbers.
+Can be any CSI escape, such as I<90> for default dark grey,
+or alternatively I<1;30> for bold black.
+
+In case of additional colors,
+the last is used for values equal to the maximum, the first for minima.
+If unspecified, these are green and red respectively (I<31 90 32>).
+
+=item --spark[=<glyphs>]
+
+Replace lines by I<sparklines>,
+single characters corresponding to input values.
+A specified sequence of unicode characters will be used for
+Of a specified sequence of unicode characters,
+the first one will be used for non-values,
+the last one for the maximum,
+the second (if any) for the minimum,
+and any remaining will be distributed over the range of values.
+Unspecified, block fill glyphs U+2581-2588 will be used.
 
 =item -s, --stat
 
 
 =item -s, --stat
 
@@ -287,7 +424,7 @@ Total statistics after all data.
 
 =item -u, --unmodified
 
 
 =item -u, --unmodified
 
-Do not strip leading whitespace.
+Do not reformat values, keeping leading whitespace.
 Keep original value alignment, which may be significant in some programs.
 
 =item --value-length=<size>
 Keep original value alignment, which may be significant in some programs.
 
 =item --value-length=<size>
@@ -299,60 +436,90 @@ Reserved space for numbers.
 Override the maximum number of columns to use.
 Appended graphics will extend to fill up the entire screen.
 
 Override the maximum number of columns to use.
 Appended graphics will extend to fill up the entire screen.
 
+=item -h, --usage
+
+Overview of available options.
+
+=item --help
+
+Full documentation
+rendered by perldoc.
+
+=item --version
+
+Version information.
+
 =back
 
 =head1 EXAMPLES
 
 =back
 
 =head1 EXAMPLES
 
-Commonly used after counting, such as users on the current server:
+Draw a sine wave:
 
 
-    users | sed 's/ /\n/g' | sort | uniq -c | barcat
+    seq 30 | awk '{print sin($1/10)}' | barcat
 
 
-Letter frequencies in text files:
+Compare file sizes (with human-readable numbers):
 
 
-    cat /usr/share/games/fortunes/*.u8 |
-    perl -CO -nE 'say for grep length, split /\PL*/, uc' |
-    sort | uniq -c | barcat
+    du -d0 -b * | barcat -H
 
 
-Memory usage of user processes:
+Memory usage of user processes with long names truncated:
 
     ps xo %mem,pid,cmd | barcat -l40
 
 
     ps xo %mem,pid,cmd | barcat -l40
 
-Sizes (in megabytes) of all root files and directories:
+Monitor network latency from prefixed results:
+
+    ping google.com | barcat -f'time=\K' -t
+
+Commonly used after counting, for example users on the current server:
 
 
-    du -d0 -m * | barcat
+    users | sed 's/ /\n/g' | sort | uniq -c | barcat
+
+Letter frequencies in text files:
+
+    cat /usr/share/games/fortunes/*.u8 |
+    perl -CS -nE 'say for grep length, split /\PL*/, uc' |
+    sort | uniq -c | barcat
 
 Number of HTTP requests per day:
 
     cat log/access.log | cut -d\  -f4 | cut -d: -f1 | uniq -c | barcat
 
 
 Number of HTTP requests per day:
 
     cat log/access.log | cut -d\  -f4 | cut -d: -f1 | uniq -c | barcat
 
-Any kind of database query with leading counts:
+Any kind of database query with counts, preserving returned alignment:
 
     echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
     psql -t | barcat -u
 
 
     echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
     psql -t | barcat -u
 
-Exchange rate USD/EUR history from CSV download provided by ECB:
+Earthquakes worldwide magnitude 1+ in the last 24 hours:
+
+    https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
+    column -tns, | graph -f4 -u -l80%
+
+External datasets, like movies per year:
+
+    curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json |
+    perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
+
+But please get I<jq> to process JSON
+and replace the manual selection by C<< jq '.[].year' >>.
+
+Pokémon height comparison:
+
+    curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json |
+    jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
+
+USD/EUR exchange rate from CSV provided by the ECB:
 
     curl https://sdw.ecb.europa.eu/export.do \
          -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):
 
     curl https://sdw.ecb.europa.eu/export.do \
          -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:
 
     curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
     xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
     sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
 
 
     curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
     xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
     sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
 
-Movies per year from prepared JSON data:
-
-    curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json |
-    jq '.[].year' | uniq -c | barcat
-
-Pokémon height comparison:
-
-       curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json |
-       jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
-
-Git statistics, such commit count by year:
+And of course various Git statistics, such commit count by year:
 
     git log --pretty=%ci | cut -b-4 | uniq -c | barcat
 
 
     git log --pretty=%ci | cut -b-4 | uniq -c | barcat
 
@@ -360,9 +527,11 @@ Or the top 3 most frequent authors with statistics over all:
 
     git shortlog -sn | barcat -L3 -s
 
 
     git shortlog -sn | barcat -L3 -s
 
-Latency history:
+Activity of the last days (substitute date C<-v-{}d> on BSD):
 
 
-    ping google.com | barcat -f'time=\K' -t
+    ( git log --pretty=%ci --since=30day | cut -b-10
+      seq 0 30 | xargs -i date +%F -d-{}day ) |
+    sort | uniq -c | awk '$1--' | graph --spark
 
 =head1 AUTHOR
 
 
 =head1 AUTHOR