b939ea95ae88a91ddd0fe89b2017ecb1ae77259d
[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} //= -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 && ($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                 !$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
439 =item -f, --field=(<number> | <regexp>)
440
441 Compare values after a given number of whitespace separators,
442 or matching a regular expression.
443
444 Unspecified or I<-f0> means values are at the start of each line.
445 With I<-f1> the second word is taken instead.
446 A string can indicate the starting position of a value
447 (such as I<-f:> if preceded by colons),
448 or capture the numbers itself,
449 for example I<-f'(\d+)'> for the first digits anywhere.
450
451 =item --header
452
453 Prepend a chart axis with minimum and maximum values labeled.
454
455 =item -H, --human-readable
456
457 Format values using SI unit prefixes,
458 turning long numbers like I<12356789> into I<12.4M>.
459 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
460 Short integers are aligned but kept without decimal point.
461
462 =item -t, --interval[=(<seconds> | -<lines>)]
463
464 Output partial progress every given number of seconds or input lines.
465 An update can also be forced by sending a I<SIGALRM> alarm signal.
466
467 =item -l, --length=[-]<size>[%]
468
469 Trim line contents (between number and bars)
470 to a maximum number of characters.
471 The exceeding part is replaced by an abbreviation sign,
472 unless C<--length=0>.
473
474 Prepend a dash (i.e. make negative) to enforce padding
475 regardless of encountered contents.
476
477 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
478
479 Stop output after a number of lines.
480 A single value indicates the last line number (like C<head>),
481 or first line counting from the bottom if negative (like C<tail>).
482 A specific range can be given by two values.
483
484 All input is still counted and analyzed for statistics,
485 but disregarded for padding and bar size.
486
487 =item --graph-format=<character>
488
489 Glyph to repeat for the graph line.
490 Defaults to a dash C<->.
491
492 =item -m, --markers=<format>
493
494 Statistical positions to indicate on bars.
495 A single indicator glyph precedes each position:
496
497 =over 2
498
499 =item <number>
500
501 Exact value to match on the axis.
502 A vertical bar at the zero crossing is displayed by I<|0>
503 for negative values.
504 For example I<:3.14> would show a colon at pi.
505
506 =item <percentage>I<v>
507
508 Ranked value at the given percentile.
509 The default shows I<+> at I<50v> for the mean or median;
510 the middle value or average between middle values.
511 One standard deviation right of the mean is at about I<68.3v>.
512 The default includes I<< >31.73v <68.27v >>
513 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
514
515 =item I<avg>
516
517 Matches the average;
518 the sum of all values divided by the number of counted lines.
519 Indicated by default as I<=>.
520
521 =back
522
523 =item --min=<number>, --max=<number>
524
525 Bars extend from 0 or the minimum value if lower,
526 to the largest value encountered.
527 These options can be set to customize this range.
528
529 =item --palette=(<preset> | <color>...)
530
531 Override colors of parsed numbers.
532 Can be any CSI escape, such as I<90> for default dark grey,
533 or alternatively I<1;30> for bright black.
534
535 In case of additional colors,
536 the last is used for values equal to the maximum, the first for minima.
537 If unspecified, these are green and red respectively (I<31 90 32>).
538 Multiple intermediate colors will be distributed
539 relative to the size of values.
540
541 Predefined color schemes are named I<whites> and I<fire>,
542 or I<greys> and I<fire256> for 256-color variants.
543
544 =item -_, --spark
545
546 Replace lines by I<sparklines>,
547 single characters (configured by C<--indicators>)
548 corresponding to input values.
549
550 =item --indicators[=<characters>]
551
552 Prefix a unicode character corresponding to each value.
553 The first specified character will be used for non-values,
554 the remaining sequence will be distributed over the range of values.
555 Unspecified, block fill glyphs U+2581-2588 will be used.
556
557 =item -s, --stat
558
559 Total statistics after all data.
560
561 =item -u, --unmodified
562
563 Do not reformat values, keeping leading whitespace.
564 Keep original value alignment, which may be significant in some programs.
565
566 =item --value-length=<size>
567
568 Reserved space for numbers.
569
570 =item -w, --width=<columns>
571
572 Override the maximum number of columns to use.
573 Appended graphics will extend to fill up the entire screen.
574
575 =item -h, --usage
576
577 Overview of available options.
578
579 =item --help
580
581 Full pod documentation
582 as rendered by perldoc.
583
584 =item -V, --version
585
586 Version information.
587
588 =back
589
590 =head1 EXAMPLES
591
592 Draw a sine wave:
593
594     seq 30 | awk '{print sin($1/10)}' | barcat
595
596 Compare file sizes (with human-readable numbers):
597
598     du -d0 -b * | barcat -H
599
600 Memory usage of user processes with long names truncated:
601
602     ps xo rss,pid,cmd | barcat -l40
603
604 Monitor network latency from prefixed results:
605
606     ping google.com | barcat -f'time=\K' -t
607
608 Commonly used after counting, for example users on the current server:
609
610     users | tr ' ' '\n' | sort | uniq -c | barcat
611
612 Letter frequencies in text files:
613
614     cat /usr/share/games/fortunes/*.u8 |
615     perl -CS -nE 'say for grep length, split /\PL*/, uc' |
616     sort | uniq -c | barcat
617
618 Number of HTTP requests per day:
619
620     cat httpd/access.log | cut -d\  -f4 | cut -d: -f1 | uniq -c | barcat
621
622 Any kind of database query with counts, preserving returned alignment:
623
624     echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
625     psql -t | barcat -u
626
627 In PostgreSQL from within the client:
628
629     > SELECT sin(generate_series(0, 3, .1)) \g |barcat
630
631 Earthquakes worldwide magnitude 1+ in the last 24 hours:
632
633     curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
634     column -tns, | barcat -f4 -u -l80%
635
636 External datasets, like movies per year:
637
638     curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
639     jq .[].year | uniq -c | barcat
640
641 Pokémon height comparison:
642
643     curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
644     jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
645
646 USD/EUR exchange rate from CSV provided by the ECB:
647
648     curl https://sdw.ecb.europa.eu/export.do \
649          -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
650     barcat -f',\K' --value-length=7
651
652 Total population history in XML from the World Bank:
653
654     curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
655     xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
656     barcat -f1 -H
657
658 And of course various Git statistics, such commit count by year:
659
660     git log --pretty=%ci | cut -b-4 | uniq -c | barcat
661
662 Or the top 3 most frequent authors with statistics over all:
663
664     git shortlog -sn | barcat -L3 -s
665
666 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
667
668     ( git log --pretty=%ci --since=30day | cut -b-10
669       seq 0 30 | xargs -i date +%F -d-{}day ) |
670     sort | uniq -c | awk '$1--' | barcat --spark
671
672 Sparkline graphics of simple input given as inline parameters:
673
674     barcat -_ 3 1 4 1 5 0 9 2 4
675
676 Misusing the spark functionality to draw a lolcat line:
677
678     seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
679
680 =head1 AUTHOR
681
682 Mischa POSLAWSKY <perl@shiar.org>
683
684 =head1 LICENSE
685
686 GPL3+.