Search code examples
javascripthtmld3.jssunburst-diagram

How to add colours to D3 js sunburst diagram from JSON-file?


I created a sunburst diagram based on this JSON file. (existing quetions considered)

The first few lines look like this:

{
   "name": "A", "children": [
      {
         "name": "B",
         "color": "#A9DFBF",
         "children": [
            {
               "name": "C",
               "color": "#F9E79F",
               "children": [
       ....
       ....
       ....

I am trying to use the colours from the JSON file in the sunburst: in this example everything has to be green besides one yellow arc. Instead my code (s. below) gives me the following sunburst visualizations with the two alternatives I tried:

Alternative 1 of the relvant part gives: enter image description here Alternative 2 of the relevant part (search for "alternative") gives: enter image description here

Would appriciate any help on this.

Relevant part of code:

    newSlice.append('path')
        .attr('class', 'main-arc')
        //Alternative 1
        //.style('fill', d => color((d.children ? d : d.parent).data.name))
        //Alternative 2
        .style('fill', function (d) { return color(d.color);})
        .attr('d', arc);

Full code:

<head>
    <style>
        body {
            font-family: Sans-serif;
            font-size: 11px;
        }

        .slice {
            cursor: pointer;
        }

        .slice .main-arc {
            stroke: #fff;
            stroke-width: 1px;
        }

        .slice .hidden-arc {
            fill: none;
        }

        .slice text {
            pointer-events: none;
            dominant-baseline: middle;
            text-anchor: middle;
        }
    </style>

    <script src='https://d3js.org/d3.v4.min.js'></script>
</head>
<body>
    <script>
        const width = window.innerWidth,
            height = window.innerHeight,
            maxRadius = (Math.min(width, height) / 2) - 5;

        const formatNumber = d3.format(',d');

        const x = d3.scaleLinear()
            .range([0, 2 * Math.PI])
            .clamp(true);

        const y = d3.scaleSqrt()
            .range([maxRadius*.1, maxRadius]);

        const color = d3.scaleOrdinal(d3.schemeCategory20);

        const partition = d3.partition();

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

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

        const svg = d3.select('body').append('svg')
            .style('width', '100vw')
            .style('height', '100vh')
            .attr('viewBox', `${-width / 2} ${-height / 2} ${width} ${height}`)
            .on('click', () => focusOn()); // Reset zoom on canvas click

        d3.json('https://raw.githubusercontent.com/graphineer/viz/master/flare-3.json', (error, root) => {
            if (error) throw error;

            root = d3.hierarchy(root);
            root.sum(d => d.size);

            const slice = svg.selectAll('g.slice')
                .data(partition(root).descendants());

            slice.exit().remove();

            const newSlice = slice.enter()
                .append('g').attr('class', 'slice')
                .on('click', d => {
                    d3.event.stopPropagation();
                    focusOn(d);
                });

            newSlice.append('title')
                .text(d => d.data.name + '\n' + formatNumber(d.value));

            newSlice.append('path')
                .attr('class', 'main-arc')
                //Alternative 1
                //.style('fill', d => color((d.children ? d : d.parent).data.name))
                //Alternative 2
                .style('fill', function (d) { return color(d.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');

            // Add white contour
            text.append('textPath')
                .attr('startOffset','50%')
                .attr('xlink:href', (_, i) => `#hiddenArc${i}` )
                .text(d => d.data.name)
                .style('fill', 'none')
                .style('stroke', '#fff')
                .style('stroke-width', 5)
                .style('stroke-linejoin', 'round');

            text.append('textPath')
                .attr('startOffset','50%')
                .attr('xlink:href', (_, i) => `#hiddenArc${i}` )
                .text(d => d.data.name);
        });

        function focusOn(d = { x0: 0, x1: 1, y0: 0, y1: 1 }) {
            // Reset to top-level if no data point specified

            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('path.main-arc')
                .attrTween('d', d => () => arc(d));

            transition.selectAll('path.hidden-arc')
                .attrTween('d', d => () => middleArcLine(d));

            transition.selectAll('text')
                .attrTween('display', d => () => textFits(d) ? null : 'none');

            moveStackToFront(d);

            //

            function moveStackToFront(elD) {
                svg.selectAll('.slice').filter(d => d === elD)
                    .each(function(d) {
                        this.parentNode.appendChild(this);
                        if (d.parent) { moveStackToFront(d.parent); }
                    })
            }
        }
    </script>
</body>

Solution

  • Three observations:

    1. The property you want is inside the property data, created by the hierarchy generator;
    2. You don't need the color ordinal scale, since you already have the color hex value as the string;
    3. Your root node has no such color property, therefore, assign a specific color to it ("red" in the example below, using logical OR).

    So, it is:

        .style('fill', function(d) {
          return d.data.color || "red"
        })
    

    Here is your code with that change only:

    <head>
      <style>
        body {
          font-family: Sans-serif;
          font-size: 11px;
        }
    
        .slice {
          cursor: pointer;
        }
    
        .slice .main-arc {
          stroke: #fff;
          stroke-width: 1px;
        }
    
        .slice .hidden-arc {
          fill: none;
        }
    
        .slice text {
          pointer-events: none;
          dominant-baseline: middle;
          text-anchor: middle;
        }
    
      </style>
    
      <script src='https://d3js.org/d3.v4.min.js'></script>
    </head>
    
    <body>
      <script>
        const width = window.innerWidth,
          height = window.innerHeight,
          maxRadius = (Math.min(width, height) / 2) - 5;
    
        const formatNumber = d3.format(',d');
    
        const x = d3.scaleLinear()
          .range([0, 2 * Math.PI])
          .clamp(true);
    
        const y = d3.scaleSqrt()
          .range([maxRadius * .1, maxRadius]);
    
        const color = d3.scaleOrdinal(d3.schemeCategory20);
    
        const partition = d3.partition();
    
        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();
        };
    
        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;
        };
    
        const svg = d3.select('body').append('svg')
          .style('width', '100vw')
          .style('height', '100vh')
          .attr('viewBox', `${-width / 2} ${-height / 2} ${width} ${height}`)
          .on('click', () => focusOn()); // Reset zoom on canvas click
    
        d3.json('https://raw.githubusercontent.com/graphineer/viz/master/flare-3.json', (error, root) => {
          if (error) throw error;
    
          root = d3.hierarchy(root);
          root.sum(d => d.size);
    
          const slice = svg.selectAll('g.slice')
            .data(partition(root).descendants());
    
          slice.exit().remove();
    
          const newSlice = slice.enter()
            .append('g').attr('class', 'slice')
            .on('click', d => {
              d3.event.stopPropagation();
              focusOn(d);
            });
    
          newSlice.append('title')
            .text(d => d.data.name + '\n' + formatNumber(d.value));
    
          newSlice.append('path')
            .attr('class', 'main-arc')
            //Alternative 1
            //.style('fill', d => color((d.children ? d : d.parent).data.name))
            //Alternative 2
            .style('fill', function(d) {
              return d.data.color || "red"
            })
            .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');
    
          // Add white contour
          text.append('textPath')
            .attr('startOffset', '50%')
            .attr('xlink:href', (_, i) => `#hiddenArc${i}`)
            .text(d => d.data.name)
            .style('fill', 'none')
            .style('stroke', '#fff')
            .style('stroke-width', 5)
            .style('stroke-linejoin', 'round');
    
          text.append('textPath')
            .attr('startOffset', '50%')
            .attr('xlink:href', (_, i) => `#hiddenArc${i}`)
            .text(d => d.data.name);
        });
    
        function focusOn(d = {
          x0: 0,
          x1: 1,
          y0: 0,
          y1: 1
        }) {
          // Reset to top-level if no data point specified
    
          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('path.main-arc')
            .attrTween('d', d => () => arc(d));
    
          transition.selectAll('path.hidden-arc')
            .attrTween('d', d => () => middleArcLine(d));
    
          transition.selectAll('text')
            .attrTween('display', d => () => textFits(d) ? null : 'none');
    
          moveStackToFront(d);
    
          //
    
          function moveStackToFront(elD) {
            svg.selectAll('.slice').filter(d => d === elD)
              .each(function(d) {
                this.parentNode.appendChild(this);
                if (d.parent) {
                  moveStackToFront(d.parent);
                }
              })
          }
        }
    
      </script>
    </body>