Last updated: 2026-08-25
Checks: 7 0
Knit directory: flex-fusion-probes/
This reproducible R Markdown analysis was created with workflowr (version 1.7.1). The Checks tab describes the reproducibility checks that were applied when the results were created. The Past versions tab lists the development history.
Great! Since the R Markdown file has been committed to the Git repository, you know the exact version of the code that produced these results.
Great job! The global environment was empty. Objects defined in the global environment can affect the analysis in your R Markdown file in unknown ways. For reproduciblity it’s best to always run the code in an empty environment.
The command set.seed(20240925) was run prior to running
the code in the R Markdown file. Setting a seed ensures that any results
that rely on randomness, e.g. subsampling or permutations, are
reproducible.
Great job! Recording the operating system, R version, and package versions is critical for reproducibility.
Nice! There were no cached chunks for this analysis, so you can be confident that you successfully produced the results during this run.
Great job! Using relative paths to the files within your workflowr project makes it easier to run your code on other machines.
Great! You are using Git for version control. Tracking code development and connecting the code version to the results is critical for reproducibility.
The results in this page were generated with repository version f5aa6c9. See the Past versions tab to see a history of the changes made to the R Markdown and HTML files.
Note that you need to be careful to ensure that all relevant files for
the analysis have been committed to Git prior to generating the results
(you can use wflow_publish or
wflow_git_commit). workflowr only checks the R Markdown
file, but you know if there are other scripts or data files that it
depends on. Below is the status of the Git repository when the results
were generated:
Ignored files:
Ignored: .DS_Store
Ignored: .Rhistory
Ignored: .Rproj.user/
Ignored: analysis/.gitignore
Ignored: analysis/B-ALL_exp2_nonblast_fusion_analysis_cache/
Ignored: analysis/obsolete/B-ALL_nonblast_fusion_analysis_cache/
Ignored: analysis/projectionFigures/
Ignored: code/files/
Ignored: data/.DS_Store
Ignored: data/BoneMarrowMap/
Ignored: data/BoneMarrowMapBoneMarrowMap_SymphonyReference.rds
Ignored: data/BoneMarrowMapBoneMarrowMap_uwot_model.uwot
Ignored: data/CAPTURE-1-POOL-FLEX/
Ignored: data/CAPTURE-2-POOL-FLEX/
Ignored: data/Pool-240917-FLEX/
Ignored: data/RDS/
Ignored: data/Run_1_and_Run_2/
Ignored: data/Run_2/
Ignored: logs/
Ignored: output/
Ignored: projectionFigures/
Ignored: renv/library/
Ignored: renv/staging/
Untracked files:
Untracked: CLAUDE.md
Untracked: data/Config_Sample_CAPTURE-1-POOL-FLEX.csv
Untracked: data/Config_Sample_CAPTURE-2-POOL-FLEX.csv
Untracked: data/Config_Sample_Pool-240917-FLEX.csv
Untracked: data/analysis_input/ETV6_RUNX1_transcript.csv
Untracked: data/cellranger-multi_flex_fusions_pool_1.sbatch
Untracked: data/cellranger-multi_flex_fusions_pool_2.sbatch
Untracked: data/victoria_stuff/
Untracked: methods_updated.md
Unstaged changes:
Modified: analysis/B-ALL_exp2_fig4_assembly.Rmd
Modified: render_all.sh
Note that any generated files, e.g. HTML, png, CSS, etc., are not included in this status report because it is ok for generated content to have uncommitted changes.
These are the previous versions of the repository in which changes were
made to the R Markdown
(analysis/B-ALL_exp2_c369_mrd_analysis.Rmd) and HTML
(docs/B-ALL_exp2_c369_mrd_analysis.html) files. If you’ve
configured a remote Git repository (see ?wflow_git_remote),
click on the hyperlinks in the table below to view the files as they
were in that past version.
| File | Version | Author | Date | Message |
|---|---|---|---|---|
| Rmd | f5aa6c9 | Jovana Maksimovic | 2026-08-25 | analysis/B-ALL_exp2_fig4_assembly.Rmd |
| html | c8c7478 | Jovana Maksimovic | 2026-07-16 | Build site. |
| Rmd | 6be4d89 | Jovana Maksimovic | 2026-07-16 | wflow_publish("analysis/B-ALL_exp2_c369_mrd_analysis.Rmd") |
Patient C369 carries an ETV6::RUNX1 (v2) fusion and has three longitudinal samples: diagnosis, MRD (end-of-induction), and relapse. This notebook addresses two related biological questions, both answered by jointly clustering the three C369 timepoints and examining where fusion-positive cells fall relative to the transcriptionally defined blast and normal BM populations.
1. MRD blast detection. Clinical MRD at end-of-induction was 0.038% by flow cytometry / molecular testing. At this level, roughly 0.76 genuine blast cells are expected in the 1,994 recovered MRD cells. Joint clustering identifies 2 MRD cells that cluster with diagnosis blasts (0.1%). This is in close agreement with the clinical MRD estimate and represents an independent, orthogonal confirmation using single-cell fusion detection; notably, the ETV6::RUNX1 fusion is not used in the clinical MRD assay.
2. Pre-leukaemic clone detection. ETV6::RUNX1 is thought to arise in utero in a haematopoietic progenitor cell, establishing a pre-leukaemic clone that persists across multiple lineages without transformation (Mori et al.; Ford et al.). At MRD, we detect ETV6::RUNX1-v2 fusion transcripts in 33 cells that cluster within normal BM populations, spanning erythroid (n=11), myeloid (n=9), T cell (n=10), NK (n=1), and B cell (n=2) lineages. These cells do not express canonical blast markers (CD19, DNTT, MME), consistent with normal haematopoietic differentiation rather than leukaemic identity. The breadth of lineages represented supports the hypothesis that the founding fusion event occurred in an early multipotent progenitor.
# ── Full annotated joint object (all patients, all timepoints) --------------- #
seu_full <- readRDS(here("data", "RDS", "ball_exp2_annotated.seu.rds"))
cat(glue("Full dataset: {ncol(seu_full)} cells × {nrow(seu_full)} features\n"))
Full dataset: 87534 cells × 37145 features
cat("\nC369 cells per timepoint:\n")
C369 cells per timepoint:
print(table(seu_full$patient_timepoint[
grepl("C369", seu_full$patient_timepoint)
]))
C1437_Diagnosis C1437_Relapse C369_Diagnosis C369_MRD C369_Relapse
0 0 12247 2008 13001
C601_Diagnosis C658_Diagnosis C659_Diagnosis
0 0 0
The three C369 timepoints are subsetted from the full object and re-processed independently. Joint clustering allows MRD, diagnosis, and relapse cells to be positioned relative to each other without interference from other patients. SCTransform normalisation is applied on the subset, followed by PCA, UMAP, and Leiden clustering.
out_rds <- here("data", "RDS", "ball_exp2_clustered_c369.seu.rds")
seu_c369 <- cache_rds(out_rds, {
sub <- subset(seu_full,
patient_timepoint %in% c("C369_Diagnosis",
"C369_MRD",
"C369_Relapse"))
sub <- SCTransform(sub, verbose = FALSE, return.only.var.genes = TRUE)
sub <- RunPCA(sub, verbose = FALSE, npcs = 30, assay = "SCT")
sub <- RunUMAP(sub, dims = 1:30, reduction = "pca")
sub <- FindNeighbors(sub, reduction = "pca", dims = 1:30)
sub <- FindClusters(sub,
resolution = seq(0.1, 1, by = 0.1),
algorithm = "Leiden")
sub
})
# Remove very low-quality cells (nCount_RNA < 300) that passed initial QC
# but have insufficient signal for reliable cluster assignment.
# Two such cells (nCount_RNA ~150) were present in C369_MRD and fell into
# the blast cluster due to noise — they are fusion-negative and will be
# properly excluded when the QC notebook is updated.
n_before <- ncol(seu_c369)
seu_c369 <- subset(seu_c369, nCount_RNA >= 300)
cat(glue("Removed {n_before - ncol(seu_c369)} low-quality cells (nCount_RNA < 300)\n"))
Removed 123 low-quality cells (nCount_RNA < 300)
cat(glue(
"C369 joint object: {ncol(seu_c369)} cells × {nrow(seu_c369)} features\n\n"
))
C369 joint object: 27133 cells × 15029 features
cat("Cells per timepoint:\n")
Cells per timepoint:
print(table(seu_c369$patient_timepoint))
C369_Diagnosis C369_MRD C369_Relapse
12124 2008 13001
clustree(seu_c369, prefix = "SCT_snn_res.")

