Initial commit: webtrees Family Navigator Graph sidebar module
SVG-based sidebar widget displaying immediate family relationships (parents, siblings, spouses, children) with compact card layout, multi-spouse routing, wrapped rows, and ancestor/descendant indicators.
This commit is contained in:
116
resources/js/modules/lib/chart.js
Normal file
116
resources/js/modules/lib/chart.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Compact sidebar chart orchestrator.
|
||||
*
|
||||
* Renders an inline, auto-sized SVG that flows naturally in the page.
|
||||
* No zoom/pan — the graph fits the sidebar width, growing vertically as needed.
|
||||
* Wide generations (many children) are wrapped into multiple rows.
|
||||
*/
|
||||
import { computeLayout } from "./layout/layout.js";
|
||||
import { renderPersonCard } from "./chart/box.js";
|
||||
import { select } from "./d3.js";
|
||||
|
||||
export default class Chart {
|
||||
constructor(containerSelector, data) {
|
||||
this.containerSelector = containerSelector;
|
||||
this.data = data;
|
||||
this.config = {
|
||||
cardWidth: 130,
|
||||
cardHeight: 42,
|
||||
horizontalSpacing: 18,
|
||||
verticalSpacing: 45,
|
||||
};
|
||||
}
|
||||
|
||||
async render() {
|
||||
const ctr = this.containerSelector;
|
||||
const chartEl = select(`${ctr} .full-diagram-chart`);
|
||||
const containerWidth = chartEl.node().getBoundingClientRect().width || 320;
|
||||
|
||||
// Pass sidebar width so layout can wrap wide generations
|
||||
this.config.targetWidth = containerWidth;
|
||||
|
||||
const layout = computeLayout(
|
||||
this.data.persons,
|
||||
this.data.mainId,
|
||||
this.config
|
||||
);
|
||||
|
||||
// Compute bounding box of all content
|
||||
const pad = 12;
|
||||
const bounds = this.computeBounds(layout, pad);
|
||||
|
||||
// Create auto-sized SVG
|
||||
const svgWidth = bounds.width;
|
||||
const svgHeight = bounds.height;
|
||||
|
||||
const svg = chartEl
|
||||
.append("svg")
|
||||
.attr("viewBox", `${bounds.minX} ${bounds.minY} ${svgWidth} ${svgHeight}`)
|
||||
.attr("width", "100%")
|
||||
.attr("preserveAspectRatio", "xMidYMid meet")
|
||||
.style("display", "block");
|
||||
|
||||
const canvas = svg.append("g").attr("class", "full-diagram-canvas");
|
||||
|
||||
// Click navigates to person's individual page
|
||||
const onNodeClick = (data) => {
|
||||
if (data.data.url) {
|
||||
window.location.href = data.data.url;
|
||||
}
|
||||
};
|
||||
|
||||
// Draw connections first (behind cards)
|
||||
const linkGroup = canvas.append("g").attr("class", "edges");
|
||||
for (const conn of layout.connections) {
|
||||
linkGroup
|
||||
.append("path")
|
||||
.attr("class", conn.cssClass)
|
||||
.attr("d", conn.path);
|
||||
}
|
||||
|
||||
// Draw person cards
|
||||
for (const person of layout.persons) {
|
||||
renderPersonCard(canvas, person, this.config, onNodeClick);
|
||||
}
|
||||
}
|
||||
|
||||
computeBounds(layout, pad) {
|
||||
const hw = this.config.cardWidth / 2;
|
||||
const hh = this.config.cardHeight / 2;
|
||||
|
||||
let minX = Infinity, maxX = -Infinity;
|
||||
let minY = Infinity, maxY = -Infinity;
|
||||
|
||||
for (const p of layout.persons) {
|
||||
minX = Math.min(minX, p.x - hw);
|
||||
maxX = Math.max(maxX, p.x + hw);
|
||||
// Expand for ancestor/descendant indicators (14px above / 14px below card)
|
||||
const topExtra = p.data.hasMoreAncestors ? 14 : 0;
|
||||
const bottomExtra = p.data.hasMoreDescendants ? 14 : 0;
|
||||
minY = Math.min(minY, p.y - hh - topExtra);
|
||||
maxY = Math.max(maxY, p.y + hh + bottomExtra);
|
||||
}
|
||||
|
||||
// Include connector endpoints in bounds
|
||||
for (const c of layout.connections) {
|
||||
const coords = c.path.match(/-?[\d.]+/g);
|
||||
if (coords) {
|
||||
for (let i = 0; i < coords.length; i += 2) {
|
||||
const x = parseFloat(coords[i]);
|
||||
const y = parseFloat(coords[i + 1]);
|
||||
minX = Math.min(minX, x);
|
||||
maxX = Math.max(maxX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxY = Math.max(maxY, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
minX: minX - pad,
|
||||
minY: minY - pad,
|
||||
width: (maxX - minX) + pad * 2,
|
||||
height: (maxY - minY) + pad * 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user