Get More from Shiny for Python with Custom Components

Reading time:
time
min
By:
Ryszard Szymański
January 11, 2024

Imagine you are working on a dashboard, and you want to add this new type of <a href="https://appsilon.com/ux-design-of-shiny-apps-7-steps-to-design-dashboards-people-love/" target="_blank" rel="noopener">visualization</a> you saw in one of the blogs you follow, or perhaps your users showed you a visualization they liked and asked you to implement something similar.

You roll up your sleeves and start looking through <a href="https://shiny.posit.co/py/components/" target="_blank" rel="noopener noreferrer">the Shiny components gallery</a>, you check GitHub and PyPi to see if the community already has implemented a library supporting such visualization, but no luck!

Does that mean we won’t be able to implement that feature? Fortunately, thanks to the flexibility of Shiny for Python, we are able to create our own custom JavaScript components!

This means that if there is a JavaScript library with a visualization you would like to use, you can make it usable in Shiny for Python!

In this blog post, we will create a custom component for creating simple flow diagrams based on <a href="https://github.com/jerosoler/Drawflow" target="_blank" rel="noopener noreferrer">Drawflow</a> and provide controls for inserting different types of nodes.
<h3>Table of Contents</h3><ul>  <li><strong><a href="#creating-the-app">Creating the App</a></strong></li>  <li><strong><a href="#creating-our-custom-component">Creating Our Custom Component</a></strong></li>  <li><strong><a href="#adding-more-interactivity">Adding More Interactivity</a></strong></li>  <li><strong><a href="#summary">Summary</a></strong></li></ul>

<hr />

<h2 id="creating-the-app">Creating the App</h2>
<img class="aligncenter size-full wp-image-22827" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65e9e67daa096a8e39078098_fb5f6adb_drawflow.gif" alt="" width="750" height="323" />
<h2></h2>
Let’s start by creating a new Shiny for Python application in a directory named drawflow_app.
<pre><code class="language-r">
shiny create -t basic-app -m core
cd drawflow_app</code></pre>
<blockquote>Dive into <a href="https://appsilon.com/r-shiny-vs-shiny-for-python/" target="_blank" rel="noopener">R Shiny vs. Shiny for Python: What are the Key Differences</a> to uncover the essential distinctions and make your data visualization choice wisely.</blockquote>
<h2 id="creating-our-custom-component">Creating Our Custom Component</h2>
Now, let’s create a directory that will store the code for our custom component.
<pre><code class="language-r">
mkdir drawflow
</code></pre>
We will include there all static files (e.g. JavaScript code and CSS styles) that are necessary to run Drawflow:
<pre><code class="language-r">
curl -Lo drawflow/drawflow.min.css https://unpkg.com/drawflow@0.0.59/dist/drawflow.min.css
<br>curl -Lo drawflow/drawflow.min.js https://unpkg.com/drawflow@0.0.59/dist/drawflow.min.js
</code></pre>
Now, we will start writing our glue code, allowing us to use Drawflow in Shiny for Python. We first need to write a bit of JavaScript code and define a custom binding. You can think of a binding as an object that tells Shiny on the browser side how to make use of a given component.

We will write our custom binding in a file called <code>drawflowComponent.js</code>.
<pre><code class="language-r">
touch drawflow/drawflowComponent.js
</code></pre>
Now, let’s implement our binding.

<script src="https://gist.github.com/gigikenneth/c7d79728a0afaf285e226dcfc2f43bbc.js"></script>

<strong>Our binding is composed of two methods:</strong>
<ol>  <li><strong><code>find</code></strong> - it tells Shiny how to locate a given element. Here, we will identify our element by the <code>.shiny-drawflow-output</code> CSS selector</li>  <li><strong><code>renderValue</code></strong> - it tells Shiny how to use the data it got from the server to render a given element. In our case, it allows users to specify if the given drawflow instance should allow users to reroute connections in diagrams. Additionally, we will add a demo node for demonstration purposes.</li></ol>
Last but not least, we need to register our binding so Shiny becomes aware of it.

With the JavaScript part of the way, let’s write the Python wrapping code. First, we need to create our output function:
<pre><code class="language-r">
from htmltools import HTMLDependency
from shiny import App, reactive, ui
from shiny.module import resolve_id
<br>drawflow_dep = HTMLDependency(
   "drawflow",
   "0.0.59",
   source={"subdir": "drawflow"},
   script={"src": "drawflow.min.js"},
   stylesheet={"href": "drawflow.min.css"}
)
<br>drawflow_binding_dep = HTMLDependency(
   "drawflow_binding",
   "0.1.0",
   source={"subdir": "drawflow"},
   script={"src": "drawflowComponent.js"}
)
<br>def output_drawflow(id, height = "800px"):
   return ui.div(
       drawflow_dep,
       drawflow_binding_dep,
       id=resolve_id(id),
       class_="shiny-drawflow-output",
       style=f"height: {height}",
   )
