aboutsummaryrefslogblamecommitdiffstats
path: root/src/visualizer.erl
blob: 75f85d1171951ab049a8f7b79cc4b18c87a2ff40 (plain) (tree)
1
2
3
4
5
6
7






                                                                       




                                                                 

               
         


                                              
          








































































































































































































































































































































                                                                                             
%%% @doc Relation Visualizer - Multiple Rendering Modes
%%%
%%% Provides various visual representations for nested relational data.
%%% Supports different modes optimized for different use cases.
%%%
%%% == Modes ==
%%%
%%% - table - Classic flat table (default)
%%% - nested_table - Tables within table cells
%%% - tree - Hierarchical tree view with indentation
%%% - linked_tables - Separate linked tables for nested relations
%%% - outline - Indented outline format
%%%
%%% == Usage ==
%%% <pre>
%%% Results = query_planner:collect(DB, Plan),
%%% visualizer:render(Results, tree).
%%% visualizer:render(Results, nested_table).
%%% </pre>
%%%
%%% @author Nekoma Team
%%% @copyright 2025

-module(visualizer).

-export([render/1,
         render/2,
         modes/0]).

-type render_mode() :: table
                     | nested_table
                     | tree
                     | linked_tables
                     | outline.

-export_type([render_mode/0]).

%%% Public API

%% @doc Render tuples with default mode (table).
-spec render([map()]) -> ok.
render(Tuples) ->
    render(Tuples, table).

%% @doc Render tuples with specified visualization mode.
-spec render([map()], render_mode()) -> ok.
render([], _Mode) ->
    io:format("(no results)~n"),
    ok;
render(Tuples, table) ->
    render_table(Tuples);
render(Tuples, nested_table) ->
    render_nested_table(Tuples);
render(Tuples, tree) ->
    render_tree(Tuples);
render(Tuples, linked_tables) ->
    render_linked_tables(Tuples);
render(Tuples, outline) ->
    render_outline(Tuples).

%% @doc List available rendering modes.
-spec modes() -> [render_mode()].
modes() ->
    [table, nested_table, tree, linked_tables, outline].

%%% Rendering Implementations

%% Mode 1: Table with proper column width calculation
render_table(Tuples) ->
    AllKeys = get_all_keys(Tuples),

    % Calculate column widths based on content
    Widths = calculate_column_widths(Tuples, AllKeys),

    io:format("~n"),
    print_separator_with_widths(Widths),
    print_row_with_widths([atom_to_list(K) || K <- AllKeys], Widths),
    print_separator_with_widths(Widths),

    lists:foreach(
        fun(Tuple) ->
            Values = [format_value_for_table(maps:get(K, Tuple, undefined)) || K <- AllKeys],
            print_row_with_widths(Values, Widths)
        end,
        Tuples
    ),
    print_separator_with_widths(Widths),
    io:format("~n~b tuple(s)~n~n", [length(Tuples)]),
    ok.

calculate_column_widths(Tuples, Keys) ->
    lists:map(
        fun(Key) ->
            % Start with header width
            HeaderWidth = length(atom_to_list(Key)),

            % Find max value width in this column
            MaxValueWidth = lists:foldl(
                fun(Tuple, MaxW) ->
                    Value = maps:get(Key, Tuple, undefined),
                    ValueStr = format_value_for_table(Value),
                    max(MaxW, length(ValueStr))
                end,
                HeaderWidth,
                Tuples
            ),

            max(12, MaxValueWidth)  % Minimum width of 12
        end,
        Keys
    ).

format_value_for_table(V) when is_map(V) ->
    % Format nested map as inline representation
    Keys = [K || K <- maps:keys(V), K =/= meta],
    Parts = [atom_to_list(K) ++ ":" ++ format_value(maps:get(K, V)) || K <- Keys],
    "[" ++ string:join(Parts, ", ") ++ "]";
format_value_for_table(V) ->
    format_value(V).

print_separator_with_widths(Widths) ->
    io:format("+"),
    lists:foreach(
        fun(W) ->
            io:format("~s+", [lists:duplicate(W + 2, $-)])
        end,
        Widths
    ),
    io:format("~n").

print_row_with_widths(Values, Widths) ->
    io:format("| "),
    lists:foreach(
        fun({Val, Width}) ->
            io:format("~*s | ", [-Width, Val])
        end,
        lists:zip(Values, Widths)
    ),
    io:format("~n").

%% Mode 2: Nested tables (tables within cells)
render_nested_table(Tuples) ->
    io:format("~n=== Nested Table View ===~n~n"),
    lists:foreach(
        fun(Tuple) ->
            render_tuple_nested(Tuple, 0)
        end,
        Tuples
    ),
    io:format("~n~b tuple(s)~n~n", [length(Tuples)]),
    ok.

render_tuple_nested(Tuple, Indent) ->
    Keys = [K || K <- maps:keys(Tuple), K =/= meta],
    IndentStr = lists:duplicate(Indent, $\s),

    io:format("~s+-- Tuple~n", [IndentStr]),
    lists:foreach(
        fun(Key) ->
            Value = maps:get(Key, Tuple),
            case is_map(Value) andalso (Key =/= meta) of
                true ->
                    % Nested tuple - render as inner table
                    io:format("~s|  ~s:~n", [IndentStr, atom_to_list(Key)]),
                    io:format("~s|  +-------------------------+~n", [IndentStr]),
                    NestedKeys = maps:keys(Value),
                    lists:foreach(
                        fun(NK) ->
                            NV = format_value(maps:get(NK, Value)),
                            io:format("~s|  | ~-10s : ~-10s |~n",
                                     [IndentStr, atom_to_list(NK), NV])
                        end,
                        NestedKeys
                    ),
                    io:format("~s|  +-------------------------+~n", [IndentStr]);
                false ->
                    % Simple value
                    io:format("~s|  ~-15s : ~s~n",
                             [IndentStr, atom_to_list(Key), format_value(Value)])
            end
        end,
        Keys
    ),
    io:format("~s+--~n", [IndentStr]).

%% Mode 3: Tree view with hierarchy
render_tree(Tuples) ->
    io:format("~n=== Tree View ===~n~n"),
    io:format("+-- Relation (~b tuples)~n", [length(Tuples)]),
    render_tuples_as_tree(Tuples, "|  ", length(Tuples)),
    io:format("~n"),
    ok.

render_tuples_as_tree([], _Prefix, _Total) ->
    ok;
render_tuples_as_tree([Tuple], Prefix, Total) ->
    io:format("~s~n~s+-- Row ~b~n", [Prefix, Prefix, Total]),
    render_tuple_as_tree(Tuple, Prefix ++ "    ");
render_tuples_as_tree([Tuple | Rest], Prefix, Total) ->
    Index = Total - length(Rest),
    io:format("~s~n~s+-- Row ~b~n", [Prefix, Prefix, Index]),
    render_tuple_as_tree(Tuple, Prefix ++ "|   "),
    render_tuples_as_tree(Rest, Prefix, Total).

render_tuple_as_tree(Tuple, Prefix) ->
    Keys = [K || K <- maps:keys(Tuple), K =/= meta],
    render_attrs_as_tree(Keys, Tuple, Prefix).

render_attrs_as_tree([], _Tuple, _Prefix) ->
    ok;
render_attrs_as_tree([Key], Tuple, Prefix) ->
    Value = maps:get(Key, Tuple),
    case is_map(Value) andalso (Key =/= meta) of
        true ->
            io:format("~s+-- ~s:~n", [Prefix, atom_to_list(Key)]),
            render_tuple_as_tree(Value, Prefix ++ "    ");
        false ->
            io:format("~s+-- ~s: ~s~n",
                     [Prefix, atom_to_list(Key), format_value(Value)])
    end;
render_attrs_as_tree([Key | Rest], Tuple, Prefix) ->
    Value = maps:get(Key, Tuple),
    case is_map(Value) andalso (Key =/= meta) of
        true ->
            io:format("~s+-- ~s:~n", [Prefix, atom_to_list(Key)]),
            render_tuple_as_tree(Value, Prefix ++ "|   "),
            render_attrs_as_tree(Rest, Tuple, Prefix);
        false ->
            io:format("~s+-- ~s: ~s~n",
                     [Prefix, atom_to_list(Key), format_value(Value)]),
            render_attrs_as_tree(Rest, Tuple, Prefix)
    end.

%% Mode 4: Linked tables (parent + separate child tables)
render_linked_tables(Tuples) ->
    io:format("~n=== Linked Tables View ===~n~n"),

    % Collect nested relations with parent references
    {FlatTuples, NestedRels} = extract_nested_relations_with_refs(Tuples),

    % Render main table
    io:format("Main Relation:~n"),
    render_table(FlatTuples),

    % Render nested tables with parent references
    case NestedRels of
        [] -> ok;
        _ ->
            io:format("~nNested Relations:~n"),
            lists:foreach(
                fun({RowIdx, AttrName, NestedTuple}) ->
                    io:format("~n[~s for Row ~b]:~n", [atom_to_list(AttrName), RowIdx]),
                    render_table([NestedTuple])
                end,
                NestedRels
            )
    end,
    ok.

extract_nested_relations_with_refs(Tuples) ->
    % Process tuples with index tracking
    {FlatTuples, NestedRels} = lists:foldl(
        fun(Tuple, {FlatAcc, NestedAcc}) ->
            RowIdx = length(FlatAcc) + 1,

            % Flatten this tuple
            FlatTuple = maps:map(
                fun(K, V) when is_map(V), K =/= meta ->
                        lists:flatten(io_lib:format("[~b]", [RowIdx]));
                   (_K, V) -> V
                end,
                Tuple
            ),

            % Extract nested relations with parent reference
            Nested = lists:filtermap(
                fun(K) ->
                    case maps:get(K, Tuple) of
                        V when is_map(V), K =/= meta ->
                            {true, {RowIdx, K, V}};
                        _ ->
                            false
                    end
                end,
                maps:keys(Tuple)
            ),

            {[FlatTuple | FlatAcc], Nested ++ NestedAcc}
        end,
        {[], []},
        Tuples
    ),

    {lists:reverse(FlatTuples), lists:reverse(NestedRels)}.

%% Mode 5: Outline format (indented text)
render_outline(Tuples) ->
    io:format("~n=== Outline View ===~n~n"),
    lists:foreach(
        fun(Tuple) ->
            render_tuple_outline(Tuple, 0)
        end,
        Tuples
    ),
    io:format("~n~b tuple(s)~n~n", [length(Tuples)]),
    ok.

render_tuple_outline(Tuple, Indent) ->
    Keys = [K || K <- maps:keys(Tuple), K =/= meta],
    IndentStr = lists:duplicate(Indent, $\s),

    io:format("~s+-- Tuple~n", [IndentStr]),
    lists:foreach(
        fun(Key) ->
            Value = maps:get(Key, Tuple),
            case is_map(Value) andalso (Key =/= meta) of
                true ->
                    io:format("~s|   ~s:~n", [IndentStr, atom_to_list(Key)]),
                    render_tuple_outline(Value, Indent + 4);
                false ->
                    io:format("~s|   ~s: ~s~n",
                             [IndentStr, atom_to_list(Key), format_value(Value)])
            end
        end,
        Keys
    ),
    io:format("~s+--~n", [IndentStr]).

%%% Helper Functions

get_all_keys(Tuples) ->
    lists:usort(
        lists:flatmap(
            fun(Tuple) ->
                [K || K <- maps:keys(Tuple), K =/= meta]
            end,
            Tuples
        )
    ).

format_value(undefined) -> "(null)";
format_value(V) when is_integer(V) -> integer_to_list(V);
format_value(V) when is_float(V) -> float_to_list(V, [{decimals, 2}]);
format_value(V) when is_atom(V) -> atom_to_list(V);
format_value(V) when is_binary(V) -> binary_to_list(V);
format_value(V) when is_list(V) -> V;
format_value(V) when is_map(V) -> "(nested)";
format_value(_) -> "(complex)".