defmodule ElementTui.Components.XYGraph do # TODO: Allow pushing extra x, y points? alias ElementTui.Element alias ElementTui.Components.XYGraph defstruct bounds: nil, data: nil, convert_xy: nil, to_label: nil def new(data) do state = %XYGraph{ data: data, convert_xy: fn xy -> xy end, to_label: fn {x, y} -> {"#{x}", "#{y}"} end } bounds(state) end def convert_xy(state, func) do %XYGraph{state | convert_xy: func} end def to_label(state, func) do %XYGraph{state | to_label: func} end def bounds(%XYGraph{data: []} = state) do %XYGraph{state | bounds: {-1, 1, -1, 1}} end def 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 def update_bounds(%XYGraph{bounds: bounds} = xygraph, func) do %XYGraph{xygraph | bounds: func.(bounds)} end def render( %XYGraph{ bounds: {xmin, xmax, ymin, ymax}, data: data, convert_xy: convert_xy, to_label: to_label }, screenx, screeny, screenw, screenh ) do # 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, 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, screenw, 1) ln = String.length(xlmax) Element.text_line(xlmax) |> ElementTui.render(screenx + screenw - ln, screeny + screenh, 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) end) end end