Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/upgrade to daily graphs for dollar quotes #13

Merged
merged 8 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading