Skip to content

Commit

Permalink
Add new Hi-C color schemes with log scale mode (#4336)
Browse files Browse the repository at this point in the history
  • Loading branch information
cmdcolin committed Apr 12, 2024
1 parent d1c5d9e commit ffdb3dd
Show file tree
Hide file tree
Showing 12 changed files with 157 additions and 66 deletions.
1 change: 1 addition & 0 deletions packages/core/PluginLoader.ts
Expand Up @@ -257,6 +257,7 @@ export default class PluginLoader {
return [moduleName, module]
}),
)
return this
}

async load(baseUri?: string) {
Expand Down
46 changes: 18 additions & 28 deletions packages/core/util/io/RemoteFileWithRangeCache.ts
Expand Up @@ -48,35 +48,26 @@ export class RemoteFileWithRangeCache extends RemoteFile {
url: RequestInfo,
init?: RequestInit,
): Promise<PolyfilledResponse> {
if (!fetchers[String(url)]) {
fetchers[String(url)] = this.fetchBinaryRange.bind(this)
const str = String(url)
if (!fetchers[str]) {
fetchers[str] = this.fetchBinaryRange.bind(this)
}
// if it is a range request, route it through the range cache
const requestHeaders = init?.headers
let range
if (requestHeaders) {
if (requestHeaders instanceof Headers) {
range = requestHeaders.get('range')
} else if (Array.isArray(requestHeaders)) {
;[, range] = requestHeaders.find(([key]) => key === 'range') || [
undefined,
undefined,
]
} else {
range = requestHeaders.range
}
}
const range = new Headers(init?.headers)?.get('range')
if (range) {
const rangeParse = /bytes=(\d+)-(\d+)/.exec(range)
if (rangeParse) {
const [, start, end] = rangeParse
const s = Number.parseInt(start, 10)
const e = Number.parseInt(end, 10)
const response = (await globalRangeCache.getRange(url, s, e - s + 1, {
signal: init?.signal,
})) as BinaryRangeResponse
const { headers } = response
return new Response(response.buffer, { status: 206, headers })
const len = e - s
const { buffer, headers } = (await globalRangeCache.getRange(
url,
s,
len + 1,
{ signal: init?.signal },
)) as BinaryRangeResponse
return new Response(buffer, { status: 206, headers })
}
}
return super.fetch(url, init)
Expand All @@ -89,17 +80,16 @@ export class RemoteFileWithRangeCache extends RemoteFile {
options: { headers?: HeadersInit; signal?: AbortSignal } = {},
): Promise<BinaryRangeResponse> {
const requestDate = new Date()
const requestHeaders = {
...options.headers,
range: `bytes=${start}-${end}`,
}
const res = await super.fetch(url, {
...options,
headers: requestHeaders,
headers: {
...options.headers,
range: `bytes=${start}-${end}`,
},
})
const responseDate = new Date()
if (res.status !== 206) {
const errorMessage = `HTTP ${res.status} (${res.statusText}) when fetching ${url} bytes ${start}-${end}`
if (!res.ok) {
const errorMessage = `HTTP ${res.status} fetching ${url} bytes ${start}-${end}`
const hint = ' (should be 206 for range requests)'
throw new Error(`${errorMessage}${res.status === 200 ? hint : ''}`)
}
Expand Down
21 changes: 12 additions & 9 deletions packages/product-core/src/rpcWorker.ts
Expand Up @@ -35,15 +35,18 @@ async function getPluginManager(
) {
// Load runtime plugins
const config = await receiveConfiguration()
const pluginLoader = new PluginLoader(config.plugins, opts)
pluginLoader.installGlobalReExports(self)
const runtimePlugins = await pluginLoader.load(config.windowHref)
const plugins = [...corePlugins.map(p => ({ plugin: p })), ...runtimePlugins]
const pluginManager = new PluginManager(plugins.map(P => new P.plugin()))
pluginManager.createPluggableElements()
pluginManager.configure()

return pluginManager
const pluginLoader = new PluginLoader(
config.plugins,
opts,
).installGlobalReExports(self)
return new PluginManager(
[
...corePlugins.map(p => ({ plugin: p })),
...(await pluginLoader.load(config.windowHref)),
].map(P => new P.plugin()),
)
.createPluggableElements()
.configure()
}

interface WrappedFuncArgs {
Expand Down
2 changes: 2 additions & 0 deletions plugins/hic/package.json
Expand Up @@ -36,6 +36,8 @@
"clean": "rimraf dist esm *.tsbuildinfo"
},
"dependencies": {
"d3-interpolate": "^2.0.1",
"d3-scale-chromatic": "^2.0.0",
"hic-straw": "^2.0.3"
},
"peerDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions plugins/hic/src/HicAdapter/HicAdapter.ts
Expand Up @@ -135,9 +135,9 @@ export default class HicAdapter extends BaseFeatureDataAdapter {
'BP',
res,
)
records.forEach(record => {
for (const record of records) {
observer.next(record)
})
}
statusCallback('')
observer.complete()
}, opts.signal) as any // eslint-disable-line @typescript-eslint/no-explicit-any
Expand Down
45 changes: 40 additions & 5 deletions plugins/hic/src/HicRenderer/HicRenderer.tsx
Expand Up @@ -13,6 +13,10 @@ import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter'
import { getAdapter } from '@jbrowse/core/data_adapters/dataAdapterCache'
import { AnyConfigurationModel } from '@jbrowse/core/configuration'
import { colord } from '@jbrowse/core/util/colord'
import { firstValueFrom } from 'rxjs'
import { scaleSequential, scaleSequentialLog } from 'd3-scale'
import { interpolateViridis } from 'd3-scale-chromatic'
import { interpolateRgbBasis } from 'd3-interpolate'

interface HicFeature {
bin1: number
Expand Down Expand Up @@ -62,6 +66,8 @@ export default class HicRenderer extends ServerSideRendererType {
resolution,
sessionId,
adapterConfig,
useLogScale,
colorScheme,
regions,
} = props
const [region] = regions
Expand All @@ -77,8 +83,8 @@ export default class HicRenderer extends ServerSideRendererType {
const width = (region.end - region.start) / bpPerPx
const w = res / (bpPerPx * Math.sqrt(2))
const baseColor = colord(readConfObject(config, 'baseColor'))
const offset = Math.floor(region.start / res)
if (features.length) {
const offset = features[0].bin1
let maxScore = 0
let minBin = 0
let maxBin = 0
Expand All @@ -89,6 +95,31 @@ export default class HicRenderer extends ServerSideRendererType {
maxBin = Math.max(Math.max(bin1, bin2), maxBin)
}
await abortBreakPoint(signal)
const colorSchemes = {
juicebox: ['rgba(0,0,0,0)', 'red'],
fall: interpolateRgbBasis([
'rgb(255, 255, 255)',
'rgb(255, 255, 204)',
'rgb(255, 237, 160)',
'rgb(254, 217, 118)',
'rgb(254, 178, 76)',
'rgb(253, 141, 60)',
'rgb(252, 78, 42)',
'rgb(227, 26, 28)',
'rgb(189, 0, 38)',
'rgb(128, 0, 38)',
'rgb(0, 0, 0)',
]),
viridis: interpolateViridis,
}
const m = useLogScale ? maxScore : maxScore / 20

// @ts-expect-error
const x1 = colorSchemes[colorScheme] || colorSchemes.juicebox
const scale = useLogScale
? scaleSequentialLog(x1).domain([1, m])
: scaleSequential(x1).domain([0, m])
ctx.save()

if (region.reversed === true) {
ctx.scale(-1, 1)
Expand All @@ -101,13 +132,16 @@ export default class HicRenderer extends ServerSideRendererType {
count: counts,
maxScore,
baseColor,
scale,
useLogScale,
})
ctx.fillRect((bin1 - offset) * w, (bin2 - offset) * w, w, w)
if (+Date.now() - start > 400) {
await abortBreakPoint(signal)
start = +Date.now()
}
}
ctx.restore()
}
}

Expand Down Expand Up @@ -148,10 +182,11 @@ export default class HicRenderer extends ServerSideRendererType {
sessionId,
adapterConfig,
)
const features = await (dataAdapter as BaseFeatureDataAdapter)
.getFeatures(regions[0], args)
.pipe(toArray())
.toPromise()
const features = await firstValueFrom(
(dataAdapter as BaseFeatureDataAdapter)
.getFeatures(regions[0], args)
.pipe(toArray()),
)
// cast to any to avoid return-type conflict, because the
// types of features returned by our getFeatures are quite
// different from the base interface
Expand Down
4 changes: 2 additions & 2 deletions plugins/hic/src/HicRenderer/configSchema.ts
Expand Up @@ -23,8 +23,8 @@ const HicRenderer = ConfigurationSchema(
color: {
type: 'color',
description: 'the color of each feature in a hic alignment',
defaultValue: `jexl:colorString(hsl(alpha(baseColor,min(1,count/(maxScore/20)))))`,
contextVariable: ['count', 'maxScore', 'baseColor'],
defaultValue: `jexl:interpolate(count,scale)`,
contextVariable: ['count', 'maxScore', 'baseColor', 'scale'],
},

/**
Expand Down
58 changes: 52 additions & 6 deletions plugins/hic/src/LinearHicDisplay/model.ts
Expand Up @@ -28,6 +28,14 @@ export default (configSchema: AnyConfigurationSchemaType) =>
* #property
*/
resolution: types.optional(types.number, 1),
/**
* #property
*/
useLogScale: false,
/**
* #property
*/
colorScheme: types.maybe(types.string),
}),
)
.views(self => {
Expand Down Expand Up @@ -60,6 +68,8 @@ export default (configSchema: AnyConfigurationSchemaType) =>
rpcDriverName: self.rpcDriverName,
displayModel: self,
resolution: self.resolution,
useLogScale: self.useLogScale,
colorScheme: self.colorScheme,
}
},
}
Expand All @@ -71,6 +81,18 @@ export default (configSchema: AnyConfigurationSchemaType) =>
setResolution(n: number) {
self.resolution = n
},
/**
* #action
*/
setUseLogScale(f: boolean) {
self.useLogScale = f
},
/**
* #action
*/
setColorScheme(f?: string) {
self.colorScheme = f
},
}))
.views(self => {
const { trackMenuItems: superTrackMenuItems } = self
Expand All @@ -81,20 +103,44 @@ export default (configSchema: AnyConfigurationSchemaType) =>
trackMenuItems() {
return [
...superTrackMenuItems(),
{
label: 'Use log scale',
type: 'checkbox',
checked: self.useLogScale,
onClick: () => self.setUseLogScale(!self.useLogScale),
},
{
label: 'Color scheme',
type: 'subMenu',
subMenu: [
{
label: 'Fall',
onClick: () => self.setColorScheme('fall'),
},
{
label: 'Viridis',
onClick: () => self.setColorScheme('viridis'),
},
{
label: 'Juicebox',
onClick: () => self.setColorScheme('juicebox'),
},
{
label: 'Clear',
onClick: () => self.setColorScheme(undefined),
},
],
},
{
label: 'Resolution',
subMenu: [
{
label: 'Finer resolution',
onClick: () => {
self.setResolution(self.resolution * 2)
},
onClick: () => self.setResolution(self.resolution * 2),
},
{
label: 'Coarser resolution',
onClick: () => {
self.setResolution(self.resolution / 2)
},
onClick: () => self.setResolution(self.resolution / 2),
},
],
},
Expand Down
2 changes: 2 additions & 0 deletions plugins/hic/src/declare.d.ts
@@ -1,2 +1,4 @@
declare module '@jbrowse/core/util/offscreenCanvasPonyfill'
declare module 'hic-straw'
declare module 'd3-interpolate'
declare module 'd3-scale-chromatic'
29 changes: 17 additions & 12 deletions plugins/hic/src/index.ts
Expand Up @@ -2,16 +2,18 @@ import Plugin from '@jbrowse/core/Plugin'
import PluginManager from '@jbrowse/core/PluginManager'
import { FileLocation } from '@jbrowse/core/util/types'
import { colord, Colord } from '@jbrowse/core/util/colord'
import HicRendererF from './HicRenderer'
import HicTrackF from './HicTrack'
import LinearHicDisplayF from './LinearHicDisplay'
import HicAdapterF from './HicAdapter'
import {
AdapterGuesser,
getFileName,
AdapterGuesser,
TrackTypeGuesser,
} from '@jbrowse/core/util/tracks'

// locals
import HicRendererF from './HicRenderer'
import HicTrackF from './HicTrack'
import LinearHicDisplayF from './LinearHicDisplay'
import HicAdapterF from './HicAdapter'

export default class HicPlugin extends Plugin {
name = 'HicPlugin'

Expand Down Expand Up @@ -41,20 +43,19 @@ export default class HicPlugin extends Plugin {
return obj
} else if (adapterHint === adapterName) {
return obj
} else {
return adapterGuesser(file, index, adapterHint)
}
return adapterGuesser(file, index, adapterHint)
}
},
)
pluginManager.addToExtensionPoint(
'Core-guessTrackTypeForLocation',
(trackTypeGuesser: TrackTypeGuesser) => {
return (adapterName: string) => {
if (adapterName === 'HicAdapter') {
return 'HicTrack'
}
return trackTypeGuesser(adapterName)
}
return (adapterName: string) =>
adapterName === 'HicAdapter'
? 'HicTrack'
: trackTypeGuesser(adapterName)
},
)
}
Expand All @@ -64,5 +65,9 @@ export default class HicPlugin extends Plugin {
jexl.addFunction('alpha', (color: Colord, n: number) => color.alpha(n))
jexl.addFunction('hsl', (color: Colord) => colord(color.toHsl()))
jexl.addFunction('colorString', (color: Colord) => color.toHex())
jexl.addFunction(
'interpolate',
(count: number, scale: (n: number) => string) => scale(count),
)
}
}

0 comments on commit ffdb3dd

Please sign in to comment.