Repository: vgteam/sequenceTubeMap
Pull request #280 (2023-05-25 23:52:13)

Use track list as the source of truth about what files are selected, and avoid empty tracks
This removes the dedicated variables for what files are selected in each dropdown, and instead uses an array of tracks.

It also makes graph/haplotype paths use a `sourceTrackID` like reads do.

It also removes the assumptions that the graph is track 0, the haplotypes if any are track 1, and the reads are tracks 2 and 3. Any file type should be able to be any track number.

This is all in service of making following a copied link actually populate the file dropdowns. When you visit a copied link, it fills in the view target's tracks. Now we use that information to render the dropdowns, so #242 should be fixed.


Patch:
diff --git a/src/App.js b/src/App.js
index c3f5911..a3c87f4 100644
--- a/src/App.js
+++ b/src/App.js
@@ -14,6 +14,31 @@ import Footer from "./components/Footer";
 import { dataOriginTypes } from "./enums";
 import config from "./config.json";
 
+const EXAMPLE_TRACKS = [
+  // Fake tracks for the generated examples.
+  // TODO: Move over there.
+  {"files": [{"type": "graph", "name": "fakeGraph"}]},
+  {"files": [{"type": "read", "name": "fakeReads"}]}
+];
+
+function addTracksToColorSchemes(tracks, schemes) {
+  
+  console.log("Initializing coloring for tracks: ", tracks, "combining with:", schemes);
+
+  for (const key in tracks) {
+    if (schemes[key] === undefined) {
+      // We need to adopt a color scheme
+      if (tracks[key].files[0].type === "read") {
+        schemes[key] = {...config.defaultReadColorPalette};
+      } else {
+        schemes[key] = {...config.defaultHaplotypeColorPalette};
+      }
+    }
+  }
+  
+  return schemes;
+}
+
 class App extends Component {
   constructor(props) {
     super(props);
@@ -36,11 +61,7 @@ class App extends Component {
         showReads: true,
         showSoftClips: true,
         colorReadsByMappingQuality: false,
-        colorSchemes: [
-                  {...config.defaultHaplotypeColorPalette},
-                  {...config.defaultHaplotypeColorPalette},
-                  {...config.defaultReadColorPalette},
-                  {...config.defaultReadColorPalette}],
+        colorSchemes: addTracksToColorSchemes(this.defaultViewTarget.tracks, []),
         mappingQualityCutoff: 0,
       },
     };
@@ -58,9 +79,9 @@ class App extends Component {
 
   /**
    * @param {ViewTarget} viewTarget - The new data that is selected to view
-   *    setCurrentViewTarget updates the current viewTarget to the new viewTarget that's passed in
-   *    before calling setCurrentViewTarget, viewTarget refers to what is currently being displayed
-   *    See types.ts for more info on viewTarget typ
+   *    setCurrentViewTarget updates the current viewTarget to the new viewTarget that's passed in.
+   *    Before calling setCurrentViewTarget, viewTarget refers to what is currently being displayed.
+   *    See types.ts for more info on viewTarget type.
    */
   setCurrentViewTarget = (viewTarget) => {
     // Update the viewTarge
@@ -71,9 +92,25 @@ class App extends Component {
       !isEqual(this.state.viewTarget, newViewTarget) ||
       this.state.dataOrigin !== dataOriginTypes.API
     ) {
-      this.setState({
-        viewTarget: newViewTarget,
-        dataOrigin: dataOriginTypes.API,
+      
+      console.log("Adopting view target: ", newViewTarget)
+
+      this.setState((state) => {
+        // Make sure we have color schemes.
+        let newColorSchemes = state.visOptions.colorSchemes.slice();
+        addTracksToColorSchemes(newViewTarget.tracks, newColorSchemes);
+
+
+        console.log("Adopting color schemes: ", newColorSchemes)
+
+        return {
+          viewTarget: newViewTarget,
+          dataOrigin: dataOriginTypes.API,
+          visOptions: {
+            ...state.visOptions,
+            colorSchemes: newColorSchemes, 
+          }
+        };
       });
     }
   };
@@ -111,18 +148,27 @@ class App extends Component {
   // value is the value to set. For "mainPalette" and "auxPalette" this is the name
   // of a color palette, such as "reds".
   setColorSetting = (key, index, value) => {
-    let newcolors = [...this.state.visOptions.colorSchemes]
-    newcolors[index][key] = value;
-    this.setState((state) => ({
-      visOptions: {
-        ...state.visOptions,
-        colors: newcolors,
-      },
-    }));
+    this.setState((state) => {
+      let newcolors = [...state.visOptions.colorSchemes]
+      if (newcolors[index] === undefined) {
+        // Handle the set call from example data maybe coming before we set up any nonempty real tracks.
+        // TODO: COme up with a better way to do this.
+        newcolors[index] = {...config.defaultReadColorPalette};
+      }
+      newcolors[index][key] = value;
+      console.log('Set index ' + index + ' key ' + key + ' to ' + value);
+      console.log('New colors: ', newcolors);
+      return {
+        visOptions: {
+          ...state.visOptions,
+          colors: newcolors,
+        },
+      };
+    });
   };
 
   setDataOrigin = (dataOrigin) => {
-    this.setState({ dataOrigin });
+    this.setState({dataOrigin});
   };
 
   render() {
@@ -145,6 +191,7 @@ class App extends Component {
         />
         <CustomizationAccordion
           visOptions={this.state.visOptions}
+          tracks={this.state.dataOrigin === dataOriginTypes.API ? this.state.viewTarget.tracks : EXAMPLE_TRACKS}
           toggleFlag={this.toggleVisOptionFlag}
           handleMappingQualityCutoffChange={
             this.handleMappingQualityCutoffChange
diff --git a/src/components/CustomizationAccordion.js b/src/components/CustomizationAccordion.js
index 7c57ea1..d5f9e50 100644
--- a/src/components/CustomizationAccordion.js
+++ b/src/components/CustomizationAccordion.js
@@ -50,6 +50,32 @@ class VisualizationOptions extends Component {
         </option>
       );
     });
+
+    let readTrackNumber = 1;
+
+    let trackSettingsList = [];
+    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;
+      if (type === "graph") {
+        trackSettingsList.push(
+          <TrackSettings key={key} label="Graph Paths" fileType={type} trackColorSettings={visOptions.colorSchemes[key]} setTrackColorSetting={this.setColorWithIndex(key)} />
+        );
+      } else if (type === "haplotype") {
+        // TODO: Do nothing for now. Haplotypes get assigned to the graph as their source track right now.
+      } else if (type === "read") {
+        if (visOptions.showReads) {
+          trackSettingsList.push(
+            <TrackSettings key={key} label={"Read Track " + readTrackNumber} fileType={type} trackColorSettings={visOptions.colorSchemes[key]} setTrackColorSetting={this.setColorWithIndex(key)} />
+          );
+          readTrackNumber++;
+        }
+      } else {
+        throw new Error("Unknown track type " + type);
+      }
+    }
+
     return (
       <Container>
         <div id="accordion">
@@ -165,13 +191,7 @@ class VisualizationOptions extends Component {
                     </React.Fragment>
                   )}
                 </FormGroup>
-                <TrackSettings label="Haplotype" fileType="haplotype" trackColorSettings={visOptions.colorSchemes[1]} setTrackColorSetting={this.setColorWithIndex(1)} /> 
-                {visOptions.showReads && (
-                    <React.Fragment>
-                      <TrackSettings label="GAM Index 1" fileType="read" trackColorSettings={visOptions.colorSchemes[2]} setTrackColorSetting={this.setColorWithIndex(2)} />
-                      <TrackSettings label="GAM Index 2" fileType="read" trackColorSettings={visOptions.colorSchemes[3]} setTrackColorSetting={this.setColorWithIndex(3)} />
-                    </React.Fragment>
-                  )}
+                {trackSettingsList}
               </CardBody>
             </Collapse>
           </Card>
@@ -184,6 +204,7 @@ class VisualizationOptions extends Component {
 VisualizationOptions.propTypes = {
   handleMappingQualityCutoffChange: PropTypes.func.isRequired,
   setColorSetting: PropTypes.func.isRequired,
+  tracks: PropTypes.array.isRequired,
 };
 
 export default VisualizationOptions;
diff --git a/src/components/ExampleSelectButtons.js b/src/components/ExampleSelectButtons.js
index ebd45f8..456981f 100644
--- a/src/components/ExampleSelectButtons.js
+++ b/src/components/ExampleSelectButtons.js
@@ -6,11 +6,11 @@ import { dataOriginTypes } from "../enums";
 class ExampleSelectButtons extends Component {
   handleClick = (dataOrigin, haploColor, readColor) => {
     this.props.setDataOrigin(dataOrigin);
-    //set haplotype color
-    this.props.setColorSetting("forward", 0, haploColor);
+    // Set haplotype color. Examples always have haplotypes as track 0.
+    this.props.setColorSetting("mainPalette", 0, haploColor);
     if (readColor) {
-      //set read color
-      this.props.setColorSetting("forward", 1, readColor);
+      // Set read color. Examples always have reads as track 1.
+      this.props.setColorSetting("mainPalette", 1, readColor);
     }
   };
 
diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js
index 4bcbe21..232e2bc 100644
--- a/src/components/HeaderForm.js
+++ b/src/components/HeaderForm.js
@@ -34,10 +34,6 @@ const CLEAR_STATE = {
   // Select: The file name (or string "none") that is displayed in each
   // dropdown. From the corresponding SelectOptions list.
   // File: The file name actually used (or undefined)
-  graphSelect: "none",
-  gbwtSelect: "none",
-  gamSelect: "none",
-  gam2Select: "none",
   bedSelect: "none",
 
   // This tracks several arrays of BED region data, stored by data type, with
@@ -47,10 +43,6 @@ const CLEAR_STATE = {
   pathNames: ["none"],
 
   tracks: [],
-  graphFile: undefined,
-  gbwtFile: undefined,
-  gamFile: undefined,
-  gamFile2: undefined,
   bedFile: undefined,
   dataPath: undefined,
   region: "",
@@ -95,6 +87,11 @@ function createTrack(file) {
 
 // Checks if all file names in the track are equal
 function tracksEqual(curr, next) {
+  if ((curr === undefined) !== (next === undefined)) {
+    // One is undefined and the other isn't
+    return false;
+  }
+
   if (curr.files.length !== next.files.length) {
     return false;
   }
@@ -112,6 +109,35 @@ function tracksEqual(curr, next) {
   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) {
+    // Different lengths so not equal
+    return false;
+  }
+  for (let i = 0; i < currViewTarget.tracks.length; i++) {
+    if (!tracksEqual(currViewTarget.tracks[i], nextViewTarget.tracks[i])){
+      // Different tracks so not equal
+      return false;
+    }
+  }
+
+  // Update if regions are not equal
+  if (currViewTarget.region !== nextViewTarget.region) {
+     return false;
+  }
+
+  return true;
+
+}
+
 class HeaderForm extends Component {
   state = EMPTY_STATE;
   componentDidMount() {
@@ -142,22 +168,21 @@ class HeaderForm extends Component {
   initState = () => {
     // Populate state with either viewTarget or the first example
     let ds = this.props.defaultViewTarget ?? DATA_SOURCES[0];
-    const graphSelect = ds.graphFile ? ds.graphFile : "none";
     const bedSelect = ds.bedFile ? ds.bedFile : "none";
     const dataPath = ds.dataPath;
     if (bedSelect !== "none") {
       this.getBedRegions(bedSelect, dataPath);
     }
-    if (graphSelect !== "none") {
-      this.getPathNames(graphSelect, dataPath);
+    for (const key in ds.tracks) {
+      if (ds.tracks[key].files[0].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.setState((state) => {
       const stateVals = {
-        graphFile: ds.graphFile,
-        graphSelect: graphSelect,
-        gbwtFile: ds.gbwtFile,
-        gamFile: ds.gamFile,
-        gamFile2: ds.gamFile2,
+        tracks: ds.tracks,
         bedFile: ds.bedFile,
         bedSelect: bedSelect,
         dataPath: dataPath,
@@ -169,6 +194,79 @@ class HeaderForm extends Component {
     });
   };
 
+  setTrackFile = (type, index, file) => {
+    // Set the nth track of the given type to the given file.
+    // If there is no nth track of that type, create one.
+    // If the file is "none", remove that track.
+    this.setState((state) => {
+      console.log('Set file ' + type + ' index ' + index + ' to ' + file + ' over ' + JSON.stringify(state.tracks));
+
+      // Make a modified copy of the tracks
+      let newTracks = [];
+
+      // Find the nth track of this type, if any.
+      let seenTracksOfType = 0;
+      let maxKey = -1;
+      for (const key in state.tracks) {
+        let track = state.tracks[key];
+        if (track.files[0].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;
+              newTracks[key] = newTrack;
+            }
+            // If the file is "none" we drop the track.
+          } else {
+            // We want to keep it as is
+            newTracks[key] = track;
+          }
+          seenTracksOfType++;
+        } else {
+          console.log("See file of other type");
+          // We want to keep all tracks of other types as is
+          newTracks[key] = track;
+        }
+        if (parseInt(key) > maxKey) {
+          maxKey = parseInt(key);
+        }
+      }
+      
+      console.log('Saw ' + seenTracksOfType + ' tracks of type vs index ' + index)
+      if (seenTracksOfType === index && file !== "none") {
+        // We need to add this track
+        console.log('Create track at index ' + (maxKey + 1))
+        newTracks[maxKey + 1] = createTrack({"type": type, "name": file});
+      }
+
+      // Add the new tracks to the state
+      let newState = Object.assign({}, state);
+      newState.tracks = newTracks;
+      console.log('Set result: ' + JSON.stringify(newTracks));
+      return newState;
+    });
+  }
+
+  getTrackFile = (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) {
+        if (seenTracksOfType === index) {
+          // This is the one. Return its filename.
+          return track.files[0].name;
+        }
+        seenTracksOfType++;
+      } 
+    }
+    // Not found
+    return "none";
+  }
+
   getMountedFilenames = async () => {
     this.setState({ error: null });
     try {
@@ -192,36 +290,25 @@ class HeaderForm extends Component {
 
         if (this.state.dataPath === "mounted") {
           this.setState((state) => {
-            const graphSelect = json.graphFiles.includes(state.graphSelect)
-              ? state.graphSelect
-              : "none";
-            const gbwtSelect = json.gbwtFiles.includes(state.gbwtSelect)
-              ? state.gbwtSelect
-              : "none";
-            const gamSelect = json.gamIndices.includes(state.gamSelect)
-              ? state.gamSelect
-              : "none";
-            const gam2Select = json.gamIndices.includes(state.gam2Select)
-              ? state.gam2Select
-              : "none";
             const bedSelect = json.bedFiles.includes(state.bedSelect)
               ? state.bedSelect
               : "none";
             if (bedSelect !== "none") {
               this.getBedRegions(bedSelect, "mounted");
             }
-            if (graphSelect !== "none") {
-              this.getPathNames(graphSelect, "mounted");
-            }
+            for (const key in state.tracks) {
+              if (state.tracks[key].files[0].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");
+              }
+            } 
             return {
               graphSelectOptions: json.graphFiles,
               gbwtSelectOptions: json.gbwtFiles,
               gamSelectOptions: json.gamIndices,
               bedSelectOptions: json.bedFiles,
-              graphSelect,
-              gbwtSelect,
-              gamSelect,
-              gam2Select,
               bedSelect,
             };
           });
@@ -325,10 +412,6 @@ class HeaderForm extends Component {
       this.setState((state) => {
         return {
           ...CLEAR_STATE,
-          graphFile: state.graphSelect,
-          gbwtFile: state.gbwtSelect,
-          gamFile: state.gamSelect,
-          gamFile2: state.gam2Select,
           bedFile: "none",
           // not sure why we would like to keep the previous selection when changing data sources. What I know is it creates a bug for the regions, where the tubemap tries to read the previous bedFile (e.g. defaulted to example 1), can't find it and raises an error
           // bedFile: state.bedSelect,
@@ -353,13 +436,15 @@ class HeaderForm extends Component {
             // Without bedFile, we have no regions
             this.setState({ regionInfo: {} });
           }
-          this.getPathNames(ds.graphFile, dataPath);
+          for (const key in ds.tracks) {
+            if (ds.tracks[key].files[0].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.setState({
-            graphFile: ds.graphFile,
-            graphSelect: ds.graphFile,
-            gbwtFile: ds.gbwtFile,
-            gamFile: ds.gamFile,
-            gamFile2: ds.gamFile2,
+            tracks: ds.tracks, 
             bedFile: ds.bedFile,
             bedSelect: bedSelect,
             dataPath: dataPath,
@@ -373,14 +458,7 @@ class HeaderForm extends Component {
     }
   };
   getNextViewTarget = () => ({
-
-    tracks: [
-      createTrack({name: this.state.graphFile, type: fileTypes.GRAPH}), 
-      createTrack({name: this.state.gbwtFile, type: fileTypes.HAPLOTYPE}), 
-      createTrack({name: this.state.gamFile, type: fileTypes.READ}),
-      createTrack({name: this.state.gamFile2, type: fileTypes.READ})
-    ],
-
+    tracks: this.state.tracks,
     bedFile: this.state.bedFile,
     name: this.state.name,
     region: this.state.region,
@@ -397,20 +475,13 @@ class HeaderForm extends Component {
 
     const nextViewTarget = this.getNextViewTarget();
     const currViewTarget = this.props.getCurrentViewTarget();
-
-    // Update if view target tracks are not equal
-    for (let i = 0; i < currViewTarget.tracks.length; i++) {
-      if (!tracksEqual(currViewTarget.tracks[i], nextViewTarget.tracks[i])){
-        this.props.setCurrentViewTarget(nextViewTarget);
-        break;
-      }
-    }
-
-    // Update if regions are not equal
-    if (currViewTarget.region !== nextViewTarget.region) {
-       this.props.setCurrentViewTarget(nextViewTarget);
+    
+    if (!viewTargetsEqual(currViewTarget, nextViewTarget)) {
+      // Update the view if the view target has changed.
+      this.props.setCurrentViewTarget(nextViewTarget);
     }
 
+   
   };
 
   getRegionCoords = (desc) => {
@@ -449,14 +520,16 @@ class HeaderForm extends Component {
     const value = event.target.value;
     this.setState({ [id]: value });
     if (id === "graphSelect") {
-      this.getPathNames(value, this.state.dataPath);
-      this.setState({ graphFile: value });
+      if (value && value !== "none") {
+        this.getPathNames(value, this.state.dataPath);
+      }
+      this.setTrackFile(fileTypes.GRAPH, 0, value);
     } else if (id === "gbwtSelect") {
-      this.setState({ gbwtFile: value });
+      this.setTrackFile(fileTypes.HAPLOTYPE, 0, value);
     } else if (id === "gamSelect") {
-      this.setState({ gamFile: value });
+      this.setTrackFile(fileTypes.READ, 0, value);
     } else if (id === "gam2Select") {
-      this.setState({ gamFile2: value });
+      this.setTrackFile(fileTypes.READ, 1, value);
     } else if (id === "bedSelect") {
       if (value !== "none") {
         this.getBedRegions(value, this.state.dataPath);
@@ -498,7 +571,18 @@ class HeaderForm extends Component {
   };
 
   handleFileUpload = (fileType, fileName) => {
-    this.setState({ [fileType]: fileName });
+    /// Apply a file that was uploaded as a track.
+    if (fileType === "graphFile") {
+      this.setTrackFile(fileTypes.GRAPH, 0, fileName);
+    } else if (fileType === "gbwtFile") {
+      this.setTrackFile(fileTypes.HAPLOTYPE, 0, fileName);
+    } else if (fileType === "gamFile") {
+      this.setTrackFile(fileTypes.READ, 0, fileName);
+    } else if (fileType === "gamFile2") {
+      this.setTrackFile(fileTypes.READ, 1, fileName);
+    } else {
+      throw new Error("Unknown file type " + fileType + " uploaded: " + fileName);
+    }
   };
 
   showFileSizeAlert = () => {
@@ -601,12 +685,12 @@ class HeaderForm extends Component {
               &nbsp;
               {mountedFilesFlag && (
                 <MountedDataFormRow
-                  graphSelect={this.state.graphSelect}
+                  graphSelect={this.getTrackFile(fileTypes.GRAPH, 0)}
                   graphSelectOptions={this.state.graphSelectOptions}
-                  gbwtSelect={this.state.gbwtSelect}
+                  gbwtSelect={this.getTrackFile(fileTypes.HAPLOTYPE, 0)}
                   gbwtSelectOptions={this.state.gbwtSelectOptions}
-                  gamSelect={this.state.gamSelect}
-                  gam2Select={this.state.gam2Select}
+                  gamSelect={this.getTrackFile(fileTypes.READ, 0)}
+                  gam2Select={this.getTrackFile(fileTypes.READ, 1)}
                   gamSelectOptions={this.state.gamSelectOptions}
                   bedSelect={this.state.bedSelect}
                   bedSelectOptions={this.state.bedSelectOptions}
diff --git a/src/components/TubeMapContainer.js b/src/components/TubeMapContainer.js
index f816d5c..370f358 100644
--- a/src/components/TubeMapContainer.js
+++ b/src/components/TubeMapContainer.js
@@ -141,36 +141,52 @@ class TubeMapContainer extends Component {
         const error = "Fetching remote data returned error";
         throw new Error(error);
       } else {
-        const nodes = tubeMap.vgExtractNodes(json.graph);
-        const tracks = tubeMap.vgExtractTracks(json.graph);
-        // Call vgExtractReads on each individual read and store in readsArr
-        let readsArr = [];
-        for (const gam of Object.values(json.gam)) {
-          // Include readsArr length to prevent duplicate ids
-          readsArr.push(tubeMap.vgExtractReads(nodes, tracks, gam, readsArr.length));
-        }
-        
         // go through viewTarget and create array of read file track numbers
-        let sourceTrackIDs = [];
+        let readTrackIDs = [];
+        // And the graph track number if any
+        let graphTrackID = null;
+        // And the haplotype track number if any
+        let haplotypeTrackID = null;
         for (let i = 0; i < this.props.viewTarget.tracks.length; i++) {
           const track = this.props.viewTarget.tracks[i];
-          //add track index to array if the track contains a gam file
           for (const file of track.files) {
             if (file.type === "read") {
-              sourceTrackIDs.push(i);
+              //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(sourceTrackIDs);
-        // Go through every read and assign it a source file number
-        for (let i = 0; i < readsArr.length; i++) {
-          for (let j = 0; j < readsArr[i].length; j++) {
-            readsArr[i][j].sourceTrackID = sourceTrackIDs[i];
-          }
-        }
+        console.log("Graph track: " + graphTrackID + " Haplotype track: " + haplotypeTrackID);
+
+        const nodes = tubeMap.vgExtractNodes(json.graph);
+        // TODO: For now claim haplotypes came from the graph
+        const tracks = tubeMap.vgExtractTracks(json.graph, graphTrackID, graphTrackID);
 
+        // Call vgExtractReads on each file of reads and store in readsArr
+        let readsArr = [];
+        // Count total reads seen so far.
+        let totalReads = 0;
+        for (const gam of Object.values(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.
+          let newReads = tubeMap.vgExtractReads(nodes, tracks, gam, totalReads, readTrackIDs[readsArr.length]);
+          readsArr.push(newReads);
+          totalReads += newReads.length;
+        }
+        
         // concatenate all reads together
         const reads = readsArr.flat();
 
@@ -220,21 +236,25 @@ class TubeMapContainer extends Component {
       case dataOriginTypes.EXAMPLE_6:
         vg = JSON.parse(data.k3138);
         nodes = tubeMap.vgExtractNodes(vg);
-        tracks = tubeMap.vgExtractTracks(vg);
+        tracks = tubeMap.vgExtractTracks(vg, 0, 0); // Examples have paths and haplotypes as track 0.
         reads = tubeMap.vgExtractReads(
           nodes,
           tracks,
-          this.readsFromStringToArray(data.demoReads)
+          this.readsFromStringToArray(data.demoReads),
+          0,
+          1 // Examples always have reads as track 1
         );
         break;
       case dataOriginTypes.EXAMPLE_7:
         vg = data.reverseAlignmentGraph;
         nodes = tubeMap.vgExtractNodes(vg);
-        tracks = tubeMap.vgExtractTracks(vg);
+        tracks = tubeMap.vgExtractTracks(vg, 0, 0); // Examples have paths and haplotypes as track 0.
         reads = tubeMap.vgExtractReads(
           nodes,
           tracks,
-          data.mixedAlignmentReads
+          data.mixedAlignmentReads,
+          0,
+          1 // Examples always have reads as track 1
         );
         break;
       case dataOriginTypes.NO_DATA:
diff --git a/src/config.json b/src/config.json
index fc5b15f..0b1f947 100644
--- a/src/config.json
+++ b/src/config.json
@@ -2,13 +2,11 @@
   "BACKEND_URL": false,
   "DATA_SOURCES": [
     {
-      "tracks": [{"files": [{"name": "snp1kg-BRCA1.vg.xg", "type": "graph"}]},
-                 {"files": [{"name": null, "type": "haplotype"}]},
-                 {"files": [{"name": "NA12878-BRCA1.sorted.gam", "type": "read"}]}],
-                   
       "name": "snp1kg-BRCA1",
-      "graphFile": "snp1kg-BRCA1.vg.xg",
-      "gamFile": "NA12878-BRCA1.sorted.gam",
+      "tracks": [
+        {"files": [{"type": "graph", "name": "snp1kg-BRCA1.vg.xg"}]},
+        {"files": [{"type": "read", "name": "NA12878-BRCA1.sorted.gam"}]}
+      ],
       "dataPath": "default",
       "region": "17:1-100",
       "bedFile": "snp1kg-BRCA1.bed",
@@ -16,16 +14,20 @@
     },
     {
       "name": "vg \"small\" example",
-      "graphFile": "x.vg.xg",
-      "gbwtFile": "x.vg.gbwt",
+      "tracks": [
+        {"files": [{"type": "graph", "name": "x.vg.xg"}]},
+        {"files": [{"type": "haplotype", "name": "x.vg.gbwt"}]}
+      ],
       "dataPath": "mounted",
       "dataType": "built-in",
       "region": "x:1-100"
     },
     {
       "name": "cactus",
-      "graphFile": "cactus.vg.xg",
-      "gamFile": "cactus-NA12879.sorted.gam",
+      "tracks": [
+        {"files": [{"type": "graph", "name": "cactus.vg.xg"}]},
+        {"files": [{"type": "read", "name": "cactus-NA12879.sorted.gam"}]}
+      ],
       "dataPath": "mounted",
       "bedFile": "cactus.bed",
       "region": "ref:1-100",
@@ -33,9 +35,11 @@
     },
     {
       "name": "cactus multiple reads",
-      "graphFile": "cactus.vg.xg",
-      "gamFile": "cactus0_10.sorted.gam",
-      "gamFile2": "cactus10_20.sorted.gam",
+      "tracks": [
+        {"files": [{"type": "graph", "name": "cactus.vg.xg"}]},
+        {"files": [{"type": "read", "name": "cactus0_10.sorted.gam"}]},
+        {"files": [{"type": "read", "name": "cactus10_20.sorted.gam"}]}
+      ],
       "dataPath": "mounted",
       "bedFile": "cactus.bed",
       "region": "ref:1-100",
diff --git a/src/server.js b/src/server.js
index c0251d1..fd2e56f 100644
--- a/src/server.js
+++ b/src/server.js
@@ -208,16 +208,41 @@ function getFileFromType(track, type) {
   return "none";
 }
 
-// Returns an array of the first gam file of every track
-function getGams(tracks) {
-  let gams = [];
+// Given a collection of tracks (each of which may have a files array with
+// items with a type and a name), generate the filenames for the first file of
+// the given type for each track with such a file.
+//
+// This is a fancy ES6 generator.
+function* eachFileOfType(tracks, type) {
   for (const track of tracks) {
-    const file = getFileFromType(track, fileTypes.READ);
+    const file = getFileFromType(track, type);
     if (file && file !== "none") {
-      gams.push(file);
+      yield file;
     }
   }
-  return gams;
+}
+
+// Get the first files of the given type from all the given tracks.
+function getFilesOfType(tracks, type) {
+  let results = [];
+  for (const file of eachFileOfType(tracks, type)) {
+    results.push(file);
+  }
+  return results;
+}
+
+// Get the first file from the first track with a file of the given type, or
+// undefined if no such track exists.
+function getFirstFileOfType(tracks, type) {
+  for (const file of eachFileOfType(tracks, type)) {
+    return file;
+  }
+  return undefined;
+}
+
+// Returns an array of the first gam file of every track with a gam file
+function getGams(tracks) {
+  return getFilesOfType(tracks, fileTypes.READ);
 }
 
 api.post("/getChunkedData", (req, res, next) => {
@@ -227,6 +252,7 @@ api.post("/getChunkedData", (req, res, next) => {
   console.time("request-duration");
   console.log("http POST getChunkedData received");
   console.log(`region = ${req.body.region}`);
+  console.log(`tracks = ${JSON.stringify(req.body.tracks)}`);
 
   // Assign each request a UUID. v1 UUIDs can be very similar for similar
   // timestamps on the same node, but are still guaranteed to be unique within
@@ -243,11 +269,9 @@ api.post("/getChunkedData", (req, res, next) => {
 
 
   // We always have an graph file
-  const graphFile = getFileFromType(req.body.tracks[0], fileTypes.GRAPH);
+  const graphFile = getFirstFileOfType(req.body.tracks, fileTypes.GRAPH);
   // We sometimes have a GBWT with haplotypes that override any in the graph file
-  const gbwtFile = getFileFromType(req.body.tracks[1], fileTypes.HAPLOTYPE);
-  // We sometimes have a GAM file with reads
-  //const gamFile = getFileFromType(req.body.tracks[2], fileTypes.READ);
+  const gbwtFile = getFirstFileOfType(req.body.tracks, fileTypes.HAPLOTYPE);
   // We sometimes have a BED file with regions to look at
   const bedFile = req.bedFile;
 
diff --git a/src/util/demo-data.js b/src/util/demo-data.js
index 662625b..cd1d80b 100644
--- a/src/util/demo-data.js
+++ b/src/util/demo-data.js
@@ -1,82 +1,82 @@
 export const inputNodes = [
-  { name: "A", width: 1, seq: "A" },
-  { name: "B", width: 2, seq: "AA" },
-  { name: "C", width: 1, seq: "T" },
-  { name: "D", width: 3, seq: "GGG" },
-  { name: "E", width: 1, seq: "A" },
-  { name: "F", width: 1, seq: "G" },
-  { name: "G", width: 3, seq: "ATG" },
-  { name: "H", width: 1, seq: "T" },
-  { name: "I", width: 1, seq: "C" },
-  { name: "J", width: 3, seq: "TAA" },
-  { name: "K", width: 1, seq: "C" },
-  { name: "L", width: 1, seq: "G" },
-  { name: "M", width: 1, seq: "C" },
-  { name: "N", width: 1, seq: "A" },
-  { name: "O", width: 1, seq: "C" },
-  { name: "P", width: 2, seq: "AA" },
-  { name: "Q", width: 1, seq: "T" },
-  { name: "R", width: 3, seq: "CG" },
-  { name: "S", width: 2, seq: "GA" },
-  { name: "T", width: 3, seq: "GTT" },
-  { name: "U", width: 1, seq: "A" },
-  { name: "V", width: 1, seq: "G" },
-  { name: "W", width: 8, seq: "TTGTCTCT" },
-  { name: "X", width: 1, seq: "T" },
-  { name: "Y", width: 1, seq: "C" },
-  { name: "Z", width: 1, seq: "A" },
-  { name: "AA", width: 3, seq: "CGA" },
-  { name: "AB", width: 1, seq: "T" },
-  { name: "AC", width: 1, seq: "G" },
-  { name: "AD", width: 1, seq: "A" },
-  { name: "AE", width: 1, seq: "G" },
-  { name: "AF", width: 1, seq: "T" },
-  { name: "AG", width: 3, seq: "GTG" },
+  { sourceTrackID: 0, name: "A", width: 1, seq: "A" },
+  { sourceTrackID: 0, name: "B", width: 2, seq: "AA" },
+  { sourceTrackID: 0, name: "C", width: 1, seq: "T" },
+  { sourceTrackID: 0, name: "D", width: 3, seq: "GGG" },
+  { sourceTrackID: 0, name: "E", width: 1, seq: "A" },
+  { sourceTrackID: 0, name: "F", width: 1, seq: "G" },
+  { sourceTrackID: 0, name: "G", width: 3, seq: "ATG" },
+  { sourceTrackID: 0, name: "H", width: 1, seq: "T" },
+  { sourceTrackID: 0, name: "I", width: 1, seq: "C" },
+  { sourceTrackID: 0, name: "J", width: 3, seq: "TAA" },
+  { sourceTrackID: 0, name: "K", width: 1, seq: "C" },
+  { sourceTrackID: 0, name: "L", width: 1, seq: "G" },
+  { sourceTrackID: 0, name: "M", width: 1, seq: "C" },
+  { sourceTrackID: 0, name: "N", width: 1, seq: "A" },
+  { sourceTrackID: 0, name: "O", width: 1, seq: "C" },
+  { sourceTrackID: 0, name: "P", width: 2, seq: "AA" },
+  { sourceTrackID: 0, name: "Q", width: 1, seq: "T" },
+  { sourceTrackID: 0, name: "R", width: 3, seq: "CG" },
+  { sourceTrackID: 0, name: "S", width: 2, seq: "GA" },
+  { sourceTrackID: 0, name: "T", width: 3, seq: "GTT" },
+  { sourceTrackID: 0, name: "U", width: 1, seq: "A" },
+  { sourceTrackID: 0, name: "V", width: 1, seq: "G" },
+  { sourceTrackID: 0, name: "W", width: 8, seq: "TTGTCTCT" },
+  { sourceTrackID: 0, name: "X", width: 1, seq: "T" },
+  { sourceTrackID: 0, name: "Y", width: 1, seq: "C" },
+  { sourceTrackID: 0, name: "Z", width: 1, seq: "A" },
+  { sourceTrackID: 0, name: "AA", width: 3, seq: "CGA" },
+  { sourceTrackID: 0, name: "AB", width: 1, seq: "T" },
+  { sourceTrackID: 0, name: "AC", width: 1, seq: "G" },
+  { sourceTrackID: 0, name: "AD", width: 1, seq: "A" },
+  { sourceTrackID: 0, name: "AE", width: 1, seq: "G" },
+  { sourceTrackID: 0, name: "AF", width: 1, seq: "T" },
+  { sourceTrackID: 0, name: "AG", width: 3, seq: "GTG" },
 ];
 
 // prettier-ignore
 export const inputTracks1 = [
-  { id: 0, name: 'Track A', indexOfFirstBase: 1, sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'U', 'W', 'X', 'Z', 'AA', 'AE', 'AG'], freq: 3000 },
-  { id: 1, name: 'Track B', sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'Q', 'S', 'U', 'AA', 'AE', 'AG'], freq: 15 },
-  { id: 2, name: 'Track C', sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'V', 'W', 'X', 'Z', 'AB', 'AE', 'AG'], freq: 300 },
-  { id: 3, name: 'Track D', sequence: ['B', 'C', 'D', 'E', 'G', 'H', 'J', 'L', 'M', 'N', 'P', 'R', 'S', 'U', 'W', 'Y', 'Z', 'AC', 'AF', 'AG'], freq: 4 },
-  { id: 4, name: 'Track E', sequence: ['B', 'D', 'F', 'G', 'I', 'J', 'L', 'M', 'O', 'P', 'Q', 'S', 'T', 'V', 'W', 'Y', 'Z', 'AD', 'AF', 'AG'], freq: 2 },
+  { id: 0, sourceTrackID: 0, name: 'Track A', indexOfFirstBase: 1, sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'U', 'W', 'X', 'Z', 'AA', 'AE', 'AG'], freq: 3000 },
+  { id: 1, sourceTrackID: 0, name: 'Track B', sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'Q', 'S', 'U', 'AA', 'AE', 'AG'], freq: 15 },
+  { id: 2, sourceTrackID: 0, name: 'Track C', sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'V', 'W', 'X', 'Z', 'AB', 'AE', 'AG'], freq: 300 },
+  { id: 3, sourceTrackID: 0, name: 'Track D', sequence: ['B', 'C', 'D', 'E', 'G', 'H', 'J', 'L', 'M', 'N', 'P', 'R', 'S', 'U', 'W', 'Y', 'Z', 'AC', 'AF', 'AG'], freq: 4 },
+  { id: 4, sourceTrackID: 0, name: 'Track E', sequence: ['B', 'D', 'F', 'G', 'I', 'J', 'L', 'M', 'O', 'P', 'Q', 'S', 'T', 'V', 'W', 'Y', 'Z', 'AD', 'AF', 'AG'], freq: 2 },
 ];
 
 // prettier-ignore
 export const inputTracks2 = [
-  { id: 0, name: 'Track A', indexOfFirstBase: 1, sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'U', 'W', 'X', 'Z', 'AA', 'AG'], freq: 4000 },
-  { id: 1, name: 'Track B', sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'Q', 'S', 'U', 'AA', 'AG'], freq: 150 },
-  { id: 2, name: 'Track C', sequence: ['A', 'B', 'D', 'F', '-H', '-G', '-E', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'AB', 'V', 'W', 'X', '-AA', '-Z', 'AG'], freq: 30 },
-  { id: 3, name: 'Track D', sequence: ['B', 'C', 'D', 'E', 'G', 'H', 'J', 'L', '-P', '-N', '-M', 'R', 'S', 'U', 'W', 'Y', 'Z', 'AC', 'AF', 'AG'], freq: 10 },
-  { id: 4, name: 'Track E', sequence: ['B', 'D', 'F', '-J', '-I', '-G', 'L', 'M', 'O', 'P', 'Q', 'S', 'T', 'V', 'W', 'Y', 'Z', 'AD', 'AF', 'AG'], freq: 3 },
+  { id: 0, sourceTrackID: 0, name: 'Track A', indexOfFirstBase: 1, sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'U', 'W', 'X', 'Z', 'AA', 'AG'], freq: 4000 },
+  { id: 1, sourceTrackID: 0, name: 'Track B', sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'Q', 'S', 'U', 'AA', 'AG'], freq: 150 },
+  { id: 2, sourceTrackID: 0, name: 'Track C', sequence: ['A', 'B', 'D', 'F', '-H', '-G', '-E', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'AB', 'V', 'W', 'X', '-AA', '-Z', 'AG'], freq: 30 },
+  { id: 3, sourceTrackID: 0, name: 'Track D', sequence: ['B', 'C', 'D', 'E', 'G', 'H', 'J', 'L', '-P', '-N', '-M', 'R', 'S', 'U', 'W', 'Y', 'Z', 'AC', 'AF', 'AG'], freq: 10 },
+  { id: 4, sourceTrackID: 0, name: 'Track E', sequence: ['B', 'D', 'F', '-J', '-I', '-G', 'L', 'M', 'O', 'P', 'Q', 'S', 'T', 'V', 'W', 'Y', 'Z', 'AD', 'AF', 'AG'], freq: 3 },
 ];
 
 // prettier-ignore
 export const inputTracks3 = [
-  { id: 0, name: 'Track A', indexOfFirstBase: 1, sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'U', 'W', 'X', 'Z', 'AA', 'AE', 'AG'] },
-  { id: 1, name: 'Track B', sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'Q', 'S', 'U', 'AA', 'AE', 'AG'] },
-  { id: 2, name: 'Track C', sequence: ['A', 'B', 'D', '-H', 'G', '-E', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'AB', 'V', 'W', 'X', '-AE', '-AA', '-Z', 'AG'] },
-  { id: 3, name: 'Track D', sequence: ['B', 'C', 'D', 'E', 'G', 'H', 'J', 'L', '-P', '-N', '-M', 'R', 'S', 'U', 'W', 'Y', 'Z', 'AC', 'AF', 'AG'] },
-  { id: 4, name: 'Track E', sequence: ['B', 'D', 'F', 'G', 'I', 'J', 'L', 'M', 'O', 'P', 'Q', 'S', 'T', 'V', 'W', 'Y', 'Z', 'AD', 'AF', 'AG'] },
+  { id: 0, sourceTrackID: 0, name: 'Track A', indexOfFirstBase: 1, sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'U', 'W', 'X', 'Z', 'AA', 'AE', 'AG'] },
+  { id: 1, sourceTrackID: 0, name: 'Track B', sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'Q', 'S', 'U', 'AA', 'AE', 'AG'] },
+  { id: 2, sourceTrackID: 0, name: 'Track C', sequence: ['A', 'B', 'D', '-H', 'G', '-E', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'AB', 'V', 'W', 'X', '-AE', '-AA', '-Z', 'AG'] },
+  { id: 3, sourceTrackID: 0, name: 'Track D', sequence: ['B', 'C', 'D', 'E', 'G', 'H', 'J', 'L', '-P', '-N', '-M', 'R', 'S', 'U', 'W', 'Y', 'Z', 'AC', 'AF', 'AG'] },
+  { id: 4, sourceTrackID: 0, name: 'Track E', sequence: ['B', 'D', 'F', 'G', 'I', 'J', 'L', 'M', 'O', 'P', 'Q', 'S', 'T', 'V', 'W', 'Y', 'Z', 'AD', 'AF', 'AG'] },
 ];
 
 // prettier-ignore
 export const inputTracks4 = [
-  { id: 0, name: 'Track A', indexOfFirstBase: 1, sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'U', 'W', 'X', 'Z', 'AA', 'AE', 'AG'] },
-  { id: 1, name: 'Track B', sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'J', 'N', 'Q', 'S', 'U', 'AA', 'AE', 'AG'] },
-  { id: 2, name: 'Track C', sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'V', 'W', 'X', 'Z', 'AB', 'AE', 'AG'] },
-  { id: 3, name: 'Track D', sequence: ['B', 'C', 'D', 'E', 'D', 'E', 'G', 'H', 'J', 'L', 'M', 'N', 'P', 'R', 'S', 'U', 'W', 'Y', 'Z', 'AC', 'AF', 'AG'] },
-  { id: 4, name: 'Track E', sequence: ['B', 'D', 'F', 'G', 'I', 'J', 'L', 'M', 'O', 'P', 'Q', 'S', 'T', 'V', 'W', 'Y', 'Z', 'AD', 'AF', 'AG'] },
+  { id: 0, sourceTrackID: 0, name: 'Track A', indexOfFirstBase: 1, sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'U', 'W', 'X', 'Z', 'AA', 'AE', 'AG'] },
+  { id: 1, sourceTrackID: 0, name: 'Track B', sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'J', 'N', 'Q', 'S', 'U', 'AA', 'AE', 'AG'] },
+  { id: 2, sourceTrackID: 0, name: 'Track C', sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'V', 'W', 'X', 'Z', 'AB', 'AE', 'AG'] },
+  { id: 3, sourceTrackID: 0, name: 'Track D', sequence: ['B', 'C', 'D', 'E', 'D', 'E', 'G', 'H', 'J', 'L', 'M', 'N', 'P', 'R', 'S', 'U', 'W', 'Y', 'Z', 'AC', 'AF', 'AG'] },
+  { id: 4, sourceTrackID: 0, name: 'Track E', sequence: ['B', 'D', 'F', 'G', 'I', 'J', 'L', 'M', 'O', 'P', 'Q', 'S', 'T', 'V', 'W', 'Y', 'Z', 'AD', 'AF', 'AG'] },
 ];
 
 // prettier-ignore
 export const inputTracks5 = [
-  { id: 0, name: 'Track A', indexOfFirstBase: 1, sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'U', 'W', 'X', 'Z', 'AA', 'AE', 'AG'] },
-  { id: 1, name: 'Track B', sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'Q', 'K', 'M', 'N', 'S', 'U', 'AA', 'AE', 'AG'] },
-  { id: 2, name: 'Track C', sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'V', 'W', 'X', 'Z', 'AB', 'AE', 'AG'] },
-  { id: 3, name: 'Track D', sequence: ['B', 'C', 'D', 'H', 'E', 'G', 'J', 'L', 'M', 'N', 'P', 'R', 'S', 'U', 'W', 'Y', 'Z', 'AC', 'AF', 'AG'] },
-  { id: 4, name: 'Track E', sequence: ['B', 'D', 'F', 'G', 'I', 'J', 'L', 'M', 'O', 'P', 'Q', 'S', 'T', 'V', 'W', 'Y', 'Z', 'AD', 'AF', 'AG'] },
+  { id: 0, sourceTrackID: 0, name: 'Track A', indexOfFirstBase: 1, sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'U', 'W', 'X', 'Z', 'AA', 'AE', 'AG'] },
+  { id: 1, sourceTrackID: 0, name: 'Track B', sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'Q', 'K', 'M', 'N', 'S', 'U', 'AA', 'AE', 'AG'] },
+  { id: 2, sourceTrackID: 0, name: 'Track C', sequence: ['A', 'B', 'D', 'E', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'S', 'V', 'W', 'X', 'Z', 'AB', 'AE', 'AG'] },
+  { id: 3, sourceTrackID: 0, name: 'Track D', sequence: ['B', 'C', 'D', 'H', 'E', 'G', 'J', 'L', 'M', 'N', 'P', 'R', 'S', 'U', 'W', 'Y', 'Z', 'AC', 'AF', 'AG'] },
+  { id: 4, sourceTrackID: 0, name: 'Track E', sequence: ['B', 'D', 'F', 'G', 'I', 'J', 'L', 'M', 'O', 'P', 'Q', 'S', 'T', 'V', 'W', 'Y', 'Z', 'AD', 'AF', 'AG'] },
 ];
 
 export const k3138 = `
diff --git a/src/util/tubemap.js b/src/util/tubemap.js
index b07392d..ad2217f 100644
--- a/src/util/tubemap.js
+++ b/src/util/tubemap.js
@@ -2343,11 +2343,19 @@ function generateTrackColor(track, highlight) {
 
   if (typeof highlight === "undefined") highlight = "plain";
   let trackColor;
-  if (track.hasOwnProperty("type") && track.type === "read") {
-    const sourceID = track.sourceTrackID;
-    if (!config.colorSchemes[sourceID]) {
+
+  const sourceID = track.sourceTrackID;
+  if (!config.colorSchemes[sourceID]) {
+    if (track.hasOwnProperty("type") && track.type === "read") {
+      // Default to read colors
       config.colorSchemes[sourceID] = externalConfig.defaultReadColorPalette;
+    } else {
+      // Default to haplotype colors
+      config.colorSchemes[sourceID] = externalConfig.defaultHaplotypeColorPalette;
     }
+  }
+
+  if (track.hasOwnProperty("type") && track.type === "read") {
     if (config.colorSchemes[sourceID].colorReadsByMappingQuality) {
       trackColor = d3.interpolateRdYlGn(
         Math.min(60, track.mapping_quality) / 60
@@ -2364,14 +2372,13 @@ function generateTrackColor(track, highlight) {
     }
   } else {
     if (config.showExonsFlag === false || highlight !== "plain") {
-      if (!config.colorSchemes[1]) {
-        config.colorSchemes[1] = externalConfig.defaultHaplotypeColorPalette;
-      }
-      // don't repeat the color of the first track (reference) to highilight is better
+      // Don't repeat the color of the first track (reference) to highilight is better.
+      // TODO: Allow using color 0 for other schemes not the same as the one for the reference path.
+      // TODO: Stop reads from taking this color?
       if (track.id === 0) {
-        trackColor = getColorSet(config.colorSchemes[1].mainPalette)[0];
+        trackColor = getColorSet(config.colorSchemes[sourceID].mainPalette)[0];
       } else {
-        const colorSet = getColorSet(config.colorSchemes[1].mainPalette);
+        const colorSet = getColorSet(config.colorSchemes[sourceID].mainPalette);
         trackColor = colorSet[((track.id - 1) % (colorSet.length - 1)) + 1];
       }
     } else {
@@ -3680,7 +3687,7 @@ function generateNodeWidth() {
 }
 
 // extract track info from vg-json
-export function vgExtractTracks(vg) {
+export function vgExtractTracks(vg, pathSourceTrackId, haplotypeSourceTrackID) {
   const result = [];
   vg.path.forEach((path, index) => {
     const sequence = [];
@@ -3710,7 +3717,14 @@ export function vgExtractTracks(vg) {
     track.id = index;
     track.sequence = sequence;
     track.isCompletelyReverse = isCompletelyReverse;
-    if (path.hasOwnProperty("freq")) track.freq = path.freq;
+    if (path.hasOwnProperty("freq")) {
+      // This is a haplotype
+      track.freq = path.freq;
+      track.sourceTrackID = haplotypeSourceTrackID;
+    } else {
+      // This is a path
+      track.sourceTrackID = pathSourceTrackId;
+    }
     if (path.hasOwnProperty("name")) track.name = path.name;
     if (path.hasOwnProperty("indexOfFirstBase")) {
       track.indexOfFirstBase = Number(path.indexOfFirstBase);
@@ -3799,7 +3813,10 @@ function compareReadsByLeftEnd2(a, b) {
   return 0;
 }
 
-export function vgExtractReads(myNodes, myTracks, myReads, idLength = 0) {
+// 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) {
   if (DEBUG) {
     console.log("Reads:");
     console.log(myReads);
@@ -3808,7 +3825,7 @@ export function vgExtractReads(myNodes, myTracks, myReads, idLength = 0) {
 
   const nodeNames = [];
   myNodes.forEach((node) => {
-    nodeNames.push(node.name, 10);
+    nodeNames.push(node.name, 10); // TODO: why 10?
   });
 
   for (let i = 0; i < myReads.length; i += 1) {
@@ -3892,7 +3909,8 @@ export function vgExtractReads(myNodes, myTracks, myReads, idLength = 0) {
       }
     } else {
       const track = {};
-      track.id = myTracks.length + extracted.length + idLength;
+      track.id = myTracks.length + extracted.length + idOffset;
+      track.sourceTrackID = sourceTrackID;
       track.sequence = sequence;
       track.sequenceNew = sequenceNew;
       track.type = "read";


Test patch:
diff --git a/src/end-to-end.test.js b/src/end-to-end.test.js
index b26e645..da54358 100644
--- a/src/end-to-end.test.js
+++ b/src/end-to-end.test.js
@@ -304,7 +304,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?tracks[0][files][0][name]=snp1kg-BRCA1.vg.xg&tracks[0][files][0][type]=graph&tracks[1][files][0][name]=&tracks[1][files][0][type]=haplotype&tracks[2][files][0][name]=NA12878-BRCA1.sorted.gam&tracks[2][files][0][type]=read&name=snp1kg-BRCA1&graphFile=snp1kg-BRCA1.vg.xg&gamFile=NA12878-BRCA1.sorted.gam&dataPath=default&region=17:1-100&bedFile=snp1kg-BRCA1.bed&dataType=built-in";
+    "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&region=17:1-100&bedFile=snp1kg-BRCA1.bed&dataType=built-in";
   // Set up dropdown
   await act(async () => {
     let dropdown = document.getElementById("dataSourceSelect");
@@ -338,7 +338,7 @@ it("produces correct link for view before & after go is pressed", async () => {
   await clickCopyLink();
 
   const expectedLinkCactus =
-    "http://localhost?tracks[0][files][0][name]=cactus.vg.xg&tracks[0][files][0][type]=graph&tracks[1][files][0][type]=haplotype&tracks[2][files][0][name]=cactus-NA12879.sorted.gam&tracks[2][files][0][type]=read&tracks[3][files][0][type]=read&bedFile=cactus.bed&name=cactus&region=ref:1-100&dataPath=mounted&dataType=built-in";
+    "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&region=ref:1-100&dataPath=mounted&dataType=built-in";
   // Make sure link has changed after pressing go
   expect(fakeClipboard).toEqual(expectedLinkCactus);
 });