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