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

[research] Decorate module output #1384

Closed
gogonzo opened this issue May 23, 2024 · 9 comments
Closed

[research] Decorate module output #1384

gogonzo opened this issue May 23, 2024 · 9 comments

Comments

@gogonzo
Copy link
Contributor

gogonzo commented May 23, 2024

We need to find an optimal way to allow app developer to "decorate" output of the module (including interactivity). In tmc modules are developed as follows.

  • there is a template_xy function which generates a list of calls needed to produce an output
  • template_xy is evaluated in qenv and plots use to be assigned to p binding (it is defined in the template)
  • then p variable is extracted from qenv and passed to plot_with_settings
template_xy <- function(... <many arguments like dataname xvar yvar>) {
}

srv_xy <- function(...) {
  ...
  all_q <- reactive({
      my_calls <- template_xy(...)
      teal.code::eval_code(anl_q(), as.expression(my_calls))
  })

  plot_r <- reactive(all_q()[["p"]])
  plot_with_settings(plot_r)
}

We need to find a nice way to include any arbitrary code to change the p. No holds barred!
Issue should be addressed by POC

Definition of Done

@gogonzo
Copy link
Contributor Author

gogonzo commented Aug 22, 2024

Discussion 1 summary

@kpagacz and I discussed the issue and decided to first focus on allowing app developers to provide arbitrary code to modify the module’s output. Interactivity is not yet included. We agreed almost immediately that decorators need to be provided through the tm_xyz argument and passed down by server_args to the srv_xyz. Discussion about the other ways seems irrelevant, but we will come back to this later. We considered three scenarios, though the list is not exhaustive.

1. Decorator passed as expression list

tm_xy <- function(
  ...
  decorator = expression(
    ggplot2::flip_axis(...) +
    ggplot2::ggtitle(...) +
    ggplot2::theme(...)
  )
)

Above call could be passed to the server and in the reactive which produces the plot call it would look something like this:

plot_call <- ...
if (length(decorator)) {
  plot_call <- substitute(p <- p + <decorator>) 
}
  • ✅ simple api to evaluate inside of the module. Can be automatically generated from ggplot_args
  • ✅ doesn't require knowing anything about p object created inside of the module. p doesn't have to be used as it will be automatically added to the final call
  • ❌ with above it won't be possible to do more than just appending ggplot elements.

2. Decorator as a function

tm_xy <- function(
  ...,
  decorator = expression(
    function(plot, data) {
      plot + 
        ggplot2::flip_axis(...) +
        ggplot2::ggtitle(...) + 
        ggplot2::theme(...) +
        ggplot2::geom_points(data, aes(x = x, y = y))
     }
  )
)

Inside of the module above would be consumed in the following way. Because function is passed as expression it will be added to SRC for reproducibility.

substitute(
  p <- decorate(iris, p),
  list(
    decorate = eval(decorator)
  )
)
  • ✅ doesn't need to know data object name nor p
  • ❌ not elegant SRC output

3. teal_transform_module

If we consider that some decorations will depend on the app user input (limiting axes range, changing title etc.) then teal_transform_module could be a solution.

  • ✅ standardised way of modifying objects for teal
  • ❌ require to know the bindings names inside of the qenv like plot or ADSL

@insightsengineering insightsengineering locked and limited conversation to collaborators Aug 22, 2024
@insightsengineering insightsengineering unlocked this conversation Aug 23, 2024
@gogonzo
Copy link
Contributor Author

gogonzo commented Aug 23, 2024

Discussion 2 summary

After short meeting, we agreed that in order to execute any custom call inside of the teal_module one of the following is needed:

  1. App-developer can provide a function(plot) without knowing how plot is named inside the srv_xy. Inside of the srv_xy body, module-developer would call decorator(plot = p). Function can have more arguments, for example function(plot, data) for example to add ggplot elements dependent on some data.
  2. App-developer can provide a call which can be executed inside of the module, but then app-developer should know the names of the objects as plot and data doesn't have to be the exact names inside of the teal_module. Then the module documentation should explicitly say that plot object is named p and data object is named ADSL. In most of the cases dataname is not fixed, so app-developer would have to know that if module receives iris then iris needs to be included in a decorate.

@gogonzo
Copy link
Contributor Author

gogonzo commented Sep 16, 2024

Conclusions from the last (general) meeting

  1. ui and server is needed for decoration in some cases
  2. J&J need only to provide a working decorate-call
  3. If we implement (2), it shouldn't put the burden on app developer to know the names of internal objects. Objects used by app developer should be a standard convention for decorate api
  4. If (3) will have a standard naming convention, then teal_module(s) could also use this standard convention to name objects - technically renaming names in the teal_module could have the same effect
  5. Decorators based on data will be a rabbit hole and would require app developer to understand teal_module reactivity (to know what is expected shape of the final dataset which plot/table is based on)
  6. We might need an access to the inputs also

Initial apps showing apps the problem

Initial app for plot decoration (simple case, one output)
library(teal)
identity_decorator <- list(
  ui = function(id) NULL,
  server = function(id, data) {
    data
  }
)

tm_decorated_plot <- function(label = "module", decorator = identity_decorator) {
  module(
    label = label,
    ui = function(id, decorator) {
      ns <- NS(id)
      div(
        selectInput(ns("dataname"), label = "select dataname", choices = NULL),
        selectInput(ns("x"), label = "select x", choices = NULL),
        selectInput(ns("y"), label = "select y", choices = NULL),
        decorator$ui(ns("decorate")),
        plotOutput(ns("plot")),
        verbatimTextOutput(ns("text"))
      )
    },
    server = function(id, data, decorator) {
      moduleServer(id, function(input, output, session) {
        observeEvent(data(), {
          updateSelectInput(inputId = "dataname", choices = teal.data::datanames(data()))
        })

        observeEvent(input$dataname, {
          updateSelectInput(inputId = "x", choices = colnames(data()[[input$dataname]]))
          updateSelectInput(inputId = "y", label = "select y", choices = colnames(data()[[input$dataname]]))
        })

        q1 <- reactive({
          req(input$dataname, input$x, input$y)
          data() |>
            within(
              {
                plot <- ggplot2::ggplot(dataname, ggplot2::aes(x = x, y = y)) +
                  ggplot2::geom_point()
              },
              dataname = as.name(input$dataname),
              x = as.name(input$x),
              y = as.name(input$y)
            )
        })

        q2 <- decorator$server("decorate", data = q1)

        plot_r <- reactive({
          req(q2())
          q2()[["plot"]]
        })

        output$plot <- renderPlot(plot_r())
        output$text <- renderText({
          teal.code::get_code(q2())
        })
      })
    },
    ui_args = list(decorator = decorator),
    server_args = list(decorator = decorator)
  )
}

app <- init(
  data = teal_data(iris = iris, mtcars = mtcars),
  modules = modules(
    tm_decorated_plot("1")
  )
)

runApp(app)

@insightsengineering insightsengineering deleted a comment from m7pr Sep 17, 2024
@shajoezhu

This comment was marked as resolved.

@donyunardi

This comment was marked as resolved.

@gogonzo
Copy link
Contributor Author

gogonzo commented Oct 7, 2024

POC

Create a decoration object

App developer can create a decorator in multiple ways. It is allowed to pass decorator to the module as teal_transform_module, language or function.

  1. teal_transform_module without ui. Nothing will be displayed, only server code will be executed.

    static decorator
    static_decorator <- teal_transform_module(
      server = function(id, data) {
        moduleServer(id, function(input, output, session) {
          reactive({
            req(data())
            data() |> within({
              plot <- plot +
                ggtitle("This is title") +
                xlab("x axis")
            })
          })
        })
      }
    )
  2. Using language will result in teal_transform_module without ui.

    language cunstructor
    static_decorator_lang <- quote(plot <- plot +
      ggtitle("This is title") +
      xlab("x axis title"))
  3. Adding interactivity requires creating a proper shiny module.

    interactive decorator
    interactive_decorator <- teal_transform_module(
      ui = function(id) {
        ns <- NS(id)
        div(
          textInput(ns("x_axis_title"), "X axis title", value = "x axis")
        )
      },
      server = function(id, data) {
        moduleServer(id, function(input, output, session) {
          reactive({
            req(data())
            data() |> within(
              {
                plot <- plot +
                  ggtitle("This is title") +
                  xlab(x_axis_title)
              },
              x_axis_title = input$x_axis_title
            )
          })
        })
      }
    )
  4. Thanks to the function-constructor app developer doesn't need to know what is the object name inside of the module's
    server. One can create any single argument function. Mapping x -> plot happens in decorate_teal_data and
    requires to specify output_name = "plot" (modules' developer duty).

    function constructor
    fun_decorator <- function(x) {
       x <- x + ggtitle("This is title") + xlab("x_axis_title")
     }
  5. Failures in decorators are handled by internal teal mechanism called trigger_on_success, which will never
    break data object inside of the module. Instead decorator will be ignored and relevant error message will be shown.

    failing decorator
    failing_decorator <- teal_transform_module(
      ui = function(id) {
        ns <- NS(id)
        div(
          textInput(ns("x_axis_title"), "X axis title", value = "x axis")
        )
      },
      server = function(id, data) {
        moduleServer(id, function(input, output, session) {
          reactive(stop("Hihi"))
        })
      }
    )

Example module and app

To include decorator to the teal_module one needs to add an extra argument to the module-constructor function tm_.
Decorator passed to the constructor needs to go through decorate_teal_data() in order to cast language or function object into teal_transform_module. decorate_teal_data() wouldn't be necessary if an app developer
always provides teal_transform_module. We could extend teal_transform_module() constructor by expr argument instead but using function would be impossible then.

Decorators should be passed via ui_args and server_args to the module's ui and server, where they should be
consumed by ui/srv_transform_data.

example module
library(ggplot2)
tm_decorated_plot <- function(label = "module", decorator = teal_transform_module()) {
  decorator_obj <- decorate_teal_data(x = decorator, output_name = "plot")

  module(
    label = label,
    ui = function(id, decorator) {
      ns <- NS(id)
      div(
        selectInput(ns("dataname"), label = "select dataname", choices = NULL),
        selectInput(ns("x"), label = "select x", choices = NULL),
        selectInput(ns("y"), label = "select y", choices = NULL),
        ui_transform_data(ns("decorate"), transforms = decorator),
        plotOutput(ns("plot")),
        verbatimTextOutput(ns("text"))
      )
    },
    server = function(id, data, decorator) {
      moduleServer(id, function(input, output, session) {
        observeEvent(data(), {
          updateSelectInput(inputId = "dataname", choices = teal.data::datanames(data()))
        })

        observeEvent(input$dataname, {
          updateSelectInput(inputId = "x", choices = colnames(data()[[input$dataname]]))
          updateSelectInput(inputId = "y", label = "select y", choices = colnames(data()[[input$dataname]]))
        })

        q1 <- reactive({
          req(input$dataname, input$x, input$y)
          data() |>
            within(
              {
                plot <- ggplot2::ggplot(dataname, ggplot2::aes(x = x, y = y)) +
                  ggplot2::geom_point()
              },
              dataname = as.name(input$dataname),
              x = as.name(input$x),
              y = as.name(input$y)
            )
        })

        q2 <- srv_transform_data("decorate", data = q1, transforms = decorator)

        plot_r <- reactive({
          req(q2())
          q2()[["plot"]]
        })

        output$plot <- renderPlot(plot_r())
        output$text <- renderText({
          teal.code::get_code(q2())
        })
      })
    },
    ui_args = list(decorator = decorator_obj),
    server_args = list(decorator = decorator_obj)
  )
}
example app
app <- init(
  data = teal_data(iris = iris, mtcars = mtcars),
  modules = modules(
    tm_decorated_plot("identity"),
    tm_decorated_plot("no-ui", decorator = static_decorator),
    tm_decorated_plot("lang", decorator = static_decorator_lang),
    tm_decorated_plot("fun", decorator = fun_decorator),
    tm_decorated_plot("interactive", decorator = interactive_decorator),
    tm_decorated_plot("failing", decorator = failing_decorator)
  )
)

shinyApp(app$ui, app$server)
tmg app
pkgload::load_all("teal")
pkgload::load_all("teal.modules.general")
library(teal.widgets)

data <- teal_data()
data <- within(data, {
  require(nestcolor)
  ADSL <- rADSL
})
join_keys(data) <- default_cdisc_join_keys[c("ADSL")]

footnote_decorator <- teal_transform_module(
  label = "Footnote",
  ui = function(id) {
    ns <- NS(id)
    div(
      textInput(ns("footnote"), "Footnote", value = "Collaboration")
    )
  },
  server = function(id, data) {
    moduleServer(id, function(input, output, session) {
      reactive({
        req(data())
        within(
          data(),
          g <- g + labs(caption = footnote),
          footnote = input$footnote
        )
      })
    })
  }
)

fact_vars_adsl <- names(Filter(isTRUE, sapply(data[["ADSL"]], is.factor)))
vars <- choices_selected(variable_choices(data[["ADSL"]], fact_vars_adsl))

app <- init(
  data = data,
  modules = modules(
    tm_a_regression(
      label = "Regression",
      response = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          label = "Select variable:",
          choices = "BMRKR1",
          selected = "BMRKR1",
          multiple = FALSE,
          fixed = TRUE
        )
      ),
      regressor = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          label = "Select variables:",
          choices = variable_choices(data[["ADSL"]], c("AGE", "SEX", "RACE")),
          selected = "AGE",
          multiple = TRUE,
          fixed = FALSE
        )
      ),
      ggplot2_args = ggplot2_args(
        labs = list(subtitle = "Plot generated by Regression Module")
      ),
      decorator = list(default = footnote_decorator)
    ),
    tm_outliers(
      outlier_var = list(
        data_extract_spec(
          dataname = "ADSL",
          select = select_spec(
            label = "Select variable:",
            choices = variable_choices(data[["ADSL"]], c("AGE", "BMRKR1")),
            selected = "AGE",
            multiple = FALSE,
            fixed = FALSE
          )
        )
      ),
      categorical_var = list(
        data_extract_spec(
          dataname = "ADSL",
          filter = filter_spec(
            vars = vars,
            choices = value_choices(data[["ADSL"]], vars$selected),
            selected = value_choices(data[["ADSL"]], vars$selected),
            multiple = TRUE
          )
        )
      ),
      boxplot_decorator = footnote_decorator,
      density_decorator = quote(g <- g + labs(caption = "(custom caption)")),
      ggplot2_args = list(
        ggplot2_args(
          labs = list(subtitle = "Plot generated by Outliers Module")
        )
      )
    )
  )
)

