Skip to content
Draft
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
59 changes: 59 additions & 0 deletions backend/src/monarch_py/utils/association_type_mappings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,62 @@
category: "biolink:CaseToGeneAssociation"
subject_category: "biolink:Case"
object_category: "biolink:Gene"
# Clinical measurement (LOINC) sections. These edges all carry the generic
# biolink:Association category, so each section is distinguished by predicate +
# object category. The same mappings drive the reverse "... Clinical Measurements"
# sections on the phenotype/chemical/anatomy/etc. pages via the object_label.
- key: clinical_measurement_correlated_phenotypes
subject_label: Correlated Phenotypes
object_label: Correlated Clinical Measurements
category:
- "biolink:Association"
predicate:
- "biolink:correlated_with"
subject_category:
- "biolink:ClinicalMeasurement"
object_category:
- "biolink:PhenotypicFeature"
- key: clinical_measurement_related_chemicals
subject_label: Related Chemicals
object_label: Related Clinical Measurements
category:
- "biolink:Association"
predicate:
- "biolink:related_to"
subject_category:
- "biolink:ClinicalMeasurement"
object_category:
- "biolink:ChemicalEntity"
- key: clinical_measurement_related_anatomy
subject_label: Related Anatomy
object_label: Related Clinical Measurements
category:
- "biolink:Association"
predicate:
- "biolink:related_to"
subject_category:
- "biolink:ClinicalMeasurement"
object_category:
- "biolink:AnatomicalEntity"
- key: clinical_measurement_related_proteins
subject_label: Related Proteins
object_label: Related Clinical Measurements
category:
- "biolink:Association"
predicate:
- "biolink:related_to"
subject_category:
- "biolink:ClinicalMeasurement"
object_category:
- "biolink:Protein"
- key: clinical_measurement_related_cells
subject_label: Related Cells
object_label: Related Clinical Measurements
category:
- "biolink:Association"
predicate:
- "biolink:related_to"
subject_category:
- "biolink:ClinicalMeasurement"
object_category:
- "biolink:Cell"
2 changes: 1 addition & 1 deletion backend/tests/fixtures/association_counts_query.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion backend/tests/fixtures/association_counts_response.py

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions backend/tests/unit/test_solr_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,46 @@
from monarch_py.utils.utils import dict_diff


def test_parse_association_counts_clinical_measurement():
"""A clinical-measurement (LOINC) section surfaces with its key/label even
though its edges share the generic biolink:Association category."""
from monarch_py.implementations.solr.solr_query_utils import (
build_association_count_suffixes,
build_association_counts_query,
)

node = "LOINC:2345-7"
fragment = (
'category:"biolink:Association" AND predicate:"biolink:correlated_with" '
'AND subject_category:"biolink:ClinicalMeasurement" '
'AND object_category:"biolink:PhenotypicFeature"'
)
suffixes = build_association_count_suffixes([node]).all_suffixes
direct = f'({fragment}) {suffixes["direct_subject"]}'
closure = f'({fragment}) {suffixes["closure_subject"]}'

# sanity: these queries are actually part of the built counts query
built = build_association_counts_query([node]).facet_queries
assert direct in built and closure in built

response = {
"responseHeader": {"QTime": 1, "params": {}},
"response": {"numFound": 0, "start": 0, "docs": []},
"facet_counts": {"facet_fields": {}, "facet_queries": {direct: 12, closure: 30}},
}
parsed = parse_association_counts(SolrQueryResult(**response), entities=[node]).model_dump()
assert parsed["items"] == [
{
"key": "clinical_measurement_correlated_phenotypes",
"label": "Correlated Phenotypes",
"category": "biolink:Association",
"count": 30,
"count_direct": 12,
"count_with_orthologs": None,
}
]


