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

1441 Introduce transformators parameter in modules #826

Merged
merged 25 commits into from
Jan 20, 2025
Merged

Conversation

m7pr
Copy link
Contributor

@m7pr m7pr commented Jan 14, 2025

insightsengineering/teal#1441

Included transformators parameter in modules. This is passed to teal::module that does assertions and checks on this parameter.

Did not introduce for tm_data_table, tm_file_viewer and tm_front_page as we also didn't introduce decorators for those modules. We can always introduce, if there is a user request, but I don't think there will ever be.

Lastly, a food for thought: default decorators value is NULL where default transformators value is list(). Maybe we can introduce a check, that allows transformators to be NULL on default, and which changes NULL to list().

@m7pr m7pr changed the title Introduce transformators parameter in modules 1441 Introduce transformators parameter in modules Jan 14, 2025
@m7pr m7pr added the core label Jan 14, 2025
@m7pr m7pr requested a review from llrs-roche January 14, 2025 14:18
m7pr and others added 10 commits January 14, 2025 15:19
Merge branch '1441_transformators@main' of https://github.com/insightsengineering/teal.modules.general into 1441_transformators@main

# Conflicts:
#	man/tm_a_pca.Rd
#	man/tm_a_regression.Rd
#	man/tm_g_association.Rd
#	man/tm_g_bivariate.Rd
#	man/tm_g_distribution.Rd
#	man/tm_g_response.Rd
#	man/tm_g_scatterplot.Rd
#	man/tm_g_scatterplotmatrix.Rd
#	man/tm_missing_data.Rd
#	man/tm_outliers.Rd
#	man/tm_t_crosstable.Rd
Copy link
Contributor

github-actions bot commented Jan 14, 2025

Unit Tests Summary

  1 files   22 suites   13m 14s ⏱️
145 tests 107 ✅ 38 💤 0 ❌
476 runs  438 ✅ 38 💤 0 ❌

Results for commit 1764c3e.

♻️ This comment has been updated with latest results.

Copy link
Contributor

github-actions bot commented Jan 14, 2025

Unit Test Performance Difference

Test Suite $Status$ Time on main $±Time$ $±Tests$ $±Skipped$ $±Failures$ $±Errors$
examples 💚 $5.16$ $-4.57$ $-3$ $+38$ $0$ $0$
shinytest2-tm_a_regression 💔 $51.97$ $+1.30$ $0$ $0$ $0$ $0$
shinytest2-tm_g_scatterplot 💔 $73.07$ $+1.28$ $0$ $0$ $0$ $0$
shinytest2-tm_outliers 💔 $111.47$ $+1.56$ $0$ $0$ $0$ $0$
shinytest2-tm_variable_browser 💔 $58.12$ $+1.64$ $0$ $0$ $0$ $0$
Additional test case details
Test Suite $Status$ Time on main $±Time$ Test Case
examples 💚 $1.72$ $-1.64$ example_add_facet_labels.Rd

Results for commit de74d65

♻️ This comment has been updated with latest results.

@llrs-roche llrs-roche self-assigned this Jan 14, 2025
Copy link
Contributor

@llrs-roche llrs-roche left a comment

Choose a reason for hiding this comment

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

There are 15 modules:

ls -1 *.R | grep "tm_*"
tm_a_pca.Ro
tm_a_regression.R
tm_data_table.R
tm_file_viewer.R
tm_front_page.R
tm_g_association.R
tm_g_bivariate.R
tm_g_distribution.R
tm_g_response.R
tm_g_scatterplot.R
tm_g_scatterplotmatrix.R
tm_missing_data.R
tm_outliers.R
tm_t_crosstable.R
tm_variable_browser.R

Minus the three mentioned as not implemented: tm_data_table, tm_file_viewer and tm_front_page, this leaves tm_variable_browser as not implemented and not commented on. But as it doesn't have decorators I guess it falls on the same list.

About the difference between the defaults on transformators and decorators, I think it makes sense to have the same default. The suggested idea could work well with minimal changes.

@m7pr
Copy link
Contributor Author

m7pr commented Jan 15, 2025

About the difference between the defaults on transformators and decorators, I think it makes sense to have the same default. The suggested idea could work well with minimal changes.

@llrs-roche I was thinking about changing defaults for decorators parameter to list() hence we now have transformators = list(). Previously we specified decorators = NULL to be default, because all other parameters are NULL when they are empty. However with the addition of transformators, we can now have two parameters to be set to list() by default.

