defmodule Rx.Kino do @compile {:no_warn_undefined, {Kino.Image, :new, 2}} @compile {:no_warn_undefined, {Kino.Layout, :grid, 2}} @moduledoc """ Optional helpers for rendering captured Rx plots with Kino. Requires the optional `{:kino, "~> 0.19.0"}` dependency. This module does not depend on Livebook directly; it only calls Kino rendering APIs when available. """ @doc """ Renders one captured `%Rx.Plot{}` as a Kino image. """ def image(%Rx.Plot{format: :png, data: data}) when is_binary(data) do ensure_kino!() kino_adapter().image(data, :png) end def image(%Rx.Plot{} = plot) do raise ArgumentError, "Rx.Kino.image/1 supports only PNG plots, got: #{inspect(plot.format)}" end def image(_other) do raise ArgumentError, "Rx.Kino.image/1 expects a %Rx.Plot{}" end @doc """ Renders captured plots as one image or a Kino grid. A single plot returns a single `Kino.Image`. Multiple plots return `Kino.Layout.grid/2` with `columns: 2` by default. """ def plots(plots, opts \\ []) def plots([%Rx.Plot{} = plot], opts) when is_list(opts) do validate_plots_opts!(opts) image(plot) end def plots(plots, opts) when is_list(plots) and is_list(opts) do opts = validate_plots_opts!(opts) ensure_kino!() images = Enum.map(plots, &image/1) kino_adapter().grid(images, columns: opts[:columns]) end def plots(%Rx.PlotResult{plots: plots}, opts) when is_list(opts) do plots(plots, opts) end def plots(_other, _opts) do raise ArgumentError, "Rx.Kino.plots/2 expects a list of %Rx.Plot{} structs" end @doc """ Captures R plots with `Rx.plot/3` and renders them with Kino. The `:columns` option controls the Kino grid and is not passed to `Rx.plot/3`. """ def plot(source, globals \\ %{}, opts \\ []) def plot(source, opts, []) when is_binary(source) and is_list(opts) do plot(source, %{}, opts) end def plot(source, globals, opts) when is_binary(source) and is_map(globals) and is_list(opts) do {columns, plot_opts} = Keyword.pop(opts, :columns, 2) validate_columns!(columns, "Rx.Kino.plot/3") ensure_kino!() source |> Rx.plot(globals, plot_opts) |> plots(columns: columns) end def plot(source, globals, opts) when is_binary(source) and is_list(opts) do raise ArgumentError, "globals must be a map with string keys, got: #{inspect(globals)}" end defp validate_plots_opts!(opts) do opts = Keyword.validate!(opts, columns: 2) columns = opts[:columns] validate_columns!(columns, "Rx.Kino.plots/2") opts end defp validate_columns!(columns, function) do unless is_integer(columns) and columns > 0 do raise ArgumentError, "#{function} option :columns must be a positive integer" end end defp ensure_kino! do case Application.get_env(:rx, :kino_adapter) do nil -> unless Code.ensure_loaded?(Kino.Image) and Code.ensure_loaded?(Kino.Layout) do raise RuntimeError, "Kino is not available. Add `{:kino, \"~> 0.19.0\"}` to your deps." end adapter -> if function_exported?(adapter, :ensure_available!, 0) do adapter.ensure_available!() else :ok end end end defp kino_adapter do Application.get_env(:rx, :kino_adapter, Rx.Kino.Adapter) end end defmodule Rx.Kino.Adapter do @compile {:no_warn_undefined, {Kino.Image, :new, 2}} @compile {:no_warn_undefined, {Kino.Layout, :grid, 2}} @moduledoc false def image(data, type), do: Kino.Image.new(data, type) def grid(terms, opts), do: Kino.Layout.grid(terms, opts) end