Extension points

What are extension points?

Extension points let plugin developers register callbacks that are invoked at specific places in the application.

Using extension points

The basic API is that producers can say:

const ret = pluginManager.evaluateExtensionPoint('ExtensionPointName', {
  value: 1,
})

And consumers can say:

pluginManager.addToExtensionPoint(
  'ExtensionPointName',
  (arg: { value: number }) => {
    return { value: arg.value + 1 }
  },
)

pluginManager.addToExtensionPoint(
  'ExtensionPointName',
  (arg: { value: number }) => {
    return { value: arg.value + 1 }
  },
)

Each registered callback receives the return value of the previous one as its argument (chained). In the example above, ret would be {value:3}.

API description of extension points

Evaluation API

// extra props are optional, can pass an extra context object your extension
// point receives
pluginManager.evaluateExtensionPoint(extensionPointName, args, props)

args are accumulated (each callback's return value becomes the next callback's args), while props is passed through unchanged.

There is also an async version:

// extra props are optional, can pass an extra context object your extension
// point receives
pluginManager.evaluateAsyncExtensionPoint(extensionPointName, args, props)

Registration API

pluginManager.addToExtensionPoint(extensionPointName, args => {
  /* do something */
  return newArgs // returned value is passed as args to the next registered callback
})

addToExtensionPoint creates the extension point if it doesn't exist yet. The returned value becomes the args for the next callback in the chain.

Current listing of extension points used in codebase

Here are the extension points in the core codebase:

Core-extendPluggableElement

type: synchronous

  • args - PluggableElement - the pluggable element being installed
  • props - none

Used to add extra functionality to e.g. state tree models, for example extra right-click context menus. Your callback receives every pluggable element registered to the system.

https://github.com/GMOD/jbrowse-components/blob/6ceeac51f8bcecfc3b0a99e23f2277a6e5a7662e/plugins/dotplot-view/src/extensionPoints.ts#L9-L43

Core-guessAdapterForLocation

type: synchronous

  • args - adapter config

used to infer an adapter type given a location type from the "Add track" workflow. you will receive a callback asking if you can provide an adapter config given a location object

https://github.com/GMOD/jbrowse-components/blob/6ceeac51f8bcecfc3b0a99e23f2277a6e5a7662e/plugins/gff3/src/index.ts#L27-L53

Core-guessTrackTypeForLocation

type: synchronous

  • args - FileLocation object

used to infer a track type given a location type from the "Add track workflow"

example https://github.com/GMOD/jbrowse-components/blob/6ceeac51f8bcecfc3b0a99e23f2277a6e5a7662e/plugins/alignments/src/index.ts#L108-L118

Core-extendSession

type: synchronous

used to extend the session model itself with new features

  • args - AbstractSessionModel - instance of the session model

Core-replaceAbout

type: synchronous

adds option to provide a different component for the "About this track" dialog

  • args - a ReactComponent, by default the AboutTrack dialog
  • props - an argument of the format below
interface props {
  session: AbstractSessionModel
  config: AnyConfigurationModel
}

Example: returns a new about track dialog for a particular track

pluginManager.addToExtensionPoint(
  'Core-replaceAbout',
  (DefaultAboutComponent, { session, config }) => {
    return config.trackId === 'volvox.inv.vcf'
      ? NewAboutComponent
      : DefaultAboutComponent
  },
)

Core-extraAboutPanel

type: synchronous

adds an extra panel to the "About this track" dialog

interface props {
  session: AbstractSessionModel
  config: AnyConfigurationModel
}

Return value: An object with the name of the panel and the React component to use for the panel

Example: adds an extra about dialog panel for a particular track ID

pluginManager.addToExtensionPoint(
  'Core-extraAboutPanel',
  (DefaultAboutExtra, { session, config }) => {
    return config.trackId === 'volvox_sv_test'
      ? { name: 'More info', Component: ExtraAboutPanel }
      : DefaultAboutExtra
  },
)

Core-customizeAbout

type: synchronous

  • args - a config snapshot Record<string, unknown> for the track, with formatAbout already applied to it

Return value: New config snapshot object

Core-replaceWidget

type: synchronous

adds option to provide a different component for a given widget, drawer or modal

  • args - a ReactComponent
  • props - an object of the type below
interface props {
  session: AbstractSessionModel
  model: WidgetModel
}

See also: Core-extraFeaturePanel

Return value: The new React component you want to use

Note: Core-replaceWidget is called any time any widget opens, so if you are trying to only customize e.g. the feature details widget, you can filter on widget.trackId because only feature detail widgets has a 'trackId' field. You can filter on widget.type also but this is stringly typed, and may vary depending on track type.

