• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sgobotta / ex_finance / c067f8bda3b04742f390d3a9e1f6ee1f76f4636c-PR-19

17 Oct 2025 05:26AM UTC coverage: 52.723% (-0.4%) from 53.145%
c067f8bda3b04742f390d3a9e1f6ee1f76f4636c-PR-19

Pull #19

github

sgobotta
Add support to render multiple currencies on the same chart
Pull Request #19: Add support to render many currencies in a single chart

668 of 1267 relevant lines covered (52.72%)

22.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

23.6
//lib/ex_finance_web/live/public/currency_live/show.ex
1
defmodule ExFinanceWeb.Public.CurrencyLive.Show do
2
  use ExFinanceWeb, :live_view
3
  use ExFinanceWeb.Navigation, :action
4

5
  alias ExFinance.Currencies
6
  alias ExFinance.Currencies.Currency
7
  alias ExFinanceWeb.Utils.DatetimeUtils
8

9
  require Logger
10

11
  @impl true
12
  def mount(_params, _session, socket) do
2✔
13
    if connected?(socket) do
2✔
14
      Process.send_after(self(), :update_chart, 50)
1✔
15
    end
16

17
    {:ok,
18
     socket
19
     |> assign_currencies()
20
     |> assign_interval()}
21
  end
22

23
  @impl true
24
  def handle_event("interval_change", %{"interval" => interval}, socket) do
×
25
    interval = String.to_existing_atom(interval)
×
26
    Process.send_after(self(), :update_chart, 50)
×
27

28
    {:noreply,
29
     socket
30
     |> assign_interval(interval)}
31
  end
32

33
  @impl true
34
  def handle_info(:update_chart, socket) do
×
35
    with %Currency{
×
36
           type: type,
37
           supplier_name: supplier_name
38
         } <- socket.assigns.currency,
×
39
         {:ok, history} <-
×
40
           Currencies.fetch_currency_history(
41
             supplier_name,
42
             type,
43
             socket.assigns.interval
×
44
           ) do
45
      all_series =
×
46
        build_all_series(
47
          socket.assigns.currencies
×
48
          |> Enum.filter(&(&1.id != socket.assigns.currency.id)),
×
49
          socket.assigns.interval
×
50
        )
51

52
      dataset = build_dataset(socket.assigns.currency, history, by: :trend)
×
53

54
      socket =
×
55
        Enum.reduce(
56
          dataset ++ List.flatten(all_series),
57
          socket,
58
          fn data, acc ->
59
            push_event(acc, "reset-dataset", %{label: data.label})
×
60
          end
61
        )
62

63
      socket =
×
64
        Enum.reduce(
65
          dataset ++ List.flatten(all_series),
66
          socket,
67
          fn data, acc ->
68
            push_event(acc, "new-point", data)
×
69
          end
70
        )
71

72
      {:noreply, socket}
73
    else
74
      _error ->
75
        {:noreply,
76
         socket
77
         |> push_event("reset-dataset", %{label: socket.assigns.currency.name})
×
78
         |> push_event("new-point", %{
79
           data_label: get_datetime_label(DateTime.utc_now()),
80
           label: socket.assigns.currency.name,
×
81
           value: 0
82
         })
83
         |> put_flash(
84
           :error,
85
           gettext("There was an error loading the price chart")
×
86
         )}
87
    end
88
  end
89

90
  @impl true
91
  def handle_params(%{"id" => id}, _uri, socket) do
2✔
92
    currency = Currencies.get_currency!(id)
2✔
93

94
    {:noreply,
95
     socket
96
     |> assign(:page_title, currency.name)
2✔
97
     |> assign_header_action()
98
     |> assign(
99
       :section_title,
100
       gettext("%{cedear} price", cedear: currency.name)
2✔
101
     )
102
     |> assign(:currency, currency)}
103
  end
104

105
  # ----------------------------------------------------------------------------
106
  # Assignment functions
107
  #
108

109
  defp assign_currencies(socket) do
110
    currencies =
2✔
111
      Currencies.list_currencies()
112
      |> Enum.filter(
113
        &(&1.type in ["blue", "bna", "official", "ccl", "mep", "crypto"])
2✔
114
      )
115

116
    assign(socket, :currencies, currencies)
2✔
117
  end
118

119
  @spec assign_interval(Phoenix.LiveView.Socket.t(), Currencies.interval()) ::
120
          Phoenix.LiveView.Socket.t()
121
  defp assign_interval(socket, interval \\ :daily),
2✔
122
    do: assign(socket, :interval, interval)
2✔
123

124
  defp build_all_series(currencies, interval) do
125
    Enum.map(currencies, fn %Currency{
×
126
                              type: type,
127
                              supplier_name: supplier_name
128
                            } = currency ->
129
      with {:ok, history} <-
×
130
             Currencies.fetch_currency_history(supplier_name, type, interval) do
131
        build_dataset(currency, history, by: :type)
×
132
      end
133
    end)
