pathView = {
let pathViewCanvas = html`<div style="width:${genomeSpaceZoomViewContentCurveDimensions.w}px; height:${genomeSpaceZoomViewContentCurveDimensions.h}px; max-width:${genomeSpaceZoomViewContentCurveDimensions.w}px; max-height:${genomeSpaceZoomViewContentCurveDimensions.h}px; position: relative; z-index: 1; top:0px; left:0px;"></div>`;
const pathScaleFactor =
curveRectSize / genomeSpaceZoomViewContentCurveDimensions.w;
const pathPanelWidth = curveRectSize;
const pathPanelHeight = pathPanelWidth;
const zoomFactor = 2 ** (zoomOrder - viewOrder);
function rescaleOrigin(x, y) {
const rescaledOrigin = {
x: x - pathPanelWidth / 2,
y: y - pathPanelHeight / 2
};
if (rescaledOrigin.x <= 0) {
rescaledOrigin.x = 0;
}
if (
rescaledOrigin.x + pathPanelWidth >=
genomeSpaceZoomViewContentCurveDimensions.w
) {
rescaledOrigin.x =
genomeSpaceZoomViewContentCurveDimensions.w - pathPanelWidth;
}
if (rescaledOrigin.y <= 0) {
rescaledOrigin.y = 0;
}
if (
rescaledOrigin.y + pathPanelHeight >=
genomeSpaceZoomViewContentCurveDimensions.h
) {
rescaledOrigin.y =
genomeSpaceZoomViewContentCurveDimensions.h - pathPanelHeight;
}
rescaledOrigin.x /= pathScaleFactor;
rescaledOrigin.y /= pathScaleFactor;
return rescaledOrigin;
}
function handlePathClick(x, y) {
console.log(`handlePathClick > ${[x, y]}`);
const pathPanelOrigin = rescaleOrigin(pointXY.x, pointXY.y);
if (
typeof pathPanelOrigin === "undefined" ||
typeof pathPanelOrigin.x === "undefined"
)
return;
const curveX = (x + pathPanelOrigin.x) / zoomFactor;
const curveY = (y + pathPanelOrigin.y) / zoomFactor;
console.log(`curveX ${curveX} | curveY ${curveY}`);
nearestBaseToClickXYInView({ x: curveX, y: curveY }, curveZoom, zoomOrder);
}
function handlePathDoubleclick(x, y) {
const pathPanelOrigin = rescaleOrigin(pointXY.x, pointXY.y);
if (
typeof pathPanelOrigin === "undefined" ||
typeof pathPanelOrigin.x === "undefined"
)
return;
const curveX = (x + pathPanelOrigin.x) / zoomFactor;
const curveY = (y + pathPanelOrigin.y) / zoomFactor;
// const curveBinIdx = hilbert.getValAtXY(curveX, curveY);
const curveBinIdx = pathHilbert.getValAtXY(curveX, curveY);
const curveStateInfo = curveMappedStatesZoomByBinIndex()[curveBinIdx];
const curveColorIdx = curveStateInfo.idx - 1;
const curveStatePointsStartRange =
curveStateInfo.idx !== -1
? [
curveStateInfo.points[0].start,
curveStateInfo.points[curveStateInfo.points.length - 1].start
]
: [];
if (curveStateInfo.idx !== -1) {
const region = {
chrom: `chr${chromosome}`,
start: curveStatePointsStartRange[0],
stop: curveStatePointsStartRange[1],
midpoint:
curveStatePointsStartRange[0] +
parseInt(
(curveStatePointsStartRange[1] - curveStatePointsStartRange[0]) / 2
),
padding: 25000
};
const urlFromRegion = genomeSpaceZoomScoreViewLabelExtLinkUrlFromPosition(
region.chrom,
region.midpoint,
region.padding
);
// console.log(`urlFromRegion ${JSON.stringify(urlFromRegion)}`);
const body = document.body;
const externalLink = document.createElement("a");
externalLink.id = "epilogos_viewer_from_datum";
externalLink.target = "_blank";
externalLink.href = urlFromRegion;
body.appendChild(externalLink);
const evfd = document.getElementById("epilogos_viewer_from_datum");
document.getElementById("epilogos_viewer_from_datum").click();
body.removeChild(externalLink);
}
}
function renderPathTooltipRow(k, v) {
return (
"<tr>" +
`<td class='curveNodeCell curveNodeCellKey'>${k}</td>` +
`<td class='curveNodeCell curveNodeCellValue'>${v}</td>` +
"</tr>"
);
}
function showPathBin(curveX, curveY) {
// console.log(`pathView > showPathBin ${[curveX, curveY]}`);
const relativeCurveX =
// zoomFactor * curveX * curveZoomRange.cellWidth * 0.9985;
curveX * curveZoomRange.cellWidth * 2 ** (zoomOrder - viewOrder);
const relativeCurveY =
// zoomFactor * curveY * curveZoomRange.cellWidth * 0.9985;
curveY * curveZoomRange.cellWidth * 2 ** (zoomOrder - viewOrder);
const pathPanelOrigin = rescaleOrigin(pointXY.x, pointXY.y);
// showPathTooltip(
// relativeCurveX - pathPanelOrigin.x,
// relativeCurveY - pathPanelOrigin.y
// );
// console.log(
// `relativeCurveX ${relativeCurveX} relativeCurveY ${relativeCurveY}`
// );
const maxZoomOrder = 12;
const strokeWidth = Math.min(maxZoomOrder - zoomOrder, 3);
const root = d3.select("#genomeSpaceZoomViewContent");
root.selectAll("svg.hilbertCurveDecoration").remove(); // clear out old SVG paths, or else they accumulate
const pathSvg = root
.append("svg")
.attr("id", "hilbertCurveDecoration")
.attr("class", "hilbertCurveDecoration")
.style("position", "absolute")
.style("z-index", "1001")
.style("top", relativeCurveY - (maxZoomOrder - zoomOrder) - 1)
.style("left", relativeCurveX - (maxZoomOrder - zoomOrder) - 1)
.attr(
"transform",
`translate(${-pathPanelOrigin.x}, ${-pathPanelOrigin.y})` // move path along with origin
)
.attr(
"width",
curveZoomRange.cellWidth * 2 ** (zoomOrder - viewOrder) + zoomOrder
)
.attr(
"height",
curveZoomRange.cellWidth * 2 ** (zoomOrder - viewOrder) + zoomOrder
);
const selectedBin = pathSvg.append("g");
selectedBin
.append("rect")
.attr("class", "selectionBox")
.attr("x", `${maxZoomOrder - zoomOrder}`)
.attr("y", `${maxZoomOrder - zoomOrder}`)
.attr("width", curveZoomRange.cellWidth * 2 ** (zoomOrder - viewOrder))
.attr("height", curveZoomRange.cellWidth * 2 ** (zoomOrder - viewOrder))
.attr("fill", "none")
.attr("stroke", "#00f")
.attr("stroke-width", `${strokeWidth}`);
}
function clearPathBin() {
d3.select("#genomeSpaceZoomViewContent")
.selectAll("svg.hilbertCurveDecoration")
.remove();
}
function showPathTooltip(x, y) {
// console.log(`pathView > showPathTooltip ${[x, y]}`);
clearPathBin();
const el = d3.select("#val-tooltip");
const valTooltip =
el.size() === 0
? d3
.select("#genomeSpaceZoomViewContent")
.append("div")
.attr("id", "val-tooltip")
: el;
const xMid = genomeSpaceZoomViewContentCurveDimensions.w / 2;
const xRight = x > xMid;
const yMid = genomeSpaceZoomViewContentCurveDimensions.h / 2;
const yBottom = y > yMid;
const pathPanelOrigin = rescaleOrigin(pointXY.x, pointXY.y);
if (
typeof pathPanelOrigin === "undefined" ||
typeof pathPanelOrigin.x === "undefined"
)
return;
const curveX = (x + pathPanelOrigin.x) / zoomFactor;
const curveY = (y + pathPanelOrigin.y) / zoomFactor;
// console.log(`curveX ${curveX} | curveY ${curveY}`);
let nodeLabelHTML = "<table class='curveNodeTable'>";
const curveBinIdx = pathHilbert.getValAtXY(curveX, curveY);
const curveStateInfo = curveMappedStatesZoomByBinIndex[curveBinIdx];
// console.log(`curveStateInfo ${JSON.stringify(curveStateInfo)}`);
try {
const curveColorIdx = curveStateInfo.idx - 1;
const curveStateName =
curveStateInfo.idx !== -1 ? curveColorsForStates[curveColorIdx][0] : "";
const curveStateColorHex =
curveStateInfo.idx !== -1 ? curveColorsForStates[curveColorIdx][1] : "";
const curveStatePointsStartRange =
curveStateInfo.idx !== -1
? [
curveStateInfo.points[0].start,
curveStateInfo.points[curveStateInfo.points.length - 1].start
]
: [];
if (curveStateInfo.idx !== -1) {
nodeLabelHTML += renderPathTooltipRow("Bin", curveBinIdx);
nodeLabelHTML += renderPathTooltipRow(
"State",
`<span style="background-color:${curveStateColorHex}; display:inline-flex; height:8px; width:8px"></span> ${curveStateName}`
);
nodeLabelHTML += renderPathTooltipRow("Chrom", `chr${chromosome}`);
nodeLabelHTML += renderPathTooltipRow(
"Start",
`${curveStatePointsStartRange[0]}`
);
nodeLabelHTML += renderPathTooltipRow(
"End",
`${curveStatePointsStartRange[1]}`
);
nodeLabelHTML += "</table>";
const informationBox = valTooltip
.style("display", "inline")
.style("z-index", "1000")
.style("left", `${x + (xRight ? -180 : 20)}px`)
.style("top", `${y + (yBottom ? -110 : 20)}px`)
.html(nodeLabelHTML);
}
} catch (err) {
// console.log(`pathView | showPathTooltip | err > ${JSON.stringify(err)}`);
}
}
function clearPathTooltip() {
// console.log(`pathView > clearPathTooltip`);
d3.select("#val-tooltip").style("display", "none");
}
function drawPath(x, y) {
// console.log(`drawPath > ${[x, y]}`);
if (
zoomOptions.includes("Show Hilbert curve") === -1 ||
!mutable genomeSpaceZoomViewPathEnabled
)
return;
const pathPanelOrigin = rescaleOrigin(x, y);
if (
typeof pathPanelOrigin === "undefined" ||
typeof pathPanelOrigin.x === "undefined"
)
return;
// console.log(`pathPanelOrigin ${JSON.stringify(pathPanelOrigin)}`);
/*
From the starting curveBinIdx value, we have 2^(viewOrder - 2) allowed path vertices
types or "hits" before we know we are going off-screen, whatever the zoomOrder value
happens to be.
For instance, given a small subpath of a larger Hilbert curve (picture that more of
the curve is described outside the bounds of the subpath endpoints):
... - x
|
x -- x -- x x -- x
| | |
... x -- o x -- x
| |
x x -- x x
| | | |
x -- x x -- x
Starting at point 'o', we can go up, down, left, or right (U, D, L, R) no more than
two node units in either direction, before we are outside the visible bounds of the
subpath.
These labels (U, D, etc.) are contained in the array `curveZoomRange.pathVertices`.
We start at the centerpoint 'o' and walk along the path in reverse, decrementing an
"allowed move" counter for each direction from the values in this array.
In case we reach the start node, we halt. If we reach a negative value for an
allowed move, we halt. Either node index is the lower bound of the contiguous
subpath.
We start at the centerpoint 'o' and walk along the path in forward direction,
decrementing an "allowed move" counter for each direction. In case we reach the
end node, we halt. If we reach a negative value for a move, we halt. Either node
index is the upper bound of the subpath.
Once we have the lower and upper bounds, we can draw a path that starts and ends at
these points. We can also retrieve the genomic interval defined by these node bounds.
This is a "true" genomic interval in that it represents the path drawn from the
centerpoint outwards, until the path ends are out-of-view.
Perhaps we use a dash array (https://stackoverflow.com/questions/56822311/) or
perhaps write out a path that starts and ends with the desired path vertices, at the
required start and end points. Either way we only draw the portion of the path that
reflects the centerpoint selection and its contiguous subpath bounds.
*/
// if (mutable binSelectionStartIdx !== 0) {
// mutable binSelectionStartIdx = 0;
// }
// if (mutable binSelectionEndIdx !== Math.pow(4, zoomOrder) - 1) {
// mutable binSelectionEndIdx !== Math.pow(4, zoomOrder) - 1;
// }
const curveBinStepSize = parseInt(bpForChromosome / Math.pow(4, zoomOrder));
const curveBinIdx = linPos / curveBinStepSize;
const curveBinXY = pathHilbert.getXyAtVal(curveBinIdx);
if (mutable binSelectionIdx !== curveBinIdx) {
mutable binSelectionIdx = curveBinIdx;
mutable binSelectionStartPosition = adjustToBinWidth(
linPos,
binResolution
);
}
// console.log(
// `-> linPos ${linPos} | curveBinIdx ${curveBinIdx} | curveBinXY ${JSON.stringify(
// curveBinXY
// )} | curveZoomRange.pathVertices ${
// curveZoomRange.pathVertices[curveBinIdx]
// }`
// );
const allowedDirectionMoves = Math.pow(2, viewOrder - 1);
const backwardsDirectionChecks = Object.fromEntries(
["U", "D", "L", "R"].map((d) => [d, allowedDirectionMoves])
);
//
// adjust allowed direction moves, if the selected
// bin is along the edge of the functional annotation
// view
//
if (curveBinXY[0] < allowedDirectionMoves) {
backwardsDirectionChecks["L"] = curveBinXY[0];
backwardsDirectionChecks["R"] +=
allowedDirectionMoves - backwardsDirectionChecks["L"] - 1;
}
if (curveBinXY[0] > Math.pow(2, zoomOrder) - 1 - allowedDirectionMoves) {
backwardsDirectionChecks["R"] =
Math.pow(2, zoomOrder) - 1 - curveBinXY[0];
backwardsDirectionChecks["L"] +=
allowedDirectionMoves - backwardsDirectionChecks["R"] - 1;
}
if (curveBinXY[1] < allowedDirectionMoves) {
backwardsDirectionChecks["U"] = curveBinXY[1];
backwardsDirectionChecks["D"] +=
allowedDirectionMoves - backwardsDirectionChecks["U"] - 1;
}
if (curveBinXY[1] > Math.pow(2, zoomOrder) - 1 - allowedDirectionMoves) {
backwardsDirectionChecks["D"] =
Math.pow(2, zoomOrder) - 1 - curveBinXY[1];
backwardsDirectionChecks["U"] +=
allowedDirectionMoves - backwardsDirectionChecks["D"] - 1;
}
const forwardsDirectionChecks = { ...backwardsDirectionChecks };
const flipDirection = { L: "R", R: "L", U: "D", D: "U" };
// console.log(
// `start | backwardsDirectionChecks ${JSON.stringify(
// backwardsDirectionChecks
// )}`
// );
for (
let curveBinTestIdx = curveBinIdx - 1;
curveBinTestIdx >= 0;
--curveBinTestIdx
) {
const direction =
flipDirection[curveZoomRange.pathVertices[curveBinTestIdx]];
backwardsDirectionChecks[direction] -= 1;
switch (direction) {
case "L":
backwardsDirectionChecks["R"] += 1;
break;
case "R":
backwardsDirectionChecks["L"] += 1;
break;
case "U":
backwardsDirectionChecks["D"] += 1;
break;
case "D":
backwardsDirectionChecks["U"] += 1;
break;
}
// console.log(
// `curveBinTestIdx ${curveBinTestIdx} | direction ${direction} | backwardsDirectionChecks ${JSON.stringify(
// backwardsDirectionChecks
// )}`
// );
if (backwardsDirectionChecks[direction] < 0 || curveBinTestIdx === 0) {
if (mutable binSelectionStartIdx !== curveBinTestIdx) {
mutable binSelectionStartIdx = curveBinTestIdx;
}
break;
}
}
// console.log(
// `end | binSelectionStartIdx ${binSelectionStartIdx} | backwardsDirectionChecks ${JSON.stringify(
// backwardsDirectionChecks
// )}`
// );
// console.log(
// `start | forwardsDirectionChecks ${JSON.stringify(
// forwardsDirectionChecks
// )}`
// );
const maxBinTestIdx = Math.pow(4, zoomOrder) - 1;
for (
let curveBinTestIdx = curveBinIdx;
curveBinTestIdx <= maxBinTestIdx;
++curveBinTestIdx
) {
const direction = curveZoomRange.pathVertices[curveBinTestIdx];
forwardsDirectionChecks[direction] -= 1;
switch (direction) {
case "L":
forwardsDirectionChecks["R"] += 1;
break;
case "R":
forwardsDirectionChecks["L"] += 1;
break;
case "U":
forwardsDirectionChecks["D"] += 1;
break;
case "D":
forwardsDirectionChecks["U"] += 1;
break;
}
if (
forwardsDirectionChecks[direction] < 0 ||
curveBinTestIdx === maxBinTestIdx - 1
) {
if (mutable binSelectionEndIdx !== curveBinTestIdx + 1) {
mutable binSelectionEndIdx = curveBinTestIdx + 1;
}
break;
}
// console.log(
// `curveBinTestIdx ${curveBinTestIdx} | direction ${direction} | forwardsDirectionChecks ${JSON.stringify(
// forwardsDirectionChecks
// )}`
// );
}
// console.log(
// `end | binSelectionEndIdx ${binSelectionEndIdx} | forwardsDirectionChecks ${JSON.stringify(
// forwardsDirectionChecks
// )}`
// );
const root = d3.select(pathViewCanvas).style("text-align", "center");
root.selectAll("*").remove(); // clear out old SVG paths, or else they accumulate
const pathSvg = root
.append("svg")
.attr("id", "hilbertCurve")
.attr("class", "hilbertCurve")
.attr(
"transform",
`translate(${-pathPanelOrigin.x}, ${-pathPanelOrigin.y})` // move path along with origin
)
.attr(
"width",
genomeSpaceZoomViewContentCurveDimensions.w * 2 ** zoomOrder
)
.attr(
"height",
genomeSpaceZoomViewContentCurveDimensions.h * 2 ** zoomOrder
);
function constructPath() {
const canvas = pathSvg.append("g");
canvas.append("path").attr("class", "skeleton");
canvas.append("path");
pathSvg
.selectAll("path")
.datum(pathHilbertData)
.attr("d", (d) => pathHilbertPath(d.pathVertices))
.attr(
"stroke-dasharray",
`0 ${binSelectionStartIdx} ${
binSelectionEndIdx - binSelectionStartIdx
} ${Math.pow(4, zoomOrder) - binSelectionEndIdx - 1}`
)
.attr("transform", (d) => {
d.startCell = [0, 0];
// console.log(`d.cellWidth ${JSON.stringify(d.cellWidth)}`);
// console.log(`d.startCell ${JSON.stringify(d.startCell)}`);
return `scale(${d.cellWidth / pathScaleFactor}) translate(${
d.startCell[0] + 0.5
}, ${d.startCell[1] + 0.5})`;
});
const curveX =
zoomFactor * curveBinXY[0] * curveZoomRange.cellWidth * 0.9985;
const curveY =
zoomFactor * curveBinXY[1] * curveZoomRange.cellWidth * 0.9985;
// console.log(
// `curveBinXY ${curveBinXY} | curveX ${curveX} | curveY ${curveY}`
// );
const maxZoomOrder = 12;
const selectedBinAttr = {
x: curveX + curveZoomRange.cellWidth / 25,
y: curveY + curveZoomRange.cellWidth / 25,
width: zoomFactor * curveZoomRange.cellWidth * 0.95,
height: zoomFactor * curveZoomRange.cellWidth * 0.95,
strokeWidth: Math.min(maxZoomOrder - zoomOrder, 3)
};
// console.log(`selectedBinAttr ${JSON.stringify(selectedBinAttr)}`);
const selectedBin = pathSvg.append("g");
selectedBin
.append("rect")
.attr("class", "selectionBox")
.attr("x", selectedBinAttr.x)
.attr("y", selectedBinAttr.y)
.attr("width", selectedBinAttr.width)
.attr("height", selectedBinAttr.height)
.attr("fill", "none")
.attr("stroke", "#300")
.attr("stroke-width", `${selectedBinAttr.strokeWidth}`);
}
constructPath();
}
pathViewCanvas.drawPath = drawPath;
pathViewCanvas.handlePathClick = handlePathClick;
pathViewCanvas.handlePathDoubleclick = handlePathDoubleclick;
pathViewCanvas.showPathBin = showPathBin;
pathViewCanvas.clearPathBin = clearPathBin;
pathViewCanvas.showPathTooltip = showPathTooltip;
pathViewCanvas.clearPathTooltip = clearPathTooltip;
return pathViewCanvas;
}