Differential transcript usage
The tutorials target the JBrowse v5 beta, and the v4.3.0 release
on the download page lacks some of what they show. To install the
beta of JBrowse Web, run npm install -g @jbrowse/cli@next, then
jbrowse create jbrowse2 --branch v5.0.0-beta.13. Desktop beta builds are coming soon.
Differential transcript usage tests whether a gene changes which of its isoforms it expresses between two conditions, here skeletal muscle and liver. We run the test with satuRn, a Bioconductor package, on ENCODE quantifications, write each transcript's statistic into the GFF3 attribute column, and color the gene track by it.
Prerequisites
- a JBrowse to open the finished GFF3 and coverage tracks in: Desktop takes a local file by path, Web through Add track
curlpython3- htslib (
bgzip,tabix) - R with satuRn, SummarizedExperiment, edgeR and limma, all Bioconductor
Where the data comes from
ENCODE's ENTEx panel, four skeletal-muscle and four liver donors, quantified with RSEM against GENCODE v29.
The build script fetches these files, so there is nothing to download by hand.
- eight RSEM per-transcript quantification tables, the four muscle donors then the four liver donors: www.encodeproject.org/…/ENCFF353NZM.tsvhttps://www.encodeproject.org/files/ENCFF353NZM/@@download/ENCFF353NZM.tsv, www.encodeproject.org/…/ENCFF172SLW.tsvhttps://www.encodeproject.org/files/ENCFF172SLW/@@download/ENCFF172SLW.tsv, www.encodeproject.org/…/ENCFF140GJI.tsvhttps://www.encodeproject.org/files/ENCFF140GJI/@@download/ENCFF140GJI.tsv, www.encodeproject.org/…/ENCFF576DOG.tsvhttps://www.encodeproject.org/files/ENCFF576DOG/@@download/ENCFF576DOG.tsv, www.encodeproject.org/…/ENCFF996LRE.tsvhttps://www.encodeproject.org/files/ENCFF996LRE/@@download/ENCFF996LRE.tsv, www.encodeproject.org/…/ENCFF641ADT.tsvhttps://www.encodeproject.org/files/ENCFF641ADT/@@download/ENCFF641ADT.tsv, www.encodeproject.org/…/ENCFF392VYD.tsvhttps://www.encodeproject.org/files/ENCFF392VYD/@@download/ENCFF392VYD.tsv, www.encodeproject.org/…/ENCFF383KWZ.tsvhttps://www.encodeproject.org/files/ENCFF383KWZ/@@download/ENCFF383KWZ.tsv
- the GENCODE v29 annotation those quantifications were made against: ftp.ebi.ac.uk/…/gencode.v29.annotation.gff3.gzhttps://ftp.ebi.ac.uk/pub/databases/gencode/Gencode_human/release_29/gencode.v29.annotation.gff3.gz
Read by URL (no download needed)
- the four coverage bigWigs the demo's track config loads, one donor per tissue, plus and minus strand: www.encodeproject.org/…/ENCFF007ZBY.bigWighttps://www.encodeproject.org/files/ENCFF007ZBY/@@download/ENCFF007ZBY.bigWig, www.encodeproject.org/…/ENCFF518WGP.bigWighttps://www.encodeproject.org/files/ENCFF518WGP/@@download/ENCFF518WGP.bigWig, www.encodeproject.org/…/ENCFF565QRM.bigWighttps://www.encodeproject.org/files/ENCFF565QRM/@@download/ENCFF565QRM.bigWig, www.encodeproject.org/…/ENCFF253OSP.bigWighttps://www.encodeproject.org/files/ENCFF253OSP/@@download/ENCFF253OSP.bigWig
Building the GFF3
The build takes the ENCODE quantifications to a colorable GFF3 in three steps, and one script runs all three.
Build the matrices. The script downloads the eight RSEM tables listed above, with the accessions written into it, and writes a count matrix, which feeds the model, and a TPM matrix, which feeds the effect size.
Test usage. satuRn fits a
quasi-binomial model to each transcript's share of its gene's reads and tests
that share between the two tissues. It needs a transcript-by-sample count matrix
cnt, a coldata table with one tissue row per sample, and a txinfo table
with an isoform_id and a gene_id for each transcript:
# after filterByExpr, any gene left with one isoform goes too: usage is a
# within-gene proportion, so a lone isoform is always 100%
keep <- edgeR::filterByExpr(cnt, group = coldata$tissue)
cnt <- cnt[keep, ]
multi <- names(which(table(txinfo$gene_id) > 1))
cnt <- cnt[txinfo$isoform_id[txinfo$gene_id %in% multi], ]
# rowData has to carry isoform_id and gene_id: satuRn reads each transcript's
# gene from there, since usage is a share of the gene's reads
se <- SummarizedExperiment(
assays = list(counts = cnt),
colData = coldata,
rowData = txinfo
)
# the formula names the colData column holding the groups. 0 + tissue drops the
# intercept, so each tissue gets its own coefficient and the contrast below is
# the difference between two of them
se <- satuRn::fitDTU(object = se, formula = ~ 0 + tissue, parallel = FALSE)
design <- model.matrix(~ 0 + tissue, data = coldata)
colnames(design) <- levels(factor(coldata$tissue))
L <- limma::makeContrasts(muscle_vs_liver = muscle - liver, levels = design)
# sort = FALSE leaves the result rows in the order the assay had them, which is
# what lets the isoform fractions be indexed straight into the result
se <- satuRn::testDTU(object = se, contrasts = L, sort = FALSE)
res <- rowData(se)[["fitDTUResult_muscle_vs_liver"]]res holds, per transcript, the p-value, the regular and empirical FDRs and the
model estimates. The script computes each transcript's isoform fraction, its
share of the gene's expression, from the TPM matrix.1
Write the statistics into GENCODE. The annotation has to be the release the
quantifications were made against. RSEM names each transcript with its version
(ENST00000356708.11) and GENCODE raises that version whenever it revises the
transcript, so joining against a later release drops every transcript revised
since, with no error. ENCODE lists the release on each quantification's file
page as its genome annotation (V29 for all eight here).
The script keeps each gene with a called transcript that is meaningfully expressed, since a fraction can swing widely on a handful of reads. It subsets those genes out of the GENCODE v29 GFF3 and appends each transcript's numbers to its attribute column.
To do the same join over your own results, write results.tsv with one row per
transcript and the columns isoform_id, regular_FDR and dIF (the
isoform-fraction change). This version keeps every GENCODE row, appending dif,
fdr, dtu and dif_called to each transcript row and dtu_transcripts and
dtu_top_dif to the gene row of each gene with a called transcript. The track
reads those keys:
import csv
import gzip
stats = {r['isoform_id']: r
for r in csv.DictReader(open('results.tsv'), delimiter='\t')}
genes = {}
called = {}
records = []
with gzip.open('gencode.v29.annotation.gff3.gz', 'rt') as fh:
for line in fh:
if line.startswith('#'):
continue
cols = line.rstrip('\n').split('\t')
d = dict(kv.split('=', 1) for kv in cols[8].split(';') if '=' in kv)
if cols[2] == 'gene':
genes[d['ID']] = cols
elif cols[2] == 'transcript':
r = stats.get(d['transcript_id'])
if r:
dif, fdr = float(r['dIF']), float(r['regular_FDR'])
direction = 'ns'
if fdr < 0.05 and abs(dif) > 0.1:
direction = 'muscle' if dif > 0 else 'liver'
cols[8] += f';dif={dif:.3f};fdr={fdr:.3g};dtu={direction}'
if direction != 'ns':
cols[8] += f';dif_called={dif:.3f}'
called.setdefault(d['Parent'], []).append(dif)
records.append((cols[0], int(cols[3]), int(cols[4]), cols))
for gene_id, difs in called.items():
top = max(difs, key=abs)
genes[gene_id][8] += f';dtu_transcripts={len(difs)};dtu_top_dif={top:.3f}'
records.sort(key=lambda r: r[:3])
with open('dtu_muscle_vs_liver.gff3', 'w') as out:
out.write('##gff-version 3\n')
for _, _, _, cols in records:
out.write('\t'.join(cols) + '\n')The rows come out in coordinate order, so indexing is the ordinary pair:
bgzip -f dtu_muscle_vs_liver.gff3
tabix -f -p gff dtu_muscle_vs_liver.gff3.gzsatuRn's statistics in the GFF3 attribute column
The track reads each transcript's statistics from the attribute column. A transcript row from the finished file, wrapped:
chr10 HAVANA transcript 7788129 7807815 . + .
ID=ENST00000356708.11;Parent=ENSG00000165629.19;gene_name=ATP5F1C;
transcript_name=ATP5F1C-202;...;
dif=-0.299;fdr=0.0022;if_muscle=0.075;if_liver=0.375;
tpm_muscle=10.03;tpm_liver=28.88;dtu=liver;dif_called=-0.299The transcript statistics sit on the transcript row, and the exons, CDS and UTRs
take the transcript value. The gene row has dtu_transcripts, the count of
called isoforms, and dtu_top_dif, the largest ΔIF among them, which the
track's mouseover reads. The keys are lowercase because the GFF parser
lowercases them, so a color field named dIF reads nothing and paints every
transcript grey.
dtu is muscle, liver or ns, as the script above calls it. dif_called
is dif on the called transcripts and absent on the rest, so a transcript the
test could not separate has no value to color and stays grey.
Loading GRCh38
GENCODE v29 is a GRCh38 annotation and the coverage is on GRCh38, so we load the hg38 assembly before either track.
Goes in the assemblies array of config.json. See Assemblies.
{
"name": "hg38",
"uri": "https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz",
"refNameAliases": {
"uri": "https://s3.amazonaws.com/jbrowse.org/genomes/GRCh38/hg38_aliases.txt"
},
"cytobands": "https://jbrowse.org/genomes/GRCh38/cytoBand.txt"
}jbrowse add-assembly https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz \
--name hg38 \
--refNameAliases https://s3.amazonaws.com/jbrowse.org/genomes/GRCh38/hg38_aliases.txt \
--config '{"cytobands":"https://jbrowse.org/genomes/GRCh38/cytoBand.txt"}'hg38 is one of the genomes JBrowse Desktop hosts, with gene tracks already set up: on the start screen click Show all available genomes and pick it. To load the files of this config instead:
In JBrowse Desktop, Open new genome on the start screen (or File → Open genome... in a session), then Open from a URL and paste, one per line:
https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz
https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz.fai
https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz.gziJBrowse reads the format off the file name. Then fill in:
- Genome name:
hg38 - refName aliases (under More options):
https://s3.amazonaws.com/jbrowse.org/genomes/GRCh38/hg38_aliases.txt - cytobands (under More options):
https://jbrowse.org/genomes/GRCh38/cytoBand.txt
Coloring each isoform by its usage change
In grammar-of-graphics terms, color is a color scale: its field,
dif_called (a called transcript's isoform-fraction change), goes through the
threshold cuts in domain to the colors in range, liver-preferred below
zero and muscle-preferred above. A value on a cut takes the interval above it.
- The key lists every interval under the
title, plus a(no value)row for uncalled transcripts. - A UTR follows
colorunlessutrColoris set. - Clicking an isoform opens its numbers in the details panel.
Goes in the tracks array of config.json. See Tracks.
{
"trackId": "dtu_muscle_vs_liver",
"name": "Transcript usage: skeletal muscle vs liver (satuRn)",
"uri": "https://jbrowse.org/demos/dtu/dtu_muscle_vs_liver.gff3.gz",
"assemblyNames": ["hg38"],
"displayDefaults": {
"subfeatureLabels": "below",
"color": {
"field": "dif_called",
"scale": "threshold",
"domain": ["-0.6", "-0.3", "0", "0.3", "0.6"],
"range": [
"#124f95",
"#2370cc",
"#6394d5",
"#d5716a",
"#c63335",
"#901e21"
],
"labels": [
"liver, below -0.6",
"liver, -0.6 to -0.3",
"liver, -0.3 to 0",
"muscle, 0 to 0.3",
"muscle, 0.3 to 0.6",
"muscle, 0.6 and above"
],
"title": "ΔIF, muscle - liver"
},
"labels": {
"name": "jexl:feature.transcript_name||feature.gene_name||feature.name||feature.id"
},
"mouseover": "jexl:feature.gene_name+': '+feature.dtu_transcripts+' isoform(s) with a usage shift, largest ΔIF '+feature.dtu_top_dif"
}
}jbrowse add-track https://jbrowse.org/demos/dtu/dtu_muscle_vs_liver.gff3.gz \
--trackId dtu_muscle_vs_liver \
--name "Transcript usage: skeletal muscle vs liver (satuRn)" \
--assemblyNames hg38 \
--displayDefaults "{\"subfeatureLabels\":\"below\",\"color\":{\"field\":\"dif_called\",\"scale\":\"threshold\",\"domain\":[\"-0.6\",\"-0.3\",\"0\",\"0.3\",\"0.6\"],\"range\":[\"#124f95\",\"#2370cc\",\"#6394d5\",\"#d5716a\",\"#c63335\",\"#901e21\"],\"labels\":[\"liver, below -0.6\",\"liver, -0.6 to -0.3\",\"liver, -0.3 to 0\",\"muscle, 0 to 0.3\",\"muscle, 0.3 to 0.6\",\"muscle, 0.6 and above\"],\"title\":\"ΔIF, muscle - liver\"},\"labels\":{\"name\":\"jexl:feature.transcript_name||feature.gene_name||feature.name||feature.id\"},\"mouseover\":\"jexl:feature.gene_name+': '+feature.dtu_transcripts+' isoform(s) with a usage shift, largest ΔIF '+feature.dtu_top_dif\"}"In JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"trackId": "dtu_muscle_vs_liver",
"name": "Transcript usage: skeletal muscle vs liver (satuRn)",
"uri": "https://jbrowse.org/demos/dtu/dtu_muscle_vs_liver.gff3.gz",
"assemblyNames": ["hg38"],
"displayDefaults": {
"subfeatureLabels": "below",
"color": {
"field": "dif_called",
"scale": "threshold",
"domain": ["-0.6", "-0.3", "0", "0.3", "0.6"],
"range": [
"#124f95",
"#2370cc",
"#6394d5",
"#d5716a",
"#c63335",
"#901e21"
],
"labels": [
"liver, below -0.6",
"liver, -0.6 to -0.3",
"liver, -0.3 to 0",
"muscle, 0 to 0.3",
"muscle, 0.3 to 0.6",
"muscle, 0.6 and above"
],
"title": "ΔIF, muscle - liver"
},
"labels": {
"name": "jexl:feature.transcript_name||feature.gene_name||feature.name||feature.id"
},
"mouseover": "jexl:feature.gene_name+': '+feature.dtu_transcripts+' isoform(s) with a usage shift, largest ΔIF '+feature.dtu_top_dif"
}
}Open this track in JBrowse Web ↗
Open this track in JBrowse Desktop ↗ (JBrowse Desktop 5.0+)
Add the four coverage tracks below and open them with the transcript track at
chr10:7,787,600-7,812,400, the whole of ATP5F1C, an ATP synthase subunit.
The gene is on the plus strand, so the two plus-strand tracks have its reads.
satuRn used no genomic coordinates, so the coverage is an independent check on
the color.
Each coverage track scales to its peak until the tracks share an axis.
Y axis... → Share axis with on one track, with the other ticked, gives both one axis that follows the view, so the two tissues compare by height. In a config, each track names the same group:
Goes in the tracks array of config.json. See Tracks.
{
"trackId": "liver_plus",
"name": "Liver RNA-seq, + strand (ENCSR135IAL)",
"uri": "https://jbrowse.org/demos/dtu/ENCFF565QRM.liver.plus.bigWig",
"assemblyNames": ["hg38"],
"displayDefaults": {
"scales": { "y": { "autoscaleGroup": "coverage" } }
}
}jbrowse add-track https://jbrowse.org/demos/dtu/ENCFF565QRM.liver.plus.bigWig \
--trackId liver_plus \
--name "Liver RNA-seq, + strand (ENCSR135IAL)" \
--assemblyNames hg38 \
--displayDefaults '{"scales":{"y":{"autoscaleGroup":"coverage"}}}'In JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"trackId": "liver_plus",
"name": "Liver RNA-seq, + strand (ENCSR135IAL)",
"uri": "https://jbrowse.org/demos/dtu/ENCFF565QRM.liver.plus.bigWig",
"assemblyNames": ["hg38"],
"displayDefaults": {
"scales": { "y": { "autoscaleGroup": "coverage" } }
}
}Open this track in JBrowse Web ↗
Open this track in JBrowse Desktop ↗ (JBrowse Desktop 5.0+)
The other three tracks differ in their id, name and file:
Goes in the tracks array of config.json. See Tracks.
{
"trackId": "liver_minus",
"name": "Liver RNA-seq, - strand (ENCSR135IAL)",
"uri": "https://jbrowse.org/demos/dtu/ENCFF253OSP.liver.minus.bigWig",
"assemblyNames": ["hg38"],
"displayDefaults": {
"scales": { "y": { "autoscaleGroup": "coverage" } }
}
}jbrowse add-track https://jbrowse.org/demos/dtu/ENCFF253OSP.liver.minus.bigWig \
--trackId liver_minus \
--name "Liver RNA-seq, - strand (ENCSR135IAL)" \
--assemblyNames hg38 \
--displayDefaults '{"scales":{"y":{"autoscaleGroup":"coverage"}}}'In JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"trackId": "liver_minus",
"name": "Liver RNA-seq, - strand (ENCSR135IAL)",
"uri": "https://jbrowse.org/demos/dtu/ENCFF253OSP.liver.minus.bigWig",
"assemblyNames": ["hg38"],
"displayDefaults": {
"scales": { "y": { "autoscaleGroup": "coverage" } }
}
}Open this track in JBrowse Web ↗
Open this track in JBrowse Desktop ↗ (JBrowse Desktop 5.0+)
Goes in the tracks array of config.json. See Tracks.
{
"trackId": "muscle_plus",
"name": "Skeletal muscle RNA-seq, + strand (ENCSR609NZM)",
"uri": "https://jbrowse.org/demos/dtu/ENCFF007ZBY.muscle.plus.bigWig",
"assemblyNames": ["hg38"],
"displayDefaults": {
"scales": { "y": { "autoscaleGroup": "coverage" } }
}
}jbrowse add-track https://jbrowse.org/demos/dtu/ENCFF007ZBY.muscle.plus.bigWig \
--trackId muscle_plus \
--name "Skeletal muscle RNA-seq, + strand (ENCSR609NZM)" \
--assemblyNames hg38 \
--displayDefaults '{"scales":{"y":{"autoscaleGroup":"coverage"}}}'In JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"trackId": "muscle_plus",
"name": "Skeletal muscle RNA-seq, + strand (ENCSR609NZM)",
"uri": "https://jbrowse.org/demos/dtu/ENCFF007ZBY.muscle.plus.bigWig",
"assemblyNames": ["hg38"],
"displayDefaults": {
"scales": { "y": { "autoscaleGroup": "coverage" } }
}
}Open this track in JBrowse Web ↗
Open this track in JBrowse Desktop ↗ (JBrowse Desktop 5.0+)
Goes in the tracks array of config.json. See Tracks.
{
"trackId": "muscle_minus",
"name": "Skeletal muscle RNA-seq, - strand (ENCSR609NZM)",
"uri": "https://jbrowse.org/demos/dtu/ENCFF518WGP.muscle.minus.bigWig",
"assemblyNames": ["hg38"],
"displayDefaults": {
"scales": { "y": { "autoscaleGroup": "coverage" } }
}
}jbrowse add-track https://jbrowse.org/demos/dtu/ENCFF518WGP.muscle.minus.bigWig \
--trackId muscle_minus \
--name "Skeletal muscle RNA-seq, - strand (ENCSR609NZM)" \
--assemblyNames hg38 \
--displayDefaults '{"scales":{"y":{"autoscaleGroup":"coverage"}}}'In JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"trackId": "muscle_minus",
"name": "Skeletal muscle RNA-seq, - strand (ENCSR609NZM)",
"uri": "https://jbrowse.org/demos/dtu/ENCFF518WGP.muscle.minus.bigWig",
"assemblyNames": ["hg38"],
"displayDefaults": {
"scales": { "y": { "autoscaleGroup": "coverage" } }
}
}Open this track in JBrowse Web ↗
Open this track in JBrowse Desktop ↗ (JBrowse Desktop 5.0+)
Reproduce it end to end
One script wraps every step above,
build_dtu_demo.sh:
curl -fO https://raw.githubusercontent.com/GMOD/jbrowse-components/main/scripts/build_dtu_demo.sh
bash build_dtu_demo.sh dtu_build # writes ./dtu_build/The script needs the Prerequisites on your PATH and writes
dtu_muscle_vs_liver.gff3.gz with its .tbi index, a local build of the file
the track configuration above loads from jbrowse.org. Point the track's uri at
the local copy to open your own run.
See also
- Tutorial: RNA-seq visualization
- User guide: Gene track
- Config guide: Using jexl callbacks
Citations
- Gilis J, Vitting-Seerup K, Van den Berge K, Clement L. satuRn: Scalable analysis of differential transcript usage for bulk and single-cell RNA-sequencing applications. F1000Research 10:374 (2021), the method behind the statistic drawn here.
- Li B, Dewey CN. RSEM: accurate transcript quantification from RNA-Seq data with or without a reference genome. BMC Bioinformatics 12:323 (2011), the quantifier ENCODE ran.
Notes
-
The script takes the isoform fraction from TPM, because read counts scale with effective length and bias a count-based fraction toward long isoforms. It gates on satuRn's regular FDR, because the empirical FDR assumes most tests are null and this contrast breaks that assumption. ↩
Feedback on this tutorial is welcome: contact us.