Skip to content

Commit 6d9a25c

Browse files
author
Selcuk
committed
Dark mode and plot height fixed
1 parent df66dc4 commit 6d9a25c

1 file changed

Lines changed: 96 additions & 31 deletions

File tree

src/components/CombinedTreeAlignment.tsx

Lines changed: 96 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export type CombinedTreeAlignmentProps = {
4343
// Style
4444
fontSize?: number;
4545
leafRowSpacing?: number;
46+
// Dark mode
47+
isDarkMode?: boolean;
4648
// Receptor data for GPCRdb numbering (will use conservationFile from receptor object)
4749
receptor?: { conservationFile?: string } | null;
4850
};
@@ -198,9 +200,36 @@ export function CombinedTreeAlignment({
198200
mirrorRightToLeft = false,
199201
fontSize = 12,
200202
leafRowSpacing = 28,
203+
isDarkMode,
201204
receptor,
202205
}: CombinedTreeAlignmentProps) {
203206
const containerRef = useRef<HTMLDivElement | null>(null);
207+
// Mirror site-wide dark mode like other components (SequenceLogoChart, etc.)
208+
const [detectedDarkMode, setDetectedDarkMode] = useState<boolean>(false);
209+
useEffect(() => {
210+
const updateTheme = () => {
211+
try {
212+
const html = document.documentElement;
213+
const body = document.body;
214+
const hasDarkClass = html.classList.contains('dark') || body.classList.contains('dark');
215+
const hasDarkData = html.getAttribute('data-theme') === 'dark' || body.getAttribute('data-theme') === 'dark';
216+
setDetectedDarkMode(hasDarkClass || hasDarkData);
217+
} catch {
218+
setDetectedDarkMode(false);
219+
}
220+
};
221+
updateTheme();
222+
const htmlObserver = new MutationObserver(updateTheme);
223+
const bodyObserver = new MutationObserver(updateTheme);
224+
htmlObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] });
225+
if (document.body) bodyObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'data-theme'] });
226+
return () => {
227+
htmlObserver.disconnect();
228+
bodyObserver.disconnect();
229+
};
230+
}, []);
231+
// Effective dark mode: explicit prop wins when provided; otherwise use detected
232+
const effectiveDarkMode = typeof isDarkMode === 'boolean' ? isDarkMode : detectedDarkMode;
204233
const [containerSize, setContainerSize] = useState<{ w: number; h: number }>({ w: width || 800, h: height || 500 });
205234