shinyApp(app$ui, app$server)

Design

image

To improve

  • Error handling when a decorator throws an error.
  • Rewrite modules so that they always have a graph as graph (or plot, g, p etc.) and table as
    table (or t etc.) and listings as listings (or l etc.). This simplifies/standardizes a construction of
    teal_transform_module which can be re-used in multiple modules.

@gogonzo
Copy link
Contributor Author

gogonzo commented Oct 8, 2024

Conclusions from the last meeting

  1. There seems to be a consensus that tm_xyz should accept teal_transform_module which means that extra constructor decorate_teal_data is not needed.

  2. Consequence of (1) is also that an app developer should know the output-object name when creating a transformers/decorators.

  3. (1) and (2) requires very explicit statement in the documentation that output-object is named <name> and we should rather keep consistent naming across the modules (table, listing, graph or t, l, g).

  4. Re-using teal_transform_module might fail when output-object name is different than it is in a "shared" teal_transform_module. This means that it is better to have library of functions which create teal_transform_module rather than ready-to-use decorators. See following example gg_xlab_decorator function and example usage.

    example builder ```r gg_xlab_decorator <- function(output_name) { teal_transform_module( ui = function(id) { ns <- NS(id) div( textInput(ns("x_axis_title"), "X axis title", value = "x axis") ) }, server = function(id, data) { moduleServer(id, function(input, output, session) { reactive({ req(data()) data() |> within( { output_name <- output_name + xlab(x_axis_title) }, x_axis_title = input$x_axis_title, output_name = as.name(output_name) ) }) }) } ) } ```

    Function can be used as follows

    tm_decorated_plot("from-fun", decorator = gg_xlab_decorator("graph"))
  5. teal_transform_module-constructor will be extended to accept expression as a simplified form of a server to avoid writing boilerplate code around EXPR (function(id, data) moduleServer(..., reactive( data() |> within(EXPR) ))).

  6. NOTE: Once teal_transform_module is created it is very difficult to robustly change output-object name as it requires parsing server function and detecting within.teal_data or eval_code call. Task is nearly impossible if some parts of this server is calling some wrapper functions which contain eval_code call.

Example app with simplified teal_transform_module constructorsstatic_decorator <- teal_transform_module(
server = function(id, data) {
moduleServer(id, function(input, output, session) {
reactive({
req(data())
data() |> within({
plot <- plot +
ggtitle("This is title") +
xlab("x axis")
})
})
})
}
)

static_decorator_lang <- teal_transform_module(server = quote(plot <- plot +
ggtitle("This is title") +
xlab("x axis title")))

interactive_decorator <- teal_transform_module(
ui = function(id) {
ns <- NS(id)
div(
textInput(ns("x_axis_title"), "X axis title", value = "x axis")
)
},
server = function(id, data) {
moduleServer(id, function(input, output, session) {
reactive({
req(data())
data() |> within(
{
plot <- plot +
ggtitle("This is title") +
xlab(x_axis_title)
},
x_axis_title = input$x_axis_title
)
})
})
}
)

interactive_decorator_lang <- teal_transform_module(
ui = function(id) {
ns <- NS(id)
div(
textInput(ns("x_axis_title"), "X axis title", value = "x axis")
)
},
server = expression(
plot <- plot +
ggtitle("This is title") +
xlab(x_axis_title)
)
)

gg_xlab_decorator <- function(output_name) {
teal_transform_module(
ui = function(id) {
ns <- NS(id)
div(
textInput(ns("x_axis_title"), "X axis title", value = "x axis")
)
},
server = function(id, data) {
moduleServer(id, function(input, output, session) {
reactive({
req(data())
data() |> within(
{
output_name <- output_name +
xlab(x_axis_title)
},
x_axis_title = input$x_axis_title,
output_name = as.name(output_name)
)
})
})
}
)
}

failing_decorator <- teal_transform_module(
ui = function(id) {
ns <- NS(id)
div(
textInput(ns("x_axis_title"), "X axis title", value = "x axis")
)
},
server = function(id, data) {
moduleServer(id, function(input, output, session) {
reactive(stop("Hihi"))
})
}
)

tm_decorated_plot <- function(label = "module", transforms = list(), decorator = teal_transform_module()) {
module(
label = label,
ui = function(id, decorator) {
ns <- NS(id)
div(
selectInput(ns("dataname"), label = "select dataname", choices = NULL),
selectInput(ns("x"), label = "select x", choices = NULL),
selectInput(ns("y"), label = "select y", choices = NULL),
ui_transform_data(ns("decorate"), transforms = decorator),
plotOutput(ns("plot")),
verbatimTextOutput(ns("text"))
)
},
server = function(id, data, decorator) {
moduleServer(id, function(input, output, session) {
observeEvent(data(), {
updateSelectInput(inputId = "dataname", choices = teal.data::datanames(data()))
})

    observeEvent(input$dataname, {
      updateSelectInput(inputId = "x", choices = colnames(data()[[input$dataname]]))
      updateSelectInput(inputId = "y", label = "select y", choices = colnames(data()[[input$dataname]]))
    })

    q1 <- reactive({
      req(input$dataname, input$x, input$y)
      data() |>
        within(
          {
            plot <- ggplot2::ggplot(dataname, ggplot2::aes(x = x, y = y)) +
              ggplot2::geom_point()
          },
          dataname = as.name(input$dataname),
          x = as.name(input$x),
          y = as.name(input$y)
        )
    })

    q2 <- srv_transform_data("decorate", data = q1, transforms = decorator)

    plot_r <- reactive({
      req(q2())
      q2()[["plot"]]
    })

    output$plot <- renderPlot(plot_r())
    output$text <- renderText({
      teal.code::get_code(q2())
    })
  })
},
ui_args = list(decorator = decorator),
server_args = list(decorator = decorator)

)
}

library(ggplot2)
app <- init(
data = teal_data(iris = iris, mtcars = mtcars),
modules = modules(
tm_decorated_plot("identity"),
tm_decorated_plot("no-ui", decorator = static_decorator),
tm_decorated_plot("lang", decorator = static_decorator_lang),
tm_decorated_plot("interactive", decorator = interactive_decorator),
tm_decorated_plot("interactive-from lang", decorator = interactive_decorator_lang),
tm_decorated_plot("from-fun", decorator = gg_xlab_decorator("plot")),
tm_decorated_plot("failing", decorator = failing_decorator)
)
)

shinyApp(app$ui, app$server)

</details>

@gogonzo
Copy link
Contributor Author

gogonzo commented Oct 9, 2024

Improved POC

In response to the conclusions from the last meeting we implemented a slightly different design.

image
  1. decorators specified for tm_ constructor should be a list of teal_transform_module.

  2. There is a way to simplify building of teal_transform_module's server by calling make_teal_transform_server(expr). This function allows to simply write an expression which will be wrapped in necessary boilerplate shiny-server code. When expression contains a name which matches the input, then it will be substituted with input value. Consider following, where ui has input named gg_title and in the expression there is ggtitle(gg_title). This would result in gg_title to be replaced with input$gg_title value.

    simplified server constructor
    teal_transform_module(
      ui = function(id) {
        ns <- NS(id)
        textInput(ns("gg_title"), ...)
      },
      server = make_teal_transform_server(expression(graph <- graph + ggtitle(gg_title)))
    )
  3. When created teal_transform_module has constant object names. This means that it is hard to robustly reuse
    transformer in any tm_ module. It is recomended for external parties to collect library of "decorators" as functions
    to potentially adjust the content of the evaluated expression to teal_modules's internals. See the following example
    and focus on output_name.

    function constructor
    gg_xlab_decorator <- function(output_name) {
      teal_transform_module(
        ui = function(id) {
          ns <- NS(id)
          div(
            textInput(ns("x_axis_title"), "X axis title", value = "x axis")
          )
        },
        server = function(id, data) {
          moduleServer(id, function(input, output, session) {
            reactive({
              req(data())
              data() |> within(
                {
                  output_name <- output_name +
                    xlab(x_axis_title)
                },
                x_axis_title = input$x_axis_title,
                output_name = as.name(output_name)
              )
            })
          })
        }
      )
    }
example app
pkgload::load_all("teal")

static_decorator <- teal_transform_module(
  server = function(id, data) {
    moduleServer(id, function(input, output, session) {
      reactive({
        req(data())
        data() |> within({
          plot <- plot +
            ggtitle("This is title") +
            xlab("x axis")
        })
      })
    })
  }
)

static_decorator_lang <- teal_transform_module(
  server = make_teal_transform_server(
    expression(
      plot <- plot +
        ggtitle("This is title") +
        xlab("x axis title")
    )
  )
)

