Skip to contents

Writing iSEE extensions

In this vignette, we illustrate how to:

  • Extend an existing panel class into a new panel class
  • Derive a new panel class from an existing panel

Load example data

library("iSEE")

sce <- readRDS(
  system.file("datasets", "sce_pbmc3k.RDS", package = "iUSEiSEE")
)

Philosophy

iSEE panels are implemented as S4 classes that store the state of each panel in designated slots, and most of the functionality is implemented as S4 methods that describe various behavioural aspects of each panel class.

As a result, new classes can be created simply by inheritance from existing classes, overwriting methods to produce a different behaviour.

Deriving an S4 class

In this demo, let us create a new class called ReducedDimensionHexagonPlot, that inherits from the existing class ReducedDimensionPlot() (implemented in the iSEE package).

The objective is to produce a panel that displays the same information as the original ReducedDimensionPlot() panel, but summarising data points into hexagonal bins.

One major motivation for this functionality is that arbitrarily large data sets comprising many data points may be summarised into a modest number of hexagonal bins, providing a boost in speed when rendering the plot.

However, this is not without caveats:

  • Summarising can take many forms (e.g., counting, averaging).
  • The ideal number of bins may be different for different data sets.

To proceed, the new class needs at least one additional slot to store the number of hexagonal bins to create (i.e., a resolution for the plot).

setClass("ReducedDimensionHexagonPlot", contains="ReducedDimensionPlot", 
         slots=c(BinResolution = "numeric"))

To properly initialise instances of the new class, the new slot must be populated with a default value in the event that users do not specify a bin resolution.

For this, we need to create a new method for the generic initialize() and the new class. We set the default resolution to 20 hexagonal bins along each axis.

setMethod("initialize", "ReducedDimensionHexagonPlot", function(.Object, ...) {
  args <- list(...)
  
  args <- .emptyDefault(args, "BinResolution", 20)
  
  do.call(callNextMethod, c(list(.Object), args))
})

Creating a constructor function

It is best practice to give users a function to create objects a particular class. Traditionally, the function is named identically to the class.

In this example, the function passes all its arguments to the new() function. This is standard practice in iSEE, where the arguments for constructor functions are typically the values of the various slots that describe the initial state of the panel.

ReducedDimensionHexagonPlot <- function(...) {
  new("ReducedDimensionHexagonPlot", ...)
}

At this point, we can already demonstrate that we have a functional new panel class… that is a carbon copy of the parent class it inherits from!

app <- iSEE(sce, initial = list(
  ReducedDimensionHexagonPlot(PanelWidth = 6L),
  ReducedDimensionPlot(PanelWidth = 6L)
))

How can we even tell which is which?!

Changing the public panel name

The generic .fullName() declares the label shown in the interface, at the top of each panel.

Let us create a method for the new class, that gives it a different name, highlighting the hexagonal binning in the plot.

setMethod(".fullName", "ReducedDimensionHexagonPlot", 
          function(x) "Hexagonal reduced dimension plot")

Let’s launch the app to see the effect

app <- iSEE(sce, initial = list(
  ReducedDimensionHexagonPlot(PanelWidth = 6L),
  ReducedDimensionPlot(PanelWidth = 6L)
))

Changing the background panel colour

Similarly, the generic .panelColor() declares the background colour of the panel header (as well as the colour of brushes for compatible panels).

Let us create a method for the new class, that gives it a different colour.

setMethod(".panelColor", "ReducedDimensionHexagonPlot", function(x) "#991717")

Let’s launch the app to see the effect

app <- iSEE(sce, initial = list(
  ReducedDimensionHexagonPlot(PanelWidth = 6L),
  ReducedDimensionPlot(PanelWidth = 6L)
))

Altering the user interface

At this point, there is still no component in the user interface to control the resolution value store in the new slot.

A number of S4 methods control the interface elements displayed in the various groups of parameters located under each panel.

Here, we are discussing a parameter that will control the size of bins. Moreover, we are about to replace data points by hexagonal bins, meaning that the visual parameters controlling the size of data points are about to become useless.

