@@ -179,20 +179,26 @@ const CustomSequenceLogo: React.FC<Props> = ({ fastaNames, folder }) => {
179179 // Map geneName → gpcrdb string array (indexed by alignment column, 0-based)
180180 const [ referenceMaps , setReferenceMaps ] = useState < Record < string , string [ ] > > ( { } ) ;
181181
182- // Computed array for current selection (order fixed by class mapping)
183- const classReferenceOrder = [ 'ClassA' , 'ClassT' , 'ClassB1' , 'ClassB2' , 'ClassC' , 'ClassF' , 'ClassOlf' , 'GP157' , 'GP143' ] as const ;
182+ // Computed array for current selection (order fixed by class mapping) – memoized so identity never changes
183+ const classReferenceOrder = useMemo (
184+ ( ) => [ 'ClassA' , 'ClassT' , 'ClassB1' , 'ClassB2' , 'ClassC' , 'ClassF' , 'ClassOlf' , 'GP157' , 'GP143' ] as const ,
185+ [ ]
186+ ) ;
184187 type ClassKey = typeof classReferenceOrder [ number ] ;
185- const classToGene : Record < ClassKey , string > = {
186- ClassA : 'HRH2' ,
187- ClassT : 'T2R39' ,
188- ClassB1 : 'PTH1R' ,
189- ClassB2 : 'AGRL3' ,
190- ClassC : 'CASR' ,
191- ClassF : 'FZD7' ,
192- ClassOlf : 'O52I2' ,
193- GP157 : 'GP157' ,
194- GP143 : 'GP143' ,
195- } ;
188+ const classToGene = useMemo < Record < ClassKey , string > > (
189+ ( ) => ( {
190+ ClassA : 'HRH2' ,
191+ ClassT : 'T2R39' ,
192+ ClassB1 : 'PTH1R' ,
193+ ClassB2 : 'AGRL3' ,
194+ ClassC : 'CASR' ,
195+ ClassF : 'FZD7' ,
196+ ClassOlf : 'O52I2' ,
197+ GP157 : 'GP157' ,
198+ GP143 : 'GP143'
199+ } ) ,
200+ [ ]
201+ ) ;
196202 const [ referenceInfo , setReferenceInfo ] = useState < { geneName : string ; gpcrdbMap : string [ ] } [ ] > ( [ ] ) ;
197203
198204 // (Column width slider removed – fixed width used)
@@ -1512,6 +1518,49 @@ const CustomSequenceLogo: React.FC<Props> = ({ fastaNames, folder }) => {
15121518 URL . revokeObjectURL ( url ) ;
15131519 } ;
15141520
1521+ // Download EPS function
1522+ const downloadEPS = ( ) => {
1523+ const yAxisContainer = yAxisContainerRef . current ;
1524+ const chartContainer = chartContainerRef . current ;
1525+ if ( ! yAxisContainer || ! chartContainer ) return ;
1526+ const yAxisSvg = yAxisContainer . querySelector ( 'svg' ) ;
1527+ const chartSvg = chartContainer . querySelector ( 'svg' ) ;
1528+ if ( ! yAxisSvg || ! chartSvg ) return ;
1529+
1530+ const yAxisW = parseInt ( yAxisSvg . getAttribute ( 'width' ) || '80' , 10 ) ;
1531+ const chartW = parseInt ( chartSvg . getAttribute ( 'width' ) || '800' , 10 ) ;
1532+ const totalW = yAxisW + chartW ;
1533+ const totalH = parseInt ( chartSvg . getAttribute ( 'height' ) || '400' , 10 ) ;
1534+
1535+ // build combined <svg> exactly as in downloadSVG()
1536+ const combined = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'svg' ) ;
1537+ combined . setAttribute ( 'width' , totalW . toString ( ) ) ;
1538+ combined . setAttribute ( 'height' , totalH . toString ( ) ) ;
1539+ combined . setAttribute ( 'viewBox' , `0 0 ${ totalW } ${ totalH } ` ) ;
1540+ combined . setAttribute ( 'xmlns' , 'http://www.w3.org/2000/svg' ) ;
1541+ const yClone = yAxisSvg . cloneNode ( true ) as SVGElement ;
1542+ combined . appendChild ( yClone ) ;
1543+ const cClone = ( chartSvg . cloneNode ( true ) as SVGElement ) ;
1544+ cClone . setAttribute ( 'x' , yAxisW . toString ( ) ) ;
1545+ combined . appendChild ( cClone ) ;
1546+
1547+ const svgStr = new XMLSerializer ( ) . serializeToString ( combined ) ;
1548+
1549+ // simple EPS wrapper
1550+ const header =
1551+ '%!PS-Adobe-3.0 EPSF-3.0\n' +
1552+ `%%BoundingBox: 0 0 ${ totalW } ${ totalH } \n` ;
1553+ const epsBlob = new Blob ( [ header + svgStr ] , { type : 'application/postscript' } ) ;
1554+ const url = URL . createObjectURL ( epsBlob ) ;
1555+ const a = document . createElement ( 'a' ) ;
1556+ a . href = url ;
1557+ a . download = 'custom_sequence_logo.eps' ;
1558+ document . body . appendChild ( a ) ;
1559+ a . click ( ) ;
1560+ document . body . removeChild ( a ) ;
1561+ URL . revokeObjectURL ( url ) ;
1562+ } ;
1563+
15151564 // Track previous data to avoid unnecessary rebuilds
15161565 const [ previousDataHash , setPreviousDataHash ] = useState < string > ( '' ) ;
15171566
@@ -1704,7 +1753,6 @@ const CustomSequenceLogo: React.FC<Props> = ({ fastaNames, folder }) => {
17041753 }
17051754 } ) ;
17061755
1707- const maxPositions = positionsWithGaps . length ; // Removed unused variable
17081756 const gapWidth = barWidthEstimate * 0.5 ; // Half column width for gaps
17091757
17101758 // Total width accounting for gaps
@@ -1805,12 +1853,18 @@ const CustomSequenceLogo: React.FC<Props> = ({ fastaNames, folder }) => {
18051853 const receptorY_scale = d3 . scaleLinear ( ) . domain ( [ 0 , yDomainMax ] ) . range ( [ logoAreaHeight , 0 ] ) ;
18061854
18071855 // Add y-axis line with tick marks only at min and max, no labels
1808- const yAxis = d3 . axisLeft ( receptorY_scale ) . tickValues ( [ 0 , yDomainMax ] ) . tickFormat ( ( ) => '' ) ;
1856+ const yAxis = d3 . axisLeft ( receptorY_scale )
1857+ . tickValues ( [ 0 , yDomainMax ] )
1858+ . tickFormat ( ( ) => '' )
1859+ . tickSize ( 0 ) ;
18091860 yAxisSvg
18101861 . append ( 'g' )
18111862 . attr ( 'transform' , `translate(${ yAxisWidth - 1 } , ${ receptorY } )` )
1812- . attr ( 'class' , 'axis text-foreground' )
1813- . call ( yAxis ) ;
1863+ . attr ( 'class' , 'axis' )
1864+ . call ( yAxis )
1865+ . call ( g => g . select ( '.domain' )
1866+ . attr ( 'stroke' , '#888' )
1867+ . attr ( 'stroke-width' , 2 ) ) ;
18141868 } ) ;
18151869
18161870 const letterPromises : Promise < void > [ ] = [ ] ;
@@ -1834,8 +1888,10 @@ const CustomSequenceLogo: React.FC<Props> = ({ fastaNames, folder }) => {
18341888 . attr ( 'y' , receptorY )
18351889 . attr ( 'width' , positionWidth )
18361890 . attr ( 'height' , logoAreaHeight )
1837- . attr ( 'fill' , 'rgba(255, 0, 0, 0.1)' )
1838- . attr ( 'stroke' , 'rgba(255, 0, 0, 0.3)' )
1891+ . attr ( 'fill' , '#ff0000' )
1892+ . attr ( 'fill-opacity' , 0.1 )
1893+ . attr ( 'stroke' , '#ff0000' )
1894+ . attr ( 'stroke-opacity' , 0.3 )
18391895 . attr ( 'stroke-width' , 1 )
18401896 . attr ( 'pointer-events' , 'none' )
18411897 . style ( 'mix-blend-mode' , 'multiply' ) ;
@@ -2065,7 +2121,8 @@ const CustomSequenceLogo: React.FC<Props> = ({ fastaNames, folder }) => {
20652121 . attr ( 'y' , overlapPlotOffset + rIdx * ( dotRowHeight + dotGap ) )
20662122 . attr ( 'width' , totalWidth )
20672123 . attr ( 'height' , dotRowHeight )
2068- . attr ( 'fill' , rIdx % 2 ? 'rgba(0,0,0,0.03)' : 'rgba(0,0,0,0.06)' ) ;
2124+ . attr ( 'fill' , '#000000' )
2125+ . attr ( 'fill-opacity' , rIdx % 2 ? 0.03 : 0.06 ) ;
20692126 } ) ;
20702127
20712128 // Determine top variant per position (most abundant across rows)
@@ -2096,7 +2153,7 @@ const CustomSequenceLogo: React.FC<Props> = ({ fastaNames, folder }) => {
20962153 } ) ;
20972154
20982155 // Define colors for primary, secondary, tertiary overlaps
2099- const overlapColors = [ '#475c6c' , '#8a8583 ' , '#eed7a1' ] ; // user-provided palette
2156+ const overlapColors = [ '#475c6c' , '#591F0A ' , '#eed7a1' , '#8a8583' , '#FBCAEF '] ; // user-provided palette
21002157
21012158 // Draw dots per receptor/position, coloring by overlap rank if present
21022159 data . forEach ( ( receptorData , rIdx ) => {
@@ -2274,7 +2331,8 @@ const CustomSequenceLogo: React.FC<Props> = ({ fastaNames, folder }) => {
22742331 . attr ( 'y' , referencePlotOffset + idx * referenceRowHeight )
22752332 . attr ( 'width' , totalWidth )
22762333 . attr ( 'height' , referenceRowHeight )
2277- . attr ( 'fill' , idx % 2 ? 'rgba(0,0,0,0.03)' : 'rgba(0,0,0,0.06)' ) ;
2334+ . attr ( 'fill' , '#000000' )
2335+ . attr ( 'fill-opacity' , idx % 2 ? 0.03 : 0.06 ) ;
22782336 } ) ;
22792337
22802338 referenceInfo . forEach ( ( ref , refIdx ) => {
@@ -2439,23 +2497,30 @@ const CustomSequenceLogo: React.FC<Props> = ({ fastaNames, folder }) => {
24392497 }
24402498
24412499 if ( displayedRegionGroups . length > 0 ) {
2442- // Background stripe
2443- chartSvg . append ( 'rect' )
2444- . attr ( 'x' , 0 )
2445- . attr ( 'y' , hrh2RegionPlotOffset )
2446- . attr ( 'width' , totalWidth )
2447- . attr ( 'height' , hrh2RegionHeight )
2448- . attr ( 'fill' , 'rgba(0,0,0,0.02)' ) ;
2500+ // Clamp background stripe and blocks to just the displayed region span
2501+ if ( displayedRegionGroups . length > 0 ) {
2502+ const first = displayedRegionGroups [ 0 ] ;
2503+ const last = displayedRegionGroups [ displayedRegionGroups . length - 1 ] ;
2504+ const xStart = x . getX ( first . startDisplayPos . toString ( ) ) ;
2505+ const xEnd = x . getX ( last . endDisplayPos . toString ( ) ) + x . bandwidth ( ) ;
2506+
2507+ chartSvg . append ( 'rect' )
2508+ . attr ( 'x' , xStart )
2509+ . attr ( 'y' , hrh2RegionPlotOffset )
2510+ . attr ( 'width' , xEnd - xStart )
2511+ . attr ( 'height' , hrh2RegionHeight )
2512+ . attr ( 'fill' , 'rgba(0,0,0,0.02)' ) ;
2513+ }
24492514
2450- // Render region blocks with alternating grey colors
2515+ // Render region blocks
24512516 displayedRegionGroups . forEach ( ( regionGroup , regionIndex ) => {
2452- const startX = x . getX ( regionGroup . startDisplayPos . toString ( ) ) ;
2453- const endX = x . getX ( regionGroup . endDisplayPos . toString ( ) ) + x . bandwidth ( ) ;
2517+ // Use the same alternating greys as our reference rows
2518+ const fillColor = regionIndex % 2 ? 'rgba(0,0,0,0.03)' : 'rgba(0,0,0,0.06)' ;
2519+
2520+ const startX = x . getX ( regionGroup . startDisplayPos . toString ( ) ) ;
2521+ const endX = x . getX ( regionGroup . endDisplayPos . toString ( ) ) + x . bandwidth ( ) ;
24542522 const regionWidth = endX - startX ;
24552523
2456- // Use alternating grey colors
2457- const fillColor = regionIndex % 2 ? 'rgba(0,0,0,0.06)' : 'rgba(0,0,0,0.12)' ;
2458-
24592524 // Region block
24602525 chartSvg . append ( 'rect' )
24612526 . attr ( 'class' , 'hrh2-region-block' )
@@ -2576,13 +2641,17 @@ const CustomSequenceLogo: React.FC<Props> = ({ fastaNames, folder }) => {
25762641 . domain ( [ 0 , maxConservation ] )
25772642 . range ( [ conservationBarHeight - 10 , 10 ] ) )
25782643 . tickValues ( [ 0 , maxConservation ] )
2579- . tickFormat ( d => `${ d } %` ) ;
2644+ . tickFormat ( d => `${ d } %` )
2645+ . tickSize ( 0 ) ;
25802646
25812647 yAxisSvg
25822648 . append ( 'g' )
25832649 . attr ( 'transform' , `translate(${ yAxisWidth - 1 } , ${ barChartY } )` )
2584- . attr ( 'class' , 'axis text-foreground ' )
2650+ . attr ( 'class' , 'axis' )
25852651 . call ( conservationAxis )
2652+ . call ( g => g . select ( '.domain' )
2653+ . attr ( 'stroke' , '#888' )
2654+ . attr ( 'stroke-width' , 2 ) )
25862655 . selectAll ( 'text' )
25872656 . style ( 'font-size' , '12px' )
25882657 . style ( 'font-family' , 'Helvetica' ) ;
@@ -2646,6 +2715,9 @@ const CustomSequenceLogo: React.FC<Props> = ({ fastaNames, folder }) => {
26462715 < Button onClick = { downloadSVG } variant = "outline" size = "sm" >
26472716 Download SVG
26482717 </ Button >
2718+ < Button onClick = { downloadEPS } variant = "outline" size = "sm" >
2719+ Download EPS
2720+ </ Button >
26492721 </ div >
26502722
26512723 { /* Alignment selection controls */ }
@@ -2896,6 +2968,13 @@ const CustomSequenceLogo: React.FC<Props> = ({ fastaNames, folder }) => {
28962968 } ) ( ) }
28972969 </ div >
28982970
2971+ { /* Download SVG button (vector export) */ }
2972+ < div className = "mb-4" >
2973+ < Button onClick = { downloadSVG } variant = "outline" size = "sm" >
2974+ Download SVG
2975+ </ Button >
2976+ </ div >
2977+
28992978 { /* Chart container placeholder (SVGs rendered via d3) */ }
29002979 < div className = "relative w-full flex overflow-x-hidden mb-4" >
29012980 < div ref = { yAxisContainerRef } className = "flex-shrink-0 z-10 bg-card" />
0 commit comments