-
-
Save prasannapattam/f2ac32972c8df4ffafc0bd255492b479 to your computer and use it in GitHub Desktop.
export interface FamilyModel { | |
id: string; | |
personId: number; | |
label1: string; | |
label2: string; | |
name: string; | |
gender: string; | |
photo: string; | |
parentId: string; | |
personOrder?: number; | |
childOrder: number; // currently not used in UI | |
size?: number; | |
spouseId?: number; | |
spouseName?: string; | |
spouseLabel1: string; | |
spouseGender?: string; | |
spouseOrder?: number; // currently not used in UI | |
spouseDrillTo: boolean; | |
primarySpouseId?: string; // used in multiple spouses for the second spouse | |
spousePhoto: string; | |
path?: string; | |
} |
export default new (class TreeConfiguration { | |
chartContainer = '.chart-container'; // root svg | |
// height & width | |
nodeWidth = 150; | |
nodeWidthSpouse = 320; | |
nodeWidthDiff = this.nodeWidthSpouse - this.nodeWidth * 2; | |
linkShift = Math.round((this.nodeWidth + this.nodeWidthDiff) / 2); // for moving the link coming from parent | |
nodeHeight = 40; | |
nodeHeightSpouse = 40; | |
// margins | |
siblingsMargin = 20; | |
childrenMargin = 30; | |
neightbourMargin = 10; | |
rootMargin = 0; | |
// child & spouse lines | |
linkStroke = 'lightgray'; | |
linkStrokeWidth = 2; | |
connectionStroke = 'black'; | |
connectionStrokeWidth = 2; | |
})(); |
<template> | |
<div class="chart-container" style="width: 100%; min-height: 100%"></div> | |
</template> | |
<style lang="scss"> | |
.person-member { | |
display: flex; | |
flex-wrap: wrap; | |
height: 40px; | |
align-items: center; | |
justify-content: center; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.24); | |
} | |
.male-member { | |
background: radial-gradient(circle, #b0e0e6 0%, #00ced1 100%); | |
} | |
.female-member { | |
background: radial-gradient(circle, #ffb6c1 0%, #ff69b4 100%); | |
} | |
.female-spouse { | |
margin-left: -15px; | |
} | |
.male-spouse { | |
margin-right: -15px; | |
} | |
div.line { | |
margin-top: 13px; | |
width: 5px; | |
} | |
div.line hr { | |
border-style: solid; | |
border-width: 2px 0 0; | |
} | |
.person-box { | |
display: flex; | |
height: 40px; | |
align-items: center; | |
width: 100%; | |
} | |
.person-icon { | |
width: 40px; | |
height: 40px; | |
} | |
.person-icon-div { | |
width: 40px; | |
height: 40px; | |
} | |
.person-name { | |
padding-left: 1px; | |
text-align: left; | |
justify-content: center; | |
} | |
.selected-person { | |
height: 42px; | |
border: 3px solid #152785; | |
} | |
.view-person { | |
position: relative; | |
left: -8px; | |
top: -44px; | |
color: black; | |
text-decoration: none; | |
font-size: 10px; | |
background-color: white; | |
width: 20px; | |
} | |
.rotate { | |
transform: rotate(-45deg); | |
-webkit-transform: rotate(-45deg); | |
-moz-transform: rotate(-45deg); | |
-ms-transform: rotate(-45deg); | |
-o-transform: rotate(-45deg); | |
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); | |
} | |
.profile-photo { | |
border-radius: 50%; | |
} | |
.drill-to { | |
position: relative; | |
left: 125px; | |
top: -35px; | |
font-size: 25px; | |
} | |
.hide { | |
display: none; | |
} | |
</style> | |
<script setup lang="ts"> | |
import * as d3 from 'd3'; | |
import { onMounted, watch } from 'vue'; | |
import { useQuasar } from 'quasar'; | |
import { FamilyModel } from 'src/core/models/family.model'; | |
import { UserProfileModel } from 'src/core/models/user/user-profile.model'; | |
import maleIcon from 'src/assets/images/male.svg'; | |
import femaleIcon from 'src/assets/images/female.svg'; | |
import treeConfiguration from 'src/core/services/tree-configuration'; | |
import { OrgChart, Connection, NodeId } from 'd3-org-chart'; | |
import { HierarchyNode } from 'd3-hierarchy'; | |
import session1 from 'src/core/services/session'; | |
import { constants } from 'src/core/common/constants'; | |
import { PersonService } from 'src/core/services/person.service'; | |
// import PersonDetailsDialog from 'src/components/PersonDetailsDialog.vue'; | |
import { eventBus } from 'src/core/common/event-bus'; | |
import { RelatedPersonModel } from 'src/core/models/related-person.model'; | |
import { useSessionStore } from 'src/stores/session.store'; | |
import { useRouter } from 'vue-router'; | |
const session = useSessionStore(); | |
const router = useRouter(); | |
// class variables | |
let chart: OrgChart<FamilyModel>; | |
let selectedPersonId: number; | |
let loadTreePersonId = 1; // this is the id used to loadTree | |
let prevSelectedPersonId: number; | |
let familyData: FamilyModel[] = []; | |
const $q = useQuasar(); | |
onMounted(async () => { | |
await loadTree(session.userProfile.personId); | |
}); | |
// event bus related events | |
eventBus.on('search-person', (personId: number) => { | |
loadTree(personId); | |
}); | |
eventBus.on('related-persons', (relatedPerson: RelatedPersonModel) => { | |
loadRelatedTree(relatedPerson.firstPersonId, relatedPerson.relatedPersonId); | |
}); | |
watch([session1.refreshDetails], ([newRefresh], [oldRefesh]) => { | |
// refresh | |
if (oldRefesh !== newRefresh) { | |
if (newRefresh) { | |
refresh(); | |
// wait for 100ms to rest the refreshDetails so that watch triggers in all the screens | |
setTimeout(() => { | |
session1.refreshDetails.value = false; | |
}, 100); | |
} | |
} | |
}); | |
async function loadRelatedTree(firstPersonId: number, relatedPersonId: number) { | |
selectedPersonId = -1; | |
familyData = await PersonService.getRelationship( | |
firstPersonId, | |
relatedPersonId | |
); | |
d3.select('.svg-chart-container').remove(); | |
renderChart(familyData); | |
prevSelectedPersonId = -1; // always highlight related person | |
selectRelatedNodes(firstPersonId, relatedPersonId); | |
} | |
function selectRelatedNodes(firstPersonId: number, relatedPersonId: number) { | |
let personElement = document.getElementsByClassName( | |
`person-${firstPersonId}` | |
)[0]; | |
personElement.classList.add('selected-person'); | |
personElement = document.getElementsByClassName( | |
`person-${relatedPersonId}` | |
)[0]; | |
personElement.classList.add('selected-person'); | |
} | |
async function loadTree( | |
personId: number, | |
refreshPersonId: number | undefined = undefined | |
) { | |
if (refreshPersonId !== undefined) selectedPersonId = refreshPersonId; | |
else selectedPersonId = personId; | |
loadTreePersonId = personId; | |
familyData = await PersonService.getHierarchy(personId); | |
d3.select('.svg-chart-container').remove(); | |
renderChart(familyData); | |
prevSelectedPersonId = selectedPersonId; | |
} | |
function renderChart(data: FamilyModel[]) { | |
chart = new OrgChart<FamilyModel>() | |
.container(treeConfiguration.chartContainer) | |
.data(data) | |
//.layout('left') | |
.onNodeClick((nodeId: NodeId) => nodeClicked(nodeId)) | |
.rootMargin(treeConfiguration.rootMargin) | |
.nodeWidth((d: HierarchyNode<FamilyModel>) => { | |
if (d.data.id === constants.rootId) return 0; | |
if (d.data.primarySpouseId !== undefined) { | |
return treeConfiguration.nodeWidth; | |
} | |
return d.data.spouseId !== undefined | |
? treeConfiguration.nodeWidthSpouse | |
: treeConfiguration.nodeWidth; | |
}) | |
.nodeHeight((d: HierarchyNode<FamilyModel>) => { | |
if (d.data.id === constants.rootId) return 0; | |
else | |
return d.data.spouseId !== undefined | |
? treeConfiguration.nodeHeightSpouse | |
: treeConfiguration.nodeHeight; | |
}) | |
.childrenMargin(() => treeConfiguration.childrenMargin) | |
.siblingsMargin(() => treeConfiguration.siblingsMargin) | |
.neightbourMargin(() => treeConfiguration.neightbourMargin) | |
.linkUpdate(function (d: HierarchyNode<FamilyModel>) { | |
// drawing the connecting line | |
if ( | |
d.data.parentId === constants.rootId || | |
d.data.primarySpouseId !== undefined | |
) { | |
return; | |
} else { | |
d3.select(this) | |
.attr('stroke', () => treeConfiguration.linkStroke) | |
.attr('stroke-width', () => treeConfiguration.linkStrokeWidth); | |
} | |
}) | |
.connectionsUpdate(function () { | |
d3.select(this) | |
.attr('stroke', () => treeConfiguration.connectionStroke) | |
.attr('stroke-width', () => treeConfiguration.connectionStrokeWidth) | |
.lower(); | |
}) | |
.nodeContent(function (d: HierarchyNode<FamilyModel>) { | |
const personData: FamilyModel = <FamilyModel>d.data; | |
let extraCss = ''; | |
if (personData.primarySpouseId !== undefined) { | |
if (personData.gender === 'M') { | |
extraCss = 'female-spouse'; | |
} else { | |
extraCss = 'male-spouse'; | |
} | |
} | |
let nodeHtml = `<div class="row ${extraCss}">`; | |
if (personData.primarySpouseId !== undefined) { | |
// additional spouses | |
nodeHtml += getPersonNodeContent(personData, 'spouse'); | |
} else if (personData.gender === 'F') { | |
nodeHtml += getPersonNodeContent(personData, 'spouse'); | |
nodeHtml += getPersonNodeContent(personData, 'person'); | |
} else { | |
nodeHtml += getPersonNodeContent(personData, 'person'); | |
nodeHtml += getPersonNodeContent(personData, 'spouse'); | |
} | |
nodeHtml += '</div>'; | |
return nodeHtml; | |
}) | |
.compact(false); | |
// changing the links for persons who has spouse | |
chart.layoutBindings().top.linkX = (d: HierarchyNode<FamilyModel>) => { | |
let x = d.x; | |
if (d.data === undefined) { | |
// Using x & y locations get the corresponding person data | |
const allNodes = chart.getChartState().allNodes; | |
allNodes.forEach((node) => { | |
if (node.x === d.x && node.y === d.y) { | |
if (node.data.gender === 'M') { | |
x = d.x + d.width / 2; | |
} else { | |
x = d.x - d.width / 2; | |
} | |
} | |
}); | |
} else if (d.data.spouseId !== undefined && d.data.gender === 'M') { | |
x = d.x - treeConfiguration.linkShift; // for parent to child link | |
} else if (d.data.spouseId !== undefined && d.data.gender === 'F') { | |
x = d.x + treeConfiguration.linkShift; // for parent to child link | |
} else { | |
x = d.x; | |
} | |
return x; | |
}; | |
chart.layoutBindings().top.linkY = (d: HierarchyNode<FamilyModel>) => { | |
if (d.data === undefined) { | |
// connections | |
return d.y + d.height / 2; | |
} else { | |
return d.y; | |
} | |
}; | |
chart.layoutBindings().top.linkJoinX = (d: HierarchyNode<FamilyModel>) => { | |
let x = d.x; | |
if (d.data === undefined) { | |
// connections | |
// Using x & y locations get the corresponding person data | |
const allNodes = chart.getChartState().allNodes; | |
allNodes.forEach((node) => { | |
if (node.x === d.x && node.y === d.y) { | |
if (node.data.gender === 'M') { | |
x = d.x - d.width / 2 - 15; | |
} else { | |
x = d.x + d.width / 2 + 15; | |
} | |
} | |
}); | |
} else { | |
x = d.x; | |
} | |
return x; | |
}; | |
chart.layoutBindings().top.linkJoinY = (d: HierarchyNode<FamilyModel>) => { | |
if (d.data === undefined) { | |
// connections | |
return d.y + d.height / 2; | |
} else { | |
return d.y; | |
} | |
}; | |
// checking for multiple spouses | |
const multipleSpouseConnections: Connection[] = []; | |
data.forEach((model) => { | |
if (model.primarySpouseId !== undefined) { | |
multipleSpouseConnections.push({ | |
from: model.primarySpouseId, | |
to: model.id, | |
label: '', | |
}); | |
} | |
}); | |
chart.connections(multipleSpouseConnections); | |
chart.render(); | |
// getting the selectedPerons to show | |
const selectedPersons = familyData.filter( | |
(p) => p.personId === selectedPersonId | |
); | |
if (selectedPersons.length > 0) { | |
const nodeId: string = selectedPersons[0].id; | |
chart.setCentered(nodeId); | |
} | |
chart.expandAll(); | |
// hiding the root node | |
const rootIdNode = document.querySelector( | |
'.person-' + constants.rootPersonId | |
); | |
const parentRootIdNode = rootIdNode?.closest('.node'); | |
if (parentRootIdNode !== undefined && parentRootIdNode !== null) | |
parentRootIdNode.style.display = 'none'; | |
} | |
function getPersonNodeContent(personData: FamilyModel, personType: string) { | |
const person: UserProfileModel = <UserProfileModel>{}; | |
if (personType === 'spouse') { | |
if (personData.spouseId !== undefined) { | |
person.personId = personData.spouseId; | |
} else { | |
return ''; | |
} | |
person.personName = personData.spouseLabel1; | |
person.gender = personData.spouseGender; | |
person.photo = personData.spousePhoto; | |
} else { | |
person.personId = personData.personId; | |
person.personName = personData.label1; | |
person.gender = personData.gender; | |
person.photo = personData.photo; | |
} | |
let personCssClass, personIcon; | |
let photoClass = ''; | |
if (person.gender === 'M') { | |
personCssClass = 'male-member'; | |
personIcon = maleIcon; | |
} else { | |
personCssClass = 'female-member'; | |
personIcon = femaleIcon; | |
} | |
if (person.photo !== undefined && person.photo !== null) { | |
personIcon = person.photo; | |
photoClass = 'profile-photo'; | |
} | |
let nodeContent = ''; | |
if ( | |
personData.spouseId !== undefined && | |
person.gender === 'F' && | |
personData.primarySpouseId === undefined | |
) { | |
nodeContent += '<div class="line"><hr/></div>'; | |
} | |
let selectedPersonCssClass = ''; | |
if (selectedPersonId === person.personId) { | |
selectedPersonCssClass = 'selected-person'; | |
} | |
let drillToHide = 'hide'; | |
if (personType === 'spouse' && personData.spouseDrillTo) { | |
drillToHide = ''; // hide drill to icon | |
} | |
nodeContent += ` | |
<div class="col ${personCssClass} person-member person-${person.personId} ${selectedPersonCssClass}"> | |
<div class="person-box" onclick="window.selectedPersonId=${person.personId};"> | |
<div class="person-icon-div" > | |
<div onclick="window.viewPersonId=${person.personId};"> | |
<img src="${personIcon}" class="person-icon ${photoClass}" /> | |
<div class="view-person rotate">view</div> | |
</div> | |
<i class="fa-solid fa-street-view drill-to ${drillToHide}"></i> | |
</div> | |
<div class="col person-name">${person.personName}</div> | |
</div> | |
</div>`; | |
return nodeContent; | |
//} | |
} | |
function nodeClicked(id: NodeId) { | |
// highlight the user clicked node | |
highlightSelectedNode(); | |
if (window.viewPersonId != undefined) { | |
showDetails(window.viewPersonId); | |
window.viewPersonId = undefined; | |
} else { | |
const person = familyData.filter((p) => p.id === id)[0]; | |
if (person.spouseId === window.selectedPersonId && person.spouseDrillTo) { | |
loadTree(person.spouseId); | |
} | |
} | |
} | |
function highlightSelectedNode() { | |
// remove prev selected person class and selecting currnet one | |
let personElement; | |
if (prevSelectedPersonId > 0) { | |
personElement = document.getElementsByClassName( | |
`person-${prevSelectedPersonId}` | |
)[0]; | |
personElement.classList.remove('selected-person'); | |
} | |
selectedPersonId = window.selectedPersonId; | |
personElement = document.getElementsByClassName( | |
`person-${selectedPersonId}` | |
)[0]; | |
personElement.classList.add('selected-person'); | |
prevSelectedPersonId = selectedPersonId; | |
} | |
function showDetails(viewPersonId: number) { | |
router.push({ | |
name: 'person-detail', | |
params: { | |
detailPersonId: viewPersonId, | |
}, | |
}); | |
} | |
function refresh() { | |
loadTree(loadTreePersonId, session1.refreshPersonId); | |
session1.refreshPersonId = -1; | |
} | |
</script> |
This code was written one year back and currently I don't have sample data.
Here is a sample data I think will work (you may need to debug to check whether this data works or not)
`[
{
id: '1-2', // this is unique id for husband and wife
personId: 1,
label1: 'Father',
label2: 'Father',
name: 'Father',
gender: "M",
photo: "",
parentId: -1, // root parent
personOrder?: ,
childOrder: 1; // currently not used in UI
size?: 1; // probably not used
spouseId?: 2;
spouseName?: "Mother",
spouseLabel1: "Mother",
spouseGender?: "F",
spouseOrder?: 1; // currently not used in UI
spouseDrillTo: false, // this is required to show multiple trees
primarySpouseId?: undefined; // used in multiple spouses for the second spouse
spousePhoto: undefined,
path?: undefined, // may not be used
}
{
id: '3-4', // this is unique id for husband and wife
personId: 3,
label1: 'Son',
label2: 'Son',
name: 'Son',
gender: "M",
photo: "",
parentId: '1-2', // root parent
personOrder?: ,
childOrder: 1; // currently not used in UI
size?: 1; // probably not used
spouseId?: 4;
spouseName?: "Wife - 1",
spouseLabel1: "Wife - 1",
spouseGender?: "F",
spouseOrder?: 1; // currently not used in UI
spouseDrillTo: false, // this is required to show multiple trees
primarySpouseId?: undefined; // used in multiple spouses for the second spouse
spousePhoto: undefined,
path?: undefined, // may not be used
}
{
id: '3-5', // this is unique id for husband and wife
personId: 3,
label1: 'Son',
label2: 'Son',
name: 'Son',
gender: "M",
photo: "",
parentId: '1-2', // root parent
personOrder?: ,
childOrder: 1; // currently not used in UI
size?: 1; // probably not used
spouseId?: 5;
spouseName?: "Wife - 2",
spouseLabel1: "Wife - 2",
spouseGender?: "F",
spouseOrder?: 1; // currently not used in UI
spouseDrillTo: false, // this is required to show multiple trees
primarySpouseId?: 4; // used in multiple spouses for the second spouse
spousePhoto: undefined,
path?: undefined, // may not be used
}
]`
Hi @prasannapattam thanks for the great implementation can you please post an example of how the data is structured if multiple marriage exist.