From e2370f2f311febef01e802a3545099c7eedbcd06 Mon Sep 17 00:00:00 2001 From: Kip Cole Date: Tue, 11 Jun 2024 09:07:44 +1000 Subject: [PATCH 1/9] Support a formatter option for Kino.DataTable.new/2 --- lib/kino/data_table.ex | 43 +++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/lib/kino/data_table.ex b/lib/kino/data_table.ex index 8c87a8b4..098a6b0d 100644 --- a/lib/kino/data_table.ex +++ b/lib/kino/data_table.ex @@ -43,14 +43,23 @@ 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 `:__column__`. Defaults to the builtin + formatter. + """ @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, &__MODULE__.value_to_string/2) {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 @@ -83,7 +92,8 @@ defmodule Kino.DataTable do """ def update(kino, tabular, opts \\ []) do {data_rows, data_columns, count, inspected} = prepare_data(tabular, opts) - Kino.Table.update(kino, {data_rows, data_columns, count, inspected}) + formatter = Keyword.get(opts, :formatter, &__MODULE__.value_to_string/2) + Kino.Table.update(kino, {data_rows, data_columns, count, inspected, formatter}) end defp prepare_data(tabular, opts) do @@ -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} @@ -174,8 +184,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), - inspected: inspected + columns: + Enum.map(data_columns, fn key -> %{key: key, label: formatter.(:__column__, key)} end), + inspected: inspected, + formatter: formatter }} end @@ -256,7 +268,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 -> + state.formatter.(column.key, Map.fetch!(record, column.key)) + end) end) total_rows = count || state.total_rows @@ -279,9 +293,10 @@ defmodule Kino.DataTable do end end - defp value_to_string(value) when is_atom(value), do: inspect(value) + @doc false + def value_to_string(_key, value) when is_atom(value), do: inspect(value) - defp value_to_string(value) when is_list(value) do + def value_to_string(_key, value) when is_list(value) do if List.ascii_printable?(value) do List.to_string(value) else @@ -289,7 +304,7 @@ defmodule Kino.DataTable do end end - defp value_to_string(value) when is_binary(value) do + def value_to_string(_key, value) when is_binary(value) do inspect_opts = Inspect.Opts.new([]) if String.printable?(value, inspect_opts.limit) do @@ -299,7 +314,7 @@ defmodule Kino.DataTable do end end - defp value_to_string(value) do + def value_to_string(_key, value) do if mod = String.Chars.impl_for(value) do mod.to_string(value) else @@ -308,7 +323,7 @@ defmodule Kino.DataTable do end @impl true - def on_update({data_rows, data_columns, count, inspected}, state) do + def on_update({data_rows, data_columns, count, inspected, formatter}, state) do {count, slicing_fun, slicing_cache} = init_slicing(data_rows, count) {:ok, @@ -318,8 +333,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), - inspected: inspected + columns: + Enum.map(data_columns, fn key -> %{key: key, label: formatter.(:__column__, key)} end), + inspected: inspected, + formatter: formatter }} end end From 2426e1e362afdfe7a13d1553baf011c5034a481b Mon Sep 17 00:00:00 2001 From: Kip Cole Date: Tue, 11 Jun 2024 17:17:53 +1000 Subject: [PATCH 2/9] Change __column__ to __header__ Make Kino.DataTable.value_to_string/2 private again Make default formatter be &value_to_string/2 (no module) --- lib/kino/data_table.ex | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/kino/data_table.ex b/lib/kino/data_table.ex index 098a6b0d..75240499 100644 --- a/lib/kino/data_table.ex +++ b/lib/kino/data_table.ex @@ -46,7 +46,7 @@ defmodule Kino.DataTable do * `: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 `:__column__`. Defaults to the builtin + the key is the special value `:__header__`. Defaults to the builtin formatter. """ @@ -54,7 +54,7 @@ defmodule Kino.DataTable do def new(tabular, opts \\ []) do name = Keyword.get(opts, :name, "Data") sorting_enabled = Keyword.get(opts, :sorting_enabled, true) - formatter = Keyword.get(opts, :formatter, &__MODULE__.value_to_string/2) + formatter = Keyword.get(opts, :formatter, &value_to_string/2) {data_rows, data_columns, count, inspected} = prepare_data(tabular, opts) Kino.Table.new( @@ -92,7 +92,7 @@ defmodule Kino.DataTable do """ def update(kino, tabular, opts \\ []) do {data_rows, data_columns, count, inspected} = prepare_data(tabular, opts) - formatter = Keyword.get(opts, :formatter, &__MODULE__.value_to_string/2) + formatter = Keyword.get(opts, :formatter, &value_to_string/2) Kino.Table.update(kino, {data_rows, data_columns, count, inspected, formatter}) end @@ -185,7 +185,7 @@ defmodule Kino.DataTable do slicing_fun: slicing_fun, slicing_cache: slicing_cache, columns: - Enum.map(data_columns, fn key -> %{key: key, label: formatter.(:__column__, key)} end), + Enum.map(data_columns, fn key -> %{key: key, label: formatter.(:__header__, key)} end), inspected: inspected, formatter: formatter }} @@ -293,10 +293,9 @@ defmodule Kino.DataTable do end end - @doc false - def value_to_string(_key, value) when is_atom(value), do: inspect(value) + defp value_to_string(_key, value) when is_atom(value), do: inspect(value) - def value_to_string(_key, 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 @@ -304,7 +303,7 @@ defmodule Kino.DataTable do end end - def value_to_string(_key, 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 @@ -314,7 +313,7 @@ defmodule Kino.DataTable do end end - def value_to_string(_key, value) do + defp value_to_string(_key, value) do if mod = String.Chars.impl_for(value) do mod.to_string(value) else @@ -334,7 +333,7 @@ defmodule Kino.DataTable do slicing_fun: slicing_fun, slicing_cache: slicing_cache, columns: - Enum.map(data_columns, fn key -> %{key: key, label: formatter.(:__column__, key)} end), + Enum.map(data_columns, fn key -> %{key: key, label: formatter.(:__header__, key)} end), inspected: inspected, formatter: formatter }} From 44f68c53c9a34c86a27938e0b42ce041d2b8aeb8 Mon Sep 17 00:00:00 2001 From: Kip Cole Date: Wed, 12 Jun 2024 06:27:16 +1000 Subject: [PATCH 3/9] Remove :formatter option for Kino.DataTable.update/2 --- lib/kino/data_table.ex | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/kino/data_table.ex b/lib/kino/data_table.ex index 098a6b0d..e93cd415 100644 --- a/lib/kino/data_table.ex +++ b/lib/kino/data_table.ex @@ -92,8 +92,7 @@ defmodule Kino.DataTable do """ def update(kino, tabular, opts \\ []) do {data_rows, data_columns, count, inspected} = prepare_data(tabular, opts) - formatter = Keyword.get(opts, :formatter, &__MODULE__.value_to_string/2) - Kino.Table.update(kino, {data_rows, data_columns, count, inspected, formatter}) + Kino.Table.update(kino, {data_rows, data_columns, count, inspected}) end defp prepare_data(tabular, opts) do @@ -323,7 +322,7 @@ defmodule Kino.DataTable do end @impl true - def on_update({data_rows, data_columns, count, inspected, formatter}, state) do + def on_update({data_rows, data_columns, count, inspected}, state) do {count, slicing_fun, slicing_cache} = init_slicing(data_rows, count) {:ok, @@ -334,9 +333,11 @@ defmodule Kino.DataTable do slicing_fun: slicing_fun, slicing_cache: slicing_cache, columns: - Enum.map(data_columns, fn key -> %{key: key, label: formatter.(:__column__, key)} end), + Enum.map(data_columns, fn key -> + %{key: key, label: state.formatter.(:__column__, key)} + end), inspected: inspected, - formatter: formatter + formatter: state.formatter }} end end From 1e9bc9d14f2654106aa61e337c2483ffc0ca78eb Mon Sep 17 00:00:00 2001 From: Kip Cole Date: Wed, 12 Jun 2024 06:27:46 +1000 Subject: [PATCH 4/9] Remove :formatter option for Kino.DataTable.update/2 --- lib/kino/data_table.ex | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/kino/data_table.ex b/lib/kino/data_table.ex index e93cd415..f74801f1 100644 --- a/lib/kino/data_table.ex +++ b/lib/kino/data_table.ex @@ -292,10 +292,9 @@ defmodule Kino.DataTable do end end - @doc false - def value_to_string(_key, value) when is_atom(value), do: inspect(value) + defp value_to_string(_key, value) when is_atom(value), do: inspect(value) - def value_to_string(_key, 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 @@ -303,7 +302,7 @@ defmodule Kino.DataTable do end end - def value_to_string(_key, 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 @@ -313,7 +312,7 @@ defmodule Kino.DataTable do end end - def value_to_string(_key, value) do + defp value_to_string(_key, value) do if mod = String.Chars.impl_for(value) do mod.to_string(value) else From 9d9257e6558438dc2e13f7627ec7062e033ad34f Mon Sep 17 00:00:00 2001 From: Kip Cole Date: Wed, 12 Jun 2024 06:54:50 +1000 Subject: [PATCH 5/9] Add test case for data table formatter --- test/kino/data_table_test.exs | 15 ++++++++++++++ .../test_modules/data_table_formatter.ex | 20 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 test/support/test_modules/data_table_formatter.ex diff --git a/test/kino/data_table_test.exs b/test/kino/data_table_test.exs index 35279d43..c480c424 100644 --- a/test/kino/data_table_test.exs +++ b/test/kino/data_table_test.exs @@ -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 + 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"}, diff --git a/test/support/test_modules/data_table_formatter.ex b/test/support/test_modules/data_table_formatter.ex new file mode 100644 index 00000000..b9fa347d --- /dev/null +++ b/test/support/test_modules/data_table_formatter.ex @@ -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 \ No newline at end of file From 40d7468c3037b23ae1ba6bdbae59c46e82de1f6f Mon Sep 17 00:00:00 2001 From: Kip Cole Date: Wed, 12 Jun 2024 06:56:34 +1000 Subject: [PATCH 6/9] Update :formatter option docs --- lib/kino/data_table.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/kino/data_table.ex b/lib/kino/data_table.ex index 51e3904f..dbf8962b 100644 --- a/lib/kino/data_table.ex +++ b/lib/kino/data_table.ex @@ -46,8 +46,9 @@ defmodule Kino.DataTable do * `: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__`. Defaults to the builtin - formatter. + 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() From 342661f40af4407254f352a0bad75ca0eb01ffee Mon Sep 17 00:00:00 2001 From: Kip Cole Date: Wed, 12 Jun 2024 17:24:07 +1000 Subject: [PATCH 7/9] Remove unused parameter key from value_to_string --- lib/kino/data_table.ex | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/kino/data_table.ex b/lib/kino/data_table.ex index dbf8962b..9c9cc4fe 100644 --- a/lib/kino/data_table.ex +++ b/lib/kino/data_table.ex @@ -295,20 +295,20 @@ defmodule Kino.DataTable do end end - defp value_to_string(key, value, nil) do - value_to_string(key, value) + defp value_to_string(_key, value, nil) do + value_to_string(value) end defp value_to_string(key, value, formatter) do case formatter.(key, value) do {:ok, string} -> string - :default -> value_to_string(key, value) + :default -> value_to_string(value) end end - defp value_to_string(_key, value) when is_atom(value), do: inspect(value) + defp value_to_string(value) when is_atom(value), do: inspect(value) - defp value_to_string(_key, value) when is_list(value) do + defp value_to_string(value) when is_list(value) do if List.ascii_printable?(value) do List.to_string(value) else @@ -316,7 +316,7 @@ defmodule Kino.DataTable do end end - defp value_to_string(_key, value) when is_binary(value) do + defp value_to_string(value) when is_binary(value) do inspect_opts = Inspect.Opts.new([]) if String.printable?(value, inspect_opts.limit) do @@ -326,7 +326,7 @@ defmodule Kino.DataTable do end end - defp value_to_string(_key, value) do + defp value_to_string(value) do if mod = String.Chars.impl_for(value) do mod.to_string(value) else From 9977c8fcea245ecd30e90724d44518c501992baf Mon Sep 17 00:00:00 2001 From: Kip Cole Date: Wed, 12 Jun 2024 17:30:32 +1000 Subject: [PATCH 8/9] Make data table formatter option test more explicit --- test/kino/data_table_test.exs | 11 +++++++--- .../test_modules/data_table_formatter.ex | 20 ------------------- 2 files changed, 8 insertions(+), 23 deletions(-) delete mode 100644 test/support/test_modules/data_table_formatter.ex diff --git a/test/kino/data_table_test.exs b/test/kino/data_table_test.exs index c480c424..ace6ff13 100644 --- a/test/kino/data_table_test.exs +++ b/test/kino/data_table_test.exs @@ -277,14 +277,19 @@ defmodule Kino.DataTableTest do test "supports a formatter option" do entries = %{x: 1..3, y: [1.1, 1.2, 1.3]} - formatter = &Kino.DataTable.Formatter.format/2 + formatter = + fn + :__header__, value -> {:ok, "h:#{value}"} + :x, value when is_integer(value) -> {:ok, "x:#{value}"} + _, _ -> :default + end 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"] | _], + columns: [%{key: "0", label: "h:x"}, %{key: "1", label: "h:y"}], + data: [["x:1", "1.1"], ["x:2", "1.2"], ["x:3", "1.3"]], total_rows: 3 } } = data diff --git a/test/support/test_modules/data_table_formatter.ex b/test/support/test_modules/data_table_formatter.ex deleted file mode 100644 index b9fa347d..00000000 --- a/test/support/test_modules/data_table_formatter.ex +++ /dev/null @@ -1,20 +0,0 @@ -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 \ No newline at end of file From 4ab66b46763084a86482a2dd3bc9d2b1014dd022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Wed, 12 Jun 2024 09:40:41 +0200 Subject: [PATCH 9/9] Update test/kino/data_table_test.exs --- test/kino/data_table_test.exs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/kino/data_table_test.exs b/test/kino/data_table_test.exs index ace6ff13..4e319035 100644 --- a/test/kino/data_table_test.exs +++ b/test/kino/data_table_test.exs @@ -277,12 +277,14 @@ defmodule Kino.DataTableTest do test "supports a formatter option" do entries = %{x: 1..3, y: [1.1, 1.2, 1.3]} + formatter = fn :__header__, value -> {:ok, "h:#{value}"} :x, value when is_integer(value) -> {:ok, "x:#{value}"} _, _ -> :default end + kino = Kino.DataTable.new(entries, keys: [:x, :y], formatter: formatter) data = connect(kino)