Jellyfisher is an R package (a htmlwidget) for visualizing tumor evolution and subclonal compositions using Jellyfish plots. The package is based on the Jellyfish visualization tool, bringing its functionality to R users. Jellyfisher supports both ClonEvol results and plain data frames, making it compatible with various tools and workflows.
The input data should follow specific structures for samples, phylogeny, subclonal compositions, and optional ranks.
The jellyfisher
package includes an example data set
(jellyfisher_example_tables
) based on the following
publication:
Lahtinen, A., Lavikka, K., Virtanen, A., et al. “Evolutionary states and
trajectories characterized by distinct pathways stratify patients with
ovarian high-grade serous carcinoma.” Cancer Cell
41, 1103–1117.e12 (2023). DOI: 10.1016/j.ccell.2023.04.017.
sample
(string): specifies the unique identifier for
each sample.displayName
(string, optional): allows for specifying a
custom name for each sample. If the column is omitted, the
sample
column is used as the display name.rank
(integer): specifies the position of each sample
in the Jellyfish plot. For example, different stages of a disease can be
ranked in chronological order: diagnosis (1), interval (2), and relapse
(3). The zeroth rank is reserved for the root of the sample tree. Ranks
can be any integer, and unused ranks are automatically excluded from the
plot. If the rank
column is absent, ranks are assigned
based on each sample’s depth in the sample tree.parent
(string): identifies the parent sample for each
entry. Samples without a specified parent are treated as children of an
imaginary root sample.head(jellyfisher_example_tables$samples, 25)
#> sample displayName rank parent patient
#> 35 EOC69_pOme1_DNA1 pOme1 1 EOC69
#> 36 EOC69_pOva1_DNA2 pOva1 1 EOC69
#> 37 EOC69_r1Vag1_DNA1 r1Vag1 10 EOC69
#> 257 EOC495_pLNL1_DNA1 pLNL1 1 EOC495
#> 258 EOC495_pLNL2_DNA1 pLNL2 1 EOC495
#> 259 EOC495_pLNR_DNA1 pLNR 1 EOC495
#> 260 EOC495_pOvaL6_DNA1 pOvaL6 1 EOC495
#> 261 EOC495_pOvaL7_DNA1 pOvaL7 1 EOC495
#> 262 EOC495_pPerL_DNA1 pPerL 1 EOC495
#> 317 EOC677_pAsc_DNA1 pAsc 1 EOC677
#> 318 EOC677_pPer1_DNA pPer1 1 EOC677
#> 319 EOC677_r2Asc_DNA r2Asc 11 EOC677_rAsc_DNA4 EOC677
#> 320 EOC677_rAsc_DNA4 rAsc 9 EOC677_pAsc_DNA1 EOC677
#> 363 EOC809_p2Bow1_c_DNA2 p2Bow1_c 3 EOC809
#> 364 EOC809_p2Ome1_c_DNA1 p2Ome1_c 3 EOC809
#> 365 EOC809_p2OvaL1_c_DNA6 p2OvaL1_c 3 EOC809
#> 366 EOC809_p2Per1_cO_DNA2 p2Per1_cO 3 EOC809
#> 367 EOC809_r1Bow1_DNA1 r1Bow1 10 EOC809_p2Bow1_c_DNA2 EOC809
subclone
(string): specifies subclone IDs, which can be
any string.parent
(string): designates the parent subclone. The
subclone without a parent is considered the root of the phylogeny.color
(string, optional): specifies the color for the
subclone. If the column is omitted, colors will be generated
automatically.branchLength
(number): specifies the length of the
branch leading to the subclone. The length may be based on, for example,
the number of unique mutations in the subclone. The branch length is
shown in the Jellyfish plot’s legend as a bar chart. It is also used
when generating a phylogeny-aware color scheme.head(jellyfisher_example_tables$phylogeny, 25)
#> subclone parent color branchLength patient
#> 44 1 -1 #cccccc 2742 EOC69
#> 45 2 12 #a6cee3 478 EOC69
#> 46 5 9 #ff99ff 68 EOC69
#> 47 6 5 #fdbf6f 244 EOC69
#> 48 8 1 #bbbb77 2433 EOC69
#> 49 9 8 #cf8d30 313 EOC69
#> 50 11 8 #ff7f00 868 EOC69
#> 51 12 1 #3de4c5 4762 EOC69
#> 52 13 9 #ff1aff 1017 EOC69
#> 411 1 -1 #cccccc 1426 EOC495
#> 412 2 5 #a6cee3 184 EOC495
#> 413 3 6 #b2df8a 246 EOC495
#> 414 4 1 #cab2d6 2874 EOC495
#> 415 5 7 #ff99ff 864 EOC495
#> 416 6 5 #fdbf6f 154 EOC495
#> 417 7 1 #fb9a99 179 EOC495
#> 418 8 7 #bbbb77 631 EOC495
#> 419 9 4 #cf8d30 415 EOC495
#> 501 1 -1 #cccccc 4961 EOC677
#> 502 2 1 #a6cee3 239 EOC677
#> 503 4 5 #cab2d6 437 EOC677
#> 504 5 10 #ff99ff 1802 EOC677
#> 505 6 9 #fdbf6f 979 EOC677
#> 506 9 10 #cf8d30 223 EOC677
#> 507 10 1 #41ae76 314 EOC677
Subclonal compositions are specified in a tidy format, where each row represents a subclone in a sample.
sample
(string): specifies the sample ID.subclone
(string): specifies the subclone ID.clonalPrevalence
(number): specifies the clonal
prevalence of the subclone in the sample. The clonal prevalence is the
proportion of the subclone in the sample. The clonal prevalences in a
sample must sum to 1.head(jellyfisher_example_tables$compositions, 25)
#> sample subclone clonalPrevalence patient
#> 98 EOC69_pOme1_DNA1 5 0.2250 EOC69
#> 99 EOC69_pOme1_DNA1 6 0.0965 EOC69
#> 100 EOC69_pOme1_DNA1 13 0.6660 EOC69
#> 101 EOC69_pOva1_DNA2 6 0.4175 EOC69
#> 102 EOC69_pOva1_DNA2 11 0.5225 EOC69
#> 103 EOC69_pOva1_DNA2 13 0.0360 EOC69
#> 104 EOC69_r1Vag1_DNA1 2 0.3845 EOC69
#> 105 EOC69_r1Vag1_DNA1 12 0.5970 EOC69
#> 880 EOC495_pLNL1_DNA1 4 0.5575 EOC495
#> 881 EOC495_pLNL1_DNA1 9 0.4405 EOC495
#> 882 EOC495_pLNL2_DNA1 4 0.4635 EOC495
#> 883 EOC495_pLNL2_DNA1 9 0.5345 EOC495
#> 884 EOC495_pLNR_DNA1 1 0.1595 EOC495
#> 885 EOC495_pLNR_DNA1 4 0.5060 EOC495
#> 886 EOC495_pLNR_DNA1 5 0.0350 EOC495
#> 887 EOC495_pLNR_DNA1 9 0.2950 EOC495
#> 888 EOC495_pOvaL6_DNA1 3 0.5320 EOC495
#> 889 EOC495_pOvaL6_DNA1 5 0.0665 EOC495
#> 890 EOC495_pOvaL6_DNA1 6 0.3995 EOC495
#> 891 EOC495_pOvaL7_DNA1 2 0.5440 EOC495
#> 892 EOC495_pOvaL7_DNA1 5 0.4390 EOC495
#> 893 EOC495_pPerL_DNA1 1 0.1155 EOC495
#> 894 EOC495_pPerL_DNA1 8 0.8850 EOC495
#> 1063 EOC677_pAsc_DNA1 2 0.3180 EOC677
#> 1064 EOC677_pAsc_DNA1 9 0.2440 EOC677
The ranks in the example data set are used to indicate the time points when the samples were acquired.
rank
(integer): specifies the rank number. The zeroth
rank is reserved for the inferred root of the sample tree. However, you
are free to define a title for it.title
(string): specifies the title for the rank.The three tables are passed to the jellyfisher
function
as a named list. The function generates an interactive Jellyfish plot
based on the input data. If the data set contains multiple patients, the
Jellyfisher htmlwidget shows navigation buttons to switch between
patients.
When plotting multiple patients, Jellyfisher shows buttons (Previous
and Next) to navigate between patients. When the data contains only one
patient, these buttons are hidden. The package also provides a
select_patients
function to filter the data set with
ease.
The sample trees in the example data set were constructed as follows:
“For each sample, we checked whether an earlier time point included exactly one sample from the same anatomical location. If such a sample existed, it was assigned as the parent; otherwise, the inferred root was used as the parent.”
However, this mechanistic approach may not always produce credible sample trees.
The r1Bow1 (bowel) sample in the following jellyfish plot is derived from an earlier bowel sample p2Bow1_c, which has no traces of the subclone 12.
jellyfisher_example_tables |>
select_patients("EOC809") |>
jellyfisher(width = "100%", height = 600)
Using the set_parents
function, we can adjust the parent
of the r1Bow1 sample to be p2Per1_cO (peritoneum),
which is a possible source of the metastasis due to its proximity. The
high prevalence of subclone 12 in this sample suggests that it is the
likely source of the metastasis in the r1Bow1 sample.
While ranks (the columns) can indicate the time points when the samples were acquired, they can also be used to simply show the sample’s depth in the sample tree. For instance, the following plot shows all the samples on the same rank, indicating that they were diagnostic samples acquired at the same time.
jellyfisher_example_tables |>
select_patients("EOC495") |>
jellyfisher(width = "100%", height = 650)
However, one can argue that the LN (lymph node) samples represent a later development in the disease, and thus, they should be placed on a later rank. We can remove the existing ranks, define new parent-child relationships, and let Jellyfisher assign the ranks based on the sample tree depth.
tables <- jellyfisher_example_tables |>
select_patients("EOC495")
# Remove existing ranks. The ranks will be assigned automatically based
# on samples' depths in the sample tree.
tables$samples$rank <- NA
# Rank titles should be removed as well because they are no longer valid.
tables$ranks <- NULL
tables |>
set_parents(list("EOC495_pLNL1_DNA1" = "EOC495_pLNR_DNA1",
"EOC495_pLNL2_DNA1" = "EOC495_pLNL1_DNA1")) |>
jellyfisher(width = "100%", height = 500)
If we think that the lymph node samples represent an even later
development, we can manually assign ranks to the samples. The
set_ranks
function provides an easy way to do this.
Typically in tumor evolution studies, the focus is on the subclonal
compositions of the tumor cells. Thus, the root node in the phylogenetic
tree represents the founding clone. However, the data may also contain
non-aberrant cells, i.e., the tumor purity is less than 100%. For these
cases, the normalsInPhylogenyRoot
option instructs
Jellyfisher to treat the root node as non-aberrant cells. The option has
two consequences: (1) No tentacles are drawn between the root subclones,
and (2) the root subclone is colored as white when the phylogeny-aware
color scheme is used.
# Subclone N at the root represents the non-aberrant cells.
# The letter N has no special meaning in Jellyfisher.
non_aberrant <- list(
samples = data.frame(sample = c("A", "B")),
compositions = data.frame(
sample = c("A", "A", "A", "B", "B", "B"),
subclone = c("N", "1", "2", "N", "1", "2"),
clonalPrevalence = c(0.2, 0.4, 0.4, 0.3, 0.3, 0.4)
),
phylogeny = data.frame(
subclone = c("N", "1", "2"),
parent = c(NA, "N", "1")
)
)
non_aberrant |>
jellyfisher(options = list(
normalsAtPhylogenyRoot = TRUE
),
width = "100%", height = 350)
In the above plot, the root clone, which represents the non-aberrant cells, is hidden from the inferred root sample. However, sometimes a patient may have multiple independent clones, and in these cases the root clone is shown:
sessionInfo()
#> R version 4.4.2 (2024-10-31)
#> Platform: aarch64-apple-darwin20
#> Running under: macOS Sequoia 15.3
#>
#> 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] C/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
#>
#> time zone: Europe/Helsinki
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] jellyfisher_1.0.4
#>
#> loaded via a namespace (and not attached):
#> [1] vctrs_0.6.5 cli_3.6.3 knitr_1.49 rlang_1.1.5
#> [5] xfun_0.50 stringi_1.8.4 generics_0.1.3 jsonlite_1.8.9
#> [9] glue_1.8.0 htmltools_0.5.8.1 sass_0.4.9 rmarkdown_2.29
#> [13] evaluate_1.0.3 jquerylib_0.1.4 tibble_3.2.1 fastmap_1.2.0
#> [17] yaml_2.3.10 lifecycle_1.0.4 stringr_1.5.1 compiler_4.4.2
#> [21] dplyr_1.1.4 htmlwidgets_1.6.4 pkgconfig_2.0.3 rstudioapi_0.17.1
#> [25] digest_0.6.37 R6_2.5.1 tidyselect_1.2.1 pillar_1.10.1
#> [29] magrittr_2.0.3 bslib_0.9.0 tools_4.4.2 cachem_1.1.0