diff --git a/package-lock.json b/package-lock.json index 8904c661..63fd399f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "react-scripts": "5.0.1", "react-select": "^5.7.3", "react-select-event": "^5.5.1", + "react-switch": "^7.0.0", "reactjs-popup": "^2.0.5", "reactstrap": "^9.1.9", "readers-writer-lock": "^1.0.0", @@ -16707,6 +16708,18 @@ "@testing-library/dom": ">=7" } }, + "node_modules/react-switch": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/react-switch/-/react-switch-7.0.0.tgz", + "integrity": "sha512-KkDeW+cozZXI6knDPyUt3KBN1rmhoVYgAdCJqAh7st7tk8YE6N0iR89zjCWO8T8dUTeJGTR0KU+5CHCRMRffiA==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -32009,6 +32022,14 @@ "@testing-library/dom": ">=7" } }, + "react-switch": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/react-switch/-/react-switch-7.0.0.tgz", + "integrity": "sha512-KkDeW+cozZXI6knDPyUt3KBN1rmhoVYgAdCJqAh7st7tk8YE6N0iR89zjCWO8T8dUTeJGTR0KU+5CHCRMRffiA==", + "requires": { + "prop-types": "^15.7.2" + } + }, "react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/package.json b/package.json index b1ed9ccf..7d870d43 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "react-scripts": "5.0.1", "react-select": "^5.7.3", "react-select-event": "^5.5.1", + "react-switch": "^7.0.0", "reactjs-popup": "^2.0.5", "reactstrap": "^9.1.9", "readers-writer-lock": "^1.0.0", diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js index dae178fd..a0a8166e 100644 --- a/src/components/HeaderForm.js +++ b/src/components/HeaderForm.js @@ -11,6 +11,10 @@ import RegionInput from "./RegionInput"; import TrackPicker from "./TrackPicker"; import BedFileDropdown from "./BedFileDropdown"; import FormHelperText from "@mui/material/FormHelperText"; +import PopupDialog from "./PopupDialog.js"; +import Switch from "react-switch"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faGear } from "@fortawesome/free-solid-svg-icons"; import { parseRegion, stringifyRegion, @@ -194,6 +198,10 @@ function viewTargetsEqual(currViewTarget, nextViewTarget) { return false; } + if (currViewTarget.removeSequences !== nextViewTarget.removeSequences) { + return false; + } + return true; } @@ -317,6 +325,8 @@ class HeaderForm extends Component { dataType: ds.dataType, name: ds.name, simplify: ds.simplify, + popupOpen: false, + removeSequences: ds.removeSequences }; return stateVals; }); @@ -557,6 +567,7 @@ class HeaderForm extends Component { region: this.state.region, dataType: this.state.dataType, simplify: this.state.simplify && !readsExist(this.state.tracks), + removeSequences: this.state.removeSequences && !readsExist(this.state.tracks) }); handleGoButton = () => { @@ -825,6 +836,15 @@ class HeaderForm extends Component { this.setState({ simplify: !this.state.simplify }); }; + /* Function for toggling display of node sequences */ + toggleIncludeSequences = () => { + this.setState({ removeSequences: !this.state.removeSequences }); + }; + + togglePopup = () => { + this.setState({ popupOpen: !this.state.popupOpen }); + }; + render() { let errorDiv = null; if (this.state.error) { @@ -948,25 +968,45 @@ class HeaderForm extends Component { region={this.state.region} /> )} + {customFilesFlag && ( -
- {DataPositionFormRowComponent} +
+
+ {DataPositionFormRowComponent} +
+
+ {!readsExist(this.state.tracks) && ( + <> + + +
+ {/* Toggle for simplify small variants */} + + {/* Toggle for remove node sequences */} + +
+
+ + )} - {/* Button for simplify */} - {!readsExist(this.state.tracks) && ( - - )} +
)} diff --git a/src/config.json b/src/config.json index 89483970..e71eed1d 100644 --- a/src/config.json +++ b/src/config.json @@ -10,7 +10,8 @@ "region": "17:1-100", "bedFile": "exampleData/internal/snp1kg-BRCA1.bed", "dataType": "built-in", - "simplify": false + "simplify": false, + "removeSequences": false }, { "name": "vg \"small\" example", diff --git a/src/end-to-end.test.js b/src/end-to-end.test.js index 923a3f80..2ac04be6 100644 --- a/src/end-to-end.test.js +++ b/src/end-to-end.test.js @@ -339,7 +339,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?name=snp1kg-BRCA1&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®ion=17%3A1-100&bedFile=exampleData%2Finternal%2Fsnp1kg-BRCA1.bed&dataType=built-in&simplify=false"; + "http://localhost?name=snp1kg-BRCA1&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®ion=17%3A1-100&bedFile=exampleData%2Finternal%2Fsnp1kg-BRCA1.bed&dataType=built-in&simplify=false&removeSequences=false"; // Set up dropdown await act(async () => { let dropdown = document.getElementById("dataSourceSelect"); @@ -373,7 +373,7 @@ it("produces correct link for view before & after go is pressed", async () => { await clickCopyLink(); const expectedLinkCactus = - "http://localhost?tracks[0][trackFile]=exampleData%2Fcactus.vg.xg&tracks[0][trackType]=graph&tracks[1][trackFile]=exampleData%2Fcactus-NA12879.sorted.gam&tracks[1][trackType]=read&bedFile=exampleData%2Fcactus.bed&name=cactus®ion=ref%3A1-100&dataType=built-in&simplify=false"; + "http://localhost?tracks[0][trackFile]=exampleData%2Fcactus.vg.xg&tracks[0][trackType]=graph&tracks[1][trackFile]=exampleData%2Fcactus-NA12879.sorted.gam&tracks[1][trackType]=read&bedFile=exampleData%2Fcactus.bed&name=cactus®ion=ref%3A1-100&dataType=built-in&simplify=false&removeSequences=false"; // Make sure link has changed after pressing go expect(fakeClipboard).toEqual(expectedLinkCactus); }, 20000); diff --git a/src/server.mjs b/src/server.mjs index ed80e4d8..5c66b80b 100644 --- a/src/server.mjs +++ b/src/server.mjs @@ -382,6 +382,48 @@ api.post("/getChunkedData", (req, res, next) => { }); }); + +/* +graph = { + node: [ + { + sequence: "AGCT" + id: "1" + }, + { + sequence: "AGCTAG" + id: "2" + } + ], + edge: [], + path: [] +} +removing sequence would result in +graph = { + node: [ + { + id: "1" + }, + { + id: "2" + } + ], + edge: [], + path: [] +} +*/ + +// read a graph object and remove "sequence" fields in place +function removeNodeSequencesInPlace(graph){ + console.log("graph:", graph) + if (!graph.node){ + return; + } + graph.node.forEach(function(node) { + node.sequence = ""; + }) +} + // Handle a chunked data (tube map view) request. Returns a promise. On error, // either the promise rejects *or* next() is called with an error, or both. // TODO: This is a terrible mixed design for error handling; we need to either @@ -497,6 +539,15 @@ async function getChunkedData(req, res, next) { req.simplify = true; } + // client is going to send removeSequences = true if they don't want sequences of nodes to be displayed + req.removeSequences = false; + if (req.body.removeSequences) { + if (readsExist(req.body.tracks)) { + throw new BadRequestError("Can't remove node sequences if read tracks exist."); + } + req.removeSequences = true; + } + // check the bed file if this region has been pre-fetched let chunkPath = ""; if (req.withBed) { @@ -746,6 +797,9 @@ async function getChunkedData(req, res, next) { return; } req.graph = JSON.parse(graphAsString); + if (req.removeSequences){ + removeNodeSequencesInPlace(req.graph) + } req.region = [rangeRegion.start, rangeRegion.end]; // vg chunk always puts the path we reference on first automatically if (!sentResponse) { @@ -855,6 +909,10 @@ async function getChunkedData(req, res, next) { return; } req.graph = JSON.parse(graphAsString); + if (req.removeSequences){ + removeNodeSequencesInPlace(req.graph) + } + console.log("remove sequences? ", req.graph) req.region = [rangeRegion.start, rangeRegion.end]; // We might not have the path we are referencing on appearing first.