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