---
title: "Getting started with igvShiny"
author:
- name: Arkadiusz Gladki
package: igvShiny
output:
  BiocStyle::html_document:
    toc: true
    toc_depth: 2
vignette: >
  %\VignetteIndexEntry{Getting started with igvShiny}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
    collapse = TRUE,
    comment = "#>"
    )
```

# Who this is for

You have a Shiny app and some genomic data — intervals, alignments, variants,
association statistics — and you want to look at it in a genome browser without
leaving R. This vignette takes you from installation to a working app with a
track loaded from your own `data.frame`.

It assumes you have written a Shiny app before. It does not assume you know
[igv.js](https://github.com/igvteam/igv.js), the JavaScript browser `igvShiny`
wraps.

For the complete API — every track type and the options each one accepts — see
the *Track options reference* vignette. For a live app you can click through
before installing anything, see the
[demo on Posit Connect Cloud][live-demo].

[live-demo]: https://gladkia-igvshiny-demo.share.connect.posit.cloud

# Install

```{r install, eval=FALSE}
if (!requireNamespace("BiocManager", quietly = TRUE))
    install.packages("BiocManager")

BiocManager::install("igvShiny")
```

```{r load-package, results="hide", message=FALSE, warning=FALSE}
library(igvShiny)
```

# The three moving parts

An `igvShiny` browser is an htmlwidget, so it follows the usual Shiny pattern of
an output function in the UI and a render function on the server. What is
specific to this package is the third part: the browser has to be told which
genome to display *before* it is created.

| Part | Function | Where |
|------|----------|-------|
| Genome specification | `parseAndValidateGenomeSpec()` | top of the app |
| Placeholder in the layout | `igvShinyOutput()` | UI |
| The widget itself | `renderIgvShiny()` + `igvShiny()` | server |

`parseAndValidateGenomeSpec()` returns a validated list. Building it separately
means a bad genome name fails immediately, with a readable message, rather than
producing an empty browser in the page:

```{r genome-spec}
genomeOptions <- parseAndValidateGenomeSpec(
    genomeName = "hg38",
    initialLocus = "NDUFS2"
    )
str(genomeOptions)
```

`initialLocus` accepts a gene symbol, a `chrom:start-end` string, or `"all"` for
the whole-genome view.

# A complete app

Everything above assembled — this is the smallest useful `igvShiny` app:

```{r minimal-app, eval=FALSE}
library(shiny)
library(igvShiny)

genomeOptions <- parseAndValidateGenomeSpec(
    genomeName = "hg38",
    initialLocus = "NDUFS2"
    )

ui <- fluidPage(
    titlePanel("igvShiny"),
    igvShinyOutput("igv")
    )

server <- function(input, output, session) {
    output$igv <- renderIgvShiny({
        igvShiny(genomeOptions)
        })
    }

shinyApp(ui, server)
```

Two notes on sizing. `igvShinyOutput()` takes `height` as an explicit pixel
measure (`"800px"`); percentages do not work, because the browser needs a
concrete height to lay its panels out. And the widget only draws once its
container is visible, so if you put it behind a `tabsetPanel()` tab, expect it
to appear when that tab is first opened.

# Loading a track from your own data

Tracks are added by sending a message to a browser that already exists, so the
loaders are called from the server, take `session` and the widget `id`, and are
usually wired to an event:

```{r bed-track, eval=FALSE}
tbl <- data.frame(
    chrom = c("chr1", "chr1", "chr1"),
    start = c(7432000, 7437000, 7443000),
    end   = c(7436000, 7442000, 7447000),
    value = c(0.2, 0.9, 0.4),
    stringsAsFactors = FALSE
    )

server <- function(input, output, session) {
    output$igv <- renderIgvShiny({
        igvShiny(genomeOptions)
        })

    observeEvent(input$addTrack, {
        loadBedTrack(
            session,
            id = "igv",
            trackName = "my regions",
            tbl = tbl,
            color = "darkblue"
            )
        })
    }
```

`loadBedTrack()` needs `chrom`, `start` and `end` columns; anything else is
ignored. The other loaders follow the same shape — `loadBedGraphTrack()`,
`loadSegTrack()`, `loadVcfTrack()`, `loadGwasTrack()`,
`loadBamTrackFromLocalData()`, `loadSpliceJunctionTrackFromLocalData()` — and
each has a `*FromURL()` counterpart for data that already lives on a web
server:

```{r url-track, eval=FALSE}
gff3 <- paste0(
    "https://s3.amazonaws.com/igv.org.genomes/hg38/",
    "Homo_sapiens.GRCh38.94.chr.gff3.gz"
    )

