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)
+ }
+ }
}
+
}