Example of Core-replaceWidget - add widget above the default widget

pluginManager.addToExtensionPoint(
  'Core-replaceWidget',
  (DefaultWidget, { model }) => {
    // replace widget for this particular track ID
    return model.trackId !== 'volvox.inv.vcf'
      ? DefaultWidget
      : function NewWidget(props) {
          // this new widget adds a custom panel above the old DefaultWidget,
          // but you can replace it with any contents that you want
          return (
            <div>
              <div>Custom content here above the default details widget</div>
              <DefaultWidget {...props} />
            </div>
          )
        }
  },
)

Note 1: it is not always possible to retrieve the configuration associated with a track that produced the feature details. Therefore, we check model.trackId that produced the popup instead.

Note 2: If you want e.g. a "User copy" of your track to get same treatment, might use a regex to loose match the trackId (the copy of a track will have a timestamp and -sessionTrack added to it).

Core-extraFeaturePanel

type: synchronous

  • args - a ReactComponent, the default AboutTrack dialog
  • props - an object of the type below
interface props {
  model: BaseFeatureWidget // a widget model, has model.trackId defined if you want to check track
  feature: Record<string, unknown> // snapshot of feature object
  session: AbstractSessionModel
}

Note: the model has properties model.trackId, model.trackType, and model.track, though model.track may be undefined if the user closed the track, while trackId and trackType will be defined even if user closed the track

Return value: An object with the name of the panel and the React component to use for the panel

Example:

pluginManager.addToExtensionPoint(
  'Core-extraFeaturePanel',
  (DefaultFeatureExtra, { model }) => {
    return model.trackId === 'volvox_filtered_vcf'
      ? { name: 'Extra info', Component: ExtraFeaturePanel }
      : DefaultFeatureExtra
  },
)

Core-preProcessTrackConfig

type: synchronous

  • args - SnapshotIn<AnyConfigurationModel> - Copy of the current track config

Return value: A new track config

Example:

pluginManager.addToExtensionPoint('Core-preProcessTrackConfig', snap => {
  return {
    ...snap.metadata,
    extraMetadata: 'extra metadata',
  }
})

TrackSelector-multiTrackMenuItems

type: synchronous

  • args - MenuItem[] - an array of items that you can accumulate on
  • props - an object of the form below
interface props {
  session: AbstractSessionModel
}

used to add new menu items to the "shopping cart" in the header of the hierarchical track menu when tracks are added to the selection

example https://github.com/GMOD/jbrowse-components/blob/6ceeac51f8bcecfc3b0a99e23f2277a6e5a7662e/plugins/wiggle/src/CreateMultiWiggleExtension/index.ts#L10-L67

TrackSelector-folderDialog

type: synchronous

Replaces the dialog that opens when a user clicks a folder category (supertrack) in the hierarchical track selector. The default dialog shows a faceted track selector scoped to the tracks in that category. Use this extension point to provide a custom UI for a specific category.

  • args - a React component — the default DefaultFolderDialog
  • props - an object of the type below
interface props {
  categoryId: string // internal ID of the folder category, e.g. "Tracks-Wiggle,My Subcategory"
  model: HierarchicalTrackSelectorModel
  subtracks: TreeNode[] // flat list of all track nodes under this category (recursive)
}

Return value: A React component that will be rendered as the dialog. The component receives the following props:

interface DialogProps {
  model: HierarchicalTrackSelectorModel
  title: string // the display name of the category
  subtracks: TreeNode[] // same flat list of track nodes passed in props above
  handleClose: () => void
}

The categoryId format is Tracks-{categoryPath}, where categoryPath is the comma-joined path of category names matching the track's category config field. For example, a track with "category": ["Wiggle", "My Subcategory"] produces categoryId = "Tracks-Wiggle,My Subcategory".

Example: custom dialog for a specific folder category

