Technical Musings: Console plotting in Erlang

Monday, April 25, 2011

Console plotting in Erlang

UPDATE: This script is now on gist: https://gist.github.com/dgulino/4750139

Years ago I created a simple Python script to plot a list of numbers in a simple ASCII line graph. I use this script all the time; I extract (with awk,perl,pyliner,...) a column of numbers out of a log file and pipe it in to this script. Very useful since even to today most admin work is done in a character based terminal.

UPDATE (4/28/2011): Also check my other terminal console escript entries: runavg.escript, escript to beam

All numbers are rounded to an integer.  It auto resizes new graph entries when a new max is set (default max is 0), and plots a new max line in bold.  If you set a threshold, it outputs any line over that threshold in red.  Or both.

I created an Erlang escript version of it, just as an exercise. It requires 'tput' to be in the path, this even works with the cygwin version. It also uses An Erlang version of getopt. I didn't bother to install it, I just copied getopt.erl to my src dir and compiled it. Also chmod u+x tgraph.escript

It's called as part of a pipe:
$ cat test.txt | ./tgraph.escript -t 40 -c 40

or with the file as a parameter:
./tgraph.escript test.txt -t 40 -c 40

test.txt:
1
50
20
3
45
34.0
12
0
1000
0
100
34


Output:

****************************************1
****************************************50
****************20
**3
************************************45
***************************34
**********12
0
****************************************1000
0
****100
*34


tgraph.escript:
#!/usr/bin/env escript
%% -*- erlang -*-
%%! -smp disable
%% Author: Drew Gulino
-module(tgraph).

-export([main/1]).

main(CmdLine) ->
 OptSpecList = option_spec_list(),
 case getopt:parse(OptSpecList, CmdLine) of
  {ok, {Options, NonOptArgs}} ->
   true;
  {error, {Reason, Data}} ->
   Options = [],
   NonOptArgs = [],
   io:format("Error: ~s ~p~n~n", [Reason, Data]),
   version(),
   getopt:usage(OptSpecList, "tgraph")
 end,
 Symbol = get_opt_value(symbol,Options),
 Columns = get_opt_value(columns,Options),
 Display_number = get_opt_value(display_number,Options),
 Threshold = get_opt_value(threshold,Options),
 Maximum = get_opt_value(maximum,Options), 
 
Bold = strip_newlines(os:cmd("tput bold")),
 Init = strip_newlines(os:cmd("tput init")),
 Dim = strip_newlines(os:cmd("tput sgr0")),
 Red = strip_newlines(os:cmd("tput setaf 1")),
 %Green = strip_newlines(os:cmd("tput setaf 2")),
 %Yellow = strip_newlines(os:cmd("tput setaf 3")),
 %Blue = strip_newlines(os:cmd("tput setaf 4")),
 %Magenta = strip_newlines(os:cmd("tput setaf 5")),

 case NonOptArgs of
  [] -> 
  F = standard_io;
  _ ->
  {ok, F} = file:open(NonOptArgs, read)
 end,
  proc_file(F, {Symbol, Columns, Display_number, Threshold, Maximum} , {Bold, Init, Dim, Red}).

version() ->
 io:format("Version: 1.1\n").

get_opt_value(Key, Options) ->
 case lists:keyfind(Key,1,Options) of
  {Key, Value} ->
  Value
 end,
 Value.

option_spec_list() ->
 %CurrentUser = os:getenv("USER"),
 [
 %% {Name, ShortOpt, LongOpt, ArgSpec, HelpMsg}
 {help, $h, "help", undefined, "Show the program options"},
 {version, $v, "version", undefined, "Version"},
 {display_number, $n, "display_number", {boolean, true}, "Display number w/graph"},
 {columns, $c, "columns", {integer, 72}, "Display columns (default = 72)"},
 {symbol, $s, "symbol", {string, "*"}, "Symbol to display (default = '*')"},
 {threshold, $t, "threshold", {integer, 0}, "Will color lines over this value"},
 {maximum, $m, "maximum", {integer, 0}, "Presets the scale for this maximum value (default = 0)"}
 ].

proc_file(F, Options, Tput) ->
{Symbol, Columns, Display_number, Threshold, Maximum} = Options, 
{Bold, Init, Dim, Red} = Tput,
%Columns = erlang:list_to_integer(os:cmd("tput cols")) - 8},
L = io:get_line(F, ''),
 case L of
  eof ->
   ok;
  "\n" ->
   false;
  Line ->
   Stripped = strip_newlines(Line),
   Num = cast_to_integer(Stripped),
   io:put_chars(Init),
   case Num > 0 of
    true ->
     case Num >= Maximum of
      true ->
       NewMax = Num,
       io:put_chars(Bold);
      false ->
       NewMax = Maximum,    
       io:put_chars(Dim)
     end,
     Scale = Columns / NewMax, 
     Graph = lists:map(fun(_) -> io_lib:format(Symbol,[]) end , lists:seq(1,erlang:round(Num * Scale))),
     case Threshold of
      0 -> 
       false;
      _ ->
       case Num >= Threshold of
        true ->
         io:put_chars(Red);
        false -> 
         %io:put_chars(Init)
         false
       end
     end,
     case Display_number of
      true ->
       io:format("~s~p~n",[Graph,Num]);
      false ->
       io:format("~p~n",[Graph])
     end,                                
     NewOptions = {Symbol, Columns, Display_number, Threshold, NewMax},
     proc_file(F,NewOptions, Tput);
    false ->
     io:put_chars(Dim),
     io:put_chars(Init),
     io:format("~p~n",[Num]),
     proc_file(F,Options, Tput)
   end
 end.

strip_newlines(String) ->
 string:strip(re:replace(String,"(.*)[\n\r]","\\1", [global,{return,list}])).

cast_to_integer([]) ->
 [];
cast_to_integer(Input) when is_integer(Input) ->
 Input;
cast_to_integer(Input) when is_float(Input) ->
 erlang:round(Input);
cast_to_integer(Input) when is_list(Input)->
 case lists:member($., Input) of
  true ->
   erlang:round(erlang:list_to_float(Input));
  false ->      
   erlang:list_to_integer(Input)
end.

UPDATE (4/26/2001):
Here's a link to the original python script: tgraph.py

UPDATE (4/27/2011):
Version 1.1:
1) Fixed bug where lines were never dimmed after being bolded in cygwin
2) Changed the compiler flags to disable smp and not register the process name.  Both not needed.  One note: Couldn't get +Bc to work in Windows.  This should change the break key to Ctrl-C, but still stays Ctrl-Break.
3) Moved tput initialization out of working loop, now runs quickly.

No comments: