Packages

Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.

Current section

Files

Jump to
raxol lib raxol plugins visualization chart_renderer.ex
Raw

lib/raxol/plugins/visualization/chart_renderer.ex

defmodule Raxol.Plugins.Visualization.ChartRenderer do
@moduledoc """
Handles rendering logic for chart visualizations within the VisualizationPlugin.
"""
require Raxol.Core.Runtime.Log
alias Raxol.Plugins.Visualization.DrawingUtils
alias Raxol.Style
alias Raxol.Terminal.Cell
# Define module attributes for thresholds previously in the plugin
@max_chart_data_points 100
@doc """
Public entry point for rendering chart content.
Handles bounds checking, error handling, and calls the internal drawing logic.
Expects bounds to be a map like %{width: w, height: h}.
"""
def render_chart_content(
data,
opts,
%{width: width, height: height} = bounds,
_state
) do
title = Map.get(opts, :title, "Chart")
handle_bounds_check(width >= 5 and height >= 3, data, title, bounds)
end
# --- Private Helper Functions ---
# Pattern matching for bounds check instead of if statement
defp handle_bounds_check(false, _data, _title, bounds) do
Raxol.Core.Runtime.Log.warning_with_context(
"[ChartRenderer] Bounds too small for chart rendering: #{inspect(bounds)}",
%{}
)
DrawingUtils.draw_box_with_text("!", bounds)
end
defp handle_bounds_check(true, data, title, bounds) do
case Raxol.Core.ErrorHandling.safe_call(fn ->
# First, sample the data if it's too large
sampled_data = sample_chart_data(data)
# Log if sampling occurred
log_sampling(data, sampled_data)
# Draw the chart with sampled data
draw_tui_bar_chart(sampled_data, title, bounds)
end) do
{:ok, result} ->
result
{:error, reason} ->
Raxol.Core.Runtime.Log.error(
"[ChartRenderer] Error rendering chart: #{inspect(reason)}"
)
DrawingUtils.draw_box_with_text("[Render Error]", bounds)
end
end
# Pattern matching for chart validation instead of if statement
defp handle_chart_validation(
{true, true, true},
data,
title,
_bounds,
width,
height
) do
{max_value, min_value} = calculate_value_bounds(data)
{chart_height, chart_width} = calculate_chart_dimensions(width, height)
grid = initialize_grid(width, height, title, max_value, min_value)
_draw_chart_content(
grid,
data,
max_value,
min_value,
chart_height,
chart_width,
height,
width
)
end
defp handle_chart_validation(
_validation,
data,
_title,
bounds,
_width,
_height
) do
message = get_chart_error_message(data)
DrawingUtils.draw_box_with_text(message, bounds)
end
defp get_chart_error_message([]), do: "[No Data]"
defp get_chart_error_message(_), do: "!"
# Pattern matching for max value check instead of if statement
defp _handle_max_value_check(
true,
_value,
_max_value,
_min_value,
_chart_height
),
do: 0
defp _handle_max_value_check(false, value, max_value, min_value, chart_height) do
round(chart_height * (value - min_value) / max(1, max_value - min_value))
end
# Pattern matching for sampling decision instead of if statement
defp handle_sampling_decision(true, data, _data_length) do
# No sampling needed
data
end
defp handle_sampling_decision(false, data, data_length) do
# Calculate sampling interval
interval = ceil(data_length / @max_chart_data_points)
# Take every nth element
data
|> Enum.with_index()
|> Enum.filter(fn {_item, index} -> rem(index, interval) == 0 end)
|> Enum.map(fn {item, _index} -> item end)
end
# Pattern matching for sampling log instead of if statement
defp handle_sampling_log({true, true}, data_length, sampled_length) do
Raxol.Core.Runtime.Log.debug(
"[ChartRenderer] Data sampled for chart: #{data_length} -> #{sampled_length} points"
)
end
defp handle_sampling_log(_condition, _data_length, _sampled_length), do: :ok
# --- Private Chart Drawing Logic ---
@doc false
# Draws a simple text-based bar chart within the given bounds.
# Assumes data is a list of {label, value} tuples or maps with :label and :value keys.
defp draw_tui_bar_chart(data, title, %{width: width, height: height} = bounds) do
handle_chart_validation(
{is_list(data), width > 4, height > 4},
data,
title,
bounds,
width,
height
)
end
defp calculate_value_bounds(data) do
values =
Enum.map(data, fn
{_label, value} -> value
%{value: value} -> value
_ -> 0
end)
max_value = Enum.max_by(data, &elem(&1, 1), fn -> {nil, 0} end) |> elem(1)
min_value = Enum.min([0 | values])
{max_value, min_value}
end
defp calculate_chart_dimensions(width, height) do
chart_height = max(1, height - 2)
chart_width = max(1, width - 4)
{chart_height, chart_width}
end
defp initialize_grid(width, height, title, max_value, min_value) do
grid = List.duplicate(List.duplicate(Cell.new(" "), width), height)
grid = DrawingUtils.draw_text_centered(grid, 0, title)
grid = DrawingUtils.draw_text(grid, 1, 0, Integer.to_string(max_value))
grid =
DrawingUtils.draw_text(grid, height - 2, 0, Integer.to_string(min_value))
_draw_y_axis(grid, height)
end
defp _draw_y_axis(grid, height) do
Enum.reduce(1..(height - 2), grid, fn y, acc_grid ->
axis_style = Style.new(fg: :dark_gray)
DrawingUtils.put_cell(acc_grid, y, 3, %{Cell.new("|") | style: axis_style})
end)
end
defp _draw_chart_content(
grid,
data,
max_value,
min_value,
chart_height,
chart_width,
height,
_width
) do
num_bars = Enum.count(data)
total_bar_area_width = max(1, chart_width - (num_bars - 1))
bar_width = max(1, div(total_bar_area_width, num_bars))
spacing =
case num_bars > 1 do
true -> 1
false -> 0
end
Enum.reduce(Enum.with_index(data), {grid, 4}, fn {{label, value}, _index},
{acc_grid, current_x} ->
bar_height =
_calculate_bar_height(value, max_value, min_value, chart_height)
bar_start_y = height - 2 - bar_height
new_grid = _draw_bar(acc_grid, bar_width, bar_start_y, height, current_x)
_draw_label_and_advance(
new_grid,
label,
bar_width,
height,
current_x,
spacing
)
end)
|> elem(0)
end
defp _calculate_bar_height(value, max_value, min_value, chart_height) do
_handle_max_value_check(
max_value == 0,
value,
max_value,
min_value,
chart_height
)
end
defp _draw_bar(grid, bar_width, bar_start_y, height, current_x) do
Enum.reduce(0..(bar_width - 1), grid, fn w_offset, inner_grid ->
_draw_bar_column(inner_grid, bar_start_y, height, current_x + w_offset)
end)
end
defp _draw_bar_column(grid, bar_start_y, height, x) do
Enum.reduce(bar_start_y..(height - 2), grid, fn y, acc_grid ->
style = Style.new(bg: :blue, fg: :blue)
cell = %{Cell.new("█") | style: style}
DrawingUtils.put_cell(acc_grid, y, x, cell)
end)
end
defp _draw_label_and_advance(
grid,
label,
bar_width,
height,
current_x,
spacing
) do
label_str = _format_label(label, bar_width)
final_grid = DrawingUtils.draw_text(grid, height - 1, current_x, label_str)
next_x = current_x + bar_width + spacing
{final_grid, next_x}
end
defp _format_label(label, bar_width) do
case label do
l when is_binary(l) -> l
l -> inspect(l)
end
|> String.slice(0, bar_width)
end
# --- Private Data Handling ---
@doc false
# Sample data if it exceeds the threshold using simple interval sampling.
defp sample_chart_data(data) when is_list(data) do
data_length = length(data)
handle_sampling_decision(
data_length <= @max_chart_data_points,
data,
data_length
)
end
# Return non-lists as is
defp sample_chart_data(other), do: other
defp log_sampling(original_data, sampled_data) do
data_length =
case original_data do
data when is_list(data) -> length(data)
_ -> 0
end
sampled_length =
case sampled_data do
data when is_list(data) -> length(data)
_ -> 0
end
handle_sampling_log(
{data_length != sampled_length, data_length > 0},
data_length,
sampled_length
)
end
end