pluginManager.addToExtensionPoint(
  'TrackSelector-folderDialog',
  (DefaultComponent, { categoryId, model, subtracks }) => {
    if (categoryId !== 'Tracks-Wiggle,My Subcategory') {
      return DefaultComponent
    }

    const React = pluginManager.jbrequire('react')
    const { observer } = pluginManager.jbrequire('mobx-react')
    const { Dialog, DialogTitle, DialogContent, DialogActions, Button } =
      pluginManager.jbrequire('@mui/material')

    return observer(function MyFolderDialog({
      model,
      title,
      subtracks,
      handleClose,
    }) {
      const { shownTrackIds, view } = model
      const tracks = subtracks.filter(s => s.type === 'track')

      return React.createElement(
        Dialog,
        { open: true, onClose: handleClose, maxWidth: 'sm', fullWidth: true },
        React.createElement(DialogTitle, null, title),
        React.createElement(
          DialogContent,
          null,
          ...tracks.map(track =>
            React.createElement(
              'div',
              {
                key: track.trackId,
                onClick: () => view.toggleTrack(track.trackId),
                style: {
                  padding: 12,
                  marginBottom: 8,
                  border: shownTrackIds.has(track.trackId)
                    ? '2px solid #1976d2'
                    : '2px solid #ddd',
                  cursor: 'pointer',
                },
              },
              track.name,
            ),
          ),
        ),
        React.createElement(
          DialogActions,
          null,
          React.createElement(Button, { onClick: handleClose }, 'Close'),
        ),
      )
    })
  },
)

A more complete example using this extension point is in test_data/volvox/umd_plugin.js (search for TrackSelector-folderDialog).

LaunchView-LinearGenomeView

type: async

Launches a linear genome view. Rarely extended directly, but useful as a reference for implementing a LaunchView for your own view type.

  • args - an object with the following format
interface args {
  session: AbstractSessionModel // the session model
  assembly: string // assembly name
  loc: string // locstring
  tracks: string[] // array of track IDs
}

https://github.com/GMOD/jbrowse-components/blob/6ceeac51f8bcecfc3b0a99e23f2277a6e5a7662e/plugins/linear-genome-view/src/index.ts#L131-L189

LaunchView-CircularView

type: async

Launches a circular view.

  • args - an object with the following format
interface args {
  session: AbstractSessionModel // the session model
  assembly: string // assembly name
  tracks: string[] // array of track IDs
}

https://github.com/GMOD/jbrowse-components/blob/6ceeac51f8bcecfc3b0a99e23f2277a6e5a7662e/plugins/circular-view/src/index.ts#L30-L66

LaunchView-SvInspectorView

type: async

Launches an SV inspector.

  • args - an object with the following format
interface args {
  session: AbstractSessionModel // the session model
  assembly: string // assembly name
  uri: string // uri for file to load into the SV inspector
  fileType?: string // type of file referred to by the uri ("VCF"|"CSV"|"BEDPE",etc) if uri extension does not properly hint at the file type
}

https://github.com/GMOD/jbrowse-components/blob/6ceeac51f8bcecfc3b0a99e23f2277a6e5a7662e/plugins/sv-inspector/src/index.ts#L21-L61

LaunchView-SpreadsheetView

type: async

Launches a spreadsheet view.

  • args - an object with the following format
interface args {
  session: AbstractSessionModel // the session model
  assembly: string // assembly name
  uri: string // uri for file to load into the SV inspector
  fileType?: string // type of file referred to by the uri ("VCF"|"CSV"|"BEDPE",etc) if uri extension does not properly hint at the file type
}

https://github.com/GMOD/jbrowse-components/blob/6ceeac51f8bcecfc3b0a99e23f2277a6e5a7662e/plugins/spreadsheet-view/src/index.ts#L26-L59

LaunchView-DotplotView

type: async

Launches a dotplot view.

interface args {
  session: AbstractSessionModel // the session model
  views: {
    loc: string
    assembly: string
    tracks?: string[]
  }[] // array of length 2, for vert and horiz
  tracks: string[] // synteny track IDs to load on open
}

https://github.com/GMOD/jbrowse-components/blob/6ceeac51f8bcecfc3b0a99e23f2277a6e5a7662e/plugins/dotplot-view/src/LaunchDotplotView.ts#L7-L46

LaunchView-LinearSyntenyView

type: async

Launches a linear synteny view.

interface args {
  session: AbstractSessionModel // the session model
  views: {
    loc: string // locstring
    assembly: string // assembly name
    tracks?: string[] // trackIDs to open on top and bottom
  }[] // array of length 2, for top and bottom rows of synteny view
  tracks: string[] // synteny track IDs to load on open
}

https://github.com/GMOD/jbrowse-components/blob/6ceeac51f8bcecfc3b0a99e23f2277a6e5a7662e/plugins/linear-comparative-view/src/LaunchLinearSyntenyView.ts#L9-L68

LinearGenomeView-TracksContainer

type: synchronous

  • args - React.ReactNode[] - an array of rendered react components (ReactNode) which you can append to
  • props - an object of the type below
interface props {
  model: LinearGenomeViewModel // instance of the linear genome view model
}

Allows rendering a custom component as a child of the LinearGenomeView's "TracksContainer". Used to render highlights for example with a div of height 100% over the TracksContainer

LinearGenomeView-searchResultSelected

