diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 578721824..acabc4b0d 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -30,7 +30,7 @@ Disabling the banner is as simple as commenting out this section. --> - + diff --git a/frontend/src/components/AppAssociationTabs.vue b/frontend/src/components/AppAssociationTabs.vue new file mode 100644 index 000000000..c02e71879 --- /dev/null +++ b/frontend/src/components/AppAssociationTabs.vue @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + diff --git a/frontend/src/components/AppBackToTopButton.vue b/frontend/src/components/AppBackToTopButton.vue new file mode 100644 index 000000000..7db8cb764 --- /dev/null +++ b/frontend/src/components/AppBackToTopButton.vue @@ -0,0 +1,61 @@ + + + + + + + + + diff --git a/frontend/src/components/AppButton.vue b/frontend/src/components/AppButton.vue index 8d6b9cf53..bfb5942e3 100644 --- a/frontend/src/components/AppButton.vue +++ b/frontend/src/components/AppButton.vue @@ -7,8 +7,21 @@ :type="type" @click="copy ? copyToClipboard(text) : click" > + {{ text }} - + + + + @@ -21,29 +34,38 @@ type Props = { text?: string; /** icon to show */ icon?: string; + /** where to place the icon relative to text */ + iconPosition?: "left" | "right"; /** location to link to */ to?: string; /** on click action */ click?: () => unknown; /** visual design */ - design?: "normal" | "circle" | "small" | "tile"; + design?: "normal" | "circle" | "small" | "tile" | "big" | "link"; /** color */ color?: "primary" | "secondary" | "none"; /** whether to copy text prop to clipboard on click */ copy?: boolean; /** html button type attribute */ type?: string; + /** show an “i” info icon on the right */ + info?: boolean; + /** tooltip text for the info icon */ + infoTooltip?: string; }; const props = withDefaults(defineProps(), { text: "", icon: "", + iconPosition: "right", to: "", click: undefined, design: "normal", color: "primary", copy: false, type: "button", + info: false, + infoTooltip: "", }); /** element ref */ @@ -84,7 +106,6 @@ defineExpose({ button }); &.primary { background: $theme-light; } - &.secondary { background: $light-gray; } @@ -106,6 +127,12 @@ defineExpose({ button }); width: fit-content; } + &.big { + min-width: min(300px, 100% - 40px); + padding: 0px 30px; + font-size: 1.75rem; + } + &.circle { border-radius: 999px; color: $off-black; @@ -115,7 +142,6 @@ defineExpose({ button }); min-height: 2em; padding: 0.25em 0.75em; } - &:not(.text) { width: 2.5em; height: 2.5em; @@ -124,7 +150,6 @@ defineExpose({ button }); &.primary { background: $theme-light; } - &.secondary { background: $light-gray; } @@ -143,11 +168,9 @@ defineExpose({ button }); &.primary { color: $theme; } - &.secondary { color: $off-black; } - &:hover, &:focus { color: $black; @@ -166,7 +189,6 @@ defineExpose({ button }); &.primary { background: $theme-light; } - &.secondary { background: $light-gray; } @@ -177,13 +199,61 @@ defineExpose({ button }); box-shadow: $outline; } } + + /* link-style button*/ + &.link { + min-height: unset; + padding: 0; + gap: 6px; + background: transparent; + color: $theme; + text-decoration: none; + + &:hover .truncate, + &:focus .truncate { + text-decoration: underline; + text-decoration-thickness: 2px; + text-underline-offset: 3px; + } + + .truncate { + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 3px; + } + + &:focus { + border-radius: 6px; + outline: none; + box-shadow: 0 0 0 3px rgba($theme, 0.25); + } + } } - - diff --git a/frontend/src/components/AppCitation.vue b/frontend/src/components/AppCitation.vue index 5371bf9fa..142161b46 100644 --- a/frontend/src/components/AppCitation.vue +++ b/frontend/src/components/AppCitation.vue @@ -33,6 +33,15 @@ type Props = { const props = defineProps(); +/** name/label */ +const title = computed(() => props.title || ""); + +/** link */ +const link = computed(() => props.link || ""); + +/** authors */ +const authors = computed(() => props.authors || ""); + /** joined details as string */ const detailsString = computed(() => (props.details || []).filter((e) => e).join(" · "), diff --git a/frontend/src/components/AppDownloadTable.vue b/frontend/src/components/AppDownloadTable.vue index 4d01da34d..683f8d9c0 100644 --- a/frontend/src/components/AppDownloadTable.vue +++ b/frontend/src/components/AppDownloadTable.vue @@ -17,7 +17,9 @@ import type { DownloadItem } from "@/data/downloads"; import AppButton from "./AppButton.vue"; -defineProps<{ items: DownloadItem[] }>(); +const { items } = withDefaults(defineProps<{ items?: DownloadItem[] }>(), { + items: () => [], +}); diff --git a/frontend/src/components/TheHeroTools.vue b/frontend/src/components/TheHeroTools.vue index ea28bbe48..db3ff11e1 100644 --- a/frontend/src/components/TheHeroTools.vue +++ b/frontend/src/components/TheHeroTools.vue @@ -1,29 +1,39 @@ - Phenotype Similarity Tools - | - Text Annotator + Phenotype Similarity Tools + + Text Annotator diff --git a/frontend/src/components/TheSearchTerms.vue b/frontend/src/components/TheSearchTerms.vue index cca521ebb..d10c55828 100644 --- a/frontend/src/components/TheSearchTerms.vue +++ b/frontend/src/components/TheSearchTerms.vue @@ -16,7 +16,7 @@ import AppNodeBadge from "@/components/AppNodeBadge.vue"; const searchSuggestions = [ { term: "Ehlers-Danlos syndrome", - id: "MONDO:0007523", + id: "MONDO:0020066", category: "biolink:Disease", }, { term: "Fanconi anemia", id: "MONDO:0019391", category: "biolink:Disease" }, diff --git a/frontend/src/components/TheTableOfContents.vue b/frontend/src/components/TheTableOfContents.vue index 8b03280ac..084e58e32 100644 --- a/frontend/src/components/TheTableOfContents.vue +++ b/frontend/src/components/TheTableOfContents.vue @@ -13,6 +13,7 @@ aria-label="Page table of contents" @click.stop > + { + const opts = + node.association_counts?.map((ac) => ({ + id: ac.category || "", + label: startCase(ac.label), + count: ac.count, + })) ?? []; + + const ordered = opts.filter((o) => !HIDDEN_CATEGORIES.has(o.id)); + + // keep current special-order rule exactly + const idxCausal = ordered.findIndex( + (i) => i.id === "biolink:CausalGeneToDiseaseAssociation", + ); + const idxGenePh = ordered.findIndex( + (i) => i.id === "biolink:GeneToPhenotypicFeatureAssociation", + ); + if (idxCausal > -1 && idxGenePh > -1 && idxCausal > idxGenePh) { + const [causal] = ordered.splice(idxCausal, 1); + ordered.splice(idxGenePh, 0, causal); + } + return ordered; + }); + return { options }; +} diff --git a/frontend/src/composables/use-clinical-resources.ts b/frontend/src/composables/use-clinical-resources.ts new file mode 100644 index 000000000..ce4a556b9 --- /dev/null +++ b/frontend/src/composables/use-clinical-resources.ts @@ -0,0 +1,95 @@ +import { computed } from "vue"; +import type { ExpandedCurie, Node } from "@/api/model"; + +const RESOURCE_DEFS = [ + { + prefix: "OMIM:", + label: "OMIM", + tooltip: + "Online Mendelian Inheritance in Man: A curated catalog of human genes, variants, and genetic phenotypes—primarily for clinicians and researchers.", + }, + { + prefix: "NORD:", + label: "NORD", + tooltip: + "National Organization for Rare Disorders: Lay-friendly rare disease summaries, advocacy resources, and support services—for patients, families, and healthcare providers", + }, + { + prefix: "GARD:", + label: "GARD", + tooltip: + "Genetic and Rare Diseases Information Center: health information on genetic and rare conditions—for patients, caregivers, and providers seeking plain-language guidance.", + }, + { + prefix: "Orphanet:", + label: "ORPHANET", + tooltip: + "Orphanet: European reference portal on rare diseases and orphan drugs with disease pages, epidemiology, coding, and expert centers—for clinicians, researchers, policymakers, and patient communities.", + }, + { + prefix: "MEDGEN:", + label: "MEDGEN", + tooltip: + "Medical Genetics: NCBI’s structured knowledge base linking diseases, phenotypes, genes, and clinical resources—for clinical geneticists, informaticians, and researchers", + }, +] as const; + +const RESOURCE_PREFIXES = RESOURCE_DEFS.map((rec) => rec.prefix); + +// helper: does an id start with any clinical prefix? +const isClinicalId = (id: string) => + RESOURCE_PREFIXES.some((pre) => id.startsWith(pre)); + +export type ClinicalResourceEntry = { + id: string; + url: string; + label: string; + source: "external" | "mapping"; + tooltip?: string; +}; + +export function useClinicalResources(node: Node) { + const clinicalResources = computed(() => { + const out: ClinicalResourceEntry[] = []; + for (const { prefix, label, tooltip } of RESOURCE_DEFS) { + const ext = node.external_links?.find((link: ExpandedCurie) => + link.id.startsWith(prefix), + ); + if (ext) { + out.push({ + id: ext.id, + url: ext.url || "", + label, + source: "external", + tooltip, + }); + continue; + } + const map = node.mappings?.find((item: ExpandedCurie) => + item.id.startsWith(prefix), + ); + if (map) { + out.push({ + id: map.id, + url: map.url || "", + label, + source: "mapping", + tooltip, + }); + } + } + return out; + }); + + // mappings not in clinical set + const otherMappings = computed( + () => node.mappings?.filter((mapping) => !isClinicalId(mapping.id)) || [], + ); + + // external refs not in clinical set + const externalRefs = computed( + () => node.external_links?.filter((link) => !isClinicalId(link.id)) || [], + ); + + return { clinicalResources, otherMappings, externalRefs }; +} diff --git a/frontend/src/config/resourceNames.ts b/frontend/src/config/resourceNames.ts new file mode 100644 index 000000000..24ac91526 --- /dev/null +++ b/frontend/src/config/resourceNames.ts @@ -0,0 +1,95 @@ +export const RESOURCE_NAME_MAP: Record = { + MGI: "Mouse Genome Informatics", + STRING: "Search Tool for the Retrieval of Interacting Genes/Proteins", + BIOGRID: "Biological General Repository for Interaction Datasets", + PANTHER: "Protein ANalysis THrough Evolutionary Relationships", + ZFIN: "Zebrafish Information Network", + UNIPROT: "Universal Protein Resource", + ENSEMBL: "Ensembl Genome Database Project", + FLYBASE: "FlyBase (Drosophila Genetics & Genomics Database)", + BGEE: "Bgee: Gene Expression Evolution Database", + REACTOME: "Reactome Pathway Knowledgebase", + RGD: "Rat Genome Database", + UPHENO: "Unified Phenotype Ontology", + INTERPRO: "InterPro (Protein Families, Domains & Sites)", + INTACT: "IntAct Molecular Interaction Database", + POMBASE: "PomBase (Schizosaccharomyces pombe Model Organism Database)", + OMIM: "Online Mendelian Inheritance in Man", + FBBT: "Drosophila Anatomy Ontology (FBbt)", + WORMBASE: "WormBase (Caenorhabditis Genetics & Genomics Database)", + ORPHANET: "Orphanet (Rare Diseases Resource)", + XENBASE: "Xenbase (Xenopus Model Organism Database)", + GO: "Gene Ontology", + SGD: "Saccharomyces Genome Database", + CHEBI: "Chemical Entities of Biological Interest (ChEBI)", + ZP: "Zebrafish Phenotype Ontology", + PR: "Protein Ontology (PRO)", + MONDO: "Monarch Disease Ontology (MONDO)", + OBA: "Ontology of Biological Attributes", + UBERON: "Uberon Multi-Species Anatomy Ontology", + XPO: "Xenopus Phenotype Ontology", + HP: "Human Phenotype Ontology", + SYNGO: "SynGO (Synaptic Gene Ontology)", + HPA: "Human Protein Atlas", + GOC: "Gene Ontology Consortium", + CLINVAR: "Clinical Variants Resource(ClinVar)", + DICTYBASE: "dictyBase (Dictyostelium Model Organism Database)", + WBBT: "WormBase Anatomy Ontology (WBbt)", + MP: "Mammalian Phenotype Ontology", + CTD: "Comparative Toxicogenomics Database", + WB: "WormBase", + COMPLEXPORTAL: "Complex Portal (Macromolecular Complexes, EMBL-EBI)", + RHEA: "Rhea Biochemical Reactions Knowledgebase", + CLINGEN: "Clinical Genome Resource (ClinGen)", + ZFA: "Zebrafish Anatomy Ontology (ZFA)", + PINC: "Protein Interaction Network Curations", + FYPO: "Fission Yeast Phenotype Ontology", + EMAPA: "Mouse Developmental Anatomy Ontology (EMAPA)", + CL: "Cell Ontology", + AGBASE: "AgBase (Functional Genomics for Agriculture)", + XAO: "Xenopus Anatomy Ontology", + MAXO: "Medical Action Ontology", + CAFA: "Critical Assessment of Functional Annotation", + WBPHENOTYPE: "WormBase Phenotype Ontology", + NCBITAXON: "NCBI Taxonomy", + RNACENTRAL: "RNAcentral (Non-coding RNA Sequence Database)", + CACAO: "Community Assessment of Community Annotation with Ontologies", + PATO: "Phenotype and Trait Ontology", + DDPHENO: "Dictyostelium discoideum Phenotype Ontology", + DISPROT: "DisProt (Database of Protein Disorder)", + HSAPDV: "Human Developmental Stages Ontology (HsapDv)", + DFLAT: "Developmental FunctionaL Annotation at Tufts", + NBO: "Neurobehavior Ontology", + YUBIOLAB: "YuBioLab Gene Ontology Annotation", + FBDV: "FlyBase Drosophila Development Ontology", + "GO-CENTRAL": "Gene Ontology Central", + "ARUK-UCL": "Alzheimer’s Research UK – UCL Gene Ontology Annotation", + "PARKINSONSUK-UCL": "Parkinson’s UK – UCL Gene Ontology Annotation", + "HGNC-UCL": "HGNC – UCL Gene Ontology Annotation", + "NTNU-SB": "NTNU Systems Biology Gene Ontology Annotation", + LIFEDB: "Localization and Interaction Explorer Database (LIFEdb)", + RO: "Relations Ontology", + DDANAT: "Dictyostelium Anatomy Ontology", + HGNC: "HUGO Gene Nomenclature Committee", + FBCV: "FlyBase Controlled Vocabulary", + SO: "Sequence Ontology", + MPATH: "Mouse Pathology Ontology", + "SYSCILIA-CCNET": "Syscilia Ciliopathy Network Dataset", + WBLS: "WormBase Life Stage Ontology", + GDB: "GDB Human Genome Database", + "SYNGO-UCL": "SynGO – UCL Gene Ontology Annotation", + ZFS: "Zebrafish Stage Ontology", + "ROSLIN-INSTITUTE": + "The Roslin Institute – University of Edinburgh GO Annotation", + BFO: "Basic Formal Ontology", + MTBBASE: "Mouse Tumor Biology Database", + "GOC-OWL": "Gene Ontology OWL Builds", + "GO-NOCTUA": "GO Noctua Curation Platform", + DIBU: "Unknown/placeholder label (no authoritative expansion available)", + "PHI-BASE": "Pathogen–Host Interactions Database", + ALLIANCEGENOME: "Alliance of Genome Resources", + AGRKB: "Alliance of Genome Resources Knowledgebase", + PHENIO: "Phenio (Integrated Cross-Species Phenotype Ontology)", + "HPO-ANNOTATIONS": "Human Phenotype Ontology Annotations", + MEDGEN: "MedGen (NCBI Medical Genetics Portal)", +}; diff --git a/frontend/src/pages/PageHomeV2.vue b/frontend/src/pages/PageHomeV2.vue index 2a3f87034..8b5b4819a 100644 --- a/frontend/src/pages/PageHomeV2.vue +++ b/frontend/src/pages/PageHomeV2.vue @@ -57,7 +57,6 @@ import { TOOL_LINKS } from "@/data/toolEntityConfig"; justify-content: center; width: 100%; max-width: 60em; - gap: 2em; } diff --git a/frontend/src/pages/ResourceInfoPage.vue b/frontend/src/pages/ResourceInfoPage.vue index 8bbd528bc..77f19502f 100644 --- a/frontend/src/pages/ResourceInfoPage.vue +++ b/frontend/src/pages/ResourceInfoPage.vue @@ -135,7 +135,6 @@ const item = computed(() => { return data[props.itemType]?.[props.id] ?? null; }); -console.log("item", item.value); // ------------------------------ // 5. Tabs logic and visibility // ------------------------------ diff --git a/frontend/src/pages/knowledgeGraph/PagePhenotypeExplore.vue b/frontend/src/pages/knowledgeGraph/PagePhenotypeExplore.vue index 620a14c63..963e43c9a 100644 --- a/frontend/src/pages/knowledgeGraph/PagePhenotypeExplore.vue +++ b/frontend/src/pages/knowledgeGraph/PagePhenotypeExplore.vue @@ -81,7 +81,7 @@ $wrap: 1000px; } .tab-item.active { border: 1px solid #ccc; - border-bottom: 3px solid $theme; // keep consistent height + border-bottom: 3px solid $theme; background-color: $theme; color: $white; diff --git a/frontend/src/pages/knowledgeGraph/PageSources.vue b/frontend/src/pages/knowledgeGraph/PageSources.vue index c5d7ee3c0..d3264a369 100644 --- a/frontend/src/pages/knowledgeGraph/PageSources.vue +++ b/frontend/src/pages/knowledgeGraph/PageSources.vue @@ -39,13 +39,13 @@ - {{ source.label }} + {{ resourceFullName(source.label) }} {{ source.id }} - {{ source.count }} + {{ source.count.toLocaleString() }} @@ -59,6 +59,7 @@ import AppBreadcrumb from "@/components/AppBreadcrumb.vue"; import AppSection from "@/components/AppSection.vue"; import PageTitle from "@/components/ThePageTitle.vue"; import { useKnowledgeSources } from "@/composables/use-knowledge-sources"; +import { RESOURCE_NAME_MAP } from "@/config/resourceNames"; const activeTab = ref<"primary" | "aggregator">("primary"); @@ -77,7 +78,8 @@ onMounted(() => fetchAll()); function generateInforesLink(id: string) { return `https://w3id.org/information-resource-registry/${id.replace("infores:", "")}`; } - +const resourceFullName = (label?: string) => + RESOURCE_NAME_MAP[(label ?? "").toUpperCase()] ?? label ?? ""; const currentSources = computed(() => activeTab.value === "primary" ? primarySources.value diff --git a/frontend/src/pages/node/AssociationsTable.vue b/frontend/src/pages/node/AssociationsTable.vue index a9a8cd179..a2ec27054 100644 --- a/frontend/src/pages/node/AssociationsTable.vue +++ b/frontend/src/pages/node/AssociationsTable.vue @@ -1,7 +1,3 @@ - - - - - {{ row.original_predicate }} - - + + Does NOT have + - - - - - {{ row.subject_specialization_qualifier }} - + + + {{ sourceNames(row.primary_knowledge_source).join(", ") }} - {{ row.subject_specialization_qualifier }} + No info @@ -190,11 +164,6 @@ /> No info - - - - - diff --git a/frontend/src/pages/node/HistoPheno.vue b/frontend/src/pages/node/HistoPheno.vue index 66df8149c..09919d2a5 100644 --- a/frontend/src/pages/node/HistoPheno.vue +++ b/frontend/src/pages/node/HistoPheno.vue @@ -73,7 +73,11 @@ const options = computed(() => ({ color: "#000000", }, }, - yaxis: {}, + yaxis: { + labels: { + formatter: (val: string | number) => String(val).replace(/_/g, " "), + }, + }, grid: { xaxis: { lines: { diff --git a/frontend/src/pages/node/PageNode.vue b/frontend/src/pages/node/PageNode.vue index 424e91a60..219dc075c 100644 --- a/frontend/src/pages/node/PageNode.vue +++ b/frontend/src/pages/node/PageNode.vue @@ -5,7 +5,7 @@ - + {{ $route.params.id }} diff --git a/frontend/src/pages/node/SectionAssociations.vue b/frontend/src/pages/node/SectionAssociations.vue index c368f07ce..a45ce9417 100644 --- a/frontend/src/pages/node/SectionAssociations.vue +++ b/frontend/src/pages/node/SectionAssociations.vue @@ -6,83 +6,94 @@ - - {{ category.label }} + + {{ + sectionTitle(category.id, category.label) + }} No associations with - - - - - - + + + setDirect(category.id, which === 'direct' ? 'true' : 'false') + " + /> + + - + (debouncedSearchValues[category.id] = value)" @change="(value) => (debouncedSearchValues[category.id] = value)" /> - + onTotals({ categoryId: String(category.id), ...p })" + @inferred-label="onInferredLabel" /> - diff --git a/frontend/src/pages/node/SectionBreadcrumbs.vue b/frontend/src/pages/node/SectionBreadcrumbs.vue index ccbfd4d0d..a5ec5d199 100644 --- a/frontend/src/pages/node/SectionBreadcrumbs.vue +++ b/frontend/src/pages/node/SectionBreadcrumbs.vue @@ -1,5 +1,5 @@ - + Breadcrumbs diff --git a/frontend/src/pages/node/SectionClinicalReources.vue b/frontend/src/pages/node/SectionClinicalReources.vue new file mode 100644 index 000000000..2a44e468b --- /dev/null +++ b/frontend/src/pages/node/SectionClinicalReources.vue @@ -0,0 +1,155 @@ + + + + + + + + {{ brandText(res.id, res.label) }} + + {{ res.id }} + + + + + + + + + Heritability : + {{ node?.inheritance?.name }} + + + + Casual Genes : + + + + + Frequency : + {{ frequencyLabel }} + + + + + + + + + diff --git a/frontend/src/pages/node/SectionHierarchy.vue b/frontend/src/pages/node/SectionHierarchy.vue index 790154e8e..b1aa9958a 100644 --- a/frontend/src/pages/node/SectionHierarchy.vue +++ b/frontend/src/pages/node/SectionHierarchy.vue @@ -8,7 +8,6 @@ width="full" alignment="left" class="inset" - design="bare" > Hierarchy diff --git a/frontend/src/pages/node/SectionOverview.vue b/frontend/src/pages/node/SectionOverview.vue index 1870282ac..e44495674 100644 --- a/frontend/src/pages/node/SectionOverview.vue +++ b/frontend/src/pages/node/SectionOverview.vue @@ -26,7 +26,7 @@ - + - - - - {{ node.inheritance?.name }} - - - - - - - - - - - - - {{ node?.subsets?.includes("rare") ? "Rare" : "Common" }} - - - - - - {{ node.id }} - - + + + + + + + {{ mapping.id }} + + + + + + + {{ link.id }} + + - - - - - {{ mapping.id }} + + + {{ node.id }} - - - - - - {{ mapping.id }} + + + + + {{ node.provided_by_link?.id || node.provided_by }} - - - - - {{ node.provided_by_link?.id || node.provided_by }} - - - - - - {{ mapping.id }} + + + + + + + + + {{ node.id }} - - - - - - {{ link.id }} - - + + + + + {{ node.inheritance?.name }} + + + + + + + + + + + + + {{ node?.subsets?.includes("rare") ? "Rare" : "Common" }} + + + + + + + {{ mapping.id }} + + + + + + + {{ mapping.id }} + + + + + + + {{ node.provided_by_link?.id || node.provided_by }} + + + + + + + {{ mapping.id }} + + + + + + + {{ link.id }} + + +