Skip to content

Instantly share code, notes, and snippets.

@micahstubbs
Created October 9, 2017 18:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save micahstubbs/7c025e79f256e9325002f89430c87ea9 to your computer and use it in GitHub Desktop.
Save micahstubbs/7c025e79f256e9325002f89430c87ea9 to your computer and use it in GitHub Desktop.
zoomable sunburst with sunlight colors
border: no
height: 960
license: Apache-2.0
<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>
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);
}
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment