Packages
ElementTuiComponents: TUI component companion library for ElementTui.
Current section
Files
Jump to
Current section
Files
lib/components/xygraph.ex
defmodule ElementTui.Components.XYGraph do
alias ElementTui.Element
alias ElementTui.Components.XYGraph
defstruct bounds: nil,
data: []
def new(data) do
state = %XYGraph{
data: data
}
extract_bounds(state)
end
def push(%XYGraph{data: data, bounds: nil} = state, {x, y} = xy) do
%XYGraph{state | data: [xy | data], bounds: {x, x, y, y}}
end
def push(%XYGraph{data: data, bounds: {xmin, xmax, ymin, ymax}} = state, {x, y} = xy) do
%XYGraph{
state
| data: [xy | data],
bounds: {Cmp.min(xmin, x), Cmp.max(xmax, x), Cmp.min(ymin, y), Cmp.max(ymax, y)}
}
end
@doc """
Convert the x, y values to numeric values.
This is useful for converting the x, y values to something else, For example, DateTime values need to be converted to a numeric value before plotting (e.g. seconds since epoch).
Takes a function that takes a tuple of x, y values and returns a tuple of converted x, y values.
"""
def convert_xy(state, func) do
Map.put(state, :convert_xy, func)
end
def to_label(state, func) do
Map.put(state, :to_label, func)
end
@doc """
Update/change the bounds on the graph
Accepts a function that takes the current bounds (`{xmin, xmax, ymin, ymax}`) and returns the new bounds.
"""
def update_bounds(state, func) do
%{state | bounds: func.(state.bounds)}
end
def render(
%XYGraph{
bounds: {xmin, xmax, ymin, ymax},
data: data
} = state,
screenx,
screeny,
screenw,
screenh
) do
convert_xy = Map.get(state, :convert_xy, fn xy -> xy end)
to_label = Map.get(state, :to_label, fn {x, y} -> {"#{x}", "#{y}"} end)
# x should be numeric
{xmin_f, ymin_f} = convert_xy.({xmin, ymin})
{xmax_f, ymax_f} = convert_xy.({xmax, ymax})
{dx, dy} = {xmax_f - xmin_f, ymax_f - ymin_f}
# Axis
{xlmin, ylmin} = to_label.({xmin, ymin})
{xlmax, ylmax} = to_label.({xmax, ymax})
{dx, xlmax} =
if dx == 0 do
# We have no way to know the new value, because xmax and xmax_f are not the same
{1, "N/A"}
else
{dx, xlmax}
end
{dy, ylmax} =
if dy == 0 do
{1, "N/A"}
else
{dy, ylmax}
end
margin_x = Cmp.max(String.length(ylmin), String.length(ylmax)) + 1
margin_y = 1
Element.text_line(ylmin)
|> ElementTui.render(screenx, screeny + screenh - margin_y - 1, margin_x, 1)
Element.text_line(ylmax)
|> ElementTui.render(screenx, screeny, margin_x, 1)
Element.text_line(xlmin)
|> ElementTui.render(screenx + margin_x, screeny + screenh - 1, screenw, 1)
ln = String.length(xlmax)
Element.text_line(xlmax)
|> ElementTui.render(screenx + screenw - ln, screeny + screenh - 1, ln, 1)
# Data to screen location. The -1 is for corner cases to stop them falling of the edges
# Ideally add some tests around this.
{dxscreen, dyscreen} = {1.0 * (screenw - margin_x - 1), 1.0 * (screenh - margin_y)}
# Scale to screen x, y
data
|> Enum.map(fn xy ->
{x, y} = convert_xy.(xy)
{dxscreen * (x - xmin_f) / dx, dyscreen * (y - ymin_f) / dy}
end)
|> Enum.each(fn {x, y} ->
Element.text_line("x")
|> ElementTui.render(screenx + margin_x + floor(x), screeny + ceil(dyscreen - y) - 1, 1, 1)
end)
end
defp extract_bounds(%XYGraph{data: []} = state) do
%XYGraph{state | bounds: {-1, 1, -1, 1}}
end
defp extract_bounds(%XYGraph{data: [{x, y} | xys]} = state) do
bounds =
xys
|> Enum.reduce({x, x, y, y}, fn {x, y}, {xmin, xmax, ymin, ymax} ->
{Cmp.min(xmin, x), Cmp.max(xmax, x), Cmp.min(ymin, y), Cmp.max(ymax, y)}
end)
%XYGraph{state | bounds: bounds}
end
end