5fd0a16841389f3c4ad38899e4253ef30f91c299
[barcat.git] / barcat
1 #!/usr/bin/perl -CA
2 use 5.014;
3 use warnings;
4 use utf8;
5 use List::Util qw( min max sum );
6 use open qw( :std :utf8 );
7 use re '/msx';
8
9 our $VERSION = '1.09';
10
11 my %opt;
12 if (@ARGV) {
13 require Getopt::Long;
14 Getopt::Long->import('2.33', qw( :config gnu_getopt ));
15 GetOptions(\%opt,
16         'ascii|a!',
17         'color|C!',
18         'M' => sub { $opt{color} = 0 },
19         'field|f=s' => sub {
20                 eval {
21                         local $_ = $_[1];
22                         s/\A[0-9]+\z/(?:\\S*\\h+){$_}\\K/;
23                         s{\A[+]([0-9]*)\z}{
24                                 (!!$1 && '(?:\d+\D+\b){'.$1.'}\K') . '\s* (?=\d)'
25                         }e;
26                         $opt{anchor} = qr/$_/;
27                 } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r;
28         },
29         'count|c!',
30         'human-readable|H!',
31         'sexagesimal!',
32         'reformat!',
33         'interval|t:i',
34         'trim|length|l=s' => sub {
35                 my ($optname, $optval) = @_;
36                 $optval =~ s/%$// and $opt{trimpct}++;
37                 $optval =~ m/\A-?[0-9]+\z/ or die(
38                         "Value \"$optval\" invalid for option $optname",
39                         " (number or percentage expected)\n"
40                 );
41                 $opt{trim} = $optval;
42         },
43         'value-length=i',
44         'minval=f',
45         'maxval=f',
46         'limit|L:s' => sub {
47                 my ($optname, $optval) = @_;
48                 $optval ||= 0;
49                 $optval =~ /\A-[0-9]+\z/ and $optval .= '-';  # tail shorthand
50                 $optval =~ s/[+]/--/;
51                 my ($start, $end) =
52                 $optval =~ m/\A (?: (-? [0-9]+)? - )? (-? [0-9]+)? \z/ or die(
53                         "Value \"$optval\" invalid for option limit",
54                         " (range expected)\n"
55                 );
56                 $start ||= 1;
57                 $start--;
58                 s/\A-0*\z// and $_ ||= undef for $end // ();
59
60                 $opt{hidemin} = sub {
61                         my ($lines) = @_;
62                         if ($start < 0) {
63                                 return max(0, $lines + $start + 2);
64                         }
65                         return $start;
66                 } if $start;
67                 $opt{hidemax} = sub {
68                         my ($limit, $offset) = @_;
69                         if ($end < 0) {
70                                 return $offset - $end - 1; # count
71                         }
72                         elsif ($start < 0) {
73                                 return $limit - $end + 1; # bottom
74                         }
75                         elsif ($end <= $limit) {
76                                 return $end - 1; # less
77                         }
78                         return $limit;
79                 } if defined $end;
80         },
81         'log|e!',
82         'header!',
83         'markers|m=s',
84         'graph-format=s' => sub {
85                 $opt{'graph-format'} = substr $_[1], 0, 1;
86         },
87         'spark|_!',
88         'indicators:s',
89         'palette=s' => sub {
90                 $opt{palette} = {
91                         ''     => [],
92                         fire   => [qw( 90 31 91 33 93 97 96 )],
93                         fire256=> [map {"38;5;$_"} qw(
94                                 235  52 88 124 160 196
95                                 202 208 214 220 226  227 228 229 230 231  159
96                         )],
97                         whites => [qw( 1;30 0;37 1;37 )],
98                         grays  => [map {"38;5;$_"} 0, 232..255, 15],
99                         random => [map {"38;5;$_"} List::Util::shuffle(17..231)],
100                         rainbow=> [map {"38;5;$_"}
101                                 196, # r
102                                 (map { 196 + $_*6   } 0..4), # +g
103                                 (map { 226 - $_*6*6 } 0..4), # -r
104                                 (map {  46 + $_     } 0..4), # +b
105                                 (map {  51 - $_*6   } 0..4), # -g
106                                 (map {  21 + $_*6*6 } 0..4), # +r
107                                 (map { 201 - $_     } 0..4), # -b
108                                 196,
109                         ],
110                 }->{$_[1]} // do {
111                         my @vals = split /[^0-9;]/, $_[1]
112                                 or die "Empty palette resulting from \"$_[1]\"\n";
113                         \@vals;
114                 };
115         },
116         'stat|s!',
117         'report=s',
118         'signal-stat=s',
119         'unmodified|u!',
120         'width|w=i',
121         'version|V' => sub {
122                 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
123                 say "barcat $mascot version $VERSION";
124                 exit;
125         },
126         'usage|h' => sub {
127                 /^=/ ? last : print for readline *DATA;  # text between __END__ and pod
128                 exit;
129         },
130         'help|?'  => sub {
131                 require Pod::Usage;
132                 Pod::Usage::pod2usage(
133                         -exitval => 0, -perldocopt => '-oman', -verbose => 2,
134                 );
135         },
136 ) or exit 64;  # EX_USAGE
137 }
138
139 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
140 $opt{color} //= $ENV{NO_COLOR} ? 0 : -t *STDOUT;  # enable on tty
141 $opt{'graph-format'} //= '-';
142 $opt{trim}   *= $opt{width} / 100 if $opt{trimpct};
143 $opt{units}   = [split //, ' kMGTPEZYRQqryzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
144         if $opt{'human-readable'};
145 $opt{'value-length'} = 4 if $opt{units};
146 $opt{'value-length'} = 1 if $opt{unmodified};
147 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
148 $opt{markers} //= '=avg <31.73v >68.27v +50v |0';
149 $opt{report} //= join('',
150         '${partsum+; $_ .= " of "}',
151         '${sum+; color(1); $_ .= " total in "}',
152         '${count#} values',
153         '${lines#; $_ = $_ != @order && " over $_ lines"}',
154         sprintf('${count: (%s)}', join ', ',
155                 '${0v; color(31)} min',
156                 '${avg; $opt{reformat} or $_ = sprintf "%0.2f", $_; color(36)} avg',
157                 '${100v; color(32)} max',
158         ),
159 );
160 $opt{palette} //= $opt{color} && [31, 90, 32];
161 $opt{indicators} = [split //, $opt{indicators} ||
162         ($opt{ascii} ? ' .oO' : $opt{spark} ? ' ▁▂▃▄▅▆▇█' : ' ▏▎▍▌▋▊▉█')
163 ] if defined $opt{indicators} or $opt{spark};
164 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
165         and undef $opt{interval};
166
167 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
168 $opt{'value-format'} = $opt{sexagesimal} ? sub {
169         my $s = abs($_[0]) + .5;
170         sprintf('%s%d:%02d:%02d', $_[0] < 0 && '-', $s/3600, $s/60%60, $s%60);
171 } : $opt{units} && sub {
172         my $unit = (
173                 log(abs $_[0] || 1) / log(10)
174                 - 3 * (abs($_[0]) < .9995)   # shift to smaller unit if below 1
175                 + 1e-15  # float imprecision
176         );
177         my $decimal = ($unit % 3) == ($unit < 0);
178         $unit -= log($decimal ? .995 : .9995) / log(10);  # rounded
179         $decimal = ($unit % 3) == ($unit < 0);
180         $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/;  # integer 0..999
181         sprintf('%*.*f%1s',
182                 3 + ($_[0] < 0), # digits plus optional negative sign
183                 $decimal,  # tenths
184                 $_[0] / 1000 ** int($unit/3),  # number
185                 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
186                         $opt{units}->[$unit/3]  # suffix
187         );
188 } and $opt{reformat}++;
189 $opt{'value-format'} ||= sub { sprintf '%.8g', $_[0] };
190
191
192 my (@lines, @values, @order, %uniq);
193
194 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
195 $SIG{ALRM} = sub {
196         show_lines();
197         alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
198 };
199 $SIG{INT} = \&show_exit;
200
201 if (defined $opt{interval}) {
202         $opt{interval} ||= 1;
203         alarm $opt{interval} if $opt{interval} > 0;
204
205         eval {
206                 require Tie::Array::Sorted;
207                 tie @order, 'Tie::Array::Sorted', sub { $_[0] <=> $_[1] };
208         } or warn $@, "Expect slowdown with large datasets!\n"
209                 unless $opt{count};
210 }
211
212 my $float = qr<[0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )?>; # positive numberish
213 my $valmatch = $opt{anchor} // qr/\A/;
214 $valmatch .= !$opt{count} ? qr/( \h* -? $float |)/ :
215         $opt{anchor} ? qr/(\S*)/ : qr/(.*)/;
216
217 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
218         s/\r?\n\z//;
219         my $valnum;
220         if ($opt{count}) {
221                 $valnum = m/$valmatch/ && $1;
222                 $uniq{$valnum}++ and next;
223                 push @values, $valnum;
224                 s/\A/\n /;
225         }
226         else {
227                 s/\A\h*// unless $opt{unmodified};
228                 $valnum = s/$valmatch/\n/ && $1;
229                 push @values, $valnum;
230                 push @order, $valnum if length $valnum;
231         }
232
233         if (defined $opt{trim} and defined $valnum) {
234                 my $trimpos = abs $opt{trim};
235                 $trimpos -= length $valnum if $opt{unmodified};
236                 if ($trimpos <= 1) {
237                         $_ = substr $_, 0, 2;
238                 }
239                 elsif (length > $trimpos) {
240                         # cut and replace (intentional lvalue for speed, contrary to PBP)
241                         substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
242                 }
243         }
244         push @lines, $_;
245 }
246 continue {
247         show_lines() if defined $opt{interval} and $opt{interval} < 0
248                 and $. % $opt{interval} == 0;
249 }
250
251 $SIG{INT} = 'DEFAULT';
252
253 sub color {
254         $opt{color} and defined $_[0] or return '';
255         return "\e[$_[0]m" if defined wantarray;
256         $_ = color(@_) . $_ . color(0) if defined;
257 }
258
259 sub show_lines {
260
261 state $nr = $opt{hidemin} ? $opt{hidemin}->($#lines) : 0;
262 @lines > $nr or return;
263
264 my $limit = $opt{hidemax} ? $opt{hidemax}->($#lines, $nr) : $#lines;
265
266 if ($opt{count}) {
267         $_ = $uniq{$_} for @values[$nr .. $limit];
268         @order = @values;
269 }
270
271 @order = sort { $a <=> $b } @order unless tied @order;
272 my $maxval = $opt{maxval} // (
273         $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
274         $order[-1]
275 ) // 0;
276 my $minval = $opt{minval} // min $order[0] // (), 0;
277 my $range = $maxval - $minval;
278 $range &&= log $range if $opt{log};
279 my $lenval = $opt{'value-length'} // max map { length } @order;
280 my $len    = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
281         max(map { length $values[$_] && length $lines[$_] } $nr .. $limit)
282         // 0;  # left padding
283 my $size   = defined $opt{width} && $range &&
284         ($opt{width} - $lenval - $len - !!$opt{indicators});  # bar multiplication
285
286 my @barmark;
287 if ($opt{markers} and $size > 0) {
288         for my $markspec (split /\h/, $opt{markers}) {
289                 my ($char, $func) = split //, $markspec, 2;
290                 my $increment = $func =~ s/[+]\z//;
291                 my @pos = eval {
292                         if ($func =~ /\A\/($float)\z/) {
293                                 my @range = my $multiple = my $next = $1;
294                                 while ($next < $maxval) {
295                                         $multiple *= 10 if $opt{log};
296                                         push @range, $next += $multiple;
297                                 }
298                                 return @range;
299                         }
300                         return calc($func);
301                 } or do {
302                         warn "Invalid marker $char: $@" if $@;
303                         next;
304                 };
305                 for my $pos (@pos) {
306                         $pos -= $minval;
307                         $pos &&= log $pos if $opt{log};
308                         $pos >= 0 or next;
309                         $increment ||= $minval && !$pos;
310                         color(36) for $barmark[$pos / $range * $size + $increment + .5] = $char;
311                 }
312         }
313
314         state $lastmax = $maxval;
315         if ($maxval > $lastmax) {
316                 print ' ' x ($lenval + $len);
317                 print color(90);
318                 printf '%-*s',
319                         ($lastmax - $minval) * $size / $range + .5,
320                         '-' x (($values[$nr - 1] - $minval) * $size / $range);
321                 print color(92);
322                 say '+' x (($range - $lastmax) * $size / $range + .5);
323                 print color(0);
324                 $lastmax = $maxval;
325         }
326 }
327
328 say(
329         color(31), sprintf('%*s', $lenval, $minval),
330         color(90), '-', color(36), '+',
331         color(32), sprintf('%*s', $size - 3, $maxval),
332         color(90), '-', color(36), '+',
333         color(0),
334 ) if $opt{header};
335
336 while ($nr <= $limit) {
337         my $val = $values[$nr];
338         my $rel;
339         if (length $val) {
340                 $rel = $val - $minval;
341                 $rel &&= log $rel if $opt{log};
342                 $rel = min(1, $rel / $range) if $range; # 0..1
343         }
344         my $color = !length $val || !$opt{palette} ? undef :
345                 $val == $order[-1] ? $opt{palette}->[-1] : # max
346                 $val == $order[0] ? $opt{palette}->[0] : # min
347                 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
348         my $indicator = $opt{indicators} && $opt{indicators}->[
349                 !length($val) || !$#{$opt{indicators}} ? 0 : # blank
350                 $#{$opt{indicators}} < 2 ? 1 :
351                 $val >= $order[-1] ? -1 :
352                 $rel * ($#{$opt{indicators}} - 1e-14) + 1
353         ];
354
355         if ($opt{spark}) {
356                 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
357                 print color($color), $_ for $indicator;
358                 next;
359         }
360         print $indicator if defined $indicator;
361
362         if (length $val) {
363                 $val = sprintf("%*s", $lenval,
364                         $opt{reformat} ? $opt{'value-format'}->($val) : $val
365                 );
366                 color($color) for $val;
367         }
368         my $line = $lines[$nr] =~ s/\n/$val/r;
369         if (not length $val) {
370                 say $line;
371                 next;
372         }
373         printf '%-*s', $len + length($val), $line;
374         if ($rel and $size) {
375                 print $barmark[$_] // $opt{'graph-format'}
376                         for 1 .. $rel * $size + .5;
377         }
378         say '';
379 }
380 continue {
381         $nr++;
382 }
383 say $opt{palette} ? color(0) : '' if $opt{spark};
384 %uniq = () if $opt{interval} and $opt{count};
385
386         return $nr;
387 }
388
389 sub show_stat {
390         my %vars = (
391                 partsum => undef,
392                 count => int @order,
393                 lines => int @lines,
394         );
395         my $linemin = !$opt{hidemin} ? 0 :
396                 ($vars{start} = $opt{hidemin}->($#lines));
397         my $linemax = !$opt{hidemax} ? $#lines :
398                 ($vars{end} = $opt{hidemax}->($#lines, $vars{start}));
399         if (@order) {
400                 $vars{partsum} = sum(0, grep {length} @values[$linemin .. $linemax])
401                         if $linemin <= $linemax and ($opt{hidemin} or $opt{hidemax});
402         }
403         say varfmt($opt{report}, \%vars);
404         return 1;
405 }
406
407 sub calc {
408         my ($func) = @_;
409         if ($func eq 'avg') {
410                 return calc('sum') / @order;
411         }
412         elsif ($func eq 'sum') {
413                 state $cache;         # avoid recount
414                 state $cachednr = 0;  # if unchanged
415                 unless (@order == $cachednr) {
416                         $cache = sum(@order);
417                         $cachednr = @order;
418                 }
419                 return $cache;
420         }
421         elsif ($func =~ /\A([0-9.]+)v\z/) {
422                 $1 <= 100 or die(
423                         "percentile $1 out of bounds\n"
424                 );
425                 my $index = $#order * $1 / 100;
426                 my $f = $index - int $index;
427                 my $val = $order[$index];
428                 if ($f) {
429                         my $next = $order[$index + 1];
430                         $val -= $f * ($val - $next);
431                 }
432                 return $val;
433         }
434         elsif ($func =~ /\A-?[0-9.]+\z/) {
435                 return $func;
436         }
437         else {
438                 die "$func unknown\n";
439         }
440 }
441
442 sub varfmt {
443         my ($fmt, $vars) = @_;
444         $fmt =~ s[\$\{ \h*+ ((?: [^{}]++ | \{(?1)\} )+) \}]{
445                 my ($name, $op, $cmd) = split /\s*([;:])/, $1, 2;
446                 my $format = $name =~ s/\+// || $name !~ s/\#// && $opt{reformat};
447                 local $_ = exists $vars->{$name} ? $vars->{$name} : calc($name);
448                 defined && do {
449                         $_ = $opt{'value-format'}->($_) if $format;
450                         if ($cmd and $op eq ':') {
451                                 $_ = !!$_ && varfmt($cmd, $vars);
452                         }
453                         elsif ($cmd) {
454                                 eval $cmd;
455                                 warn "Error in \$$name report: $@" if $@;
456                         }
457                         $_;
458                 }
459         }eg;
460         return $fmt;
461 }
462
463 sub show_exit {
464         show_lines();
465         show_stat() if $opt{stat};
466         exit 130 if @_;  # 0x80+signo
467         exit;
468 }
469
470 show_exit();
471
472 __END__
473 Usage:                                               /\_/\
474   barcat [OPTIONS] [FILES|NUMBERS]                  (=•.•=)
475                                                     (u   u)
476 Options:
477   -a, --[no-]ascii         Restrict user interface to ASCII characters
478   -C, --[no-]color         Force colored output of values and bar markers
479   -c, --count              Omit repetitions and count the number of
480                            occurrences
481   -f, --field=([+]N|REGEXP)
482                            Compare values after a given number of whitespace
483                            separators
484       --header             Prepend a chart axis with minimum and maximum
485                            values labeled
486   -H, --human-readable     Format values using SI unit prefixes
487       --sexagesimal        Convert seconds to HH:MM:SS time format
488   -t, --interval[=(N|-LINES)]
489                            Output partial progress every given number of
490                            seconds or input lines
491   -l, --length=[-]SIZE[%]  Trim line contents (between number and bars)
492   -L, --limit=[N|[-]START(-[END]|+N)]
493                            Select a range of lines to display
494   -e, --log                Logarithmic (exponential) scale instead of linear
495       --graph-format=CHAR  Glyph to repeat for the graph line
496   -m, --markers=FORMAT     Statistical positions to indicate on bars
497       --min=N, --max=N     Bars extend from 0 or the minimum value if lower
498       --palette=(PRESET|COLORS)
499                            Override colors of parsed numbers
500   -_, --spark              Replace lines by sparklines
501       --indicators[=CHARS] Prefix a unicode character corresponding to each
502                            value
503   -s, --stat               Total statistics after all data
504   -u, --unmodified         Do not reformat values, keeping leading whitespace
505       --value-length=SIZE  Reserved space for numbers
506   -w, --width=COLUMNS      Override the maximum number of columns to use
507   -h, --usage              Overview of available options
508       --help               Full pod documentation
509   -V, --version            Version information
510
511 =encoding utf8
512
513 =head1 NAME
514
515 barcat - concatenate texts with graph to visualize values
516
517 =head1 SYNOPSIS
518
519 B<barcat> [I<options>] [I<file>... | I<numbers>]
520
521 =head1 DESCRIPTION
522
523 Visualizes relative sizes of values read from input
524 (parameters, file(s) or STDIN).
525 Contents are concatenated similar to I<cat>,
526 but numbers are reformatted and a bar graph is appended to each line.
527
528 It can has various options for input and output (re)formatting,
529 but remains limited to one-dimensional charts.
530 For more complex graphing needs
531 you'll need a larger animal like I<gnuplot>.
532
533 =head1 OPTIONS
534
535 =over
536
537 =item B<-a>, B<-->[B<no->]B<ascii>
538
539 Restrict user interface to ASCII characters,
540 replacing default UTF-8 by their closest approximation.
541 Input is always interpreted as UTF-8 and shown as is.
542
543 =item B<-C>, B<-->[B<no->]B<color>
544
545 Force colored output of values and bar markers.
546 Defaults on if output is a tty,
547 disabled otherwise such as when piped or redirected.
548 Can also be disabled by setting B<-M>
549 or the I<NO_COLOR> environment variable.
550
551 =item B<-c>, B<--count>
552
553 Omit repetitions and count the number of occurrences.
554 Similar to piping input through C<sort | uniq -c>
555 but keeping the order of first appearances.
556
557 Lines are omitted if they (or a specified field) are identical,
558 and the amount of matches is prepended and used as values
559 for bars and subsequent statistics.
560
561 =item B<-f>, B<--field>=([B<+>]I<number> | I<regexp>)
562
563 Compare values after a given number of whitespace separators,
564 or matching a regular expression.
565
566 Unspecified or B<-f0> means values are at the start of each line.
567 With B<-f1> the second word is taken instead.
568 A string can indicate the starting position of a value
569 (such as B<-f:> if preceded by colons),
570 or capture the numbers itself,
571 for example B<-f'(\d+)'> for the first digits anywhere.
572 A shorthand for this is C<+0>, or C<+N> to find the Nth number.
573
574 =item B<--header>
575
576 Prepend a chart axis with minimum and maximum values labeled.
577
578 =item B<-H>, B<--human-readable>
579
580 Format values using SI unit prefixes,
581 turning long numbers like C<12356789> into C<12.4M>.
582 Also changes an exponent C<1.602176634e-19> to C<160.2z>.
583 Short integers are aligned but kept without decimal point.
584
585 =item B<--sexagesimal>
586
587 Convert seconds to HH:MM:SS time format.
588
589 =item B<-t>, B<--interval>[=(I<seconds> | B<->I<lines>)]
590
591 Output partial progress every given number of seconds or input lines.
592 An update can also be forced by sending a I<SIGALRM> alarm signal.
593
594 =item B<-l>, B<--length>=[B<->]I<size>[B<%>]
595
596 Trim line contents (between number and bars)
597 to a maximum number of characters.
598 The exceeding part is replaced by an abbreviation sign,
599 unless B<--length=0>.
600
601 Prepend a dash (i.e. make negative) to enforce padding
602 regardless of encountered contents.
603
604 =item B<-L>, B<--limit>=[I<count> | [B<->]I<start>(B<->[I<end>] | B<+>I<count>)]
605
606 Select a range of lines to display.
607 A single integer indicates the last line number (like I<head>),
608 or first line counting from the bottom if negative (like I<tail>).
609
610 A range consists of a starting line number followed by either
611 a dash C<-> to an optional end, or plus sign C<+> with count.
612
613 All hidden input is still counted and analyzed for statistics,
614 but disregarded for padding and bar size.
615
616 =item B<-e>, B<--log>
617
618 Logarithmic (B<e>xponential) scale instead of linear
619 to compare orders of magnitude.
620
621 =item B<--graph-format>=I<character>
622
623 Glyph to repeat for the graph line.
624 Defaults to a dash C<->.
625
626 =item B<-m>, B<--markers>=I<format>
627
628 Statistical positions to indicate on bars.
629 A single indicator glyph precedes each position:
630
631 =over 2
632
633 =item I<number>
634
635 Exact value to match on the axis.
636 A vertical bar at the zero crossing is displayed by C<|0>
637 for negative values.
638 For example C<π3.14> would locate pi.
639
640 =item B</>I<interval>
641
642 Repeated at every multiple of a number.
643 For example C<:/1> for a grid at every integer.
644
645 =item I<percentage>B<v>
646
647 Ranked value at the given percentile,
648 or score at or below which a percentage falls
649 in its frequency distribution (inclusive).
650
651 The default shows C<+> at C<50v> for the mean or median:
652 the middle value or interpolation between two values.
653 One standard deviation above the median is at about C<68v>.
654 The default includes C<< <31.73v >68.27v >>
655 to encompass all I<normal> results, or 68% of all entries, by I<< <--> >>.
656
657 =item B<avg>
658
659 Matches the average (arithmetic mean);
660 the sum of all values divided by the number of counted lines.
661 Indicated by default as C<=>.
662
663 =back
664
665 =item B<--min>=I<number>, B<--max>=I<number>
666
667 Bars extend from 0 or the minimum value if lower,
668 to the largest value encountered.
669 These options can be set to customize this range.
670
671 =item B<--palette>=(I<preset> | I<color>...)
672
673 Override colors of parsed numbers.
674 Can be any CSI escape, such as C<90> for default dark gray,
675 or alternatively C<1;30> for bright black.
676
677 In case of additional colors,
678 the last is used for values equal to the maximum, the first for minima.
679 If unspecified, these are green and red respectively (C<31 90 32>).
680 Multiple intermediate colors will be distributed
681 relative to the size of values.
682
683 A non-numeric name can refer to a predefined color scheme:
684
685 =over 8
686
687 =item B<whites>
688
689 Minimal set of monochrome brightnesses.
690
691 =item B<grays>
692
693 Utilize the 24 grayscale ramp in 256-color terminals.
694
695 =item B<fire>
696
697 Gradient red to white in 7 out of 16 colors.
698
699 =item B<fire256>
700
701 Extended to 17 colors out of 256.
702
703 =item B<rainbow>
704
705 Saturated red to green to blue to red.
706
707 =item B<random>
708
709 All 215 extended colors in unrelated orders.
710
711 =back
712
713 =item B<-_>, B<--spark>
714
715 Replace lines by I<sparklines>,
716 single characters (configured by B<--indicators>)
717 corresponding to input values.
718
719 =item B<--indicators>[=I<characters>]
720
721 Prefix a unicode character corresponding to each value.
722 The first specified character will be used for non-values,
723 the remaining sequence will be distributed over the range of values.
724 Unspecified, block fill glyphs U+2581-2588 will be used.
725
726 =item B<-s>, B<--stat>
727
728 Total statistics after all data.
729
730 While processing (possibly a neverending pipe),
731 intermediate results are also shown on signal I<SIGINFO> if available (control+t on BSDs)
732 or I<SIGQUIT> otherwise (ctrl+\ on linux).
733
734 =item B<-u>, B<--unmodified>
735
736 Do not reformat values, keeping leading whitespace.
737 Keep original value alignment, which may be significant in some programs.
738
739 =item B<--value-length>=I<size>
740
741 Reserved space for numbers.
742
743 =item B<-w>, B<--width>=I<columns>
744
745 Override the maximum number of columns to use.
746 Appended graphics will extend to fill up the entire screen,
747 otherwise determined by the environment variable I<COLUMNS>
748 or by running the I<tput> command.
749
750 =item B<-h>, B<--usage>
751
752 Overview of available options.
753
754 =item B<--help>
755
756 Full pod documentation
757 as rendered by perldoc.
758
759 =item B<-V>, B<--version>
760
761 Version information.
762
763 =back
764
765 =head1 EXAMPLES
766
767 Draw a sine wave:
768
769     seq 30 | awk '{print sin($1/10)}' | barcat
770
771 Compare file sizes (with human-readable numbers):
772
773     du -d0 -b * | barcat -H
774
775 Same from formatted results, selecting the first numeric value:
776
777     tree -s --noreport | barcat -H -f+
778
779 Compare media metadata, like image size or play time:
780
781     exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat
782
783     exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat --sexagesimal
784
785     find -type f -print0 | xargs -0 -L1 \
786     ffprobe -show_format -of json -v error |
787     jq -r '.format|.duration+" "+.bit_rate+" "+.filename' | barcat --sex
788
789 Memory usage of user processes with long names truncated:
790
791     ps xo rss,pid,cmd | barcat -l40
792
793 Monitor network latency from prefixed results:
794
795     ping google.com | barcat -f'time=\K' -t
796
797 Commonly used after counting, eg letter frequencies in text files:
798
799     cat /usr/share/games/fortunes/*.u8 |
800     perl -CS -nE 'say for grep length, split /\PL*/, uc' |
801     sort | uniq -c | barcat
802
803 Users on the current server while preserving order:
804
805     users | tr ' ' '\n' | barcat -c
806
807 Number of HTTP requests per day:
808
809     barcat -cf'\[([^:]+)' httpd/access.log
810
811 Any kind of database query results, preserving returned alignment:
812
813     echo 'SELECT sin(value * .1) FROM generate_series(0, 30) value' |
814     psql -t | barcat -u
815
816 In PostgreSQL from within the client; a fancy C<\dt+> perhaps:
817
818     > SELECT schemaname, relname, pg_total_relation_size(relid)
819       FROM pg_statio_user_tables ORDER BY idx_blks_hit
820       \g |barcat -uHf+
821
822 Same thing in SQLite (requires the sqlite3 client):
823
824     > .once |barcat -Hf+
825     > SELECT name, sum(pgsize) FROM dbstat GROUP BY 1;
826
827 Earthquakes worldwide magnitude 1+ in the last 24 hours:
828
829     curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
830     column -ts, -n | barcat -f4 -u -l80%
831
832 External datasets, like movies per year:
833
834     curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
835     jq .[].year | uniq -c | barcat
836
837 Pokémon height comparison:
838
839     curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
840     jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
841
842 USD/EUR exchange rate from CSV provided by the ECB:
843
844     curl https://sdw.ecb.europa.eu/export.do \
845          -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
846     barcat -f',\K' --value-length=7
847
848 Total population history in XML from the World Bank:
849
850     curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
851     xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
852     barcat -f1 -H --markers=+/1e9
853
854 Population and other information for all countries:
855
856     curl http://download.geonames.org/export/dump/countryInfo.txt |
857     grep -v '^#\s' | column -ts$'\t' -n | barcat -f+2 -e -u -l150 -s
858
859 And of course various Git statistics, such commit count by year:
860
861     git log --pretty=%ci | cut -b-4 | uniq -c | barcat
862
863 Or the top 3 most frequent authors with statistics over all:
864
865     git shortlog -sn | barcat -L3 -s
866
867 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
868
869     ( git log --pretty=%ci --since=30day | cut -b-10
870       seq 0 30 | xargs -i date +%F -d-{}day ) |
871     sort | uniq -c | awk '$1--' | barcat --spark
872
873 Sparkline graphics of simple input given as inline parameters:
874
875     barcat -_ 3 1 4 1 5 0 9 2 4
876
877 Misusing the spark functionality to draw a lolcat line:
878
879     seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
880
881 =head1 AUTHOR
882
883 Mischa POSLAWSKY <perl@shiar.org>
884
885 =head1 LICENSE
886
887 GPL3+.