Repository: vgteam/sequenceTubeMap
Pull request #392 (2024-02-14 17:53:18)

Left-right button and region description integration
This attaches the left-right button stuff to the region description stuff so that the description updates when you go left and right.

It also includes chunk generation script changes to let you set palettes from the command line.

To do the demo today with the Lancet data I used this bit of Bash:

```
rm -f exampleData/lancet_regions.bed
for REGION_NAME in $((cd /Users/anovak/Downloads/STM_DataShare_Nov07_2023/giraffe_alignments/ && ls normal.*.sorted.gam) | sed 's/normal\.//g' | sed 's/\.sorted\.gam//g') ; do
    CONTIG="$(echo "${REGION_NAME}" | cut -f1 -d'_')"
    COORD_START="$(echo "${REGION_NAME}" | cut -f2 -d'_')"
    COORD_END="$(echo "${REGION_NAME}" | cut -f3 -d'_')"
    (cd exampleData && rm -Rf "lancet_regions-${REGION_NAME}" && ../scripts/prepare_local_chunk.sh -x /Users/anovak/Downloads/STM_DataShare_Nov07_2023/giraffe_alignments/${REGION_NAME}.giraffe.gbz -r "${CONTIG}:${COORD_START}-${COORD_END}" -o "lancet_regions-${REGION_NAME}" -g /Users/anovak/Downloads/STM_DataShare_Nov07_2023/giraffe_alignments/tumor.${REGION_NAME}.sorted.gam -g /Users/anovak/Downloads/STM_DataShare_Nov07_2023/giraffe_alignments/normal.${REGION_NAME}.sorted.gam -p '{"mainPalette": "reds", "auxPalette": "reds"}' -p '{"mainPalette": "blues", "auxPalette": "blues"}' >> lancet_regions.bed)
done
sort exampleData/lancet_regions.bed -k2n -s | sort -k1.4n,2 -s >exampleData/lancet_regions.sorted.bed
rm exampleData/lancet_regions.bed
```

This goes through the Lancet regions, parses them out into coordinate components, and makes each into a pre-extracted chunk, putting the resulting BED line in a BED file. I put the tumor data in red, and the normal data in blue. I have to `cd` into the directory where the chunk directories and the BED file are when doing this, to make sure that the relative paths in the BED file to the chunk directories are from where the BED file actually is.

Once I have the full BED file, I sort it first by the region start coordinate as a number, and then stably by the part of the contig name after the first 3 "chr" characters, to get the BED in coordinate order.

A remaining problem with this approach is that the chunk JSON files refer to the files in my `Downloads` directory, so if you open the track settings when looking at a region, it asks the server about those files and then pops up an error saying they aren't allowed to be accessed.


Patch:
diff --git a/scripts/prepare_chunks.sh b/scripts/prepare_chunks.sh
index 547f0ae..3c470fb 100755
--- a/scripts/prepare_chunks.sh
+++ b/scripts/prepare_chunks.sh
@@ -4,18 +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}";;
@@ -84,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
diff --git a/scripts/prepare_local_chunk.sh b/scripts/prepare_local_chunk.sh
index fcfa94e..a6463ef 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"
diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js
index 14bf7b8..ca0282d 100644
--- a/src/components/HeaderForm.js
+++ b/src/components/HeaderForm.js
@@ -574,48 +574,42 @@ 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
     // 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
       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 ?? 0; i++) {
+      if (coords === regionStringFromRegionIndex(i, this.state.regionInfo)) {
+        return this.state.regionInfo["desc"]?.[i] ?? null;
+      }
+    }
+    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 +688,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 +703,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 +713,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 5353695..9a68a8a 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) => (


Test patch:
diff --git a/src/components/RegionInput.test.js b/src/components/RegionInput.test.js
index 3e1f3bb..e0d11f1 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
   );
 });