diff --git a/packages/core/TextSearch/TextSearchManager.ts b/packages/core/TextSearch/TextSearchManager.ts index e657eb920d..8c77521381 100644 --- a/packages/core/TextSearch/TextSearchManager.ts +++ b/packages/core/TextSearch/TextSearchManager.ts @@ -1,17 +1,12 @@ import BaseResult from './BaseResults' import PluginManager from '../PluginManager' import QuickLRU from '../util/QuickLRU' -import { SearchType, BaseTextSearchAdapter } from '../data_adapters/BaseAdapter' +import { + BaseTextSearchAdapter, + BaseTextSearchArgs, +} from '../data_adapters/BaseAdapter' import { readConfObject, AnyConfigurationModel } from '../configuration' -export interface BaseArgs { - queryString: string - searchType?: SearchType - signal?: AbortSignal - limit?: number - pageNumber?: number -} - export interface SearchScope { includeAggregateIndexes: boolean assemblyName: string @@ -91,7 +86,7 @@ export default class TextSearchManager { * limit of results to return, searchType...prefix | full | exact", etc. */ async search( - args: BaseArgs, + args: BaseTextSearchArgs, searchScope: SearchScope, rankFn: (results: BaseResult[]) => BaseResult[], ) { diff --git a/packages/core/assemblyManager/assembly.ts b/packages/core/assemblyManager/assembly.ts index dd85201d89..e716d56065 100644 --- a/packages/core/assemblyManager/assembly.ts +++ b/packages/core/assemblyManager/assembly.ts @@ -5,6 +5,7 @@ import AbortablePromiseCache from 'abortable-promise-cache' // locals import { getConf, AnyConfigurationModel } from '../configuration' import { + BaseOptions, BaseRefNameAliasAdapter, RegionsAdapter, } from '../data_adapters/BaseAdapter' @@ -111,12 +112,6 @@ function getAdapterId(adapterConf: unknown) { type RefNameAliases = Record -export interface BaseOptions { - signal?: AbortSignal - sessionId: string - statusCallback?: Function -} - interface CacheData { adapterConf: unknown self: Assembly @@ -148,10 +143,12 @@ export default function assemblyFactory( ) { const adapterLoads = new AbortablePromiseCache({ cache: new QuickLRU({ maxSize: 1000 }), + + // @ts-expect-error async fill( args: CacheData, signal?: AbortSignal, - statusCallback?: Function, + statusCallback?: (arg: string) => void, ) { const { adapterConf, self, options } = args return loadRefNameMap( diff --git a/packages/core/data_adapters/BaseAdapter.ts b/packages/core/data_adapters/BaseAdapter.ts deleted file mode 100644 index bee5f1848b..0000000000 --- a/packages/core/data_adapters/BaseAdapter.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { Observable, firstValueFrom, merge } from 'rxjs' -import { toArray } from 'rxjs/operators' -import { isStateTreeNode, getSnapshot } from 'mobx-state-tree' - -// locals -import { ObservableCreate } from '../util/rxjs' -import { checkAbortSignal, sum, max, min } from '../util' -import { Feature } from '../util/simpleFeature' -import { - readConfObject, - AnyConfigurationModel, - ConfigurationSchema, -} from '../configuration' -import { getSubAdapterType } from './dataAdapterCache' -import { AugmentedRegion as Region, NoAssemblyRegion } from '../util/types' -import { blankStats, rectifyStats, scoresToStats } from '../util/stats' -import BaseResult from '../TextSearch/BaseResults' -import idMaker from '../util/idMaker' -import PluginManager from '../PluginManager' - -export interface BaseOptions { - signal?: AbortSignal - bpPerPx?: number - sessionId?: string - statusCallback?: (message: string) => void - headers?: Record - [key: string]: unknown -} - -export type SearchType = 'full' | 'prefix' | 'exact' - -export interface BaseArgs { - searchType?: SearchType - queryString: string - signal?: AbortSignal - limit?: number - pageNumber?: number -} -// see -// https://www.typescriptlang.org/docs/handbook/2/classes.html#abstract-construct-signatures -// for why this is the abstract construct signature -export interface AnyAdapter { - new ( - config: AnyConfigurationModel, - getSubAdapter?: getSubAdapterType, - pluginManager?: PluginManager | undefined, - ): AnyDataAdapter -} - -export type AnyDataAdapter = - | BaseAdapter - | BaseFeatureDataAdapter - | BaseRefNameAliasAdapter - | BaseTextSearchAdapter - | RegionsAdapter - | BaseSequenceAdapter - -export interface SequenceAdapter - extends BaseFeatureDataAdapter, - RegionsAdapter {} - -const EmptyConfig = ConfigurationSchema('empty', {}) - -export abstract class BaseAdapter { - public id: string - - static capabilities = [] as string[] - - constructor( - public config: AnyConfigurationModel = EmptyConfig.create(), - public getSubAdapter?: getSubAdapterType, - public pluginManager?: PluginManager, - ) { - // note: we use switch on jest here for more simple feature IDs - // in test environment - if (typeof jest === 'undefined') { - const data = isStateTreeNode(config) ? getSnapshot(config) : config - this.id = `${idMaker(data)}` - } else { - this.id = 'test' - } - } - - getConf(arg: string | string[]) { - return readConfObject(this.config, arg) - } - - /** - * Called to provide a hint that data tied to a certain region will not be - * needed for the foreseeable future and can be purged from caches, etc - * @param region - Region - */ - public abstract freeResources(region: Region): void -} - -export interface Stats { - featureDensity?: number - fetchSizeLimit?: number - bytes?: number -} - -/** - * Base class for feature adapters to extend. Defines some methods that - * subclasses must implement. - */ -export abstract class BaseFeatureDataAdapter extends BaseAdapter { - /** - * Get all reference sequence names used in the data source - * - * NOTE: If an adapter is unable to determine the reference sequence names, - * the array will be empty - * @param opts - Feature adapter options - */ - public abstract getRefNames(opts?: BaseOptions): Promise - // public abstract async getRefNames(opts?: BaseOptions): Promise - // await this.setup() - // const { refNames } = this.metadata - // return refNames - // } - // - - /** - * Get features from the data source that overlap a region - * @param region - Region - * @param opts - Feature adapter options - * @returns Observable of Feature objects in the region - */ - public abstract getFeatures( - region: Region, - opts?: BaseOptions, - ): Observable - // public abstract getFeatures( - // region: Region, - // opts: BaseOptions, - // ): Observable { - // return ObservableCreate(observer => { - // const records = getRecords(assembly, refName, start, end) - // records.forEach(record => { - // observer.next(this.recordToFeature(record)) - // }) - // observer.complete() - // }) - // } - - /** - * Return "header info" that is fetched from the data file, or other info - * that would not simply be in the config of the file. The return value can - * be `{tag:string, data: any}[]` e.g. list of tags with their values which - * is how VCF,BAM,CRAM return values for getInfo or it can be a nested JSON - * object - */ - public async getHeader(_opts?: BaseOptions): Promise { - return null - } - - /** - * Return info that is primarily used for interpreting the data that is there, - * primarily in reference to being used for augmenting feature details panels - */ - public async getMetadata(_opts?: BaseOptions): Promise { - return null - } - - /** - * Checks if the store has data for the given assembly and reference - * sequence, and then gets the features in the region if it does. - */ - public getFeaturesInRegion(region: Region, opts: BaseOptions = {}) { - return ObservableCreate(async observer => { - const hasData = await this.hasDataForRefName(region.refName, opts) - checkAbortSignal(opts.signal) - if (!hasData) { - observer.complete() - } else { - this.getFeatures(region, opts).subscribe(observer) - } - }) - } - - /** - * Checks if the store has data for the given assembly and reference - * sequence, and then gets the features in the region if it does. - * - * Currently this just calls getFeatureInRegion for each region. Adapters - * that are frequently called on multiple regions simultaneously may - * want to implement a more efficient custom version of this method. - * - * Currently this just calls getFeatureInRegion for each region. Adapters that - * are frequently called on multiple regions simultaneously may want to - * implement a more efficient custom version of this method. - * - * @param regions - Regions - * @param opts - Feature adapter options - * @returns Observable of Feature objects in the regions - */ - public getFeaturesInMultipleRegions( - regions: Region[], - opts: BaseOptions = {}, - ) { - return merge( - ...regions.map(region => { - return this.getFeaturesInRegion(region, opts) - }), - ) - } - - /** - * Check if the store has data for the given reference name. - * @param refName - Name of the reference sequence - * @returns Whether data source has data for the given reference name - */ - public async hasDataForRefName(refName: string, opts: BaseOptions = {}) { - const refNames = await this.getRefNames(opts) - return refNames.includes(refName) - } - - public async getRegionStats(region: Region, opts?: BaseOptions) { - const feats = this.getFeatures(region, opts) - return scoresToStats(region, feats) - } - - public async getMultiRegionStats(regions: Region[] = [], opts?: BaseOptions) { - if (!regions.length) { - return blankStats() - } - const feats = await Promise.all( - regions.map(region => this.getRegionStats(region, opts)), - ) - - const scoreMax = max(feats.map(a => a.scoreMax)) - const scoreMin = min(feats.map(a => a.scoreMin)) - const scoreSum = sum(feats.map(a => a.scoreSum)) - const scoreSumSquares = sum(feats.map(a => a.scoreSumSquares)) - const featureCount = sum(feats.map(a => a.featureCount)) - const basesCovered = sum(feats.map(a => a.basesCovered)) - - return rectifyStats({ - scoreMin, - scoreMax, - featureCount, - basesCovered, - scoreSumSquares, - scoreSum, - }) - } - - public async estimateRegionsStats(regions: Region[], opts?: BaseOptions) { - if (!regions.length) { - throw new Error('No regions to estimate stats for') - } - const region = regions[0] - let lastTime = +Date.now() - const statsFromInterval = async (length: number, expansionTime: number) => { - const { start, end } = region - const sampleCenter = start * 0.75 + end * 0.25 - const query = { - ...region, - start: Math.max(0, Math.round(sampleCenter - length / 2)), - end: Math.min(Math.round(sampleCenter + length / 2), end), - } - - const features = await firstValueFrom( - this.getFeatures(query, opts).pipe(toArray()), - ) - - return maybeRecordStats( - length, - { featureDensity: features.length / length }, - features.length, - expansionTime, - ) - } - - const maybeRecordStats = async ( - interval: number, - stats: Stats, - statsSampleFeatures: number, - expansionTime: number, - ): Promise => { - const refLen = region.end - region.start - if (statsSampleFeatures >= 70 || interval * 2 > refLen) { - return stats - } else if (expansionTime <= 5000) { - const currTime = +Date.now() - expansionTime += currTime - lastTime - lastTime = currTime - return statsFromInterval(interval * 2, expansionTime) - } else { - console.warn( - "Stats estimation reached timeout, or didn't get enough features", - ) - return { featureDensity: Number.POSITIVE_INFINITY } - } - } - - return statsFromInterval(1000, 0) - } -} - -export interface RegionsAdapter extends BaseAdapter { - getRegions(opts: BaseOptions): Promise -} - -export abstract class BaseSequenceAdapter - extends BaseFeatureDataAdapter - implements RegionsAdapter -{ - async estimateRegionsStats() { - return { featureDensity: 0 } - } - - abstract getRegions(opts: BaseOptions): Promise -} - -export function isSequenceAdapter( - thing: AnyDataAdapter, -): thing is BaseSequenceAdapter { - return 'getRegions' in thing && 'getFeatures' in thing -} - -export function isRegionsAdapter( - thing: AnyDataAdapter, -): thing is RegionsAdapter { - return 'getRegions' in thing -} - -export function isFeatureAdapter( - thing: AnyDataAdapter, -): thing is BaseFeatureDataAdapter { - return 'getFeatures' in thing -} - -export interface Alias { - refName: string - aliases: string[] -} -export interface BaseRefNameAliasAdapter extends BaseAdapter { - getRefNameAliases(opts: BaseOptions): Promise -} -export function isRefNameAliasAdapter( - thing: object, -): thing is BaseRefNameAliasAdapter { - return 'getRefNameAliases' in thing -} -export interface BaseTextSearchAdapter extends BaseAdapter { - searchIndex(args: BaseArgs): Promise -} -export function isTextSearchAdapter( - thing: AnyDataAdapter, -): thing is BaseTextSearchAdapter { - return 'searchIndex' in thing -} diff --git a/packages/core/data_adapters/BaseAdapter/BaseAdapter.ts b/packages/core/data_adapters/BaseAdapter/BaseAdapter.ts new file mode 100644 index 0000000000..ebc20cf137 --- /dev/null +++ b/packages/core/data_adapters/BaseAdapter/BaseAdapter.ts @@ -0,0 +1,43 @@ +import { isStateTreeNode, getSnapshot } from 'mobx-state-tree' + +// locals +import { readConfObject, AnyConfigurationModel } from '../../configuration' +import { getSubAdapterType } from '../dataAdapterCache' +import { AugmentedRegion as Region } from '../../util/types' +import idMaker from '../../util/idMaker' +import PluginManager from '../../PluginManager' +import { ConfigurationSchema } from '../../configuration' + +const EmptyConfig = ConfigurationSchema('empty', {}) + +export abstract class BaseAdapter { + public id: string + + static capabilities = [] as string[] + + constructor( + public config: AnyConfigurationModel = EmptyConfig.create(), + public getSubAdapter?: getSubAdapterType, + public pluginManager?: PluginManager, + ) { + // note: we use switch on jest here for more simple feature IDs + // in test environment + if (typeof jest === 'undefined') { + const data = isStateTreeNode(config) ? getSnapshot(config) : config + this.id = `${idMaker(data)}` + } else { + this.id = 'test' + } + } + + getConf(arg: string | string[]) { + return readConfObject(this.config, arg) + } + + /** + * Called to provide a hint that data tied to a certain region will not be + * needed for the foreseeable future and can be purged from caches, etc + * @param region - Region + */ + public abstract freeResources(region: Region): void +} diff --git a/packages/core/data_adapters/BaseAdapter/BaseFeatureDataAdapter.ts b/packages/core/data_adapters/BaseAdapter/BaseFeatureDataAdapter.ts new file mode 100644 index 0000000000..0a4d95dbe4 --- /dev/null +++ b/packages/core/data_adapters/BaseAdapter/BaseFeatureDataAdapter.ts @@ -0,0 +1,256 @@ +import { Observable, firstValueFrom, merge } from 'rxjs' +import { toArray } from 'rxjs/operators' + +// locals +import { BaseAdapter } from './BaseAdapter' +import { BaseOptions } from './BaseOptions' +import { FeatureDensityStats } from './types' +import { ObservableCreate } from '../../util/rxjs' +import { checkAbortSignal, sum, max, min } from '../../util' +import { Feature } from '../../util/simpleFeature' +import { AugmentedRegion as Region } from '../../util/types' +import { blankStats, rectifyStats, scoresToStats } from '../../util/stats' + +/** + * Base class for feature adapters to extend. Defines some methods that + * subclasses must implement. + */ +export abstract class BaseFeatureDataAdapter extends BaseAdapter { + /** + * Get all reference sequence names used in the data source + * Example: + * public async getRefNames(opts?: BaseOptions): Promise\ \} + * await this.setup() + * const \{ refNames \} = this.metadata + * return refNames + * \} + * + * + * NOTE: If an adapter is unable to determine the reference sequence names, + * the array will be empty + * @param opts - Feature adapter options + */ + public abstract getRefNames(opts?: BaseOptions): Promise + + /** + * Get features from the data source that overlap a region + * Example: + * public getFeatures( + * region: Region, + * opts: BaseOptions, + * ): Observable \{ + * return ObservableCreate(observer =\> \{ + * const records = getRecords(assembly, refName, start, end) + * records.forEach(record =\> \{ + * observer.next(this.recordToFeature(record)) + * \}) + * observer.complete() + * \}) + * \} + * @param region - Region + * @param opts - Feature adapter options + * @returns Observable of Feature objects in the region + */ + public abstract getFeatures( + region: Region, + opts?: BaseOptions, + ): Observable + + /** + * Return "header info" that is fetched from the data file, or other info + * that would not simply be in the config of the file. The return value can + * be `{tag:string, data: any}[]` e.g. list of tags with their values which + * is how VCF,BAM,CRAM return values for getInfo or it can be a nested JSON + * object + */ + public async getHeader(_opts?: BaseOptions): Promise { + return null + } + + /** + * Return info that is primarily used for interpreting the data that is there, + * primarily in reference to being used for augmenting feature details panels + */ + public async getMetadata(_opts?: BaseOptions): Promise { + return null + } + + /** + * Checks if the store has data for the given assembly and reference + * sequence, and then gets the features in the region if it does. + */ + public getFeaturesInRegion(region: Region, opts: BaseOptions = {}) { + return ObservableCreate(async observer => { + const hasData = await this.hasDataForRefName(region.refName, opts) + checkAbortSignal(opts.signal) + if (!hasData) { + observer.complete() + } else { + this.getFeatures(region, opts).subscribe(observer) + } + }) + } + + /** + * Checks if the store has data for the given assembly and reference + * sequence, and then gets the features in the region if it does. + * + * Currently this just calls getFeatureInRegion for each region. Adapters that + * are frequently called on multiple regions simultaneously may want to + * implement a more efficient custom version of this method. + * + * Currently this just calls getFeatureInRegion for each region. Adapters that + * are frequently called on multiple regions simultaneously may want to + * implement a more efficient custom version of this method. + * + * @param regions - Regions + * @param opts - Feature adapter options + * @returns Observable of Feature objects in the regions + */ + public getFeaturesInMultipleRegions( + regions: Region[], + opts: BaseOptions = {}, + ) { + return merge( + ...regions.map(region => this.getFeaturesInRegion(region, opts)), + ) + } + + /** + * Check if the store has data for the given reference name. + * @param refName - Name of the reference sequence + * @returns Whether data source has data for the given reference name + */ + public async hasDataForRefName(refName: string, opts: BaseOptions = {}) { + const refNames = await this.getRefNames(opts) + return refNames.includes(refName) + } + + /** + * Calculates the minimum score, maximum score, and other statistics from + * features over a region, primarily used for quantitative tracks + */ + public async getRegionQuantitativeStats(region: Region, opts?: BaseOptions) { + const feats = this.getFeatures(region, opts) + return scoresToStats(region, feats) + } + /** + * Calculates the minimum score, maximum score, and other statistics from + * features over multiple regions, primarily used for quantitative tracks + */ + public async getMultiRegionQuantitativeStats( + regions: Region[] = [], + opts?: BaseOptions, + ) { + if (!regions.length) { + return blankStats() + } + const feats = await Promise.all( + regions.map(region => this.getRegionQuantitativeStats(region, opts)), + ) + + const scoreMax = max(feats.map(a => a.scoreMax)) + const scoreMin = min(feats.map(a => a.scoreMin)) + const scoreSum = sum(feats.map(a => a.scoreSum)) + const scoreSumSquares = sum(feats.map(a => a.scoreSumSquares)) + const featureCount = sum(feats.map(a => a.featureCount)) + const basesCovered = sum(feats.map(a => a.basesCovered)) + + return rectifyStats({ + scoreMin, + scoreMax, + featureCount, + basesCovered, + scoreSumSquares, + scoreSum, + }) + } + + /** + * Calculates the "feature density" of a region. The primary purpose of this + * API is to alert the user if they are going to be downloading too much + * information, and give them a hint to zoom in to see more. The default + * implementation samples from the regions, downloads feature data with + * getFeatures, and returns an object with the form \{featureDensity:number\} + * + * Derived classes can override this to return alternative calculations for + * featureDensity, or they can also return an object containing a byte size + * calculation with the format \{bytes:number, fetchSizeLimit:number\} where + * fetchSizeLimit is the adapter-defined limit for what it thinks is 'too much + * data' (e.g. CRAM and + * BAM may vary on what they think too much data is) + */ + getRegionFeatureDensityStats(region: Region, opts?: BaseOptions) { + let lastTime = +Date.now() + const statsFromInterval = async (length: number, expansionTime: number) => { + const { start, end } = region + const sampleCenter = start * 0.75 + end * 0.25 + + const features = await firstValueFrom( + this.getFeatures( + { + ...region, + start: Math.max(0, Math.round(sampleCenter - length / 2)), + end: Math.min(Math.round(sampleCenter + length / 2), end), + }, + opts, + ).pipe(toArray()), + ) + + return maybeRecordStats( + length, + { featureDensity: features.length / length }, + features.length, + expansionTime, + ) + } + + const maybeRecordStats = async ( + interval: number, + stats: FeatureDensityStats, + statsSampleFeatures: number, + expansionTime: number, + ): Promise => { + const refLen = region.end - region.start + if (statsSampleFeatures >= 70 || interval * 2 > refLen) { + return stats + } else if (expansionTime <= 5000) { + const currTime = +Date.now() + expansionTime += currTime - lastTime + lastTime = currTime + return statsFromInterval(interval * 2, expansionTime) + } else { + console.warn( + "Stats estimation reached timeout, or didn't get enough features", + ) + return { featureDensity: Number.POSITIVE_INFINITY } + } + } + + return statsFromInterval(1000, 0) + } + + /** + * Calculates the "feature density" of a set of regions. The primary purpose + * of this API is to alert the user if they are going to be downloading too + * much information, and give them a hint to zoom in to see more. The default + * implementation samples from the regions, downloads feature data with + * getFeatures, and returns an object with the form \{featureDensity:number\} + * + * Derived classes can override this to return alternative calculations for + * featureDensity, or they can also return an object containing a byte size + * calculation with the format \{bytes:number, fetchSizeLimit:number\} where + * fetchSizeLimit is the adapter-defined limit for what it thinks is 'too much + * data' (e.g. CRAM and + * BAM may vary on what they think too much data is) + */ + public async getMultiRegionFeatureDensityStats( + regions: Region[], + opts?: BaseOptions, + ) { + if (!regions.length) { + throw new Error('No regions supplied') + } + return this.getRegionFeatureDensityStats(regions[0], opts) + } +} diff --git a/packages/core/data_adapters/BaseAdapter/BaseOptions.ts b/packages/core/data_adapters/BaseAdapter/BaseOptions.ts new file mode 100644 index 0000000000..759e096103 --- /dev/null +++ b/packages/core/data_adapters/BaseAdapter/BaseOptions.ts @@ -0,0 +1,18 @@ +export interface BaseOptions { + signal?: AbortSignal + bpPerPx?: number + sessionId?: string + statusCallback?: (message: string) => void + headers?: Record + [key: string]: unknown +} + +export type SearchType = 'full' | 'prefix' | 'exact' + +export interface BaseTextSearchArgs { + queryString: string + searchType?: SearchType + signal?: AbortSignal + limit?: number + pageNumber?: number +} diff --git a/packages/core/data_adapters/BaseAdapter/BaseRefNameAliasAdapter.ts b/packages/core/data_adapters/BaseAdapter/BaseRefNameAliasAdapter.ts new file mode 100644 index 0000000000..49e8707008 --- /dev/null +++ b/packages/core/data_adapters/BaseAdapter/BaseRefNameAliasAdapter.ts @@ -0,0 +1,10 @@ +import { BaseAdapter } from './BaseAdapter' +import { BaseOptions } from './types' + +export interface Alias { + refName: string + aliases: string[] +} +export interface BaseRefNameAliasAdapter extends BaseAdapter { + getRefNameAliases(opts: BaseOptions): Promise +} diff --git a/packages/core/data_adapters/BaseAdapter/BaseSequenceAdapter.ts b/packages/core/data_adapters/BaseAdapter/BaseSequenceAdapter.ts new file mode 100644 index 0000000000..f4ae574c22 --- /dev/null +++ b/packages/core/data_adapters/BaseAdapter/BaseSequenceAdapter.ts @@ -0,0 +1,18 @@ +import { NoAssemblyRegion } from '../../util' +import { BaseOptions } from './types' +import { RegionsAdapter } from '../BaseAdapter' +import { BaseFeatureDataAdapter } from './BaseFeatureDataAdapter' + +export abstract class BaseSequenceAdapter + extends BaseFeatureDataAdapter + implements RegionsAdapter +{ + async getMultiRegionFeatureDensityStats() { + return { featureDensity: 0 } + } + + /** + * Fetches a list of 'regions' with refName, start, and extends + */ + abstract getRegions(opts: BaseOptions): Promise +} diff --git a/packages/core/data_adapters/BaseAdapter/BaseTextSearchAdapter.ts b/packages/core/data_adapters/BaseAdapter/BaseTextSearchAdapter.ts new file mode 100644 index 0000000000..930d087ef6 --- /dev/null +++ b/packages/core/data_adapters/BaseAdapter/BaseTextSearchAdapter.ts @@ -0,0 +1,7 @@ +import BaseResult from '../../TextSearch/BaseResults' +import { BaseTextSearchArgs } from './types' +import { BaseAdapter } from './BaseAdapter' + +export interface BaseTextSearchAdapter extends BaseAdapter { + searchIndex(args: BaseTextSearchArgs): Promise +} diff --git a/packages/core/data_adapters/BaseAdapter/RegionsAdapter.ts b/packages/core/data_adapters/BaseAdapter/RegionsAdapter.ts new file mode 100644 index 0000000000..7516c38fcc --- /dev/null +++ b/packages/core/data_adapters/BaseAdapter/RegionsAdapter.ts @@ -0,0 +1,7 @@ +import { NoAssemblyRegion } from '../../util' +import { BaseAdapter } from './BaseAdapter' +import { BaseOptions } from './types' + +export interface RegionsAdapter extends BaseAdapter { + getRegions(opts: BaseOptions): Promise +} diff --git a/packages/core/data_adapters/BaseAdapter/index.ts b/packages/core/data_adapters/BaseAdapter/index.ts new file mode 100644 index 0000000000..bf73c6b315 --- /dev/null +++ b/packages/core/data_adapters/BaseAdapter/index.ts @@ -0,0 +1,24 @@ +import { AnyConfigurationModel } from '../../configuration' +import PluginManager from '../../PluginManager' +import { getSubAdapterType } from '../dataAdapterCache' +import { AnyDataAdapter } from './util' + +export * from './util' +export * from './types' +export { BaseAdapter } from './BaseAdapter' +export { BaseFeatureDataAdapter } from './BaseFeatureDataAdapter' +export { BaseSequenceAdapter } from './BaseSequenceAdapter' +export type { BaseTextSearchAdapter } from './BaseTextSearchAdapter' +export type { BaseRefNameAliasAdapter } from './BaseRefNameAliasAdapter' +export type { RegionsAdapter } from './RegionsAdapter' + +// see +// https://www.typescriptlang.org/docs/handbook/2/classes.html#abstract-construct-signatures +// for why this is the abstract construct signature +export interface AnyAdapter { + new ( + config: AnyConfigurationModel, + getSubAdapter?: getSubAdapterType, + pluginManager?: PluginManager | undefined, + ): AnyDataAdapter +} diff --git a/packages/core/data_adapters/BaseAdapter/types.ts b/packages/core/data_adapters/BaseAdapter/types.ts new file mode 100644 index 0000000000..717c3a2dc5 --- /dev/null +++ b/packages/core/data_adapters/BaseAdapter/types.ts @@ -0,0 +1,24 @@ +export interface BaseOptions { + signal?: AbortSignal + bpPerPx?: number + sessionId?: string + statusCallback?: (message: string) => void + headers?: Record + [key: string]: unknown +} + +export type SearchType = 'full' | 'prefix' | 'exact' + +export interface BaseTextSearchArgs { + queryString: string + searchType?: SearchType + signal?: AbortSignal + limit?: number + pageNumber?: number +} + +export interface FeatureDensityStats { + featureDensity?: number + fetchSizeLimit?: number + bytes?: number +} diff --git a/packages/core/data_adapters/BaseAdapter/util.ts b/packages/core/data_adapters/BaseAdapter/util.ts new file mode 100644 index 0000000000..a392383962 --- /dev/null +++ b/packages/core/data_adapters/BaseAdapter/util.ts @@ -0,0 +1,38 @@ +import { BaseAdapter } from './BaseAdapter' +import { BaseFeatureDataAdapter } from './BaseFeatureDataAdapter' +import { BaseRefNameAliasAdapter } from './BaseRefNameAliasAdapter' +import { BaseSequenceAdapter } from './BaseSequenceAdapter' +import { BaseTextSearchAdapter } from './BaseTextSearchAdapter' +import { RegionsAdapter } from './RegionsAdapter' + +export type AnyDataAdapter = + | BaseAdapter + | BaseFeatureDataAdapter + | BaseRefNameAliasAdapter + | BaseTextSearchAdapter + | RegionsAdapter + | BaseSequenceAdapter + +export function isSequenceAdapter(t: AnyDataAdapter): t is BaseSequenceAdapter { + return 'getRegions' in t && 'getFeatures' in t +} + +export function isRegionsAdapter(t: AnyDataAdapter): t is RegionsAdapter { + return 'getRegions' in t +} + +export function isFeatureAdapter( + t: AnyDataAdapter, +): t is BaseFeatureDataAdapter { + return 'getFeatures' in t +} + +export function isRefNameAliasAdapter(t: object): t is BaseRefNameAliasAdapter { + return 'getRefNameAliases' in t +} + +export function isTextSearchAdapter( + t: AnyDataAdapter, +): t is BaseTextSearchAdapter { + return 'searchIndex' in t +} diff --git a/packages/core/rpc/coreRpcMethods.ts b/packages/core/rpc/coreRpcMethods.ts index 0c8602c4f8..e81beffae9 100644 --- a/packages/core/rpc/coreRpcMethods.ts +++ b/packages/core/rpc/coreRpcMethods.ts @@ -5,5 +5,5 @@ export { default as CoreGetFileInfo } from './methods/CoreGetFileInfo' export { default as CoreGetFeatures } from './methods/CoreGetFeatures' export { default as CoreRender } from './methods/CoreRender' export { default as CoreFreeResources } from './methods/CoreFreeResources' +export { default as CoreGetFeatureDensityStats } from './methods/CoreGetFeatureDensityStats' export { type RenderArgs } from './methods/util' -export { default as CoreEstimateRegionStats } from './methods/CoreEstimateRegionStats' diff --git a/packages/core/rpc/methods/CoreEstimateRegionStats.ts b/packages/core/rpc/methods/CoreGetFeatureDensityStats.ts similarity index 87% rename from packages/core/rpc/methods/CoreEstimateRegionStats.ts rename to packages/core/rpc/methods/CoreGetFeatureDensityStats.ts index 4ce1dc43ba..98bb7c7823 100644 --- a/packages/core/rpc/methods/CoreEstimateRegionStats.ts +++ b/packages/core/rpc/methods/CoreGetFeatureDensityStats.ts @@ -6,8 +6,8 @@ import { RemoteAbortSignal } from '../remoteAbortSignals' import { isFeatureAdapter } from '../../data_adapters/BaseAdapter' import { renameRegionsIfNeeded, Region } from '../../util' -export default class CoreEstimateRegionStats extends RpcMethodType { - name = 'CoreEstimateRegionStats' +export default class CoreGetFeatureDensityStats extends RpcMethodType { + name = 'CoreGetFeatureDensityStats' async serializeArguments( args: RenderArgs & { @@ -44,6 +44,9 @@ export default class CoreEstimateRegionStats extends RpcMethodType { if (!isFeatureAdapter(dataAdapter)) { throw new Error('Adapter does not support retrieving features') } - return dataAdapter.estimateRegionsStats(regions, deserializedArgs) + return dataAdapter.getMultiRegionFeatureDensityStats( + regions, + deserializedArgs, + ) } } diff --git a/packages/core/util/stats.test.ts b/packages/core/util/stats.test.ts index 1d3ae55699..349a0250af 100644 --- a/packages/core/util/stats.test.ts +++ b/packages/core/util/stats.test.ts @@ -5,7 +5,7 @@ import { rectifyStats, scoresToStats, calcPerBaseStats, - UnrectifiedFeatureStats, + UnrectifiedQuantitativeStats, } from './stats' test('calc std', () => { @@ -21,12 +21,12 @@ test('calc std', () => { test('test rectify', () => { // mean of 0 bases covered = 0 expect( - rectifyStats({ basesCovered: 0 } as UnrectifiedFeatureStats).scoreMean, + rectifyStats({ basesCovered: 0 } as UnrectifiedQuantitativeStats).scoreMean, ).toEqual(0) const s = rectifyStats({ featureCount: 10, scoreSum: 1000, - } as UnrectifiedFeatureStats) + } as UnrectifiedQuantitativeStats) expect(s.scoreMean).toEqual(100) expect(s.featureCount).toEqual(10) @@ -36,7 +36,7 @@ test('test rectify', () => { featureCount: 3, scoreSum: 6, scoreSumSquares: 14, - } as UnrectifiedFeatureStats).scoreStdDev, + } as UnrectifiedQuantitativeStats).scoreStdDev, ).toEqual(1) // calculated from a webapp about sample standard deviations }) diff --git a/packages/core/util/stats.ts b/packages/core/util/stats.ts index d34081b47a..4fdeaf9f16 100644 --- a/packages/core/util/stats.ts +++ b/packages/core/util/stats.ts @@ -5,7 +5,7 @@ import { reduce } from 'rxjs/operators' import { NoAssemblyRegion } from './types' import { Feature } from './simpleFeature' -export interface UnrectifiedFeatureStats { +export interface UnrectifiedQuantitativeStats { scoreMin: number scoreMax: number scoreSum: number @@ -13,7 +13,7 @@ export interface UnrectifiedFeatureStats { featureCount: number basesCovered: number } -export interface FeatureStats extends UnrectifiedFeatureStats { +export interface QuantitativeStats extends UnrectifiedQuantitativeStats { featureDensity: number scoreMean: number scoreStdDev: number @@ -58,7 +58,7 @@ export function calcStdFromSums( * @returns - a summary stats object with * scoreMean, scoreStdDev, and featureDensity added */ -export function rectifyStats(s: UnrectifiedFeatureStats) { +export function rectifyStats(s: UnrectifiedQuantitativeStats) { return { ...s, scoreMean: (s.scoreSum || 0) / (s.featureCount || s.basesCovered || 1), @@ -68,7 +68,7 @@ export function rectifyStats(s: UnrectifiedFeatureStats) { s.featureCount || s.basesCovered, ), featureDensity: (s.featureCount || 1) / s.basesCovered, - } as FeatureStats + } as QuantitativeStats } /** @@ -170,5 +170,5 @@ export function blankStats() { featureCount: 0, featureDensity: 0, basesCovered: 0, - } as FeatureStats + } as QuantitativeStats } diff --git a/plugins/alignments/src/BamAdapter/BamAdapter.ts b/plugins/alignments/src/BamAdapter/BamAdapter.ts index 75be125740..98b1dcbd38 100644 --- a/plugins/alignments/src/BamAdapter/BamAdapter.ts +++ b/plugins/alignments/src/BamAdapter/BamAdapter.ts @@ -227,7 +227,10 @@ export default class BamAdapter extends BaseFeatureDataAdapter { }, signal) } - async estimateRegionsStats(regions: Region[], opts?: BaseOptions) { + async getMultiRegionFeatureDensityStats( + regions: Region[], + opts?: BaseOptions, + ) { const { bam } = await this.configure() // this is a method to avoid calling on htsget adapters // @ts-expect-error @@ -236,7 +239,7 @@ export default class BamAdapter extends BaseFeatureDataAdapter { const fetchSizeLimit = this.getConf('fetchSizeLimit') return { bytes, fetchSizeLimit } } else { - return super.estimateRegionsStats(regions, opts) + return super.getMultiRegionFeatureDensityStats(regions, opts) } } diff --git a/plugins/alignments/src/CramAdapter/CramAdapter.ts b/plugins/alignments/src/CramAdapter/CramAdapter.ts index b49a535415..0305c24054 100644 --- a/plugins/alignments/src/CramAdapter/CramAdapter.ts +++ b/plugins/alignments/src/CramAdapter/CramAdapter.ts @@ -278,7 +278,10 @@ export default class CramAdapter extends BaseFeatureDataAdapter { } // we return the configured fetchSizeLimit, and the bytes for the region - async estimateRegionsStats(regions: Region[], opts?: BaseOptions) { + async getMultiRegionFeatureDensityStats( + regions: Region[], + opts?: BaseOptions, + ) { const bytes = await this.bytesForRegions(regions, opts) const fetchSizeLimit = this.getConf('fetchSizeLimit') return { diff --git a/plugins/alignments/src/LinearAlignmentsDisplay/models/model.tsx b/plugins/alignments/src/LinearAlignmentsDisplay/models/model.tsx index d593a9b21c..18dca0fb61 100644 --- a/plugins/alignments/src/LinearAlignmentsDisplay/models/model.tsx +++ b/plugins/alignments/src/LinearAlignmentsDisplay/models/model.tsx @@ -20,6 +20,7 @@ import { import { BaseDisplay } from '@jbrowse/core/pluggableElementTypes/models' import PluginManager from '@jbrowse/core/PluginManager' import { MenuItem } from '@jbrowse/core/ui' +import { FeatureDensityStats } from '@jbrowse/core/data_adapters/BaseAdapter' const minDisplayHeight = 20 @@ -234,9 +235,9 @@ function stateModelFactory( /** * #action */ - updateStatsLimit(stats?: unknown) { - self.PileupDisplay.updateStatsLimit(stats) - self.SNPCoverageDisplay.updateStatsLimit(stats) + setFeatureDensityStatsLimit(stats?: FeatureDensityStats) { + self.PileupDisplay.setFeatureDensityStatsLimit(stats) + self.SNPCoverageDisplay.setFeatureDensityStatsLimit(stats) }, /** diff --git a/plugins/alignments/src/LinearPileupDisplay/model.ts b/plugins/alignments/src/LinearPileupDisplay/model.ts index 47c35413a0..7508005e7e 100644 --- a/plugins/alignments/src/LinearPileupDisplay/model.ts +++ b/plugins/alignments/src/LinearPileupDisplay/model.ts @@ -253,7 +253,7 @@ function stateModelFactory(configSchema: AnyConfigurationSchemaType) { if ( !view.initialized || - !self.estimatedStatsReady || + !self.featureDensityStatsReady || self.regionTooLarge ) { return diff --git a/plugins/alignments/src/LinearSNPCoverageDisplay/models/model.ts b/plugins/alignments/src/LinearSNPCoverageDisplay/models/model.ts index d68116bedd..72c8581207 100644 --- a/plugins/alignments/src/LinearSNPCoverageDisplay/models/model.ts +++ b/plugins/alignments/src/LinearSNPCoverageDisplay/models/model.ts @@ -218,7 +218,7 @@ function stateModelFactory( if ( !view.initialized || - !self.estimatedStatsReady || + !self.featureDensityStatsReady || self.regionTooLarge ) { return diff --git a/plugins/alignments/src/SNPCoverageAdapter/SNPCoverageAdapter.ts b/plugins/alignments/src/SNPCoverageAdapter/SNPCoverageAdapter.ts index 84427ee07f..a1f6d2e586 100644 --- a/plugins/alignments/src/SNPCoverageAdapter/SNPCoverageAdapter.ts +++ b/plugins/alignments/src/SNPCoverageAdapter/SNPCoverageAdapter.ts @@ -94,9 +94,12 @@ export default class SNPCoverageAdapter extends BaseFeatureDataAdapter { }, opts.signal) } - async estimateRegionsStats(regions: Region[], opts?: BaseOptions) { + async getMultiRegionFeatureDensityStats( + regions: Region[], + opts?: BaseOptions, + ) { const { subadapter } = await this.configure() - return subadapter.estimateRegionsStats(regions, opts) + return subadapter.getMultiRegionFeatureDensityStats(regions, opts) } async getRefNames(opts: BaseOptions = {}) { diff --git a/plugins/hic/src/HicAdapter/HicAdapter.ts b/plugins/hic/src/HicAdapter/HicAdapter.ts index 7cbf71ed34..77f96dcdc2 100644 --- a/plugins/hic/src/HicAdapter/HicAdapter.ts +++ b/plugins/hic/src/HicAdapter/HicAdapter.ts @@ -142,7 +142,7 @@ export default class HicAdapter extends BaseFeatureDataAdapter { } // don't do feature stats estimation, similar to bigwigadapter - async estimateRegionsStats(_regions: Region[]) { + async getMultiRegionFeatureDensityStats(_regions: Region[]) { return { featureDensity: 0 } } diff --git a/plugins/legacy-jbrowse/src/JBrowse1TextSearchAdapter/JBrowse1TextSearchAdapter.ts b/plugins/legacy-jbrowse/src/JBrowse1TextSearchAdapter/JBrowse1TextSearchAdapter.ts index 511af73240..d32cdc5e5c 100644 --- a/plugins/legacy-jbrowse/src/JBrowse1TextSearchAdapter/JBrowse1TextSearchAdapter.ts +++ b/plugins/legacy-jbrowse/src/JBrowse1TextSearchAdapter/JBrowse1TextSearchAdapter.ts @@ -1,6 +1,6 @@ import { BaseTextSearchAdapter, - BaseArgs, + BaseTextSearchArgs, BaseAdapter, } from '@jbrowse/core/data_adapters/BaseAdapter' import BaseResult from '@jbrowse/core/TextSearch/BaseResults' @@ -57,7 +57,7 @@ export default class JBrowse1TextSearchAdapter return this.httpMap.getBucket(query) } - async searchIndex(args: BaseArgs) { + async searchIndex(args: BaseTextSearchArgs) { const { searchType, queryString } = args const tracks = this.tracksNames || (await this.httpMap.getTrackNames()) const str = queryString.toLowerCase() diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/components/TooLargeMessage.tsx b/plugins/linear-genome-view/src/BaseLinearDisplay/components/TooLargeMessage.tsx index e99854870f..3856d247a5 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/components/TooLargeMessage.tsx +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/components/TooLargeMessage.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Stats } from '@jbrowse/core/data_adapters/BaseAdapter' +import { FeatureDensityStats } from '@jbrowse/core/data_adapters/BaseAdapter' // locals import BlockMsg from '../components/BlockMsg' @@ -9,8 +9,8 @@ function TooLargeMessage({ }: { model: { regionTooLargeReason: string - estimatedRegionsStats?: Stats - updateStatsLimit: (s?: Stats) => void + featureDensityStats?: FeatureDensityStats + setFeatureDensityStatsLimit: (s?: FeatureDensityStats) => void reload: () => void } }) { @@ -19,12 +19,16 @@ function TooLargeMessage({ { - model.updateStatsLimit(model.estimatedRegionsStats) + model.setFeatureDensityStatsLimit(model.featureDensityStats) model.reload() }} buttonText="Force load" - message={`${regionTooLargeReason ? `${regionTooLargeReason}. ` : ''} - Zoom in to see features or force load (may be slow).`} + message={[ + regionTooLargeReason, + 'Zoom in to see features or force load (may be slow)', + ] + .filter(f => !!f) + .join('. ')} /> ) } diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx b/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx index f365f4793e..04704b17ee 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx @@ -16,7 +16,7 @@ import { Feature, ReactRendering, } from '@jbrowse/core/util' -import { Stats } from '@jbrowse/core/data_adapters/BaseAdapter' +import { FeatureDensityStats } from '@jbrowse/core/data_adapters/BaseAdapter' import { BaseBlock } from '@jbrowse/core/util/blockTypes' import { Region } from '@jbrowse/core/util/types' import CompositeMap from '@jbrowse/core/util/compositeMap' @@ -32,7 +32,7 @@ import { LinearGenomeViewModel, ExportSvgOptions } from '../../LinearGenomeView' import { Tooltip } from '../components/BaseLinearDisplay' import TooLargeMessage from '../components/TooLargeMessage' import BlockState, { renderBlockData } from './serverSideRenderedBlock' -import { getId, getDisplayStr, estimateRegionsStatsPre } from './util' +import { getId, getDisplayStr, getFeatureDensityStatsPre } from './util' type LGV = LinearGenomeViewModel @@ -89,8 +89,10 @@ function stateModelFactory() { message: '', featureIdUnderMouse: undefined as undefined | string, contextMenuFeature: undefined as undefined | Feature, - estimatedRegionsStatsP: undefined as undefined | Promise, - estimatedRegionsStats: undefined as undefined | Stats, + featureDensityStatsP: undefined as + | undefined + | Promise, + featureDensityStats: undefined as undefined | FeatureDensityStats, })) .views(self => ({ get height() { @@ -215,7 +217,7 @@ function stateModelFactory() { * #getter */ get currentBytesRequested() { - return self.estimatedRegionsStats?.bytes || 0 + return self.featureDensityStats?.bytes || 0 }, /** @@ -223,7 +225,7 @@ function stateModelFactory() { */ get currentFeatureScreenDensity() { const view = getContainingView(self) as LGV - return (self.estimatedRegionsStats?.featureDensity || 0) * view.bpPerPx + return (self.featureDensityStats?.featureDensity || 0) * view.bpPerPx }, /** @@ -235,8 +237,8 @@ function stateModelFactory() { /** * #getter */ - get estimatedStatsReady() { - return !!self.estimatedRegionsStats || !!self.userBpPerPxLimit + get featureDensityStatsReady() { + return !!self.featureDensityStats || !!self.userBpPerPxLimit }, /** @@ -245,7 +247,7 @@ function stateModelFactory() { get maxAllowableBytes() { return ( self.userByteSizeLimit || - self.estimatedRegionsStats?.fetchSizeLimit || + self.featureDensityStats?.fetchSizeLimit || (getConf(self, 'fetchSizeLimit') as number) ) }, @@ -288,37 +290,37 @@ function stateModelFactory() { /** * #action */ - async estimateRegionsStats() { - if (!self.estimatedRegionsStatsP) { - self.estimatedRegionsStatsP = estimateRegionsStatsPre(self).catch( + async getFeatureDensityStats() { + if (!self.featureDensityStatsP) { + self.featureDensityStatsP = getFeatureDensityStatsPre(self).catch( e => { - this.setRegionsStatsP(undefined) + this.setFeatureDensityStatsP(undefined) throw e }, ) } - return self.estimatedRegionsStatsP + return self.featureDensityStatsP }, /** * #action */ - setRegionsStatsP(arg: any) { - self.estimatedRegionsStatsP = arg + setFeatureDensityStatsP(arg: any) { + self.featureDensityStatsP = arg }, /** * #action */ - setRegionsStats(estimatedRegionsStats?: Stats) { - self.estimatedRegionsStats = estimatedRegionsStats + setFeatureDensityStats(featureDensityStats?: FeatureDensityStats) { + self.featureDensityStats = featureDensityStats }, /** * #action */ - clearRegionsStats() { - self.estimatedRegionsStatsP = undefined - self.estimatedRegionsStats = undefined + clearFeatureDensityStats() { + self.featureDensityStatsP = undefined + self.featureDensityStats = undefined }, /** * #action @@ -347,7 +349,7 @@ function stateModelFactory() { /** * #action */ - updateStatsLimit(stats?: Stats) { + setFeatureDensityStatsLimit(stats?: FeatureDensityStats) { const view = getContainingView(self) as LGV if (stats?.bytes) { self.userByteSizeLimit = stats.bytes @@ -406,13 +408,12 @@ function stateModelFactory() { * #action */ clearFeatureSelection() { - const session = getSession(self) - session.clearSelection() + getSession(self).clearSelection() }, /** * #action */ - setFeatureIdUnderMouse(feature: string | undefined) { + setFeatureIdUnderMouse(feature?: string) { self.featureIdUnderMouse = feature }, /** @@ -434,11 +435,15 @@ function stateModelFactory() { * region is too large if: * - stats are ready * - region is greater than 20kb (don't warn when zoomed in less than that) - * - and bytes is greater than max allowed bytes or density greater than max density + * - and bytes is greater than max allowed bytes or density greater than max + * density */ get regionTooLarge() { const view = getContainingView(self) as LGV - if (!self.estimatedStatsReady || view.dynamicBlocks.totalBp < 20_000) { + if ( + !self.featureDensityStatsReady || + view.dynamicBlocks.totalBp < 20_000 + ) { return false } return ( @@ -481,10 +486,10 @@ function stateModelFactory() { } try { - const estimatedRegionsStats = await self.estimateRegionsStats() + const featureDensityStats = await self.getFeatureDensityStats() if (isAlive(self)) { - self.setRegionsStats(estimatedRegionsStats) + self.setFeatureDensityStats(featureDensityStats) superReload() } } catch (e) { @@ -498,7 +503,7 @@ function stateModelFactory() { afterAttach() { // this autorun performs stats estimation // - // the chain of events calls estimateRegionsStats against the data + // the chain of events calls getFeatureDensityStats against the data // adapter which by default uses featureDensity, but can also respond // with a byte size estimate and fetch size limit (data adapter can // define what is too much data) @@ -519,7 +524,7 @@ function stateModelFactory() { // don't re-estimate featureDensity even if zoom level changes, // jbrowse1-style assume it's sort of representative - if (self.estimatedRegionsStats?.featureDensity !== undefined) { + if (self.featureDensityStats?.featureDensity !== undefined) { self.setCurrBpPerPx(view.bpPerPx) return } @@ -529,11 +534,11 @@ function stateModelFactory() { return } - self.clearRegionsStats() + self.clearFeatureDensityStats() self.setCurrBpPerPx(view.bpPerPx) - const estimatedRegionsStats = await self.estimateRegionsStats() + const featureDensityStats = await self.getFeatureDensityStats() if (isAlive(self)) { - self.setRegionsStats(estimatedRegionsStats) + self.setFeatureDensityStats(featureDensityStats) } } catch (e) { if (!isAbortException(e) && isAlive(self)) { @@ -600,7 +605,7 @@ function stateModelFactory() { return { ...getParentRenderProps(self), notReady: - self.currBpPerPx !== view.bpPerPx || !self.estimatedRegionsStats, + self.currBpPerPx !== view.bpPerPx || !self.featureDensityStatsReady, rpcDriverName: self.rpcDriverName, displayModel: self, onFeatureClick(_: unknown, featureId?: string) { diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/models/util.ts b/plugins/linear-genome-view/src/BaseLinearDisplay/models/util.ts index 1f187a8742..332776e05d 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/models/util.ts +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/models/util.ts @@ -1,4 +1,5 @@ -import { Stats } from '@jbrowse/core/data_adapters/BaseAdapter' +import { AnyConfigurationModel } from '@jbrowse/core/configuration' +import { FeatureDensityStats } from '@jbrowse/core/data_adapters/BaseAdapter' import { getContainingView, getSession } from '@jbrowse/core/util' import { getRpcSessionId } from '@jbrowse/core/util/tracks' import { IAnyStateTreeNode, isAlive } from 'mobx-state-tree' @@ -35,7 +36,12 @@ export function getId(id: string, index: number) { return `clip-${isJest ? id : 'jest'}-${index}` } -export async function estimateRegionsStatsPre(self: IAnyStateTreeNode) { +export async function getFeatureDensityStatsPre( + self: IAnyStateTreeNode & { + adapterConfig?: AnyConfigurationModel + setMessage: (arg: string) => void + }, +) { const view = getContainingView(self) as LinearGenomeViewModel const regions = view.staticBlocks.contentBlocks @@ -48,7 +54,7 @@ export async function estimateRegionsStatsPre(self: IAnyStateTreeNode) { } const sessionId = getRpcSessionId(self) - return rpcManager.call(sessionId, 'CoreEstimateRegionStats', { + return rpcManager.call(sessionId, 'CoreGetFeatureDensityStats', { sessionId, regions, adapterConfig, @@ -57,5 +63,5 @@ export async function estimateRegionsStatsPre(self: IAnyStateTreeNode) { self.setMessage(message) } }, - }) as Promise + }) as Promise } diff --git a/plugins/trix/src/TrixTextSearchAdapter/TrixTextSearchAdapter.ts b/plugins/trix/src/TrixTextSearchAdapter/TrixTextSearchAdapter.ts index df887ed03a..3aa62a5d99 100644 --- a/plugins/trix/src/TrixTextSearchAdapter/TrixTextSearchAdapter.ts +++ b/plugins/trix/src/TrixTextSearchAdapter/TrixTextSearchAdapter.ts @@ -1,8 +1,8 @@ import Trix from '@gmod/trix' import { BaseTextSearchAdapter, - BaseArgs, BaseAdapter, + BaseTextSearchArgs, } from '@jbrowse/core/data_adapters/BaseAdapter' import { openLocation } from '@jbrowse/core/util/io' import BaseResult from '@jbrowse/core/TextSearch/BaseResults' @@ -67,7 +67,7 @@ export default class TrixTextSearchAdapter * @param args - search options/arguments include: search query * limit of results to return, searchType...prefix | full | exact", etc. */ - async searchIndex(args: BaseArgs) { + async searchIndex(args: BaseTextSearchArgs) { const query = args.queryString.toLowerCase() const strs = query.split(' ') const results = await this.trixJs.search(query) @@ -109,12 +109,11 @@ export default class TrixTextSearchAdapter }) }) - if (args.searchType === 'exact') { - return formatted.filter( - res => res.getLabel().toLowerCase() === args.queryString.toLowerCase(), - ) - } - return formatted + return args.searchType === 'exact' + ? formatted.filter( + r => r.getLabel().toLowerCase() === args.queryString.toLowerCase(), + ) + : formatted } freeResources() {} diff --git a/plugins/wiggle/src/BigWigAdapter/BigWigAdapter.test.ts b/plugins/wiggle/src/BigWigAdapter/BigWigAdapter.test.ts index 4f3d9ef1ce..470d6d8db8 100644 --- a/plugins/wiggle/src/BigWigAdapter/BigWigAdapter.test.ts +++ b/plugins/wiggle/src/BigWigAdapter/BigWigAdapter.test.ts @@ -36,7 +36,7 @@ describe('adapter can fetch features from volvox.bw', () => { }) it('get region stats', async () => { expect( - await adapter.getRegionStats({ + await adapter.getRegionQuantitativeStats({ refName: 'ctgA', start: 10000, end: 40000, @@ -47,14 +47,19 @@ describe('adapter can fetch features from volvox.bw', () => { it('get local stats', async () => { expect( - await adapter.getMultiRegionStats([ + await adapter.getMultiRegionQuantitativeStats([ { refName: 'ctgA', start: 10000, end: 39999, assemblyName: 'volvox', }, - { refName: 'ctgB', start: 0, end: 99, assemblyName: 'volvox' }, + { + refName: 'ctgB', + start: 0, + end: 99, + assemblyName: 'volvox', + }, ]), ).toMatchSnapshot() }) diff --git a/plugins/wiggle/src/BigWigAdapter/BigWigAdapter.ts b/plugins/wiggle/src/BigWigAdapter/BigWigAdapter.ts index 2223217387..c5f52ed66e 100644 --- a/plugins/wiggle/src/BigWigAdapter/BigWigAdapter.ts +++ b/plugins/wiggle/src/BigWigAdapter/BigWigAdapter.ts @@ -7,7 +7,10 @@ import { AugmentedRegion as Region } from '@jbrowse/core/util/types' import { openLocation } from '@jbrowse/core/util/io' import { updateStatus, Feature } from '@jbrowse/core/util' import { ObservableCreate } from '@jbrowse/core/util/rxjs' -import { rectifyStats, UnrectifiedFeatureStats } from '@jbrowse/core/util/stats' +import { + rectifyStats, + UnrectifiedQuantitativeStats, +} from '@jbrowse/core/util/stats' interface WiggleOptions extends BaseOptions { resolution?: number @@ -61,7 +64,7 @@ export default class BigWigAdapter extends BaseFeatureDataAdapter { public async getGlobalStats(opts?: BaseOptions) { const { header } = await this.setup(opts) - return rectifyStats(header.totalSummary as UnrectifiedFeatureStats) + return rectifyStats(header.totalSummary as UnrectifiedQuantitativeStats) } public getFeatures(region: Region, opts: WiggleOptions = {}) { @@ -103,7 +106,7 @@ export default class BigWigAdapter extends BaseFeatureDataAdapter { } // always render bigwig instead of calculating a feature density for it - async estimateRegionsStats(_regions: Region[]) { + async getMultiRegionFeatureDensityStats(_regions: Region[]) { return { featureDensity: 0 } } diff --git a/plugins/wiggle/src/LinearWiggleDisplay/models/model.tsx b/plugins/wiggle/src/LinearWiggleDisplay/models/model.tsx index ceea8fdb5a..28be6964e9 100644 --- a/plugins/wiggle/src/LinearWiggleDisplay/models/model.tsx +++ b/plugins/wiggle/src/LinearWiggleDisplay/models/model.tsx @@ -17,15 +17,14 @@ import { LinearGenomeViewModel, } from '@jbrowse/plugin-linear-genome-view' import { when } from 'mobx' -import { isAlive, types, Instance } from 'mobx-state-tree' +import { types, Instance } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' import { axisPropsFromTickScale } from 'react-d3-axis-mod' import { getNiceDomain, getScale, - getStats, - statsAutorun, + quantitativeStatsAutorun, YSCALEBAR_LABEL_OFFSET, } from '../../util' @@ -134,7 +133,7 @@ function stateModelFactory( /** * #action */ - updateStats(stats: { scoreMin: number; scoreMax: number }) { + updateQuantitativeStats(stats: { scoreMin: number; scoreMax: number }) { const { scoreMin, scoreMax } = stats const EPSILON = 0.000001 if (!self.stats) { @@ -677,23 +676,11 @@ function stateModelFactory( */ async reload() { self.setError() - const aborter = new AbortController() - self.setLoading(aborter) - try { - const stats = await getStats(self, { - signal: aborter.signal, - ...self.renderProps(), - }) - if (isAlive(self)) { - self.updateStats(stats) - superReload() - } - } catch (e) { - self.setError(e) - } + superReload() }, + afterAttach() { - statsAutorun(self) + quantitativeStatsAutorun(self) }, /** * #action diff --git a/plugins/wiggle/src/MultiLinearWiggleDisplay/models/model.tsx b/plugins/wiggle/src/MultiLinearWiggleDisplay/models/model.tsx index 914e10b6c7..52ce478198 100644 --- a/plugins/wiggle/src/MultiLinearWiggleDisplay/models/model.tsx +++ b/plugins/wiggle/src/MultiLinearWiggleDisplay/models/model.tsx @@ -30,8 +30,7 @@ import { import { getNiceDomain, getScale, - getStats, - statsAutorun, + quantitativeStatsAutorun, YSCALEBAR_LABEL_OFFSET, } from '../../util' @@ -110,7 +109,7 @@ const stateModelFactory = ( clearLayout() { self.layout = [] }, - updateStats(stats: { scoreMin: number; scoreMax: number }) { + updateQuantitativeStats(stats: { scoreMin: number; scoreMax: number }) { const { scoreMin, scoreMax } = stats const EPSILON = 0.000001 if (!self.stats) { @@ -662,27 +661,12 @@ const stateModelFactory = ( type ExportSvgOpts = Parameters[0] return { - // re-runs stats and refresh whole display on reload async reload() { self.setError() - const aborter = new AbortController() - let stats - try { - self.setLoading(aborter) - stats = await getStats(self, { - signal: aborter.signal, - ...self.renderProps(), - }) - if (isAlive(self)) { - self.updateStats(stats) - superReload() - } - } catch (e) { - self.setError(e) - } + superReload() }, afterAttach() { - statsAutorun(self) + quantitativeStatsAutorun(self) addDisposer( self, autorun(async () => { diff --git a/plugins/wiggle/src/MultiWiggleAdapter/MultiWiggleAdapter.ts b/plugins/wiggle/src/MultiWiggleAdapter/MultiWiggleAdapter.ts index b1d2bc9408..51eb7e9303 100644 --- a/plugins/wiggle/src/MultiWiggleAdapter/MultiWiggleAdapter.ts +++ b/plugins/wiggle/src/MultiWiggleAdapter/MultiWiggleAdapter.ts @@ -106,7 +106,7 @@ export default class MultiWiggleAdapter extends BaseFeatureDataAdapter { } // always render bigwig instead of calculating a feature density for it - async estimateRegionsStats(_regions: Region[]) { + async getMultiRegionFeatureDensityStats(_regions: Region[]) { return { featureDensity: 0 } } diff --git a/plugins/wiggle/src/WiggleRPC/MultiWiggleGetSources.ts b/plugins/wiggle/src/WiggleRPC/MultiWiggleGetSources.ts new file mode 100644 index 0000000000..77355065cc --- /dev/null +++ b/plugins/wiggle/src/WiggleRPC/MultiWiggleGetSources.ts @@ -0,0 +1,68 @@ +import RpcMethodType from '@jbrowse/core/pluggableElementTypes/RpcMethodType' +import SerializableFilterChain from '@jbrowse/core/pluggableElementTypes/renderers/util/serializableFilterChain' +import { getAdapter } from '@jbrowse/core/data_adapters/dataAdapterCache' +import { RenderArgs } from '@jbrowse/core/rpc/coreRpcMethods' +import { renameRegionsIfNeeded, Region } from '@jbrowse/core/util' +import { RemoteAbortSignal } from '@jbrowse/core/rpc/remoteAbortSignals' +import { AnyConfigurationModel } from '@jbrowse/core/configuration' + +export class MultiWiggleGetSources extends RpcMethodType { + name = 'MultiWiggleGetSources' + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async deserializeArguments(args: any, rpcDriverClassName: string) { + const l = await super.deserializeArguments(args, rpcDriverClassName) + return { + ...l, + filters: args.filters + ? new SerializableFilterChain({ + filters: args.filters, + }) + : undefined, + } + } + + async serializeArguments( + args: RenderArgs & { + signal?: AbortSignal + statusCallback?: (arg: string) => void + }, + rpcDriverClassName: string, + ) { + const pm = this.pluginManager + const assemblyManager = pm.rootModel?.session?.assemblyManager + if (!assemblyManager) { + return args + } + + const renamedArgs = await renameRegionsIfNeeded(assemblyManager, { + ...args, + filters: args.filters?.toJSON().filters, + }) + + return super.serializeArguments(renamedArgs, rpcDriverClassName) + } + + async execute( + args: { + adapterConfig: AnyConfigurationModel + signal?: RemoteAbortSignal + sessionId: string + headers?: Record + regions: Region[] + bpPerPx: number + }, + rpcDriverClassName: string, + ) { + const pm = this.pluginManager + const deserializedArgs = await this.deserializeArguments( + args, + rpcDriverClassName, + ) + const { regions, adapterConfig, sessionId } = deserializedArgs + const { dataAdapter } = await getAdapter(pm, sessionId, adapterConfig) + + // @ts-expect-error + return dataAdapter.getSources(regions, deserializedArgs) + } +} diff --git a/plugins/wiggle/src/WiggleRPC/WiggleGetGlobalQuantitativeStats.ts b/plugins/wiggle/src/WiggleRPC/WiggleGetGlobalQuantitativeStats.ts new file mode 100644 index 0000000000..f12161dcb4 --- /dev/null +++ b/plugins/wiggle/src/WiggleRPC/WiggleGetGlobalQuantitativeStats.ts @@ -0,0 +1,44 @@ +import RpcMethodType from '@jbrowse/core/pluggableElementTypes/RpcMethodType' +import SerializableFilterChain from '@jbrowse/core/pluggableElementTypes/renderers/util/serializableFilterChain' +import { getAdapter } from '@jbrowse/core/data_adapters/dataAdapterCache' +import { RemoteAbortSignal } from '@jbrowse/core/rpc/remoteAbortSignals' +import { QuantitativeStats } from '@jbrowse/core/util/stats' +import { AnyConfigurationModel } from '@jbrowse/core/configuration' + +export class WiggleGetGlobalQuantitativeStats extends RpcMethodType { + name = 'WiggleGetGlobalQuantitativeStats' + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async deserializeArguments(args: any, rpcDriverClassName: string) { + const l = await super.deserializeArguments(args, rpcDriverClassName) + return { + ...l, + filters: args.filters + ? new SerializableFilterChain({ + filters: args.filters, + }) + : undefined, + } + } + + async execute( + args: { + adapterConfig: AnyConfigurationModel + signal?: RemoteAbortSignal + headers?: Record + sessionId: string + }, + rpcDriverClassName: string, + ): Promise { + const pm = this.pluginManager + const deserializedArgs = await this.deserializeArguments( + args, + rpcDriverClassName, + ) + const { adapterConfig, sessionId } = deserializedArgs + const { dataAdapter } = await getAdapter(pm, sessionId, adapterConfig) + + // @ts-expect-error + return dataAdapter.getGlobalStats(deserializedArgs) + } +} diff --git a/plugins/wiggle/src/WiggleRPC/WiggleGetMultiRegionQuantitativeStats.ts b/plugins/wiggle/src/WiggleRPC/WiggleGetMultiRegionQuantitativeStats.ts new file mode 100644 index 0000000000..c90a8f7ace --- /dev/null +++ b/plugins/wiggle/src/WiggleRPC/WiggleGetMultiRegionQuantitativeStats.ts @@ -0,0 +1,73 @@ +import RpcMethodType from '@jbrowse/core/pluggableElementTypes/RpcMethodType' +import SerializableFilterChain from '@jbrowse/core/pluggableElementTypes/renderers/util/serializableFilterChain' +import { RenderArgs } from '@jbrowse/core/rpc/coreRpcMethods' +import { getAdapter } from '@jbrowse/core/data_adapters/dataAdapterCache' +import { RemoteAbortSignal } from '@jbrowse/core/rpc/remoteAbortSignals' +import { Region, renameRegionsIfNeeded } from '@jbrowse/core/util' +import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter' + +export class WiggleGetMultiRegionQuantitativeStats extends RpcMethodType { + name = 'WiggleGetMultiRegionQuantitativeStats' + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async deserializeArguments(args: any, rpcDriverClassName: string) { + const l = await super.deserializeArguments(args, rpcDriverClassName) + return { + ...l, + filters: args.filters + ? new SerializableFilterChain({ + filters: args.filters, + }) + : undefined, + } + } + + async serializeArguments( + args: RenderArgs & { + signal?: AbortSignal + statusCallback?: (arg: string) => void + }, + rpcDriverClassName: string, + ) { + const pm = this.pluginManager + const assemblyManager = pm.rootModel?.session?.assemblyManager + if (!assemblyManager) { + return args + } + + const renamedArgs = await renameRegionsIfNeeded(assemblyManager, { + ...args, + filters: args.filters?.toJSON().filters, + }) + + return super.serializeArguments(renamedArgs, rpcDriverClassName) + } + + async execute( + args: { + adapterConfig: {} + signal?: RemoteAbortSignal + sessionId: string + headers?: Record + regions: Region[] + bpPerPx: number + }, + rpcDriverClassName: string, + ) { + const pm = this.pluginManager + const deserializedArgs = await this.deserializeArguments( + args, + rpcDriverClassName, + ) + const { regions, adapterConfig, sessionId } = deserializedArgs + const { dataAdapter } = await getAdapter(pm, sessionId, adapterConfig) + + if (dataAdapter instanceof BaseFeatureDataAdapter) { + return dataAdapter.getMultiRegionQuantitativeStats( + regions, + deserializedArgs, + ) + } + throw new Error('Data adapter not found') + } +} diff --git a/plugins/wiggle/src/WiggleRPC/rpcMethods.ts b/plugins/wiggle/src/WiggleRPC/rpcMethods.ts index fda05aa760..0d0e364a58 100644 --- a/plugins/wiggle/src/WiggleRPC/rpcMethods.ts +++ b/plugins/wiggle/src/WiggleRPC/rpcMethods.ts @@ -1,170 +1,3 @@ -import RpcMethodType from '@jbrowse/core/pluggableElementTypes/RpcMethodType' -import SerializableFilterChain from '@jbrowse/core/pluggableElementTypes/renderers/util/serializableFilterChain' -import { getAdapter } from '@jbrowse/core/data_adapters/dataAdapterCache' -import { RenderArgs } from '@jbrowse/core/rpc/coreRpcMethods' -import { renameRegionsIfNeeded, Region } from '@jbrowse/core/util' -import { RemoteAbortSignal } from '@jbrowse/core/rpc/remoteAbortSignals' -import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter' -import { FeatureStats } from '@jbrowse/core/util/stats' - -export class WiggleGetGlobalStats extends RpcMethodType { - name = 'WiggleGetGlobalStats' - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async deserializeArguments(args: any, rpcDriverClassName: string) { - const l = await super.deserializeArguments(args, rpcDriverClassName) - return { - ...l, - filters: args.filters - ? new SerializableFilterChain({ - filters: args.filters, - }) - : undefined, - } - } - - async execute( - args: { - adapterConfig: {} - signal?: RemoteAbortSignal - headers?: Record - sessionId: string - }, - rpcDriverClassName: string, - ): Promise { - const pm = this.pluginManager - const deserializedArgs = await this.deserializeArguments( - args, - rpcDriverClassName, - ) - const { adapterConfig, sessionId } = deserializedArgs - const { dataAdapter } = await getAdapter(pm, sessionId, adapterConfig) - - // @ts-expect-error - return dataAdapter.getGlobalStats(deserializedArgs) - } -} - -export class WiggleGetMultiRegionStats extends RpcMethodType { - name = 'WiggleGetMultiRegionStats' - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async deserializeArguments(args: any, rpcDriverClassName: string) { - const l = await super.deserializeArguments(args, rpcDriverClassName) - return { - ...l, - filters: args.filters - ? new SerializableFilterChain({ - filters: args.filters, - }) - : undefined, - } - } - - async serializeArguments( - args: RenderArgs & { - signal?: AbortSignal - statusCallback?: (arg: string) => void - }, - rpcDriverClassName: string, - ) { - const pm = this.pluginManager - const assemblyManager = pm.rootModel?.session?.assemblyManager - if (!assemblyManager) { - return args - } - - const renamedArgs = await renameRegionsIfNeeded(assemblyManager, { - ...args, - filters: args.filters?.toJSON().filters, - }) - - return super.serializeArguments(renamedArgs, rpcDriverClassName) - } - - async execute( - args: { - adapterConfig: {} - signal?: RemoteAbortSignal - sessionId: string - headers?: Record - regions: Region[] - bpPerPx: number - }, - rpcDriverClassName: string, - ) { - const pm = this.pluginManager - const deserializedArgs = await this.deserializeArguments( - args, - rpcDriverClassName, - ) - const { regions, adapterConfig, sessionId } = deserializedArgs - const { dataAdapter } = await getAdapter(pm, sessionId, adapterConfig) - - if (dataAdapter instanceof BaseFeatureDataAdapter) { - return dataAdapter.getMultiRegionStats(regions, deserializedArgs) - } - throw new Error('Data adapter not found') - } -} - -export class MultiWiggleGetSources extends RpcMethodType { - name = 'MultiWiggleGetSources' - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async deserializeArguments(args: any, rpcDriverClassName: string) { - const l = await super.deserializeArguments(args, rpcDriverClassName) - return { - ...l, - filters: args.filters - ? new SerializableFilterChain({ - filters: args.filters, - }) - : undefined, - } - } - - async serializeArguments( - args: RenderArgs & { - signal?: AbortSignal - statusCallback?: (arg: string) => void - }, - rpcDriverClassName: string, - ) { - const pm = this.pluginManager - const assemblyManager = pm.rootModel?.session?.assemblyManager - if (!assemblyManager) { - return args - } - - const renamedArgs = await renameRegionsIfNeeded(assemblyManager, { - ...args, - filters: args.filters?.toJSON().filters, - }) - - return super.serializeArguments(renamedArgs, rpcDriverClassName) - } - - async execute( - args: { - adapterConfig: {} - signal?: RemoteAbortSignal - sessionId: string - headers?: Record - regions: Region[] - bpPerPx: number - }, - rpcDriverClassName: string, - ) { - const pm = this.pluginManager - const deserializedArgs = await this.deserializeArguments( - args, - rpcDriverClassName, - ) - const { regions, adapterConfig, sessionId } = deserializedArgs - const { dataAdapter } = await getAdapter(pm, sessionId, adapterConfig) - - // @ts-expect-error - return dataAdapter.getSources(regions, deserializedArgs) - } -} +export * from './MultiWiggleGetSources' +export * from './WiggleGetMultiRegionQuantitativeStats' +export * from './WiggleGetGlobalQuantitativeStats' diff --git a/plugins/wiggle/src/index.ts b/plugins/wiggle/src/index.ts index 8ba2a85c15..c5092449bb 100644 --- a/plugins/wiggle/src/index.ts +++ b/plugins/wiggle/src/index.ts @@ -30,8 +30,8 @@ import MultiWiggleAddTrackWidgetF from './MultiWiggleAddTrackWidget' import * as utils from './util' import { - WiggleGetGlobalStats, - WiggleGetMultiRegionStats, + WiggleGetGlobalQuantitativeStats, + WiggleGetMultiRegionQuantitativeStats, MultiWiggleGetSources, } from './WiggleRPC/rpcMethods' @@ -101,8 +101,8 @@ export default class WigglePlugin extends Plugin { }, ) - pm.addRpcMethod(() => new WiggleGetGlobalStats(pm)) - pm.addRpcMethod(() => new WiggleGetMultiRegionStats(pm)) + pm.addRpcMethod(() => new WiggleGetGlobalQuantitativeStats(pm)) + pm.addRpcMethod(() => new WiggleGetMultiRegionQuantitativeStats(pm)) pm.addRpcMethod(() => new MultiWiggleGetSources(pm)) } diff --git a/plugins/wiggle/src/util.ts b/plugins/wiggle/src/util.ts index e1606a1747..25e07d5c3f 100644 --- a/plugins/wiggle/src/util.ts +++ b/plugins/wiggle/src/util.ts @@ -5,7 +5,7 @@ import { getSession, getContainingView, } from '@jbrowse/core/util' -import { FeatureStats } from '@jbrowse/core/util/stats' +import { QuantitativeStats } from '@jbrowse/core/util/stats' import { getRpcSessionId } from '@jbrowse/core/util/tracks' import { addDisposer, isAlive } from 'mobx-state-tree' @@ -176,7 +176,7 @@ export function groupBy(array: T[], predicate: (v: T) => string) { return result } -export async function getStats( +export async function getQuantitativeStats( self: { adapterConfig: AnyConfigurationModel autoscaleType: string @@ -187,7 +187,7 @@ export async function getStats( signal?: AbortSignal filters?: string[] }, -): Promise { +): Promise { const { rpcManager } = getSession(self) const nd = getConf(self, 'numStdDev') || 3 const { adapterConfig, autoscaleType } = self @@ -204,11 +204,11 @@ export async function getStats( } if (autoscaleType === 'global' || autoscaleType === 'globalsd') { - const results: FeatureStats = (await rpcManager.call( + const results: QuantitativeStats = (await rpcManager.call( sessionId, - 'WiggleGetGlobalStats', + 'WiggleGetGlobalQuantitativeStats', params, - )) as FeatureStats + )) as QuantitativeStats const { scoreMin, scoreMean, scoreStdDev } = results // globalsd uses heuristic to avoid unnecessary scoreMin<0 // if the scoreMin is never less than 0 @@ -225,7 +225,7 @@ export async function getStats( const { dynamicBlocks, bpPerPx } = getContainingView(self) as LGV const results = (await rpcManager.call( sessionId, - 'WiggleGetMultiRegionStats', + 'WiggleGetMultiRegionQuantitativeStats', { ...params, regions: dynamicBlocks.contentBlocks.map(region => { @@ -238,7 +238,7 @@ export async function getStats( }), bpPerPx, }, - )) as FeatureStats + )) as QuantitativeStats const { scoreMin, scoreMean, scoreStdDev } = results // localsd uses heuristic to avoid unnecessary scoreMin<0 if the @@ -255,19 +255,23 @@ export async function getStats( if (autoscaleType === 'zscale') { return rpcManager.call( sessionId, - 'WiggleGetGlobalStats', + 'WiggleGetGlobalQuantitativeStats', params, - ) as Promise + ) as Promise } throw new Error(`invalid autoscaleType '${autoscaleType}'`) } -export function statsAutorun(self: { - estimatedStatsReady: boolean +export function quantitativeStatsAutorun(self: { + featureDensityStatsReady: boolean regionTooLarge: boolean + error: unknown setLoading: (aborter: AbortController) => void setError: (error: unknown) => void - updateStats: (stats: FeatureStats, statsRegion: string) => void + updateQuantitativeStats: ( + stats: QuantitativeStats, + statsRegion: string, + ) => void renderProps: () => Record adapterConfig: AnyConfigurationModel autoscaleType: string @@ -284,20 +288,21 @@ export function statsAutorun(self: { if ( !view.initialized || - !self.estimatedStatsReady || - self.regionTooLarge + !self.featureDensityStatsReady || + self.regionTooLarge || + self.error ) { return } const statsRegion = JSON.stringify(view.dynamicBlocks) - const wiggleStats = await getStats(self, { + const wiggleStats = await getQuantitativeStats(self, { signal: aborter.signal, ...self.renderProps(), }) if (isAlive(self)) { - self.updateStats(wiggleStats, statsRegion) + self.updateQuantitativeStats(wiggleStats, statsRegion) } } catch (e) { if (!isAbortException(e) && isAlive(self)) { diff --git a/products/jbrowse-img/src/renderRegion.tsx b/products/jbrowse-img/src/renderRegion.tsx index 39a30dde73..43cebc3288 100644 --- a/products/jbrowse-img/src/renderRegion.tsx +++ b/products/jbrowse-img/src/renderRegion.tsx @@ -367,7 +367,7 @@ function process( else if (opt.startsWith('force:')) { const [, force] = opt.split(':') if (force) { - display.updateStatsLimit({ bytes: Number.MAX_VALUE }) + display.setFeatureDensityStatsLimit({ bytes: Number.MAX_VALUE }) } } diff --git a/website/docs/developer_guides/pluggable_elements.md b/website/docs/developer_guides/pluggable_elements.md index 1fc0319a1d..5de09427e4 100644 --- a/website/docs/developer_guides/pluggable_elements.md +++ b/website/docs/developer_guides/pluggable_elements.md @@ -203,8 +203,7 @@ custom behaviors to a web-worker or server side process. The wiggle plugin, for example, registers two custom RPC method types: -- `WiggleGetGlobalStats` -- `WiggleGetMultiRegionStats` +- `WiggleGetMultiRegionQuantitativeStats` These methods can run in the webworker when available. diff --git a/website/docs/models/BaseLinearDisplay.md b/website/docs/models/BaseLinearDisplay.md index b1aadc8592..c3ae59e654 100644 --- a/website/docs/models/BaseLinearDisplay.md +++ b/website/docs/models/BaseLinearDisplay.md @@ -137,7 +137,7 @@ any ```js // type -;(blockKey: string, x: number, y: number) => any +;(blockKey: string, x: number, y: number) => string ``` #### getter: getFeatureByID @@ -175,7 +175,7 @@ number any ``` -#### getter: estimatedStatsReady +#### getter: featureDensityStatsReady ```js // type @@ -241,7 +241,7 @@ trackMenuItems: () => MenuItem[] ```js // type signature -contextMenuItems: () => { label: string; icon: OverridableComponent> & { muiName: string; }; onClick: () => void; }[] +contextMenuItems: () => MenuItem[] ``` #### method: renderProps @@ -267,32 +267,32 @@ renderSvg: (opts: ExportSvgOptions & { overrideHeight: number; theme: ThemeOptio setMessage: (message: string) => void ``` -#### action: estimateRegionsStats +#### action: getFeatureDensityStats ```js // type signature -estimateRegionsStats: (regions: Region[], opts: { headers?: Record; signal?: AbortSignal; filters?: string[]; }) => Promise<{}> +getFeatureDensityStats: () => Promise ``` -#### action: setRegionStatsP +#### action: setFeatureDensityStatsP ```js // type signature -setRegionStatsP: (p?: Promise) => void +setFeatureDensityStatsP: (arg: any) => void ``` -#### action: setRegionStats +#### action: setFeatureDensityStats ```js // type signature -setRegionStats: (estimatedRegionStats?: Stats) => void +setFeatureDensityStats: (featureDensityStats?: FeatureDensityStats) => void ``` -#### action: clearRegionStats +#### action: clearRegionsStats ```js // type signature -clearRegionStats: () => void +clearRegionsStats: () => void ``` #### action: setHeight @@ -316,11 +316,11 @@ resizeHeight: (distance: number) => number setScrollTop: (scrollTop: number) => void ``` -#### action: updateStatsLimit +#### action: setFeatureDensityStatsLimit ```js // type signature -updateStatsLimit: (stats: Stats) => void +setFeatureDensityStatsLimit: (stats?: FeatureDensityStats) => void ``` #### action: addBlock @@ -362,7 +362,7 @@ clearFeatureSelection: () => void ```js // type signature -setFeatureIdUnderMouse: (feature: string) => void +setFeatureIdUnderMouse: (feature?: string) => void ``` #### action: reload diff --git a/website/docs/models/LinearAlignmentsDisplay.md b/website/docs/models/LinearAlignmentsDisplay.md index 0772d25836..86bd637b43 100644 --- a/website/docs/models/LinearAlignmentsDisplay.md +++ b/website/docs/models/LinearAlignmentsDisplay.md @@ -187,11 +187,11 @@ setSNPCoverageHeight: (n: number) => void setSNPCoverageDisplay: (configuration: { [x: string]: any; } & NonEmptyObject & { setSubschema(slotName: string, data: unknown): any; } & IStateTreeNode) => void ``` -#### action: updateStatsLimit +#### action: setFeatureDensityStatsLimit ```js // type signature -updateStatsLimit: (stats: unknown) => void +setFeatureDensityStatsLimit: (stats?: FeatureDensityStats) => void ``` #### action: setPileupDisplay diff --git a/website/docs/models/LinearBasicDisplay.md b/website/docs/models/LinearBasicDisplay.md index 141aa3a5bc..bd34068e89 100644 --- a/website/docs/models/LinearBasicDisplay.md +++ b/website/docs/models/LinearBasicDisplay.md @@ -161,5 +161,5 @@ setDisplayMode: (val: string) => void ```js // type signature -setMaxHeight: (val: number) => void +setMaxHeight: (val?: number) => void ``` diff --git a/website/docs/models/LinearWiggleDisplay.md b/website/docs/models/LinearWiggleDisplay.md index d17f943bb3..af5530f696 100644 --- a/website/docs/models/LinearWiggleDisplay.md +++ b/website/docs/models/LinearWiggleDisplay.md @@ -345,11 +345,11 @@ trackMenuItems: () => (MenuDivider | MenuSubHeader | NormalMenuItem | CheckboxMe ### LinearWiggleDisplay - Actions -#### action: updateStats +#### action: updateQuantitativeStats ```js // type signature -updateStats: (stats: { scoreMin: number; scoreMax: number; }) => void +updateQuantitativeStats: (stats: { scoreMin: number; scoreMax: number; }) => void ``` #### action: setColor