Skip to content

Conversation

@averissimo
Copy link
Contributor

@averissimo averissimo commented Nov 5, 2025

Pull Request

Thanks to @StefanThoma's for working on the first prototype

Changes description

  • Adds rmarkdown module
  • Renders the output to module UI and to Reporter
  • Source code is not verified as it adds a comment on how to render it
  • Adds download button to get the original Rmd file
    • Option in module to enable or disable this functionality
    • downloaded Rmd has module's data included (data pre-processing + filter)
  • Uses extra_transform parameter to add "generic decorators" to the top of the UI
    • name is temporary
  • Ensure figures are displayed properly in reporter (PDF output)
  • Avoid encoding images to base64 (mayb keep it in raw or equivalent)

To discuss

  • Should we render the .R to get all the R code from the report and add it to the code of the qenv?
  • [limitation] Use of markdown when rendering doesn't print title nor author
    • Rendering it on the page breaks teal
    • Wrapping in iframe doesn't extend full height and then creates strange reports
  • Should we use transformers to add extra input / data manipulation
    • Or keep a parameters that allows to add transforms (for example extra inputs) to the module by the app developer

(topics below added from conversation with @gogonzo )

  • should teal_report contain markdown elements?
  • should teal_data evaluate rmarkdown::render() and include this to @code
  • should markdown be evaluated with rmarkdown::render(envir = <qenv>) instead of params = list()?
    • visibility concern here on developing the Rmd

App

pkgload::load_all("../teal.modules.general")

data <- teal_data()
data <- within(data, {
  CO2 <- CO2
  CO2[["primary_key"]] <- seq_len(nrow(CO2))
})
join_keys(data) <- join_keys(join_key("CO2", "CO2", "primary_key"))

static_decorator <- teal_transform_module(
  label = "N Cols selector",
  ui = function(id) {
    ns <- NS(id)
    tags$div(
      numericInput(ns("n_rows"), "Show n rows", value = 5, min = 0, max = 200, step = 5)
    )
  },
  server = function(id, data) {
    moduleServer(id, function(input, output, session) {
      reactive({
        req(data())
        within(data(), {
          n_rows <- n_rows_value
        }, n_rows_value = input$n_rows)
      })
    })
  }
)

app <- init(
  data = data,
  modules = modules(
    tm_rmarkdown(
      label = "RMarkdown Module",
      rmd_file = system.file(file.path("sample_files", "test.Rmd"), package = "teal.modules.general"),
      allow_download = FALSE,
      extra_transform = list(static_decorator)
    )
  )
) |> shiny::runApp()

@averissimo averissimo added the core label Nov 5, 2025
@gogonzo gogonzo self-assigned this Nov 6, 2025
@averissimo averissimo marked this pull request as ready for review November 6, 2025 12:59
@averissimo
Copy link
Contributor Author

Marked as ready to review to start the discussion

@github-actions
Copy link
Contributor

github-actions bot commented Nov 6, 2025

badge

Code Coverage Summary

Filename                      Stmts    Miss  Cover    Missing
--------------------------  -------  ------  -------  -------------------------------------------
R/tm_a_pca.R                    864     864  0.00%    141-1132
R/tm_a_regression.R             751     751  0.00%    180-1027
R/tm_data_table.R               201     201  0.00%    100-348
R/tm_file_viewer.R              172     172  0.00%    47-254
R/tm_front_page.R               143     132  7.69%    77-246
R/tm_g_association.R            320     320  0.00%    161-547
R/tm_g_bivariate.R              664     406  38.86%   332-786, 827, 932, 949, 967, 978-1000
R/tm_g_distribution.R          1106    1106  0.00%    156-1404
R/tm_g_response.R               345     345  0.00%    179-594
R/tm_g_scatterplot.R            709     709  0.00%    261-1065
R/tm_g_scatterplotmatrix.R      272     253  6.99%    200-501, 562, 576
R/tm_missing_data.R            1080    1080  0.00%    124-1380
R/tm_outliers.R                1029    1029  0.00%    162-1341
R/tm_rmarkdown.R                171     171  0.00%    111-332
R/tm_t_crosstable.R             263     263  0.00%    177-482
R/tm_variable_browser.R         887     881  0.68%    89-1113, 1164-1347
R/utils.R                       185     120  35.14%   87-249, 278-304, 316-325, 330, 344-363, 452
R/zzz.R                           2       2  0.00%    2-3
TOTAL                          9164    8805  3.92%