| Version | Author | Date |
|---|---|---|
| c8c7478 | Jovana Maksimovic | 2026-07-16 |
Clusters are annotated as blast or normal BM using majority-vote BoneMarrowMap labels. Blast clusters are then split by dominant timepoint (diagnosis vs relapse). MRD blast cells are identified separately as fusion-positive cells at the MRD timepoint that fall within blast-annotated clusters.
# Resolution to use for annotation — inspect outputs and adjust if needed
res_use <- "SCT_snn_res.0.3"
blast_types <- c("Pro-B", "Pre-B", "Early Lymphoid")
cluster_votes <- seu_c369@meta.data %>%
as_tibble(rownames = "cell_id") %>%
mutate(is_blast = predicted_CellType_Broad %in% blast_types) %>%
group_by(.data[[res_use]]) %>%
summarise(
n_cells = n(),
pct_blast = mean(is_blast),
maj_tp = names(sort(table(patient_timepoint), decreasing = TRUE))[1],
.groups = "drop"
) %>%
mutate(
cluster_class = case_when(
pct_blast > 0.5 & maj_tp == "C369_Diagnosis" ~ "Diagnosis blast",
pct_blast > 0.5 & maj_tp == "C369_Relapse" ~ "Relapse blast",
pct_blast > 0.5 & maj_tp == "C369_MRD" ~ "MRD blast",
TRUE ~ "Normal BM"
)
)
cat("Cluster classification:\n")
Cluster classification:
print(cluster_votes)
# A tibble: 11 × 5
SCT_snn_res.0.3 n_cells pct_blast maj_tp cluster_class
<fct> <int> <dbl> <chr> <chr>
1 1 9356 0.988 C369_Relapse Relapse blast
2 2 5806 0.984 C369_Diagnosis Diagnosis blast
3 3 2864 0.00594 C369_Diagnosis Normal BM
4 4 2677 0.991 C369_Diagnosis Diagnosis blast
5 5 2075 0.977 C369_Diagnosis Diagnosis blast
6 6 1359 0.995 C369_Relapse Relapse blast
7 7 1130 0.00796 C369_Relapse Normal BM
8 8 989 0.987 C369_Relapse Relapse blast
9 9 491 0 C369_MRD Normal BM
10 10 337 0 C369_MRD Normal BM
11 11 49 0 C369_MRD Normal BM
# Assign cell-level labels
seu_c369$cell_class <- cluster_votes$cluster_class[
match(as.character(seu_c369@meta.data[[res_use]]),
as.character(cluster_votes[[res_use]]))
]
# Fusion status
seu_c369$fusion_detected <- seu_c369$patient_fusion == "ETV6-RUNX1-v2-fusion"
# Timepoint short labels
seu_c369$timepoint <- recode(seu_c369$patient_timepoint,
"C369_Diagnosis" = "Diagnosis",
"C369_MRD" = "MRD",
"C369_Relapse" = "Relapse"
)
cat("\nCell class breakdown:\n")
Cell class breakdown:
print(table(seu_c369$cell_class, seu_c369$timepoint))
Diagnosis MRD Relapse
Diagnosis blast 10443 1 114
Normal BM 1640 2006 1225
Relapse blast 41 1 11662
umap_df <- as.data.frame(Embeddings(seu_c369, "umap"))
colnames(umap_df) <- c("UMAP_1", "UMAP_2")
umap_df$timepoint <- seu_c369$timepoint
umap_df$fusion <- seu_c369$fusion_detected
umap_df$cell_class <- seu_c369$cell_class
umap_df$bmm_label <- seu_c369$predicted_CellType_Broad
ggplot(umap_df, aes(UMAP_1, UMAP_2, colour = timepoint)) +
geom_point(size = 0.4, alpha = 0.6) +
scale_colour_manual(values = tp_cols, name = NULL) +
guides(colour = guide_legend(override.aes = list(size = 3, alpha = 1))) +
labs(title = "C369 — joint UMAP by timepoint") +
theme_pub() +
theme(legend.position = "right")

| Version | Author | Date |
|---|---|---|
| c8c7478 | Jovana Maksimovic | 2026-07-16 |
# Plot fusion+ cells on top
umap_df_ord <- umap_df %>% arrange(fusion)
ggplot(umap_df_ord, aes(UMAP_1, UMAP_2,
colour = fusion, size = fusion, alpha = fusion)) +
geom_point() +
scale_colour_manual(
values = c("TRUE" = "#0072B2", "FALSE" = "grey85"),
labels = c("TRUE" = "ETV6::RUNX1-v2", "FALSE" = "Unselected"),
name = NULL
) +
scale_size_manual(values = c("TRUE" = 1.2, "FALSE" = 0.3), guide = "none") +
scale_alpha_manual(values = c("TRUE" = 1, "FALSE" = 0.5), guide = "none") +
guides(colour = guide_legend(override.aes = list(size = 3, alpha = 1))) +
labs(title = "C369 — joint UMAP by fusion detection") +
theme_pub() +
theme(legend.position = "right")

| Version | Author | Date |
|---|---|---|
| c8c7478 | Jovana Maksimovic | 2026-07-16 |
# One panel per timepoint; fusion+ cells highlighted; other timepoints greyed
make_tp_panel <- function(tp) {
df <- umap_df %>%
mutate(
highlight = case_when(
timepoint == tp & fusion ~ "Fusion+",
timepoint == tp & !fusion ~ "Unselected",
TRUE ~ "Other"
),
highlight = factor(highlight, levels = c("Fusion+", "Unselected", "Other"))
) %>%
arrange(desc(highlight == "Other"))
ggplot(df, aes(UMAP_1, UMAP_2, colour = highlight,
size = highlight, alpha = highlight)) +
geom_point() +
scale_colour_manual(
values = c("Fusion+" = "#0072B2", "Unselected" = "#9970AB", "Other" = "grey92"),
name = NULL
) +
scale_size_manual(
values = c("Fusion+" = 1.5, "Unselected" = 0.4, "Other" = 0.3),
guide = "none"
) +
scale_alpha_manual(
values = c("Fusion+" = 1, "Unselected" = 0.7, "Other" = 0.6),
guide = "none"
) +
guides(colour = guide_legend(override.aes = list(size = 3, alpha = 1))) +
ggtitle(tp) +
theme_pub(base_size = 10) +
theme(legend.position = "bottom",
plot.title = element_text(hjust = 0.5))
}
p_dx <- make_tp_panel("Diagnosis")
p_mrd <- make_tp_panel("MRD")
p_rel <- make_tp_panel("Relapse")
(p_dx | p_mrd | p_rel) +
plot_layout(guides = "collect") +
plot_annotation(theme = theme(legend.position = "bottom"))

