Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Unclosed Quarto panel tabsets after a knitr chunk error no longer break later report sections. `open_tabset()` tracks depth, and `register_tabset_chunk_hooks()` closes leftover fences when a chunk fails.

### Changed

- Colocalization heatmap text now explains that proteins are selected by mean abundance (up to 40 markers), globally across samples or within each cell type.
Expand Down
6 changes: 4 additions & 2 deletions DEVELOPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,8 @@ Tab UI is created with Quarto fenced divs:

The `tabset_*` helpers write these divs for you. Tab titles come from markdown headings (`#` level set by the `level` argument). Use `level` consistently within a section so tab nesting matches the surrounding heading hierarchy (e.g. `level = 5` under a `####` markdown heading).

Report setup calls `register_tabset_chunk_hooks()`. If a knitr chunk errors after `open_tabset()` or a `tabset_*` helper has written an opening fence, the hook appends the missing `:::` so later sections still parse. In `.qmd` chunks, call `open_tabset()` instead of writing the opening fence with `cat()`. Markdown tabsets written outside R chunks are unchanged.

### `tabset_plotlist()`

Renders a **named list of plots** as a tabset — one tab per plot.
Expand Down Expand Up @@ -409,10 +411,10 @@ Often used inside a manually opened tabset:

```r
#| results: 'asis'
cat("::: {.panel-tabset .nav-pills}\n")
open_tabset()
section_table(key_tables$pool, "Hash pool metrics", 3)
section_table(key_tables$sample, "Sample metrics", 3)
cat(":::\n")
close_tabset()
```

### `style_table()`
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export(merge_cell_types)
export(merge_data)
export(merged_cell_types)
export(order_cd_markers)
export(open_tabset)
export(order_sample_alias_factors)
export(plot_embedding)
export(plot_embeddings_samplewise)
Expand All @@ -80,6 +81,7 @@ export(process_data)
export(read_qc_files)
export(read_samplesheet)
export(register_es_data_workflow)
export(register_tabset_chunk_hooks)
export(run_abundance_anova)
export(run_proximity_anova)
export(section_intro)
Expand Down
164 changes: 139 additions & 25 deletions R/plot_helpers.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,134 @@ plot_anchor_slug <- function(...) {
gsub("^-+|-+$", "", x)
}

.tabset_state <- new.env(parent = emptyenv())
.tabset_state$depth <- 0L
.tabset_state$stack <- integer(0)

#' Reset tracked Quarto tabset depth
#'
#' Intended for tests. Report rendering resets state when
#' [register_tabset_chunk_hooks()] is called.
#'
#' @return `NULL`, invisibly.
#'
#' @noRd
reset_tabset_state <- function() {
.tabset_state$depth <- 0L
.tabset_state$stack <- integer(0)
invisible(NULL)
}

#' Current number of opened tabsets that have not been closed
#'
#' @return Integer depth.
#'
#' @noRd
tabset_depth <- function() {
.tabset_state$depth
}

#' Markdown fences that close leftover tabsets down to a depth
#'
#' Updates the tracked depth. Does not print; callers that run during knitr
#' chunk evaluation should `cat()` the result, while knitr output hooks should
#' return it so it is appended to the chunk markdown.
#'
#' @param until Integer depth to restore. Tabsets opened above this depth are
#' closed.
#'
#' @return A string of `:::` fences, or `""` when nothing is unclosed.
#'
#' @noRd
unclosed_tabset_fences <- function(until = 0L) {
until <- as.integer(until)
if (length(until) != 1L || is.na(until) || until < 0L) {
until <- 0L
}

n <- .tabset_state$depth - until
if (n <= 0L) {
return("")
}

.tabset_state$depth <- until
paste(rep(":::", n), collapse = "\n")
}

#' Open a Quarto panel tabset
#'
#' Emits the fenced div that starts a `{.panel-tabset}` and records it so
#' leftover tabsets can be closed if a knitr chunk errors. Prefer this over
#' writing the fence with `cat()` in `.qmd` files.
#'
#' @return The new tabset depth, invisibly.
#'
#' @export
open_tabset <- function() {
cat("::: {.panel-tabset .nav-pills}\n")
.tabset_state$depth <- .tabset_state$depth + 1L
invisible(.tabset_state$depth)
}

#' Close Tabset
#'
#' Closes a tabset opened with [open_tabset()] or [tabset_figure_table()].
#' Does nothing when no tracked tabset is open.
#'
#' @return The remaining tabset depth, invisibly.
#'
#' @export
close_tabset <- function() {
fence <- unclosed_tabset_fences(until = max(0L, .tabset_state$depth - 1L))
if (nzchar(fence)) {
cat(fence, "\n", sep = "")
}
invisible(.tabset_state$depth)
}

