lib/workbench_web/live/balance_day_live/index.ex

defmodule WorkbenchWeb.BalanceDayLive.Index do
  use WorkbenchWeb, :live_view
  require Ecto.Query

  defmodule QueryParams do
    defstruct ~w(size)a

    @default_size 90

    def new(params) do
      %QueryParams{
        size: int_param(params, ["size"], @default_size)
      }
    end

    defp int_param(params, keys, default) do
      param = get_in(params, keys) || ""

      param
      |> Integer.parse()
      |> case do
        {val, _} -> val
        :error -> default
      end
    end
  end

  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  def handle_params(params, _uri, socket) do
    query_params = QueryParams.new(params)

    socket =
      socket
      |> assign(:balances, balances(size: query_params.size))

    {:noreply, socket}
  end

  def days(balances) do
    balances
    |> Enum.map(& &1.time)
    |> Enum.map(&format_timestamp/1)
  end

  def usd_min(balances) do
    balances
    |> Enum.map(& &1.min)
    |> Enum.map(&Decimal.to_float/1)
  end

  def usd_max(balances) do
    balances
    |> Enum.map(& &1.max)
    |> Enum.map(&Decimal.to_float/1)
  end

  @timestamp_format "{YYYY}-{0M}-{0D} {h12}:{m} {s}"
  def format_timestamp(timestamp) do
    timestamp
    |> Timex.format!(@timestamp_format)
  end

  defp balances(size: size) do
    Ecto.Query.from(
      b in Workbench.Balance,
      select: %{
        time: fragment("DATE_TRUNC('day', ?) AS time", b.finish_time),
        min: min(b.usd),
        max: max(b.usd)
      },
      group_by: [fragment("time")],
      order_by: [desc: fragment("time")],
      limit: ^size
    )
    |> Workbench.Repo.all()
    |> Enum.reverse()
  end
end