Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refact: igv code with more memoization/less redef #271

Merged
merged 6 commits into from
Jul 13, 2023
Merged
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
161 changes: 85 additions & 76 deletions src/components/explorer/IndividualTracks.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import PropTypes from "prop-types";
import { Button, Divider, Modal, Switch, Table, Empty } from "antd";
import { debounce } from "lodash";
import igv from "igv/dist/igv.esm";
Expand Down Expand Up @@ -27,26 +28,68 @@ const DEBOUNCE_WAIT = 500;

// minimal documentation here: https://github.com/igvteam/igv.js/wiki/Tracks-2.0

// tracks should have a "format" value: "vcf", "cram", "bigWig", etc
// tracks should have a "format" value: "vcf", "cram", "bigWig", etc.
// There is no default value. "If not specified format is inferred from file name extension"
// works if not specified, even from a url where the filename is obscured, but is much slower
// works if not specified, even from a URL where the filename is obscured, but is much slower
// appears to be case-insensitive

// tracks can also have a "type" property, but this is inferred from the format value

// reduce VISIBILITY_WINDOW above for better performance

// verify url set is for this individual (may have stale urls from previous request)
const hasFreshUrls = (files, urls) => files.every((f) => urls.hasOwnProperty(f.filename));

const IndividualTracks = ({individual}) => {
const {accessToken} = useSelector(state => state.auth);
const TrackControlTable = React.memo(({ toggleView, allFoundFiles }) => {
const trackTableColumns = [
{
title: "File",
key: "filename",
render: (_, track) => track.filename,
},
{
title: "File type",
key: "fileType",
render: (_, track) => track.description,
},
{
title: "View track",
key: "view",
align: "center",
render: (_, track) => <Switch checked={track.viewInIgv} onChange={() => toggleView(track)} />,
},
]; // Don't bother memoizing since toggleView and allFoundFiles both change with allTracks anyway

return (
<Table
bordered
size="small"
pagination={false}
columns={trackTableColumns}
rowKey="filename"
dataSource={allFoundFiles}
style={{ display: "inline-block" }}
/>
);
});
TrackControlTable.propTypes = {
toggleView: PropTypes.func,
allFoundFiles: PropTypes.arrayOf(PropTypes.object),
};

const IndividualTracks = ({ individual }) => {
const { accessToken } = useSelector((state) => state.auth);

const igvRef = useRef(null);
const igvRendered = useRef(false);
const igvUrls = useSelector((state) => state.drs.igvUrlsByFilename);
const isFetchingIgvUrls = useSelector((state) => state.drs.isFetchingIgvUrls);

// read stored position only on first render
const igvPosition = useSelector((state) => state.explorer.igvPosition, () => true);
const igvPosition = useSelector(
(state) => state.explorer.igvPosition,
() => true,
);

const dispatch = useDispatch();
const biosamplesData = (individual?.phenopackets ?? []).flatMap((p) => p.biosamples);
Expand All @@ -60,25 +103,28 @@ const IndividualTracks = ({individual}) => {

// by default, don't view crams (user can turn them on in track controls)
viewableResults = viewableResults.map((v) => {
return v.file_format.toLowerCase() === "cram" ? {...v, viewInIgv: false} : v;
return v.file_format.toLowerCase() === "cram" ? { ...v, viewInIgv: false } : v;
});

const [allTracks, setAllTracks] = useState(
viewableResults.sort((r1, r2) => (r1.file_format > r2.file_format ? 1 : -1)),
);

const allFoundFiles = allTracks.filter(
(t) => (igvUrls[t.filename]?.dataUrl && igvUrls[t.filename]?.indexUrl) || igvUrls[t.filename]?.url);
const allFoundFiles = useMemo(
() =>
allTracks.filter(
(t) => (igvUrls[t.filename]?.dataUrl && igvUrls[t.filename]?.indexUrl) || igvUrls[t.filename]?.url,
),
[allTracks, igvUrls],
);

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

// hardcode for hg19/GRCh37, fix requires updates elsewhere in Bento
const genome = "hg19";

// verify url set is for this individual (may have stale urls from previous request)
const hasFreshUrls = (files, urls) => files.every((f) => urls.hasOwnProperty(f.filename));

const toggleView = (track) => {
const toggleView = useCallback((track) => {
const wasViewing = track.viewInIgv;
const updatedTrackObject = { ...track, viewInIgv: !wasViewing };
setAllTracks(allTracks.map((t) => (t.filename === track.filename ? updatedTrackObject : t)));
Expand All @@ -97,18 +143,18 @@ const IndividualTracks = ({individual}) => {
visibilityWindow: VISIBILITY_WINDOW,
});
}
};
}, [allTracks]);

const storeIgvPosition = (referenceFrame) => {
const {chr, start, end} = referenceFrame[0];
const { chr, start, end } = referenceFrame[0];
const position = `${chr}:${start}-${end}`;
dispatch(setIgvPosition(position));
};

// retrieve urls on mount
useEffect(() => {
if (allTracks.length) {
// don't search if all urls already known
// don't search if all urls already known
if (hasFreshUrls(allTracks, igvUrls)) {
return;
}
Expand All @@ -119,10 +165,10 @@ const IndividualTracks = ({individual}) => {
// update access token whenever necessary
useEffect(() => {
if (BENTO_URL) {
igv.setOauthToken(accessToken, (new URL(BENTO_URL)).host);
igv.setOauthToken(accessToken, new URL(BENTO_URL).host);
}
if (BENTO_PUBLIC_URL) {
igv.setOauthToken(accessToken, (new URL(BENTO_PUBLIC_URL)).host);
igv.setOauthToken(accessToken, new URL(BENTO_PUBLIC_URL).host);
}
}, [accessToken]);

Expand All @@ -144,9 +190,7 @@ const IndividualTracks = ({individual}) => {
(t) => t.viewInIgv && igvUrls[t.filename].dataUrl && igvUrls[t.filename].indexUrl,
);

const unindexedTracks = allFoundFiles.filter(
(t) => t.viewInIgv && igvUrls[t.filename].url,
);
const unindexedTracks = allFoundFiles.filter((t) => t.viewInIgv && igvUrls[t.filename].url);

const igvIndexedTracks = indexedTracks.map((t) => ({
format: t.file_format,
Expand Down Expand Up @@ -177,65 +221,33 @@ const IndividualTracks = ({individual}) => {
tracks: igvTracks,
};

igv.createBrowser(igvRef.current, igvOptions).then( (browser) => {
igv.createBrowser(igvRef.current, igvOptions).then((browser) => {
igv.browser = browser;
igvRendered.current = true;

igv.browser.on("locuschange", debounce((referenceFrame) => {
storeIgvPosition(referenceFrame);
}, DEBOUNCE_WAIT));
igv.browser.on(
"locuschange",
debounce((referenceFrame) => {
storeIgvPosition(referenceFrame);
}, DEBOUNCE_WAIT),
);
});
}, [igvUrls]);

const trackTableColumns = [
{
title: "File",
key: "filename",
render: (_, track) => track.filename,
},
{
title: "File type",
key: "fileType",
render: (_, track) => track.description,
},
{
title: "View track",
key: "view",
align: "center",
render: (_, track) => <Switch checked={track.viewInIgv} onChange={() => toggleView(track)} />,
},
];

const TrackControlTable = () => {
return <Table
bordered
size="small"
pagination={false}
columns={trackTableColumns}
rowKey="filename"
dataSource={allFoundFiles}
style={{ display: "inline-block" }}
/>;
};

return (
<>
{allFoundFiles.length ? (
<Button icon="setting" style={{ marginRight: "8px" }} onClick={() => setModalVisible(true)}>
Configure Tracks
</Button>
) : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
<div ref={igvRef} />
<Divider />
<Modal
visible={modalVisible}
onOk={() => setModalVisible(false)}
onCancel={() => setModalVisible(false)}
zIndex={MODAL_Z_INDEX}
width={600}
>
<TrackControlTable />
</Modal>
{allFoundFiles.length ? (
<Button icon="setting" style={{ marginRight: "8px" }} onClick={() => setModalVisible(true)}>
Configure Tracks
</Button>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
<div ref={igvRef} />
<Divider />
<Modal visible={modalVisible} onOk={closeModal} onCancel={closeModal} zIndex={MODAL_Z_INDEX} width={600}>
<TrackControlTable toggleView={toggleView} allFoundFiles={allFoundFiles} />
</Modal>
</>
);
};
Expand All @@ -246,10 +258,7 @@ IndividualTracks.propTypes = {

function isViewable(file) {
const viewable = ["vcf", "cram", "bigwig", "bw"];
if (viewable.includes(file.file_format?.toLowerCase()) || viewable.includes(guessFileType(file.filename))) {
return true;
}
return false;
return viewable.includes(file.file_format?.toLowerCase()) || viewable.includes(guessFileType(file.filename));
}

export default IndividualTracks;