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

Fix left right buttons #387

Merged
merged 9 commits into from
Jan 24, 2024
2 changes: 2 additions & 0 deletions src/common.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const removeCommas = (input) => {
// or
// { contig, start, distance }
//
// a region string could look like: "17:1-100"
//
// For distance, + is used as the coordinate separator. For start/end ranges, - is used.
// The coordinates are set off from the contig by the last colon.
// Commas in coordinates are removed.
Expand Down
4 changes: 4 additions & 0 deletions src/components/DataPositionFormRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class DataPositionFormRow extends Component {
color="primary"
id="goLeftButton"
onClick={this.props.handleGoLeft}
disabled={this.props.uploadInProgress || !this.props.canGoLeft}
>
<FontAwesomeIcon icon={faStepBackward} size="lg" />
</Button>
Expand All @@ -97,6 +98,7 @@ class DataPositionFormRow extends Component {
color="primary"
id="goRightButton"
onClick={this.props.handleGoRight}
disabled={this.props.uploadInProgress || !this.props.canGoRight}
>
<FontAwesomeIcon icon={faStepForward} size="lg" />
</Button>
Expand Down Expand Up @@ -125,6 +127,8 @@ DataPositionFormRow.propTypes = {
uploadInProgress: PropTypes.bool.isRequired,
getCurrentViewTarget: PropTypes.func.isRequired,
viewTargetHasChange: PropTypes.bool.isRequired,
canGoLeft: PropTypes.bool.isRequired,
canGoRight: PropTypes.bool.isRequired,
};

export default DataPositionFormRow;
146 changes: 143 additions & 3 deletions src/components/HeaderForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,25 @@ const CLEAR_STATE = {
// Description for the selected region, is not displayed when empty
desc: "",

// This tracks several arrays of BED region data, stored by data type, with
// This tracks several arrays (desc, chr, start, end) of BED region data, with
// one entry in each array per region.
// desc: description of region, i.e. "region with no source graph available"
// chr: path in graph where the region is on, i.e. in ref:2000-3000, "ref" is the chr
// start: start of the region, i.e. in ref:2000-3000, 2000 is the start
// end: end of the region, i.e. in ref:2000-3000, 3000 is the end
// chunk: url/directory for preexisting cached chunk, or empty string if not available
// tracks: object full of tracks to apply when user selects region, or null
// so regionInfo might look like:
/*
{
chr: [ '17', '17' ],
start: [ '1', '1000' ],
end: [ '100', '1200' ],
desc: [ '17_1_100', '17_1000_1200' ],
chunk: [ '', '' ],
tracks: [ null, null ]
}
*/
regionInfo: {},

pathNames: [],
Expand Down Expand Up @@ -171,6 +188,77 @@ function viewTargetsEqual(currViewTarget, nextViewTarget) {
return true;
}

/* determine the current region: accepts a region string and returns the region index

example of regionInfo:
{
chr: [ '17', '17' ],
start: [ '1', '1000' ],
end: [ '100', '1200' ],
desc: [ '17_1_100', '17_1000_1200' ],
chunk: [ '', '' ],
tracks: [ null, null ]
}

examples:
if the regionString is "17:1-100", it would be parsed into {contig: "17", start: 1, end: 100} -> 0
if the regionString is "17:1000-1200", it would be parsed into {contig: "17", start: 1000, end: 1200} -> 1
if the regionString is "17:2000-3000", it cannot be found - return null

The function uses this approach to find the regionIndex given regionString and regionInfo:
function (region string){
parse(region string) -> return {contig, start, end}
loop over chr in region info
determine if contig, start, end are present at the current index
if present: return index
return null
}
*/
export const determineRegionIndex = (regionString, regionInfo) => {
let parsedRegion;
try {
parsedRegion = parseRegion(regionString);
} catch(error) {
return null;
}
if (!regionInfo["chr"]){
return null;
}
for (let i = 0; i < regionInfo["chr"].length; i++){
if ((parseInt(regionInfo["start"][i]) === parsedRegion.start)
&& (parseInt(regionInfo["end"][i]) === parsedRegion.end)
&& (regionInfo["chr"][i] === parsedRegion.contig)){
return i;
}
}
return null;
}

/*
This function takes in a regionIndex and regionInfo, and reconstructs a regionString from them
assumes that index is valid in regionInfo

example of regionInfo:
{
chr: [ '17', '17' ],
start: [ '1', '1000' ],
end: [ '100', '1200' ],
desc: [ '17_1_100', '17_1000_1200' ],
chunk: [ '', '' ],
tracks: [ null, null ]
}

example of regionIndex: 0

example of regionString: "17:1-100"
*/
export const regionStringFromRegionIndex = (regionIndex, regionInfo) => {
let regionStart = regionInfo["start"][regionIndex];
let regionEnd = regionInfo["end"][regionIndex];
let regionContig = regionInfo["chr"][regionIndex];
return regionContig + ":" + regionStart + "-" + regionEnd;
}

class HeaderForm extends Component {
state = EMPTY_STATE;
componentDidMount() {
Expand Down Expand Up @@ -504,6 +592,9 @@ class HeaderForm extends Component {
return regionString;
};




// 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
Expand Down Expand Up @@ -627,12 +718,57 @@ class HeaderForm extends Component {
);
}


/* Offset the region left or right by the given negative or positive fraction*/
// offset: +1 or -1
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);
this.setState(
(state) => ({
region: regionString,
}),
() => this.handleGoButton()
);
}

canGoLeft = (regionIndex) => {
if (this.state.bedFile){
return (regionIndex > 0);
} else {
return true;
}
}

canGoRight = (regionIndex) => {
if (this.state.bedFile){
if (!this.state.regionInfo["chr"]){
return false;
}
return (regionIndex < ((this.state.regionInfo["chr"].length) - 1));
} else {
return true;
}
}


handleGoRight = () => {
this.budgeRegion(0.5);
if (this.state.bedFile){
this.jumpRegion(1);
} else {
this.budgeRegion(0.5);
}
};

handleGoLeft = () => {
this.budgeRegion(-0.5);
if (this.state.bedFile){
this.jumpRegion(-1);
} else {
this.budgeRegion(-0.5);
}
};

showFileSizeAlert = () => {
Expand Down Expand Up @@ -736,9 +872,13 @@ class HeaderForm extends Component {
uploadInProgress={this.state.uploadInProgress}
getCurrentViewTarget={this.props.getCurrentViewTarget}
viewTargetHasChange={viewTargetHasChange}
canGoLeft={this.canGoLeft(determineRegionIndex(this.state.region, this.state.regionInfo))}
canGoRight={this.canGoRight(determineRegionIndex(this.state.region, this.state.regionInfo))}
/>
);



return (
<div>
<Container>
Expand Down
64 changes: 64 additions & 0 deletions src/components/HeaderForm.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { determineRegionIndex, regionStringFromRegionIndex } from "./HeaderForm.js";


// test for determineRegionIndex and regionStringFromRegionIndex
describe("determine regionIndex and corresponding region strings for various region inputs", () => {
// TEST #1
it("determine regionIndex and regionString for smaller regionInfo lists", async () => {
let regionInfo = {
chr: [ 'ref', 'ref' ],
start: [ '1000', '2000' ],
end: [ '2000', '3000' ],
desc: [ '17_1_100', 'ref_2000_3000' ],
chunk: [ '', '' ],
tracks: [ null, null ]
}
let regionString = "ref:2000-3000";
let regionIndex = determineRegionIndex(regionString, regionInfo);
expect(regionIndex).toBe(1);
expect(regionStringFromRegionIndex(regionIndex, regionInfo)).toBe("ref:2000-3000");
})
// TEST #2
it("determine regionIndex and regionString for larger regionInfo lists", async () => {
let regionInfo = {
chr: [ '17', 'ref', '17', 'ref', '17', 'ref' ],
start: [ '100', '200', '2000', '3000', '4000', '5000' ],
end: [ '200', '300', '3000', '4000', '5000', '6000' ],
desc: [ '17_100_200', '17_200_300', 'ref_2000_3000', 'ref_3000_4000', 'ref_4000_5000', 'ref_5000_6000' ],
chunk: [ '', '', '', '', '', '', '' ],
tracks: [ null, null, null, null, null, null ]
}
let regionString = "17:4000-5000";
let regionIndex = determineRegionIndex(regionString, regionInfo);
expect(regionIndex).toBe(4);
expect(regionStringFromRegionIndex(regionIndex, regionInfo)).toBe("17:4000-5000");
})
// TEST #3
it("determine regionIndex to be null for input of region not found in regionInfo", async () => {
let regionInfo = {
chr: [ '17', 'ref', '17', 'ref', '17', 'ref' ],
start: [ '100', '200', '2000', '3000', '4000', '5000' ],
end: [ '200', '300', '3000', '4000', '5000', '6000' ],
desc: [ '17_100_200', '17_200_300', 'ref_2000_3000', 'ref_3000_4000', 'ref_4000_5000', 'ref_5000_6000' ],
chunk: [ '', '', '', '', '', '', '' ],
tracks: [ null, null, null, null, null, null ]
}
let regionString = "17:5000-7000";
let regionIndex = determineRegionIndex(regionString, regionInfo);
expect(regionIndex).toBe(null);
})
// TEST #4
it("determine regionIndex and regionString to be null given empty regionInfo", async () => {
let regionInfo = {
chr: [],
start: [],
end: [],
desc: [],
chunk: [],
tracks: []
}
let regionString = "17:5000-7000";
let regionIndex = determineRegionIndex(regionString, regionInfo);
expect(regionIndex).toBe(null);
})
});
Loading