Current section
Files
Jump to
Current section
Files
src/asciiart.erl
-module(asciiart).
-export([ init/0
, dimensions/1
, render/1
, char/3
, char/2
, line/4
, line/3
, string/4
, string/3
, plot/1
, plot/2
, draw/2
, draw/1
, visible/3
]).
%%====================================================================
%% Types
%%====================================================================
-type vector() :: {integer(), integer()}.
-type cont() :: fun((canvas()) -> canvas()).
-opaque canvas() :: #{vector() => char()}.
-type plot_data() :: [{char(), [{float(), float()}]}].
-export_type([vector/0, canvas/0]).
-define(epsilon, 1.0e-6).
%%====================================================================
%% API functions
%%====================================================================
-spec init() -> canvas().
init() ->
#{}.
-spec render(canvas()) -> iolist().
render(Cnv) ->
{{Xm, Ym}, {XM, YM}} = dimensions(Cnv),
[[[maps:get({X, Y}, Cnv, $ ) || X <- lists:seq(Xm, XM)], $\n]
|| Y <- lists:reverse(lists:seq(Ym, YM))].
-spec draw([cont()], canvas()) -> canvas().
draw(Ops, Cnv) ->
lists:foldl(fun(F, Acc) -> F(Acc) end, Cnv, Ops).
-spec draw([cont()]) -> canvas().
draw(Ops) ->
draw(Ops, init()).
-spec dimensions(canvas()) -> {vector(), vector()}.
dimensions(Cnv) ->
Fun = fun({X, Y}, _, {{Xm, Ym}, {XM, YM}}) ->
{ {min(X, Xm), min(Y, Ym)}
, {max(X, XM), max(Y, YM)}
}
end,
maps:fold(Fun, {{1, 1}, {1, 1}}, Cnv).
-spec char(canvas(), vector(), char()) -> canvas().
char(Cnv, Pos, Char) ->
Cnv #{Pos => Char}.
-spec char(vector(), char()) -> cont().
char(Pos, Char) ->
fun(Cnv) -> char(Cnv, Pos, Char) end.
-spec line(canvas(), vector(), vector(), char()) -> canvas().
line(Cnv, {X1, Y1}, {X2, Y2}, Char) ->
X = X2 - X1,
Y = Y2 - Y1,
N = max(1, max(abs(X), abs(Y))),
lists:foldl( fun(Pos, Cnv) -> char(Cnv, Pos, Char) end
, Cnv
, [{ X1 + round(X * I / N)
, Y1 + round(Y * I / N)
} || I <- lists:seq(0, N)]
).
-spec line(vector(), vector(), char()) -> cont().
line(F, T, C) ->
fun(Cnv) -> line(Cnv, F, T, C) end.
-spec string(canvas(), vector(), string(), left | right) -> canvas().
string(Cnv, _, [], _) ->
Cnv;
string(Cnv, {X, Y}, String, Direction) ->
XL = case Direction of
right ->
lists:seq(X, X + length(String) - 1);
left ->
lists:seq(X - length(String) + 1, X)
end,
L = lists:zip(XL, String),
lists:foldl( fun({X, Char}, Cnv) ->
char(Cnv, {X, Y}, Char)
end
, Cnv
, L
).
-spec string(vector(), string(), left | right) -> cont().
string(Pos, Str, Dir) ->
fun(Cnv) -> string(Cnv, Pos, Str, Dir) end.
-spec plot(plot_data()) -> canvas().
plot(Datapoints) ->
plot(Datapoints, #{}).
-spec plot(plot_data(), map()) -> canvas().
plot(Datapoints, Config) ->
AllDatapoints = lists:append([L || {_, L} <- Datapoints]),
{XX, YY} = lists:unzip(AllDatapoints),
Xm = bound(min, Config, XX),
XM = bound(max, Config, XX),
Ym = bound(min, Config, YY),
YM = bound(max, Config, YY),
DX = max(?epsilon, XM - Xm),
DY = max(?epsilon, YM - Ym),
%% Dimensions of the plot:
AspectRatio = maps:get(aspect_ratio, Config, 0.2),
Width = max(length(Datapoints) * 2, 70),
Height = round(Width * AspectRatio),
Frame = {{Xm, Ym}, {Width / DX, Height / DY}},
%% Draw axis
Cnv0 = draw( [ %% Vertical:
line({0, 0}, {0, Height - 1}, $|)
, char({0, Height}, $^)
%% Labels:
, string({-2, 0}, print_num(Ym), left)
, string({-2, Height}, print_num(YM), left)
%% Horizontal:
, line({0, 0}, {Width - 1, 0}, $-)
, char({Width, 0}, $>)
, char({0, 0}, $+)
%% Labels
, string({0, -1}, print_num(Xm), right)
, string({Width, -1}, print_num(XM), left)
]
, init()
),
lists:foldl( fun({Char, Data}, Acc) ->
draw_datapoints(Frame, Char, Data, Acc)
end
, Cnv0
, Datapoints
).
draw_datapoints(Frame, Char, Data, Acc) ->
lists:foldl( fun(Coords, Acc) ->
char(Acc, plot_coord(Frame, Coords), Char)
end
, Acc
, Data
).
print_num(Num) when is_integer(Num) ->
integer_to_list(Num);
print_num(Num) ->
lists:flatten(io_lib:format("~.6..f", [Num])).
plot_coord({{Xm, Ym}, {SX, SY}}, {X, Y}) ->
{round((X - Xm) * SX), round((Y - Ym) * SY)}.
bound(Fun, Cfg, L) ->
N = case L of
[] -> 0;
_ -> lists:Fun(L)
end,
case maps:get(include_zero, Cfg, true) of
true ->
erlang:Fun(0, N);
false ->
N
end.
-spec visible(char(), string(), [term()]) -> iolist().
visible(Char, Fmt, Args) ->
Str = string:split(lists:flatten(io_lib:format(Fmt, Args)), "\n", all),
Width = max(80, lists:max([length(I) || I <- Str])),
N = length(Str),
Text = [string({4, Y}, S, right)
|| {Y, S} <- lists:zip( lists:seq(1, N)
, lists:reverse(Str)
)],
Cnv = draw([ asciiart:line({1, -1}, {Width, -1}, Char)
, asciiart:line({1, N + 2}, {Width, N + 2}, Char)
, asciiart:line({1, 0}, {1, N + 1}, Char)
, asciiart:line({2, 0}, {2, N + 1}, Char)
, asciiart:line({Width - 1, 0}, {Width - 1, N + 1}, Char)
, asciiart:line({Width, 0}, {Width, N + 1}, Char)
] ++ Text),
[$\n, render(Cnv), $\n].