Thus, the generic .defineVisualSizeInterface() is the natural choice to use for replacing the size parameters of the parent ReducedDimensionHexagonPlot() class by a new HTML element that displays a numeric input allowing users to change the hexagonal bin resolution.

setMethod(".defineVisualSizeInterface", "ReducedDimensionHexagonPlot", function(x) {
  plot_name <- .getEncodedName(x)
  
  tagList(
    .numericInput.iSEE(x, "BinResolution", label="Bin resolution:",
                       min=1, value=x[["BinResolution"]], step = 1)
  )
})
app <- iSEE(sce, initial = list(
  ReducedDimensionHexagonPlot(PanelWidth = 6L, VisualBoxOpen = TRUE, VisualChoices = "Size"),
  ReducedDimensionPlot(PanelWidth = 6L, VisualBoxOpen = TRUE, VisualChoices = "Size")
))

Altering the panel output

Time for the most exciting and challenging task: making the panel produce a different type of plot!

To this end, the generic .generateDotPlot() is key. Given a panel state, axis labels, and an environment that contains the SummarizedExperiment() object and possibly other relevant values such as incoming selections,

setMethod(".generateDotPlot", "ReducedDimensionHexagonPlot", function(x, labels, envir) {
  plot_cmds <- list()
  plot_cmds[["ggplot"]] <- "ggplot() +"
  color_lab <- "Count"
  new_aes <- .buildAes()
  plot_cmds[["hex"]] <- sprintf(
    "ggplot2::geom_hex(%s, bins = %i, plot.data) +",
    new_aes,
    as.integer(x[["BinResolution"]])
  )
  plot_cmds[["theme_base"]] <- "theme_bw()"
  
  list(plot=.textEval(plot_cmds, envir), commands=plot_cmds)
})
app <- iSEE(sce, initial = list(
  ReducedDimensionHexagonPlot(PanelWidth = 6L, VisualBoxOpen = TRUE, VisualChoices = "Size"),
  ReducedDimensionPlot(PanelWidth = 6L, VisualBoxOpen = TRUE, VisualChoices = "Size")
))

Adding reactivity

At this point, if users change the bin resolution using the newly added interface element, they will notice that nothing seems to happen.

That is because we have not yet added a Shiny observer responding to this particular event.

The .createObservers() generic is invoked to create observers for all the instances of each panel class added to the user interface.

The .createProtectedParameterObservers() function provides a convenient way to create observers responding to events that change the data being plotted and potentially break active multiple selections, if any. This function automatically ensures that any change of value in the interface element is updated in the panel state and triggers re-rendering of the panel plot.

setMethod(".createObservers", "ReducedDimensionHexagonPlot", function(x, se, input, session, pObjects, rObjects) {
    callNextMethod()

    plot_name <- .getEncodedName(x)

    .createProtectedParameterObservers(plot_name,
        fields=c("BinResolution"),
        input=input, pObjects=pObjects, rObjects=rObjects)

    invisible(NULL)
})

With the new observer in place, we can launch the app one more time, to toy with the bin resolution and watch the panel plot being re-rendered each time.

app <- iSEE(sce, initial = list(
  ReducedDimensionHexagonPlot(PanelWidth = 6L, VisualBoxOpen = TRUE, VisualChoices = "Size"),
  ReducedDimensionPlot(PanelWidth = 6L)
))

Further work

At this point, there are many more aspects of the plotting behaviour that we should test and adjust, making sure that all the choices of parameters presented to end users are sensible, and that all combinations of parameters are handled without error.

However, that work represents many hours of careful testing and implementation that is far beyond the scope of this short package demo.

Adventurous souls may be interested in a more mature implementation of the ReducedDimensionHexPlot() panel class, implemented in the iSEEhex package. Source code can be found on GitHub on this page.

Additional resources

The book Extending iSEE is the original effort from developers to provide guidance for writing iSEE extensions using example use cases for illustration.

