public
Description: Console ascii art plotter - quick-and-dirty data visualization, e.g. for log statistics
Homepage: http://anyall.org/blog/2008/05/conplot-a-console-plotter/
Clone URL: git://github.com/brendano/conplot.git
conplot / conplot.rb
100755 134 lines (112 sloc) 3.754 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#!/usr/bin/env ruby
#
# conplot.rb -- reads a column of numbers and plots them on the console
# Main site: http://github.com/brendano/conplot
# Introduction: http://anyall.org/blog/2008/05/conplot-a-console-plotter/
# Brendan O'Connor - brenocon@gmail.com - anyall.org
 
 
require 'pp'
 
module ConPlot
  class Plot
    def initialize
      @x_min = nil
      @x_max = nil
      @y_min = nil
      @y_max = nil
 
      @x = []
      @y = []
 
      @plot_left_margin = 6
      
    end
    
    def console_rows
      24
    end
 
    def console_cols
      80
    end
 
    def print_console
      plot_vert_offset = 0
      plot_horiz_offset = @plot_left_margin
      plot_width = console_cols - plot_horiz_offset
      plot_height = console_rows - plot_vert_offset
 
      screen = (0...plot_height).map { [' ']*plot_width }
 
      raise "vectors not aligned" unless @x.size == @y.size
      (0...@x.size).each do |i|
        screen_x = (@x[i] - @x_min).to_f / (@x_max - @x_min) * plot_width + plot_horiz_offset
        screen_y = (@y[i] - @y_min).to_f / (@y_max - @y_min) * plot_height + plot_vert_offset
        screen[screen_y.to_i][screen_x.to_i] = 'o'
      end
 
      add_vert_ticks(screen, plot_height)
      add_horiz_ticks(screen, plot_width, plot_horiz_offset)
 
      screen.reverse.each do |row|
        # p row
        puts row.join("")
      end
    end
 
    # should special case when x's are all sequential integers or so
    def add_horiz_ticks(screen, plot_width, plot_horiz_offset)
      screen_tick_points = [ [0,:left], [plot_width-1,:right] ]
      screen_tick_points.each do |screen_tick_x, justify|
        x_range = @x_max - @x_min
        tick_point = (screen_tick_x.to_f / screen[0].size) * x_range + @x_min
        mark_str = reasonable_num_str(tick_point, x_range)
        screen_x = plot_horiz_offset + screen_tick_x
        landing_spots = (justify==:left) ? (screen_x .. (screen_x+mark_str.size)).to_a :
          ( (screen_x - mark_str.size) .. screen_x).to_a
        (0...mark_str.size).each do |i|
          screen[0][ landing_spots[i] ] = [ mark_str[i] ].pack("U*")
        end
      end
    end
 
    def reasonable_num_str(tick_point, range)
      s= "%f" % tick_point if range <= 0.5
      s= "%.3f" % tick_point if range > 0.5
      s= "%.2f" % tick_point if range > 3
      s= "%.1f" % tick_point if range > 30
      s= "%.0f" % tick_point if range > 3000
      s
    end
 
    def add_vert_ticks(screen, plot_height)
      # tick_points = [@y_min, @y_min + (@y_max-@y_min)/2, @y_max]
      # tick_points.each do |tick_point|
      screen_tick_points = [0, (screen.size/4).to_i, (screen.size/2).to_i, (screen.size*3/4).to_i, screen.size-1]
      screen_tick_points.each do |screen_tick_y|
        y_range = @y_max - @y_min
        tick_point = (screen_tick_y.to_f / screen.size) * y_range + @y_min
        mark_str = reasonable_num_str(tick_point, y_range)
        (0...mark_str.size).each do |i|
          screen[screen_tick_y.to_i][i] = [mark_str[i]].pack("U*")
        end
      end
    end
 
        # screen_tick_y = (tick_point - @y_min).to_f / (@y_max-@y_min) * (plot_height-1)
 
    def set_and_adjust_y(y)
      @y = y
      @x = (0...y.size).to_a
      range = (y.max - y.min).abs
      @y_min = y.min - range*0.05
      @y_max = y.max + range*0.05
      @x_min = 0
      @x_max = y.size - 1
    end
 
    def set_and_adjust_scatterplot( xy_pairs )
      raise "not implemented"
    end
  end
end
 
 
if $0 == __FILE__
  numbers = $stdin.readlines.map do |l|
    if l =~ /^\s*$/
      nil
    else
      begin; l.to_f
      rescue; nil
      end
    end
  end.compact
  if numbers.empty?
    puts "No inputs."
    exit
  end
  plot = ConPlot::Plot.new
  plot.set_and_adjust_y(numbers)
  plot.print_console
end