interactive_decorator <- teal_transform_module(
  ui = function(id) {
    ns <- NS(id)
    div(
      textInput(ns("x_axis_title"), "X axis title", value = "x axis")
    )
  },
  server = function(id, data) {
    moduleServer(id, function(input, output, session) {
      reactive({
        req(data())
        data() |> within(
          {
            plot <- plot +
              ggtitle("This is title") +
              xlab(x_axis_title)
          },
          x_axis_title = input$x_axis_title
        )
      })
    })
  }
)

interactive_decorator_lang <- teal_transform_module(
  ui = function(id) {
    ns <- NS(id)
    div(
      textInput(ns("x_axis_title"), "X axis title", value = "x axis")
    )
  },
  server = make_teal_transform_server(
    expression(
      plot <- plot +
        ggtitle("This is title") +
        xlab(x_axis_title)
    )
  )
)

gg_xlab_decorator <- function(output_name) {
  teal_transform_module(
    ui = function(id) {
      ns <- NS(id)
      div(
        textInput(ns("x_axis_title"), "X axis title", value = "x axis")
      )
    },
    server = function(id, data) {
      moduleServer(id, function(input, output, session) {
        reactive({
          req(data())
          data() |> within(
            {
              output_name <- output_name +
                xlab(x_axis_title)
            },
            x_axis_title = input$x_axis_title,
            output_name = as.name(output_name)
          )
        })
      })
    }
  )
}

failing_decorator <- teal_transform_module(
  ui = function(id) {
    ns <- NS(id)
    div(
      textInput(ns("x_axis_title"), "X axis title", value = "x axis")
    )
  },
  server = function(id, data) {
    moduleServer(id, function(input, output, session) {
      reactive(stop("Hihi"))
    })
  }
)

tm_decorated_plot <- function(label = "module", transforms = list(), decorators = teal_transform_module()) {
  module(
    label = label,
    ui = function(id, decorators) {
      ns <- NS(id)
      div(
        selectInput(ns("dataname"), label = "select dataname", choices = NULL),
        selectInput(ns("x"), label = "select x", choices = NULL),
        selectInput(ns("y"), label = "select y", choices = NULL),
        ui_teal_transform_module(ns("decorate"), transforms = decorators),
        plotOutput(ns("plot")),
        verbatimTextOutput(ns("text"))
      )
    },
    server = function(id, data, decorators) {
      moduleServer(id, function(input, output, session) {
        observeEvent(data(), {
          updateSelectInput(inputId = "dataname", choices = teal.data::datanames(data()))
        })

        observeEvent(input$dataname, {
          updateSelectInput(inputId = "x", choices = colnames(data()[[input$dataname]]))
          updateSelectInput(inputId = "y", label = "select y", choices = colnames(data()[[input$dataname]]))
        })

        q1 <- reactive({
          req(input$dataname, input$x, input$y)
          data() |>
            within(
              {
                plot <- ggplot2::ggplot(dataname, ggplot2::aes(x = x, y = y)) +
                  ggplot2::geom_point()
              },
              dataname = as.name(input$dataname),
              x = as.name(input$x),
              y = as.name(input$y)
            )
        })

        q2 <- srv_teal_transform_module("decorate", data = q1, transforms = decorators)

        plot_r <- reactive({
          req(q2())
          q2()[["plot"]]
        })

        output$plot <- renderPlot(plot_r())
        output$text <- renderText({
          teal.code::get_code(q2())
        })
      })
    },
    ui_args = list(decorators = decorators),
    server_args = list(decorators = decorators)
  )
}

library(ggplot2)
app <- init(
  data = teal_data(iris = iris, mtcars = mtcars),
  modules = modules(
    tm_decorated_plot("identity"),
    tm_decorated_plot("no-ui", decorator = static_decorator),
    tm_decorated_plot("lang", decorator = static_decorator_lang),
    tm_decorated_plot("interactive", decorator = interactive_decorator),
    tm_decorated_plot("interactive-from lang", decorator = interactive_decorator_lang),
    tm_decorated_plot("from-fun", decorator = gg_xlab_decorator("plot")),
    tm_decorated_plot("failing", decorator = failing_decorator)
  )
)

shinyApp(app$ui, app$server)
Example tmg app
pkgload::load_all("teal")
pkgload::load_all("teal.modules.general")
library(teal.widgets)

data <- teal_data()
data <- within(data, {
  require(nestcolor)
  ADSL <- rADSL
})
join_keys(data) <- default_cdisc_join_keys[c("ADSL")]

footnote_decorator <- teal_transform_module(
  label = "Footnote",
  ui = function(id) {
    ns <- NS(id)
    div(
      textInput(ns("footnote"), "Footnote", value = "Collaboration")
    )
  },
  server = function(id, data) {
    moduleServer(id, function(input, output, session) {
      reactive({
        req(data())
        within(
          data(),
          g <- g + labs(caption = footnote),
          footnote = input$footnote
        )
      })
    })
  }
)

fact_vars_adsl <- names(Filter(isTRUE, sapply(data[["ADSL"]], is.factor)))
vars <- choices_selected(variable_choices(data[["ADSL"]], fact_vars_adsl))

app <- init(
  data = data,
  modules = modules(
    tm_a_regression(
      label = "Regression",
      response = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          label = "Select variable:",
          choices = "BMRKR1",
          selected = "BMRKR1",
          multiple = FALSE,
          fixed = TRUE
        )
      ),
      regressor = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          label = "Select variables:",
          choices = variable_choices(data[["ADSL"]], c("AGE", "SEX", "RACE")),
          selected = "AGE",
          multiple = TRUE,
          fixed = FALSE
        )
      ),
      ggplot2_args = ggplot2_args(
        labs = list(subtitle = "Plot generated by Regression Module")
      ),
      decorator = list(default = footnote_decorator)
    ),
    tm_outliers(
      outlier_var = list(
        data_extract_spec(
          dataname = "ADSL",
          select = select_spec(
            label = "Select variable:",
            choices = variable_choices(data[["ADSL"]], c("AGE", "BMRKR1")),
            selected = "AGE",
            multiple = FALSE,
            fixed = FALSE
          )
        )
      ),
      categorical_var = list(
        data_extract_spec(
          dataname = "ADSL",
          filter = filter_spec(
            vars = vars,
            choices = value_choices(data[["ADSL"]], vars$selected),
            selected = value_choices(data[["ADSL"]], vars$selected),
            multiple = TRUE
          )
        )
      ),
      boxplot_decorator = footnote_decorator,
      density_decorator = footnote_decorator,
      ggplot2_args = list(
        ggplot2_args(
          labs = list(subtitle = "Plot generated by Outliers Module")
        )
      )
    )
  )
)

shinyApp(app$ui, app$server)
Regression footnote
pkgload::load_all("teal")
pkgload::load_all("teal.modules.general")
library(teal.widgets)

data <- teal_data()
data <- within(data, {
  require(nestcolor)
  ADSL <- rADSL
})
join_keys(data) <- default_cdisc_join_keys[c("ADSL")]

# footnote based on the `fit`
footnote_regression <- teal_transform_module(
  server = make_teal_transform_server(expression(
    g <- g + labs(caption = deparse(summary(fit)[[1]]))
  ))
)

fact_vars_adsl <- names(Filter(isTRUE, sapply(data[["ADSL"]], is.factor)))
vars <- choices_selected(variable_choices(data[["ADSL"]], fact_vars_adsl))

app <- init(
  data = data,
  modules = modules(
    tm_a_regression(
      label = "Regression",
      response = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          label = "Select variable:",
          choices = "BMRKR1",
          selected = "BMRKR1",
          multiple = FALSE,
          fixed = TRUE
        )
      ),
      regressor = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          label = "Select variables:",
          choices = variable_choices(data[["ADSL"]], c("AGE", "SEX", "RACE")),
          selected = "AGE",
          multiple = TRUE,
          fixed = FALSE
        )
      ),
      ggplot2_args = ggplot2_args(
        labs = list(subtitle = "Plot generated by Regression Module")
      ),
      decorators = list(footnote_regression)
    )
  )
)

shinyApp(app$ui, app$server)

Design challenges:

  • transforms and decorators they are very similar concepts. transforms modify module's data input while decorators modify module's output object. module function has transforms argument, which is attached to the teal_module object and executed by teal before teal_module's server is called. We would like to document decorators also, but decorators fits better as ui/server_args and thus they don't have a dedicated place in the module() formals. Just consider a module which is defined as follows:
    • ui is a function of id and additional args (ui_args)
    • server is a function if id, data and additional args (server_args)
      If we decide to feed a ui and server with decorators as a separate module() argument it means it goes beyond the definition.

@gogonzo gogonzo transferred this issue from insightsengineering/teal.modules.clinical Oct 16, 2024
@gogonzo gogonzo added this to the Decorate modules' output milestone Oct 16, 2024
@gogonzo
Copy link
Contributor Author

gogonzo commented Oct 30, 2024

Closing this issue as research has been done

@gogonzo gogonzo closed this as completed Oct 30, 2024
averissimo added a commit that referenced this issue Nov 29, 2024
closes #1383 #1384 

Companion PRs:

- insightsengineering/teal.modules.general#795

<details>
<summary>example tmg app</summary>

```r
pkgload::load_all("teal")
pkgload::load_all("teal.modules.general")
library(teal.widgets)

data <- teal_data()
data <- within(data, {
  require(nestcolor)
  ADSL <- rADSL
})
join_keys(data) <- default_cdisc_join_keys[c("ADSL")]

footnote_regression <- teal_transform_module(
  server = make_teal_transform_server(expression(
    plot <- plot + labs(caption = deparse(summary(fit)[[1]]))
  ))
)

fact_vars_adsl <- names(Filter(isTRUE, sapply(data[["ADSL"]], is.factor)))
vars <- choices_selected(variable_choices(data[["ADSL"]], fact_vars_adsl))

app <- init(
  data = data,
  modules = modules(
    tm_a_regression(
      label = "Regression",
      response = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          label = "Select variable:",
          choices = "BMRKR1",
          selected = "BMRKR1",
          multiple = FALSE,
          fixed = TRUE
        )
      ),
      regressor = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          label = "Select variables:",
          choices = variable_choices(data[["ADSL"]], c("AGE", "SEX", "RACE")),
          selected = "AGE",
          multiple = TRUE,
          fixed = FALSE
        )
      ),
      ggplot2_args = ggplot2_args(
        labs = list(subtitle = "Plot generated by Regression Module")
      ),
      decorators = list(footnote_regression)
    )
  )
)

shinyApp(app$ui, app$server)

```

</details>

---------

Signed-off-by: Marcin <[email protected]>
Signed-off-by: André Veríssimo <[email protected]>
Co-authored-by: 27856297+dependabot-preview[bot]@users.noreply.github.com <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Marcin <[email protected]>
Co-authored-by: Konrad Pagacz <[email protected]>
Co-authored-by: m7pr <[email protected]>
Co-authored-by: Pawel Rucki <[email protected]>
Co-authored-by: André Veríssimo <[email protected]>
Co-authored-by: Lluís Revilla <[email protected]>
averissimo added a commit to insightsengineering/teal.modules.general that referenced this issue Nov 29, 2024
Partner to insightsengineering/teal#1357
Introduces decorators to modules. More about decorators in here
insightsengineering/teal#1384

<details><summary>Example with 1 tab per module</summary>

```r
pkgload::load_all("../teal")
pkgload::load_all(".")

# ######################################################
#
#   _____                           _
#  |  __ \                         | |
#  | |  | | ___  ___ ___  _ __ __ _| |_ ___  _ __ ___
#  | |  | |/ _ \/ __/ _ \| '__/ _` | __/ _ \| '__/ __|
#  | |__| |  __/ (_| (_) | | | (_| | || (_) | |  \__ \
#  |_____/ \___|\___\___/|_|  \__,_|\__\___/|_|  |___/
#
#
#
#  Decorators
# #####################################################

