Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions PanTS-Demo/src/helpers/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,22 @@ describe("buildSearchParams", () => {
expect(params.get("sort_by")).toBe("quality");
expect(params.get("per_page")).toBe("12");
});

it("maps the dataset selection to ?dataset= (empty/both = all)", () => {
expect(buildSearchParams(base).get("dataset")).toBe("all"); // none selected
expect(buildSearchParams({ ...base, dataset: ["PanTS"] }).get("dataset")).toBe("pants");
expect(buildSearchParams({ ...base, dataset: ["CancerVerse"] }).get("dataset")).toBe("cancerverse");
expect(
buildSearchParams({ ...base, dataset: ["PanTS", "CancerVerse"] }).get("dataset")
).toBe("all");
});
});

describe("parseFiltersFromParams", () => {
it("round-trips filters through the URL query string", () => {
const filters: SearchFilters = {
tumor: "tumor",
dataset: ["CancerVerse"],
sex: ["F"],
age: ["50-59"],
manufacturer: ["GE"],
Expand Down
21 changes: 20 additions & 1 deletion PanTS-Demo/src/helpers/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type TumorFilter = "any" | "tumor" | "no_tumor";

export type SearchFilters = {
tumor: TumorFilter;
dataset: string[]; // "PanTS" / "CancerVerse"; empty = both (Any)
sex: string[]; // M / F / UNKNOWN
age: string[]; // "0-9" … "90-99" / "UNKNOWN"
manufacturer: string[]; // scanner manufacturer (from facets)
Expand All @@ -15,6 +16,7 @@ export type SearchFilters = {

export const EMPTY_FILTERS: SearchFilters = {
tumor: "any",
dataset: [],
sex: [],
age: [],
manufacturer: [],
Expand All @@ -24,7 +26,7 @@ export const EMPTY_FILTERS: SearchFilters = {
};

// The multi-select array keys (everything except `tumor`).
export type MultiFilterKey = "sex" | "age" | "manufacturer" | "ctPhase" | "siteNat" | "year";
export type MultiFilterKey = "dataset" | "sex" | "age" | "manufacturer" | "ctPhase" | "siteNat" | "year";

// Minimal shape of an item returned by /api/search and /api/random.
export type SearchItem = {
Expand Down Expand Up @@ -53,6 +55,14 @@ export const buildSearchParams = (
opts: { sortBy?: string; perPage?: number } = {}
): URLSearchParams => {
const params = new URLSearchParams();
// Dataset dispatch → backend ?dataset=. Empty or both = all (show PanTS + CancerVerse);
// exactly one selected restricts to that dataset.
const ds = filters.dataset ?? [];
const hasPanTS = ds.includes("PanTS");
const hasCV = ds.includes("CancerVerse");
if (hasCV && !hasPanTS) params.set("dataset", "cancerverse");
else if (hasPanTS && !hasCV) params.set("dataset", "pants");
else params.set("dataset", "all"); // both or neither → everything
filters.sex.forEach((v) => params.append("sex[]", v));
if (filters.tumor === "tumor") params.set("tumor", "1");
else if (filters.tumor === "no_tumor") params.set("tumor", "0");
Expand All @@ -71,8 +81,14 @@ export const buildSearchParams = (
export const parseFiltersFromParams = (params: URLSearchParams): SearchFilters => {
const tumorRaw = params.get("tumor");
const tumor: TumorFilter = tumorRaw === "1" ? "tumor" : tumorRaw === "0" ? "no_tumor" : "any";
const datasetRaw = (params.get("dataset") || "").toLowerCase();
const dataset =
datasetRaw === "pants" ? ["PanTS"] :
datasetRaw === "cancerverse" || datasetRaw === "cv" ? ["CancerVerse"] :
[]; // "all"/absent → both (Any)
return {
tumor,
dataset,
sex: params.getAll("sex[]"),
age: params.getAll("age_bin[]"),
manufacturer: params.getAll("manufacturer[]"),
Expand All @@ -84,6 +100,9 @@ export const parseFiltersFromParams = (params: URLSearchParams): SearchFilters =

export const countActiveFilters = (f: SearchFilters): number =>
(f.tumor !== "any" ? 1 : 0) +
// dataset only counts as an active filter when it restricts to a single dataset
// (empty or both = "Any", i.e. no restriction).
((f.dataset?.length ?? 0) === 1 ? 1 : 0) +
f.sex.length +
f.age.length +
f.manufacturer.length +
Expand Down
32 changes: 32 additions & 0 deletions PanTS-Demo/src/routes/Homepage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ const TUMOR_OPTIONS: { value: TumorFilter; label: string }[] = [
{ value: "no_tumor", label: "No tumor" },
];

// Which dataset(s) to search. Empty = both (backend ?dataset=all). CancerVerse is
// CT-only (no masks yet), so those cases sort after the mask-complete PanTS cases.
const DATASET_OPTIONS = [
{ value: "PanTS", label: "PanTS" },
{ value: "CancerVerse", label: "CancerVerse" },
];

// Values match the backend /api/search params: sex -> M/F/UNKNOWN, age -> age_bin[].
const SEX_OPTIONS = [
{ value: "M", label: "Male" },
Expand Down Expand Up @@ -698,6 +705,31 @@ export default function Homepage() {
gap: "20px",
}}
>
{/* Dataset */}
<div className="flex flex-col gap-2.5">
<span className="flex items-center gap-2">
<span style={filterLabelStyle}>Dataset</span>
<span style={multiSelectTagStyle}>Multi-Select</span>
</span>
<div className="flex flex-wrap gap-2">
<button
style={pillStyle(filters.dataset.length === 0)}
onClick={() => setFilters((f) => ({ ...f, dataset: [] }))}
>
Any
</button>
{DATASET_OPTIONS.map((opt) => (
<button
key={opt.value}
style={pillStyle(filters.dataset.includes(opt.value))}
onClick={() => toggleMulti("dataset", opt.value)}
>
{opt.label}
</button>
))}
</div>
</div>

{/* Tumor */}
<div className="flex flex-col gap-2.5">
<span style={filterLabelStyle}>Tumor</span>
Expand Down
Loading