Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2650f7a
Preview view kinds: one registry, a header badge, and the method chec…
alisman Oct 23, 2026
7a8c823
acquired view (preview): alterations acquired under therapy, before v…
alisman Oct 23, 2026
d17888a
ctdna view (preview): plasma cfDNA against tissue, agreement per gene…
alisman Oct 23, 2026
559240e
exclusivity view (preview): mutual exclusivity against each tumour's …
alisman Oct 23, 2026
b4415a6
txendpoints view (preview): real-world treatment endpoints from a tre…
alisman Oct 23, 2026
8485bbf
predictive view (preview): gene × treatment interaction per gene
alisman Oct 23, 2026
ed992d6
cutpoint view (preview): a biomarker's best survival cut, corrected f…
alisman Oct 23, 2026
f5c2fef
forest view (preview): one exposure's hazard ratio per cancer type, r…
alisman Oct 23, 2026
1dc73ba
adjusted view (preview): covariate-adjusted alteration enrichment bet…
alisman Oct 23, 2026
c31bcd7
instability view (preview): aneuploidy score, FGA and arm-level gains…
alisman Oct 23, 2026
9a60ab3
riskmodel option validateOn (preview): external validation on another…
alisman Oct 23, 2026
37fbe05
actionability view (preview): highest OncoKB level per patient, and p…
alisman Oct 23, 2026
8120a6b
drivernegative view (preview): genes enriched in patients without a c…
alisman Oct 23, 2026
3eada16
e2e: scroll the oncoprint to the top before hovering or dragging on it
alisman Oct 23, 2026
0e42092
NEW mark on view kinds cBioPortal has no counterpart for
alisman Oct 23, 2026
382af7d
NEW mark: orange text, and on the Examples tab's filter chips
alisman Oct 23, 2026
16adf12
Examples tab: the views and topics as link lists in columns
alisman Oct 23, 2026
7e84125
Add a view: pills on a grey fill
alisman Oct 23, 2026
b9bbf2b
multistate view (preview): the illness-death model, start to progress…
alisman Oct 23, 2026
22a6fcb
rmst view (preview): restricted mean survival time and landmark analysis
alisman Oct 23, 2026
9ecc5a5
hla view (preview): HLA class I homozygosity and HLA LOH, and surviva…
alisman Oct 23, 2026
422197c
ctdna option chFilter (preview): clonal-haematopoiesis-aware plasma c…
alisman Oct 23, 2026
ec97883
organotropism view (preview): which genes go with metastasis to which…
alisman Oct 23, 2026
6fd6635
molsubtype view (preview): endometrial molecular subtypes per patient…
alisman Oct 23, 2026
2dabe75
germline view (preview): germline carriers per gene and their tumours…
alisman Oct 23, 2026
741a4a7
Preview views read stored numbers through finiteSql
alisman Oct 23, 2026
27b049b
View settings take uploaded columns wherever they take a clinical att…
alisman Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ ClickHouse SQL; clients never send SQL. Live at https://cbiolab.vercel.app (UI a
error for a kind known only at run time is `plainResult(kind, {…})`. **Kinds never import each other:** a
kind's runner, figure and `<kind>-<part>.ts` modules import only one another and the shared
modules (`lib/views/`: run, shared, rows, covariates; `lib/figures/`: common, volcano, bars,
forest) — `local/kind-boundary` in `eslint.config.mjs` enforces it; what two kinds need moves
forest, km) — `local/kind-boundary` in `eslint.config.mjs` enforces it; what two kinds need moves
into a shared module, listed there. Kinds registered through `drawn()`
have their figure drawn from the result and a throw reported as the view's error.
- **`lib/oncoprint.ts`** — spec → rows (ClickHouse fetch); **`lib/oncoprint-assemble.ts`** — rows →
Expand Down
8 changes: 6 additions & 2 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
--color-border: var(--line); --color-input: var(--line); --color-ring: var(--accent);
--color-paper: var(--paper); --color-link: var(--accent-ink);
--color-ok: var(--ok); --color-ok-soft: var(--ok-soft);
--color-new: var(--new);
--color-mark: var(--mark); --color-mark-soft: var(--mark-soft);
--color-amp: var(--amp); --color-del: var(--del);
--color-white: #fff; --color-black: #000; --color-transparent: transparent; --color-current: currentColor;
Expand Down Expand Up @@ -89,6 +90,8 @@
--accent: #0f86a1; --accent-ink: #0b6478; --accent-soft: #e3f0f3;
--line: #dde4e8; --code-bg: #f1f5f7; --mark: #b25e09; --mark-soft: #fbf0e2;
--ok: #2e7d32; --ok-soft: #e6f2e6; --err: #b3261e; --err-soft: #fbeae9;
/* the NEW mark's text (app/lab/new-mark.tsx) */
--new: #ea580c;
/* copy-number calls as text: amplification red, deep deletion blue */
--amp: #c62828; --del: #1f5fbf;

