From 8695b0e1338fa7cf81ea970c57e21ed362bb8c71 Mon Sep 17 00:00:00 2001 From: Jon Rumsey Date: Sun, 17 Jul 2022 17:19:39 -0700 Subject: [PATCH 001/113] Removed fluid from container to shore up header component overflow. --- src/components/HeaderForm.js | 68 ++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index dd774fb8..f315452e 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -257,7 +257,7 @@ class HeaderForm extends Component { dataType: dataTypes.FILE_UPLOAD, error: this.state.error, }; - this.setState(newState, () => {}); + this.setState(newState, () => { }); } else if (value === dataTypes.MOUNTED_FILES) { this.setState((state) => { return { @@ -462,8 +462,12 @@ class HeaderForm extends Component { return (
- {errorDiv} - + + + + {errorDiv} + + Logo @@ -535,34 +539,36 @@ class HeaderForm extends Component { /> )} - { - this.setState({ fileSizeAlert: false }); - }} - className="mt-3" - > - File size too big! - You may only upload files with a maximum size of{" "} - {MAX_UPLOAD_SIZE_DESCRIPTION}. - - {examplesFlag ? ( - - ) : ( - - )} + + { + this.setState({ fileSizeAlert: false }); + }} + className="mt-3" + > + File size too big! + You may only upload files with a maximum size of{" "} + {MAX_UPLOAD_SIZE_DESCRIPTION}. + + {examplesFlag ? ( + + ) : ( + + )} + From a07cda396cc0e782e51e13bbc78558fb557fe12e Mon Sep 17 00:00:00 2001 From: ducku Date: Fri, 9 Jun 2023 18:55:39 -0700 Subject: [PATCH 002/113] track picker integration in headerform --- src/components/HeaderForm.js | 118 +++++++++++++++++++--------------- src/components/RegionInput.js | 1 + src/components/TrackPicker.js | 2 + src/server.mjs | 10 ++- 4 files changed, 74 insertions(+), 57 deletions(-) diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index 1be130ff..1ace6fd5 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -10,6 +10,8 @@ import MountedDataFormRow from "./MountedDataFormRow"; import FileUploadFormRow from "./FileUploadFormRow"; import ExampleSelectButtons from "./ExampleSelectButtons"; import RegionInput from "./RegionInput"; +import TrackPicker from "./TrackPicker"; +import SelectionDropdown from "./SelectionDropdown"; import { parseRegion, stringifyRegion } from "../common.mjs"; // See src/Types.ts @@ -65,10 +67,7 @@ const EMPTY_STATE = { // These ones are for selecting entire files and need to be preserved when // switching dataType. - graphSelectOptions: ["none"], - gbwtSelectOptions: ["none"], - gamSelectOptions: ["none"], - bedSelectOptions: ["none"], + fileSelectOptions: [], }; // Creates track to be stored in ViewTarget @@ -139,6 +138,7 @@ function viewTargetsEqual(currViewTarget, nextViewTarget) { } + class HeaderForm extends Component { state = EMPTY_STATE; componentDidMount() { @@ -250,16 +250,19 @@ class HeaderForm extends Component { }); } - getTrackFile = (type, index) => { + getTrackFile = (tracks, type, index) => { // Get the file used in the nth track of the gicen type, or "none" if no // such track exists. let seenTracksOfType = 0; - for (const key in this.state.tracks) { - let track = this.state.tracks[key]; - if (track.files[0].type === type) { + for (const key in tracks) { + let track = tracks[key]; + if (track === -1) { + continue; + } + if (track.trackFile.type === type) { if (seenTracksOfType === index) { // This is the one. Return its filename. - return track.files[0].name; + return track.trackFile.name; } seenTracksOfType++; } @@ -278,15 +281,12 @@ class HeaderForm extends Component { "Content-Type": "application/json", }, }); - if (json.graphFiles === undefined) { + if (json.files.length === 0) { // We did not get back a graph, only (possibly) an error. const error = json.error || "Server did not return a list of mounted filenames."; this.setState({ error: error }); } else { - json.graphFiles.unshift("none"); - json.gbwtFiles.unshift("none"); - json.gamIndices.unshift("none"); json.bedFiles.unshift("none"); if (this.state.dataPath === "mounted") { @@ -306,9 +306,7 @@ class HeaderForm extends Component { } } return { - graphSelectOptions: json.graphFiles, - gbwtSelectOptions: json.gbwtFiles, - gamSelectOptions: json.gamIndices, + fileSelectOptions: json.files, bedSelectOptions: json.bedFiles, bedSelect, }; @@ -316,9 +314,7 @@ class HeaderForm extends Component { } else { this.setState((state) => { return { - graphSelectOptions: json.graphFiles, - gbwtSelectOptions: json.gbwtFiles, - gamSelectOptions: json.gamIndices, + fileSelectOptions: json.files, bedSelectOptions: json.bedFiles, }; }); @@ -370,6 +366,7 @@ class HeaderForm extends Component { }; getPathNames = async (graphFile, dataPath) => { + console.log("getting path names for ", graphFile, dataPath); this.setState({ error: null }); try { const json = await fetchAndParse(`${this.props.apiUrl}/getPathNames`, { @@ -386,6 +383,7 @@ class HeaderForm extends Component { throw new Error("Server did not send back an array of path names"); } this.setState((state) => { + console.log("setting path name", pathNames); return { pathNames: pathNames, }; @@ -516,28 +514,33 @@ class HeaderForm extends Component { } this.setState({ region: coords }); }; - handleInputChange = (event) => { + + handleInputChange = (newTracks) => { + this.setState((state) => { + let newState = Object.assign({}, state); + newState.tracks = newTracks; + console.log('Set result: ' + JSON.stringify(newTracks)); + return newState; + }); + + // update path names + const graphFile = this.getTrackFile(newTracks, fileTypes.GRAPH, 0); + if (graphFile && graphFile !== "none"){ + this.getPathNames(graphFile, this.state.dataPath); + } + }; + + handleBedChange = (event) => { const id = event.target.id; const value = event.target.value; this.setState({ [id]: value }); - if (id === "graphSelect") { - if (value && value !== "none") { - this.getPathNames(value, this.state.dataPath); - } - this.setTrackFile(fileTypes.GRAPH, 0, value); - } else if (id === "gbwtSelect") { - this.setTrackFile(fileTypes.HAPLOTYPE, 0, value); - } else if (id === "gamSelect") { - this.setTrackFile(fileTypes.READ, 0, value); - } else if (id === "gam2Select") { - this.setTrackFile(fileTypes.READ, 1, value); - } else if (id === "bedSelect") { - if (value !== "none") { - this.getBedRegions(value, this.state.dataPath); - } - this.setState({ bedFile: value }); + + if (value !== "none") { + this.getBedRegions(value, this.state.dataPath); } - }; + this.setState({ bedFile: value }); + + } // Budge the region left or right by the given negative or positive fraction // of its width. @@ -652,8 +655,8 @@ class HeaderForm extends Component { const examplesFlag = this.state.dataType === dataTypes.EXAMPLES; console.log( - "Rendering header form with graphSelectOptions: ", - this.state.graphSelectOptions + "Rendering header form with fileSelectOptions: ", + this.state.fileSelectOptions ); return ( @@ -687,18 +690,31 @@ class HeaderForm extends Component {   {mountedFilesFlag && ( - + + + + + + +   + + )} {!examplesFlag && ( { // Generate autocomplete options for regions from regionInfo // Add : to pathNames + console.log("rendering with pathnames: ", pathNames); const pathNamesColon = pathNames.map((name) => name + ":"); const pathsWithRegion = []; diff --git a/src/components/TrackPicker.js b/src/components/TrackPicker.js index ab118cef..969c977f 100644 --- a/src/components/TrackPicker.js +++ b/src/components/TrackPicker.js @@ -5,6 +5,7 @@ import { Button } from 'reactstrap' import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faList } from '@fortawesome/free-solid-svg-icons'; import PopupDialog from './PopupDialog.js'; +import config from "./../config.json"; export const TrackPicker = ({ tracks, // expects a trackList, same as trackListDisplay @@ -43,6 +44,7 @@ TrackPicker.propTypes = { } TrackPicker.defaultProps = { + tracks: {1: config.defaultTrackProps}, availableColors: [], onChange: () => {} } diff --git a/src/server.mjs b/src/server.mjs index cf6a342c..b3c57eb2 100644 --- a/src/server.mjs +++ b/src/server.mjs @@ -956,9 +956,7 @@ function pickDataPath(reqDataPath) { api.get("/getFilenames", (req, res) => { console.log("received request for filenames"); const result = { - graphFiles: [], - gbwtFiles: [], - gamIndices: [], + files: [], // store a list of file object, excluding bed files, { name: string; type: filetype;} bedFiles: [], }; @@ -966,13 +964,13 @@ api.get("/getFilenames", (req, res) => { // list files in folder fs.readdirSync(MOUNTED_DATA_PATH).forEach((file) => { if (endsWithExtensions(file, GRAPH_EXTENSIONS)) { - result.graphFiles.push(file); + result.files.push({"name": file, "type": "graph"}); } if (endsWithExtensions(file, HAPLOTYPE_EXTENSIONS)) { - result.gbwtFiles.push(file); + result.files.push({"name": file, "type": "haplotype"}); } if (file.endsWith(".sorted.gam")) { - result.gamIndices.push(file); + result.files.push({"name": file, "type": "read"}); } if (file.endsWith(".bed")) { result.bedFiles.push(file); From 85566ea3d5bfcb5629e4dcca1b599d9e03675bfd Mon Sep 17 00:00:00 2001 From: ducku Date: Fri, 9 Jun 2023 22:34:46 -0700 Subject: [PATCH 003/113] merge track system into mounted files, coloring still broken --- src/App.js | 13 ++--- src/Types.ts | 12 ++++- src/components/CustomizationAccordion.js | 2 +- src/components/HeaderForm.js | 61 ++++++++++++++++++------ src/components/RadioRow.js | 2 +- src/components/TrackPicker.js | 1 - src/components/TrackPickerDisplay.js | 3 -- src/components/TubeMapContainer.js | 42 ++++++++-------- src/config.json | 18 +++---- src/server.mjs | 15 +++--- src/util/tubemap.js | 3 ++ 11 files changed, 108 insertions(+), 64 deletions(-) diff --git a/src/App.js b/src/App.js index a3c87f4c..a6f73746 100644 --- a/src/App.js +++ b/src/App.js @@ -21,14 +21,16 @@ const EXAMPLE_TRACKS = [ {"files": [{"type": "read", "name": "fakeReads"}]} ]; -function addTracksToColorSchemes(tracks, schemes) { +function getColorSchemesFromTrack(tracks) { - console.log("Initializing coloring for tracks: ", tracks, "combining with:", schemes); + let schemes = []; for (const key in tracks) { if (schemes[key] === undefined) { // We need to adopt a color scheme - if (tracks[key].files[0].type === "read") { + if (tracks[key].trackColorSettings !== undefined) { + schemes[key] = tracks[key].trackColorSettings; + } else if (tracks[key].trackFile.type === "read") { schemes[key] = {...config.defaultReadColorPalette}; } else { schemes[key] = {...config.defaultHaplotypeColorPalette}; @@ -61,7 +63,7 @@ class App extends Component { showReads: true, showSoftClips: true, colorReadsByMappingQuality: false, - colorSchemes: addTracksToColorSchemes(this.defaultViewTarget.tracks, []), + colorSchemes: getColorSchemesFromTrack(this.defaultViewTarget.tracks), mappingQualityCutoff: 0, }, }; @@ -97,8 +99,7 @@ class App extends Component { this.setState((state) => { // Make sure we have color schemes. - let newColorSchemes = state.visOptions.colorSchemes.slice(); - addTracksToColorSchemes(newViewTarget.tracks, newColorSchemes); + let newColorSchemes = getColorSchemesFromTrack(newViewTarget.tracks); console.log("Adopting color schemes: ", newColorSchemes) diff --git a/src/Types.ts b/src/Types.ts index b87344e8..f623193c 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -22,7 +22,13 @@ type file = { // Contains information necessary to make a track type track = { - files: Array + trackFile: file; + trackType: filetype; + trackColorSettings: ColorScheme; +} + +type tracks = { + trackID: track; } // Describes something the Tube Map can look at, specifically a region and the files the region is in. @@ -82,4 +88,6 @@ type ColorScheme = { // Stores the assigned colorschemes of all tracks // Entries correspond to their track counterpart, e.g colorSchemes[0] corresponds to tracks[0] -type colorSchemes = Array; \ No newline at end of file +type colorSchemes = { + trackID: ColorScheme; +} \ No newline at end of file diff --git a/src/components/CustomizationAccordion.js b/src/components/CustomizationAccordion.js index d5f9e506..65d25ce2 100644 --- a/src/components/CustomizationAccordion.js +++ b/src/components/CustomizationAccordion.js @@ -57,7 +57,7 @@ class VisualizationOptions extends Component { for (let key in this.props.tracks) { // Generate settings controls for each track let track = this.props.tracks[key]; - let type = track.files[0].type; + let type = track.trackFile.type; if (type === "graph") { trackSettingsList.push( diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index 1ace6fd5..ba36562e 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -92,9 +92,30 @@ function tracksEqual(curr, next) { return false; } - if (curr.files.length !== next.files.length) { - return false; + + + const curr_file = curr.trackFile.name; + const next_file = next.trackFile.name; + + const curr_settings = curr.trackColorSettings; + const next_settings = curr.trackColorSettings; + + // check if color settings are equal + if (curr_settings.mainPalette !== next_settings.mainPalette || + curr_settings.auxPalette !== next_settings.auxPalette || + curr_settings.colorReadsByMappingQuality !== next_settings.colorReadsByMappingQuality) { + + console.log("tracks have differnt color settings"); + return false; + } + + //count falsy file names as the same + if ((!curr_file && !next_file) || curr_file === next_file) { + return true; } + return false; + + /* //loop through file names to see if they're equal for (let i = 0; i < curr.files.length; i++) { const curr_file = curr.files[i].name; @@ -106,24 +127,36 @@ function tracksEqual(curr, next) { } return false; } + return true; + */ } // Checks if two view targets are the same. They are the same if they have the // same tracks and the same region. function viewTargetsEqual(currViewTarget, nextViewTarget) { + // Update if one is undefined and the other isn't if ((currViewTarget === undefined) !== (nextViewTarget === undefined)) { return false; } // Update if view target tracks are not equal - if (currViewTarget.tracks.length !== nextViewTarget.tracks.length) { + if (Object.keys(currViewTarget.tracks).length !== Object.keys(nextViewTarget.tracks).length) { // Different lengths so not equal return false; } - for (let i = 0; i < currViewTarget.tracks.length; i++) { - if (!tracksEqual(currViewTarget.tracks[i], nextViewTarget.tracks[i])){ + + for (const key in currViewTarget.tracks) { + const currTrack = currViewTarget.tracks[key]; + const nextTrack = nextViewTarget.tracks[key]; + + // if the key doesn't exist in the other track + if (!currTrack || !nextTrack) { + return false; + } + + if (!tracksEqual(currTrack, nextTrack)) { // Different tracks so not equal return false; } @@ -175,10 +208,10 @@ class HeaderForm extends Component { this.getBedRegions(bedSelect, dataPath); } for (const key in ds.tracks) { - if (ds.tracks[key].files[0].type === fileTypes.GRAPH) { + if (ds.tracks[key].trackFile.type === fileTypes.GRAPH) { // Load the paths for any graph tracks console.log("Get path names for track: ", ds.tracks[key]); - this.getPathNames(ds.tracks[key].files[0].name, dataPath); + this.getPathNames(ds.tracks[key].trackFile.name, dataPath); } } this.setState((state) => { @@ -210,13 +243,13 @@ class HeaderForm extends Component { let maxKey = -1; for (const key in state.tracks) { let track = state.tracks[key]; - if (track.files[0].type === type) { + if (track.trackFile.type === type) { console.log("See file " + seenTracksOfType + " of right type"); if (seenTracksOfType === index) { if (file !== "none") { // We want to adjust it, so keep a modified copy of it let newTrack = JSON.parse(JSON.stringify(track)); - newTrack.files[0].name = file; + newTrack.trackFile.name = file; newTracks[key] = newTrack; } // If the file is "none" we drop the track. @@ -298,11 +331,11 @@ class HeaderForm extends Component { this.getBedRegions(bedSelect, "mounted"); } for (const key in state.tracks) { - if (state.tracks[key].files[0].type === fileTypes.GRAPH) { + if (state.tracks[key].trackFile.type === fileTypes.GRAPH) { // Load the paths for any graph tracks. // TODO: Do we need to do this now? console.log("Get path names for track: ", state.tracks[key]); - this.getPathNames(state.tracks[key].files[0].name, "mounted"); + this.getPathNames(state.tracks[key].trackFile.name, "mounted"); } } return { @@ -366,7 +399,6 @@ class HeaderForm extends Component { }; getPathNames = async (graphFile, dataPath) => { - console.log("getting path names for ", graphFile, dataPath); this.setState({ error: null }); try { const json = await fetchAndParse(`${this.props.apiUrl}/getPathNames`, { @@ -383,7 +415,6 @@ class HeaderForm extends Component { throw new Error("Server did not send back an array of path names"); } this.setState((state) => { - console.log("setting path name", pathNames); return { pathNames: pathNames, }; @@ -436,10 +467,10 @@ class HeaderForm extends Component { this.setState({ regionInfo: {} }); } for (const key in ds.tracks) { - if (ds.tracks[key].files[0].type === fileTypes.GRAPH) { + if (ds.tracks[key].trackFile.type === fileTypes.GRAPH) { // Load the paths for any graph tracks. console.log("Get path names for track: ", ds.tracks[key]); - this.getPathNames(ds.tracks[key].files[0].name, dataPath); + this.getPathNames(ds.tracks[key].trackFile.name, dataPath); } } this.setState({ diff --git a/src/components/RadioRow.js b/src/components/RadioRow.js index 601286fb..6eb2da68 100644 --- a/src/components/RadioRow.js +++ b/src/components/RadioRow.js @@ -51,7 +51,7 @@ class RadioRow extends Component { }); return ( - {this.props.rowHeading}: + {this.props.rowHeading}: {colorRadios} ); diff --git a/src/components/TrackPicker.js b/src/components/TrackPicker.js index 969c977f..4995b6ac 100644 --- a/src/components/TrackPicker.js +++ b/src/components/TrackPicker.js @@ -45,7 +45,6 @@ TrackPicker.propTypes = { TrackPicker.defaultProps = { tracks: {1: config.defaultTrackProps}, - availableColors: [], onChange: () => {} } diff --git a/src/components/TrackPickerDisplay.js b/src/components/TrackPickerDisplay.js index 3d2e4592..86ff9708 100644 --- a/src/components/TrackPickerDisplay.js +++ b/src/components/TrackPickerDisplay.js @@ -80,8 +80,6 @@ export const TrackPickerDisplay = ({ // call onChange if the track list is valid and changes have been made if (validTrackList && JSON.stringify(newTrackList) !== JSON.stringify(tracks)) { - console.log("tracks", tracks); - console.log("trackChanges", trackListChanges); console.log("calling Track Picker Display onChange with ", newTrackList); onChange(newTrackList); // clear out pending changes @@ -120,7 +118,6 @@ TrackPickerDisplay.propTypes = { } TrackPickerDisplay.defaultProps = { - availableColors: [], onChange: () => {} } diff --git a/src/components/TubeMapContainer.js b/src/components/TubeMapContainer.js index ff796ab9..f52c3419 100644 --- a/src/components/TubeMapContainer.js +++ b/src/components/TubeMapContainer.js @@ -79,10 +79,10 @@ class TubeMapContainer extends Component { // to be changed, add options to change colorReadsByMappingQuality individually tubeMap.setColorReadsByMappingQualityFlag(visOptions.colorReadsByMappingQuality); - for (let i = 0; i < visOptions.colorSchemes.length; i++) { - // update tubemap colors - tubeMap.setColorSet(i, visOptions.colorSchemes[i]); + for (const trackID in visOptions.colorSchemes) { + tubeMap.setColorSet(trackID, visOptions.colorSchemes[trackID]); } + tubeMap.setMappingQualityCutoff(visOptions.mappingQualityCutoff); } @@ -167,24 +167,25 @@ class TubeMapContainer extends Component { let graphTrackID = null; // And the haplotype track number if any let haplotypeTrackID = null; - for (let i = 0; i < this.props.viewTarget.tracks.length; i++) { + + console.log("getting track ids"); + for (const i in this.props.viewTarget.tracks) { const track = this.props.viewTarget.tracks[i]; - for (const file of track.files) { - if (file.type === "read") { - //add track index to array if the track contains a gam file - readTrackIDs.push(i); - break; - } - if (file.type === "graph") { - // Or note if it is a graph (one allowed) - graphTrackID = i; - break; - } - if (file.type === "haplotype") { - // Or a collection of haplotypes (one allowed) - haplotypeTrackID = i; - break; - } + console.log(track); + if (track.trackFile.type === "read") { + //add track index to array if the track contains a gam file + readTrackIDs.push(i); + break; + } + if (track.trackFile.type === "graph") { + // Or note if it is a graph (one allowed) + graphTrackID = i; + break; + } + if (track.trackFile.type === "haplotype") { + // Or a collection of haplotypes (one allowed) + haplotypeTrackID = i; + break; } } @@ -202,6 +203,7 @@ class TubeMapContainer extends Component { // For each returned list of reads from a file, convert all those reads to tube map format. // Include total read count to prevent duplicate ids. // Also include the source track's ID. + console.log("readTrackIDs", readTrackIDs); let newReads = tubeMap.vgExtractReads(nodes, tracks, gam, totalReads, readTrackIDs[readsArr.length]); readsArr.push(newReads); totalReads += newReads.length; diff --git a/src/config.json b/src/config.json index 0b1f947f..22bdaa00 100644 --- a/src/config.json +++ b/src/config.json @@ -4,8 +4,8 @@ { "name": "snp1kg-BRCA1", "tracks": [ - {"files": [{"type": "graph", "name": "snp1kg-BRCA1.vg.xg"}]}, - {"files": [{"type": "read", "name": "NA12878-BRCA1.sorted.gam"}]} + {"trackFile": {"type": "graph", "name": "snp1kg-BRCA1.vg.xg"}}, + {"trackFile": {"type": "read", "name": "NA12878-BRCA1.sorted.gam"}} ], "dataPath": "default", "region": "17:1-100", @@ -15,8 +15,8 @@ { "name": "vg \"small\" example", "tracks": [ - {"files": [{"type": "graph", "name": "x.vg.xg"}]}, - {"files": [{"type": "haplotype", "name": "x.vg.gbwt"}]} + {"trackFile": {"type": "graph", "name": "x.vg.xg"}}, + {"trackFile": {"type": "haplotype", "name": "x.vg.gbwt"}} ], "dataPath": "mounted", "dataType": "built-in", @@ -25,8 +25,8 @@ { "name": "cactus", "tracks": [ - {"files": [{"type": "graph", "name": "cactus.vg.xg"}]}, - {"files": [{"type": "read", "name": "cactus-NA12879.sorted.gam"}]} + {"trackFile": {"type": "graph", "name": "cactus.vg.xg"}}, + {"trackFile": {"type": "read", "name": "cactus-NA12879.sorted.gam"}} ], "dataPath": "mounted", "bedFile": "cactus.bed", @@ -36,9 +36,9 @@ { "name": "cactus multiple reads", "tracks": [ - {"files": [{"type": "graph", "name": "cactus.vg.xg"}]}, - {"files": [{"type": "read", "name": "cactus0_10.sorted.gam"}]}, - {"files": [{"type": "read", "name": "cactus10_20.sorted.gam"}]} + {"trackFile": {"type": "graph", "name": "cactus.vg.xg"}}, + {"trackFile": {"type": "read", "name": "cactus0_10.sorted.gam"}}, + {"trackFile": {"type": "read", "name": "cactus10_20.sorted.gam"}} ], "dataPath": "mounted", "bedFile": "cactus.bed", diff --git a/src/server.mjs b/src/server.mjs index b3c57eb2..1f075216 100644 --- a/src/server.mjs +++ b/src/server.mjs @@ -218,10 +218,8 @@ function endsWithExtensions(file, extensions) { // returns the file name of the specified type in that track // returns falsy value if file type is not found function getFileFromType(track, type) { - for (const file of track.files) { - if (file.type == type) { - return file.name; - } + if (track.trackFile.type == type) { + return track.trackFile.name; } return "none"; } @@ -232,8 +230,8 @@ function getFileFromType(track, type) { // // This is a fancy ES6 generator. function* eachFileOfType(tracks, type) { - for (const track of tracks) { - const file = getFileFromType(track, type); + for (const key in tracks) { + const file = getFileFromType(tracks[key], type); if (file && file !== "none") { yield file; } @@ -295,6 +293,11 @@ api.post("/getChunkedData", (req, res, next) => { let gamFiles = getGams(req.body.tracks); + console.log("graphFile ", graphFile); + console.log("gbwtFile ", gbwtFile); + console.log("bedFile ", bedFile); + console.log("gamFiles ", gamFiles); + req.withGam = true; if (!gamFiles || !gamFiles.length) { req.withGam = false; diff --git a/src/util/tubemap.js b/src/util/tubemap.js index 88d191e0..a3ddbb0a 100644 --- a/src/util/tubemap.js +++ b/src/util/tubemap.js @@ -2349,6 +2349,9 @@ function getColorSet(colorSetName) { function generateTrackColor(track, highlight) { + //console.log("color schemes", config.colorSchemes); + //console.log("source ID", track.sourceTrackID); + if (typeof highlight === "undefined") highlight = "plain"; let trackColor; From 9ab01f6296a3875f2a1bef01ef27dfd9e1e8e34f Mon Sep 17 00:00:00 2001 From: ducku Date: Fri, 9 Jun 2023 22:52:19 -0700 Subject: [PATCH 004/113] fix read coloring --- src/components/TubeMapContainer.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/TubeMapContainer.js b/src/components/TubeMapContainer.js index f52c3419..bdad4b1a 100644 --- a/src/components/TubeMapContainer.js +++ b/src/components/TubeMapContainer.js @@ -168,24 +168,21 @@ class TubeMapContainer extends Component { // And the haplotype track number if any let haplotypeTrackID = null; - console.log("getting track ids"); + console.log("getting viewTarget ", this.props.viewTarget); for (const i in this.props.viewTarget.tracks) { const track = this.props.viewTarget.tracks[i]; console.log(track); if (track.trackFile.type === "read") { //add track index to array if the track contains a gam file readTrackIDs.push(i); - break; } if (track.trackFile.type === "graph") { // Or note if it is a graph (one allowed) graphTrackID = i; - break; } if (track.trackFile.type === "haplotype") { // Or a collection of haplotypes (one allowed) haplotypeTrackID = i; - break; } } From 1caf11ece33219a6366e049dad3f01ac80abd170 Mon Sep 17 00:00:00 2001 From: ducku Date: Tue, 13 Jun 2023 21:31:50 -0700 Subject: [PATCH 005/113] track compare typo --- src/components/HeaderForm.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index ba36562e..7b0f16aa 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -6,7 +6,6 @@ import { fetchAndParse } from "../fetchAndParse"; // import defaultConfig from '../config.default.json'; import config from "../config.json"; import DataPositionFormRow from "./DataPositionFormRow"; -import MountedDataFormRow from "./MountedDataFormRow"; import FileUploadFormRow from "./FileUploadFormRow"; import ExampleSelectButtons from "./ExampleSelectButtons"; import RegionInput from "./RegionInput"; @@ -98,17 +97,18 @@ function tracksEqual(curr, next) { const next_file = next.trackFile.name; const curr_settings = curr.trackColorSettings; - const next_settings = curr.trackColorSettings; + const next_settings = next.trackColorSettings; // check if color settings are equal - if (curr_settings.mainPalette !== next_settings.mainPalette || - curr_settings.auxPalette !== next_settings.auxPalette || - curr_settings.colorReadsByMappingQuality !== next_settings.colorReadsByMappingQuality) { - - console.log("tracks have differnt color settings"); - return false; + if (curr_settings && next_settings){ + if (curr_settings.mainPalette !== next_settings.mainPalette || + curr_settings.auxPalette !== next_settings.auxPalette || + curr_settings.colorReadsByMappingQuality !== next_settings.colorReadsByMappingQuality) { + + console.log("tracks have differnt color settings"); + return false; + } } - //count falsy file names as the same if ((!curr_file && !next_file) || curr_file === next_file) { return true; From c7ab6a8ef0569d9ebd4b7ee6334914210d1c8f7d Mon Sep 17 00:00:00 2001 From: shreyasun Date: Mon, 19 Jun 2023 17:43:11 -0700 Subject: [PATCH 006/113] Including a try-catch condition to handle case if link is not copied. Displays popup if link is not copied. --- src/components/CopyLink.js | 47 +++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/src/components/CopyLink.js b/src/components/CopyLink.js index bbdd4d46..ff37e5b9 100644 --- a/src/components/CopyLink.js +++ b/src/components/CopyLink.js @@ -3,43 +3,64 @@ import { Button } from "reactstrap"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faLink } from "@fortawesome/free-solid-svg-icons"; import * as qs from 'qs'; +import PopupDialog from "./PopupDialog"; const UNCLICKED_TEXT = " Copy link to data"; const CLICKED_TEXT = " Copied link!"; +// uses Clipboard API to write text to clipboard export const writeToClipboard = (text) => { navigator.clipboard.writeText(text); }; - - // For testing purposes let copyCallback = writeToClipboard; + +// sets value of copyCallback export const setCopyCallback = (callback) => (copyCallback = callback); + export function CopyLink(props) { // Button to copy a link with viewTarget to the data selected - const [text, setText] = useState(UNCLICKED_TEXT); + const [dialogLink, setDialogLink] = useState(undefined); + const handleCopyLink = () => { // Turn viewTarget into a URL query string const viewTarget = props.getCurrentViewTarget(); - // Don't stringify objects for readability // https://github.com/ljharb/qs#stringifying const params = qs.stringify(viewTarget, { encode: false }); + // complete url const full = window.location.origin + "?" + params; console.log(full); - copyCallback(full); - - // Write link to clipboard - // Update button text to show we've copied - setText(CLICKED_TEXT); + + try{ + // copy full to clipboard + copyCallback(full); + // change text + setText(CLICKED_TEXT); + } + catch(error){ + setText(UNCLICKED_TEXT); + setDialogLink(full); + console.log("copy error: ", error) + } + }; + const [open, setOpen] = useState(false); + const close = () => setOpen(false); return ( - + <> + + {/* conditional rendering information from: https://legacy.reactjs.org/docs/conditional-rendering.html */} + {(dialogLink != null) && + {dialogLink} + } + + ); } From d00aa350deb3f76bc20906d286c4fe1414f91801 Mon Sep 17 00:00:00 2001 From: shreyasun Date: Tue, 20 Jun 2023 12:58:12 -0700 Subject: [PATCH 007/113] Including a try-catch condition to handle case if link is not copied. Fixed popup rendering so popup is displayed if link is not copied upon button click. --- src/components/CopyLink.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/CopyLink.js b/src/components/CopyLink.js index ff37e5b9..a7557111 100644 --- a/src/components/CopyLink.js +++ b/src/components/CopyLink.js @@ -10,6 +10,7 @@ const CLICKED_TEXT = " Copied link!"; // uses Clipboard API to write text to clipboard export const writeToClipboard = (text) => { + throw new Error("boom"); navigator.clipboard.writeText(text); }; // For testing purposes @@ -25,6 +26,7 @@ export function CopyLink(props) { const handleCopyLink = () => { + setOpen(!open); // Turn viewTarget into a URL query string const viewTarget = props.getCurrentViewTarget(); // Don't stringify objects for readability @@ -33,19 +35,19 @@ export function CopyLink(props) { // complete url const full = window.location.origin + "?" + params; console.log(full); - + try{ // copy full to clipboard copyCallback(full); // change text setText(CLICKED_TEXT); } - catch(error){ + catch(err){ setText(UNCLICKED_TEXT); setDialogLink(full); - console.log("copy error: ", error) + console.log("copy error") } - + }; const [open, setOpen] = useState(false); const close = () => setOpen(false); @@ -56,9 +58,10 @@ export function CopyLink(props) { {text} {/* conditional rendering information from: https://legacy.reactjs.org/docs/conditional-rendering.html */} - {(dialogLink != null) && - {dialogLink} - } + {(dialogLink != null) && +

Link to Data

+ {dialogLink} +
} ); From 0df516f38832f5f36b5423c8722107651d1dc769 Mon Sep 17 00:00:00 2001 From: shreyasun Date: Tue, 20 Jun 2023 12:59:36 -0700 Subject: [PATCH 008/113] Including a try-catch condition to handle case if link is not copied. Fixed popup rendering so popup is displayed if link is not copied upon button click. --- src/components/CopyLink.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/CopyLink.js b/src/components/CopyLink.js index a7557111..9214f542 100644 --- a/src/components/CopyLink.js +++ b/src/components/CopyLink.js @@ -10,7 +10,6 @@ const CLICKED_TEXT = " Copied link!"; // uses Clipboard API to write text to clipboard export const writeToClipboard = (text) => { - throw new Error("boom"); navigator.clipboard.writeText(text); }; // For testing purposes @@ -63,7 +62,6 @@ export function CopyLink(props) { {dialogLink} } - ); } From 84552ee39887aa32ea221b28880f588652e29517 Mon Sep 17 00:00:00 2001 From: shreyasun Date: Tue, 20 Jun 2023 20:16:12 -0700 Subject: [PATCH 009/113] ncluding a try-catch condition to handle case if link is not copied. Fixed popup rendering so popup with appropriate content is displayed. --- src/components/CopyLink.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CopyLink.js b/src/components/CopyLink.js index 9214f542..870aa853 100644 --- a/src/components/CopyLink.js +++ b/src/components/CopyLink.js @@ -58,8 +58,8 @@ export function CopyLink(props) { {/* conditional rendering information from: https://legacy.reactjs.org/docs/conditional-rendering.html */} {(dialogLink != null) && -

Link to Data

- {dialogLink} +

Link to Data

+

Use this link to return to this view. Right click link to copy this view location.

} ); From 022c296aed2cd75959cf04c2b4aaeae57ea497dd Mon Sep 17 00:00:00 2001 From: shreyasun Date: Wed, 21 Jun 2023 09:32:16 -0700 Subject: [PATCH 010/113] Including a try-catch condition to handle case if link is not copied. Fixed popup rendering so popup with appropriate content is displayed when link is not copied --- src/components/CopyLink.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/CopyLink.js b/src/components/CopyLink.js index 870aa853..5e756f27 100644 --- a/src/components/CopyLink.js +++ b/src/components/CopyLink.js @@ -12,6 +12,7 @@ const CLICKED_TEXT = " Copied link!"; export const writeToClipboard = (text) => { navigator.clipboard.writeText(text); }; + // For testing purposes let copyCallback = writeToClipboard; @@ -23,8 +24,8 @@ export function CopyLink(props) { const [text, setText] = useState(UNCLICKED_TEXT); const [dialogLink, setDialogLink] = useState(undefined); - const handleCopyLink = () => { + // open popup setOpen(!open); // Turn viewTarget into a URL query string const viewTarget = props.getCurrentViewTarget(); @@ -48,8 +49,10 @@ export function CopyLink(props) { } }; + const [open, setOpen] = useState(false); const close = () => setOpen(false); + return ( <> {/* conditional rendering information from: https://legacy.reactjs.org/docs/conditional-rendering.html */} {(dialogLink != null) && -

