Skip to main content

Writing a plugin using jbrowse-plugin-template

JBrowse 2 plugins can be used to add new pluggable elements (views, tracks, adapters, etc.), and to modify behavior of the application by adding code that watches the application's state.

For the full list of what kinds of pluggable element types plugins can add, see the pluggable elements page.

The following tutorial will walk you through establishing your developer environment, spinning up a plugin, and running a local JBrowse instance with your custom plugin functionality.

Prerequisites

  • git
  • A stable and recent version of node
  • yarn or npm
  • basic familiarity with the command line, React, package management, and npm

First we're going to install and set up the project for development.

Use git to clone the plugin template

The easiest way to start developing your plugin for JBrowse 2 is to use the plugin template. There is also a lightweight alternative based on esbuild: jbrowse-plugin-esbuild-template.

To clone the plugin template project, on the command line run:

# change jbrowse-plugin-my-project to whatever you wish
git clone https://github.com/GMOD/jbrowse-plugin-template.git jbrowse-plugin-my-project
cd jbrowse-plugin-my-project

Initialize the project

To initialize your project run,

yarn init

You'll be asked a few questions relating to your new project.

Most fields can be left blank, but make sure to enter a descriptive name for your plugin in the first field.

:::note Tip

A typical naming convention for JBrowse plugins is "jbrowse-plugin-", or, if you are going to publish to an NPM organization, we advise "@myscope/jbrowse-plugin-".

:::

You also need to install the dependencies:

yarn # or npm i

Setup JBrowse 2

Finally, we're going to run:

yarn setup

which will grab the latest release version of JBrowse 2 (in the .jbrowse directory) and make it easy for you to run within your plugin project.

To run JBrowse:

yarn browse

You should see something like the following:

yarn run v1.22.10
$ npm-run-all jbrowse:*
$ shx cp jbrowse_config.json .jbrowse/config.json
$ cross-var serve --listen $npm_package_config_browse_port .jbrowse

We still need to run the plugin though; we need both to be running to test our plugin.

Open a new tab in your terminal and navigate again to your plugin project, then we're going to run our plugin:

cd jbrowse-plugin-my-project
yarn start

Now you can navigate to http://localhost:8999/, and see your running JBrowse instance!

Your browser should look something like the above screenshot.
Your browser should look something like the above screenshot.
info

At this point, you must be running your plugin on port 9000 to see a running JBrowse instance, otherwise you will meet a screen asking you to configure your instance.

If you'd like to change this port, you can edit the "port" fields under "config" in the package.json file.

We can verify our plugin has been added to our JBrowse session by clicking the first square on the splash screen "Empty," and then navigating Add -> Hello View in the menu bar. This is the example pluggable element that is added in the template plugin project.

For this tutorial, we're going to be creating a custom widget, and using a Jexl callback to open it when we click a chord on the circular genome view.

A screenshot of the finished product of this tutorial: a widget with a jexl callback on the circular view.
A screenshot of the finished product of this tutorial: a widget with a jexl callback on the circular view.

Add new files, stubs, and install dependencies

Add a new directory under src called CircularViewChordWidget with two files CircularViewChordWidget.tsx, and index.tsx.

This component is essentially just a React component we're going to embed in a JBrowse widget.

A widget's index.tsx

The index file is going to export what our pluginManager needs to recognize the widget: a ReactComponent, a configSchema, and a stateModelFactory.

CircularViewChordWidget/index.tsx

import { ConfigurationSchema } from '@jbrowse/core/configuration'
import PluginManager from '@jbrowse/core/PluginManager'
import { ElementId } from '@jbrowse/core/util/types/mst'
import { types } from '@jbrowse/mobx-state-tree'

export { default as ReactComponent } from './CircularViewChordWidget'
export const configSchema = ConfigurationSchema('CircularViewChordWidget', {})

export function stateModelFactory(pluginManager: PluginManager) {
const stateModel = types
.model('CircularViewChordWidget', {
id: ElementId,
type: types.literal('CircularViewChordWidget'),
featureData: types.frozen({}),
})
.actions(self => ({
setFeatureData(data: any) {
self.featureData = data
},
clearFeatureData() {
self.featureData = {}
},
}))

return stateModel
}

With @jbrowse/mobx-state-tree, we're defining the properties of our widget and the actions (mutations) it can take. You can add any model properties you need — they're accessible in your React component via model.

