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> -
+
-
-

- <%= @currency.name %> -

- <%= if @currency.info_type == :reference do %> -

- <%= render_price(@currency.variation_price) %> -

- <% end %> - <%= if @currency.info_type == :market do %> -

- <%= render_price(@currency.buy_price) %> - <%= render_price( - @currency.sell_price - ) %> -

+
+
+

+ <%= @currency.name %> +

+ <%= if @currency.info_type == :reference do %> +

+ <%= render_price(@currency.variation_price) %> +

+ <% end %> + <%= if @currency.info_type == :market do %> +

+ <%= render_price(@currency.buy_price) %> - <%= render_price( + @currency.sell_price + ) %> +

+

+ Spread: + + <%= render_spread(@currency) %> + +

+ <% end %>

- Spread: - - <%= render_spread(@currency) %> - + <%= render_update_time(@currency) %>

- <% end %> -

- <%= render_update_time(@currency) %> -

- + +
+
+
+

+ <%= render_variation_percent(@currency) %> +

+
+
-
-
-

- <%= render_variation_percent(@currency) %> -

+
+
+ + +
+ <%= render_chart(@socket) %>
<%!--
--%> -
- <%= render_chart(@socket) %> -
<.back navigate={~p"/currencies"}><%= gettext("Back to currencies") %> diff --git a/lib/redis/stream.ex b/lib/redis/stream.ex index 4ecd851..6ba10c6 100644 --- a/lib/redis/stream.ex +++ b/lib/redis/stream.ex @@ -82,6 +82,9 @@ defmodule Redis.Stream.Entry do end) end + @spec get_datetime(t()) :: DateTime.t() + def get_datetime(%__MODULE__{datetime: datetime}), do: datetime + @spec parse_stream_entry_id(String.t()) :: DateTime.t() defp parse_stream_entry_id(entry_id) do entry_id diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 10ae477..a7c97d7 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -80,7 +80,7 @@ msgid "Settings" msgstr "" #: lib/ex_finance_web/live/admin/currency_live/show.html.heex:25 -#: lib/ex_finance_web/live/public/currency_live/show.html.heex:68 +#: lib/ex_finance_web/live/public/currency_live/show.html.heex:85 #, elixir-autogen, elixir-format msgid "Back to currencies" msgstr "" @@ -161,3 +161,18 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Enter a Cedear price to find out if the security value is below or above the fair price." msgstr "" + +#: lib/ex_finance_web/live/public/currency_live/show.html.heex:67 +#, elixir-autogen, elixir-format +msgid "Daily" +msgstr "" + +#: lib/ex_finance_web/live/public/currency_live/show.html.heex:73 +#, elixir-autogen, elixir-format +msgid "Monthly" +msgstr "" + +#: lib/ex_finance_web/live/public/currency_live/show.html.heex:70 +#, elixir-autogen, elixir-format +msgid "Weekly" +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 1bf9b07..920f1e4 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -80,7 +80,7 @@ msgid "Settings" msgstr "" #: lib/ex_finance_web/live/admin/currency_live/show.html.heex:25 -#: lib/ex_finance_web/live/public/currency_live/show.html.heex:68 +#: lib/ex_finance_web/live/public/currency_live/show.html.heex:85 #, elixir-autogen, elixir-format msgid "Back to currencies" msgstr "" @@ -161,3 +161,18 @@ msgstr "" #, elixir-autogen, elixir-format, fuzzy msgid "Enter a Cedear price to find out if the security value is below or above the fair price." msgstr "" + +#: lib/ex_finance_web/live/public/currency_live/show.html.heex:67 +#, elixir-autogen, elixir-format +msgid "Daily" +msgstr "" + +#: lib/ex_finance_web/live/public/currency_live/show.html.heex:73 +#, elixir-autogen, elixir-format +msgid "Monthly" +msgstr "" + +#: lib/ex_finance_web/live/public/currency_live/show.html.heex:70 +#, elixir-autogen, elixir-format +msgid "Weekly" +msgstr "" diff --git a/priv/gettext/es_AR/LC_MESSAGES/default.po b/priv/gettext/es_AR/LC_MESSAGES/default.po index 301605d..b45af78 100644 --- a/priv/gettext/es_AR/LC_MESSAGES/default.po +++ b/priv/gettext/es_AR/LC_MESSAGES/default.po @@ -80,7 +80,7 @@ msgid "Settings" msgstr "Configuración" #: lib/ex_finance_web/live/admin/currency_live/show.html.heex:25 -#: lib/ex_finance_web/live/public/currency_live/show.html.heex:68 +#: lib/ex_finance_web/live/public/currency_live/show.html.heex:85 #, elixir-autogen, elixir-format msgid "Back to currencies" msgstr "Volver a cotizaciones" @@ -161,3 +161,18 @@ msgstr "Invitame un café en cafecito.app" #, elixir-autogen, elixir-format, fuzzy msgid "Enter a Cedear price to find out if the security value is below or above the fair price." msgstr "Ingresa un precio de Cedear para conocer si el valor del título se encuentra por debajo o encima del precio justo." + +#: lib/ex_finance_web/live/public/currency_live/show.html.heex:67 +#, elixir-autogen, elixir-format +msgid "Daily" +msgstr "Diario" + +#: lib/ex_finance_web/live/public/currency_live/show.html.heex:73 +#, elixir-autogen, elixir-format +msgid "Monthly" +msgstr "Mensual" + +#: lib/ex_finance_web/live/public/currency_live/show.html.heex:70 +#, elixir-autogen, elixir-format +msgid "Weekly" +msgstr "Semanal"