Link to Data

-

Use this link to return to this view. Right click link to copy this view location.

+
Link to Data
+

Data
Click this link to return to this view. Right click link to copy this view location.

} ); From 00fcc3ccd85d76e02f62d1387a0554d9a31919c0 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Thu, 22 Jun 2023 15:46:28 -0400 Subject: [PATCH 011/113] Specify a dev container to try and make Codespaces work --- .devcontainer/devcontainer.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..fa917df0 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,9 @@ +{ + "image": "quay.io/vgteam/vg:v1.49.0", + "forwardPorts": [3000, 3001], + "features": { + "ghcr.io/devcontainers/features/node:1": {} + }, + "postCreateCommand": ". ${NVM_DIR}/nvm.sh && nvm install" +} + From 08680d9a4a0145bd6aacb465af4e6de34f78ab9a Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Thu, 22 Jun 2023 20:20:43 +0000 Subject: [PATCH 012/113] Get API URL along with protocol --- src/App.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/App.js b/src/App.js index 81ae40ca..1412d950 100644 --- a/src/App.js +++ b/src/App.js @@ -43,6 +43,8 @@ class App extends Component { constructor(props) { super(props); + console.log('Tube map statting up with API URL: ' + props.apiUrl) + // Set defaultViewTarget to either URL params (if present) or the first example this.defaultViewTarget = urlParamsToViewTarget(document.location) ?? config.DATA_SOURCES[0]; @@ -213,7 +215,7 @@ App.defaultProps = { // the config or the browser, but needs to be swapped out in the fake // browser testing environment to point to a real testing backend. // Note that host includes the port. - apiUrl: (config.BACKEND_URL || `http://${window.location.host}`) + "/api/v0", + apiUrl: (config.BACKEND_URL || `${window.location.origin}`) + "/api/v0", }; export default App; From dc66a24b42eaa45e10afeed10d12bfc0370c855f Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Thu, 22 Jun 2023 20:22:57 +0000 Subject: [PATCH 013/113] Don't forward internal API port --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fa917df0..a2fecc2c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "image": "quay.io/vgteam/vg:v1.49.0", - "forwardPorts": [3000, 3001], + "forwardPorts": [3000], "features": { "ghcr.io/devcontainers/features/node:1": {} }, From b1fea19eaaafe56b3c062a5ebaee25b0eaf1c7ba Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Thu, 22 Jun 2023 20:26:15 +0000 Subject: [PATCH 014/113] Add git in dev container --- .devcontainer/devcontainer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a2fecc2c..d28fa87c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,8 @@ "image": "quay.io/vgteam/vg:v1.49.0", "forwardPorts": [3000], "features": { - "ghcr.io/devcontainers/features/node:1": {} + "ghcr.io/devcontainers/features/node:1": {}, + "ghcr.io/devcontainers/features/git:1": {} }, "postCreateCommand": ". ${NVM_DIR}/nvm.sh && nvm install" } From 311fd8050f88fe00dda3e6f20c1a867140d2f3e7 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Thu, 22 Jun 2023 21:00:57 +0000 Subject: [PATCH 015/113] Try to get Bash to decide if we are on Codespaces Then it can set up the dev server websockets --- .devcontainer/devcontainer.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d28fa87c..2dadad8b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,10 @@ "forwardPorts": [3000], "features": { "ghcr.io/devcontainers/features/node:1": {}, - "ghcr.io/devcontainers/features/git:1": {} + "ghcr.io/devcontainers/features/git:1": {}, + "ghcr.io/devcontainers-contrib/features/bash-command:1": { + "command": "echo 'if [[ ! -z \"$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN\" ]] ; then export WDS_SOCKET_HOST=\"${CODESPACE_NAME}-3000.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}\"; export WDS_SOCKET_PORT=443; fi' >>/etc/bash.bashrc" + } }, "postCreateCommand": ". ${NVM_DIR}/nvm.sh && nvm install" } From 0515eebf5d2e5d9a3b0f99898efc1e60fee37a28 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Thu, 22 Jun 2023 17:42:27 -0400 Subject: [PATCH 016/113] Hackily set Webpack to use Github's port forward This adds a line to the devcontainer global bashrc that checks if the Github Codespaces variables are set, and if so sets Webpack to use the Codespaces prot forward for its live reload websocket. We should maybe use a Dockerfile-based build to drop this line in the global bashrc instead. --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2dadad8b..f918169f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,7 @@ "ghcr.io/devcontainers/features/node:1": {}, "ghcr.io/devcontainers/features/git:1": {}, "ghcr.io/devcontainers-contrib/features/bash-command:1": { - "command": "echo 'if [[ ! -z \"$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN\" ]] ; then export WDS_SOCKET_HOST=\"${CODESPACE_NAME}-3000.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}\"; export WDS_SOCKET_PORT=443; fi' >>/etc/bash.bashrc" + "command": "echo aWYgW1sgISAteiAiJHtHSVRIVUJfQ09ERVNQQUNFU19QT1JUX0ZPUldBUkRJTkdfRE9NQUlOfSIgXV0gOyB0aGVuIGV4cG9ydCBXRFNfU09DS0VUX0hPU1Q9IiR7Q09ERVNQQUNFX05BTUV9LTMwMDAuJHtHSVRIVUJfQ09ERVNQQUNFU19QT1JUX0ZPUldBUkRJTkdfRE9NQUlOfSI7IGV4cG9ydCBXRFNfU09DS0VUX1BPUlQ9NDQzOyBmaQo= | base64 --decode >>/etc/bash.bashrc # Line to tell Webpack dev server to ignore HOST and use the Github Codespace port forward if we are in a Codespace. Need to base64 here because double quotes cannot be escaped through the devcontainer build process." } }, "postCreateCommand": ". ${NVM_DIR}/nvm.sh && nvm install" From b6bdb0d84ea94d6b13913737c635652995df45c1 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Thu, 22 Jun 2023 17:45:25 -0400 Subject: [PATCH 017/113] Make sure there's an editor --- .devcontainer/devcontainer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f918169f..ab97b3ed 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,6 +4,7 @@ "features": { "ghcr.io/devcontainers/features/node:1": {}, "ghcr.io/devcontainers/features/git:1": {}, + "ghcr.io/devcontainers-contrib/features/apt-packages:1": {"packages": "nano"}, "ghcr.io/devcontainers-contrib/features/bash-command:1": { "command": "echo aWYgW1sgISAteiAiJHtHSVRIVUJfQ09ERVNQQUNFU19QT1JUX0ZPUldBUkRJTkdfRE9NQUlOfSIgXV0gOyB0aGVuIGV4cG9ydCBXRFNfU09DS0VUX0hPU1Q9IiR7Q09ERVNQQUNFX05BTUV9LTMwMDAuJHtHSVRIVUJfQ09ERVNQQUNFU19QT1JUX0ZPUldBUkRJTkdfRE9NQUlOfSI7IGV4cG9ydCBXRFNfU09DS0VUX1BPUlQ9NDQzOyBmaQo= | base64 --decode >>/etc/bash.bashrc # Line to tell Webpack dev server to ignore HOST and use the Github Codespace port forward if we are in a Codespace. Need to base64 here because double quotes cannot be escaped through the devcontainer build process." } From 746de209fd217919c9c8392c046df62f9853dfd4 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Thu, 22 Jun 2023 17:49:20 -0400 Subject: [PATCH 018/113] Use apt-get since apt doesn't work --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ab97b3ed..980603e5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,7 @@ "features": { "ghcr.io/devcontainers/features/node:1": {}, "ghcr.io/devcontainers/features/git:1": {}, - "ghcr.io/devcontainers-contrib/features/apt-packages:1": {"packages": "nano"}, + "ghcr.io/devcontainers-contrib/features/apt-get-packages:1": {"packages": "nano"}, "ghcr.io/devcontainers-contrib/features/bash-command:1": { "command": "echo aWYgW1sgISAteiAiJHtHSVRIVUJfQ09ERVNQQUNFU19QT1JUX0ZPUldBUkRJTkdfRE9NQUlOfSIgXV0gOyB0aGVuIGV4cG9ydCBXRFNfU09DS0VUX0hPU1Q9IiR7Q09ERVNQQUNFX05BTUV9LTMwMDAuJHtHSVRIVUJfQ09ERVNQQUNFU19QT1JUX0ZPUldBUkRJTkdfRE9NQUlOfSI7IGV4cG9ydCBXRFNfU09DS0VUX1BPUlQ9NDQzOyBmaQo= | base64 --decode >>/etc/bash.bashrc # Line to tell Webpack dev server to ignore HOST and use the Github Codespace port forward if we are in a Codespace. Need to base64 here because double quotes cannot be escaped through the devcontainer build process." } From 9c7cfe2d9589567e41c991851fdf37e01e1ee3ae Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Thu, 22 Jun 2023 18:08:22 -0400 Subject: [PATCH 019/113] Point at the actual dev server port and not the API port --- .devcontainer/devcontainer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 980603e5..9c103b5d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,12 +1,12 @@ { "image": "quay.io/vgteam/vg:v1.49.0", - "forwardPorts": [3000], + "forwardPorts": [3001], "features": { "ghcr.io/devcontainers/features/node:1": {}, "ghcr.io/devcontainers/features/git:1": {}, "ghcr.io/devcontainers-contrib/features/apt-get-packages:1": {"packages": "nano"}, "ghcr.io/devcontainers-contrib/features/bash-command:1": { - "command": "echo aWYgW1sgISAteiAiJHtHSVRIVUJfQ09ERVNQQUNFU19QT1JUX0ZPUldBUkRJTkdfRE9NQUlOfSIgXV0gOyB0aGVuIGV4cG9ydCBXRFNfU09DS0VUX0hPU1Q9IiR7Q09ERVNQQUNFX05BTUV9LTMwMDAuJHtHSVRIVUJfQ09ERVNQQUNFU19QT1JUX0ZPUldBUkRJTkdfRE9NQUlOfSI7IGV4cG9ydCBXRFNfU09DS0VUX1BPUlQ9NDQzOyBmaQo= | base64 --decode >>/etc/bash.bashrc # Line to tell Webpack dev server to ignore HOST and use the Github Codespace port forward if we are in a Codespace. Need to base64 here because double quotes cannot be escaped through the devcontainer build process." + "command": "echo aWYgW1sgISAteiAiJHtHSVRIVUJfQ09ERVNQQUNFU19QT1JUX0ZPUldBUkRJTkdfRE9NQUlOfSIgXV0gOyB0aGVuIGV4cG9ydCBXRFNfU09DS0VUX0hPU1Q9IiR7Q09ERVNQQUNFX05BTUV9LTMwMDEuJHtHSVRIVUJfQ09ERVNQQUNFU19QT1JUX0ZPUldBUkRJTkdfRE9NQUlOfSI7IGV4cG9ydCBXRFNfU09DS0VUX1BPUlQ9NDQzOyBmaQo= | base64 --decode >>/etc/bash.bashrc # Line to tell Webpack dev server to ignore HOST and use the Github Codespace port forward if we are in a Codespace. Need to base64 here because double quotes cannot be escaped through the devcontainer build process." } }, "postCreateCommand": ". ${NVM_DIR}/nvm.sh && nvm install" From 9beabc098210b2fe20f21121faf9471c3b11fed9 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Thu, 22 Jun 2023 22:37:11 +0000 Subject: [PATCH 020/113] Use HOST and HTTPS variables to wrangle Webpack Create React App doesn't provide a WDS_ env var to control https://github.com/webpack/webpack-dev-server/blob/540c43852ea33f9cb18820e1cef05d5ddb86cc3e/lib/Server.js#L562-L569 when it makes the config at https://github.com/facebook/create-react-app/blob/20edab4894b301f6b90dad0f90a2f82c52a7ac66/packages/react-scripts/config/webpackDevServer.config.js#L19-L22 So we have to actually put the server into HTTPS mode if we want WSS used for the hot reload server. Luckily, Github's proxy ignores self-signed certs. But we still need to set HOST or the proxy weirdly doesn't work with HTTPS ports. See https://github.com/orgs/community/discussions/28563#discussioncomment-4984423 --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9c103b5d..ef21cb8f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ "ghcr.io/devcontainers/features/git:1": {}, "ghcr.io/devcontainers-contrib/features/apt-get-packages:1": {"packages": "nano"}, "ghcr.io/devcontainers-contrib/features/bash-command:1": { - "command": "echo aWYgW1sgISAteiAiJHtHSVRIVUJfQ09ERVNQQUNFU19QT1JUX0ZPUldBUkRJTkdfRE9NQUlOfSIgXV0gOyB0aGVuIGV4cG9ydCBXRFNfU09DS0VUX0hPU1Q9IiR7Q09ERVNQQUNFX05BTUV9LTMwMDEuJHtHSVRIVUJfQ09ERVNQQUNFU19QT1JUX0ZPUldBUkRJTkdfRE9NQUlOfSI7IGV4cG9ydCBXRFNfU09DS0VUX1BPUlQ9NDQzOyBmaQo= | base64 --decode >>/etc/bash.bashrc # Line to tell Webpack dev server to ignore HOST and use the Github Codespace port forward if we are in a Codespace. Need to base64 here because double quotes cannot be escaped through the devcontainer build process." + "command": "echo aWYgW1sgISAteiAiJHtHSVRIVUJfQ09ERVNQQUNFU19QT1JUX0ZPUldBUkRJTkdfRE9NQUlOfSIgXV0gOyB0aGVuIGV4cG9ydCBXRFNfU09DS0VUX0hPU1Q9IiR7Q09ERVNQQUNFX05BTUV9LTMwMDEuJHtHSVRIVUJfQ09ERVNQQUNFU19QT1JUX0ZPUldBUkRJTkdfRE9NQUlOfSI7IGV4cG9ydCBXRFNfU09DS0VUX1BPUlQ9NDQzOyBleHBvcnQgSE9TVD0wLjAuMC4wOyBleHBvcnQgSFRUUFM9dHJ1ZTsgZmkK | base64 --decode >>/etc/bash.bashrc # Line to tell Webpack dev server to ignore HOST and use the Github Codespace port forward if we are in a Codespace. Need to base64 here because double quotes cannot be escaped through the devcontainer build process." } }, "postCreateCommand": ". ${NVM_DIR}/nvm.sh && nvm install" From 53e590754544e90360ae31ad0ef4daaab43954e8 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Thu, 22 Jun 2023 22:45:44 +0000 Subject: [PATCH 021/113] Tell Github the dev server port is HTTPS --- .devcontainer/devcontainer.json | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ef21cb8f..b8a5982e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "image": "quay.io/vgteam/vg:v1.49.0", - "forwardPorts": [3001], + "forwardPorts": [3000, 3001], "features": { "ghcr.io/devcontainers/features/node:1": {}, "ghcr.io/devcontainers/features/git:1": {}, @@ -9,6 +9,16 @@ "command": "echo aWYgW1sgISAteiAiJHtHSVRIVUJfQ09ERVNQQUNFU19QT1JUX0ZPUldBUkRJTkdfRE9NQUlOfSIgXV0gOyB0aGVuIGV4cG9ydCBXRFNfU09DS0VUX0hPU1Q9IiR7Q09ERVNQQUNFX05BTUV9LTMwMDEuJHtHSVRIVUJfQ09ERVNQQUNFU19QT1JUX0ZPUldBUkRJTkdfRE9NQUlOfSI7IGV4cG9ydCBXRFNfU09DS0VUX1BPUlQ9NDQzOyBleHBvcnQgSE9TVD0wLjAuMC4wOyBleHBvcnQgSFRUUFM9dHJ1ZTsgZmkK | base64 --decode >>/etc/bash.bashrc # Line to tell Webpack dev server to ignore HOST and use the Github Codespace port forward if we are in a Codespace. Need to base64 here because double quotes cannot be escaped through the devcontainer build process." } }, - "postCreateCommand": ". ${NVM_DIR}/nvm.sh && nvm install" +"postCreateCommand": ". ${NVM_DIR}/nvm.sh && nvm install", +"portsAttributes": { + "3001": { + "label": "devserver", + "protocol": "https" + }, + "3000": { + "label": "api", + "onAutoForward": "silent" + } +} } From 355838e35bbd2133736db5d806a5c099ebfa37bb Mon Sep 17 00:00:00 2001 From: shreyasun Date: Thu, 22 Jun 2023 16:07:01 -0700 Subject: [PATCH 022/113] Modified open and close props for popup to render popup upon button click when link is not copied. --- src/components/CopyLink.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/CopyLink.js b/src/components/CopyLink.js index 5e756f27..e6aa5768 100644 --- a/src/components/CopyLink.js +++ b/src/components/CopyLink.js @@ -25,8 +25,6 @@ export function CopyLink(props) { const [dialogLink, setDialogLink] = useState(undefined); const handleCopyLink = () => { - // open popup - setOpen(!open); // Turn viewTarget into a URL query string const viewTarget = props.getCurrentViewTarget(); // Don't stringify objects for readability @@ -50,8 +48,7 @@ export function CopyLink(props) { }; - const [open, setOpen] = useState(false); - const close = () => setOpen(false); + const close = () => setDialogLink(undefined); return ( <> @@ -59,11 +56,12 @@ export function CopyLink(props) { {text} - {/* conditional rendering information from: https://legacy.reactjs.org/docs/conditional-rendering.html */} - {(dialogLink != null) && +
Link to Data
+ {/* Received warning on build that simply using target="_blank" is a security risk in older browsers, so included rel="noopener noreferrer" as per this post: + https://stackoverflow.com/questions/57628890/why-people-use-rel-noopener-noreferrer-instead-of-just-rel-noreferrer */}

Data
Click this link to return to this view. Right click link to copy this view location.

-
} +
); } From a08263e3e73a67a335900d28976698f8d46ea280 Mon Sep 17 00:00:00 2001 From: ducku Date: Sun, 25 Jun 2023 18:19:28 -0700 Subject: [PATCH 023/113] end to end and app test fixes --- src/Types.ts | 2 +- src/components/CustomizationAccordion.js | 3 +++ src/components/HeaderForm.js | 2 +- src/end-to-end.test.js | 33 ++++++++++++++++++------ 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/Types.ts b/src/Types.ts index f623193c..394482c0 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -28,7 +28,7 @@ type track = { } type tracks = { - trackID: track; + [key: number]: track; } // Describes something the Tube Map can look at, specifically a region and the files the region is in. diff --git a/src/components/CustomizationAccordion.js b/src/components/CustomizationAccordion.js index 0450718c..c7de8ae1 100644 --- a/src/components/CustomizationAccordion.js +++ b/src/components/CustomizationAccordion.js @@ -57,6 +57,9 @@ class VisualizationOptions extends Component { for (let key in this.props.tracks) { // Generate settings controls for each track let track = this.props.tracks[key]; + if (!track.trackFile) { + continue; + } let type = track.trackFile.type; if (type === "graph") { trackSettingsList.push( diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index 7b0f16aa..ffba43a4 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -314,7 +314,7 @@ class HeaderForm extends Component { "Content-Type": "application/json", }, }); - if (json.files.length === 0) { + if (!json.files || json.files.length === 0) { // We did not get back a graph, only (possibly) an error. const error = json.error || "Server did not return a list of mounted filenames."; diff --git a/src/end-to-end.test.js b/src/end-to-end.test.js index fda9d9b7..110ce3e1 100644 --- a/src/end-to-end.test.js +++ b/src/end-to-end.test.js @@ -36,12 +36,14 @@ let root = undefined; // Mock clipboard (string) let fakeClipboard = undefined; +let dom; + // This needs to be called by global and per-scope beforeEach async function setUp() { setCopyCallback((value) => (fakeClipboard = value)); // Create the application. - render(); + dom = render(); } // This needs to be called by global and per-scope afterEach @@ -309,7 +311,7 @@ describe("When we wait for it to load", () => { it("produces correct link for view before & after go is pressed", async () => { // First test that after pressing go, the link reflects the dat form const expectedLinkBRCA1 = - "http://localhost?name=snp1kg-BRCA1&tracks[0][files][0][type]=graph&tracks[0][files][0][name]=snp1kg-BRCA1.vg.xg&tracks[1][files][0][type]=read&tracks[1][files][0][name]=NA12878-BRCA1.sorted.gam&dataPath=default®ion=17:1-100&bedFile=snp1kg-BRCA1.bed&dataType=built-in"; + "http://localhost?name=snp1kg-BRCA1&tracks[0][trackFile][type]=graph&tracks[0][trackFile][name]=snp1kg-BRCA1.vg.xg&tracks[1][trackFile][type]=read&tracks[1][trackFile][name]=NA12878-BRCA1.sorted.gam&dataPath=default®ion=17:1-100&bedFile=snp1kg-BRCA1.bed&dataType=built-in"; // Set up dropdown await act(async () => { let dropdown = document.getElementById("dataSourceSelect"); @@ -343,7 +345,7 @@ it("produces correct link for view before & after go is pressed", async () => { await clickCopyLink(); const expectedLinkCactus = - "http://localhost?tracks[0][files][0][type]=graph&tracks[0][files][0][name]=cactus.vg.xg&tracks[1][files][0][type]=read&tracks[1][files][0][name]=cactus-NA12879.sorted.gam&bedFile=cactus.bed&name=cactus®ion=ref:1-100&dataPath=mounted&dataType=built-in"; + "http://localhost?tracks[0][trackFile][type]=graph&tracks[0][trackFile][name]=cactus.vg.xg&tracks[1][trackFile][type]=read&tracks[1][trackFile][name]=cactus-NA12879.sorted.gam&bedFile=cactus.bed&name=cactus®ion=ref:1-100&dataPath=mounted&dataType=built-in"; // Make sure link has changed after pressing go expect(fakeClipboard).toEqual(expectedLinkCactus); }); @@ -362,17 +364,32 @@ it("can retrieve the list of mounted graph files", async () => { }); // Find the select box's input - let graphSelectInput = screen.getByLabelText(/graph file:/i); - expect(graphSelectInput).toBeTruthy(); + let trackSelectButton = screen.queryByTestId("TrackPickerButton"); + expect(trackSelectButton).toBeTruthy(); + + // open track selection + await act(async () => { + //fireEvent.click(trackSelectButton); + userEvent.click(trackSelectButton); + }); + + + // add a new track + await waitFor(() => { + fireEvent.click(screen.queryByTestId("track-add-button-component")); + }); + // We shouldn't see the option before we open the dropdown expect(screen.queryByText("cactus.vg.xg")).not.toBeInTheDocument(); + // Make sure the right entry eventually shows up (since we could be racing // the initial load from the component mounting) await waitFor(() => { - // Open the selector and see if it is there - selectEvent.openMenu(graphSelectInput); - expect(screen.getByText("cactus.vg.xg")).toBeInTheDocument(); + // try to select a graph file + fireEvent.keyDown(screen.queryByTestId('file-select-component1').firstChild, {key: "ArrowDown"}); }); + + expect(screen.queryByText("cactus.vg.xg")).toBeTruthy(); }); From 132b869408decc760798461b1158e526313283d5 Mon Sep 17 00:00:00 2001 From: ducku Date: Sun, 25 Jun 2023 20:09:38 -0700 Subject: [PATCH 024/113] tracks now contain a trackFile and a trackType instead of an array of file object. AvailableTracks now takes an array of tracks --- src/App.js | 8 +++---- src/Types.ts | 8 +------ src/components/CustomizationAccordion.js | 3 ++- src/components/HeaderForm.js | 26 +++++++++++------------ src/components/TrackFilePicker.demo.js | 12 +++++------ src/components/TrackFilePicker.js | 12 +++++------ src/components/TrackFilePicker.test.js | 20 ++++++++--------- src/components/TrackList.demo.js | 10 ++++----- src/components/TrackList.test.js | 3 ++- src/components/TrackListItem.demo.js | 12 +++++------ src/components/TrackListItem.js | 1 + src/components/TrackListItem.test.js | 10 ++++----- src/components/TrackPicker.demo.js | 12 +++++------ src/components/TrackPicker.test.js | 8 ++++--- src/components/TrackPickerDisplay.demo.js | 12 +++++------ src/components/TrackPickerDisplay.test.js | 18 +++++++++------- src/components/TubeMapContainer.js | 8 +++---- src/config.json | 18 ++++++++-------- src/server.mjs | 10 ++++----- src/util/tubemap.js | 6 ++---- 20 files changed, 104 insertions(+), 113 deletions(-) diff --git a/src/App.js b/src/App.js index a670d2e8..3d0165e7 100644 --- a/src/App.js +++ b/src/App.js @@ -21,7 +21,7 @@ const EXAMPLE_TRACKS = [ {"files": [{"type": "read", "name": "fakeReads"}]} ]; -function getColorSchemesFromTrack(tracks) { +function getColorSchemesFromTracks(tracks) { let schemes = []; @@ -30,7 +30,7 @@ function getColorSchemesFromTrack(tracks) { // We need to adopt a color scheme if (tracks[key].trackColorSettings !== undefined) { schemes[key] = tracks[key].trackColorSettings; - } else if (tracks[key].trackFile.type === "read") { + } else if (tracks[key].trackType === "read") { schemes[key] = {...config.defaultReadColorPalette}; } else { schemes[key] = {...config.defaultHaplotypeColorPalette}; @@ -63,7 +63,7 @@ class App extends Component { showReads: true, showSoftClips: true, colorReadsByMappingQuality: false, - colorSchemes: getColorSchemesFromTrack(this.defaultViewTarget.tracks), + colorSchemes: getColorSchemesFromTracks(this.defaultViewTarget.tracks), mappingQualityCutoff: 0, }, }; @@ -99,7 +99,7 @@ class App extends Component { this.setState((state) => { // Make sure we have color schemes. - let newColorSchemes = getColorSchemesFromTrack(newViewTarget.tracks); + let newColorSchemes = getColorSchemesFromTracks(newViewTarget.tracks); console.log("Adopting color schemes: ", newColorSchemes) diff --git a/src/Types.ts b/src/Types.ts index 394482c0..9ac28e0a 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -13,16 +13,10 @@ type DataType = "built-in" | "file-upload" | "mounted files" | "examples"; // Files like GBZ contains graph and maybe haplotype and so can be either type filetype = "graph" | "haplotype" | "read" | "bed"; -// Describes a file via name and type(graph, haplotype) -// e.g name: cactus.xg, type: graph -type file = { - name: string; - type: filetype; -}; // Contains information necessary to make a track type track = { - trackFile: file; + trackFile: string; // Name of file trackType: filetype; trackColorSettings: ColorScheme; } diff --git a/src/components/CustomizationAccordion.js b/src/components/CustomizationAccordion.js index c7de8ae1..7fae9711 100644 --- a/src/components/CustomizationAccordion.js +++ b/src/components/CustomizationAccordion.js @@ -57,10 +57,11 @@ class VisualizationOptions extends Component { for (let key in this.props.tracks) { // Generate settings controls for each track let track = this.props.tracks[key]; + console.log(track); if (!track.trackFile) { continue; } - let type = track.trackFile.type; + let type = track.trackType; if (type === "graph") { trackSettingsList.push( diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index ffba43a4..3fab6dc4 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -93,8 +93,8 @@ function tracksEqual(curr, next) { - const curr_file = curr.trackFile.name; - const next_file = next.trackFile.name; + const curr_file = curr.trackFile; + const next_file = next.trackFile; const curr_settings = curr.trackColorSettings; const next_settings = next.trackColorSettings; @@ -208,10 +208,10 @@ class HeaderForm extends Component { this.getBedRegions(bedSelect, dataPath); } for (const key in ds.tracks) { - if (ds.tracks[key].trackFile.type === fileTypes.GRAPH) { + if (ds.tracks[key].trackType === fileTypes.GRAPH) { // Load the paths for any graph tracks console.log("Get path names for track: ", ds.tracks[key]); - this.getPathNames(ds.tracks[key].trackFile.name, dataPath); + this.getPathNames(ds.tracks[key].trackFile, dataPath); } } this.setState((state) => { @@ -243,13 +243,13 @@ class HeaderForm extends Component { let maxKey = -1; for (const key in state.tracks) { let track = state.tracks[key]; - if (track.trackFile.type === type) { + if (track.trackType === type) { console.log("See file " + seenTracksOfType + " of right type"); if (seenTracksOfType === index) { if (file !== "none") { // We want to adjust it, so keep a modified copy of it let newTrack = JSON.parse(JSON.stringify(track)); - newTrack.trackFile.name = file; + newTrack.trackFile = file; newTracks[key] = newTrack; } // If the file is "none" we drop the track. @@ -292,10 +292,10 @@ class HeaderForm extends Component { if (track === -1) { continue; } - if (track.trackFile.type === type) { + if (track.trackType === type) { if (seenTracksOfType === index) { // This is the one. Return its filename. - return track.trackFile.name; + return track.trackFile; } seenTracksOfType++; } @@ -331,11 +331,11 @@ class HeaderForm extends Component { this.getBedRegions(bedSelect, "mounted"); } for (const key in state.tracks) { - if (state.tracks[key].trackFile.type === fileTypes.GRAPH) { + if (state.tracks[key].trackType === fileTypes.GRAPH) { // Load the paths for any graph tracks. // TODO: Do we need to do this now? console.log("Get path names for track: ", state.tracks[key]); - this.getPathNames(state.tracks[key].trackFile.name, "mounted"); + this.getPathNames(state.tracks[key].trackFile, "mounted"); } } return { @@ -467,10 +467,10 @@ class HeaderForm extends Component { this.setState({ regionInfo: {} }); } for (const key in ds.tracks) { - if (ds.tracks[key].trackFile.type === fileTypes.GRAPH) { + if (ds.tracks[key].trackType === fileTypes.GRAPH) { // Load the paths for any graph tracks. console.log("Get path names for track: ", ds.tracks[key]); - this.getPathNames(ds.tracks[key].trackFile.name, dataPath); + this.getPathNames(ds.tracks[key].trackFile, dataPath); } } this.setState({ @@ -724,7 +724,7 @@ class HeaderForm extends Component { diff --git a/src/components/TrackFilePicker.demo.js b/src/components/TrackFilePicker.demo.js index d8e13cac..a13cfdca 100644 --- a/src/components/TrackFilePicker.demo.js +++ b/src/components/TrackFilePicker.demo.js @@ -7,13 +7,13 @@ import TrackFilePicker from "./TrackFilePicker"; export default ( takes const dropDownOptions = fileOptions.map((option) => ({ - label: option["name"], + label: option, value: option, })); @@ -53,7 +51,7 @@ export const TrackFilePicker = ({
- - - +   )} + {!examplesFlag && ( )} + + {mountedFilesFlag && +
+ + +
+ } + {uploadFilesFlag && ( )} + + {examplesFlag ? ( ) : ( + !mountedFilesFlag && { // based off of https://react-popup.elazizi.com/controlled-popup/#using-open-prop return( -
+ <> @@ -28,7 +28,7 @@ export const PopupDialog = ({ -
+ ) } diff --git a/src/components/TrackPicker.js b/src/components/TrackPicker.js index 4995b6ac..8c1afa1c 100644 --- a/src/components/TrackPicker.js +++ b/src/components/TrackPicker.js @@ -20,7 +20,7 @@ export const TrackPicker = ({ return(
- + { // Need to set width to null because the default fixed width is too small for the track lits items. } From eb7060d4955701b907378f74496648560afd7752 Mon Sep 17 00:00:00 2001 From: ducku Date: Thu, 13 Jul 2023 21:30:28 -0700 Subject: [PATCH 062/113] include tracks in bed-info by pulling from chunk directory's json file --- scripts/prepare_chunks.sh | 10 +++++-- src/components/HeaderForm.js | 4 +-- src/components/RegionInput.js | 3 +- src/server.mjs | 54 ++++++++++++++++++++++++++++++----- 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/scripts/prepare_chunks.sh b/scripts/prepare_chunks.sh index efcbfb4e..c044eaa0 100755 --- a/scripts/prepare_chunks.sh +++ b/scripts/prepare_chunks.sh @@ -32,23 +32,27 @@ for GAM_FILE in "${GAM_FILES[@]}"; do # Put gam files into array format if [ -z "$GAM_FILES_JSON" ] then - GAM_FILES_JSON="$GAM_FILE" + GAM_FILES_JSON="[$GAM_FILES_JSON\"$GAM_FILE\"" else - GAM_FILES_JSON="$GAM_FILES_JSON, $GAM_FILE" + GAM_FILES_JSON="$GAM_FILES_JSON, \"$GAM_FILE\"" fi vg_chunk_params=" $vg_chunk_params -a $GAM_FILE" done +GAM_FILES_JSON="$GAM_FILES_JSON]" + +echo $GAM_FILES_JSON # Call vg chunk vg chunk $vg_chunk_params > $OUTDIR/chunk.vg + # Cosntruct JSON JSON_STRING=$(jq -n \ --arg graph_file "$XG_FILE" \ --arg haplotype_file "$GBWT" \ - --arg gam_files "[$GAM_FILES_JSON]" \ + --argjson gam_files "$GAM_FILES_JSON" \ '$ARGS.named' ) printf "%s\n" "$JSON_STRING" > $OUTDIR/tracks.json diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index a2a1f027..8bb6d23b 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -291,7 +291,7 @@ class HeaderForm extends Component { return "none"; } - getChunkPath = async (bedFile, dataPath) => { + getChunkPath = async (bedFile, dataPath, parsedRegion) => { this.setState({ error: null }); try { const json = await fetchAndParse(`${this.props.apiUrl}/getChunkPath`, { @@ -300,7 +300,7 @@ class HeaderForm extends Component { headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ bedFile, dataPath }), + body: JSON.stringify({ bedFile, dataPath, parsedRegion }), }); if (!json.chunkPath) { diff --git a/src/components/RegionInput.js b/src/components/RegionInput.js index 58d63f42..3f22440f 100644 --- a/src/components/RegionInput.js +++ b/src/components/RegionInput.js @@ -34,7 +34,7 @@ export const RegionInput = ({ // Autocomplete selectable options const displayRegions = [...pathsWithRegion, ...pathNamesColon]; - + console.log("region info", regionInfo); return ( <> { // We need to parse the BED file we have been referred to so we can look up // the pre-parsed chunk. We don't want the client re-uploading the BED to // us on every request. - getChunkPath(bedFile, dataPath); + chunkPath = getChunkPath(bedFile, dataPath, parsedRegion); } // We always need a range-version of the region, to fill in req.region, to @@ -653,7 +653,8 @@ function returnErrorMiddleware(err, req, res, next) { // Hook up the error handling middleware. app.use(returnErrorMiddleware); -function getChunkPath(bedFile, dataPath) { +// Gets the chunk path from a region specified in a bedfile +function getChunkPath(bedFile, dataPath, parsedRegion) { let chunkPath = ""; let regionInfo = getBedRegions(bedFile, dataPath); @@ -665,15 +666,14 @@ function getChunkPath(bedFile, dataPath) { } if (stringifyRegion(entryRegion) === stringifyRegion(parsedRegion)) { // A BED entry is defined for this region exactly - if (regionInfo["chunk"][i] !== "") { + if (regionInfo["chunk_path"][i] !== "") { // And a chunk file is stored for it, so use that. - chunkPath = regionInfo["chunk"][i]; + chunkPath = regionInfo["chunk_path"][i]; break; } } } // check that the 'chunk.vg' file exists in the chunk folder - chunkPath = `${dataPath}${chunkPath}`; if (chunkPath.endsWith("/")) { chunkPath = chunkPath.substring(0, chunkPath.length - 1); } @@ -1098,7 +1098,7 @@ api.post("/getChunkPath", (req, res) => { throw new BadRequestError("No data path specified"); } else { dataPath = pickDataPath(req.body.dataPath); - result.chunkPath = getChunkPath(req.body.bedFile, dataPath); + result.chunkPath = getChunkPath(req.body.bedFile, dataPath, req.body.parsedRegion); res.json(result); } }); @@ -1120,7 +1120,7 @@ function getBedRegions(bedFile, dataPath) { throw new BadRequestError("BED file not found: " + bedFile); } - let bed_info = { chr: [], start: [], end: [], desc: [], chunk: [] }; + let bed_info = { chr: [], start: [], end: [], desc: [], chunk: [], chunk_path: [], tracks: []}; // Load and parse the BED file let bed_data = fs.readFileSync(bed_path).toString(); @@ -1144,6 +1144,46 @@ function getBedRegions(bedFile, dataPath) { chunk = records[4]; } bed_info["chunk"].push(chunk); + const chunk_path = `${dataPath}${chunk}`; + bed_info["chunk_path"].push(chunk_path); + + + let tracks = {}; + let trackID = 1; + + const track_json = path.join(chunk_path, "tracks.json"); + // if json file specifying the tracks exists + if (fs.existsSync(track_json)) { + const json_data = JSON.parse(fs.readFileSync(track_json)); + + if (json_data["graph_file"] !== "") { + tracks[trackID] = {...config.defaultTrackProps}; + tracks[trackID]["trackFile"] = json_data["graph_file"]; + tracks[trackID]["trackType"] = fileTypes["GRAPH"]; + trackID += 1; + } + + if (json_data["haplotype_file"] !== "") { + tracks[trackID] = {...config.defaultTrackProps}; + tracks[trackID]["trackFile"] = json_data["haplotype_file"]; + tracks[trackID]["trackType"] = fileTypes["HAPLOTYPE"]; + tracks[trackID]["trackColorSettings"] = {...config.defaultHaplotypeColorPalette}; + trackID += 1; + } + + for (const gam_file of json_data["gam_files"]) { + tracks[trackID] = {...config.defaultTrackProps}; + tracks[trackID]["trackFile"] = gam_file; + tracks[trackID]["trackType"] = fileTypes["READ"]; + tracks[trackID]["trackColorSettings"] = {...config.defaultReadColorPalette}; + trackID += 1; + } + + } + + bed_info["tracks"].push(tracks); + + }); return bed_info; From af164964d19b3909975bb6d9ed055a185849015a Mon Sep 17 00:00:00 2001 From: shreyasun Date: Fri, 14 Jul 2023 09:59:03 -0700 Subject: [PATCH 063/113] Started display of node data on single click --- src/util/tubemap.js | 52 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/src/util/tubemap.js b/src/util/tubemap.js index 426d8b56..212c8f31 100644 --- a/src/util/tubemap.js +++ b/src/util/tubemap.js @@ -3109,28 +3109,60 @@ function drawNodes(dNodes) { .on("mouseover", nodeMouseOver) .on("mouseout", nodeMouseOut) .on("dblclick", nodeDoubleClick) + .on("click", nodeSingleClick) .style("fill", config.transparentNodesFlag ? "none" : "#fff") .style("fill-opacity", config.showExonsFlag ? "0.4" : "0.6") .style("stroke", "black") .style("stroke-width", "2px") .append("svg:title") - .text((d) => getPopUpText(d)); + .text((d) => getPopUpNodeText(d)); } -function getPopUpText(node) { +function getPopUpNodeText(node) { return ( - `Node ID: ${node.name}` + (node.switched ? ` (reversed)` : ``) + `\n` + - `Node Length: ${node.sequenceLength} bases\n` + - `Haplotypes: ${node.degree}\n` + - `Aligned Reads: ${ - node.incomingReads.length + - node.internalReads.length + - node.outgoingReads.length - }` + `Node ID: ${node.name}` + (node.switched ? ` (reversed)` : ``) + `\n` ); } +// Get any track object by ID. +// Because of reordering of input tracks, the ID doesn't always match the index. +function getNodeByName(nodeName) { + if (typeof nodeName !== "number") { + throw new Error("Node Names must be numbers"); + } + // We just do a scan. + // TODO: index! + for (let i = 0; i < nodes.length; i++) { + if (nodes[i].name === nodeName) { + console.log("Found node with name ", nodeName, " at index ", i); + return nodes[i]; + } + } +} + + +function nodeSingleClick() { + /* jshint validthis: true */ + // Get the node name + const nodeName = Number(d3.select(this).attr("name")); + let currentNode = getNodeByName(nodeName); + console.log("Node ", nodeName, " is ", currentNode); + if (currentNode === undefined) { + console.error("Missing node: ", nodeName); + return; + } + let nodeAttributes = []; + nodeAttributes.push(["Node ID:", currentNode.name + currentNode.switched ? " (reversed)" : ""]) + nodeAttributes.push(["Node Length:", currentNode.sequenceLength + " bases"]) + nodeAttributes.push(["Haplotypes:", currentNode.degree]) + nodeAttributes.push(["Aligned Reads:", currentNode.incomingReads.length + currentNode.internalReads.length + currentNode.outgoingReads.length]); + + console.log("Single Click"); + console.log(config.showInfoCallback) + config.showInfoCallback(nodeAttributes) +} + // draw seqence labels for nodes function drawLabels(dNodes) { if (config.nodeWidthOption === 0) { From 57e92d553932ed3b35f072bad643676eceb7a7cc Mon Sep 17 00:00:00 2001 From: shreyasun Date: Fri, 14 Jul 2023 10:24:54 -0700 Subject: [PATCH 064/113] Created variable for DataPositionFormRow component so the code looks cleaner by only having a variable repeated, not the entire component --- src/components/HeaderForm.js | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index 94b85921..7219e70a 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -675,6 +675,15 @@ class HeaderForm extends Component { this.state.fileSelectOptions ); + const DataPositionFormRowComponent = + return (
{errorDiv} @@ -739,14 +748,7 @@ class HeaderForm extends Component { {mountedFilesFlag &&
- + {DataPositionFormRowComponent} ) : ( - !mountedFilesFlag && - + !mountedFilesFlag && DataPositionFormRowComponent )} From 46af08800c002e27153da2729fba61770650cae2 Mon Sep 17 00:00:00 2001 From: ducku Date: Sun, 16 Jul 2023 16:18:05 -0700 Subject: [PATCH 065/113] selecting a region option updates tracks --- exampleData/cactus1-20.bed | 2 + .../chunk-ref-1-20/chunk-1_0_ref_0_1926.gam | Bin 0 -> 816 bytes exampleData/chunk-ref-1-20/chunk.vg | 122 ++++++++++++++++++ .../chunk_0_ref_0_1926.annotate.txt | 0 .../chunk-ref-1-20/chunk_0_ref_0_1926.gam | Bin 0 -> 751 bytes exampleData/chunk-ref-1-20/regions.tsv | 1 + exampleData/chunk-ref-1-20/tracks.json | 8 ++ src/components/HeaderForm.js | 37 ++---- src/components/RegionInput.js | 19 ++- src/components/RegionInput.test.js | 2 +- src/server.mjs | 27 +--- 11 files changed, 164 insertions(+), 54 deletions(-) create mode 100644 exampleData/cactus1-20.bed create mode 100644 exampleData/chunk-ref-1-20/chunk-1_0_ref_0_1926.gam create mode 100644 exampleData/chunk-ref-1-20/chunk.vg create mode 100644 exampleData/chunk-ref-1-20/chunk_0_ref_0_1926.annotate.txt create mode 100644 exampleData/chunk-ref-1-20/chunk_0_ref_0_1926.gam create mode 100644 exampleData/chunk-ref-1-20/regions.tsv create mode 100644 exampleData/chunk-ref-1-20/tracks.json diff --git a/exampleData/cactus1-20.bed b/exampleData/cactus1-20.bed new file mode 100644 index 00000000..cd6c7662 --- /dev/null +++ b/exampleData/cactus1-20.bed @@ -0,0 +1,2 @@ +ref 1 10 region one to ten chunk-ref-1-20 +ref 10 20 region ten to twenty chunk-ref-1-20 \ No newline at end of file diff --git a/exampleData/chunk-ref-1-20/chunk-1_0_ref_0_1926.gam b/exampleData/chunk-ref-1-20/chunk-1_0_ref_0_1926.gam new file mode 100644 index 0000000000000000000000000000000000000000..bbacfe60e68e8299dd340b6cf609c283c2ff9445 GIT binary patch literal 816 zcmV-01JC>)iwFb&00000{{{d;LjnL31MSvNYZE~f0C2NuOa3*PZpjuhAZ}qG6sp;6 zQWAS8!yp5SwS*Dc91HmZo{BebeiJVW_2MV+?$K``coEN@eDBTdCR=yY6rx@_Afd zF>oGZy0TGtO@H8vnpY|LJ8ZkNo%56aTE$~tx8yfqmQ4;BrwzSv#PT4gtqwgwx1bOX7qJVWUa#_V4@68bZAQpe?rLxXKV2 zW^%a>M#RNui{yz6c5yz7UC*2owIH+}0Y$A=ke&%!G*<{aq5Hnto91%Cp8&3V)P54z z->cHBv1Gfd6(B*N?~fD76rvz@F%Wu;c7Hhm@PtD zHn?E9k%L{ZB0?IdtqT<)h)|c5$*6=$*;)rtIC5NN_7Z^VtnRqhAeyjAes(QejA6Lk zAmJ*|QHeCU7G+mF&-D?xFkvuB6m2gG(}FCplM)OUu1JwQ6@Ixi!(Jf5$w)_%I^o9c zkg^Q8c70acsH`*bpaeum>At6})QUJ$u?h*Dv9pZWCfh8k%V_Dk%x%|a@}vbp z;J)dTQ(ayZ)McC{qoun?0>mL0+|m$oRi2J)gFPdnHfuYwb&89gEASq5Te77R5~)Jo z;)08S7{di*qdYRYvDqgkWp{hKov(~CB|9LZeYT$=8?s^1&GHX29tJrEw{61Kq)0DO zOon&t8QI(Nzj~m3?Oy^}g&~p8nw@6$XMwHCs{WXrbWT?L%iYrdeJbu%3m=Jbi+nt2 uz54p))A4Tt)=Gh33;+NhiwFb&00000{{{d;LjnLB00RI3000000000mSba_a literal 0 HcmV?d00001 diff --git a/exampleData/chunk-ref-1-20/chunk.vg b/exampleData/chunk-ref-1-20/chunk.vg new file mode 100644 index 00000000..5068af8f --- /dev/null +++ b/exampleData/chunk-ref-1-20/chunk.vg @@ -0,0 +1,122 @@ +VGó + +GT +d +`GGAAGTGTTTGCTACCAAGTTTATTTGCAGTGTTAACAGCACAACATTTACAAAACGTATTTTGTACAATCAAGTCTTCACTGCCCTTGCACACTG +d +`GGGGGGCTAGGGAAGACCTAGTCCTTCCAACAGCTATAAACAGTCCTGGATAATGGGTTTATGAAAAACACTTTTTCTTCCTTCAGCAAGCAAAAT +d +`TATTTATGAAGCTGTATGGTTTCAGCAACAGGGAGCAAAGGAAAAAAATCACCTCAAAGAAAGCAACAGCTTCCTTCCTGGTGGGATCTGTCATTT +d +`TATAGATATGAAATATTCATGCCAGAGGTCTTATATTTTAAGAGGAATGGATTATATACCAGAGCTACAACAATAAACATTTTACTTATTACTAAT +d +`GAGGAATTAGAAGACTGTCTTTGGAAACCGGTTCTTGAAAATCTTCTGCTGTTTTAGAACACATTCTTTAGAAATCTAGCAAATATATCTCAGACT +d +`TTTAGAAATCTCTTCTAGTTTCATTTTCCTTTTTTTTTTTTTTTTTTTGAGCCACAGTCTCACTGTCACCCAGGCTGGAGTGCCGTGGTATGATCT +d +`TGGCTCACTGCAACCTCCACCTCCCGGGCTGAAGTGATTCTCCTGCCTTAGCCACCTGAGTAGCTGGGATTACAGGTGTCCACCACCATGACCGGC +d +`TAATTTCTGTATTTTTAGTAGAGATGGGGTTTCACCATGTTGGCCAGGCTGGTTTCGAACTCCTGACCTCCAGTGATCTGCCCACCTTGGCCTCCC +d +`AAAGTGCTGGGATTACAGGCGTGAGCCACCATGCCCAGGTTTCAAGTTTCCTTTTCATTTCTAATACCTGCCTCAGAATTTCCTCCCCAATGTTCC + +d +`ACTCCAACATTTGAGAACTGCCCAAGGACTATTCTGACTTTAAGTCACATAATCGATCCCAAGCACTCTCCTTCCATTGAAGGGTCTGACTCTCTG +d +`CCTTTGTGAACACAGGGTTTTAGAGAAGTAAACTTAGGGAAACCAGCTATTCTCTTGAGGCCAAGCCACTCTGTGCTTCCAGCCCTAAGCCAACAA +d +`CAGCCTGAATAGAAAGAATAGGGCTGATAAATAATGAATCAGCATCTTGCTCAATTGGTGGCGTTTAAATGGTTTTAAAATCTTCTCAGGTGAAAA +d +`ATTACCATAATTTTGTGCTCATGGCAGATTTCCAAGGGAGACTTCAAGCAGAAAATCTTTAAGGGACCCTTGCATAGCCAGAAGTCCTTTTCAGGC +d +`TGATGTACATAAAATATTTAGTAGCCAGGACAGTAGAAGGACTGAAGAGTGAGAGGAGCTCCCAGGGCCTGGAAAGGCCACTTTGTAAGCTCATTC +d +`TTGGGGTCCTGTGGCTCTGTACCTGTGGCTGGCTGCAGTCAGTAGTGGCTGTGGGGGATCTGGGGTATCAGGTAGGTGTCCAGCTCCTGGCACTGG +d +`TAGAGTGCTACACTGTCCAACACCCACTCTCGGGTCACCACAGGTGCCTCACACATCTGCCCAATTGCTGGAGACAGAGAACACAAGCAGAGATTA +d +`GTGTCAATTCATTCTCCTGGACTAGGCTCTAATCAATCGACTCCAGGGTCCTGGTTGTATGAGTTCTTAGGATTAATGAGGTAGAAGCTAATTTTT +d +`TTTTTTTTTTTTTGAGACGGAGTCTTGCTCTGTCGCCGAGGCTAGAGTGTGATGGCGCAATCTCGGCTCATTCAACCTCCGCCTCCTGGGTTCAAG +d +`CAATTCTCCTGTCTCTGCCTCCTGAGTAGCTGGAATTACAGGCACATGCCATCACACCCAGCTAATTTTTGTATTTTTAGTAGAGACGGGGGTTTC +d +`ACAATGTTGGCCAGGCTGCTCTGGAACTCCTGACCTCAGGTGATCCACCCACCTTGGCCTCCCAAAGTGCTGGGATTACAGGCGTGAGCCACTGCA + +CCTGG   + +      ä +GI262359905[79264] + ( + ``( + ``( + ``( + ``( + ``( + ``( + ``( + ``(  + ``( + + ``(  + ``(  + + ``(  + ``( + ``( + ``( + ``( + ``( + ``( + ``( + ``(Ä +GI528476558[0] +( +``( +``( +``( +``( +``( +``( +``( + ``(  + +``( + + ``(  + ``(  + ``(  +``( +``( +``( +``( +``( +``( +``( +``( +(¹ +ref +( +``( +``( +``( +``( +``( +``( +``( + ``(  + +``( + + ``(  + ``(  + ``(  +``( +``( +``( +``( +``( +``( +``( +``( +( \ No newline at end of file diff --git a/exampleData/chunk-ref-1-20/chunk_0_ref_0_1926.annotate.txt b/exampleData/chunk-ref-1-20/chunk_0_ref_0_1926.annotate.txt new file mode 100644 index 00000000..e69de29b diff --git a/exampleData/chunk-ref-1-20/chunk_0_ref_0_1926.gam b/exampleData/chunk-ref-1-20/chunk_0_ref_0_1926.gam new file mode 100644 index 0000000000000000000000000000000000000000..71b70cfa07a0f0b96f1af89aa7f9509ffd392bd7 GIT binary patch literal 751 zcmVaz`Z1m$)Vn|nhj*D&D4*FGu1tWivcYYBjcY*@gx zVyjAAN}Wsq7x2QUMG|{H_|lWKB}u^kFSh)Y^r*Zu03P-R^AiyHu< z&>@5yA!mbIF(oNE!cCa6bke#cxSFgFfYxlC;A(+&$PTM<%`L85*;Sou>E5ngE0ARY zrW&P|J>>7eH80}&j<_g7iq+thK*LoC6>NYB52aR$0#eU111EqIQ8mXFA?{&%S}&vw zr}+_QZo6~9-VGf<)MGsWh5%b+U$V=(g0#r)q;R)KDuJIvOGdd98Fqy-GQyQq8s^nr z5N62U5~6?wC1gvstSd@Y$}-PZUK&0&H!1VPlGMbKZibx+3RI*Aq~#^rR-r|YR%Kgy z^*szb!!2Z3lN24qM{M>$Fhlm55QQu(AyZ7%6{f1svU}uSuB2L?V?kb+H#LP>K=!xk zZa1kUnNhHj0rh=sV7Q_;L=K=KUhAeK>!GuQ8t%Y<5pYG*Y@jgzIQ%6L;_L%|%DRJY z<_y3N*Z~0i;Xq@1&hfvY!5?=V<_w(8gDKcUoBV>U2kQq53V!7mIpA=r7PILh#8%Qy z#q;P#%5C_-C{rtOf=7zXPFC6_p>x8s7vMeK*u55&F92=i { - this.setState({ error: null }); - try { - const json = await fetchAndParse(`${this.props.apiUrl}/getChunkPath`, { - signal: this.cancelSignal, - method: "GET", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ bedFile, dataPath, parsedRegion }), - }); - - if (!json.chunkPath) { - const error = json.error || "Server did not send back chunkPath" - this.setState({ error: error }); - } else { - return json.chunkPath; - } - - } catch (error) { - this.handleFetchError( - error, - `GET to ${this.props.apiUrl}/getChunkPath failed:` - ); - } - } - getMountedFilenames = async () => { this.setState({ error: null }); try { @@ -544,7 +517,7 @@ class HeaderForm extends Component { const regionString = regionChr.concat(":", regionStart, "-", regionEnd); return regionString; }; - handleRegionChange = (value) => { + handleRegionChange = (value, tracks) => { // After user selects a region name or coordinates, // update path and region let coords = value; @@ -557,6 +530,12 @@ class HeaderForm extends Component { coords = this.getRegionCoords(value); } this.setState({ region: coords }); + + // Override current tracks with new tracks from chunk dir + if (tracks) { + this.setState({ tracks: tracks }); + console.log("New tracks have been population"); + } }; handleInputChange = (newTracks) => { diff --git a/src/components/RegionInput.js b/src/components/RegionInput.js index 3f22440f..506628b9 100644 --- a/src/components/RegionInput.js +++ b/src/components/RegionInput.js @@ -20,6 +20,9 @@ export const RegionInput = ({ const pathNamesColon = pathNames.map((name) => name + ":"); const pathsWithRegion = []; + // Store possible options from bed file and match them with their respective tracks + let optionToTrack = {}; + if (regionInfo && !isEmpty(regionInfo)) { // Stitch path name + region start and end for (const [index, path] of regionInfo["chr"].entries()) { @@ -27,14 +30,20 @@ export const RegionInput = ({ path + ":" + regionInfo.start[index] + "-" + regionInfo.end[index] ); } + + // populate optionToTrack with paths with region + pathsWithRegion.forEach((path, i) => optionToTrack[path] = regionInfo.tracks[i]); + // Add descriptions pathsWithRegion.push(...regionInfo["desc"]); + + // populate optionToTrack with paths with description as keys + regionInfo.desc.forEach((desc, i) => optionToTrack[desc] = regionInfo.tracks[i]); } // Autocomplete selectable options const displayRegions = [...pathsWithRegion, ...pathNamesColon]; - console.log("region info", regionInfo); return ( <> { - handleRegionChange(newInputValue); + // If an option is selected, should have a match in optionToTrack + if (event.target.textContent in optionToTrack) { + // also pass tracks associated with the option + handleRegionChange(newInputValue, optionToTrack[event.target.textContent]); + } else { + handleRegionChange(newInputValue, null); + } }} options={displayRegions} diff --git a/src/components/RegionInput.test.js b/src/components/RegionInput.test.js index 93dbf5f3..2c6449b5 100644 --- a/src/components/RegionInput.test.js +++ b/src/components/RegionInput.test.js @@ -60,5 +60,5 @@ test("it calls handleRegionChange when region is changed with new region", async await userEvent.clear(input); await userEvent.type(input, NEW_REGION); - expect(handleRegionChangeMock).toHaveBeenLastCalledWith(NEW_REGION); + expect(handleRegionChangeMock).toHaveBeenLastCalledWith(NEW_REGION, null); }); diff --git a/src/server.mjs b/src/server.mjs index ed31d5bb..1a6562e8 100644 --- a/src/server.mjs +++ b/src/server.mjs @@ -1085,24 +1085,6 @@ api.post("/getBedRegions", (req, res) => { } }); -// request for getting the chunk path of the specified bed file -api.post("/getChunkPath", (req, res) => { - console.log("received request for getChunkPath"); - const result = { - chunkPath: "" - } - - if (!req.body.bedFile) { - throw new BadRequestError("No BED file specified"); - } else if (!req.body.dataPath) { - throw new BadRequestError("No data path specified"); - } else { - dataPath = pickDataPath(req.body.dataPath); - result.chunkPath = getChunkPath(req.body.bedFile, dataPath, req.body.parsedRegion); - res.json(result); - } -}); - // Load up the given BED file, relative to the given pre-resolved dataPath, and // return a data structure decribing all the pre-cached regions it defines. // Validates file paths for user-accessibility. May throw. @@ -1120,7 +1102,7 @@ function getBedRegions(bedFile, dataPath) { throw new BadRequestError("BED file not found: " + bedFile); } - let bed_info = { chr: [], start: [], end: [], desc: [], chunk: [], chunk_path: [], tracks: []}; + let bed_info = { chr: [], start: [], end: [], desc: [], chunk: [], tracks: []}; // Load and parse the BED file let bed_data = fs.readFileSync(bed_path).toString(); @@ -1144,16 +1126,17 @@ function getBedRegions(bedFile, dataPath) { chunk = records[4]; } bed_info["chunk"].push(chunk); - const chunk_path = `${dataPath}${chunk}`; - bed_info["chunk_path"].push(chunk_path); + let tracks = {}; let trackID = 1; + const chunk_path = `${dataPath}${chunk}`; const track_json = path.join(chunk_path, "tracks.json"); - // if json file specifying the tracks exists + // If json file specifying the tracks exists if (fs.existsSync(track_json)) { + // Read json file and create a tracks object from it const json_data = JSON.parse(fs.readFileSync(track_json)); if (json_data["graph_file"] !== "") { From a1737bf55aa128eb570a0392c8c1698679070b74 Mon Sep 17 00:00:00 2001 From: ducku Date: Fri, 21 Jul 2023 18:55:51 -0700 Subject: [PATCH 066/113] Prepare Chunk optimization and recreating example chunk and beds --- README.md | 11 +++- ...ef_0_1926.gam => chunk-1_0_ref_0_1921.gam} | Bin exampleData/chunk-ref-1-20/chunk.vg | 57 ++++++++---------- ...te.txt => chunk_0_ref_0_1921.annotate.txt} | 0 ..._ref_0_1926.gam => chunk_0_ref_0_1921.gam} | Bin exampleData/chunk-ref-1-20/regions.tsv | 2 +- .../chunk.vg | 0 .../chunk_0_ref_1955_5023.annotate.txt | 0 .../chunk_0_ref_1955_5023.gam | Bin exampleData/chunk-ref-2000-3000/regions.tsv | 1 + exampleData/chunk-ref-2000-3000/tracks.json | 7 +++ exampleData/test_prechunk/regions.tsv | 1 - scripts/prepare_chunks.sh | 29 ++++----- 13 files changed, 58 insertions(+), 50 deletions(-) rename exampleData/chunk-ref-1-20/{chunk-1_0_ref_0_1926.gam => chunk-1_0_ref_0_1921.gam} (100%) rename exampleData/chunk-ref-1-20/{chunk_0_ref_0_1926.annotate.txt => chunk_0_ref_0_1921.annotate.txt} (100%) rename exampleData/chunk-ref-1-20/{chunk_0_ref_0_1926.gam => chunk_0_ref_0_1921.gam} (100%) rename exampleData/{test_prechunk => chunk-ref-2000-3000}/chunk.vg (100%) rename exampleData/{test_prechunk => chunk-ref-2000-3000}/chunk_0_ref_1955_5023.annotate.txt (100%) rename exampleData/{test_prechunk => chunk-ref-2000-3000}/chunk_0_ref_1955_5023.gam (100%) create mode 100644 exampleData/chunk-ref-2000-3000/regions.tsv create mode 100644 exampleData/chunk-ref-2000-3000/tracks.json delete mode 100644 exampleData/test_prechunk/regions.tsv diff --git a/README.md b/README.md index d4d762e5..c6910b55 100644 --- a/README.md +++ b/README.md @@ -131,18 +131,27 @@ That can sometimes up to 10-20 seconds. If you already know of regions/subgraphs that you will be looking at, you can pre-fetch the data in advance. This will save some time during the interactive visualization, especially if there are a lot of regions to visualize. +This is a 2 step process that involves creating the chunk and linking it to a bed file + +1. The subgraphs need to be pre-fetched using `vg chunk` like shown in [`prepare_chunks.sh`](scripts/prepare_chunks.sh). For example: ``` ./prepare_chunk.sh -x mygraph.xg -h mygraph.gbwt -r chr1:1-100 -o chunk-chr1-1-100 -g mygam1.gam -g mygam2.gam ``` +2. Then compile those regions in a BED file with two additional columns: - a description of the region (column 4) - the path to the output directory of the chunk, `chunk-chr1-1-100` in the example above, (column 5). -See an example in [`cactus.bed`](exampleData/cactus.bed). +``` +ref 1 10 region one to ten chunk-ref-1-20 +ref 10 20 region ten to twenty chunk-ref-1-20 +``` +Note each column is seperated by tabs + This BED file will be read if placed in the `dataPath` directory, like for other files to mount (see above). #### Development Mode diff --git a/exampleData/chunk-ref-1-20/chunk-1_0_ref_0_1926.gam b/exampleData/chunk-ref-1-20/chunk-1_0_ref_0_1921.gam similarity index 100% rename from exampleData/chunk-ref-1-20/chunk-1_0_ref_0_1926.gam rename to exampleData/chunk-ref-1-20/chunk-1_0_ref_0_1921.gam diff --git a/exampleData/chunk-ref-1-20/chunk.vg b/exampleData/chunk-ref-1-20/chunk.vg index 5068af8f..501edb40 100644 --- a/exampleData/chunk-ref-1-20/chunk.vg +++ b/exampleData/chunk-ref-1-20/chunk.vg @@ -1,4 +1,4 @@ -VGó +VG¶  GT d @@ -41,35 +41,32 @@ d d `CAATTCTCCTGTCTCTGCCTCCTGAGTAGCTGGAATTACAGGCACATGCCATCACACCCAGCTAATTTTTGTATTTTTAGTAGAGACGGGGGTTTC d -`ACAATGTTGGCCAGGCTGCTCTGGAACTCCTGACCTCAGGTGATCCACCCACCTTGGCCTCCCAAAGTGCTGGGATTACAGGCGTGAGCCACTGCA - -CCTGG   +`ACAATGTTGGCCAGGCTGCTCTGGAACTCCTGACCTCAGGTGATCCACCCACCTTGGCCTCCCAAAGTGCTGGGATTACAGGCGTGAGCCACTGCA    -      ä -GI262359905[79264] - ( - ``( - ``( - ``( - ``( - ``( - ``( - ``( - ``(  - ``( +      Ô +GI262359905[79269] + ``( + ``( + ``( + ``( + ``( + ``( + ``( + ``( + ``(  + ``(  - ``(  - ``(  + ``(   - ``(  - ``( - ``( - ``( - ``( - ``( - ``( - ``( - ``(Ä + ``(  + ``(  + ``( + ``( + ``( + ``( + ``( + ``( + ``(¶ GI528476558[0] ( ``( @@ -93,8 +90,7 @@ d ``( ``( ``( -``( -(¹ +``(« ref ( ``( @@ -118,5 +114,4 @@ d ``( ``( ``( -``( -( \ No newline at end of file +``( \ No newline at end of file diff --git a/exampleData/chunk-ref-1-20/chunk_0_ref_0_1926.annotate.txt b/exampleData/chunk-ref-1-20/chunk_0_ref_0_1921.annotate.txt similarity index 100% rename from exampleData/chunk-ref-1-20/chunk_0_ref_0_1926.annotate.txt rename to exampleData/chunk-ref-1-20/chunk_0_ref_0_1921.annotate.txt diff --git a/exampleData/chunk-ref-1-20/chunk_0_ref_0_1926.gam b/exampleData/chunk-ref-1-20/chunk_0_ref_0_1921.gam similarity index 100% rename from exampleData/chunk-ref-1-20/chunk_0_ref_0_1926.gam rename to exampleData/chunk-ref-1-20/chunk_0_ref_0_1921.gam diff --git a/exampleData/chunk-ref-1-20/regions.tsv b/exampleData/chunk-ref-1-20/regions.tsv index 9e266d66..2ffe033a 100644 --- a/exampleData/chunk-ref-1-20/regions.tsv +++ b/exampleData/chunk-ref-1-20/regions.tsv @@ -1 +1 @@ -ref 0 1927 chunk-ref-1-20/chunk_0_ref_0_1926.gam chunk-ref-1-20/chunk_0_ref_0_1926.annotate.txt +ref 0 1922 chunk-ref-1-20/chunk_0_ref_0_1921.gam chunk-ref-1-20/chunk_0_ref_0_1921.annotate.txt diff --git a/exampleData/test_prechunk/chunk.vg b/exampleData/chunk-ref-2000-3000/chunk.vg similarity index 100% rename from exampleData/test_prechunk/chunk.vg rename to exampleData/chunk-ref-2000-3000/chunk.vg diff --git a/exampleData/test_prechunk/chunk_0_ref_1955_5023.annotate.txt b/exampleData/chunk-ref-2000-3000/chunk_0_ref_1955_5023.annotate.txt similarity index 100% rename from exampleData/test_prechunk/chunk_0_ref_1955_5023.annotate.txt rename to exampleData/chunk-ref-2000-3000/chunk_0_ref_1955_5023.annotate.txt diff --git a/exampleData/test_prechunk/chunk_0_ref_1955_5023.gam b/exampleData/chunk-ref-2000-3000/chunk_0_ref_1955_5023.gam similarity index 100% rename from exampleData/test_prechunk/chunk_0_ref_1955_5023.gam rename to exampleData/chunk-ref-2000-3000/chunk_0_ref_1955_5023.gam diff --git a/exampleData/chunk-ref-2000-3000/regions.tsv b/exampleData/chunk-ref-2000-3000/regions.tsv new file mode 100644 index 00000000..ead04632 --- /dev/null +++ b/exampleData/chunk-ref-2000-3000/regions.tsv @@ -0,0 +1 @@ +ref 1955 5024 chunk-ref-2000-3000/chunk_0_ref_1955_5023.gam chunk-ref-2000-3000/chunk_0_ref_1955_5023.annotate.txt diff --git a/exampleData/chunk-ref-2000-3000/tracks.json b/exampleData/chunk-ref-2000-3000/tracks.json new file mode 100644 index 00000000..5ff79bd3 --- /dev/null +++ b/exampleData/chunk-ref-2000-3000/tracks.json @@ -0,0 +1,7 @@ +{ + "graph_file": "cactus.vg", + "haplotype_file": "", + "gam_files": [ + "cactus-NA12879.sorted.gam" + ] +} diff --git a/exampleData/test_prechunk/regions.tsv b/exampleData/test_prechunk/regions.tsv deleted file mode 100644 index 5e8f52e9..00000000 --- a/exampleData/test_prechunk/regions.tsv +++ /dev/null @@ -1 +0,0 @@ -ref 1955 5024 test_prechunk/chunk_0_ref_1955_5023.gam test_prechunk/chunk_0_ref_1955_5023.annotate.txt diff --git a/scripts/prepare_chunks.sh b/scripts/prepare_chunks.sh index c044eaa0..b2cb5f45 100755 --- a/scripts/prepare_chunks.sh +++ b/scripts/prepare_chunks.sh @@ -13,6 +13,12 @@ do esac done +if ! command -v jq &> /dev/null +then + echo "This script requires jq, exiting..." + exit +fi + echo "XG File: " $XG_FILE echo "Haplotype File: " $GBWT echo "Region: " $REGION @@ -23,38 +29,29 @@ mkdir -p $OUTDIR vg_chunk_params="-x $XG_FILE -g -c 20 -p $REGION -T -b $OUTDIR/chunk -E $OUTDIR/regions.tsv" -GAM_FILES_JSON="" +GAM_FILES_STRING="" echo "Gam Files:" for GAM_FILE in "${GAM_FILES[@]}"; do echo " - $GAM_FILE" - # Put gam files into array format - if [ -z "$GAM_FILES_JSON" ] - then - GAM_FILES_JSON="[$GAM_FILES_JSON\"$GAM_FILE\"" - else - GAM_FILES_JSON="$GAM_FILES_JSON, \"$GAM_FILE\"" - fi + # Put gam files into string format to be parsed by jq + GAM_FILES_STRING="$GAM_FILES_STRING$GAM_FILE\n" vg_chunk_params=" $vg_chunk_params -a $GAM_FILE" done -GAM_FILES_JSON="$GAM_FILES_JSON]" - -echo $GAM_FILES_JSON - # Call vg chunk vg chunk $vg_chunk_params > $OUTDIR/chunk.vg +GAM_FILES_JSON=$(printf "$GAM_FILES_STRING" | jq -R '[.]' | jq -n '[inputs[]]') -# Cosntruct JSON + +# Construct tracks JSON, containing all tracks used to create the chunk JSON_STRING=$(jq -n \ --arg graph_file "$XG_FILE" \ --arg haplotype_file "$GBWT" \ --argjson gam_files "$GAM_FILES_JSON" \ '$ARGS.named' ) -printf "%s\n" "$JSON_STRING" > $OUTDIR/tracks.json - - +printf "%s\n" "$JSON_STRING" > $OUTDIR/tracks.json \ No newline at end of file From d1a1cdf643bf3e388baf00909e4af712259e63b6 Mon Sep 17 00:00:00 2001 From: ducku Date: Sat, 22 Jul 2023 14:06:39 -0700 Subject: [PATCH 067/113] fix region input onclick dropdown --- src/components/RegionInput.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/RegionInput.js b/src/components/RegionInput.js index 506628b9..d7192678 100644 --- a/src/components/RegionInput.js +++ b/src/components/RegionInput.js @@ -44,12 +44,14 @@ export const RegionInput = ({ // Autocomplete selectable options const displayRegions = [...pathsWithRegion, ...pathNamesColon]; + console.log("displayRegions", displayRegions); return ( <> option.title || option.toString()} + autoHighlight + getOptionLabel={(option) => option.label || option.toString()} value={region} inputValue={region} data-testid="autocomplete" @@ -70,10 +72,11 @@ export const RegionInput = ({ )} From 3b2cbfe4c9355ea18ac38060c93b418b65433713 Mon Sep 17 00:00:00 2001 From: ducku Date: Fri, 28 Jul 2023 13:38:02 -0700 Subject: [PATCH 068/113] Revert "fix region input onclick dropdown" This reverts commit d1a1cdf643bf3e388baf00909e4af712259e63b6. --- src/components/RegionInput.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/RegionInput.js b/src/components/RegionInput.js index d7192678..506628b9 100644 --- a/src/components/RegionInput.js +++ b/src/components/RegionInput.js @@ -44,14 +44,12 @@ export const RegionInput = ({ // Autocomplete selectable options const displayRegions = [...pathsWithRegion, ...pathNamesColon]; - console.log("displayRegions", displayRegions); return ( <> option.label || option.toString()} + getOptionLabel={(option) => option.title || option.toString()} value={region} inputValue={region} data-testid="autocomplete" @@ -72,11 +70,10 @@ export const RegionInput = ({ )} From 933b705333a23027945a6a5dd08ea2a25795cfe7 Mon Sep 17 00:00:00 2001 From: ducku Date: Sat, 29 Jul 2023 14:45:27 -0700 Subject: [PATCH 069/113] move selection dropdown menu's zIndex to the top --- src/components/RegionInput.js | 4 ++-- src/components/SelectionDropdown.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/RegionInput.js b/src/components/RegionInput.js index 58d63f42..47ea7098 100644 --- a/src/components/RegionInput.js +++ b/src/components/RegionInput.js @@ -44,7 +44,7 @@ export const RegionInput = ({ value={region} inputValue={region} data-testid="autocomplete" - id="regionInput" + id="regionInput" onInputChange={(event, newInputValue) => { handleRegionChange(newInputValue); @@ -55,7 +55,7 @@ export const RegionInput = ({ Date: Sat, 29 Jul 2023 14:51:46 -0700 Subject: [PATCH 070/113] Revert "allow prepare_chunk script to take multiple gam files" This reverts commit 4995b739bd8e7f5a3d19e2073eefad6b59f477a6. --- README.md | 2 +- scripts/prepare_chunks.sh | 33 ++++++--------------------------- 2 files changed, 7 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index d4d762e5..26061857 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ This will save some time during the interactive visualization, especially if the The subgraphs need to be pre-fetched using `vg chunk` like shown in [`prepare_chunks.sh`](scripts/prepare_chunks.sh). For example: ``` -./prepare_chunk.sh -x mygraph.xg -h mygraph.gbwt -r chr1:1-100 -o chunk-chr1-1-100 -g mygam1.gam -g mygam2.gam +XG=mygraph.xg GAM=mygam.gam GBWT=mygraph.gbwt REGION=chr1:1-100 OUTDIR=chunk-chr1-1-100 ./prepare_chunks.sh ``` Then compile those regions in a BED file with two additional columns: diff --git a/scripts/prepare_chunks.sh b/scripts/prepare_chunks.sh index ed1502c1..e4cf7511 100755 --- a/scripts/prepare_chunks.sh +++ b/scripts/prepare_chunks.sh @@ -1,34 +1,13 @@ #!/usr/bin/env bash set -e - -while getopts x:h:g:r:o: flag -do - case "${flag}" in - x) XG_FILE=${OPTARG};; - h) GBWT=${OPTARG};; - g) GAM_FILES+=("$OPTARG");; - r) REGION=${OPTARG};; - o) OUTDIR=${OPTARG};; - esac -done - -echo "XG File: " $XG_FILE -echo "Haplotype File: " $GBWT -echo "Region: " $REGION -echo "Output Directory: " $OUTDIR +echo "xg file: ${XG}" +echo "gbwt file: ${GBWT}" +echo "gam file: ${GAM}" +echo "region: ${REGION}" +echo "output directory: ${OUTDIR}" rm -fr $OUTDIR mkdir -p $OUTDIR -vg_chunk_params="-x $XG_FILE -g -c 20 -p $REGION -T -b $OUTDIR/chunk -E $OUTDIR/regions.tsv" - -echo "Gam Files:" -for GAM_FILE in "${GAM_FILES[@]}"; do - echo " - $GAM_FILE" - vg_chunk_params=" $vg_chunk_params -a $GAM_FILE" -done - -echo $vg_chunk_params - -vg chunk $vg_chunk_params > $OUTDIR/chunk.vg +vg chunk -x $XG_FILE -a $GAM_FILE -g -c 20 -p $REGION -T -b $OUTDIR/chunk -E $OUTDIR/regions.tsv > $OUTDIR/chunk.vg From 718920bf552c1469af8d3cdb9ea273b9cb1ac1f5 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Mon, 31 Jul 2023 14:18:29 -0700 Subject: [PATCH 071/113] Drop commented-out code --- src/components/RegionInput.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/RegionInput.js b/src/components/RegionInput.js index 47ea7098..b520a1e1 100644 --- a/src/components/RegionInput.js +++ b/src/components/RegionInput.js @@ -55,7 +55,6 @@ export const RegionInput = ({ Date: Mon, 31 Jul 2023 18:37:49 -0400 Subject: [PATCH 072/113] Parse vg chunk GAM names and send back as an ordered array --- src/components/TubeMapContainer.js | 4 +-- src/server.mjs | 39 +++++++++++++++++++++++++----- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/components/TubeMapContainer.js b/src/components/TubeMapContainer.js index b03f9be5..7c0e42f0 100644 --- a/src/components/TubeMapContainer.js +++ b/src/components/TubeMapContainer.js @@ -174,8 +174,8 @@ class TubeMapContainer extends Component { let readsArr = []; // Count total reads seen so far. let totalReads = 0; - console.log("json gams", Object.values(json.gam)); - for (const gam of Object.values(json.gam)) { + console.log("json gams", json.gam); + for (const gam of json.gam) { // For each returned list of reads from a file, convert all those reads to tube map format. // Include total read count to prevent duplicate ids. // Also include the source track's ID. diff --git a/src/server.mjs b/src/server.mjs index 1a6562e8..fddd34d0 100644 --- a/src/server.mjs +++ b/src/server.mjs @@ -742,7 +742,7 @@ function processAnnotationFile(req, res, next) { } } -function processGamFile(req, res, next, gamFile) { +function processGamFile(req, res, next, gamFile, gamFileNumber) { try{ if (!isAllowedPath(gamFile)) { // This is probably under SCRATCH_DATA_PATH @@ -774,7 +774,8 @@ function processGamFile(req, res, next, gamFile) { .map(function (a) { return JSON.parse(a); }); - req.gamObj[gamFile] = gamArr; + // Organize the results by number + req.gamResults[gamFileNumber] = gamArr; req.gamRemaining -= 1; if (req.gamRemaining == 0) { processRegionFile(req, res, next); @@ -799,10 +800,36 @@ function processGamFiles(req, res, next) { } }); - req.gamObj = {}; + // Parse a GAM chunk name and get the GAM number from it + // Names are like: + // */chunk_*.gam for 0 + // */chunk-1_*.gam for 1, 2, 3, etc. + let gamNameToNumber = (gamName) => { + const pattern = /.*\/chunk(-([0-9])+)?_.*\.gam/ + let matches = gamName.match(pattern) + if (!matches) { + throw new InternalServerError("Bad GAM name " + gamName) + } + if (matches[2] !== undefined) { + // We have a number + return parseInt(matches[2]) + } + // If there's no number we are chunk 0 + return 0 + } + + // Sort all the GAM files we found in order of their chunk number, + // ascending. This will also be the order of the GAM files passed to chunk, + // and so the order we got the tracks in, and thus the order we want the + // results in. + gamFiles.sort((a, b) => { + return gamNameToNumber(a) - gamNameToNumber(b) + }) + + req.gamResults = []; req.gamRemaining = gamFiles.length; - for (const gamFile of gamFiles){ - processGamFile(req, res, next, gamFile); + for (let i = 0; i < gamFiles.length; i++){ + processGamFile(req, res, next, gamFiles[i], i); } console.timeEnd("processing gam files"); @@ -864,7 +891,7 @@ function cleanUpAndSendResult(req, res, next) { // TODO: Any standard error output will make an error response. result.error = req.error.toString("utf-8"); result.graph = req.graph; - result.gam = req.withGam === true ? req.gamObj : []; + result.gam = req.withGam === true ? req.gamResults : []; result.region = req.region; res.json(result); console.timeEnd("request-duration"); From 5d9ab4d444c269612787abb1ea40e07f7392ecb6 Mon Sep 17 00:00:00 2001 From: shreyasun Date: Mon, 31 Jul 2023 20:14:12 -0700 Subject: [PATCH 073/113] Popup Dialog to present information about node. Included functions to compute the number of reads visiting the node and the node coverage by reads. --- src/components/PopUpInfoDialog.js | 43 +++++ src/components/TubeMapContainer.js | 5 +- src/util/tubemap.js | 254 +++++++++++++++++++---------- 3 files changed, 215 insertions(+), 87 deletions(-) create mode 100644 src/components/PopUpInfoDialog.js diff --git a/src/components/PopUpInfoDialog.js b/src/components/PopUpInfoDialog.js new file mode 100644 index 00000000..ac243afb --- /dev/null +++ b/src/components/PopUpInfoDialog.js @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from "prop-types"; +import PopupDialog from './PopupDialog.js'; + +export const PopUpInfoDialog = ({ + open, + attributes, + close, +}) => { + return( +
+ +
Object Information
+ + + {/* Node info here */} + {(attributes || []).map(function(attribute){ + return + + + + })} + +
{attribute[0]}{attribute[1]}
+
+
+ ) +} + + +export default PopUpInfoDialog; + +PopUpInfoDialog.propTypes = { + open: PropTypes.bool.isRequired, + /* array argument of track attribute pairs containing attribute name as a string and attribute value + as a string or number */ + attributes: PropTypes.arrayOf( + PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + ) + ), + close: PropTypes.func.isRequired, +} diff --git a/src/components/TubeMapContainer.js b/src/components/TubeMapContainer.js index b03f9be5..d59cee50 100644 --- a/src/components/TubeMapContainer.js +++ b/src/components/TubeMapContainer.js @@ -7,7 +7,8 @@ import TubeMap from "./TubeMap"; import * as tubeMap from "../util/tubemap"; import { dataOriginTypes } from "../enums"; import { fetchAndParse } from "../fetchAndParse"; -import PopUpTrackInfo from "./PopUpTrackInfo"; +import PopUpInfoDialog from "./PopUpInfoDialog"; + class TubeMapContainer extends Component { state = { @@ -111,7 +112,7 @@ class TubeMapContainer extends Component { return (
- +
(Number(a) - Number(b))); + allSources.sort((a, b) => Number(a) - Number(b)); console.log("All sources: ", allSources); @@ -679,7 +679,6 @@ function placeReads() { // Makes the given node bigger if needed and moves other nodes down if needed. // If topMargin is set, applies that amount of spacing down from whatever is above the reads. function placeReadSet(readIDs, node, topMargin) { - // Parse arguments if (!topMargin) { topMargin = 0; @@ -689,12 +688,22 @@ function placeReadSet(readIDs, node, topMargin) { let toPlace = new Set(readIDs); // Get arrays of the read entry/exit/internal-ness records we want to work on - let incomingReads = node.incomingReads.filter(([readID, pathIndex]) => toPlace.has(readID)); - let outgoingReads = node.outgoingReads.filter(([readID, pathIndex]) => toPlace.has(readID)); - let internalReads = node.internalReads.filter((readID) => toPlace.has(readID)); + let incomingReads = node.incomingReads.filter(([readID, pathIndex]) => + toPlace.has(readID) + ); + let outgoingReads = node.outgoingReads.filter(([readID, pathIndex]) => + toPlace.has(readID) + ); + let internalReads = node.internalReads.filter((readID) => + toPlace.has(readID) + ); // Only actually use the top margin if we have any reads on the node. - if (incomingReads.length === 0 && outgoingReads.length === 0 && internalReads.length === 0) { + if ( + incomingReads.length === 0 && + outgoingReads.length === 0 && + internalReads.length === 0 + ) { topMargin = 0; } @@ -1117,7 +1126,9 @@ function generateTrackIndexSequences(tracksOrReads) { let nodeIndex = nodeMap.get(forward(nodeName)); if (nodeIndex === 0) { // If a node index is ever 0, we can't visit it in reverse, so we don't allow that to happen. - throw new Error('Node ' + forward(nodeName) + ' has prohibited index 0'); + throw new Error( + "Node " + forward(nodeName) + " has prohibited index 0" + ); } if (isReverse(nodeName) !== switched) { // If we visit the node in reverse XOR the node is switched, go through @@ -1126,7 +1137,7 @@ function generateTrackIndexSequences(tracksOrReads) { } else { // If either the node isn't switched and we go through it forward, or // the node is switched *and* we go through it backward, go through it - // left to right as displayed. + // left to right as displayed. track.indexSequence.push(nodeIndex); } }); @@ -1638,7 +1649,7 @@ function generateNodeDegree() { // Optimize the orientations for nodes in the global `nodes` for displaying the // paths in the global `tracks` and the read paths, if applicable, in the -// global `reads` +// global `reads` function switchNodeOrientation() { let pivotPath = tracks[0]; let countPaths = tracks.slice(1, tracks.length); @@ -1662,7 +1673,6 @@ function switchNodeOrientation() { // orientation, pass it as pivotPath. // References and modifies the global nodes variable. function switchNodeOrientationForPaths(paths, pivotPath) { - const toSwitch = new Map(); let nodeName; let prevNode; @@ -2069,7 +2079,7 @@ function generateLaneAssignment() { } } }); - + // Now sweep left to right across order slots and assign vertical lanes to all the segments. for (let i = 0; i <= maxOrder; i += 1) { generateSingleLaneAssignment(assignments[i], i); // this is where the lanes get assigned @@ -2404,9 +2414,6 @@ function calculateTrackWidth() { } } - - - function getColorSet(colorSetName) { // single color hex if (colorSetName.startsWith("#")) { @@ -2433,10 +2440,9 @@ function getColorSet(colorSetName) { } function generateTrackColor(track, highlight) { - if (typeof highlight === "undefined") highlight = "plain"; let trackColor; - + const sourceID = track.sourceTrackID; if (!config.colorSchemes[sourceID]) { if (track.hasOwnProperty("type") && track.type === "read") { @@ -2444,7 +2450,8 @@ function generateTrackColor(track, highlight) { config.colorSchemes[sourceID] = externalConfig.defaultReadColorPalette; } else { // Default to haplotype colors - config.colorSchemes[sourceID] = externalConfig.defaultHaplotypeColorPalette; + config.colorSchemes[sourceID] = + externalConfig.defaultHaplotypeColorPalette; } } @@ -3119,33 +3126,30 @@ function drawNodes(dNodes) { } function getPopUpNodeText(node) { - - return ( - `Node ID: ${node.name}` + (node.switched ? ` (reversed)` : ``) + `\n` - ); + return `Node ID: ${node.name}` + (node.switched ? ` (reversed)` : ``) + `\n`; } -// Get any track object by ID. -// Because of reordering of input tracks, the ID doesn't always match the index. +// Get any node object by name. function getNodeByName(nodeName) { - if (typeof nodeName !== "number") { - throw new Error("Node Names must be numbers"); + if (typeof nodeName !== "string") { + throw new Error("Node Names must be strings"); } // We just do a scan. // TODO: index! - for (let i = 0; i < nodes.length; i++) { + console.log("All nodes:", nodes); // + for (let i = 1; i < nodes.length; i++) { + // changes i to start from 1 instead of 0 if (nodes[i].name === nodeName) { - console.log("Found node with name ", nodeName, " at index ", i); + console.log("Found node with name", nodeName, "at index ", i); return nodes[i]; } } } - function nodeSingleClick() { /* jshint validthis: true */ - // Get the node name - const nodeName = Number(d3.select(this).attr("name")); + // Get the node name + const nodeName = d3.select(this).attr("id"); let currentNode = getNodeByName(nodeName); console.log("Node ", nodeName, " is ", currentNode); if (currentNode === undefined) { @@ -3153,14 +3157,88 @@ function nodeSingleClick() { return; } let nodeAttributes = []; - nodeAttributes.push(["Node ID:", currentNode.name + currentNode.switched ? " (reversed)" : ""]) - nodeAttributes.push(["Node Length:", currentNode.sequenceLength + " bases"]) - nodeAttributes.push(["Haplotypes:", currentNode.degree]) - nodeAttributes.push(["Aligned Reads:", currentNode.incomingReads.length + currentNode.internalReads.length + currentNode.outgoingReads.length]); + nodeAttributes.push([ + "Node ID:", + currentNode.name + currentNode.switched ? "(reversed)" : "", + ]); + nodeAttributes.push(["Node Length:", currentNode.sequenceLength + " bases"]); + nodeAttributes.push(["Haplotypes:", currentNode.degree]); + nodeAttributes.push([ + "Aligned Reads:", + currentNode.incomingReads.length + + currentNode.internalReads.length + + currentNode.outgoingReads.length, + ]); + nodeAttributes.push([ + "Number of Reads Visiting Node:", + numReadsVisitNode(currentNode), + ]); + nodeAttributes.push(["Coverage:", coverage(currentNode, reads)]); console.log("Single Click"); - console.log(config.showInfoCallback) - config.showInfoCallback(nodeAttributes) + console.log("node show info callback", config.showInfoCallback); + config.showInfoCallback(nodeAttributes); +} + +export function numReadsVisitNode(node) { + let countReads = new Set(); + // incoming reads are reads that enter the node but don't start within it. They are represented as + // an array of subarrays which have 2 elements: an index indicating the read index and read path's index. + // The first node will not have any incoming reads. + for (let readVisit of node.incomingReads) { + countReads.add(readVisit[0]); + } + // internal reads are reads that start and end within the node. They are represented as + // an array of values which indicate the read index. + for (let read of node.internalReads) { + countReads.add(read); + } + // outgoing reads are reads that exit the node when the read starts within it. They are represented as + // an array of subarrays which have 2 elements: an index indicating the read index and read path's index. + // The last node will not have any outgoing reads. + for (let readVisit of node.outgoingReads) { + countReads.add(readVisit[0]); + } + return countReads.size; +} + +// computes average number of reads passing through each base in the node +export function coverage(node, allReads) { + if (node.sequenceLength === 0) { + return 0.0; + } + let countBases = 0; + for (let readVisit of node.incomingReads) { + let readNum = readVisit[0]; + let readPathIndex = readVisit[1]; + let currRead = allReads[readNum]; + let numNodes = currRead.sequenceNew.length; + // if current node is the last node on the read path, add the finalNodeCoverLength number of bases + if (numNodes === readPathIndex + 1) { + countBases += currRead.finalNodeCoverLength; + // otherwise add the node's sequence length (width of node in bases) + } else { + countBases += node.sequenceLength; + } + } + // internal reads + for (let readVisit of node.internalReads) { + // compute coverage of read by finalNodeCoverLength and firstNodeOffset fields + // indicating read's starting and ending points within the node. + let readNum = readVisit; + let currRead = allReads[readNum]; + countBases += currRead.finalNodeCoverLength - currRead.firstNodeOffset; + } + // outgoing reads + for (let readVisit of node.outgoingReads) { + let readNum = readVisit[0]; + let currRead = allReads[readNum]; + // coverage of outgoing read would be the the distance between the end of the node and the + // starting point of the read within the node + countBases += node.sequenceLength - currRead.firstNodeOffset; + } + // average coverage is total number of bases traversed by all reads divided by sequence length (width of node in bases) + return Math.round((countBases / node.sequenceLength) * 100) / 100; } // draw seqence labels for nodes @@ -3779,19 +3857,21 @@ function trackSingleClick() { return; } let track_attributes = []; - track_attributes.push(["Name", current_track.name]) + track_attributes.push(["Name", current_track.name]); if (current_track.type === "read") { - track_attributes.push(["Sample Name", current_track.sample_name]) - track_attributes.push(["Primary Alignment?", current_track.is_secondary ? "Secondary" : "Primary"]) - track_attributes.push(["Read Group", current_track.read_group]) - track_attributes.push(["Score", current_track.score]) - track_attributes.push(["CIGAR string", current_track.cigar_string]) - track_attributes.push(["Mapping Quality", current_track.mapping_quality]) + track_attributes.push(["Sample Name", current_track.sample_name]); + track_attributes.push([ + "Primary Alignment?", + current_track.is_secondary ? "Secondary" : "Primary", + ]); + track_attributes.push(["Read Group", current_track.read_group]); + track_attributes.push(["Score", current_track.score]); + track_attributes.push(["CIGAR string", current_track.cigar_string]); + track_attributes.push(["Mapping Quality", current_track.mapping_quality]); } console.log("Single Click"); - console.log("read path", ); - console.log(config.showInfoCallback) - config.showInfoCallback(track_attributes) + console.log("read path"); + config.showInfoCallback(track_attributes); } // show track name when hovering mouse @@ -4006,51 +4086,50 @@ function compareReadsByLeftEnd2(a, b) { } // converts readPath, a vg Path object expressed as a JS object, to a CIGAR string -export function cigar_string (readPath) { +export function cigar_string(readPath) { if (DEBUG) { console.log("readPath mapping:", readPath.mapping); } let cigar = []; for (let i = 0; i < readPath.mapping.length; i += 1) { - let mapping = readPath.mapping[i] + let mapping = readPath.mapping[i]; for (let j = 0; j < mapping.edit.length; j += 1) { - let edit = mapping.edit[j] + let edit = mapping.edit[j]; // from_length is not 0, and from_length = to_length, this indicates a match - if (edit.from_length && edit.from_length === edit.to_length){ - cigar = append_cigar_operation(edit.from_length, 'M', cigar); - } - else { + if (edit.from_length && edit.from_length === edit.to_length) { + cigar = append_cigar_operation(edit.from_length, "M", cigar); + } else { // from_length can be 0, and from_length = to_length, this indicates a match - if (edit.from_length === edit.to_length){ - cigar = append_cigar_operation(edit.from_length, 'M', cigar); - } + if (edit.from_length === edit.to_length) { + cigar = append_cigar_operation(edit.from_length, "M", cigar); + } // if from_length > to_length, this indicates a deletion - else if (edit.from_length > edit.to_length){ + else if (edit.from_length > edit.to_length) { const del = edit.from_length - edit.to_length; const eq = edit.to_length; - if (eq){ - cigar = append_cigar_operation(eq, 'M', cigar); + if (eq) { + cigar = append_cigar_operation(eq, "M", cigar); } - cigar = append_cigar_operation(del, 'D', cigar); - } + cigar = append_cigar_operation(del, "D", cigar); + } // if from_length < to_length, this indicates an insertion - else if (edit.from_length < edit.to_length){ + else if (edit.from_length < edit.to_length) { const ins = edit.to_length - edit.from_length; const eq = edit.from_length; - if (eq){ - cigar = append_cigar_operation(eq, 'M', cigar); + if (eq) { + cigar = append_cigar_operation(eq, "M", cigar); } - cigar = append_cigar_operation(ins, 'I', cigar); + cigar = append_cigar_operation(ins, "I", cigar); } // if to_length is undefined, this indicates a deletion - else if (edit.from_length && edit.to_length === undefined){ + else if (edit.from_length && edit.to_length === undefined) { const del = edit.from_length; - cigar = append_cigar_operation(del, 'D', cigar); + cigar = append_cigar_operation(del, "D", cigar); } // if from_length is undefined, this indicates an insertion - else if (edit.from_length === undefined && edit.to_length){ + else if (edit.from_length === undefined && edit.to_length) { const ins = edit.to_length; - cigar = append_cigar_operation(ins, 'I', cigar); + cigar = append_cigar_operation(ins, "I", cigar); } } } @@ -4061,15 +4140,14 @@ export function cigar_string (readPath) { return cigar.join(""); } -function append_cigar_operation (length, operator, cigar){ +function append_cigar_operation(length, operator, cigar) { let last_operation = cigar[cigar.length - 1]; let last_length = cigar[cigar.length - 2]; // if duplicate operations, add the two operations and replace the most recent operation with this - if (last_operation === operator){ + if (last_operation === operator) { let newLength = last_length + length; cigar[cigar.length - 2] = newLength; - } - else { + } else { cigar.push(length); cigar.push(operator); } @@ -4079,7 +4157,13 @@ function append_cigar_operation (length, operator, cigar){ // Pull out reads from a server response into tube map internal format. // Use myTracks, and idOffset to compute IDs for each read. // Assign each read the given sourceTrackID. -export function vgExtractReads(myNodes, myTracks, myReads, idOffset, sourceTrackID) { +export function vgExtractReads( + myNodes, + myTracks, + myReads, + idOffset, + sourceTrackID +) { if (DEBUG) { console.log("Reads:"); console.log(myReads); From b825c3562c610d32dfb1ab9dd77701519345bfe9 Mon Sep 17 00:00:00 2001 From: shreyasun Date: Mon, 31 Jul 2023 20:15:19 -0700 Subject: [PATCH 074/113] Configured test for node coverage statistic using sample nodes and reads. --- src/util/tubemap.test.js | 304 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 299 insertions(+), 5 deletions(-) diff --git a/src/util/tubemap.test.js b/src/util/tubemap.test.js index e1d77edd..aeaa60b5 100644 --- a/src/util/tubemap.test.js +++ b/src/util/tubemap.test.js @@ -1,6 +1,9 @@ import { cigar_string } from "./tubemap"; +import { coverage } from "./tubemap"; +// cigar string test describe('cigar_string', () => { + // TEST 1 it('it can handle an edit that is a match and deletion', async () => { const readPath = { mapping: [ @@ -16,7 +19,7 @@ describe('cigar_string', () => { } expect(cigar_string(readPath)).toBe("4M2D"); }); - + // TEST 2 it('it can handle an edit that is a match and insertion', async () => { const readPath = { mapping: [ @@ -32,7 +35,7 @@ describe('cigar_string', () => { }; expect(cigar_string(readPath)).toBe("4M2I"); }); - + // TEST 3 it('it can handle edit with double-digit length values', async () => { const readPath = { mapping: [ @@ -71,7 +74,7 @@ describe('cigar_string', () => { }; expect(cigar_string(readPath)).toBe("32M14I10M40D"); }); - + // TEST 4 it('it can handle a mapping with multiple edits', async () => { const readPath = { mapping: [ @@ -163,7 +166,7 @@ describe('cigar_string', () => { }; expect(cigar_string(readPath)).toBe("8M1I1M1D15M22D3M1I8M1I24M1D9I"); }); - + // TEST 5 it('it can handle multiple edits with lengths of 0', async () => { const readPath = { mapping: [ @@ -211,4 +214,295 @@ describe('cigar_string', () => { }; expect(cigar_string(readPath)).toBe("0M"); }); - }); \ No newline at end of file + }); + + +// Node coverage test +describe('coverage', () => { + // TEST #1 + it('can handle zero-length node without reads', async () => { + const node = { + sequenceLength: 0, + incomingReads: [], + internalReads: [], + outgoingReads: [], + } + const reads = []; + expect(coverage(node, reads)).toBe(0.00); + }) + // TEST #2 + it('can handle node of length 1 with 1 incoming read', async () => { + const node = { + sequenceLength: 1, + incomingReads: [[1, 1]], + internalReads: [], + outgoingReads: [], + } + const reads = [ + { + "id": 1, + "type": "read", + "firstNodeOffset": 0, + "finalNodeCoverLength": 1, + "sequenceNew": + [ + { + "nodeName": "1", + "mismatches": [] + }, + { + "nodeName": "11", + "mismatches": [] + }, + { + "nodeName": "13", + "mismatches": [] + } + ] + }, + { + "id": 2, + "sequenceNew": [ + { + "nodeName": "1", + "mismatches": [] + }, + { + "nodeName": "12", + "mismatches": [] + }, + { + "nodeName": "13", + "mismatches": [] + } + ], + "firstNodeOffset": 0, + "finalNodeCoverLength": 1, + } + ]; + expect(coverage(node, reads)).toBe(1.00); + }) + // TEST #3 + it('can handle node of length of 6 with 2 outgoing reads and 2 internal reads', async () => { + const node = { + sequenceLength: 6, + incomingReads: [], + internalReads: [0, 1], + outgoingReads: [[2, 1], [3, 1]], + } + const reads = [ + { + "id": 1, + "sequenceNew": [ + { + "nodeName": "1", + "mismatches": [ + { + "type": "insertion", + "pos": 0, + "seq": "CACAGTGAAAAGGCTCTGAGAAAGTCGGCTGGCCTAAGTCTCAAGAACAGTCATTCATG" + } + ] + } + ], + "firstNodeOffset": 0, + "finalNodeCoverLength": 3, + }, + { + "id": 2, + "sequenceNew": [ + { + "nodeName": "1", + "mismatches": [ + { + "type": "insertion", + "pos": 0, + "seq": "TCAAGAACAGTCATTCATG" + } + ] + } + ], + "firstNodeOffset": 1, + "finalNodeCoverLength": 5, + }, + { + "id": 3, + "sequenceNew": [ + { + "nodeName": "1", + "mismatches": [] + }, + { + "nodeName": "11", + "mismatches": [] + }, + { + "nodeName": "13", + "mismatches": [] + } + ], + "firstNodeOffset": 2, + "finalNodeCoverLength": 6, + }, + { + "id": 4, + "sourceTrackID": "1", + "sequence": [ + "1", + "12", + "13" + ], + "sequenceNew": [ + { + "nodeName": "1", + "mismatches": [] + }, + { + "nodeName": "12", + "mismatches": [] + }, + { + "nodeName": "13", + "mismatches": [] + } + ], + "firstNodeOffset": 4, + "finalNodeCoverLength": 6, + } + ]; + expect(coverage(node, reads)).toBe(2.17); + }) + // TEST #4 + it('can handle node of length of 30 with 2 incoming reads, 4 internal reads, and 3 outgoing reads', async () => { + const node = { + sequenceLength: 30, + incomingReads: [[0, 2], [1, 2]], + internalReads: [2, 3, 4, 5], + outgoingReads: [[6, 2], [7, 2], [8, 2]], + } + const reads = [ + { + "id": 1, + "sequenceNew": [ + { + "nodeName": "1", + "mismatches": [ + { + "type": "insertion", + "pos": 0, + "seq": "CACAG" + } + ] + } + ], + "firstNodeOffset": 2, + "finalNodeCoverLength": 5, + }, + { + "id": 2, + "sequenceNew": [ + { + "nodeName": "1", + "mismatches": [ + { + "type": "insertion", + "pos": 0, + "seq": "TCACATG" + } + ] + } + ], + "firstNodeOffset": 3, + "finalNodeCoverLength": 7, + }, + { + "id": 3, + "sequenceNew": [ + { + "nodeName": "1", + "mismatches": [] + }, + { + "nodeName": "13", + "mismatches": [] + } + ], + "firstNodeOffset": 16, + "finalNodeCoverLength": 28, + }, + { + "id": 4, + "sourceTrackID": "1", + "sequenceNew": [ + { + "nodeName": "1", + "mismatches": [] + }, + ], + "firstNodeOffset": 7, + "finalNodeCoverLength": 27, + }, + { + "id": 5, + "sourceTrackID": "1", + "sequenceNew": [ + { + "nodeName": "13", + "mismatches": [] + }, + ], + "firstNodeOffset": 3, + "finalNodeCoverLength": 20, + }, + { + "id": 6, + "sourceTrackID": "1", + "sequenceNew": [ + { + "nodeName": "12", + "mismatches": [] + }, + ], + "firstNodeOffset": 20, + "finalNodeCoverLength": 29, + }, + { + "id": 7, + "sourceTrackID": "1", + "sequenceNew": [ + { + "nodeName": "1", + "mismatches": [] + }, + ], + "firstNodeOffset": 1, + "finalNodeCoverLength": 29, + }, + { + "id": 15, + "sourceTrackID": "1", + "sequenceNew": [ + { + "nodeName": "1", + "mismatches": [] + }, + ], + "firstNodeOffset": 7, + "finalNodeCoverLength": 30, + }, + { + "id": 9, + "sourceTrackID": "1", + "sequenceNew": [ + { + "nodeName": "1", + "mismatches": [] + }, + ], + "firstNodeOffset": 3, + "finalNodeCoverLength": 29, + }, + ]; + expect(coverage(node, reads)).toBe(6.57); + }) +}) \ No newline at end of file From 725420d4d9b83e65e6755026e5e88ef83e70bcd9 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Wed, 2 Aug 2023 12:00:44 -0400 Subject: [PATCH 075/113] Adapt to drawing test GAMs in a different order making more pieces --- src/end-to-end.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/end-to-end.test.js b/src/end-to-end.test.js index 7002968a..f528d828 100644 --- a/src/end-to-end.test.js +++ b/src/end-to-end.test.js @@ -298,7 +298,7 @@ describe("When we wait for it to load", () => { // See if correct svg rendered let svg = document.getElementById("svg"); expect(svg).toBeTruthy(); - expect(svg.getElementsByTagName("title").length).toEqual(20); + expect(svg.getElementsByTagName("title").length).toEqual(23); }); }); From 485c7808e5e6705ecf1cb3f2a720a9d9e58540f4 Mon Sep 17 00:00:00 2001 From: ducku Date: Thu, 3 Aug 2023 15:42:59 -0700 Subject: [PATCH 076/113] allow selection dropdown to have creatable elements --- src/components/SelectionDropdown.js | 12 ++-- src/server.mjs | 90 ++++++++++++++++++++++++----- 2 files changed, 84 insertions(+), 18 deletions(-) diff --git a/src/components/SelectionDropdown.js b/src/components/SelectionDropdown.js index 056613d4..9ba99d76 100644 --- a/src/components/SelectionDropdown.js +++ b/src/components/SelectionDropdown.js @@ -1,6 +1,7 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; -import Select from "react-select"; +//import Select from "react-select"; +import CreatableSelect from 'react-select/creatable'; /** * A searchable selection dropdown component. @@ -63,13 +64,16 @@ export class SelectionDropdown extends Component { }; return ( -