Skip to content

Commit

Permalink
Add descendant tree view (fixes #236)
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidMStraub committed Oct 17, 2023
1 parent 1a2da6a commit 7909a44
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 43 deletions.
53 changes: 41 additions & 12 deletions src/charts/TreeChart.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function TreeChart(
{
depth = 3,
padding = 20, // horizontal padding for first and last column
gapX = 20, // horizontal gap between boxes
gapX = 30, // horizontal gap between boxes
gapY = 5, // vertical gap between boxes
stroke = '#555', // stroke for links
strokeWidth = 1, // stroke width for links
Expand All @@ -42,6 +42,7 @@ export function TreeChart(
imgPadding = 10,
childrenTriangle = true,
getImageUrl = null,
orientation = 'LTR',
} = {}
) {
// Create a hierarchical data structure based on the input data
Expand All @@ -65,15 +66,25 @@ export function TreeChart(
if (d.x < x0) x0 = d.x
})

if (orientation === 'RTL') {
descendants.forEach(d => {
// eslint-disable-next-line no-param-reassign
d.y = -d.y
})
}
// Use the required curve
if (typeof curve !== 'function') throw new Error('Unsupported curve')
const width = trueDepth * boxWidth + (trueDepth - 1) * gapX + 2 * padding
const [minX, maxX] = getMinMaxX(descendants)
const height = maxX - minX + boxHeight
const yOffset = minX - boxHeight / 2
const xOffset =
orientation === 'RTL'
? boxWidth / 2 + padding - width
: -boxWidth / 2 - padding

const svg = create('svg')
.attr('viewBox', [-boxWidth / 2 - padding, yOffset, width, height])
.attr('viewBox', [xOffset, yOffset, width, height])
.attr('width', width)
.attr('height', height)
.attr('style', 'max-width: 100%; height: auto; height: intrinsic;')
Expand All @@ -91,12 +102,25 @@ export function TreeChart(
.selectAll('path')
.data(root.links())
.join('path')
.attr(
'd',
link(curve)
.x(d => d.y)
.y(d => d.x)
)
.attr('d', d => {
const sourceX = d.source.x
const sourceY =
orientation === 'LTR'
? d.source.y + boxWidth / 2 - 10
: d.source.y - boxWidth / 2 + 10
const targetX = d.target.x
const targetY =
orientation === 'LTR'
? d.target.y - boxWidth / 2 + 10
: d.target.y + boxWidth / 2 - 10

return link(curve)
.x(dd => dd.y)
.y(dd => dd.x)({
source: {x: sourceX, y: sourceY},
target: {x: targetX, y: targetY},
})
})

const node = svg
.append('g')
Expand Down Expand Up @@ -154,19 +178,24 @@ export function TreeChart(
e.preventDefault()
}

function yPos(d) {
return orientation === 'LTR'
? d.y - boxWidth / 2 - 12
: d.y + boxWidth / 2 + 12
}

if (childrenTriangle) {
const triangle = symbol().type(symbolTriangle).size(200)

const angle = orientation === 'LTR' ? -90 : 90

node
.append('path')
.filter(d => d.depth === 0)
.attr('d', triangle)
.attr(
'transform',
d =>
`translate(${d.y - boxWidth / 2 - 12},${
d.x
}) rotate(-90) scale(-1, 0.5)`
d => `translate(${yPos(d)},${d.x}) rotate(${angle}) scale(-1, 0.5)`
)
.attr('fill', '#bbb')
.attr('id', 'triangle-children')
Expand Down
16 changes: 12 additions & 4 deletions src/charts/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,19 @@ export const getDescendantTree = (data, handle, depth, i = 0, label = 'p') => {
if (depth === 1) {
return tree
}
const childHandles = (person?.extended?.families || [])
.flatMap(fam => fam.child_ref_list)
.map(cref => cref.ref)
const childHandles =
(person?.extended?.families || [])
.flatMap(fam => fam.child_ref_list)
.filter(childRef => childRef.frel === 'Birth')
.map(cref => cref.ref) ?? []
tree.children = childHandles.map((childHandle, childInd) =>
getTree(data, childHandle, depth - 1, false, i + 1, `${label}c${childInd}`)
getDescendantTree(
data,
childHandle,
depth - 1,
i + 1,
`${label}c${childInd}`
)
)
return tree
}
14 changes: 12 additions & 2 deletions src/components/GrampsjsTreeChart.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class GrampsjsTreeChart extends GrampsjsTranslateMixin(LitElement) {
grampsId: {type: String},
depth: {type: Number},
data: {type: Array},
descendants: {type: Boolean},
gapX: {type: Number},
}
}

Expand All @@ -50,6 +52,7 @@ class GrampsjsTreeChart extends GrampsjsTranslateMixin(LitElement) {
this.grampsId = ''
this.depth = 5
this.data = []
this.gapX = 30
}

render() {
Expand All @@ -71,13 +74,18 @@ class GrampsjsTreeChart extends GrampsjsTranslateMixin(LitElement) {
if (!handle) {
return ''
}
const data = getTree(this.data, handle, this.depth, false)

const data = this.descendants
? getDescendantTree(this.data, handle, this.depth)
: getTree(this.data, handle, this.depth, false)
return html`
<div id="container">
${TreeChart(data, {
depth: this.depth,
childrenTriangle: this._hasChildren(),
getImageUrl: d => getImageUrl(d?.data?.person || {}, 200),
orientation: this.descendants ? 'RTL' : 'LTR',
gapX: this.gapX,
})}
</div>
`
Expand All @@ -94,7 +102,9 @@ class GrampsjsTreeChart extends GrampsjsTranslateMixin(LitElement) {

renderChildrenMenu() {
const {handle} = getPersonByGrampsId(this.data, this.grampsId)
const data = getDescendantTree(this.data, handle, 2)
const data = this.descendants
? getTree(this.data, handle, 2, false)
: getDescendantTree(this.data, handle, 2)
const {children} = data
if (!children || !children.length) {
return ''
Expand Down
9 changes: 7 additions & 2 deletions src/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,13 @@ export function renderIcon(path, color = '#999999') {
></i>`
}

export function renderIconSvg(path, color = '#999999') {
return html`<svg height="24" width="24" viewBox="0 0 24 24">
export function renderIconSvg(path, color = '#999999', rotate = 0) {
return html`<svg
height="24"
width="24"
viewBox="0 0 24 24"
transform="rotate(${rotate})"
>
<path fill="${color}" d="${path}" />
</svg>`
}
Expand Down
36 changes: 36 additions & 0 deletions src/views/GrampsjsViewDescendantChart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {html} from 'lit'

import {GrampsjsViewTreeChartBase} from './GrampsjsViewTreeChartBase.js'
import '../components/GrampsjsTreeChart.js'

export class GrampsjsViewDescendantChart extends GrampsjsViewTreeChartBase {
constructor() {
super()
this.nAnc = 1
this.nDesc = 1
this._setAnc = false
}

_resetLevels() {
this.nDesc = 1
}

renderChart() {
return html`
<grampsjs-tree-chart
descendants
grampsId=${this.grampsId}
depth=${this.nDesc + 1}
.data=${this._data}
gapX="60"
.strings=${this.strings}
>
</grampsjs-tree-chart>
`
}
}

window.customElements.define(
'grampsjs-view-descendant-chart',
GrampsjsViewDescendantChart
)
41 changes: 39 additions & 2 deletions src/views/GrampsjsViewTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import '@material/mwc-tab'

import {mdiFamilyTree} from '@mdi/js'
import {GrampsjsView} from './GrampsjsView.js'
import './GrampsjsViewDescendantChart.js'
import './GrampsjsViewTreeChart.js'
import './GrampsjsViewFanChart.js'
import {fireEvent} from '../util.js'
Expand Down Expand Up @@ -83,7 +84,8 @@ export class GrampsjsViewTree extends GrampsjsView {
return html`
${this.renderTabs()}
${this._currentTabId === 0 ? this._renderPedigree() : ''}
${this._currentTabId === 1 ? this._renderFan() : ''}
${this._currentTabId === 1 ? this._renderDescendantTree() : ''}
${this._currentTabId === 2 ? this._renderFan() : ''}
`
}

Expand All @@ -101,14 +103,32 @@ export class GrampsjsViewTree extends GrampsjsView {
hasImageIcon
label="${this._('Ancestor Tree')}"
><span slot="icon"
>${renderIconSvg(mdiFamilyTree, 'var(--mdc-theme-primary)')}</span
>${renderIconSvg(
mdiFamilyTree,
'var(--mdc-theme-primary)',
-90
)}</span
>
</mwc-tab>
<mwc-tab
@click=${() => {
this._currentTabId = 1
}}
hasImageIcon
label="${this._('Descendant Tree')}"
><span slot="icon"
>${renderIconSvg(
mdiFamilyTree,
'var(--mdc-theme-primary)',
90
)}</span
>
</mwc-tab>
<mwc-tab
@click=${() => {
this._currentTabId = 2
}}
hasImageIcon
label="${this._('Fan Chart')}"
><span slot="icon"
>${renderIconSvg(
Expand Down Expand Up @@ -155,6 +175,23 @@ export class GrampsjsViewTree extends GrampsjsView {
`
}

_renderDescendantTree() {
return html`
<grampsjs-view-descendant-chart
@tree:back="${this._prevPerson}"
@tree:person="${this._goToPerson}"
@tree:home="${this._backToHomePerson}"
grampsId=${this.grampsId}
?active=${this.active}
.strings=${this.strings}
.settings=${this.settings}
?disableBack=${this._history.length < 2}
?disableHome=${this.grampsId === this.settings.homePerson}
>
</grampsjs-view-descendant-chart>
`
}

_prevPerson() {
this._history.pop()
this.grampsId = this._history.pop()
Expand Down
45 changes: 24 additions & 21 deletions src/views/GrampsjsViewTreeChartBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,34 +115,37 @@ export class GrampsjsViewTreeChartBase extends GrampsjsView {
>
<mwc-dialog id="menu-controls">
<table>
${
this._setAnc
? html` <tr>
<td>${this._('Max Ancestor Generations')}</td>
<td>
<mwc-textfield
value=${this.nAnc}
type="number"
min="1"
@change=${this._handleChangeAnc}
></mwc-textfield>
</td>
</tr>`
: ''
}${
this._setDesc
? html`
<tr>
<td>${this._('Max Ancestor Generations')}</td>
<td>${this._('Max Descendant Generations')}</td>
<td>
<mwc-textfield
value=${this.nAnc}
value=${this.nDesc}
type="number"
min="1"
@change=${this._handleChangeAnc}
min="0"
@change=${this._handleChangeDesc}
></mwc-textfield>
</td>
</tr>
${
this._setDesc
? html`
<tr>
<td>${this._('Max Descendant Generations')}</td>
<td>
<mwc-textfield
value=${this.nDesc}
type="number"
min="0"
@change=${this._handleChangeDesc}
></mwc-textfield>
</td>
</tr>
`
: ''
}
`
: ''
}
</table>
<mwc-button slot="primaryAction" dialogAction="close"
>${this._('done')}</mwc-button
Expand Down

0 comments on commit 7909a44

Please sign in to comment.