<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Graphe du Secret Santa</title> <title>D3.js Interactive Cycle Graph with Alternating Curved Arrows</title> <script src="https://d3js.org/d3.v5.min.js"></script> <style> body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; } svg { display: block; } .node { cursor: pointer; } .legend text { font-size: 10px; font-family: sans-serif; text-anchor: start; } </style> </head> <body> <script> // Données du graphe d3.csv("santa.csv").then(function(data) { // Dimensions du SVG en plein écran const width = window.innerWidth; const height = window.innerHeight; const uniqueNames = data.map(d => d.Source); // Création des nœuds avec des positions initiales const nodes = uniqueNames.map((name, index) => ({id: index, label: name})); const links = data.map((d, i) => ({ source: i, target: uniqueNames.indexOf(d.Cible) })); links.push({ source: 0, target: 1 }); // Création de la force de simulation avec une force de répulsion plus forte const simulation = d3.forceSimulation(nodes) .force("charge", d3.forceManyBody().strength(-300)) .force("link", d3.forceLink(links).distance(100)) .force("center", d3.forceCenter(width / 2, height / 2)) .force("collide", d3.forceCollide().radius(20)) .force("x", d3.forceX().strength(0.1).x(width / 2)) .force("y", d3.forceY().strength(0.1).y(height / 2)); // Création du SVG avec zoom const svg = d3.select("body").append("svg") .attr("width", width) .attr("height", height) .call(d3.zoom().on("zoom", function () { svg.attr("transform", d3.event.transform) })) .append("g"); // Ajout des flèches svg.append("defs").selectAll("marker") .data(["arrow"]) .enter().append("marker") .attr("id", function (d) { return d; }) .attr("viewBox", "0 -5 10 10") .attr("refX", 18) .attr("refY", 0) .attr("markerWidth", 4) .attr("markerHeight", 4) .attr("orient", "auto") .append("path") .attr("d", "M0,-5L10,0L0,5") .attr("class", "arrow-head"); // Ajout des liens const link = svg.selectAll("path") .data(links) .enter().append("path") .attr("fill", "none") .attr("stroke", "#ee288b") .attr("stroke-width", "2") .attr("marker-end", "url(#arrow)"); // Ajout des nœuds const node = svg.selectAll(".node") .data(nodes) .enter().append("g") .attr("class", "node") .call(d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended)); node.append("circle") .attr("r", 3) // Taille des points noirs .attr("fill", "#000"); // Couleur des points noirs // Ajout des légendes pour les nœuds const legend = svg.selectAll(".legend") .data(nodes) .enter().append("g") .attr("class", "legend") .attr("transform", function (d) { return "translate(" + (d.x + 10) + "," + (d.y) + ")"; }); legend.append("text") .text(function (d) { return d.label; }); // Met à jour la position des éléments à chaque itération de la simulation simulation.on("tick", function () { link.attr("d", function (d) { const dx = d.target.x - d.source.x, dy = d.target.y - d.source.y, dr = Math.sqrt(dx * dx + dy * dy), sweep = 1; return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0," + sweep + " " + d.target.x + "," + d.target.y; }); node.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; }); legend.attr("transform", function (d) { return "translate(" + (d.x + 10) + "," + (d.y) + ")"; }); }); // Fonctions de gestion du glisser-déposer function dragstarted(d) { if (!d3.event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(d) { d.fx = d3.event.x; d.fy = d3.event.y; } function dragended(d) { if (!d3.event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } }); </script> </body> </html>