Tree view: configurable generation depth (ancestors/descendants + All)
Depth was hardcoded (3 ancestors, 2 descendants). Add a controls row to set each direction independently — a slider plus a number stepper, with an "All" toggle per direction — applied around whoever is currently focused. - ancestor/descendant depth held in state; effective value is a large cap when "All" is on (the chart only renders people that exist, so the cap is free). - changes apply to the live chart via setAncestryDepth/setProgenyDepth + updateTree without a full rebuild. - fan mode (ancestors only) takes the ancestor depth via its `generations` prop, capped at 8 to avoid the radial layout's 2^n blow-up; its descendants control is disabled with a note. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
@@ -46,6 +46,16 @@ export default function TreePage() {
|
||||
const [homeId, setHomeId] = useState<string | null>(null);
|
||||
const [mode, setMode] = useState<Mode>("landscape");
|
||||
const [renderNote, setRenderNote] = useState<string | null>(null);
|
||||
// How many generations to show around the focus, each independently settable
|
||||
// (or "all"). ALL_DEPTH is just a number bigger than any real lineage; the
|
||||
// chart only renders people that exist, so a high cap costs nothing.
|
||||
const [ancDepth, setAncDepth] = useState(3); // ancestors (backwards)
|
||||
const [progDepth, setProgDepth] = useState(2); // descendants (forwards)
|
||||
const [ancAll, setAncAll] = useState(false);
|
||||
const [progAll, setProgAll] = useState(false);
|
||||
const ALL_DEPTH = 100;
|
||||
const effAnc = ancAll ? ALL_DEPTH : ancDepth;
|
||||
const effProg = progAll ? ALL_DEPTH : progDepth;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -185,9 +195,9 @@ export default function TreePage() {
|
||||
chart.setCardHtml().setCardDisplay([["first name", "last name"], ["birthday"]]);
|
||||
if (mode === "portrait") chart.setOrientationVertical();
|
||||
else chart.setOrientationHorizontal();
|
||||
// Show enough generations that a recenter reveals grandparents + children.
|
||||
chart.setAncestryDepth?.(3);
|
||||
chart.setProgenyDepth?.(2);
|
||||
// Generations to show around the focus (configurable; see depth controls).
|
||||
chart.setAncestryDepth?.(effAnc);
|
||||
chart.setProgenyDepth?.(effProg);
|
||||
// Default card click recenters the whole hourglass; sync focus for the
|
||||
// "Open profile" link after every (re)build.
|
||||
chart.setAfterUpdate?.(() => {
|
||||
@@ -219,6 +229,15 @@ export default function TreePage() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [status, mode, people, rels, events]);
|
||||
|
||||
// Apply depth changes to the already-built chart without a full rebuild
|
||||
// (landscape/portrait only; the fan reads its own `generations` prop).
|
||||
useEffect(() => {
|
||||
if (mode === "fan" || !chartRef.current) return;
|
||||
chartRef.current.setAncestryDepth?.(effAnc);
|
||||
chartRef.current.setProgenyDepth?.(effProg);
|
||||
chartRef.current.updateTree?.();
|
||||
}, [effAnc, effProg, mode]);
|
||||
|
||||
// Jump the tree (or fan) to a person and rebuild the hourglass around them.
|
||||
const goTo = useCallback(
|
||||
(id: string) => {
|
||||
@@ -261,6 +280,68 @@ export default function TreePage() {
|
||||
</button>
|
||||
);
|
||||
|
||||
// Slider + number stepper + "All" for one generation direction.
|
||||
const DepthControl = ({
|
||||
label,
|
||||
icon,
|
||||
value,
|
||||
all,
|
||||
onValue,
|
||||
onAll,
|
||||
disabled,
|
||||
}: {
|
||||
label: string;
|
||||
icon: string;
|
||||
value: number;
|
||||
all: boolean;
|
||||
onValue: (v: number) => void;
|
||||
onAll: (b: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}) => (
|
||||
<div className={`flex items-center gap-2 ${disabled ? "opacity-40" : ""}`}>
|
||||
<span className="flex w-24 items-center gap-1 text-xs text-[var(--muted)]">
|
||||
<span aria-hidden>{icon}</span> {label}
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={12}
|
||||
step={1}
|
||||
value={all ? 12 : value}
|
||||
disabled={disabled || all}
|
||||
onChange={(e) => onValue(Number(e.target.value))}
|
||||
className="w-28 accent-bronze"
|
||||
aria-label={`${label} generations`}
|
||||
/>
|
||||
{all ? (
|
||||
<span className="w-10 text-center text-sm font-medium text-bronze">All</span>
|
||||
) : (
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={99}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={(e) => onValue(Math.max(0, Math.min(99, Number(e.target.value) || 0)))}
|
||||
className="h-7 w-12 rounded-md border border-[var(--border)] bg-[var(--surface)] px-1 text-center text-sm"
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => onAll(!all)}
|
||||
title={`Show all ${label.toLowerCase()}`}
|
||||
className={`rounded-md px-2 py-1 text-xs transition-colors ${
|
||||
all
|
||||
? "bg-bronze text-paper"
|
||||
: "border border-[var(--border)] text-[var(--muted)] hover:text-[var(--foreground)]"
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
@@ -319,6 +400,38 @@ export default function TreePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{status === "ready" && (
|
||||
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-lg border border-[var(--border)] bg-[var(--surface)] px-4 py-2.5">
|
||||
<span className="text-sm font-medium">Generations</span>
|
||||
<DepthControl
|
||||
label="Ancestors"
|
||||
icon="↑"
|
||||
value={ancDepth}
|
||||
all={ancAll}
|
||||
onValue={(v) => {
|
||||
setAncAll(false);
|
||||
setAncDepth(v);
|
||||
}}
|
||||
onAll={setAncAll}
|
||||
/>
|
||||
<DepthControl
|
||||
label="Descendants"
|
||||
icon="↓"
|
||||
value={progDepth}
|
||||
all={progAll}
|
||||
onValue={(v) => {
|
||||
setProgAll(false);
|
||||
setProgDepth(v);
|
||||
}}
|
||||
onAll={setProgAll}
|
||||
disabled={mode === "fan"}
|
||||
/>
|
||||
{mode === "fan" && (
|
||||
<span className="text-xs text-[var(--muted)]">Fan shows ancestors only.</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === "empty" && (
|
||||
<p className="text-[var(--muted)]">No people yet — add some under People, or import a GEDCOM.</p>
|
||||
)}
|
||||
@@ -338,6 +451,7 @@ export default function TreePage() {
|
||||
nameOf={nameOf}
|
||||
yearOf={yearOf}
|
||||
onSelect={setFocusId}
|
||||
generations={Math.min(effAnc, 8)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user