I made a demo Shiny app that’s hosted with GitHub Pages but runs serverless in the browser, thanks to {shinylive}. It converts a copied table to Govspeak Markdown, the format required for publishing reports by the UK’s government.
Rise
Statistical reports in the UK public sector are often prepared as Word documents. However, they need to be uploaded to the UK government’s publishing system as a special, simplified flavour of Markdown called ‘Govspeak’. There’s an online tool to help with this, but it doesn’t yet handle the conversion of tables.
I wrote recently about a little function I made to transform Word tables to Govspeak. It suits me to run the function within R, but this approach isn’t ideal for colleagues who aren’t R users (imagine!).
A Shiny app could be useful here. But that’s a bit of a faff; where would I serve it from? I don’t have access to a server and all my free slots on shinyapps.io are taken up.
But! Thanks to recent developments1 in Shinylive and {webR}, I can serve the app from GitHub pages and have all the computation happen in the user’s browser. This is a gamechanger.
And shine
The first step was to create a Shiny app to ‘govspeakify’ a table. Nothing fancy, I just wanted:
A text field to receive a copied table.
Some interactive options for additional Govspeak formatting2.
A button to convert the table to Govspeak.
The Govspeak output printed to the screen.
A button to copy the output to the clipboard.
So, a button-click triggers the conversion of the pasted table via eventReactive(), given the user-supplied formatting options. The output is presented back to the user, along with a button to copy it, thanks to {rclipboard}.
You can find the app code in a GitHub repo. I prepared the app in a single app.R file, along with an R/conversion.R file with bespoke functions. I housed these in a govspeakify-tables subfolder.
There’s a number of things I want to add or improve to this proof of concept. For example, some more defensive programming to protect against invalid inputs and perhaps some more explanations and styling. Also the ability to upload a full docx file, extract and ‘govspeakify’ all tables and return them in a text file.
You’ll be able to break the app very easily, but it does what I need it to do for now.
And live!
So, I have the Shiny app; how do we prepare it? The {shinylive} package has a one-liner function that will generate a single folder containing your app and all the necessary bits and bobs from the Shinylive project so it can be served by GitHub Pages but perform computations in the user’s browser.
The steps are:
Run shinylive::export("govspeakify-tables", "docs") to take the Shiny app and assets from the govspeakify-tables folder and generate a deployable version of it in a folder called docs/ (since this is a folder name that GitHub Pages can serve from).
Run httpuv::runStaticServer("docs") to launch the app in a local static server and check that it works as intended (this requires the development version of {httpuv} at the time of writing, which you can install from GitHub). You could also test it out by pasting code into the online Shinylive editor.
Push to your GitHub repo and set up GitHub Pages to serve the docs/ folder (from the repo, go to the ‘Settings’ tab, click ‘Pages’ in the sidebar, select the ‘main’ branch and ‘/docs’ folder from the dropdowns, then click the ‘Save’ button).
GitHub will take a moment to ready your app, but it’s then available on the web via a URL in the form ‘https://username.github.io/repo-name’. The Govspeakify Tables app can be found here: https://matt-dray.github.io/govspeakify-tables (may take a sec to load).
Some of these instructions and links may change as tools like Shinylive (the asset-preparation system), {shinylive} (the package) and {webR} continue to develop. I realised later that Rami has also recorded these steps in a README, so you may want to look there in future for more up-to-date information.
Bottom line: Shinylive is a huge deal for creating small, nimble apps that are free from the tyranny of server management.
It occurred to me that the arrival of Shinylive might finally be the death knell for {crosstalk}. {crosstalk} allows for certain htmlwidgets to speak to each other so that, for example, you can select from a dropdown and the points displayed on a graph or table will get filtered. In other words, you get a Shiny-like experience without {shiny} and without a server. I spoke about {crosstalk} in 2018 and it hasn’t really been developed since then. I think I’ll probably use Shinylive in Quarto from now on.
Environment
Session info
Last rendered: 2023-10-11 17:12:59 CEST
R version 4.3.1 (2023-06-16)
Platform: aarch64-apple-darwin20 (64-bit)
Running under: macOS Ventura 13.2.1
Matrix products: default
BLAS: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRblas.0.dylib
LAPACK: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.11.0
locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
time zone: Europe/Rome
tzcode source: internal
attached base packages:
[1] stats graphics grDevices utils datasets methods base
loaded via a namespace (and not attached):
[1] htmlwidgets_1.6.2 compiler_4.3.1 fastmap_1.1.1 cli_3.6.1
[5] tools_4.3.1 htmltools_0.5.6.1 rstudioapi_0.15.0 yaml_2.3.7
[9] rmarkdown_2.25 knitr_1.44 jsonlite_1.8.7 xfun_0.40
[13] digest_0.6.33 rlang_1.1.1 fontawesome_0.5.2 evaluate_0.22
Footnotes
Thanks to George Stagg, Winston Chang, Barret Schloerke and many others! See Joe Cheng’s slides from Shinylive at the 2023 Posit conference.↩︎
To mark-up row titles (i.e. content in the first column is suffixed with #) and totals rows (all content in the row is emboldened between asterisks), and to provide a regular expression for characters to ignore when evaluating numeric columns (e.g. recognise that "75%" is 75 and "1,000" is 1000) so that they’ll be marked-up as right-aligned in the output.↩︎