diff --git a/examples/interactive-filtering.html b/examples/interactive-filtering.html new file mode 100644 index 000000000..90669b4b7 --- /dev/null +++ b/examples/interactive-filtering.html @@ -0,0 +1,1514 @@ + + + + + + Interactive filtering in igv.js + + + + + + + + + + + +

Interactive filtering in IGV

+
+
+ + +
+ + + + + + diff --git a/js/feature/featureTrack.js b/js/feature/featureTrack.js index dca3f4634..b30dc04f0 100755 --- a/js/feature/featureTrack.js +++ b/js/feature/featureTrack.js @@ -6,11 +6,12 @@ import {reverseComplementSequence} from "../util/sequenceUtils.js" import {aminoAcidSequenceRenderThreshold, renderFeature} from "./render/renderFeature.js" import {renderSnp} from "./render/renderSnp.js" import {renderFusionJuncSpan} from "./render/renderFusionJunction.js" -import {StringUtils} from "../../node_modules/igv-utils/src/index.js" +import {StringUtils, FeatureUtils} from "../../node_modules/igv-utils/src/index.js" import {ColorTable, PaletteColorTable} from "../util/colorPalletes.js" -import {isSecureContext, expandRegion} from "../util/igvUtils.js" +import {isSecureContext} from "../util/igvUtils.js" import {IGVColor} from "../../node_modules/igv-utils/src/index.js" + class FeatureTrack extends TrackBase { static defaultColor = 'rgb(0,0,150)' @@ -104,6 +105,30 @@ class FeatureTrack extends TrackBase { } + set filter(f) { + this._filter = f + this._repackCachedFeatures() + this.trackView.repaintViews() + } + + + inViewFeatures() { + const inViewFeatures = [] + for (let viewport of this.trackView.viewports) { + if (viewport.isVisible() && viewport.featureCache) { + const referenceFrame = viewport.referenceFrame + const start = referenceFrame.start + const end = start + referenceFrame.toBP(viewport.getWidth()) + const viewFeatures = FeatureUtils.findOverlapping(viewport.featureCache.features, start, end) + for (let f of viewFeatures) { + inViewFeatures.push(f) + } + } + } + return inViewFeatures + } + + /** * Return true if this track can be searched for genome location by feature property. * This track is searchable if its featureSource is searchable. @@ -208,6 +233,7 @@ class FeatureTrack extends TrackBase { options.rowLastX = [] options.rowLastLabelX = [] for (let feature of features) { + if (this._filter && !this._filter(feature)) continue if (feature.start > bpStart && feature.end < bpEnd) { const row = this.displayMode === "COLLAPSED" ? 0 : feature.row || 0 if (!rowFeatureCount[row]) { @@ -225,6 +251,8 @@ class FeatureTrack extends TrackBase { let lastPxEnd = [] const selectedFeatures = [] for (let feature of features) { + + if (this._filter && !this._filter(feature)) continue if (feature.end < bpStart) continue if (feature.start > bpEnd) break diff --git a/js/feature/featureUtils.js b/js/feature/featureUtils.js index 155fb0290..04dfbed21 100644 --- a/js/feature/featureUtils.js +++ b/js/feature/featureUtils.js @@ -71,7 +71,14 @@ async function computeWGFeatures(allFeatures, genome, maxWGCount) { return wgFeatures } -function packFeatures(features, maxRows) { +/** + * Assigns a row to each feature such that features do not overlap. + * + * @param features + * @param maxRows + * @param filter Function thta takes a feature and returns a boolean indicating visibility + */ +function packFeatures(features, maxRows, filter) { maxRows = maxRows || 1000 if (features == null || features.length === 0) { @@ -81,14 +88,18 @@ function packFeatures(features, maxRows) { const chrFeatureMap = {} const chrs = [] for (let feature of features) { - const chr = feature.chr - let flist = chrFeatureMap[chr] - if (!flist) { - flist = [] - chrFeatureMap[chr] = flist - chrs.push(chr) + if(filter && !filter(feature)) { + feature.row = undefined; + } else { + const chr = feature.chr + let flist = chrFeatureMap[chr] + if (!flist) { + flist = [] + chrFeatureMap[chr] = flist + chrs.push(chr) + } + flist.push(feature) } - flist.push(feature) } // Loop through chrosomosomes and pack features; diff --git a/js/trackViewport.js b/js/trackViewport.js index fe1a52533..a2a4061d2 100644 --- a/js/trackViewport.js +++ b/js/trackViewport.js @@ -260,6 +260,11 @@ class TrackViewport extends Viewport { this.loading = false this.hideMessage() this.stopSpinner() + + // Notify listeners, like any interactive filtering handlers, + // that data is ready for this track. + this.browser.fireEvent('featuresloaded', [this]) + return this.featureCache } } catch (error) { diff --git a/js/variant/variant.js b/js/variant/variant.js index f47efbccf..f20ade298 100644 --- a/js/variant/variant.js +++ b/js/variant/variant.js @@ -55,6 +55,7 @@ class Variant { this.init() } + getAttributeValue(key) { if (STANDARD_FIELDS.has(key)) { key = STANDARD_FIELDS.get(key) diff --git a/js/variant/variantTrack.js b/js/variant/variantTrack.js index 1cec00c50..0823ab43e 100644 --- a/js/variant/variantTrack.js +++ b/js/variant/variantTrack.js @@ -31,9 +31,10 @@ import {createCheckbox} from "../igv-icons.js" import {ColorTable, PaletteColorTable} from "../util/colorPalletes.js" import SampleInfo from "../sample/sampleInfo.js" import {makeVCFChords, sendChords} from "../jbrowse/circularViewUtils.js" -import {FileUtils, StringUtils, IGVColor} from "../../node_modules/igv-utils/src/index.js" +import {FileUtils, StringUtils, IGVColor, FeatureUtils} from "../../node_modules/igv-utils/src/index.js" import CNVPytorTrack from "../cnvpytor/cnvpytorTrack.js" import {doSortByAttributes} from "../sample/sampleUtils.js" +import {packFeatures} from "../feature/featureUtils.js" import {createElementWithString} from "../ui/utils/dom-utils.js" const isString = StringUtils.isString @@ -127,7 +128,7 @@ class VariantTrack extends TrackBase { this.header = await this.getHeader() // Set colorBy, if not explicitly set default to allele frequency, if available, otherwise default to none (undefined) - if(this.header.INFO) { + if (this.header.INFO) { const infoFields = new Set(Object.keys(this.header.INFO)) if (this.config.colorBy) { this.colorBy = this.config.colorBy @@ -303,6 +304,8 @@ class VariantTrack extends TrackBase { // Loop through variants. A variant == a row in a VCF file for (let v of features) { + + if (this._filter && !this._filter(v)) continue if (v.end < bpStart) continue if (v.start > bpEnd) break @@ -984,8 +987,56 @@ class VariantTrack extends TrackBase { this.trackView.stopSpinner() } }, 100) + } + + // Methods to support filtering api + set filter(f) { + this._filter = f + // TODO - repack? Repacking will cause features to move vertically, which might be unexpected + //this._repackCachedFeatures() + this.trackView.repaintViews() + } + + getInViewFeatures() { + const inViewFeatures = [] + for (let viewport of this.trackView.viewports) { + if (viewport.isVisible()) { + const referenceFrame = viewport.referenceFrame + const chr = referenceFrame.chr + const start = referenceFrame.start + const end = start + referenceFrame.toBP(viewport.getWidth()) + + // We use the cached features to avoid async load. If the + // feature is not already loaded it is by definition not in view. + if (viewport.cachedFeatures) { + const viewFeatures = FeatureUtils.findOverlapping(viewport.cachedFeatures, start, end) + for (let f of viewFeatures) { + if(!this._filter || this._filter(f)) { + inViewFeatures.push(f) + } + } + } + } + } + return inViewFeatures + } + + getFilterableAttributes() { + return this.header.INFO + } + /** + * Repack cached features, if any, for all viewports on this track + */ + _repackCachedFeatures() { + for (let viewport of this.trackView.viewports) { + if (viewport.cachedFeatures) { + const maxRows = this.config.maxRows || Number.MAX_SAFE_INTEGER + packFeatures(viewport.cachedFeatures, maxRows, this._filter) + } + } } + }