javascriptd3.jschord-diagram

Update D3v7 chord layout


Recently I am trying to understand D3 Chord graphs, especially with the D3v7. I got two different datasets and want to dynamically update those sets. In general it works beside two problems:

  1. The first object in each datasets will be ignored. I do not why as any console.log shows the existing data but it is simply not drawn. For example dataset 1 based on groups1 and paths1 is missing object A and in dataset2 object E is missing.

  2. As soon as the graph gets updated the old elements are still there and the graph creates new paths without removing the old ones. Each click and change increases the D3 elements in the browser, instead replacing them.

Since D3v3 and D3v7 a bit changed and I can´t figure out quickly enough to help myself.

////////////////////////////////////////////////////////////
        ////////////////////// Set-Up Data /////////////////////////
        ////////////////////////////////////////////////////////////
        
        let groups1 = [
            { name: "A", color: "blue" },
            { name: "B", color: "red" },
            { name: "C", color: "green" },
            { name: "D", color: "yellow" },
        ]

        let paths1 = [
            { from: groups1.name = "A", to: groups1.name = "A", strength: 0 },
            { from: groups1.name = "A", to: groups1.name = "B", strength: 5 },
            { from: groups1.name = "A", to: groups1.name = "C", strength: 5 },
            { from: groups1.name = "A", to: groups1.name = "D", strength: 5 },
            { from: groups1.name = "B", to: groups1.name = "A", strength: 5 },
            { from: groups1.name = "B", to: groups1.name = "B", strength: 0 },
            { from: groups1.name = "B", to: groups1.name = "C", strength: 5 },
            { from: groups1.name = "B", to: groups1.name = "D", strength: 5 },
            { from: groups1.name = "C", to: groups1.name = "A", strength: 5 },
            { from: groups1.name = "C", to: groups1.name = "B", strength: 5 },
            { from: groups1.name = "C", to: groups1.name = "C", strength: 0 },
            { from: groups1.name = "C", to: groups1.name = "D", strength: 5 },
            { from: groups1.name = "D", to: groups1.name = "D", strength: 0 },
            { from: groups1.name = "D", to: groups1.name = "A", strength: 5 },
            { from: groups1.name = "D", to: groups1.name = "B", strength: 5 },
            { from: groups1.name = "D", to: groups1.name = "C", strength: 5 },
        ]


        let groups2 = [
            { name: "E", color: "#301E1E" },
            { name: "F", color: "#083E77" },
            { name: "G", color: "#342350" },
        ]

        let paths2 = [
            { from: groups2.name = "E", to: groups2.name = "E", strength: 0 },
            { from: groups2.name = "E", to: groups2.name = "F", strength: 5 },
            { from: groups2.name = "E", to: groups2.name = "G", strength: 5 },
            { from: groups2.name = "F", to: groups2.name = "E", strength: 5 },
            { from: groups2.name = "F", to: groups2.name = "F", strength: 0 },
            { from: groups2.name = "F", to: groups2.name = "G", strength: 5 },
            { from: groups2.name = "G", to: groups2.name = "E", strength: 5 },
            { from: groups2.name = "G", to: groups2.name = "F", strength: 5 },
            { from: groups2.name = "G", to: groups2.name = "G", strength: 0 },
        ]

        let chord = []
        let matrix = []

        function getMatrix(paths, groups) {
            matrix = []

            var mapPaths = paths.map(item => {
                const container = {}
                    container.from = groups.findIndex(ele => ele.name == item.from)
                    container.to = groups.findIndex(ele => ele.name == item.to)
                    container.strength = item.strength
                return container
            })
            
            mapPaths.forEach(function (item) {
                // initialize sub-arra if not yet exists
                if (!matrix[item.to]) {
                    matrix[item.to] = []
                }

                matrix[item.to][item.from] = item.strength
            })

            return matrix
        }

        // --- --- --- --- --- ---

        const vw = document.documentElement.clientWidth / 2
        const vh = document.documentElement.clientHeight / 2

        let innerRadius = Math.min(vw, vh) * 0.5 
        let outerRadius = innerRadius * 1.1;
        
        let svg = d3.select("#chart").append("svg")
            .attr("width", vw)
            .attr("height", vh)
            .attr("overflow", "unset")  

        var chordGenerator = d3.chord()
            .padAngle(0.10)
            .sortSubgroups(d3.ascending)
            .sortChords(d3.descending)

        chord = chordGenerator(getMatrix(paths1, groups1))

        var arc = d3.arc()
            .innerRadius(innerRadius * 1.01)
            .outerRadius(outerRadius)

        var ribbon = d3.ribbon()
            .radius(innerRadius);
    
        window.update = update;

        update(paths1, groups1)

        function update(thisPaths, thisGroups) {
            let groups = thisGroups
            let paths = thisPaths

            getMatrix(thisPaths, thisGroups)

            const chords = chordGenerator(matrix);

            let wrapper = svg.append("g")
                .attr("transform", "translate(" + vw / 2 + "," + vh / 2 + ")")

            let ribbons = wrapper.append("g");
            
            let g = wrapper.selectAll("g")
                    .data(chord.groups)
                    .enter()
                    .append("g")
                    .attr("class", "group")

            const ribbonsUpdate = ribbons.selectAll("path")
                .data(chords, ({ source, target }) => source.index + '-' + target.index)

            const duration = 1000;

            ribbonsUpdate
                .transition()
                .duration(duration)
                .attr("d", ribbon)
                .style("fill", function (d) { return groups[d.target.index].color; })
                
            ribbonsUpdate
                .enter()
                .append("path")
                .attr("opacity", 0)
                .attr("d", ribbon)
                .style("fill", function (d) { return groups[d.target.index].color; })
                .transition()
                .duration(duration)
                .attr('opacity', 1)
                
            // adding Arc Blocks
            g.append("path")
                .style("fill", function (d) { return groups[d.index].color; })
                .attr("d", arc)
                .style("opacity", 1)

            // adding labels
            g.append("text")
                .each(function(d){ d.angle = (d.startAngle + d.endAngle) / 2; })
                .attr("dy", ".35em")
                .attr("class", "titles")
                .attr("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
                .attr("transform", function(d) {
                    return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")"
                    + "translate(" + (outerRadius + 10) + ")"
                    + (d.angle > Math.PI ? "rotate(180)" : "");
                })
                .text(function(d,i){ return groups[i].name; })

            ribbonsUpdate
                .exit()
                .transition()
                .duration(duration)
                .attr("opacity", 0)
                .remove();
        }

        document.getElementById('data1').onclick = () => {
            chord = chordGenerator(getMatrix(paths1, groups1))
            update(paths1, groups1);
        }

        document.getElementById('data2').onclick = () => {
            chord = chordGenerator(getMatrix(paths2, groups2))
            update(paths2, groups2);
        }
body {
        font-size: 16px;
        font-family: 'Oswald', sans-serif;
        text-align: center;
        background-color: #ECF0F3;
        cursor: default;
        overflow: hidden;
    }
<!DOCTYPE html>
<html>

<head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
    <title>Animated_D3v7</title>

    <!-- jQuery -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
    
    <!-- D3.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.6.1/d3.min.js" charset="utf-8"></script>
    <!-- fontawesome stylesheet https://fontawesome.com/ -->
    <script src="https://kit.fontawesome.com/98a5e27706.js" crossorigin="anonymous"></script>
</head>

<style>
    body {
        font-size: 16px;
        font-family: 'Oswald', sans-serif;
        background-color: #ECF0F3;
        cursor: default;
        overflow: hidden;
    }

</style>

<body>
    <button id="data1">data 1</button>
    <button id="data2">data 2</button>
    
    <div id="chart"></div>
    
</body>

</html>


Solution

  • Problem 1 is solved by:

    let g = wrapper.selectAll("g.group") // add class of 'group' to the selector
    

    There's some ambiguity with the selection otherwise because you have already added a g to wrapper in the previous statement: let ribbons = wrapper.append("g");

    Problem 2 seems a bit more involved.

    Firstly, every time update is called a new g is created under svg and assigned to wrapper. You can define wrapper outside of update just the once.

    Secondly, update uses both chord and chords where the former is updated outside of update and the latter within. You can dispense with the former and just update chord and matrix within update.

    Thirdly, you can set-up a g for each of arcs, ribbons and labels outside and then the enter, update and exit logic works against those path and text elements within them.

    I've simplified a few other little bits in the working example below:

    const vw = document.documentElement.clientWidth / 2
    const vh = document.documentElement.clientHeight / 2
    const innerRadius = Math.min(vw, vh) * 0.4 
    const outerRadius = innerRadius * 1.1;
    const duration = 1000;
    
    const svg = d3.select("#chart").append("svg")
        .attr("width", vw)
        .attr("height", vh)
    
    const wrapper = svg.append("g")
        .attr("transform", "translate(" + vw / 2 + "," + vh / 2 + ")")
        
    const ribbonsG = wrapper.append("g").attr("id", "ribbons")
    const arcsG = wrapper.append("g").attr("id", "arcs")
    const labelsG = wrapper.append("g").attr("id", "labels")
        
    const chordGenerator = d3.chord()
        .padAngle(0.10)
        .sortSubgroups(d3.ascending)
        .sortChords(d3.descending)
    
    const arc = d3.arc()
        .innerRadius(innerRadius * 1.01)
        .outerRadius(outerRadius)
    
    const ribbon = d3.ribbon()
        .radius(innerRadius);
        
    update(paths1, groups1)
    
    function getMatrix(paths, groups) {
        matrix = []
    
        var mapPaths = paths.map(item => {
            const container = {}
                container.from = groups.findIndex(ele => ele.name == item.from)
                container.to = groups.findIndex(ele => ele.name == item.to)
                container.strength = item.strength
            return container
        })
    
        mapPaths.forEach(function (item) {
            // initialize sub-arra if not yet exists
            if (!matrix[item.to]) {
                matrix[item.to] = []
            }
    
            matrix[item.to][item.from] = item.strength
        })
    
        return matrix
    }
    
    function update(thisPaths, thisGroups) {
        const groups = thisGroups
        const paths = thisPaths
        const chords = chordGenerator(getMatrix(thisPaths, thisGroups));
    
        // ribbons
        const ribbonsUpdate = ribbonsG
            .selectAll("path.ribbon")
            .data(chords, ({ source, target }) => source.index + '-' + target.index)
        
        const ribbonsEnter = ribbonsUpdate
            .enter()
            .append("path")
            
        ribbonsUpdate
            .merge(ribbonsEnter)
            .attr("opacity", 0)
            .attr("class", "ribbon")        
            .transition()
            .duration(duration)
            .attr("d", ribbon)
            .style("fill", function (d) { return groups[d.target.index].color; })
            .attr('opacity', 1)
    
        ribbonsUpdate
            .exit()
            .transition()
            .duration(duration)
            .attr("opacity", 0)
            .remove();
            
        // arcs
        const arcsUpdate = arcsG
            .selectAll("path.arc")
            .data(chords.groups)
    
        const arcsEnter = arcsUpdate
            .enter()
            .append("path")
    
        arcsUpdate
            .merge(arcsEnter)
            .attr("opacity", 0)
            .attr("class", "arc")
            .transition()
            .duration(duration)
            .attr("d", arc)
            .style("fill", function (d) { return groups[d.index].color; })
            .attr('opacity', 1)
    
        arcsUpdate
            .exit()
            .transition()
            .duration(duration)
            .attr("opacity", 0)
            .remove();
    
        // adding labels
        const labelsUpdate = labelsG
            .selectAll("text.titles")
            .data(chords.groups)
    
        const labelsEnter = labelsUpdate
            .enter()
            .append("text")
    
        labelsUpdate
            .merge(labelsEnter)
            .attr("class", "titles")
            .attr("opacity", 0)
            .transition()
            .duration(duration)
            .each(function(d){ d.angle = (d.startAngle + d.endAngle) / 2; })
            .attr("dy", ".35em")
            .attr("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
            .attr("transform", function(d) {
                return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")"
                    + "translate(" + (outerRadius + 10) + ")"
                    + (d.angle > Math.PI ? "rotate(180)" : "");
            })
            .text(function(d,i){ return groups[i].name; })        
            .attr("opacity", 1)
            
        labelsUpdate
            .exit()
            .remove()
    }
    
    document.getElementById('data1').onclick = () => {
        update(paths1, groups1);
    }
    
    document.getElementById('data2').onclick = () => {
        update(paths2, groups2);
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.6.1/d3.min.js" charset="utf-8"></script>
    <div id="chart"></div>
    <button id="data1">data 1</button>
    <button id="data2">data 2</button>
    <script>
    let groups1 = [
        { name: "A", color: "blue" },
        { name: "B", color: "red" },
        { name: "C", color: "green" },
        { name: "D", color: "yellow" },
    ]
    
    let paths1 = [
        { from: groups1.name = "A", to: groups1.name = "A", strength: 0 },
        { from: groups1.name = "A", to: groups1.name = "B", strength: 5 },
        { from: groups1.name = "A", to: groups1.name = "C", strength: 5 },
        { from: groups1.name = "A", to: groups1.name = "D", strength: 5 },
        { from: groups1.name = "B", to: groups1.name = "A", strength: 5 },
        { from: groups1.name = "B", to: groups1.name = "B", strength: 0 },
        { from: groups1.name = "B", to: groups1.name = "C", strength: 5 },
        { from: groups1.name = "B", to: groups1.name = "D", strength: 5 },
        { from: groups1.name = "C", to: groups1.name = "A", strength: 5 },
        { from: groups1.name = "C", to: groups1.name = "B", strength: 5 },
        { from: groups1.name = "C", to: groups1.name = "C", strength: 0 },
        { from: groups1.name = "C", to: groups1.name = "D", strength: 5 },
        { from: groups1.name = "D", to: groups1.name = "D", strength: 0 },
        { from: groups1.name = "D", to: groups1.name = "A", strength: 5 },
        { from: groups1.name = "D", to: groups1.name = "B", strength: 5 },
        { from: groups1.name = "D", to: groups1.name = "C", strength: 5 },
    ]
    
    
    let groups2 = [
        { name: "E", color: "#301E1E" },
        { name: "F", color: "#083E77" },
        { name: "G", color: "#11AA99" },
    ]
    
    let paths2 = [
        { from: groups2.name = "E", to: groups2.name = "E", strength: 0 },
        { from: groups2.name = "E", to: groups2.name = "F", strength: 5 },
        { from: groups2.name = "E", to: groups2.name = "G", strength: 5 },
        { from: groups2.name = "F", to: groups2.name = "E", strength: 5 },
        { from: groups2.name = "F", to: groups2.name = "F", strength: 0 },
        { from: groups2.name = "F", to: groups2.name = "G", strength: 5 },
        { from: groups2.name = "G", to: groups2.name = "E", strength: 5 },
        { from: groups2.name = "G", to: groups2.name = "F", strength: 5 },
        { from: groups2.name = "G", to: groups2.name = "G", strength: 0 },
    ]
    </script>