defmodule KinoProgressBar do
@moduledoc """
Documentation for `KinoProgressBar`.
"""
use Kino.JS, assets_path: "lib/assets"
use Kino.JS.Live
# TODO Add rangeless option
@doc """
Create a new progress bar.
## Options:
* `range`: The range of values for the progress bar. Defaults to
`{0, 100}`.
* `current`: Current value. Defaults to 0.
* `width`: The width of the progress bar in pixels. Defaults to
600.
* `format_current`: How to display the current value. Defaults to
appending a percent symbol.
* `color`: The color scheme for the current bar. Defaults to
`:blue`. Possible values: `:red`, `:blue`, `:gray`, `:green`,
`{:color, CSS_COLOR_STRING}`.
* `throttle`: Elixir can handle 10_000 messages per second without
issue, but browsers react poorly if you try to update the DOM
10_000 times per second. So, this kino throttles the rate at
which client updates are sent to at most one every `throttle`
milliseconds. The default is 500, so at most two updates are sent
per second.
"""
def new(title, opts \\ []) do
width = Keyword.get(opts, :width, 600)
range = Keyword.get(opts, :range, {0, 100})
format_current = Keyword.get(opts, :format_current, fn x -> "#{x}%" end)
color = Keyword.get(opts, :color, :blue) |> color_to_css()
throttle = Keyword.get(opts, :throttle, 500)
current = Keyword.get(opts, :current, 0)
Kino.JS.Live.new(__MODULE__, %{
title: title,
width: width,
range: range,
current: current,
format_current: format_current,
color: color,
throttle: throttle,
last_client_update: System.monotonic_time(:millisecond)
})
end
@doc """
Update the current value of the progress bar.
This is throttled per the `throttle` parameter set when the progress
bar was created.
"""
def set_current(kino, current) do
Kino.JS.Live.cast(kino, {:set_current, current})
end
@impl true
def init(data, ctx) do
{:ok, assign(ctx, data)}
end
@impl true
def handle_connect(ctx) do
{:ok,
%{
title: ctx.assigns.title,
width: ctx.assigns.width,
current_px: current_px(ctx.assigns),
current_text: ctx.assigns.format_current.(ctx.assigns.current),
color: ctx.assigns.color
}, ctx}
end
@impl true
def handle_cast({:set_current, current}, ctx) do
ctx = assign(ctx, current: current)
now = System.monotonic_time(:millisecond)
cond do
now - ctx.assigns.last_client_update >= ctx.assigns.throttle ->
{:noreply, ctx |> broadcast_set_current()}
true ->
# Kino.JS.Live doesn't support timeouts like GenServer
:timer.send_after(
ctx.assigns.throttle - (now - ctx.assigns.last_client_update),
:broadcast_set_current
)
{:noreply, ctx}
end
end
@impl true
def handle_info(:broadcast_set_current, ctx) do
{:noreply, ctx |> broadcast_set_current()}
end
defp current_px(%{width: width, current: current, range: range}) do
floor(width * bar_fraction(current, range))
end
defp bar_fraction(x, {lower, upper}) do
x = min(max(lower, x), upper)
(x - lower) / (upper - lower)
end
defp color_to_css(:red), do: "rgb(239 68 68)"
defp color_to_css(:green), do: "rgb(34 197 94)"
defp color_to_css(:blue), do: "rgb(59 130 246)"
defp color_to_css(:gray), do: "rgb(107 114 128)"
defp color_to_css({:color, color}), do: color
defp broadcast_set_current(ctx) do
broadcast_event(ctx, "set_current", %{
current_px: current_px(ctx.assigns),
current_text: ctx.assigns.format_current.(ctx.assigns.current)
})
assign(ctx, last_client_update: System.monotonic_time(:millisecond))
end
end