Repository: vgteam/sequenceTubeMap (js)

Issue:
Documentation for link-to-view URL parameters
Now that we have #160, people [might like to be able to compose these URLs themselves](https://github.com/vgteam/sequenceTubeMap/issues/126#issuecomment-843695691), so we should have information somewhere on what the URL structure is and what all the keys that can be used are and what the values mean.

Pull request:
Doc link to view url params
Fixes #197 

Patch:
diff --git a/public/help/help.md b/public/help/help.md
index 295a6d2..6b61319 100644
--- a/public/help/help.md
+++ b/public/help/help.md
@@ -1,6 +1,7 @@
 #### Sequence Tube Map Usage Guide
 The Sequence Tube Map is used to generate visualizations of genomic sequence graphs. This guide will demonstrate how to select custom data to visualize. [You can also read this guide on Github.](https://github.com/vgteam/sequenceTubeMap/blob/master/public/help/help.md)
 
+
 ##### Selecting Tracks
 The following procedure describes adding and updating settings of custom tracks. You can use a custom track to load your own graph file, haplotype database, or file of aligned reads.
 
@@ -30,3 +31,143 @@ The following procedure describes adding and updating settings of custom tracks.
 4. If simplifying the BED file chunk or graph is possible, users will see a "Simplify Off" button, which when clicked with toggle to "Simplify On". This option enables vg simplify, which would remove small snarls. This option will only appear when there aren't any reads to be displayed. 
 5. Click Go to see the selected tracks render in the visualization area.
 ![Go Button](helpGuideImages/img9.png)
+
+##### How to make link-to-view URLs
+Users can compose URLs that link to a specific view, or a visualization of genomic data. The link describes which tracks and files will be displayed. 
+
+###### Query String Encoding
+The link has a [query string](https://en.wikipedia.org/wiki/Query_string#Structure) that consists of key-value pairs separated by ampersands, where the keys are not URL-econded and the values are URL-encoded. 
+
+Keys can use brackets to encode hierarchical structures such as arrays and objects. 
+   * For arrays, put a number in brackets to assign a new value to or access a value in that entry in the array.
+   * For objects, put the key's name in brackets to assign a new value to or access a value in that entry in the object. 
+
+The key's name should **not** be in quotes.
+
+Example of an array of JSON objects: 
+```
+object = 
+[
+   {
+      "a": "b",
+   }, 
+   {
+      "c": "d",
+      "e": {
+         "f": "g"
+      }
+   }, 
+]
+```
+
+This array's query string representation would be: `object[0][a]=b&object[1][c]=d&object[e][f]=g`.
+
+
+###### Fields that can be included in the link URL:
+1. `tracks` 
+
+   Array of tracks. Tracks are objects consisting of `trackFile`, `trackType`, and `trackColorSettings`.
+
+   A track object in JSON format might look like this:
+   ```
+   {
+      "trackFile": "exampleData/internal/snp1kg-BRCA1.vg.xg",
+      "trackType": "graph",
+      "trackColorSettings": {
+         "mainPalette": "greys",
+         "auxPalette": "ygreys",
+         "colorReadsByMappingQuality": false
+      }
+   }
+   ```
+
+   The collection of tracks wil be encoded as query string parameters starting with tracks.
+
+   * `trackFile`: the path (from the server working directory) or URL (any HTTP/HTTPS URL) to the track file. 
+     * Examples:
+         * File in server working directory: `tracks[0][trackFile]=exampleData/internal/snp1kg-BRCA1.vg.xg`.
+         * HTTP/HTTPS URL: `tracks[0][trackFile]=https://public.gi.ucsc.edu/~anovak/graphs/trivial-brca1.vg`.
+   * `trackType`: the type of the track, which can be `graph`, `haplotype`, `read`.
+      * Example: `tracks[0][trackType]=graph`.
+   * `trackColorSettings` (optional): the color information for the track. It has subfields:
+      * `mainPalette` and `auxPalette` (optional): The "main" and "auxilliary" color palettes to use, respectively. The values for each palette can be a hex code starting with a `#`, or one of the premade palette names: `greys`, `ygreys`, `blues`, `reds`, `plainColors`, or `lightColors`.
+      * The palettes are used differently for different track types.
+         * Graphs: the `mainPalette` colors the primary reference path while `auxPalette` is used for the other paths. 
+         * Haplotypes: only the `mainPalette` is used. 
+         * Reads: the `mainPalette` colors the forward-strand reads and the `auxPalette` colors the reverse-strand reads. 
+         * Examples:
+            * `tracks[0][trackColorSettings][mainPalette]=greys`.
+            * `tracks[0][trackColorSettings][auxPalette]=ygreys`.
+      * `colorReadsByMappingQuality` (optional) is a boolean value that determines if reads are colored based on their mapping quality or not. It defaults to `false`. 
+         * Example: `tracks[0][trackColorSettings][colorReadsByMappingQuality]=true`.
+   
+
+2. `region`
+
+   Region coordinates. This region will be loaded in the Tube Map visualization once the link is followed. Syntax of region coordinates is documented at step 3 of
+   [Displaying Visualizations](#displaying-visualizations). 
+      * Example: `region=17%3A1-100`.
+
+
+3. `bedFile` (optional)
+
+   A BED file containing coordinates for regions of interest, some of which may have associated pre-extracted Tube Map views. More information on BED file structure and creation is documented [here](https://github.com/vgteam/sequenceTubeMap?tab=readme-ov-file#preparing-subgraphs-in-advance). 
+   * Examples:
+      * File in server working directory: `bedFile=exampleData/internal/snp1kg-BRCA1.bed`.
+      * HTTP/HTTPS URL: `bedFile=https://raw.githubusercontent.com/vgteam/sequenceTubeMap/ca4f2485231ee4182173bec19489ba940b27461a/exampleData/cactus.bed`.
+      * No BED file: `bedFile=none`.
+   
+4. `dataType`
+
+   Information about the type of data. Data can be `built-in`, `mounted files`, or synthetic `examples`.
+      * `built-in`: Predefined data sources from the server configuration file. If the `dataType` field is set to `built-in`, you should set the `name` field to the name of a data source defined in `DATA_SOURCES` in `config.json`.
+      * `mounted files`: If the `dataType` field is set to `mounted files`, the `name` field must be set to `custom`, and you should provide custom tracks along with an optional `bedFile`.
+      * Examples: 
+         * `dataType=built-in`.
+         * `dataType=mounted%20files`.
+
+5. `simplify`
+
+   Whether small snarls (e.g. single base indels and SNPs) are displayed or removed. It defaults to false, which means that the small snarls are not removed. Currently, this option cannot be used if there are any read tracks.
+      * Example: `simplify=false`.
+
+6. `name` 
+
+   Name of data. This is a field that indicates the name of preset data, which is defined in `DATA_SOURCES` in `config.json`. `name` is used when `dataType` is set to `built-in`. You do not have to use these presets. If `dataType` is `mounted files`, this field should be set to `custom`.
+      * Examples:
+         * `name=snp1kg-BRCA1`.
+         * `name=custom`.
+
+
+Examples of links:
+   * Link with preexisting data: 
+      ```
+      http://127.0.0.1:3001?
+      tracks[0][trackFile]=exampleData%2Finternal%2Fsnp1kg-BRCA1.vg.xg&
+      tracks[0][trackType]=graph&tracks[0][trackColorSettings][mainPalette]=greys&
+      tracks[0][trackColorSettings][auxPalette]=ygreys&
+      tracks[1][trackFile]=exampleData%2Finternal%2FNA12878-BRCA1.sorted.gam&
+      tracks[1][trackType]=read&bedFile=exampleData%2Finternal%2Fsnp1kg-BRCA1.bed&
+      name=snp1kg-BRCA1&
+      region=17%3A1000-1200&
+      dataType=built-in&
+      simplify=false
+      ```
+   * Link with custom data: 
+      ```
+      http://127.0.0.1:3001?
+      tracks[1][trackType]=graph&
+      tracks[1][trackColorSettings][mainPalette]=%23000000&
+      tracks[1][trackColorSettings][auxPalette]=greys&
+      tracks[1][trackColorSettings][colorReadsByMappingQuality]=false&
+      tracks[1][trackFile]=exampleData%2Fcactus.vg&
+      tracks[2][trackType]=read&
+      tracks[2][trackColorSettings][mainPalette]=blues&
+      tracks[2][trackColorSettings][auxPalette]=reds&
+      tracks[2][trackColorSettings][colorReadsByMappingQuality]=false&
+      tracks[2][trackFile]=exampleData%2Fcactus0_10.sorted.gam&
+      bedFile=none&
+      region=ref%3A2000-3000&
+      dataType=mounted%20files&
+      simplify=false
+      ```
\ No newline at end of file
diff --git a/scripts/prepare_chunks.sh b/scripts/prepare_chunks.sh
index 3c470fb..547f0ae 100755
--- a/scripts/prepare_chunks.sh
+++ b/scripts/prepare_chunks.sh
@@ -4,21 +4,18 @@ 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  [-p '{\"mainPalette\": \"blues\", \"auxPalette\": \"reds\"}'] [-g mygam2.gam [-p ...] ...]] >> 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 [-g mygam2.gam ...]] >> regions.bed"
     exit 1
 }
 
-GAM_FILES=()
-GAM_PALETTES=()
 NODE_COLORS=()
 
-while getopts x:h:g:p:r:o:d:n: flag
+while getopts x:h:g: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}";;
@@ -87,19 +84,13 @@ fi
 
 # construct track JSON for each gam file
 echo >&2 "Gam Files:"
