Hello (my english isn't that good, so the rest is translate with GoogleTrad, i'm french =D)
For a project, I am creating a radialtree with html, js code and the D3js library.
I try to reproduce the first tree, the most complete. The 2nd is what I'm getting so far. first tree that i'm trying te reproduce 2nd tree, my work
If you look at the center of the circle, in the first radialtree the branches go in a "straight line" from the center. In the second radialtree, the branches form a curve at the start, then go to the child nodes. I can't thwart this starting turn, can you help me?
Here is my full code:
See snippet
// radial tree
let root = {
"name": "Point 1", "info":"FirstNode", "weight": 117, "children": [
{"name":"CULTIVER", "weight": 21,
"children":[{"name":"LA QUALITE DES RELATIONS (n=13)", "weight": 13},
{"name":"ENTRAIDE ET COLLABORATION (n=3)", "weight": 3},
{"name":"L'ARTICULATION DE TEMPS COLLECTIFS ET INDIVIDUEL (n=3)", "weight": 3},
{"name":"LA CONVIVIALITE (n=2)", "weight": 2}]},
{"name":"LE CADRE ET L'ANIMATION", "weight": 12,
"children":[{"name":"UN CADRE STRUCTURE ET BIENVEILLANT QUI PERMET L'ECOUTE (n=3)", "weight": 3},
{"name":"LE RESPECT DE L'AUTONOMIE (n=2)", "weight": 2},
{"name":"LE RESPECT DU RYTHME DE CHACUN (n=2)", "weight": 2},
{"name":"LE PROFESSIONNALISME DE L'EQUIPE (n=5)", "weight": 5}]},
{"name":"LE COLLECTIF", "weight": 14,
"children":[{"name":"LE TRAVAIL (n=6)", "weight": 6},
{"name":"PRECISER (n=5)", "weight": 5},
{"name":"LA REPRISE (n=2)", "weight": 2},
{"name":"LA REGLE (n=1)", "weight": 1}]},
{"name":"RESTITUTION", "weight": 2,
"children":[{"name":"CREATION (n=1)", "weight": 1},
]
}]};
let maxDistance; // Déclaration de la variable maxDistance en dehors de la fonction
let createRadialTree = function (input) {
let height = 1500;
let width = 1500;
let svg = d3.select('#radialTreeGroup')
.append('svg')
.attr('width', width)
.attr('height', height);
let diameter = height * 8.1;
let radius = diameter / 30.1;
let tree = d3.tree()
.size([2 * Math.PI, radius])
.separation(function (a, b) {
if (a.parent === b.parent) {
return 1;
} else if (a.depth === b.depth) {
return 1.2;
} else {
return 2;
}
});
let data = d3.hierarchy(input);
let treeData = tree(data);
let nodes = treeData.descendants();
let links = treeData.links();
nodes.forEach(function (node) {
let totalChildren = node.descendants().length - 1;
node.totalChildren = totalChildren;
});
let linkWidthScale = d3.scaleLinear()
.domain([0, d3.max(nodes, function (d) { return d.totalChildren; })])
.range([0, 15]);
let graphGroup = svg.append('g')
.attr('transform', "translate(" + (width / 2) + "," + (height / 2) + ")");
maxDistance = calculateMaxDistance(nodes);
graphGroup.append("circle")
.attr("class", "background-circle")
.attr("r", maxDistance)
.style("fill", "#f0f0f000");
// Ajouter un cercle fixe au centre du graphe
graphGroup.append("circle")
.attr("class", "center-circle")
.attr("r", 10) // rayon du cercle au centre
.style("fill", "#194353");
graphGroup.selectAll(".link")
.data(links)
.join("path")
.attr("class", "link")
.style("stroke-width", function (d) {
return linkWidthScale(d.target.data.weight || 1);
})
.attr("stroke-linecap", "round")
.attr("d", d3.linkRadial()
.angle(function (d) {
if (d.depth === 0) { // Vérifie si c'est le nœud parent (racine)
return 0; // Angle fixe pour le nœud parent au centre
} else {
return d.x;
}
})
.radius(function (d) { return d.y; })
);
let nodeSizeScale = d3.scaleLinear()
.domain([0, d3.max(nodes, function(d) { return d.depth; })])
.range([15, 4]);
let node = graphGroup
.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.attr("class", function(d) { return "node node-level-" + d.depth; })
.attr("transform", function (d) {
let angle = (d.x - Math.PI / 2) * 180 / Math.PI; // Ajustement de l'angle de rotation
let radius = d.y; // Rayon du cercle
return `rotate(${angle}) translate(${radius}, 0)`;
});
node.append("circle")
.attr("r", function(d) { return nodeSizeScale(d.depth); });
node.filter(function (d) { return d.depth === 2; })
.append("text")
.attr("class", "node-text")
.attr("dx", function (d) { return d.x < Math.PI ? 14 : -14; })
.attr("dy", ".31em")
.attr("text-anchor", function (d) { return d.x < Math.PI ? "start" : "end"; })
.attr("transform", function (d) { return d.x < Math.PI ? null : "rotate(180)"; })
.selectAll("tspan")
.data(function (d) {
return d.data.name.split("\n");
})
.enter()
.append("tspan")
.attr("x", 0)
.attr("dy", function (d, i) { return i ? "1.2em" : 0; })
.text(function (d) { return d; })
.call(wrapText, 300); // Appel à la fonction wrapText avec une largeur de 20 pixels
};
function calculateMaxDistance(nodes) {
let maxDistance = 0;
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].depth === 2) {
maxDistance = Math.max(maxDistance, nodes[i].y);
}
}
return maxDistance;
}
function wrapText(text, width) {
text.each(function (d) {
if (d.depth < 1) {
return;
}
let text = d3.select(this);
let words = text.text().split(/\s+/).reverse();
let lineHeight = 1.2; // Ajustez la valeur ici pour définir l'interligne souhaité
let y = text.attr("y");
let x = text.attr("x");
let dy = parseFloat(text.attr("dy")) || 0;
let tspan = text.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "em");
let line = [];
let lineNumber = 0;
let word;
let wordCount = words.length;
while ((word = words.pop())) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text
.append("tspan")
.attr("x", x)
.attr("y", y)
.attr("dx", x) // Ajout de l'attribut dx conditionnellement
.attr("dy", lineHeight + "em") // Utilisez une valeur fixe pour l'interligne
.text(word);
}
}
});
}
// code svg
const svgWidth = 1500;
const svgHeight = 1500;
const pieChartGroup = d3.select("#pieChartGroup")
.attr("transform", `"translate(" + (width / 2) + "," + (height / 2) + "`);
const radialTreeGroup = d3.select("#radialTreeGroup")
.attr("transform", `translate()`);
// code création radial tree
createRadialTree(root);
// Opération à appliquer à toutes les datas ayant le label "Bout"
function applyOperation(data) {
const updatedData = data.map(d => {
if (d.label === "Bout") {
// Effectuer l'opération souhaitée sur la valeur ici
d.value = ((2.3 / 2) + (((d.value)-1)*1.1) + (1.4/2));
}
else if (d.label === "") {
// Effectuer l'opération souhaitée sur la valeur ici
d.value = ((((d.value) - 1) * 1.1) + (1.4));
}
return d;
});
return updatedData;
}
// Données pour les secteurs du graphique
const data = [
{ label: "Bout", value: 4},
{ label: "", value: 4},
{ label: "", value: 4},
{ label: "", value: 3},
{ label: "", value: 2},
{ label: "", value: 3},
{ label: "", value: 3},
{ label: "", value: 2},
{ label: "", value: 3},
{ label: "", value: 2},
{ label: "", value: 1},
{ label: "", value: 3},
{ label: "", value: 2},
{ label: "Bout", value: 1},
// Ajoutez autant de secteurs que vous le souhaitez avec leurs angles respectifs
// Value = degré, valeur
];
// Appliquer l'opération aux données ayant le label "Bout"
const updatedData = applyOperation(data);
console.log(updatedData);
// Dimensions du graphique
const width = 1500;
const height = 1500;
const radius = maxDistance;
// Deux jeux de couleurs alternées
const colors = ["#98d9ff", "#d4efff"];
// Création d'un générateur d'angles
const pie = d3.pie()
.value(d => d.value)
.sort(null);
// Sélection de la zone du graphique
const svg = d3.select("#pieChartGroup")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${width / 2}, ${height / 2})`);
// Création des arcs pour les secteurs
const arc = d3.arc()
.innerRadius(0)
.outerRadius(radius);
// Génération du graphique
const arcs = svg.selectAll("arc")
.data(pie(data))
.enter()
.append("g");
arcs.append("path")
.attr("d", arc)
.attr("fill", (d, i) => colors[i % colors.length]); // Alterne entre les deux jeux de couleurs
// Ajout d'étiquettes à chaque secteur (optionnel)
// arcs.append("text")
// .attr("transform", d => `translate(${arc.centroid(d)})`)
// .attr("text-anchor", "middle")
// .text(d => d.data.label);
pieChartGroup.lower(0); // pour mettre les secteurs en arrière-plan
// EXPORT
// Fonction pour exporter le graphe en PNG
function exportGraphToPng() {
const combinedSvg = document.getElementById("combinedSvg");
html2canvas(combinedSvg).then(function(canvas) {
// Convertir le canvas en image
const imgData = canvas.toDataURL("image/png");
// Convertir l'image en un objet Blob
const blob = dataURLtoBlob(imgData);
// Utiliser la librairie FileSaver.js pour déclencher le téléchargement
saveAs(blob, "graph.png");
});
}
// Attendez que le contenu de la page soit chargé
document.addEventListener('DOMContentLoaded', function() {
// Associer l'événement de clic au bouton pour déclencher l'export
const exportButton = document.getElementById("exportButton");
exportButton.addEventListener("click", exportGraphToPng);
// ... Le reste de votre code JavaScript existant ...
});
svg {
border: solid 1px rgb(255, 51, 0);
display: block;
margin: 0 auto;
}
.link {
fill: none;
stroke: #194353;
stroke-width: 5.5px;
}
.node {
fill: white;
stroke: black;
}
.node-text {
font-family: 'Roboto', sans-serif;
font-size: 12px;
fill: black;
stroke: none;
white-space: pre-line;
}
.background-circle {
fill: #f0f0f000;
stroke: rgba(255, 255, 255, 0);
stroke-width: 0px;
}
.center-circle {
fill: #194353;
}
<!DOCTYPE html>
<html>
<head>
<title>D3.js Radial Tree Example</title>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
<style>
</style>
</head>
<body>
<svg id="combinedSvg" width="1500" height="1500">
<g id="radialTreeGroup">
<!-- Contenu de l'arbre radial -->
</g>
<g id="pieChartGroup">
<!-- Contenu du graphique à secteurs -->
</g>
</svg>
<div id="exportButtonContainer">
<button id="exportButton">Exporter le graphe en PNG</button>
</div>
<script>
</script>
</body>
</html>
Thanks for your help !
If you look at the center of the circle, in the first radialtree the branches go in a "straight line" from the center. In the second radialtree, the branches form a curve at the start, then go to the child nodes. I can't thwart this starting turn, can you help me?
You can handle first links differently for example for your paths when you draw links:
.attr("d",(d)=>{
if (d.source.depth === 0) {
// strait line for first link
return d3.linkRadial()
.angle(0)
.radius(function (d) {
return d.y;
})(d)
} else {
return d3.linkRadial()
.angle(function (d, i) {
return d.x;
})
.radius(function (d) {
return d.y;
})(d)
}
}
)
.attr('transform',(d)=>{
if(d.target.depth<= 1 && d.source.depth===0) {
// rotate first line only
return`rotate(${d.target.x*(180/Math.PI)})`;
}
return ''
});
// radial tree
let root = {
"name": "Point 1",
"info": "FirstNode",
"weight": 117,
"children": [{
"name": "CULTIVER",
"weight": 21,
"children": [{
"name": "LA QUALITE DES RELATIONS (n=13)",
"weight": 13
},
{
"name": "ENTRAIDE ET COLLABORATION (n=3)",
"weight": 3
},
{
"name": "L'ARTICULATION DE TEMPS COLLECTIFS ET INDIVIDUEL (n=3)",
"weight": 3
},
{
"name": "LA CONVIVIALITE (n=2)",
"weight": 2
}
]
},
{
"name": "LE CADRE ET L'ANIMATION",
"weight": 12,
"children": [{
"name": "UN CADRE STRUCTURE ET BIENVEILLANT QUI PERMET L'ECOUTE (n=3)",
"weight": 3
},
{
"name": "LE RESPECT DE L'AUTONOMIE (n=2)",
"weight": 2
},
{
"name": "LE RESPECT DU RYTHME DE CHACUN (n=2)",
"weight": 2
},
{
"name": "LE PROFESSIONNALISME DE L'EQUIPE (n=5)",
"weight": 5
}
]
},
{
"name": "LE COLLECTIF",
"weight": 14,
"children": [{
"name": "LE TRAVAIL (n=6)",
"weight": 6
},
{
"name": "PRECISER (n=5)",
"weight": 5
},
{
"name": "LA REPRISE (n=2)",
"weight": 2
},
{
"name": "LA REGLE (n=1)",
"weight": 1
}
]
},
{
"name": "RESTITUTION",
"weight": 2,
"children": [{
"name": "CREATION (n=1)",
"weight": 1
}, ]
}
]
};
let maxDistance; // Déclaration de la variable maxDistance en dehors de la fonction
let createRadialTree = function(input) {
let height = 1500;
let width = 1500;
let svg = d3.select('#radialTreeGroup')
.append('svg')
.attr('width', width)
.attr('height', height);
let diameter = height * 8.1;
let radius = diameter / 30.1;
let tree = d3.tree()
.size([2 * Math.PI, radius])
.separation(function(a, b) {
if (a.parent === b.parent) {
return 1;
} else if (a.depth === b.depth) {
return 1.2;
} else {
return 2;
}
});
let data = d3.hierarchy(input);
let treeData = tree(data);
let nodes = treeData.descendants();
let links = treeData.links();
nodes.forEach(function(node) {
let totalChildren = node.descendants().length - 1;
node.totalChildren = totalChildren;
});
let linkWidthScale = d3.scaleLinear()
.domain([0, d3.max(nodes, function(d) {
return d.totalChildren;
})])
.range([0, 15]);
let graphGroup = svg.append('g')
.attr('transform', "translate(" + (width / 2) + "," + (height / 2) + ")");
maxDistance = calculateMaxDistance(nodes);
graphGroup.append("circle")
.attr("class", "background-circle")
.attr("r", maxDistance)
.style("fill", "#f0f0f000");
// Ajouter un cercle fixe au centre du graphe
graphGroup.append("circle")
.attr("class", "center-circle")
.attr("r", 10) // rayon du cercle au centre
.style("fill", "#194353");
graphGroup.selectAll(".link")
.data(links)
.join("path")
.attr("class", "link")
.style("stroke-width", function(d) {
return linkWidthScale(d.target.data.weight || 1);
})
.attr("stroke-linecap", "round")
.attr("d", (d) => {
if (d.source.depth === 0) {
// strait line for first link
return d3.linkRadial()
.angle(0)
.radius(function(d) {
return d.y;
})(d)
} else {
return d3.linkRadial()
.angle(function(d, i) {
return d.x;
})
.radius(function(d) {
return d.y;
})(d)
}
})
.attr('transform', (d) => {
if (d.target.depth <= 1 && d.source.depth === 0) {
// rotate first line only
return `rotate(${d.target.x*(180/Math.PI)})`;
}
return ''
});
let nodeSizeScale = d3.scaleLinear()
.domain([0, d3.max(nodes, function(d) {
return d.depth;
})])
.range([15, 4]);
let node = graphGroup
.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.attr("class", function(d) {
return "node node-level-" + d.depth;
})
.attr("transform", function(d) {
let angle = (d.x - Math.PI / 2) * 180 / Math.PI; // Ajustement de l'angle de rotation
let radius = d.y; // Rayon du cercle
return `rotate(${angle}) translate(${radius}, 0)`;
});
node.append("circle")
.attr("r", function(d) {
return nodeSizeScale(d.depth);
});
node.filter(function(d) {
return d.depth === 2;
})
.append("text")
.attr("class", "node-text")
.attr("dx", function(d) {
return d.x < Math.PI ? 14 : -14;
})
.attr("dy", ".31em")
.attr("text-anchor", function(d) {
return d.x < Math.PI ? "start" : "end";
})
.attr("transform", function(d) {
return d.x < Math.PI ? null : "rotate(180)";
})
.selectAll("tspan")
.data(function(d) {
return d.data.name.split("\n");
})
.enter()
.append("tspan")
.attr("x", 0)
.attr("dy", function(d, i) {
return i ? "1.2em" : 0;
})
.text(function(d) {
return d;
})
.call(wrapText, 300); // Appel à la fonction wrapText avec une largeur de 20 pixels
};
function calculateMaxDistance(nodes) {
let maxDistance = 0;
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].depth === 2) {
maxDistance = Math.max(maxDistance, nodes[i].y);
}
}
return maxDistance;
}
function wrapText(text, width) {
text.each(function(d) {
if (d.depth < 1) {
return;
}
let text = d3.select(this);
let words = text.text().split(/\s+/).reverse();
let lineHeight = 1.2; // Ajustez la valeur ici pour définir l'interligne souhaité
let y = text.attr("y");
let x = text.attr("x");
let dy = parseFloat(text.attr("dy")) || 0;
let tspan = text.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "em");
let line = [];
let lineNumber = 0;
let word;
let wordCount = words.length;
while ((word = words.pop())) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text
.append("tspan")
.attr("x", x)
.attr("y", y)
.attr("dx", x) // Ajout de l'attribut dx conditionnellement
.attr("dy", lineHeight + "em") // Utilisez une valeur fixe pour l'interligne
.text(word);
}
}
});
}
// code svg
const svgWidth = 1500;
const svgHeight = 1500;
const pieChartGroup = d3.select("#pieChartGroup")
.attr("transform", `"translate(" + (width / 2) + "," + (height / 2) + "`);
const radialTreeGroup = d3.select("#radialTreeGroup")
.attr("transform", `translate()`);
// code création radial tree
createRadialTree(root);
// Opération à appliquer à toutes les datas ayant le label "Bout"
function applyOperation(data) {
const updatedData = data.map(d => {
if (d.label === "Bout") {
// Effectuer l'opération souhaitée sur la valeur ici
d.value = ((2.3 / 2) + (((d.value) - 1) * 1.1) + (1.4 / 2));
} else if (d.label === "") {
// Effectuer l'opération souhaitée sur la valeur ici
d.value = ((((d.value) - 1) * 1.1) + (1.4));
}
return d;
});
return updatedData;
}
// Données pour les secteurs du graphique
const data = [{
label: "Bout",
value: 4
},
{
label: "",
value: 4
},
{
label: "",
value: 4
},
{
label: "",
value: 3
},
{
label: "",
value: 2
},
{
label: "",
value: 3
},
{
label: "",
value: 3
},
{
label: "",
value: 2
},
{
label: "",
value: 3
},
{
label: "",
value: 2
},
{
label: "",
value: 1
},
{
label: "",
value: 3
},
{
label: "",
value: 2
},
{
label: "Bout",
value: 1
},
// Ajoutez autant de secteurs que vous le souhaitez avec leurs angles respectifs
// Value = degré, valeur
];
// Appliquer l'opération aux données ayant le label "Bout"
const updatedData = applyOperation(data);
console.log(updatedData);
// Dimensions du graphique
const width = 1500;
const height = 1500;
const radius = maxDistance;
// Deux jeux de couleurs alternées
const colors = ["#98d9ff", "#d4efff"];
// Création d'un générateur d'angles
const pie = d3.pie()
.value(d => d.value)
.sort(null);
// Sélection de la zone du graphique
const svg = d3.select("#pieChartGroup")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${width / 2}, ${height / 2})`);
// Création des arcs pour les secteurs
const arc = d3.arc()
.innerRadius(0)
.outerRadius(radius);
// Génération du graphique
const arcs = svg.selectAll("arc")
.data(pie(data))
.enter()
.append("g");
arcs.append("path")
.attr("d", arc)
.attr("fill", (d, i) => colors[i % colors.length]); // Alterne entre les deux jeux de couleurs
// Ajout d'étiquettes à chaque secteur (optionnel)
// arcs.append("text")
// .attr("transform", d => `translate(${arc.centroid(d)})`)
// .attr("text-anchor", "middle")
// .text(d => d.data.label);
pieChartGroup.lower(0); // pour mettre les secteurs en arrière-plan
// EXPORT
// Fonction pour exporter le graphe en PNG
function exportGraphToPng() {
const combinedSvg = document.getElementById("combinedSvg");
html2canvas(combinedSvg).then(function(canvas) {
// Convertir le canvas en image
const imgData = canvas.toDataURL("image/png");
// Convertir l'image en un objet Blob
const blob = dataURLtoBlob(imgData);
// Utiliser la librairie FileSaver.js pour déclencher le téléchargement
saveAs(blob, "graph.png");
});
}
// Attendez que le contenu de la page soit chargé
document.addEventListener('DOMContentLoaded', function() {
// Associer l'événement de clic au bouton pour déclencher l'export
const exportButton = document.getElementById("exportButton");
exportButton.addEventListener("click", exportGraphToPng);
// ... Le reste de votre code JavaScript existant ...
});
svg {
border: solid 1px rgb(255, 51, 0);
display: block;
margin: 0 auto;
}
.link {
fill: none;
stroke: #194353;
stroke-width: 5.5px;
}
.node {
fill: white;
stroke: black;
}
.node-text {
font-family: 'Roboto', sans-serif;
font-size: 12px;
fill: black;
stroke: none;
white-space: pre-line;
}
.background-circle {
fill: #f0f0f000;
stroke: rgba(255, 255, 255, 0);
stroke-width: 0px;
}
.center-circle {
fill: #194353;
}
<!DOCTYPE html>
<html>
<head>
<title>D3.js Radial Tree Example</title>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
<style>
</style>
</head>
<body>
<svg id="combinedSvg" width="1500" height="1500">
<g id="radialTreeGroup">
<!-- Contenu de l'arbre radial -->
</g>
<g id="pieChartGroup">
<!-- Contenu du graphique à secteurs -->
</g>
</svg>
<div id="exportButtonContainer">
<button id="exportButton">Exporter le graphe en PNG</button>
</div>
<script>
</script>
</body>
</html>