3
votes

I'm doing some research on streamlining the creation of marketing graphics in my company.

One in particular, a sunburst chart, is produced in Illustrator (hard to update) and I want to switch it to a D3 graphic (easy to update via JSON or CSV file). The problem is the sunburst chart is in reversed order... the root node is on the outside ring with data flowing inwards. Note: I can't swap the order of the data, it has to be this way.

Is it possible to flip the draw order of a sunburst chart in D3? I've been experimenting with the sort function... it flipped the sort order of a tree root chart as expected... but doesn't seem to affect the sort order of a sunburst chart.

This is the sunburst chart I am working with (from here):

    // JSON data
    var nodeData = {
        "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}]
        }]
    };

    // Variables
    var width = 500;
    var height = 500;
    var radius = Math.min(width, height) / 2;
    var color = d3.scaleOrdinal(d3.schemeCategory20b);

    // Create primary <g> element
    var g = d3.select('svg')
        .attr('width', width)
        .attr('height', height)
        .append('g')
        .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');

    // Data strucure
    var partition = d3.partition()
        .size([2 * Math.PI, radius]);

    // Find data root
    var root = d3.hierarchy(nodeData)
        .sum(function (d) { return d.size});

    // Size arcs
    partition(root);
    var arc = d3.arc()
        .startAngle(function (d) { return d.x0 })
        .endAngle(function (d) { return d.x1 })
        .innerRadius(function (d) { return d.y0 })
        .outerRadius(function (d) { return d.y1 });

    // Put it all together
    g.selectAll('path')
        .data(root.descendants())
        .enter().append('path')
        .attr("display", function (d) { return d.depth ? null : "none"; })
        .attr("d", arc)
        .style('stroke', '#fff')
        .style("fill", function (d) { return color((d.children ? d : d.parent).data.name); });
<script src="https://d3js.org/d3.v4.min.js"></script>
 <svg></svg>
 

Many thanx! jB

1
Yes, it is possible. The ultimate form this takes depends on your code. It could be as easy as swapping the values of the range of the scale that determines arc radii (simple example based off of this, not modified for zooming). But without sharing your code it will be very difficult to provide a specific solution.Andrew Reid
Andrew... Thanx so much for your help. Right now, I'm experimenting with a sample Sunburst build found here... bl.ocks.org/denjn5/e1cdbbe586ac31747b4a304f8f86efa5 I'm working on a proof-of-concept to sell the graphic stakeholders. I'll likely link up with coder to build the final version... my coding skills are a lil rusty. ;-)jboissy

1 Answers

2
votes

This should be pretty straight forward, the inner and outer radius of each arc need to be modified. In the original they are set here:

var arc = d3.arc()
    .startAngle(function (d) { return d.x0 })
    .endAngle(function (d) { return d.x1 })
    .innerRadius(function (d) { return d.y0 })
    .outerRadius(function (d) { return d.y1 });

Rather than take d.y0 and d.y1 as the inner and outer radii, we need to take those values and subtract them from the ultimate radius of the sunburst. The radius is found when scaling the sunburst's size:

var partition = d3.partition()
    .size([2 * Math.PI, radius]);

So, we can use:

var arc = d3.arc()
    .startAngle(function (d) { return d.x0 })
    .endAngle(function (d) { return d.x1 })
    .innerRadius(function (d) { return radius - d.y1 })
    .outerRadius(function (d) { return radius - d.y0 });

This places parents to the outside of their children.

I swapped d.y1 and d.y0 as d.y0 refers to the smaller number it is the inner radius when moving outwards normally, but with the returned value equal to radius-d.y0, it is now used to define the outer radius

Here's an example:

    // JSON data
    var nodeData = {
        "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}]
        }]
    };

    // Variables
    var width = 500;
    var height = 500;
    var radius = Math.min(width, height) / 2;
    var color = d3.scaleOrdinal(d3.schemeCategory20b);

    // Create primary <g> element
    var g = d3.select('svg')
        .attr('width', width)
        .attr('height', height)
        .append('g')
        .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');

    // Data strucure
    var partition = d3.partition()
        .size([2 * Math.PI, radius]);

    // Find data root
    var root = d3.hierarchy(nodeData)
        .sum(function (d) { return d.size});

    // Size arcs
    partition(root);

    var arc = d3.arc()
        .startAngle(function (d) { return d.x0 })
        .endAngle(function (d) { return d.x1 })
        .innerRadius(function (d) { return radius - d.y1; })
        .outerRadius(function (d) { return radius - d.y0; });

    // Put it all together
    g.selectAll('path')
        .data(root.descendants())
        .enter().append('path')
        .attr("d", arc)
        .style('stroke', '#fff')
        .style("fill", function (d) { return color((d.children ? d : d.parent).data.name); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg></svg>

Note, I'm displaying the root node here, in the original it isn't shown (creating a donut). If you want to keep the donut effect, we can continue to hide the root, but add some padding to the radii values:

  // JSON data
    var nodeData = {
        "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}]
        }]
    };

    // Variables
    var width = 500;
    var height = 500;
    var radius = Math.min(width, height) / 2;
    var color = d3.scaleOrdinal(d3.schemeCategory20b);

    // Create primary <g> element
    var g = d3.select('svg')
        .attr('width', width)
        .attr('height', height)
        .append('g')
        .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');

    // Data strucure
    var partition = d3.partition()
        .size([2 * Math.PI, radius]);

    // Find data root
    var root = d3.hierarchy(nodeData)
        .sum(function (d) { return d.size});

    // Size arcs
    partition(root);
    var arc = d3.arc()
        .startAngle(function (d) { return d.x0 })
        .endAngle(function (d) { return d.x1 })
        .innerRadius(function (d) { return radius - d.y1 + root.y1; })
        .outerRadius(function (d) { return radius - d.y0 + root.y1; });

    // Put it all together
    g.selectAll('path')
        .data(root.descendants())
        .enter().append('path')
        .attr("display", function (d) { return d.depth ? null : "none"; })
        .attr("d", arc)
        .style('stroke', '#fff')
        .style("fill", function (d) { return color((d.children ? d : d.parent).data.name); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg></svg>

Here I'm just adding the root node's outer radius (which is the width of the arc, as the inner radius is 0) to the radii values. Note that if you want to maintain the donut and show the root node, you'll need to adjust the sizing of the sunburst diagram appropriately. Though I am more inclined to call this a sun implosion diagram.