Skip to content

Commit 9a47176

Browse files
author
Selcuk
committed
Fixes to visualization and coloring.
1 parent 6d9a25c commit 9a47176

1 file changed

Lines changed: 82 additions & 47 deletions

File tree

src/components/CombinedTreeAlignment.tsx

Lines changed: 82 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ export type CombinedTreeAlignmentProps = {
4343
// Style
4444
fontSize?: number;
4545
leafRowSpacing?: number;
46+
// Sequence area padding
47+
sequenceTopPadding?: number;
48+
sequenceBottomPadding?: number;
49+
// Overall container padding
50+
containerTopPadding?: number;
51+
containerBottomPadding?: number;
52+
// Alignment area padding
53+
alignmentRightPadding?: number;
4654
// Dark mode
4755
isDarkMode?: boolean;
4856
// Receptor data for GPCRdb numbering (will use conservationFile from receptor object)
@@ -200,12 +208,29 @@ export function CombinedTreeAlignment({
200208
mirrorRightToLeft = false,
201209
fontSize = 12,
202210
leafRowSpacing = 28,
211+
sequenceTopPadding = 4,
212+
sequenceBottomPadding = 4,
213+
containerTopPadding = 0,
214+
containerBottomPadding = 0,
215+
alignmentRightPadding = 4,
203216
isDarkMode,
204217
receptor,
205218
}: CombinedTreeAlignmentProps) {
206219
const containerRef = useRef<HTMLDivElement | null>(null);
207220
// Mirror site-wide dark mode like other components (SequenceLogoChart, etc.)
208-
const [detectedDarkMode, setDetectedDarkMode] = useState<boolean>(false);
221+
// Initialize synchronously from document to avoid a light→dark flash on first paint
222+
const [detectedDarkMode, setDetectedDarkMode] = useState<boolean>(() => {
223+
try {
224+
const html = typeof document !== 'undefined' ? document.documentElement : null;
225+
const body = typeof document !== 'undefined' ? document.body : null;
226+
if (!html || !body) return false;
227+
const hasDarkClass = html.classList.contains('dark') || body.classList.contains('dark');
228+
const hasDarkData = html.getAttribute('data-theme') === 'dark' || body.getAttribute('data-theme') === 'dark';
229+
return hasDarkClass || hasDarkData;
230+
} catch {
231+
return false;
232+
}
233+
});
209234
useEffect(() => {
210235
const updateTheme = () => {
211236
try {
@@ -307,17 +332,17 @@ export function CombinedTreeAlignment({
307332
});
308333
}, [receptor?.conservationFile]);
309334

310-
// Autosize to parent if width/height not provided
335+
// Only autosize width if not provided (height is now calculated based on content)
311336
useEffect(() => {
312-
if (width && height) return;
337+
if (width) return;
313338
const el = containerRef.current;
314339
if (!el) return;
315-
const update = () => setContainerSize({ w: el.clientWidth || 800, h: el.clientHeight || 500 });
340+
const update = () => setContainerSize({ w: el.clientWidth || 800, h: containerSize.h });
316341
update();
317342
const ro = new ResizeObserver(update);
318343
ro.observe(el);
319344
return () => ro.disconnect();
320-
}, [width, height]);
345+
}, [width, containerSize.h]);
321346

322347
const parsedTree = useMemo(() => {
323348
try {
@@ -332,22 +357,14 @@ export function CombinedTreeAlignment({
332357

333358
// Derived drawing metrics
334359
const treePadding = 16; // left padding and bottom padding
335-
const treeTopOffset = treePadding + fontSize + 20; // extra space for scale bar
336360
// Compact header height for rotated GPCRdb numbers
337361
// alignmentHeaderHeight will be computed dynamically below based on widest label
362+
363+
// Fine-tune vertical gaps relative to the header bottom
364+
const headerToSeqGapPx = Math.max(2, Math.round(fontSize * 0.5)); // slightly larger gap for sequences
338365

339-
// Calculate dynamic row spacing to fit container height when size not explicitly provided
340-
const dynamicRowSpacing = useMemo(() => {
341-
if (!parsedTree || (width && height)) {
342-
return leafRowSpacing; // Use fixed spacing if explicit dimensions provided
343-
}
344-
const tempLayout = layoutTree(parsedTree, leafRowSpacing, collapsed);
345-
const numVisibleRows = tempLayout.rowNodes.length;
346-
if (numVisibleRows <= 1) return leafRowSpacing;
347-
const availableHeight = containerSize.h - treeTopOffset - treePadding;
348-
const calculatedSpacing = Math.max(16, availableHeight / numVisibleRows);
349-
return Math.min(calculatedSpacing, leafRowSpacing * 2);
350-
}, [parsedTree, collapsed, containerSize.h, leafRowSpacing, width, height, treeTopOffset, treePadding]);
366+
// Use fixed row spacing - no dynamic calculation based on container
367+
const dynamicRowSpacing = leafRowSpacing;
351368

352369
const laidOut = useMemo(() => {
353370
if (!parsedTree) return null;
@@ -380,10 +397,11 @@ export function CombinedTreeAlignment({
380397
if (!ctx) return sampleText.length * fontSize * (0.6 + LETTER_SPACING_EM); // fallback adjusted for letter spacing
381398
const monoFont = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace";
382399
ctx.font = `bold ${fontSize}px ${monoFont}`;
383-
const baseWidth = ctx.measureText(sampleText).width;
384-
// Add letter spacing for each character (0.2em per character)
385-
const letterSpacingTotal = sampleText.length * (fontSize * LETTER_SPACING_EM);
386-
return baseWidth + letterSpacingTotal;
400+
const baseCharWidth = ctx.measureText('M').width;
401+
const charWidthWithSpacing = baseCharWidth + fontSize * LETTER_SPACING_EM;
402+
// Calculate width as: (n-1) * charWidthWithSpacing + baseCharWidth
403+
// This accounts for letter spacing between characters but not after the last one
404+
return sampleText.length > 0 ? (sampleText.length - 1) * charWidthWithSpacing + baseCharWidth : 0;
387405
}, [cleanedSequences, fontSize]);
388406

389407
// Number of columns (characters) to render in the alignment/header
@@ -447,21 +465,33 @@ export function CombinedTreeAlignment({
447465

448466
const contentHeight = useMemo(() => {
449467
if (width && height) return height;
450-
if (!laidOut) return containerSize.h;
451-
// Calculate the exact height needed for the content including header space
452-
const desired = laidOut.totalHeight + treePadding + (cleanedSequences.length > 0 ? alignmentHeaderHeight : 0);
453-
// Only use container height as minimum if content is smaller, otherwise use exact content height
454-
return desired > 0 ? desired : containerSize.h;
455-
}, [laidOut, treePadding, containerSize.h, width, height, cleanedSequences.length, alignmentHeaderHeight]);
468+
if (!laidOut) return 400; // fallback minimum height
469+
470+
// Calculate height based purely on number of sequences/rows
471+
const numSequenceRows = laidOut.visibleLeaves.length;
472+
const headerPx = cleanedSequences.length > 0 ? alignmentHeaderHeight : 0;
473+
const sequenceAreaHeight = numSequenceRows * leafRowSpacing;
474+
475+
// Total: header + gap + top padding + sequence area + bottom padding + container padding
476+
const totalHeight = headerPx +
477+
headerToSeqGapPx +
478+
sequenceTopPadding +
479+
sequenceAreaHeight +
480+
sequenceBottomPadding +
481+
containerTopPadding +
482+
containerBottomPadding;
483+
484+
return Math.max(totalHeight, 200); // ensure minimum reasonable height
485+
}, [laidOut, cleanedSequences.length, alignmentHeaderHeight, leafRowSpacing, headerToSeqGapPx, sequenceTopPadding, sequenceBottomPadding, containerTopPadding, containerBottomPadding, width, height]);
456486

457487
// Height of just the alignment body (rows area) excluding header and external paddings
458488
const alignmentBodyHeight = useMemo(() => {
459489
if (!laidOut) return 0;
460490
const leaves = laidOut.visibleLeaves;
461491
if (leaves.length === 0) return 0;
462492
const maxY = Math.max(...leaves.map(l => l.y || 0));
463-
return Math.max(0, maxY + dynamicRowSpacing / 2);
464-
}, [laidOut, dynamicRowSpacing]);
493+
return Math.max(0, maxY + dynamicRowSpacing / 2 + sequenceTopPadding + sequenceBottomPadding);
494+
}, [laidOut, dynamicRowSpacing, sequenceTopPadding, sequenceBottomPadding]);
465495

466496
// Consistent character width used for both headers and background stripes
467497
const columnCharWidth = useMemo(() => {
@@ -475,20 +505,22 @@ export function CombinedTreeAlignment({
475505
return baseWidth + fontSize * LETTER_SPACING_EM;
476506
}, [fontSize]);
477507

478-
// Colors: responsive to dark mode
479-
const backgroundColor = effectiveDarkMode ? '#1f2937' : '#ffffff'; // gray-800 : white
508+
// Colors: responsive to dark mode - use CSS variables to match other components
509+
const backgroundColor = useMemo(() => {
510+
if (typeof document === 'undefined') return effectiveDarkMode ? '#2A2A2A' : '#FDFBF7';
511+
const computedStyle = getComputedStyle(document.documentElement);
512+
return computedStyle.getPropertyValue('--card').trim() || (effectiveDarkMode ? '#2A2A2A' : '#FDFBF7');
513+
}, [effectiveDarkMode]);
514+
480515
const strokeColor = effectiveDarkMode ? '#9ca3af' : '#333333'; // gray-400 : dark gray
481516
const textColor = effectiveDarkMode ? '#f9fafb' : '#111111'; // gray-50 : almost black
482517
const leafGuideColor = effectiveDarkMode ? '#6b7280' : '#bdbdbd'; // gray-500 : light gray
483518
const connectorColor = effectiveDarkMode ? '#9ca3af' : '#9e9e9e'; // gray-400 : medium gray
484519
const alternatingStripeColor = effectiveDarkMode ? '#4b5563' : '#cbd5e1'; // dark: gray-600, light: slate-300
485-
const errorBgColor = effectiveDarkMode ? '#1f2937' : '#ffffff'; // gray-800 : white
520+
const errorBgColor = backgroundColor;
486521
const errorTextColor = effectiveDarkMode ? '#ef4444' : '#b91c1c'; // red-500 : red-700
487522

488-
// Fine-tune vertical gaps relative to the header bottom
489-
// Note: headerToTreeGapPx is currently unused after aligning tree to sequences; keep for quick tuning if needed.
490-
// const headerToTreeGapPx = Math.max(0, Math.round(fontSize * 0.2));
491-
const headerToSeqGapPx = Math.max(2, Math.round(fontSize * 0.5)); // slightly larger gap for sequences
523+
// Note: headerToTreeGapPx was removed after aligning tree to sequences
492524

493525
if (!parsedTree) {
494526
return (
@@ -501,12 +533,12 @@ export function CombinedTreeAlignment({
501533
}
502534

503535
const leftWidth = treeWidthPx + Math.max(0, labelsRightX - treeWidthPx) + treePadding * 2;
504-
// Use precise measured content width to eliminate empty space
505-
const alignmentTotalWidth = alignmentContentWidth;
506-
const totalWidth = leftWidth + alignmentTotalWidth; // Remove alignPadding gap
536+
// Use precise measured content width plus right padding for better visibility
537+
const alignmentTotalWidth = alignmentContentWidth + alignmentRightPadding;
538+
const totalWidth = leftWidth + alignmentTotalWidth;
507539

508540
return (
509-
<div ref={containerRef} style={{ width: width ? `${width}px` : '100%', height: height ? `${height}px` : '100%', overflow: 'auto', position: 'relative', background: backgroundColor }}>
541+
<div ref={containerRef} style={{ width: width ? `${width}px` : '100%', height: height ? `${height}px` : 'auto', overflow: 'auto', position: 'relative', background: backgroundColor }}>
510542
{/* Content area with proper width to avoid empty space on right */}
511543
<div style={{ position: 'relative', width: totalWidth, height: contentHeight, background: backgroundColor }}>
512544
{/* Sticky header overlay: GPCRdb column numbers (placed before tree SVG to avoid being pushed down). */}
@@ -575,7 +607,7 @@ export function CombinedTreeAlignment({
575607
{/* Optional mirror for RTL */}
576608
<g transform={mirrorRightToLeft ? `translate(${leftWidth},0) scale(-1,1)` : undefined}>
577609
{/* Tree drawing origin padding: align directly under header to match sequence start offset. */}
578-
<g transform={`translate(${treePadding}, ${alignmentHeaderHeight + headerToSeqGapPx})`}>
610+
<g transform={`translate(${treePadding}, ${alignmentHeaderHeight + headerToSeqGapPx + sequenceTopPadding})`}>
579611
{/* Draw branches */}
580612
{laidOut && (
581613
<TreeBranches
@@ -641,9 +673,9 @@ export function CombinedTreeAlignment({
641673
{/* Background behind sequences to ensure header overlap looks clean */}
642674
<rect
643675
x={0}
644-
y={-dynamicRowSpacing / 2}
676+
y={-sequenceTopPadding}
645677
width={alignmentTotalWidth}
646-
height={alignmentBodyHeight + dynamicRowSpacing / 2}
678+
height={alignmentBodyHeight}
647679
fill={backgroundColor}
648680
/>
649681
{/* Alternating column background stripes */}
@@ -653,9 +685,9 @@ export function CombinedTreeAlignment({
653685
<rect
654686
key={`bg-${i}`}
655687
x={i * columnCharWidth}
656-
y={-dynamicRowSpacing / 2}
688+
y={-sequenceTopPadding}
657689
width={columnCharWidth}
658-
height={alignmentBodyHeight + dynamicRowSpacing / 2}
690+
height={alignmentBodyHeight}
659691
fill={alternatingStripeColor} /* Stripe fill */
660692
/>
661693
) : null
@@ -671,6 +703,7 @@ export function CombinedTreeAlignment({
671703
sequences={cleanedSequences}
672704
fallbackText={alignmentText}
673705
isDarkMode={effectiveDarkMode}
706+
topPadding={sequenceTopPadding}
674707
/>
675708
</g>
676709
</svg>
@@ -972,6 +1005,7 @@ function AlignmentOnly({
9721005
sequences,
9731006
fallbackText,
9741007
isDarkMode,
1008+
topPadding = 0,
9751009
}: {
9761010
rowNodes: NewickNode[];
9771011
leaves: NewickNode[];
@@ -982,6 +1016,7 @@ function AlignmentOnly({
9821016
sequences: { header: string; sequence: string }[];
9831017
fallbackText: string;
9841018
isDarkMode: boolean;
1019+
topPadding?: number;
9851020
}) {
9861021
const monoFont = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace";
9871022
const rows: React.ReactNode[] = [];
@@ -1016,7 +1051,7 @@ function AlignmentOnly({
10161051
};
10171052

10181053
leaves.forEach((leaf, idx) => {
1019-
const y = leaf.y || 0;
1054+
const y = (leaf.y || 0) + topPadding;
10201055
const sequence = getSequenceForLeaf(leaf.name || '');
10211056

10221057
// Only render if we have a sequence (not null while loading)

0 commit comments

Comments
 (0)