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:
Jonatan Kłosko 2021-05-21 17:51:31 +02:00 committed by GitHub
parent 1a1057153e
commit 7804ff1d82
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 3169 additions and 4668 deletions

View file

@ -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

View 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

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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

View file

@ -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,

View file

@ -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}

View file

@ -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"""

View 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