Code source
Relations entre les principales dynasties ayant régné sur la France du Ve au XVIe siècle.
Définition des nœuds (JSON)
{
"nodes": [
{
"id": "merovingiens",
"parentId": "root",
"label": "Mérovingiens",
"sublabel": "481–751",
"highlight": true
},
{
"id": "clovis",
"parentId": "merovingiens",
"label": "Clovis Ier",
"sublabel": "r. 481–511"
},
{
"id": "fils-clovis",
"parentId": "clovis",
"label": "Fils de Clovis",
"sublabel": "r. 511–639"
},
{
"id": "rois-faineants",
"parentId": "fils-clovis",
"label": "Rois fainéants",
"sublabel": "r. 639–751",
"dash": true
},
{
"id": "carolingiens",
"parentId": "root",
"label": "Carolingiens",
"sublabel": "751–987",
"highlight": true
},
{
"id": "pippin",
"parentId": "carolingiens",
"label": "Pippin le Bref",
"sublabel": "r. 751–768"
},
{
"id": "charlemagne",
"parentId": "pippin",
"label": "Charlemagne",
"sublabel": "r. 768–814",
"highlight": true
},
{
"id": "louis-pieux",
"parentId": "charlemagne",
"label": "Louis le Pieux",
"sublabel": "r. 814–840"
},
{
"id": "traite-verdun",
"parentId": "louis-pieux",
"label": "Traité de Verdun",
"sublabel": "843"
},
{
"id": "charles-chauve",
"parentId": "traite-verdun",
"label": "Charles le Chauve",
"sublabel": "Francie occ."
},
{
"id": "derniers-caros",
"parentId": "charles-chauve",
"label": "Derniers Carolingiens",
"sublabel": "877–987",
"dash": true
},
{
"id": "capetiens",
"parentId": "root",
"label": "Capétiens",
"sublabel": "987–1792",
"highlight": true
},
{
"id": "hugues-capet",
"parentId": "capetiens",
"label": "Hugues Capet",
"sublabel": "r. 987–996",
"highlight": true
},
{
"id": "capetiens-directs",
"parentId": "hugues-capet",
"label": "Capétiens directs",
"sublabel": "987–1328"
},
{
"id": "valois",
"parentId": "capetiens-directs",
"label": "Valois",
"sublabel": "1328–1589"
},
{
"id": "bourbons",
"parentId": "valois",
"label": "Bourbons",
"sublabel": "1589–1792"
}
],
"transitions": [
{
"from": "rois-faineants",
"to": "pippin",
"label": "751",
"sublabel": "Usurpation"
},
{
"from": "derniers-caros",
"to": "hugues-capet",
"label": "987",
"sublabel": "Élection"
}
]
}Rendu D3 (JavaScript)
Ce fichier contient la fonction de rendu qui transforme les données JSON en diagramme SVG. Il est partagé par tous les diagrammes de ce type — une seule source, pas de duplication.
Rendu D3 (JavaScript)
/**
* render-relationship-diagram.js — Build-time SVG renderer for relationship diagrams
* using D3 hierarchy tree layout (DOM-free).
*
* Usage: node scripts/render-relationship-diagram.js
*/
import { writeFileSync, existsSync, mkdirSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import { stratify, tree } from 'd3-hierarchy';
const __dirname = dirname(fileURLToPath(import.meta.url));
const MEDIA_DIR = resolve(__dirname, '../src/content/media');
// ── Relationship diagram spec ──
// Node format: { id, parentId, label, sublabel, highlight }
const DIAGRAMS = [
{
id: 'dynasties-francaises',
alt: 'Schéma des relations entre les dynasties royales françaises, des Mérovingiens aux Bourbons',
caption: 'Relations entre les principales dynasties ayant régné sur la France du Ve au XVIe siècle.',
sourceId: 'britannica-france-medieval',
nodes: [
// Virtual root
{ id: 'root', parentId: null, label: '', sublabel: '' },
// Mérovingiens
{ id: 'merovingiens', parentId: 'root', label: 'Mérovingiens', sublabel: '481–751', highlight: true },
{ id: 'clovis', parentId: 'merovingiens', label: 'Clovis Ier', sublabel: 'r. 481–511' },
{ id: 'fils-clovis', parentId: 'clovis', label: 'Fils de Clovis', sublabel: 'r. 511–639' },
{ id: 'rois-faineants', parentId: 'fils-clovis', label: 'Rois fainéants', sublabel: 'r. 639–751', dash: true },
// Carolingiens
{ id: 'carolingiens', parentId: 'root', label: 'Carolingiens', sublabel: '751–987', highlight: true },
{ id: 'pippin', parentId: 'carolingiens', label: 'Pippin le Bref', sublabel: 'r. 751–768' },
{ id: 'charlemagne', parentId: 'pippin', label: 'Charlemagne', sublabel: 'r. 768–814', highlight: true },
{ id: 'louis-pieux', parentId: 'charlemagne', label: 'Louis le Pieux', sublabel: 'r. 814–840' },
{ id: 'traite-verdun', parentId: 'louis-pieux', label: 'Traité de Verdun', sublabel: '843' },
{ id: 'charles-chauve', parentId: 'traite-verdun', label: 'Charles le Chauve', sublabel: 'Francie occ.' },
{ id: 'derniers-caros', parentId: 'charles-chauve', label: 'Derniers Carolingiens', sublabel: '877–987', dash: true },
// Capétiens
{ id: 'capetiens', parentId: 'root', label: 'Capétiens', sublabel: '987–1792', highlight: true },
{ id: 'hugues-capet', parentId: 'capetiens', label: 'Hugues Capet', sublabel: 'r. 987–996', highlight: true },
{ id: 'capetiens-directs', parentId: 'hugues-capet', label: 'Capétiens directs', sublabel: '987–1328' },
{ id: 'valois', parentId: 'capetiens-directs', label: 'Valois', sublabel: '1328–1589' },
{ id: 'bourbons', parentId: 'valois', label: 'Bourbons', sublabel: '1589–1792' },
],
// Transitions between dynasties (displayed as edge labels)
transitions: [
{ from: 'rois-faineants', to: 'pippin', label: '751', sublabel: 'Usurpation' },
{ from: 'derniers-caros', to: 'hugues-capet', label: '987', sublabel: 'Élection' },
],
},
];
function renderRelationshipDiagram(spec) {
const { nodes, transitions = [] } = spec;
const width = 760;
const height = 560;
const margin = { top: 50, right: 40, bottom: 50, left: 40 };
const innerW = width - margin.left - margin.right;
const innerH = height - margin.top - margin.bottom;
// Build hierarchy (stratify handles the virtual root)
const root = stratify()
.id((d) => d.id)
.parentId((d) => d.parentId)(nodes);
// Tree layout
const treeLayout = tree()
.size([innerW, innerH])
.separation((a, b) => (a.parent === b.parent ? 2 : 4));
treeLayout(root);
// Color palette
const colors = {
merovingiens: { fill: '#1a5276', stroke: '#2980b9' },
carolingiens: { fill: '#922b21', stroke: '#e74c3c' },
capetiens: { fill: '#1e8449', stroke: '#27ae60' },
};
const defaultColor = { fill: '#2c3e50', stroke: '#4a5568' };
function getColor(nodeId) {
const path = nodeId.split('/');
if (path.some(p => p.startsWith('merov') || p === 'clovis' || p === 'fils-clovis' || p === 'rois-faineants')) return colors.merovingiens;
if (path.some(p => p.startsWith('carol') || p === 'pippin' || p === 'charlemagne' || p === 'louis-pieux' || p === 'traite-verdun' || p === 'charles-chauve' || p === 'derniers-caros')) return colors.carolingiens;
if (path.some(p => p.startsWith('capet') || p === 'hugues-capet' || p === 'capetiens-directs' || p === 'valois' || p === 'bourbons')) return colors.capetiens;
return defaultColor;
}
const parts = [];
// Title
parts.push(`<text x="${width / 2}" y="20" fill="#e2e8f0" font-size="15" font-weight="600" text-anchor="middle" font-family="system-ui, sans-serif">Relations entre les dynasties royales françaises</text>`);
// Build id->node map for quick lookup
const nodeMap = {};
root.descendants().forEach((d) => { nodeMap[d.id] = d; });
// Draw parent-child edges
root.links().forEach((link) => {
const source = link.source;
const target = link.target;
if (source.id === 'root' || target.id === 'root') return; // skip virtual root edges
const sx = source.x + margin.left;
const sy = source.y + margin.top;
const tx = target.x + margin.left;
const ty = target.y + margin.top;
const isDashed = target.data.dash;
const dashAttr = isDashed ? ' stroke-dasharray="6,4"' : '';
parts.push(`<line x1="${sx}" y1="${sy}" x2="${tx}" y2="${ty}" stroke="#4a5568" stroke-width="1.5"${dashAttr}/>`);
});
// Draw transitions (edges between non-parent-child nodes)
transitions.forEach((t) => {
const source = nodeMap[t.from];
const target = nodeMap[t.to];
if (!source || !target) return;
const sx = source.x + margin.left;
const sy = source.y + margin.top;
const tx = target.x + margin.left;
const ty = target.y + margin.top;
// Dashed curved line
const midX = (sx + tx) / 2;
const midY = (sy + ty) / 2;
const ctrlY = Math.min(sy, ty) - 30;
parts.push(`<path d="M${sx} ${sy} Q${midX} ${ctrlY} ${tx} ${ty}" fill="none" stroke="#b7950b" stroke-width="1.5" stroke-dasharray="5,4"/>`);
// Transition label
parts.push(`<rect x="${midX - 22}" y="${ctrlY - 12}" width="44" height="24" rx="12" fill="#1a1a2e" stroke="#b7950b" stroke-width="1"/>`);
parts.push(`<text x="${midX}" y="${ctrlY + 2}" fill="#f4d03f" font-size="11" font-weight="600" text-anchor="middle" font-family="system-ui, sans-serif">${t.label}</text>`);
parts.push(`<text x="${midX}" y="${ctrlY + 13}" fill="#cbd5e1" font-size="9" text-anchor="middle" font-family="system-ui, sans-serif">${t.sublabel}</text>`);
});
// Draw nodes
root.descendants().forEach((d) => {
if (d.id === 'root') return; // skip virtual root
const x = d.x + margin.left;
const y = d.y + margin.top;
const color = getColor(d.id);
const isHighlight = d.data.highlight;
const nodeW = 130;
const nodeH = d.data.sublabel ? 40 : 28;
const rx = 6;
// Shadow for highlight nodes
if (isHighlight) {
parts.push(`<rect x="${x - nodeW / 2 + 2}" y="${y - nodeH / 2 + 2}" width="${nodeW}" height="${nodeH}" rx="${rx}" fill="#000" opacity="0.3"/>`);
}
// Node background
parts.push(`<rect x="${x - nodeW / 2}" y="${y - nodeH / 2}" width="${nodeW}" height="${nodeH}" rx="${rx}" fill="${color.fill}" stroke="${color.stroke}" stroke-width="${isHighlight ? 2.5 : 1.5}"/>`);
// Label
const labelY = d.data.sublabel ? y - 3 : y + 4;
parts.push(`<text x="${x}" y="${labelY}" fill="#fff" font-size="12" font-weight="${isHighlight ? '600' : '400'}" text-anchor="middle" font-family="system-ui, sans-serif">${d.data.label}</text>`);
// Sublabel
if (d.data.sublabel) {
parts.push(`<text x="${x}" y="${y + 14}" fill="#94a3b8" font-size="9" text-anchor="middle" font-family="system-ui, sans-serif">${d.data.sublabel}</text>`);
}
});
// Legend
const legendItems = [
{ label: 'Mérovingiens', color: colors.merovingiens.fill },
{ label: 'Carolingiens', color: colors.carolingiens.fill },
{ label: 'Capétiens', color: colors.capetiens.fill },
];
let legendX = width / 2 - ((legendItems.length * 130) - 10) / 2;
const legendY = height - 12;
legendItems.forEach((item) => {
parts.push(`<rect x="${legendX}" y="${legendY - 7}" width="12" height="12" rx="2" fill="${item.color}"/>`);
parts.push(`<text x="${legendX + 18}" y="${legendY + 3}" fill="#94a3b8" font-size="10" font-family="system-ui, sans-serif">${item.label}</text>`);
legendX += 130;
});
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img" aria-label="${spec.alt}">
<rect width="${width}" height="${height}" fill="#1a1a2e" rx="8"/>
${parts.join('\n ')}
</svg>`;
}
// ── Main ──
for (const spec of DIAGRAMS) {
console.log(`Rendering relationship diagram: ${spec.id}...`);
const svg = renderRelationshipDiagram(spec);
const svgPath = resolve(MEDIA_DIR, `${spec.id}.svg`);
writeFileSync(svgPath, svg);
console.log(` → Saved SVG (${svg.length} bytes)`);
// Extract authored data (nodes + transitions, excluding rendering boilerplate)
const sourceData = {
nodes: spec.nodes.filter((n) => n.id !== 'root'),
transitions: spec.transitions,
};
const metadata = {
id: spec.id,
alt: spec.alt,
caption: spec.caption,
credit: 'Généré avec D3 hierarchy',
license: 'CC BY-SA 4.0',
licenseUrl: 'https://creativecommons.org/licenses/by-sa/4.0/',
sourceId: spec.sourceId,
sourceCode: [
{
lang: 'json',
code: JSON.stringify(sourceData, null, 2),
label: 'Définition des nœuds (JSON)',
},
],
renderingSource: {
filePath: 'scripts/render-relationship-diagram.js',
label: 'Rendu D3 (JavaScript)',
},
};
const jsonPath = resolve(MEDIA_DIR, `${spec.id}.json`);
writeFileSync(jsonPath, JSON.stringify(metadata, null, 2) + '\n');
console.log(` → Saved metadata`);
}
console.log('Done!');