Imported from magicsunday/webtrees-descendants-chart (
AGENTS.md). Install upstream withnpx skills add magicsunday/webtrees-descendants-chart. Copyright stays with the author.
Overview
This repository hosts the webtrees descendants chart module — an interactive SVG descendants (rectangular tree) chart of an individual's descendants using D3.js, installed as a Composer package inside webtrees.
Setup/env
- PHP 8.3+ with extensions dom and json is required; composer installs dependencies into .build/vendor and binaries into .build/bin.
- Node.js tooling is used for asset builds (rollup). Install dev dependencies via
npm installwhen touching frontend resources. - All PHP/Node tooling runs inside the webtrees Docker buildbox — never directly on the NAS or in phpfpm:
cd /volume2/docker/webtrees && make bash cd app/vendor/magicsunday/webtrees-descendants-chart - JS bundles are built via the node Docker container from the module directory:
make install,make build. - After PHP or JS changes visible in the browser:
docker restart webtrees-phpfpm-1. - After JS changes, always verify in the browser via Playwright before claiming success.
Build & tests
composer ci:testMUST run before every commit — catches Biome, PHPStan, PHP-CS-Fixer, Rector, PHPUnit, Jest, and CPD issues before they reach GitHub CI.- Individual checks:
composer ci:test:php:phpstan,composer ci:test:php:unit,composer ci:test:php:cgl,composer ci:test:js:lint,composer ci:test:js:unit. - Single PHPUnit test:
composer ci:test:php:unit -- --filter TestClassName. - Auto-fix:
composer ci:cgl(PHP style),composer ci:rector(Rector),npm run lint:fix(Biome). - JS bundles:
make build(rollup),make watch(dev rebuild loop). - Translations:
make lang(compile .po → .mo). All locale files must have non-emptymsgstrentries. - Keep PHPStan, PHPCS, and CPD clean on affected code; add PHPUnit attribute-based coverage (positive and negative cases) for every class/method introduced or modified.
- If
node_moduleshas permission issues (from node container), clean via:docker compose run --rm buildbox-root bash -c "rm -rf app/vendor/magicsunday/webtrees-descendants-chart/node_modules". - Typecheck scope is intentionally limited to production sources (
jsconfig.jsonincludes onlyresources/js/modules/**/*.js). Tests underresources/js/tests/**are validated by jest at runtime but skipped bytsc --noEmitbecause the partial mock objects required to isolate units would otherwise produce dozens of structural-incompatibility errors against the realConfiguration/Selectionshapes. The strict-typecheck gate applies to shipped code; test scaffolding stays loose.
Architecture
Data flow: PHP → JSON → D3
Module.php (entry point, registers routes)
→ page.phtml (form + AJAX container, localStorage via Storage)
→ chart.phtml (import() loads ES module, passes config as JS object)
→ DataFacade.php (builds Node tree from Individual records)
→ DateProcessor, NameProcessor, ImageProcessor (from module-base)
→ NodeData → JSON → D3 hierarchy → SVG rendering
PHP (src/)
- Module.php — Entry point, extends webtrees DescendantsChartModule, registers chart route.
- Configuration.php — Reads form parameters from request (POST/GET) with user preference fallback.
- Facade/DataFacade.php — Builds hierarchical Node tree, recursing through spouse families to the configured generation depth. Passes
$spouseand the married-names mode (off/married_only/birth_and_married) into NameProcessor; inbirth_and_marriedmode it also callsgetMarriedSurnames()and appends the bracketed suffix toname+lastNames. - Model/Node, NodeData — Tree node with JSON serialization for D3.
- Shared classes from
magicsunday/webtrees-module-base(composer dependency^1.1):Processor/DateProcessor— date extraction; descendants currently uses the legacy locale-aware methods (getBirthDate,getDeathDate,getLifetimeDescription).Processor/NameProcessor,Processor/ImageProcessor,Processor/PlaceProcessor— name/image/place extraction. NameProcessor's 3-arg constructor($individual, $spouse, $useMarriedName)is used here.Model/Symbols— backed enum for genealogical symbols (Birth ★, Death †, MarriageDateUnknown sentinel).Module/VersionInformation— GitHub release-checking with file cache.- For local edits to module-base while developing descendants-chart, run
make link-base(symlinks.build/vendor/.../webtrees-module-base→ the sibling clone). Reverse withmake unlink-baseor anycomposer install/update.
JS (resources/js/modules/)
Flat layout — every file is descendants-specific glue. Reusable base classes (Storage, ChartExport, ChartOverlay, ChartZoom, Orientations, measureText, truncateNames) live in the external @magicsunday/webtrees-chart-lib package, shared with the fan- and pedigree-chart modules. Consumed via Git URL pinned in package.json (github:magicsunday/webtrees-chart-lib#vX.Y.Z); chart-lib's prepare script builds its dist/ during install, so npm ci --ignore-scripts will break the build.
index.js— ExportsDescendantsChartclass (ES module entry point for Rollup).page-entry.js/page-init.js— UMD bundle (descendants-chart-page.min.js) loaded bypage.phtml. Owns localStorage form-state wiring;initPage()resolves user options and publishes them underWebtreesDescendantsChart.chartOptionsforchart.phtmlgetters.chart.js(D3 hierarchy.tree layout, click handling),hierarchy.js(D3 hierarchy),tree.js(collapse/expand + d3.tree separation hook),configuration.js.family-tree.js(CoupleNode → FamilyNode tree),separation.js(pickGaphalf-sibling detection — see issue #84).chart/—box/{image,text}.js,orientation-collection.js,svg.js.tree/—date.js(DateRenderer),name.js,node-drawer.js,link-drawer.js,connection-builder.js.constants.js,d3.js(re-export facade).
Views (resources/views/)
descendants-chart/page.phtml— Form + AJAX container. Loadsdescendants-chart-page.min.jsand callsWebtreesDescendantsChart.initPage({ ajaxUrl }).descendants-chart/chart.phtml— AJAX response:<script type="module">withimport()to load ES module bundle. Reads user overrides fromWebtreesDescendantsChart.chartOptions ?? PHP defaults.descendants-chart/form/*.phtml— Form partials (generations, layout, orientation, married-names toggle).charts/chart.phtml— Block template override (home page widget), usesdata-wt-ajax-urlpattern.
Key patterns
- ES module loading:
import().then(({ DescendantsChart }) => ...)in<script type="module">, avoiding thewebtrees.load()race condition. - Storage flow:
page-init.jsreads localStorage → publishes resolved options underWebtreesDescendantsChart.chartOptions→chart.phtmlgetter readsopts.x ?? PHP default. - Orientation strategy: chart layout is configurable in 4 directions (top-bottom, bottom-top, left-right, right-left) via the orientation-collection.js + matching
Orientation*classes (node coordinate transforms + elbow connector geometry). - Married-name modes: configurable via
marriedNamesMode(3-value:off,married_only,birth_and_married).married_onlyswaps the primary to a_MARNMrecord matching the spouse's surname (NameProcessor'suseMarriedNameflag).birth_and_marriedkeeps the birth-name primary and appends the married surname in brackets, e.g.Schmidt (Müller)— appended both to the full-name string and thelastNamesarray so the JS renderer (which locates eachlastNamesentry inside the full string viaindexOf) shows the bracketed entry. Migrates from the legacy booleandefault_showMarriedNamespreference (true →married_only, false →off). - Block template: Overrides core
modules/charts/chart.phtml— must stay in sync with webtrees core changes (e.g. VanillaJS conversion).
Release process
Runs inside the buildbox (requires git, node, npm, composer, jq, zip, gh, sed):
make release 2.1.0
make release 2.1.0 NOTES_FILE=/tmp/notes.md
make release VERSION=2.1.0 NOTES="Bug fix release"
Pipeline (make release X.Y.Z):
release-check— tools, semver, clean tree, no detached HEAD, no activemake link-basesymlink, gh auth (orGH_TOKEN).release-prepare—sed_editmacro updatesCUSTOM_VERSIONinsrc/Module.phpwith a post-write assertion;jq_editmacro (with--indent 4and post-write assertion) updatespackage.jsonversion +composer.jsonwebtrees pin → clean+rebuild JS bundles viabuild-js-fresh→ commit →dist→ tag (only after dist succeeds, so a dist failure does not leave a dangling tag).dist— symlink guard fires before composer touches anything,composer install --no-dev --no-interaction,git archive HEAD(respects.gitattributesexport-ignore), bundlesmagicsunday/webtrees-module-baseinto the zip'svendor/so manual ZIP installs work without composer, strips allcomposer.jsonfiles (find vendor -name composer.json -delete), atomic write via.tmp+ rename +zip -Tintegrity check.dist-smoke— separate target, asserts required entries (module.php, LICENSE, the versioned JS bundle, module-basesrc/) are present and forbidden ones (composer.json, assets/) are absent. CI runs this on every push.release-publish—git push --tags,gh release createwith the zip + notes, emitsRELEASE_PUBLISHED version=Xmarker for agent observers.release-bump— bump toVERSION+1-dev, keep the~2.2.0(stable) webtrees constraint, push. The dev tree tracks stable webtrees, notdev-main, because thedev-maini18n refactor removed APIs the module still uses and stable is what end users install; re-add|| dev-mainhere (and adapt the code) when webtrees' next stable nears.
Code style
PHP
- Follow PSR-12 with
declare(strict_types=1)in every file. - No
mixedtypes — use specific types or union types. - No
empty()— use explicit comparisons (=== '',=== [],=== null). - Use enums or typed constants instead of magic numbers/strings.
- Prefer value objects over complex plain arrays.
- Provide explicit parentheses for complex conditional expressions.
- Use expressive variable names; English inline comments only at complex logic.
- Docblocks: Always multi-line format. Describe purpose, not just repeat the method name. Keep
@paramonly when it adds information beyond type+name. Keep@returntags.
JavaScript
- ES modules only; vanilla JS except D3.
@privateon all non-public methods — this is the JS equivalent of PHPprivate.@returnon all getters and methods that return a value.- No single-line docblocks — always use multi-line
/**\n * text\n */format. - Use
@return(not@returns) for consistency. - Docblock order: description →
@param→@return→@private. - Parenthesize arithmetic in comparisons:
if (si < (group.length - 1)). - Parenthesize sub-conditions in compound booleans:
if ((a > 0) && (b < 1)). - No single-letter variable names except loop iterators.
for...ofon arrays (notfor...in).Object.prototype.hasOwnProperty.call()instead of direct.hasOwnProperty()for prototype-pollution safety.- Always include
.jsextension on relative ES module imports (BiomeuseImportExtensions).
Security
- Do not commit secrets or PII; rely on secret managers and keep .build outputs out of version control.
PR/commit checklist
composer ci:testmust pass before every commit.- Commit subjects — and the pull-request title — are governed by the shared
commit-conventiongate; the normative rule and its full rationale live inmagicsunday/.github/.github/workflows/commit-convention.yml@main, which self-tests a decision table before applying it. In short: aGH--prefixed subject must match^GH-\d+: [A-Z], every other subject^[A-Z]— a capitalised English imperative — and conventional-commit prefixes (feat:,Fix:, …) as well as path-like starts (src/…: …) are rejected whatever their case. It runs on every pull request via.github/workflows/commit-lint.yml, advisory untilcommit-convention / Commit conventionis a required context in branch protection. - Branches for an issue are named exactly
GH-<N>; theGH-<N>:prefix marks work that belongs to that issue, so a drive-by fix on the branch keeps its own unprefixed subject. - The pull-request body closes the issue with
Closes #<N>— theGH-<N>:subject prefix is not a GitHub link and closes nothing. - Never add a
Co-Authored-By:trailer or any other AI attribution. - Keep PRs small and focused (~≤300 net LOC) with atomic commits.
- Ensure coverage ≥90% on touched PHP paths.
- After PR receives review comments: assess, fix, commit, reply with commit hash, resolve threads via GraphQL.
- Never comment on GitHub issues/PRs without explicit user approval.
When stuck
- Check composer scripts (
composer run-script --list) and the README for expected workflows. - The fan-chart module follows the same architectural patterns and is the canonical reference for tooling/CI/test setup.
House Rules
- Maintain strict typing and PHPStan level max alignment.
- Prefer interfaces where sensible; mark data-only classes as
readonly. - Avoid external JavaScript libraries beyond D3.
- Always use Playwright to verify JS changes in the browser — don't just trust the tests.
- Use
jq(notsed) for JSON file manipulation in build scripts — Alpineseddoes not support the GNU regex extensions needed for nested JSON edits.sedis fine for non-JSON files (PHP, YAML); see thesed_editandjq_editmacros inMake/release.mk.