diff --git a/packages/core/PluginLoader.ts b/packages/core/PluginLoader.ts index 7a9eb75b0a..183f79d309 100644 --- a/packages/core/PluginLoader.ts +++ b/packages/core/PluginLoader.ts @@ -257,6 +257,7 @@ export default class PluginLoader { return [moduleName, module] }), ) + return this } async load(baseUri?: string) { diff --git a/packages/core/util/io/RemoteFileWithRangeCache.ts b/packages/core/util/io/RemoteFileWithRangeCache.ts index eab4eb02eb..003f788472 100644 --- a/packages/core/util/io/RemoteFileWithRangeCache.ts +++ b/packages/core/util/io/RemoteFileWithRangeCache.ts @@ -48,35 +48,26 @@ export class RemoteFileWithRangeCache extends RemoteFile { url: RequestInfo, init?: RequestInit, ): Promise { - 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) @@ -89,17 +80,16 @@ export class RemoteFileWithRangeCache extends RemoteFile { options: { headers?: HeadersInit; signal?: AbortSignal } = {}, ): Promise { 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 : ''}`) } diff --git a/packages/product-core/src/rpcWorker.ts b/packages/product-core/src/rpcWorker.ts index 9dd5684e0b..bff52da481 100644 --- a/packages/product-core/src/rpcWorker.ts +++ b/packages/product-core/src/rpcWorker.ts @@ -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 { diff --git a/plugins/hic/package.json b/plugins/hic/package.json index 1b5e3bc4e6..cc0c8d6902 100644 --- a/plugins/hic/package.json +++ b/plugins/hic/package.json @@ -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": { diff --git a/plugins/hic/src/HicAdapter/HicAdapter.ts b/plugins/hic/src/HicAdapter/HicAdapter.ts index ab7d4a4ac1..6eff982bd2 100644 --- a/plugins/hic/src/HicAdapter/HicAdapter.ts +++ b/plugins/hic/src/HicAdapter/HicAdapter.ts @@ -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 diff --git a/plugins/hic/src/HicRenderer/HicRenderer.tsx b/plugins/hic/src/HicRenderer/HicRenderer.tsx index 21bf64e8ae..9c64f55b74 100644 --- a/plugins/hic/src/HicRenderer/HicRenderer.tsx +++ b/plugins/hic/src/HicRenderer/HicRenderer.tsx @@ -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 @@ -62,6 +66,8 @@ export default class HicRenderer extends ServerSideRendererType { resolution, sessionId, adapterConfig, + useLogScale, + colorScheme, regions, } = props const [region] = regions @@ -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 @@ -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) @@ -101,6 +132,8 @@ 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) { @@ -108,6 +141,7 @@ export default class HicRenderer extends ServerSideRendererType { start = +Date.now() } } + ctx.restore() } } @@ -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 diff --git a/plugins/hic/src/HicRenderer/configSchema.ts b/plugins/hic/src/HicRenderer/configSchema.ts index d4b4fc1d68..5594fd2b94 100644 --- a/plugins/hic/src/HicRenderer/configSchema.ts +++ b/plugins/hic/src/HicRenderer/configSchema.ts @@ -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'], }, /** diff --git a/plugins/hic/src/LinearHicDisplay/model.ts b/plugins/hic/src/LinearHicDisplay/model.ts index e90c2d1c8e..3460009215 100644 --- a/plugins/hic/src/LinearHicDisplay/model.ts +++ b/plugins/hic/src/LinearHicDisplay/model.ts @@ -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 => { @@ -60,6 +68,8 @@ export default (configSchema: AnyConfigurationSchemaType) => rpcDriverName: self.rpcDriverName, displayModel: self, resolution: self.resolution, + useLogScale: self.useLogScale, + colorScheme: self.colorScheme, } }, } @@ -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 @@ -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), }, ], }, diff --git a/plugins/hic/src/declare.d.ts b/plugins/hic/src/declare.d.ts index 1d029eaf8e..35e8cd4a7f 100644 --- a/plugins/hic/src/declare.d.ts +++ b/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' diff --git a/plugins/hic/src/index.ts b/plugins/hic/src/index.ts index ee9841af09..c39118f8d5 100644 --- a/plugins/hic/src/index.ts +++ b/plugins/hic/src/index.ts @@ -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' @@ -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) }, ) } @@ -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), + ) } } diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts b/plugins/linear-genome-view/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts index 88362f3992..65cee6eee4 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts @@ -149,7 +149,6 @@ const blockState = types renderInProgress = undefined }, setError(error: unknown) { - console.error(error) if (renderInProgress && !renderInProgress.signal.aborted) { renderInProgress.abort() } diff --git a/yarn.lock b/yarn.lock index 7ecaef100d..9e047e6e75 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7974,13 +7974,21 @@ d3-color@^3.0.2: resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== -"d3-interpolate@1.2.0 - 2": +"d3-interpolate@1 - 2", "d3-interpolate@1.2.0 - 2", d3-interpolate@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== dependencies: d3-color "1 - 2" +d3-scale-chromatic@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz#c13f3af86685ff91323dc2f0ebd2dabbd72d8bab" + integrity sha512-LLqy7dJSL8yDy7NRmf6xSlsFZ6zYvJ4BcWFE4zBrOPnQERv9zj24ohnXKRbyi9YHnYV+HN1oEO3iFK971/gkzA== + dependencies: + d3-color "1 - 2" + d3-interpolate "1 - 2" + d3-scale@^3.0.2: version "3.3.0" resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.3.0.tgz#28c600b29f47e5b9cd2df9749c206727966203f3"