| Version | Author | Date |
|---|---|---|
| c8c7478 | Jovana Maksimovic | 2026-07-16 |
Genuine residual blasts are identified by cluster membership: MRD cells that fall within blast-annotated clusters are transcriptionally indistinguishable from diagnosis blasts, providing stronger evidence of blast identity than BoneMarrowMap annotation alone. This approach is independent of fusion detection, allowing the two criteria to serve as orthogonal lines of evidence. We then cross-tabulate cluster membership with fusion detection status to identify the single cell satisfying all three criteria for genuine MRD blast identity.
n_mrd <- sum(seu_c369$patient_timepoint == "C369_MRD")
n_mrd_blast <- sum(seu_c369$patient_timepoint == "C369_MRD" &
seu_c369$cell_class %in% c("Diagnosis blast", "Relapse blast",
"MRD blast"))
n_mrd_fusion <- sum(seu_c369$patient_timepoint == "C369_MRD" &
seu_c369$fusion_detected)
cat(glue("MRD sample size: {n_mrd} cells\n"))
MRD sample size: 2008 cells
cat(glue("MRD cells in blast clusters: {n_mrd_blast} ({round(n_mrd_blast/n_mrd*100,2)}%)\n"))
MRD cells in blast clusters: 2 (0.1%)
cat(glue("MRD fusion+ cells (any): {n_mrd_fusion} ({round(n_mrd_fusion/n_mrd*100,2)}%)\n"))
MRD fusion+ cells (any): 35 (1.74%)
cat(glue("Clinical MRD (flow/molecular): 0.038%\n\n"))
Clinical MRD (flow/molecular): 0.038%
# Cross-tabulation: cluster class × fusion status for MRD cells
cat("MRD cells — cluster class × fusion status:\n")
MRD cells — cluster class × fusion status:
mrd_cross <- seu_c369@meta.data %>%
filter(patient_timepoint == "C369_MRD") %>%
count(cell_class, fusion_detected) %>%
pivot_wider(names_from = fusion_detected,
values_from = n, values_fill = 0) %>%
rename(`fusion-` = `FALSE`, `fusion+` = `TRUE`)
print(mrd_cross)
# A tibble: 3 × 3
cell_class `fusion+` `fusion-`
<chr> <int> <int>
1 Diagnosis blast 1 0
2 Normal BM 33 1973
3 Relapse blast 1 0
cat("\nMRD blast cells — BoneMarrowMap annotation:\n")
MRD blast cells — BoneMarrowMap annotation:
seu_c369@meta.data %>%
filter(patient_timepoint == "C369_MRD",
cell_class %in% c("Diagnosis blast", "Relapse blast", "MRD blast")) %>%
count(predicted_CellType_Broad, fusion_detected) %>%
print()
predicted_CellType_Broad fusion_detected n
1 Pro-B TRUE 1
2 <NA> TRUE 1
MRD cells are coloured by their assigned identity: blast cluster membership (putative genuine residual blasts), fusion-positive in normal BM clusters (putative pre-leukaemic progenitor progeny), or fusion-negative normal BM. Diagnosis and relapse cells form the background.
umap_df <- umap_df %>%
mutate(
mrd_class = case_when(
timepoint == "MRD" &
cell_class %in% c("Diagnosis blast", "Relapse blast", "MRD blast")
~ "MRD blast (fusion+)",
timepoint == "MRD" & fusion & cell_class == "Normal BM"
~ "MRD normal BM (fusion+)",
timepoint == "MRD" & !fusion
~ "MRD normal BM (fusion-)",
timepoint != "MRD"
~ "Dx / Relapse"
),
mrd_class = factor(mrd_class, levels = c(
"MRD blast (fusion+)",
"MRD normal BM (fusion+)",
"MRD normal BM (fusion-)",
"Dx / Relapse"
))
) %>%
arrange(desc(mrd_class == "Dx / Relapse"),
desc(mrd_class == "MRD normal BM (fusion-)"))
# All MRD cells use pale lavender (#DEC9E9 = C369_MRD_blast colour).
# Shape + stroke encode evidence strength:
# MRD blast (fusion+) — shape 21, bold dark purple outline (confirmed blast)
# MRD normal BM (fusion+) — shape 21, thin medium purple outline (ambiguous)
# MRD normal BM (fusion-) — shape 16, no outline (background MRD)
# Dx / Relapse — shape 16, grey (context only)
mrd_fills <- c(
"MRD blast (fusion+)" = "#DEC9E9",
"MRD normal BM (fusion+)" = "#DEC9E9",
"MRD normal BM (fusion-)" = "#DEC9E9",
"Dx / Relapse" = "grey88"
)
mrd_stroke_cols <- c(
"MRD blast (fusion+)" = "#6A2E8A", # dark purple — confirmed blast
"MRD normal BM (fusion+)" = "#9970AB", # medium purple — ambiguous
"MRD normal BM (fusion-)" = "#DEC9E9", # matches fill — no visible outline
"Dx / Relapse" = "grey88" # matches fill — no visible outline
)
mrd_shapes <- c(
"MRD blast (fusion+)" = 21,
"MRD normal BM (fusion+)" = 21,
"MRD normal BM (fusion-)" = 16,
"Dx / Relapse" = 16
)
mrd_strokes <- c(
"MRD blast (fusion+)" = 0.8,
"MRD normal BM (fusion+)" = 0.4,
"MRD normal BM (fusion-)" = 0,
"Dx / Relapse" = 0
)
# Compute cluster region centroids for annotation labels
# Using predicted_CellType_Broad to identify lineage groups in the background
region_annots <- bind_rows(
# Blast clusters — labelled by timepoint
umap_df %>%
filter(mrd_class == "Dx / Relapse",
cell_class %in% c("Diagnosis blast", "Relapse blast")) %>%
mutate(lineage = if_else(cell_class == "Diagnosis blast",
"C369 Dx blasts", "C369 Rel blasts")) %>%
group_by(lineage) %>%
summarise(x = median(UMAP_1), y = median(UMAP_2), .groups = "drop"),
# Normal BM — labelled by lineage (Pro-B/Pre-B excluded: in B-ALL these
# are blast cells labelled by BoneMarrowMap, not normal B-lineage cells)
umap_df %>%
filter(mrd_class == "Dx / Relapse", cell_class == "Normal BM") %>%
mutate(lineage = case_when(
bmm_label %in% c("Late Erythroid", "Early Erythroid",
"Erythroid Progenitor", "MEP") ~ "Erythroid /\nProgenitors",
bmm_label %in% c("Monocyte", "Pro-Monocyte", "cDC", "pDC") ~ "Myeloid",
bmm_label %in% c("Naive T", "CD4 Memory T",
"CD8 Memory T", "NK") ~ "T / NK",
bmm_label %in% c("B", "Plasma Cell") ~ "B cells",
TRUE ~ NA_character_
)) %>%
filter(!is.na(lineage)) %>%
group_by(lineage) %>%
summarise(x = median(UMAP_1), y = median(UMAP_2), .groups = "drop")
)
ggplot(umap_df, aes(UMAP_1, UMAP_2,
fill = mrd_class,
colour = mrd_class,
shape = mrd_class,
size = mrd_class,
alpha = mrd_class,
stroke = mrd_class)) +
geom_point() +
# Cluster region labels
geom_text(data = region_annots,
aes(x = x, y = y, label = lineage),
inherit.aes = FALSE,
size = 2.8, colour = "grey30", fontface = "italic",
lineheight = 0.85) +
scale_fill_manual(values = mrd_fills, name = NULL) +
scale_colour_manual(values = mrd_stroke_cols, name = NULL) +
scale_shape_manual(values = mrd_shapes, name = NULL) +
scale_discrete_manual(aesthetics = "stroke", values = mrd_strokes,
guide = "none") +
scale_size_manual(
values = c(
"MRD blast (fusion+)" = 2.2,
"MRD normal BM (fusion+)" = 1.2,
"MRD normal BM (fusion-)" = 1.2,
"Dx / Relapse" = 1.0
),
guide = "none"
) +
scale_alpha_manual(
values = c(
"MRD blast (fusion+)" = 1,
"MRD normal BM (fusion+)" = 0.9,
"MRD normal BM (fusion-)" = 0.4,
"Dx / Relapse" = 0.4
),
guide = "none"
) +
guides(
fill = guide_legend(
override.aes = list(
size = 3,
alpha = 1,
shape = c(21, 21, 16, 16),
fill = c("#DEC9E9", "#DEC9E9", "#DEC9E9", "grey88"),
colour = c("#6A2E8A", "#9970AB", "#DEC9E9", "grey88"),
stroke = c(0.8, 0.4, 0, 0)
),
ncol = 1
),
colour = "none",
shape = "none"
) +
labs(title = NULL, x = NULL, y = NULL) +
annotation_custom(grob = grid::grobTree(
grid::segmentsGrob(
x0 = 0.05, y0 = 0.05, x1 = 0.16, y1 = 0.05,
arrow = grid::arrow(length = grid::unit(0.15, "cm"), type = "closed"),
gp = grid::gpar(fill = "grey20", col = "grey20", lwd = 2.5)
),
grid::segmentsGrob(
x0 = 0.05, y0 = 0.05, x1 = 0.05, y1 = 0.16,
arrow = grid::arrow(length = grid::unit(0.15, "cm"), type = "closed"),
gp = grid::gpar(fill = "grey20", col = "grey20", lwd = 2.5)
),
grid::textGrob("UMAP", x = 0.105, y = 0.03,
gp = grid::gpar(fontsize = 8, col = "grey20", fontface = "bold"))
)) +
coord_cartesian(clip = "off") +
theme_pub() +
theme(
legend.position = "right",
legend.direction = "vertical",
legend.text = element_text(size = 8),
axis.text = element_blank(),
axis.ticks = element_blank(),
axis.line = element_blank(),
panel.grid = element_blank(),
panel.border = element_blank(),
panel.background = element_blank()
) -> fig4_H
fig4_H

