D3.js Error transition arc "<path> attribute d: Expected arc flag ('0' or '1'),"

105 Views Asked by At

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));
            };
        }

Image of Error enter image description here

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.

1

There are 1 best solutions below

3
Luke Woodward On BEST ANSWER

The reason why you are still getting the errors 'Expected arc flag (0 or 1)' despite using an arcTween function is that you are overwriting the attrTween immediately afterwards with a call to attr:

                        .transition().duration(500).attrTween("d", arcTween)
                        .attr("d", arc);

If you put a breakpoint in your arcTween function, 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.interpolate does not work at all well with the output of d3.hierarchy. d3.interpolate interpolates 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 by d3.hierarchy have parent properties as well as children, so when d3.interpolate reaches a pair of child nodes, it will then attempt to interpolate the parent nodes. However, d3.interpolate does 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.interpolate on objects with the parent properties removed:

            function deepCopyWithoutParents(node) {
                var newValue = Object.assign({}, node);
                delete newValue.parent;
                if (newValue.children) {
                    newValue.children = newValue.children.map(deepCopyWithoutParents);
                }
                return newValue;
            }

            function arcTween(a) {
                if (!this._current) {
                    this._current = deepCopyWithoutParents(a);
                }
                var i = d3.interpolate(this._current, deepCopyWithoutParents(a));
                return function(t) {
                    return arc(i(t));
                };
            }

The deepCopyWithoutParents object returns a deep copy of the data objects used here, with the parent properties removed and assuming that children is 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 middleArcLine instead of arc, and use that instead of arcTween for 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 initialise this._current to store the values from data1 before the first transition runs, and also to set this._current to 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._current is used to store the current data, i.e. the data being transitioned away from. The data passed in to the attrTween functions is the data being transitioned to.

At the moment, this._current is only being set the first time a transition runs. this._current gets set to the corresponding data within data2. So when you call sun(data1) after data2 has been shown, you see the transitions, because the chart is transitioning from data2 to data1. When you call sun(data2) however, the transitions run from data2 to data2. The chart may be showing data1, but it will jump straight from data1 to data2 because data2 is what is set as the start point of the transition. Of course, with data2 being both the start and end of the transition, you don't see anything happen.

To set this._current appropriately, add the following small function:

            function setCurrent(d) {
                this._current = deepCopyWithoutParents(d);
            }

Then you need to add .each(setCurrent) to the code that creates the arcs. This ensures that this._current is set on each arc to the initial data:

                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)
                            .each(setCurrent);  // add this line

                newSlice.append('path')
                        .attr('class', 'hidden-arc')
                        .attr('id', (_, i) => `hiddenArc${i}`)
                        .attr('d', middleArcLine)
                        .each(setCurrent);      // add this line

(Of course, remove the semicolons from the ends of the previous lines too.)

Secondly, call setCurrent when the arc transitions end. Do this by adding .on("end", setCurrent) to the arc transitions:

.transition().duration(500).attrTween("d", arcTween).on("end", setCurrent);

(and similarly for the other call to attrTween). This is necessary so that once the transitions end, this._current is 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.