From 4fb35307174f30db24168cc028f38d13cf6756fd Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla <crf85@cornell.edu> Date: Fri, 15 Mar 2024 17:30:34 -0400 Subject: [PATCH 01/16] -feat: check for minimal_mode url parameter -feat: render minimal_mode version when url parameter is true -feat: remove hover listener (always keep visualization active) -style: remove box-shadow --- src/App.tsx | 315 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 312 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e307c00a..9b180efd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,7 +31,7 @@ const DATABSE_THUMBNAILS = await db.get(); const GENERATED_THUMBNAILS = {}; const INIT_VIS_PANEL_WIDTH = window.innerWidth; -const VIS_PADDING = 60; +let VIS_PADDING = 60; const ZOOM_PADDING = 200; const ZOOM_DURATION = 500; @@ -48,6 +48,13 @@ const allDrivers = [ function App(props: RouteComponentProps) { // URL parameters const urlParams = new URLSearchParams(props.location.search); + + // Flag URL Parameter for "minimal_mode", which if true only shows the visualization panel + const minimal_mode = urlParams.get('minimal_mode'); + + // Overwrite the padding + VIS_PADDING = 0; + // !! instead of using `urlParams.get('external')`, we directly parse the external URL in order to include // any inlined parameters of the external link (e.g., private AWS link with authentication info.) let externalUrl = null; @@ -94,7 +101,7 @@ function App(props: RouteComponentProps) { const [filteredSamples, setFilteredSamples] = useState(selectedSamples); const [showOverview, setShowOverview] = useState(true); const [showPutativeDriver, setShowPutativeDriver] = useState(true); - const [interactiveMode, setInteractiveMode] = useState(false); + const [interactiveMode, setInteractiveMode] = useState(minimal_mode ?? false); const [visPanelWidth, setVisPanelWidth] = useState(INIT_VIS_PANEL_WIDTH - VIS_PADDING * 2); const [overviewChr, setOverviewChr] = useState(''); const [genomeViewChr, setGenomeViewChr] = useState(''); @@ -104,7 +111,7 @@ function App(props: RouteComponentProps) { const [selectedSvId, setSelectedSvId] = useState<string>(''); const [breakpoints, setBreakpoints] = useState<[number, number, number, number]>([1, 100, 1, 100]); const [bpIntervals, setBpIntervals] = useState<[number, number, number, number] | undefined>(); - const [mouseOnVis, setMouseOnVis] = useState(false); + const [mouseOnVis, setMouseOnVis] = useState(minimal_mode ?? false); const [jumpButtonInfo, setJumpButtonInfo] = useState<{ id: string; x: number; y: number; direction: 'leftward' | 'rightward'; zoomTo: () => void }>(); const mousePos = useRef({ x: -100, y: -100 }); @@ -609,6 +616,308 @@ function App(props: RouteComponentProps) { }; }); + if (minimal_mode) { + return ( + <ErrorBoundary> + <div + className="vis-panel-container" + style={{ width: '100%', height: '100%' }} + onMouseMove={e => { + const top = e.clientY; + const left = e.clientX; + const width = window.innerWidth; + const height = window.innerHeight; + mousePos.current = { x: left, y: top }; + }} + onWheel={() => setJumpButtonInfo(undefined)} + onClick={() => { + setJumpButtonInfo(undefined); + }} + > + <div id="vis-panel" className="vis-panel"> + <div + id="gosling-panel" + className="gosling-panel" + style={{ + width: `calc(100% - ${VIS_PADDING * 2}px)`, + height: `calc(100% - ${VIS_PADDING * 2}px)`, + padding: VIS_PADDING + }} + > + {goslingComponent} + {jumpButtonInfo ? ( + <button + className="jump-to-bp-btn" + style={{ + position: 'fixed', + left: `${ + jumpButtonInfo.x + 20 + (jumpButtonInfo.direction === 'leftward' ? -60 : 0) + }px`, + top: `${jumpButtonInfo.y}px` + }} + onClick={() => jumpButtonInfo.zoomTo()} + > + {jumpButtonInfo.direction === 'leftward' ? '←' : '→'} + </button> + ) : null} + <div + style={{ + width: '100%', + height: '100%', + top: VIS_PADDING, + left: VIS_PADDING, + opacity: 0.9, + zIndex: 2, + pointerEvents: interactiveMode ? 'none' : 'auto' + }} + /> + <div + style={{ + pointerEvents: 'none', + width: '100%', + height: '100%', + position: 'relative', + zIndex: 998 + }} + > + <select + style={{ + pointerEvents: 'auto', + top: '3px' + }} + className="nav-dropdown" + onChange={e => { + setShowSamples(false); + const chr = e.currentTarget.value; + setTimeout(() => setOverviewChr(chr), 300); + }} + value={overviewChr} + disabled={!showOverview} + > + {[WHOLE_CHROMOSOME_STR, ...CHROMOSOMES].map(chr => { + return ( + <option key={chr} value={chr}> + {chr} + </option> + ); + })} + </select> + <img + src={legend} + style={{ + position: 'absolute', + right: '3px', + top: '3px', + zIndex: 998, + width: '120px' + }} + /> + <select + style={{ + pointerEvents: 'auto', + // !! This should be identical to how the height of circos determined. + top: `${Math.min(visPanelWidth, 600)}px` + }} + className="nav-dropdown" + onChange={e => { + setShowSamples(false); + const chr = e.currentTarget.value; + setTimeout(() => setGenomeViewChr(chr), 300); + }} + value={genomeViewChr} + disabled={!showOverview} + > + {CHROMOSOMES.map(chr => { + return ( + <option key={chr} value={chr}> + {chr} + </option> + ); + })} + </select> + <svg + className="gene-search-icon" + viewBox="0 0 16 16" + style={{ + top: `${Math.min(visPanelWidth, 600) + 6}px` + // visibility: demo.assembly === 'hg38' ? 'visible' : 'hidden' + }} + > + <path + fillRule="evenodd" + d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z" + /> + </svg> + <input + type="text" + className="gene-search" + placeholder="Search Gene (e.g., MYC)" + // alt={demo.assembly === 'hg38' ? 'Search Gene' : 'Not currently available for this assembly.'} + style={{ + pointerEvents: 'auto', + top: `${Math.min(visPanelWidth, 600)}px` + // cursor: demo.assembly === 'hg38' ? 'auto' : 'not-allowed', + // visibility: demo.assembly === 'hg38' ? 'visible' : 'hidden' + }} + // disabled={demo.assembly === 'hg38' ? false : true} + // onChange={(e) => { + // const keyword = e.target.value; + // if(keyword !== "" && !keyword.startsWith("c")) { + // gosRef.current.api.suggestGene(keyword, (suggestions) => { + // setGeneSuggestions(suggestions); + // }); + // setSuggestionPosition({ + // left: searchBoxRef.current.getBoundingClientRect().left, + // top: searchBoxRef.current.getBoundingClientRect().top + searchBoxRef.current.getBoundingClientRect().height, + // }); + // } else { + // setGeneSuggestions([]); + // } + // setSearchKeyword(keyword); + // }} + onKeyDown={e => { + const keyword = (e.target as HTMLTextAreaElement).value; + switch (e.key) { + case 'ArrowUp': + break; + case 'ArrowDown': + break; + case 'Enter': + // https://github.com/gosling-lang/gosling.js/blob/7555ab711023a0c3e2076a448756a9ba3eeb04f7/src/core/api.ts#L156 + gosRef.current.hgApi.api.zoomToGene( + `${demo.id}-mid-ideogram`, + keyword, + 10000, + 1000 + ); + break; + case 'Esc': + case 'Escape': + break; + } + }} + /> + <button + style={{ + pointerEvents: 'auto', + // !! This should be identical to how the height of circos determined. + top: `${Math.min(visPanelWidth, 600)}px` + }} + className="zoom-in-button" + onClick={e => { + const trackId = `${demo.id}-mid-ideogram`; + const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; + if (end - start < 100) return; + const delta = (end - start) / 3.0; + gosRef.current.api.zoomTo( + trackId, + `chr1:${start + delta}-${end - delta}`, + 0, + ZOOM_DURATION + ); + }} + > + + + </button> + <button + style={{ + pointerEvents: 'auto', + // !! This should be identical to how the height of circos determined. + top: `${Math.min(visPanelWidth, 600)}px` + }} + className="zoom-out-button" + onClick={e => { + const trackId = `${demo.id}-mid-ideogram`; + const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; + const delta = (end - start) / 2.0; + gosRef.current.api.zoomTo( + trackId, + `chr1:${start}-${end}`, + delta, + ZOOM_DURATION + ); + }} + > + - + </button> + <button + style={{ + pointerEvents: 'auto', + // !! This should be identical to how the height of circos determined. + top: `${Math.min(visPanelWidth, 600)}px` + }} + className="zoom-left-button" + onClick={e => { + const trackId = `${demo.id}-mid-ideogram`; + const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; + if (end - start < 100) return; + const delta = (end - start) / 4.0; + gosRef.current.api.zoomTo( + trackId, + `chr1:${start - delta}-${end - delta}`, + 0, + ZOOM_DURATION + ); + }} + > + ← + </button> + <button + style={{ + pointerEvents: 'auto', + // !! This should be identical to how the height of circos determined. + top: `${Math.min(visPanelWidth, 600)}px` + }} + className="zoom-right-button" + onClick={e => { + const trackId = `${demo.id}-mid-ideogram`; + const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; + const delta = (end - start) / 4.0; + gosRef.current.api.zoomTo( + trackId, + `chr1:${start + delta}-${end + delta}`, + 0, + ZOOM_DURATION + ); + }} + > + → + </button> + </div> + </div> + </div> + <div + style={{ + width: '100%', + height: '100%', + visibility: 'collapse', + boxShadow: interactiveMode ? 'inset 0 0 4px 2px #2399DB' : 'none', + zIndex: 9999, + background: 'none', + position: 'absolute', + top: 0, + left: 0, + pointerEvents: 'none' + }} + /> + <div + style={{ + background: 'none', + position: 'absolute', + bottom: 20, + left: VIS_PADDING, + pointerEvents: 'none', + visibility: demo.bam ? 'collapse' : 'visible' + }} + > + {'ⓘ No read alignment data available for this sample.'} + </div> + <div id="hidden-gosling" style={{ visibility: 'collapse', position: 'fixed' }} /> + </div> + </ErrorBoundary> + ); + } + return ( <ErrorBoundary> <div From 84fc1aad34f18cba14cd31c5ca651031a2a65a45 Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla <crf85@cornell.edu> Date: Tue, 19 Mar 2024 14:14:03 -0400 Subject: [PATCH 02/16] fix: adjust padding in minimal mode --- src/App.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 9b180efd..0aed3f96 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -52,8 +52,10 @@ function App(props: RouteComponentProps) { // Flag URL Parameter for "minimal_mode", which if true only shows the visualization panel const minimal_mode = urlParams.get('minimal_mode'); - // Overwrite the padding - VIS_PADDING = 0; + // Overwrite the padding in minimal mode + if (minimal_mode) { + VIS_PADDING = 0; + } // !! instead of using `urlParams.get('external')`, we directly parse the external URL in order to include // any inlined parameters of the external link (e.g., private AWS link with authentication info.) From dd045e3e0f049de986e9de044531e3f19898f680 Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla <crf85@cornell.edu> Date: Tue, 19 Mar 2024 17:25:19 -0400 Subject: [PATCH 03/16] -feat: create object for vis padding -feat: add buttons for scrolling to top and bottom of navigation --- src/App.tsx | 413 ++++++++-------------------------------------------- 1 file changed, 63 insertions(+), 350 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 0aed3f96..0acad18d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,7 +31,6 @@ const DATABSE_THUMBNAILS = await db.get(); const GENERATED_THUMBNAILS = {}; const INIT_VIS_PANEL_WIDTH = window.innerWidth; -let VIS_PADDING = 60; const ZOOM_PADDING = 200; const ZOOM_DURATION = 500; @@ -48,14 +47,13 @@ const allDrivers = [ function App(props: RouteComponentProps) { // URL parameters const urlParams = new URLSearchParams(props.location.search); - - // Flag URL Parameter for "minimal_mode", which if true only shows the visualization panel - const minimal_mode = urlParams.get('minimal_mode'); - - // Overwrite the padding in minimal mode - if (minimal_mode) { - VIS_PADDING = 0; - } + const isMinimalMode = urlParams.get('minimal_mode'); + const VIS_PADDING = { + top: 60, + right: isMinimalMode ? 20 : 60, + bottom: isMinimalMode ? 0 : 60, + left: isMinimalMode ? 20 : 60 + }; // !! instead of using `urlParams.get('external')`, we directly parse the external URL in order to include // any inlined parameters of the external link (e.g., private AWS link with authentication info.) @@ -103,8 +101,8 @@ function App(props: RouteComponentProps) { const [filteredSamples, setFilteredSamples] = useState(selectedSamples); const [showOverview, setShowOverview] = useState(true); const [showPutativeDriver, setShowPutativeDriver] = useState(true); - const [interactiveMode, setInteractiveMode] = useState(minimal_mode ?? false); - const [visPanelWidth, setVisPanelWidth] = useState(INIT_VIS_PANEL_WIDTH - VIS_PADDING * 2); + const [interactiveMode, setInteractiveMode] = useState(isMinimalMode ?? false); + const [visPanelWidth, setVisPanelWidth] = useState(INIT_VIS_PANEL_WIDTH - VIS_PADDING.left * 2); const [overviewChr, setOverviewChr] = useState(''); const [genomeViewChr, setGenomeViewChr] = useState(''); const [drivers, setDrivers] = useState( @@ -113,7 +111,7 @@ function App(props: RouteComponentProps) { const [selectedSvId, setSelectedSvId] = useState<string>(''); const [breakpoints, setBreakpoints] = useState<[number, number, number, number]>([1, 100, 1, 100]); const [bpIntervals, setBpIntervals] = useState<[number, number, number, number] | undefined>(); - const [mouseOnVis, setMouseOnVis] = useState(minimal_mode ?? false); + const [mouseOnVis, setMouseOnVis] = useState(isMinimalMode ?? false); const [jumpButtonInfo, setJumpButtonInfo] = useState<{ id: string; x: number; y: number; direction: 'leftward' | 'rightward'; zoomTo: () => void }>(); const mousePos = useRef({ x: -100, y: -100 }); @@ -311,7 +309,7 @@ function App(props: RouteComponentProps) { window.addEventListener( 'resize', debounce(() => { - setVisPanelWidth(window.innerWidth - VIS_PADDING * 2); + setVisPanelWidth(window.innerWidth - VIS_PADDING.left * 2); }, 500) ); }, []); @@ -618,308 +616,6 @@ function App(props: RouteComponentProps) { }; }); - if (minimal_mode) { - return ( - <ErrorBoundary> - <div - className="vis-panel-container" - style={{ width: '100%', height: '100%' }} - onMouseMove={e => { - const top = e.clientY; - const left = e.clientX; - const width = window.innerWidth; - const height = window.innerHeight; - mousePos.current = { x: left, y: top }; - }} - onWheel={() => setJumpButtonInfo(undefined)} - onClick={() => { - setJumpButtonInfo(undefined); - }} - > - <div id="vis-panel" className="vis-panel"> - <div - id="gosling-panel" - className="gosling-panel" - style={{ - width: `calc(100% - ${VIS_PADDING * 2}px)`, - height: `calc(100% - ${VIS_PADDING * 2}px)`, - padding: VIS_PADDING - }} - > - {goslingComponent} - {jumpButtonInfo ? ( - <button - className="jump-to-bp-btn" - style={{ - position: 'fixed', - left: `${ - jumpButtonInfo.x + 20 + (jumpButtonInfo.direction === 'leftward' ? -60 : 0) - }px`, - top: `${jumpButtonInfo.y}px` - }} - onClick={() => jumpButtonInfo.zoomTo()} - > - {jumpButtonInfo.direction === 'leftward' ? '←' : '→'} - </button> - ) : null} - <div - style={{ - width: '100%', - height: '100%', - top: VIS_PADDING, - left: VIS_PADDING, - opacity: 0.9, - zIndex: 2, - pointerEvents: interactiveMode ? 'none' : 'auto' - }} - /> - <div - style={{ - pointerEvents: 'none', - width: '100%', - height: '100%', - position: 'relative', - zIndex: 998 - }} - > - <select - style={{ - pointerEvents: 'auto', - top: '3px' - }} - className="nav-dropdown" - onChange={e => { - setShowSamples(false); - const chr = e.currentTarget.value; - setTimeout(() => setOverviewChr(chr), 300); - }} - value={overviewChr} - disabled={!showOverview} - > - {[WHOLE_CHROMOSOME_STR, ...CHROMOSOMES].map(chr => { - return ( - <option key={chr} value={chr}> - {chr} - </option> - ); - })} - </select> - <img - src={legend} - style={{ - position: 'absolute', - right: '3px', - top: '3px', - zIndex: 998, - width: '120px' - }} - /> - <select - style={{ - pointerEvents: 'auto', - // !! This should be identical to how the height of circos determined. - top: `${Math.min(visPanelWidth, 600)}px` - }} - className="nav-dropdown" - onChange={e => { - setShowSamples(false); - const chr = e.currentTarget.value; - setTimeout(() => setGenomeViewChr(chr), 300); - }} - value={genomeViewChr} - disabled={!showOverview} - > - {CHROMOSOMES.map(chr => { - return ( - <option key={chr} value={chr}> - {chr} - </option> - ); - })} - </select> - <svg - className="gene-search-icon" - viewBox="0 0 16 16" - style={{ - top: `${Math.min(visPanelWidth, 600) + 6}px` - // visibility: demo.assembly === 'hg38' ? 'visible' : 'hidden' - }} - > - <path - fillRule="evenodd" - d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z" - /> - </svg> - <input - type="text" - className="gene-search" - placeholder="Search Gene (e.g., MYC)" - // alt={demo.assembly === 'hg38' ? 'Search Gene' : 'Not currently available for this assembly.'} - style={{ - pointerEvents: 'auto', - top: `${Math.min(visPanelWidth, 600)}px` - // cursor: demo.assembly === 'hg38' ? 'auto' : 'not-allowed', - // visibility: demo.assembly === 'hg38' ? 'visible' : 'hidden' - }} - // disabled={demo.assembly === 'hg38' ? false : true} - // onChange={(e) => { - // const keyword = e.target.value; - // if(keyword !== "" && !keyword.startsWith("c")) { - // gosRef.current.api.suggestGene(keyword, (suggestions) => { - // setGeneSuggestions(suggestions); - // }); - // setSuggestionPosition({ - // left: searchBoxRef.current.getBoundingClientRect().left, - // top: searchBoxRef.current.getBoundingClientRect().top + searchBoxRef.current.getBoundingClientRect().height, - // }); - // } else { - // setGeneSuggestions([]); - // } - // setSearchKeyword(keyword); - // }} - onKeyDown={e => { - const keyword = (e.target as HTMLTextAreaElement).value; - switch (e.key) { - case 'ArrowUp': - break; - case 'ArrowDown': - break; - case 'Enter': - // https://github.com/gosling-lang/gosling.js/blob/7555ab711023a0c3e2076a448756a9ba3eeb04f7/src/core/api.ts#L156 - gosRef.current.hgApi.api.zoomToGene( - `${demo.id}-mid-ideogram`, - keyword, - 10000, - 1000 - ); - break; - case 'Esc': - case 'Escape': - break; - } - }} - /> - <button - style={{ - pointerEvents: 'auto', - // !! This should be identical to how the height of circos determined. - top: `${Math.min(visPanelWidth, 600)}px` - }} - className="zoom-in-button" - onClick={e => { - const trackId = `${demo.id}-mid-ideogram`; - const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; - if (end - start < 100) return; - const delta = (end - start) / 3.0; - gosRef.current.api.zoomTo( - trackId, - `chr1:${start + delta}-${end - delta}`, - 0, - ZOOM_DURATION - ); - }} - > - + - </button> - <button - style={{ - pointerEvents: 'auto', - // !! This should be identical to how the height of circos determined. - top: `${Math.min(visPanelWidth, 600)}px` - }} - className="zoom-out-button" - onClick={e => { - const trackId = `${demo.id}-mid-ideogram`; - const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; - const delta = (end - start) / 2.0; - gosRef.current.api.zoomTo( - trackId, - `chr1:${start}-${end}`, - delta, - ZOOM_DURATION - ); - }} - > - - - </button> - <button - style={{ - pointerEvents: 'auto', - // !! This should be identical to how the height of circos determined. - top: `${Math.min(visPanelWidth, 600)}px` - }} - className="zoom-left-button" - onClick={e => { - const trackId = `${demo.id}-mid-ideogram`; - const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; - if (end - start < 100) return; - const delta = (end - start) / 4.0; - gosRef.current.api.zoomTo( - trackId, - `chr1:${start - delta}-${end - delta}`, - 0, - ZOOM_DURATION - ); - }} - > - ← - </button> - <button - style={{ - pointerEvents: 'auto', - // !! This should be identical to how the height of circos determined. - top: `${Math.min(visPanelWidth, 600)}px` - }} - className="zoom-right-button" - onClick={e => { - const trackId = `${demo.id}-mid-ideogram`; - const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; - const delta = (end - start) / 4.0; - gosRef.current.api.zoomTo( - trackId, - `chr1:${start + delta}-${end + delta}`, - 0, - ZOOM_DURATION - ); - }} - > - → - </button> - </div> - </div> - </div> - <div - style={{ - width: '100%', - height: '100%', - visibility: 'collapse', - boxShadow: interactiveMode ? 'inset 0 0 4px 2px #2399DB' : 'none', - zIndex: 9999, - background: 'none', - position: 'absolute', - top: 0, - left: 0, - pointerEvents: 'none' - }} - /> - <div - style={{ - background: 'none', - position: 'absolute', - bottom: 20, - left: VIS_PADDING, - pointerEvents: 'none', - visibility: demo.bam ? 'collapse' : 'visible' - }} - > - {'ⓘ No read alignment data available for this sample.'} - </div> - <div id="hidden-gosling" style={{ visibility: 'collapse', position: 'fixed' }} /> - </div> - </ErrorBoundary> - ); - } - return ( <ErrorBoundary> <div @@ -930,10 +626,10 @@ function App(props: RouteComponentProps) { const width = window.innerWidth; const height = window.innerHeight; if ( - VIS_PADDING < top && - top < height - VIS_PADDING && - VIS_PADDING < left && - left < width - VIS_PADDING + VIS_PADDING.top < top && + top < height - VIS_PADDING.top && + VIS_PADDING.left < left && + left < width - VIS_PADDING.left ) { setMouseOnVis(true); } else { @@ -1258,9 +954,9 @@ function App(props: RouteComponentProps) { id="gosling-panel" className="gosling-panel" style={{ - width: `calc(100% - ${VIS_PADDING * 2}px)`, - height: `calc(100% - ${VIS_PADDING * 2}px)`, - padding: VIS_PADDING + width: `calc(100% - ${VIS_PADDING.left * 2}px)`, + height: `calc(100% - ${VIS_PADDING.top * 2}px)`, + padding: `${VIS_PADDING.top}px ${VIS_PADDING.right}px ${VIS_PADDING.bottom}px ${VIS_PADDING.left}px` }} > {goslingComponent} @@ -1292,55 +988,72 @@ function App(props: RouteComponentProps) { ? '#00000000' : 'lightgray' }`, - top: VIS_PADDING, - left: VIS_PADDING, + top: VIS_PADDING.top, + left: VIS_PADDING.left, opacity: 0.9, zIndex: 2, pointerEvents: interactiveMode ? 'none' : 'auto' }} /> + {isMinimalMode ? ( + <div + className="navigation-buttons" + style={{ + position: 'fixed', + height: '500px', + width: '500px', + zIndex: 997 + }} + > + <button + onClick={() => { + setTimeout( + () => + document + .getElementById('gosling-panel') + ?.scrollTo({ top: 0, behavior: 'smooth' }), + 0 + ); + }} + > + Circular View + </button> + <button + onClick={() => { + setTimeout( + () => + document + .getElementById('gosling-panel') + ?.scrollTo({ top: 1000, behavior: 'smooth' }), + 0 + ); + }} + > + Linear View + </button> + </div> + ) : null} <div style={{ pointerEvents: 'none', width: '100%', height: '100%', position: 'relative', - zIndex: 998 + zIndex: 997 }} > - <select - style={{ - pointerEvents: 'auto', - top: '3px' - }} - className="nav-dropdown" - onChange={e => { - setShowSamples(false); - const chr = e.currentTarget.value; - setTimeout(() => setOverviewChr(chr), 300); - }} - value={overviewChr} - disabled={!showOverview} - > - {[WHOLE_CHROMOSOME_STR, ...CHROMOSOMES].map(chr => { - return ( - <option key={chr} value={chr}> - {chr} - </option> - ); - })} - </select> <img src={legend} style={{ position: 'absolute', right: '3px', top: '3px', - zIndex: 998, + zIndex: 997, width: '120px' }} /> <select + id="linear-view" style={{ pointerEvents: 'auto', // !! This should be identical to how the height of circos determined. @@ -1516,7 +1229,7 @@ function App(props: RouteComponentProps) { ? 'visible' : 'collapse', position: 'absolute', - right: `${VIS_PADDING}px`, + right: `${VIS_PADDING.right}px`, top: '60px', background: 'lightgray', color: 'black', @@ -1549,7 +1262,7 @@ function App(props: RouteComponentProps) { background: 'none', position: 'absolute', bottom: 20, - left: VIS_PADDING, + left: VIS_PADDING.left, pointerEvents: 'none', visibility: demo.bam ? 'collapse' : 'visible' }} From 702ac68a54e7461b476c6e4f14665b1744a67895 Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla <crf85@cornell.edu> Date: Tue, 26 Mar 2024 11:47:06 -0400 Subject: [PATCH 04/16] style: navigation button styles --- src/App.css | 32 ++++++++++++++++++++++++++++++++ src/App.tsx | 30 +++++++++++++----------------- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/App.css b/src/App.css index 533e4501..497cf0ac 100644 --- a/src/App.css +++ b/src/App.css @@ -230,6 +230,38 @@ a:hover { border-radius: 0px; } +.navigation-buttons { + position: fixed; + z-index: 998; + display: flex; + margin-top: 20px; + margin-left: 25px; +} + +.navigation-button { + cursor: pointer; + font-size: 16px; + height: 38px; + width: 140px; + padding: 2px 10px; + border: 1px solid lightgrey; + box-shadow: #0000001a 5px 5px 10px; +} + +.navigation-button:hover { + background: lightgrey; +} +.navigation-button:active { + background: rgb(197, 197, 197); +} + +.navigation-button-circular { + border-radius: 8px 0px 0px 8px; +} +.navigation-button-linear { + border-radius: 0px 8px 8px 0px; +} + .zoom-in-button, .zoom-out-button, .zoom-left-button, diff --git a/src/App.tsx b/src/App.tsx index 0acad18d..c603e9a6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -996,17 +996,11 @@ function App(props: RouteComponentProps) { }} /> {isMinimalMode ? ( - <div - className="navigation-buttons" - style={{ - position: 'fixed', - height: '500px', - width: '500px', - zIndex: 997 - }} - > + <div className="navigation-buttons"> <button - onClick={() => { + className=" navigation-button navigation-button-circular" + onClick={e => { + console.log(e); setTimeout( () => document @@ -1019,14 +1013,16 @@ function App(props: RouteComponentProps) { Circular View </button> <button + className="navigation-button navigation-button-linear" onClick={() => { - setTimeout( - () => - document - .getElementById('gosling-panel') - ?.scrollTo({ top: 1000, behavior: 'smooth' }), - 0 - ); + setTimeout(() => { + const scroll_height = document.getElementById('gosling-panel').scrollHeight; + console.log(scroll_height); + document + .getElementById('gosling-panel') + ?.scrollTo({ top: scroll_height, behavior: 'smooth' }), + 0; + }); }} > Linear View From 399f5bae243a84d28318358e142b8962f8c0f02e Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla <crf85@cornell.edu> Date: Tue, 26 Mar 2024 19:34:31 -0400 Subject: [PATCH 05/16] -style: interaction styles for buttons -feat: add export buttons --- src/App.css | 19 +- src/App.tsx | 731 +++++++++++++++++++++++++++------------------------- 2 files changed, 395 insertions(+), 355 deletions(-) diff --git a/src/App.css b/src/App.css index 497cf0ac..4048997b 100644 --- a/src/App.css +++ b/src/App.css @@ -224,6 +224,7 @@ a:hover { border: 1px solid grey; position: absolute; left: 3px; + scroll-margin-top: 50px; } .nav-dropdown:focus { @@ -234,8 +235,6 @@ a:hover { position: fixed; z-index: 998; display: flex; - margin-top: 20px; - margin-left: 25px; } .navigation-button { @@ -248,17 +247,20 @@ a:hover { box-shadow: #0000001a 5px 5px 10px; } -.navigation-button:hover { +.navigation-button:hover:not(:disabled) { background: lightgrey; } -.navigation-button:active { +.navigation-button:active:not(:disabled) { background: rgb(197, 197, 197); } +.navigation-button:disabled:hover { + cursor: default; +} -.navigation-button-circular { +.navigation-button:first-of-type { border-radius: 8px 0px 0px 8px; } -.navigation-button-linear { +.navigation-button:last-of-type { border-radius: 0px 8px 8px 0px; } @@ -542,6 +544,11 @@ a:hover { /* background: #ffffff99; */ } +.sample-label.minimal-mode { + left: 300px; + top: 8px; +} + .menu-title svg { vertical-align: middle; } diff --git a/src/App.tsx b/src/App.tsx index c603e9a6..57af6275 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -48,11 +48,12 @@ function App(props: RouteComponentProps) { // URL parameters const urlParams = new URLSearchParams(props.location.search); const isMinimalMode = urlParams.get('minimal_mode'); + const [currentSection, setCurrentSection] = useState('circular'); const VIS_PADDING = { - top: 60, - right: isMinimalMode ? 20 : 60, + top: isMinimalMode ? 0 : 60, + right: isMinimalMode ? 0 : 60, bottom: isMinimalMode ? 0 : 60, - left: isMinimalMode ? 20 : 60 + left: isMinimalMode ? 0 : 60 }; // !! instead of using `urlParams.get('external')`, we directly parse the external URL in order to include @@ -625,15 +626,17 @@ function App(props: RouteComponentProps) { const left = e.clientX; const width = window.innerWidth; const height = window.innerHeight; - if ( - VIS_PADDING.top < top && - top < height - VIS_PADDING.top && - VIS_PADDING.left < left && - left < width - VIS_PADDING.left - ) { - setMouseOnVis(true); - } else { - setMouseOnVis(false); + if (!isMinimalMode) { + if ( + VIS_PADDING.top < top && + top < height - VIS_PADDING.top && + VIS_PADDING.left < left && + left < width - VIS_PADDING.left + ) { + setMouseOnVis(true); + } else { + setMouseOnVis(false); + } } mousePos.current = { x: left, y: top }; }} @@ -644,55 +647,63 @@ function App(props: RouteComponentProps) { setJumpButtonInfo(undefined); }} > - <span - style={{ - height: '50px', - width: '100%', - background: 'white', - position: 'absolute', - zIndex: 999, - opacity: 0.8 - }} - ></span> - <svg - className="config-button" - viewBox="0 0 16 16" - visibility={showSmallMultiples ? 'visible' : 'collapse'} - onClick={() => { - setShowSamples(true); - }} - > - <title>Menu</title> - <path - fillRule="evenodd" - d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z" - /> - </svg> - <div className="sample-label"> - <a className="chromoscope-title" href="./"> - CHROMOSCOPE - </a> - <a - className="title-about-link" + {!isMinimalMode && ( + <span + style={{ + height: '50px', + width: '100%', + background: 'white', + position: 'absolute', + zIndex: 999, + opacity: 0.8 + }} + ></span> + )} + {!isMinimalMode && ( + <svg + className="config-button" + viewBox="0 0 16 16" + visibility={showSmallMultiples ? 'visible' : 'collapse'} onClick={() => { - setShowAbout(true); + setShowSamples(true); }} > - <svg - xmlns="http://www.w3.org/2000/svg" - width="20" - height="20" - fill="currentColor" - viewBox="0 0 16 16" - > - <path d="M5.933.87a2.89 2.89 0 0 1 4.134 0l.622.638.89-.011a2.89 2.89 0 0 1 2.924 2.924l-.01.89.636.622a2.89 2.89 0 0 1 0 4.134l-.637.622.011.89a2.89 2.89 0 0 1-2.924 2.924l-.89-.01-.622.636a2.89 2.89 0 0 1-4.134 0l-.622-.637-.89.011a2.89 2.89 0 0 1-2.924-2.924l.01-.89-.636-.622a2.89 2.89 0 0 1 0-4.134l.637-.622-.011-.89a2.89 2.89 0 0 1 2.924-2.924l.89.01.622-.636zM7.002 11a1 1 0 1 0 2 0 1 1 0 0 0-2 0zm1.602-2.027c.04-.534.198-.815.846-1.26.674-.475 1.05-1.09 1.05-1.986 0-1.325-.92-2.227-2.262-2.227-1.02 0-1.792.492-2.1 1.29A1.71 1.71 0 0 0 6 5.48c0 .393.203.64.545.64.272 0 .455-.147.564-.51.158-.592.525-.915 1.074-.915.61 0 1.03.446 1.03 1.084 0 .563-.208.885-.822 1.325-.619.433-.926.914-.926 1.64v.111c0 .428.208.745.585.745.336 0 .504-.24.554-.627z" /> - </svg> - About - </a> - <span className="dimed">{' | '}</span> - {/* {demo.cancer.charAt(0).toUpperCase() + demo.cancer.slice(1) + ' • ' + demo.id} */} - {demo.cancer.charAt(0).toUpperCase() + demo.cancer.slice(1)} - <small>{demo.id}</small> + <title>Menu</title> + <path + fillRule="evenodd" + d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z" + /> + </svg> + )} + <div className={'sample-label' + (isMinimalMode ? ' minimal-mode' : '')}> + {!isMinimalMode && ( + <> + <a className="chromoscope-title" href="./"> + CHROMOSCOPE + </a> + <a + className="title-about-link" + onClick={() => { + setShowAbout(true); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + width="20" + height="20" + fill="currentColor" + viewBox="0 0 16 16" + > + <path d="M5.933.87a2.89 2.89 0 0 1 4.134 0l.622.638.89-.011a2.89 2.89 0 0 1 2.924 2.924l-.01.89.636.622a2.89 2.89 0 0 1 0 4.134l-.637.622.011.89a2.89 2.89 0 0 1-2.924 2.924l-.89-.01-.622.636a2.89 2.89 0 0 1-4.134 0l-.622-.637-.89.011a2.89 2.89 0 0 1-2.924-2.924l.01-.89-.636-.622a2.89 2.89 0 0 1 0-4.134l.637-.622-.011-.89a2.89 2.89 0 0 1 2.924-2.924l.89.01.622-.636zM7.002 11a1 1 0 1 0 2 0 1 1 0 0 0-2 0zm1.602-2.027c.04-.534.198-.815.846-1.26.674-.475 1.05-1.09 1.05-1.986 0-1.325-.92-2.227-2.262-2.227-1.02 0-1.792.492-2.1 1.29A1.71 1.71 0 0 0 6 5.48c0 .393.203.64.545.64.272 0 .455-.147.564-.51.158-.592.525-.915 1.074-.915.61 0 1.03.446 1.03 1.084 0 .563-.208.885-.822 1.325-.619.433-.926.914-.926 1.64v.111c0 .428.208.745.585.745.336 0 .504-.24.554-.627z" /> + </svg> + About + </a> + <span className="dimed">{' | '}</span> + {/* {demo.cancer.charAt(0).toUpperCase() + demo.cancer.slice(1) + ' • ' + demo.id} */} + {demo.cancer.charAt(0).toUpperCase() + demo.cancer.slice(1)} + <small>{demo.id}</small> + </> + )} <span className="title-btn" onClick={() => gosRef.current?.api.exportPng()}> <svg className="button" viewBox="0 0 16 16"> <title>Export Image</title> @@ -790,86 +801,10 @@ function App(props: RouteComponentProps) { ⚠️ Chromoscope is optimized for Google Chrome </a> ) : null} - <a - className="title-github-link" - href="https://github.com/hms-dbmi/chromoscope" - target="_blank" - rel="noreferrer" - > - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> - <title>GitHub</title> - <path - fill="currentColor" - d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" - ></path> - </svg> - GitHub - </a> - <a className="title-doc-link" href="https://chromoscope.bio/" target="_blank" rel="noreferrer"> - <svg - xmlns="http://www.w3.org/2000/svg" - width="20" - height="20" - fill="currentColor" - viewBox="0 0 16 16" - > - <path d="M12 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zM5 4h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1 0-1zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zM5 8h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1 0-1zm0 2h3a.5.5 0 0 1 0 1H5a.5.5 0 0 1 0-1z" /> - </svg> - Documentation - </a> - </div> - <div id="vis-panel" className="vis-panel"> - <div className={'vis-overview-panel ' + (!showSamples ? 'hide' : '')}> - <div - className="title" - onClick={e => { - if (e.target === e.currentTarget) setShowSamples(false); - }} - > - <svg - className="config-button" - viewBox="0 0 16 16" - onClick={() => { - setShowSamples(false); - }} - > - <title>Close</title> - <path - d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" - fill="currentColor" - ></path> - </svg> - <div className="sample-label"> - <b>CHROMOSCOPE</b> - <a - className="title-about-link" - onClick={() => { - setShowAbout(true); - }} - > - <svg - xmlns="http://www.w3.org/2000/svg" - width="20" - height="20" - fill="currentColor" - viewBox="0 0 16 16" - > - <path d="M5.933.87a2.89 2.89 0 0 1 4.134 0l.622.638.89-.011a2.89 2.89 0 0 1 2.924 2.924l-.01.89.636.622a2.89 2.89 0 0 1 0 4.134l-.637.622.011.89a2.89 2.89 0 0 1-2.924 2.924l-.89-.01-.622.636a2.89 2.89 0 0 1-4.134 0l-.622-.637-.89.011a2.89 2.89 0 0 1-2.924-2.924l.01-.89-.636-.622a2.89 2.89 0 0 1 0-4.134l.637-.622-.011-.89a2.89 2.89 0 0 1 2.924-2.924l.89.01.622-.636zM7.002 11a1 1 0 1 0 2 0 1 1 0 0 0-2 0zm1.602-2.027c.04-.534.198-.815.846-1.26.674-.475 1.05-1.09 1.05-1.986 0-1.325-.92-2.227-2.262-2.227-1.02 0-1.792.492-2.1 1.29A1.71 1.71 0 0 0 6 5.48c0 .393.203.64.545.64.272 0 .455-.147.564-.51.158-.592.525-.915 1.074-.915.61 0 1.03.446 1.03 1.084 0 .563-.208.885-.822 1.325-.619.433-.926.914-.926 1.64v.111c0 .428.208.745.585.745.336 0 .504-.24.554-.627z" /> - </svg> - About - </a> - <span className="dimed">{' | '}</span> Samples - <input - type="text" - className="sample-text-box" - placeholder="Search samples by ID" - onChange={e => setFilterSampleBy(e.target.value)} - hidden - /> - </div> + {!isMinimalMode && ( + <> <a className="title-github-link" - style={{ position: 'absolute' }} href="https://github.com/hms-dbmi/chromoscope" target="_blank" rel="noreferrer" @@ -888,7 +823,6 @@ function App(props: RouteComponentProps) { href="https://chromoscope.bio/" target="_blank" rel="noreferrer" - style={{ position: 'absolute' }} > <svg xmlns="http://www.w3.org/2000/svg" @@ -901,55 +835,143 @@ function App(props: RouteComponentProps) { </svg> Documentation </a> - <button - className="thumbnail-generate-button" - onClick={() => setGenerateThumbnails(!generateThumbnails)} - style={{ visibility: doneGeneratingThumbnails ? 'hidden' : 'visible' }} + </> + )} + </div> + <div id="vis-panel" className="vis-panel"> + {!isMinimalMode && ( + <div className={'vis-overview-panel ' + (!showSamples ? 'hide' : '')}> + <div + className="title" + onClick={e => { + if (e.target === e.currentTarget) setShowSamples(false); + }} > - {generateThumbnails ? 'Stop Generating Thumbnails' : 'Generate Missing Thumbnails'} - </button> - </div> - <div className="overview-root"> - <div className="overview-left"> - <CancerSelector - onChange={url => { - fetch(url).then(response => - response.text().then(d => { - let externalDemo = JSON.parse(d); - if (Array.isArray(externalDemo) && externalDemo.length >= 0) { - setFilteredSamples(externalDemo); - externalDemo = - externalDemo[ - demoIndex.current < externalDemo.length - ? demoIndex.current - : 0 - ]; - } - if (externalDemo) { - externalDemoUrl.current = url; - setDemo(externalDemo); - } - }) - ); - }} - /> - <HorizontalLine /> - <SampleConfigForm - onAdd={config => { - setFilteredSamples([ - { - ...config, - group: 'default' - }, - ...filteredSamples - ]); + <svg + className="config-button" + viewBox="0 0 16 16" + onClick={() => { + setShowSamples(false); }} - /> + > + <title>Close</title> + <path + d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" + fill="currentColor" + ></path> + </svg> + <div className="sample-label"> + <b>CHROMOSCOPE</b> + <a + className="title-about-link" + onClick={() => { + setShowAbout(true); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + width="20" + height="20" + fill="currentColor" + viewBox="0 0 16 16" + > + <path d="M5.933.87a2.89 2.89 0 0 1 4.134 0l.622.638.89-.011a2.89 2.89 0 0 1 2.924 2.924l-.01.89.636.622a2.89 2.89 0 0 1 0 4.134l-.637.622.011.89a2.89 2.89 0 0 1-2.924 2.924l-.89-.01-.622.636a2.89 2.89 0 0 1-4.134 0l-.622-.637-.89.011a2.89 2.89 0 0 1-2.924-2.924l.01-.89-.636-.622a2.89 2.89 0 0 1 0-4.134l.637-.622-.011-.89a2.89 2.89 0 0 1 2.924-2.924l.89.01.622-.636zM7.002 11a1 1 0 1 0 2 0 1 1 0 0 0-2 0zm1.602-2.027c.04-.534.198-.815.846-1.26.674-.475 1.05-1.09 1.05-1.986 0-1.325-.92-2.227-2.262-2.227-1.02 0-1.792.492-2.1 1.29A1.71 1.71 0 0 0 6 5.48c0 .393.203.64.545.64.272 0 .455-.147.564-.51.158-.592.525-.915 1.074-.915.61 0 1.03.446 1.03 1.084 0 .563-.208.885-.822 1.325-.619.433-.926.914-.926 1.64v.111c0 .428.208.745.585.745.336 0 .504-.24.554-.627z" /> + </svg> + About + </a> + <span className="dimed">{' | '}</span> Samples + <input + type="text" + className="sample-text-box" + placeholder="Search samples by ID" + onChange={e => setFilterSampleBy(e.target.value)} + hidden + /> + </div> + <a + className="title-github-link" + style={{ position: 'absolute' }} + href="https://github.com/hms-dbmi/chromoscope" + target="_blank" + rel="noreferrer" + > + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <title>GitHub</title> + <path + fill="currentColor" + d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" + ></path> + </svg> + GitHub + </a> + <a + className="title-doc-link" + href="https://chromoscope.bio/" + target="_blank" + rel="noreferrer" + style={{ position: 'absolute' }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + width="20" + height="20" + fill="currentColor" + viewBox="0 0 16 16" + > + <path d="M12 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zM5 4h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1 0-1zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zM5 8h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1 0-1zm0 2h3a.5.5 0 0 1 0 1H5a.5.5 0 0 1 0-1z" /> + </svg> + Documentation + </a> + <button + className="thumbnail-generate-button" + onClick={() => setGenerateThumbnails(!generateThumbnails)} + style={{ visibility: doneGeneratingThumbnails ? 'hidden' : 'visible' }} + > + {generateThumbnails ? 'Stop Generating Thumbnails' : 'Generate Missing Thumbnails'} + </button> + </div> + <div className="overview-root"> + <div className="overview-left"> + <CancerSelector + onChange={url => { + fetch(url).then(response => + response.text().then(d => { + let externalDemo = JSON.parse(d); + if (Array.isArray(externalDemo) && externalDemo.length >= 0) { + setFilteredSamples(externalDemo); + externalDemo = + externalDemo[ + demoIndex.current < externalDemo.length + ? demoIndex.current + : 0 + ]; + } + if (externalDemo) { + externalDemoUrl.current = url; + setDemo(externalDemo); + } + }) + ); + }} + /> + <HorizontalLine /> + <SampleConfigForm + onAdd={config => { + setFilteredSamples([ + { + ...config, + group: 'default' + }, + ...filteredSamples + ]); + }} + /> + </div> + <div className="overview-status">{`Total of ${filteredSamples.length} samples loaded`}</div> + <div className="overview-container">{smallOverviewWrapper}</div> </div> - <div className="overview-status">{`Total of ${filteredSamples.length} samples loaded`}</div> - <div className="overview-container">{smallOverviewWrapper}</div> </div> - </div> + )} <div id="gosling-panel" className="gosling-panel" @@ -975,32 +997,34 @@ function App(props: RouteComponentProps) { {jumpButtonInfo.direction === 'leftward' ? '←' : '→'} </button> ) : null} - <div - style={{ - width: '100%', - height: '100%', - boxShadow: `inset 0 0 0 3px ${ - interactiveMode && mouseOnVis - ? '#2399DB' - : !interactiveMode && mouseOnVis - ? 'lightgray' - : !interactiveMode && !mouseOnVis - ? '#00000000' - : 'lightgray' - }`, - top: VIS_PADDING.top, - left: VIS_PADDING.left, - opacity: 0.9, - zIndex: 2, - pointerEvents: interactiveMode ? 'none' : 'auto' - }} - /> + {!isMinimalMode && ( + <div + style={{ + width: '100%', + height: '100%', + boxShadow: `inset 0 0 0 3px ${ + interactiveMode && mouseOnVis + ? '#2399DB' + : !interactiveMode && mouseOnVis + ? 'lightgray' + : !interactiveMode && !mouseOnVis + ? '#00000000' + : 'lightgray' + }`, + top: VIS_PADDING.top, + left: VIS_PADDING.left, + opacity: 0.9, + zIndex: 2, + pointerEvents: interactiveMode ? 'none' : 'auto' + }} + /> + )} {isMinimalMode ? ( <div className="navigation-buttons"> <button - className=" navigation-button navigation-button-circular" - onClick={e => { - console.log(e); + className="navigation-button navigation-button-circular" + onClick={() => { + setCurrentSection('circular'); setTimeout( () => document @@ -1009,21 +1033,24 @@ function App(props: RouteComponentProps) { 0 ); }} + disabled={currentSection === 'circular'} > Circular View </button> <button className="navigation-button navigation-button-linear" onClick={() => { + setCurrentSection('linear'); setTimeout(() => { - const scroll_height = document.getElementById('gosling-panel').scrollHeight; - console.log(scroll_height); - document - .getElementById('gosling-panel') - ?.scrollTo({ top: scroll_height, behavior: 'smooth' }), + document.getElementById('linear-view')?.scrollIntoView({ + block: 'start', + inline: 'nearest', + behavior: 'smooth' + }), 0; }); }} + disabled={currentSection === 'linear'} > Linear View </button> @@ -1218,41 +1245,45 @@ function App(props: RouteComponentProps) { </div> </div> </div> - <div - style={{ - visibility: - ((!interactiveMode && !mouseOnVis) || (interactiveMode && mouseOnVis)) && !showSamples - ? 'visible' - : 'collapse', - position: 'absolute', - right: `${VIS_PADDING.right}px`, - top: '60px', - background: 'lightgray', - color: 'black', - padding: '6px', - pointerEvents: 'none', - zIndex: 9999, - boxShadow: '0 0 20px 2px rgba(0, 0, 0, 0.2)' - }} - > - {!interactiveMode - ? 'Click inside to use interactions on visualizations' - : 'Click outside to deactivate interactions and scroll the page'} - </div> - <div - style={{ - width: '100%', - height: '100%', - visibility: 'collapse', - boxShadow: interactiveMode ? 'inset 0 0 4px 2px #2399DB' : 'none', - zIndex: 9999, - background: 'none', - position: 'absolute', - top: 0, - left: 0, - pointerEvents: 'none' - }} - /> + {!isMinimalMode && ( + <div + style={{ + visibility: + ((!interactiveMode && !mouseOnVis) || (interactiveMode && mouseOnVis)) && !showSamples + ? 'visible' + : 'collapse', + position: 'absolute', + right: `${VIS_PADDING.right}px`, + top: '60px', + background: 'lightgray', + color: 'black', + padding: '6px', + pointerEvents: 'none', + zIndex: 9999, + boxShadow: '0 0 20px 2px rgba(0, 0, 0, 0.2)' + }} + > + {!interactiveMode + ? 'Click inside to use interactions on visualizations' + : 'Click outside to deactivate interactions and scroll the page'} + </div> + )} + {!isMinimalMode && ( + <div + style={{ + width: '100%', + height: '100%', + visibility: 'collapse', + boxShadow: interactiveMode ? 'inset 0 0 4px 2px #2399DB' : 'none', + zIndex: 9999, + background: 'none', + position: 'absolute', + top: 0, + left: 0, + pointerEvents: 'none' + }} + /> + )} <div style={{ background: 'none', @@ -1269,114 +1300,116 @@ function App(props: RouteComponentProps) { className={showAbout ? 'about-modal-container' : 'about-modal-container-hidden'} onClick={() => setShowAbout(false)} /> - <div className={showAbout ? 'about-modal' : 'about-modal-hidden'}> - <button className="about-modal-close-button" onClick={() => setShowAbout(false)}> - <svg - xmlns="http://www.w3.org/2000/svg" - width="30" - height="30" - viewBox="0 0 16 16" - strokeWidth="2" - stroke="none" - fill="currentColor" - strokeLinecap="round" - strokeLinejoin="round" - > - <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"></path> - </svg> - </button> - <p> - <b>Chromoscope</b> - <span className="dimed">{' | '}</span>About - </p> + {!isMinimalMode && ( + <div className={showAbout ? 'about-modal' : 'about-modal-hidden'}> + <button className="about-modal-close-button" onClick={() => setShowAbout(false)}> + <svg + xmlns="http://www.w3.org/2000/svg" + width="30" + height="30" + viewBox="0 0 16 16" + strokeWidth="2" + stroke="none" + fill="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + > + <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"></path> + </svg> + </button> + <p> + <b>Chromoscope</b> + <span className="dimed">{' | '}</span>About + </p> - <p> - Whole genome sequencing is now routinely used to profile mutations in DNA in the soma and in the - germline, informing molecular diagnoses of disease and therapeutic decisions. Structural - variants (SVs) are the main new type of alterations we see more of, and they are often - diagnostic, prognostic, or therapy-informing. However, the size and complexity of SV data, - combined with the difficulty of obtaining accurate SV calls, pose challenges in the - interpretation of SVs, requiring tedious visual inspection of potentially pathogenic variants - with multiple visualization tools. - </p> + <p> + Whole genome sequencing is now routinely used to profile mutations in DNA in the soma and in + the germline, informing molecular diagnoses of disease and therapeutic decisions. Structural + variants (SVs) are the main new type of alterations we see more of, and they are often + diagnostic, prognostic, or therapy-informing. However, the size and complexity of SV data, + combined with the difficulty of obtaining accurate SV calls, pose challenges in the + interpretation of SVs, requiring tedious visual inspection of potentially pathogenic + variants with multiple visualization tools. + </p> - <p> - To overcome the problems with the interpretation of SVs, we developed Chromoscope, an - open-source web-based application for the interactive visualization of structural variants. - Chromoscope has several innovative features which unlock the insights from whole genome - sequencing: visualization at multiple scale levels simultaneously, effective navigation across - scales, easy setup for loading users' large datasets, and a feature to export, share, and - further customize visualizations. We anticipate that Chromoscope will accelerate the exploration - and interpretation of SVs by a broad range of scientists and clinicians, leading to new insights - into genomic biomarkers. - </p> - <h4>Learn more about Chromoscope</h4> - <ul> - <li> - <b>GitHub:</b>{' '} - <a href="https://github.com/hms-dbmi/chromoscope" target="_blank" rel="noreferrer"> - https://github.com/hms-dbmi/chromoscope - </a> - </li> - <li> - <b>Documentation:</b>{' '} - <a href="https://chromoscope.bio/" target="_blank" rel="noreferrer"> - https://chromoscope.bio/ - </a> - </li> - <li> - <b>Preprint:</b>{' '} - <a href="https://osf.io/pyqrx/" target="_blank" rel="noreferrer"> - L'Yi et al. Chromoscope: interactive multiscale visualization for structural - variation in human genomes, OSF, 2023. + <p> + To overcome the problems with the interpretation of SVs, we developed Chromoscope, an + open-source web-based application for the interactive visualization of structural variants. + Chromoscope has several innovative features which unlock the insights from whole genome + sequencing: visualization at multiple scale levels simultaneously, effective navigation + across scales, easy setup for loading users' large datasets, and a feature to export, + share, and further customize visualizations. We anticipate that Chromoscope will accelerate + the exploration and interpretation of SVs by a broad range of scientists and clinicians, + leading to new insights into genomic biomarkers. + </p> + <h4>Learn more about Chromoscope</h4> + <ul> + <li> + <b>GitHub:</b>{' '} + <a href="https://github.com/hms-dbmi/chromoscope" target="_blank" rel="noreferrer"> + https://github.com/hms-dbmi/chromoscope + </a> + </li> + <li> + <b>Documentation:</b>{' '} + <a href="https://chromoscope.bio/" target="_blank" rel="noreferrer"> + https://chromoscope.bio/ + </a> + </li> + <li> + <b>Preprint:</b>{' '} + <a href="https://osf.io/pyqrx/" target="_blank" rel="noreferrer"> + L'Yi et al. Chromoscope: interactive multiscale visualization for structural + variation in human genomes, OSF, 2023. + </a> + </li> + </ul> + <h4>The Team</h4> + <ul> + <li> + <b>Sehi L'Yi</b> + {hidiveLabRef} + </li> + <li> + <b>Dominika Maziec</b> + {parkLabRef} + </li> + <li> + <b>Victoria Stevens</b> + {parkLabRef} + </li> + <li> + <b>Trevor Manz</b> + {hidiveLabRef} + </li> + <li> + <b>Alexander Veit</b> + {parkLabRef} + </li> + <li> + <b>Michele Berselli</b> + {parkLabRef} + </li> + <li> + <b>Peter J Park</b> + {parkLabRef} + </li> + <li> + <b>Dominik Glodzik</b> + {parkLabRef} + </li> + <li> + <b>Nils Gehlenborg</b> + {hidiveLabRef} + </li> + </ul> + <div className="about-modal-footer"> + <a href="https://dbmi.hms.harvard.edu/" target="_blank" rel="noreferrer"> + Department of Biomedical Informatics, Harvard Medical School </a> - </li> - </ul> - <h4>The Team</h4> - <ul> - <li> - <b>Sehi L'Yi</b> - {hidiveLabRef} - </li> - <li> - <b>Dominika Maziec</b> - {parkLabRef} - </li> - <li> - <b>Victoria Stevens</b> - {parkLabRef} - </li> - <li> - <b>Trevor Manz</b> - {hidiveLabRef} - </li> - <li> - <b>Alexander Veit</b> - {parkLabRef} - </li> - <li> - <b>Michele Berselli</b> - {parkLabRef} - </li> - <li> - <b>Peter J Park</b> - {parkLabRef} - </li> - <li> - <b>Dominik Glodzik</b> - {parkLabRef} - </li> - <li> - <b>Nils Gehlenborg</b> - {hidiveLabRef} - </li> - </ul> - <div className="about-modal-footer"> - <a href="https://dbmi.hms.harvard.edu/" target="_blank" rel="noreferrer"> - Department of Biomedical Informatics, Harvard Medical School - </a> + </div> </div> - </div> + )} <div className="move-to-top-btn" onClick={() => { From 7153266814c5fa29ee24caa59adf1825998ad6bb Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla <crf85@cornell.edu> Date: Tue, 2 Apr 2024 14:05:04 -0400 Subject: [PATCH 06/16] feat: keep buttons enabled/remove section state --- src/App.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 57af6275..b90cdd02 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -48,7 +48,6 @@ function App(props: RouteComponentProps) { // URL parameters const urlParams = new URLSearchParams(props.location.search); const isMinimalMode = urlParams.get('minimal_mode'); - const [currentSection, setCurrentSection] = useState('circular'); const VIS_PADDING = { top: isMinimalMode ? 0 : 60, right: isMinimalMode ? 0 : 60, @@ -1024,7 +1023,6 @@ function App(props: RouteComponentProps) { <button className="navigation-button navigation-button-circular" onClick={() => { - setCurrentSection('circular'); setTimeout( () => document @@ -1033,14 +1031,12 @@ function App(props: RouteComponentProps) { 0 ); }} - disabled={currentSection === 'circular'} > Circular View </button> <button className="navigation-button navigation-button-linear" onClick={() => { - setCurrentSection('linear'); setTimeout(() => { document.getElementById('linear-view')?.scrollIntoView({ block: 'start', @@ -1050,7 +1046,6 @@ function App(props: RouteComponentProps) { 0; }); }} - disabled={currentSection === 'linear'} > Linear View </button> From b1ebccb2a71847e38de20daa5b3de02742090b76 Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla <crf85@cornell.edu> Date: Tue, 9 Apr 2024 14:07:55 -0400 Subject: [PATCH 07/16] fix: use boolean to track minimal mode activation --- src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index b90cdd02..97afe1af 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,7 +47,7 @@ const allDrivers = [ function App(props: RouteComponentProps) { // URL parameters const urlParams = new URLSearchParams(props.location.search); - const isMinimalMode = urlParams.get('minimal_mode'); + const isMinimalMode = urlParams.get('minimal_mode') === 'true'; const VIS_PADDING = { top: isMinimalMode ? 0 : 60, right: isMinimalMode ? 0 : 60, From 92476815366d2dd3bb14c89f59d892a6c51f8377 Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla <crf85@cornell.edu> Date: Mon, 29 Apr 2024 12:51:15 -0400 Subject: [PATCH 08/16] feat: add triangle down icon --- src/icon.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/icon.ts b/src/icon.ts index 32d46f52..b8fd31ea 100644 --- a/src/icon.ts +++ b/src/icon.ts @@ -333,5 +333,16 @@ export const ICONS: Record<string, ICON_INFO> = { ], stroke: 'currentColor', fill: 'none' + }, + TRIANGLE_DOWN: { + width: 11, + height: 7, + viewBox: '0 0 11 7', + path: [ + 'M0.5 1H10.5L5.5 6L0.5 1Z', + 'M5.5 6L0.5 1H10.5L5.5 6ZM5.5 6V5.28571' + ], + stroke: 'currentColor', + fill: 'none' } }; From e07a17ffb0cc2231ca80142ddbb1f31f10ca7dab Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla <crf85@cornell.edu> Date: Mon, 29 Apr 2024 12:57:36 -0400 Subject: [PATCH 09/16] -feat: Dropdown component for export buttons -feat: intersection observer for hiding legend -style: styles for minimal mode buttons and spacing --- src/App.css | 242 ++++++++++++++++++++++++++++++++------ src/App.tsx | 220 ++++++++++++++++++++-------------- src/ui/ExportDropdown.tsx | 70 +++++++++++ 3 files changed, 405 insertions(+), 127 deletions(-) create mode 100644 src/ui/ExportDropdown.tsx diff --git a/src/App.css b/src/App.css index 4048997b..94d7a74d 100644 --- a/src/App.css +++ b/src/App.css @@ -224,46 +224,13 @@ a:hover { border: 1px solid grey; position: absolute; left: 3px; - scroll-margin-top: 50px; + scroll-margin-top: 100px; } .nav-dropdown:focus { border-radius: 0px; } -.navigation-buttons { - position: fixed; - z-index: 998; - display: flex; -} - -.navigation-button { - cursor: pointer; - font-size: 16px; - height: 38px; - width: 140px; - padding: 2px 10px; - border: 1px solid lightgrey; - box-shadow: #0000001a 5px 5px 10px; -} - -.navigation-button:hover:not(:disabled) { - background: lightgrey; -} -.navigation-button:active:not(:disabled) { - background: rgb(197, 197, 197); -} -.navigation-button:disabled:hover { - cursor: default; -} - -.navigation-button:first-of-type { - border-radius: 8px 0px 0px 8px; -} -.navigation-button:last-of-type { - border-radius: 0px 8px 8px 0px; -} - .zoom-in-button, .zoom-out-button, .zoom-left-button, @@ -544,11 +511,6 @@ a:hover { /* background: #ffffff99; */ } -.sample-label.minimal-mode { - left: 300px; - top: 8px; -} - .menu-title svg { vertical-align: middle; } @@ -786,3 +748,205 @@ a:hover { opacity: 1; background-color: #7aaded; } + +/* Minimal Mode styles */ +.minimal_mode { + + .gosling-panel { + overflow-y: scroll; + overflow-x: hidden; + } + + .sample-label { + left: 300px; + top: 8px; + } + + .navigation-buttons { + position: fixed; + z-index: 998; + display: flex; + flex-direction: column; + top: 3px; + left: 3px; + } + + .navigation-button { + background-color: #F6F6F6; + cursor: pointer; + font-size: 1rem; + font-family: Inter; + height: 40px; + width: 210px; + padding: 2px 10px; + border: 1px solid #D3D3D3; + } + + .navigation-button:hover:not(:disabled) { + background-color: #EBEBEB; + } + .navigation-button:active:not(:disabled) { + background-color: #e6e4e4; + } + .navigation-button:first-of-type { + border-radius: 8px 8px 0px 0px; + } + .navigation-button:last-of-type { + border-radius: 0px 0px 8px 8px; + } + + /* Force scrollbar to show */ + ::-webkit-scrollbar { + -webkit-appearance: none; + width: 10px; + } + + ::-webkit-scrollbar-thumb { + width: 10px; + border-radius: 4px; + background-color: rgba(0, 0, 0, .5); + box-shadow: 0 0 1px rgba(255, 255, 255, .5); + } + ::-webkit-scrollbar:hover { + cursor: pointer; + } + + /* Styles for the navigation on the right side of visualization */ + .external-links { + position: absolute; + z-index: 998; + height: auto; + width: auto; + top: 3px; + right: 12px; + + .external-links-nav { + display: flex; + flex-direction: column; + justify-content: space-between; + + + .open-in-chromoscope-link { + background-color: #F6F6F6; + font-size: 0.9rem; + font-family: Inter; + font-weight: 400; + display: flex; + height: 35px; + justify-content: center; + border: 1px solid #D3D3D3; + border-radius: 8px; + + .link-group { + margin: auto; + + .external-link-icon { + margin: auto; + margin-left: 8px; + fill: black; + stroke: black; + } + } + + } + + .open-in-chromoscope-link:hover { + text-decoration: none; + cursor: pointer; + background-color: #EBEBEB; + } + + .open-in-chromoscope-link:active { + background-color: #e6e4e4; + } + + .export-links { + border-radius: 4px; + margin-top: 4px; + + .export-dropdown { + height: auto; + background-color: #F6F6F6; + right: 0px; + border-radius: 8px; + border: 1px solid #D3D3D3; + transition: all 100ms; + overflow: hidden; + + .export-button { + width: 210px; + height: 35px; + border-radius: inherit; + border: 0px solid; + font-weight: 400; + background-color: transparent; + + .export-title { + font-size: .9rem; + font-family: Inter; + } + + .button.triangle-down { + width: 11px; + height: 7px; + margin-left: 8px; + } + } + + .export-button:hover { + cursor: pointer; + background-color: #EBEBEB; + } + + .export-button:active { + background-color: #e6e4e4; + } + + .nav-list { + list-style-type: none; + padding: 0px 10px; + display: flex; + flex-direction: row; + height: 50px; + background-color: white; + margin: 0px 8px 8px 8px; + border-radius: 3px; + + .nav-list-item { + display: flex; + margin: auto; + } + + + .title-btn { + display: flex; + position: relative; + width: 25px; + height: 25px; + margin-left: 0px; + } + + .title-btn.png { + padding: 0px; + border: none; + background-color: transparent; + } + } + + } + + .export-dropdown.open { + background-color: #EBEBEB; + border-radius: 8px; + border: 1px solid #C3C3C3; + transition: all 100ms; + + .export-button { + border: none; + } + } + } + } + + } +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 97afe1af..4ee18cad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,6 +22,7 @@ import HorizontalLine from './ui/horizontal-line'; import SampleConfigForm from './ui/sample-config-form'; import { BrowserDatabase } from './browser-log'; import legend from './legend.png'; +import { ExportDropdown } from './ui/ExportDropdown'; const db = new Database(); const log = new BrowserDatabase(); @@ -102,7 +103,7 @@ function App(props: RouteComponentProps) { const [showOverview, setShowOverview] = useState(true); const [showPutativeDriver, setShowPutativeDriver] = useState(true); const [interactiveMode, setInteractiveMode] = useState(isMinimalMode ?? false); - const [visPanelWidth, setVisPanelWidth] = useState(INIT_VIS_PANEL_WIDTH - VIS_PADDING.left * 2); + const [visPanelWidth, setVisPanelWidth] = useState(INIT_VIS_PANEL_WIDTH - ( isMinimalMode ? 10 : VIS_PADDING.left * 2) ); const [overviewChr, setOverviewChr] = useState(''); const [genomeViewChr, setGenomeViewChr] = useState(''); const [drivers, setDrivers] = useState( @@ -304,7 +305,7 @@ function App(props: RouteComponentProps) { } }, [genomeViewChr]); - // change the width of the visualization panel + // change the width of the visualization panel and register intersection observer useEffect(() => { window.addEventListener( 'resize', @@ -312,6 +313,23 @@ function App(props: RouteComponentProps) { setVisPanelWidth(window.innerWidth - VIS_PADDING.left * 2); }, 500) ); + + // In minimal mode, lower opacity of legend image as circular view + // moves out of the screen + if (isMinimalMode) { + const legendElement = document.querySelector<HTMLElement>(".circular-view-legend"); + let options = { + root: document.querySelector(".minimal_mode"), + rootMargin: "-250px 0px 0px 0px", + threshold: [1, 0.5, 0.25, 0], + }; + + let observer = new IntersectionObserver((entry) => { + legendElement.style.opacity = "" + entry[0].intersectionRatio ** 2; + }, options); + + observer.observe(legendElement); + } }, []); const getThumbnail = (d: SampleType) => { @@ -619,6 +637,7 @@ function App(props: RouteComponentProps) { return ( <ErrorBoundary> <div + className={isMinimalMode ? "minimal_mode" : ""} style={{ width: '100%', height: '100%' }} onMouseMove={e => { const top = e.clientY; @@ -674,7 +693,7 @@ function App(props: RouteComponentProps) { /> </svg> )} - <div className={'sample-label' + (isMinimalMode ? ' minimal-mode' : '')}> + <div className="sample-label"> {!isMinimalMode && ( <> <a className="chromoscope-title" href="./"> @@ -703,90 +722,94 @@ function App(props: RouteComponentProps) { <small>{demo.id}</small> </> )} - <span className="title-btn" onClick={() => gosRef.current?.api.exportPng()}> - <svg className="button" viewBox="0 0 16 16"> - <title>Export Image</title> - {ICONS.PNG.path.map(p => ( - <path fill="currentColor" key={p} d={p} /> - ))} - </svg> - </span> - <span - className="title-btn" - onClick={() => { - const a = document.createElement('a'); - a.setAttribute( - 'href', - `data:text/plain;charset=utf-8,${encodeURIComponent( - getHtmlTemplate(currentSpec.current) - )}` - ); - a.download = 'visualization.html'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - }} - style={{ marginLeft: 40 }} - > - <svg className="button" viewBox="0 0 16 16"> - <title>Export HTML</title> - {ICONS.HTML.path.map(p => ( - <path fill="currentColor" key={p} d={p} /> - ))} - </svg> - </span> - <span - className="title-btn" - onClick={() => { - const a = document.createElement('a'); - a.setAttribute( - 'href', - `data:text/plain;charset=utf-8,${encodeURIComponent(currentSpec.current)}` - ); - a.download = 'visualization.json'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - }} - style={{ marginLeft: 70 }} - > - <svg className="button" viewBox="0 0 16 16"> - <title>Export Gosling Spec (JSON)</title> - {ICONS.JSON.path.map(p => ( - <path fill="currentColor" key={p} d={p} /> - ))} - </svg> - </span> - <span - className="title-btn" - onClick={() => { - const { xDomain } = gosRef.current.hgApi.api.getLocation(`${demo.id}-mid-ideogram`); - if (xDomain) { - // urlParams.set('demoIndex', demoIndex.current + ''); - // urlParams.set('domain', xDomain.join('-')); - let newUrl = window.location.origin + window.location.pathname + '?'; - newUrl += `demoIndex=${demoIndex.current}`; - newUrl += `&domain=${xDomain.join('-')}`; - if (externalDemoUrl.current) { - newUrl += `&external=${externalDemoUrl.current}`; - } else if (externalUrl) { - newUrl += `&external=${externalUrl}`; - } - navigator.clipboard - .writeText(newUrl) - .then(() => - alert('The URL of the current session has been copied to your clipboard.') + { !isMinimalMode && ( + <> + <span className="title-btn" onClick={() => gosRef.current?.api.exportPng()}> + <svg className="button" viewBox="0 0 16 16"> + <title>Export Image</title> + {ICONS.PNG.path.map(p => ( + <path fill="currentColor" key={p} d={p} /> + ))} + </svg> + </span> + <span + className="title-btn" + onClick={() => { + const a = document.createElement('a'); + a.setAttribute( + 'href', + `data:text/plain;charset=utf-8,${encodeURIComponent( + getHtmlTemplate(currentSpec.current) + )}` ); - } - }} - style={{ marginLeft: 100 }} - > - <svg className="button" viewBox="0 0 16 16"> - <title>Export Link</title> - <path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1.002 1.002 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4.018 4.018 0 0 1-.128-1.287z" /> - <path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243L6.586 4.672z" /> - </svg> - </span> + a.download = 'visualization.html'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }} + style={{ marginLeft: 40 }} + > + <svg className="button" viewBox="0 0 16 16"> + <title>Export HTML</title> + {ICONS.HTML.path.map(p => ( + <path fill="currentColor" key={p} d={p} /> + ))} + </svg> + </span> + <span + className="title-btn" + onClick={() => { + const a = document.createElement('a'); + a.setAttribute( + 'href', + `data:text/plain;charset=utf-8,${encodeURIComponent(currentSpec.current)}` + ); + a.download = 'visualization.json'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }} + style={{ marginLeft: 70 }} + > + <svg className="button" viewBox="0 0 16 16"> + <title>Export Gosling Spec (JSON)</title> + {ICONS.JSON.path.map(p => ( + <path fill="currentColor" key={p} d={p} /> + ))} + </svg> + </span> + <span + className="title-btn" + onClick={() => { + const { xDomain } = gosRef.current.hgApi.api.getLocation(`${demo.id}-mid-ideogram`); + if (xDomain) { + // urlParams.set('demoIndex', demoIndex.current + ''); + // urlParams.set('domain', xDomain.join('-')); + let newUrl = window.location.origin + window.location.pathname + '?'; + newUrl += `demoIndex=${demoIndex.current}`; + newUrl += `&domain=${xDomain.join('-')}`; + if (externalDemoUrl.current) { + newUrl += `&external=${externalDemoUrl.current}`; + } else if (externalUrl) { + newUrl += `&external=${externalUrl}`; + } + navigator.clipboard + .writeText(newUrl) + .then(() => + alert('The URL of the current session has been copied to your clipboard.') + ); + } + }} + style={{ marginLeft: 100 }} + > + <svg className="button" viewBox="0 0 16 16"> + <title>Export Link</title> + <path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1.002 1.002 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4.018 4.018 0 0 1-.128-1.287z" /> + <path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243L6.586 4.672z" /> + </svg> + </span> + </> + )} {!isChrome() ? ( <a style={{ @@ -1051,6 +1074,26 @@ function App(props: RouteComponentProps) { </button> </div> ) : null} + { + // External links and export buttons + isMinimalMode ? ( + <div className="external-links"> + <nav className="external-links-nav"> + <a className="open-in-chromoscope-link" href=""> + <div className="link-group"> + <span>Open in Chromoscope</span> + <svg className="external-link-icon" width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M9.8212 1.73104L10.6894 0.875H9.47015H7.66727C7.55064 0.875 7.46966 0.784774 7.46966 0.6875C7.46966 0.590226 7.55064 0.5 7.66727 0.5H11.1553C11.2719 0.5 11.3529 0.590226 11.3529 0.6875V4.125C11.3529 4.22227 11.2719 4.3125 11.1553 4.3125C11.0387 4.3125 10.9577 4.22228 10.9577 4.125V2.34824V1.15307L10.1067 1.9922L5.71834 6.31907C5.71831 6.3191 5.71828 6.31913 5.71825 6.31916C5.64039 6.39579 5.51053 6.39576 5.43271 6.31907C5.35892 6.24635 5.35892 6.1308 5.43271 6.05808L5.4328 6.05799L9.8212 1.73104ZM1.19116 2.40625C1.19116 1.73964 1.74085 1.1875 2.43519 1.1875H4.87682C4.99345 1.1875 5.07443 1.27773 5.07443 1.375C5.07443 1.47227 4.99345 1.5625 4.87682 1.5625H2.43519C1.97411 1.5625 1.58638 1.93419 1.58638 2.40625V9.28125C1.58638 9.75331 1.97411 10.125 2.43519 10.125H9.41129C9.87237 10.125 10.2601 9.75331 10.2601 9.28125V6.875C10.2601 6.77773 10.3411 6.6875 10.4577 6.6875C10.5743 6.6875 10.6553 6.77773 10.6553 6.875V9.28125C10.6553 9.94786 10.1056 10.5 9.41129 10.5H2.43519C1.74085 10.5 1.19116 9.94786 1.19116 9.28125V2.40625Z" fill="black" stroke="black"/> + </svg> + </div> + </a> + <div className="export-links"> + <ExportDropdown gosRef={gosRef} currentSpec={currentSpec} /> + </div> + </nav> + </div> + ) : null + } <div style={{ pointerEvents: 'none', @@ -1061,11 +1104,12 @@ function App(props: RouteComponentProps) { }} > <img + className="circular-view-legend" src={legend} style={{ position: 'absolute', - right: '3px', - top: '3px', + right: isMinimalMode ? '10px' : '3px', + top: isMinimalMode ? '425px' : '3px', zIndex: 997, width: '120px' }} diff --git a/src/ui/ExportDropdown.tsx b/src/ui/ExportDropdown.tsx new file mode 100644 index 00000000..368d0ec6 --- /dev/null +++ b/src/ui/ExportDropdown.tsx @@ -0,0 +1,70 @@ +import React, {useState, useEffect} from "react"; +import { ICONS } from "../icon"; +import { getHtmlTemplate } from "../html-template"; + + + +const ExportButton = ({ title, icon }) => { + return ( + <svg className="button" viewBox="0 0 16 16"> + <title>{title}</title> + {ICONS[icon].path.map(p => ( + <path fill="currentColor" key={p} d={p} /> + ))} + </svg> + ) +} + +export const ExportDropdown = ({ gosRef, currentSpec }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + <div + className={"export-dropdown" + (isOpen ? " open" : " closed")} + onClick={() => setIsOpen(!isOpen)} + aria-expanded={isOpen} + > + <button className="export-button"> + <span className="export-title">Export</span> + <svg className="button triangle-down" viewBox={ICONS.TRIANGLE_DOWN.viewBox}> + <title>Triange Down</title> + {ICONS.TRIANGLE_DOWN.path.map(p => ( + <path fill="currentColor" key={p} d={p} /> + ))} + </svg> + </button> + {isOpen ? <nav className="export-options"> + <ul className="nav-list"> + <li className="nav-list-item"> + <button className="title-btn png" onClick={(e) =>{ e.stopPropagation(); gosRef.current?.api.exportPng() }}> + <ExportButton title="Export PNG" icon="PNG" /> + </button> + </li> + + <li className="nav-list-item"> + <a + className="title-btn" + href={`data:text/plain;charset=utf-8,${encodeURIComponent( + getHtmlTemplate(currentSpec.current) + )}`} + download="visualization.html" + onClick={(e) =>{ e.stopPropagation() }} + > + <ExportButton title="Export HTML" icon="HTML" /> + </a> + </li> + <li className="nav-list-item"> + <a + className="title-btn" + href={`data:text/plain;charset=utf-8,${encodeURIComponent(currentSpec.current)}`} + download="visualization.json" + onClick={(e) =>{ e.stopPropagation() }} + > + <ExportButton title="Export JSON" icon="JSON" /> + </a> + </li> + </ul> + </nav> : null} + </div> + ); +} \ No newline at end of file From f51d78af20880e44dfda5dcbe343ab1c2258a2ea Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla <crf85@cornell.edu> Date: Tue, 30 Apr 2024 14:58:13 -0400 Subject: [PATCH 10/16] feat: add spacing argument to generateSpec() --- src/main-spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main-spec.ts b/src/main-spec.ts index 63720449..d580920f 100644 --- a/src/main-spec.ts +++ b/src/main-spec.ts @@ -19,10 +19,11 @@ export interface SpecOption extends SampleType { svReads: { name: string; type: string }[]; crossChr: boolean; bpIntervals: [number, number, number, number] | undefined; + spacing: number; } function generateSpec(opt: SpecOption): GoslingSpec { - const { assembly, id, bam, bai, width, selectedSvId, breakpoints, bpIntervals } = opt; + const { assembly, id, bam, bai, width, selectedSvId, breakpoints, bpIntervals, spacing } = opt; const topViewWidth = Math.min(width, 600); const midViewWidth = width; @@ -39,7 +40,7 @@ function generateSpec(opt: SpecOption): GoslingSpec { arrangement: 'vertical', centerRadius: 0.5, assembly, - spacing: 40, + spacing, style: { outlineWidth: 1, outline: 'lightgray', From a566fe928a7a8d371980653c6c7f2f6988733b18 Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla <crf85@cornell.edu> Date: Tue, 30 Apr 2024 14:59:18 -0400 Subject: [PATCH 11/16] -feat: pass spacing variable -feat: reformat the linear view controls (add container) -style: adjut spacing of controls --- src/App.css | 61 ++++++++- src/App.tsx | 347 +++++++++++++++++++++++++++------------------------- 2 files changed, 239 insertions(+), 169 deletions(-) diff --git a/src/App.css b/src/App.css index 94d7a74d..a1c9d9f3 100644 --- a/src/App.css +++ b/src/App.css @@ -224,7 +224,7 @@ a:hover { border: 1px solid grey; position: absolute; left: 3px; - scroll-margin-top: 100px; + scroll-margin-top: 50px; } .nav-dropdown:focus { @@ -749,6 +749,58 @@ a:hover { background-color: #7aaded; } +/* Styles for the linear view controls */ +.linear-view-controls { + position: absolute; + display: flex; + flex-direction: row; + height: 30px; + justify-content: center; + left: 50%; + transform: translate(-50%, 0px); + + .chromosome-select { + position: relative; + height: auto; + } + .gene-search { + position: relative; + left: 0px; + padding: 0px 0px 0px 10px; + width: auto; + height: auto; + display: flex; + + svg { + position: relative; + top: auto; + left: auto; + margin: auto; + margin-left: 0px; + } + input { + position: relative; + left: 0; + margin-left: 0; + border: none; + width: 175px; + } + } + .directional-controls { + display: flex; + margin: auto 0px auto 16px; + gap: 16px; + .control-group { + .control { + position: relative; + left: 0px; + margin-left: 0px; + line-height: 28px; + } + } + } +} + /* Minimal Mode styles */ .minimal_mode { @@ -949,4 +1001,11 @@ a:hover { } } + + .linear-view-controls { + .chromosome-select { + left: 0px; + } + } + } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 4ee18cad..4e36f1ae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -513,7 +513,8 @@ function App(props: RouteComponentProps) { breakpoints: breakpoints, crossChr: false, bpIntervals, - svReads + svReads, + spacing: isMinimalMode ? 100 : 40 }); currentSpec.current = JSON.stringify(spec); // console.log('spec', spec); @@ -1114,173 +1115,183 @@ function App(props: RouteComponentProps) { width: '120px' }} /> - <select - id="linear-view" - style={{ - pointerEvents: 'auto', - // !! This should be identical to how the height of circos determined. - top: `${Math.min(visPanelWidth, 600)}px` - }} - className="nav-dropdown" - onChange={e => { - setShowSamples(false); - const chr = e.currentTarget.value; - setTimeout(() => setGenomeViewChr(chr), 300); - }} - value={genomeViewChr} - disabled={!showOverview} - > - {CHROMOSOMES.map(chr => { - return ( - <option key={chr} value={chr}> - {chr} - </option> - ); - })} - </select> - <svg - className="gene-search-icon" - viewBox="0 0 16 16" - style={{ - top: `${Math.min(visPanelWidth, 600) + 6}px` - // visibility: demo.assembly === 'hg38' ? 'visible' : 'hidden' - }} - > - <path - fillRule="evenodd" - d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z" - /> - </svg> - <input - type="text" - className="gene-search" - placeholder="Search Gene (e.g., MYC)" - // alt={demo.assembly === 'hg38' ? 'Search Gene' : 'Not currently available for this assembly.'} - style={{ - pointerEvents: 'auto', - top: `${Math.min(visPanelWidth, 600)}px` - // cursor: demo.assembly === 'hg38' ? 'auto' : 'not-allowed', - // visibility: demo.assembly === 'hg38' ? 'visible' : 'hidden' - }} - // disabled={demo.assembly === 'hg38' ? false : true} - // onChange={(e) => { - // const keyword = e.target.value; - // if(keyword !== "" && !keyword.startsWith("c")) { - // gosRef.current.api.suggestGene(keyword, (suggestions) => { - // setGeneSuggestions(suggestions); - // }); - // setSuggestionPosition({ - // left: searchBoxRef.current.getBoundingClientRect().left, - // top: searchBoxRef.current.getBoundingClientRect().top + searchBoxRef.current.getBoundingClientRect().height, - // }); - // } else { - // setGeneSuggestions([]); - // } - // setSearchKeyword(keyword); - // }} - onKeyDown={e => { - const keyword = (e.target as HTMLTextAreaElement).value; - switch (e.key) { - case 'ArrowUp': - break; - case 'ArrowDown': - break; - case 'Enter': - // https://github.com/gosling-lang/gosling.js/blob/7555ab711023a0c3e2076a448756a9ba3eeb04f7/src/core/api.ts#L156 - gosRef.current.hgApi.api.zoomToGene( - `${demo.id}-mid-ideogram`, - keyword, - 10000, - 1000 - ); - break; - case 'Esc': - case 'Escape': - break; - } - }} - /> - <button - style={{ - pointerEvents: 'auto', - // !! This should be identical to how the height of circos determined. - top: `${Math.min(visPanelWidth, 600)}px` - }} - className="zoom-in-button" - onClick={e => { - const trackId = `${demo.id}-mid-ideogram`; - const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; - if (end - start < 100) return; - const delta = (end - start) / 3.0; - gosRef.current.api.zoomTo( - trackId, - `chr1:${start + delta}-${end - delta}`, - 0, - ZOOM_DURATION - ); - }} - > - + - </button> - <button - style={{ - pointerEvents: 'auto', - // !! This should be identical to how the height of circos determined. - top: `${Math.min(visPanelWidth, 600)}px` - }} - className="zoom-out-button" - onClick={e => { - const trackId = `${demo.id}-mid-ideogram`; - const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; - const delta = (end - start) / 2.0; - gosRef.current.api.zoomTo(trackId, `chr1:${start}-${end}`, delta, ZOOM_DURATION); - }} - > - - - </button> - <button - style={{ - pointerEvents: 'auto', - // !! This should be identical to how the height of circos determined. - top: `${Math.min(visPanelWidth, 600)}px` - }} - className="zoom-left-button" - onClick={e => { - const trackId = `${demo.id}-mid-ideogram`; - const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; - if (end - start < 100) return; - const delta = (end - start) / 4.0; - gosRef.current.api.zoomTo( - trackId, - `chr1:${start - delta}-${end - delta}`, - 0, - ZOOM_DURATION - ); - }} - > - ← - </button> - <button - style={{ - pointerEvents: 'auto', - // !! This should be identical to how the height of circos determined. - top: `${Math.min(visPanelWidth, 600)}px` - }} - className="zoom-right-button" - onClick={e => { - const trackId = `${demo.id}-mid-ideogram`; - const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; - const delta = (end - start) / 4.0; - gosRef.current.api.zoomTo( - trackId, - `chr1:${start + delta}-${end + delta}`, - 0, - ZOOM_DURATION - ); - }} - > - → - </button> + <div className="linear-view-controls" style={{ top: `${ Math.min(visPanelWidth, isMinimalMode ? 650 : 600)}px` }}> + <select + id="linear-view" + style={{ + pointerEvents: 'auto', + // !! This should be identical to how the height of circos determined. + // top: `${Math.min(visPanelWidth, 600)}px` + }} + className="nav-dropdown chromosome-select" + onChange={e => { + setShowSamples(false); + const chr = e.currentTarget.value; + setTimeout(() => setGenomeViewChr(chr), 300); + }} + value={genomeViewChr} + disabled={!showOverview} + > + {CHROMOSOMES.map(chr => { + return ( + <option key={chr} value={chr}> + {chr} + </option> + ); + })} + </select> + <div className="gene-search"> + <svg + className="gene-search-icon" + viewBox="0 0 16 16" + style={{ + // top: `${Math.min(visPanelWidth, 600) + 6}px` + // visibility: demo.assembly === 'hg38' ? 'visible' : 'hidden' + }} + > + <path + fillRule="evenodd" + d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z" + /> + </svg> + <input + type="text" + className="gene-search" + placeholder="Search Gene (e.g., MYC)" + // alt={demo.assembly === 'hg38' ? 'Search Gene' : 'Not currently available for this assembly.'} + style={{ + pointerEvents: 'auto', + // top: `${Math.min(visPanelWidth, 600)}px` + // cursor: demo.assembly === 'hg38' ? 'auto' : 'not-allowed', + // visibility: demo.assembly === 'hg38' ? 'visible' : 'hidden' + }} + // disabled={demo.assembly === 'hg38' ? false : true} + // onChange={(e) => { + // const keyword = e.target.value; + // if(keyword !== "" && !keyword.startsWith("c")) { + // gosRef.current.api.suggestGene(keyword, (suggestions) => { + // setGeneSuggestions(suggestions); + // }); + // setSuggestionPosition({ + // left: searchBoxRef.current.getBoundingClientRect().left, + // top: searchBoxRef.current.getBoundingClientRect().top + searchBoxRef.current.getBoundingClientRect().height, + // }); + // } else { + // setGeneSuggestions([]); + // } + // setSearchKeyword(keyword); + // }} + onKeyDown={e => { + const keyword = (e.target as HTMLTextAreaElement).value; + switch (e.key) { + case 'ArrowUp': + break; + case 'ArrowDown': + break; + case 'Enter': + // https://github.com/gosling-lang/gosling.js/blob/7555ab711023a0c3e2076a448756a9ba3eeb04f7/src/core/api.ts#L156 + gosRef.current.hgApi.api.zoomToGene( + `${demo.id}-mid-ideogram`, + keyword, + 10000, + 1000 + ); + break; + case 'Esc': + case 'Escape': + break; + } + }} + /> + </div> + <div className='directional-controls'> + <div className='control-group zoom'> + <button + style={{ + pointerEvents: 'auto', + // !! This should be identical to how the height of circos determined. + // top: `${Math.min(visPanelWidth, 600)}px` + }} + className="zoom-in-button control" + onClick={e => { + const trackId = `${demo.id}-mid-ideogram`; + const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; + if (end - start < 100) return; + const delta = (end - start) / 3.0; + gosRef.current.api.zoomTo( + trackId, + `chr1:${start + delta}-${end - delta}`, + 0, + ZOOM_DURATION + ); + }} + > + + + </button> + <button + style={{ + pointerEvents: 'auto', + // !! This should be identical to how the height of circos determined. + // top: `${Math.min(visPanelWidth, 600)}px` + }} + className="zoom-out-button control" + onClick={e => { + const trackId = `${demo.id}-mid-ideogram`; + const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; + const delta = (end - start) / 2.0; + gosRef.current.api.zoomTo(trackId, `chr1:${start}-${end}`, delta, ZOOM_DURATION); + }} + > + - + </button> + </div> + <div className='control-group pan'> + <button + style={{ + pointerEvents: 'auto', + // !! This should be identical to how the height of circos determined. + // top: `${Math.min(visPanelWidth, 600)}px` + }} + className="zoom-left-button control" + onClick={e => { + const trackId = `${demo.id}-mid-ideogram`; + const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; + if (end - start < 100) return; + const delta = (end - start) / 4.0; + gosRef.current.api.zoomTo( + trackId, + `chr1:${start - delta}-${end - delta}`, + 0, + ZOOM_DURATION + ); + }} + > + ← + </button> + <button + style={{ + pointerEvents: 'auto', + // !! This should be identical to how the height of circos determined. + // top: `${Math.min(visPanelWidth, 600)}px` + }} + className="zoom-right-button control" + onClick={e => { + const trackId = `${demo.id}-mid-ideogram`; + const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; + const delta = (end - start) / 4.0; + gosRef.current.api.zoomTo( + trackId, + `chr1:${start + delta}-${end + delta}`, + 0, + ZOOM_DURATION + ); + }} + > + → + </button> + </div> + </div> + </div> </div> </div> </div> From bacfad8b75926c6f2790ead6456dc4edc3e0076a Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla <crf85@cornell.edu> Date: Tue, 30 Apr 2024 15:37:43 -0400 Subject: [PATCH 12/16] -style: correct styling for placeholder input -style: center controls only in minimal mode --- src/App.css | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/App.css b/src/App.css index a1c9d9f3..8f200363 100644 --- a/src/App.css +++ b/src/App.css @@ -756,25 +756,26 @@ a:hover { flex-direction: row; height: 30px; justify-content: center; - left: 50%; - transform: translate(-50%, 0px); + left: 3px; .chromosome-select { position: relative; height: auto; + left: 0px; } .gene-search { position: relative; left: 0px; - padding: 0px 0px 0px 10px; + padding: 0px; width: auto; height: auto; display: flex; svg { - position: relative; - top: auto; - left: auto; + position: absolute; + top: 50%; + transform: translate(0px, -50%); + left: 8px; margin: auto; margin-left: 0px; } @@ -783,7 +784,8 @@ a:hover { left: 0; margin-left: 0; border: none; - width: 175px; + width: 180px; + padding-left: 35px; } } .directional-controls { @@ -791,6 +793,7 @@ a:hover { margin: auto 0px auto 16px; gap: 16px; .control-group { + display: flex; .control { position: relative; left: 0px; @@ -1003,9 +1006,9 @@ a:hover { } .linear-view-controls { - .chromosome-select { - left: 0px; - } + left: 50%; + transform: translate(-50%, 0px); + } } \ No newline at end of file From 49395f386dc5b7ca6585edee6ae90a158dfb2115 Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla <crf85@cornell.edu> Date: Wed, 8 May 2024 18:08:08 -0400 Subject: [PATCH 13/16] -feat: export dropdown -chore: adjust opacity calculation for legend -chore: update title for navigation buttons --- src/App.css | 68 ++++++++++------------ src/App.tsx | 117 ++++++++++++++++++++++++-------------- src/icon.ts | 5 +- src/ui/ExportDropdown.tsx | 102 ++++++++++++++++++++------------- 4 files changed, 165 insertions(+), 127 deletions(-) diff --git a/src/App.css b/src/App.css index 8f200363..a33deb9f 100644 --- a/src/App.css +++ b/src/App.css @@ -749,15 +749,15 @@ a:hover { background-color: #7aaded; } -/* Styles for the linear view controls */ -.linear-view-controls { +/* Styles for the variant view controls */ +.variant-view-controls { position: absolute; display: flex; flex-direction: row; height: 30px; justify-content: center; left: 3px; - + .chromosome-select { position: relative; height: auto; @@ -806,7 +806,6 @@ a:hover { /* Minimal Mode styles */ .minimal_mode { - .gosling-panel { overflow-y: scroll; overflow-x: hidden; @@ -827,18 +826,18 @@ a:hover { } .navigation-button { - background-color: #F6F6F6; + background-color: #f6f6f6; cursor: pointer; font-size: 1rem; font-family: Inter; height: 40px; width: 210px; padding: 2px 10px; - border: 1px solid #D3D3D3; + border: 1px solid #d3d3d3; } .navigation-button:hover:not(:disabled) { - background-color: #EBEBEB; + background-color: #ebebeb; } .navigation-button:active:not(:disabled) { background-color: #e6e4e4; @@ -859,8 +858,8 @@ a:hover { ::-webkit-scrollbar-thumb { width: 10px; border-radius: 4px; - background-color: rgba(0, 0, 0, .5); - box-shadow: 0 0 1px rgba(255, 255, 255, .5); + background-color: rgba(0, 0, 0, 0.5); + box-shadow: 0 0 1px rgba(255, 255, 255, 0.5); } ::-webkit-scrollbar:hover { cursor: pointer; @@ -879,17 +878,16 @@ a:hover { display: flex; flex-direction: column; justify-content: space-between; - .open-in-chromoscope-link { - background-color: #F6F6F6; + background-color: #f6f6f6; font-size: 0.9rem; font-family: Inter; font-weight: 400; display: flex; height: 35px; justify-content: center; - border: 1px solid #D3D3D3; + border: 1px solid #d3d3d3; border-radius: 8px; .link-group { @@ -902,32 +900,31 @@ a:hover { stroke: black; } } - } .open-in-chromoscope-link:hover { text-decoration: none; cursor: pointer; - background-color: #EBEBEB; + background-color: #ebebeb; } - + .open-in-chromoscope-link:active { background-color: #e6e4e4; } - + .export-links { border-radius: 4px; margin-top: 4px; .export-dropdown { height: auto; - background-color: #F6F6F6; + background-color: #f6f6f6; right: 0px; border-radius: 8px; - border: 1px solid #D3D3D3; + border: 1px solid #d3d3d3; transition: all 100ms; overflow: hidden; - + .export-button { width: 210px; height: 35px; @@ -935,22 +932,22 @@ a:hover { border: 0px solid; font-weight: 400; background-color: transparent; - + .export-title { - font-size: .9rem; + font-size: 0.9rem; font-family: Inter; } - + .button.triangle-down { width: 11px; height: 7px; margin-left: 8px; } } - + .export-button:hover { cursor: pointer; - background-color: #EBEBEB; + background-color: #ebebeb; } .export-button:active { @@ -966,13 +963,12 @@ a:hover { background-color: white; margin: 0px 8px 8px 8px; border-radius: 3px; - + .nav-list-item { display: flex; margin: auto; } - - + .title-btn { display: flex; position: relative; @@ -980,35 +976,31 @@ a:hover { height: 25px; margin-left: 0px; } - + .title-btn.png { padding: 0px; border: none; background-color: transparent; } } - } - + .export-dropdown.open { - background-color: #EBEBEB; + background-color: #ebebeb; border-radius: 8px; - border: 1px solid #C3C3C3; + border: 1px solid #c3c3c3; transition: all 100ms; - + .export-button { border: none; } } } } - } - .linear-view-controls { + .variant-view-controls { left: 50%; transform: translate(-50%, 0px); - } - -} \ No newline at end of file +} diff --git a/src/App.tsx b/src/App.tsx index 4e36f1ae..bd9b3230 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -103,7 +103,9 @@ function App(props: RouteComponentProps) { const [showOverview, setShowOverview] = useState(true); const [showPutativeDriver, setShowPutativeDriver] = useState(true); const [interactiveMode, setInteractiveMode] = useState(isMinimalMode ?? false); - const [visPanelWidth, setVisPanelWidth] = useState(INIT_VIS_PANEL_WIDTH - ( isMinimalMode ? 10 : VIS_PADDING.left * 2) ); + const [visPanelWidth, setVisPanelWidth] = useState( + INIT_VIS_PANEL_WIDTH - (isMinimalMode ? 10 : VIS_PADDING.left * 2) + ); const [overviewChr, setOverviewChr] = useState(''); const [genomeViewChr, setGenomeViewChr] = useState(''); const [drivers, setDrivers] = useState( @@ -313,21 +315,21 @@ function App(props: RouteComponentProps) { setVisPanelWidth(window.innerWidth - VIS_PADDING.left * 2); }, 500) ); - - // In minimal mode, lower opacity of legend image as circular view - // moves out of the screen + + // Lower opacity of legend image as it leaves viewport if (isMinimalMode) { - const legendElement = document.querySelector<HTMLElement>(".circular-view-legend"); - let options = { - root: document.querySelector(".minimal_mode"), - rootMargin: "-250px 0px 0px 0px", - threshold: [1, 0.5, 0.25, 0], + const legendElement = document.querySelector<HTMLElement>('.genome-view-legend'); + const options = { + root: document.querySelector('.minimal_mode'), + rootMargin: '-250px 0px 0px 0px', + threshold: [0.9, 0.75, 0.5, 0.25, 0] }; - - let observer = new IntersectionObserver((entry) => { - legendElement.style.opacity = "" + entry[0].intersectionRatio ** 2; + + const observer = new IntersectionObserver(entry => { + // Set intersection ratio as opacity (round up to one decimal place) + legendElement.style.opacity = '' + Math.ceil(10 * entry[0].intersectionRatio) / 10; }, options); - + observer.observe(legendElement); } }, []); @@ -638,7 +640,7 @@ function App(props: RouteComponentProps) { return ( <ErrorBoundary> <div - className={isMinimalMode ? "minimal_mode" : ""} + className={isMinimalMode ? 'minimal_mode' : ''} style={{ width: '100%', height: '100%' }} onMouseMove={e => { const top = e.clientY; @@ -723,7 +725,7 @@ function App(props: RouteComponentProps) { <small>{demo.id}</small> </> )} - { !isMinimalMode && ( + {!isMinimalMode && ( <> <span className="title-btn" onClick={() => gosRef.current?.api.exportPng()}> <svg className="button" viewBox="0 0 16 16"> @@ -797,7 +799,9 @@ function App(props: RouteComponentProps) { navigator.clipboard .writeText(newUrl) .then(() => - alert('The URL of the current session has been copied to your clipboard.') + alert( + 'The URL of the current session has been copied to your clipboard.' + ) ); } }} @@ -1056,13 +1060,13 @@ function App(props: RouteComponentProps) { ); }} > - Circular View + Genome View </button> <button - className="navigation-button navigation-button-linear" + className="navigation-button navigation-button-variant" onClick={() => { setTimeout(() => { - document.getElementById('linear-view')?.scrollIntoView({ + document.getElementById('variant-view')?.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth' @@ -1071,7 +1075,7 @@ function App(props: RouteComponentProps) { }); }} > - Linear View + Variant View </button> </div> ) : null} @@ -1083,8 +1087,19 @@ function App(props: RouteComponentProps) { <a className="open-in-chromoscope-link" href=""> <div className="link-group"> <span>Open in Chromoscope</span> - <svg className="external-link-icon" width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M9.8212 1.73104L10.6894 0.875H9.47015H7.66727C7.55064 0.875 7.46966 0.784774 7.46966 0.6875C7.46966 0.590226 7.55064 0.5 7.66727 0.5H11.1553C11.2719 0.5 11.3529 0.590226 11.3529 0.6875V4.125C11.3529 4.22227 11.2719 4.3125 11.1553 4.3125C11.0387 4.3125 10.9577 4.22228 10.9577 4.125V2.34824V1.15307L10.1067 1.9922L5.71834 6.31907C5.71831 6.3191 5.71828 6.31913 5.71825 6.31916C5.64039 6.39579 5.51053 6.39576 5.43271 6.31907C5.35892 6.24635 5.35892 6.1308 5.43271 6.05808L5.4328 6.05799L9.8212 1.73104ZM1.19116 2.40625C1.19116 1.73964 1.74085 1.1875 2.43519 1.1875H4.87682C4.99345 1.1875 5.07443 1.27773 5.07443 1.375C5.07443 1.47227 4.99345 1.5625 4.87682 1.5625H2.43519C1.97411 1.5625 1.58638 1.93419 1.58638 2.40625V9.28125C1.58638 9.75331 1.97411 10.125 2.43519 10.125H9.41129C9.87237 10.125 10.2601 9.75331 10.2601 9.28125V6.875C10.2601 6.77773 10.3411 6.6875 10.4577 6.6875C10.5743 6.6875 10.6553 6.77773 10.6553 6.875V9.28125C10.6553 9.94786 10.1056 10.5 9.41129 10.5H2.43519C1.74085 10.5 1.19116 9.94786 1.19116 9.28125V2.40625Z" fill="black" stroke="black"/> + <svg + className="external-link-icon" + width="12" + height="11" + viewBox="0 0 12 11" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M9.8212 1.73104L10.6894 0.875H9.47015H7.66727C7.55064 0.875 7.46966 0.784774 7.46966 0.6875C7.46966 0.590226 7.55064 0.5 7.66727 0.5H11.1553C11.2719 0.5 11.3529 0.590226 11.3529 0.6875V4.125C11.3529 4.22227 11.2719 4.3125 11.1553 4.3125C11.0387 4.3125 10.9577 4.22228 10.9577 4.125V2.34824V1.15307L10.1067 1.9922L5.71834 6.31907C5.71831 6.3191 5.71828 6.31913 5.71825 6.31916C5.64039 6.39579 5.51053 6.39576 5.43271 6.31907C5.35892 6.24635 5.35892 6.1308 5.43271 6.05808L5.4328 6.05799L9.8212 1.73104ZM1.19116 2.40625C1.19116 1.73964 1.74085 1.1875 2.43519 1.1875H4.87682C4.99345 1.1875 5.07443 1.27773 5.07443 1.375C5.07443 1.47227 4.99345 1.5625 4.87682 1.5625H2.43519C1.97411 1.5625 1.58638 1.93419 1.58638 2.40625V9.28125C1.58638 9.75331 1.97411 10.125 2.43519 10.125H9.41129C9.87237 10.125 10.2601 9.75331 10.2601 9.28125V6.875C10.2601 6.77773 10.3411 6.6875 10.4577 6.6875C10.5743 6.6875 10.6553 6.77773 10.6553 6.875V9.28125C10.6553 9.94786 10.1056 10.5 9.41129 10.5H2.43519C1.74085 10.5 1.19116 9.94786 1.19116 9.28125V2.40625Z" + fill="black" + stroke="black" + /> </svg> </div> </a> @@ -1105,21 +1120,24 @@ function App(props: RouteComponentProps) { }} > <img - className="circular-view-legend" + className="genome-view-legend" src={legend} style={{ position: 'absolute', right: isMinimalMode ? '10px' : '3px', - top: isMinimalMode ? '425px' : '3px', + top: isMinimalMode ? '350px' : '3px', zIndex: 997, width: '120px' }} /> - <div className="linear-view-controls" style={{ top: `${ Math.min(visPanelWidth, isMinimalMode ? 650 : 600)}px` }}> + <div + className="variant-view-controls" + style={{ top: `${Math.min(visPanelWidth, isMinimalMode ? 650 : 600)}px` }} + > <select - id="linear-view" + id="variant-view" style={{ - pointerEvents: 'auto', + pointerEvents: 'auto' // !! This should be identical to how the height of circos determined. // top: `${Math.min(visPanelWidth, 600)}px` }} @@ -1144,10 +1162,12 @@ function App(props: RouteComponentProps) { <svg className="gene-search-icon" viewBox="0 0 16 16" - style={{ - // top: `${Math.min(visPanelWidth, 600) + 6}px` - // visibility: demo.assembly === 'hg38' ? 'visible' : 'hidden' - }} + style={ + { + // top: `${Math.min(visPanelWidth, 600) + 6}px` + // visibility: demo.assembly === 'hg38' ? 'visible' : 'hidden' + } + } > <path fillRule="evenodd" @@ -1160,7 +1180,7 @@ function App(props: RouteComponentProps) { placeholder="Search Gene (e.g., MYC)" // alt={demo.assembly === 'hg38' ? 'Search Gene' : 'Not currently available for this assembly.'} style={{ - pointerEvents: 'auto', + pointerEvents: 'auto' // top: `${Math.min(visPanelWidth, 600)}px` // cursor: demo.assembly === 'hg38' ? 'auto' : 'not-allowed', // visibility: demo.assembly === 'hg38' ? 'visible' : 'hidden' @@ -1204,18 +1224,19 @@ function App(props: RouteComponentProps) { }} /> </div> - <div className='directional-controls'> - <div className='control-group zoom'> + <div className="directional-controls"> + <div className="control-group zoom"> <button style={{ - pointerEvents: 'auto', + pointerEvents: 'auto' // !! This should be identical to how the height of circos determined. // top: `${Math.min(visPanelWidth, 600)}px` }} className="zoom-in-button control" onClick={e => { const trackId = `${demo.id}-mid-ideogram`; - const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; + const [start, end] = + gosRef.current?.hgApi.api.getLocation(trackId).xDomain; if (end - start < 100) return; const delta = (end - start) / 3.0; gosRef.current.api.zoomTo( @@ -1230,32 +1251,39 @@ function App(props: RouteComponentProps) { </button> <button style={{ - pointerEvents: 'auto', + pointerEvents: 'auto' // !! This should be identical to how the height of circos determined. // top: `${Math.min(visPanelWidth, 600)}px` }} className="zoom-out-button control" onClick={e => { const trackId = `${demo.id}-mid-ideogram`; - const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; + const [start, end] = + gosRef.current?.hgApi.api.getLocation(trackId).xDomain; const delta = (end - start) / 2.0; - gosRef.current.api.zoomTo(trackId, `chr1:${start}-${end}`, delta, ZOOM_DURATION); + gosRef.current.api.zoomTo( + trackId, + `chr1:${start}-${end}`, + delta, + ZOOM_DURATION + ); }} > - </button> </div> - <div className='control-group pan'> + <div className="control-group pan"> <button style={{ - pointerEvents: 'auto', + pointerEvents: 'auto' // !! This should be identical to how the height of circos determined. // top: `${Math.min(visPanelWidth, 600)}px` }} className="zoom-left-button control" onClick={e => { const trackId = `${demo.id}-mid-ideogram`; - const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; + const [start, end] = + gosRef.current?.hgApi.api.getLocation(trackId).xDomain; if (end - start < 100) return; const delta = (end - start) / 4.0; gosRef.current.api.zoomTo( @@ -1270,14 +1298,15 @@ function App(props: RouteComponentProps) { </button> <button style={{ - pointerEvents: 'auto', + pointerEvents: 'auto' // !! This should be identical to how the height of circos determined. // top: `${Math.min(visPanelWidth, 600)}px` }} className="zoom-right-button control" onClick={e => { const trackId = `${demo.id}-mid-ideogram`; - const [start, end] = gosRef.current?.hgApi.api.getLocation(trackId).xDomain; + const [start, end] = + gosRef.current?.hgApi.api.getLocation(trackId).xDomain; const delta = (end - start) / 4.0; gosRef.current.api.zoomTo( trackId, diff --git a/src/icon.ts b/src/icon.ts index b8fd31ea..76a7a041 100644 --- a/src/icon.ts +++ b/src/icon.ts @@ -338,10 +338,7 @@ export const ICONS: Record<string, ICON_INFO> = { width: 11, height: 7, viewBox: '0 0 11 7', - path: [ - 'M0.5 1H10.5L5.5 6L0.5 1Z', - 'M5.5 6L0.5 1H10.5L5.5 6ZM5.5 6V5.28571' - ], + path: ['M0.5 1H10.5L5.5 6L0.5 1Z', 'M5.5 6L0.5 1H10.5L5.5 6ZM5.5 6V5.28571'], stroke: 'currentColor', fill: 'none' } diff --git a/src/ui/ExportDropdown.tsx b/src/ui/ExportDropdown.tsx index 368d0ec6..188e20d0 100644 --- a/src/ui/ExportDropdown.tsx +++ b/src/ui/ExportDropdown.tsx @@ -1,10 +1,13 @@ -import React, {useState, useEffect} from "react"; -import { ICONS } from "../icon"; -import { getHtmlTemplate } from "../html-template"; +import React, { useState, useEffect } from 'react'; +import { ICONS } from '../icon'; +import { getHtmlTemplate } from '../html-template'; +type ExportButtonProps = { + title: string; + icon: string; +}; - -const ExportButton = ({ title, icon }) => { +const ExportButton = ({ title, icon }: ExportButtonProps) => { return ( <svg className="button" viewBox="0 0 16 16"> <title>{title}</title> @@ -12,15 +15,20 @@ const ExportButton = ({ title, icon }) => { <path fill="currentColor" key={p} d={p} /> ))} </svg> - ) -} + ); +}; + +type ExportDropdownProps = { + gosRef: React.RefObject<any>; + currentSpec: React.MutableRefObject<string>; +}; -export const ExportDropdown = ({ gosRef, currentSpec }) => { +export const ExportDropdown = ({ gosRef, currentSpec }: ExportDropdownProps) => { const [isOpen, setIsOpen] = useState(false); return ( - <div - className={"export-dropdown" + (isOpen ? " open" : " closed")} + <div + className={'export-dropdown' + (isOpen ? ' open' : ' closed')} onClick={() => setIsOpen(!isOpen)} aria-expanded={isOpen} > @@ -33,38 +41,50 @@ export const ExportDropdown = ({ gosRef, currentSpec }) => { ))} </svg> </button> - {isOpen ? <nav className="export-options"> - <ul className="nav-list"> - <li className="nav-list-item"> - <button className="title-btn png" onClick={(e) =>{ e.stopPropagation(); gosRef.current?.api.exportPng() }}> - <ExportButton title="Export PNG" icon="PNG" /> - </button> - </li> + {isOpen ? ( + <nav className="export-options"> + <ul className="nav-list"> + <li className="nav-list-item"> + <button + className="title-btn png" + onClick={e => { + e.stopPropagation(); + gosRef.current?.api.exportPng(); + }} + > + <ExportButton title="Export PNG" icon="PNG" /> + </button> + </li> - <li className="nav-list-item"> - <a - className="title-btn" - href={`data:text/plain;charset=utf-8,${encodeURIComponent( - getHtmlTemplate(currentSpec.current) - )}`} - download="visualization.html" - onClick={(e) =>{ e.stopPropagation() }} - > - <ExportButton title="Export HTML" icon="HTML" /> - </a> - </li> - <li className="nav-list-item"> - <a - className="title-btn" - href={`data:text/plain;charset=utf-8,${encodeURIComponent(currentSpec.current)}`} - download="visualization.json" - onClick={(e) =>{ e.stopPropagation() }} - > - <ExportButton title="Export JSON" icon="JSON" /> - </a> - </li> + <li className="nav-list-item"> + <a + className="title-btn" + href={`data:text/plain;charset=utf-8,${encodeURIComponent( + getHtmlTemplate(currentSpec.current) + )}`} + download="visualization.html" + onClick={e => { + e.stopPropagation(); + }} + > + <ExportButton title="Export HTML" icon="HTML" /> + </a> + </li> + <li className="nav-list-item"> + <a + className="title-btn" + href={`data:text/plain;charset=utf-8,${encodeURIComponent(currentSpec.current)}`} + download="visualization.json" + onClick={e => { + e.stopPropagation(); + }} + > + <ExportButton title="Export JSON" icon="JSON" /> + </a> + </li> </ul> - </nav> : null} + </nav> + ) : null} </div> ); -} \ No newline at end of file +}; From 2c35c2309ee6c259f3a52591fdcc09dd8ba501a7 Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla <crf85@cornell.edu> Date: Wed, 8 May 2024 18:17:36 -0400 Subject: [PATCH 14/16] -feat: add open in chromoscope link button -fix: typo --- src/App.tsx | 27 +++++++++++++++++++++++++-- src/ui/ExportDropdown.tsx | 2 +- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index bd9b3230..431fb7c8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1084,7 +1084,30 @@ function App(props: RouteComponentProps) { isMinimalMode ? ( <div className="external-links"> <nav className="external-links-nav"> - <a className="open-in-chromoscope-link" href=""> + <button + className="open-in-chromoscope-link" + tabIndex={0} + onClick={e => { + e.preventDefault(); + const { xDomain } = gosRef.current.hgApi.api.getLocation( + `${demo.id}-mid-ideogram` + ); + if (xDomain) { + // urlParams.set('demoIndex', demoIndex.current + ''); + // urlParams.set('domain', xDomain.join('-')); + let newUrl = + window.location.origin + window.location.pathname + '?'; + newUrl += `demoIndex=${demoIndex.current}`; + newUrl += `&domain=${xDomain.join('-')}`; + if (externalDemoUrl.current) { + newUrl += `&external=${externalDemoUrl.current}`; + } else if (externalUrl) { + newUrl += `&external=${externalUrl}`; + } + window.open(newUrl, '_blank'); + } + }} + > <div className="link-group"> <span>Open in Chromoscope</span> <svg @@ -1102,7 +1125,7 @@ function App(props: RouteComponentProps) { /> </svg> </div> - </a> + </button> <div className="export-links"> <ExportDropdown gosRef={gosRef} currentSpec={currentSpec} /> </div> diff --git a/src/ui/ExportDropdown.tsx b/src/ui/ExportDropdown.tsx index 188e20d0..6bdbbcf4 100644 --- a/src/ui/ExportDropdown.tsx +++ b/src/ui/ExportDropdown.tsx @@ -35,7 +35,7 @@ export const ExportDropdown = ({ gosRef, currentSpec }: ExportDropdownProps) => <button className="export-button"> <span className="export-title">Export</span> <svg className="button triangle-down" viewBox={ICONS.TRIANGLE_DOWN.viewBox}> - <title>Triange Down</title> + <title>Triangle Down</title> {ICONS.TRIANGLE_DOWN.path.map(p => ( <path fill="currentColor" key={p} d={p} /> ))} From fabc541528cc7edd27ff31b4168789885ea4b046 Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla <crf85@cornell.edu> Date: Thu, 16 May 2024 12:07:54 -0400 Subject: [PATCH 15/16] fix: push track mouseover-menu to top z-index --- src/App.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/App.css b/src/App.css index a33deb9f..dfbfc295 100644 --- a/src/App.css +++ b/src/App.css @@ -17,6 +17,7 @@ body, 'Segoe UI Symbol'; height: 100%; overflow: hidden; + z-index: 0; } body { @@ -749,6 +750,10 @@ a:hover { background-color: #7aaded; } +.track-mouseover-menu { + z-index: 999; +} + /* Styles for the variant view controls */ .variant-view-controls { position: absolute; From 5c87b6cbc987ef03cd761c53a411d89db722c2d2 Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla <crf85@cornell.edu> Date: Thu, 16 May 2024 12:09:02 -0400 Subject: [PATCH 16/16] fix: remove typo --- src/App.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/App.css b/src/App.css index dfbfc295..03dd2148 100644 --- a/src/App.css +++ b/src/App.css @@ -17,7 +17,6 @@ body, 'Segoe UI Symbol'; height: 100%; overflow: hidden; - z-index: 0; } body {