I am currently trying to create a sunburst chart using the sunburst library. Thanks to the help from this forum, I was able to add an update function. Now, I always get the following error message at the transition of an ARC-element when my Datasource will be updated (image below):
<path> attribute d: Expected arc flag ('0' or '1'),"
As shown here and here, there seems to be a problem with the transition of the ARC-elements. The D3's default transition can't interpolate my ARC-Elements correctly.
So, as described in the entries, I added the custom interpolator as a function and linked it to my transition.
Unfortunately it does not work, the error still occurs.
Can someone please explain to me why it doesn't work and how the error can be corrected?
arcTween function:
// Custom interpolator
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
My Code:
// Data
const data1 = {
"name": "TOPICS",
"id": 1,
"children": [{
"name": "Topic A",
"id": 2,
"children": [{
"name": "Sub A1",
"id": 5,
"size": 10
}, {
"name": "Sub A2",
"id": 6,
"size": 4
}]
}, {
"name": "Topic B",
"id": 3,
"children": [{
"name": "Sub B1",
"id": 7,
"size": 3
}, {
"name": "Sub B2",
"id": 8,
"size": 3
}, {
"name": "Sub B3",
"id": 9,
"size": 3
}]
}, {
"name": "Topic C",
"id": 4,
"children": [{
"name": "Sub A3",
"id": 10,
"size": 4
}, {
"name": "Sub A4",
"id": 11,
"size": 4
}]
}]
};
const data2 = {
"name": "TOPICS",
"id": 1,
"children": [{
"name": "Topic A",
"id": 2,
"children": [{
"name": "Sub A1",
"id": 5,
"size": 4
}, {
"name": "Sub A2",
"id": 6,
"size": 4
}]
}, {
"name": "Topic B",
"id": 3,
"children": [{
"name": "Sub B1",
"id": 7,
"size": 3
}, {
"name": "Sub B2",
"id": 8,
"size": 3
}, {
"name": "Sub B3",
"id": 9,
"size": 3
}]
}]
};
//-------------------------------------------------------------------------------------------
// Declare variables
let i_region_static_id = "sunburst",
parentDiv = document.getElementById(i_region_static_id),
width = parentDiv.clientWidth,
height = 450,
root,
rootDepth,
x,
y,
color = d3.scaleOrdinal(d3.schemeCategory10);
maxRadius = (Math.min(width, height) / 2) - 5;
const partition = d3.partition();
//-----------------------------------------------------------------------------------
// SVG-Element
let svg = d3.select('#' + i_region_static_id).append('svg')
.style('width', width)
.style('height', height)
.attr('viewBox', `${-width / 2} ${-height / 2} ${width} ${height}`)
.on('dblclick', d => {
if (event.detail === 2) focusOn() // Double click
});
//-----------------------------------------------------------------------------------
// X-Scale
x = d3.scaleLinear()
.range([0, 2 * Math.PI])
.clamp(true);
//-----------------------------------------------------------------------------------
// Y-Scale
y = d3.scaleSqrt()
.range([maxRadius * .1, maxRadius]);
//-------------------------------------------------------------------------------------------
// 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;
};
//-----------------------------------------------------------------------------------
// Create Arc generator
const 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)))
//-----------------------------------------------------------------------------------
const 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();
}
//-------------------------------------------------------------------------------------------
// 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}) {
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 and Update sun
function updateSun(pData) {
const valueAccessor = (d) => d.size;
root = d3.hierarchy(pData); //set data
// Durch den ValueAccessor wird dem Parent Element die Summe der Values, von den Childs, zugeordnet
// Wird genutzt, um die ARC Abmaße zu bestimmen
valueAccessor == null ? root.count() : root.sum((d) => Math.max(0, valueAccessor(d)));
// Sortiert Nodes
root.sort((d) => d3.descending(d.value))
const slice = svg.selectAll('g.slice')
.data(
partition(root).descendants(),
function(d) { return d.data.id; }
);
// Enter Section
const newSlice = slice.enter()
.append('g').attr('class', 'slice')
.attr('display', d => d.depth < 2 ? null : 'none') // Hide levels lower depth
.on('dblclick', (e, d) => {
e.stopPropagation();
focusOn(d);
}
)
.each(function(d, i) {
// Append main-arc
d3.select(this).append('path')
.attr('class', 'main-arc')
.attr('d', arc)
setCurrent // New Line Added
// Append hidden-arc
d3.select(this).append('path') .attr('class', 'hidden-arc')
.attr('d', middleArcLine)
setCurrent // New Line Added
// Append text
d3.select(this).append('text')
// Append textPath for Textstring
.append('textPath')
.attr('startOffset', '50%')
.attr('fill', d => 'black');
})
.merge(slice)
.each(function(d, i) {
// Update Section
// Go back to Level 0 before next Update
if (rootDepth > 0){
// console.log(rootDepth)
focusOn(); // New Line Added
}
// console.log(rootDepth)
d3.select(this).select('path.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
.transition().delay(500).attrTween("d", arcTween).on("end", setCurrent) // New Line Added
d3.select(this).select('path.hidden-arc')
.attr('id', 'hiddenArc' + i)
.transition().delay(500).attrTween("d", arcTweenLabel).on("end", setCurrent) // New Line Added
d3.select(this).select('text')
.attr('display', d => textFits(d) ? null : 'none')
d3.select(this).select('textPath')
.attr('xlink:href', '#hiddenArc' + i) // Supply the id of the path along which you want to place the text.
.text(d => d.data.name); // Set text in sector
})
// Delete Section
slice.exit().transition().duration(500).style("fill-opacity", 0.2).remove();
//-------------------------------------------------------------------------------------------
// New Section Added
// SetCurrent used to store current data
function setCurrent(d) {
this._current = deepCopyWithoutParents(d);
}
// Remove Parent of Childnodes
function deepCopyWithoutParents(node) {
var newValue = Object.assign({}, node);
delete newValue.parent;
if (newValue.children) {
newValue.children = newValue.children.map(deepCopyWithoutParents);
}
return newValue;
}
// Custom interpolator Main-Slices
function arcTween(a) {
// console.log(a)
// console.log(this._current)
// console.log(deepCopyWithoutParents(a))
if (!this._current) {
this._current = deepCopyWithoutParents(a);
}
var i = d3.interpolate(this._current, deepCopyWithoutParents(a));
return function(t) {
return arc(i(t));
};
}
// Custom interpolator Hidden Slices, for Labels
function arcTweenLabel(a) {
//console.log(a)
//console.log(this._current)
//console.log(deepCopyWithoutParents(a))
if (!this._current) {
this._current = deepCopyWithoutParents(a);
}
var i = d3.interpolate(this._current, deepCopyWithoutParents(a));
return function(t) {
return middleArcLine(i(t));
};
}
}
//-------------------------------------------------------------------------------------------
updateSun(data1)
let i = 0;
d3.interval(() => {
if (i++ % 2 === 0) {
console.log("data2")
updateSun(data2);
} else {
console.log("data1")
updateSun(data1);
}
}, 6000)
.slice {
cursor: pointer;
}
.slice .main-arc {
stroke: #fff;
stroke-width: 1px;
}
.slice .hidden-arc {
fill: none;
}
.slice text {
pointer-events: none;
text-anchor: middle;
}
<!DOCTYPE html>
<html>
<!-- Code Vorschlage mit STRG+Leertaste aktivieren-->
<head>
<meta charset="utf-8" /> <!-- Welche Sonderzeichen verwendet werden können -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- Wie sollte sich die Seite auf einem Handy verhalten -->
<title> Sunbrust </title> <!-- title als Tag -->
<!-- Load plotly.js into the DOM -->
<script src='https://cdn.plot.ly/plotly-2.11.1.min.js'></script>
<style>
</style>
</head>
<body>
<div id="sunburst"></div>
<script src="https://d3js.org/d3.v7.js" charset="utf-8"></script>
<!-- <script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script> -->
<script src="https://unpkg.com/d3fc" charset="utf-8"></script>
</body> <!-- HTML schließender Tag-->
</html> <!-- HTML schließender Tag-->
EDIT: I am very grateful @Luke Woodward detailed explanation and help. I have updated the code based on @Luke Woodward comment and made some progress (see Comments with "New Line Added" or "New Section Added").
The text transition is still missing at the moment.

The reason why you are still getting the errors 'Expected arc flag (0 or 1)' despite using an
arcTweenfunction is that you are overwriting theattrTweenimmediately afterwards with a call toattr:If you put a breakpoint in your
arcTweenfunction, it will never be hit, because the function will never be called.So the first step is to remove the
.attr("d", ...)calls after your calls to.arcTween.However, do not immediately re-run your code following this change. If you do, you might find your browser starts using a lot of CPU, the tab stops responding and you have to kill the tab.
The problem here is that
d3.interpolatedoes not work at all well with the output ofd3.hierarchy.d3.interpolateinterpolates a pair of objects by interpolating the corresponding properties, and interpolates a pair of arrays by interpolating the corresponding elements. The nodes that are output byd3.hierarchyhave parent properties as well as children, so whend3.interpolatereaches a pair of child nodes, it will then attempt to interpolate the parent nodes. However,d3.interpolatedoes not detect that it has already set up an interpolation for the parent nodes, so starts to interpolate thae parent nodes again, and their children, and so on, and so on...The solution to this problem is to call
d3.interpolateon objects with the parent properties removed:The
deepCopyWithoutParentsobject returns a deep copy of the data objects used here, with theparentproperties removed and assuming thatchildrenis the only property that isn't a single value.The next thing you might notice is that the text labels initially appear in the right place but then move to the wrong place as soon as the transitions start. You need to write a separate tween function for the text labels, one that calls
middleArcLineinstead ofarc, and use that instead ofarcTweenfor the text-label transitions.As I wrote in the disclaimer at the top, this is not a complete answer (at least not yet). In particular, you may find the first animation completes immediately, as does the third, fifth, seventh, etc. I think you can fix this if you can find a way to initialisethis._currentto store the values fromdata1before the first transition runs, and also to setthis._currentto the end value when a transition completes.With not much further effort it's possible to set
this._current, both as part of the initial creation, and on transition end. On each arc,this._currentis used to store the current data, i.e. the data being transitioned away from. The data passed in to theattrTweenfunctions is the data being transitioned to.At the moment,
this._currentis only being set the first time a transition runs.this._currentgets set to the corresponding data withindata2. So when you callsun(data1)afterdata2has been shown, you see the transitions, because the chart is transitioning fromdata2todata1. When you callsun(data2)however, the transitions run fromdata2todata2. The chart may be showingdata1, but it will jump straight fromdata1todata2becausedata2is what is set as the start point of the transition. Of course, withdata2being both the start and end of the transition, you don't see anything happen.To set
this._currentappropriately, add the following small function:Then you need to add
.each(setCurrent)to the code that creates the arcs. This ensures thatthis._currentis set on each arc to the initial data:(Of course, remove the semicolons from the ends of the previous lines too.)
Secondly, call
setCurrentwhen the arc transitions end. Do this by adding.on("end", setCurrent)to the arc transitions:(and similarly for the other call to
attrTween). This is necessary so that once the transitions end,this._currentis set to the data that has been transitioned to, so that the next transition will have this data as its start point and can transition away from it.