diff --git a/assets/js/charts/line_chart.js b/assets/js/charts/line_chart.js index e0a8eaa..d998104 100644 --- a/assets/js/charts/line_chart.js +++ b/assets/js/charts/line_chart.js @@ -45,12 +45,28 @@ export default class { this.chart = new Chart(ctx, config) } + resetDataset(label) { + const dataset = this._findDataset(label) + if (dataset) { + dataset.data = [] + } + this.chart.config.data.labels = [] + this.chart.update() + } + addPoint(data_label, label, value, backgroundColor, borderColor) { this.chart.config.data.labels.push(data_label) const dataset = this._findDataset(label) || this._createDataset( label, backgroundColor, borderColor ) dataset.data.push({x: Date.now(), y: value}) + + const numericYValues = dataset.data.map(point => parseFloat(point.y)) + const suggestedMin = Math.min(...numericYValues); + const suggestedMax = Math.max(...numericYValues); + this.chart.config.options.scales.y.suggestedMin = suggestedMin - 50 + this.chart.config.options.scales.y.suggestedMax = suggestedMax + 50 + this.chart.update() } diff --git a/assets/js/hooks/line_chart_hook.js b/assets/js/hooks/line_chart_hook.js index 6b13b50..11b2de1 100644 --- a/assets/js/hooks/line_chart_hook.js +++ b/assets/js/hooks/line_chart_hook.js @@ -7,6 +7,10 @@ export default { mounted() { this.chart = new RealtimeLineChart(this.el) + this.handleEvent('reset-dataset', ({ label }) => { + this.chart.resetDataset(label) + }) + this.handleEvent('new-point', ({ background_color, border_color, diff --git a/lib/ex_finance/currencies.ex b/lib/ex_finance/currencies.ex index 84bb00c..08367c5 100644 --- a/lib/ex_finance/currencies.ex +++ b/lib/ex_finance/currencies.ex @@ -9,8 +9,12 @@ defmodule ExFinance.Currencies do alias ExFinance.Currencies.{Currency, Supplier} alias ExFinance.Repo + alias Redis.Stream + require Logger + @type interval :: :daily | :weekly | :monthly + ## Events @doc """ @@ -370,20 +374,20 @@ defmodule ExFinance.Currencies do """ @spec fetch_currency_history(String.t(), String.t()) :: {:ok, [Redis.Stream.Entry.t()]} | :error - def fetch_currency_history(supplier_name, type) do + def fetch_currency_history(supplier_name, type, interval \\ :daily) do stream_name = get_stream_name("currency-history_" <> supplier_name <> "_" <> type) - days_before_now = -20 + before_now = look_into_the_past(-20, interval) since = DateTime.utc_now() - |> DateTime.add(days_before_now, :day) + |> DateTime.add(before_now, :day) |> DateTime.to_unix(:millisecond) with {:ok, entries} <- Redis.Client.fetch_reverse_stream_since(stream_name, since), - filtered_entries <- filter_history_entries(entries), + filtered_entries <- filter_history_entries(entries, interval), history <- map_currency_history(filtered_entries) do {:ok, history} else @@ -403,16 +407,53 @@ defmodule ExFinance.Currencies do :error end - @spec filter_history_entries([Redis.Stream.Entry.t()]) :: [ + @spec look_into_the_past(integer(), atom()) :: integer() + defp look_into_the_past(days, :daily), do: days + defp look_into_the_past(days, :monthly), do: days * 30 + defp look_into_the_past(days, :weekly), do: days * 7 + + @spec filter_history_entries([Redis.Stream.Entry.t()], atom()) :: [ Redis.Stream.Entry.t() ] - defp filter_history_entries(entries) do + defp filter_history_entries(entries, interval) do + entries + |> group_history_by(interval) + |> Enum.map(fn {_datetime, entries} -> + entries + |> Enum.sort_by( + &DateTime.to_date(Stream.Entry.get_datetime(&1)), + {:desc, Date} + ) + |> hd() + end) + |> Enum.sort_by( + &DateTime.to_date(Stream.Entry.get_datetime(&1)), + {:asc, Date} + ) + end + + @spec group_history_by([Redis.Stream.Entry.t()], interval()) :: map() + defp group_history_by(entries, :daily) do + entries + |> Enum.group_by(&DateTime.to_date(Stream.Entry.get_datetime(&1))) + end + + defp group_history_by(entries, :monthly) do + entries + |> Enum.group_by(fn %Stream.Entry{} = entry -> + Stream.Entry.get_datetime(entry) + |> DateTime.to_date() + |> Date.beginning_of_month() + end) + end + + defp group_history_by(entries, :weekly) do entries - |> Enum.group_by(fn entry -> - "#{entry.datetime.year}-#{entry.datetime.month}-#{entry.datetime.day}" + |> Enum.group_by(fn %Stream.Entry{} = entry -> + Stream.Entry.get_datetime(entry) + |> DateTime.to_date() + |> Date.beginning_of_week() end) - |> Enum.map(fn {_datetime, entries} -> hd(entries) end) - |> Enum.sort_by(& &1.datetime, :asc) end @spec map_currency_history([Redis.Stream.Entry.t()]) :: [ diff --git a/lib/ex_finance_web/live/public/currency_live/show.ex b/lib/ex_finance_web/live/public/currency_live/show.ex index 9bb8ace..77282fd 100644 --- a/lib/ex_finance_web/live/public/currency_live/show.ex +++ b/lib/ex_finance_web/live/public/currency_live/show.ex @@ -8,10 +8,22 @@ defmodule ExFinanceWeb.Public.CurrencyLive.Show do @impl true def mount(_params, _session, socket) do if connected?(socket) do - Process.send_after(self(), :update_chart, 500) + Process.send_after(self(), :update_chart, 50) end - {:ok, socket} + {:ok, + socket + |> assign_interval()} + end + + @impl true + def handle_event("interval_change", %{"interval" => interval}, socket) do + interval = String.to_existing_atom(interval) + Process.send_after(self(), :update_chart, 50) + + {:noreply, + socket + |> assign_interval(interval)} end @impl true @@ -22,7 +34,13 @@ defmodule ExFinanceWeb.Public.CurrencyLive.Show do supplier_name: supplier_name } <- socket.assigns.currency, {:ok, history} <- - Currencies.fetch_currency_history(supplier_name, type) do + Currencies.fetch_currency_history( + supplier_name, + type, + socket.assigns.interval + ) do + socket = push_event(socket, "reset-dataset", %{label: currency_name}) + socket = Enum.reduce(build_dataset(currency_name, history), socket, fn data, acc -> @@ -34,6 +52,7 @@ defmodule ExFinanceWeb.Public.CurrencyLive.Show do _error -> {:noreply, socket + |> push_event("reset-dataset", %{label: socket.assigns.currency.name}) |> push_event("new-point", %{ data_label: get_datetime_label(DateTime.utc_now()), label: socket.assigns.currency.name, @@ -167,13 +186,12 @@ defmodule ExFinanceWeb.Public.CurrencyLive.Show do defp render_chart(assigns) do ~H""" - + """ end + + @spec assign_interval(Phoenix.LiveView.Socket.t(), Currencies.interval()) :: + Phoenix.LiveView.Socket.t() + defp assign_interval(socket, interval \\ :daily), + do: assign(socket, :interval, interval) end diff --git a/lib/ex_finance_web/live/public/currency_live/show.html.heex b/lib/ex_finance_web/live/public/currency_live/show.html.heex index 4bbed79..2584bff 100644 --- a/lib/ex_finance_web/live/public/currency_live/show.html.heex +++ b/lib/ex_finance_web/live/public/currency_live/show.html.heex @@ -1,68 +1,111 @@ <.header> -
+ Spread: + + <%= render_spread(@currency) %> + +
+ <% end %>- Spread: - - <%= render_spread(@currency) %> - + <%= render_update_time(@currency) %>
- <% end %> -- <%= render_update_time(@currency) %> -
- + +