Skip to content

Commit

Permalink
Merge pull request #13 from sgobotta/feature/upgrade-to-daily-graphs-…
Browse files Browse the repository at this point in the history
…for-dollar-quotes

feature/upgrade to daily graphs for dollar quotes
  • Loading branch information
sgobotta authored Mar 13, 2024
2 parents 493e64a + c09d82b commit 1c03b91
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 74 deletions.
16 changes: 16 additions & 0 deletions assets/js/charts/line_chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
4 changes: 4 additions & 0 deletions assets/js/hooks/line_chart_hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
61 changes: 51 additions & 10 deletions lib/ex_finance/currencies.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down Expand Up @@ -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
Expand All @@ -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()]) :: [
Expand Down
38 changes: 28 additions & 10 deletions lib/ex_finance_web/live/public/currency_live/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ->
Expand All @@ -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,
Expand Down Expand Up @@ -167,13 +186,12 @@ defmodule ExFinanceWeb.Public.CurrencyLive.Show do

defp render_chart(assigns) do
~H"""
<canvas
id="chart-canvas"
phx-update="ignore"
phx-hook="LineChart"
height="200"
width="300"
/>
<canvas id="chart-canvas" phx-update="ignore" phx-hook="LineChart" />
"""
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
145 changes: 94 additions & 51 deletions lib/ex_finance_web/live/public/currency_live/show.html.heex
Original file line number Diff line number Diff line change
@@ -1,68 +1,111 @@
<.header>
<div id={@currency.id} class="p-4 sm:w-1/2 lg:w-1/3 w-full cursor-default">
<div
id={@currency.id}
class="p-4 xs:w-full sm:w-2/3 md:w-2/3 lg:w-1/2 xl:w-1/3 w-full cursor-default"
>
<div
class="flex items-center justify-between p-4 rounded-lg bg-white shadow-zinc-500 shadow-md duration-500
class="p-4 rounded-lg bg-white shadow-zinc-500 shadow-md duration-500
border-zinc-300 border-[1px]"
id={"currencies-#{@currency.id}-card"}
>
<div class="text-left">
<h2 class={"text-gray-900 text-lg font-bold border-b border-#{get_color_by_currency_type(@currency)}-500"}>
<%= @currency.name %>
</h2>
<%= if @currency.info_type == :reference do %>
<h3 class={"mt-2 text-xl font-bold text-#{get_color_by_currency_type(@currency)}-500 text-left cursor-text"}>
<%= render_price(@currency.variation_price) %>
</h3>
<% end %>
<%= if @currency.info_type == :market do %>
<h3 class={"mt-2 text-xl font-bold text-#{get_color_by_currency_type(@currency)}-500 text-left cursor-text"}>
<%= render_price(@currency.buy_price) %> - <%= render_price(
@currency.sell_price
) %>
</h3>
<div class="flex items-center justify-between">
<div class="text-left">
<h2 class={"text-gray-900 text-lg font-bold border-b border-#{get_color_by_currency_type(@currency)}-500"}>
<%= @currency.name %>
</h2>
<%= if @currency.info_type == :reference do %>
<h3 class={"mt-2 text-xl font-bold text-#{get_color_by_currency_type(@currency)}-500 text-left cursor-text"}>
<%= render_price(@currency.variation_price) %>
</h3>
<% end %>
<%= if @currency.info_type == :market do %>
<h3 class={"mt-2 text-xl font-bold text-#{get_color_by_currency_type(@currency)}-500 text-left cursor-text"}>
<%= render_price(@currency.buy_price) %> - <%= render_price(
@currency.sell_price
) %>
</h3>
<p class="text-sm font-semibold text-gray-400 text-left">
Spread:
<span class={"text-#{get_color_by_currency_type(@currency)}-500"}>
<%= render_spread(@currency) %>
</span>
</p>
<% end %>
<p class="text-sm font-semibold text-gray-400 text-left">
Spread:
<span class={"text-#{get_color_by_currency_type(@currency)}-500"}>
<%= render_spread(@currency) %>
</span>
<%= render_update_time(@currency) %>
</p>
<% end %>
<p class="text-sm font-semibold text-gray-400 text-left">
<%= render_update_time(@currency) %>
</p>
<button class={"
cursor-default
text-sm mt-6 px-4 py-2
bg-#{get_color_by_currency_type(@currency)}-400 text-white
rounded-lg tracking-wider
hover:bg-#{get_color_by_currency_type(@currency)}-300
outline-none
"}>
<%= render_info_type(@currency) %>
</button>
<button class={"
cursor-default
text-sm mt-6 px-4 py-2
bg-#{get_color_by_currency_type(@currency)}-400 text-white
rounded-lg tracking-wider
hover:bg-#{get_color_by_currency_type(@currency)}-300
outline-none
"}>
<%= render_info_type(@currency) %>
</button>
</div>
<div class={"
bg-gradient-to-tr
from-#{get_color_by_price_direction(@currency)}-400 to-#{get_color_by_price_direction(@currency)}-300
w-32 h-32
rounded-full
shadow-2xl shadow-#{get_color_by_price_direction(@currency)}-400
border-#{get_color_by_price_direction(@currency)}-500 border-dashed border-2
flex justify-center items-center
"}>
<div>
<h1 class="text-white text-2xl">
<%= render_variation_percent(@currency) %>
</h1>
</div>
</div>
</div>
<div class={"
bg-gradient-to-tr
from-#{get_color_by_price_direction(@currency)}-400 to-#{get_color_by_price_direction(@currency)}-300
w-32 h-32
rounded-full
shadow-2xl shadow-#{get_color_by_price_direction(@currency)}-400
border-#{get_color_by_price_direction(@currency)}-500 border-dashed border-2
flex justify-center items-center
"}>
<div>
<h1 class="text-white text-2xl">
<%= render_variation_percent(@currency) %>
</h1>
<div class="w-full cursor-default mt-4">
<div class="inline-flex w-full justify-evenly font-normal">
<button
class={"
#{if @interval == :daily,
do: "text-base text-#{get_color_by_currency_type(@currency)}-500 font-semibold",
else: "text-sm hover:text-zinc-500"
}
"}
phx-click="interval_change"
phx-value-interval={:daily}
>
<%= gettext("Daily") %>
</button>
<button
class={"
#{if @interval == :weekly,
do: "text-base text-#{get_color_by_currency_type(@currency)}-500 font-semibold",
else: "text-sm hover:text-zinc-500"
}
"}
phx-click="interval_change"
phx-value-interval={:weekly}
>
<%= gettext("Weekly") %>
</button>
<button
class={"
#{if @interval == :monthly,
do: "text-base text-#{get_color_by_currency_type(@currency)}-500 font-semibold",
else: "text-sm hover:text-zinc-500"
}
"}
phx-click="interval_change"
phx-value-interval={:monthly}
>
<%= gettext("Monthly") %>
</button>
</div>
<%= render_chart(@socket) %>
</div>
</div>
</div>
</.header>

<%!-- <div class="h-full w-full md:w-1/2 md:h-1/2 flex justify-center row-span-2 items-center pr-6"> --%>
<div class="p-4 sm:w-1/2 lg:w-1/3 w-full cursor-default">
<%= render_chart(@socket) %>
</div>

<.back navigate={~p"/currencies"}><%= gettext("Back to currencies") %></.back>
3 changes: 3 additions & 0 deletions lib/redis/stream.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 1c03b91

Please sign in to comment.