type: async

  • args - undefined
  • props - an object of the type below
interface props {
  session: AbstractSessionModel
  result: BaseResult // the search result that was selected
  model: LinearGenomeViewModel // the linear genome view model
  assemblyName: string // the assembly name
}

Called when a search result is selected in the LinearGenomeView search box. This fires after navigation has occurred (if the result has a location). Useful for plugins that want to take additional action when a search result is selected, such as selecting a corresponding feature in a track.

Example: selecting a feature in a custom track when a search result is chosen

import type BaseResult from '@jbrowse/core/TextSearch/BaseResults'

pluginManager.addToExtensionPoint(
  'LinearGenomeView-searchResultSelected',
  (_, { session, result, model, assemblyName }) => {
    const trackId = result.getTrackId()
    if (trackId === 'my_custom_track') {
      // perform custom action, e.g. select the feature in the track
    }
    return _
  },
)

DotplotView-ImportFormSyntenyOptions

type: synchronous

  • args - DotplotImportFormSyntenyOption[] - an array of custom radio options to add to the dotplot import form's synteny track selector
  • props - an object of the type below
interface props {
  model: DotplotViewModel // instance of the dotplot view model
  assembly1: string // name of the y-axis assembly
  assembly2: string // name of the x-axis assembly
}

Allows plugins to add custom radio options to the DotplotView import form. When a user selects a custom radio option, the plugin's React component is rendered.

Each option in the array should have the following structure:

interface DotplotImportFormSyntenyOption {
  value: string // unique identifier for the radio option
  label: string // display text for the radio option
  ReactComponent: React.FC<{
    model: DotplotViewModel
    assembly1: string
    assembly2: string
  }>
}

Example: adding a custom synteny option that fetches data from a server

import type { DotplotImportFormSyntenyOption } from '@jbrowse/plugin-dotplot-view'

pluginManager.addToExtensionPoint(
  'DotplotView-ImportFormSyntenyOptions',
  (
    options: DotplotImportFormSyntenyOption[],
    { model, assembly1, assembly2 },
  ) => {
    return [
      ...options,
      {
        value: 'my-server-synteny',
        label: 'Load from my server',
        ReactComponent: MySyntenyServerComponent,
      },
    ]
  },
)

LinearSyntenyView-ImportFormSyntenyOptions

type: synchronous

  • args - LinearSyntenyImportFormSyntenyOption[] - an array of custom radio options to add to the linear synteny view import form's synteny track selector
  • props - an object of the type below
interface props {
  model: LinearSyntenyViewModel // instance of the linear synteny view model
  assembly1: string // name of the top assembly
  assembly2: string // name of the bottom assembly
  selectedRow: number // which row is currently selected (0-indexed)
}

Allows plugins to add custom radio options to the LinearSyntenyView import form. When a user selects a custom radio option, the plugin's React component is rendered. This is similar to DotplotView-ImportFormSyntenyOptions but includes an additional selectedRow prop since the linear synteny view can have multiple rows.

Each option in the array should have the following structure:

interface LinearSyntenyImportFormSyntenyOption {
  value: string // unique identifier for the radio option
  label: string // display text for the radio option
  ReactComponent: React.FC<{
    model: LinearSyntenyViewModel
    assembly1: string
    assembly2: string
    selectedRow: number
  }>
}

Example: adding a custom synteny option

import type { LinearSyntenyImportFormSyntenyOption } from '@jbrowse/plugin-linear-comparative-view'

pluginManager.addToExtensionPoint(
  'LinearSyntenyView-ImportFormSyntenyOptions',
  (
    options: LinearSyntenyImportFormSyntenyOption[],
    { model, assembly1, assembly2, selectedRow },
  ) => {
    return [
      ...options,
      {
        value: 'my-server-synteny',
        label: 'Load from my server',
        ReactComponent: MySyntenyServerComponent,
      },
    ]
  },
)

Extension point footnote

Users that want to add further extension points can do so, by simply calling

const returnVal = pluginManager.evaluateExtensionPoint(
  'YourCustomNameHere',
  processThisValue,
  extraContext,
)

Then, any code that had used:

pluginManager.addToExtensionPoint(
  'YourCustomNameHere',
  (processThisValue, extraContext) => {
    /* the first arg is the "processThisValue" from the extension point, it may
    get mutated if multiple extension points are chained together

    the second argument to the extension point is the extra context from
    evaluating the extension point. it does not get mutated even if there is a
    chain of values, it is passed as is to each one*/
    return processThisValue
  },
)

The naming system, "Core-" just refers to the fact that these extension points are from our core codebase. Plugin developers may choose their own prefix to avoid collisions.