#!/usr/bin/env ruby
USAGE = <<-EOF
Plot one or several numbers in ASCII-art horizontal bars.

Every line of input is expected to contain either whitespace-separated
numbers or whitespace-separated headers, and if it is numbers, it
generates a line of output, showing those numbers both as digits and
graphically.  The idea is that you pipe the input from a process that
generates a line every so often, and dplot graphs it.

dplot selects a round-number scale big enough to hold the last page of
samples.  Page size is 24 by default, but can be set with the -x
switch.  If headers are provided, they are displayed every page as
well, like vmstat, or whenever they change.  The output is scaled to
80 columns by default, with an equal amount of horizontal space being
given to each numerical value; the width can be set with the -y
switch.

The -h switch converts large numbers to “human-readable” form for
output by expressing them in Ki, Mi, Gi, or Ti.  There’s also a --si
switch that uses powers of 1000, with K, M, G, or T.  These are handy
if you’re monitoring something like a filesize or amount of space free
on your disk.  (df has a -B1 option for reporting free space in bytes
instead of kilobytes.)

When the scale of a column changes, dplot will print a blank line and
re-display the last page of output, choosing new scales for all
columns.  This happens either when a number is too big to graph with
the old scale, or when the entire last page of output is small enough
to redisplay with a smaller scale.  -q prevents the last page of
output from being redisplayed upon rescaling, which might be handy if
you’re feeding the output to a printer or something.

To avoid unnecessary rescaling, when dplot chooses a scale for a
column, it chooses a scale that will hold about 1.41 times the maximum
value recently observed.  This way, even if you start dplot while
numbers are increasing, they will have to increase for a while before
it has to choose new scales.

By default dplot assumes that all inputs are unsigned.  If it ever
sees a negative number, it will rescale that column to a symmetric
signed range instead, with a zero in the middle of the visible range.
-s tells dplot to assume all inputs are signed.

By default dplot uses ANSI escape sequences to draw the bar graphs in
solid color.  -a tells it to use plain ASCII text instead, using #
characters and spaces.

If the numbers being provided are not independently varying
quantities, but rather fractions of some total, dplot has a -f option
to use a single horizontal scale for all of them, rescaled to the
output width before every line, using horizontally-stacked bars.  This
option displays the titles of the quantities horizontally centered
within bars, but only when the bars are wide enough that they can fit.
If a bar becomes wide enough for its title to fit, when it hasn't been
sufficiently within the last page, its title will then be displayed
even though it’s not at the top of a page.

-f will try to allocate a separate color for each input quantity.  -f
-a will try to allocate a separate ASCII-art character instead, using
' ', '#', '-', '@', '*', '%', '_', '=', '~', and '.'.  If there are
many input quantities, some of them will be assigned the same color or
character.  If two indistinguishable bars collide, dplot will
reallocate colors and redisplay the last page.

The xdplot command does the same thing as dplot, but in an X window,
with a more conventional horizontal X-axis.

The pngdplot command does the same thing as xdplot, but takes a -o
option to specify the name of a PNG file to rewrite with the current
plot display after every input line.

EOF

# TODO next:
# - handle multiple fields
# - handle headers
# - reprint page on re-autorange

def log10(x)
  Math.log(x) / Math.log(10)
end

def round_up(x)
  log = log10(x).ceil
  pow = 10**log
  case
  when x <= pow / 5 then pow / 5
  when x <= pow / 2 then pow / 2
  else pow
  end
end

use_ansi = ARGV.index('-a').nil?
human_readable = !ARGV.index('-h').nil?
width = 80
max = 0
max_field_width = 0

STDIN.each do |line|
  fields = line.split
  value_s = fields[0]
  value = Float(value_s)

  if human_readable
    value_s = case
              when value > 3 * 2.0**40 then "%.1fTi" % (value / 2.0**40)
              when value > 3 * 2.0**30 then "%.1fGi" % (value / 2.0**30)
              when value > 3 * 2.0**20 then "%.1fMi" % (value / 2.0**20)
              when value > 3 * 2.0**10 then "%.1fKi" % (value / 2.0**10)
              when value == value.floor then "%d" % value
              when value > 3 then "%.1f" % value
              else "%.2g" % value
              end
  end

  if value.abs > max
    max = round_up(value.abs * 1.414)
    puts
  end

  max_field_width = value_s.length if value_s.length > max_field_width
  barwidth = width - max_field_width - 1
  scaled = max > 0 ? value / max * barwidth : 0

  if use_ansi
    bar = "\e[44m#{' ' * scaled}\e[0m#{' ' * (barwidth - scaled)}"
  else
    bar = ('#' * scaled).ljust(barwidth)
  end

  puts "#{value_s.rjust(max_field_width)} #{bar}"
end