What are your thoughts on this @gogonzo ? Should we unify decorators and transformators default values? First is now set to NULL and the other to list() by default.

@llrs-roche
Copy link
Contributor

My 2 cents: On one hand I like that these two arguments doing similar things have similar arguments default that differentiate from the others. On the other hand I would prefer to have NULL as default, but list seems like a good indicator that more than one can be provided 👍

@gogonzo
Copy link
Contributor

gogonzo commented Jan 16, 2025

About the difference between the defaults on transformators and decorators, I think it makes sense to have the same default. The suggested idea could work well with minimal changes.

@llrs-roche I was thinking about changing defaults for decorators parameter to list() hence we now have transformators = list(). Previously we specified decorators = NULL to be default, because all other parameters are NULL when they are empty. However with the addition of transformators, we can now have two parameters to be set to list() by default.

What are your thoughts on this @gogonzo ? Should we unify decorators and transformators default values? First is now set to NULL and the other to list() by default.

transformators has to be a list otherwise teal will fail. decorators are inheriting from the transformer so I don't see any reason why their defaults differ. NULL possible only when you make a change in teal to accept transformers as NULL.

@m7pr
Copy link
Contributor Author

m7pr commented Jan 16, 2025

Thanks for backing up the discussion. I'll proceed with changing decorators defaults to list()

R/utils.R Show resolved Hide resolved
@m7pr
Copy link
Contributor Author

m7pr commented Jan 17, 2025

Example with no decorators and no transformators
# general data example
data <- teal_data()
data <- within(data, {
  require(nestcolor)
  USArrests <- USArrests
})

app <- init(
  data = data,
  modules = modules(
    tm_a_pca(
      "PCA",
      dat = data_extract_spec(
        dataname = "USArrests",
        select = select_spec(
          choices = variable_choices(
            data = data[["USArrests"]], c("Murder", "Assault", "UrbanPop", "Rape")
          ),
          selected = c("Murder", "Assault"),
          multiple = TRUE
        ),
        filter = NULL
      )
    )
  )
)
if (interactive()) {
  shinyApp(app$ui, app$server)
}
Example with decorators and no transformators
footnote_dec <- teal_transform_module(
  label = "Footnote",
  ui = function(id) shiny::textInput(shiny::NS(id, "footnote"), "Footnote", value = "I am a good decorator"),
  server = function(id, data) {
    moduleServer(id, function(input, output, session) {
      logger::log_info("🟢 Footnote called to action!", namespace = "teal.modules.general")
      reactive(
        within(
          data(),
          {
            footnote_str <- footnote
            plot <- plot + ggplot2::labs(caption = footnote_str)
          },
          footnote = input$footnote
        )
      )
    })
  }
)


title_plot <- teal_transform_module(
  server = make_teal_transform_server(
    expression(
      logger::log_info("🔴 Title being called to action!", namespace = "teal.modules.general"),
      plot <- plot + ggplot2::ggtitle("A title to the plot")
    )
  )
)

# CDISC data example
data <- teal_data()
data <- within(data, {
  require(nestcolor)
  ADSL <- rADSL
})
join_keys(data) <- default_cdisc_join_keys[names(data)]

app <- init(
  data = data,
  modules = modules(
    tm_g_response(
      label = "Response Plots",
      decorators = list(footnote_dec, title_plot),
      response = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          label = "Select variable:",
          choices = variable_choices(data[["ADSL"]], c("BMRKR2", "COUNTRY")),
          selected = "BMRKR2",
          multiple = FALSE,
          fixed = FALSE
        )
      ),
      x = data_extract_spec(
        dataname = "ADSL",
        select = select_spec(
          label = "Select variable:",
          choices = variable_choices(data[["ADSL"]], c("SEX", "RACE")),
          selected = "RACE",
          multiple = FALSE,
          fixed = FALSE
        )
      )
    )
  )
)
if (interactive()) {
  shinyApp(app$ui, app$server)
}
Example with no decorators and with transformators
# general data example
data <- teal_data()
data <- within(data, {
  require(nestcolor)
  CO2 <- data.frame(CO2)
})

head_transformator <- 
  teal_transform_module(
    label = "Custom transformator for CO2",
    ui = function(id) {
      ns <- NS(id)
      tags$div(
        numericInput(ns("n_rows"), "Number of rows to subset", value = 6, min = 1, max = 150, step = 1)
      )
    },
    server = function(id, data) {
      moduleServer(id, function(input, output, session) {
        reactive({
          within(data(),
                 {
                   CO2 <- head(CO2, num_rows)
                 },
                 num_rows = input$n_rows
          )
        })
      })
    }
  )

