diff --git a/src/App.js b/src/App.js index 24d25228..beea0316 100644 --- a/src/App.js +++ b/src/App.js @@ -68,6 +68,7 @@ class App extends Component { alphaReadsByMappingQuality: false, colorSchemes: getColorSchemesFromTracks(this.defaultViewTarget.tracks), mappingQualityCutoff: 0, + hiddenTracks: [], }, APIInterface: new ServerAPI(props.apiUrl) }; @@ -95,6 +96,7 @@ class App extends Component { visOptions: { ...state.visOptions, colorSchemes: [], + hiddenTracks: [], }, }; } else if (mode === "server") { @@ -107,6 +109,7 @@ class App extends Component { visOptions: { ...state.visOptions, colorSchemes: getColorSchemesFromTracks(this.defaultViewTarget.tracks), + hiddenTracks: [], }, }; } else { @@ -199,6 +202,15 @@ class App extends Component { })); }; + handleTrackVisibilityChanged = (hiddenTrackIds) => { + this.setState((state) => ({ + visOptions: { + ...state.visOptions, + hiddenTracks: hiddenTrackIds, + }, + })); + }; + // Set a color scheme setting for a particular track. // // key is the name of the setting to set, and may be "mainPalette", "auxPalette", or "colorByMappingQuality". @@ -251,6 +263,7 @@ class App extends Component { dataOrigin={this.state.dataOrigin} visOptions={this.state.visOptions} APIInterface={this.state.APIInterface} + onTrackVisibilityChanged={this.handleTrackVisibilityChanged} /> { tubeMap.create({ svgID: "#svg", @@ -50,6 +58,7 @@ class TubeMap extends Component { tubeMap.setShowReadsFlag(visOptions.showReads); tubeMap.setSoftClipsFlag(visOptions.showSoftClips); tubeMap.setColoredNodes(visOptions.coloredNodes); + tubeMap.setHiddenTrackIds(visOptions.hiddenTracks || []); for (let key of Object.keys(visOptions.colorSchemes)) { // Apply color-by-mapping-quality parameter to all the schemes. @@ -76,7 +85,8 @@ TubeMap.propTypes = { reads: PropTypes.array.isRequired, region: PropTypes.array.isRequired, visOptions: PropTypes.object.isRequired, - nodeSequences: PropTypes.bool + nodeSequences: PropTypes.bool, + onTrackVisibilityChanged: PropTypes.func, }; TubeMap.defaultProps = { diff --git a/src/components/TubeMapContainer.js b/src/components/TubeMapContainer.js index f3478f64..530b44db 100644 --- a/src/components/TubeMapContainer.js +++ b/src/components/TubeMapContainer.js @@ -126,6 +126,7 @@ class TubeMapContainer extends Component { ...this.props.visOptions, }} nodeSequences={!this.props.viewTarget.removeSequences} + onTrackVisibilityChanged={this.props.onTrackVisibilityChanged} /> @@ -326,6 +327,7 @@ TubeMapContainer.propTypes = { viewTarget: PropTypes.object.isRequired, visOptions: PropTypes.object.isRequired, APIInterface: PropTypes.object.isRequired, + onTrackVisibilityChanged: PropTypes.func, }; export default TubeMapContainer; diff --git a/src/util/tubemap.js b/src/util/tubemap.js index 062938d8..5efb45dc 100644 --- a/src/util/tubemap.js +++ b/src/util/tubemap.js @@ -159,6 +159,8 @@ const config = { showInfoCallback: function (info) { alert(info); }, + hiddenTrackIds: new Set(), + visibilityChangedCallback: null, }; // variables for storing info which can be directly translated into drawing instructions @@ -191,6 +193,13 @@ export function create(params) { // And then leave a hole in the array at 0 which we won't iterate over. delete inputNodes[0]; inputTracks = deepCopy(params.tracks); // deep copy + inputTracks.forEach((t) => { + if (!t.type) t.type = "haplotype"; + }); + config.hiddenTrackIds.forEach((id) => { + const track = inputTracks.find((t) => t.id === id); + if (track) track.hidden = true; + }); inputReads = params.reads || null; inputRegion = params.region; bed = params.bed || null; @@ -311,6 +320,14 @@ export function changeTrackVisibility(trackID) { } else { inputTracks[i].hidden = true; } + if (inputTracks[i].hidden) { + config.hiddenTrackIds.add(trackID); + } else { + config.hiddenTrackIds.delete(trackID); + } + if (config.visibilityChangedCallback) { + config.visibilityChangedCallback(Array.from(config.hiddenTrackIds)); + } } createTubeMap(); } @@ -318,12 +335,19 @@ export function changeTrackVisibility(trackID) { // to select/deselect all export function changeAllTracksVisibility(value) { let i = 0; + config.hiddenTrackIds.clear(); while (i < inputTracks.length) { inputTracks[i].hidden = !value; + if (!value) { + config.hiddenTrackIds.add(inputTracks[i].id); + } var checkbox = document.getElementById(`showTrack${inputTracks[i].id}`); checkbox.checked = value; i += 1; } + if (config.visibilityChangedCallback) { + config.visibilityChangedCallback(Array.from(config.hiddenTrackIds)); + } createTubeMap(); } @@ -418,6 +442,18 @@ export function setMappingQualityCutoff(value) { } } +export function setHiddenTrackIds(ids) { + config.hiddenTrackIds = new Set(ids || []); +} + +export function setVisibilityChangedCallback(callback) { + config.visibilityChangedCallback = callback; +} + +export function getHiddenTrackIds() { + return Array.from(config.hiddenTrackIds); +} + // main function createTubeMap() { console.log("Recreating tube map in", svgID); @@ -4224,24 +4260,23 @@ function drawLegend() { content += ''; const listeners = []; - // This is in terms of tracks, but when we change visibility we need to touch inputTracks, so we need to set up listeners by track ID. - for (let i = 0; i < tracks.length; i += 1) { - if (tracks[i].type === "haplotype") { + for (let i = 0; i < inputTracks.length; i += 1) { + if (inputTracks[i].type === "haplotype") { content += ``; - if (tracks[i].hasOwnProperty("name")) { - content += ``; + if (inputTracks[i].hasOwnProperty("name")) { + content += ``; } else { - content += ``; + content += ``; } - content += ``; - listeners.push(tracks[i].id); + const isChecked = !config.hiddenTrackIds.has(inputTracks[i].id); + content += ``; + listeners.push(inputTracks[i].id); } } content += " { document diff --git a/src/util/tubemap.test.js b/src/util/tubemap.test.js index 80d10c20..cb97a30c 100644 --- a/src/util/tubemap.test.js +++ b/src/util/tubemap.test.js @@ -1,4 +1,11 @@ -import { cigar_string, coverage, axisIntervals } from "./tubemap"; +import { + cigar_string, + coverage, + axisIntervals, + setHiddenTrackIds, + getHiddenTrackIds, + setVisibilityChangedCallback, +} from "./tubemap"; // cigar string test describe("cigar_string", () => { @@ -865,6 +872,54 @@ describe("coverage", () => { }); +describe("hiddenTrackIds", () => { + it("starts empty", async () => { + expect(getHiddenTrackIds()).toStrictEqual([]); + }); + + it("can set and get hidden track IDs", async () => { + setHiddenTrackIds([1, 3, 5]); + expect(getHiddenTrackIds()).toStrictEqual([1, 3, 5]); + }); + + it("can set empty list", async () => { + setHiddenTrackIds([]); + expect(getHiddenTrackIds()).toStrictEqual([]); + }); + + it("can set null/undefined to empty", async () => { + setHiddenTrackIds(null); + expect(getHiddenTrackIds()).toStrictEqual([]); + setHiddenTrackIds(undefined); + expect(getHiddenTrackIds()).toStrictEqual([]); + }); + + it("deduplicates IDs via Set", async () => { + setHiddenTrackIds([1, 1, 2, 2, 3]); + const result = getHiddenTrackIds(); + expect(result.sort((a, b) => a - b)).toStrictEqual([1, 2, 3]); + }); + + it("replaces previous values on set", async () => { + setHiddenTrackIds([1, 2]); + setHiddenTrackIds([3, 4]); + expect(getHiddenTrackIds()).toStrictEqual([3, 4]); + }); +}); + +describe("visibilityChangedCallback", () => { + it("can set callback", async () => { + const cb = () => {}; + setVisibilityChangedCallback(cb); + // No error thrown + }); + + it("can clear callback with null", async () => { + setVisibilityChangedCallback(null); + // No error thrown + }); +}); + describe("axisIntervals", () => { // TEST 1 it("can handle an empty array", async () => {
ColorTracknameShow Track
${tracks[i].name}${inputTracks[i].name}${tracks[i].id}${inputTracks[i].id}