Expand All @@ -114,6 +117,7 @@
--accent: #45b8c2; --accent-ink: #7fd3da; --accent-soft: #16333a;
--line: #2a353c; --code-bg: #1a242a; --mark: #e0a35c; --mark-soft: #35270f;
--ok: #7bc47f; --ok-soft: #17301a; --err: #f2b8b5; --err-soft: #3a2323;
--new: #fb923c;
--amp: #ff8a80; --del: #82b1ff;
--on-accent: #0d1417;
}
Expand Down Expand Up @@ -669,8 +673,8 @@ main.analysis-main { max-width: min(132rem, 100%); }
.extag.on { background: var(--accent-ink); border-color: var(--accent-ink); color: var(--bg); }
.extag.on .meta { color: var(--bg); opacity: 0.8; }
.extag.is-static { cursor: default; }
.exfacet { align-items: baseline; }
.exfacet-l { font-size: var(--t-xs); color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; min-width: 3.6rem; }
.exfacet { margin-top: var(--s4); }
.exfacet-l { font-size: var(--t-sm); font-weight: 700; color: var(--ink); text-transform: uppercase; letter-spacing: 0.06em; padding-bottom: var(--s1); border-bottom: 1px solid var(--line); }
.exempty { color: var(--muted); }
.vkind {
font-size: var(--t-xs); padding: 0.1rem 0.45rem; border-radius: var(--r-pill); white-space: nowrap;
Expand Down
11 changes: 8 additions & 3 deletions app/lab/analysis-sections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,17 @@ import type { DocStore } from "./doc-store.ts";
import type { SavedCohort, SavedWorkspace } from "./use-saved.ts";
import type { ExploreResult } from "../../lib/types.ts";
import type { ViewCtx } from "./view-cards.tsx";
import { isNewKind } from "../../lib/new-in-lab.ts";
import { NewMark } from "./new-mark.tsx";

/** A view kind already in the document: filled with the accent colour, so "on" cannot be mistaken
* for a chip still loading (a pale pulsing fill). */
const VIEW_ON = "aria-pressed:bg-primary aria-pressed:text-primary-foreground hover:aria-pressed:bg-primary/90 " +
"hover:aria-pressed:text-primary-foreground dark:aria-pressed:bg-primary dark:hover:aria-pressed:bg-primary/90";

/** A kind not yet in the document: a grey fill, so each pill reads apart from its neighbours on the white card. */
const ADD_PILL = "border-transparent bg-border text-foreground hover:bg-border/70";

/** What the Analysis tab's sections read from the page and the actions they can take; the page builds one per render. */
export interface AnalysisCtx {
VIEW_KINDS: { kind: string; label: string; why?: () => string | null; loading?: () => boolean; }[];
Expand Down Expand Up @@ -448,7 +453,7 @@ export function ViewColumn({ ctx }: { ctx: AnalysisCtx }) {
if (present) return (
<Button key={kind} variant="outline" size="xs" className={cn(CHIP, VIEW_ON)} aria-pressed disabled={busy}
title={"remove the " + label.toLowerCase() + " view" + (present > 1 ? "s (" + present + ")" : "")}
onClick={() => removeViews((v) => v.kind === kind)}>{label}{present > 1 && " ×" + present}<X /></Button>
onClick={() => removeViews((v) => v.kind === kind)}>{label}{isNewKind(kind) && <NewMark />}{present > 1 && " ×" + present}<X /></Button>
);
const full = (doc.views || []).length >= 6;
// with no cohorts yet there is no study to ask about: the view's cohorts decide later
Expand All @@ -458,10 +463,10 @@ export function ViewColumn({ ctx }: { ctx: AnalysisCtx }) {
: waiting ? "loading this study's catalog — a moment"
: (known && why ? why() : null);
return (
<Button key={kind} variant="outline" size="xs" className={cn(CHIP, waiting && "chip-wave disabled:opacity-100")} aria-pressed={false}
<Button key={kind} variant="outline" size="xs" className={cn(CHIP, ADD_PILL, waiting && "chip-wave disabled:opacity-100")} aria-pressed={false}
aria-busy={waiting || undefined} disabled={!!blocked}
title={blocked || ("add a " + label.toLowerCase() + " view, then choose its cohorts")}
onClick={() => addView(kind)}><Plus />{label}</Button>
onClick={() => addView(kind)}><Plus />{label}{isNewKind(kind) && <NewMark />}</Button>
);
})}
</div>
Expand Down
23 changes: 16 additions & 7 deletions app/lab/examples-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,21 @@ import { parseExamplesUrl } from "../../lib/site.ts";
import type { CohortDef } from "../../lib/types.ts";
import { Tabs, TabsList, TabsTrigger } from "../_ui/tabs";
import CohortPills from "./pills";
import { isNewKind } from "../../lib/new-in-lab.ts";
import { NewMark } from "./new-mark.tsx";

/** a tag filter: an outline chip, filled while it is the one shown */
const TAG = "group rounded-pill px-2.5 text-sm font-normal aria-pressed:border-link aria-pressed:bg-link aria-pressed:text-card aria-pressed:hover:bg-link/90 aria-pressed:hover:text-card";
/** the views and topics lists: links flowing down columns, the chosen one in bold */
const FACET_COLS = "mt-1 columns-[11rem] gap-x-6";
const FACET_ITEM = "items-baseline text-sm font-normal aria-pressed:font-semibold aria-pressed:text-foreground aria-pressed:underline";
const TAG_N = "text-muted-foreground group-aria-pressed:text-card/80";
const FEATURES = new Map(EXAMPLES.map(e => [e, exampleFeatures(e)]));
const byName = (a: string, b: string) => a.localeCompare(b, undefined, { sensitivity: "base" });
const VIEWS = [...new Set([...FEATURES.values()].flatMap(f => f.views))].sort(byName);
const TOPICS = [...new Set([...FEATURES.values()].flatMap(f => f.topics))].sort(byName);
/** chips (a view kind, or a topic) under which at least one example draws a NEW view */
const HAS_NEW = new Set([...FEATURES.values()].filter(f => f.views.some(isNewKind)).flatMap(f => [...f.views.filter(isNewKind), ...f.topics]));
const has = (ex: DslExample, t: string) => { const f = FEATURES.get(ex)!; return f.views.includes(t) || f.topics.includes(t); };

const hostOf = (url: string) => { try { return new URL(url).host; } catch { return url; } };
Expand Down Expand Up @@ -85,12 +92,14 @@ export function ExamplesTab({ tag, onTag, query, onQuery, busy, onRun, link, els
useEffect(() => () => onQuery(latest.current), [onQuery]);
const searched = EXAMPLES.filter(e => matchesSearch(e, draft));
const shown = tag === "all" ? searched : searched.filter(e => has(e, tag));
const chip = (t: string) => {
const item = (t: string) => {
const n = searched.filter(e => has(e, t)).length;
return (
<Button key={t} variant="outline" size="xs" className={cn(TAG, n === 0 && "opacity-50")} aria-pressed={tag === t} onClick={() => onTag(tag === t ? "all" : t)}>
{t} <span className={TAG_N}>{n}</span>
</Button>
<div key={t} className="break-inside-avoid py-0.5">
<Button variant="link" size="inline" className={cn(FACET_ITEM, n === 0 && "opacity-50")} aria-pressed={tag === t} onClick={() => onTag(tag === t ? "all" : t)}>
<span className="inline-block first-letter:uppercase">{t}</span> <span className="text-muted-foreground">{n}</span>{HAS_NEW.has(t) && <NewMark />}
</Button>
</div>
);
};

Expand Down Expand Up @@ -132,8 +141,8 @@ export function ExamplesTab({ tag, onTag, query, onQuery, busy, onRun, link, els
all <span className={TAG_N}>{searched.length}</span>
</Button>
</div>
<div className="extags exfacet"><span className="exfacet-l">views</span>{VIEWS.map(chip)}</div>
<div className="extags exfacet"><span className="exfacet-l">topics</span>{TOPICS.map(chip)}</div>
<div className="exfacet"><div className="exfacet-l">views</div><div className={FACET_COLS}>{VIEWS.map(item)}</div></div>
<div className="exfacet"><div className="exfacet-l">topics</div><div className={FACET_COLS}>{TOPICS.map(item)}</div></div>
</div>

{shown.length === 0 && (
Expand All @@ -152,7 +161,7 @@ export function ExamplesTab({ tag, onTag, query, onQuery, busy, onRun, link, els
const { views, topics } = FEATURES.get(ex)!;
return (
<div className="card excard" key={ex.title}>
<h3>{ex.title}</h3>
<h3>{ex.title}{views.some(isNewKind) && <NewMark />}</h3>
<div className="extags small">
{views.map(k => <span key={k} className="vkind" title="a view this example draws">{k}</span>)}
{topics.map(t => <span key={t} className="extag is-static">{t}</span>)}
Expand Down
Loading
Loading