const data1= {
"name": "TOPICS", "children": [{
"name": "Topic A",
"children": [{"name": "Sub A1", "size": 4}, {"name": "Sub A2", "size": 4}]
}, {
"name": "Topic B",
"children": [{"name": "Sub B1", "size": 3}, {"name": "Sub B2", "size": 3}, {
"name": "Sub B3", "size": 3}]
}, {
"name": "Topic C",
"children": [{"name": "Sub A1", "size": 4}, {"name": "Sub A2", "size": 4}]
}]
};
const data2= {
"name": "TOPICS", "children": [{
"name": "Topic A",
"children": [{"name": "Sub A1", "size": 4}, {"name": "Sub A2", "size": 4}]
}, {
"name": "Topic B",
"children": [{"name": "Sub B1", "size": 3}, {"name": "Sub B2", "size": 3}, {
"name": "Sub B3", "size": 3}]
}]
};
/*declare variables*/
var i_region_static_id = "sunburst",
parentDiv = document.getElementById(i_region_static_id),
boost = 'N',
svg,
result,
jsObj,
root,
rootDepth,
x,
y,
arc,
middleArcLine,
middleAngle,
slice,
lastActivatedNode,
width = parentDiv.clientWidth,
height = 450,
maxRadius,
color;
maxRadius = (Math.min(width, height) / 2) - 5;
//-----------------------------------------------------------------------------------
//SVG-Element
svg = d3.select('#' + i_region_static_id).append('svg')
.style('width', width) //'100vw')
.style('height', height) //'100vh')
.attr('viewBox', `${-width / 2} ${-height / 2} ${width} ${height}`)
.on('dblclick', d => {
if(event.detail === 2) focusOn() //double click
}
); // Reset zoom on canvas click
//-----------------------------------------------------------------------------------
// X-Scale
x = d3.scaleLinear()
.range([0, 2 * Math.PI])
.clamp(true);
//-----------------------------------------------------------------------------------
// Y-Scale
y = d3.scaleSqrt()
.range([maxRadius*.1, maxRadius]);
const partition = d3.partition();
/*text-fit constant*/
const textFits = d => {
const CHAR_SPACE = 6;
const deltaAngle = x(d.x1) - x(d.x0);
const r = Math.max(0, (y(d.y0) + y(d.y1)) / 2);
const perimeter = r * deltaAngle;
return d.data.name.length * CHAR_SPACE < perimeter;
};
//-------------------------------------------------------------------------------------------
/* initialize sun */
function initialize_sun(pData){
/*set default color scheme*/
switch('COLOR20'){
case 'COLOR20': color = d3.scaleOrdinal(d3.schemeCategory20); break;
case 'COLOR20B': color = d3.scaleOrdinal(d3.schemeCategory20b); break;
case 'COLOR20C': color = d3.scaleOrdinal(d3.schemeCategory20c); break;
case 'COLOR10': color = d3.scaleOrdinal(d3.schemeCategory10);
}
arc = d3.arc()
.startAngle(d => x(d.x0))
.endAngle(d => x(d.x1))
.innerRadius(d => Math.max(0, y(d.y0)))
.outerRadius(d => Math.max(0, y(d.y1)));
middleArcLine = d => {
const halfPi = Math.PI/2;
const angles = [x(d.x0) - halfPi, x(d.x1) - halfPi];
const r = Math.max(0, (y(d.y0) + y(d.y1)) / 2);
middleAngle = (angles[1] + angles[0]) / 2;
const invertDirection = middleAngle > 0 && middleAngle < Math.PI; // On lower quadrants write text ccw
if (invertDirection) { angles.reverse(); }
const path = d3.path();
path.arc(0, 0, r, angles[0], angles[1], invertDirection);
return path.toString();
};
root = d3.hierarchy(pData); //set data
root.sum(d =>
(d.children == undefined) ? ((d.size == undefined) ? 1 : d.size) : 0 //parent value defined by childrens values
);
slice = svg.selectAll('g.slice')
.data(
partition(root)
.descendants()
);
slice.exit().remove();
const newSlice = slice.enter()
.append('g').attr('class', 'slice')
.attr('display', d => d.depth < 2 ? null : 'none') //hide levels lower depth
.on('dblclick', d => {
if (event.detail === 2){ //double click
d3.event.stopPropagation();
focusOn(d);
}
})
.on('click', d => {
if (event.detail === 1) { //single click
if('Y' == 'Y') activateNode(d); //visual selection if highlight setting activated by developer
}
})
newSlice.append('path')
.attr('class', 'main-arc')
.style('fill', d => (d.data.color == undefined) ? color((d.children ? d : d.parent).data.name) : d.data.color) //set source color, otherwise default color
.attr('d', arc);
newSlice.append('path')
.attr('class', 'hidden-arc')
.attr('id', (_, i) => `hiddenArc${i}`)
.attr('d', middleArcLine);
const text = newSlice.append('text')
.attr('display', d => textFits(d) ? null : 'none'); //hide text on lower levels
text.append('textPath')
.attr('startOffset','50%')
.attr('xlink:href', (_, i) => `#hiddenArc${i}` )
.text(d => d.data.name) //set text in sector
.attr('fill', d => calculateContrast(d));
}
//-------------------------------------------------------------------------------------------
/*update sun (similar to initialize sun with some adjustments)*/
function update_sun(pData){
console.log(pData)
svg = d3.select('#' + i_region_static_id).selectAll('svg');
var svg_helper = d3.select('#' + i_region_static_id).selectAll('svg')._groups[0];
/*remove children*/
while (svg_helper[0].firstChild) {
svg_helper[0].removeChild(svg_helper[0].lastChild);
}
/*get region size before update -> so region size will not change when updated*/
width = parseInt(svg_helper[0].style.width);
height = parseInt(svg_helper[0].style.height);
maxRadius = (Math.min(width, height) / 2) - 5;
arc = d3.arc()
.startAngle(d => x(d.x0))
.endAngle(d => x(d.x1))
.innerRadius(d => Math.max(0, y(d.y0)))
.outerRadius(d => Math.max(0, y(d.y1)));
middleArcLine = d => {
const halfPi = Math.PI/2;
const angles = [x(d.x0) - halfPi, x(d.x1) - halfPi];
const r = Math.max(0, (y(d.y0) + y(d.y1)) / 2);
const middleAngle = (angles[1] + angles[0]) / 2;
const invertDirection = middleAngle > 0 && middleAngle < Math.PI; // On lower quadrants write text ccw
if (invertDirection) { angles.reverse(); }
const path = d3.path();
path.arc(0, 0, r, angles[0], angles[1], invertDirection);
return path.toString();
};
svg.style('width', width) //'100vw')
.style('height', height) //'100vh')
.attr('viewBox', `${-width / 2} ${-height / 2} ${width} ${height}`)
.on('dblclick', d => {
if(event.detail === 2) focusOn() //double click
}
); // Reset zoom on canvas click
//root = JSON.parse(pData); //set updated data
root = d3.hierarchy(pData);
root.sum(d =>
(d.children == undefined) ? ((d.size == undefined) ? 1 : d.size) : 0
);
slice = svg.selectAll('g.slice')
.data(
partition(root)
.descendants()
);
slice.exit().remove();
const newSlice = slice.enter()
.append('g').attr('class', 'slice')
.attr('display', d => d.depth < 2 ? null : 'none') ////hide levels lower depth
.on('dblclick', d => {
if (event.detail === 2){ //double click
d3.event.stopPropagation();
focusOn(d);
}
})
.on('click', d => {
if (event.detail === 1) { //single click
if('Y' == 'Y') activateNode(d); //visual selection if highlight setting activated by developer
}
})
newSlice.append('path')
.attr('class', 'main-arc')
.style('fill', d => (d.data.color == undefined) ? color((d.children ? d : d.parent).data.name) : d.data.color) //set source color, otherwise default color
.attr('d', arc);
newSlice.append('path')
.attr('class', 'hidden-arc')
.attr('id', (_, i) => `hiddenArc${i}`)
.attr('d', middleArcLine);
const text = newSlice.append('text')
.attr('display', d => textFits(d) ? null : 'none');
text.append('textPath')
.attr('startOffset','50%')
.attr('xlink:href', (_, i) => `#hiddenArc${i}` )
.text(d => d.data.name)
.attr('fill', d => calculateContrast(d));
}
//-------------------------------------------------------------------------------------------
/* visually select a node */
function activateNode(elD){
//if other node has been activated -> deactivated node
if(lastActivatedNode != undefined){
lastActivatedNode
.selectAll('.main-arc')
.style('stroke', null)
.style('stroke-width', null)
lastActivatedNode
.selectAll('text')
.style('font-weight', 'normal');
}
//find clicked node
var nodeSlice = svg.selectAll(".slice")
.filter(d => d === elD);
//set lastActivatedNode for upcoming deactivation
lastActivatedNode = nodeSlice;
//foreground node
nodeSlice.each(function(d) {
this.parentNode.appendChild(this)
});
//add highlighting
nodeSlice.selectAll('.main-arc')
.style('stroke', '#000')
.style('stroke-width', '2px')
//highlight text
nodeSlice.selectAll('text')
.style('font-weight', 'bold');
}
//-------------------------------------------------------------------------------------------
/*Returns Black/White depending on node color*/
function calculateContrast(d){
var nodeColor = (d.data.color == undefined) ? color((d.children ? d : d.parent).data.name) : d.data.color;
var rgb = /^#?([a-fd]{2})([a-fd]{2})([a-fd]{2})$/i.exec(nodeColor);
const o = (rgb != undefined) ? Math.round(((parseInt(rgb[1], 16) * 299) + (parseInt(rgb[2], 16) * 587) + (parseInt(rgb[3], 16) * 114)) /1000) : 0;
return o < 150 ? 'white' : 'black'
}
//-------------------------------------------------------------------------------------------
/*check if node in depth*/
function maxDepth(d){
if(rootDepth == undefined){ //if user clicks next to sun = root undefined
rootDepth = 0;
}
return ((d.depth - rootDepth) < 2 );
}
//-------------------------------------------------------------------------------------------
function focusOn(d = { x0: 0, x1: 1, y0: 0, y1: 1 }) {
// Reset to top-level if no data point specified
// Activate top-level node if no data point specified
if(d.data == undefined){
svg.selectAll(".slice")
.filter(d => d.parent == undefined && d.children != undefined)
.each(function(d){
activateNode(d);
}
);
}
root = d; // root-node
rootDepth = root.depth; /*root node depth for maxDepth(d)*/
const transition = svg.transition()
.duration(750)
.tween('scale', () => {
const xd = d3.interpolate(x.domain(), [d.x0, d.x1]),
yd = d3.interpolate(y.domain(), [d.y0, 1]);
return t => { x.domain(xd(t)); y.domain(yd(t)); };
});
transition.selectAll('.slice')
.attr('display', d => maxDepth(d) ? null : 'none'); //display nodes only in depth for transition
transition.selectAll('path.main-arc')
.filter(d => maxDepth(d))
.attrTween('d', d => () => arc(d));
transition.selectAll('path.hidden-arc')
.filter(d => maxDepth(d))
.attrTween('d', d => () => middleArcLine(d));
transition.selectAll('text')
.filter(d => maxDepth(d))
.attrTween('display', d => () => textFits(d)? null : 'none'); //display text only in depth
moveStackToFront(d);
/*foreground nodes -> inner nodes higher than outer nodes*/
function moveStackToFront(elD) {
svg.selectAll('.slice').filter(d => d === elD)
.each(function(d) {
this.parentNode.appendChild(this);
if (d.parent) { moveStackToFront(d.parent); }
})
}
}
//-------------------------------------------------------------------------------------------
initialize_sun(data2)
let i = 0;
d3.interval(() => {
if (i++ % 2 === 0) {
update_sun(data1);
} else {
update_sun(data2);
}
}, 4000)
.slice {
cursor: pointer;
}
.slice .main-arc {
stroke: #fff;
stroke-width: 1px;
}
.slice .hidden-arc {
fill: none;
}
.slice text {
pointer-events: none;
text-anchor: middle;
}
<div id="sunburst"></div>
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script src="https://unpkg.com/d3fc" charset="utf-8"></script>