loadGFF3TrackFromURL(
    session,
    id = "igv",
    trackName = "genes",
    gff3URL = gff3,
    indexURL = paste0(gff3, ".tbi"),
    color = "darkgreen",
    trackHeight = 100
    )
```

The `From URL` variants ask the *user's browser* to fetch the file, not R. A URL
that works in `download.file()` can still fail here if the server does not send
permissive CORS headers.

Tracks the user added during a session can be cleared with
`removeUserAddedTracks(session, id = "igv")`, or individually by name with
`removeTracksByName()`.

## Loading tracks at startup

Tracks that should always be present do not need an event. Pass them to
`igvShiny()` through `tracks`, as a list of igv.js track configurations:

```{r startup-tracks, eval=FALSE}
output$igv <- renderIgvShiny({
    igvShiny(
        genomeOptions,
        tracks = list(
            list(
                name = "genes",
                type = "annotation",
                format = "gff3",
                url = paste0(
                    "https://s3.amazonaws.com/igv.org.genomes/hg38/",
                    "Homo_sapiens.GRCh38.94.chr.gff3.gz"
                    ),
                indexed = FALSE
                )
            )
        )
    })
```

Keys that igv.js does not recognise are dropped with a warning rather than
passed through, so a typo produces a message instead of a silently empty track.

## Drawing two tracks in one panel

A `merged` track holds other tracks instead of a url of its own, and draws them
on top of each other in a single panel. Splice junction arcs over the coverage
they were spliced out of — the view sashimi plots are wanted for — is the case
it is worth knowing:

```{r merged-track, eval=FALSE}
sample <- paste0(
    "https://raw.githubusercontent.com/igvteam/igv-data/main/data/test/",
    "splice_junctions/splice_junction_track_test_cases_sampleA.",
    "chr15-92835700-93031800"
    )

output$igv <- renderIgvShiny({
    igvShiny(
        genomeOptions,
        tracks = list(
            list(
                name = "sampleA",
                type = "merged",
                height = 180,
                tracks = list(
                    list(
                        type = "wig",
                        format = "bigwig",
                        url = paste0(sample, ".bigWig"),
                        color = "rgb(190,190,190)"
                        ),
                    list(
                        type = "junction",
                        format = "bed",
                        url = paste0(sample, ".SJ.out.bed.gz"),
                        indexURL = paste0(sample, ".SJ.out.bed.gz.tbi"),
                        colorBy = "motif"
                        )
                    )
                )
            )
        )
    })
```

Member tracks go through the same option allowlist as any other track. A member
left with no usable url is dropped on its own, with a warning; the merged track
itself is dropped only once no member survives, rather than sent on empty, as
igv.js throws while building the browser if it is handed one.

# Moving around, and knowing where you are

To drive the browser from R, use `showGenomicRegion()`:

```{r navigate, eval=FALSE}
observeEvent(input$goto, {
    showGenomicRegion(
        session,
        id = "igv",
        region = "chr5:88,700,000-88,800,000"
        )
    })
```

Reading the position back is asynchronous, which is the one part of the API that
surprises people. `getGenomicRegion()` does not return the region; it asks the
browser for it, and the answer arrives later as a Shiny input named
`currentGenomicRegion.<id>`:

```{r read-region, eval=FALSE}
observeEvent(input$whereAmI, {
    getGenomicRegion(session, id = "igv")
    })

observeEvent(input[["currentGenomicRegion.igv"]], {
    message("now showing: ", input[["currentGenomicRegion.igv"]])
    })
```

The same input fires whenever the user pans or zooms, so an observer on it is
enough if you only want to follow the view rather than poll it.

Two other inputs are available: `igvReady`, which fires once the browser has
finished initialising — the right place to load tracks automatically — and
`trackClick`, which carries the feature the user clicked on.

# Inside a Shiny module

`igvShiny` works in modules with no extra arguments: the widget reads the
namespace from the session it is created in. The one thing to remember is that
the loaders address the browser by its *HTML element* id, not by the input name
— so they need the namespaced id, `ns("igv")`, and not `"igv"`:

```{r module, eval=FALSE}
igvModuleUI <- function(id) {
    ns <- NS(id)
    tagList(
        actionButton(ns("addTrack"), "Add track"),
        igvShinyOutput(ns("igv"))
        )
    }

igvModuleServer <- function(id, genomeOptions, tbl) {
    moduleServer(id, function(input, output, session) {
        ns <- session$ns

        output$igv <- renderIgvShiny({
            igvShiny(genomeOptions)
            })

        observeEvent(input$addTrack, {
            loadBedTrack(
                session,
                id = ns("igv"),
                trackName = "regions",
                tbl = tbl
                )
            })

        # the region input keeps its unnamespaced name inside the module
        observeEvent(input[["currentGenomicRegion.igv"]], {
            message("now showing: ", input[["currentGenomicRegion.igv"]])
            })
        })
    }
```

That observer follows the user panning and zooming. Asking for the region
explicitly with `getGenomicRegion()` is currently unreliable inside a module
unless the module id happens to be `"igv"` — see
[issue #134](https://github.com/gladkia/igvShiny/issues/134).

A runnable version is in `inst/demos/modules.R`.

# Stock genomes

Any genome igv.js knows can be requested by name — `get_css_genomes()` lists
them, reading the same registry the bundled igv.js does
([genomes3.json](https://igv.org/genomes/genomes3.json)).

Naming a stock genome means R fetches no sequence data: it reads the registry
only to validate the name, and the user's browser streams the sequence from
wherever the registry entry points. Since
igv.js 3.x that is a `.2bit` file, preferred over the entry's FASTA when both
are listed, and for the genomes igvShiny offers those `.2bit` files are hosted
by UCSC (`hgdownload.soe.ucsc.edu`). A UCSC outage therefore leaves a stock
genome blank, with 403s or timeouts in the JavaScript console — the package
itself is fine, and custom genomes (below) are unaffected because they carry
their own URLs.

# Custom genomes

For a genome outside the registry, supply the sequence yourself: a FASTA file,
its index, and optionally an annotation, either as local paths
(`dataMode = "localFiles"`) or URLs (`dataMode = "http"`). The package ships a
small example genome:

```{r custom-genome}
data_directory <- system.file(package = "igvShiny", "extdata")

customOptions <- parseAndValidateGenomeSpec(
    genomeName = "ribosomal RNA gene",
    initialLocus = "U13369.1:7,276-8,225",
    stockGenome = FALSE,
    dataMode = "localFiles",
    fasta = file.path(data_directory, "ribosomal-RNA-gene.fasta"),
    fastaIndex = file.path(data_directory, "ribosomal-RNA-gene.fasta.fai"),
    genomeAnnotation = file.path(data_directory, "ribosomal-RNA-gene.gff3")
    )
customOptions[c("genomeName", "dataMode", "stockGenome")]
```

The result goes to `igvShiny()` exactly like a stock genome specification.

The same genome served over HTTP instead, which is what you want once the app
runs somewhere other than your laptop. `dataMode = "http"` makes
`parseAndValidateGenomeSpec()` send a `HEAD` request to every URL, so this
chunk is shown rather than run — building the vignette stays offline:

```{r custom-genome-http, eval=FALSE}
base_url <- "https://gladki.pl/igvr/testFiles"

remoteOptions <- parseAndValidateGenomeSpec(
    genomeName = "ribosomal RNA gene",
    initialLocus = "U13369.1:7,276-8,225",
    stockGenome = FALSE,
    dataMode = "http",
    fasta = paste0(base_url, "/ribosomal-RNA-gene.fasta"),
    fastaIndex = paste0(base_url, "/ribosomal-RNA-gene.fasta.fai"),
    genomeAnnotation = paste0(base_url, "/ribosomal-RNA-gene.gff3")
    )
remoteOptions[c("genomeName", "dataMode", "stockGenome")]
```

`inst/demos/genomes.R` shows both variants side by side, picked with radio
buttons.

# When something does not show up

**The browser is blank.** Almost always the genome, not the track. Check the
JavaScript console: a 404 on the FASTA or its index leaves an empty browser.
Some upstream genome URLs have broken in the past without any change on our
side.

**A track loads but is empty.** Chromosome naming. If your data says `1` and the
genome says `chr1`, igv.js finds nothing and reports nothing. Compare one
interval against the locus shown in the search box.

**Nothing at all happens after a loader call.** The loaders are one-way messages
to the browser: no error comes back to R when they fail. Call the loader with
`quiet = FALSE` to log what was sent, and read the JavaScript console for what
happened to it.

**Local data files 404.** Tracks built from R data are written to a temporary
directory that Shiny serves. If you have set `TRACKS_DIR`, make sure the process
can write there; `get_tracks_dir()` reports the directory in use.

# Where to go next

* The *Track options reference* vignette — every track type and its options
* `inst/demos/` — one runnable app per feature, each under sixty lines
* `inst/showcase/igvShinyDemo.R` — most of the API in a single app, and what
  the public Posit Connect demo serves
* [igv.js documentation](https://github.com/igvteam/igv.js/wiki) — the
  underlying browser, useful when you need a track option this package passes
  through but does not document itself

# Session Info

```{r session-info}
sessionInfo()
```