If you have a particularly complex model, consider moving it into a separate model.ts and exporting it from index.ts, similar to how the ReactComponent is exported.

A widget's ReactComponent

Now that we have our model set up, let's build a simple widget that will open when we click the circular genome view chord.

CircularViewChordWidget.tsx

import { observer } from 'mobx-react'
import {
FeatureDetails,
BaseCard,
} from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail'

const CircularViewChordWidget = observer(({ model }: { model: any }) => {
const { featureData } = model
return (
<div>
<BaseCard title={featureData.name}>
<FeatureDetails feature={featureData} model={model} />
</BaseCard>
</div>
)
})

export default CircularViewChordWidget

The observer wrapper from mobx-react ensures the component re-renders when model properties change. @jbrowse/core exports reusable components like FeatureDetails and BaseCard — if you find something in the app you'd like to reuse, check whether it's exported, and if not make a request.

Now that we have our component built, we can install it into our plugin and test it out.

Register pluggable elements with JBrowse

The file src/index.ts exports your plugin and installs all the necessary components to JBrowse at runtime such that it runs properly.

Your src/index.ts file is going to look something like the following right now:

import Plugin from '@jbrowse/core/Plugin'
import PluginManager from '@jbrowse/core/PluginManager'
import { ViewType } from '@jbrowse/core/pluggableElementTypes'
import { SessionWithWidgets, isAbstractMenuManager } from '@jbrowse/core/util'
import { version } from '../package.json'
import {
ReactComponent as HelloViewReactComponent,
stateModel as helloViewStateModel,
} from './HelloView'

export default class SomeNewPluginPlugin extends Plugin {
name = 'SomeNewPluginPlugin'
version = version

install(pluginManager: PluginManager) {
pluginManager.addViewType(() => {
return new ViewType({
name: 'HelloView',
stateModel: helloViewStateModel,
ReactComponent: HelloViewReactComponent,
})
})
}

configure(pluginManager: PluginManager) {
if (isAbstractMenuManager(pluginManager.rootModel)) {
pluginManager.rootModel.appendToMenu('Add', {
label: 'Hello View',
onClick: (session: SessionWithWidgets) => {
session.addView('HelloView', {})
},
})
}
}
}

You'll notice we're already adding a new view type and configuring the rootModel in the template's project. We can use these patterns to add our widget.

src/index.ts

// imports
// ...
import { ViewType, WidgetType } from '@jbrowse/core/pluggableElementTypes'
// notice we're importing the components we exported from src/CircularViewChordWidget/index.ts
import {
configSchema as circularViewChordWidgetConfigSchema,
stateModelFactory as circularViewChordWidgetStateModelFactory,
ReactComponent as CircularViewChordWidgetComponent
} from './CircularViewChordWidget'
// ...
install(pluginManager: PluginManager) {
// ...
pluginManager.addWidgetType(() => {
return new WidgetType({
name: 'CircularViewChordWidget',
heading: 'Chord Details',
configSchema: circularViewChordWidgetConfigSchema,
stateModel: circularViewChordWidgetStateModelFactory(pluginManager),
ReactComponent: CircularViewChordWidgetComponent,
})
})
}
// ...

This is also where we'll add our Jexl callback function:

src/index.ts

// ...
import { getSession } from '@jbrowse/core/util'
// ...
// Jexl callback functions are adding inside configure in the plugin class
configure(pluginManager: PluginManager) {
// ...
/* .jexl.addFunction is the method to add a function
the first parameter is the name of your jexl function, and how you'll
call it
the second parameter is the supplementary properties the function
needs, here, we need these three properties for
the circular view's chord click function */
pluginManager.jexl.addFunction(
'openWidgetOnChordClick',
(feature: any, chordTrack: any) => {
// the session contains a ton of necessary information about the
// present state of the app, here we use it to call the
// showWidget function to show our widget upon chord click
const session = getSession(chordTrack)

if (session) {
// @ts-expect-error
session.showWidget(
// @ts-expect-error
session.addWidget(
'CircularViewChordWidget',
'circularViewChordWidget',
{ featureData: feature.toJSON() },
),
)
session.setSelection(feature)
}
},
)
}
// ...

Now that we've configured the jexl function to our JBrowse session, we can use it essentially anywhere.