206235
// Parse FASTA and use the cleaning hook
@@ -390,7 +419,7 @@ export function CombinedTreeAlignment({
390419
const canvas = document.createElement('canvas');
391420
const ctx = canvas.getContext('2d');
392421
if (!ctx) return laidOut.visibleLeaves.map(() => 0);
393-
ctx.font = `${fontSize}px sans-serif`;
422+
ctx.font = `${fontSize}px Arial, sans-serif`;
394423
return laidOut.visibleLeaves.map(leaf => {
395424
const name = leaf.name || '';
396425
const parts = name.split('|');
@@ -425,6 +454,15 @@ export function CombinedTreeAlignment({
425454
return desired > 0 ? desired : containerSize.h;
426455
}, [laidOut, treePadding, containerSize.h, width, height, cleanedSequences.length, alignmentHeaderHeight]);
427456

457+
// Height of just the alignment body (rows area) excluding header and external paddings
458+
const alignmentBodyHeight = useMemo(() => {
459+
if (!laidOut) return 0;
460+
const leaves = laidOut.visibleLeaves;
461+
if (leaves.length === 0) return 0;
462+
const maxY = Math.max(...leaves.map(l => l.y || 0));
463+
return Math.max(0, maxY + dynamicRowSpacing / 2);
464+
}, [laidOut, dynamicRowSpacing]);
465+
428466
// Consistent character width used for both headers and background stripes
429467
const columnCharWidth = useMemo(() => {
430468
const canvas = document.createElement('canvas');
@@ -437,11 +475,15 @@ export function CombinedTreeAlignment({
437475
return baseWidth + fontSize * LETTER_SPACING_EM;
438476
}, [fontSize]);
439477

440-
// Colors: charcoal on white background
441-
const strokeColor = '#333333';
442-
const textColor = '#111111';
443-
const leafGuideColor = '#bdbdbd';
444-
const connectorColor = '#9e9e9e';
478+
// Colors: responsive to dark mode
479+
const backgroundColor = effectiveDarkMode ? '#1f2937' : '#ffffff'; // gray-800 : white
480+
const strokeColor = effectiveDarkMode ? '#9ca3af' : '#333333'; // gray-400 : dark gray
481+
const textColor = effectiveDarkMode ? '#f9fafb' : '#111111'; // gray-50 : almost black
482+
const leafGuideColor = effectiveDarkMode ? '#6b7280' : '#bdbdbd'; // gray-500 : light gray
483+
const connectorColor = effectiveDarkMode ? '#9ca3af' : '#9e9e9e'; // gray-400 : medium gray
484+
const alternatingStripeColor = effectiveDarkMode ? '#4b5563' : '#cbd5e1'; // dark: gray-600, light: slate-300
485+
const errorBgColor = effectiveDarkMode ? '#1f2937' : '#ffffff'; // gray-800 : white
486+
const errorTextColor = effectiveDarkMode ? '#ef4444' : '#b91c1c'; // red-500 : red-700
445487

446488
// Fine-tune vertical gaps relative to the header bottom
447489
// Note: headerToTreeGapPx is currently unused after aligning tree to sequences; keep for quick tuning if needed.
@@ -451,7 +493,7 @@ export function CombinedTreeAlignment({
451493
if (!parsedTree) {
452494
return (
453495
<div ref={containerRef} style={{ width: width ? `${width}px` : '100%', height: height ? `${height}px` : '420px' }}>
454-
<div style={{ background: '#ffffff', color: '#b91c1c', padding: 8, border: '1px solid #e5e7eb' }}>
496+
<div style={{ background: errorBgColor, color: errorTextColor, padding: 8, border: `1px solid ${isDarkMode ? '#4b5563' : '#e5e7eb'}` }}>
455497
Invalid Newick provided.
456498
</div>
457499
</div>
@@ -464,16 +506,16 @@ export function CombinedTreeAlignment({
464506
const totalWidth = leftWidth + alignmentTotalWidth; // Remove alignPadding gap
465507

466508
return (
467-
<div ref={containerRef} style={{ width: width ? `${width}px` : '100%', height: height ? `${height}px` : '100%', overflow: 'auto', position: 'relative', background: '#ffffff' }}>
509+
<div ref={containerRef} style={{ width: width ? `${width}px` : '100%', height: height ? `${height}px` : '100%', overflow: 'auto', position: 'relative', background: backgroundColor }}>
468510
{/* Content area with proper width to avoid empty space on right */}
469-
<div style={{ position: 'relative', width: totalWidth, height: contentHeight, background: '#ffffff' }}>
511+
<div style={{ position: 'relative', width: totalWidth, height: contentHeight, background: backgroundColor }}>
470512
{/* Sticky header overlay: GPCRdb column numbers (placed before tree SVG to avoid being pushed down). */}
471513
{laidOut && (
472514
// Make the header sticky without affecting flow: height 0 + overflow visible prevents pushing tree down
473515
<div style={{ position: 'sticky', top: 0, left: leftWidth, zIndex: 15, width: alignmentTotalWidth, height: 0, pointerEvents: 'none', overflow: 'visible' }}>
474516
<svg width={alignmentTotalWidth} height={alignmentHeaderHeight} viewBox={`0 0 ${alignmentTotalWidth} ${alignmentHeaderHeight}`} style={{ display: 'block' }}>
475-
{/* Solid white background to ensure header is opaque */}
476-
<rect x={0} y={0} width={alignmentTotalWidth} height={alignmentHeaderHeight} fill="#ffffff" />
517+
{/* Solid background to ensure header is opaque */}
518+
<rect x={0} y={0} width={alignmentTotalWidth} height={alignmentHeaderHeight} fill={backgroundColor} />
477519
{/* Alternating background stripes behind GPCRdb header */}
478520
<g>
479521
{Array.from({ length: numColumns }).map((_, i) => (
@@ -484,7 +526,7 @@ export function CombinedTreeAlignment({
484526
y={0}
485527
width={columnCharWidth}
486528
height={alignmentHeaderHeight}
487-
fill="#f3f4f6"
529+
fill={alternatingStripeColor}
488530
/>
489531
) : null
490532
))}
@@ -510,12 +552,12 @@ export function CombinedTreeAlignment({
510552
width={leftWidth}
511553
height={contentHeight}
512554
viewBox={`0 0 ${leftWidth} ${Math.max(contentHeight, 10)}`}
513-
style={{ position: 'sticky', left: 0, top: 0, zIndex: 20, background: '#ffffff' }}
555+
style={{ position: 'sticky', left: 0, top: 0, zIndex: 20, background: backgroundColor }}
514556
>
515-
{/* White background for entire tree column (also covers the sticky header area above content) */}
516-
<rect x={0} y={0} width={leftWidth} height={Math.max(contentHeight, 10)} fill="#ffffff" />
557+
{/* Background for entire tree column (also covers the sticky header area above content) */}
558+
<rect x={0} y={0} width={leftWidth} height={Math.max(contentHeight, 10)} fill={backgroundColor} />
517559
{/* Top cap to ensure header disappears behind tree when scrolling right */}
518-
<rect x={0} y={0} width={leftWidth} height={alignmentHeaderHeight} fill="#ffffff" />
560+
<rect x={0} y={0} width={leftWidth} height={alignmentHeaderHeight} fill={backgroundColor} />
519561
{/* Scale bar (top-left) */}
520562
{laidOut && (
521563
<SimpleScaleBar
@@ -543,6 +585,7 @@ export function CombinedTreeAlignment({
543585
showSupport={showSupportOnBranches}
544586
textColor={textColor}
545587
collapsed={collapsed}
588+
backgroundColor={backgroundColor}
546589
/>
547590
)}
548591

@@ -560,6 +603,7 @@ export function CombinedTreeAlignment({
560603
return next;
561604
})
562605
}
606+
backgroundColor={backgroundColor}
563607
/>
564608
)}
565609

@@ -594,13 +638,13 @@ export function CombinedTreeAlignment({
594638
>
595639
{/* Alignment content: start under header with adjustable gap */}
596640
<g transform={`translate(0, ${alignmentHeaderHeight + headerToSeqGapPx})`}>
597-
{/* White background behind sequences to ensure header overlap looks clean */}
641+
{/* Background behind sequences to ensure header overlap looks clean */}
598642
<rect
599643
x={0}
600-
y={0}
644+
y={-dynamicRowSpacing / 2}
601645
width={alignmentTotalWidth}
602-
height={Math.max(0, contentHeight - alignmentHeaderHeight - headerToSeqGapPx)}
603-
fill="#ffffff"
646+
height={alignmentBodyHeight + dynamicRowSpacing / 2}
647+
fill={backgroundColor}
604648
/>
605649
{/* Alternating column background stripes */}
606650
<g>
@@ -609,10 +653,10 @@ export function CombinedTreeAlignment({
609653
<rect
610654
key={`bg-${i}`}
611655
x={i * columnCharWidth}
612-
y={0}
656+
y={-dynamicRowSpacing / 2}
613657
width={columnCharWidth}
614-
height={Math.max(0, contentHeight - alignmentHeaderHeight - headerToSeqGapPx)}
615-
fill="#f3f4f6" /* Tailwind gray-100 */
658+
height={alignmentBodyHeight + dynamicRowSpacing / 2}
659+
fill={alternatingStripeColor} /* Stripe fill */
616660
/>
617661
) : null
618662
))}
@@ -626,6 +670,7 @@ export function CombinedTreeAlignment({
626670
fontSize={fontSize}
627671
sequences={cleanedSequences}
628672
fallbackText={alignmentText}
673+
isDarkMode={effectiveDarkMode}
629674
/>
630675
</g>
631676
</svg>
@@ -642,13 +687,15 @@ function TreeBranches({
642687
showSupport,
643688
textColor,
644689
collapsed,
690+
backgroundColor,
645691
}: {
646692
node: NewickNode;
647693
strokeColor: string;
648694
fontSize: number;
649695
showSupport: boolean;
650696
textColor: string;
651697
collapsed: Set<string>;
698+
backgroundColor: string;
652699
}) {
653700
const elements: React.ReactNode[] = [];
654701

@@ -693,8 +740,8 @@ function TreeBranches({
693740
height={rectH}
694741
rx={3}
695742
ry={3}
696-
fill="#ffffff"
697-
opacity={0.80}
743+
fill={backgroundColor}
744+
opacity={0.90}
698745
/>,
699746
);
700747
elements.push(
@@ -768,6 +815,7 @@ function LeafLabels({
768815
y={y}
769816
fontSize={fontSize}
770817
fill={textColor}
818+
fontFamily="Arial, sans-serif"
771819
textAnchor="end"
772820
dominantBaseline="middle"
773821
transform={mirror ? `scale(-1,1) translate(${-2 * rightRailX},0)` : undefined}
@@ -794,12 +842,14 @@ function NodeCircles({
794842
strokeColor,
795843
collapsed,
796844
onToggle,
845+
backgroundColor,
797846
}: {
798847
node: NewickNode;
799848
radius: number;
800849
strokeColor: string;
801850
collapsed: Set<string>;
802851
onToggle: (id: string) => void;
852+
backgroundColor: string;
803853
}) {
804854
const elements: React.ReactNode[] = [];
805855

@@ -817,7 +867,7 @@ function NodeCircles({
817867
cx={cx}
818868
cy={cy}
819869
r={radius}
820-
fill={isCollapsed ? strokeColor : '#ffffff'}
870+
fill={isCollapsed ? strokeColor : backgroundColor}
821871
stroke={strokeColor}
822872
strokeWidth={1}
823873
style={{ cursor: 'pointer' }}
@@ -868,7 +918,7 @@ function AlignmentColumnHeaders({
868918

869919
// charWidth provided by parent for consistency with backgrounds
870920

871-
const monoFont = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace";
921+
const headerFont = "Arial, sans-serif";
872922
// Anchor labels to the bottom of the header so rotation doesn't clip at the top
873923
const yAnchor = Math.max(2, headerHeight - 2);
874924

@@ -895,7 +945,7 @@ function AlignmentColumnHeaders({
895945
y={yAnchor}
896946
fontSize={fontSize}
897947
fill={textColor}
898-
fontFamily={monoFont}
948+
fontFamily={headerFont}
899949
textAnchor="start"
900950
dominantBaseline="hanging"
901951
transform={`rotate(-90, ${x}, ${yAnchor})`}
@@ -921,6 +971,7 @@ function AlignmentOnly({
921971
fontSize,
922972
sequences,
923973
fallbackText,
974+
isDarkMode,
924975
}: {
925976
rowNodes: NewickNode[];
926977
leaves: NewickNode[];
@@ -930,6 +981,7 @@ function AlignmentOnly({
930981
fontSize: number;
931982
sequences: { header: string; sequence: string }[];
932983
fallbackText: string;
984+
isDarkMode: boolean;
933985
}) {
934986
const monoFont = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace";
935987
const rows: React.ReactNode[] = [];
@@ -988,7 +1040,7 @@ function AlignmentOnly({
9881040
x={xStart + i * charWidth + charWidth / 2}
9891041
dy={0}
9901042
textAnchor="middle"
991-
fill={residueColor(residue)}
1043+
fill={residueColor(residue, isDarkMode)}
9921044
>
9931045
{residue}
9941046
</tspan>
@@ -1000,15 +1052,28 @@ function AlignmentOnly({
10001052

10011053
return <g>{rows}</g>;
10021054
}
1003-
function residueColor(residue: string): string | undefined {
1004-
const mapping: Record<string, string> = {
1055+
function residueColor(residue: string, isDarkMode: boolean = false): string | undefined {
1056+
// Light mode colors
1057+
const lightMapping: Record<string, string> = {
10051058
FCB315: 'WYHF',
10061059
'7D2985': 'STQN',
10071060
'231F20': 'PGA',
10081061
DD6030: 'ED',
10091062
'7CAEC4': 'RK',
10101063
B4B4B4: 'VCIML',
10111064
};
1065+
1066+
// Dark mode colors (adjusted for better contrast)
1067+
const darkMapping: Record<string, string> = {
1068+
'FFD700': 'WYHF', // Gold for aromatic
1069+
'DA70D6': 'STQN', // Orchid for polar
1070+
'F0F0F0': 'PGA', // Light gray for small (inverted)
1071+
'FF6347': 'ED', // Tomato for acidic
1072+
'87CEEB': 'RK', // Sky blue for basic
1073+
'C0C0C0': 'VCIML', // Silver for hydrophobic
1074+
};
1075+
1076+
const mapping = isDarkMode ? darkMapping : lightMapping;
10121077
const ch = residue.toUpperCase();
10131078
for (const [hex, acids] of Object.entries(mapping)) {
10141079
if (acids.includes(ch)) return `#${hex}`;

0 commit comments

Comments
 (0)