#' Register knitr hooks that close leftover panel tabsets
#'
#' When Quarto is run with `execute: error: true`, a chunk can fail after
#' [open_tabset()] (or a `tabset_*` helper) has written an opening fence.
#' This hook restores the tabset depth from the start of the chunk by appending
#' the missing `:::` fences so later sections of the report still parse.
#'
#' Call once during report setup (see `inst/quarto/shared/preprocessing.qmd`).
#'
#' @return `NULL`, invisibly.
#'
#' @export
register_tabset_chunk_hooks <- function() {
reset_tabset_state()
knitr::opts_chunk$set(pixelatorES_tabset_guard = TRUE)
knitr::knit_hooks$set(
pixelatorES_tabset_guard = function(before, options, envir) {
if (before) {
.tabset_state$stack <- c(.tabset_state$stack, .tabset_state$depth)
return(NULL)
}

n_stack <- length(.tabset_state$stack)
start <- if (n_stack > 0L) {
.tabset_state$stack[[n_stack]]
} else {
0L
}
if (n_stack > 0L) {
.tabset_state$stack <- .tabset_state$stack[-n_stack]
}

fences <- unclosed_tabset_fences(until = start)
if (!nzchar(fences)) {
return(NULL)
}

paste0("\n\n", fences, "\n\n")
}
)
invisible(NULL)
}

resolve_anchor_prefix <- function(anchor_prefix = NULL) {
if (is.null(anchor_prefix)) {
label <- knitr::opts_current$get("label")
Expand Down Expand Up @@ -89,13 +217,12 @@ tabset_plotlist <-

prefix <- resolve_anchor_prefix(anchor_prefix)

# Start the tabset
cat("::: {.panel-tabset .nav-pills}\n")
open_tabset()
if (close) {
on.exit(close_tabset(), add = TRUE)
}

title_plotlist(plots, level, anchor_prefix = prefix)

# Close the tabset
if (close) cat(":::\n")
}


Expand Down Expand Up @@ -133,9 +260,11 @@ tabset_nested_plotlist <-

prefix <- resolve_anchor_prefix(anchor_prefix)

# Start the tabset
cat("\n\n")
cat("::: {.panel-tabset .nav-pills}\n")
open_tabset()
if (close) {
on.exit(close_tabset(), add = TRUE)
}

for (tab in seq_along(plots)) {
if (inherits(plots[[tab]], "list")) {
Expand Down Expand Up @@ -163,15 +292,14 @@ tabset_nested_plotlist <-
)
}
}
# Close the tabset
if (close) cat(":::\n")
}


#' Tabset a figure and a table
#'
#' Tabset a figure and a table at a given header level. After using this
#' function, you should close the tabset with `close_tabset()`.
#' function, you should close the tabset with [close_tabset()]. If the chunk
#' errors before that call, [register_tabset_chunk_hooks()] closes the fence.
#'
#' @param figure A ggplot object representing the figure to be tabsetted or a list of ggplot objects to be tabsetted as
#' nested tabs.
Expand All @@ -194,8 +322,7 @@ tabset_figure_table <- function(figure, table, level = 2,
title = title_plotlist
)

# Start the tabset
cat("::: {.panel-tabset .nav-pills}\n")
open_tabset()

cat(paste0(strrep("#", level), " Figure\n\n"))

Expand All @@ -211,19 +338,6 @@ tabset_figure_table <- function(figure, table, level = 2,
return(table)
}

#' Close Tabset
#'
#' Closes the tabset that was opened with `tabset_figure_table`.
#'
#' @return Nothing.
#'
#' @export
#'
close_tabset <-
function() {
cat(":::\n")
}

#' Create a Void Plot
#'
#' Creates a ggplot object with no data and a void theme.
Expand Down
5 changes: 3 additions & 2 deletions inst/quarto/pixelatorES.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ params:
#| include: false

library(pixelatorES)
register_tabset_chunk_hooks()
report <- get_es_workflow_report(params$workflow)
```

Expand All @@ -41,12 +42,12 @@ for (child in report$preamble) {
#| echo: false

cat("\n# \n\n")
cat("::: {.panel-tabset .nav-pills}\n\n")
open_tabset()
for (section in report$sections) {
# Two newlines so the heading starts a new block even when the preceding
# child ends with a list or paragraph
cat("\n\n## ", section$title, "\n\n", sep = "")
cat(knitr::knit_child(section$child, quiet = TRUE))
}
cat("\n:::\n")
close_tabset()
```
3 changes: 3 additions & 0 deletions inst/quarto/shared/preprocessing.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ library(knitr)

# Hook to convert PNG -> WebP after each plot is saved
knit_hooks$set(plot = convert_png_to_webp)

# Close leftover panel-tabset fences when a chunk errors
register_tabset_chunk_hooks()
```

```{r}
Expand Down
4 changes: 2 additions & 2 deletions inst/quarto/workflows/amplicon_demux/quality_metrics.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ eval_standard_key_metrics <-
#| label: key_metrics_pool
#| eval: !expr eval_hashed_key_metrics
#| results: 'asis'
cat("::: {.panel-tabset .nav-pills}\n")
open_tabset()
if (!is.null(key_tables$pool)) {
section_table(key_tables$pool, "Hash pool metrics", 3)
}
if (!is.null(key_tables$sample)) {
section_table(key_tables$sample, "Sample metrics", 3)
}
cat(":::\n")
close_tabset()
```

```{r}
Expand Down
5 changes: 3 additions & 2 deletions man/close_tabset.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions man/open_tabset.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions man/register_tabset_chunk_hooks.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion man/tabset_figure_table.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading