NO_COLOR environment as default --color
[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.08';
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         'C' => sub { $opt{color} = 0 },
19         'field|f=s' => sub {
20                 eval {
21                         local $_ = $_[1];
22                         $opt{anchor} = /\A[0-9]+\z/ ? qr/(?:\S*\h+){$_}\K/ : qr/$_/;
23                 } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r;
24         },
25         'human-readable|H!',
26         'interval|t:i',
27         'trim|length|l=s' => sub {
28                 my ($optname, $optval) = @_;
29                 $optval =~ s/%$// and $opt{trimpct}++;
30                 $optval =~ m/\A-?[0-9]+\z/ or die(
31                         "Value \"$optval\" invalid for option $optname",
32                         " (number or percentage expected)\n"
33                 );
34                 $opt{trim} = $optval;
35         },
36         'value-length=i',
37         'hidemin=i',
38         'hidemax=i',
39         'minval=f',
40         'maxval=f',
41         'limit|L:s' => sub {
42                 my ($optname, $optval) = @_;
43                 $optval ||= 0;
44                 $optval =~ /\A-[0-9]+\z/ and $optval .= '-';  # tail shorthand
45                 ($opt{hidemin}, $opt{hidemax}) =
46                 $optval =~ m/\A (?: (-? [0-9]+)? - )? ([0-9]+)? \z/ or die(
47                         "Value \"$optval\" invalid for option limit",
48                         " (range expected)\n"
49                 );
50         },
51         'header!',
52         'markers|m=s',
53         'graph-format=s' => sub {
54                 $opt{'graph-format'} = substr $_[1], 0, 1;
55         },
56         'spark|_!',
57         'indicators:s',
58         'palette=s' => sub {
59                 $opt{palette} = {
60                         ''     => [],
61                         fire   => [qw( 90 31 91 33 93 97 96 )],
62                         fire256=> [map {"38;5;$_"} qw(
63                                 235  52 88 124 160 196
64                                 202 208 214 220 226  227 228 229 230 231  159
65                         )],
66                         whites => [qw( 1;30 0;37 1;37 )],
67                         greys  => [map {"38;5;$_"} 0, 232..255, 15],
68                         random => [map {"38;5;$_"} List::Util::shuffle(17..231)],
69                         rainbow=> [map {"38;5;$_"}
70                                 196, # r
71                                 (map { 196 + $_*6   } 0..4), # +g
72                                 (map { 226 - $_*6*6 } 0..4), # -r
73                                 (map {  46 + $_     } 0..4), # +b
74                                 (map {  51 - $_*6   } 0..4), # -g
75                                 (map {  21 + $_*6*6 } 0..4), # +r
76                                 (map { 201 - $_     } 0..4), # -b
77                                 196,
78                         ],
79                 }->{$_[1]} // do {
80                         my @vals = split /[^0-9;]/, $_[1]
81                                 or die "Empty palette resulting from \"$_[1]\"\n";
82                         \@vals;
83                 };
84         },
85         'stat|s!',
86         'signal-stat=s',
87         'unmodified|u!',
88         'width|w=i',
89         'version|V' => sub {
90                 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
91                 say "barcat $mascot version $VERSION";
92                 exit;
93         },
94         'usage|h' => sub {
95                 /^=/ ? last : print for readline *DATA;  # text between __END__ and pod
96                 exit;
97         },
98         'help|?'  => sub {
99                 require Pod::Usage;
100                 Pod::Usage::pod2usage(
101                         -exitval => 0, -perldocopt => '-oman', -verbose => 2,
102                 );
103         },
104 ) or exit 64;  # EX_USAGE
105 }
106
107 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
108 $opt{color} //= $ENV{NO_COLOR} ? 0 : -t *STDOUT;  # enable on tty
109 $opt{'graph-format'} //= '-';
110 $opt{trim}   *= $opt{width} / 100 if $opt{trimpct};
111 $opt{units}   = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
112         if $opt{'human-readable'};
113 $opt{anchor} //= qr/\A/;
114 $opt{'value-length'} = 6 if $opt{units};
115 $opt{'value-length'} = 1 if $opt{unmodified};
116 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
117 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
118 $opt{palette} //= $opt{color} && [31, 90, 32];
119 $opt{indicators} = [split //, $opt{indicators} ||
120         ($opt{ascii} ? ' .oO' : $opt{spark} ? ' ▁▂▃▄▅▆▇█' : ' ▏▎▍▌▋▊▉█')
121 ] if defined $opt{indicators} or $opt{spark};
122 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
123 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
124         and undef $opt{interval};
125
126 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
127 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
128 $opt{'value-format'} = $opt{units} && sub {
129         my $unit = (
130                 log(abs $_[0] || 1) / log(10)
131                 - 3 * (abs($_[0]) < .9995)   # shift to smaller unit if below 1
132                 + 1e-15  # float imprecision
133         );
134         my $decimal = ($unit % 3) == ($unit < 0);
135         $unit -= log($decimal ? .995 : .9995) / log(10);  # rounded
136         $decimal = ($unit % 3) == ($unit < 0);
137         $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/;  # integer 0..999
138         sprintf('%*.*f%1s',
139                 3 + ($_[0] < 0), # digits plus optional negative sign
140                 $decimal,  # tenths
141                 $_[0] / 1000 ** int($unit/3),  # number
142                 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
143                         $opt{units}->[$unit/3]  # suffix
144         );
145 };
146
147
148 my (@lines, @values, @order);
149
150 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
151 $SIG{ALRM} = sub {
152         show_lines();
153         alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
154 };
155 $SIG{INT} = \&show_exit;
156
157 if (defined $opt{interval}) {
158         $opt{interval} ||= 1;
159         alarm $opt{interval} if $opt{interval} > 0;
160
161         eval {
162                 require Tie::Array::Sorted;
163                 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
164         } or warn $@, "Expect slowdown with large datasets!\n";
165 }
166
167 my $valmatch = qr<
168         $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
169 >x;
170 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
171         s/\r?\n\z//;
172         s/\A\h*// unless $opt{unmodified};
173         my $valnum = s/$valmatch/\n/ && $1;
174         push @values, $valnum;
175         push @order, $valnum if length $valnum;
176         if (defined $opt{trim} and defined $valnum) {
177                 my $trimpos = abs $opt{trim};
178                 $trimpos -= length $valnum if $opt{unmodified};
179                 if ($trimpos <= 1) {
180                         $_ = substr $_, 0, 2;
181                 }
182                 elsif (length > $trimpos) {
183                         # cut and replace (intentional lvalue for speed, contrary to PBP)
184                         substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
185                 }
186         }
187         push @lines, $_;
188         show_lines() if defined $opt{interval} and $opt{interval} < 0
189                 and $. % $opt{interval} == 0;
190 }
191
192 if ($opt{'zero-missing'}) {
193         push @values, (0) x 10;
194 }
195
196 $SIG{INT} = 'DEFAULT';
197
198 sub color {
199         $opt{color} and defined $_[0] or return '';
200         return "\e[$_[0]m" if defined wantarray;
201         $_ = color(@_) . $_ . color(0) if defined;
202 }
203
204 sub show_lines {
205
206 state $nr =
207         $opt{hidemin} < 0 ? max(0, @lines + $opt{hidemin} + 1) :
208         $opt{hidemin};
209 @lines > $nr or return;
210
211 my $limit = $#lines;
212 if (defined $opt{hidemax}) {
213         if ($opt{hidemin} and $opt{hidemin} < 0) {
214                 $limit -= $opt{hidemax} - 1;
215         }
216         elsif ($opt{hidemax} <= $limit) {
217                 $limit = $opt{hidemax} - 1;
218         }
219 }
220
221 @order = sort { $b <=> $a } @order unless tied @order;
222 my $maxval = $opt{maxval} // (
223         $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
224         $order[0]
225 ) // 0;
226 my $minval = $opt{minval} // min $order[-1] // (), 0;
227 my $range = $maxval - $minval;
228 my $lenval = $opt{'value-length'} // max map { length } @order;
229 my $len    = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
230         max map { length $values[$_] && length $lines[$_] }
231                 0 .. min $#lines, $opt{hidemax} || ();  # left padding
232 my $size   = defined $opt{width} && $range &&
233         ($opt{width} - $lenval - $len - !!$opt{indicators}) / $range;  # bar multiplication
234
235 my @barmark;
236 if ($opt{markers} and $size > 0) {
237         for my $markspec (split /\h/, $opt{markers}) {
238                 my ($char, $func) = split //, $markspec, 2;
239                 my $pos = eval {
240                         if ($func eq 'avg') {
241                                 return sum(@order) / @order;
242                         }
243                         elsif ($func =~ /\A([0-9.]+)v\z/) {
244                                 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
245                                 my $index = $#order * $1 / 100;
246                                 return ($order[$index] + $order[$index + .5]) / 2;
247                         }
248                         elsif ($func =~ /\A-?[0-9.]+\z/) {
249                                 return $func;
250                         }
251                         else {
252                                 die "Unknown marker $char: $func\n";
253                         }
254                 };
255                 defined $pos or do {
256                         warn $@ if $@;
257                         next;
258                 };
259                 $pos -= $minval;
260                 $pos >= 0 or next;
261                 color(36) for $barmark[$pos * $size] = $char;
262         }
263
264         state $lastmax = $maxval;
265         if ($maxval > $lastmax) {
266                 print ' ' x ($lenval + $len);
267                 printf color(90);
268                 printf '%-*s',
269                         ($lastmax - $minval) * $size + .5,
270                         '-' x (($values[$nr - 1] - $minval) * $size);
271                 print color(92);
272                 say '+' x (($range - $lastmax) * $size + .5);
273                 print color(0);
274                 $lastmax = $maxval;
275         }
276 }
277
278 say(
279         color(31), sprintf('%*s', $lenval, $minval),
280         color(90), '-', color(36), '+',
281         color(32), sprintf('%*s', $size * $range - 3, $maxval),
282         color(90), '-', color(36), '+',
283         color(0),
284 ) if $opt{header};
285
286 while ($nr <= $limit) {
287         my $val = $values[$nr];
288         my $rel = length $val && $range && min(1, ($val - $minval) / $range);
289         my $color = !length $val || !$opt{palette} ? undef :
290                 $val == $order[0] ? $opt{palette}->[-1] : # max
291                 $val == $order[-1] ? $opt{palette}->[0] : # min
292                 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
293         my $indicator = $opt{indicators} && $opt{indicators}->[
294                 !length($val) || !$#{$opt{indicators}} ? 0 : # blank
295                 $#{$opt{indicators}} < 2 ? 1 :
296                 $val >= $order[0] ? -1 :
297                 $rel * ($#{$opt{indicators}} - 1e-14) + 1
298         ];
299
300         if ($opt{spark}) {
301                 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
302                 print color($color), $_ for $indicator;
303                 next;
304         }
305         print $indicator if defined $indicator;
306
307         if (length $val) {
308                 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
309                         sprintf "%*s", $lenval, $val;
310                 color($color) for $val;
311         }
312         my $line = $lines[$nr] =~ s/\n/$val/r;
313         if (not length $val) {
314                 say $line;
315                 next;
316         }
317         printf '%-*s', $len + length($val), $line;
318         print $barmark[$_] // $opt{'graph-format'}
319                 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
320         say '';
321 }
322 continue {
323         $nr++;
324 }
325 say $opt{palette} ? color(0) : '' if $opt{spark};
326
327         return $nr;
328 }
329
330 sub show_stat {
331         if ($opt{hidemin} or $opt{hidemax}) {
332                 my $linemin = $opt{hidemin};
333                 my $linemax = ($opt{hidemax} || @lines) - 1;
334                 if ($linemin < 0) {
335                         $linemin += @lines;
336                         $linemax = @lines - $linemax;
337                 }
338                 printf '%.8g of ', $opt{'sum-format'}->(
339                         sum(grep {length} @values[$linemin .. $linemax]) // 0
340                 );
341         }
342         if (@order) {
343                 my $total = sum @order;
344                 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
345                 printf ' in %d values', scalar @order;
346                 printf ' over %d lines', scalar @lines if @order != @lines;
347                 printf(' (%s min, %s avg, %s max)',
348                         color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
349                         color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
350                         color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
351                 );
352         }
353         say '';
354         return 1;
355 }
356
357 sub show_exit {
358         show_lines();
359         show_stat() if $opt{stat};
360         exit 130 if @_;  # 0x80+signo
361         exit;
362 }
363
364 show_exit();
365
366 __END__
367 Usage:                                               /\_/\
368   barcat [OPTIONS] [FILES|NUMBERS]                  (=•.•=)
369                                                     (u   u)
370 Options:
371   -a, --[no-]ascii         Restrict user interface to ASCII characters
372   -c, --[no-]color         Force colored output of values and bar markers
373   -f, --field=(N|REGEXP)   Compare values after a given number of whitespace
374                            separators
375       --header             Prepend a chart axis with minimum and maximum
376                            values labeled
377   -H, --human-readable     Format values using SI unit prefixes
378   -t, --interval[=(N|-LINES)]
379                            Output partial progress every given number of
380                            seconds or input lines
381   -l, --length=[-]SIZE[%]  Trim line contents (between number and bars)
382   -L, --limit[=(N|-LAST|START-[END])]
383                            Stop output after a number of lines
384       --graph-format=CHAR  Glyph to repeat for the graph line
385   -m, --markers=FORMAT     Statistical positions to indicate on bars
386       --min=N, --max=N     Bars extend from 0 or the minimum value if lower
387       --palette=(PRESET|COLORS)
388                            Override colors of parsed numbers
389   -_, --spark              Replace lines by sparklines
390       --indicators[=CHARS] Prefix a unicode character corresponding to each
391                            value
392   -s, --stat               Total statistics after all data
393   -u, --unmodified         Do not reformat values, keeping leading whitespace
394       --value-length=SIZE  Reserved space for numbers
395   -w, --width=COLUMNS      Override the maximum number of columns to use
396   -h, --usage              Overview of available options
397       --help               Full pod documentation
398   -V, --version            Version information
399
400 =encoding utf8
401
402 =head1 NAME
403
404 barcat - concatenate texts with graph to visualize values
405
406 =head1 SYNOPSIS
407
408 B<barcat> [<options>] [<file>... | <numbers>]
409
410 =head1 DESCRIPTION
411
412 Visualizes relative sizes of values read from input
413 (parameters, file(s) or STDIN).
414 Contents are concatenated similar to I<cat>,
415 but numbers are reformatted and a bar graph is appended to each line.
416
417 Don't worry, barcat does not drink and divide.
418 It can has various options for input and output (re)formatting,
419 but remains limited to one-dimensional charts.
420 For more complex graphing needs
421 you'll need a larger animal like I<gnuplot>.
422
423 =head1 OPTIONS
424
425 =over
426
427 =item -a, --[no-]ascii
428
429 Restrict user interface to ASCII characters,
430 replacing default UTF-8 by their closest approximation.
431 Input is always interpreted as UTF-8 and shown as is.
432
433 =item -c, --[no-]color
434
435 Force colored output of values and bar markers.
436 Defaults on if output is a tty,
437 disabled otherwise such as when piped or redirected.
438 Can also be disabled by setting I<-C>
439 or the I<NO_COLOR> environment variable.
440
441 =item -f, --field=(<number> | <regexp>)
442
443 Compare values after a given number of whitespace separators,
444 or matching a regular expression.
445
446 Unspecified or I<-f0> means values are at the start of each line.
447 With I<-f1> the second word is taken instead.
448 A string can indicate the starting position of a value
449 (such as I<-f:> if preceded by colons),
450 or capture the numbers itself,
451 for example I<-f'(\d+)'> for the first digits anywhere.
452
453 =item --header
454
455 Prepend a chart axis with minimum and maximum values labeled.
456
457 =item -H, --human-readable
458
459 Format values using SI unit prefixes,
460 turning long numbers like I<12356789> into I<12.4M>.
461 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
462 Short integers are aligned but kept without decimal point.
463
464 =item -t, --interval[=(<seconds> | -<lines>)]
465
466 Output partial progress every given number of seconds or input lines.
467 An update can also be forced by sending a I<SIGALRM> alarm signal.
468
469 =item -l, --length=[-]<size>[%]
470
471 Trim line contents (between number and bars)
472 to a maximum number of characters.
473 The exceeding part is replaced by an abbreviation sign,
474 unless C<--length=0>.
475
476 Prepend a dash (i.e. make negative) to enforce padding
477 regardless of encountered contents.
478
479 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
480
481 Stop output after a number of lines.
482 A single value indicates the last line number (like C<head>),
483 or first line counting from the bottom if negative (like C<tail>).
484 A specific range can be given by two values.
485
486 All input is still counted and analyzed for statistics,
487 but disregarded for padding and bar size.
488
489 =item --graph-format=<character>
490
491 Glyph to repeat for the graph line.
492 Defaults to a dash C<->.
493
494 =item -m, --markers=<format>
495
496 Statistical positions to indicate on bars.
497 A single indicator glyph precedes each position:
498
499 =over 2
500
501 =item <number>
502
503 Exact value to match on the axis.
504 A vertical bar at the zero crossing is displayed by I<|0>
505 for negative values.
506 For example I<:3.14> would show a colon at pi.
507
508 =item <percentage>I<v>
509
510 Ranked value at the given percentile.
511 The default shows I<+> at I<50v> for the mean or median;
512 the middle value or average between middle values.
513 One standard deviation right of the mean is at about I<68.3v>.
514 The default includes I<< >31.73v <68.27v >>
515 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
516
517 =item I<avg>
518
519 Matches the average;
520 the sum of all values divided by the number of counted lines.
521 Indicated by default as I<=>.
522
523 =back
524
525 =item --min=<number>, --max=<number>
526
527 Bars extend from 0 or the minimum value if lower,
528 to the largest value encountered.
529 These options can be set to customize this range.
530
531 =item --palette=(<preset> | <color>...)
532
533 Override colors of parsed numbers.
534 Can be any CSI escape, such as I<90> for default dark grey,
535 or alternatively I<1;30> for bright black.
536
537 In case of additional colors,
538 the last is used for values equal to the maximum, the first for minima.
539 If unspecified, these are green and red respectively (I<31 90 32>).
540 Multiple intermediate colors will be distributed
541 relative to the size of values.
542
543 Predefined color schemes are named I<whites> and I<fire>,
544 or I<greys> and I<fire256> for 256-color variants.
545
546 =item -_, --spark
547
548 Replace lines by I<sparklines>,
549 single characters (configured by C<--indicators>)
550 corresponding to input values.
551
552 =item --indicators[=<characters>]
553
554 Prefix a unicode character corresponding to each value.
555 The first specified character will be used for non-values,
556 the remaining sequence will be distributed over the range of values.
557 Unspecified, block fill glyphs U+2581-2588 will be used.
558
559 =item -s, --stat
560
561 Total statistics after all data.
562
563 =item -u, --unmodified
564
565 Do not reformat values, keeping leading whitespace.
566 Keep original value alignment, which may be significant in some programs.
567
568 =item --value-length=<size>
569
570 Reserved space for numbers.
571
572 =item -w, --width=<columns>
573
574 Override the maximum number of columns to use.
575 Appended graphics will extend to fill up the entire screen.
576
577 =item -h, --usage
578
579 Overview of available options.
580
581 =item --help
582
583 Full pod documentation
584 as rendered by perldoc.
585
586 =item -V, --version
587
588 Version information.
589
590 =back
591
592 =head1 EXAMPLES
593
594 Draw a sine wave:
595
596     seq 30 | awk '{print sin($1/10)}' | barcat
597
598 Compare file sizes (with human-readable numbers):
599
600     du -d0 -b * | barcat -H
601
602 Memory usage of user processes with long names truncated:
603
604     ps xo rss,pid,cmd | barcat -l40
605
606 Monitor network latency from prefixed results:
607
608     ping google.com | barcat -f'time=\K' -t
609
610 Commonly used after counting, for example users on the current server:
611
612     users | tr ' ' '\n' | sort | uniq -c | barcat
613
614 Letter frequencies in text files:
615
616     cat /usr/share/games/fortunes/*.u8 |
617     perl -CS -nE 'say for grep length, split /\PL*/, uc' |
618     sort | uniq -c | barcat
619
620 Number of HTTP requests per day:
621
622     cat httpd/access.log | cut -d\  -f4 | cut -d: -f1 | uniq -c | barcat
623
624 Any kind of database query with counts, preserving returned alignment:
625
626     echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
627     psql -t | barcat -u
628
629 In PostgreSQL from within the client:
630
631     > SELECT sin(generate_series(0, 3, .1)) \g |barcat
632
633 Earthquakes worldwide magnitude 1+ in the last 24 hours:
634
635     curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
636     column -tns, | barcat -f4 -u -l80%
637
638 External datasets, like movies per year:
639
640     curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
641     jq .[].year | uniq -c | barcat
642
643 Pokémon height comparison:
644
645     curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
646     jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
647
648 USD/EUR exchange rate from CSV provided by the ECB:
649
650     curl https://sdw.ecb.europa.eu/export.do \
651          -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
652     barcat -f',\K' --value-length=7
653
654 Total population history in XML from the World Bank:
655
656     curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
657     xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
658     barcat -f1 -H
659
660 And of course various Git statistics, such commit count by year:
661
662     git log --pretty=%ci | cut -b-4 | uniq -c | barcat
663
664 Or the top 3 most frequent authors with statistics over all:
665
666     git shortlog -sn | barcat -L3 -s
667
668 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
669
670     ( git log --pretty=%ci --since=30day | cut -b-10
671       seq 0 30 | xargs -i date +%F -d-{}day ) |
672     sort | uniq -c | awk '$1--' | barcat --spark
673
674 Sparkline graphics of simple input given as inline parameters:
675
676     barcat -_ 3 1 4 1 5 0 9 2 4
677
678 Misusing the spark functionality to draw a lolcat line:
679
680     seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
681
682 =head1 AUTHOR
683
684 Mischa POSLAWSKY <perl@shiar.org>
685
686 =head1 LICENSE
687
688 GPL3+.