plot_grob_decorator <- function(default_footnote = "I am a good decorator", .var_to_replace = "plot") {
  teal_transform_module(
    label = "Caption (grob)",
    ui = function(id) shiny::textInput(shiny::NS(id, "footnote"), "Footnote", value = default_footnote),
    server = function(id, data) {
      moduleServer(id, function(input, output, session) {
        logger::log_info("🟠 plot_grob with default: {default_footnote}!", namespace = "teal.modules.general")
        reactive({
          req(data(), input$footnote)
          logger::log_info("changing the footnote {default_footnote}", namespace = "teal.modules.general")
          teal.code::eval_code(data(), substitute(
            {
              footnote_grob <- grid::textGrob(footnote, x = 0, hjust = 0, gp = grid::gpar(fontsize = 10, fontface = "italic", col = "gray50"))
              # Arrange the plot and footnote
              .var_to_replace <- gridExtra::arrangeGrob(
                .var_to_replace,
                footnote_grob,
                ncol = 1,
                heights = grid::unit.c(grid::unit(1, "npc") - grid::unit(1, "lines"), grid::unit(1, "lines"))
              )
            },
            env = list(
              footnote = input$footnote,
              .var_to_replace = as.name(.var_to_replace)
            )))
        })
      })
    }
  )
}
caption_decorator <- function(default_caption = "I am a good decorator", .var_to_replace = "plot") {
  teal_transform_module(
    label = "Caption",
    ui = function(id) shiny::textInput(shiny::NS(id, "footnote"), "Footnote", value = default_caption),
    server = make_teal_transform_server(
      substitute({
        my_name <- .var_name
        .var_to_replace <- .var_to_replace + ggplot2::labs(caption = footnote)
      }, env = list(.var_to_replace = as.name(.var_to_replace), .var_name = .var_to_replace))
    )
  )
}
table_decorator <- function(.color1 = "#f9f9f9", .color2 = "#f0f0f0", .var_to_replace = "table") {
  teal_transform_module(
    label = "Table color",
    ui = function(id) {
      selectInput(
        NS(id, "style"),
        "Table Style",
        choices = c("Default", "Color1", "Color2"),
        selected = "Default"
      )
    },
    server = function(id, data) {
      moduleServer(id, function(input, output, session) {
        logger::log_info("🔵 Table row color called to action!", namespace = "teal.modules.general")
        reactive({
          req(data(), input$style)
          logger::log_info("changing the Table row color '{input$style}'", namespace = "teal.modules.general")
          teal.code::eval_code(data(), substitute({
            .var_to_replace <- switch(
              style,
              "Color1" = DT::formatStyle(
                .var_to_replace,
                columns = attr(.var_to_replace$x, "colnames")[-1],
                target = "row",
                backgroundColor = .color1
              ),
              "Color2" = DT::formatStyle(
                .var_to_replace,
                columns = attr(.var_to_replace$x, "colnames")[-1],
                target = "row",
                backgroundColor = .color2
              ),
              .var_to_replace
            )
          }, env = list(
            style = input$style,
            .var_to_replace = as.name(.var_to_replace),
            .color1 = .color1,
            .color2 = .color2
          )))
        })
      })
    }
  )
}
head_decorator <- function(default_value = 6, .var_to_replace = "object") {
  teal_transform_module(
    label = "Head",
    ui = function(id) shiny::numericInput(shiny::NS(id, "n"), "Footnote", value = default_value),
    server = make_teal_transform_server(
      substitute({
        .var_to_replace <- utils::head(.var_to_replace, n = n)
      }, env = list(.var_to_replace = as.name(.var_to_replace)))
    )
  )
}
treelis_subtitle_decorator <- function(default_caption = "I am a good decorator", .var_to_replace = "plot") {
  teal_transform_module(
    label = "Caption",
    ui = function(id) shiny::textInput(shiny::NS(id, "footnote"), "Footnote", value = default_caption),
    server = make_teal_transform_server(
      substitute({
        .var_to_replace <- update(.var_to_replace, sub = footnote)
      }, env = list(.var_to_replace = as.name(.var_to_replace)))
    )
  )
}
insert_rrow_decorator <- function(default_caption = "I am a good new row", .var_to_replace = "table") {
  teal_transform_module(
    label = "New row",
    ui = function(id) shiny::textInput(shiny::NS(id, "new_row"), "New row", value = default_caption),
    server = make_teal_transform_server(
      substitute({
        .var_to_replace <- rtables::insert_rrow(.var_to_replace, rtables::rrow(new_row))
      }, env = list(.var_to_replace = as.name(.var_to_replace)))
    )
  )
}
do_nothing_decorator <- teal_transform_module(server = function(id, data) moduleServer(id, function(input, output, session) data))

# ##########################################
#
#   _             _      _       _
#  | |           | |    | |     | |
#  | |_ ___  __ _| |  __| | __ _| |_ __ _
#  | __/ _ \/ _` | | / _` |/ _` | __/ _` |
#  | ||  __/ (_| | || (_| | (_| | || (_| |
#   \__\___|\__,_|_| \__,_|\__,_|\__\__,_|
#                ______
#               |______|
#
#  teal_data
# #########################################

data <- teal_data(join_keys = default_cdisc_join_keys[c("ADSL", "ADRS")])
data <- within(data, {
  require(nestcolor)
  ADSL <- rADSL
  ADRS <- rADRS
})

# For tm_outliers
fact_vars_adsl <- names(Filter(isTRUE, sapply(data[["ADSL"]], is.factor)))
vars <- choices_selected(variable_choices(data[["ADSL"]], fact_vars_adsl))

# For tm_g_distribution

vars1 <- choices_selected(
  variable_choices(data[["ADSL"]], c("ARM", "COUNTRY", "SEX")),
  selected = NULL
)


