Skip to content

Commit 017cbfd

Browse files
Shaun Mahonyclaude
andcommitted
STAMP 2.0: version bump, logo, toolbar, FBP merge, DB metadata, tree rework
- Bump version to 2.0 in main.cpp, layout.tsx, and package.json - Add STAMP logo to header and results page - Replace RCToggle with LogoToolbar (RC, Axes, PNG, SVG) across all views - Merge FBP into Multiple Alignment section with TRANSFAC download - Add JASPAR DB metadata (matrix ID, source, link) to similarity matches - Display alignment offset/range/orientation info in match entries - Rewrite TreeViewer: ultrametric layout, proper cladogram elbows, inline SequenceLogo at leaves and internal nodes, actual/uniform branch length toggle - Make SequenceLogo layout axes-independent (toggling axes only hides elements) - Update logoRenderer and resultsHtml to match axes-independent layout - Enrich processor to parse MA0139.1::CTCF format for DB metadata Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 6d961b1 commit 017cbfd

File tree

14 files changed

+1359
-429
lines changed

14 files changed

+1359
-429
lines changed

src/main.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//////////////////////////////////////////////////////////////////////////////////
22
//
3-
// STAMP version 1.3
3+
// STAMP version 2.0
44
//
55
// Written By: Shaun Mahony
66
// Bug fixes by: Gert Hulselmans
@@ -96,7 +96,7 @@ int main (int argc, char *argv[])
9696
}
9797

9898
//Welcome message
99-
if(!silent && !htmlOutput && !webMode){printf("\n\tSTAMP\n\tSimilarity, Tree-building, & Alignment of Motifs and Profiles\n\n\tShaun Mahony\n\tDepartment of Biochemistry & Molecular Biology\n\tPenn State University\n\tVersion 1.3 (September 2016)\n\n");}
99+
if(!silent && !htmlOutput && !webMode){printf("\n\tSTAMP\n\tSimilarity, Tree-building, & Alignment of Motifs and Profiles\n\n\tShaun Mahony\n\tDepartment of Biochemistry & Molecular Biology\n\tPenn State University\n\tVersion 2.0\n\n");}
100100

101101
if(argc ==1) //First and Foremost, the help option
102102
{ DisplayHelp();

web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "stamp-web",
3-
"version": "0.1.0",
3+
"version": "2.0.0",
44
"private": true,
55
"scripts": {
66
"dev": "next dev",

web/public/stamp-logo.jpg

244 KB
Loading

web/src/app/jobs/[jobId]/page.tsx

Lines changed: 63 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import { JobProgress } from "@/components/job/JobProgress";
77
import { Card, CardHeader, CardTitle } from "@/components/ui/Card";
88
import { Button } from "@/components/ui/Button";
99
import { SequenceLogo } from "@/components/motif/SequenceLogo";
10-
import { RCToggle } from "@/components/motif/RCToggle";
10+
import { LogoToolbar } from "@/components/motif/LogoToolbar";
1111
import { TreeViewer } from "@/components/results/TreeViewer";
1212
import { MatchTable } from "@/components/results/MatchTable";
1313
import { MultipleAlignmentViewer } from "@/components/results/MultipleAlignmentViewer";
14+
import { exportLogosAsPng, exportLogosAsSvg } from "@/lib/export/logoRenderer";
15+
import type { LogoSpec } from "@/lib/export/logoRenderer";
1416
import type { JobResults, StampParams } from "@/types";
1517

1618
interface JobData {
@@ -65,9 +67,6 @@ function CollapsibleSection({
6567
);
6668
}
6769

68-
/**
69-
* Human-readable labels for STAMP parameter values.
70-
*/
7170
const METRIC_LABELS: Record<string, string> = {
7271
PCC: "Pearson Correlation (PCC)",
7372
ALLR: "Average Log-Likelihood Ratio (ALLR)",
@@ -101,8 +100,8 @@ export default function JobPage() {
101100
const { status, error: sseError } = useJobStatus(jobId);
102101
const [job, setJob] = useState<JobData | null>(null);
103102
const [loading, setLoading] = useState(true);
104-
const [fbpRc, setFbpRc] = useState(false);
105103
const [inputRc, setInputRc] = useState(false);
104+
const [inputShowAxes, setInputShowAxes] = useState(true);
106105

107106
const fetchJob = useCallback(async () => {
108107
try {
@@ -118,18 +117,40 @@ export default function JobPage() {
118117
}
119118
}, [jobId]);
120119

121-
// Initial fetch
122120
useEffect(() => {
123121
fetchJob();
124122
}, [fetchJob]);
125123

126-
// Re-fetch when status changes to complete
127124
useEffect(() => {
128125
if (status === "complete" || status === "failed") {
129126
fetchJob();
130127
}
131128
}, [status, fetchJob]);
132129

130+
const handleInputPng = useCallback(() => {
131+
if (!job?.results?.inputMotifs) return;
132+
const specs: LogoSpec[] = job.results.inputMotifs.map((m) => ({
133+
label: m.name,
134+
matrix: m.matrix,
135+
reverseComplement: inputRc,
136+
showAxes: inputShowAxes,
137+
height: 80,
138+
}));
139+
exportLogosAsPng(specs, "input-motifs.png");
140+
}, [job, inputRc, inputShowAxes]);
141+
142+
const handleInputSvg = useCallback(() => {
143+
if (!job?.results?.inputMotifs) return;
144+
const specs: LogoSpec[] = job.results.inputMotifs.map((m) => ({
145+
label: m.name,
146+
matrix: m.matrix,
147+
reverseComplement: inputRc,
148+
showAxes: inputShowAxes,
149+
height: 80,
150+
}));
151+
exportLogosAsSvg(specs, "input-motifs.svg");
152+
}, [job, inputRc, inputShowAxes]);
153+
133154
if (loading) {
134155
return (
135156
<div className="mx-auto max-w-6xl px-4 py-8">
@@ -163,16 +184,19 @@ export default function JobPage() {
163184
<div className="mx-auto max-w-6xl px-4 py-8 space-y-6">
164185
{/* Header */}
165186
<div className="flex items-start justify-between">
166-
<div>
167-
<h1 className="text-2xl font-bold text-gray-900">
168-
Job {jobId.slice(0, 8)}...
169-
</h1>
170-
<p className="text-sm text-gray-500 mt-1">
171-
{job.input.motifCount} motif{job.input.motifCount !== 1 ? "s" : ""}
172-
{job.input.fileName ? ` from ${job.input.fileName}` : ""}
173-
{" \u00B7 "}
174-
Submitted {new Date(job.createdAt).toLocaleString()}
175-
</p>
187+
<div className="flex items-center gap-4">
188+
<img src="/stamp-logo.jpg" alt="STAMP" style={{ width: 150 }} />
189+
<div>
190+
<h1 className="text-2xl font-bold text-gray-900">
191+
Job {jobId.slice(0, 8)}...
192+
</h1>
193+
<p className="text-sm text-gray-500 mt-1">
194+
{job.input.motifCount} motif{job.input.motifCount !== 1 ? "s" : ""}
195+
{job.input.fileName ? ` from ${job.input.fileName}` : ""}
196+
{" \u00B7 "}
197+
Submitted {new Date(job.createdAt).toLocaleString()}
198+
</p>
199+
</div>
176200
</div>
177201
<div className="flex items-center gap-3">
178202
{isComplete && (
@@ -186,14 +210,13 @@ export default function JobPage() {
186210
</div>
187211
</div>
188212

189-
{/* Progress — hidden once results are ready */}
213+
{/* Progress */}
190214
{!isComplete && (
191215
<Card>
192216
<JobProgress status={status} error={sseError || job.error || undefined} />
193217
</Card>
194218
)}
195219

196-
{/* Error display when complete but had errors */}
197220
{isComplete && (sseError || job.error) && (
198221
<div className="p-4 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
199222
{sseError || job.error}
@@ -207,10 +230,16 @@ export default function JobPage() {
207230
<CollapsibleSection
208231
title="Input Parameters"
209232
titleRight={
210-
<RCToggle active={inputRc} onToggle={() => setInputRc((v) => !v)} />
233+
<LogoToolbar
234+
rc={inputRc}
235+
onToggleRc={() => setInputRc((v) => !v)}
236+
showAxes={inputShowAxes}
237+
onToggleAxes={() => setInputShowAxes((v) => !v)}
238+
onDownloadPng={handleInputPng}
239+
onDownloadSvg={handleInputSvg}
240+
/>
211241
}
212242
>
213-
{/* Alignment settings summary */}
214243
{p && (
215244
<div className="mb-4 grid grid-cols-2 gap-x-8 gap-y-1 text-sm">
216245
<div>
@@ -264,7 +293,6 @@ export default function JobPage() {
264293
</div>
265294
)}
266295

267-
{/* Input motif logos */}
268296
{results.inputMotifs && results.inputMotifs.length > 0 && (
269297
<div className="space-y-3">
270298
{results.inputMotifs.map((motif) => (
@@ -275,6 +303,7 @@ export default function JobPage() {
275303
<SequenceLogo
276304
matrix={motif.matrix}
277305
height={80}
306+
showAxes={inputShowAxes}
278307
reverseComplement={inputRc}
279308
/>
280309
</div>
@@ -283,42 +312,31 @@ export default function JobPage() {
283312
)}
284313
</CollapsibleSection>
285314

286-
{/* 2. Multiple Alignment */}
315+
{/* 2. Multiple Alignment + FBP */}
287316
{results.multipleAlignment && results.multipleAlignment.length > 0 && (
288317
<CollapsibleSection title="Multiple Alignment">
289-
<MultipleAlignmentViewer alignment={results.multipleAlignment} />
290-
</CollapsibleSection>
291-
)}
292-
293-
{/* 3. FBP Profile */}
294-
{results.fbpProfile && (
295-
<CollapsibleSection
296-
title="Familial Binding Profile (FBP)"
297-
titleRight={
298-
<RCToggle active={fbpRc} onToggle={() => setFbpRc((v) => !v)} />
299-
}
300-
>
301-
<div className="flex justify-center">
302-
<SequenceLogo
303-
matrix={results.fbpProfile}
304-
height={120}
305-
reverseComplement={fbpRc}
306-
/>
307-
</div>
318+
<MultipleAlignmentViewer
319+
alignment={results.multipleAlignment}
320+
fbp={results.fbpProfile}
321+
/>
308322
</CollapsibleSection>
309323
)}
310324

311-
{/* 4. Similarity Matches */}
325+
{/* 3. Similarity Matches */}
312326
{results.matchPairs && results.matchPairs.length > 0 && (
313327
<CollapsibleSection title="Similarity Matches">
314328
<MatchTable matchPairs={results.matchPairs} />
315329
</CollapsibleSection>
316330
)}
317331

318-
{/* 5. Phylogenetic Tree */}
332+
{/* 4. Phylogenetic Tree */}
319333
{results.treeNewick && (
320334
<CollapsibleSection title="Phylogenetic Tree">
321-
<TreeViewer newick={results.treeNewick} />
335+
<TreeViewer
336+
newick={results.treeNewick}
337+
alignment={results.multipleAlignment || undefined}
338+
internalProfiles={results.internalProfiles || undefined}
339+
/>
322340
</CollapsibleSection>
323341
)}
324342
</>

web/src/app/layout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ export default function RootLayout({
2121
<div className="min-h-screen flex flex-col">
2222
<header className="border-b border-gray-200 bg-white">
2323
<div className="mx-auto max-w-6xl px-4 py-4 flex items-center justify-between">
24-
<a href="/" className="text-xl font-bold text-gray-900">
25-
STAMP
24+
<a href="/" className="flex items-center gap-2">
25+
<img src="/stamp-logo.jpg" alt="STAMP" className="h-14" />
2626
</a>
2727
<nav className="flex gap-6 text-sm text-gray-600">
2828
<a href="/" className="hover:text-gray-900">
@@ -41,7 +41,7 @@ export default function RootLayout({
4141
</header>
4242
<main className="flex-1">{children}</main>
4343
<footer className="border-t border-gray-200 bg-gray-50 py-6 text-center text-sm text-gray-500">
44-
STAMP v1.3 &mdash; Similarity, Tree-building, &amp; Alignment of
44+
STAMP v2.0 &mdash; Similarity, Tree-building, &amp; Alignment of
4545
Motifs and Profiles
4646
</footer>
4747
</div>
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"use client";
2+
3+
interface LogoToolbarProps {
4+
rc: boolean;
5+
onToggleRc: () => void;
6+
showAxes: boolean;
7+
onToggleAxes: () => void;
8+
onDownloadPng: () => void;
9+
onDownloadSvg: () => void;
10+
}
11+
12+
/**
13+
* Toolbar with RC toggle, axes toggle, and PNG/SVG download buttons.
14+
* Used alongside every group of motif logos.
15+
*/
16+
export function LogoToolbar({
17+
rc,
18+
onToggleRc,
19+
showAxes,
20+
onToggleAxes,
21+
onDownloadPng,
22+
onDownloadSvg,
23+
}: LogoToolbarProps) {
24+
const btnBase =
25+
"inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-colors";
26+
const activeClass = "bg-blue-100 text-blue-700 hover:bg-blue-200";
27+
const inactiveClass = "bg-gray-100 text-gray-500 hover:bg-gray-200";
28+
29+
return (
30+
<div className="flex items-center gap-1">
31+
{/* RC toggle */}
32+
<button
33+
onClick={onToggleRc}
34+
title={rc ? "Showing reverse complement" : "Show reverse complement"}
35+
className={`${btnBase} ${rc ? activeClass : inactiveClass}`}
36+
>
37+
<svg
38+
width="14"
39+
height="14"
40+
viewBox="0 0 14 14"
41+
fill="none"
42+
stroke="currentColor"
43+
strokeWidth="1.5"
44+
strokeLinecap="round"
45+
strokeLinejoin="round"
46+
>
47+
<path d="M1 5h10M8 2l3 3-3 3" />
48+
<path d="M13 9H3M6 12L3 9l3-3" />
49+
</svg>
50+
RC
51+
</button>
52+
53+
{/* Axes toggle */}
54+
<button
55+
onClick={onToggleAxes}
56+
title={showAxes ? "Hide axes" : "Show axes"}
57+
className={`${btnBase} ${showAxes ? activeClass : inactiveClass}`}
58+
>
59+
<svg
60+
width="14"
61+
height="14"
62+
viewBox="0 0 14 14"
63+
fill="none"
64+
stroke="currentColor"
65+
strokeWidth="1.5"
66+
strokeLinecap="round"
67+
strokeLinejoin="round"
68+
>
69+
{/* Simple axes icon */}
70+
<path d="M2 2v10h10" />
71+
<path d="M2 8l3-3 2 2 4-4" />
72+
</svg>
73+
Axes
74+
</button>
75+
76+
{/* PNG download */}
77+
<button
78+
onClick={onDownloadPng}
79+
title="Download as PNG"
80+
className={`${btnBase} ${inactiveClass}`}
81+
>
82+
<svg
83+
width="14"
84+
height="14"
85+
viewBox="0 0 14 14"
86+
fill="none"
87+
stroke="currentColor"
88+
strokeWidth="1.5"
89+
strokeLinecap="round"
90+
strokeLinejoin="round"
91+
>
92+
<path d="M7 2v7M4 7l3 3 3-3" />
93+
<path d="M2 10v2h10v-2" />
94+
</svg>
95+
PNG
96+
</button>
97+
98+
{/* SVG download */}
99+
<button
100+
onClick={onDownloadSvg}
101+
title="Download as SVG"
102+
className={`${btnBase} ${inactiveClass}`}
103+
>
104+
<svg
105+
width="14"
106+
height="14"
107+
viewBox="0 0 14 14"
108+
fill="none"
109+
stroke="currentColor"
110+
strokeWidth="1.5"
111+
strokeLinecap="round"
112+
strokeLinejoin="round"
113+
>
114+
<path d="M7 2v7M4 7l3 3 3-3" />
115+
<path d="M2 10v2h10v-2" />
116+
</svg>
117+
SVG
118+
</button>
119+
</div>
120+
);
121+
}

0 commit comments

Comments
 (0)