Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
FileSummaryTerm,
FormFacet,
} from "@databiosphere/findable-ui/lib/components/Export/common/entities";
import { ExportMethod } from "@databiosphere/findable-ui/lib/components/Export/components/ExportMethod/exportMethod";
import { CurrentQuery } from "@databiosphere/findable-ui/lib/components/Export/components/ExportSummary/components/ExportCurrentQuery/exportCurrentQuery";
import { Summary } from "@databiosphere/findable-ui/lib/components/Export/components/ExportSummary/components/ExportSelectedDataSummary/exportSelectedDataSummary";
import { ANCHOR_TARGET } from "@databiosphere/findable-ui/lib/components/Links/common/entities";
Expand All @@ -36,13 +37,15 @@ import {
ChipProps as MChipProps,
FadeProps as MFadeProps,
} from "@mui/material";
import { ExportEntity } from "app/components/Export/components/AnVILExplorer/components/ExportEntity/exportEntity";
import React, { ComponentProps, ReactNode } from "react";
import {
ANVIL_CMG_CATEGORY_KEY,
ANVIL_CMG_CATEGORY_LABEL,
DATASET_RESPONSE,
} from "../../../../../site-config/anvil-cmg/category";
import { ROUTES } from "../../../../../site-config/anvil-cmg/dev/export/routes";
import { mapDiagnosisValue } from "../../../../../site-config/anvil-cmg/dev/index/common/utils";
import {
AggregatedBioSampleResponse,
AggregatedDatasetResponse,
Expand Down Expand Up @@ -97,14 +100,12 @@ import {
} from "../../../../apis/azul/common/utils";
import * as C from "../../../../components";
import * as MDX from "../../../../components/common/MDXContent/anvil-cmg";
import { RequestAccess } from "../../../../components/Detail/components/AnVILCMG/components/RequestAccess/requestAccess";
import { Description } from "../../../../components/Detail/components/MDX/components/Description/description";
import { ExportMethod } from "@databiosphere/findable-ui/lib/components/Export/components/ExportMethod/exportMethod";
import { METADATA_KEY } from "../../../../components/Index/common/entities";
import { getPluralizedMetadataLabel } from "../../../../components/Index/common/indexTransformer";
import { SUMMARY_DISPLAY_TEXT } from "./summaryMapper/constants";
import { mapExportSummary } from "./summaryMapper/summaryMapper";
import { ExportEntity } from "app/components/Export/components/AnVILExplorer/components/ExportEntity/exportEntity";
import { RequestAccess } from "../../../../components/Detail/components/AnVILCMG/components/RequestAccess/requestAccess";

/**
* Build props for activity type BasicCell component from the given activities response.
Expand Down Expand Up @@ -662,7 +663,7 @@ export const buildDiagnoses = (
): React.ComponentProps<typeof C.NTagCell> => {
return {
label: getPluralizedMetadataLabel(METADATA_KEY.DIAGNOSIS),
values: getAggregatedDiagnoses(response),
values: getAggregatedDiagnoses(response).map(mapDiagnosisValue),
};
};

Expand Down
178 changes: 178 additions & 0 deletions scripts/lookup-diagnosis-terms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
#!/usr/bin/env npx ts-node
/**
* Fetch HP and OMIM term names from authoritative sources and generate a JS constant.
*
* - HP terms: parsed from the official hp.obo file (obophenotype GitHub)
* - OMIM terms: parsed from the HPO phenotype.hpoa annotations file
* - Term IDs: extracted live from the AnVIL Azul API
*
* Usage: npx ts-node scripts/lookup-diagnosis-terms.ts > site-config/anvil-cmg/dev/index/common/diagnosis.ts
*/

const AZUL_URL =
"https://service.explore.anvilproject.org/index/datasets?size=1&filters=%7B%7D";
const HP_OBO_URL =
"https://raw.githubusercontent.com/obophenotype/human-phenotype-ontology/master/hp.obo";
const HPOA_URL =
"https://github.com/obophenotype/human-phenotype-ontology/releases/latest/download/phenotype.hpoa";

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- API response shape is dynamic
async function fetchJson(url: string): Promise<any> {
const resp = await fetch(url);
if (!resp.ok) throw new Error(`Failed to fetch ${url}: ${resp.status}`);
return resp.json();
}

async function fetchText(url: string): Promise<string> {
const resp = await fetch(url, { redirect: "follow" });
if (!resp.ok) throw new Error(`Failed to fetch ${url}: ${resp.status}`);
return resp.text();
}

async function getTermIdsFromAzul(): Promise<{
hpIds: Set<string>;
omimIds: Set<string>;
}> {
console.error("Fetching term IDs from AnVIL Azul API...");
const data = await fetchJson(AZUL_URL);
const facets = data.termFacets ?? {};

const hpIds = new Set<string>();
const omimIds = new Set<string>();

for (const key of ["diagnoses.disease", "diagnoses.phenotype"]) {
const terms = facets[key]?.terms ?? [];
for (const t of terms) {
const term: string | undefined = t.term;
if (!term) continue;
// Some entries have multiple IDs separated by semicolons
for (const part of term.split(";")) {
const trimmed = part.trim();
if (trimmed.startsWith("HP:")) hpIds.add(trimmed);
else if (trimmed.startsWith("OMIM:")) omimIds.add(trimmed);
}
}
}

console.error(` Found ${hpIds.size} HP terms, ${omimIds.size} OMIM terms`);
return { hpIds, omimIds };
}

async function buildHpMap(hpIds: Set<string>): Promise<Map<string, string>> {
console.error("Downloading hp.obo...");
const obo = await fetchText(HP_OBO_URL);

const hpNames = new Map<string, string>();
let currentId: string | null = null;
let currentName: string | null = null;
let altIds: string[] = [];

const saveCurrent = (): void => {
if (currentId && currentName) {
if (hpIds.has(currentId)) hpNames.set(currentId, currentName);
for (const alt of altIds) {
if (hpIds.has(alt)) hpNames.set(alt, currentName);
}
}
};

for (const line of obo.split("\n")) {
const trimmed = line.trim();
if (trimmed === "[Term]") {
saveCurrent();
currentId = null;
currentName = null;
altIds = [];
} else if (trimmed.startsWith("id: HP:")) {
currentId = trimmed.slice(4);
} else if (trimmed.startsWith("name: ") && currentId) {
currentName = trimmed.slice(6).replace(/^obsolete\s+/, "");
} else if (trimmed.startsWith("alt_id: HP:")) {
altIds.push(trimmed.slice(8));
}
}
saveCurrent();

console.error(` Resolved ${hpNames.size}/${hpIds.size} HP terms`);
const missing = [...hpIds].filter((id) => !hpNames.has(id));
if (missing.length)
console.error(` Missing HP terms: ${missing.sort().join(", ")}`);
return hpNames;
}

async function buildOmimMap(
omimIds: Set<string>
): Promise<Map<string, string>> {
console.error("Downloading phenotype.hpoa...");
const hpoa = await fetchText(HPOA_URL);

const omimNames = new Map<string, string>();
for (const line of hpoa.split("\n")) {
if (line.startsWith("#") || line.startsWith("database_id")) continue;
const parts = line.split("\t");
if (parts.length < 2) continue;
const dbId = parts[0].trim();
const diseaseName = parts[1].trim();
if (omimIds.has(dbId) && !omimNames.has(dbId)) {
omimNames.set(dbId, diseaseName);
}
}

console.error(` Resolved ${omimNames.size}/${omimIds.size} OMIM terms`);
const missing = [...omimIds].filter((id) => !omimNames.has(id));
if (missing.length)
console.error(` Missing OMIM terms: ${missing.sort().join(", ")}`);
return omimNames;
}

function generateJs(mapping: Map<string, string>): string {
const lines: string[] = [];
lines.push("/**");
lines.push(
" * Mapping of HP (Human Phenotype Ontology) and OMIM term IDs to their names."
);
lines.push(
" * Auto-generated by scripts/lookup-diagnosis-terms.ts from authoritative sources:"
);
lines.push(
" * - HP terms: hp.obo from obophenotype/human-phenotype-ontology"
);
lines.push(" * - OMIM terms: phenotype.hpoa from HPO project");
lines.push(" * - Term IDs: AnVIL Azul API (explore.anvilproject.org)");
lines.push(" */");
lines.push(
"export const DIAGNOSIS_DISPLAY_VALUE: Record<string, string> = {"
);

const sortedKeys = [...mapping.keys()].sort();
for (const termId of sortedKeys) {
const name = mapping.get(termId)!;
const escaped = name.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
lines.push(` "${termId}": "${escaped}",`);
}

lines.push("};");
lines.push("");
return lines.join("\n");
}

async function main(): Promise<void> {
const { hpIds, omimIds } = await getTermIdsFromAzul();
const [hpMap, omimMap] = await Promise.all([
buildHpMap(hpIds),
buildOmimMap(omimIds),
]);

const combined = new Map<string, string>([...hpMap, ...omimMap]);
const js = generateJs(combined);
process.stdout.write(js);

console.error(
`\nGenerated mapping for ${combined.size} terms (${hpMap.size} HP + ${omimMap.size} OMIM)`
);
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
13 changes: 7 additions & 6 deletions site-config/anvil-cmg/dev/config.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { APIEndpoints } from "@databiosphere/findable-ui/lib/apis/azul/common/entities";
import { FILTER_SORT } from "@databiosphere/findable-ui/lib/common/filters/sort/config/types";
import { SystemStatusBindResponseFn } from "@databiosphere/findable-ui/lib/config/entities";
import { CATALOG_DEFAULT } from "../../../app/apis/azul/anvil-cmg/common/constants";
import * as C from "../../../app/components/index";
import { mapSelectCategoryValue } from "../../../app/config/utils";
import { buildDataDictionary } from "../../../app/viewModelBuilders/azul/anvil-cmg/common/dataDictionaryMapper/dataDictionaryMapper";
import { TABLE_OPTIONS } from "../../../app/viewModelBuilders/azul/anvil-cmg/common/dataDictionaryMapper/tableOptions";
import { bindSystemStatusResponse } from "../../../app/viewModelBuilders/azul/common/systemStatusMapper/systemStatusMapper";
import { FLATTEN, GIT_HUB_REPO_URL } from "../../common/constants";
import { SiteConfig } from "../../common/entities";
import { ANVIL_CMG_CATEGORY_KEY, ANVIL_CMG_CATEGORY_LABEL } from "../category";
import { announcements } from "./announcements/announcements";
import { authenticationConfig } from "./authentication/authentication";
import dataDictionary from "./dataDictionary/data-dictionary.json";
import { exportConfig } from "./export/export";
import { activitiesEntityConfig } from "./index/activitiesEntityConfig";
import { biosamplesEntityConfig } from "./index/biosamplesEntityConfig";
import { mapAccessibleValue } from "./index/common/utils";
import { mapAccessibleValue, mapDiagnosisValue } from "./index/common/utils";
import { datasetsEntityConfig } from "./index/datasetsEntityConfig";
import { donorsEntityConfig } from "./index/donorsEntityConfig";
import { filesEntityConfig } from "./index/filesEntityConfig";
import { floating } from "./layout/floating";
import dataDictionary from "./dataDictionary/data-dictionary.json";
import { TABLE_OPTIONS } from "../../../app/viewModelBuilders/azul/anvil-cmg/common/dataDictionaryMapper/tableOptions";
import { buildDataDictionary } from "../../../app/viewModelBuilders/azul/anvil-cmg/common/dataDictionaryMapper/dataDictionaryMapper";
import { buildSummaries } from "./index/summaryViewModelBuilder";
import { FILTER_SORT } from "@databiosphere/findable-ui/lib/common/filters/sort/config/types";
import { floating } from "./layout/floating";

// Template constants
const APP_TITLE = "AnVIL Data Explorer";
Expand Down Expand Up @@ -78,6 +78,7 @@ export function makeConfig(
{
key: ANVIL_CMG_CATEGORY_KEY.DIAGNOSE_DISEASE,
label: ANVIL_CMG_CATEGORY_LABEL.DIAGNOSE_DISEASE,
mapSelectCategoryValue: mapSelectCategoryValue(mapDiagnosisValue),
},
{
key: ANVIL_CMG_CATEGORY_KEY.DONOR_ORGANISM_TYPE,
Expand Down
Loading
Loading