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
56 changes: 56 additions & 0 deletions rust/crates/du-db/src/variant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,62 @@ pub async fn set_coordinates_batch(
Ok(affected)
}

/// One tree branch a variant is assigned to (defines). Surfaced on the Variant Browser detail.
#[derive(Debug, Clone)]
pub struct BranchAssignment {
/// Haplogroup (branch) name, e.g. `R-M269`.
pub haplogroup_name: String,
/// `"Y_DNA"` or `"MT_DNA"` — selects the tree page for the link.
pub dna_type: String,
/// The link is marked provisional/low-confidence.
pub low_confidence: bool,
/// The defining variant's canonical name. Differs from the viewed variant when the tree
/// placement lives on a same-site sibling row (the de-novo coordinate-named variant vs a
/// separately-named catalog row).
pub via_name: String,
}

/// The tree branch(es) a variant is assigned to (defines) in the *current* tree.
///
/// Matched by genomic **site**, not just the row's own links: the de-novo tree places a
/// coordinate-named variant (`chrY:<pos><ref>>>...`) while the same physical SNP may also exist
/// as a separately-named catalog row (e.g. a YBrowse name like `V3739`) carrying no direct
/// link. `site` is a `{"hs1":{"contig":…,"position":…}}` containment blob built from the
/// viewed variant's hs1 coordinate (the tree is hs1-native) — GIN-indexed on `coordinates` —
/// or `None` when it has no hs1 coordinate (then only the variant's own direct links match).
pub async fn tree_branches(
pool: &PgPool,
variant_id: VariantId,
site: Option<serde_json::Value>,
) -> Result<Vec<BranchAssignment>, DbError> {
let rows: Vec<(String, String, bool, String)> = sqlx::query_as(
"WITH cand AS ( \
SELECT $1::bigint AS id \
UNION \
SELECT id FROM core.variant WHERE $2::jsonb IS NOT NULL AND coordinates @> $2::jsonb \
) \
SELECT DISTINCT h.name, h.haplogroup_type::text, hv.low_confidence, def.canonical_name \
FROM cand \
JOIN tree.haplogroup_variant hv ON hv.variant_id = cand.id AND hv.valid_until IS NULL \
JOIN core.variant def ON def.id = cand.id \
JOIN tree.haplogroup h ON h.id = hv.haplogroup_id \
ORDER BY h.name",
)
.bind(variant_id.0)
.bind(site)
.fetch_all(pool)
.await?;
Ok(rows
.into_iter()
.map(|(haplogroup_name, dna_type, low_confidence, via_name)| BranchAssignment {
haplogroup_name,
dna_type,
low_confidence,
via_name,
})
.collect())
}

/// Region types whose sequence is structurally unreliable for Y-SNP placement
/// (multi-copy / repeat-rich), so a variant landing inside one should not be
/// trusted as branch-defining without scrutiny. AZF intervals are deliberately
Expand Down
35 changes: 35 additions & 0 deletions rust/crates/du-web/src/routes/variants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@ struct CoordView {
change: Option<String>,
}

/// A tree branch this variant defines, flattened for the template.
struct BranchView {
name: String,
/// Link to the branch on the relevant tree page.
tree_url: String,
low_confidence: bool,
/// The defining variant's name, shown only when it differs from the viewed variant
/// (the placement is carried by a same-site sibling row).
via: Option<String>,
}