init(
  data = data,
  modules = modules(
    # ###################################################
    #
    #                                    _
    #                                   (_)
    #   _ __ ___  __ _ _ __ ___  ___ ___ _  ___  _ __
    #  | '__/ _ \/ _` | '__/ _ \/ __/ __| |/ _ \| '_ \
    #  | | |  __/ (_| | | |  __/\__ \__ \ | (_) | | | |
    #  |_|  \___|\__, |_|  \___||___/___/_|\___/|_| |_|
    #             __/ |
    #            |___/
    #
    #  regression
    # ##################################################
    tm_a_regression(
      label = "Regression",
      response = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          label = "Select variable:",
          choices = "BMRKR1",
          selected = "BMRKR1",
          multiple = FALSE,
          fixed = TRUE
        )
      ),
      regressor = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          label = "Select variables:",
          choices = variable_choices(data[["ADSL"]], c("AGE", "SEX", "RACE")),
          selected = "AGE",
          multiple = TRUE,
          fixed = FALSE
        )
      ),
      decorators = list(caption_decorator("I am Regression", "plot"))
    ),
    # #########################################################
    #
    #       _ _     _        _ _           _   _
    #      | (_)   | |      (_) |         | | (_)
    #    __| |_ ___| |_ _ __ _| |__  _   _| |_ _  ___  _ __
    #   / _` | / __| __| '__| | '_ \| | | | __| |/ _ \| '_ \
    #  | (_| | \__ \ |_| |  | | |_) | |_| | |_| | (_) | | | |
    #   \__,_|_|___/\__|_|  |_|_.__/ \__,_|\__|_|\___/|_| |_|
    #
    #
    #
    #  distribution
    # ########################################################
    tm_g_distribution(
      dist_var = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          choices = variable_choices(data[["ADSL"]], c("AGE", "BMRKR1")),
          selected = "BMRKR1",
          multiple = FALSE,
          fixed = FALSE
        )
      ),
      strata_var = data_extract_spec(
        dataname = "ADSL",
        filter = filter_spec(
          vars = vars1,
          multiple = TRUE
        )
      ),
      group_var = data_extract_spec(
        dataname = "ADSL",
        filter = filter_spec(
          vars = vars1,
          multiple = TRUE
        )
      ),
      decorators = list(
        histogram_plot = caption_decorator("I am density!", "histogram_plot"),
        qq_plot = caption_decorator("I am QQ!", "qq_plot"),
        summary_table = table_decorator("#FFA500", "#800080", "summary_table"),
        test_table = table_decorator("#2FA000", "#80FF80", "test_table")
      )
    ),
    # ####################
    #
    #
    #
    #   _ __   ___ __ _
    #  | '_ \ / __/ _` |
    #  | |_) | (_| (_| |
    #  | .__/ \___\__,_|
    #  | |
    #  |_|
    #
    #  pca
    # ###################
    tm_a_pca(
      "PCA",
      dat = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          choices = variable_choices(data = data[["ADSL"]], c("BMRKR1", "AGE", "EOSDY")),
          selected = c("BMRKR1", "AGE")
        )
      ),
      decorators = list(
        elbow_plot = caption_decorator("I am PCA / elbow", "elbow_plot"),
        circle_plot = caption_decorator("I am a PCA / circle", "circle_plot"),
        biplot = caption_decorator("I am a PCA / bipot", "biplot"),
        eigenvector_plot = caption_decorator("I am a PCA / eigenvector", "eigenvector_plot")
      )
    ),
    ######################################
    #
    #               _   _ _
    #              | | | (_)
    #    ___  _   _| |_| |_  ___ _ __ ___
    #   / _ \| | | | __| | |/ _ \ '__/ __|
    #  | (_) | |_| | |_| | |  __/ |  \__ \
    #   \___/ \__,_|\__|_|_|\___|_|  |___/
    #
    #
    #
    #  outliers
    # #####################################
    tm_outliers(
      outlier_var = list(
        data_extract_spec(
          dataname = "ADSL",
          select = select_spec(
            label = "Select variable:",
            choices = variable_choices(data[["ADSL"]], c("AGE", "BMRKR1")),
            selected = "AGE",
            multiple = FALSE,
            fixed = FALSE
          )
        )
      ),
      categorical_var = list(
        data_extract_spec(
          dataname = "ADSL",
          filter = filter_spec(
            vars = vars,
            choices = value_choices(data[["ADSL"]], vars$selected),
            selected = value_choices(data[["ADSL"]], vars$selected),
            multiple = TRUE
          )
        )
      ),
      decorators = list(
        box_plot = caption_decorator("I am a good decorator", "box_plot"),
        density_plot = caption_decorator("I am a good decorator", "density_plot"),
        cumulative_plot = caption_decorator("I am a good decorator", "cumulative_plot"),
        table = table_decorator("#FFA500", "#800080")
      )
    ),
    # #######################################################
    #
    #                            _       _   _
    #                           (_)     | | (_)
    #    __ _ ___ ___  ___   ___ _  __ _| |_ _  ___  _ __
    #   / _` / __/ __|/ _ \ / __| |/ _` | __| |/ _ \| '_ \
    #  | (_| \__ \__ \ (_) | (__| | (_| | |_| | (_) | | | |
    #   \__,_|___/___/\___/ \___|_|\__,_|\__|_|\___/|_| |_|
    #
    #
    #
    #  association
    # ######################################################
    tm_g_association(
      ref = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          choices = variable_choices(
            data[["ADSL"]],
            c("SEX", "RACE", "COUNTRY", "ARM", "STRATA1", "STRATA2", "ITTFL", "BMRKR2")
          ),
          selected = "RACE"
        )
      ),
      vars = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          choices = variable_choices(
            data[["ADSL"]],
            c("SEX", "RACE", "COUNTRY", "ARM", "STRATA1", "STRATA2", "ITTFL", "BMRKR2")
          ),
          selected = "BMRKR2",
          multiple = TRUE
        )
      ),
      decorators = list(plot_grob_decorator("I am a good grob (association)"))
    ),
    # ################################################
    #
    #       _       _         _        _     _
    #      | |     | |       | |      | |   | |
    #    __| | __ _| |_ __ _ | |_ __ _| |__ | | ___
    #   / _` |/ _` | __/ _` || __/ _` | '_ \| |/ _ \
    #  | (_| | (_| | || (_| || || (_| | |_) | |  __/
    #   \__,_|\__,_|\__\__,_| \__\__,_|_.__/|_|\___|
    #                     ______
    #                    |______|
    #
    #  data_table
    # ###############################################
    tm_data_table(
      variables_selected = list(
        iris = c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width", "Species")
      ),
      dt_args = list(caption = "IRIS Table Caption"),
      decorators = list(table_decorator())
    ),
    # ########################################################
    #
    #                                 _        _     _
    #                                | |      | |   | |
    #    ___ _ __ ___  ___ ___ ______| |_ __ _| |__ | | ___
    #   / __| '__/ _ \/ __/ __|______| __/ _` | '_ \| |/ _ \
    #  | (__| | | (_) \__ \__ \      | || (_| | |_) | |  __/
    #   \___|_|  \___/|___/___/       \__\__,_|_.__/|_|\___|
    #
    #
    #
    #  cross-table
    # #######################################################
    tm_t_crosstable(
      label = "Cross Table",
      x = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          choices = variable_choices(data[["ADSL"]], subset = function(data) {
            idx <- !vapply(data, inherits, logical(1), c("Date", "POSIXct", "POSIXlt"))
            return(names(data)[idx])
          }),
          selected = "COUNTRY",
          multiple = TRUE,
          ordered = TRUE
        )
      ),
      y = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          choices = variable_choices(data[["ADSL"]], subset = function(data) {
            idx <- vapply(data, is.factor, logical(1))
            return(names(data)[idx])
          }),
          selected = "SEX"
        )
      ),
      decorators = list(insert_rrow_decorator("I am a good new row"))
    ),
    # #######################################################################################
    #
    #                 _   _                  _       _                     _        _
    #                | | | |                | |     | |                   | |      (_)
    #   ___  ___ __ _| |_| |_ ___ _ __ _ __ | | ___ | |_   _ __ ___   __ _| |_ _ __ ___  __
    #  / __|/ __/ _` | __| __/ _ \ '__| '_ \| |/ _ \| __| | '_ ` _ \ / _` | __| '__| \ \/ /
    #  \__ \ (_| (_| | |_| ||  __/ |  | |_) | | (_) | |_  | | | | | | (_| | |_| |  | |>  <
    #  |___/\___\__,_|\__|\__\___|_|  | .__/|_|\___/ \__| |_| |_| |_|\__,_|\__|_|  |_/_/\_\
    #                                 | |
    #                                 |_|
    #
    #  scatterplot matrix
    # ######################################################################################
    tm_g_scatterplotmatrix(
      label = "Scatterplot matrix",
      variables = list(
        data_extract_spec(
          dataname = "ADSL",
          select = select_spec(
            choices = variable_choices(data[["ADSL"]]),
            selected = c("AGE", "RACE", "SEX"),
            multiple = TRUE,
            ordered = TRUE
          )
        ),
        data_extract_spec(
          dataname = "ADRS",
          filter = filter_spec(
            label = "Select endpoints:",
            vars = c("PARAMCD", "AVISIT"),
            choices = value_choices(data[["ADRS"]], c("PARAMCD", "AVISIT"), c("PARAM", "AVISIT")),
            selected = "INVET - END OF INDUCTION",
            multiple = TRUE
          ),
          select = select_spec(
            choices = variable_choices(data[["ADRS"]]),
            selected = c("AGE", "AVAL", "ADY"),
            multiple = TRUE,
            ordered = TRUE
          )
        )
      ),
      decorators = list(treelis_subtitle_decorator("I am a Scatterplot matrix", "plot"))
    ),
    # #############################################
    #
    #
    #
    #   _ __ ___  ___ _ __   ___  _ __  ___  ___
    #  | '__/ _ \/ __| '_ \ / _ \| '_ \/ __|/ _ \
    #  | | |  __/\__ \ |_) | (_) | | | \__ \  __/
    #  |_|  \___||___/ .__/ \___/|_| |_|___/\___|
    #                | |
    #                |_|
    #
    #  response
    # ############################################
    tm_g_response(
      label = "Response",
      response = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(choices = variable_choices(data[["ADSL"]], c("BMRKR2", "COUNTRY")))
      ),
      x = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(choices = variable_choices(data[["ADSL"]], c("SEX", "RACE")), selected = "RACE")
      ),
      decorators = list(caption_decorator("I am a Response", "plot"))
    ),
    # ############################################
    #
    #   _     _                 _       _
    #  | |   (_)               (_)     | |
    #  | |__  ___   ____ _ _ __ _  __ _| |_ ___
    #  | '_ \| \ \ / / _` | '__| |/ _` | __/ _ \
    #  | |_) | |\ V / (_| | |  | | (_| | ||  __/
    #  |_.__/|_| \_/ \__,_|_|  |_|\__,_|\__\___|
    #
    #
    #
    #  bivariate
    # ###########################################
    tm_g_bivariate(
      x = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(choices = variable_choices(data[["ADSL"]]), selected = "AGE")
      ),
      y = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(choices = variable_choices(data[["ADSL"]]), selected = "SEX")
      ),
      row_facet = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(choices = variable_choices(data[["ADSL"]]), selected = "ARM")
      ),
      col_facet = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(choices = variable_choices(data[["ADSL"]]), selected = "COUNTRY")
      ),
      decorators = list(caption_decorator("I am a Bivariate", "plot"))
    ),
    #####################################################
    #
    #                 _   _                  _       _
    #                | | | |                | |     | |
    #   ___  ___ __ _| |_| |_ ___ _ __ _ __ | | ___ | |_
    #  / __|/ __/ _` | __| __/ _ \ '__| '_ \| |/ _ \| __|
    #  \__ \ (_| (_| | |_| ||  __/ |  | |_) | | (_) | |_
    #  |___/\___\__,_|\__|\__\___|_|  | .__/|_|\___/ \__|
    #                                 | |
    #                                 |_|
    #
    #  scatterplot
    # ####################################################
    tm_g_scatterplot(
      label = "Scatterplot",
      x = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(choices = variable_choices(data[["ADSL"]], c("AGE", "BMRKR1", "BMRKR2")))
      ),
      y = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          choices = variable_choices(data[["ADSL"]], c("AGE", "BMRKR1", "BMRKR2")),
          selected = "BMRKR1"
        )
      ),
      color_by = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          choices = variable_choices(data[["ADSL"]], c("AGE", "BMRKR1", "BMRKR2", "RACE", "REGION1")),
          selected = NULL
        )
      ),
      size_by = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(choices = variable_choices(data[["ADSL"]], c("AGE", "BMRKR1")))
      ),
      row_facet = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          choices = variable_choices(data[["ADSL"]], c("BMRKR2", "RACE", "REGION1")),
          selected = NULL
        )
      ),
      col_facet = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          choices = variable_choices(data[["ADSL"]], c("BMRKR2", "RACE", "REGION1")),
          selected = NULL
        )
      ),
      decorators = list(caption_decorator("I am a scatterplot", "plot"))
    ),
    # ##############################################################
    #
    #             _         _                    _       _
    #            (_)       (_)                  | |     | |
    #   _ __ ___  _ ___ ___ _ _ __   __ _     __| | __ _| |_ __ _
    #  | '_ ` _ \| / __/ __| | '_ \ / _` |   / _` |/ _` | __/ _` |
    #  | | | | | | \__ \__ \ | | | | (_| |  | (_| | (_| | || (_| |
    #  |_| |_| |_|_|___/___/_|_| |_|\__, |   \__,_|\__,_|\__\__,_|
    #                                __/ |_____
    #                               |___/______|
    #
    #  missing_data
    # #############################################################
    tm_missing_data(
      label = "Missing data",
      decorators = list(
        summary_plot = plot_grob_decorator("A", "summary_plot"),
        combination_plot = plot_grob_decorator("B", "combination_plot"),
        summary_table = table_decorator("table", .color1 = "#f0000055"),
        by_subject_plot = caption_decorator("by_subject_plot")
      )
    ),
    example_module(decorators = list(head_decorator(6)))
  )
) |> shiny::runApp()
```

---------

Signed-off-by: Marcin <[email protected]>
Signed-off-by: André Veríssimo <[email protected]>
Co-authored-by: go_gonzo <[email protected]>
Co-authored-by: Konrad Pagacz <[email protected]>
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: 27856297+dependabot-preview[bot]@users.noreply.github.com <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: André Veríssimo <[email protected]>
averissimo added a commit to insightsengineering/teal.modules.clinical that referenced this issue Dec 17, 2024
- Partner to insightsengineering/teal#1357
- Introduces decorators to modules. More about decorators in here
insightsengineering/teal#1384
- Part 1 of
insightsengineering/teal#1371 (comment)

### Changes description

- Adds internal wrapper around `srv_decorate_data` as utility to append
code after decorator _(such as `print(plot)`)_
- Implements decorators in modules

#### Checklist for final review:

Double check check for every module:

- Works with and without decorators
- Has param and section in documentation
- Code shows in "Show R code"
- Reporter shows both the outputs and code

#### Todo on feature branch

- [x] Link the `teal_transform_module` parameter to an extended
explanation as [suggested
here](https://github.com/insightsengineering/teal.modules.clinical/pull/1252/files/a78c0baa0996fb30fdff551bccb7bab0ec86caa6#r1870909229)
- [x] Meet with SME to validate some changes in template, topics:
  - modules with listing/dt
- Merge all modules
- [x] Part 1 of
insightsengineering/teal#1371 (comment)
- [x] Part 2 of
insightsengineering/teal#1371 (comment)
- [x] Accept changes to snapshots in regression testing
#1304

#### Example apps

Not all modules could be used in same App as the examples' data are not
100% compatible. Hence the 2 apps below.

<details>
<summary>Example app</summary>

```r
# Load packages
pkgload::load_all("../teal.modules.clinical", export_all = FALSE)

# Decorators ------------------------------------------------------------------
insert_rrow_decorator <- function(default_caption = "I am a good new row", .var_to_replace = "table") {
  teal_transform_module(
    label = "New rtables row",
    ui = function(id) shiny::textInput(shiny::NS(id, "new_row"), "New row", value = default_caption),
    server = make_teal_transform_server(
      substitute({
        .var_to_replace <- rtables::insert_rrow(.var_to_replace, rtables::rrow(new_row))
      }, env = list(.var_to_replace = as.name(.var_to_replace)))
    )
  )
}
add_title_decorator <- function(default_check = TRUE, .var_to_replace = "plot") {
  teal_transform_module(
    label = "Title",
    ui = function(id) shiny::checkboxInput(NS(id, "flag"), "Add title?", TRUE),
    server = make_teal_transform_server(
      substitute({
        if (flag) .var_to_replace <-
            .var_to_replace + ggplot2::ggtitle("Title added by decorator")
      },
      env = list(.var_to_replace = as.name(.var_to_replace))
      )
    )
  )
}
caption_decorator <- function(default_caption = "I am a good decorator", .var_to_replace = "plot") {
  teal_transform_module(
    label = "Caption",
    ui = function(id) shiny::textInput(shiny::NS(id, "footnote"), "Footnote", value = default_caption),
    server = make_teal_transform_server(
      substitute({
        .var_to_replace <- .var_to_replace + ggplot2::labs(caption = footnote)
      }, env = list(.var_to_replace = as.name(.var_to_replace)))
    )
  )
}
change_theme_decorator <- function(default_check = TRUE, .var_to_replace = "plot") {
  teal_transform_module(
    label = "Theme",
    ui = function(id) shiny::checkboxInput(NS(id, "flag"), "Apply dark theme?", TRUE),
    server = make_teal_transform_server(
      substitute({
        if (flag) .var_to_replace <- .var_to_replace + ggplot2::theme_dark()
      },
      env = list(.var_to_replace = as.name(.var_to_replace))
      )
    )
  )
}
add_cowplot_title_decorator <- function(default_check = TRUE, .var_to_replace = "plot") {
  teal_transform_module(
    label = "Cowplot title",
    ui = function(id) shiny::checkboxInput(NS(id, "flag"), "Add title?", TRUE),
    server = make_teal_transform_server(
      substitute({
        if (flag) .var_to_replace <-
            .var_to_replace +
            ggplot2::ggtitle("Title added by decorator") +
            cowplot::theme_cowplot()
      },
      env = list(.var_to_replace = as.name(.var_to_replace))
      )
    )
  )
}
rlisting_footer <- function(default_footer = "I am a good footer", .var_to_replace = "table_listing") {
  teal_transform_module(
    label = "New row",
    ui = function(id) shiny::textInput(shiny::NS(id, "footer"), "footer", value = default_footer),
    server = make_teal_transform_server(
      substitute({
        rlistings::main_footer(.var_to_replace) <- footer
      }, env = list(.var_to_replace = as.name(.var_to_replace)))
    )
  )
}

