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) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
[
|
||||
{ "date": "2026-07-26", "text": "In the pedigree, a dog that fills more than one spot (pedigree collapse, common in a breed's older lines) now carries a ×N badge — tap it to highlight every place that dog appears in the tree" },
|
||||
{ "date": "2026-07-26", "text": "The pedigree now reads top-down like a family tree — your dog on top with its sire and dam branching below — showing three generations at a glance, with each dog expandable to trace the line further back" },
|
||||
{ "date": "2026-07-26", "text": "The pedigree ID set in Settings now syncs reliably to your other devices — it's no longer dropped when two devices' clocks disagree" },
|
||||
{ "date": "2026-07-26", "text": "New 🌳 Pedigree page: add your dog's SKK chip or registration number in Settings to unlock it, then explore its ancestry as a tree — the first generations show at once and the line fills in further back as it's traced from SKK Hunddata. It's cached, so it reopens instantly and works offline" },
|
||||
|
||||
@@ -291,6 +291,7 @@
|
||||
<p id="pedigree-status" class="pedigree-status" hidden></p>
|
||||
<div id="pedigree-choose" class="pedigree-choose" hidden></div>
|
||||
<div id="pedigree-subject" class="pedigree-subject" hidden></div>
|
||||
<p id="pedigree-repeat-note" class="muted-note pedigree-repeat-note" hidden>Some ancestors appear in more than one place further back (pedigree collapse). Expand the tree to reveal their ×N badges, then tap one to highlight every spot that dog appears.</p>
|
||||
<div id="pedigree-tree" class="pedigree-tree"></div>
|
||||
</main>
|
||||
</div><!-- /#pedigree-screen -->
|
||||
|
||||
@@ -1223,6 +1223,27 @@ section.collapsed > :not(h2) { display: none; }
|
||||
.ped-sire > .ped-card::after { content: "♂"; color: var(--sleep); }
|
||||
.ped-dam > .ped-card::after { content: "♀"; color: var(--training); }
|
||||
|
||||
/* pedigree collapse: a dog filling more than one position gets a ×N badge, a
|
||||
stable hue, and lights up (with every copy) when tapped. */
|
||||
.ped-card.ped-repeat { cursor: pointer; }
|
||||
.ped-repeat-badge {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
padding: 2px 5px;
|
||||
border-radius: 999px;
|
||||
color: #fff;
|
||||
background: hsl(var(--repeat-hue, 0), 58%, 48%);
|
||||
}
|
||||
.ped-card.ped-lit {
|
||||
border-color: hsl(var(--repeat-hue, 0), 70%, 50%);
|
||||
box-shadow: 0 0 0 2px hsl(var(--repeat-hue, 0), 70%, 50%), var(--shadow);
|
||||
}
|
||||
.pedigree-repeat-note { margin: 0 0 10px; }
|
||||
|
||||
/* expand/collapse toggle (top-left of the card) */
|
||||
.ped-toggle {
|
||||
position: absolute;
|
||||
|
||||
Reference in New Issue
Block a user