Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a :formatter option to Kino.DataTable.new/2 #441

Merged
merged 10 commits into from
Jun 12, 2024
50 changes: 40 additions & 10 deletions lib/kino/data_table.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,24 @@ defmodule Kino.DataTable do
data. Sorting requires traversal of the whole enumerable, so it
may not be desirable for large lazy enumerables. Defaults to `true`

* `:formatter` - a 2-arity function that is used to format the data
in the table. The first parameter passed is the `key` (column name) and
the second is the value to be formatted. When formatting column headings
the key is the special value `:__header__`. The formatter function must
return either `{:ok, string}` or `:default`. When the return value is
`:default` the default data table formatting is applied.

"""
@spec new(Table.Reader.t(), keyword()) :: t()
def new(tabular, opts \\ []) do
name = Keyword.get(opts, :name, "Data")
sorting_enabled = Keyword.get(opts, :sorting_enabled, true)
formatter = Keyword.get(opts, :formatter)
{data_rows, data_columns, count, inspected} = prepare_data(tabular, opts)

Kino.Table.new(__MODULE__, {data_rows, data_columns, count, name, sorting_enabled, inspected},
Kino.Table.new(
__MODULE__,
{data_rows, data_columns, count, name, sorting_enabled, inspected, formatter},
export: fn state -> {"text", state.inspected} end
)
end
Expand Down Expand Up @@ -162,7 +172,7 @@ defmodule Kino.DataTable do
end

@impl true
def init({data_rows, data_columns, count, name, sorting_enabled, inspected}) do
def init({data_rows, data_columns, count, name, sorting_enabled, inspected, formatter}) do
features = Kino.Utils.truthy_keys(pagination: true, sorting: sorting_enabled)
info = %{name: name, features: features}

Expand All @@ -174,8 +184,12 @@ defmodule Kino.DataTable do
total_rows: count,
slicing_fun: slicing_fun,
slicing_cache: slicing_cache,
columns: Enum.map(data_columns, fn key -> %{key: key, label: value_to_string(key)} end),
inspected: inspected
columns:
Enum.map(data_columns, fn key ->
%{key: key, label: value_to_string(:__header__, key, formatter)}
end),
inspected: inspected,
formatter: formatter
}}
end

Expand Down Expand Up @@ -256,7 +270,9 @@ defmodule Kino.DataTable do

data =
Enum.map(records, fn record ->
Enum.map(state.columns, &(Map.fetch!(record, &1.key) |> value_to_string()))
Enum.map(state.columns, fn column ->
value_to_string(column.key, Map.fetch!(record, column.key), state.formatter)
end)
end)

total_rows = count || state.total_rows
Expand All @@ -279,17 +295,28 @@ defmodule Kino.DataTable do
end
end

defp value_to_string(value) when is_atom(value), do: inspect(value)
defp value_to_string(key, value, nil) do
value_to_string(key, value)
end

defp value_to_string(key, value, formatter) do
case formatter.(key, value) do
{:ok, string} -> string
:default -> value_to_string(key, value)
end
end

defp value_to_string(_key, value) when is_atom(value), do: inspect(value)
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved

defp value_to_string(value) when is_list(value) do
defp value_to_string(_key, value) when is_list(value) do
if List.ascii_printable?(value) do
List.to_string(value)
else
inspect(value)
end
end

defp value_to_string(value) when is_binary(value) do
defp value_to_string(_key, value) when is_binary(value) do
inspect_opts = Inspect.Opts.new([])

if String.printable?(value, inspect_opts.limit) do
Expand All @@ -299,7 +326,7 @@ defmodule Kino.DataTable do
end
end

defp value_to_string(value) do
defp value_to_string(_key, value) do
if mod = String.Chars.impl_for(value) do
mod.to_string(value)
else
Expand All @@ -318,7 +345,10 @@ defmodule Kino.DataTable do
total_rows: count,
slicing_fun: slicing_fun,
slicing_cache: slicing_cache,
columns: Enum.map(data_columns, fn key -> %{key: key, label: value_to_string(key)} end),
columns:
Enum.map(data_columns, fn key ->
%{key: key, label: value_to_string(:__header__, key, state.formatter)}
end),
inspected: inspected
}}
end
Expand Down
15 changes: 15 additions & 0 deletions test/kino/data_table_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,21 @@ defmodule Kino.DataTableTest do
})
end

test "supports a formatter option" do
entries = %{x: 1..3, y: [1.1, 1.2, 1.3]}
formatter = &Kino.DataTable.Formatter.format/2
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
kino = Kino.DataTable.new(entries, keys: [:x, :y], formatter: formatter)
data = connect(kino)

assert %{
content: %{
columns: [%{key: "0", label: "X"}, %{key: "1", label: "Y"}],
data: [["__1__", "1.1"], ["__2__", "1.2"], ["__3__", "1.3"] | _],
total_rows: 3
}
} = data
end

test "supports data update" do
entries = [
%User{id: 1, name: "Sherlock Holmes"},
Expand Down
20 changes: 20 additions & 0 deletions test/support/test_modules/data_table_formatter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule Kino.DataTable.Formatter do
def format(:__header__, value) do
string =
value
|> to_string()
|> String.capitalize()
|> String.replace("_", " ")

{:ok, string}
end

def format(_key, value) when is_integer(value) do
{:ok, "__#{value}__"}
end

def format(_key, _value) do
:default
end

end
Loading