From ae362fa6d44fce758a6d6fc0fe637b092934cbaa Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Wed, 14 Feb 2024 11:35:40 -0500 Subject: [PATCH 1/5] Integrate left/right buttons and region description display --- src/components/HeaderForm.js | 65 ++++++++++++++--------------------- src/components/RegionInput.js | 2 +- 2 files changed, 27 insertions(+), 40 deletions(-) diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index 14bf7b8a..8db1f64c 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -574,7 +574,7 @@ class HeaderForm extends Component { } }; - getRegionCoords = (desc) => { + getRegionCoordsByDesc = (desc) => { // Given a region description (string), return the actual corresponding coordinates // Returns null if there is no corresponding coords // i: number that corresponds to record @@ -583,39 +583,30 @@ class HeaderForm extends Component { if (i === -1) // Not found return null; - // Find corresponding chr, start, and end - const regionChr = this.state.regionInfo["chr"][i]; - const regionStart = this.state.regionInfo["start"][i]; - const regionEnd = this.state.regionInfo["end"][i]; - // Combine chr, start, and end to get region string - const regionString = regionChr.concat(":", regionStart, "-", regionEnd); - return regionString; + return regionStringFromRegionIndex(i, this.state.regionInfo); }; - - + // Get the description of the region with the given coordinates, or null if no such region exists. + getRegionDescByCoords = (coords) => { + for (let i = 0; i < this.state.regionInfo["chr"].length; i++) { + if (coords === regionStringFromRegionIndex(i, this.state.regionInfo)) { + return this.state.regionInfo["desc"][i]; + } + } + return null; + } - // In addition to a new region value, also takes tracks and chunk associated with the region - // Update current track if the new tracks are valid - // Otherwise check if the current bed file is a url, and if tracks can be fetched from said url + // Adopt a new region + // Update the region description + // Update current tracks if the stored tracks for the region are valid + // Otherwise check if the current bed file has associated tracks // Tracks remain unchanged if neither condition is met - handleRegionChange = async (value, desc) => { - // Update region description - this.setState({ desc: desc }); - - // After user selects a region name or coordinates, - // update path, region, and associated tracks(if applicable) - - // Update path and region - let coords = value; - if ( - this.state.regionInfo.hasOwnProperty("desc") && - this.state.regionInfo["desc"].includes(value) - ) { - // Just a description was selected, get coords - coords = this.getRegionCoords(value); - } - this.setState({ region: coords }); + handleRegionChange = async (coords) => { + // Update region coords and description + this.setState({ + region: coords, + desc: this.getRegionDescByCoords(coords), + }); let coordsToMetaData = {}; @@ -694,7 +685,7 @@ class HeaderForm extends Component { // Budge the region left or right by the given negative or positive fraction // of its width. - budgeRegion(fraction) { + async budgeRegion(fraction) { let parsedRegion = parseRegion(this.state.region); if (parsedRegion.distance !== undefined) { @@ -709,11 +700,9 @@ class HeaderForm extends Component { parsedRegion.start = Math.max(0, Math.round(parsedRegion.start + shift)); parsedRegion.end = Math.max(0, Math.round(parsedRegion.end + shift)); } - + + await this.handleRegionChange(stringifyRegion(parsedRegion)); this.setState( - (state) => ({ - region: stringifyRegion(parsedRegion), - }), () => this.handleGoButton() ); } @@ -721,16 +710,14 @@ class HeaderForm extends Component { /* Offset the region left or right by the given negative or positive fraction*/ // offset: +1 or -1 - jumpRegion(offset) { + async jumpRegion(offset) { let regionIndex = determineRegionIndex(this.state.region, this.state.regionInfo) ?? 0; if ((offset === -1 && this.canGoLeft(regionIndex)) || (offset === 1 && this.canGoRight(regionIndex))){ regionIndex += offset; } let regionString = regionStringFromRegionIndex(regionIndex, this.state.regionInfo); + await this.handleRegionChange(regionString); this.setState( - (state) => ({ - region: regionString, - }), () => this.handleGoButton() ); } diff --git a/src/components/RegionInput.js b/src/components/RegionInput.js index 53536955..9a68a8a1 100644 --- a/src/components/RegionInput.js +++ b/src/components/RegionInput.js @@ -68,7 +68,7 @@ export const RegionInput = ({ if (regionObject) { regionValue = regionObject.value; } - handleRegionChange(regionValue, regionToDesc.get(regionValue)); + handleRegionChange(regionValue); }} options={displayRegions} renderInput={(params) => ( From fde2e8e96f742262ed7ab406028015bde6cf342b Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Wed, 14 Feb 2024 11:59:43 -0500 Subject: [PATCH 2/5] Allow adding palettes on the command line for premade chunks --- scripts/prepare_chunks.sh | 1 + scripts/prepare_local_chunk.sh | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/scripts/prepare_chunks.sh b/scripts/prepare_chunks.sh index 547f0ae5..a3517c68 100755 --- a/scripts/prepare_chunks.sh +++ b/scripts/prepare_chunks.sh @@ -8,6 +8,7 @@ function usage() { exit 1 } +GAM_FILES=() NODE_COLORS=() while getopts x:h:g:r:o:d:n: flag diff --git a/scripts/prepare_local_chunk.sh b/scripts/prepare_local_chunk.sh index fcfa94ec..a6463eff 100755 --- a/scripts/prepare_local_chunk.sh +++ b/scripts/prepare_local_chunk.sh @@ -4,17 +4,20 @@ set -e function usage() { echo >&2 "${0}: Prepare a tube map chunk and BED line on standard output from a pre-made subgraph. Only supports paths, not haplotypes." echo >&2 - echo >&2 "Usage: ${0} -x subgraph.xg -r chr1:1-100 [-d 'Description of region'] [-n 123 [-n 456]] -o chunk-chr1-1-100 [-g mygam1.gam [-g mygam2.gam ...]] >> regions.bed" + echo >&2 "Usage: ${0} -x subgraph.xg -r chr1:1-100 [-d 'Description of region'] [-n 123 [-n 456]] -o chunk-chr1-1-100 [-g mygam1.gam [-p '{\"mainPalette\": \"blues\", \"auxPalette\": \"reds\"}'] [-g mygam2.gam [-p ...] ...]] >> regions.bed" exit 1 } +GAM_FILES=() +GAM_PALETTES=() NODE_COLORS=() -while getopts x:g:r:o:d:n: flag +while getopts x:g:p:r:o:d:n: flag do case "${flag}" in x) GRAPH_FILE=${OPTARG};; g) GAM_FILES+=("$OPTARG");; + p) GAM_PALETTES+=("$OPTARG");; r) REGION=${OPTARG};; o) OUTDIR=${OPTARG};; d) DESC="${OPTARG}";; @@ -82,13 +85,16 @@ printf "${REGION_CONTIG}\t${REGION_START}\t${REGION_END}" > $OUTDIR/regions.tsv echo >&2 "Gam Files:" GAM_NUM=0 -READ_PALETTE="$(cat "$(dirname ${BASH_SOURCE[0]})/../src/config.json" | jq '.defaultReadColorPalette')" +DEFAULT_READ_PALETTE="$(cat "$(dirname ${BASH_SOURCE[0]})/../src/config.json" | jq '.defaultReadColorPalette')" for GAM_FILE in "${GAM_FILES[@]}"; do echo >&2 " - $GAM_FILE" - + GAM_PALETTE="${GAM_PALETTES[${GAM_NUM}]}" + if [[ -z "${GAM_PALETTE}" ]] ; then + GAM_PALETTE="${DEFAULT_READ_PALETTE}" + fi GAM_FILE_PATH=$(realpath --relative-to $(dirname ${BASH_SOURCE[0]})/../ $GAM_FILE) # construct track JSON for each gam file - jq -n --arg trackFile "${GAM_FILE_PATH}" --arg trackType "read" --argjson trackColorSettings "$READ_PALETTE" '$ARGS.named' >> $OUTDIR/temp.json + jq -n --arg trackFile "${GAM_FILE_PATH}" --arg trackType "read" --argjson trackColorSettings "$GAM_PALETTE" '$ARGS.named' >> $OUTDIR/temp.json # Work out a chunk-internal GAM name with the same leading numbering vg chunk uses if [[ "${GAM_NUM}" == "0" ]] ; then GAM_LEADER="chunk" From 424f2a73ff571cea0843b6ccb7551ed13cbd59b9 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Wed, 14 Feb 2024 12:47:18 -0500 Subject: [PATCH 3/5] Port over palette code to chunk extraction script --- scripts/prepare_chunks.sh | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/scripts/prepare_chunks.sh b/scripts/prepare_chunks.sh index a3517c68..3c470fb2 100755 --- a/scripts/prepare_chunks.sh +++ b/scripts/prepare_chunks.sh @@ -4,19 +4,21 @@ set -e function usage() { echo >&2 "${0}: Extract graph and read chunks for a region, producing a referencing line for a BED file on standard output" echo >&2 - echo >&2 "Usage: ${0} -x mygraph.xg [-h mygraph.gbwt] -r chr1:1-100 [-d 'Description of region'] [-n 123 [-n 456]] -o chunk-chr1-1-100 [-g mygam1.gam [-g mygam2.gam ...]] >> regions.bed" + echo >&2 "Usage: ${0} -x mygraph.xg [-h mygraph.gbwt] -r chr1:1-100 [-d 'Description of region'] [-n 123 [-n 456]] -o chunk-chr1-1-100 [-g mygam1.gam [-p '{\"mainPalette\": \"blues\", \"auxPalette\": \"reds\"}'] [-g mygam2.gam [-p ...] ...]] >> regions.bed" exit 1 } GAM_FILES=() +GAM_PALETTES=() NODE_COLORS=() -while getopts x:h:g:r:o:d:n: flag +while getopts x:h:g:p:r:o:d:n: flag do case "${flag}" in x) GRAPH_FILE=${OPTARG};; h) HAPLOTYPE_FILE=${OPTARG};; g) GAM_FILES+=("$OPTARG");; + p) GAM_PALETTES+=("$OPTARG");; r) REGION=${OPTARG};; o) OUTDIR=${OPTARG};; d) DESC="${OPTARG}";; @@ -85,13 +87,19 @@ fi # construct track JSON for each gam file echo >&2 "Gam Files:" -READ_PALETTE="$(cat "$(dirname ${BASH_SOURCE[0]})/../src/config.json" | jq '.defaultReadColorPalette')" +GAM_NUM=0 +DEFAULT_READ_PALETTE="$(cat "$(dirname ${BASH_SOURCE[0]})/../src/config.json" | jq '.defaultReadColorPalette')" echo >&2 "Read Palette: $READ_PALETTE" for GAM_FILE in "${GAM_FILES[@]}"; do + GAM_PALETTE="${GAM_PALETTES[${GAM_NUM}]}" + if [[ -z "${GAM_PALETTE}" ]] ; then + GAM_PALETTE="${DEFAULT_READ_PALETTE}" + fi GAM_FILE_PATH=$(realpath --relative-to $(dirname ${BASH_SOURCE[0]})/../ $GAM_FILE) echo >&2 " - $GAM_FILE_PATH" - jq -n --arg trackFile "${GAM_FILE_PATH}" --arg trackType "read" --argjson trackColorSettings "$READ_PALETTE" '$ARGS.named' >> $OUTDIR/temp.json + jq -n --arg trackFile "${GAM_FILE_PATH}" --arg trackType "read" --argjson trackColorSettings "$GAM_PALETTE" '$ARGS.named' >> $OUTDIR/temp.json vg_chunk_params+=(-a $GAM_FILE) + GAM_NUM=$((GAM_NUM + 1)) done # put all tracks objects into an array From 4f879620ae57d7f21115154be49c5d17b68d032d Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Wed, 14 Feb 2024 12:59:35 -0500 Subject: [PATCH 4/5] Handle not having a BED selected --- src/components/HeaderForm.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index 8db1f64c..ca0282d5 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -579,6 +579,9 @@ class HeaderForm extends Component { // Returns null if there is no corresponding coords // i: number that corresponds to record // Find index of given description in regionInfo + if (!this.state.regionInfo["desc"]) { + return null; + } const i = this.state.regionInfo["desc"].findIndex((d) => d === desc); if (i === -1) // Not found @@ -588,9 +591,9 @@ class HeaderForm extends Component { // Get the description of the region with the given coordinates, or null if no such region exists. getRegionDescByCoords = (coords) => { - for (let i = 0; i < this.state.regionInfo["chr"].length; i++) { + for (let i = 0; i < this.state.regionInfo["chr"]?.length ?? 0; i++) { if (coords === regionStringFromRegionIndex(i, this.state.regionInfo)) { - return this.state.regionInfo["desc"][i]; + return this.state.regionInfo["desc"]?.[i] ?? null; } } return null; From 523a2490b287548039bcc68a3a448e2c667d1ac4 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Wed, 14 Feb 2024 13:01:00 -0500 Subject: [PATCH 5/5] Stop expecting second argument in test --- src/components/RegionInput.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/RegionInput.test.js b/src/components/RegionInput.test.js index 3e1f3bb5..e0d11f11 100644 --- a/src/components/RegionInput.test.js +++ b/src/components/RegionInput.test.js @@ -61,7 +61,6 @@ test("it calls handleRegionChange when region is changed with new region", async await userEvent.type(input, NEW_REGION); expect(handleRegionChangeMock).toHaveBeenLastCalledWith( - NEW_REGION, - undefined + NEW_REGION ); });