From d733106ff3ecb5c0239544fbba81ad33e7e8857d Mon Sep 17 00:00:00 2001 From: Cristine Guadelupe Date: Thu, 9 May 2024 18:48:07 +0800 Subject: [PATCH] Supports update --- lib/kino/explorer.ex | 58 ++++++++++++++--- mix.lock | 2 +- test/kino/explorer_test.exs | 124 +++++++++++++++++++++--------------- 3 files changed, 123 insertions(+), 61 deletions(-) diff --git a/lib/kino/explorer.ex b/lib/kino/explorer.ex index 8c57530..d881a66 100644 --- a/lib/kino/explorer.ex +++ b/lib/kino/explorer.ex @@ -27,7 +27,11 @@ defmodule Kino.Explorer do @legacy_numeric_types [:float, :integer] @doc """ - Creates a new kino displaying a given data frame or series. + Creates a new kino displaying a given data frame or series as a rich table. + + ## Options + + * `:name` - The displayed name of the table. Defaults to `"DataFrame or Series"` """ @spec new(DataFrame.t() | Series.t(), keyword()) :: t() def new(data, opts \\ []) @@ -39,21 +43,37 @@ defmodule Kino.Explorer do def new(%Series{} = s, opts) do name = Keyword.get(opts, :name, "Series") - column_name = name |> String.replace(" ", "_") |> String.downcase() |> String.to_atom() - df = DataFrame.new([{column_name, s}]) - Kino.Table.new(__MODULE__, {df, name}, export: fn state -> {"text", inspect(state.df[0])} end) + Kino.Table.new(__MODULE__, {s, name}, export: fn state -> {"text", inspect(state.df[0])} end) end + @doc """ + Updates the table to display a new data frame. + + ## Examples + + df = Explorer.Datasets.iris() + kino = Kino.Explorer.new(data) + + Once created, you can update the table to display new data: + + new_df = Explorer.Datasets.fossil_fuels() + Kino.Explorer.update(kino, new_df) + """ + def update(kino, df), do: Kino.Table.update(kino, df) + @impl true def init({df, name}) do - lazy = lazy?(df) - groups = df.groups - df = DataFrame.ungroup(df) - total_rows = if !lazy, do: DataFrame.n_rows(df) - columns = columns(df, lazy, groups) + {lazy, groups, df, total_rows, columns} = prepare_data(df, name) info = info(columns, lazy, name) - {:ok, info, %{df: df, total_rows: total_rows, columns: columns, groups: groups}} + {:ok, info, %{df: df, total_rows: total_rows, columns: columns, groups: groups, name: name}} + end + + @impl true + def on_update(df, state) do + {_lazy, groups, df, total_rows, columns} = prepare_data(df, state.name) + + {:ok, %{state | df: df, total_rows: total_rows, columns: columns, groups: groups}} end @impl true @@ -229,4 +249,22 @@ defmodule Kino.Explorer do defp df_to_export(df, rows_spec) do df |> relocate(rows_spec[:relocates]) |> order_by(rows_spec[:order]) |> DataFrame.collect() end + + defp prepare_data(%DataFrame{} = df, _name), do: prepare_data(df) + + defp prepare_data(%Series{} = s, name) do + column_name = name |> String.replace(" ", "_") |> String.downcase() |> String.to_atom() + df = DataFrame.new([{column_name, s}]) + prepare_data(df) + end + + defp prepare_data(df) do + lazy = lazy?(df) + groups = df.groups + df = DataFrame.ungroup(df) + total_rows = if !lazy, do: DataFrame.n_rows(df) + columns = columns(df, lazy, groups) + + {lazy, groups, df, total_rows, columns} + end end diff --git a/mix.lock b/mix.lock index 4cd942a..419354f 100644 --- a/mix.lock +++ b/mix.lock @@ -5,7 +5,7 @@ "ex_doc": {:hex, :ex_doc, "0.31.0", "06eb1dfd787445d9cab9a45088405593dd3bb7fe99e097eaa71f37ba80c7a676", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5350cafa6b7f77bdd107aa2199fe277acf29d739aba5aee7e865fc680c62a110"}, "explorer": {:hex, :explorer, "0.8.1", "0b7300030407a1d9c90096205395f112daa078148f9fc0b5616df30469b4e080", [:mix], [{:adbc, "~> 0.1", [hex: :adbc, repo: "hexpm", optional: true]}, {:aws_signature, "~> 0.3", [hex: :aws_signature, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:fss, "~> 0.1", [hex: :fss, repo: "hexpm", optional: false]}, {:nx, "~> 0.4", [hex: :nx, repo: "hexpm", optional: true]}, {:rustler, "~> 0.31.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}, {:table, "~> 0.1.2", [hex: :table, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "4982756e2d1a1ee52a4acc913800a522b7b14ccb43801ed05d839e531724b798"}, "fss": {:hex, :fss, "0.1.1", "9db2344dbbb5d555ce442ac7c2f82dd975b605b50d169314a20f08ed21e08642", [:mix], [], "hexpm", "78ad5955c7919c3764065b21144913df7515d52e228c09427a004afe9c1a16b0"}, - "kino": {:git, "https://github.com/livebook-dev/kino.git", "baaa84a76b0440f7a2b8c9e77f806ce161a65f71", []}, + "kino": {:git, "https://github.com/livebook-dev/kino.git", "5eb60c207e19bbdf5fcb9dc04b9c5c2a22d15bed", []}, "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, diff --git a/test/kino/explorer_test.exs b/test/kino/explorer_test.exs index 8fc1890..12c2d7b 100644 --- a/test/kino/explorer_test.exs +++ b/test/kino/explorer_test.exs @@ -18,8 +18,8 @@ defmodule Kino.ExplorerTest do end test "column definitions include type" do - widget = Kino.Explorer.new(people_df()) - data = connect(widget) + kino = Kino.Explorer.new(people_df()) + data = connect(kino) assert %{ features: [:export, :pagination, :sorting, :relocate], @@ -34,8 +34,8 @@ defmodule Kino.ExplorerTest do end test "rows order matches the given data frame by default" do - widget = Kino.Explorer.new(people_df()) - data = connect(widget) + kino = Kino.Explorer.new(people_df()) + data = connect(kino) assert %{ content: %{ @@ -54,13 +54,13 @@ defmodule Kino.ExplorerTest do end test "supports sorting by other columns" do - widget = Kino.Explorer.new(people_df()) + kino = Kino.Explorer.new(people_df()) - connect(widget) + connect(kino) - push_event(widget, "order_by", %{"key" => "1", "direction" => "desc"}) + push_event(kino, "order_by", %{"key" => "1", "direction" => "desc"}) - assert_broadcast_event(widget, "update_content", %{ + assert_broadcast_event(kino, "update_content", %{ columns: [ %{key: "0", label: "id", type: "number"}, %{key: "1", label: "name", type: "text"}, @@ -78,8 +78,8 @@ defmodule Kino.ExplorerTest do test "supports pagination" do df = Explorer.DataFrame.new(%{n: Enum.to_list(1..25)}) - widget = Kino.Explorer.new(df) - data = connect(widget) + kino = Kino.Explorer.new(df) + data = connect(kino) assert %{ content: %{ @@ -89,9 +89,9 @@ defmodule Kino.ExplorerTest do } } = data - push_event(widget, "show_page", %{"page" => 2}) + push_event(kino, "show_page", %{"page" => 2}) - assert_broadcast_event(widget, "update_content", %{ + assert_broadcast_event(kino, "update_content", %{ page: 2, max_page: 3, data: [["11", "12", "13", "14", "15", "16", "17", "18", "19", "20"]] @@ -101,8 +101,8 @@ defmodule Kino.ExplorerTest do test "supports pagination limit" do df = Explorer.DataFrame.new(%{n: Enum.to_list(1..25)}) - widget = Kino.Explorer.new(df) - data = connect(widget) + kino = Kino.Explorer.new(df) + data = connect(kino) assert %{ content: %{ @@ -112,9 +112,9 @@ defmodule Kino.ExplorerTest do } } = data - push_event(widget, "limit", %{"limit" => 15}) + push_event(kino, "limit", %{"limit" => 15}) - assert_broadcast_event(widget, "update_content", %{ + assert_broadcast_event(kino, "update_content", %{ page: 1, max_page: 2, data: [["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"]] @@ -122,13 +122,13 @@ defmodule Kino.ExplorerTest do end test "supports relocate" do - widget = Kino.Explorer.new(people_df()) + kino = Kino.Explorer.new(people_df()) - connect(widget) + connect(kino) - push_event(widget, "relocate", %{"from_index" => 1, "to_index" => 0}) + push_event(kino, "relocate", %{"from_index" => 1, "to_index" => 0}) - assert_broadcast_event(widget, "update_content", %{ + assert_broadcast_event(kino, "update_content", %{ columns: [ %{key: "1", label: "name", type: "text"}, %{key: "0", label: "id", type: "number"}, @@ -151,8 +151,8 @@ defmodule Kino.ExplorerTest do woman: [true, false, false, false, nil, nil, nil] }) - widget = Kino.Explorer.new(df) - data = connect(widget) + kino = Kino.Explorer.new(df) + data = connect(kino) assert %{ content: %{ @@ -193,8 +193,8 @@ defmodule Kino.ExplorerTest do test "support data summary for all nils" do df = Explorer.DataFrame.new(%{id: [nil, nil, nil, nil]}) - widget = Kino.Explorer.new(df) - data = connect(widget) + kino = Kino.Explorer.new(df) + data = connect(kino) assert %{ content: %{ @@ -213,8 +213,8 @@ defmodule Kino.ExplorerTest do test "support data summary for lists" do df = Explorer.DataFrame.new(%{list: Explorer.Series.from_list([[1, 2], [1]])}) - widget = Kino.Explorer.new(df) - data = connect(widget) + kino = Kino.Explorer.new(df) + data = connect(kino) assert %{ content: %{ @@ -236,8 +236,8 @@ defmodule Kino.ExplorerTest do test "support data summary for lists with nil" do df = Explorer.DataFrame.new(%{list: Explorer.Series.from_list([[1, 2], [1], nil])}) - widget = Kino.Explorer.new(df) - data = connect(widget) + kino = Kino.Explorer.new(df) + data = connect(kino) assert %{ content: %{ @@ -259,8 +259,8 @@ defmodule Kino.ExplorerTest do test "does not break on lists with internal nulls" do df = Explorer.DataFrame.new(%{list: Explorer.Series.from_list([[1, 2], [1, nil]])}) - widget = Kino.Explorer.new(df) - data = connect(widget) + kino = Kino.Explorer.new(df) + data = connect(kino) assert %{ content: %{ @@ -279,8 +279,8 @@ defmodule Kino.ExplorerTest do }) |> Explorer.DataFrame.group_by(:name) - widget = Kino.Explorer.new(df) - data = connect(widget) + kino = Kino.Explorer.new(df) + data = connect(kino) assert %{ content: %{ @@ -310,8 +310,8 @@ defmodule Kino.ExplorerTest do test "supports infinity" do df = Explorer.DataFrame.new(a: [:infinity]) - widget = Kino.Explorer.new(df) - data = connect(widget) + kino = Kino.Explorer.new(df) + data = connect(kino) assert %{ content: %{ @@ -343,8 +343,8 @@ defmodule Kino.ExplorerTest do dtypes: [d: :binary] ) - widget = Kino.Explorer.new(df) - data = connect(widget) + kino = Kino.Explorer.new(df) + data = connect(kino) types = ["text", "number", "uri", "binary", "list"] assert get_in(data.content.columns, [Access.all(), :type]) == types @@ -355,8 +355,8 @@ defmodule Kino.ExplorerTest do Explorer.Datasets.iris() |> Explorer.DataFrame.filter_with(&Explorer.Series.equal(&1["sepal_length"], 3)) - widget = Kino.Explorer.new(df) - data = connect(widget) + kino = Kino.Explorer.new(df) + data = connect(kino) assert %{ features: [:export, :pagination, :sorting, :relocate], @@ -377,8 +377,8 @@ defmodule Kino.ExplorerTest do df = Explorer.DataFrame.new([x: [1, 2], y: [<<110, 120>>, <<200, 210>>]], dtypes: [y: :binary]) - widget = Kino.Explorer.new(df) - data = connect(widget) + kino = Kino.Explorer.new(df) + data = connect(kino) assert %{ features: [:export, :pagination, :sorting, :relocate], @@ -390,8 +390,8 @@ defmodule Kino.ExplorerTest do test "supports lazy data frames" do df = Explorer.Datasets.iris() |> Explorer.DataFrame.lazy() - widget = Kino.Explorer.new(df) - data = connect(widget) + kino = Kino.Explorer.new(df) + data = connect(kino) assert %{ features: [:export, :pagination, :sorting, :relocate], @@ -437,8 +437,8 @@ defmodule Kino.ExplorerTest do test "supports export" do df = Explorer.DataFrame.new(%{n: Enum.to_list(1..25)}) - widget = Kino.Explorer.new(df) - data = connect(widget) + kino = Kino.Explorer.new(df) + data = connect(kino) assert %{ export: %{formats: ["CSV", "NDJSON", "Parquet"]}, @@ -452,7 +452,7 @@ defmodule Kino.ExplorerTest do for format <- ["CSV", "NDJSON", "Parquet"] do extension = ".#{String.downcase(format)}" - push_event(widget, "download", %{"format" => format}) + push_event(kino, "download", %{"format" => format}) assert_receive({:event, "download_content", {:binary, exported, data}, _}) assert %{format: ^extension} = exported assert is_binary(data) @@ -462,8 +462,8 @@ defmodule Kino.ExplorerTest do test "supports export for lazy data frames" do df = Explorer.DataFrame.new(%{n: Enum.to_list(1..25)}, lazy: true) - widget = Kino.Explorer.new(df) - data = connect(widget) + kino = Kino.Explorer.new(df) + data = connect(kino) assert %{ export: %{formats: ["CSV", "NDJSON", "Parquet"]}, @@ -477,7 +477,7 @@ defmodule Kino.ExplorerTest do for format <- ["CSV", "NDJSON", "Parquet"] do extension = ".#{String.downcase(format)}" - push_event(widget, "download", %{"format" => format}) + push_event(kino, "download", %{"format" => format}) assert_receive({:event, "download_content", {:binary, exported, data}, _}) assert %{format: ^extension} = exported assert is_binary(data) @@ -510,8 +510,8 @@ defmodule Kino.ExplorerTest do df = Explorer.DataFrame.new(%{list: Explorer.Series.from_list([[1, 2], [1]])}) rows_spec = %{order: nil, relocates: []} - widget = Kino.Explorer.new(df) - data = connect(widget) + kino = Kino.Explorer.new(df) + data = connect(kino) assert %{export: %{formats: ["NDJSON", "Parquet"]}} = data @@ -521,4 +521,28 @@ defmodule Kino.ExplorerTest do assert {:ok, %{extension: ^extension}} = exported end end + + test "supports update" do + df = Explorer.DataFrame.new(%{n: Enum.to_list(1..25)}) + + kino = Kino.Explorer.new(df) + data = connect(kino) + + assert %{ + content: %{ + page: 1, + max_page: 3, + data: [["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]] + } + } = data + + new_df = Explorer.DataFrame.new(%{n: Enum.to_list(25..50)}) + Kino.Explorer.update(kino, new_df) + + assert_broadcast_event(kino, "update_content", %{ + page: 1, + max_page: 3, + data: [["25", "26", "27", "28", "29", "30", "31", "32", "33", "34"]] + }) + end end