Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 28 additions & 22 deletions src/components/explorer/IndividualTracks.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,10 @@ const buildIgvTrack = (igvUrls, track) => {
};

const IGV_JS_ANNOTATION_ALIASES = {
GRCh37: "hg19",
GRCh38: "hg38",
NCBI37: "mm9",
GRCm38: "mm10",
grch37: "hg19",
grch38: "hg38",
ncbi37: "mm9",
grcm38: "mm10",
};

const IndividualTracks = ({ individual }) => {
Expand All @@ -140,21 +140,25 @@ const IndividualTracks = ({ individual }) => {
const { hasAttempted: igvGenomesAttempted, itemsByID: igvGenomesByID } = useIgvGenomes();
const referenceGenomes = useReferenceGenomes(); // Reference service genomes

const availableBrowserGenomes = useMemo(() => {
const availableBrowserGenomesByIDLower = useMemo(() => {
if (!igvGenomesAttempted || !referenceGenomes.hasAttempted) {
return {};
}

// Record of {lowercase assembly ID: IGV.js reference object}
const availableGenomes = {};

// For now, we prefer igv.js built-in genomes with the same ID over local copies for the browser, since it comes
// with gene annotation tracks. TODO: in the future, this should switch to preferring local copies.
referenceGenomes.items.forEach((g) => {
availableGenomes[g.id] = {
const idLower = g.id.toLowerCase();
availableGenomes[idLower] = {
id: g.id,
fastaURL: g.fasta,
indexURL: g.fai,
cytobandURL: (igvGenomesByID[g.id] ?? igvGenomesByID[IGV_JS_ANNOTATION_ALIASES[g.id]])?.cytobandURL,
// igvGenomesByID stores the IGV.js genome ID in lowercase form. We don't store cytobands in the reference
// service, so patch them in from the IGV.js JSON.
cytobandURL: (igvGenomesByID[idLower] ?? igvGenomesByID[IGV_JS_ANNOTATION_ALIASES[idLower]])?.cytobandURL,
tracks: g.gff3_gz
? [
{
Expand Down Expand Up @@ -216,22 +220,22 @@ const IndividualTracks = ({ individual }) => {

const allFoundFiles = useMemo(() => allTracks.filter((t) => !!igvUrls[t.filename]?.url), [allTracks, igvUrls]);

const [selectedAssemblyID, setSelectedAssemblyID] = useState(undefined);
const [selectedAssemblyIDLower, setSelectedAssemblyIDLower] = useState(undefined);

const trackAssemblyIDs = useMemo(
() => Array.from(new Set(allFoundFiles.map((t) => t.genome_assembly_id))).sort(),
[allFoundFiles],
);

useEffect(() => {
if (Object.keys(availableBrowserGenomes).length) {
if (Object.keys(availableBrowserGenomesByIDLower).length) {
if (trackAssemblyIDs.length && trackAssemblyIDs[0]) {
const asmID = trackAssemblyIDs[0]; // TODO: first available
const asmID = trackAssemblyIDs[0].toLowerCase(); // TODO: first available
console.debug("auto-selected assembly ID:", asmID);
setSelectedAssemblyID(asmID);
setSelectedAssemblyIDLower(asmID);
}
}
}, [availableBrowserGenomes, trackAssemblyIDs]);
}, [availableBrowserGenomesByIDLower, trackAssemblyIDs]);

const [modalVisible, setModalVisible] = useState(false);

Expand Down Expand Up @@ -294,16 +298,16 @@ const IndividualTracks = ({ individual }) => {
return cleanup;
}

if (!Object.keys(availableBrowserGenomes).length || !selectedAssemblyID) {
if (!Object.keys(availableBrowserGenomesByIDLower).length || !selectedAssemblyIDLower) {
console.debug("no available browser genomes / selected assembly ID yet");
return cleanup;
}

console.debug("igv.createBrowser effect dependencies:", [
igvUrls,
viewableResults,
availableBrowserGenomes,
selectedAssemblyID,
availableBrowserGenomesByIDLower,
selectedAssemblyIDLower,
]);

if (creatingIgvBrowser || igvBrowserRef.current) {
Expand All @@ -319,13 +323,15 @@ const IndividualTracks = ({ individual }) => {
setCreatingIgvBrowser(true);

const initialIgvTracks = allFoundFiles
.filter((t) => t.viewInIgv && t.genome_assembly_id === selectedAssemblyID && igvUrls[t.filename].url)
.filter(
(t) => t.viewInIgv && t.genome_assembly_id.toLowerCase() === selectedAssemblyIDLower && igvUrls[t.filename].url,
)
.map((t) => buildIgvTrack(igvUrls, t));

const selectedBentoReference = referenceGenomes.itemsByID[selectedAssemblyID];
const selectedBentoReference = referenceGenomes.itemsByIDLower[selectedAssemblyIDLower];

const igvOptions = {
reference: availableBrowserGenomes[selectedAssemblyID],
reference: availableBrowserGenomesByIDLower[selectedAssemblyIDLower],
locus: igvPosition,
tracks: initialIgvTracks,
...(referenceService && selectedBentoReference?.gff3_gz
Expand Down Expand Up @@ -365,7 +371,7 @@ const IndividualTracks = ({ individual }) => {
// browser will be re-rendered if a track's visibility changes. By using viewableResults as a dependency
// instead, the browser is only re-rendered if the overall track set (i.e., individual) changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [igvUrls, viewableResults, availableBrowserGenomes, selectedAssemblyID]);
}, [igvUrls, viewableResults, availableBrowserGenomesByIDLower, selectedAssemblyIDLower]);

return (
<>
Expand All @@ -390,9 +396,9 @@ const IndividualTracks = ({ individual }) => {
<div style={{ marginBottom: 12 }}>
Assembly:{" "}
<Select
value={selectedAssemblyID}
onChange={(v) => setSelectedAssemblyID(v)}
options={trackAssemblyIDs.map((a) => ({ value: a, label: a }))}
value={selectedAssemblyIDLower}
onChange={(v) => setSelectedAssemblyIDLower(v)}
options={trackAssemblyIDs.map((a) => ({ value: a.toLowerCase(), label: a }))}
/>
</div>
<TrackControlTable toggleView={toggleView} allFoundFiles={allFoundFiles} />
Expand Down
3 changes: 2 additions & 1 deletion src/modules/explorer/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,8 @@ export const igvGenomes = (state = { items: [], itemsByID: {}, isFetching: false
return {
...state,
items: action.data ?? [],
itemsByID: Object.fromEntries((action.data ?? []).map((g) => [g.id, g])),
// lowercase IDs to handle inconsistent casing between the different services
itemsByID: Object.fromEntries((action.data ?? []).map((g) => [g.id.toLowerCase(), g])),
};
case FETCH_IGV_GENOMES.FINISH:
return { ...state, isFetching: false, hasAttempted: true };
Expand Down
10 changes: 8 additions & 2 deletions src/modules/reference/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type ReferenceGenomesState = {
isDeletingIDs: Record<string, boolean>; // TODO: refactor into set
items: Genome[];
itemsByID: Record<string, Genome>;
itemsByIDLower: Record<string, Genome>;
};

export const referenceGenomes: Reducer<ReferenceGenomesState> = (
Expand All @@ -20,19 +21,23 @@ export const referenceGenomes: Reducer<ReferenceGenomesState> = (
isDeletingIDs: {},
items: [],
itemsByID: {},
itemsByIDLower: {},
},
action,
) => {
switch (action.type) {
// FETCH_REFERENCE_GENOMES
case FETCH_REFERENCE_GENOMES.REQUEST:
return { ...state, isFetching: true };
case FETCH_REFERENCE_GENOMES.RECEIVE:
case FETCH_REFERENCE_GENOMES.RECEIVE: {
const genomesByID = arrayToObjectByProperty(action.data, "id") as Record<string, Genome>;
return {
...state,
items: action.data,
itemsByID: arrayToObjectByProperty(action.data, "id"),
itemsByID: genomesByID,
itemsByIDLower: Object.fromEntries(Object.entries(genomesByID).map(([k, v]) => [k.toLowerCase(), v])),
};
}
case FETCH_REFERENCE_GENOMES.FINISH:
return { ...state, isFetching: false, hasAttempted: true };

Expand All @@ -45,6 +50,7 @@ export const referenceGenomes: Reducer<ReferenceGenomesState> = (
...state,
items: state.items.filter((g) => g.id !== genomeID),
itemsByID: objectWithoutProp(state.itemsByID, genomeID),
itemsByIDLower: objectWithoutProp(state.itemsByIDLower, genomeID.toLowerCase()),
};
}
case DELETE_REFERENCE_GENOME.FINISH: {
Expand Down