While we could programmatically tell certain displays to use this jexl function when they perform an action, for our use case (clicking a chord on the circular view), we can simply write it into our config file.

Setup the configuration for proper testing

To open a view in JBrowse, we need an assembly configured, append the following to your jbrowse_config.json file (i.e. after the "plugins" field):

{
"assemblies": [
{
"name": "hg38",
"aliases": ["GRCh38"],
"sequence": {
"type": "ReferenceSequenceTrack",
"trackId": "P6R5xbRqRr",
"adapter": {
"type": "BgzipFastaAdapter",
"fastaLocation": {
"uri": "https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz",
"locationType": "UriLocation"
},
"faiLocation": {
"uri": "https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz.fai",
"locationType": "UriLocation"
},
"gziLocation": {
"uri": "https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz.gzi",
"locationType": "UriLocation"
}
}
},
"refNameAliases": {
"adapter": {
"type": "RefNameAliasAdapter",
"location": {
"uri": "https://jbrowse.org/genomes/GRCh38/hg38_aliases.txt",
"locationType": "UriLocation"
}
}
}
}
]
}

Take some time to dissect what's being added here:

  • we're adding the assembly GRCh38
  • it can be referenced either by its name (hg38) or its aliases (GRCh38)
  • it has a sequence, which has a BgzipFastaAdapter from which the reference sequence is derived
  • these FASTA's are hosted on jbrowse.org, referenced as a UriLocation
  • there is also a refNameAliases text file being used to derive the reference names of the assembly

We're now going to add a track that will make use of our jexl function. As mentioned previously, you could add your jexl function programmatically to all tracks of this type, but for now we're just adding it to our assembly for this specific track.

"tracks": [
{
"type": "VariantTrack",
"trackId": "demo_vcf",
"name": "demo_vcf",
"assemblyNames": ["hg38"],
"category": ["Annotation"],
"adapter": {
"type": "VcfAdapter",
"vcfLocation": {
"locationType": "UriLocation",
"uri": "https://s3.amazonaws.com/jbrowse.org/genomes/hg19/skbr3/reads_lr_skbr3.fa_ngmlr-0.2.3_mapped.bam.sniffles1kb_auto_l8_s5_noalt.new.vcf"
}
},
"displays": [
{
"type": "ChordVariantDisplay",
"displayId": "demo_ch_v_disp",
"onChordClick": "jexl:openWidgetOnChordClick(feature, track, pluginManager)",
"renderer": { "type": "StructuralVariantChordRenderer" }
}
]
}
]

Take some time to dissect what's being added here:

  • this is a track that will appear in our track list when we run JBrowse against this assembly
  • it's a VariantTrack called "demo_vcf"
  • it derives its data from a given UriLocation, the file is a .vcf file using the VcfAdapter
  • it declares its display, the ChordVariantDisplay, and specifies its onChordClick callback function
  • the specified onChordClick callback function is that which we defined in our plugin class, the jexl function
A screenshot of what it will look like when you add a track to your configuration; that is, it will be available in the add track menu when you open a view.
A screenshot of what it will look like when you add a track to your configuration; that is, it will be available in the add track menu when you open a view.

Testing it out

Run JBrowse with your new plugin and manually test

Everything is in place to test this widget we've added to the plugin project out.

If you shut your instance down, restart JBrowse and your plugin (yarn browse and yarn start).

Navigate to localhost:8999/?config=localhost:9000/jbrowse_config.json or equivalent to see JBrowse running with your config.

Now navigate:

  1. Click Start a new sessionEmpty
  2. In the top menu bar, click AddCircular view
  3. When the view is open, click Open beside the assembly
  4. Click the far right three rows of rectangles icon in the top left of the circular view, Open track selector
  5. Select the track we populated from our config
  6. Click any chord in the circular view

Expected result:

The widget opens on the right-hand side with two panels, one with our editable widget byline, and one with our feature data.

A screenshot of the widget displayed after clicking on the chord.
A screenshot of the widget displayed after clicking on the chord.

:::info Troubleshooting

If you get to this point and note that nothing happens, open the developer tools in your browser and investigate the console errors. Also check your running process in your terminal for any errors. Review the code you added to ensure you didn't miss any imports or statements. Check over your config file to ensure that "plugins", "assemblies", and "tracks" are all present for the configuration to work properly.

