From 9372072b90d243a3d0a4c410510bd18c8604e19b Mon Sep 17 00:00:00 2001 From: gonzalofrag Date: Wed, 3 Jul 2024 15:44:40 -0400 Subject: [PATCH] features --- .gitignore | 3 +- load-from-file.html | 10 ++++ sample-data/sample.features.txt | 7 +++ src/Chromosome.js | 5 ++ src/DataSet.js | 3 ++ src/Feature.js | 8 ++++ src/FeatureImporter.js | 58 +++++++++++++++++++++++ src/GenomeMap.js | 4 ++ src/GenotypeCanvas.js | 82 +++++++++++++++++++++++++++++++-- src/flapjack-bytes.js | 20 ++++++++ 10 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 sample-data/sample.features.txt create mode 100644 src/Feature.js create mode 100644 src/FeatureImporter.js diff --git a/.gitignore b/.gitignore index 7121a84..9f80107 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build node_modules package-lock.json -BrAPI.js \ No newline at end of file +BrAPI.js +/nbproject/private/ diff --git a/load-from-file.html b/load-from-file.html index 312937d..8397658 100644 --- a/load-from-file.html +++ b/load-from-file.html @@ -16,6 +16,15 @@
+ +
+
+ + + +
+
+
@@ -33,6 +42,7 @@ mapFileDom: "mapfile", genotypeFileDom: "genofile", phenotypeFileDom: "phenofile", + featureFileDom: "featurefile", overviewWidth: null, overviewHeight: 200, minGenotypeAutoWidth: 600, minGenotypeAutoHeight: 600, }); diff --git a/sample-data/sample.features.txt b/sample-data/sample.features.txt new file mode 100644 index 0000000..5a31665 --- /dev/null +++ b/sample-data/sample.features.txt @@ -0,0 +1,7 @@ +# fjFile = FEATURES +gene1 2H 14.3 16.2 +gene2 2H 20 35 +gene3 2H 49 63 +gene4 2H 34 48 +gene5 2H 20 54 +gene6 2H 30 41 diff --git a/src/Chromosome.js b/src/Chromosome.js index 4b3153e..f52eb38 100644 --- a/src/Chromosome.js +++ b/src/Chromosome.js @@ -4,6 +4,7 @@ export default class Chromosome { this.name = name; this.end = end; this.markers = markers; + this.features = []; this.markers.sort((a, b) => (a.position > b.position ? 1 : -1)); } @@ -11,4 +12,8 @@ export default class Chromosome { markerCount() { return this.markers.length; } + insertFeatures(features){ + this.features = features; + this.features.sort((a, b) => (a.start > b.start ? 1 : -1)); + } } diff --git a/src/DataSet.js b/src/DataSet.js index 012e0d5..d46045a 100644 --- a/src/DataSet.js +++ b/src/DataSet.js @@ -48,6 +48,9 @@ export default class DataSet { markersToRenderOn(chromosomeIndex, markerStart, markerEnd) { return this.genomeMap.markersToRenderOn(chromosomeIndex, markerStart, markerEnd); } + featuresToRenderOn(chromosomeIndex, markerStart, markerEnd) { + return this.genomeMap.featuresToRenderOn(chromosomeIndex, markerStart, markerEnd); + } markerAt(markerIndex) { return this.genomeMap.markerAt(markerIndex); diff --git a/src/Feature.js b/src/Feature.js new file mode 100644 index 0000000..0fa424f --- /dev/null +++ b/src/Feature.js @@ -0,0 +1,8 @@ +export default class Feature { + constructor(name, chromosome, startPosition, endPosition) { + this.name = name; + this.chromosome = chromosome; + this.start = startPosition; + this.end = endPosition; + } +} diff --git a/src/FeatureImporter.js b/src/FeatureImporter.js new file mode 100644 index 0000000..386b091 --- /dev/null +++ b/src/FeatureImporter.js @@ -0,0 +1,58 @@ +//import Marker from './Marker'; +import Feature from './Feature'; +import Chromosome from './Chromosome'; +import GenomeMap from './GenomeMap'; + +export default class FeatureImporter { + constructor() { + this.featureNames = []; + this.featureData = []; + this.chromosomeNames = new Set(); + } + + processFeatureFileLine(line) { + if (line.startsWith('#') || (!line || line.length === 0) || line.startsWith('\t')) { + return; + } + + // Only parse our default map file lines (i.e. not the special fixes for + // exactly specifying the chromosome length) + // http://flapjack.hutton.ac.uk/en/latest/projects_&_data_formats.html#data-sets-maps-and-genotypes + const tokens = line.split('\t'); + if (tokens.length === 4) { + const featureName = tokens[0]; + const chromosome = tokens[1]; + const startPos = tokens[2]; + const endPos = tokens[3]; + + // Keep track of the chromosomes that we've found + this.chromosomeNames.add(chromosome); + + // Create a marker object and add it to our array of markers + const feature = new Feature(featureName, chromosome, parseFloat(startPos.replace(/,/g, ''), 10), parseFloat(endPos.replace(/,/g, ''), 10)); + this.featureData.push(feature); + } + } + + createFeatures(genomeMap) { + genomeMap.chromosomes.forEach(chromosome => { + const chromosomeFeatures = this.featureData.filter(m => m.chromosome === chromosome.name); + chromosome.insertFeatures(chromosomeFeatures); + }); + return genomeMap; + } + + parseFile(fileContents, genomeMap) { + const features = fileContents.split(/\r?\n/); + for (let feature = 0; feature < features.length; feature += 1) { + this.processFeatureFileLine(features[feature]); + } + + const map = this.createFeatures(genomeMap); + + return map; + } + + // A method which converts BrAPI markerpositions into Flapjack markers for + // rendering + } \ No newline at end of file diff --git a/src/GenomeMap.js b/src/GenomeMap.js index cba56af..c9cff22 100644 --- a/src/GenomeMap.js +++ b/src/GenomeMap.js @@ -66,6 +66,10 @@ export default class GenomeMap { }; } + featuresToRenderOn(chromosomeIndex, dataStart, dataEnd) { + return this.chromosomes[chromosomeIndex]; + } + markerAt(dataIndex) { const foundChromosomes = this.intervalTree.search(dataIndex, dataIndex); const chromosome = foundChromosomes[0]; diff --git a/src/GenotypeCanvas.js b/src/GenotypeCanvas.js index f8207da..73b796b 100644 --- a/src/GenotypeCanvas.js +++ b/src/GenotypeCanvas.js @@ -11,6 +11,22 @@ export default class GenotypeCanvas { this.canvas.style.display = "block"; this.drawingContext = this.canvas.getContext('2d'); + this.featureCanvas = document.createElement('canvas'); + this.featureCanvas.width = width; + this.featureCanvas.height = height; + this.featureCanvas.style.display = "block"; + this.featureDrawingContext = this.canvas.getContext('2d'); + + // var featureCanvas = document.getElementById('featureCanvas'); + + // if (!featureCanvas || !featureCanvas.getContext) { + // console.error("Canvas not supported or ID not found: featureCanvas"); + // return; + // } + // var ctx1 = canvas1.getContext('2d'); + // ctx1.fillStyle = 'red'; + // ctx1.fillRect(10, 10, 50, 50); + this.backBuffer = document.createElement('canvas'); this.backBuffer.width = width; this.backBuffer.height = height; @@ -31,7 +47,7 @@ export default class GenotypeCanvas { this.verticalScrollbar = new ScrollBar(width, this.alleleCanvasHeight() + this.scrollbarHeight, this.scrollbarWidth, this.alleleCanvasHeight(), true); this.horizontalScrollbar = new ScrollBar(this.alleleCanvasWidth(), - height, this.alleleCanvasWidth(), this.scrollbarHeight, false); + height, this.alleleCanvasWidth(), this.scrollbarHeight, false); this.translatedX = 0; this.translatedY = 0; @@ -158,7 +174,12 @@ export default class GenotypeCanvas { return mapMarkerPos; } + calcMapFeaturePos(position, mapScaleFactor, drawStart) { + let mapFeaturePos = ((position) * (mapScaleFactor)); + mapFeaturePos = drawStart > 0 ? mapFeaturePos + drawStart : mapFeaturePos; + return mapFeaturePos; + } highlightMarker(dataWidth, markerStart, markerEnd, xPos) { if (this.markerUnderMouse) { const renderData = this.dataSet.markersToRenderOn(this.selectedChromosome, markerStart, markerEnd); @@ -180,12 +201,12 @@ export default class GenotypeCanvas { const lastMarkerPos = chromosome.markers[renderData.firstMarker + dW].position; const scaleFactor = lastMarkerPos == 0 ? 0 /* hack for cases where variants are not positioned */ : chromosomeWidth / (lastMarkerPos - firstMarkerPos); - this.highlightMarkerName(firstMarkerPos, scaleFactor, scaleFactor == 0 ? xPos : drawStart); + //this.highlightMarkerName(firstMarkerPos, scaleFactor, scaleFactor == 0 ? xPos : drawStart); this.drawingContext.save(); // Translate to the correct position to draw the map - this.drawingContext.translate(this.alleleCanvasXOffset, 10); + this.drawingContext.translate(this.alleleCanvasXOffset, 18); xPos += (this.boxSize / 2); this.drawingContext.strokeStyle = '#F00'; @@ -389,6 +410,13 @@ export default class GenotypeCanvas { this.renderScrollbars(); } + renderFeature(mapCanvas, feature, mapScaleFactor, drawStart){ + const mapFeatureStartPos = this.calcMapFeaturePos(feature.start, mapScaleFactor, drawStart); + const mapFeatureEndPos = this.calcMapFeaturePos(feature.end, mapScaleFactor, drawStart); + mapCanvas.fillRect(mapFeatureStartPos, 1, mapFeatureEndPos, 10); + console.log("renderFeature",feature,mapFeatureStartPos,mapFeatureEndPos); + } + renderMarker(mapCanvas, marker, genoMarkerPos, firstMarkerPos, mapScaleFactor, drawStart) { const mapMarkerPos = this.calcMapMarkerPos(marker, firstMarkerPos, mapScaleFactor, drawStart); @@ -432,12 +460,46 @@ export default class GenotypeCanvas { this.renderMarker(this.backContext, marker, xPos, firstMarkerPos, scaleFactor, scaleFactor == 0 ? xPos : drawStart); } } + renderFeatures(renderData,featureData) { + const chrStart = 0; + const chrEnd = this.dataSet.genomeMap.chromosomes[this.selectedChromosome].markerCount() * this.boxSize; + const drawStart = -this.translatedX; + + const chromosome = this.dataSet.genomeMap.chromosomes[this.selectedChromosome]; + + const potentialWidth = drawStart > 0 ? this.alleleCanvasWidth() - drawStart : this.alleleCanvasWidth(); + const chromosomeWidth = Math.min(chrEnd - this.translatedX, potentialWidth, chrEnd - chrStart); + + // The data array can have too many markers in it due to the gaps between + // chromosomes, this is a fudge to ensure we don't try to draw too many markers + const chromosomeMarkerWidth = Math.max(0, Math.floor(chromosomeWidth / this.boxSize)); + const dataWidth = Math.min(renderData.lastMarker - renderData.firstMarker, chromosomeMarkerWidth); + + const firstMarkerPos = chromosome.markers[renderData.firstMarker].position; + const lastMarkerPos = chromosome.markers[renderData.firstMarker + dataWidth].position; + const scaleFactor = lastMarkerPos == 0 ? 0 /* hack for cases where variants are not positioned */ : chromosomeWidth / (lastMarkerPos - firstMarkerPos); + + // this.dataSet.genomeMap.chromosomes[this.selectedChromosome].features.forEach(feature => { + // this.renderFeature(this.backContext, feature, scaleFactor, drawStart); + // }) + featureData.features.forEach(feature => { + this.renderFeature(this.backContext, feature, scaleFactor, drawStart); + }) + + // for (let markerIndex = renderData.firstMarker; markerIndex <= renderData.lastMarker; markerIndex += 1) { + // const marker = this.dataSet.genomeMap.chromosomes[this.selectedChromosome].markers[markerIndex]; + // let xPos = drawStart + (markerIndex * this.boxSize); + // xPos += (this.boxSize / 2); + // this.renderMarker(this.backContext, marker, xPos, firstMarkerPos, scaleFactor, scaleFactor == 0 ? xPos : drawStart); + // } + } renderChromosome(chromosomeData) { const width = this.dataSet.genomeMap.chromosomes[this.selectedChromosome].markerCount() * this.boxSize; this.backContext.strokeRect(-this.translatedX, 1, width, 10); } + renderMap(markerStart, markerEnd) { this.backContext.save(); // Set the line style for drawing the map and markers @@ -451,13 +513,25 @@ export default class GenotypeCanvas { this.backContext.clip(region); // Translate to the correct position to draw the map - this.backContext.translate(this.alleleCanvasXOffset, 10); + this.backContext.translate(this.alleleCanvasXOffset, 18); const renderData = this.dataSet.markersToRenderOn(this.selectedChromosome, markerStart, markerEnd); + const featureData = this.dataSet.featuresToRenderOn(this.selectedChromosome, markerStart, markerEnd); this.renderChromosome(renderData); this.renderMarkers(renderData); + this.backContext.translate(this.alleleCanvasXOffset, -18); + this.backContext.fillStyle = 'blue'; + + + // const featureData = [ + // {start:10,end:20},{start:40,end:60} + // ]; + this.renderFeatures(renderData, featureData); + + this.backContext.translate(this.alleleCanvasXOffset, 36); + this.backContext.strokeStyle = 'gray'; this.backContext.restore(); } diff --git a/src/flapjack-bytes.js b/src/flapjack-bytes.js index ca2f26c..b066e9f 100644 --- a/src/flapjack-bytes.js +++ b/src/flapjack-bytes.js @@ -3,6 +3,7 @@ import GenotypeCanvas from './GenotypeCanvas'; import OverviewCanvas from './OverviewCanvas'; import CanvasController from './CanvasController'; import MapImporter from './MapImporter'; +import FeatureImporter from './FeatureImporter'; import GenotypeImporter from './GenotypeImporter'; import PhenotypeImporter from './PhenotypeImporter'; import SimilarityLineSort from './sort/SimilarityLineSort'; @@ -30,6 +31,7 @@ export default function GenotypeRenderer() { const boxSize = 17; let genomeMap; + let genomeFeatures; let phenotypes; let traits; let dataSet; @@ -1148,6 +1150,13 @@ export default function GenotypeRenderer() { setProgressBarLabel("Loading file contents..."); let loadingPromises = []; + let featurePromise; + + if (config.featureFileDom !== undefined){ + const featureFile = document.getElementById(config.featureFileDom.replace('#', '')).files[0]; + featurePromise = loadFromFile(featureFile); + loadingPromises.push(featurePromise); + } if (config.mapFileDom !== undefined){ const mapFile = document.getElementById(config.mapFileDom.replace('#', '')).files[0]; @@ -1157,6 +1166,16 @@ export default function GenotypeRenderer() { mapPromise = mapPromise.then((result) => { const mapImporter = new MapImporter(); genomeMap = mapImporter.parseFile(result); + if (featurePromise) { + // Load feature data + featurePromise = featurePromise.then((result) => { + const featureImporter = new FeatureImporter(); + genomeFeatures = featureImporter.parseFile(result, genomeMap); + }).catch(reason => { + console.error(reason); + genomeFeatures = undefined; + }); + } }).catch(reason => { console.error(reason); genomeMap = undefined; @@ -1165,6 +1184,7 @@ export default function GenotypeRenderer() { loadingPromises.push(mapPromise); } + if (config.phenotypeFileDom !== undefined){ const phenotypeFile = document.getElementById(config.phenotypeFileDom.replace('#', '')).files[0]; let phenotypePromise = loadFromFile(phenotypeFile);