# End of decorators -----------------------------------------------------------

library(dplyr)

# arm_ref_comp <- list(ARMCD = list(ref = "ARM B", comp = c("ARM A", "ARM C")))

arm_ref_comp <- list(
  ACTARMCD = list(ref = "ARM B", comp = c("ARM A", "ARM C")),
  ARM = list(ref = "B: Placebo", comp = c("A: Drug X", "C: Combination"))
)

data <- within(teal_data(), {
  ADSL <- tmc_ex_adsl |>
    mutate(ITTFL = factor("Y") |> with_label("Intent-To-Treat Population Flag")) |>
    mutate(DTHFL = case_when(!is.na(DTHDT) ~ "Y", TRUE ~ "") |> with_label("Subject Death Flag"))

  ADAE <- tmc_ex_adae |>
    filter(!((AETOXGR == 1) & (AESEV == "MILD") & (ARM == "A: Drug X")))

  ADAE$ASTDY <- structure(
    as.double(ADAE$ASTDY, unit = attr(ADAE$ASTDY, "units", exact = TRUE)),
    label = attr(ADAE$ASTDY, "label", exact = TRUE)
  )

  .lbls_adae <- col_labels(tmc_ex_adae)
  ADAE <- tmc_ex_adae %>%
    mutate_if(is.character, as.factor) #' be certain of having factors
  col_labels(ADAE) <- .lbls_adae

  ADTTE <- tmc_ex_adtte

  ADLB <- tmc_ex_adlb |>
    mutate(AVISIT == forcats::fct_reorder(AVISIT, AVISITN, min)) |>
    mutate(
      ONTRTFL = case_when(
        AVISIT %in% c("SCREENING", "BASELINE") ~ "",
        TRUE ~ "Y"
      ) |> with_label("On Treatment Record Flag")
    )

  ADVS <- tmc_ex_advs

  ADRS <- tmc_ex_adrs |>
    mutate(
      AVALC = d_onco_rsp_label(AVALC) |>
        with_label("Character Result/Finding")
    ) |>
    filter(PARAMCD != "OVRINV" | AVISIT == "FOLLOW UP") |>
    filter(PARAMCD %in% c("BESRSPI", "INVET"))

  ADAETTE <- tmc_ex_adaette %>%
    filter(PARAMCD %in% c("AETTE1", "AETTE2", "AETTE3")) %>%
    mutate(is_event = CNSR == 0) %>%
    mutate(n_events = as.integer(is_event))

  .add_event_flags <- function(dat) {
    dat <- dat %>%
      mutate(
        TMPFL_SER = AESER == "Y",
        TMPFL_REL = AEREL == "Y",
        TMPFL_GR5 = AETOXGR == "5",
        TMP_SMQ01 = !is.na(SMQ01NAM),
        TMP_SMQ02 = !is.na(SMQ02NAM),
        TMP_CQ01 = !is.na(CQ01NAM)
      )
    column_labels <- list(
      TMPFL_SER = "Serious AE",
      TMPFL_REL = "Related AE",
      TMPFL_GR5 = "Grade 5 AE",
      TMP_SMQ01 = aesi_label(dat[["SMQ01NAM"]], dat[["SMQ01SC"]]),
      TMP_SMQ02 = aesi_label("Y.9.9.9.9/Z.9.9.9.9 AESI"),
      TMP_CQ01 = aesi_label(dat[["CQ01NAM"]])
    )
    col_labels(dat)[names(column_labels)] <- as.character(column_labels)
    dat
  }

  ADEX <- tmc_ex_adex

  set.seed(1, kind = "Mersenne-Twister")
  .labels <- col_labels(ADEX, fill = FALSE)
  ADEX <- ADEX %>%
    distinct(USUBJID, .keep_all = TRUE) %>%
    mutate(
      PARAMCD = "TDURD",
      PARAM = "Overall duration (days)",
      AVAL = sample(x = seq(1, 200), size = n(), replace = TRUE),
      AVALU = "Days"
    ) %>%
    bind_rows(ADEX)
  col_labels(ADEX) <- .labels

  ADCM <- tmc_ex_adcm

  ADMH <- tmc_ex_admh

  ADCM$CMASTDTM <- ADCM$ASTDTM
  ADCM$CMAENDTM <- ADCM$AENDTM

  ADEG <- tmc_ex_adeg

  # smq
  .names_baskets <- grep("^(SMQ|CQ).*NAM$", names(ADAE), value = TRUE)
  .names_scopes <- grep("^SMQ.*SC$", names(ADAE), value = TRUE)

  .cs_baskets <- choices_selected(
    choices = variable_choices(ADAE, subset = .names_baskets),
    selected = .names_baskets
  )

  .cs_scopes <- choices_selected(
    choices = variable_choices(ADAE, subset = .names_scopes),
    selected = .names_scopes,
    fixed = TRUE
  )

  # summary
  ADSL$EOSDY[1] <- NA_integer_
})
join_keys(data) <- default_cdisc_join_keys[names(data)]
adcm_keys <- c("STUDYID", "USUBJID", "ASTDTM", "CMSEQ", "ATC1", "ATC2", "ATC3", "ATC4")
join_keys(data)["ADCM", "ADCM"] <- adcm_keys

# Use in choices selected -----------------------------------------------------

ADSL <- data[["ADSL"]]
ADQS <- data[["ADQS"]]
ADAE <- data[["ADAE"]]
ADTTE <- data[["ADTTE"]]
ADLB <- data[["ADLB"]]
ADAE <- data[["ADAE"]]
ADVS <- data[["ADVS"]]
ADRS <- data[["ADRS"]]
ADAETTE <- data[["ADAETTE"]]
ADEX <- data[["ADEX"]]
ADCM <- data[["ADCM"]]
ADMH <- data[["ADMH"]]
ADEG <- data[["ADEG"]]

# Init ------------------------------------------------------------------------

