block by micahstubbs 7c025e79f256e9325002f89430c87ea9

zoomable sunburst with sunlight colors

Full Screen

an iteration with a different color palette

here we construct a color scheme from the Sunlight Foundation Style Guide‘s network 30-color palette

a fork of Zoomable Sunburst with Labels from @vastur


Adapted from Mike Bostock’s Zoomable Sunburst to include arc labels.

Click on any arc to zoom in. Click on the center circle to zoom out. Click on canvas background to reset zoom.

index.html

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

        .slice {
            cursor: pointer;
        }

        .slice .main-arc {
            stroke: #E5E2E0;
            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 src='vis.js'></script>
</body>

vis.js

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 * 0.1, maxRadius]);

// sunlight style guide network colors
// https://github.com/amycesal/dataviz-style-guide/blob/master/Sunlight-StyleGuide-DataViz.pdf
const dark = [
  '#B08B12',
  '#BA5F06',
  '#8C3B00',
  '#6D191B',
  '#842854',
  '#5F7186',
  '#193556',
  '#137B80',
  '#144847',
  '#254E00'
];

const mid = [
  '#E3BA22',
  '#E58429',
  '#BD2D28',
  '#D15A86',
  '#8E6C8A',
  '#6B99A1',
  '#42A5B3',
  '#0F8C79',
  '#6BBBA1',
  '#5C8100'
];

const light = [
  '#F2DA57',
  '#F6B656',
  '#E25A42',
  '#DCBDCF',
  '#B396AD',
  '#B0CBDB',
  '#33B6D0',
  '#7ABFCC',
  '#C8D7A1',
  '#A0B700'
];

const palettes = [light, mid, dark];
const lightGreenFirstPalette = palettes
  .map(d => d.reverse())
  .reduce((a, b) => a.concat(b));

const color = d3.scaleOrdinal(lightGreenFirstPalette);

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://gist.githubusercontent.com/mbostock/4348373/raw/85f18ac90409caa5529b32156aa6e71cf985263f/flare.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')
      .style('fill', d => color((d.children ? d : d.parent).data.name))
      .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', '#E5E2E0')
      .style('stroke-width', 12)
      .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);
        }
      });
  }
}