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