Diff against main

Filename            Stmts    Miss  Cover
----------------  -------  ------  --------
R/tm_rmarkdown.R     +171    +171  +100.00%
TOTAL                +171    +171  -0.07%

Results for commit: d05971a

Minimum allowed coverage is 80%

♻️ This comment has been updated with latest results

Copy link
Contributor

@gogonzo gogonzo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't both documets be reproducible?

  • document downloaded from tm_rmarkdown
  • document downloaded via reporter

R/tm_rmarkdown.R Outdated
Comment on lines 177 to 178
params = datasets[["rmd_data"]],
envir = new.env(parent = globalenv()),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing data through params breaks reproducibility. In Rmd one has to use params$<dataset name>. I think data should be passed via envir.

Skärmavbild 2025-11-06 kl  13 46 01
Suggested change
params = datasets[["rmd_data"]],
envir = new.env(parent = globalenv()),
envir = q_r(),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with this, it's a bit more custom that using params, but it would be cleaner.

The only reason to use params would be to use existing features of R Markdown, instead of custom workflow

That is, if data is passed via envir, what would be the ideal workflow for the statistician to develop the original R Markdown file?

1. Using envir = data() in rmarkdown::render()

  1. Statistician will have a code chunk that generates whatever data they require
  2. Removes chunk:
    • Manually before sending it or use chunk opts to hide it {r eval = FALSE, include = FALSE}
    • We provide a flag in envir?
      • {r eval = !(exists(".teal_takes_care_data") && isTRUE(.teal_takes_care_data)), include = !(exists(".teal_takes_care_data") && isTRUE(.teal_takes_care_data))}
  3. Send to app developer
  4. App developer uses it module

2. Using params = as.list(data()) in rmarkdown::render()

As it is right now as it has an override mechanism

---
# ...
params:
  ADSL: !r teal.data::rADSL
  # ...
---
  1. Statistician develops Rmd with params set to whatever data they use
  2. Send to app developer
  3. App developer uses it on module

3. Other?

?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is, if data is passed via envir, what would be the ideal workflow for the statistician to develop the original R Markdown file?

Maybe sort of a debug-mode where data() is dumped into environment and app-developer can just execute code-chunks one by one.