</code></pre>
We start off by defining HTML dependencies through the <code>HTMLDependency</code> Function - those are objects holding the static assets that need to be fetched by the browser when using our custom component.

We were not able to find existing documentation for <a href="https://github.com/posit-dev/py-htmltools" target="_blank" rel="noopener noreferrer">Python <code>htmltools</code></a>, but you can learn more about how the R/Shiny equivalent works <a href="https://unleash-shiny.rinterface.com/htmltools-dependencies.html" target="_blank" rel="noopener noreferrer">in this documentation</a>.

Next, we define the <code>output_drawflow</code> function, which will include the actual app code. Note that we set the class of the div element to <code>shiny-drawflow-output</code>. This is important as this is the class name that our JavaScript binding we just wrote will be looking for.

Now, we need to create a <code>render</code> decorator for our component:
<pre><code class="language-r">
from dataclasses import dataclass, asdict
from shiny.render.transformer import (
   output_transformer,
   resolve_value_fn,
)
<br>
@dataclass
class Drawflow:
   reroute: bool = True
<br>
@output_transformer
async def render_drawflow(
   _meta,
   _fn,
):
   res = await resolve_value_fn(_fn)
<br>    return {
       "drawflow": asdict(res)
   }
</code></pre>
We first define a <a href="https://docs.python.org/3/library/dataclasses.html">dataclass</a> called <code>Drawflow</code> , which will contain the settings for our <code>drawflow</code> instance.

When it comes to our <code>render_drawflow</code> decorator, it will first resolve the result of the decorated function and then prepare the data to be sent to the browser. In our case, we just convert our dataclass into a dictionary so that it can get serialized to JSON.

Now let’s try it out in our app!
<pre><code class="language-r">
from shiny import App, reactive, ui
<br>... # our Python wrapping code
<br>app_ui = ui.page_fluid(
   ui.panel_title("Custom components!"),
   output_drawflow(id="ui_drawflow")
)
<br>
def server(input, output, session):
<br>    @output
   @render_drawflow
   def ui_drawflow():
       drawflow = Drawflow(reroute=True)
       return drawflow
<br>
app = App(app_ui, server)
</code></pre>
<img class="aligncenter size-full wp-image-22838" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65e9e67e20be65bb9e6445e8_922eeeed_custom_components.webp" alt="A user interface element labeled 'Demo' with a toggle on the right, highlighted by a cyan border, under the heading 'Custom components!' indicating an interactive element for UI customization" width="1600" height="741" />

We are now able to see our drawflow component!

But displaying a single block is quite boring isn’t it? Let’s fix that!
<h2 id="adding-more-interactivity">Adding More Interactivity</h2>
We will add buttons that will insert different types of nodes into our component! To do that, we will add a <code>customMessageHandler</code> (<a href="https://shiny.posit.co/r/articles/build/js-send-message/" target="_blank" rel="noopener">here you can read about the equivalent in R/Shiny</a>).

It’s a function that we will register on the browser side, which we will later be able to invoke from the server side of Shiny!

Let’s add it to our binding file!

<script src="https://gist.github.com/gigikenneth/a9c3492ed46dd92c1ddd6668d7cf50ae.js"></script>

With <code>Shiny.addCustomMessageHandler</code> we defined our custom message handler. The first argument corresponds to the id of the custom message handler. We will be using it later on in our server code.

The second argument is the actual JavaScript function that will be run in the browser after invoking it from the server side.

In our case, it’s for adding nodes of different types;
<ul>  <li>Start nodes which contain 1 output.</li>  <li>Intermediate nodes which contain 1 input and 1 output.</li>  <li>End nodes which contain 1 input.</li></ul>
We also needed to make small adjustments to our original binding:
<ul>  <li>We removed the demo node.</li>  <li>We store the created editor object in a global map object - this allows us then to refer to it in our <code>customMessageHandler</code>.</li></ul>
Let’s use it in our app!
<pre><code class="language-r">
from shiny import App, reactive, ui, session
<br>... # our Python wrapping code
<br>async def add_node(id, node_type):
   current_session = session.get_current_session()
   await current_session.send_custom_message(
       type="add_node",
       message={
           "id": id,
           "type": node_type
       }
   )
