Going live with Shinylive

Am I a web developer?
shiny
web app
python
quarto
Author

Lindsay Lee

Published

August 1, 2024

I had a meeting at work recently where someone described a long, tedious process her team had to go through to write up a specifically-formatted report on a regular basis. She asked for help finding ways to reduce the time it takes to write these documents, namely by automating the sections that are fairly rote.

One option I thought of immediately was to build a Shiny app. I was thinking the user could input some key data points into the app, and then the app would analyze that data and produce a Word doc in a particular format based on the results.

I’ve dabbled in Shiny before, but one struggle was figuring out how to host it. You could use a service like shinyapps.io, but using the free version isn’t very enterprise-friendly, and the non-free version is, well, not free. Then as if the gods were smiling down upon me, I came across a post in LinkedIn about shinylive, which is a flavor of shiny that runs completely in your web browser. In theory it would be possible to “host” a shinylive application on a Quarto website hosted on GitHub Pages completely for free. What better way to test this idea than on this very Quarto website hosted on GitHub Pages?

I started off by following the instructions for the Quarto extension for shinylive. I installed the shinylive python package with:

pip install shinylive --upgrade

…and added the extension to my quarto project (the repo for this website) with:

quarto add quarto-ext/shinylive

In the YAML header for this post I added:

filters:
  - shinylive

Then the code for your shinylive application should all live in one code chunk marked with {shinylive-python} and containing #| standalone: true. This “standalone” bit apparently tells the code this chunk is a complete Shiny application instead of one spread across multiple files or chunks in the document, which they luckily say will be supported in the future.

I wanted my shinylive application to have a download button that allows the user to download a docx file. I found the python package python-docx which can accomplish this. Next I had to figure out how to get shinylive to install and use this package. The documentation and examples say you can create a requirements.txt file. But when I tried to create a requirements.txt file in the same folder as my quarto file, the application wouldn’t work because python-docx wasn’t being installed, indicating the requirements were not being recognized. After a lot of digging I came across this GitHub issue that showed how you can get your quarto-based shinylive application to recognize other files by creating them dynamically in the same code chunk using a format like:

## file: requirements.txt
python-docx
lxml

Reading back at the quarto extension documentation, I see they also explain how to do this there. If only I had originally thought to scroll down.

To create my actual prototype shinylive app, I followed the patterns in the examples for dynamically creating a file and allowing the user to download it. I also referred to the python-docx example code for how to create a Word doc with this package. Testing my app in the shinylive examples play area was helpful for debugging, which allowed me to removing variables like the fact that I’m rendering specifically in a Quarto site, that I’m publishing to GitHib Pages, etc.

Without further ado, here is my gorgeous and perfect app! You can see the code in the repo for this site.

#| standalone: true

import io
import docx
from shiny import App, render, ui

app_ui = ui.page_fluid(
    ui.input_slider("n", "How did the student do on the test?", 0, 100, 40),
    ui.download_button("download2", "Download")
)

def server(input, output, session):

    @render.download(filename="file.docx")
    def download2():
        #create document object
        doc = docx.Document()

        #add content to doc -------

        #add a title
        doc.add_heading('This is a report', 0)

        #add a paragraph
        p = doc.add_paragraph('A plain paragraph having some ')
        p.add_run('bold').bold = True
        p.add_run(' and some ')
        p.add_run('italic.').italic = True

        #add a heading
        doc.add_heading('Results', level=1)

        #add some lists
        doc.add_paragraph(
            'first item in unordered list', style='List Bullet'
        )
        doc.add_paragraph(
            'first item in ordered list', style='List Number'
        )

        #define a function that outputs a string based on the value of input.n()
        def get_interpretation(score):
            if score < 50:
                return "They did poorly"
            elif score < 70:
                return "They did okay"
            elif score < 90:
                return "They did well"
            else:
                return "They did excellent"
        
        #add a table, pulling from value inputted by user
        records = (
            ('Score 1', input.n() , get_interpretation(input.n())),
        )

        
        table = doc.add_table(rows=1, cols=3)
        hdr_cells = table.rows[0].cells
        hdr_cells[0].text = 'Score name'
        hdr_cells[1].text = 'Score value'
        hdr_cells[2].text = 'Interpretation'
        
        for score_name, score_value, score_desc in records:
            row_cells = table.add_row().cells
            row_cells[0].text = score_name
            row_cells[1].text = str(score_value)
            row_cells[2].text = score_desc

        #add a page break
        doc.add_page_break()


        #save document ------------------
        with io.BytesIO() as buf:
            doc.save(buf)
            yield buf.getvalue()


app = App(app_ui, server)


## file: requirements.txt
python-docx
lxml

One significant drawback with this so far is like I’ve already mentioned, your app has to be in one big code chunk, at least if you are embedding your shinylive app in Quarto. This limits how complex your app can be, which may be an issue in my particular use case. Another issue for me is that for data security reasons, my end users may want something that they can install and render on their own local computer without use of the internet. I’ve tried some light googling to see if it’s easy to create a desktop app from shiny and it doesn’t look so easy.

So we’ll see if this actually ends up being useful for the user I had in mind, but even if it isn’t, it was cool to learn anyway. Another fun python fact I learned: to force a tuple of tuples to remain a tuple of tuples even if it’s only got one tuple in it, slap a comma at the end of your tuple like so:

records = (
    ('Score 1', input.n() , get_interpretation(input.n())), #this last comma is key
)

Lessons abound!