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