Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new Hi-C color schemes with log scale mode #4336

Merged
merged 5 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/PluginLoader.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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),
)
}
}
Loading
Loading