mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-06 03:34:57 +08:00
Set up Vega-Lite plots rendering (#287)
* Set up Vega-Lite plots rendering * Automatically recognise VegaLite specification * Improve matching VegaLite result * Update naming * StringFormatter -> DefaultFormatter
This commit is contained in:
parent
1a1057153e
commit
7804ff1d82
9 changed files with 3169 additions and 4668 deletions
|
@ -19,6 +19,7 @@ import ScrollOnUpdate from "./scroll_on_update";
|
|||
import VirtualizedLines from "./virtualized_lines";
|
||||
import Menu from "./menu";
|
||||
import UserForm from "./user_form";
|
||||
import VegaLite from "./vega_lite";
|
||||
import morphdomCallbacks from "./morphdom_callbacks";
|
||||
import { loadUserData } from "./lib/user";
|
||||
|
||||
|
@ -31,6 +32,7 @@ const hooks = {
|
|||
VirtualizedLines,
|
||||
Menu,
|
||||
UserForm,
|
||||
VegaLite,
|
||||
};
|
||||
|
||||
const csrfToken = document
|
||||
|
|
42
assets/js/vega_lite/index.js
Normal file
42
assets/js/vega_lite/index.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import vegaEmbed from "vega-embed";
|
||||
import { getAttributeOrThrow } from "../lib/attribute";
|
||||
|
||||
/**
|
||||
* A hook used to render graphics according to the given
|
||||
* Vega-Lite specification.
|
||||
*
|
||||
* The hook expects a `vega_lite:<id>` event with `{ spec }` payload,
|
||||
* where `spec` is the graphic definition as an object.
|
||||
*
|
||||
* Configuration:
|
||||
*
|
||||
* * `data-id` - plot id
|
||||
*
|
||||
*/
|
||||
const VegaLite = {
|
||||
mounted() {
|
||||
this.props = getProps(this);
|
||||
this.state = {
|
||||
container: null,
|
||||
};
|
||||
|
||||
this.state.container = document.createElement("div");
|
||||
this.el.appendChild(this.state.container);
|
||||
|
||||
this.handleEvent(`vega_lite:${this.props.id}`, ({ spec }) => {
|
||||
vegaEmbed(this.state.container, spec, {});
|
||||
});
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.props = getProps(this);
|
||||
},
|
||||
};
|
||||
|
||||
function getProps(hook) {
|
||||
return {
|
||||
id: getAttributeOrThrow(hook.el, "data-id"),
|
||||
};
|
||||
}
|
||||
|
||||
export default VegaLite;
|
7730
assets/package-lock.json
generated
7730
assets/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -25,7 +25,10 @@
|
|||
"remixicon": "^2.5.0",
|
||||
"scroll-into-view-if-needed": "^2.2.28",
|
||||
"tailwindcss": "^2.1.1",
|
||||
"topbar": "^1.0.1"
|
||||
"topbar": "^1.0.1",
|
||||
"vega": "^5.20.2",
|
||||
"vega-embed": "^6.18.1",
|
||||
"vega-lite": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.0",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Livebook.Evaluator.StringFormatter do
|
||||
defmodule Livebook.Evaluator.DefaultFormatter do
|
||||
@moduledoc false
|
||||
|
||||
# The formatter used by Livebook for rendering the results.
|
||||
|
@ -18,9 +18,17 @@ defmodule Livebook.Evaluator.StringFormatter do
|
|||
{:inspect, inspected}
|
||||
end
|
||||
|
||||
@compile {:no_warn_undefined, {VegaLite, :to_spec, 1}}
|
||||
|
||||
def format({:ok, value}) do
|
||||
inspected = inspect(value, inspect_opts())
|
||||
{:inspect, inspected}
|
||||
cond do
|
||||
is_struct(value, VegaLite) and function_exported?(VegaLite, :to_spec, 1) ->
|
||||
{:vega_lite_spec, VegaLite.to_spec(value)}
|
||||
|
||||
true ->
|
||||
inspected = inspect(value, inspect_opts())
|
||||
{:inspect, inspected}
|
||||
end
|
||||
end
|
||||
|
||||
def format({:error, kind, error, stacktrace}) do
|
|
@ -20,7 +20,7 @@ defmodule Livebook.Runtime.ErlDist do
|
|||
@required_modules [
|
||||
Livebook.Evaluator,
|
||||
Livebook.Evaluator.IOProxy,
|
||||
Livebook.Evaluator.StringFormatter,
|
||||
Livebook.Evaluator.DefaultFormatter,
|
||||
Livebook.Completion,
|
||||
Livebook.Runtime.ErlDist,
|
||||
Livebook.Runtime.ErlDist.Manager,
|
||||
|
|
|
@ -24,7 +24,7 @@ defmodule Livebook.Runtime.ErlDist.EvaluatorSupervisor do
|
|||
def start_evaluator(supervisor) do
|
||||
case DynamicSupervisor.start_child(
|
||||
supervisor,
|
||||
{Evaluator, [formatter: Evaluator.StringFormatter]}
|
||||
{Evaluator, [formatter: Evaluator.DefaultFormatter]}
|
||||
) do
|
||||
{:ok, pid} -> {:ok, pid}
|
||||
{:ok, pid, _} -> {:ok, pid}
|
||||
|
|
|
@ -138,7 +138,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
|
||||
<%= if @cell_view.outputs != [] do %>
|
||||
<div class="mt-2">
|
||||
<%= render_outputs(assigns) %>
|
||||
<%= render_outputs(assigns, @socket) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
@ -223,19 +223,19 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
"""
|
||||
end
|
||||
|
||||
defp render_outputs(assigns) do
|
||||
defp render_outputs(assigns, socket) do
|
||||
~L"""
|
||||
<div class="flex flex-col rounded-lg border border-gray-200 divide-y divide-gray-200 font-editor">
|
||||
<%= for {output, index} <- @cell_view.outputs |> Enum.reverse() |> Enum.with_index(), output != :ignored do %>
|
||||
<div class="p-4">
|
||||
<%= render_output(output, "#{@cell_view.id}-output#{index}") %>
|
||||
<div class="p-4 max-w-full overflow-y-auto tiny-scrollbar">
|
||||
<%= render_output(socket, output, "#{@cell_view.id}-output#{index}") %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_output(output, id) when is_binary(output) do
|
||||
defp render_output(_socket, output, id) when is_binary(output) do
|
||||
# Captured output usually has a trailing newline that we can ignore,
|
||||
# because each line is itself a block anyway.
|
||||
output = String.replace_suffix(output, "\n", "")
|
||||
|
@ -252,7 +252,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
"""
|
||||
end
|
||||
|
||||
defp render_output({:inspect, inspected}, id) do
|
||||
defp render_output(_socket, {:inspect, inspected}, id) do
|
||||
lines = ansi_to_html_lines(inspected)
|
||||
assigns = %{lines: lines, id: id}
|
||||
|
||||
|
@ -266,7 +266,11 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
"""
|
||||
end
|
||||
|
||||
defp render_output({:error, formatted}, _id) do
|
||||
defp render_output(socket, {:vega_lite_spec, spec}, id) do
|
||||
live_component(socket, LivebookWeb.SessionLive.VegaLiteComponent, id: id, spec: spec)
|
||||
end
|
||||
|
||||
defp render_output(_socket, {:error, formatted}, _id) do
|
||||
assigns = %{formatted: formatted}
|
||||
|
||||
~L"""
|
||||
|
|
22
lib/livebook_web/live/session_live/vega_lite_component.ex
Normal file
22
lib/livebook_web/live/session_live/vega_lite_component.ex
Normal file
|
@ -0,0 +1,22 @@
|
|||
defmodule LivebookWeb.SessionLive.VegaLiteComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
socket = assign(socket, id: assigns.id)
|
||||
{:ok, push_event(socket, "vega_lite:#{socket.assigns.id}", %{"spec" => assigns.spec})}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<div id="<%= @id %>" phx-hook="VegaLite" phx-update="ignore" data-id="<%= @id %>">
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
Loading…
Add table
Reference in a new issue