UNPKG

higlass

Version:

HiGlass Hi-C / genomic / large data viewer

349 lines (280 loc) 9.7 kB
// @ts-nocheck import { bisector } from 'd3-array'; import { format } from 'd3-format'; import absToChr from './utils/abs-to-chr'; class SearchField { constructor(chromInfo) { this.chromInfo = chromInfo; this.chromInfoBisector = bisector((d) => d.pos).left; } scalesToPositionText(xScale, yScale, twoD = false) { if (this.chromInfo === null) { return ''; } // chromosome info hasn't been loaded yet if (!xScale || !yScale) { return ''; } const x1 = absToChr(xScale.domain()[0], this.chromInfo); const x2 = absToChr(xScale.domain()[1], this.chromInfo); const y1 = absToChr(yScale.domain()[0], this.chromInfo); const y2 = absToChr(yScale.domain()[1], this.chromInfo); let positionString = null; const stringFormat = format(',d'); if (x1[0] !== x2[0]) { // different chromosomes positionString = `${x1[0]}:${stringFormat(Math.floor(x1[1]))}-${ x2[0] }:${stringFormat(Math.ceil(x2[1]))}`; } else { // same chromosome positionString = `${x1[0]}:${stringFormat( Math.floor(x1[1]), )}-${stringFormat(Math.ceil(x2[1]))}`; } if (twoD) { if (y1[0] !== y2[0]) { // different chromosomes positionString += ` & ${y1[0]}:${stringFormat(Math.floor(y1[1]))}-${ y2[0] }:${stringFormat(Math.ceil(y2[1]))}`; } else { // same chromosome positionString += ` & ${y1[0]}:${stringFormat( Math.floor(y1[1]), )}-${stringFormat(Math.ceil(y2[1]))}`; } } if (x1[2] <= 0 || x2[2] > 0 || (twoD && (y1[2] <= 0 || y2[2] > 0))) { // did any of the coordinates exceed the genome boundaries positionString += ` [offset ${x1[2]},${x2[2]}`; if (twoD) { positionString += `:${y1[2]},${y2[2]}`; } positionString += ']'; } return positionString; } convertNumberNotation(numStr) { // Convert K or M notations // e.g. "1.5M" to "1500000" // or "0.05M" to "50000" // or even "00.05M" or "00.050M" to "50000" let newNumStr = numStr; if ( !newNumStr.includes('M', newNumStr.length - 1) && !newNumStr.includes('K', newNumStr.length - 1) ) { // Nothing to convert return newNumStr; } let numZerosToAdd = 0; let decPointPosFromEnd = 0; // Handle 'M' or 'N' notations if (newNumStr.includes('M', newNumStr.length - 1)) { numZerosToAdd = 6; newNumStr = newNumStr.replace('M', ''); } else { numZerosToAdd = 3; newNumStr = newNumStr.replace('K', ''); } if (Number.isNaN(+newNumStr)) { // Without 'K' or 'M' notation, the string should be converted to a valid number. return numStr; } // Drop the needless characters for the simplicity (e.g., "00.5" to "0.5" or "1,000" to "1000"). newNumStr = (+newNumStr).toString(); // Handle a decimal point if (newNumStr.includes('.')) { decPointPosFromEnd = newNumStr.length - 1 - newNumStr.indexOf('.'); newNumStr = (+newNumStr.replace('.', '')).toString(); } const totalZerosToAdd = numZerosToAdd - decPointPosFromEnd; if (totalZerosToAdd < 0) { // The value is smaller than 1 (e.g. "0.00005K") return numStr; } // Finally, add zeros at the end. newNumStr += '0'.repeat(totalZerosToAdd); return newNumStr; } parsePosition(positionText, prevChr = null) { // Parse chr:position strings... // i.e. chr1:1000 // or chr2:20000 const positionParts = positionText.split(':'); let chr = null; let pos = 0; if (positionParts.length > 1) { chr = positionParts[0]; pos = +this.convertNumberNotation(positionParts[1].replace(/,/g, '')); // chromosome specified } else if (positionParts[0] in this.chromInfo.chrPositions) { // is this an entire chromosome chr = positionParts[0]; pos = 0; if (prevChr !== null) { // this chromosome is part of a range so we actually // want to search to the end of it pos = +this.chromInfo.chromLengths[chr]; } } else { // no it's just a position without a chromosome pos = +this.convertNumberNotation(positionParts[0].replace(/,/g, '')); // no chromosome specified chr = null; if (prevChr) chr = prevChr; } // If chromosome doesn't exit let retPos = null; if (chr === null) { // queries like chr1:1000-2000 chr = prevChr; // no chromosome provided, so this is just a number retPos = pos; } else if (chr in this.chromInfo.chrPositions) { // chromosome provided, everything is fine retPos = this.chromInfo.chrPositions[chr].pos + pos; } // retPos is the genome position of this pair return [chr, pos, retPos]; } matchRangesToLarger(range1, range2) { // if one range is wider than the other, then adjust the other // so that it is just as wide if (range1[1] - range1[0] < range2[1] - range2[0]) { const toExpand = range2[1] - range2[0] - (range1[1] - range1[0]); return [[range1[0] - toExpand / 2, range1[1] + toExpand / 2], range2]; } const toExpand = range1[1] - range1[0] - (range2[1] - range2[0]); return [range1, [range2[0] - toExpand / 2, range2[1] + toExpand / 2]]; } getSearchRange(term) { // Get the genomic regions associated with this term // Example terms: // tp53 // tp53 (nm_000546) // tp53 to adh1b // tp53 (nm_000546) to adh1b if (term.length === 0) { return null; } // shitty ass regex to deal with negative positions // (which aren't even valid genomic coordinates) let parts = term.split('-'); // split on a parts = parts.filter((d) => d.length > 0); let range = null; if (parts[0].indexOf('-') === 0) { parts[0] = parts[0].slice(3, parts[0].length); } if (parts.length > 1) { // calculate the range in one direction let [chr1, chrPos1, genomePos1] = this.parsePosition(parts[0]); let [chr2, chrPos2, genomePos2] = this.parsePosition(parts[1], chr1); const tempRange1 = [genomePos1, genomePos2]; [chr1, chrPos1, genomePos1] = this.parsePosition(parts[1]); [chr2, chrPos2, genomePos2] = this.parsePosition(parts[0], chr1); if (chr1 === null && chr2 !== null) { // somembody entered a string like chr17:1000-2000 // and when we try to search the rever, the first chromosome // is null // we have to pass in the previous chromosome as a prevChrom [chr1, chrPos1, genomePos1] = this.parsePosition(parts[1], chr2); } const tempRange2 = [genomePos1, genomePos2]; // return the wider of the two ranges // e.g. searching for chr1-chr2 vs chr2-chr1 if (tempRange2[1] - tempRange2[0] > tempRange1[1] - tempRange1[0]) return tempRange2; return tempRange1; } // only a locus specified and no range // is the locus an entire chromosome? if (parts[0] in this.chromInfo.chrPositions) { const chromPosition = +this.chromInfo.chrPositions[parts[0]].pos; // if somebody has entered an entire chromosome, we return // it's length as the range range = [ chromPosition, chromPosition + +this.chromInfo.chromLengths[parts[0]], ]; } else { // e.g. ("chr1:540340") const [chr1, chrPos1, pos1] = this.parsePosition(parts[0]); range = [pos1 - 8000000, pos1 + 8000000]; } if (range[0] > range[1]) { return [range[1], range[0]]; } return range; } parseOffset(offsetText) { /** * Convert offset text to a 2D array of offsets * * @param offsetText(string): 14,17:20,22 * * @return offsetArray: [[14,17],[20,22]] */ const parts = offsetText.split(':'); if (parts.length === 0) { return [ [0, 0], [0, 0], ]; } if (parts.length === 1) { const sparts = parts[0].split(','); return [ [+sparts[0], +sparts[1]], [0, 0], ]; } const sparts0 = parts[0].split(','); const sparts1 = parts[1].split(','); return [ [+sparts0[0], +sparts0[1]], [+sparts1[0], +sparts1[1]], ]; } searchPosition(text) { let range1 = null; let range2 = null; text = text.trim(); // remove whitespace from the ends of the string // extract offset const offsetRe = /\[offset (.+?)\]/.exec(text); // the offset is the distance before the first chromosome // or the distance after the last chromosome of the given let offset = [ [0, 0], [0, 0], ]; if (offsetRe) { text = text.replace(offsetRe[0], ''); // offset = this.parseOffset(offsetRe[1]); } const parts = text.split(' & '); if (parts.length > 1) { // we need to move both axes // although it's possible that the first axis will be empty // i.e. someone enters " and p53" // in that case, we only move the second axis and keep the first where it is range1 = this.getSearchRange(parts[0].split(' ')[0]); range2 = this.getSearchRange(parts[1].split(' ')[0]); } else { // we just need to position the first axis range1 = this.getSearchRange(parts[0]); } if (range1 !== null && range2 !== null) { [range1, range2] = this.matchRangesToLarger(range1, range2); } if (range1) { range1[0] += offset[0][0]; range1[1] += offset[0][1]; } if (range2) { range2[0] += offset[1][0]; range2[1] += offset[1][1]; } return [range1, range2]; } } export default SearchField;