From 1cbd158f61b16e583d8f625b9961570602b77668 Mon Sep 17 00:00:00 2001 From: Peter Kerpedjiev Date: Tue, 19 Nov 2024 22:34:59 -0800 Subject: [PATCH 1/4] Working on rendering as pairs --- package.json | 2 +- src/PileupTrack.js | 110 +++++++------ src/bam-fetcher-worker.js | 324 +++++++++++++++++++++++--------------- src/bam-utils.js | 5 +- src/index.html | 209 ++++++------------------ 5 files changed, 315 insertions(+), 335 deletions(-) diff --git a/package.json b/package.json index bc9a5a2..ea46ae1 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "scripts": { "build-es": "rm -rf ./es/* && npx babel ./src/ --out-dir ./es/ --env-name esm", "build": "npm run build-es && webpack --mode production", - "start": "webpack serve --mode development -c webpack.config.js", + "start": "webpack serve --mode development --port 8087 -c webpack.config.js", "prerelease": "rm -rf dist/*; npm run build; zip -r dist.zip dist" }, "dependencies": { diff --git a/src/PileupTrack.js b/src/PileupTrack.js index 9363f66..210a388 100644 --- a/src/PileupTrack.js +++ b/src/PileupTrack.js @@ -1,6 +1,11 @@ import BAMDataFetcher from './bam-fetcher'; import { spawn, BlobWorker } from 'threads'; -import { PILEUP_COLORS, cigarTypeToText, areMatesRequired, calculateInsertSize } from './bam-utils'; +import { + PILEUP_COLORS, + cigarTypeToText, + areMatesRequired, + calculateInsertSize, +} from './bam-utils'; import MyWorkerWeb from 'raw-loader!../dist/worker.js'; @@ -206,13 +211,11 @@ const PileupTrack = (HGC, ...args) => { this.pLabel.addChild(this.loadingText); this.externalInit(options); - } - // Some of the initialization code is factored out, so that we can + // Some of the initialization code is factored out, so that we can // reset/reinitialize if an option change requires it - externalInit(options){ - + externalInit(options) { // we scale the entire view up until a certain point // at which point we redraw everything to get rid of // artifacts @@ -236,7 +239,6 @@ const PileupTrack = (HGC, ...args) => { // graphics for highliting reads under the cursor this.mouseOverGraphics = new HGC.libraries.PIXI.Graphics(); - this.fetching = new Set(); this.rendering = new Set(); @@ -250,7 +252,6 @@ const PileupTrack = (HGC, ...args) => { } this.setUpShaderAndTextures(); - } initTile() {} @@ -274,7 +275,11 @@ const PileupTrack = (HGC, ...args) => { setUpShaderAndTextures() { const colorDict = PILEUP_COLORS; - if (this.options && this.options.colorScale && this.options.colorScale.length == 6) { + if ( + this.options && + this.options.colorScale && + this.options.colorScale.length == 6 + ) { [ colorDict.A, colorDict.T, @@ -283,8 +288,11 @@ const PileupTrack = (HGC, ...args) => { colorDict.N, colorDict.X, ] = this.options.colorScale.map((x) => this.colorToArray(x)); - } - else if (this.options && this.options.colorScale && this.options.colorScale.length == 11) { + } else if ( + this.options && + this.options.colorScale && + this.options.colorScale.length == 11 + ) { [ colorDict.A, colorDict.T, @@ -298,8 +306,10 @@ const PileupTrack = (HGC, ...args) => { colorDict.RR, colorDict.RL, ] = this.options.colorScale.map((x) => this.colorToArray(x)); - } else if(this.options && this.options.colorScale){ - console.error("colorScale must contain 6 or 11 entries. See https://github.com/higlass/higlass-pileup#options.") + } else if (this.options && this.options.colorScale) { + console.error( + 'colorScale must contain 6 or 11 entries. See https://github.com/higlass/higlass-pileup#options.', + ); } if (this.options && this.options.plusStrandColor) { @@ -682,14 +692,21 @@ varying vec4 vColor; } const insertSizeHtml = this.getInsertSizeMouseoverHtml(read); - const chimericReadHtml = read.mate_ids.length > 1 ? `Chimeric alignment
`: ``; + const chimericReadHtml = + read.mate_ids.length > 1 + ? `Chimeric alignment
` + : ``; let mappingOrientationHtml = ``; if (read.mappingOrientation) { let style = ``; if (read.colorOverride) { - const color = Object.keys(PILEUP_COLORS)[read.colorOverride]; - const htmlColor = this.colorArrayToString(PILEUP_COLORS[color]); + const color = Object.keys(PILEUP_COLORS)[ + read.colorOverride + ]; + const htmlColor = this.colorArrayToString( + PILEUP_COLORS[color], + ); style = `style="color:${htmlColor};"`; } mappingOrientationHtml = ` Mapping orientation: ${read.mappingOrientation}
`; @@ -763,13 +780,11 @@ varying vec4 vColor; return ''; } - getInsertSizeMouseoverHtml(read){ + getInsertSizeMouseoverHtml(read) { let insertSizeHtml = ``; if ( this.options.highlightReadsBy.includes('insertSize') || - this.options.highlightReadsBy.includes( - 'insertSizeAndPairOrientation', - ) + this.options.highlightReadsBy.includes('insertSizeAndPairOrientation') ) { if ( read.mate_ids.length === 1 && @@ -780,30 +795,32 @@ varying vec4 vColor; const insertSize = calculateInsertSize(read, mate); let style = ``; if ( - ('largeInsertSizeThreshold' in this.options && insertSize > this.options.largeInsertSizeThreshold) || - ('smallInsertSizeThreshold' in this.options && insertSize < this.options.smallInsertSizeThreshold) + ('largeInsertSizeThreshold' in this.options && + insertSize > this.options.largeInsertSizeThreshold) || + ('smallInsertSizeThreshold' in this.options && + insertSize < this.options.smallInsertSizeThreshold) ) { - const color = Object.keys(PILEUP_COLORS)[read.colorOverride || read.color]; + const color = Object.keys(PILEUP_COLORS)[ + read.colorOverride || read.color + ]; const htmlColor = this.colorArrayToString(PILEUP_COLORS[color]); style = `style="color:${htmlColor};"`; - } + } insertSizeHtml = `Insert size: ${insertSize}
`; } } return insertSizeHtml; } - outlineMate(read, yScaleBand){ + outlineMate(read, yScaleBand) { read.mate_ids.forEach((mate_id) => { - if(!this.readsById[mate_id]){ + if (!this.readsById[mate_id]) { return; } const mate = this.readsById[mate_id]; // We assume the mate height is the same, but width might be different - const mate_width = - this._xScale(mate.to) - this._xScale(mate.from); - const mate_height = - yScaleBand.bandwidth() * this.valueScaleTransform.k; + const mate_width = this._xScale(mate.to) - this._xScale(mate.from); + const mate_height = yScaleBand.bandwidth() * this.valueScaleTransform.k; const mate_xPos = this._xScale(mate.from); const mate_yPos = transformY( this.yScaleBands[mate.groupKey](mate.row), @@ -821,7 +838,6 @@ varying vec4 vColor; ); }); this.animate(); - } calculateZoomLevel() { @@ -1080,6 +1096,7 @@ PileupTrack.config = { 'highlightReadsBy', 'smallInsertSizeThreshold', 'largeInsertSizeThreshold', + 'viewAsPairs', // 'minZoom' ], defaultOptions: { @@ -1104,7 +1121,8 @@ PileupTrack.config = { collapseWhenMaxTileWidthReached: false, minMappingQuality: 0, highlightReadsBy: [], - largeInsertSizeThreshold: 1000 + largeInsertSizeThreshold: 1000, + viewAsPairs: false, }, optionsInfo: { outlineReadOnHover: { @@ -1141,28 +1159,19 @@ PileupTrack.config = { name: 'None', }, insertSize: { - value: [ - "insertSize" - ], + value: ['insertSize'], name: 'Insert size', }, pairOrientation: { - value: [ - "pairOrientation" - ], + value: ['pairOrientation'], name: 'Pair orientation', }, insertSizeAndPairOrientation: { - value: [ - "insertSizeAndPairOrientation" - ], + value: ['insertSizeAndPairOrientation'], name: 'Insert size and pair orientation', }, insertSizeOrPairOrientation: { - value: [ - "insertSize", - "pairOrientation" - ], + value: ['insertSize', 'pairOrientation'], name: 'Insert size or pair orientation', }, }, @@ -1209,6 +1218,19 @@ PileupTrack.config = { }, }, }, + viewAsPairs: { + name: 'View as pairs', + inlineOptions: { + yes: { + value: true, + name: 'Yes', + }, + no: { + value: false, + name: 'No', + }, + }, + }, groupBy: { name: 'Group by', inlineOptions: { diff --git a/src/bam-fetcher-worker.js b/src/bam-fetcher-worker.js index c8e336e..54d1176 100644 --- a/src/bam-fetcher-worker.js +++ b/src/bam-fetcher-worker.js @@ -3,7 +3,11 @@ import { scaleLinear, scaleBand } from 'd3-scale'; import { format } from 'd3-format'; import { expose, Transfer } from 'threads/worker'; import { BamFile } from '@gmod/bam'; -import { getSubstitutions, calculateInsertSize, areMatesRequired } from './bam-utils'; +import { + getSubstitutions, + calculateInsertSize, + areMatesRequired, +} from './bam-utils'; import LRU from 'lru-cache'; import { PILEUP_COLOR_IXS } from './bam-utils'; import { parseChromsizesRows, ChromosomeInfo } from './chrominfo-utils'; @@ -111,9 +115,9 @@ const bamRecordToJson = (bamRecord, chrName, chrOffset, trackOptions) => { // We are doing this for row calculation, so that there is no overlap of clipped regions with regular ones segment.substitutions.forEach((sub) => { // left soft clipped region - if((sub.type === "S" || sub.type === "H") && sub.pos < 0){ + if ((sub.type === 'S' || sub.type === 'H') && sub.pos < 0) { fromClippingAdjustment = -sub.length; - }else if((sub.type === "S" || sub.type === "H") && sub.pos > 0){ + } else if ((sub.type === 'S' || sub.type === 'H') && sub.pos > 0) { toClippingAdjustment = sub.length; } }); @@ -125,108 +129,103 @@ const bamRecordToJson = (bamRecord, chrName, chrOffset, trackOptions) => { // This will group the segments by readName and assign mates to reads const findMates = (segments) => { - - const segmentsByReadName = groupBy(segments, "readName"); - - Object.entries(segmentsByReadName).forEach(([readName, segmentGroup]) => - { - if(segmentGroup.length === 2){ - const read = segmentGroup[0]; - const mate = segmentGroup[1]; - read.mate_ids = [mate.id]; - mate.mate_ids = [read.id]; - } - else if(segmentGroup.length > 2){ - // It might be useful to distinguish reads from chimeric alignments in the future, - // e.g., if we want to highlight read orientations of split reads. Not doing this for now. - // See flags here: https://broadinstitute.github.io/picard/explain-flags.html - // var supplementaryAlignmentMask = 1 << 11; - // var firstInPairMask = 1 << 6; - // const isFirstInPair = segment.flags & firstInPairMask; - // const isSupplementaryAlignment = segment.flags & supplementaryAlignmentMask; - - // For simplicity a read will be a mate of every other read in the group. - // it will only be used for the mouseover and it is probably useful, if the whole group is highlighted on hover - const ids = segmentGroup.map((segment) => segment.id); - segmentGroup.forEach((segment) => { - segment.mate_ids = ids; - }); - } + const segmentsByReadName = groupBy(segments, 'readName'); + + Object.entries(segmentsByReadName).forEach(([readName, segmentGroup]) => { + if (segmentGroup.length === 2) { + const read = segmentGroup[0]; + const mate = segmentGroup[1]; + read.mate_ids = [mate.id]; + mate.mate_ids = [read.id]; + + read.mates = [mate]; + mate.mates = [read]; + } else if (segmentGroup.length > 2) { + // It might be useful to distinguish reads from chimeric alignments in the future, + // e.g., if we want to highlight read orientations of split reads. Not doing this for now. + // See flags here: https://broadinstitute.github.io/picard/explain-flags.html + // var supplementaryAlignmentMask = 1 << 11; + // var firstInPairMask = 1 << 6; + // const isFirstInPair = segment.flags & firstInPairMask; + // const isSupplementaryAlignment = segment.flags & supplementaryAlignmentMask; + + // For simplicity a read will be a mate of every other read in the group. + // it will only be used for the mouseover and it is probably useful, if the whole group is highlighted on hover + segmentGroup.forEach((segment) => { + segment.mates = segmentGroup.filter((s) => s != segment); + segment.mate_ids = segment.mates.map((s) => s.id); + }); } - ); + }); - return segmentsByReadName -} + return segmentsByReadName; +}; const prepareHighlightedReads = (segments, trackOptions) => { - - const outlineMateOnHover = trackOptions.outlineMateOnHover; + const outlineMateOnHover = trackOptions.outlineMateOnHover; const highlightIS = trackOptions.highlightReadsBy.includes('insertSize'); const highlightPO = trackOptions.highlightReadsBy.includes('pairOrientation'); - const highlightISandPO = trackOptions.highlightReadsBy.includes('insertSizeAndPairOrientation'); + const highlightISandPO = trackOptions.highlightReadsBy.includes( + 'insertSizeAndPairOrientation', + ); let segmentsByReadName; if (highlightIS || highlightPO || highlightISandPO) { segmentsByReadName = findMates(segments); } else if (outlineMateOnHover) { - findMates(segments); return; } else { return; } - Object.entries(segmentsByReadName).forEach(([readName, segmentGroup]) => - { - // We are only highlighting insert size and pair orientation for normal (non chimeric reads) - if(segmentGroup.length === 2){ - - // Changes to read or mate will change the values in the original segments array (reference) - const read = segmentGroup[0]; - const mate = segmentGroup[1]; - read.colorOverride = null; - mate.colorOverride = null; - const segmentDistance = calculateInsertSize(read, mate); - const hasLargeInsertSize = - trackOptions.largeInsertSizeThreshold && - segmentDistance > trackOptions.largeInsertSizeThreshold; - const hasSmallInsertSize = - trackOptions.smallInsertSizeThreshold && - segmentDistance < trackOptions.smallInsertSizeThreshold; - const hasLLOrientation = read.strand === '+' && mate.strand === '+'; - const hasRROrientation = read.strand === '-' && mate.strand === '-'; - const hasRLOrientation = read.from < mate.from && read.strand === '-'; - - if (highlightIS) { - if (hasLargeInsertSize) { - read.colorOverride = PILEUP_COLOR_IXS.LARGE_INSERT_SIZE; - } else if (hasSmallInsertSize) { - read.colorOverride = PILEUP_COLOR_IXS.SMALL_INSERT_SIZE; - } + Object.entries(segmentsByReadName).forEach(([readName, segmentGroup]) => { + // We are only highlighting insert size and pair orientation for normal (non chimeric reads) + if (segmentGroup.length === 2) { + // Changes to read or mate will change the values in the original segments array (reference) + const read = segmentGroup[0]; + const mate = segmentGroup[1]; + read.colorOverride = null; + mate.colorOverride = null; + const segmentDistance = calculateInsertSize(read, mate); + const hasLargeInsertSize = + trackOptions.largeInsertSizeThreshold && + segmentDistance > trackOptions.largeInsertSizeThreshold; + const hasSmallInsertSize = + trackOptions.smallInsertSizeThreshold && + segmentDistance < trackOptions.smallInsertSizeThreshold; + const hasLLOrientation = read.strand === '+' && mate.strand === '+'; + const hasRROrientation = read.strand === '-' && mate.strand === '-'; + const hasRLOrientation = read.from < mate.from && read.strand === '-'; + + if (highlightIS) { + if (hasLargeInsertSize) { + read.colorOverride = PILEUP_COLOR_IXS.LARGE_INSERT_SIZE; + } else if (hasSmallInsertSize) { + read.colorOverride = PILEUP_COLOR_IXS.SMALL_INSERT_SIZE; } + } - if ( - highlightPO || - (highlightISandPO && (hasLargeInsertSize || hasSmallInsertSize)) - ) { - if (hasLLOrientation) { - read.colorOverride = PILEUP_COLOR_IXS.LL; - read.mappingOrientation = '++'; - } else if (hasRROrientation) { - read.colorOverride = PILEUP_COLOR_IXS.RR; - read.mappingOrientation = '--'; - } else if (hasRLOrientation) { - read.colorOverride = PILEUP_COLOR_IXS.RL; - read.mappingOrientation = '-+'; - } + if ( + highlightPO || + (highlightISandPO && (hasLargeInsertSize || hasSmallInsertSize)) + ) { + if (hasLLOrientation) { + read.colorOverride = PILEUP_COLOR_IXS.LL; + read.mappingOrientation = '++'; + } else if (hasRROrientation) { + read.colorOverride = PILEUP_COLOR_IXS.RR; + read.mappingOrientation = '--'; + } else if (hasRLOrientation) { + read.colorOverride = PILEUP_COLOR_IXS.RL; + read.mappingOrientation = '-+'; } - - mate.colorOverride = read.colorOverride; - mate.mappingOrientation = read.mappingOrientation; } - } - ); + mate.colorOverride = read.colorOverride; + mate.mappingOrientation = read.mappingOrientation; + } + }); }; /** Convert mapped read information returned from a higlass @@ -287,7 +286,6 @@ const bamFiles = {}; const bamHeaders = {}; const dataOptions = {}; - const serverInfos = {}; const MAX_TILES = 20; @@ -325,13 +323,13 @@ const serverInit = (uid, server, tilesetUid, authHeader) => { const DEFAULT_DATA_OPTIONS = { maxTileWidth: 2e5, -} +}; const init = (uid, bamUrl, baiUrl, chromSizesUrl, options, tOptions) => { if (!options) { dataOptions[uid] = DEFAULT_DATA_OPTIONS; } else { - dataOptions[uid] = {...DEFAULT_DATA_OPTIONS, ...options} + dataOptions[uid] = { ...DEFAULT_DATA_OPTIONS, ...options }; } if (!bamFiles[bamUrl]) { @@ -385,7 +383,7 @@ const getCoverage = (uid, segmentList, samplingDistance) => { // getCoverage potentiall get calles before the chromInfos finished loading // Exit the function in this case - if(!chromInfos[chromSizesUrl]){ + if (!chromInfos[chromSizesUrl]) { return { coverage: coverage, maxCoverage: maxCoverage, @@ -398,7 +396,6 @@ const getCoverage = (uid, segmentList, samplingDistance) => { // Find the first position that is in the sampling set const firstFrom = from - (from % samplingDistance) + samplingDistance; for (let i = firstFrom; i < to; i = i + samplingDistance) { - if (!coverage[i]) { coverage[i] = { reads: 0, @@ -410,7 +407,7 @@ const getCoverage = (uid, segmentList, samplingDistance) => { T: 0, N: 0, }, - range: "" // Will be used to show the bounds of this coverage bin when mousing over + range: '', // Will be used to show the bounds of this coverage bin when mousing over }; } coverage[i].reads++; @@ -434,17 +431,15 @@ const getCoverage = (uid, segmentList, samplingDistance) => { } const absToChr = chromInfos[chromSizesUrl].absToChr; - Object.entries(coverage).forEach( - ([pos, entry]) => { - const from = absToChr(pos); - let range = from[0] + ":" + format(',')(from[1]); - if(samplingDistance > 1){ - const to = absToChr(parseInt(pos,10)+samplingDistance-1); - range += "-" + format(',')(to[1]); - } - entry.range = range; - } - ); + Object.entries(coverage).forEach(([pos, entry]) => { + const from = absToChr(pos); + let range = from[0] + ':' + format(',')(from[1]); + if (samplingDistance > 1) { + const to = absToChr(parseInt(pos, 10) + samplingDistance - 1); + range += '-' + format(',')(to[1]); + } + entry.range = range; + }); return { coverage: coverage, @@ -499,7 +494,7 @@ const tilesetInfo = (uid) => { }; const tile = async (uid, z, x) => { - const {maxTileWidth} = dataOptions[uid]; + const { maxTileWidth } = dataOptions[uid]; const { bamUrl, chromSizesUrl } = dataConfs[uid]; const bamFile = bamFiles[bamUrl]; @@ -536,7 +531,6 @@ const tile = async (uid, z, x) => { }; if (maxX > chromEnd) { - // the visible region extends beyond the end of this chromosome // fetch from the start until the end of the chromosome recordPromises.push( @@ -545,11 +539,16 @@ const tile = async (uid, z, x) => { chromName, minX - chromStart, chromEnd - chromStart, - fetchOptions + fetchOptions, ) .then((records) => { const mappedRecords = records.map((rec) => - bamRecordToJson(rec, chromName, cumPositions[i].pos, trackOptions[uid]), + bamRecordToJson( + rec, + chromName, + cumPositions[i].pos, + trackOptions[uid], + ), ); tileValues.set( @@ -570,9 +569,14 @@ const tile = async (uid, z, x) => { .getRecordsForRange(chromName, startPos, endPos, fetchOptions) .then((records) => { const mappedRecords = records.map((rec) => - bamRecordToJson(rec, chromName, cumPositions[i].pos, trackOptions[uid]), + bamRecordToJson( + rec, + chromName, + cumPositions[i].pos, + trackOptions[uid], + ), ); - + tileValues.set( `${uid}.${z}.${x}`, tileValues.get(`${uid}.${z}.${x}`).concat(mappedRecords), @@ -701,10 +705,31 @@ const fetchTilesDebounced = async (uid, tileIds) => { /////////////////////////////////////////////////// // See segmentsToRows concerning the role of occupiedSpaceInRows -function assignSegmentToRow(segment, occupiedSpaceInRows, padding) { - - const segmentFromWithPadding = segment.fromWithClipping - padding; - const segmentToWithPadding = segment.toWithClipping + padding; +function assignSegmentToRow( + segment, + occupiedSpaceInRows, + padding, + viewAsPairs, +) { + // if (segment.mate_ids.length == 1) + + let segmentFromWithPadding = segment.fromWithClipping - padding; + let segmentToWithPadding = segment.toWithClipping + padding; + + if (viewAsPairs) { + for (let i = 0; i < segment.mates.length; i++) { + const mate = segment.mates[i]; + + const mateFromWithPadding = mate.fromWithClipping - padding; + const mateToWithPadding = mate.toWithClipping + padding; + + segmentFromWithPadding = Math.min( + segmentFromWithPadding, + mateFromWithPadding, + ); + segmentToWithPadding = Math.max(segmentToWithPadding, mateToWithPadding); + } + } // no row has been assigned - find a suitable row and update the occupied space if (segment.row === null || segment.row === undefined) { @@ -717,6 +742,12 @@ function assignSegmentToRow(segment, occupiedSpaceInRows, padding) { const rowSpaceTo = occupiedSpaceInRows[i].to; if (segmentToWithPadding < rowSpaceFrom) { segment.row = i; + if (viewAsPairs) { + for (let mate of segment.mates) { + mate.row = i; + } + } + occupiedSpaceInRows[i] = { from: segmentFromWithPadding, to: rowSpaceTo, @@ -724,6 +755,12 @@ function assignSegmentToRow(segment, occupiedSpaceInRows, padding) { return; } else if (segmentFromWithPadding > rowSpaceTo) { segment.row = i; + if (viewAsPairs) { + for (let mate of segment.mates) { + mate.row = i; + } + } + occupiedSpaceInRows[i] = { from: rowSpaceFrom, to: segmentToWithPadding, @@ -733,6 +770,11 @@ function assignSegmentToRow(segment, occupiedSpaceInRows, padding) { } // There is no space in the existing rows, so add a new one. segment.row = occupiedSpaceInRows.length; + if (viewAsPairs) { + for (let mate of segment.mates) { + mate.row = occupiedSpaceInRows.length; + } + } occupiedSpaceInRows.push({ from: segmentFromWithPadding, to: segmentToWithPadding, @@ -757,7 +799,7 @@ function assignSegmentToRow(segment, occupiedSpaceInRows, padding) { } } -function segmentsToRows(segments, optionsIn) { +function segmentsToRows(segments, optionsIn, viewAsPairs) { const { prevRows, padding } = Object.assign( { prevRows: [], padding: 5 }, optionsIn || {}, @@ -778,7 +820,12 @@ function segmentsToRows(segments, optionsIn) { for (let i = 0; i < prevSegments.length; i++) { // prevSegments contains already assigned segments. The function below therefore just // builds the occupiedSpaceInRows array. For this, prevSegments does not need to be sorted - assignSegmentToRow(prevSegments[i], occupiedSpaceInRows, padding); + assignSegmentToRow( + prevSegments[i], + occupiedSpaceInRows, + padding, + viewAsPairs, + ); } const prevSegmentIds = new Set(prevSegments.map((x) => x.id)); @@ -790,28 +837,38 @@ function segmentsToRows(segments, optionsIn) { if (prevSegments.length === 0) { filteredSegments.sort((a, b) => a.fromWithClipping - b.fromWithClipping); filteredSegments.forEach((segment) => { - assignSegmentToRow(segment, occupiedSpaceInRows, padding); + assignSegmentToRow(segment, occupiedSpaceInRows, padding, viewAsPairs); }); newSegments = filteredSegments; } else { // We subdivide the segments into those that are left/right of the existing previous segments // Note that prevSegments is sorted const cutoff = - (prevSegments[0].fromWithClipping + prevSegments[prevSegments.length - 1].to) / 2; - const newSegmentsLeft = filteredSegments.filter((x) => x.fromWithClipping <= cutoff); + (prevSegments[0].fromWithClipping + + prevSegments[prevSegments.length - 1].to) / + 2; + const newSegmentsLeft = filteredSegments.filter( + (x) => x.fromWithClipping <= cutoff, + ); // The sort order for new segments that are appended left is reversed newSegmentsLeft.sort((a, b) => b.fromWithClipping - a.fromWithClipping); newSegmentsLeft.forEach((segment) => { - assignSegmentToRow(segment, occupiedSpaceInRows, padding); + assignSegmentToRow(segment, occupiedSpaceInRows, padding, viewAsPairs); }); - const newSegmentsRight = filteredSegments.filter((x) => x.fromWithClipping > cutoff); + const newSegmentsRight = filteredSegments.filter( + (x) => x.fromWithClipping > cutoff, + ); newSegmentsRight.sort((a, b) => a.fromWithClipping - b.fromWithClipping); newSegmentsRight.forEach((segment) => { assignSegmentToRow(segment, occupiedSpaceInRows, padding); }); - newSegments = newSegmentsLeft.concat(prevSegments, newSegmentsRight); + newSegments = newSegmentsLeft.concat( + prevSegments, + newSegmentsRight, + viewAsPairs, + ); } const outputRows = []; @@ -844,7 +901,6 @@ const renderSegments = ( prevRows, trackOptions, ) => { - const allSegments = {}; let allReadCounts = {}; let coverageSamplingDistance; @@ -863,10 +919,14 @@ const renderSegments = ( let segmentList = Object.values(allSegments); - if(trackOptions.minMappingQuality > 0){ - segmentList = segmentList.filter((s) => s.mapq >= trackOptions.minMappingQuality) + if (trackOptions.minMappingQuality > 0) { + segmentList = segmentList.filter( + (s) => s.mapq >= trackOptions.minMappingQuality, + ); } + if (areMatesRequired(trackOptions)) findMates(segmentList); + prepareHighlightedReads(segmentList, trackOptions); if (areMatesRequired(trackOptions)) { @@ -887,7 +947,7 @@ const renderSegments = ( (segment) => segment.to >= tileMinPos && segment.from <= tileMaxPos, ); } - + let [minPos, maxPos] = [Number.MAX_VALUE, -Number.MAX_VALUE]; for (let i = 0; i < segmentList.length; i++) { @@ -912,9 +972,13 @@ const renderSegments = ( // calculate the the rows of reads for each group for (let key of Object.keys(grouped)) { - const rows = segmentsToRows(grouped[key], { - prevRows: (prevRows[key] && prevRows[key].rows) || [], - }); + const rows = segmentsToRows( + grouped[key], + { + prevRows: (prevRows[key] && prevRows[key].rows) || [], + }, + trackOptions.viewAsPairs, + ); // At this point grouped[key] also contains all the segments (as array), but we only need grouped[key].rows // Therefore we get rid of everything else to save memory and increase performance grouped[key] = {}; @@ -1095,8 +1159,14 @@ const renderSegments = ( xLeft = from; xRight = to; - addRect(xLeft, yTop, xRight - xLeft, height, segment.colorOverride || segment.color); - + addRect( + xLeft, + yTop, + xRight - xLeft, + height, + segment.colorOverride || segment.color, + ); + for (const substitution of segment.substitutions) { xLeft = xScale(segment.from + substitution.pos); const width = Math.max(1, xScale(substitution.length) - xScale(0)); diff --git a/src/bam-utils.js b/src/bam-utils.js index 59a4c0b..d52cb81 100644 --- a/src/bam-utils.js +++ b/src/bam-utils.js @@ -221,14 +221,15 @@ export const getSubstitutions = (segment, seq) => { export const areMatesRequired = (trackOptions) => { return ( trackOptions.highlightReadsBy.length > 0 || - trackOptions.outlineMateOnHover + trackOptions.outlineMateOnHover || + trackOptions.viewAsPairs ); }; /** * Calculates insert size between read segements */ - export const calculateInsertSize = (segment1, segment2) => { +export const calculateInsertSize = (segment1, segment2) => { return segment1.from < segment2.from ? Math.max(0, segment2.from - segment1.to) : Math.max(0, segment1.from - segment2.to); diff --git a/src/index.html b/src/index.html index 6ed726f..83593d2 100644 --- a/src/index.html +++ b/src/index.html @@ -57,193 +57,80 @@