-GAM_NUM=0
-DEFAULT_READ_PALETTE="$(cat "$(dirname ${BASH_SOURCE[0]})/../src/config.json" | jq '.defaultReadColorPalette')"
+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 "$GAM_PALETTE" '$ARGS.named' >> $OUTDIR/temp.json
+    jq -n --arg trackFile "${GAM_FILE_PATH}" --arg trackType "read" --argjson trackColorSettings "$READ_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 a6463ef..fcfa94e 100755
--- a/scripts/prepare_local_chunk.sh
+++ b/scripts/prepare_local_chunk.sh
@@ -4,20 +4,17 @@ 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 [-p '{\"mainPalette\": \"blues\", \"auxPalette\": \"reds\"}'] [-g mygam2.gam [-p ...] ...]] >> 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 [-g mygam2.gam ...]] >> regions.bed"
     exit 1
 }
 
-GAM_FILES=()
-GAM_PALETTES=()
 NODE_COLORS=()
 
-while getopts x:g:p:r:o:d:n: flag
+while getopts x:g: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}";;
@@ -85,16 +82,13 @@ printf "${REGION_CONTIG}\t${REGION_START}\t${REGION_END}" > $OUTDIR/regions.tsv
 
 echo >&2 "Gam Files:"
 GAM_NUM=0
