First version of integrating donatso/family-chart #185

This commit is contained in:
Przemek Więch
2025-01-16 17:54:20 +01:00
parent 5526d3dbf5
commit ae7357f27c
7 changed files with 957 additions and 22 deletions

View File

@@ -47,6 +47,7 @@ import {
WikiTreeDataSource,
WikiTreeSourceSpec,
} from './datasource/wikitree';
import {DonatsoChart} from './donatso-chart';
/**
* Load GEDCOM URL from REACT_APP_STATIC_URL environment variable.
@@ -142,6 +143,7 @@ function getArguments(location: H.Location<any>): Arguments {
const chartTypes = new Map<string | undefined, ChartType>([
['relatives', ChartType.Relatives],
['fancy', ChartType.Fancy],
['donatso', ChartType.Donatso],
]);
const hash = getParam('file');
@@ -481,6 +483,30 @@ export function App() {
setShowErrorPopup(false);
}
function renderChart(selection: IndiInfo) {
if (chartType === ChartType.Donatso) {
return (
<DonatsoChart
data={data!.chartData}
selection={selection}
onSelection={onSelection}
/>
);
}
return (
<Chart
data={data!.chartData}
selection={selection}
chartType={chartType}
onSelection={onSelection}
freezeAnimation={freezeAnimation}
colors={config.color}
hideIds={config.id}
hideSex={config.sex}
/>
);
}
function renderMainArea() {
switch (state) {
case AppState.SHOWING_CHART:
@@ -523,16 +549,7 @@ export function App() {
{state === AppState.LOADING_MORE ? (
<Loader active size="small" className="loading-more" />
) : null}
<Chart
data={data!.chartData}
selection={updatedSelection}
chartType={chartType}
onSelection={onSelection}
freezeAnimation={freezeAnimation}
colors={config.color}
hideIds={config.id}
hideSex={config.sex}
/>
{renderChart(updatedSelection)}
{showSidePanel ? (
<Media greaterThanOrEqual="large" className="sidePanel">
<Tab panes={sidePanelTabs} />

View File

@@ -26,6 +26,7 @@ import {
CircleRenderer,
ChartColors as TopolaChartColors,
} from 'topola';
import {usePrevious} from './util/previous-hook';
/** How much to zoom when using the +/- buttons. */
const ZOOM_FACTOR = 1.3;
@@ -217,6 +218,7 @@ export async function downloadPdf() {
export enum ChartType {
Hourglass,
Relatives,
Donatso,
Fancy,
}
@@ -402,14 +404,6 @@ class ChartWrapper {
}
}
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
});
return ref.current;
}
export function Chart(props: ChartProps) {
const chartWrapper = useRef(new ChartWrapper());
const prevProps = usePrevious(props);

105
src/donatso-chart.tsx Normal file
View File

@@ -0,0 +1,105 @@
import {useEffect, useRef} from 'react';
import {IntlShape, useIntl} from 'react-intl';
import {IndiInfo, JsonFam, JsonGedcomData} from 'topola';
import {formatDateOrRange} from './util/date_util';
import {usePrevious} from './util/previous-hook';
const f3: any = require('family-chart');
export interface DonatsoChartProps {
data: JsonGedcomData;
selection: IndiInfo;
onSelection: (indiInfo: IndiInfo) => void;
}
function getOtherSpouse(fam: JsonFam, indi: string) {
return fam.husb === indi ? fam.wife : fam.husb;
}
function convertData(data: JsonGedcomData, intl: IntlShape) {
const famMap = new Map<string, JsonFam>();
data.fams.forEach((fam) => famMap.set(fam.id, fam));
return data.indis.map((indi) => {
const famc = famMap.get(indi.famc!);
const fams = (indi.fams || [])
.map((fam) => famMap.get(fam))
.filter((fam): fam is JsonFam => fam !== undefined);
const father = famc?.husb;
const mother = famc?.wife;
const spouses = fams
.map((fam) => getOtherSpouse(fam, indi.id))
.filter((indi): indi is string => indi !== undefined);
const children = fams.flatMap((fam) => fam.children || []);
return {
id: indi.id,
data: {
'first name': indi.firstName,
'last name': indi.lastName,
birthday: formatDateOrRange(indi.birth, intl),
avatar: indi.images?.[0]?.url,
gender: indi.sex,
},
rels: {
father,
mother,
spouses,
children,
},
};
});
}
class ChartWrapper {
private store?: any;
initializeChart(props: DonatsoChartProps, intl: IntlShape) {
const data = convertData(props.data, intl);
this.store = f3.createStore({
data,
main_id: props.selection.id,
});
const svg = f3.createSvg(document.querySelector('#dotatsoSvgContainer'));
const card = f3.elements.Card({
store: this.store,
svg,
card_display: [
(i: any) =>
`${i.data['first name'] || ''} ${i.data['last name'] || ''}`,
(i: any) => `${i.data.birthday || ''}`,
],
mini_tree: true,
link_break: false,
onCardClick: (e: any, d: any) =>
props.onSelection({id: d.data.id, generation: 0}),
});
this.store.setOnUpdate((props: any) => {
f3.view(this.store.getTree(), svg, card, props || {});
});
this.store.updateTree({initial: true});
}
updateChart(props: DonatsoChartProps, intl: IntlShape) {
const data = convertData(props.data, intl);
this.store.updateData(data);
this.store.updateMainId(props.selection.id);
this.store.updateTree();
}
}
export function DonatsoChart(props: DonatsoChartProps) {
const chartWrapper = useRef(new ChartWrapper());
const prevProps = usePrevious(props);
const intl = useIntl();
useEffect(() => {
if (!prevProps) {
chartWrapper.current.initializeChart(props, intl);
} else {
chartWrapper.current.updateChart(props, intl);
}
});
return <div id="dotatsoSvgContainer"></div>;
}

View File

@@ -232,3 +232,49 @@ div.zoom {
padding: 14px 0px 0px;
}
#dotatsoSvgContainer {
flex: 1 1 auto;
overflow: auto;
}
#dotatsoSvgContainer svg.main_svg {
width: 100%;
height: 100%;
}
#dotatsoSvgContainer rect.card-female, #dotatsoSvgContainer .card-female .card-body-rect, #dotatsoSvgContainer .card-female .text-overflow-mask {
fill: rgb(196, 138, 146);
}
#dotatsoSvgContainer rect.card-male, #dotatsoSvgContainer .card-male .card-body-rect, #dotatsoSvgContainer .card-male .text-overflow-mask {
fill: rgb(120, 159, 172);
}
#dotatsoSvgContainer .card-genderless .card-body-rect, #dotatsoSvgContainer .card-genderless .text-overflow-mask {
fill: lightgray;
}
#dotatsoSvgContainer g.card_add text {
fill: #fff;
}
#dotatsoSvgContainer .card-main-outline {
stroke: black;
stroke-width: 3px;
}
#dotatsoSvgContainer .card_family_tree rect {
transition: 0.3s;
}
#dotatsoSvgContainer .card_family_tree:hover rect {
transform: scale(1.1);
}
#dotatsoSvgContainer .card_family_tree line {
stroke: black;
}
#dotatsoSvgContainer .link {
stroke: black;
}

View File

@@ -0,0 +1,9 @@
import {useEffect, useRef} from 'react';
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
});
return ref.current;
}