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

Plug-in architecture for Streamlit #327

Open
treuille opened this issue Oct 8, 2019 · 16 comments
Open

Plug-in architecture for Streamlit #327

treuille opened this issue Oct 8, 2019 · 16 comments

Comments

@treuille
Copy link
Collaborator

@treuille treuille commented Oct 8, 2019

Problem

It would be nice if users could create their own plugins for Streamlit without forking the repo.

This issue has also come up in the community forums.

Starting point for the discussion

I think it would be cool if the basic approach was to simply write a react component in pure jsx and then run it through a Streamlit compiler, e.g.

streamlit compile my_widget.jsx

which would produce a wheel file (e.g. my_widget-VERSIONN-py2.py3-none-any.whl)

which could be pip installed or uploaded to pypi.

What about your ideas?

Please add comments below with your thoughts on requirements for this feature.

@gregmuellegger

This comment has been minimized.

Copy link

@gregmuellegger gregmuellegger commented Oct 8, 2019

Rather important is also to have the ability to write very customized widgets that are part of a project - i.e. they are not installed separately via pip. So besides the suggestion to compile an JSX file to a wheel, it would be nice to have the ability to directly load the JSX file so that it is compiled on load.

@lunixbochs

This comment has been minimized.

Copy link

@lunixbochs lunixbochs commented Oct 13, 2019

I really want custom UI in streamlit, and I'm likely to fork streamlit for now to hardcode some new UI use cases.

