Skip to content

Commit a7087b0

Browse files
committed
Add statistics panel with demographics, lifespan, and top-N lists
- New stats.js module computes figures about the whole tree: sex split, living/deceased count, average/median/max lifespan, family sizes, birth-year span, generation depth, data-coverage percentages, and top-5 lists for surnames, given names, places, and occupations - Panel renders as a collapsible <details> in the sidebar, below the graph-reload button - Sex distribution shown as a horizontal bar chart (male/female/unknown)
1 parent 2796d30 commit a7087b0

9 files changed

Lines changed: 587 additions & 12 deletions

File tree

‎README.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ Deployed automatically to GitHub Pages from `master`
4646
- **3D** — the same graph in space, stratified by birth year or by generation,
4747
with an optional time axis.
4848

49-
**Focus.** A whole GEDCOM is an unreadable cloud, so the 2D view centres on one
50-
person and draws their relatives out to a chosen distance — direct line, siblings,
49+
**Focus.** The 2D view shows the whole file by default. Focusing narrows it to one
50+
person and their relatives out to a chosen distance — direct line, siblings,
5151
and cousins to a configurable degree. The panel shows how many people are on
5252
screen and how many are hidden. `+N` chips mark where a branch was cut; clicking
5353
one opens the next generation.
@@ -108,6 +108,7 @@ loaded as native ES modules (`js/package.json` only marks the directory as
108108
| `js/render-3d.js` | the 3D scene, camera, labels and time axis |
109109
| `js/panels.js` | detail panel and all the editing forms |
110110
| `js/import.js` | the import wizard: text parsing, OCR, AI extraction, merge review |
111+
| `js/stats.js` | the statistics panel — figures about the tree as a whole |
111112
| `js/relations.js` | highlighting and the "how are these two related" tool |
112113
| `js/colors.js` | surname palette and node/link colouring |
113114
| `js/gedcom-io.js` | file loading, saving, autosave, the working-file handle |
@@ -126,7 +127,7 @@ npm install # jsdom, the only dependency, and only for tests
126127
npm test
127128
```
128129

129-
Seven suites, no framework — plain `node` scripts with `assert`:
130+
Eight suites, no framework — plain `node` scripts with `assert`:
130131

131132
| | |
132133
|---|---|
@@ -135,6 +136,7 @@ Seven suites, no framework — plain `node` scripts with `assert`:
135136
| `import.test.js` | linking imported people to existing records |
136137
| `deceased.test.js` | auto-marking long-dead people |
137138
| `hover.test.js` | quick-add buttons and node box labels |
139+
| `stats.test.js` | the statistics figures and how they are rendered |
138140
| `mobile.test.js` | the 3D view's behaviour on a narrow viewport |
139141
| `workfile.test.js` | reopening and saving back over the same file |
140142

‎i18n.js‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,42 @@ const I18N = {
3333
status: '${persons} Person${personsPlural}, ${families} Familien',
3434
statusLoaded: '${persons} Personen, ${families} Familien geladen',
3535
},
36+
stats: {
37+
title: 'Statistik',
38+
empty: 'Keine Daten geladen',
39+
people: 'Personen',
40+
male: 'Männlich',
41+
female: 'Weiblich',
42+
unknownSex: 'Unbekannt',
43+
sexM: 'Männlich',
44+
sexF: 'Weiblich',
45+
sexU: 'Unbekannt',
46+
living: 'Lebend / verstorben',
47+
livingHint: 'verstorben nach Eintrag oder Alter',
48+
avgLifespan: 'Ø Lebensalter',
49+
medianLifespan: 'Median Lebensalter',
50+
basedOn: 'aus ${n} mit beiden Daten',
51+
years: 'Jahre',
52+
oldest: 'Ältestes Alter',
53+
families: 'Familien',
54+
avgChildren: 'Ø Kinder pro Familie',
55+
avgChildrenHint: 'nur Familien mit Kindern',
56+
childless: 'Familien ohne Kinder',
57+
biggestFamily: 'Grösste Familie',
58+
childrenWord: 'Kinder',
59+
divorced: 'Geschieden',
60+
span: 'Zeitraum (Geburten)',
61+
spanHint: '${n} Jahre',
62+
generations: 'Generationen',
63+
completeness: 'Datenlage',
64+
hasBirth: 'mit Geburtsjahr',
65+
hasDeath: 'mit Sterbejahr',
66+
hasPlace: 'mit Ort',
67+
topSurnames: 'Häufigste Nachnamen',
68+
topGiven: 'Häufigste Vornamen',
69+
topPlaces: 'Häufigste Orte',
70+
topOccupations: 'Häufigste Berufe',
71+
},
3672
export: {
3773
gedcom: 'GEDCOM (.ged)',
3874
json: 'JSON (.json)',
@@ -526,6 +562,42 @@ const I18N = {
526562
status: '${persons} person${personsPlural}, ${families} families',
527563
statusLoaded: '${persons} persons, ${families} families loaded',
528564
},
565+
stats: {
566+
title: 'Statistics',
567+
empty: 'No data loaded',
568+
people: 'People',
569+
male: 'Male',
570+
female: 'Female',
571+
unknownSex: 'Unknown',
572+
sexM: 'Male',
573+
sexF: 'Female',
574+
sexU: 'Unknown',
575+
living: 'Living / deceased',
576+
livingHint: 'deceased by record or by age',
577+
avgLifespan: 'Avg. lifespan',
578+
medianLifespan: 'Median lifespan',
579+
basedOn: 'from ${n} with both dates',
580+
years: 'years',
581+
oldest: 'Longest life',
582+
families: 'Families',
583+
avgChildren: 'Avg. children per family',
584+
avgChildrenHint: 'families with children only',
585+
childless: 'Families without children',
586+
biggestFamily: 'Largest family',
587+
childrenWord: 'children',
588+
divorced: 'Divorced',
589+
span: 'Span (births)',
590+
spanHint: '${n} years',
591+
generations: 'Generations',
592+
completeness: 'Data coverage',
593+
hasBirth: 'with birth year',
594+
hasDeath: 'with death year',
595+
hasPlace: 'with a place',
596+
topSurnames: 'Most common surnames',
597+
topGiven: 'Most common given names',
598+
topPlaces: 'Most common places',
599+
topOccupations: 'Most common occupations',
600+
},
529601
export: {
530602
gedcom: 'GEDCOM (.ged)',
531603
json: 'JSON (.json)',

‎index.html‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ <h1>&#127795; <span data-i18n="app.name">Stammbaum</span></h1>
147147
<button class="sidebar-btn" onclick="autoSettle()">&#x21ba; <span data-i18n="sidebar.reloadGraph">Graph neu laden</span></button>
148148
</div>
149149

150+
<details id="stats-panel" ontoggle="toggleStatsPanel(event)">
151+
<summary data-i18n="stats.title">Statistik</summary>
152+
<div id="stats-body"></div>
153+
</details>
154+
150155
<details id="link-colors-panel">
151156
<summary data-i18n="linkColors.title">Verbindungsfarben</summary>
152157
<div class="link-color-row">

‎js/main.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as PanelsMod from './panels.js';
66
import * as RelationsMod from './relations.js';
77
import * as Render2dMod from './render-2d.js';
88
import * as Render3dMod from './render-3d.js';
9+
import * as StatsMod from './stats.js';
910
import * as TreeLayoutMod from './tree-layout.js';
1011
import { resetLinkColors, toggleAllSurnames } from './colors.js';
1112
import { _fullRebuildGraph, _tryRestoreAutosave, updateFileButtons } from './gedcom-io.js';
@@ -360,7 +361,7 @@ function _onLanguageChanged() {
360361
// runs this after the *whole* graph's synchronous evaluation has settled,
361362
// regardless of which module happened to be the entry point.
362363
queueMicrotask(() => Object.assign(window, ColorsMod, GedcomIoMod, GraphDataMod, ImportMod, PanelsMod,
363-
RelationsMod, Render2dMod, Render3dMod, TreeLayoutMod, {
364+
RelationsMod, Render2dMod, Render3dMod, StatsMod, TreeLayoutMod, {
364365
state,
365366
toggleSidebar,
366367
_initPanelSwipe,

‎js/render-3d.js‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { state } from './state.js';
22
import { _compute3DLinkColor, _nameTextColor, compute3DNodeColor } from './colors.js';
3-
import { _defaultFocusRoot, computeActiveData, updateFocusUI } from './graph-data.js';
3+
import { computeActiveData, updateFocusUI } from './graph-data.js';
44
import { _isMobile, closeDetailPanel, minimizeDetailPanel, row, showFamDetail, showIndiDetail } from './panels.js';
55
import { _famNodeVal, _tryPickRelationPerson, applyHighlight, updateHLButtons } from './relations.js';
66
import { buildAndRunSimulation, initSVG, linkColor, linkWidth, onHover, onOut, onSimEnd, renderGraph, zoomToFit } from './render-2d.js';
@@ -38,12 +38,13 @@ export function setView(view) {
3838
} else {
3939
if (state.graph3d) state.graph3d.pauseAnimation();
4040

41-
// A 2D view of a big tree with no focus is an unreadable dust cloud, so
42-
// never enter one: fall back to the selected person, else the best hub.
43-
// Trees that fit inside the budget need no focus at all.
44-
if (!state.focusRootId && state.individuals.size > state.focusLimit) {
45-
state.focusRootId = state.selectedIndiId || _defaultFocusRoot();
46-
}
41+
// No focus is picked on the way in. Switching to 2D used to quietly appoint
42+
// one — the selected person, or the best-connected hub — on the grounds that
43+
// an unfocused force layout is a dust cloud. But the chart layout is the
44+
// default now, and that draws a whole file as a readable, if wide, tree; and
45+
// a filter the reader never asked for is worse than a big picture, because
46+
// nothing on screen says people are missing. Focus stays something you turn
47+
// on deliberately.
4748

4849
if (changed) {
4950
computeActiveData();

0 commit comments

Comments
 (0)