:::

Next steps

We have a complete and tested plugin, so now we're ready to publish it to NPM and request that it be added to the plugin store.

Sometimes you might write a plugin that is specific to you or your organization's needs, but you also might want to share it with the greater community. That's where the plugin store shows off its strengths.

As a plugin developer, you can publish your plugin to NPM, and then request that your plugin be added to the plugin store. After your plugin is successfully whitelisted, you will see it within the JBrowse app's plugin store widget and you and others can freely install the plugin into their JBrowse session. Any further publications you make to the plugin via NPM will automatically be updated for the plugin available through the plugin store.

The following document will describe how to accomplish this.

Publish your plugin to NPM

The following will guide you through publishing with NPM. You'll need an NPM account and token to do this, so please set that up first through the NPM site.

If you'd prefer not to publish to NPM, you can host your plugin files elsewhere, just ensure the link is accessible publicly.

When your plugin is in a publishable state, and you have NPM credentials, you can run the following within your plugin's root directory (where package.json is found):

yarn publish

Set the version to whatever you'd like, enter your credentials, and then complete the publication process. Once you can see your package on NPM, move on to the next step.

Request your plugin be added to the plugin store

To populate your plugin to the plugin store, it must be added to the plugin list, a whitelist of JBrowse plugins.

Navigate to the plugin list repository and use the GitHub UI to Fork the repository.

Click the 'Fork' option at the top of the repository to create an editable clone of the repo.
Click the 'Fork' option at the top of the repository to create an editable clone of the repo.

:::info Tip

It's easy enough to edit the files required using the GitHub UI, but feel free to clone and push to the forked repo using your local environment as well.

:::

Optional: create an image for your plugin

An image helps communicate the capabilities of your plugin to adopters at a glance. Consider creating an 800 x 200 .png screenshot of a core feature of your plugin to show off.

We recommend using pngquant to compress your image to keep the repo manageable.

Once your image is all set, you can upload it to your forked repo (ideally in ~/jbrowse-plugin-list/img/) using the Github UI or pushing the file from your computer.

Adding the details for your plugin to the list

Once forked, you can edit the plugins.json file to include the following information regarding your new plugin:

plugins.json

{
"plugins": [
// ...other plugins already published,
{
// this plugin name needs to match what is in your package.json
"name": "SomeNewPlugin",
"authors": ["You, dear reader!"],
"description": "JBrowse 2 plugin that demonstrates adding a simple pluggable element",
// change this to your github repo for your plugin
"location": "https://github.com/ghost/jbrowse-plugin-some-new-plugin",
// assuming you published to NPM, this url is going to be mostly the same, other than the correct name of your project
"url": "https://unpkg.com/jbrowse-plugin-some-new-plugin/dist/jbrowse-plugin-some-new-plugin.umd.production.min.js",
// make sure the license is accurate, otherwise use "NONE"
"license": "MIT",
// the image url will be wherever you placed it in the repo earlier, img is appropriate
"image": "https://raw.githubusercontent.com/GMOD/jbrowse-plugin-list/main/img/plugin-screenshot-fs8.png"
}
]
}

Push your changes to the main branch of your forked repo when you're done.

Make a pull request

Now that your plugin's information is accurate, navigate again to the plugin list repository, and create a new pull request.

In the pull request UI, click "compare across forks" and select your fork as the head repository to merge into the main of jbrowse-plugin-list. Your changes should show in the editor, and you can create your PR.

Use the compare across forks option in the pull request UI to merge your forked repo's main branch into the jbrowse-plugin-list main branch.
Use the compare across forks option in the pull request UI to merge your forked repo's main branch into the jbrowse-plugin-list main branch.

Next steps

The JBrowse development team will review your plugin to ensure that it is functional, then when it is merged in the plugin will be available on the plugin store.

In this tutorial, we set up a development environment for JBrowse 2 and added a custom pluggable element to a plugin.

We also published the plugin to NPM and requested that it be added to the JBrowse plugin store, so others can access our plugin.

To learn more about the various pluggable elements available in JBrowse (and thus more that you can do with plugins!) checkout our developer guide documentation.

If you have further questions about plugin development, or development with JBrowse in general, stop by the JBrowse team gitter channel, or start a discussion on the jbrowse-components discussions forum.