How would I specify the python side of the widget with streamlit compile? Let's say I want to build a custom input/output widget, like a hex editor that acts as a bytes variable, or a structured list of input boxes that returns a list or dict in python. I think maybe this could all be done from JS, but there's probably a case where having a bit of UI logic in python would help (especially if you need to do something that JS sandbox can't access)

Is it enough to expose an API declaration like (requires 3 arguments, named a, b, c and c is optional) and use a basic json-equivalent object through the protobuf for argument and return value passing?

Can we move the existing basic UI widgets to this form (like button), to dogfood / make sure it's the canonical way of doing it?

How would I develop on a JSX widget without compiling it every time I change something?

Structured input list component example (python side)

items = multi_prompt('input list', {
    'a': 'default value',
    'b': 'value 2',
})

print(items.a) # 'default value'
items.a = 'new value from python' # updates the UI

UI output by JSX looks like:

input list

key value
a <input>default value</input>
b <input>value 2</input>

Transport would need to send python->JS

{'widget create': {'title': 'input list', 'dict': {'a': 'default value', 'b': 'value 2'}}}
{'widget update': {'a': 'new value from python'}}

JS->Python

{'widget update': {'a': 'new value from JS'}}


For a hex editor widget, I'd want something like this:

preset_blob = b'asdfasdfasdfasdf'
blob = st.hex_editor(preset_blob)
print(blob)
# what if I want to update the blob later as a slow script runs?

UI:

00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........

py -> JS
{'widget create': {'data': 'asdfasdfasdfasdf', some other parameters? cursor position? starting scroll?})
{'widget update': {'data': 'new data'})

JS -> py
{'widget update': {'data': 'new data'}) OR {'offset': 1, 'byte': '\x00'}

(I assume this would all be protobufs or some efficient binary protocol)

@lunixbochs

This comment has been minimized.

Copy link

@lunixbochs lunixbochs commented Oct 14, 2019

What's the process for adding a bidirectional UI component to streamlit right now? (allowing data updates both from python->js and js->python)

@treuille

This comment has been minimized.

Copy link
Collaborator Author

@treuille treuille commented Oct 17, 2019

One use case we should consider here is a widget which contains a Tensorflow JS model as described in this thread.

@tvst

This comment has been minimized.

Copy link
Collaborator

@tvst tvst commented Oct 24, 2019

I haven't had time to consume all the info in this thread, but just wanted to dump some thoughts.

Here's a really barebones API idea:

import streamlit.plugin_api as api

plugin = api.register_plugin(
  uuid="123e4567-e89b-12d3-a456-426655440000",
  js_callback="my_func",  // This is called in JS as my_func(domNode, serializedData)
  files={
    'js': ['my_plugin.js', 'jsfolder/*.js', 'https://foo.com/some/other/file.js'],
    'css': ['my_plugin.css', 'cssfolder/*.css', 'https://foo.com/some/other/file.css'],
    'assets': ['assetsfolder/*'],
  }
)

def draw_crazy_d3_graph(arg1, arg2):
  # do stuff here
  plugin.send(serialized_data)

What this would do:

  • It would make Streamlit serve all the necessary JS/CSS/etc files.
  • In JS, my_func(domNode, serializedData) would be called to draw the element whenever draw_crazy_d3_graph() is called.

Notes:

  • It would be great to have my_func return a ReactNode instead. But we'd have to first figure out how to make sure Streamlit and the plugin are using the same React.
  • This only supports sending data to the frontend, not receiving. Need to come up with a nice API for that.
  • Also need to figure out how to handle updates (i.e. right now only add_rows, but we may be changing this soon)
@asg017

This comment has been minimized.

Copy link

@asg017 asg017 commented Oct 25, 2019

I think my proposal for embeding Observable notebooks (#513) would also cover a lot of plugin usecases. Instead of hosting JS/CSS files, you would just have to put everything inside an Observable notebook, then import that.

For example, this notebook is a fork of a d3 force directed graph. It has cells data, which contains the data for the DAG, chart, which is an interact SVG containing the graph, and clicked, which updates whenever the user starts dragging on a node. If we want to replace data with our own data, embed chart into our Streamlit app, and send the current value of clicked from the frontend to the backend, we could do:

import streamlit as st

data = {
    "nodes": [
        {"id": "a", "group": 1},
        {"id": "b", "group": 1},
        {"id": "c", "group": 1},
    ],
    "links": [{"source": "a", "target": "b", "value":1}]
}

notebook = st.observable('d/d2c49a2d6ae46adf', redefine={"data":data})

clicked = notebook.observe("clicked")

if clicked:
    st.write("Currently clicked node: ", clicked)

Maybe st.observable could also take in local notebooks (e.g. st.observable('./notebooks/test.js')), but it's a little difficult creating your own local notebook rather than just creating one on observablehq.

@treuille

This comment has been minimized.

Copy link
Collaborator Author

@treuille treuille commented Oct 25, 2019

What's the process for adding a bidirectional UI component to streamlit right now? (allowing data updates both from python->js and js->python)

@lunixbochs : I would check out how we did the slider widget as a starting point. The only way to do this right now is to fork the repo. :/

If you create a widget of general use to the community, we would be open to merging it into the main repo (which would be exciting!) although we haven't figure out guidelines around do that so please let us know if that's what you're thinking.

@treuille

This comment has been minimized.

Copy link
Collaborator Author

@treuille treuille commented Oct 26, 2019

It would be great to have my_func return a ReactNode instead. But we'd have to first figure out how to make sure Streamlit and the plugin are using the same React.

@tvst: Could we just transfer the js as a string from Streamlit to the browser and just execute it directly in the browser?

@MarcSkovMadsen

This comment has been minimized.

Copy link

@MarcSkovMadsen MarcSkovMadsen commented Nov 3, 2019

@treuille

An example of transfering javascript etc is shown below.

I was trying to see if I use the awesome SlickGrid as in the example

For a gallery of examples of SlickGrid see http://6pac.github.io/SlickGrid/examples/

Please please. All I wan't for Christmas is the SlickGrid in Streamlit. Also if I can only send data to the frontend but not get events back. :-)

<link rel="stylesheet" href="https://mleibman.github.io/SlickGrid/slick.grid.css" type="text/css"/>
<link rel="stylesheet" href="https://mleibman.github.io/SlickGrid/css/smoothness/jquery-ui-1.8.16.custom.css" type="text/css"/>
<table width="100%">
  <tr>
    <td valign="top" width="50%">
      <div id="myGrid" style="width:600px;height:500px;"></div>
    </td>
    <td valign="top">
      <h2>Demonstrates:</h2>
      <ul>
        <li>basic grid with minimal configuration</li>
      </ul>
        <h2>View Source:</h2>
        <ul>
            <li><A href="https://github.com/mleibman/SlickGrid/blob/gh-pages/examples/example1-simple.html" target="_sourcewindow"> View the source for this example on Github</a></li>
        </ul>
    </td>
  </tr>
</table>
<script src="https://mleibman.github.io/SlickGrid/lib/jquery-1.7.min.js"></script>
<script src="https://mleibman.github.io/SlickGrid/lib/jquery.event.drag-2.2.js"></script>
<script src="https://mleibman.github.io/SlickGrid/slick.core.js"></script>
<script src="https://mleibman.github.io/SlickGrid/slick.grid.js"></script>
<script>
  var grid;
  var columns = [
    {id: "title", name: "Title", field: "title"},
    {id: "duration", name: "Duration", field: "duration"},
    {id: "%", name: "% Complete", field: "percentComplete"},
    {id: "start", name: "Start", field: "start"},
    {id: "finish", name: "Finish", field: "finish"},
    {id: "effort-driven", name: "Effort Driven", field: "effortDriven"}
  ];
  var options = {
    enableCellNavigation: true,
    enableColumnReorder: false
  };
  $(function () {
    var data = [];
    for (var i = 0; i < 500; i++) {
      data[i] = {
        title: "Task " + i,
        duration: "5 days",
        percentComplete: Math.round(Math.random() * 100),
        start: "01/01/2009",
        finish: "01/05/2009",
        effortDriven: (i % 5 == 0)
      };
    }
    grid = new Slick.Grid("#myGrid", data, columns, options);
  })
</script>
`` 
@tvst

This comment has been minimized.

Copy link
Collaborator

@tvst tvst commented Nov 4, 2019

@tvst: Could we just transfer the js as a string from Streamlit to the browser and just execute it directly in the browser?

That wouldn't solve the problem I was referring to. What I meant was that we'd need to expose our React as nicely-named globals that scripts could hook into. Or maybe pass React as an object into the script's bootstrap code. Probably not as big a deal as I made it to be 😄 . We just need to look into best practices here.

As for the proposal of passing a JS string to the browser, my worry is this sounds a lot like eval, which makes me think twice. That said, my strawman proposal above would also means we'd be executing non-Streamlit code in the app, which is basically an eval too. I have to think more deeply about this to be 100% sure whether there's a real difference between the two, but my 10-second guess is they're the same — which means both would require nicely sandboxed iframes.

@kellyamanda

This comment has been minimized.

Copy link
Collaborator

@kellyamanda kellyamanda commented Nov 4, 2019

Request from a user: I want to create a multi-level radio button, and even more precise than that, I will use it to have a slide show in my demo, combining presentation and demonstration.

Additional info from same user: Just a typical table of content where each item yields to a page/slide (with bullets, …)

@MarcSkovMadsen

This comment has been minimized.

Copy link

@MarcSkovMadsen MarcSkovMadsen commented Jan 2, 2020

I've added an example of a custom widget using a lot of different hacks to the gallery at awesome-streamlit.org.

custom_login_widget

For more information see https://discuss.streamlit.io/t/awesome-streamlit-org-change-log/1414/9.

@RomeoDespres

This comment has been minimized.

Copy link

@RomeoDespres RomeoDespres commented Feb 14, 2020

My additional thought on plugins:

On the one hand, I need widgets that Streamlit doesn't have, and thus really need to build plugins.

On the other hand, I know nothing about web graphic design, and that's why I love how Streamlit makes it beautiful with no effort. I mean, it's precisely because I don't want to write CSS and HTML templates that I use Streamlit over Flask.

So I think it would be great (and consistent with Streamlit's philosophy) if the plugin building process made it as easy as possible to match Streamlit design without effort (maybe by providing default stylesheets? as a representative for web programming noobs, I'm probably not qualified to suggest solutions)

@koaning

This comment has been minimized.

Copy link

@koaning koaning commented Mar 2, 2020

I might like to mention that it'd be grand if there was also a non jsx option. You can have web components without react. There's also plenty of folks (myself included) who prefer to stick to jquery/d3 without having to learn the node pipeline.

@bballamudi

This comment has been minimized.

Copy link

@bballamudi bballamudi commented Mar 4, 2020

@MarcSkovMadsen

The login hack is impressive -- I've been trying to do something to the effect myself!
I ran your piece of code for login functionality and would love to see how you could hide the custom state text box though.

@MarcSkovMadsen

This comment has been minimized.

Copy link

@MarcSkovMadsen MarcSkovMadsen commented Mar 5, 2020

@bballamudi . Hiding is done via the javascript i insert via bokeh (as far as i remember). The code is available as a part of the Gallery and on GitHub.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
10 participants
You can’t perform that action at this time.