| Version | Author | Date |
|---|---|---|
| c8c7478 | Jovana Maksimovic | 2026-07-16 |
Blast markers (DNTT, MME, CD19) and lineage-specific markers (LYZ — myeloid; HBA1 — erythroid; NKG7 — T/NK) are compared across six groups: Dx/Rel blasts, cluster-confirmed MRD blasts (fusion+), and the four broad normal BM cell types identified among fusion-positive MRD cells in the UMAP (B cells, Myeloid, Erythroid/Progenitors, T/NK). The MRD blast expresses DNTT and MME at levels comparable to Dx/Rel blasts, confirming its blast identity. The fusion-positive normal BM cells show no blast marker expression but express their expected lineage markers, confirming they are genuine normal cells rather than residual leukaemia.
# Markers consistent with joint clustering dotplot:
# blast: DNTT, MME, CD19 | myeloid: CD14 | erythroid: HBB | T/NK: NKG7
markers_mrd <- c("DNTT", "MME", "CD19", "CD79A", "CD14", "HBB", "NKG7")
markers_mrd <- markers_mrd[markers_mrd %in% rownames(seu_c369)]
# Cluster-based marker groups — blasts + 4 UMAP lineage groups (all normal BM cells)
seu_c369$mrd_marker_group <- case_when(
seu_c369$patient_timepoint %in% c("C369_Diagnosis", "C369_Relapse") &
seu_c369$fusion_detected &
seu_c369$cell_class %in% c("Diagnosis blast", "Relapse blast")
~ "Dx/Rel blast",
seu_c369$patient_timepoint == "C369_MRD" &
seu_c369$cell_class %in% c("Diagnosis blast", "Relapse blast", "MRD blast")
~ "MRD blast\n(fusion+)",
seu_c369$cell_class == "Normal BM" &
seu_c369$predicted_CellType_Broad %in% c("B", "Plasma Cell")
~ "B cells",
seu_c369$cell_class == "Normal BM" &
seu_c369$predicted_CellType_Broad %in% c("Monocyte", "Pro-Monocyte", "cDC", "pDC")
~ "Myeloid",
seu_c369$cell_class == "Normal BM" &
seu_c369$predicted_CellType_Broad %in% c("Late Erythroid", "Early Erythroid",
"Erythroid Progenitor", "MEP")
~ "Erythroid /\nProgenitors",
seu_c369$cell_class == "Normal BM" &
seu_c369$predicted_CellType_Broad %in% c("Naive T", "CD4 Memory T",
"CD8 Memory T", "NK")
~ "T / NK",
TRUE ~ NA_character_
)
grp_levels <- c("Dx/Rel blast", "MRD blast\n(fusion+)",
"B cells", "Myeloid", "Erythroid /\nProgenitors", "T / NK")
# Use DotPlot() to extract scaled expression — consistent with joint clustering dotplot
# Subset to cells with a valid group (exclude NAs) before calling DotPlot
DefaultAssay(seu_c369) <- "SCT"
seu_c369$mrd_marker_group <- factor(seu_c369$mrd_marker_group, levels = grp_levels)
seu_dot <- subset(seu_c369,
cells = colnames(seu_c369)[!is.na(seu_c369$mrd_marker_group)])
Idents(seu_dot) <- "mrd_marker_group"
dot_data <- DotPlot(
seu_dot,
features = markers_mrd,
idents = grp_levels
)$data %>%
mutate(
features.plot = factor(features.plot, levels = rev(markers_mrd)),
id = factor(id, levels = grp_levels)
)
group_n <- seu_dot@meta.data %>%
count(mrd_marker_group) %>%
mutate(
id = factor(mrd_marker_group, levels = grp_levels),
label = paste0("n=", comma(n))
)
ggplot(dot_data, aes(x = id, y = features.plot,
size = pct.exp, colour = avg.exp.scaled)) +
geom_point(alpha = 0.9) +
geom_text(
data = group_n,
aes(x = id, y = Inf, label = label),
size = 2.5, colour = "grey30", fontface = "italic",
vjust = -0.3, inherit.aes = FALSE
) +
geom_vline(xintercept = 1.5, linetype = "dashed",
colour = "grey60", linewidth = 0.4) +
geom_vline(xintercept = 2.5, linetype = "dashed",
colour = "grey60", linewidth = 0.4) +
scale_colour_distiller(
palette = "Purples",
direction = 1,
name = "Avg. expr.\n(scaled)",
na.value = "grey80",
guide = guide_colourbar(direction = "vertical",
title.position = "top",
barwidth = 0.5,
barheight = 4)
) +
scale_size_continuous(
name = "% expr.",
range = c(0.5, 8),
breaks = c(1, 25, 50, 75),
limits = c(0, 100),
guide = guide_legend(direction = "vertical",
title.position = "top",
ncol = 1)
) +
labs(x = NULL, y = NULL) +
theme_classic(base_size = 11) +
theme(
axis.text.x = element_text(size = 8, hjust = 0.5),
axis.text.y = element_text(size = 9),
panel.grid.major = element_line(colour = "grey92", linewidth = 0.3),
legend.position = "right",
legend.box = "vertical",
plot.margin = margin(t = 20, r = 5, b = 25, l = 5)
) +
coord_cartesian(clip = "off") -> fig4_I
fig4_I