app <- init(
  data = data,
  modules = tm_g_bivariate(
    transformators = list(head_transformator),
    x = data_extract_spec(
      dataname = "CO2",
      select = select_spec(
        label = "Select variable:",
        choices = variable_choices(data[["CO2"]]),
        selected = "conc",
        fixed = FALSE
      )
    ),
    y = data_extract_spec(
      dataname = "CO2",
      select = select_spec(
        label = "Select variable:",
        choices = variable_choices(data[["CO2"]]),
        selected = "uptake",
        multiple = FALSE,
        fixed = FALSE
      )
    ),
    row_facet = data_extract_spec(
      dataname = "CO2",
      select = select_spec(
        label = "Select variable:",
        choices = variable_choices(data[["CO2"]]),
        selected = "Type",
        fixed = FALSE
      )
    ),
    col_facet = data_extract_spec(
      dataname = "CO2",
      select = select_spec(
        label = "Select variable:",
        choices = variable_choices(data[["CO2"]]),
        selected = "Treatment",
        fixed = FALSE
      )
    )
  )
)
if (interactive()) {
  shinyApp(app$ui, app$server)
}
Example with with decorators and with transformators
head_transformator <- 
  teal_transform_module(
    label = "Custom transformator for CO2",
    ui = function(id) {
      ns <- NS(id)
      tags$div(
        numericInput(ns("n_rows"), "Number of rows to subset", value = 6, min = 1, max = 150, step = 1)
      )
    },
    server = function(id, data) {
      moduleServer(id, function(input, output, session) {
        reactive({
          within(data(),
                 {
                   CO2 <- head(CO2, num_rows)
                 },
                 num_rows = input$n_rows
          )
        })
      })
    }
  )


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)
            )))
        })
      })
    }
  )
}

# general data example
data <- teal_data()
data <- within(data, {
  require(nestcolor)
  CO2 <- CO2
  factors <- names(Filter(isTRUE, vapply(CO2, is.factor, logical(1L))))
  CO2[factors] <- lapply(CO2[factors], as.character)
})

app <- init(
  data = data,
  modules = modules(
    tm_g_association(
      ref = data_extract_spec(
        dataname = "CO2",
        select = select_spec(
          label = "Select variable:",
          choices = variable_choices(data[["CO2"]], c("Plant", "Type", "Treatment")),
          selected = "Plant",
          fixed = FALSE
        )
      ),
      vars = data_extract_spec(
        dataname = "CO2",
        select = select_spec(
          label = "Select variables:",
          choices = variable_choices(data[["CO2"]], c("Plant", "Type", "Treatment")),
          selected = "Treatment",
          multiple = TRUE,
          fixed = FALSE
        )
      ),
      transformators = list(head_transformator),
      decorators = list(plot_grob_decorator("I am a good grob (association)"))
    )
  )
)
if (interactive()) {
  shinyApp(app$ui, app$server)
}
Example with with decorators and with transformators AND DECORATORS IS NAMED
head_transformator <- 
  teal_transform_module(
    label = "Custom transformator for CO2",
    ui = function(id) {
      ns <- NS(id)
      tags$div(
        numericInput(ns("n_rows"), "Number of rows to subset", value = 6, min = 1, max = 150, step = 1)
      )
    },
    server = function(id, data) {
      moduleServer(id, function(input, output, session) {
        reactive({
          within(data(),
                 {
                   CO2 <- head(CO2, num_rows)
                 },
                 num_rows = input$n_rows
          )
        })
      })
    }
  )


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)
            )))
        })
      })
    }
  )
}

# general data example
data <- teal_data()
data <- within(data, {
  require(nestcolor)
  CO2 <- CO2
  factors <- names(Filter(isTRUE, vapply(CO2, is.factor, logical(1L))))
  CO2[factors] <- lapply(CO2[factors], as.character)
})

