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.
117 lines
3.8 KiB
JavaScript
117 lines
3.8 KiB
JavaScript
/**
|
|
* 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,
|
|
};
|
|
}
|
|
}
|