Packages

Drive R from Elixir through a persistent Rscript backend, with an experimental embedded native backend.

Current section

Files

Jump to
rx lib rx kino.ex
Raw

lib/rx/kino.ex

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