Skip to content

Commit

Permalink
Improve disk visualizations
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianCassayre committed Aug 6, 2023
1 parent 684cc85 commit 02f6098
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 75 deletions.
3 changes: 1 addition & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>Florian Cassayre genealogy</title>
</head>
<body>
<div id="root"></div>
Expand Down
20 changes: 14 additions & 6 deletions src/Homepage.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { useDataQuery } from './hooks/useDataQuery.ts';
import { DiskVisualization } from './viz/DiskVisualization.tsx';
import { Box, Grid, Typography } from '@mui/joy';
import { DiskGeographyVisualization } from './viz/DiskGeographyVisualization.tsx';
import { DiskLongevityVisualization } from './viz/DiskLongevityVisualization.tsx';

export const Homepage = () => {
const { data } = useDataQuery('geographyDisk');
return data ? (
const { data: dataLongevity } = useDataQuery('longevityDisk');
return data && dataLongevity ? (
<Box>
<Grid container justifyContent="center">
<Grid container justifyContent="center" spacing={2}>
<Grid xs={12} sm={10} md={8} lg={7} xl={6}>
<Typography level="h3" color="neutral" textAlign="center" sx={{ mb: 3 }}>
Place of birth
<Typography level="h3" color="neutral" textAlign="center" sx={{ mt: 2, mb: 3 }}>
Location
</Typography>
<DiskVisualization data={data.tree} />
<DiskGeographyVisualization data={data.tree} />
</Grid>
<Grid xs={12} sm={10} md={8} lg={7} xl={6}>
<Typography level="h3" color="neutral" textAlign="center" sx={{ mt: 2, mb: 3 }}>
Longevity
</Typography>
<DiskLongevityVisualization data={dataLongevity.tree} />
</Grid>
</Grid>
</Box>
Expand Down
1 change: 1 addition & 0 deletions src/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
{children}
<Divider sx={{ my: 2 }} />
<footer>
<Typography textAlign="center">This website is automatically generated from a Gedcom file</Typography>
<Typography textAlign="center">Date updated: {renderDateBuilt()}</Typography>
</footer>
</Container>
Expand Down
51 changes: 31 additions & 20 deletions src/scripts/compile.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,59 @@
import { readGedcom, SelectionGedcom, SelectionIndividualRecord } from 'read-gedcom';
import { readGedcom, SelectionEvent, SelectionGedcom, SelectionIndividualRecord, toJsDate } from 'read-gedcom';
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { Data, GenealogyData, GeographyDiskData, SimpleIndividualTree } from './types';
import { Data, GenealogyData, GeographyDiskData, LongevityDiskData } from './types';
import * as _ from 'radash';
import { buildIndividualTree } from './utils.ts';

const INPUT_FILE = 'genealogy.ged';
const OUTPUT_DIRECTORY = 'public/data';

const TREE_DEPTH_LIMIT = 9;
const TREE_DEPTH_SENSITIVE = 3;

const targetGenealogyData = (gedcom: SelectionGedcom): GenealogyData => ({
count: gedcom.getIndividualRecord().array().length,
});

const targetGeographyDisk = (gedcom: SelectionGedcom): GeographyDiskData => {
const limit = 9;
const root = gedcom.getIndividualRecord().arraySelect()[0];
const dataFromIndividual = (node: SelectionIndividualRecord): SimpleIndividualTree['data'] => {
const dataForIndividual = (node: SelectionIndividualRecord, depth: number): GeographyDiskData['tree']['data'] => {
const parts = node.getEventBirth().getPlace().valueAsParts()[0];
const head = 2;
const place = parts != null ? parts.slice(parts.length - head).join(', ') : '';
const place =
parts != null
? _.list(3 - 1)
.reverse()
.map((i) => (i !== 2 || depth >= TREE_DEPTH_SENSITIVE ? parts[parts.length - 1 - i] || null : null))
: null;
return { place };
};
const visit = (individual: SelectionIndividualRecord, depth: number): SimpleIndividualTree => {
const family = individual.getFamilyAsChild();
const husband = family.getHusband().getIndividualRecord().arraySelect()[0];
const wife = family.getWife().getIndividualRecord().arraySelect()[0];
const newDepth = depth + 1;
return {
...(depth < limit - 1
? {
father: husband ? visit(husband, newDepth) : undefined,
mother: wife ? visit(wife, newDepth) : undefined,
}
: {}),
data: dataFromIndividual(individual),
return {
tree: buildIndividualTree(root, dataForIndividual, TREE_DEPTH_LIMIT),
};
};

const targetLongevityDisk = (gedcom: SelectionGedcom): LongevityDiskData => {
const root = gedcom.getIndividualRecord().arraySelect()[0];
const dataForIndividual = (node: SelectionIndividualRecord): LongevityDiskData['tree']['data'] => {
const dateForEvent = (event: SelectionEvent): Date | null => {
const dateValue = event.getDate().valueAsDate()[0];
return dateValue != null && dateValue.hasDate && dateValue.isDatePunctual ? toJsDate(dateValue.date) : null;
};
const birth = dateForEvent(node.getEventBirth()),
death = dateForEvent(node.getEventDeath());
// Not the best rounding, but good enough
const longevity = birth !== null && death !== null ? death.getFullYear() - birth.getFullYear() : null;
return { longevity };
};
return {
tree: visit(root, 0),
tree: buildIndividualTree(root, dataForIndividual, TREE_DEPTH_LIMIT),
};
};

const targets = (gedcom: SelectionGedcom): Data => ({
data: targetGenealogyData(gedcom),
geographyDisk: targetGeographyDisk(gedcom),
longevityDisk: targetLongevityDisk(gedcom),
});

const generateTargets = () => {
Expand Down
9 changes: 6 additions & 3 deletions src/scripts/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface Data {
data: GenealogyData;
geographyDisk: GeographyDiskData;
longevityDisk: LongevityDiskData;
}

export interface GenealogyData {
Expand All @@ -13,8 +14,10 @@ export interface IndividualTree<D> {
data: D;
}

export type SimpleIndividualTree = IndividualTree<{ place: string }>;

export interface GeographyDiskData {
tree: SimpleIndividualTree;
tree: IndividualTree<{ place: (string | null)[] | null }>;
}

export interface LongevityDiskData {
tree: IndividualTree<{ longevity: number | null }>;
}
29 changes: 29 additions & 0 deletions src/scripts/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { SelectionIndividualRecord } from 'read-gedcom';
import { IndividualTree } from './types.ts';

export const buildIndividualTree = <D extends object>(
root: SelectionIndividualRecord,
// eslint-disable-next-line no-unused-vars
dataForIndividual: (individual: SelectionIndividualRecord, depth: number) => D,
depthLimit: number,
): IndividualTree<D> => {
if (root.length !== 1) {
throw new Error();
}
const visit = (individual: SelectionIndividualRecord, depth: number): IndividualTree<D> => {
const family = individual.getFamilyAsChild();
const husband = family.getHusband().getIndividualRecord().arraySelect()[0];
const wife = family.getWife().getIndividualRecord().arraySelect()[0];
const newDepth = depth + 1;
return {
...(depth < depthLimit - 1
? {
father: husband ? visit(husband, newDepth) : undefined,
mother: wife ? visit(wife, newDepth) : undefined,
}
: {}),
data: dataForIndividual(individual, depth),
};
};
return visit(root, 0);
};
57 changes: 57 additions & 0 deletions src/viz/DiskGeographyVisualization.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { useState } from 'react';
import { interpolateSinebow } from 'd3-scale-chromatic';
import { DiskVisualization, DiskVisualizationType } from './DiskVisualization.tsx';
import { GeographyDiskData } from '../scripts/types.ts';
import { Box, Slider, Stack } from '@mui/joy';

interface DiskGeographyVisualizationProps {
data: GeographyDiskData['tree'];
}

export const DiskGeographyVisualization: React.FC<DiskGeographyVisualizationProps> = ({ data }) => {
const [level, setLevel] = useState(1);
const formatPlace = (place: (string | null)[] | null, strict: boolean): string | null => {
if (place === null) {
return null;
}
const parts = place.slice(level);
const joinChar = ', ';
if (parts.every((v): v is string => v !== null)) {
return parts.join(joinChar);
} else if (strict) {
return null;
} else {
return parts.filter((v): v is string => v !== null).join(joinChar);
}
};
return (
<Box>
<DiskVisualization
data={data}
color={interpolateSinebow}
tooltip={(d) => (
<Stack alignItems="center">
<Box>Sosa {d.sosa}</Box>
<Box>{formatPlace(d.place, false)}</Box>
</Stack>
)}
type={DiskVisualizationType.CATEGORY}
category={(d) => formatPlace(d.place, true)}
/>
<Slider
step={1}
min={0}
max={2}
valueLabelDisplay="off"
marks={[
{ value: 0, label: 'Town' },
{ value: 1, label: 'Department' },
{ value: 2, label: 'Country' },
]}
track={false}
onChange={(e) => setLevel(parseInt((e.target as any).value))}
value={level}
/>
</Box>
);
};
41 changes: 41 additions & 0 deletions src/viz/DiskLongevityVisualization.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { useState } from 'react';
import { interpolateReds } from 'd3-scale-chromatic';
import { DiskVisualization, DiskVisualizationType } from './DiskVisualization';
import { LongevityDiskData } from '../scripts/types';
import { Box, ButtonGroup, IconButton, Stack } from '@mui/joy';
import { Female, JoinFull, Male } from '@mui/icons-material';

interface DiskLongevityVisualizationProps {
data: LongevityDiskData['tree'];
}

export const DiskLongevityVisualization: React.FC<DiskLongevityVisualizationProps> = ({ data }) => {
const [gender, setGender] = useState<boolean | null>(null);
return (
<Box>
<DiskVisualization
data={data}
color={interpolateReds}
tooltip={(d) => (
<Stack alignItems="center">
<Box>Sosa {d.sosa}</Box>
<Box>{d.longevity !== null ? `${d.longevity} years` : d.sosa >= 1 << 3 ? 'Unknown' : 'Still alive'}</Box>
</Stack>
)}
type={DiskVisualizationType.SCALE}
scale={(d) => (gender === null ? d.longevity : !!(d.sosa % 2) === gender ? d.longevity : null)}
/>
<ButtonGroup sx={{ '--ButtonGroup-radius': '40px', justifyContent: 'center', mt: 1 }}>
<IconButton onClick={() => setGender(false)} variant={gender === false ? 'solid' : 'soft'} color="primary">
<Male />
</IconButton>
<IconButton onClick={() => setGender(null)} variant={gender === null ? 'solid' : 'soft'} color="primary">
<JoinFull />
</IconButton>
<IconButton onClick={() => setGender(true)} variant={gender === true ? 'solid' : 'soft'} color="primary">
<Female />
</IconButton>
</ButtonGroup>
</Box>
);
};
Loading

0 comments on commit 02f6098

Please sign in to comment.