R/tm_rmarkdown.R Outdated
moduleServer(id, function(input, output, session) {
if (allow_download) {
output$download_rmd <- downloadHandler(
filename = function() basename(rmd_file),
Copy link
Contributor

@gogonzo gogonzo Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't downloadable rmarkdown contain also "reproducible" qenv code? IMO tm_rmarkdown(rmd_file) is just a template and returning a template doesn't make sense if it lacks data to actually execute templete-file.

Edit:

I think app user should be able to download rendered document?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't downloadable rmarkdown contain also "reproducible" qenv code? IMO tm_rmarkdown(rmd_file) is just a template and returning a template doesn't make sense if it lacks data to actually execute templete-file.

Good point. I didn't want to do that before the rendering as it mean that the pre-processing would be run again.

But this can be added in post-rendering to the Download document and in the reporter markdown version.

This does imply we need to parse the Rmd and resulting markdown:

  1. Read the md
  2. Find line after yml header -- fallback to start of document if no header present
  3. Add new R code chunk with contents of qenv

(we could even inject this in the module UI as well to keep consistency)

I think app user should be able to download rendered document?

They can, when downloading the reporter. I don't think we should overlap functionality here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See how to convert .Rmd to .R and back using knitr (thats' in teal.reporter dependency)

Copy link
Contributor Author

@averissimo averissimo Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@llrs-roche parsing and adding the section in .R or in .Rmd would have similar challenges?

From my initial findings knitr::purl() is promising, but it does have some minor limitations with chunk options (it doesn't play well when evaluating params: {r, eval = !is.null(params$CO2)})

R/tm_rmarkdown.R Outdated

teal.reporter::teal_card(out_data) <- c(
teal.reporter::teal_card(out_data),
rendered_html_r()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

markdown fits here better. We can also split it into list() suitable with teal_card()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what would be better as I think of this as a monolith element. At least until we can actually break down the executable code.

We could keep it as a single markdown object (i.e. character vector) and only render as needed. note: the current approach was meant to reduce duplicate rendering (1. Module UI; 2. Reporter previewer; 3. Reporter download).

Splitting might be difficult to achieve as there are multiline grammar and context in markdown (for instance code chunks, or just newlines without a new paragraph in text)

toHTML(c("# ada", "", "bla.", "ca.", "```r", "1+1", "3*3", "```", "some text")) # works
# vs.
lapply(c("# ada", "", "bla.", "ca.", "```r", "1+1", "3*3", "```", "some text"), toHTML) # doesn't work

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could keep it as a single markdown object

I think its good to have a single markdown string because anyway at the end (before download) we will concatenate everything to the single file. Only editor will show one combined text-field for this element (i think it is not a big issue so far)

the current approach was meant to reduce duplicate rendering

This is a fair reason. I feel (not a strong opinion) that if you render markdown using rmarkdown::render and includeMarkdown the speed should be similar to rmarkdown::render to html. rmarkdown renders in two stages to html, first by converting Rmd -> md and then pandoc renders from md -> html.

The biggest issue with including html is that html can't be converted to pdf, otf etc.

Copy link
Contributor Author

@averissimo averissimo Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gogonzo see new commits.

  • Uses qenv environment instead of params
  • Downloaded Rmd from main module adds data processing in module
  • Markdown is rendered once
    • HTML is rendered only once on top of markdown
  • Uses custom to_rmd and toHTML to achieve goals

Let me know if you have comments :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

@gogonzo
Copy link
Contributor

gogonzo commented Nov 6, 2025

Key questions:

  1. What app-user should be able to download in tm_markdown
  • Should this document be reproducible and contain pre-processing, filter-panel and transformator code? Should this document contain outputs?
  • I don't think that app-user should receive the same file as app-developer provides do the tm_markdown. app-developer uses rmd file just as a template to handle data, produce outputs (plots and tables) and produce final document by specifying document: html_document/md_document/etc.. App-user thought receives document which content is controlled by app-developer - this could be markdown file containing/not containing code-chunks, html-file, pdf file etc.
  1. What should be added to the teal_card (reporter)
  • html/md content as single teal_card element?
  • md content converted to teal_card elements (multiple elements)?
  • rmarkdown::render() call (this is a joke - markdown::render inside of markdown file would be crazy)
  • nothing

@StefanThoma
Copy link

StefanThoma commented Nov 7, 2025 via email

@averissimo
Copy link
Contributor Author

@StefanThoma that's one of our goals with this PR. It's now possible for HTML output, the other formats are a WIP, but I believe we'll make it happen

out_data@verified <- FALSE # manual change verified status as code is being injected
}

teal.reporter::teal_card(out_data) <- c(
Copy link
Contributor Author

@averissimo averissimo Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue with keeping it in markdown format: Images are rendered to file that needs to be "kept" until report rendering

Possible workarounds:

  • Modify generated md file to convert image from paths to base64
  • Keep the images until the report generation
  • Re-render during report generation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been solved in c563c71 by creating a custom reporter object for this module.

It keeps the rendered HTML and uses it as caching (only renders once)

It also keeps the contents of images in memory to be dumped and used later on.

Why memory?

  • In case files on disk change (multiple reports being added)
  • Prevent having to track and keep all images being generated

Copy link
Contributor Author

@averissimo averissimo Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's still room for improvement here, but we have a working solution.

Improvement:

  • rmarkdown fixes issue raised above (and we manipulate image tag to use base64)
  • Avoid encoding to base64 keep it in raw format for example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants