Skip to content

Commit 96e7baf

Browse files
author
Selcuk
committed
Visual changes
1 parent 4eb4447 commit 96e7baf

1 file changed

Lines changed: 116 additions & 37 deletions

File tree

src/components/CustomSequenceLogo.tsx

Lines changed: 116 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)