Render the pedigree top-down as a family tree
Replace the indented ancestry list with a top-down chart: the dog on top, sire (left) and dam (right) branching below, connected by lines drawn with each node's CSS ::before/::after (a nested <ul>/<li> tree). It shows three generations by default and each dog expands to trace the line further back; deeper levels get wide, so the view scrolls horizontally.
This commit is contained in:
+16
-13
@@ -2459,7 +2459,7 @@
|
||||
const pedBtn = document.getElementById("pedigree-btn");
|
||||
const pedRefresh = document.getElementById("pedigree-refresh");
|
||||
|
||||
const PED_OPEN_DEPTH = 4; // generations shown expanded by default; deeper collapse
|
||||
const PED_OPEN_DEPTH = 3; // show 3 generations expanded by default; deeper collapses
|
||||
let pedPollTimer = null;
|
||||
let pedNodes = {}; // latest ancestry map, for the progress count
|
||||
|
||||
@@ -2646,12 +2646,18 @@
|
||||
}
|
||||
|
||||
// The tree is ahnentafel-indexed: the dog is position 1, its sire 2n and dam
|
||||
// 2n+1. We build recursively (sire above dam) and collapse below PED_OPEN_DEPTH.
|
||||
// 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.
|
||||
function renderTree(nodes) {
|
||||
pedNodes = nodes;
|
||||
pedTree.textContent = "";
|
||||
const root = buildPedNode(nodes, 1, 0);
|
||||
if (root) pedTree.append(root);
|
||||
if (!root) return;
|
||||
const ul = document.createElement("ul");
|
||||
ul.className = "ped-tree-h";
|
||||
ul.append(root);
|
||||
pedTree.append(ul);
|
||||
}
|
||||
|
||||
function buildPedNode(nodes, pos, depth) {
|
||||
@@ -2660,8 +2666,7 @@
|
||||
const hasDam = !!nodes[String(pos * 2 + 1)];
|
||||
if (!n && !hasSire && !hasDam) return null;
|
||||
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "ped-node";
|
||||
const li = document.createElement("li");
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = "ped-card";
|
||||
@@ -2682,32 +2687,30 @@
|
||||
r.textContent = n.reg;
|
||||
card.append(r);
|
||||
}
|
||||
li.append(card);
|
||||
|
||||
if (hasSire || hasDam) {
|
||||
const kids = document.createElement("div");
|
||||
kids.className = "ped-children";
|
||||
const kids = document.createElement("ul");
|
||||
const s = buildPedNode(nodes, pos * 2, depth + 1);
|
||||
const d = buildPedNode(nodes, pos * 2 + 1, depth + 1);
|
||||
if (s) { s.classList.add("ped-sire"); kids.append(s); }
|
||||
if (d) { d.classList.add("ped-dam"); kids.append(d); }
|
||||
|
||||
const collapsed = depth >= PED_OPEN_DEPTH;
|
||||
if (collapsed) wrap.classList.add("collapsed");
|
||||
if (collapsed) li.classList.add("collapsed");
|
||||
const toggle = document.createElement("button");
|
||||
toggle.type = "button";
|
||||
toggle.className = "ped-toggle";
|
||||
toggle.setAttribute("aria-label", "Toggle ancestors");
|
||||
toggle.textContent = collapsed ? "+" : "−";
|
||||
toggle.addEventListener("click", () => {
|
||||
const nowCollapsed = wrap.classList.toggle("collapsed");
|
||||
const nowCollapsed = li.classList.toggle("collapsed");
|
||||
toggle.textContent = nowCollapsed ? "+" : "−";
|
||||
});
|
||||
card.prepend(toggle);
|
||||
wrap.append(card, kids);
|
||||
} else {
|
||||
wrap.append(card);
|
||||
li.append(kids);
|
||||
}
|
||||
return wrap;
|
||||
return li;
|
||||
}
|
||||
|
||||
// ---------- changelog dialog ----------
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
[
|
||||
{ "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" },
|
||||
{ "date": "2026-07-24", "text": "The age counter reads \"16 weeks (3 months and 3 weeks) old\" so weeks and months line up; past 4 months it drops the weeks and shows just months (e.g. \"5 months and 2 weeks old\")" },
|
||||
|
||||
+80
-28
@@ -1123,59 +1123,111 @@ section.collapsed > :not(h2) { display: none; }
|
||||
.ped-subject-name { font-size: 1.15rem; font-weight: 700; }
|
||||
.ped-subject-meta { font-size: 0.85rem; color: var(--muted); margin-top: 2px; }
|
||||
|
||||
/* the tree */
|
||||
/* Top-down family tree: the dog on top, parents branching below, connected by
|
||||
lines drawn with each <li>'s ::before/::after (the classic CSS tree). Wider
|
||||
than the screen once expanded, so the container scrolls horizontally. */
|
||||
.pedigree-tree {
|
||||
overflow-x: auto;
|
||||
padding-bottom: 24px;
|
||||
padding: 8px 0 28px;
|
||||
}
|
||||
.ped-node { position: relative; }
|
||||
.ped-tree-h, .ped-tree-h ul {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.ped-tree-h {
|
||||
/* "safe" centers when the tree fits and falls back to start-aligned (no
|
||||
clipped/unreachable left edge) once it's wider than the screen. */
|
||||
justify-content: safe center;
|
||||
min-width: max-content;
|
||||
padding: 4px 16px 12px;
|
||||
}
|
||||
.ped-tree-h ul {
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding-top: 22px; /* room for the connector from the parent above */
|
||||
}
|
||||
.ped-tree-h li {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 22px 6px 0;
|
||||
}
|
||||
/* Elbow from each child up to the horizontal bar shared by its siblings. */
|
||||
.ped-tree-h li::before,
|
||||
.ped-tree-h li::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 50%;
|
||||
height: 22px;
|
||||
border-top: 2px solid var(--border);
|
||||
}
|
||||
.ped-tree-h li::before { right: 50%; }
|
||||
.ped-tree-h li::after { left: 50%; border-left: 2px solid var(--border); }
|
||||
/* Vertical drop from a parent card down to its children's bar. */
|
||||
.ped-tree-h ul::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
height: 22px;
|
||||
border-left: 2px solid var(--border);
|
||||
}
|
||||
/* A lone parent connects with a straight line, no elbow. */
|
||||
.ped-tree-h li:only-child::before,
|
||||
.ped-tree-h li:only-child::after { display: none; }
|
||||
/* Trim the outer half-lines at the ends of a sibling row. */
|
||||
.ped-tree-h li:first-child::before,
|
||||
.ped-tree-h li:last-child::after { border: 0 none; }
|
||||
.ped-tree-h li:last-child::before { border-right: 2px solid var(--border); }
|
||||
/* The dog sits on top with no connector above it. */
|
||||
.ped-tree-h > li { padding-top: 0; }
|
||||
.ped-tree-h > li::before,
|
||||
.ped-tree-h > li::after { display: none; }
|
||||
/* Collapsed: hide the ancestry below this dog (and its connectors go with it). */
|
||||
.ped-tree-h li.collapsed > ul { display: none; }
|
||||
|
||||
.ped-card {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 6px 24px 6px 28px;
|
||||
margin: 3px 0;
|
||||
width: 140px;
|
||||
box-sizing: border-box;
|
||||
padding: 7px 18px 7px 20px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow);
|
||||
text-align: center;
|
||||
}
|
||||
.ped-name { font-weight: 600; font-size: 0.95rem; }
|
||||
.ped-name { font-weight: 600; font-size: 0.85rem; line-height: 1.2; }
|
||||
.ped-name.ped-unknown { color: var(--muted); font-weight: 500; font-style: italic; }
|
||||
.ped-titles { font-size: 0.72rem; color: var(--accent); margin-top: 1px; }
|
||||
.ped-titles { font-size: 0.66rem; color: var(--accent); margin-top: 2px; line-height: 1.2; }
|
||||
.ped-reg {
|
||||
font-size: 0.72rem;
|
||||
font-size: 0.66rem;
|
||||
color: var(--muted);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
/* lineage spine: children indented under the card */
|
||||
.ped-children {
|
||||
margin-left: 16px;
|
||||
padding-left: 14px;
|
||||
border-left: 2px solid var(--border);
|
||||
}
|
||||
.ped-node.collapsed > .ped-children { display: none; }
|
||||
|
||||
/* sire ♂ (blue) above dam ♀ (purple) markers on the parent-role cards */
|
||||
.ped-sire > .ped-card { border-left: 3px solid var(--sleep); }
|
||||
.ped-dam > .ped-card { border-left: 3px solid var(--training); }
|
||||
/* sire ♂ (blue) / dam ♀ (purple) accents on the parent cards */
|
||||
.ped-sire > .ped-card { border-top: 3px solid var(--sleep); }
|
||||
.ped-dam > .ped-card { border-top: 3px solid var(--training); }
|
||||
.ped-sire > .ped-card::after,
|
||||
.ped-dam > .ped-card::after {
|
||||
position: absolute;
|
||||
right: 7px;
|
||||
top: 6px;
|
||||
font-size: 0.75rem;
|
||||
right: 6px;
|
||||
top: 5px;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
.ped-sire > .ped-card::after { content: "♂"; color: var(--sleep); }
|
||||
.ped-dam > .ped-card::after { content: "♀"; color: var(--training); }
|
||||
|
||||
/* expand/collapse toggle */
|
||||
/* expand/collapse toggle (top-left of the card) */
|
||||
.ped-toggle {
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
width: 18px; height: 18px;
|
||||
padding: 0;
|
||||
font-size: 0.9rem;
|
||||
|
||||
Reference in New Issue
Block a user