| Version | Author | Date |
|---|---|---|
| c8c7478 | Jovana Maksimovic | 2026-07-16 |
sessioninfo::session_info()
─ Session info ───────────────────────────────────────────────────────────────
setting value
version R version 4.4.1 (2024-06-14)
os macOS 15.7.9
system aarch64, darwin20
ui X11
language (EN)
collate en_US.UTF-8
ctype en_US.UTF-8
tz Australia/Melbourne
date 2026-08-25
pandoc 3.8.3 @ /Applications/RStudio.app/Contents/Resources/app/quarto/bin/tools/aarch64/ (via rmarkdown)
quarto 1.9.38 @ /Applications/RStudio.app/Contents/Resources/app/quarto/bin/quarto
─ Packages ───────────────────────────────────────────────────────────────────
! package * version date (UTC) lib source
P abind 1.4-8 2024-09-12 [?] CRAN (R 4.4.1)
P backports 1.5.0 2024-05-23 [?] CRAN (R 4.4.1)
P BiocManager 1.30.27 2025-11-14 [?] CRAN (R 4.4.3)
P bslib 0.9.0 2025-01-30 [?] CRAN (R 4.4.1)
P cachem 1.1.0 2024-05-16 [?] CRAN (R 4.4.1)
P callr 3.7.6 2024-03-25 [?] CRAN (R 4.4.0)
P checkmate 2.3.2 2024-07-29 [?] CRAN (R 4.4.0)
P cli 3.6.5 2025-04-23 [?] CRAN (R 4.4.1)
P cluster 2.1.6 2023-12-01 [?] CRAN (R 4.4.1)
P clustree * 0.5.1 2023-11-05 [?] CRAN (R 4.4.0)
P codetools 0.2-20 2024-03-31 [?] CRAN (R 4.4.1)
P colorspace 2.1-1 2024-07-26 [?] CRAN (R 4.4.1)
P cowplot 1.1.3 2024-01-22 [?] CRAN (R 4.4.0)
P data.table 1.18.2.1 2026-01-27 [?] CRAN (R 4.4.3)
P deldir 2.0-4 2024-02-28 [?] CRAN (R 4.4.1)
P digest 0.6.37 2024-08-19 [?] CRAN (R 4.4.1)
P dotCall64 1.2 2024-10-04 [?] CRAN (R 4.4.1)
P dplyr * 1.1.4 2023-11-17 [?] CRAN (R 4.4.0)
P evaluate 1.0.3 2025-01-10 [?] CRAN (R 4.4.1)
P farver 2.1.2 2024-05-13 [?] CRAN (R 4.4.1)
P fastDummies 1.7.5 2025-01-20 [?] CRAN (R 4.4.1)
P fastmap 1.2.0 2024-05-15 [?] CRAN (R 4.4.1)
P fitdistrplus 1.2-2 2025-01-07 [?] CRAN (R 4.4.1)
P forcats * 1.0.0 2023-01-29 [?] CRAN (R 4.4.0)
P fs * 1.6.6 2025-04-12 [?] CRAN (R 4.4.1)
P future 1.49.0 2025-05-09 [?] CRAN (R 4.4.1)
P future.apply 1.11.3 2024-10-27 [?] CRAN (R 4.4.1)
P generics 0.1.4 2025-05-09 [?] CRAN (R 4.4.1)
P getPass 0.2-4 2023-12-10 [?] CRAN (R 4.4.1)
P ggforce 0.4.2 2024-02-19 [?] CRAN (R 4.4.0)
P ggplot2 * 4.0.2 2026-02-03 [?] CRAN (R 4.4.3)
P ggraph * 2.2.1 2024-03-07 [?] CRAN (R 4.4.0)
P ggrepel * 0.9.6 2024-09-07 [?] CRAN (R 4.4.1)
P ggridges 0.5.6 2024-01-23 [?] CRAN (R 4.4.0)
P git2r 0.36.2 2025-03-29 [?] CRAN (R 4.4.1)
P globals 0.18.0 2025-05-08 [?] CRAN (R 4.4.1)
P glue * 1.8.0 2024-09-30 [?] CRAN (R 4.4.1)
P goftest 1.2-3 2021-10-07 [?] CRAN (R 4.4.1)
P graphlayouts 1.2.2 2025-01-23 [?] CRAN (R 4.4.1)
P gridExtra 2.3 2017-09-09 [?] CRAN (R 4.4.1)
P gtable 0.3.6 2024-10-25 [?] CRAN (R 4.4.1)
P here * 1.0.1 2020-12-13 [?] CRAN (R 4.4.1)
P hms 1.1.3 2023-03-21 [?] CRAN (R 4.4.0)
P htmltools 0.5.8.1 2024-04-04 [?] CRAN (R 4.4.1)
P htmlwidgets 1.6.4 2023-12-06 [?] CRAN (R 4.4.0)
P httpuv 1.6.16 2025-04-16 [?] CRAN (R 4.4.1)
P httr 1.4.7 2023-08-15 [?] CRAN (R 4.4.0)
P ica 1.0-3 2022-07-08 [?] CRAN (R 4.4.1)
P igraph 2.1.4 2025-01-23 [?] CRAN (R 4.4.1)
P irlba 2.3.5.1 2022-10-03 [?] CRAN (R 4.4.0)
P jquerylib 0.1.4 2021-04-26 [?] CRAN (R 4.4.0)
P jsonlite 2.0.0 2025-03-27 [?] CRAN (R 4.4.1)
P KernSmooth 2.23-24 2024-05-17 [?] CRAN (R 4.4.1)
P knitr 1.50 2025-03-16 [?] CRAN (R 4.4.1)
P labeling 0.4.3 2023-08-29 [?] CRAN (R 4.4.1)
P later 1.4.2 2025-04-08 [?] CRAN (R 4.4.1)
P lattice 0.22-6 2024-03-20 [?] CRAN (R 4.4.1)
P lazyeval 0.2.2 2019-03-15 [?] CRAN (R 4.4.1)
P lifecycle 1.0.4 2023-11-07 [?] CRAN (R 4.4.1)
P listenv 0.9.1 2024-01-29 [?] CRAN (R 4.4.1)
P lmtest 0.9-40 2022-03-21 [?] CRAN (R 4.4.1)
P lubridate * 1.9.4 2024-12-08 [?] CRAN (R 4.4.1)
P magrittr 2.0.3 2022-03-30 [?] CRAN (R 4.4.1)
P MASS 7.3-60.2 2024-04-26 [?] CRAN (R 4.4.1)
P Matrix 1.7-0 2024-04-26 [?] CRAN (R 4.4.1)
P matrixStats 1.5.0 2025-01-07 [?] CRAN (R 4.4.1)
P memoise 2.0.1 2021-11-26 [?] CRAN (R 4.4.0)
P mime 0.13 2025-03-17 [?] CRAN (R 4.4.1)
P miniUI 0.1.2 2025-04-17 [?] CRAN (R 4.4.1)
P nlme 3.1-164 2023-11-27 [?] CRAN (R 4.4.1)
P parallelly 1.45.0 2025-06-02 [?] CRAN (R 4.4.1)
P patchwork * 1.3.0 2024-09-16 [?] CRAN (R 4.4.1)
P pbapply 1.7-4 2025-07-20 [?] CRAN (R 4.4.1)
P pillar 1.10.2 2025-04-05 [?] CRAN (R 4.4.1)
P pkgconfig 2.0.3 2019-09-22 [?] CRAN (R 4.4.1)
P plotly 4.10.4 2024-01-13 [?] CRAN (R 4.4.0)
P plyr 1.8.9 2023-10-02 [?] CRAN (R 4.4.1)
P png 0.1-8 2022-11-29 [?] CRAN (R 4.4.1)
P polyclip 1.10-7 2024-07-23 [?] CRAN (R 4.4.1)
P processx 3.8.6 2025-02-21 [?] CRAN (R 4.4.1)
P progressr 0.15.1 2024-11-22 [?] CRAN (R 4.4.1)
P promises 1.3.3 2025-05-29 [?] CRAN (R 4.4.1)
P ps 1.9.1 2025-04-12 [?] CRAN (R 4.4.1)
P purrr * 1.0.4 2025-02-05 [?] CRAN (R 4.4.1)
P R6 2.6.1 2025-02-15 [?] CRAN (R 4.4.1)
P RANN 2.6.2 2024-08-25 [?] CRAN (R 4.4.1)
P RColorBrewer 1.1-3 2022-04-03 [?] CRAN (R 4.4.1)
P Rcpp 1.1.1-1.1 2026-04-24 [?] CRAN (R 4.4.1)
P RcppAnnoy 0.0.22 2024-01-23 [?] CRAN (R 4.4.1)
P RcppHNSW 0.6.0 2024-02-04 [?] CRAN (R 4.4.1)
P readr * 2.1.5 2024-01-10 [?] CRAN (R 4.4.0)
renv 1.1.4 2025-03-20 [1] CRAN (R 4.4.1)
P reshape2 1.4.4 2020-04-09 [?] CRAN (R 4.4.0)
P reticulate 1.42.0 2025-03-25 [?] CRAN (R 4.4.1)
P rlang 1.1.6 2025-04-11 [?] CRAN (R 4.4.1)
P rmarkdown 2.29 2024-11-04 [?] CRAN (R 4.4.1)
P ROCR 1.0-11 2020-05-02 [?] CRAN (R 4.4.0)
P rprojroot 2.0.4 2023-11-05 [?] CRAN (R 4.4.1)
P RSpectra 0.16-2 2024-07-18 [?] CRAN (R 4.4.0)
P rstudioapi 0.17.1 2024-10-22 [?] CRAN (R 4.4.1)
P Rtsne 0.17 2023-12-07 [?] CRAN (R 4.4.1)
P S7 0.2.0 2024-11-07 [?] CRAN (R 4.4.1)
P sass 0.4.10 2025-04-11 [?] CRAN (R 4.4.1)
P scales * 1.4.0 2025-04-24 [?] CRAN (R 4.4.1)
P scattermore 1.2 2023-06-12 [?] CRAN (R 4.4.0)
P sctransform 0.4.2 2025-04-30 [?] CRAN (R 4.4.1)
P sessioninfo 1.2.3 2025-02-05 [?] CRAN (R 4.4.1)
P Seurat * 5.3.0 2025-04-23 [?] CRAN (R 4.4.1)
P SeuratObject * 5.1.0 2025-04-22 [?] CRAN (R 4.4.1)
P shiny 1.10.0 2024-12-14 [?] CRAN (R 4.4.1)
P sp * 2.2-0 2025-02-01 [?] CRAN (R 4.4.1)
P spam 2.11-1 2025-01-20 [?] CRAN (R 4.4.1)
P spatstat.data 3.1-6 2025-03-17 [?] CRAN (R 4.4.1)
P spatstat.explore 3.4-3 2025-05-21 [?] CRAN (R 4.4.1)
P spatstat.geom 3.4-1 2025-05-20 [?] CRAN (R 4.4.1)
P spatstat.random 3.4-1 2025-05-20 [?] CRAN (R 4.4.1)
P spatstat.sparse 3.1-0 2024-06-21 [?] CRAN (R 4.4.0)
P spatstat.univar 3.1-3 2025-05-08 [?] CRAN (R 4.4.1)
P spatstat.utils 3.1-4 2025-05-15 [?] CRAN (R 4.4.1)
P stringi 1.8.7 2025-03-27 [?] CRAN (R 4.4.1)
P stringr * 1.6.0 2025-11-04 [?] CRAN (R 4.4.1)
P survival 3.6-4 2024-04-24 [?] CRAN (R 4.4.1)
P tensor 1.5 2012-05-05 [?] CRAN (R 4.4.1)
P tibble * 3.2.1 2023-03-20 [?] CRAN (R 4.4.0)
P tidygraph 1.3.1 2024-01-30 [?] CRAN (R 4.4.0)
P tidyr * 1.3.1 2024-01-24 [?] CRAN (R 4.4.1)
P tidyselect 1.2.1 2024-03-11 [?] CRAN (R 4.4.0)
P tidyverse * 2.0.0 2023-02-22 [?] CRAN (R 4.4.0)
P timechange 0.3.0 2024-01-18 [?] CRAN (R 4.4.1)
P tweenr 2.0.3 2024-02-26 [?] CRAN (R 4.4.0)
P tzdb 0.5.0 2025-03-15 [?] CRAN (R 4.4.1)
P utf8 1.2.5 2025-05-01 [?] CRAN (R 4.4.1)
P uwot 0.2.3 2025-02-24 [?] CRAN (R 4.4.1)
P vctrs 0.6.5 2023-12-01 [?] CRAN (R 4.4.0)
P viridis 0.6.5 2024-01-29 [?] CRAN (R 4.4.0)
P viridisLite 0.4.2 2023-05-02 [?] CRAN (R 4.4.1)
P whisker 0.4.1 2022-12-05 [?] CRAN (R 4.4.1)
P withr 3.0.2 2024-10-28 [?] CRAN (R 4.4.1)
P workflowr * 1.7.1 2023-08-23 [?] CRAN (R 4.4.0)
P xfun 0.56 2026-01-18 [?] CRAN (R 4.4.3)
P xtable 1.8-4 2019-04-21 [?] CRAN (R 4.4.1)
P yaml 2.3.10 2024-07-26 [?] CRAN (R 4.4.1)
P zoo 1.8-14 2025-04-10 [?] CRAN (R 4.4.1)
[1] /Users/maksimovicjovana/projects/active/flex-fusion-probes/renv/library/macos/R-4.4/aarch64-apple-darwin20
[2] /Users/maksimovicjovana/Library/Caches/org.R-project.R/R/renv/sandbox/macos/R-4.4/aarch64-apple-darwin20/f7156815
* ── Packages attached to the search path.
P ── Loaded and on-disk path mismatch.
──────────────────────────────────────────────────────────────────────────────
sessionInfo()
R version 4.4.1 (2024-06-14)
Platform: aarch64-apple-darwin20
Running under: macOS 15.7.9
Matrix products: default
BLAS: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib
LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.0
locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
time zone: Australia/Melbourne
tzcode source: internal
attached base packages:
[1] stats graphics grDevices datasets utils methods base
other attached packages:
[1] clustree_0.5.1 ggraph_2.2.1 scales_1.4.0 fs_1.6.6
[5] glue_1.8.0 here_1.0.1 ggrepel_0.9.6 patchwork_1.3.0
[9] lubridate_1.9.4 forcats_1.0.0 stringr_1.6.0 dplyr_1.1.4
[13] purrr_1.0.4 readr_2.1.5 tidyr_1.3.1 tibble_3.2.1
[17] ggplot2_4.0.2 tidyverse_2.0.0 Seurat_5.3.0 SeuratObject_5.1.0
[21] sp_2.2-0 workflowr_1.7.1
loaded via a namespace (and not attached):
[1] RColorBrewer_1.1-3 rstudioapi_0.17.1 jsonlite_2.0.0
[4] magrittr_2.0.3 spatstat.utils_3.1-4 farver_2.1.2
[7] rmarkdown_2.29 vctrs_0.6.5 ROCR_1.0-11
[10] memoise_2.0.1 spatstat.explore_3.4-3 htmltools_0.5.8.1
[13] sass_0.4.10 sctransform_0.4.2 parallelly_1.45.0
[16] KernSmooth_2.23-24 bslib_0.9.0 htmlwidgets_1.6.4
[19] ica_1.0-3 plyr_1.8.9 plotly_4.10.4
[22] zoo_1.8-14 cachem_1.1.0 whisker_0.4.1
[25] igraph_2.1.4 mime_0.13 lifecycle_1.0.4
[28] pkgconfig_2.0.3 Matrix_1.7-0 R6_2.6.1
[31] fastmap_1.2.0 fitdistrplus_1.2-2 future_1.49.0
[34] shiny_1.10.0 digest_0.6.37 colorspace_2.1-1
[37] ps_1.9.1 rprojroot_2.0.4 tensor_1.5
[40] RSpectra_0.16-2 irlba_2.3.5.1 labeling_0.4.3
[43] progressr_0.15.1 timechange_0.3.0 spatstat.sparse_3.1-0
[46] httr_1.4.7 polyclip_1.10-7 abind_1.4-8
[49] compiler_4.4.1 withr_3.0.2 backports_1.5.0
[52] S7_0.2.0 viridis_0.6.5 fastDummies_1.7.5
[55] ggforce_0.4.2 MASS_7.3-60.2 sessioninfo_1.2.3
[58] tools_4.4.1 lmtest_0.9-40 httpuv_1.6.16
[61] future.apply_1.11.3 goftest_1.2-3 callr_3.7.6
[64] nlme_3.1-164 promises_1.3.3 grid_4.4.1
[67] checkmate_2.3.2 Rtsne_0.17 getPass_0.2-4
[70] cluster_2.1.6 reshape2_1.4.4 generics_0.1.4
[73] gtable_0.3.6 spatstat.data_3.1-6 tzdb_0.5.0
[76] hms_1.1.3 data.table_1.18.2.1 utf8_1.2.5
[79] tidygraph_1.3.1 spatstat.geom_3.4-1 RcppAnnoy_0.0.22
[82] RANN_2.6.2 pillar_1.10.2 spam_2.11-1
[85] RcppHNSW_0.6.0 later_1.4.2 splines_4.4.1
[88] tweenr_2.0.3 lattice_0.22-6 renv_1.1.4
[91] survival_3.6-4 deldir_2.0-4 tidyselect_1.2.1
[94] miniUI_0.1.2 pbapply_1.7-4 knitr_1.50
[97] git2r_0.36.2 gridExtra_2.3 scattermore_1.2
[100] xfun_0.56 graphlayouts_1.2.2 matrixStats_1.5.0
[103] stringi_1.8.7 lazyeval_0.2.2 yaml_2.3.10
[106] evaluate_1.0.3 codetools_0.2-20 BiocManager_1.30.27
[109] cli_3.6.5 uwot_0.2.3 xtable_1.8-4
[112] reticulate_1.42.0 processx_3.8.6 jquerylib_0.1.4
[115] Rcpp_1.1.1-1.1 globals_0.18.0 spatstat.random_3.4-1
[118] png_0.1-8 spatstat.univar_3.1-3 parallel_4.4.1
[121] dotCall64_1.2 listenv_0.9.1 viridisLite_0.4.2
[124] ggridges_0.5.6 rlang_1.1.6 cowplot_1.1.3