init(
  data = data,
  modules = modules(
    # -------------------------------------------------------------------------
    tm_t_summary_by(
      label = "Summary by Row Groups Table",
      dataname = "ADLB",
      arm_var = choices_selected(
        choices = variable_choices(ADSL, c("ARM", "ARMCD")),
        selected = "ARM"
      ),
      add_total = TRUE,
      by_vars = choices_selected(
        choices = variable_choices(ADLB, c("PARAM", "AVISIT")),
        selected = c("AVISIT")
      ),
      summarize_vars = choices_selected(
        choices = variable_choices(ADLB, c("AVAL", "CHG")),
        selected = c("AVAL")
      ),
      useNA = "ifany",
      paramcd = choices_selected(
        choices = value_choices(ADLB, "PARAMCD", "PARAM"),
        selected = "ALT"
      ),
      decorators = list(insert_rrow_decorator())
    ),
    # -------------------------------------------------------------------------
    tm_t_smq(
      label = "Adverse Events by SMQ Table",
      dataname = "ADAE",
      arm_var = choices_selected(
        choices = variable_choices(data[["ADSL"]], subset = c("ARM", "SEX")),
        selected = "ARM"
      ),
      add_total = FALSE,
      baskets = data[[".cs_baskets"]],
      scopes = data[[".cs_scopes"]],
      llt = choices_selected(
        choices = variable_choices(data[["ADAE"]], subset = c("AEDECOD")),
        selected = "AEDECOD"
      ),
      decorators = list(insert_rrow_decorator())
    ),
    # -------------------------------------------------------------------------
    tm_t_shift_by_grade(
      label = "Grade Laboratory Abnormality Table",
      dataname = "ADLB",
      arm_var = choices_selected(
        choices = variable_choices(ADSL, subset = c("ARM", "ARMCD")),
        selected = "ARM"
      ),
      paramcd = choices_selected(
        choices = value_choices(ADLB, "PARAMCD", "PARAM"),
        selected = "ALT"
      ),
      worst_flag_var = choices_selected(
        choices = variable_choices(ADLB, subset = c("WGRLOVFL", "WGRLOFL", "WGRHIVFL", "WGRHIFL")),
        selected = c("WGRLOVFL")
      ),
      worst_flag_indicator = choices_selected(
        value_choices(ADLB, "WGRLOVFL"),
        selected = "Y", fixed = TRUE
      ),
      anl_toxgrade_var = choices_selected(
        choices = variable_choices(ADLB, subset = c("ATOXGR")),
        selected = c("ATOXGR"),
        fixed = TRUE
      ),
      base_toxgrade_var = choices_selected(
        choices = variable_choices(ADLB, subset = c("BTOXGR")),
        selected = c("BTOXGR"),
        fixed = TRUE
      ),
      add_total = FALSE,
      decorators = list(insert_rrow_decorator())
    ),
    # -------------------------------------------------------------------------
    tm_t_shift_by_arm(
      label = "Shift by Arm Table",
      dataname = "ADEG",
      arm_var = choices_selected(
        variable_choices(ADSL, subset = c("ARM", "ARMCD")),
        selected = "ARM"
      ),
      paramcd = choices_selected(
        value_choices(ADEG, "PARAMCD"),
        selected = "HR"
      ),
      visit_var = choices_selected(
        value_choices(ADEG, "AVISIT"),
        selected = "POST-BASELINE MINIMUM"
      ),
      aval_var = choices_selected(
        variable_choices(ADEG, subset = "ANRIND"),
        selected = "ANRIND",
        fixed = TRUE
      ),
      baseline_var = choices_selected(
        variable_choices(ADEG, subset = "BNRIND"),
        selected = "BNRIND",
        fixed = TRUE
      ),
      useNA = "ifany",
      decorators = list(insert_rrow_decorator())
    ),
    # -------------------------------------------------------------------------
    tm_t_shift_by_arm_by_worst(
      label = "Shift by Arm Table (by worst)",
      dataname = "ADEG",
      arm_var = choices_selected(
        variable_choices(ADSL, subset = c("ARM", "ARMCD")),
        selected = "ARM"
      ),
      paramcd = choices_selected(
        value_choices(ADEG, "PARAMCD"),
        selected = "ECGINTP"
      ),
      worst_flag_var = choices_selected(
        variable_choices(ADEG, c("WORS02FL", "WORS01FL")),
        selected = "WORS02FL"
      ),
      worst_flag = choices_selected(
        value_choices(ADEG, "WORS02FL"),
        selected = "Y",
        fixed = TRUE
      ),
      aval_var = choices_selected(
        variable_choices(ADEG, c("AVALC", "ANRIND")),
        selected = "AVALC"
      ),
      baseline_var = choices_selected(
        variable_choices(ADEG, c("BASEC", "BNRIND")),
        selected = "BASEC"
      ),
      useNA = "ifany",
      decorators = list(insert_rrow_decorator())
    ),
    # -------------------------------------------------------------------------
    tm_t_pp_prior_medication(
      label = "Prior Medication",
      dataname = "ADCM",
      parentname = "ADSL",
      patient_col = "USUBJID",
      atirel = choices_selected(
        choices = variable_choices(ADCM, "ATIREL"),
        selected = "ATIREL"
      ),
      cmdecod = choices_selected(
        choices = variable_choices(ADCM, "CMDECOD"),
        selected = "CMDECOD"
      ),
      cmindc = choices_selected(
        choices = variable_choices(ADCM, "CMINDC"),
        selected = "CMINDC"
      ),
      cmstdy = choices_selected(
        choices = variable_choices(ADCM, "ASTDY"),
        selected = "ASTDY"
      ),
      decorators = list(
        table = rlisting_footer(.var_to_replace = "table")
      )
    ),
    # -------------------------------------------------------------------------
    tm_t_pp_medical_history(
      label = "Medical History",
      dataname = "ADMH",
      parentname = "ADSL",
      patient_col = "USUBJID",
      mhterm = choices_selected(
        choices = variable_choices(ADMH, c("MHTERM")),
        selected = "MHTERM"
      ),
      mhbodsys = choices_selected(
        choices = variable_choices(ADMH, "MHBODSYS"),
        selected = "MHBODSYS"
      ),
      mhdistat = choices_selected(
        choices = variable_choices(ADMH, "MHDISTAT"),
        selected = "MHDISTAT"
      ),
      decorators = list(insert_rrow_decorator())
    ),
    # -------------------------------------------------------------------------
    tm_t_pp_laboratory(
      label = "Vitals",
      dataname = "ADLB",
      patient_col = "USUBJID",
      paramcd = choices_selected(
        choices = variable_choices(ADLB, "PARAMCD"),
        selected = "PARAMCD"
      ),
      param = choices_selected(
        choices = variable_choices(ADLB, "PARAM"),
        selected = "PARAM"
      ),
      timepoints = choices_selected(
        choices = variable_choices(ADLB, "ADY"),
        selected = "ADY"
      ),
      anrind = choices_selected(
        choices = variable_choices(ADLB, "ANRIND"),
        selected = "ANRIND"
      ),
      aval_var = choices_selected(
        choices = variable_choices(ADLB, "AVAL"),
        selected = "AVAL"
      ),
      avalu_var = choices_selected(
        choices = variable_choices(ADLB, "AVALU"),
        selected = "AVALU"
      ),
      decorators = list(table = rlisting_footer(.var_to_replace = "table"))
    ),
    # -------------------------------------------------------------------------
    tm_t_pp_basic_info(
      label = "Basic Info",
      dataname = "ADSL",
      patient_col = "USUBJID",
      vars = choices_selected(choices = variable_choices(ADSL), selected = c("ARM", "AGE", "SEX", "COUNTRY", "RACE", "EOSSTT"))
      , decorators = list(
        table = rlisting_footer(.var_to_replace = "table")
      )
    ),
    # -------------------------------------------------------------------------
    tm_t_mult_events(
      label = "Concomitant Medications by Medication Class and Preferred Name",
      dataname = "ADCM",
      arm_var = choices_selected(c("ARM", "ARMCD"), "ARM"),
      seq_var = choices_selected("CMSEQ", selected = "CMSEQ", fixed = TRUE),
      hlt = choices_selected(
        choices = variable_choices(ADCM, c("ATC1", "ATC2", "ATC3", "ATC4")),
        selected = c("ATC1", "ATC2", "ATC3", "ATC4")
      ),
      llt = choices_selected(choices = variable_choices(ADCM, c("CMDECOD")), selected = c("CMDECOD")),
      add_total = TRUE,
      event_type = "treatment",
      decorators = list(insert_rrow_decorator())
    ),
    # -------------------------------------------------------------------------
    tm_t_logistic(
      label = "Logistic Regression",
      dataname = "ADRS",
      arm_var = choices_selected(
        choices = variable_choices(ADRS, c("ARM", "ARMCD")),
        selected = "ARM"
      ),
      arm_ref_comp = arm_ref_comp,
      paramcd = choices_selected(
        choices = value_choices(ADRS, "PARAMCD", "PARAM"),
        selected = "BESRSPI"
      ),
      cov_var = choices_selected(
        choices = c("SEX", "AGE", "BMRKR1", "BMRKR2"),
        selected = "SEX"
      ),
      decorators = list(insert_rrow_decorator())
    ),
    # -------------------------------------------------------------------------
    tm_t_exposure(
      label = "Duration of Exposure Table",
      dataname = "ADEX",
      paramcd = choices_selected(
        choices = value_choices(data[["ADEX"]], "PARAMCD", "PARAM"),
        selected = "TDURD"
      ),
      col_by_var = choices_selected(
        choices = variable_choices(data[["ADEX"]], subset = c("SEX", "ARM")),
        selected = "SEX"
      ),
      row_by_var = choices_selected(
        choices = variable_choices(data[["ADEX"]], subset = c("RACE", "REGION1", "STRATA1", "SEX")),
        selected = "RACE"
      ),
      parcat = choices_selected(
        choices = value_choices(data[["ADEX"]], "PARCAT2"),
        selected = "Drug A"
      ),
      add_total = FALSE,
      decorators = list(insert_rrow_decorator())
    ),
    # -------------------------------------------------------------------------
    tm_t_events(
      label = "Adverse Event Table",
      dataname = "ADAE",
      arm_var = choices_selected(c("ARM", "ARMCD"), "ARM"),
      llt = choices_selected(
        choices = variable_choices(ADAE, c("AETERM", "AEDECOD")),
        selected = c("AEDECOD")
      ),
      hlt = choices_selected(
        choices = variable_choices(ADAE, c("AEBODSYS", "AESOC")),
        selected = "AEBODSYS"
      ),
      add_total = TRUE,
      event_type = "adverse event",
      decorators = list(insert_rrow_decorator())
    ),
    # -------------------------------------------------------------------------
    tm_t_events_patyear(
      label = "AE Rate Adjusted for Patient-Years At Risk Table",
      dataname = "ADAETTE",
      arm_var = choices_selected(
        choices = variable_choices(ADSL, c("ARM", "ARMCD")),
        selected = "ARMCD"
      ),
      add_total = TRUE,
      events_var = choices_selected(
        choices = variable_choices(ADAETTE, "n_events"),
        selected = "n_events",
        fixed = TRUE
      ),
      paramcd = choices_selected(
        choices = value_choices(ADAETTE, "PARAMCD", "PARAM"),
        selected = "AETTE1"
      ),
      decorators = list(insert_rrow_decorator())
    ),
    # -------------------------------------------------------------------------
    tm_t_events_by_grade(
      label = "Adverse Events by Grade Table",
      dataname = "ADAE",
      arm_var = choices_selected(c("ARM", "ARMCD"), "ARM"),
      llt = choices_selected(
        choices = variable_choices(ADAE, c("AETERM", "AEDECOD")),
        selected = c("AEDECOD")
      ),
      hlt = choices_selected(
        choices = variable_choices(ADAE, c("AEBODSYS", "AESOC")),
        selected = "AEBODSYS"
      ),
      grade = choices_selected(
        choices = variable_choices(ADAE, c("AETOXGR", "AESEV")),
        selected = "AETOXGR"
      ),
      decorators = list(insert_rrow_decorator())
    ),
    # -------------------------------------------------------------------------
    tm_t_coxreg(
      label = "Cox Reg.",
      dataname = "ADTTE",
      arm_var = choices_selected(c("ARM", "ARMCD", "ACTARMCD"), "ARM"),
      arm_ref_comp = arm_ref_comp,
      paramcd = choices_selected(
        value_choices(ADTTE, "PARAMCD", "PARAM"), "OS"
      ),
      strata_var = choices_selected(
        c("COUNTRY", "STRATA1", "STRATA2"), "STRATA1"
      ),
      cov_var = choices_selected(
        c("AGE", "BMRKR1", "BMRKR2", "REGION1"), "AGE"
      ),
      multivariate = TRUE,
      decorators = list(insert_rrow_decorator())
    ),
    # -------------------------------------------------------------------------
    tm_t_abnormality(
      label = "Abnormality Table",
      dataname = "ADLB",
      arm_var = choices_selected(
        choices = variable_choices(ADSL, subset = c("ARM", "ARMCD")),
        selected = "ARM"
      ),
      add_total = FALSE,
      by_vars = choices_selected(
        choices = variable_choices(ADLB, subset = c("LBCAT", "PARAM", "AVISIT")),
        selected = c("LBCAT", "PARAM"),
        keep_order = TRUE
      ),
      baseline_var = choices_selected(
        variable_choices(ADLB, subset = "BNRIND"),
        selected = "BNRIND", fixed = TRUE
      ),
      grade = choices_selected(
        choices = variable_choices(ADLB, subset = "ANRIND"),
        selected = "ANRIND",
        fixed = TRUE
      ),
      abnormal = list(low = "LOW", high = "HIGH"),
      exclude_base_abn = FALSE,
      decorators = list(insert_rrow_decorator("I am a good new row"))
    ),
    # -------------------------------------------------------------------------
    tm_g_pp_vitals(
      label = "Vitals",
      dataname = "ADVS",
      parentname = "ADSL",
      patient_col = "USUBJID",
      plot_height = c(600L, 200L, 2000L),
      paramcd = choices_selected(
        choices = variable_choices(ADVS, "PARAMCD"),
        selected = "PARAMCD"
      ),
      xaxis = choices_selected(
        choices = variable_choices(ADVS, "ADY"),
        selected = "ADY"
      ),
      aval_var = choices_selected(
        choices = variable_choices(ADVS, "AVAL"),
        selected = "AVAL"
      ),
      decorators = list(plot = add_title_decorator("plot"))
    ),
    # -------------------------------------------------------------------------
    tm_g_pp_adverse_events(
      label = "Adverse Events",
      dataname = "ADAE",
      parentname = "ADSL",
      patient_col = "USUBJID",
      plot_height = c(600L, 200L, 2000L),
      aeterm = choices_selected(
        choices = variable_choices(ADAE, "AETERM"),
        selected = "AETERM"
      ),
      tox_grade = choices_selected(
        choices = variable_choices(ADAE, "AETOXGR"),
        selected = "AETOXGR"
      ),
      causality = choices_selected(
        choices = variable_choices(ADAE, "AEREL"),
        selected = "AEREL"
      ),
      outcome = choices_selected(
        choices = variable_choices(ADAE, "AEOUT"),
        selected = "AEOUT"
      ),
      action = choices_selected(
        choices = variable_choices(ADAE, "AEACN"),
        selected = "AEACN"
      ),
      time = choices_selected(
        choices = variable_choices(ADAE, "ASTDY"),
        selected = "ASTDY"
      ),
      decod = NULL,
      decorators = list(
        plot = caption_decorator('I am a good caption', 'plot'),
        table = rlisting_footer(.var_to_replace = 'table')
      )
    ),
    # -------------------------------------------------------------------------
    tm_g_lineplot(
      label = "Line Plot",
      dataname = "ADLB",
      strata = choices_selected(
        variable_choices(ADSL, c("ARM", "ARMCD", "ACTARMCD")),
        "ARM"
      ),
      y = choices_selected(
        variable_choices(ADLB, c("AVAL", "BASE", "CHG", "PCHG")),
        "AVAL"
      ),
      param = choices_selected(
        value_choices(ADLB, "PARAMCD", "PARAM"),
        "ALT"
      ),
      decorators = list(add_cowplot_title_decorator("plot"))
    ),
    # -------------------------------------------------------------------------
    tm_g_km(
      label = "Kaplan-Meier Plot",
      dataname = "ADTTE",
      arm_var = choices_selected(
        variable_choices(ADSL, c("ARM", "ARMCD", "ACTARMCD")),
        "ARM"
      ),
      paramcd = choices_selected(
        value_choices(ADTTE, "PARAMCD", "PARAM"),
        "OS"
      ),
      arm_ref_comp = arm_ref_comp,
      strata_var = choices_selected(
        variable_choices(ADSL, c("SEX", "BMRKR2")),
        "SEX"
      ),
      facet_var = choices_selected(
        variable_choices(ADSL, c("SEX", "BMRKR2")),
        NULL
      ),
      decorators = list(plot = add_cowplot_title_decorator(TRUE, "plot"))
    ),
    # -------------------------------------------------------------------------
    tm_g_barchart_simple(
      label = "ADAE Analysis",
      x = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          choices = variable_choices(
            ADSL,
            c(
              "ARM", "ACTARM", "SEX",
              "RACE", "ITTFL", "SAFFL", "STRATA2"
            )
          ),
          selected = "ACTARM",
          multiple = FALSE
        )
      ),
      fill = list(
        data_extract_spec(
          dataname = "ADSL",
          select = select_spec(
            choices = variable_choices(
              ADSL,
              c(
                "ARM", "ACTARM", "SEX",
                "RACE", "ITTFL", "SAFFL", "STRATA2"
              )
            ),
            selected = "SEX",
            multiple = FALSE
          )
        ),
        data_extract_spec(
          dataname = "ADAE",
          select = select_spec(
            choices = variable_choices(ADAE, c("AETOXGR", "AESEV", "AESER")),
            selected = NULL,
            multiple = FALSE
          )
        )
      ),
      x_facet = list(
        data_extract_spec(
          dataname = "ADAE",
          select = select_spec(
            choices = variable_choices(ADAE, c("AETOXGR", "AESEV", "AESER")),
            selected = "AETOXGR",
            multiple = FALSE
          )
        ),
        data_extract_spec(
          dataname = "ADSL",
          select = select_spec(
            choices = variable_choices(
              ADSL,
              c(
                "ARM", "ACTARM", "SEX",
                "RACE", "ITTFL", "SAFFL", "STRATA2"
              )
            ),
            selected = NULL,
            multiple = FALSE
          )
        )
      ),
      y_facet = list(
        data_extract_spec(
          dataname = "ADAE",
          select = select_spec(
            choices = variable_choices(ADAE, c("AETOXGR", "AESEV", "AESER")),
            selected = "AESEV",
            multiple = FALSE
          )
        ),
        data_extract_spec(
          dataname = "ADSL",
          select = select_spec(
            choices = variable_choices(
              ADSL,
              c(
                "ARM", "ACTARM", "SEX",
                "RACE", "ITTFL", "SAFFL", "STRATA2"
              )
            ),
            selected = NULL,
            multiple = FALSE
          )
        )
      ),
      decorators = list(plot = caption_decorator('The best', 'plot'))
    )
  )
) |> shiny::runApp()

