[SOLUTION] #64: Text::Graph

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

Graph.pm - metacpan.org

Author: Martin DeMello [email protected]

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