The more recent iSEETheBook launched a new effort to provide a comprehensive view of the ecosystem for all three major groups of users:

  • End users interacting with the graphical user interface in their web browser.
  • Developers of scripted web-applications using iSEE to showcase data sets.
  • Developers of extensions contributing new functionality and packages to the iSEE ecosystem.

The iSEE website is the place to watch for links to additional resources.

Session info

Session info
sessionInfo()
#> R version 4.5.1 (2025-06-13)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.2 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
#>  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
#>  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
#>  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
#> 
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats4    stats     graphics  grDevices utils     datasets  methods  
#> [8] base     
#> 
#> other attached packages:
#>  [1] iSEE_2.20.0                 SingleCellExperiment_1.30.1
#>  [3] SummarizedExperiment_1.38.1 Biobase_2.68.0             
#>  [5] GenomicRanges_1.60.0        GenomeInfoDb_1.44.1        
#>  [7] IRanges_2.42.0              S4Vectors_0.46.0           
#>  [9] BiocGenerics_0.54.0         generics_0.1.4             
#> [11] MatrixGenerics_1.20.0       matrixStats_1.5.0          
#> [13] BiocStyle_2.36.0           
#> 
#> loaded via a namespace (and not attached):
#>  [1] rlang_1.1.6             magrittr_2.0.3          shinydashboard_0.7.3   
#>  [4] clue_0.3-66             GetoptLong_1.0.5        compiler_4.5.1         
#>  [7] mgcv_1.9-3              png_0.1-8               systemfonts_1.2.3      
#> [10] vctrs_0.6.5             pkgconfig_2.0.3         shape_1.4.6.1          
#> [13] crayon_1.5.3            fastmap_1.2.0           XVector_0.48.0         
#> [16] fontawesome_0.5.3       promises_1.3.3          rmarkdown_2.29         
#> [19] UCSC.utils_1.4.0        shinyAce_0.4.4          ragg_1.4.0             
#> [22] xfun_0.52               cachem_1.1.0            jsonlite_2.0.0         
#> [25] listviewer_4.0.0        later_1.4.2             DelayedArray_0.34.1    
#> [28] parallel_4.5.1          cluster_2.1.8.1         R6_2.6.1               
#> [31] bslib_0.9.0             RColorBrewer_1.1-3      jquerylib_0.1.4        
#> [34] Rcpp_1.1.0              iterators_1.0.14        knitr_1.50             
#> [37] httpuv_1.6.16           Matrix_1.7-3            splines_4.5.1          
#> [40] igraph_2.1.4            tidyselect_1.2.1        abind_1.4-8            
#> [43] yaml_2.3.10             doParallel_1.0.17       codetools_0.2-20       
#> [46] miniUI_0.1.2            lattice_0.22-7          tibble_3.3.0           
#> [49] shiny_1.11.1            evaluate_1.0.4          desc_1.4.3             
#> [52] circlize_0.4.16         pillar_1.11.0           BiocManager_1.30.26    
#> [55] DT_0.33                 foreach_1.5.2           shinyjs_2.1.0          
#> [58] ggplot2_3.5.2           scales_1.4.0            xtable_1.8-4           
#> [61] glue_1.8.0              tools_4.5.1             colourpicker_1.3.0     
#> [64] fs_1.6.6                grid_4.5.1              colorspace_2.1-1       
#> [67] nlme_3.1-168            GenomeInfoDbData_1.2.14 vipor_0.4.7            
#> [70] cli_3.6.5               textshaping_1.0.1       S4Arrays_1.8.1         
#> [73] viridisLite_0.4.2       ComplexHeatmap_2.24.1   dplyr_1.1.4            
#> [76] gtable_0.3.6            rintrojs_0.3.4          sass_0.4.10            
#> [79] digest_0.6.37           SparseArray_1.8.1       ggrepel_0.9.6          
#> [82] rjson_0.2.23            htmlwidgets_1.6.4       farver_2.1.2           
#> [85] htmltools_0.5.8.1       pkgdown_2.1.3           lifecycle_1.0.4        
#> [88] httr_1.4.7              shinyWidgets_0.9.0      GlobalOptions_0.1.2    
#> [91] mime_0.13