app <- init(
  data = data,
  modules = modules(
    tm_g_association(
      ref = data_extract_spec(
        dataname = "CO2",
        select = select_spec(
          label = "Select variable:",
          choices = variable_choices(data[["CO2"]], c("Plant", "Type", "Treatment")),
          selected = "Plant",
          fixed = FALSE
        )
      ),
      vars = data_extract_spec(
        dataname = "CO2",
        select = select_spec(
          label = "Select variables:",
          choices = variable_choices(data[["CO2"]], c("Plant", "Type", "Treatment")),
          selected = "Treatment",
          multiple = TRUE,
          fixed = FALSE
        )
      ),
      transformators = list(head_transformator),
      decorators = list(plot = plot_grob_decorator("I am a good grob (association)"))
    )
  )
)
if (interactive()) {
  shinyApp(app$ui, app$server)
}

@m7pr
Copy link
Contributor Author

m7pr commented Jan 17, 2025

@llrs-roche would you mind revisiting PR?

Copy link
Contributor

@llrs-roche llrs-roche left a comment

Choose a reason for hiding this comment

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

All the other changes since my previous feedback look good. I left a minor comment to help users.

Not sure of the implications of the changes on normalize_decorators but I didn't notice any problem with the changes.

I tested adding two decorators with the same name (to see what would happen):

decorators = list(plot = plot_grob_decorator("I am a good grob (association)"),
                        plot= plot_grob_decorator("I am a good grob 2 (association)"))

The module uses only the first (and doesn't warn about removing the second). I don't think this was covered initially, but maybe it is a good time to consider/add it?

R/utils.R Outdated Show resolved Hide resolved
m7pr and others added 2 commits January 17, 2025 16:22
Co-authored-by: Lluís Revilla <185338939+llrs-roche@users.noreply.github.com>
m7pr added a commit to insightsengineering/teal that referenced this pull request Jan 17, 2025
…#1449)

The same changes as in
insightsengineering/teal.modules.general#826
We are unifying `decorators` and `transformators` default values for
modules
@m7pr
Copy link
Contributor Author

m7pr commented Jan 17, 2025

I tested adding two decorators with the same name (to see what would happen):

That's funny : ) I think there is a way to extend assert_decorators to handle that

@m7pr
Copy link
Contributor Author

m7pr commented Jan 20, 2025

Hey @llrs-roche @gogonzo this branch now runs tests and checks smoothly and was also covered with few examples of using decorators for one or multiple objects.

I will start an issue to cover a situation where a list of decorators has duplicates in names. And another issue to possibly extend tests to cover decorators / transformators case.

Other than that, I am merging this PR

@llrs-roche
Copy link
Contributor

Checking again and I see this error message on the example with multiple decorators, but it doesn't seem a problem with the decorators machinery, just the decorator function:

....
decorators = list(histogram_plot  = list(footnote_dec), summary_table = list(table_dup_dec)),
...

image

But I think it is better to address other problems in separate issues/PR 👍

@m7pr m7pr merged commit 798168a into main Jan 20, 2025
29 checks passed
@m7pr m7pr deleted the 1441_transformators@main branch January 20, 2025 10:54
@github-actions github-actions bot locked and limited conversation to collaborators Jan 20, 2025
@m7pr
Copy link
Contributor Author

m7pr commented Jan 20, 2025

Yeah @llrs-roche the plot in the decorator, needs to be renamed to histogram_plot and it works

footnote_dec <- teal_transform_module(
  label = "Footnote",
  ui = function(id) shiny::textInput(shiny::NS(id, "footnote"), "Footnote", value = "I am a good decorator"),
  server = function(id, data) {
    moduleServer(id, function(input, output, session) {
      logger::log_info("🟢 Footnote called to action!", namespace = "teal.modules.general")
      reactive(
        within(
          data(),
          {
            footnote_str <- footnote
            histogram_plot <- histogram_plot + ggplot2::labs(caption = footnote_str)
          },
          footnote = input$footnote
        )
      )
    })
  }
)

table_dup_dec <- teal_transform_module(
  server = make_teal_transform_server(
    expression(
      logger::log_info("🔴 Table dup being called to action!", namespace = "teal.modules.general"),
      summary_table <- rbind(summary_table, summary_table),
      if (exists("test_table")) test_table <- rbind(test_table, test_table, test_table) 
    )
  )
)

# CDISC data example
data <- teal_data()
data <- within(data, {
  ADSL <- rADSL
})
join_keys(data) <- default_cdisc_join_keys[names(data)]

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

app <- init(
  data = data,
  modules = modules(
    tm_g_distribution(
      decorators = list(histogram_plot  = list(footnote_dec), summary_table = list(table_dup_dec)),
      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
        )
      )
    )
  )
)
if (interactive()) {
  shinyApp(app$ui, app$server)
}

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants