%%% @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 == %%% %%% ``` %%% Results = query_planner:collect(DB, Plan), %%% visualizer:render(Results, tree). %%% visualizer:render(Results, nested_table). %%% ``` %%% %%% @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)".