Flag pedigree collapse with repeat badges
When a dog fills more than one position in the pedigree (pedigree collapse), mark each copy with a ×N badge and a stable per-dog colour, and let tapping any copy highlight every place that dog appears. Identity is the registration number (falling back to name), so the same ancestor at different positions is treated as one. The summary line also reports how many ancestors recur.
This commit is contained in:
+60
-5
@@ -2458,6 +2458,7 @@
|
||||
const pedTree = document.getElementById("pedigree-tree");
|
||||
const pedBtn = document.getElementById("pedigree-btn");
|
||||
const pedRefresh = document.getElementById("pedigree-refresh");
|
||||
const pedRepeatNote = document.getElementById("pedigree-repeat-note");
|
||||
|
||||
const PED_OPEN_DEPTH = 3; // show 3 generations expanded by default; deeper collapses
|
||||
let pedPollTimer = null;
|
||||
@@ -2580,20 +2581,24 @@
|
||||
// fills many positions); "generations back" is the depth of the deepest
|
||||
// position (floor(log2(pos)), since sire = 2·pos and dam = 2·pos+1).
|
||||
function pedCounts() {
|
||||
const seen = new Set();
|
||||
const counts = new Map();
|
||||
let maxPos = 1;
|
||||
for (const k in pedNodes) {
|
||||
const n = pedNodes[k];
|
||||
const key = n.reg || n.name;
|
||||
if (key) seen.add(key);
|
||||
if (key) counts.set(key, (counts.get(key) || 0) + 1);
|
||||
const p = Number(k);
|
||||
if (p > maxPos) maxPos = p;
|
||||
}
|
||||
return { distinct: seen.size, gens: Math.floor(Math.log2(maxPos)) };
|
||||
let repeated = 0;
|
||||
counts.forEach((c) => { if (c > 1) repeated++; });
|
||||
return { distinct: counts.size, repeated, gens: Math.floor(Math.log2(maxPos)) };
|
||||
}
|
||||
function pedCountText() {
|
||||
const { distinct: a, gens: g } = pedCounts();
|
||||
return `${a} ancestor${a === 1 ? "" : "s"} back ${g} generation${g === 1 ? "" : "s"}`;
|
||||
const { distinct: a, repeated: r, gens: g } = pedCounts();
|
||||
let s = `${a} ancestor${a === 1 ? "" : "s"} back ${g} generation${g === 1 ? "" : "s"}`;
|
||||
if (r > 0) s += `, ${r} appearing more than once`;
|
||||
return s;
|
||||
}
|
||||
function setPedDone() { setPedStatus(`Traced ${pedCountText()}.`, "done"); }
|
||||
function setPedStatus(text, kind) {
|
||||
@@ -2649,9 +2654,29 @@
|
||||
// 2n+1. We render it top-down like a family tree — the dog on top, parents
|
||||
// branching below — as a nested <ul>/<li> so CSS can draw the connectors.
|
||||
// Built recursively (sire left, dam right) and collapsed below PED_OPEN_DEPTH.
|
||||
// A dog's identity for spotting pedigree collapse: its registration number,
|
||||
// or its name when it has none. Ancestors sharing a key are the same dog.
|
||||
function dogKey(n) { return n ? (n.reg || n.name || "") : ""; }
|
||||
// Stable hue per repeated dog so its badge/highlight colour is consistent
|
||||
// everywhere it appears.
|
||||
function hueFor(s) {
|
||||
let h = 0;
|
||||
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
|
||||
return h % 360;
|
||||
}
|
||||
let pedRepeat = {}; // dogKey -> occurrence count, for the current tree
|
||||
|
||||
function renderTree(nodes) {
|
||||
pedNodes = nodes;
|
||||
pedTree.textContent = "";
|
||||
// Count how many positions each dog fills, to flag pedigree collapse.
|
||||
pedRepeat = {};
|
||||
for (const k in nodes) {
|
||||
const id = dogKey(nodes[k]);
|
||||
if (id) pedRepeat[id] = (pedRepeat[id] || 0) + 1;
|
||||
}
|
||||
const anyRepeat = Object.values(pedRepeat).some((c) => c > 1);
|
||||
if (pedRepeatNote) pedRepeatNote.hidden = !anyRepeat;
|
||||
const root = buildPedNode(nodes, 1, 0);
|
||||
if (!root) return;
|
||||
const ul = document.createElement("ul");
|
||||
@@ -2660,6 +2685,17 @@
|
||||
pedTree.append(ul);
|
||||
}
|
||||
|
||||
// Highlight (or unhighlight) every card that is the same dog as `key`.
|
||||
function togglePedHighlight(key) {
|
||||
const cards = pedTree.querySelectorAll(".ped-card[data-dogkey]");
|
||||
let lit = false;
|
||||
cards.forEach((c) => {
|
||||
if (c.dataset.dogkey === key && c.classList.contains("ped-lit")) lit = true;
|
||||
});
|
||||
cards.forEach((c) => c.classList.remove("ped-lit"));
|
||||
if (!lit) cards.forEach((c) => { if (c.dataset.dogkey === key) c.classList.add("ped-lit"); });
|
||||
}
|
||||
|
||||
function buildPedNode(nodes, pos, depth) {
|
||||
const n = nodes[String(pos)];
|
||||
const hasSire = !!nodes[String(pos * 2)];
|
||||
@@ -2687,6 +2723,25 @@
|
||||
r.textContent = n.reg;
|
||||
card.append(r);
|
||||
}
|
||||
|
||||
// Mark dogs that fill more than one position (pedigree collapse). A ×N badge
|
||||
// shows how many times, a stable colour ties the copies together, and tapping
|
||||
// the card lights up every place this dog appears.
|
||||
const key = dogKey(n);
|
||||
if (key && pedRepeat[key] > 1) {
|
||||
card.classList.add("ped-repeat");
|
||||
card.dataset.dogkey = key;
|
||||
card.style.setProperty("--repeat-hue", hueFor(key));
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "ped-repeat-badge";
|
||||
badge.textContent = "×" + pedRepeat[key];
|
||||
badge.title = "Appears " + pedRepeat[key] + " times in this pedigree — tap to highlight them all";
|
||||
card.append(badge);
|
||||
card.addEventListener("click", (e) => {
|
||||
if (e.target.closest(".ped-toggle")) return; // let the expander do its job
|
||||
togglePedHighlight(key);
|
||||
});
|
||||
}
|
||||
li.append(card);
|
||||
|
||||
if (hasSire || hasDam) {
|
||||
|
||||
Reference in New Issue
Block a user