people tags #387

This commit is contained in:
Hamid Reza Mohammadi 2026-10-03 18:20:49 +03:30
parent fecbf0ec3b
commit 88f39c2bab
2 changed files with 50 additions and 20 deletions

View File

@ -31,6 +31,10 @@ else
.ft-heart { font-size: 13px; fill: #a0455c; }
.ft-line { fill: none; stroke: #a99a7c; stroke-width: 1.5; }
.ft-line-spouse { stroke-dasharray: 3,3; }
/* sibling relation with no recorded shared parent - deliberately plainer than the
spouse dashing (finer dots, lighter/grayer) and gets no heart icon, so it doesn't
read as a marriage or as a confirmed shared-parent claim */
.ft-line-sibling { stroke-dasharray: 1,3; stroke: #a0a0a0; }
.ft-tooltip-panel {
position: absolute; top: 10px; left: 10px; max-width: 300px;
background: #fffdf7; border: 1px solid #8b6b4a; border-radius: 8px;

View File

@ -54,21 +54,17 @@
}
});
// A sibling pair where only one side has a recorded parent (e.g. a sibling relation was
// entered but no separate parent relation for the other sibling) used to leave the parentless
// sibling with no parent at all, turning them into their own disconnected chart root instead
// of being drawn next to their actual family. Have the parentless sibling inherit the same
// parent(s), purely so the drawing places them together - this doesn't change any real data.
siblingPairs.forEach(function (pair) {
var a = pair[0], b = pair[1];
var aHasParent = parentsOf[a] && parentsOf[a].length > 0;
var bHasParent = parentsOf[b] && parentsOf[b].length > 0;
if (aHasParent && !bHasParent) {
parentsOf[a].forEach(function (p) { addParentChild(p, b); });
} else if (bHasParent && !aHasParent) {
parentsOf[b].forEach(function (p) { addParentChild(p, a); });
}
});
// NOTE: a recorded Sibling relation deliberately does not say which parent(s), if any, are
// shared (see PersonRelationType.Sibling's own doc comment: "doesn't require either parent
// to be known/recorded") - a text might call two people brothers without saying whether
// they share a father, a mother, or both. So siblingPairs is intentionally NOT turned into
// addParentChild() calls here (an earlier version of this code did exactly that, inheriting
// ALL of the known sibling's recorded parents onto the parentless one - fabricating a
// specific shared-parentage claim the source material never made, and quietly corrupting
// isRoot/parentsOf for that person everywhere else in this function too, e.g. the gender
// tie-break above). Instead, a parentless sibling is drawn as a plain "sibling, parentage
// unspecified" attachment beside whichever sibling does have a known position - see
// tryAttachSibling below.
Object.keys(childrenOf).forEach(function (k) { childrenOf[k] = dedup(childrenOf[k]); });
Object.keys(parentsOf).forEach(function (k) { parentsOf[k] = dedup(parentsOf[k]); });
@ -122,6 +118,10 @@
// akhakhafrasiyab.ir's chart
var attachedTo = {}; // spouseId -> primaryId
var attachedList = {}; // primaryId -> [spouseId,...]
var attachedAsSibling = {}; // spouseId -> true, for ids attached via tryAttachSibling below -
// drawBox/the attached-box renderer uses this to draw a plain line
// with no heart icon, instead of implying a marriage that was
// never recorded
function tryAttach(primaryId, spouseId) {
if (attachedTo[spouseId] !== undefined || spouseId === primaryId) return false;
@ -144,6 +144,25 @@
});
});
// Siblings with no (or no other) known position are drawn beside whichever sibling does have
// one - purely a placement choice, not a parentage claim (see the long comment above
// siblingPairs). Deliberately reuses tryAttach's own "isRoot && no children of their own"
// guard, so this never steals a sibling who already has a real, independently-documented
// position (their own recorded parents, or children) in the tree.
function tryAttachSibling(primaryId, siblingId) {
if (tryAttach(primaryId, siblingId)) {
attachedAsSibling[siblingId] = true;
return true;
}
return false;
}
siblingPairs.forEach(function (pair) {
var a = pair[0], b = pair[1];
if (attachedTo[a] !== undefined || attachedTo[b] !== undefined) return;
if (tryAttachSibling(a, b)) return;
tryAttachSibling(b, a);
});
var topLevelRoots = allIds.filter(function (id) { return isRoot[id] && attachedTo[id] === undefined; });
function subtreeContains(rootId, targetId, guard) {
@ -188,6 +207,7 @@
primaryParentOf: primaryParentOf,
secondaryParentOf: secondaryParentOf,
attachedList: attachedList,
attachedAsSibling: attachedAsSibling,
topLevelRoots: topLevelRoots,
nodeX: nodeX,
nodeDepth: nodeDepth
@ -307,15 +327,21 @@
var attached = layoutInfo.attachedList[id] || [];
var edgeX = p.x + size.w / 2;
attached.forEach(function (spouseId, i) {
var isSibling = !!layoutInfo.attachedAsSibling[spouseId];
var attSize = boxSizeFor(layoutInfo.persons[spouseId], ATTACH_W, BOX_H);
var ax = edgeX + ATTACH_GAP + attSize.w / 2;
var ay = p.y;
drawBox(spouseId, ax, ay, attSize.w, attSize.h, spouseId === requestedRootId);
line(edgeX, p.y, ax - attSize.w / 2, ay, 'ft-line ft-line-spouse');
// a sibling drawn this way is placed next to the OTHER sibling purely for layout -
// it is not a marriage, so it gets a plain neutral connector and no heart icon (see
// the long comment above siblingPairs/tryAttachSibling in buildLayout)
line(edgeX, p.y, ax - attSize.w / 2, ay, isSibling ? 'ft-line ft-line-sibling' : 'ft-line ft-line-spouse');
if (!isSibling) {
var heartX = edgeX + ATTACH_GAP / 2;
var heart = el('text', { x: heartX, y: ay + 4, 'text-anchor': 'middle', 'class': 'ft-heart' });
heart.textContent = '♥';
nodesGroup.appendChild(heart);
}
maxX = Math.max(maxX, ax + attSize.w / 2);
edgeX = ax + attSize.w / 2 + ATTACH_GAP;
});