def test_parse_associations(association_response, associations):
association_response["response"]["numFound"] = association_response["response"].pop("num_found")
solr_response = SolrQueryResult(**association_response)
Expand Down
3 changes: 3 additions & 0 deletions docs/sources-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ sources:
- repo: monarch-initiative/hgnc-ingest
branch: main
filename: hgnc.md
- repo: monarch-initiative/loinc-ingest
branch: main
filename: loinc.md
- repo: monarch-initiative/maxo-annotation-ingest
branch: main
filename: maxo.md
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/api/categories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ const colorMap: { [key: string]: string } = mapKeys(

"biolink:Disease": colors.pink.dark,

"biolink:ClinicalMeasurement": colors.teal.dark,

"biolink:MolecularEntity": colors.lime.dark,
"biolink:CellularComponent": colors.lime.dark,
"biolink:BiologicalProcess": colors.lime.dark,
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/assets/icons/category-clinical-measurement.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/src/components/TheTableOfContents.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const CATEGORIES = [
"biolink:Disease",
"biolink:PhenotypicFeature",
"biolink:AnatomicalEntity",
"biolink:ClinicalMeasurement",
];
const { node } = defineProps<{ node: ApiNode | null }>();
/** toc entries */
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/pages/node/AssociationsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@
>
<AppIcon :icon="getAgentTypeMeta(row.agent_type).icon" />
</span>
<span
v-if="row.knowledge_level"
v-tooltip="
`<b>${getKnowledgeLevelMeta(row.knowledge_level).label}</b><br>${getKnowledgeLevelMeta(row.knowledge_level).description}`
"
:aria-label="`${getKnowledgeLevelMeta(row.knowledge_level).label}: ${getKnowledgeLevelMeta(row.knowledge_level).description}`"
>
<AppIcon :icon="getKnowledgeLevelMeta(row.knowledge_level).icon" />
</span>
<AppButton
v-tooltip="'View association details'"
design="small"
Expand Down Expand Up @@ -232,6 +241,7 @@ import {
import { getBreadcrumbs } from "@/pages/node/AssociationsSummary.vue";
import SectionAssociationDetails from "@/pages/node/SectionAssociationDetails.vue";
import { getAgentTypeMeta } from "@/util/agentType";
import { getKnowledgeLevelMeta } from "@/util/knowledgeLevel";
import { taxonFieldFor } from "@/util/taxonFilterConfig";
import { fieldFor } from "@/util/typeConfig";

Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/node/SectionHierarchy.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const LABELS = new Map<string, string>([
["biolink:Disease", "Disease"],
["biolink:PhenotypicFeature", "Phenotype"],
["biolink:AnatomicalEntity", "Anatomical entity"],
["biolink:ClinicalMeasurement", "Clinical measurement"],
]);

const typeNoun = computed(
Expand Down
54 changes: 54 additions & 0 deletions frontend/src/util/knowledgeLevel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/** Centralized metadata for Biolink KnowledgeLevelEnum values. */

type KnowledgeLevelMeta = { icon: string; label: string; description: string };

export const KNOWLEDGE_LEVEL_META: Record<string, KnowledgeLevelMeta> = {
knowledge_assertion: {
icon: "circle-check",
label: "Knowledge Assertion",
description:
"Directly asserted by a knowledge source or curator, representing an accepted fact",
},
logical_entailment: {
icon: "sitemap",
label: "Logical Entailment",
description:
"Derived by logical inference or reasoning from other statements",
},
prediction: {
icon: "chart-bar",
label: "Prediction",
description:
"Generated by a predictive or computational model; may be less certain",
},
statistical_association: {
icon: "equals",
label: "Statistical Association",
description: "Derived from a statistical association observed in data",
},
observation: {
icon: "eye",
label: "Observation",
description: "Based on a direct observation",
},
not_provided: {
icon: "circle-question",
label: "Not Provided",
description: "Knowledge level not available",
},
};

export const KNOWLEDGE_LEVEL_KEYS = Object.keys(KNOWLEDGE_LEVEL_META);

const FALLBACK: KnowledgeLevelMeta = {
icon: "circle-question",
label: "Unknown",
description: "Unrecognized knowledge level",
};

/** Get icon, label, and description for a knowledge_level value. */
export function getKnowledgeLevelMeta(
knowledgeLevel: string,
): KnowledgeLevelMeta {
return KNOWLEDGE_LEVEL_META[knowledgeLevel] ?? FALLBACK;
}
39 changes: 39 additions & 0 deletions frontend/unit/knowledgeLevel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, test } from "vitest";
import {
getKnowledgeLevelMeta,
KNOWLEDGE_LEVEL_KEYS,
KNOWLEDGE_LEVEL_META,
} from "@/util/knowledgeLevel";

describe("getKnowledgeLevelMeta", () => {
test("returns correct metadata for a curated assertion", () => {
const meta = getKnowledgeLevelMeta("knowledge_assertion");
expect(meta.icon).toBe("circle-check");
expect(meta.label).toBe("Knowledge Assertion");
expect(meta.description).toContain("asserted");
});

test("returns correct metadata for a prediction", () => {
const meta = getKnowledgeLevelMeta("prediction");
expect(meta.icon).toBe("chart-bar");
expect(meta.label).toBe("Prediction");
});

test("returns fallback for unknown values", () => {
const meta = getKnowledgeLevelMeta("completely_unknown_level");
expect(meta.icon).toBe("circle-question");
expect(meta.label).toBe("Unknown");
expect(meta.description).toBe("Unrecognized knowledge level");
});
});

describe("KNOWLEDGE_LEVEL_META", () => {
test("every entry has icon, label, and description", () => {
for (const key of KNOWLEDGE_LEVEL_KEYS) {
const meta = KNOWLEDGE_LEVEL_META[key];
expect(meta.icon).toBeTruthy();
expect(meta.label).toBeTruthy();
expect(meta.description).toBeTruthy();
}
});
});
Loading