-DEFAULT_READ_PALETTE="$(cat "$(dirname ${BASH_SOURCE[0]})/../src/config.json" | jq '.defaultReadColorPalette')"
+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 "$GAM_PALETTE" '$ARGS.named' >> $OUTDIR/temp.json
+    jq -n --arg trackFile "${GAM_FILE_PATH}" --arg trackType "read" --argjson trackColorSettings "$READ_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 ca0282d..14bf7b8 100644
--- a/src/components/HeaderForm.js
+++ b/src/components/HeaderForm.js
@@ -574,42 +574,48 @@ class HeaderForm extends Component {
     }
   };
 
-  getRegionCoordsByDesc = (desc) => {
+  getRegionCoords = (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;
-    return regionStringFromRegionIndex(i, this.state.regionInfo);
+    // 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;
   };
+
   
-  // 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;
-  }
   
-  // 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
+  
+  // 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
   // Tracks remain unchanged if neither condition is met
-  handleRegionChange = async (coords) => {
-    // Update region coords and description
-    this.setState({
-      region: coords,
-      desc: this.getRegionDescByCoords(coords),
-    });
+  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 });
 
     let coordsToMetaData = {};
 
@@ -688,7 +694,7 @@ class HeaderForm extends Component {
 
   // Budge the region left or right by the given negative or positive fraction
   // of its width.
-  async budgeRegion(fraction) {
+  budgeRegion(fraction) {
     let parsedRegion = parseRegion(this.state.region);
 
     if (parsedRegion.distance !== undefined) {
@@ -703,9 +709,11 @@ 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()
     );
   }
@@ -713,14 +721,16 @@ class HeaderForm extends Component {
 
   /* Offset the region left or right by the given negative or positive fraction*/
   // offset: +1 or -1
-  async jumpRegion(offset) {
+  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 9a68a8a..5353695 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);
+            handleRegionChange(regionValue, regionToDesc.get(regionValue));
           }}
           options={displayRegions}
           renderInput={(params) => (
diff --git a/src/util/tubemap.js b/src/util/tubemap.js
index 4ae9fef..c4a36de 100644
--- a/src/util/tubemap.js
+++ b/src/util/tubemap.js
@@ -674,23 +674,14 @@ function placeReads() {
   reads.forEach((read, idx) => {
     read.path.forEach((element, pathIdx) => {
       if (!element.hasOwnProperty("y")) {
-        // previous y value from pathIdx - 1 might not exist yet if that segment is also without node 
-        // use previous y value from last segment with node instead 
-        let previousValidY = null;
-        let lastIndex = pathIdx - 1;
-        while (previousValidY === null && lastIndex >= 0) {
-          previousValidY = reads[idx].path[lastIndex].y;
-          lastIndex = lastIndex - 1;
-        }
         elementsWithoutNode.push({
           readIndex: idx,
           pathIndex: pathIdx,
-          previousY: previousValidY,
+          previousY: reads[idx].path[pathIdx - 1].y,
         });
       }
     });
   });
-
   elementsWithoutNode.sort(compareNoNodeReadsByPreviousY);
   elementsWithoutNode.forEach((element) => {
     const segment = reads[element.readIndex].path[element.pathIndex];


Test patch:
diff --git a/src/components/RegionInput.test.js b/src/components/RegionInput.test.js
index e0d11f1..3e1f3bb 100644
--- a/src/components/RegionInput.test.js
+++ b/src/components/RegionInput.test.js
@@ -61,6 +61,7 @@ test("it calls handleRegionChange when region is changed with new region", async
   await userEvent.type(input, NEW_REGION);
 
   expect(handleRegionChangeMock).toHaveBeenLastCalledWith(
-    NEW_REGION
+    NEW_REGION,
+    undefined
   );
 });