/** * 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, }; } }