Fluorescent Reports
David Rach
University of Maryland, Baltimoredrach@som.umaryland.edu
1 June 2025
Source:vignettes/FluorescentReports.rmd
FluorescentReports.rmd
Introduction
This vignette builds on what can be done with the outputs from the
Luciernaga_QC()
function that was the main focus of
vignette 04_FluorescenceSignatures. Please read that vignette for all
the details regarding how the function processes the .fcs files into
individual signatures.
The following vignette covers the different functions that take these
Luciernaga_QC()
outputs, either in the “data” or “fcs”
format, and process them into useful reports to gain insight into the
fluorescence signatures that are present within your unmixing controls
(both single-color and unstained). At the end of this vignette, we will
discuss how we can extend these reports to visualize changes in
signature across time to monitor for changes in antibody-vial quality or
instrument health.
Some of these functions we will cover in this vignette will also resurface when we look at the Luciernaga unmixing functions that will be covered in Vignette_06.
Set Up
For this vignette, we will focus on characterizing the cell unmixing
controls. The following setup code is a repeat from previous vignettes,
and is used to provide the Luciernaga_QC()
outputs needed
by the functions that we will be covering.
Let’s first load the required packages by calling them with library.
library(Luciernaga)
library(flowCore)
library(flowWorkspace)
library(openCyto)
library(ggcyto)
library(data.table)
library(dplyr)
library(purrr)
library(stringr)
library(ggplot2)
library(gt)
library(plotly)
library(htmltools)
Then we can find the .fcs files stored within the
Luciernaga
packages extdata folder and sort them by their
respective type
File_Location <- system.file("extdata", package = "Luciernaga")
FCS_Pattern <- ".fcs$"
FCS_Files <- list.files(path = File_Location, pattern = FCS_Pattern,
full.names = TRUE, recursive = FALSE)
head(FCS_Files[10:30], 20)
UnstainedFCSFiles <- FCS_Files[grep("Unstained", FCS_Files)]
UnstainedBeads <- UnstainedFCSFiles[grep("Beads", UnstainedFCSFiles)]
UnstainedCells <- UnstainedFCSFiles[-grep("Beads", UnstainedFCSFiles)]
BeadFCSFiles <- FCS_Files[grep("Beads", FCS_Files)]
BeadSingleColors <- BeadFCSFiles[-grep("Unstained", BeadFCSFiles)]
CellSingleColorFiles <- FCS_Files[grep("Cells", FCS_Files)]
CellSingleColors <- CellSingleColorFiles[!str_detect("Unstained", CellSingleColorFiles)]
Now lets create a GatingSet for our single-color cell unmixing controls
MyCytoSet <- load_cytoset_from_fcs(CellSingleColors,
truncate_max_range = FALSE,
transform = FALSE)
MyCytoSet
MyGatingSet <- GatingSet(MyCytoSet)
MyGatingSet
FileLocation <- system.file("extdata", package = "Luciernaga")
MyGates <- fread(file.path(path = FileLocation, pattern = 'Gates.csv'))
gt(MyGates)
MyGatingTemplate <- gatingTemplate(MyGates)
gt_gating(MyGatingTemplate, MyGatingSet)
MyGatingSet[[1]]
Now lets create a GatingSet for our unstained cell unmixing controls
MyUnstainedCytoSet <- load_cytoset_from_fcs(UnstainedCells,
truncate_max_range = FALSE,
transform = FALSE)
MyUnstainedCytoSet
MyUnstainedGatingSet <- GatingSet(MyUnstainedCytoSet)
MyUnstainedGatingSet
FileLocation <- system.file("extdata", package = "Luciernaga")
MyGates <- fread(file.path(path = FileLocation, pattern = 'Gates.csv'))
gt(MyGates)
MyGatingTemplate <- gatingTemplate(MyGates)
gt_gating(MyGatingTemplate, MyUnstainedGatingSet)
MyUnstainedGatingSet[[1]]
Generate Luciernaga_QC Outputs
Now that the GatingSets are re-established, let’s continue where the
last vignette left off by processing all the fcs files with
Luciernaga_QC
to characterize the fluorescent signatures
within.
Let’s first provision the AFOverlap csv to handle conflicts.
FileLocation <- system.file("extdata", package = "Luciernaga")
pattern = "AutofluorescentOverlaps.csv"
AFOverlap <- list.files(path=FileLocation, pattern=pattern,
full.names = TRUE)
AFOverlap_CSV <- read.csv(AFOverlap, check.names = FALSE)
AFOverlap_CSV
And next generate a CellAF unstained signature that can be used when these fluorophore-autofluorescence overlap files are encountered:
# pData(MyUnstainedGatingSet[1])
removestrings <- c(".fcs")
TheCellAF <- map(.x=MyUnstainedGatingSet[1], .f=Luciernaga_QC, subsets="lymphocytes",
removestrings=removestrings, sample.name="GUID",
unmixingcontroltype = "cells", Unstained = TRUE,
ratiopopcutoff = 0.001, Verbose = FALSE,
AFOverlap = AFOverlap, stats = "median",
ExportType = "data", SignatureReturnNow = TRUE,
outpath = TemporaryFolder, Increments=0.1,
SecondaryPeaks=2, experiment = "FirstExperiment",
condition = "ILTPanel", SCData="subtracted",
NegativeType="default")
TheCellAF <- TheCellAF[[1]] #Removes list caused by map
gt(TheCellAF)
We will start with the ExportType = “data” return for now for both the cell Single-Color and Unstained Unmixing controls.
SingleColor_Data <- map(.x=MyGatingSet, .f=Luciernaga_QC, subsets="lymphocytes",
removestrings=removestrings, sample.name="GUID",
unmixingcontroltype = "cells", Unstained = FALSE,
ratiopopcutoff = 0.001, Verbose = FALSE,
AFOverlap = AFOverlap, stats = "median",
ExportType = "data", SignatureReturnNow = FALSE,
outpath = TemporaryFolder, Increments=0.1,
SecondaryPeaks=2, experiment = "FirstExperiment",
condition = "ILTPanel", Subtraction = "Internal",
CellAF=TheCellAF, SCData="subtracted",
NegativeType="default") %>% bind_rows()
nrow(SingleColor_Data)
Let’s now repeat this process for the Unstained GatingSet.
# pData(MyUnstainedGatingSet)
Unstained_Data <- map(.x=MyUnstainedGatingSet, .f=Luciernaga_QC, subsets="lymphocytes",
removestrings=removestrings, sample.name="GUID",
unmixingcontroltype = "cells", Unstained = TRUE,
ratiopopcutoff = 0.001, Verbose = FALSE,
AFOverlap = AFOverlap, stats = "median",
ExportType = "data", SignatureReturnNow = FALSE,
outpath = TemporaryFolder, Increments=0.1,
SecondaryPeaks=2, experiment = "FirstExperiment",
condition = "ILTPanel", Subtraction = "Internal",
CellAF=TheCellAF, SCData="subtracted",
NegativeType="default") %>% bind_rows()
nrow(Unstained_Data)
Luciernaga_Plots
The Luciernaga_Plots()
function works directly from the
Luciernaga_QC()
output when the ExportType = “data”. Let’s
start exploring what it can do.
We will first provide a file.path to the panel information, which
will be used by Luciernaga_Plots()
to arrange the
plots/report by the order of the fluorophores listed in the panel. If
there is a mismatch between the fluorophore data and the panel, it will
not rearrange but proceed to the output.
FileLocation <- system.file("extdata", package = "Luciernaga")
pattern = "^Panel.csv"
CSV <- list.files(path=FileLocation, pattern=pattern, full.names=TRUE)
TheFluorophoreOrder <- read.csv(CSV, check.names = FALSE)
The SingleColor_Data output from above currently contains the
information for every single fluorophore .fcs file that was processed.
While we can pass the entire object to Luciernaga_Plots()
and it will process a report for each, we can also tailor our output by
using dplyr to target
particular fluorophores of interest if we want to find information for a
specific fluorophore.
BUV615 <- SingleColor_Data %>% filter(str_detect(Sample, "BUV615"))
nrow(BUV615)
head(BUV615)
As you can tell, we processed Luciernaga_QC()
with
increments set at 0.1 so there was a lot of splitting into specific
Clusters for this fluorophore. This can be overwhelming for
Luciernaga_Plots()
default arguments in some individual
circumstances, such as the example below:
TheFluorophoreOrder <- read.csv(CSV, check.names = FALSE)
TheFluorophore <- TheFluorophoreOrder %>% filter(Fluorophore %in% "BUV615")
ThePlotReport <- Luciernaga_Plots(data=BUV615, RetainedType="normalized", CellPopRatio=0.001,
outfolder=NULL, filename="LuciernagaReport", returntype="patchwork",
LinePlots=TRUE, CosinePlots=TRUE, StackedBarPlots = TRUE,
HeatmapPlots = TRUE, reference = TheFluorophore)
ThePlotReport[[1]]
To work around this, when the data.frame is handed to
Luciernaga_Plot()
, we can filter by CellPopRatio to dictate
what percentage of the total outputed cells a Cluster needs to be to be
visualized. For the clusters that are excluded, these are gathered by
the function and classified as Cluster “Other”. Let’s see this in action
by setting CellPopRatio to 0.05 (so a cluster is retained if it makes up
5% stained cells):
TheFluorophoreOrder <- read.csv(CSV, check.names = FALSE)
TheFluorophore <- TheFluorophoreOrder %>% filter(Fluorophore %in% "BUV615")
ThePlotReport <- Luciernaga_Plots(data=BUV615, RetainedType="normalized", CellPopRatio=0.05,
outfolder=NULL, filename="LuciernagaReport", returntype="patchwork",
LinePlots=TRUE, CosinePlots=TRUE, StackedBarPlots = TRUE,
HeatmapPlots = TRUE, reference = TheFluorophore)
ThePlotReport[[1]]
From the above we can see the basic available formats of the
Luciernaga_Plots()
outputs. Namely, from the
Luciernaga_QC()
output we can visualize LinePlots (showing
visualized signatures), CosinePlots (showing the cosine similarity
matrix values of individual signatures relative to each other),
StackedBarPlots and HeatmapPlots (which in different ways show relative
abundance of the cluster signatures).
For the above example, we are using the returntype=“patchwork”
argument. As with other Luciernaga
packages, it takes the
same arguments that are passed to the Utility_Patchwork()
function to dictate layout. Similarly, we can set returntype = “pdf” to
generate a .pdf file with the report contents sent to the location
specified by the outfolder and filename.
ThePlotReport <- Luciernaga_Plots(data=BUV615, RetainedType="normalized", CellPopRatio=0.05,
outfolder=NULL, filename="LuciernagaReport", returntype="patchwork",
LinePlots=TRUE, CosinePlots=TRUE, StackedBarPlots = TRUE,
HeatmapPlots = TRUE, reference = TheFluorophore, thecolumns=2,
therows=2, width=7, height=9)
ThePlotReport[[1]]
In the cases above, the report returned all four plots. These can be turned on or off as desired. For example, if we want to only retrieve only individual plot elements, we would leave our desired plot value as TRUE, set the other plot arguments to FALSE.
ThePlotReport <- Luciernaga_Plots(data=BUV615, RetainedType="normalized", CellPopRatio=0.05,
outfolder=NULL, filename="LuciernagaReport", returntype="patchwork",
LinePlots=TRUE, CosinePlots=FALSE, StackedBarPlots = FALSE,
HeatmapPlots = TRUE, reference = TheFluorophore, thecolumns=2,
therows=2, width=7, height=9)
ThePlotReport[[1]]
Additionally, we would switch from returntype = “patchwork” to
“plots” to retrieve the actual ggplot2 object of interest. The returned
objects are still in a list format, so we use
rCRANpkg("purrr")
flatten function to get rid of the list,
allowing the ggplot2 object to be handed to
rCRANpkg("plotly")
ggplotly function to allow for
interactive elements.
ThePlotReport <- Luciernaga_Plots(data=BUV615, RetainedType="normalized", CellPopRatio=0.05,
outfolder=NULL, filename="LuciernagaReport", returntype="plots",
LinePlots=TRUE, CosinePlots=FALSE, StackedBarPlots = FALSE,
HeatmapPlots = FALSE, reference = TheFluorophore)
ThePlotReport <- purrr::flatten(ThePlotReport)
plotly::ggplotly(ThePlotReport[[1]])
ThePlotReport <- Luciernaga_Plots(data=BUV615, RetainedType="normalized", CellPopRatio=0.05,
outfolder=NULL, filename="LuciernagaReport", returntype="plots",
LinePlots=FALSE, CosinePlots=TRUE, StackedBarPlots = FALSE,
HeatmapPlots = FALSE, reference = TheFluorophore)
ThePlotReport <- purrr::flatten(ThePlotReport)
plotly::ggplotly(ThePlotReport[[1]])
ThePlotReport <- Luciernaga_Plots(data=BUV615, RetainedType="normalized", CellPopRatio=0.05,
outfolder=NULL, filename="LuciernagaReport", returntype="plots",
LinePlots=FALSE, CosinePlots=FALSE, StackedBarPlots = TRUE,
HeatmapPlots = FALSE, reference = TheFluorophore)
ThePlotReport <- purrr::flatten(ThePlotReport)
plotly::ggplotly(ThePlotReport[[1]])
ThePlotReport <- Luciernaga_Plots(data=BUV615, RetainedType="normalized", CellPopRatio=0.05,
outfolder=NULL, filename="LuciernagaReport", returntype="plots",
LinePlots=FALSE, CosinePlots=FALSE, StackedBarPlots = FALSE,
HeatmapPlots = TRUE, reference = TheFluorophore)
ThePlotReport <- purrr::flatten(ThePlotReport)
plotly::ggplotly(ThePlotReport[[1]])
Since the returned plots are ggplot2 objects, you can additionally
modify them further using ggplot2 arguments to change respective
elements appearance and aesthetics however you see fit. Please refer to
rCRANpkg("ggplot2")
vignettes and tutorials on how to do
this, as it’s outside of our scope for this vignette.
Having characterized the pieces of Luciernaga_Plots()
for a single fluorophore, let’s quickly see how it behaves without the
initial filtering for the entire SC_Data
TheFluorophoreOrder <- read.csv(CSV, check.names = FALSE)
ThePlotReport <- Luciernaga_Plots(data=SingleColor_Data, RetainedType="normalized", CellPopRatio=0.05,
outfolder=NULL, filename="LuciernagaReport", returntype="patchwork",
LinePlots=TRUE, CosinePlots=TRUE, StackedBarPlots = TRUE,
HeatmapPlots = TRUE, reference = TheFluorophore)
ThePlotReport[1:4]
As can be seen, it automatically processed the data for each individual fluorophore. As with the above, the arguments can be altered to turn on/off individual plot elements, change the orientation, and alter the return type as with the examples above.
TheFluorophoreOrder <- read.csv(CSV, check.names = FALSE)
ThePlotReport <- Luciernaga_Plots(data=SingleColor_Data, RetainedType="normalized", CellPopRatio=0.05,
outfolder=NULL, filename="LuciernagaReport", returntype="plots",
LinePlots=FALSE, CosinePlots=FALSE, StackedBarPlots = FALSE,
HeatmapPlots = TRUE, reference = TheFluorophore)
ThePlotReport[1:4]
The examples above relied on LuciernagaQC()
ExportType =
“data” output to generate the desired reports and plots. This can be
saved by individual users as a .csv that can be retrieved for later
use.
# Saving Elsewhere
StorageLocation <- file.path("C:", "Users", "JohnDoe", "Desktop")
TheName <- "SingleColorData.csv"
TheFinalLocation <- file.path(StorageLocation, TheName)
write.csv(SingleColor_Data, TheFinalLocation, row.names=FALSE)
# Reading in Later
TheRetrievedData <- read.csv(TheFinalLocation, check.names=FALSE)
Luciernaga_FCSToReport
Luciernaga_FCSToReport()
is a function that handles
cases when there is no LuciernagaQC()
ExportType = “data”
to allow for the generation of plots and reports, and instead, we need
to take the LuciernagaQC()
ExportType = “fcs” outputs from
their storage folder and re-derrive the equivalent data to be used with
Luciernaga_Plots()
.
To demonstrate this, let’s first switch the
Luciernaga_QC()
ExportType argument to “fcs” and store the
same single-color output .fcs files within the temporary folder in a
specific folder where we can retrieve them again when needed:
StorageLocation <- file.path(tempdir(), "LuciernagaTemporaryExamples")
if (!dir.exists(StorageLocation)) {
dir.create(StorageLocation)
}
SingleColor_Data <- map(.x=MyGatingSet[c(1:22, 25:30)], .f=Luciernaga_QC, subsets="lymphocytes",
removestrings=removestrings, sample.name="GUID",
unmixingcontroltype = "cells", Unstained = FALSE,
ratiopopcutoff = 0.001, Verbose = FALSE,
AFOverlap = AFOverlap, stats = "median",
ExportType = "fcs", Brightness=TRUE, SignatureReturnNow = FALSE,
outpath = StorageLocation, Increments=0.1,
SecondaryPeaks=2, experiment = "FirstExperiment",
condition = "ILTPanel", Subtraction = "Internal",
CellAF=TheCellAF, SCData="subtracted",
NegativeType="default")
TheLuciernagaOutputs_FCS <- list.files(StorageLocation, pattern="fcs", full.names = TRUE)
head(TheLuciernagaOutputs_FCS, 4)
TheLuciernagaOutputs_CSV <- list.files(StorageLocation, pattern="csv", full.names = TRUE)
head(TheLuciernagaOutputs_CSV, 4)
We can now proceed to evaluate
Luciernaga_FCSToReport()
:
ReferencePath <- system.file("extdata", package = "Luciernaga")
PanelPath <- file.path(ReferencePath, "Panel.csv")
#PanelNames <- read.csv(PanelPath, check.names = FALSE)
#PanelNames <- PanelNames %>% pull(Fluorophore) %>% gsub("-A", "", .)
ReportOutput <- Luciernaga_FCSToReport(path=StorageLocation, reference=PanelPath, stats="median",
RetainedType = "normalized", experiment="FirstExperiment",
condition="ILTExperiment", TheSummary = TRUE)
As you can see, we have returned a Luciernaga_QC()
style
output that we can now evaluate using Luciernaga_Plots()
as
described above:
ThePlotReport <- Luciernaga_Plots(data=ReportOutput, RetainedType="normalized", CellPopRatio=0.05,
outfolder=NULL, filename="LuciernagaReport", returntype="plots",
LinePlots=FALSE, CosinePlots=FALSE, StackedBarPlots = FALSE,
HeatmapPlots = TRUE, reference = PanelPath)
ThePlotReport[1]
Luciernaga_Brightness
One advantage of retrieving the data from LuciernagaQC()
ExportType = “fcs” outputs is that we can generate an additional type of
plot to look at the individual brightness for the cells that clustered
within the isolated signatures. This is accomplished by switching up a
couple arguments in Luciernaga_FCSToReport()
and passing
the intermediate to the Luciernaga_Brightness()
function
ReferencePath <- system.file("extdata", package = "Luciernaga")
PanelPath <- file.path(ReferencePath, "Panel.csv")
PanelNames <- read.csv(PanelPath, check.names = FALSE)
PanelNames <- PanelNames %>% filter(!str_detect(Fluorophore, "Pacific"))
PanelNames <- PanelNames %>% mutate(Fluorophore = case_when(
Fluorophore == "FITC" ~ "AlexaFluor488",
TRUE ~ Fluorophore # Keep all other values unchanged
))
PanelItems <- PanelNames %>% pull(Fluorophore) %>% gsub("-A", "", .)
BrightnessOutput <- Luciernaga_FCSToReport(path=StorageLocation, reference=PanelNames, stats="median",
RetainedType = "raw", experiment="FirstExperiment", condition="ILTExperiement",
TheSummary = FALSE)
PanelItems <- gsub(".", "", fixed=TRUE, PanelItems)
PanelItems <- gsub(" ", "", fixed=TRUE, PanelItems)
#PanelItems <- gsub("-", "", fixed=TRUE, PanelItems)
BrightnessOutput$Sample <- gsub(".", "", fixed=TRUE, BrightnessOutput$Sample)
BrightnessOutput$Sample <- gsub(" ", "", fixed=TRUE, BrightnessOutput$Sample)
#BrightnessOutput$Sample <- gsub("-", "", fixed=TRUE, BrightnessOutput$Sample)
BrightnessPlots <- map(.x=PanelItems[c(28:29)], .f=Luciernaga_Brightness, data=BrightnessOutput, reference=PanelNames, Scaled=TRUE)
ThePlot <- BrightnessPlots[[8]]
ThePlot
plotly::ggplotly(ThePlot)
In the above example, we needed to modify the list to exclude
PacificBlue Fluorophore that failed to generate in
Luciernaga_QC()
. The Panel.CSV also had a mismatch (FITC
instead AlexaFluor488) that we had to rename. And in my version the
APC-Fire810 bugged. So resolve these to fix this vignette :( and make it
easier to work with.
The resulting brightness plot is a geom_density plot showing the MFI for the respective clustered signatures. As you can tell from the ggplot2 object, a bunch of the signatures are relatively dim, which reflects the case seen in signatures.
Luciernaga_Lists
Luciernaga_Lists()
is a wrapper function that allows us
to merge in the Luciernaga_Brightness()
plots with
Luciernaga_Plots()
plots to generate a more complete view
of the fluorescent signatures for given samples. In addition to being
able to return a “pdf” plot, it can also be used to generate a “html”
output that is interactive (at the cost of being more storage space used
on your computer)
FinalPlots <- Luciernaga_Lists(ListOfList = TheMainPlots, SecondaryList = BrightnessPlots, thecolumns = 2,
therows = 3, width=9, height=7, ReturnFolder = ReferencePath,
CurrentExperiment = "Test", PlotType = "html")
Visualizing Longitudinally
All of the above functions focused their work primarily in the
context of visualizing the variation in Luciernaga_QC()
isolated fluorescent signatures within the context of a single
experiment. But what happens when we want to look longitudinally across
a series of experiments to monitor for tandem degradation in an antibody
vial, or pinpoint if the instrumental issue impacted fluorophore
signatures on a particular day?
The following functions are still in active development, and require a little more upfront coding on the part of the user to ensure that the data.frame object is tidy compatible, but we attach the workflow below.
Conclusion
And this concludes the vignette covering how to visualize the
Luciernaga_QC()
outputs to leverage insight into what is
occuring within individual fluorophores for a given experiment and
across time. Thanks for your patience and happy visualizing.