```

</details>    

<details>
<summary>Second App</summary>

```r
# Load packages
pkgload::load_all("../teal.modules.clinical", export_all = FALSE)
# Example below

insert_rrow_decorator <- function(default_caption = "I am a good new row", .var_to_replace = "table") {
  teal_transform_module(
    label = "New row",
    ui = function(id) shiny::textInput(shiny::NS(id, "new_row"), "New row", value = default_caption),
    server = make_teal_transform_server(
      substitute({
        .var_to_replace <- rtables::insert_rrow(.var_to_replace, rtables::rrow(new_row))
      }, env = list(.var_to_replace = as.name(.var_to_replace)))
    )
  )
}

# Preparation of the test case - use `EOSDY` and `DCSREAS` variables to demonstrate missing data.
data <- teal_data()
data <- within(data, {
  ADSL <- tmc_ex_adsl |>
    mutate(
      DTHFL = case_when(
        !is.na(DTHDT) ~ "Y",
        TRUE ~ ""
      ) %>% with_label("Subject Death Flag")
    )
  ADSL$EOSDY[1] <- NA_integer_

  ADAE <- tmc_ex_adae

  .add_event_flags <- function(dat) {
    dat <- dat %>%
      mutate(
        TMPFL_SER = AESER == "Y",
        TMPFL_REL = AEREL == "Y",
        TMPFL_GR5 = AETOXGR == "5",
        TMP_SMQ01 = !is.na(SMQ01NAM),
        TMP_SMQ02 = !is.na(SMQ02NAM),
        TMP_CQ01 = !is.na(CQ01NAM)
      )
    column_labels <- list(
      TMPFL_SER = "Serious AE",
      TMPFL_REL = "Related AE",
      TMPFL_GR5 = "Grade 5 AE",
      TMP_SMQ01 = aesi_label(dat[["SMQ01NAM"]], dat[["SMQ01SC"]]),
      TMP_SMQ02 = aesi_label("Y.9.9.9.9/Z.9.9.9.9 AESI"),
      TMP_CQ01 = aesi_label(dat[["CQ01NAM"]])
    )
    col_labels(dat)[names(column_labels)] <- as.character(column_labels)
    dat
  }

  #' Generating user-defined event flags.
  ADAE <- ADAE %>% .add_event_flags()

  .ae_anl_vars <- names(ADAE)[startsWith(names(ADAE), "TMPFL_")]
  .aesi_vars <- names(ADAE)[startsWith(names(ADAE), "TMP_")]

  ADTTE <- tmc_ex_adtte

  # responder

  ADRS <- tmc_ex_adrs %>%
    mutate(
      AVALC = d_onco_rsp_label(AVALC) %>%
        with_label("Character Result/Finding")
    ) %>%
    filter(PARAMCD != "OVRINV" | AVISIT == "FOLLOW UP")


  ADQS <- tmc_ex_adqs %>%
    filter(ABLFL != "Y" & ABLFL2 != "Y") %>%
    filter(AVISIT %in% c("WEEK 1 DAY 8", "WEEK 2 DAY 15", "WEEK 3 DAY 22")) %>%
    mutate(
      AVISIT = as.factor(AVISIT),
      AVISITN = rank(AVISITN) %>%
        as.factor() %>%
        as.numeric() %>%
        as.factor() #' making consecutive numeric factor
    )
})
join_keys(data) <- default_cdisc_join_keys[names(data)]

ADSL <- data[["ADSL"]]
ADRS <- data[["ADRS"]]

app <- init(
  data = data,
  modules = modules(
    # -------------------------------------------------------------------------
    tm_a_mmrm(
      label = "MMRM",
      dataname = "ADQS",
      aval_var = choices_selected(c("AVAL", "CHG"), "AVAL"),
      id_var = choices_selected(c("USUBJID", "SUBJID"), "USUBJID"),
      arm_var = choices_selected(c("ARM", "ARMCD"), "ARM"),
      visit_var = choices_selected(c("AVISIT", "AVISITN"), "AVISIT"),
      arm_ref_comp = arm_ref_comp,
      paramcd = choices_selected(
        choices = value_choices(data[["ADQS"]], "PARAMCD", "PARAM"),
        selected = "FKSI-FWB"
      ),
      cov_var = choices_selected(c("BASE", "AGE", "SEX", "BASE:AVISIT"), NULL)
      , decorators = list(
        lsmeans_table = insert_rrow_decorator("A", .var_to_replace = "lsmeans_table")
        , lsmeans_plot = add_title_decorator("B", .var_to_replace = "lsmeans_plot")
        , covariance_table = insert_rrow_decorator("C", .var_to_replace = "covariance_table")
        , fixed_effects_table = insert_rrow_decorator("D", .var_to_replace = "fixed_effects_table")
        , diagnostic_table = insert_rrow_decorator(.var_to_replace = "diagnostic_table")
        , diagnostic_plot = add_title_decorator(.var_to_replace = "diagnostic_plot")
      )
    ),
    # -------------------------------------------------------------------------
    tm_t_binary_outcome(
      label = "Responders",
      dataname = "ADRS",
      paramcd = choices_selected(
        choices = value_choices(ADRS, "PARAMCD", "PARAM"),
        selected = "BESRSPI"
      ),
      arm_var = choices_selected(
        choices = variable_choices(ADRS, c("ARM", "ARMCD", "ACTARMCD")),
        selected = "ARM"
      ),
      arm_ref_comp = arm_ref_comp,
      strata_var = choices_selected(
        choices = variable_choices(ADRS, c("SEX", "BMRKR2", "RACE")),
        selected = "RACE"
      ),
      default_responses = list(
        BESRSPI = list(
          rsp = c("Complete Response (CR)", "Partial Response (PR)"),
          levels = c(
            "Complete Response (CR)", "Partial Response (PR)",
            "Stable Disease (SD)", "Progressive Disease (PD)"
          )
        ),
        INVET = list(
          rsp = c("Stable Disease (SD)", "Not Evaluable (NE)"),
          levels = c(
            "Complete Response (CR)", "Not Evaluable (NE)", "Partial Response (PR)",
            "Progressive Disease (PD)", "Stable Disease (SD)"
          )
        ),
        OVRINV = list(
          rsp = c("Progressive Disease (PD)", "Stable Disease (SD)"),
          levels = c("Progressive Disease (PD)", "Stable Disease (SD)", "Not Evaluable (NE)")
        )
      ),
      decorators = list(insert_rrow_decorator("I am a new row"))
    ),
    # -------------------------------------------------------------------------
    tm_t_events_summary(
      label = "Adverse Events Summary",
      dataname = "ADAE",
      arm_var = choices_selected(
        choices = variable_choices("ADSL", c("ARM", "ARMCD")),
        selected = "ARM"
      ),
      flag_var_anl = choices_selected(
        choices = variable_choices("ADAE", data[[".ae_anl_vars"]]),
        selected = data[[".ae_anl_vars"]][1],
        keep_order = TRUE,
        fixed = FALSE
      ),
      flag_var_aesi = choices_selected(
        choices = variable_choices("ADAE", data[[".aesi_vars"]]),
        selected = data[[".aesi_vars"]][1],
        keep_order = TRUE,
        fixed = FALSE
      ),
      add_total = TRUE,
      decorators = list(insert_rrow_decorator())
    ),
    # -------------------------------------------------------------------------
    tm_t_summary(
      label = "Demographic Table",
      dataname = "ADSL",
      arm_var = choices_selected(c("ARM", "ARMCD"), "ARM"),
      add_total = TRUE,
      summarize_vars = choices_selected(
        c("SEX", "RACE", "BMRKR2", "EOSDY", "DCSREAS", "AGE"),
        c("SEX", "RACE")
      ),
      useNA = "ifany",
      decorators = list(insert_rrow_decorator())
    )
  )
) |> shiny::runApp()
```

</details>

---------

Signed-off-by: Marcin <[email protected]>
Signed-off-by: Lluís Revilla <[email protected]>
Signed-off-by: André Veríssimo <[email protected]>
Co-authored-by: 27856297+dependabot-preview[bot]@users.noreply.github.com <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: Marcin <[email protected]>
Co-authored-by: m7pr <[email protected]>
Co-authored-by: Lluís Revilla <[email protected]>
Co-authored-by: Lluís Revilla <[email protected]>
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Done
Development

No branches or pull requests

3 participants