134
  end
135

136
  @spec build_dataset(
137
          Currency.t(),
138
          [
139
            {NaiveDateTime.t(), Currency.t()}
140
          ],
141
          keyword()
142
        ) :: [map()]
143
  defp build_dataset(currency, currency_history, params) do
144
    dataset_trend =
×
145
      currency_history
146
      |> Enum.reduce([], fn
147
        {_ts, %Currency{info_type: :market, sell_price: price}}, acc ->
148
          acc ++ [price]
×
149

150
        {_ts, %Currency{info_type: :reference, variation_price: price}}, acc ->
151
          acc ++ [price]
×
152

153
        _other, acc ->
154
          acc
×
155
      end)
156
      |> Enum.reverse()
157
      |> get_dataset_trend
158

159
    {background_color, border_color, hover_background_color} =
×
160
      get_chart_colors(Keyword.fetch!(params, :by), currency, dataset_trend)
161

162
    currency_history
163
    |> Enum.reduce([], fn
×
164
      {datetime,
165
       %Currency{name: currency_name, info_type: :market, sell_price: price}},
166
      acc ->
167
        acc ++
×
168
          [
169
            %{
170
              data_label: get_datetime_label(datetime),
171
              label: currency_name,
172
              value: price,
173
              background_color: background_color,
174
              border_color: border_color,
175
              hover_background_color: hover_background_color,
176
              hover_border_color: hover_background_color
177
            }
178
          ]
179

180
      {datetime,
181
       %Currency{
182
         name: currency_name,
183
         info_type: :reference,
184
         variation_price: price
185
       }},
186
      acc ->
187
        acc ++
×
188
          [
189
            %{
190
              data_label: get_datetime_label(datetime),
191
              label: currency_name,
192
              value: price,
193
              background_color: background_color,
194
              border_color: border_color,
195
              hover_background_color: hover_background_color,
196
              hover_border_color: hover_background_color
197
            }
198
          ]
199

200
      _other, acc ->
201
        acc
×
202
    end)
203
  end
204

205
  defp get_dataset_trend([]), do: :bullish
×
206
  defp get_dataset_trend([_price]), do: :bullish
×
207

208
  defp get_dataset_trend([last_price, price | _rest])
×
209
       when last_price == price do
210
    :notrend
211
  end
212

213
  defp get_dataset_trend([last_price, price | _rest]) when last_price > price,
×
214
    do: :bullish
215

216
  defp get_dataset_trend(_price_history), do: :bearish
×
217

218
  defp get_chart_colors(:trend, _currency, dataset_trend) do
219
    get_colors_by_trend(get_dataset_trend(dataset_trend))
×
220
  end
221

222
  defp get_chart_colors(:type, %Currency{} = c, _dataset_trend) do
223
    get_rgb_color_by_currency_type(c)
×
224
  end
225

226
  defp get_colors_by_trend(:notrend),
227
    do:
228
      {"rgba(203, 213, 225, 1)", "rgba(100, 116, 139, 1)",
×
229
       "rgba(100, 116, 139, 1)"}
230

231
  defp get_colors_by_trend(:bullish),
232
    do:
233
      {"rgba(167, 243, 208, 1)", "rgba(16, 185, 129, 1)",
×
234
       "rgba(16, 185, 129, 1)"}
235

236
  defp get_colors_by_trend(:bearish),
237
    do:
238
      {"rgba(253, 164, 175, 1)", "rgba(244, 63, 94, 1)", "rgba(244, 63, 94, 1)"}
×
239

240
  def get_rgb_color_by_currency_type(%Currency{type: "bna"}),
241
    do:
242
      {"rgba(185, 248, 207, 0.2)", "rgba(0, 201, 81, 0.4)",
×
243
       "rgba(0, 201, 255, 1)"}
244

245
  def get_rgb_color_by_currency_type(%Currency{type: "euro"}),
246
    do:
247
      {"rgba(255, 184, 106, 0.2)", "rgba(255, 105, 0, 0.4)",
×
248
       "rgba(255, 105, 255, 1)"}
249

250
  def get_rgb_color_by_currency_type(%Currency{type: "blue"}),
251
    do:
252
      {"rgba(142, 197, 255, 0.2)", "rgba(43, 127, 255, 0.4)",
×
253
       "rgba(43, 127, 255, 1)"}
254

255
  def get_rgb_color_by_currency_type(%Currency{type: "tourist"}),
256
    do:
257
      {"rgba(255, 161, 173, 0.2)", "rgba(255, 32, 86, 0.4)",
×
258
       "rgba(255, 32, 255, 1)"}
259

260
  def get_rgb_color_by_currency_type(%Currency{type: "crypto"}),
261
    do:
262
      {"rgba(255, 210, 48, 0.2)", "rgba(253, 154, 0, 0.4)",
×
263
       "rgba(253, 154, 255, 1)"}
264

265
  def get_rgb_color_by_currency_type(%Currency{type: "ccl"}),
266
    do:
267
      {"rgba(116, 212, 255, 0.2)", "rgba(0, 166, 244, 0.4)",
×
268
       "rgba(0, 166, 255, 1)"}
269

270
  def get_rgb_color_by_currency_type(%Currency{type: "luxury"}),
271
    do:
272
      {"rgba(163, 179, 255, 0.2)", "rgba(97, 95, 255, 0.4)",
×
273
       "rgba(97, 95, 255, 1)"}
274

275
  def get_rgb_color_by_currency_type(%Currency{type: "official"}),
276
    do:
277
      {"rgba(94, 233, 181, 0.2)", "rgba(0, 188, 125, 0.4)",
×
278
       "rgba(0, 188, 255, 1)"}
279

280
  def get_rgb_color_by_currency_type(%Currency{type: "mep"}),
281
    do:
282
      {"rgba(83, 234, 253, 0.2)", "rgba(0, 184, 219, 0.4)",
×
283
       "rgba(0, 184, 255, 1)"}
284

285
  def get_rgb_color_by_currency_type(%Currency{type: "wholesaler"}),
286
    do:
287
      {"rgba(70, 236, 213, 0.2)", "rgba(0, 187, 167, 0.4)",
×
288
       "rgba(0, 187, 255, 1)"}
289

290
  def get_rgb_color_by_currency_type(%Currency{type: "future"}),
291
    do:
292
      {"rgba(196, 180, 255, 0.2)", "rgba(142, 81, 255, 0.4)",
×
293
       "rgba(142, 81, 255, 1)"}
294

295
  defp get_datetime_label(%DateTime{} = datetime),
296
    do: DatetimeUtils.human_readable_datetime(datetime, :shift_timezone)
×
297

298
  defp get_color_by_currency_type(%Currency{type: "bna"}), do: "green"
12✔
299
  defp get_color_by_currency_type(%Currency{type: "euro"}), do: "orange"
×
300
  defp get_color_by_currency_type(%Currency{type: "blue"}), do: "blue"
×
301
  defp get_color_by_currency_type(%Currency{type: "tourist"}), do: "rose"
×
302
  defp get_color_by_currency_type(%Currency{type: "crypto"}), do: "amber"
×
303
  defp get_color_by_currency_type(%Currency{type: "ccl"}), do: "sky"
×
304
  defp get_color_by_currency_type(%Currency{type: "luxury"}), do: "indigo"
×
305
  defp get_color_by_currency_type(%Currency{type: "official"}), do: "green"
×
306
  defp get_color_by_currency_type(%Currency{type: "mep"}), do: "sky"
×
307
  defp get_color_by_currency_type(%Currency{type: "wholesaler"}), do: "emerald"
×
308
  defp get_color_by_currency_type(%Currency{type: "future"}), do: "emerald"
×
309

310
  # ----------------------------------------------------------------------------
311
  # Render functions
312
  #
313

314
  defp render_variation_percent(%Currency{variation_percent: variation_percent}),
315
    do: "#{variation_percent}%"
2✔
316

317
  defp render_update_time(%Currency{price_updated_at: datetime}),
318
    do: DatetimeUtils.human_readable_datetime(datetime)
2✔
319

320
  defp render_price(price), do: "$#{price}"
2✔
321

322
  defp render_info_type(%Currency{info_type: :market}), do: "Precio de mercado"
×
323

324
  defp render_info_type(%Currency{info_type: :reference}),
2✔
325
    do: "Precio referencia"
326

327
  defp render_spread(%Currency{
328
         info_type: :market,
329
         sell_price: sell_price,
330
         buy_price: buy_price
331
       }),
332
       do: "$#{Decimal.sub(sell_price, buy_price)}"
×
333

334
  # ----------------------------------------------------------------------------
335
  # Colors functions
336
  #
337

338
  defp get_color_by_price_direction(%Currency{
×
339
         variation_percent: %Decimal{coef: 0}
340
       }),
341
       do: "gray"
342

343
  defp get_color_by_price_direction(%Currency{
×
344
         variation_percent: %Decimal{sign: -1}
345
       }),
346
       do: "red"
347

348
  defp get_color_by_price_direction(%Currency{
12✔
349
         variation_percent: %Decimal{sign: 1}
350
       }),
351
       do: "green"
352

353
  defp render_chart(assigns) do
354
    ~H"""
2✔
355
    <canvas id="chart-canvas" phx-update="ignore" phx-hook="LineChart" />
356
    """
357
  end
358

359
  defp render_header_action(assigns) do
360
    ~H"""
2✔
361
    <.navigation_back navigate={~p"/currencies"} />
2✔
362
    """
363
  end
364
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc