mirror of
https://github.com/gramps-project/gramps-web.git
synced 2026-10-17 05:10:56 +08:00
067208beeb
* Expand branches of the tree charts by one generation In the ancestor, descendant and hourglass charts, a card whose parents or children are not shown gets an arrow on the side where its branch grows. Clicking it adds one generation to that branch, keeping the selected person. The expanded people are fetched with the chart's request, so they are refreshed with it, and are forgotten when another person or chart is shown. The root person's menu button and the arrows share one style, with a backdrop that fades lines under the chevron. The person preview and the root person's shadow now belong to the card itself, so the buttons next to it neither open the preview nor cast a shadow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Show at least one generation of descendants in the tree charts With zero descendant generations, the descendant chart showed only the selected person and the hourglass chart only their ancestors. A stored value below a setting's minimum now counts as the minimum. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
269 lines
7.9 KiB
JavaScript
269 lines
7.9 KiB
JavaScript
import {describe, it, expect} from 'vitest'
|
|
import {FamilyGraph} from '../../src/charts/model/FamilyGraph.js'
|
|
import {
|
|
layoutAncestors,
|
|
layoutDescendants,
|
|
layoutHourglass,
|
|
} from '../../src/charts/layout/treeLayout.js'
|
|
|
|
const childRef = ref => ({ref, frel: 'Birth', mrel: 'Birth'})
|
|
|
|
const family = (handle, father, mother, children) => ({
|
|
handle,
|
|
father_handle: father,
|
|
mother_handle: mother,
|
|
child_ref_list: children.map(childRef),
|
|
})
|
|
|
|
const person = (handle, {parentFamily = {}, families = []} = {}) => ({
|
|
handle,
|
|
gramps_id: `I_${handle}`,
|
|
profile: {gramps_id: `I_${handle}`, sex: 'U'},
|
|
extended: {primary_parent_family: parentFamily, families},
|
|
})
|
|
|
|
// R has parents F and M, and children K1 and K2 with S. F has parents FF and
|
|
// FM, and M only has a mother MM. K1 has a child X who was not fetched.
|
|
const fR = family('fR', 'F', 'M', ['R'])
|
|
const fF = family('fF', 'FF', 'FM', ['F'])
|
|
const fM = family('fM', '', 'MM', ['M'])
|
|
const fK = family('fK', 'R', 'S', ['K1', 'K2'])
|
|
const fX = family('fX', 'K1', '', ['X'])
|
|
const graph = new FamilyGraph([
|
|
person('R', {parentFamily: fR, families: [fK]}),
|
|
person('F', {parentFamily: fF, families: [fR]}),
|
|
person('M', {parentFamily: fM, families: [fR]}),
|
|
person('FF', {families: [fF]}),
|
|
person('FM', {families: [fF]}),
|
|
person('MM', {families: [fM]}),
|
|
person('S', {families: [fK]}),
|
|
person('K1', {parentFamily: fK, families: [fX]}),
|
|
person('K2', {parentFamily: fK}),
|
|
])
|
|
|
|
const positions = layout =>
|
|
layout.nodes.map(({key, handle, generation, x, y}) => ({
|
|
key,
|
|
handle,
|
|
generation,
|
|
x,
|
|
y,
|
|
}))
|
|
|
|
const linkKeys = layout =>
|
|
layout.links.map(({source, target}) => [source.key, target.key])
|
|
|
|
describe('layoutAncestors', () => {
|
|
const layout = layoutAncestors(graph, 'R', {depth: 3})
|
|
|
|
it('places generations in columns to the right of the root person', () => {
|
|
expect(positions(layout)).toEqual([
|
|
{key: 'p', handle: 'R', generation: 0, x: 0, y: 0},
|
|
{key: 'pf', handle: 'F', generation: 1, x: 220, y: -71.25},
|
|
{key: 'pm', handle: 'M', generation: 1, x: 220, y: 71.25},
|
|
{key: 'pff', handle: 'FF', generation: 2, x: 440, y: -118.75},
|
|
{key: 'pfm', handle: 'FM', generation: 2, x: 440, y: -23.75},
|
|
{key: 'pmm', handle: 'MM', generation: 2, x: 440, y: 71.25},
|
|
])
|
|
})
|
|
|
|
it('links children to their parents', () => {
|
|
expect(linkKeys(layout)).toEqual([
|
|
['p', 'pf'],
|
|
['p', 'pm'],
|
|
['pf', 'pff'],
|
|
['pf', 'pfm'],
|
|
['pm', 'pmm'],
|
|
])
|
|
})
|
|
|
|
it('includes the boxes and horizontal padding in the bounds', () => {
|
|
expect(layout.bounds).toEqual({
|
|
xMin: -115,
|
|
xMax: 555,
|
|
yMin: -163.75,
|
|
yMax: 116.25,
|
|
})
|
|
})
|
|
|
|
it('keeps the person objects', () => {
|
|
expect(layout.nodes[1].person).toBe(graph.person('F'))
|
|
})
|
|
|
|
it('shows the root person for depth 0', () => {
|
|
expect(positions(layoutAncestors(graph, 'R', {depth: 0}))).toEqual([
|
|
{key: 'p', handle: 'R', generation: 0, x: 0, y: 0},
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('layoutDescendants', () => {
|
|
const layout = layoutDescendants(graph, 'R', {depth: 3, gapX: 60})
|
|
|
|
it('places generations in columns to the left of the root person', () => {
|
|
expect(positions(layout)).toEqual([
|
|
{key: 'p', handle: 'R', generation: 0, x: 0, y: 0},
|
|
{key: 'pc0', handle: 'K1', generation: -1, x: -250, y: -47.5},
|
|
{key: 'pc1', handle: 'K2', generation: -1, x: -250, y: 47.5},
|
|
{key: 'pc0c0', handle: undefined, generation: -2, x: -500, y: -47.5},
|
|
])
|
|
})
|
|
|
|
it('places the root person exactly at the origin', () => {
|
|
expect(layout.nodes[0].x).toBe(0)
|
|
expect(layout.nodes[0].generation).toBe(0)
|
|
})
|
|
|
|
it('keeps a child that was not fetched as an empty person', () => {
|
|
expect(layout.nodes[3].person).toEqual({})
|
|
})
|
|
|
|
it('includes the boxes and horizontal padding in the bounds', () => {
|
|
expect(layout.bounds).toEqual({
|
|
xMin: -615,
|
|
xMax: 115,
|
|
yMin: -92.5,
|
|
yMax: 92.5,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('layoutHourglass', () => {
|
|
const layout = layoutHourglass(graph, 'R', {
|
|
ancestorDepth: 2,
|
|
descendantDepth: 2,
|
|
gapX: 60,
|
|
})
|
|
|
|
it('places ancestors to the right and descendants to the left', () => {
|
|
expect(positions(layout)).toEqual([
|
|
{key: 'p', handle: 'R', generation: 0, x: 0, y: 0},
|
|
{key: 'pf', handle: 'F', generation: 1, x: 250, y: -47.5},
|
|
{key: 'pm', handle: 'M', generation: 1, x: 250, y: 47.5},
|
|
{key: 'pc0', handle: 'K1', generation: -1, x: -250, y: -47.5},
|
|
{key: 'pc1', handle: 'K2', generation: -1, x: -250, y: 47.5},
|
|
])
|
|
})
|
|
|
|
it('shares one root person node between both halves', () => {
|
|
expect(linkKeys(layout)).toEqual([
|
|
['p', 'pf'],
|
|
['p', 'pm'],
|
|
['p', 'pc0'],
|
|
['p', 'pc1'],
|
|
])
|
|
expect(layout.links.every(l => l.source === layout.nodes[0])).toBe(true)
|
|
})
|
|
|
|
it('includes both halves in the bounds', () => {
|
|
expect(layout.bounds).toEqual({
|
|
xMin: -365,
|
|
xMax: 365,
|
|
yMin: -92.5,
|
|
yMax: 92.5,
|
|
})
|
|
})
|
|
|
|
it('keeps the root person when the ancestor depth is 0', () => {
|
|
const shallow = layoutHourglass(graph, 'R', {
|
|
ancestorDepth: 0,
|
|
descendantDepth: 2,
|
|
})
|
|
expect(positions(shallow)).toEqual([
|
|
{key: 'p', handle: 'R', generation: 0, x: 0, y: 0},
|
|
{key: 'pc0', handle: 'K1', generation: -1, x: -220, y: -47.5},
|
|
{key: 'pc1', handle: 'K2', generation: -1, x: -220, y: 47.5},
|
|
])
|
|
expect(shallow.links.every(l => l.source === shallow.nodes[0])).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('vertical layouts', () => {
|
|
const layout = layoutHourglass(graph, 'R', {
|
|
ancestorDepth: 2,
|
|
descendantDepth: 2,
|
|
vertical: true,
|
|
})
|
|
|
|
it('places ancestors in rows above and descendants in rows below', () => {
|
|
expect(positions(layout)).toEqual([
|
|
{key: 'p', handle: 'R', generation: 0, x: 0, y: 0},
|
|
{key: 'pf', handle: 'F', generation: 1, x: -105, y: -130},
|
|
{key: 'pm', handle: 'M', generation: 1, x: 105, y: -130},
|
|
{key: 'pc0', handle: 'K1', generation: -1, x: -105, y: 130},
|
|
{key: 'pc1', handle: 'K2', generation: -1, x: 105, y: 130},
|
|
])
|
|
expect(layout.vertical).toBe(true)
|
|
})
|
|
|
|
it('includes the boxes and vertical padding in the bounds', () => {
|
|
expect(layout.bounds).toEqual({
|
|
xMin: -200,
|
|
xMax: 200,
|
|
yMin: -195,
|
|
yMax: 195,
|
|
})
|
|
})
|
|
|
|
it('places a single chart vertically too', () => {
|
|
const ancestors = layoutAncestors(graph, 'R', {depth: 2, vertical: true})
|
|
const descendants = layoutDescendants(graph, 'R', {
|
|
depth: 2,
|
|
vertical: true,
|
|
})
|
|
expect(ancestors.nodes.slice(1).map(node => node.y)).toEqual([-130, -130])
|
|
expect(descendants.nodes.slice(1).map(node => node.y)).toEqual([130, 130])
|
|
})
|
|
})
|
|
|
|
describe('branches', () => {
|
|
const expandable = layout =>
|
|
layout.nodes
|
|
.filter(node => node.expandable)
|
|
.map(({key, expandable: direction}) => [key, direction])
|
|
|
|
it('marks the ends of branches with relatives who are not shown', () => {
|
|
expect(expandable(layoutAncestors(graph, 'R', {depth: 2}))).toEqual([
|
|
['pf', 'ancestors'],
|
|
['pm', 'ancestors'],
|
|
])
|
|
// K2 has no children
|
|
expect(expandable(layoutDescendants(graph, 'R', {depth: 2}))).toEqual([
|
|
['pc0', 'descendants'],
|
|
])
|
|
})
|
|
|
|
it('shows one more generation in an expanded branch', () => {
|
|
const layout = layoutAncestors(graph, 'R', {
|
|
depth: 2,
|
|
expanded: new Set(['F']),
|
|
})
|
|
expect(layout.nodes.map(node => node.key)).toEqual([
|
|
'p',
|
|
'pf',
|
|
'pm',
|
|
'pff',
|
|
'pfm',
|
|
])
|
|
// FF and FM have no parents
|
|
expect(expandable(layout)).toEqual([['pm', 'ancestors']])
|
|
})
|
|
|
|
it('expands branches of both halves of an hourglass', () => {
|
|
const layout = layoutHourglass(graph, 'R', {
|
|
ancestorDepth: 2,
|
|
descendantDepth: 2,
|
|
expanded: new Set(['M', 'K1']),
|
|
})
|
|
expect(layout.nodes.map(node => node.key)).toEqual([
|
|
'p',
|
|
'pf',
|
|
'pm',
|
|
'pmm',
|
|
'pc0',
|
|
'pc1',
|
|
'pc0c0',
|
|
])
|
|
})
|
|
})
|