Select Git revision
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
backup.js 22.94 KiB
import * as d3 from 'd3'
import '../css/radialTree.css'
let DURATION = 500
let ID = 0;
export default function MyRadialTree(element, data, options){
this.graphId = ID++
this.showLeafNames = false
this.width;
this.height; //c'est un carré
this.margin = 0; //en vrai ça va se faire del fast non ?
//dimensions
this.rootSize = 100;//size of the middle circle
this.leafLineSize = 10;//size of the lines of the last nodes
this.leafOffset = 2;
this.leafNameSize = 0;//size of the outer ring, between the last node circles and the svg borders
this.leafRadius //experimental
this.leafLineRadius // radius where the lines of the last branches ends
this.leafLabelRadius// radius where labels for leaves are positioned, (== leaflineRadius + leafOffset)
this.branchRange;// size between the rootsize and the leafSize
//style
this.nodeCircleRadius = 3;
this.nodeCircleRadiusActive = 8;
this.updateDuration = 500
this.highlightDuration = 50
this.rootNodeTextSize = 25
//set options
for(let prop in options){
this[prop] = options[prop]
}
// this.color = 'gray'
this.color = 'rgb(150,150,150)'
this.highlightColor = 'white'
//trucs
this.cluster;
this.maxleaves;
// on setup la data;
this.rootNode = d3.hierarchy(data)
this.rootNode.descendants().forEach(node =>{
node._children = node.children;
})
this.currentNode = this.rootNode
this.checkboxContainer = d3.select(element)
.append('div')
.style('position','relative')
.style('text-align','start')
.style('align-self','start')
.style('width', '200px')
.append('div')
.style('position', 'absolute')
.style('top', 0)
.style('left', 0)
this.checkboxContainer.append('input')
.attr('type', 'checkbox')
.attr('id', 'toggleNames'+this.graphId)
.on("click", e=>{
this.toggleLeafNames(e.target.checked)
});
this.checkboxContainer.append('label')
.attr('for', 'toggleNames'+this.graphId)
.text('Show leaves name')
this.svgElement = d3.select(element).append('svg')
this.svg = this.svgElement.append("g")
this.rootNodeText = this.svg.append('text')
.attr('class', 'rootLabel')
.attr('fill', this.highlightColor)
.text('Bonjour')
.attr('font-size',25)
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
this.branchContainer = this.svg.append('g')
this.highlightPathContainer = this.svg.append('g')
this.svg.append('line')//parentNodeLine
.attr('class', 'parentNodeLine')
.attr('stroke', this.color)
.attr('stroke-width', 1)
.attr('y1',this.rootSize)
.attr('y2',this.rootSize)
this.rootCircle = this.svg.append('circle')
.attr('class', 'rootCircle')
.attr('stroke', this.color)
.attr('fill','none')
.attr("stroke-width",1);
this.nodeCircleContainer = this.svg.append('g')
this.svg.append('circle')//parentNodeCircle
.attr('class', 'parentNodeCircle')
.attr('fill', this.color)
.on('click', (e)=>{
if(this.currentNode.parent)
this.update(this.currentNode.parent)
})
.on('mouseover', (e)=>{
this.setRootNodeText(this.currentNode.parent.data.name)
this.svg.select('.parentNodeLine')
// .transition().duration(100)
.attr('stroke', this.highlightColor)
.attr('stroke-width', 1)
this.svg.select('.parentNodeCircle')
// .transition().duration(100)
.attr('fill', this.highlightColor)
.attr('r', this.nodeCircleRadiusActive)
this.rootCircle.attr('stroke', this.highlightColor)
})
.on('mouseout', (e)=>{
this.setRootNodeText(this.currentNode.data.name)
this.svg.select('.parentNodeLine')
// .transition("mouseout").duration(100)
.attr('stroke', this.color)
.attr('stroke-width', 1)
this.svg.select('.parentNodeCircle')
// .transition("mouseout").duration(100)
.attr('fill' ,this.color)
.attr('r', this.nodeCircleRadius)
this.rootCircle.attr('stroke', this.color)
})
let resizeObserver = new ResizeObserver(entries =>{
for (const entry of entries) {//should only be one.
this.resize(entry.contentRect.width, entry.contentRect.height);
this.update(this.currentNode);
}
});
resizeObserver.observe(element)
this.resize(element.clientWidth, element.clientHeight)
this.update(this.rootNode);
}
MyRadialTree.prototype.update = function(data){
this.currentNode = data;
this.pruneNode(data)
this.cluster(data)
let descendants = data.descendants().reverse()
let maxDepthDisplayed = descendants[0].depth;
descendants.forEach(node =>{
node.ptheta = node.theta
node.pr = node.r
node.pendPointR = node.endPointR
node.ptheta1 = node.theta1
node.ptheta2 = node.theta2
node.theta = node.x;
node.r = this.rootSize + Math.max(0, (node.depth-data.depth-1) / Math.max(1, maxDepthDisplayed-data.depth-1) * this.branchRange);
if(node.children && node.children.length > 0){
node.theta1 = node.children[0].theta
node.theta2 = node.children[node.children.length-1].theta
node.endPointR = node.children[0].r
}else{
node.theta1 = node.theta
node.theta2 = node.theta
node.endPointR = this.leafLineRadius
}
if(node == this.currentNode){//edge case si la currentnode n'a pas de children, pour pas que son trait se balade dans le vide
node.endPointR = this.rootSize;
}
node.x = node.r * -Math.sin(node.theta)
node.y = node.r * Math.cos(node.theta)
})
// this.svg.select('.rootLabel').text(this.currentNode.data.name)
this.setRootNodeText(this.currentNode.data.name)
this.drawBranches(data)
this.drawParentBranch(data)
if(this.showLeafNames)
this.drawNames(data)
}
MyRadialTree.prototype.drawBranches = function(node){
let descendants = node.descendants();
this.branchContainer.selectAll('.branch')
.data(descendants, d=>d.data.TSN)
.join(
enter => {
let g = enter.append('g')
.each(function(d) { d.branchElem = this; })//set reference to the element (geste technique)
.attr('class', 'branch')
.attr('stroke', this.color)
.attr('fill', this.color)
.attr('stroke-width', 1)
g.attr('opacity',0)
.transition().duration(DURATION).delay(DURATION/2)//.delay(DURATION)
.attr('opacity',1)
g.append('path')
.attr('class', 'nodePath')
.attr('d', d=>this.getBranchPath(d))
.attr('fill','none')
g.append('line')
.attr('class', 'nodeLine')
.attr("x1", d=>d.x)
.attr("y1", d=>d.y)
.attr("x2", d =>this.getNodeLineEndPoint(d).x)
.attr("y2", d => d.endPointR * Math.cos(d.theta))
// return g
},
update => {
update.select('.nodeLine')
.transition().duration(DURATION)
// .attr("x1", d=>d.x)
// .attr("y1", d=>d.y)
// .attr("x2", d =>this.getNodeLineEndPoint(d).x)
// .attr("y2", d =>this.getNodeLineEndPoint(d).y)
.attrTween('x1', d => radialInterpolatorX(d.pr, d.ptheta, d.r, d.theta))
.attrTween('y1', d => radialInterpolatorY(d.pr, d.ptheta, d.r, d.theta))
.attrTween('x2', d => radialInterpolatorX(d.pendPointR, d.ptheta, d.endPointR, d.theta))
.attrTween('y2', d => radialInterpolatorY(d.pendPointR, d.ptheta, d.endPointR, d.theta))
update.select('.nodePath')
.transition().duration(DURATION)
// .attr('d', d=>this.getBranchPath(d))
.attrTween('d', d => (t)=>{
let r = (1-t)*d.pendPointR + t*d.endPointR
let theta1 = (1-t)*d.ptheta1 + t*d.theta1
let theta2 = (1-t)*d.ptheta2 + t*d.theta2
let x1 = r * -Math.sin(theta1);
let y1 = r * Math.cos(theta1);
let x2 = r * -Math.sin(theta2);
let y2 = r * Math.cos(theta2);
let largearcflag = Math.abs(theta1 - theta2) > Math.PI ? 1 : 0
return `M ${x1} ${y1} A ${r} ${r} ${0} ${largearcflag} ${1} ${x2} ${y2} `;
})
},
exit => {
// let descendants = trueDescendants(this.currentNode)
// let exitOut = exit.filter(d => descendants.includes(d))
// let exitRoot = exit.filter(d => !descendants.includes(d))
// console.log(exit, exitOut, exitRoot)
// let exitRootTheta = 0
// exitRoot.select('.nodeCircle')
// .transition().duration(DURATION)
// .attr('r', 0)
// // .attr('cx', this.rootSize * -Math.sin(exitRootTheta))
// // .attr('cy', this.rootSize * Math.cos(exitRootTheta))
// .attrTween('cx', d => (t)=>{
// let angle = (1-t)*d.theta + t*exitRootTheta
// let r = (1-t)*d.r + this.rootSize
// return r * -Math.sin(angle);
// })
// .attrTween('cy', d => (t)=>{
// let angle = (1-t)*d.theta + t*exitRootTheta
// let r = (1-t)*d.r + this.rootSize
// return r * Math.cos(angle);
// })
// exitRoot.select('.nodePath')
// .transition().duration(DURATION)
// .attr('d', )
// exitRoot.select('.nodeLine')
// .transition().duration(DURATION)
// .attr('x1',this.rootSize * -Math.sin(exitRootTheta))
// .attr('y1',this.rootSize * Math.cos(exitRootTheta))
// .attr('x2',this.rootSize * -Math.sin(exitRootTheta))
// .attr('y2',this.rootSize * Math.cos(exitRootTheta))
exit
.transition().duration(DURATION)
.attr('opacity',0)
.remove()
return exit
}
)
this.nodeCircleContainer.selectAll('.nodeCircle')
.data(descendants, d=>d.data.TSN)
.join(
enter => {
let that = this;
let nodeCircle = enter.append('circle')
.each(function(d) { d.nodeCircleElem = this })
.attr('class', 'nodeCircle')
.attr('stroke', 'none')
.attr('opacity', 0)
.attr('fill', this.color)
.attr('cx', d=>d.x)
.attr('cy', d=>d.y)
.attr('r', d=>this.getNodeCircleRadius(d))
.on('mouseover', function(e,d){
console.log(e)
console.log(d)
that.setRootNodeText(d.data.name)
that.highlight(d)
})
.on('mouseout', function(e,d){
that.setRootNodeText(that.currentNode.data.name)// marche pas il faut l'update je crois
that.unhighlight(d)
})
.on('click', (e, d)=>{this.update(d)})
.transition('enter').duration(DURATION).delay(DURATION/2)//.delay(DURATION)
.attr('opacity',1)
},
update => {
update.transition('update').duration(DURATION)
.attr('r', d=>this.getNodeCircleRadius(d))
// .attr('cx',d=>d.x)//old
.attrTween('cx', d => (t)=>{
let angle = (1-t)*d.ptheta + t*d.theta
let r = (1-t)*d.pr + t*d.r
return r * -Math.sin(angle);
})
.attrTween('cy', d => (t)=>{
let angle = (1-t)*d.ptheta + t*d.theta
let r = (1-t)*d.pr + t*d.r
return r * Math.cos(angle);
})
},
exit => exit
.transition('exit').duration(DURATION)
.attr('opacity',0)
.remove()
)
}
MyRadialTree.prototype.drawParentBranch = function(node){
// if(this.currentNode.parent)
if(node.parent){//==this.rootnode?
this.svg.select('.parentNodeCircle')
.transition().duration(DURATION)
.attr('r',this.nodeCircleRadius)
.attr('cy',this.rootSize/2)
this.svg.select('.parentNodeLine')
.transition().duration(DURATION)
.attr('y2',this.rootSize/2)
}else{
this.svg.select('.parentNodeCircle')
.transition().duration(DURATION)
.attr('r',0)
.attr('cy',this.rootSize)
this.svg.select('.parentNodeLine')
.transition().duration(DURATION)
.attr('y2',this.rootSize)
}
}
MyRadialTree.prototype.drawNames = function(node){
this.svg.selectAll('.leafName')
.data(node.leaves().filter(d=>d != this.currentNode), d=>d.data.TSN)
.join(
enter => {
let that = this
let g = enter.append('g')
.each(function(d) { d.labelElem = this })
.attr('class','leafName')
.attr('transform', d=>"translate("+ this.leafLabelRadius* -Math.sin(d.theta) +","+ this.leafLabelRadius* Math.cos(d.theta)+")")
.attr('fill', this.color)
g.attr('opacity',0)
.transition().duration(DURATION).delay(DURATION/2)//.delay(DURATION)
.attr('opacity',1)
g.append('text')
.attr('alignment-baseline', 'middle')
.attr('text-anchor', d => d.theta < Math.PI ? 'end':'start')
.attr('transform', d=>{
let angle = d.theta < Math.PI ? d.theta+Math.PI : d.theta
return "rotate("+ (angle/(2*Math.PI)*360 +90) +")"
})
.text(d=>d.data.name)
.on('mouseover', function(e,d){
// that.setRootNodeText(d.data.name)
that.highlight(d)
})
.on('mouseout', function(e,d){
// that.setRootNodeText(that.currentNode.data.name)// marche pas il faut l'update je crois
that.unhighlight(d)
})
.on('click', (e, d)=>{
that.unhighlight(d)
this.update(d)
})
// return label
},
update => {
update.transition('enter').duration(DURATION)
.attrTween('transform', d =>(t)=>{
let angle = (1-t)*d.ptheta + t*d.theta
let x = this.leafLabelRadius* -Math.sin(angle)
let y = this.leafLabelRadius* Math.cos(angle)
return "translate("+ x +","+ y+")"
})
update.select('text')
.transition('update').duration(DURATION)
// .attr('transform', d=>{
// let angle = d.theta < Math.PI ? d.theta+Math.PI : d.theta
// return "rotate("+ (angle/(2*Math.PI)*360 +90) +")"
// })
// .attr('text-anchor', d => d.theta < Math.PI ? 'end':'start')
.attrTween('transform', d=>t=>{
let angle = (1-t)*d.ptheta + t*d.theta
angle = angle < Math.PI ? angle+Math.PI : angle
return "rotate("+ (angle/(2*Math.PI)*360 +90) +")"
})
.attrTween('text-anchor', d => t=>{
let angle = (1-t)*d.ptheta + t*d.theta
return angle < Math.PI ? 'end':'start'
})
},
exit => {
exit
.transition('exit').duration(DURATION)
.attr('opacity',0)
.remove()
return exit
}
)
}
MyRadialTree.prototype.setRootNodeText = function(name){
this.rootNodeText.text(name)
.attr('font-size', this.rootNodeTextSize)
this.rootNodeText.attr('font-size', ()=>{
let textWidth = this.rootNodeText.node().clientWidth//_groups[0][0])
let ratio = (this.rootSize-10)/textWidth
return ratio < 1 ? this.rootNodeTextSize*ratio : this.rootNodeTextSize
})
}
MyRadialTree.prototype.highlight = function(node){
// d3.select(node.nodeCircleElem)
// .transition().duration(this.highlightDuration)
// .attr('r', this.nodeCircleRadiusActive)
//highlight children branches and nodes
node.descendants().forEach((nd)=>{
d3.select(nd.branchElem)
.attr('stroke', this.highlightColor)
// .attr('stroke-width',2)
d3.select(nd.nodeCircleElem)
.attr('fill', this.highlightColor)
})
//highlight leaves labels
node.leaves().forEach((nd)=>{
d3.select(nd.labelElem)
.attr('fill', this.highlightColor)
})
//highlight ancestors node circles
node.ancestors().forEach((nd) =>{
d3.select(nd.nodeCircleElem)
.attr('fill', this.highlightColor)
})
this.rootCircle.attr('stroke',this.highlightColor)
//highlight ancestors
let d = `M ${node.x} ${node.y} `
let nd = node.parent;
let pnd = node
while( nd != this.currentNode){
let largearcflag = Math.abs(pnd.theta - nd.theta) > Math.PI ? 1 : 0
let sweepflag = nd.theta > pnd.theta ? 1 : 0
let endpointx = nd.endPointR * -Math.sin(nd.theta);
let endpointy = nd.endPointR * Math.cos(nd.theta);
d += `A ${nd.endPointR} ${nd.endPointR} ${1} ${largearcflag} ${sweepflag} ${endpointx} ${endpointy} L ${nd.x} ${nd.y} `
pnd = nd;
nd = nd.parent;
}
//append ancestor path
this.highlightPathContainer.append('path')
.attr('class','ancestorHighlightPath')
.attr('d', d)
.attr('fill', 'none')
.attr('stroke', this.highlightColor)
}
MyRadialTree.prototype.unhighlight = function(node){
// d3.select(node.nodeCircleElem)
// .transition().duration(this.highlightDuration)
// .attr('r', this.nodeCircleRadius)
node.descendants().forEach((nd)=>{
d3.select(nd.branchElem)
.attr('stroke', this.color)
// .attr('stroke-width',1)
d3.select(nd.nodeCircleElem)
.attr('fill', this.color)
})
node.leaves().forEach((nd)=>{
d3.select(nd.labelElem)
.attr('fill', this.color)
})
node.ancestors().forEach((nd) =>{
d3.select(nd.nodeCircleElem)
.attr('fill', this.color)
})
this.rootCircle.attr('stroke',this.color)
this.highlightPathContainer.select('.ancestorHighlightPath').remove()
}
MyRadialTree.prototype.toggleLeafNames = function(value){
this.showLeafNames = value
if(this.showLeafNames){
this.leafNameSize = 200;
}else{
this.svg.selectAll('.leafName')
.transition().duration(DURATION)
.attr('opacity',0)
.remove()
this.leafNameSize = 0;
}
this.resize(this.width, this.height)
this.update(this.currentNode)
}
MyRadialTree.prototype.pruneNode = function(node){
let offsets = []//le nombre de branches sans enfants car avec cluster, ils serront au même niveau que les dernières feuilles, donc il faudra les ajouter
let leaves = []
function countChildren(node, depth){//en fait on peut pas utiliser le node.depth parce que on doit forcément partir d'une depth de 0 même avec une sous branche dont la depth vaut +
if(!offsets[depth]) offsets[depth]=0;
if(!leaves[depth]) leaves[depth]=0;
if(!node._children){
offsets[depth]+=1;
}else{
leaves[depth]+=node._children.length
node._children.forEach(child => {
countChildren(child, depth+1)
});
}
}
countChildren(node, 0)
for(let i = 1; i< offsets.length; i++)// on cumule les offsets
offsets[i] += offsets[i-1];
for(let i = 0; i < leaves.length; i ++){
leaves[i] += offsets[i];
}
let displayableDepth = 0;
while(leaves[displayableDepth] && leaves[displayableDepth] <= this.maxleaves){
displayableDepth++
}
// console.log(displayableDepth)
node.descendants().forEach(elem=>{
elem.children = elem._children;
})
node.descendants().forEach(elem=>{
if(elem.depth == displayableDepth + node.depth){
elem.children = null
}
})
}
MyRadialTree.prototype.resize = function(containerWidth, containerHeight){
this.width = Math.min(containerWidth,containerHeight);
this.height = this.width
// this.rootSize = Math.min(100, this.height/6)
this.leafLineRadius = this.height/2 - this.leafNameSize - this.leafOffset
this.leafLabelRadius = this.leafLineRadius + this.leafOffset
this.branchRange = this.leafLineRadius - this.leafLineSize - this.rootSize
this.svgElement
.attr("width", this.width)
.attr("height", this.height)
this.svg.attr("transform", "translate(" + (this.width/2+this.margin) + "," + (this.height/2+this.margin) +")")
this.svg.select('.rootCircle')
.attr('r', this.rootSize);
this.cluster = d3.cluster()
.size([2*Math.PI , this.branchRange])
.separation((a, b) => 1);
// this.maxleaves = 400;
// maxleaf = 2*Math.PI* (this.heifht/2 - unoffsetselonlalongueurmaxdunnom?) / tailledepolice?
this.maxleaves = 2*Math.PI* this.leafLineRadius / 20
// console.log("maxleaves = "+this.maxleaves)
}
MyRadialTree.prototype.getNodeCircleRadius = function(node){
if(node == this.currentNode)
return 0;
else
return this.nodeCircleRadius;
}
MyRadialTree.prototype.getBranchPath = function(d){
if(d.children && d.children.length > 0){
let firstchild = d.children[0]
let lastchild = d.children[d.children.length-1]
let largearcflag = Math.abs(lastchild.theta - firstchild.theta) > Math.PI ? 1 : 0
return `M ${firstchild.x} ${firstchild.y} A ${firstchild.r} ${firstchild.r} ${0} ${largearcflag} ${1} ${lastchild.x} ${lastchild.y} `
}else{
return `M ${this.leafLineRadius * -Math.sin(d.theta)} ${this.leafLineRadius * -Math.sin(d.theta)} `//il interpole le flag du coup il warn
//check out curveto "C" instead
}
}
MyRadialTree.prototype.getNodeLineEndPoint = function(d){
let P = {};
if(d.children && d.children.length > 0){//pas sur qu'il faille check la length, btw il check pas le 2è statement si il y a pas children donc pas d'err
P.x = d.children[0].r * -Math.sin(d.theta);
P.y = d.children[0].r * Math.cos(d.theta);
}else{
P.x = this.leafLineRadius * -Math.sin(d.theta);
P.y = this.leafLineRadius * Math.cos(d.theta);
}
return P;
}
function trueDescendants(node){//since I use _children trick,
let trueDescendants = []
function truc(nd){
if(nd._children){
nd._children.forEach(n => {
trueDescendants.push(n);
truc(n);
});
}
}
truc(node);
return trueDescendants;
}
function radialInterpolatorX(r1, theta1, r2, theta2){
return (t)=>{
let angle = (1-t)*theta1 + t*theta2
let r = (1-t)*r1 + t*r2
return r * -Math.sin(angle);
}
}
function radialInterpolatorY(r1, theta1, r2, theta2){
return (t)=>{
let angle = (1-t)*theta1 + t*theta2
let r = (1-t)*r1 + t*r2
return r * Math.cos(angle);
}
}