Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some experimental components to run dashboards #1513

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

fonsp
Copy link
Owner

@fonsp fonsp commented Sep 27, 2021

Can we use Pluto as a framework to build dashboards? (Yes!) We already have a great environment for writing your model, for making it interactive and for visualizing results. The 'only' thing that's missing is layout: instead of showing all these elements linearly, you want to combine them into a single compact dashboard.

You can use @htl from HypertextLiteral to put multiple Pluto outputs in one cell, and with CSS flex and CSS grid, you can make any dashboard layout that your heart desires. I also added embed_display(x) to have Pluto's object viewer as a web component. This means that you can embed images, interactive arrays, tables, etc, inside @htl expressions. Cool!

data = rand(20)

coolplot = plot(data)

@htl("""
<div style="display: flex; flex-direction: row;">
	$(embed_display(data))
	$(embed_display(coolplot))
</div>
""")

Still missing: partial DOM updates

One problem arises when embedding bonds together with dependent variables:

myslider = @bind x Slider(1:20)

data = rand(x)

coolplot = plot(data)

@htl("""
<div style="display: flex; flex-direction: row;">
	$(embed_display(myslider))
	$(embed_display(data))
	$(embed_display(coolplot))
</div>
""")

Now, whenever you move the slider, data and coolplot will re-run, causing the final cell to re-run, which will re-render myslider. This causes the DOM element to lose focus, and you can't click-and-drag. Oops!

Schermopname.2021-09-27.om.12.42.22.mov

My initial solution was to use this (see JavaScript sample notebook) inside the embed_display web component to persist its last displayed element, instead of re-rendering it, when the HTML data did not change. This almost works, except embed_display now needs a way to identify itself. From which previous embed will it take the output? (We have some ideas but let's skip over this for now.)

Solution in this PR

EDIT: this has been merged, and is available through PlutoUI.ExperimentalLayout

There is a new special output object, a DivElement:

struct DivElement
	children::Vector
	style::String
end

If a cell outputs a DivElement, it won't render it as HTML in the julia process, but send the "div element data" to the frontend, which constructs it using a simple react component. If the cell re-renders, and only one of its children updates, then Pluto's diff-based state management will make sure that only the data for that child changes, and react will only update the HTML contents of that child, leaving the parent div and siblings as-is, solving the problem! DivElements can be nested and can have user-defined styles, so you can use it to create any layout.

Our example becomes:

myslider = @bind x Slider(1:20)

data = rand(x)

coolplot = plot(data)

PlutoRunner.DivElement(
	children=[
		myslider
		data
		coolplot
	],
	style="display: flex; flex-direction: row;",
)

And everything works as expected! Thanks @andreypopp for convincing me that this is necessary!

Still missing: intermediate DOM updates

Let's say that we have the same app as before, but our code takes a long time to execute:

myslider = @bind x Slider(1:20)

data = sleep(1); rand(x)

coolplot = sleep(5); plot(data)

PlutoRunner.DivElement(
	children=[
		myslider
		data
		coolplot
	],
	style="display: flex; flex-direction: row;",
)

In this case, moving the slider will re-run data, then re-run coolplot, and then re-run the last cell. This means that you have to wait 6 seconds to see anything. Ideally, we would see the new data after 1 second, and the new plot after 6 seconds. This does not play nicely with our reactivity model (it can't trigger re-running a cell multiple times). Thanks @dralletje for pointing this out!

Schermopname.2021-09-26.om.17.59.55.mov

Solution in this PR

There is a new special output object, a CellOutputMirror:

struct CellOutputMirror
	cell_id::UUID
end

Wherever this CellOutputMirror is displayed, it will magically show a mirror of the output of another cell. It refreshes automatically, without having to re-run.

The example becomes:

Let's say that we have the same app as before, but our code takes a long time to execute:

myslider = @bind x Slider(1:20)

data = sleep(1); rand(x)

coolplot = sleep(5); plot(data)

PlutoRunner.DivElement(
	children=[
		myslider
		CellOutputMirror(UUID("ac9b833-ac3839a-a234234-23434")) # id of cell that defines `data`
		CellOutputMirror(UUID("5323434-ac9b833-ac3839a-a2334")) # id of cell that defines `coolplot`
	],
	style="display: flex; flex-direction: row;",
)

Now, the last cell does not re-run when data or coolplot changes (because it does depend on those vars). Instead, it contains magical mirror that update on their own.

Schermopname.2021-09-26.om.16.39.27.mov

@fonsp fonsp mentioned this pull request Oct 27, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant