diff --git a/lib/kino/explorer.ex b/lib/kino/explorer.ex index 85eaddb..8c57530 100644 --- a/lib/kino/explorer.ex +++ b/lib/kino/explorer.ex @@ -58,8 +58,8 @@ defmodule Kino.Explorer do @impl true def get_data(rows_spec, state) do - {records, total_rows, summaries} = get_records(state, rows_spec) - columns = Enum.map(state.columns, &%{&1 | summary: summaries[&1.key]}) + {columns, records, total_rows, summaries} = get_records(state, rows_spec) + columns = Enum.map(columns, &%{&1 | summary: summaries[&1.key]}) data = records_to_data(columns, records) {:ok, %{columns: columns, data: {:columns, data}, total_rows: total_rows}, state} end @@ -98,21 +98,23 @@ defmodule Kino.Explorer do defp info(columns, lazy, name) do name = if lazy, do: "Lazy - #{name}", else: name has_composite_type_column? = Enum.any?(columns, &(&1.type == "list" || &1.type == "struct")) + features = [:export, :pagination, :sorting, :relocate] formats = if has_composite_type_column?, do: ["NDJSON", "Parquet"], else: ["CSV", "NDJSON", "Parquet"] - %{name: name, features: [:export, :pagination, :sorting], export: %{formats: formats}} + %{name: name, features: features, export: %{formats: formats}} end defp get_records(%{df: df, groups: groups}, rows_spec) do lazy = lazy?(df) - df = order_by(df, rows_spec[:order]) + df = df |> relocate(rows_spec[:relocates]) |> order_by(rows_spec[:order]) + columns = columns(df, lazy, groups) total_rows = if !lazy, do: DataFrame.n_rows(df) summaries = if total_rows && total_rows > 0, do: summaries(df, groups) df = DataFrame.slice(df, rows_spec.offset, rows_spec.limit) records = df |> DataFrame.collect() |> DataFrame.to_columns() - {records, total_rows, summaries} + {columns, records, total_rows, summaries} end defp order_by(df, nil), do: df @@ -121,6 +123,14 @@ defmodule Kino.Explorer do DataFrame.sort_with(df, &[{direction, &1[column]}]) end + defp relocate(df, []), do: df + + defp relocate(df, [%{from_index: from_index, to_index: to_index} | relocate]) do + position = if from_index > to_index, do: :before, else: :after + df = DataFrame.relocate(df, from_index, [{position, to_index}]) + relocate(df, relocate) + end + defp records_to_data(columns, records) do Enum.map(columns, fn column -> records |> Map.fetch!(column.key) |> Enum.map(&value_to_string(column.type, &1)) @@ -217,6 +227,6 @@ defmodule Kino.Explorer do defp build_top(top), do: to_string(top) defp df_to_export(df, rows_spec) do - df |> order_by(rows_spec[:order]) |> DataFrame.collect() + df |> relocate(rows_spec[:relocates]) |> order_by(rows_spec[:order]) |> DataFrame.collect() end end diff --git a/mix.exs b/mix.exs index 00e94fc..47a42f3 100644 --- a/mix.exs +++ b/mix.exs @@ -27,7 +27,7 @@ defmodule KinoExplorer.MixProject do defp deps do [ - {:kino, github: "/livebook-dev/kino"}, + {:kino, github: "livebook-dev/kino"}, {:explorer, "~> 0.8.1"}, {:ex_doc, "~> 0.31.0", only: :dev, runtime: false} ] diff --git a/mix.lock b/mix.lock index e70df2f..4cd942a 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", "b3f67b036ce76e81794e435259f337b364920efb", []}, + "kino": {:git, "https://github.com/livebook-dev/kino.git", "baaa84a76b0440f7a2b8c9e77f806ce161a65f71", []}, "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 c1cdece..8fc1890 100644 --- a/test/kino/explorer_test.exs +++ b/test/kino/explorer_test.exs @@ -22,7 +22,7 @@ defmodule Kino.ExplorerTest do data = connect(widget) assert %{ - features: [:export, :pagination, :sorting], + features: [:export, :pagination, :sorting, :relocate], content: %{ columns: [ %{key: "0", label: "id", type: "number"}, @@ -121,6 +121,28 @@ defmodule Kino.ExplorerTest do }) end + test "supports relocate" do + widget = Kino.Explorer.new(people_df()) + + connect(widget) + + push_event(widget, "relocate", %{"from_index" => 1, "to_index" => 0}) + + assert_broadcast_event(widget, "update_content", %{ + columns: [ + %{key: "1", label: "name", type: "text"}, + %{key: "0", label: "id", type: "number"}, + %{key: "2", label: "start", type: "date"} + ], + data: [ + ["Amy Santiago", "Jake Peralta", "Terry Jeffords"], + ["3", "1", "2"], + ["2023-12-12 12:12:12.121212", "2023-12-01 02:03:04.050607", "2023-11-11 11:11:11.111111"] + ], + relocates: [%{from_index: 1, to_index: 0}] + }) + end + test "supports data summary" do df = Explorer.DataFrame.new(%{ @@ -337,7 +359,7 @@ defmodule Kino.ExplorerTest do data = connect(widget) assert %{ - features: [:export, :pagination, :sorting], + features: [:export, :pagination, :sorting, :relocate], content: %{ columns: [ %{key: "0", label: "sepal_length", summary: nil, type: "number"}, @@ -359,7 +381,7 @@ defmodule Kino.ExplorerTest do data = connect(widget) assert %{ - features: [:export, :pagination, :sorting], + features: [:export, :pagination, :sorting, :relocate], content: %{ data: [["1", "2"], ["nx", "<<200, 210>>"]] } @@ -372,7 +394,7 @@ defmodule Kino.ExplorerTest do data = connect(widget) assert %{ - features: [:export, :pagination, :sorting], + features: [:export, :pagination, :sorting, :relocate], content: %{ total_rows: nil, columns: [ @@ -420,7 +442,7 @@ defmodule Kino.ExplorerTest do assert %{ export: %{formats: ["CSV", "NDJSON", "Parquet"]}, - features: [:export, :pagination, :sorting], + features: [:export, :pagination, :sorting, :relocate], content: %{ page: 1, max_page: 3, @@ -445,7 +467,7 @@ defmodule Kino.ExplorerTest do assert %{ export: %{formats: ["CSV", "NDJSON", "Parquet"]}, - features: [:export, :pagination, :sorting], + features: [:export, :pagination, :sorting, :relocate], content: %{ page: 1, max_page: nil, @@ -464,7 +486,7 @@ defmodule Kino.ExplorerTest do test "export to" do df = Explorer.DataFrame.new(%{n: Enum.to_list(1..25)}) - rows_spec = %{order: nil} + rows_spec = %{order: nil, relocates: []} for format <- ["CSV", "NDJSON", "Parquet"] do exported = Kino.Explorer.export_data(rows_spec, %{df: df}, format) @@ -475,7 +497,7 @@ defmodule Kino.ExplorerTest do test "export to for lazy data frames" do df = Explorer.DataFrame.new(%{n: Enum.to_list(1..25)}, lazy: true) - rows_spec = %{order: nil} + rows_spec = %{order: nil, relocates: []} for format <- ["CSV", "NDJSON", "Parquet"] do exported = Kino.Explorer.export_data(rows_spec, %{df: df}, format) @@ -486,7 +508,7 @@ defmodule Kino.ExplorerTest do test "export to for data frames with list-type columns" do df = Explorer.DataFrame.new(%{list: Explorer.Series.from_list([[1, 2], [1]])}) - rows_spec = %{order: nil} + rows_spec = %{order: nil, relocates: []} widget = Kino.Explorer.new(df) data = connect(widget)