diff --git a/src/components/ExampleSelectButtons.js b/src/components/ExampleSelectButtons.js index 456981f6..7f62ea7a 100644 --- a/src/components/ExampleSelectButtons.js +++ b/src/components/ExampleSelectButtons.js @@ -80,6 +80,24 @@ class ExampleSelectButtons extends Component { > Alignments to Reverse Nodes + + ); } diff --git a/src/components/TubeMapContainer.js b/src/components/TubeMapContainer.js index 47fa48ac..31f219e2 100644 --- a/src/components/TubeMapContainer.js +++ b/src/components/TubeMapContainer.js @@ -272,6 +272,32 @@ class TubeMapContainer extends Component { 0, 1 // Examples always have reads as track 1 ); + break; + case dataOriginTypes.EXAMPLE_8: + vg = data.cycleGraph; + nodes = tubeMap.vgExtractNodes(vg); + tracks = tubeMap.vgExtractTracks(vg, 0, 0); // Examples have paths and haplotypes as track 0. + reads = tubeMap.vgExtractReads( + nodes, + tracks, + data.cycleReads, + 0, + 1 // Examples always have reads as track 1 + ); + + break; + case dataOriginTypes.EXAMPLE_9: + vg = data.cycle2Graph; + nodes = tubeMap.vgExtractNodes(vg); + tracks = tubeMap.vgExtractTracks(vg, 0, 0); // Examples have paths and haplotypes as track 0. + reads = tubeMap.vgExtractReads( + nodes, + tracks, + data.cycle2Reads, + 0, + 1 // Examples always have reads as track 1 + ); + break; case dataOriginTypes.NO_DATA: // Leave the data empty. diff --git a/src/enums.js b/src/enums.js index 4ae8f1ff..fdcb2441 100644 --- a/src/enums.js +++ b/src/enums.js @@ -7,5 +7,7 @@ export const dataOriginTypes = { EXAMPLE_5: "example 5", EXAMPLE_6: "example 6", EXAMPLE_7: "example 7", + EXAMPLE_8: "example 8", + EXAMPLE_9: "example 9", NO_DATA: "no data", }; diff --git a/src/util/demo-data.js b/src/util/demo-data.js index 87164aa9..98a4a6a8 100644 --- a/src/util/demo-data.js +++ b/src/util/demo-data.js @@ -112,6 +112,943 @@ export const demoReads = ` {"sequence": "GGTTCCCACTCCATAAGGTAGTTCAGCACCGCCGTGTCCCGGCCGGGTCGCGGGGAGCCCCGGTACATCGCAGTGGGCTACGTGGACGACACGCAGTTCGTGCGGTTCGACAGCGACGCGGCGACTCCGAGGATGTAGCCGCAGGCGCCGTGGTTGGAGCAGGAGGGACCGGAGTATTGGGACCGGAGCACACGGAACATCAGGCCCGCGCACAGACTGACAAGAGTGAACCTGCCCATGCCGCGCCGCTACTACCACCAGAGCTAGGCCGGTGAATGACCCCGGCCTGGGGCGAAGGTCACGACCCCTCCTCATCCCCCACGGACGCCCCGGGTCCCCCCCGCGAGTCTCCGGCTCC", "path": {"mapping": [{"position": {"node_id": "3"}, "edit": [{"from_length": 1, "to_length": 1}], "rank": 2}, {"position": {"node_id": "5"}, "edit": [{"from_length": 16, "to_length": 16}], "rank": 3}, {"position": {"node_id": "6"}, "edit": [{"from_length": 1, "to_length": 1}], "rank": 4}, {"position": {"node_id": "8"}, "edit": [{"from_length": 10, "to_length": 10}], "rank": 5}, {"position": {"node_id": "9"}, "edit": [{"from_length": 29, "to_length": 29}], "rank": 6}, {"position": {"node_id": "10"}, "edit": [{"from_length": 1, "to_length": 1}], "rank": 7}, {"position": {"node_id": "12"}, "edit": [{"from_length": 2, "to_length": 2}], "rank": 8}, {"position": {"node_id": "13"}, "edit": [{"from_length": 32, "to_length": 32}], "rank": 9}, {"position": {"node_id": "14"}, "edit": [{"from_length": 32, "to_length": 32}], "rank": 10}, {"position": {"node_id": "15"}, "edit": [{"from_length": 6, "to_length": 6}], "rank": 11}, {"position": {"node_id": "16"}, "edit": [{"from_length": 1, "to_length": 1}], "rank": 12}, {"position": {"node_id": "18"}, "edit": [{"from_length": 10, "to_length": 10}], "rank": 13}, {"position": {"node_id": "19"}, "edit": [{"from_length": 1, "to_length": 1}], "rank": 14}, {"position": {"node_id": "21"}, "edit": [{"from_length": 3, "to_length": 3}], "rank": 15}, {"position": {"node_id": "23"}, "edit": [{"from_length": 1, "to_length": 1}], "rank": 16}, {"position": {"node_id": "24"}, "edit": [{"from_length": 10, "to_length": 10}], "rank": 17}, {"position": {"node_id": "25"}, "edit": [{"from_length": 5, "to_length": 5}], "rank": 18}, {"position": {"node_id": "26"}, "edit": [{"from_length": 1, "to_length": 1}], "rank": 19}, {"position": {"node_id": "28"}, "edit": [{"from_length": 26, "to_length": 26}], "rank": 20}, {"position": {"node_id": "29"}, "edit": [{"from_length": 2, "to_length": 2}], "rank": 21}, {"position": {"node_id": "30"}, "edit": [{"from_length": 1, "to_length": 1}], "rank": 22}, {"position": {"node_id": "32"}, "edit": [{"from_length": 29, "to_length": 29}], "rank": 23}, {"position": {"node_id": "33"}, "edit": [{"from_length": 32, "to_length": 32}], "rank": 24}, {"position": {"node_id": "34"}, "edit": [{"from_length": 32, "to_length": 32}], "rank": 25}, {"position": {"node_id": "35"}, "edit": [{"from_length": 32, "to_length": 32}], "rank": 26}, {"position": {"node_id": "36"}, "edit": [{"from_length": 8, "to_length": 8}], "rank": 27}, {"position": {"node_id": "38"}, "edit": [{"from_length": 1, "to_length": 1}], "rank": 28}, {"position": {"node_id": "39"}, "edit": [{"from_length": 23, "to_length": 23}], "rank": 29}, {"position": {"node_id": "40"}, "edit": [{"from_length": 7, "to_length": 7}], "rank": 30}]}, "score": 358, "identity": 1.0} `; +export const cycleGraph = { + edge: [ + { + from: "60080783", + from_start: true, + to: "60080786", + to_end: true, + }, + { + from: "60080783", + from_start: true, + to: "60080785", + }, + { + from: "60080783", + to: "60080785", + }, + { + from: "60080786", + to: "60080785", + }, + { + from: "60080786", + to: "60080785", + to_end: true, + }, + ], + node: [ + { + id: "60080785", + sequence: "AC", + }, + { + id: "60080783", + sequence: "TT", + }, + { + id: "60080786", + sequence: "GT", + }, + ], + path: [ + { + mapping: [ + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080786", + }, + rank: "1", + }, + { + edit: [ + { + from_length: 21, + to_length: 21, + }, + ], + position: { + node_id: "60080783", + }, + rank: "2", + }, + ], + name: "GRCh38.chr14", + indexOfFirstBase: "0", + }, + ], +}; + + +export const cycleReads = [ + { + identity: 1, + name: "Read0", + path: { + mapping: [ + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080786", + }, + rank: "1", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "2", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080783", + }, + rank: "3", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080786", + }, + rank: "4", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "5", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080783", + }, + rank: "6", + }, + ], + }, + score: 110, + sequence: "GTGTTT", + }, + { + identity: 1, + name: "Read1", + path: { + mapping: [ + { + edit: [ + { + from_length: 1, + to_length: 1, + }, + ], + position: { + node_id: "60080786", + offset: 1, + }, + rank: "1", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "2", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080783", + }, + rank: "3", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080786", + }, + rank: "4", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "5", + }, + { + edit: [ + { + from_length: 2, + to_length: 1, + }, + ], + position: { + node_id: "60080783", + offset: -2, + }, + rank: "6", + }, + ], + }, + score: 110, + sequence: "TGTTT", + }, + { + identity: 1, + name: "Read2", + path: { + mapping: [ + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080786", + }, + rank: "1", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "2", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080783", + }, + rank: "3", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080786", + }, + rank: "4", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "5", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080783", + }, + rank: "6", + }, + ], + }, + score: 110, + sequence: "GTGTTT", + }, + { + identity: 1, + name: "Read3", + path: { + mapping: [ + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080786", + }, + rank: "1", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "2", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080783", + }, + rank: "3", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080786", + }, + rank: "4", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "5", + }, + { + edit: [ + { + from_length: 2, + to_length: 3, + }, + ], + position: { + node_id: "60080783", + offset: -1, + }, + rank: "6", + }, + ], + }, + score: 110, + sequence: "GTACTT", + }, + { + identity: 1, + name: "Read4", + path: { + mapping: [ + { + edit: [ + { + from_length: 1, + to_length: 1, + }, + ], + position: { + node_id: "60080786", + offset: 1, + }, + rank: "1", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "2", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080783", + }, + rank: "3", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080786", + }, + rank: "4", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "5", + }, + { + edit: [ + { + from_length: 2, + to_length: 1, + }, + ], + position: { + node_id: "60080783", + }, + rank: "6", + }, + ], + }, + score: 110, + sequence: "TACTT", + }, +]; + +export const cycle2Graph = { + edge: [ + { + from: "60080783", + from_start: true, + to: "60080786", + to_end: true, + }, + { + from: "60080783", + from_start: true, + to: "60080785", + }, + { + from: "60080783", + to: "60080785", + }, + { + from: "60080786", + to: "60080785", + }, + { + from: "60080786", + to: "60080785", + to_end: true, + }, + ], + node: [ + { + id: "60080785", + sequence: "AC", + }, + { + id: "60080783", + sequence: "TT", + }, + { + id: "60080786", + sequence: "GT", + }, + ], + path: [ + { + mapping: [ + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080786", + }, + rank: "1", + }, + { + edit: [ + { + from_length: 21, + to_length: 21, + }, + ], + position: { + node_id: "60080783", + }, + rank: "2", + }, + ], + name: "GRCh38.chr14", + indexOfFirstBase: "0", + }, + ], +}; + + +export const cycle2Reads = [ + { + identity: 1, + name: "Read0", + path: { + mapping: [ + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080786", + }, + rank: "1", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "2", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080783", + }, + rank: "3", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080786", + }, + rank: "4", + }, + ], + }, + score: 110, + sequence: "GTGTTT", + }, + { + identity: 1, + name: "Read1", + path: { + mapping: [ + { + edit: [ + { + from_length: 1, + to_length: 1, + }, + ], + position: { + node_id: "60080786", + offset: 1, + }, + rank: "1", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "2", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080783", + }, + rank: "3", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "5", + }, + { + edit: [ + { + from_length: 2, + to_length: 1, + }, + ], + position: { + node_id: "60080783", + offset: -2, + }, + rank: "6", + }, + ], + }, + score: 110, + sequence: "TGTTT", + }, + { + identity: 1, + name: "Read2", + path: { + mapping: [ + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080786", + }, + rank: "1", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "2", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080783", + }, + rank: "3", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080786", + }, + rank: "4", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "5", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080783", + }, + rank: "6", + }, + ], + }, + score: 110, + sequence: "GTGTTT", + }, + { + identity: 1, + name: "Read3", + path: { + mapping: [ + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080786", + }, + rank: "1", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "2", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080783", + }, + rank: "3", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080786", + }, + rank: "4", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "5", + }, + { + edit: [ + { + from_length: 2, + to_length: 3, + }, + ], + position: { + node_id: "60080783", + offset: -1, + }, + rank: "6", + }, + ], + }, + score: 110, + sequence: "GTACTT", + }, + { + identity: 1, + name: "Read4", + path: { + mapping: [ + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "2", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080783", + }, + rank: "3", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080786", + }, + rank: "4", + }, + { + edit: [ + { + from_length: 2, + to_length: 2, + }, + ], + position: { + node_id: "60080785", + }, + rank: "5", + }, + { + edit: [ + { + from_length: 2, + to_length: 1, + }, + ], + position: { + node_id: "60080783", + }, + rank: "6", + }, + ], + }, + score: 110, + sequence: "TACTT", + }, +]; + export const reverseAlignmentGraph = { edge: [ { diff --git a/src/util/tubemap.js b/src/util/tubemap.js index 4ae9fef3..8ba06ee2 100644 --- a/src/util/tubemap.js +++ b/src/util/tubemap.js @@ -488,7 +488,6 @@ function createTubeMap() { generateLaneAssignment(); if (config.showExonsFlag === true && bed !== null) addTrackFeatures(); - generateNodeXCoords(); if (reads && config.showReads) { generateReadOnlyNodeAttributes(); @@ -505,6 +504,8 @@ function createTubeMap() { }); } + generateNodeXCoords(); + generateSVGShapesFromPath(nodes, tracks); if (DEBUG) { console.log("Tracks:"); @@ -676,12 +677,39 @@ function placeReads() { if (!element.hasOwnProperty("y")) { // previous y value from pathIdx - 1 might not exist yet if that segment is also without node // use previous y value from last segment with node instead - let previousValidY = null; let lastIndex = pathIdx - 1; - while (previousValidY === null && lastIndex >= 0) { - previousValidY = reads[idx].path[lastIndex].y; + let previousVisitToNode; + while ((previousVisitToNode?.node === null || !previousVisitToNode?.node) && lastIndex >= 0) { + previousVisitToNode = reads[idx].path[lastIndex]; lastIndex = lastIndex - 1; } + + let previousValidY = previousVisitToNode?.y; + + // sometimes, elements without nodes are between 2 segments going to a node we've already visited, from the same direction + // this means we're looping back to a node we've already been to, and we should sort in reverse + + // Find the next node in our path + let nextPathIndex = pathIdx + 1 + let nextVisitToNode = reads[idx].path[nextPathIndex]; + while ((nextVisitToNode?.node === null || !nextVisitToNode?.node) && nextPathIndex < reads[idx].path.length) { + nextVisitToNode = reads[idx].path[nextPathIndex]; + nextPathIndex = nextPathIndex + 1; + } + + // Specifically referring to segments between a cycle that's traversing from right to left + let betweenCycleReverseTraversal = + // A segment can be between a cycle if it there are nodes on both sides + (nextVisitToNode && previousVisitToNode) && + // Make sure the visitToNode objects are what we expect + (typeof previousVisitToNode.order !== "undefined" && typeof nextVisitToNode.order !== "undefined" && typeof nextVisitToNode.isForward !== "undefined" && typeof previousVisitToNode.isForward !== "undefined") && + // A segment is between a cycle if the next node it visits is behind the previous node it visited + ((previousVisitToNode.order > nextVisitToNode.order) || + // A segment can also be between a cycle if it's visiting the same node it just visited in the same direction + (nextVisitToNode.order === previousVisitToNode.order && nextVisitToNode.isForward === previousVisitToNode.isForward)); + + reads[idx].path[pathIdx].betweenCycleReverseTraversal = betweenCycleReverseTraversal; + elementsWithoutNode.push({ readIndex: idx, pathIndex: pathIdx, @@ -841,31 +869,61 @@ function compareNoNodeReadsByPreviousY(a, b) { const segmentA = reads[a.readIndex].path[a.pathIndex]; const segmentB = reads[b.readIndex].path[b.pathIndex]; if (segmentA.order === segmentB.order) { - return a.previousY - b.previousY; + // We want to sort in reverse order when the segment is along the reverse-going part of a cycle. + // This ensures a loop that starts on the outside, stays on the outside, + // and rolls up in order with other loops. + if (segmentA?.betweenCycleReverseTraversal && segmentB?.betweenCycleReverseTraversal) { + return b.previousY - a.previousY; + } else { + return a.previousY - b.previousY; + } } return segmentA.order - segmentB.order; } // compare read segments by where they are going to -function compareReadOutgoingSegmentsByGoingTo(a, b) { - let pathIndexA = a[1]; - let pathIndexB = b[1]; - // let readA = reads[a[0]] - // let nodeIndexA = readA.path[pathIndexA].node; - let nodeA = nodes[reads[a[0]].path[pathIndexA].node]; - let nodeB = nodes[reads[b[0]].path[pathIndexB].node]; +function compareReadOutgoingSegmentsByGoingTo([readIndexA, pathIndexA], [readIndexB, pathIndexB]) { + // Expect two arrays both containing 2 integers. + // The first index of each array contains the read index + // The second index of each array contains the path index + + // Segments are first sorted by the y value of their last node, + // then by the node they end on, + // then by length in final node + let previousValidYA = null; + let previousValidYB = null; + let lastPathIndexA = reads[readIndexA].path.length - 1; + let lastPathIndexB = reads[readIndexB].path.length - 1; + while ((previousValidYA === null || !previousValidYA) && lastPathIndexA >= 0) { + previousValidYA = reads[readIndexA].path[lastPathIndexA].y; + lastPathIndexA -= 1; + } + while ((previousValidYB === null || !previousValidYB) && lastPathIndexB >= 0) { + previousValidYB = reads[readIndexB].path[lastPathIndexB].y; + lastPathIndexB -= 1; + } + + if (previousValidYA && previousValidYB) { + return previousValidYA - previousValidYB; + } + + // Couldn't find a valid y value for at least one of the reads, sort by which node reads end on + let nodeA = nodes[reads[readIndexA].path[pathIndexA].node]; + let nodeB = nodes[reads[readIndexB].path[pathIndexB].node]; + // Follow the reads' paths until we find the node they diverge at + // Or, they go through all the same nodes and we do a tiebreaker at the end while (nodeA !== null && nodeB !== null && nodeA === nodeB) { - if (pathIndexA < reads[a[0]].path.length - 1) { + if (pathIndexA < reads[readIndexA].path.length - 1) { pathIndexA += 1; - while (reads[a[0]].path[pathIndexA].node === null) pathIndexA += 1; // skip null nodes in path - nodeA = nodes[reads[a[0]].path[pathIndexA].node]; + while (reads[readIndexA].path[pathIndexA].node === null) pathIndexA += 1; // skip null nodes in path + nodeA = nodes[reads[readIndexA].path[pathIndexA].node]; // the next node a is going to } else { nodeA = null; } - if (pathIndexB < reads[b[0]].path.length - 1) { + if (pathIndexB < reads[readIndexB].path.length - 1) { pathIndexB += 1; - while (reads[b[0]].path[pathIndexB].node === null) pathIndexB += 1; // skip null nodes in path - nodeB = nodes[reads[b[0]].path[pathIndexB].node]; + while (reads[readIndexB].path[pathIndexB].node === null) pathIndexB += 1; // skip null nodes in path + nodeB = nodes[reads[readIndexB].path[pathIndexB].node]; // the next node b is going to } else { nodeB = null; } @@ -876,10 +934,13 @@ function compareReadOutgoingSegmentsByGoingTo(a, b) { } if (nodeB !== null) return -1; // nodeB not null, nodeA null // both nodes are null -> both end in the same node - const beginDiff = reads[a[0]].firstNodeOffset - reads[b[0]].firstNodeOffset; + const beginDiff = reads[readIndexA].firstNodeOffset - reads[readIndexB].firstNodeOffset; if (beginDiff !== 0) return beginDiff; - // break tie: both reads cover the same nodes and begin at the same position -> compare by endPosition - return reads[a[0]].finalNodeCoverLength - reads[b[0]].finalNodeCoverLength; + + // break tie: both reads cover the same nodes and begin at the same position + + // One or both reads didn't have a previously valid Y value, compare by the endPosition of the read + return reads[readIndexA].finalNodeCoverLength - reads[readIndexB].finalNodeCoverLength; } // compare read segments by (y-coord of) where they are coming from