Forum: Ruby [SOLUTION] #64: Text::Graph

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
2cf6d8e639314abd751f83a72e9a2ac5?d=identicon&s=25 Martin DeMello (Guest)
on 2006-02-01 23:31
(Received via mailing list)
A port of Text::Graph, which generates pretty ascii bar graphs from
numeric datasets, like

    aaaa :       (1)
      bb :..*    (22)
     ccc :...*   (43)
  dddddd :.....* (500)
      ee :......*(1000)
       f :.....* (300)
     ghi :...*   (50)

It accepts data in the following forms (see the 'extract' method):

# { label => value, label => value, ... }
# { :values => { label => value, ...} }
# { :values => [values] }
# {:values => { label => value, label => value }, :labels => [...]}
# {:values => [values], :labels => [labels]}
# [ [label, value], [label, value], ...]
# [[values], [labels]]

Numeric parameters:
:minval
:maxval
:maxlen

Boolean parameters:
:log   # logarithmic scale
:right # label justification

Display parameters:
:marker
:fill
:separator
:style # {:bar|:line} - sets default values for marker and fill
:showval # numeric value after bar

martin

# Text::Graph
# Port of Wade Johnson's Text::Graph for perl
#   http://search.cpan.org/src/GWADEJ/Text-Graph-0.23/Graph.pm
#
# Author: Martin DeMello <martindemello@gmail.com>

module Enumerable
  def minmax
    min = 1.0/0
    max = -1.0/0
    each {|i|
      min = i if i < min
      max = i if i > max
    }
    [min, max]
  end

  def map_with_index
    a = []
    each_with_index {|e, i| a << yield(e,i)}
    a
  end
end

class TextGraph
  include Math

  def initialize(data, params = {})
    @data = extract(data)
    @params = {:style => (params[:style] || :bar)}
    apply_style(@params[:style])
    @params.update(params)
    @params[:separator] ||= " :"
  end

  def update_params(par)
    apply_style(par[:style]) if par[:style]
    @params.update(par)
  end

  def apply_style(style)
    if style == :bar
      @params[:marker] = "*"
      @params[:fill] = "*"
    elsif style == :line
      @params[:marker] = '*'
      @params[:fill] = ' '
    else
      raise "Invalid style #{style}"
    end
  end

  def extract(data)
    if data.is_a? Array
      if data.length == 2 and data[0].is_a? Array and data[1].is_a?
Array
  # [[values], [labels]]
  a = {}
  a[:values] = data[0]
  a[:labels] = data[1]
  data = a
      else
  # [ [label, value], [label, value], ...]
  a = {:values => [], :labels => []}
  data.each {|i,j| a[:labels] << i; a[:values] << j}
      end
    end

    if (data.length == 2) and data[:values] and data[:labels]
      if data[:values].is_a? Array
  # {:values => [values], :labels => [labels]}
  # do nothing
      elsif data[:values].is_a? Hash
  # {:values => { label => value, label => value }, :labels => [...]}
  a = data[:labels].map {|i| data[:values][i]}
  data[:values] = a
      else
  raise "Invalid valueset"
      end
    elsif (data.length == 1) and data[:values]
      if data[:values].is_a? Array
  # { :values => [values] }
  data[:labels] = data[:values].map {""}
      elsif data[:values].is_a? Hash
  # { :values => { label => value, ...} }
  data[:labels] = data[:values].keys.sort_by {|i| i.to_s}
  data[:values] = data[:labels].map {|i| data[:values][i]}
      else
  raise "Invalid valueset"
      end
    else
      # { label => value, label => value, ... }
      a = {}
      a[:labels] = data.keys.sort_by {|i| i.to_s}
      a[:values] = a[:labels].map {|i| data[i]}
      data = a
    end
    data[:labels].map! {|i| i.to_s}
    data
  end

  def make_within(val, min, max)
    (val < min) ? min : (val > max ? max : val)
  end

  def makebar(val, m, f)
    val = (val + 0.5).to_i
    (val > 0) ? (f*(val - 1) + m) : ""
  end

  def fmt_labels (right, labels)
    len = labels.map {|i| i.length}.max
    just = right ? :rjust : :ljust
    labels.map {|i| i.send(just, len)}
  end

  def make_labelled_lines(data)
    labels = fmt_labels(@params[:right], data[:labels])
    lines  = histogram(data)
    lines.zip(labels).map {|line, label| label + @params[:separator] +
line}
  end

  def histogram(data)
    values = data[:values].dup
    minval, maxval, maxlen =
      @params[:minval], @params[:maxval], @params[:maxlen]

    if @params[:log]
      values.map! {|i| log(i)}
      minval = log(minval) rescue 1 if minval
      maxval = log(maxval) rescue 1 if maxval
    end

    min, max = values.minmax
    minval ||= min
    maxval ||= max
    maxl = maxval - minval + 1
    maxlen ||= maxl
    scale = maxlen*1.0/maxl
    values = values.map {|i|
      j = make_within(i, minval, maxval) - minval
      makebar(j*scale, @params[:marker], @params[:fill])
    }

    if(@params[:showval])
      values = values.map_with_index {|v, i|
  v.ljust(maxlen) + "(#{data[:values][i]})"
      }
    end

    values
  end

  def to_s
    make_labelled_lines(@data).join("\n") + "\n"
  end
end


if __FILE__ == $0

  a = TextGraph.new({
    :values => [1,2,4,5,10,3,5],
    :labels => %w(aaaa bb ccc dddddd ee f ghi)
  })

  puts a

  #  aaaa   :
  #  bb     :*
  #  ccc    :***
  #  dddddd :****
  #  ee     :*********
  #  f      :**
  #  ghi    :****

  puts "-------------------------------------------------------------"

  a.update_params(:style => :line, :right => true, :showval => true)
  puts a

  #    aaaa :          (1)
  #      bb :*         (2)
  #     ccc :  *       (4)
  #  dddddd :   *      (5)
  #      ee :        * (10)
  #       f : *        (3)
  #     ghi :   *      (5)

  puts "-------------------------------------------------------------"

  b = TextGraph.new({ :a=>1, :b=>5, :c=>20, :d=>10, :e=>17 }, {:maxlen
=> 10})
  puts b

  #  a :
  #  b :**
  #  c :**********
  #  d :*****
  #  e :********

  puts "-------------------------------------------------------------"

  c = TextGraph.new({:values => { :a=>1, :b=>5, :c=>20, :d=>10, :e=>17
},
        :labels => [:a, :c, :d]},
        {:minval => 0, :maxval => 15, :showval => true})
  puts c

  #  a :*               (1)
  #  c :*************** (20)
  #  d :**********      (10)

  puts "-------------------------------------------------------------"

  d = TextGraph.new([[1,22,43,500,1000,300,50], %w(aaaa bb ccc dddddd ee
f ghi)],
        { :style => :line,
          :right => true,     # right-justify labels
          :fill => '.',       # change fill-marker
          :log => true,       # logarithmic graph
          :showval => true    # show actual values
  }
       )
       puts d

       #    aaaa :       (1)
       #      bb :..*    (22)
       #     ccc :...*   (43)
       #  dddddd :.....* (500)
       #      ee :......*(1000)
       #       f :.....* (300)
       #     ghi :...*   (50)

end
This topic is locked and can not be replied to.