#[derive(askama::Template)]
#[template(path = "variants/detail.html")]
struct DetailTemplate {
Expand All @@ -110,6 +121,7 @@ struct DetailTemplate {
common_names: Vec<String>,
rs_ids: Vec<String>,
coords: Vec<CoordView>,
branches: Vec<BranchView>,
}

async fn browser(
Expand Down Expand Up @@ -156,6 +168,28 @@ async fn detail(
.collect();
coords.sort_by(|a, b| a.build.cmp(&b.build));

// Which tree branch(es) this SNP is assigned to. Match by hs1 site (the tree is
// hs1-native), so a catalog-named SNP still surfaces the placement its de-novo
// coordinate-named sibling carries.
let site = v
.coordinates
.get(du_domain::enums::ReferenceBuild::Hs1)
.map(|c| serde_json::json!({ "hs1": { "contig": c.contig, "position": c.position } }));
let variant_name = v.canonical_name.clone();
let branches = du_db::variant::tree_branches(&st.pool, VariantId(id), site)
.await?
.into_iter()
.map(|b| {
let base = if b.dna_type == "MT_DNA" { "/mtree" } else { "/ytree" };
BranchView {
tree_url: format!("{base}?root={}", b.haplogroup_name),
via: (b.via_name != variant_name).then_some(b.via_name),
name: b.haplogroup_name,
low_confidence: b.low_confidence,
}
})
.collect();

Ok(html(&DetailTemplate {
t: locale.t,
name: v.canonical_name,
Expand All @@ -164,5 +198,6 @@ async fn detail(
common_names: v.aliases.common_names,
rs_ids: v.aliases.rs_ids,
coords,
branches,
}))
}
15 changes: 15 additions & 0 deletions rust/crates/du-web/templates/variants/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,20 @@ <h6 class="mt-3">{{ t.get("variants.detail.coordinates") }}</h6>
</tbody>
</table>
{% endif %}

<h6 class="mt-3">{{ t.get("variants.detail.branches") }}</h6>
{% if branches.is_empty() %}
<p class="detail-empty">{{ t.get("variants.detail.nobranches") }}</p>
{% else %}
<ul class="list-unstyled mb-0">
{% for b in branches %}
<li class="mb-1">
<a href="{{ b.tree_url }}"><code>{{ b.name }}</code></a>
{% if b.low_confidence %}<span class="badge text-bg-warning">{{ t.get("variants.detail.lowconf") }}</span>{% endif %}
{% if let Some(via) = b.via %}<small class="text-muted">{{ t.get("variants.detail.via") }} <code>{{ via }}</code></small>{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
4 changes: 4 additions & 0 deletions rust/locales/en.txt
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ variants.detail.aka=Also known as:
variants.detail.rsids=rs IDs:
variants.detail.coordinates=Coordinates
variants.detail.nocoords=No mapped coordinates.
variants.detail.branches=Tree branch(es)
variants.detail.nobranches=Not assigned to a tree branch.
variants.detail.via=via
variants.detail.lowconf=provisional
variants.col.build=Build
variants.col.contig=Contig
variants.col.position=Position
Expand Down
4 changes: 4 additions & 0 deletions rust/locales/es.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ variants.detail.aka=También conocido como:
variants.detail.rsids=Identificadores rs:
variants.detail.coordinates=Coordenadas
variants.detail.nocoords=Sin coordenadas asignadas.
variants.detail.branches=Rama(s) del árbol
variants.detail.nobranches=No asignado a ninguna rama del árbol.
variants.detail.via=vía
variants.detail.lowconf=provisional
variants.col.build=Ensamblaje
variants.col.contig=Contig
variants.col.position=Posición
Expand Down
4 changes: 4 additions & 0 deletions rust/locales/fr.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ variants.detail.aka=Aussi connu sous le nom de :
variants.detail.rsids=Identifiants rs :
variants.detail.coordinates=Coordonnées
variants.detail.nocoords=Aucune coordonnée cartographiée.
variants.detail.branches=Branche(s) de l'arbre
variants.detail.nobranches=Non assigné à une branche de l'arbre.
variants.detail.via=via
variants.detail.lowconf=provisoire
variants.col.build=Assemblage
variants.col.contig=Contig
variants.col.position=Position
Expand Down
9 changes: 9 additions & 0 deletions rust/migrations/0055_haplogroup_variant_variant_idx.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Index tree.haplogroup_variant by variant_id for the *current* (valid_until IS NULL) links.
--
-- The existing haplogroup_variant_current_key is UNIQUE (haplogroup_id, variant_id) — it leads
-- with haplogroup_id, so the reverse lookup "which branch(es) is this variant assigned to?" has
-- no supporting index and seq-scans the whole table. That lookup backs the Variant Browser's
-- tree-branch panel (site-matched, so it runs per variant-detail view), plus any future
-- variant->branch surfacing. Partial (valid_until IS NULL) to match the current-links workload.
CREATE INDEX IF NOT EXISTS haplogroup_variant_variant_idx
ON tree.haplogroup_variant (variant_id) WHERE valid_until IS NULL;
Loading