Skip to content

Commit 19c447e

Browse files
Improve pedigree chart
1 parent 2096b90 commit 19c447e

3 files changed

Lines changed: 340 additions & 17 deletions

File tree

app/Http/Livewire/PedigreeChart.php

Lines changed: 120 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,45 +9,149 @@ class PedigreeChartWidget extends Widget
99
{
1010
protected string $view = 'filament.widgets.pedigree-chart-widget';
1111

12+
public $rootPersonId = null;
13+
public $generations = 4;
14+
public $showDates = true;
15+
public $showPhotos = false;
16+
17+
public function mount($rootPersonId = null, $generations = 4)
18+
{
19+
$this->rootPersonId = $rootPersonId ?? Person::first()?->id;
20+
$this->generations = $generations;
21+
}
22+
1223
public function getData(): array
1324
{
25+
if (!$this->rootPersonId) {
26+
return ['tree' => [], 'rootPerson' => null];
27+
}
28+
29+
$rootPerson = Person::with(['childInFamily.husband', 'childInFamily.wife'])->find($this->rootPersonId);
30+
$tree = $this->buildPedigreeTree($rootPerson, $this->generations);
31+
1432
return [
15-
'people' => Person::with('parents')->get(),
33+
'tree' => $tree,
34+
'rootPerson' => $rootPerson,
35+
'generations' => $this->generations,
36+
'showDates' => $this->showDates,
37+
'showPhotos' => $this->showPhotos,
38+
];
39+
}
40+
41+
private function buildPedigreeTree($person, $generations, $level = 0): array
42+
{
43+
if (!$person || $level >= $generations) {
44+
return [];
45+
}
46+
47+
$personData = [
48+
'id' => $person->id,
49+
'name' => $person->fullname(),
50+
'givn' => $person->givn,
51+
'surn' => $person->surn,
52+
'sex' => $person->sex,
53+
'birth_date' => $person->birthday?->format('Y-m-d'),
54+
'death_date' => $person->deathday?->format('Y-m-d'),
55+
'level' => $level,
56+
'position' => pow(2, $level),
57+
'parents' => []
1658
];
59+
60+
// Get parents through family relationship
61+
if ($person->childInFamily) {
62+
$family = $person->childInFamily;
63+
64+
if ($family->husband) {
65+
$personData['parents']['father'] = $this->buildPedigreeTree($family->husband, $generations, $level + 1);
66+
}
67+
68+
if ($family->wife) {
69+
$personData['parents']['mother'] = $this->buildPedigreeTree($family->wife, $generations, $level + 1);
70+
}
71+
}
72+
73+
return $personData;
1774
}
1875

19-
#[\Override]
2076
public function render(): \Illuminate\Contracts\View\View
2177
{
2278
return view(static::$view, $this->getData());
2379
}
2480

25-
public function initializeChart(): void
81+
public function setRootPerson($personId): void
2682
{
27-
$this->dispatchBrowserEvent('initializeChart', ['people' => $this->getData()['people']->toJson()]);
83+
$this->rootPersonId = $personId;
84+
$this->dispatch('refreshChart');
2885
}
2986

30-
public function zoomIn(): void
87+
public function setGenerations($generations): void
3188
{
32-
$this->dispatchBrowserEvent('zoomIn');
89+
$this->generations = max(1, min(6, $generations));
90+
$this->dispatch('refreshChart');
3391
}
3492

35-
public function zoomOut(): void
93+
public function toggleDates(): void
3694
{
37-
$this->dispatchBrowserEvent('zoomOut');
95+
$this->showDates = !$this->showDates;
96+
$this->dispatch('refreshChart');
3897
}
3998

40-
public function pan($direction): void
99+
public function togglePhotos(): void
41100
{
42-
$this->dispatchBrowserEvent('pan', ['direction' => $direction]);
101+
$this->showPhotos = !$this->showPhotos;
102+
$this->dispatch('refreshChart');
43103
}
44104

45-
protected function getListeners()
105+
public function expandPerson($personId): void
46106
{
47-
return [
48-
'zoomIn' => 'zoomIn',
49-
'zoomOut' => 'zoomOut',
50-
'pan' => 'pan',
51-
];
107+
$this->setRootPerson($personId);
108+
}
109+
110+
public function renderPedigreeTree($tree, $level = 0): string
111+
{
112+
if (empty($tree)) {
113+
return '';
114+
}
115+
116+
$html = '<div class="generation-level level-' . $level . '">';
117+
118+
// Add connection line if not root level
119+
if ($level > 0) {
120+
$html .= '<div class="connection-line"></div>';
121+
}
122+
123+
// Person box
124+
$sexClass = strtolower($tree['sex'] ?? 'unknown');
125+
$html .= '<div class="person-box ' . $sexClass . '" onclick="expandPerson(' . $tree['id'] . ')">';
126+
$html .= '<button class="expand-btn" title="Expand from this person">↑</button>';
127+
$html .= '<div class="person-name">' . htmlspecialchars($tree['name']) . '</div>';
128+
129+
if ($this->showDates) {
130+
$birthDate = $tree['birth_date'] ? date('Y', strtotime($tree['birth_date'])) : '?';
131+
$deathDate = $tree['death_date'] ? date('Y', strtotime($tree['death_date'])) : '';
132+
$dateRange = $birthDate . ($deathDate ? ' - ' . $deathDate : ' - ');
133+
$html .= '<div class="person-dates">' . $dateRange . '</div>';
134+
}
135+
136+
$html .= '</div>';
137+
138+
// Parents
139+
if (!empty($tree['parents'])) {
140+
$html .= '<div class="parents-container">';
141+
142+
if (!empty($tree['parents']['father'])) {
143+
$html .= $this->renderPedigreeTree($tree['parents']['father'], $level + 1);
144+
}
145+
146+
if (!empty($tree['parents']['mother'])) {
147+
$html .= $this->renderPedigreeTree($tree['parents']['mother'], $level + 1);
148+
}
149+
150+
$html .= '</div>';
151+
}
152+
153+
$html .= '</div>';
154+
155+
return $html;
52156
}
53157
}

app/Models/Person.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,31 @@ public function familiesAsWife()
8585

8686
public function parents()
8787
{
88-
return $this->childInFamily->parents();
88+
if (!$this->childInFamily) {
89+
return collect();
90+
}
91+
92+
$parents = collect();
93+
94+
if ($this->childInFamily->husband) {
95+
$parents->push($this->childInFamily->husband);
96+
}
97+
98+
if ($this->childInFamily->wife) {
99+
$parents->push($this->childInFamily->wife);
100+
}
101+
102+
return $parents;
103+
}
104+
105+
public function father()
106+
{
107+
return $this->childInFamily?->husband;
108+
}
109+
110+
public function mother()
111+
{
112+
return $this->childInFamily?->wife;
89113
}
90114

91115
public function children()
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
<x-filament::widget class="filament-pedigree-chart-widget">
2+
<x-filament::card>
3+
<div class="pedigree-chart-header">
4+
<div class="flex justify-between items-center mb-4">
5+
<h3 class="text-lg font-semibold">Pedigree Chart</h3>
6+
<div class="flex gap-2">
7+
<select wire:model.live="generations" class="rounded border-gray-300 text-sm">
8+
<option value="2">2 Generations</option>
9+
<option value="3">3 Generations</option>
10+
<option value="4">4 Generations</option>
11+
<option value="5">5 Generations</option>
12+
<option value="6">6 Generations</option>
13+
</select>
14+
<button wire:click="toggleDates" class="px-3 py-1 text-sm rounded {{ $showDates ? 'bg-blue-500 text-white' : 'bg-gray-200' }}">
15+
Dates
16+
</button>
17+
<button wire:click="togglePhotos" class="px-3 py-1 text-sm rounded {{ $showPhotos ? 'bg-blue-500 text-white' : 'bg-gray-200' }}">
18+
Photos
19+
</button>
20+
</div>
21+
</div>
22+
</div>
23+
24+
<div id="pedigree-chart-container" class="pedigree-chart-container">
25+
@if($tree)
26+
<div class="pedigree-tree">
27+
{!! $this->renderPedigreeTree($tree) !!}
28+
</div>
29+
@else
30+
<div class="text-center py-8 text-gray-500">
31+
<p>No data available to display the pedigree chart.</p>
32+
<p class="text-sm mt-2">Please select a person to start building the tree.</p>
33+
</div>
34+
@endif
35+
</div>
36+
37+
@if($rootPerson)
38+
<div class="mt-4 text-sm text-gray-600">
39+
<p><strong>Root Person:</strong> {{ $rootPerson->fullname() }}</p>
40+
<p><strong>Generations:</strong> {{ $generations }}</p>
41+
</div>
42+
@endif
43+
</x-filament::card>
44+
45+
@push('styles')
46+
<style>
47+
.pedigree-chart-container {
48+
overflow-x: auto;
49+
overflow-y: hidden;
50+
min-height: 400px;
51+
background: #f8fafc;
52+
border-radius: 8px;
53+
padding: 20px;
54+
}
55+
56+
.pedigree-tree {
57+
display: flex;
58+
flex-direction: column;
59+
align-items: flex-start;
60+
min-width: max-content;
61+
}
62+
63+
.person-box {
64+
background: white;
65+
border: 2px solid #e2e8f0;
66+
border-radius: 8px;
67+
padding: 12px;
68+
margin: 4px;
69+
min-width: 180px;
70+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
71+
transition: all 0.3s ease;
72+
cursor: pointer;
73+
position: relative;
74+
}
75+
76+
.person-box:hover {
77+
border-color: #3b82f6;
78+
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
79+
transform: translateY(-2px);
80+
}
81+
82+
.person-box.male {
83+
border-left: 4px solid #3b82f6;
84+
}
85+
86+
.person-box.female {
87+
border-left: 4px solid #ec4899;
88+
}
89+
90+
.person-name {
91+
font-weight: 600;
92+
color: #1f2937;
93+
margin-bottom: 4px;
94+
}
95+
96+
.person-dates {
97+
font-size: 0.875rem;
98+
color: #6b7280;
99+
line-height: 1.4;
100+
}
101+
102+
.generation-level {
103+
display: flex;
104+
align-items: center;
105+
margin-bottom: 20px;
106+
}
107+
108+
.generation-level:not(:first-child) {
109+
margin-left: 40px;
110+
}
111+
112+
.connection-line {
113+
width: 40px;
114+
height: 2px;
115+
background: #d1d5db;
116+
margin: 0 10px;
117+
}
118+
119+
.parents-container {
120+
display: flex;
121+
flex-direction: column;
122+
gap: 8px;
123+
}
124+
125+
.level-0 .person-box {
126+
border-width: 3px;
127+
border-color: #059669;
128+
background: #f0fdf4;
129+
}
130+
131+
.level-1 .person-box {
132+
background: #fef3c7;
133+
}
134+
135+
.level-2 .person-box {
136+
background: #fce7f3;
137+
}
138+
139+
.level-3 .person-box {
140+
background: #e0f2fe;
141+
}
142+
143+
.expand-btn {
144+
position: absolute;
145+
top: -8px;
146+
right: -8px;
147+
width: 20px;
148+
height: 20px;
149+
border-radius: 50%;
150+
background: #3b82f6;
151+
color: white;
152+
border: none;
153+
font-size: 12px;
154+
cursor: pointer;
155+
display: flex;
156+
align-items: center;
157+
justify-content: center;
158+
}
159+
160+
.expand-btn:hover {
161+
background: #2563eb;
162+
}
163+
164+
@media (max-width: 768px) {
165+
.pedigree-chart-container {
166+
padding: 10px;
167+
}
168+
169+
.person-box {
170+
min-width: 140px;
171+
padding: 8px;
172+
}
173+
174+
.generation-level:not(:first-child) {
175+
margin-left: 20px;
176+
}
177+
}
178+
</style>
179+
@endpush
180+
181+
@push('scripts')
182+
<script>
183+
document.addEventListener('livewire:init', () => {
184+
Livewire.on('refreshChart', () => {
185+
// Chart will be refreshed automatically by Livewire
186+
console.log('Pedigree chart refreshed');
187+
});
188+
});
189+
190+
function expandPerson(personId) {
191+
@this.call('expandPerson', personId);
192+
}
193+
</script>
194+
@endpush
195+
</x-filament::widget>

0 commit comments

Comments
 (0)