Skip to content

Commit

Permalink
Merge pull request #425 from vgteam/horizontalSpace
Browse files Browse the repository at this point in the history
Resolved extra horizontal space between nodes with a coordinate bar break
  • Loading branch information
adamnovak committed Apr 30, 2024
2 parents 5ef0f26 + 1975b1f commit b6de77d
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 15 deletions.
109 changes: 96 additions & 13 deletions src/util/tubemap.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ const config = {
// 1...scale node width with log2 of number of bases within node
// 2...scale node width with log10 of number of bases within node
nodeWidthOption: 0,
nodeIntervalThreshold: 50,
showReads: true,
showSoftClips: true,
colorSchemes: {},
Expand Down Expand Up @@ -1308,10 +1309,10 @@ function getImageDimensions() {

// This needs to be the width of the ruler.
// TODO: Tell the ruler drawing code.
const RULER_WIDTH = 15;
const RULER_WIDTH = 30;
const NODE_MARGIN = 10;
// This is how much space to let us pan, around the nodes as measure by getImageDimensions()
const RAIL_SPACE = 25;
const RAIL_SPACE = RULER_WIDTH + NODE_MARGIN;

// align visualization to the top and left within svg and resize svg to correct size
// enable zooming and panning
Expand Down Expand Up @@ -2669,8 +2670,7 @@ function getReadXEnd(read) {
// position within the given node
function getXCoordinateOfBaseWithinNode(node, base) {
if (base > node.sequenceLength) return null; // equality is allowed
const nodeLeftX = node.x - 4;
const nodeRightX = node.x + node.pixelWidth + 4;
const [nodeLeftX, nodeRightX] = nodePixelCoordinatesInX(node);
return nodeLeftX + (base / node.sequenceLength) * (nodeRightX - nodeLeftX);
}

Expand Down Expand Up @@ -3467,6 +3467,42 @@ function drawLabels(dNodes) {
}
}

function nodePixelCoordinatesInX(node) {
// Add and subtract 4 to account for stroke width - TODO: figure out what the 4 means
const nodeLeftX = node.x - 4;
const nodeRightX = node.x + node.pixelWidth + 4;
return [nodeLeftX, nodeRightX];
}

// If nodes are spaced closely together (based on the threshold value) then those nodes would be grouped together
// in a larger interval. If the nodes are spaced further apart (based on the threshold) then those nodes would form a
// separate interval. If the distance between the nodes is equal to the threshold, then the nodes would be grouped together
// in a larger interval
export function axisIntervals(nodePixelCoordinates, threshold) {
if (nodePixelCoordinates.length === 0){
return [];
} else if (nodePixelCoordinates.length === 1){
return nodePixelCoordinates;
}
// Sorting an array in ascending order based on first element of subarrays - from https://stackoverflow.com/questions/48634944/sort-an-array-of-arrays-by-the-first-elements-in-the-nested-arrays
let nodePixelCoordinatesCopy = nodePixelCoordinates.slice(); // shallow copy
nodePixelCoordinatesCopy.sort((a, b) => a[0] - b[0]);
// https://keithwilliams-91944.medium.com/merge-intervals-solution-in-javascript-daa61b618ed4
let mergedIntervals = [nodePixelCoordinatesCopy[0].slice()];
for (let i = 1; i < nodePixelCoordinatesCopy.length; i++){
// compute the distance between the current interval and the current coordinate pair's starting x-value, and compare it to a threshold. If it's less than the threshold, merge the intervals.
if (nodePixelCoordinatesCopy[i][0] - mergedIntervals[mergedIntervals.length - 1][1] <= threshold) {
// update ending position to the maximum of current end value and end of current interval - can be thought of as extending the interval
mergedIntervals[mergedIntervals.length - 1][1] = Math.max(mergedIntervals[mergedIntervals.length - 1][1], nodePixelCoordinatesCopy[i][1]);
} else {
// new interval
mergedIntervals.push(nodePixelCoordinatesCopy[i].slice());
}
}
return mergedIntervals;
}


function drawRuler() {
let rulerTrackIndex = 0;
while (tracks[rulerTrackIndex].name !== trackForRuler) rulerTrackIndex += 1;
Expand Down Expand Up @@ -3530,6 +3566,9 @@ function drawRuler() {

let start_region = Number(inputRegion[0]);
let end_region = Number(inputRegion[1]);

let intervalsVisitedByNodes = [];

for (let i = 0; i < rulerTrack.indexSequence.length; i++) {
// Walk along the ruler track in ascending coordinate order.
const nodeIndex =
Expand All @@ -3539,6 +3578,10 @@ function drawRuler() {
: i
];
const currentNode = nodes[Math.abs(nodeIndex)];

// Adding node X start and end positions into an array
intervalsVisitedByNodes.push(nodePixelCoordinatesInX(currentNode));

// Each node may actually have the track's coordinates go through it
// backward. In fact, the whole track may be laid out backward.
// So xor the reverse flags, which we assume to be bools
Expand Down Expand Up @@ -3601,6 +3644,9 @@ function drawRuler() {
// Advance to the next node
indexOfFirstBaseInNode += currentNode.sequenceLength;
}

// merge intervals
var mergedIntervals = axisIntervals(intervalsVisitedByNodes, config.nodeIntervalThreshold);

// Sort ticks on X coordinate
ticks.sort(([bp1, x1], [bp2, x2]) => x1 > x2);
Expand All @@ -3621,30 +3667,67 @@ function drawRuler() {
// plot ticks highlighting the region
ticks_region.forEach((tick) => drawRulerMarkingRegion(tick[0], tick[1]));

// draw horizontal line
svg
// draw horizontal line for each interval

let axisY = minYCoordinate - 10;
mergedIntervals.forEach((interval) => {
svg
.append("line")
.attr("x1", interval[0])
.attr("y1", axisY)
.attr("x2", interval[1])
.attr("y2", axisY)
.attr("stroke-width", 1)
.attr("stroke", "black")

// starting vertical line
svg
.append("line")
.attr("x1", 0)
.attr("y1", minYCoordinate - 10)
.attr("x2", maxXCoordinate)
.attr("y2", minYCoordinate - 10)
.attr("x1", interval[0])
.attr("y1", axisY - 5)
.attr("x2", interval[0])
.attr("y2", axisY + 5)
.attr("stroke-width", 1)
.attr("stroke", "black");
.attr("stroke", "black")

// ending vertical line
svg
.append("line")
.attr("x1", interval[1])
.attr("y1", axisY - 5)
.attr("x2", interval[1])
.attr("y2", axisY + 5)
.attr("stroke-width", 1)
.attr("stroke", "black")
}
);

// Plot all the ticks
ticks.forEach((tick) => drawRulerMarking(tick[0], tick[1]));
}

function drawRulerMarking(sequencePosition, xCoordinate) {
let axisY = minYCoordinate - 10;
svg
.append("text")
.attr("text-anchor", "middle")
.attr("x", xCoordinate)
.attr("y", minYCoordinate - 13)
.text(`|${sequencePosition}`)
.attr("y", minYCoordinate - 18)
.text(`${sequencePosition}`)
.attr("font-family", fonts)
.attr("font-size", "12px")
.attr("fill", "black")
.style("pointer-events", "none");

// vertical line
svg
.append("line")
.attr("x1", xCoordinate)
.attr("y1", axisY - 5)
.attr("x2", xCoordinate)
.attr("y2", axisY + 5)
.attr("stroke-width", 1)
.attr("stroke", "black")
}

function drawRulerMarkingRegion(sequencePosition, xCoordinate) {
Expand Down
61 changes: 59 additions & 2 deletions src/util/tubemap.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { cigar_string } from "./tubemap";
import { coverage } from "./tubemap";
import { cigar_string, coverage, axisIntervals } from "./tubemap";

// cigar string test
describe("cigar_string", () => {
Expand Down Expand Up @@ -864,3 +863,61 @@ describe("coverage", () => {
expect(coverage(node, reads)).toBe(2.79);
});
});


describe("axisIntervals", () => {
// TEST 1
it("can handle an empty array", async () => {
let nodePixelCoordinates = [];
let threshold = 1;
expect(axisIntervals(nodePixelCoordinates, threshold)).toStrictEqual([]);
});
// TEST 2
it("can handle an array of one interval", async () => {
let nodePixelCoordinates = [[0, 1]];
let threshold = 1;
expect(axisIntervals(nodePixelCoordinates, threshold)).toStrictEqual([[0, 1]]);
});
// TEST 3
it("can handle an array of two interval where the difference between the intervals is less than the threshold", async () => {
let nodePixelCoordinates = [[0, 1], [1, 2]];
let threshold = 2;
expect(axisIntervals(nodePixelCoordinates, threshold)).toStrictEqual([[0, 2]]);
});
// TEST 4
it("can handle an array of two interval where the difference between the intervals is greater than the threshold", async () => {
let nodePixelCoordinates = [[0, 1], [3, 4]];
let threshold = 1;
expect(axisIntervals(nodePixelCoordinates, threshold)).toStrictEqual([[0, 1], [3, 4]]);
});
// TEST 5
it("can handle an array of two interval where the difference between the intervals is equal to the threshold", async () => {
let nodePixelCoordinates = [[0, 1], [2, 3]];
let threshold = 1;
expect(axisIntervals(nodePixelCoordinates, threshold)).toStrictEqual([[0, 3]]);
});
// TEST 6
it("can handle an array of repeating intervals", async () => {
let nodePixelCoordinates = [[0, 1], [2, 3], [2, 3], [2, 3], [2, 3], [2, 3], [4, 5]];
let threshold = 1;
expect(axisIntervals(nodePixelCoordinates, threshold)).toStrictEqual([[0, 5]]);
});
// TEST 7
it("can handle an array of repeating intervals that are more spaced out", async () => {
let nodePixelCoordinates = [[0, 1], [3, 4], [3, 4], [3, 4], [3, 4], [6, 7]];
let threshold = 1;
expect(axisIntervals(nodePixelCoordinates, threshold)).toStrictEqual([[0, 1], [3, 4], [6, 7]]);
});
// TEST 8
it("can handle an array of out of order intervals", async () => {
let nodePixelCoordinates = [[2, 3], [0, 1], [5, 6], [2, 4]];
let threshold = 1;
expect(axisIntervals(nodePixelCoordinates, threshold)).toStrictEqual([[0, 6]]);
});
// TEST 9
it("can handle an array of larger, more spaced out intervals, with a larger threshold", async () => {
let nodePixelCoordinates = [[0, 10], [12, 15], [0, 10], [18, 29], [53, 117]];
let threshold = 20;
expect(axisIntervals(nodePixelCoordinates, threshold)).toStrictEqual([[0, 29], [53, 117]]);
});
});

0 comments on commit b6de77d

Please sign in to comment.