<br>
app_ui = ui.page_fluid(
   ui.panel_title("Custom components!"),
   ui.input_action_button(id="add_start_block", label = "Add start block"),
   ui.input_action_button(id="add_intermediate_block", label = "Add intermediate block"),
   ui.input_action_button(id="add_end_block", label = "Add end block"),
   output_drawflow(id="ui_drawflow")
)
<br>
def server(input, output, session):
   @reactive.Effect
   @reactive.event(input.add_start_block)
   async def _():
       await add_node(id="ui_drawflow", node_type="start")
   
   @reactive.Effect
   @reactive.event(input.add_intermediate_block)
   async def _():
       await add_node(id="ui_drawflow", node_type="intermediate")
<br>    @reactive.Effect
   @reactive.event(input.add_end_block)
   async def _():
       await add_node(id="ui_drawflow", node_type="end")
<br>    @output
   @render_drawflow
   def ui_drawflow():
       drawflow = Drawflow(reroute=True)
       return drawflow
<br>
app = App(app_ui, server)
</code></pre>
To invoke the custom message handler from the server, we run the <code>session.send_custom_message</code> function with the id of our <code>customMessageHandler</code> along with the data to send to the browser - in our case the id of the element and the type of the node we want to add.

Let’s see the app in action!

[video width="2864" height="1324" webm="https://wordpress.appsilon.com/wp-content/uploads/2024/01/pyshiny-drawflow.webm" loop="true" autoplay="true"][/video]

&nbsp;

All right, now we have some interactivity, but we could still use some work on the styling of the blocks to make them look better. We will use the default theme generated by the <a href="https://jerosoler.github.io/drawflow-theme-generator/" target="_blank" rel="noopener">drawflow-theme-generator</a> and save it in <a href="https://github.com/Appsilon/shiny-for-python-drawflow" target="_blank" rel="noopener">drawflow/drawflowComponent.css</a>.

We need to add it to our html dependency to make it work.
<pre><code class="language-r">
drawflow_binding_dep = HTMLDependency(
   "drawflow_binding",
   "0.1.0",
   source={"subdir": "drawflow"},
   script={"src": "drawflowComponent.js"},
   stylesheet={"href": "drawflowComponent.css"}
)
</code></pre>
Now, it looks much cleaner!

<img class="aligncenter size-full wp-image-22836" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65e9e67f48d812f537099b27_685eec6f_custom_components1.webp" alt="" width="1600" height="741" />
<h2 id="summary">Summary</h2>
We created our own custom component, and now we are able to use drawflow in our Shiny for Python application.

This shows the great flexibility of Shiny for Python, which allows us to leverage existing JavaScript libraries and make use of them in our apps.

The full source code used in the blog post is <a href="https://github.com/Appsilon/shiny-for-python-drawflow" target="_blank" rel="noopener">available on GitHub</a>.

Based on <a href="https://github.com/posit-dev/py-shiny-site/pull/46" target="_blank" rel="noopener">this Pull Request</a>, it looks like the Shiny team is working on other features, such as wrapping Web Components or React components, so stay tuned!

On top of that, both the <a href="https://github.com/posit-dev/shiny-bindings/blob/main/packages/core/README.md" target="_blank" rel="noopener">shiny-bindings-core</a> and <a href="https://github.com/posit-dev/shiny-bindings/blob/main/packages/react/README.md" target="_blank" rel="noopener">shiny-bindings-react</a> could potentially be used with R/Shiny as well. This means that we might:
<ol>  <li>Be able to include Web Components in our R/Shiny apps.</li>  <li>Gain a new way of using React components in R/Shiny (alongside the existing reactR and shiny.react packages).</li></ol>
This is great news, as it could further enhance the extensibility of R/Shiny! We will be keeping an eye out on <code>shiny-bindings</code> and see how it evolves.
<blockquote>Did you find this article insightful and helpful? If so, we invite you to become a part of our vibrant community at <a href="https://shiny4all.com/" target="_blank" rel="noopener">Shiny 4 All</a>. Join us today to connect with fellow Shiny enthusiasts, share insights, and continue exploring the world of Shiny.</blockquote>

Have questions or insights?

Engage with experts, share ideas and take your data journey to the next level!

Is Your Software GxP Compliant?

Download a checklist designed for clinical managers in data departments to make sure that software meets requirements for FDA and EMA submissions.
Explore Possibilities

Share Your Data Goals with Us

From advanced analytics to platform development and pharma consulting, we craft solutions tailored to your needs.

Talk to our Experts
python
shiny for python