Created
June 19, 2019 12:35
-
-
Save edgarberm/a3c8c7a9301134959b710ac966d5665e to your computer and use it in GitHub Desktop.
d3js (v5) donut chart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge"> | |
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet"> | |
<title>Donut</title> | |
<style> | |
body { | |
font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; | |
margin: 0; | |
padding: 0; | |
-webkit-font-smoothing: antialiased; | |
} | |
.wrapper { | |
position: absolute; | |
width: 300px; | |
height: 200px; | |
border: 1px solid whitesmoke; | |
} | |
svg { | |
position: absolute; | |
left: 50%; | |
top: 50%; | |
transform: translate3d(-50%, -50%, 0); | |
} | |
g { | |
pointer-events: all; | |
} | |
.label { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate3d(-50%, -50%, 0); | |
font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; | |
text-align: center; | |
} | |
.label .label-title { | |
margin: 0; | |
font-size: 12px; | |
font-weight: normal; | |
line-height: 1.5; | |
color: #bababa; | |
text-align: center; | |
white-space: nowrap; | |
text-overflow: ellipsis; | |
overflow: hidden; | |
} | |
.label .label-value { | |
margin: 0; | |
font-size: 24px; | |
font-weight: bold; | |
line-height: 1.33; | |
color: #2c2c2c; | |
} | |
.tooltip { | |
display: flex; | |
flex-direction: row; | |
align-items: center; | |
justify-content: space-between; | |
position: absolute; | |
width: auto; | |
min-width: 136px; | |
height: 28px; | |
padding: 0 8px; | |
border-radius: 4px; | |
background-color: #2c2c2c; | |
transform: translateX(-50%); | |
opacity: 0; | |
pointer-events: none; | |
z-index: 5; | |
} | |
.tooltip:after { | |
content: ""; | |
position: absolute; | |
left: calc(50% - 4px); | |
bottom: -4px; | |
border-width: 4px 4px 0 4px; | |
border-color: #2c2c2c transparent transparent transparent; | |
border-style: solid; | |
} | |
.tooltip .name { | |
display: flex; | |
flex-direction: row; | |
align-items: center; | |
} | |
.tooltip .name p { | |
margin-right: 6px; | |
color: #bababa; | |
font-size: 12px; | |
line-height: 1.67; | |
white-space: nowrap; | |
} | |
.tooltip .name span { | |
width: 8px; | |
height: 8px; | |
margin-right: 8px; | |
border-radius: 2px; | |
} | |
.tooltip .value { | |
color: #ffffff; | |
font-size: 12px; | |
line-height: 1.67; | |
text-align: right; | |
} | |
</style> | |
</head> | |
<body> | |
<button class="one">One</button> | |
<button class="two">Two</button> | |
<div class="wrapper"></div> | |
<script src="https://d3js.org/d3.v5.min.js" crossorigin="anonymous"></script> | |
<script> | |
const jsons = [ | |
[ | |
{ id: '0', key: 'Category one', value: 50, color: '#80b622' }, | |
{ id: '1', key: 'Category two', value: 22, color: '#fdb32b' }, | |
{ id: '2', key: 'Category three', value: 17, color: '#f3522b' } | |
], | |
[ | |
{ id: '0', key: 'Category one', value: 80, color: '#80b622' }, | |
{ id: '1', key: 'Category two', value: 12, color: '#fdb32b' }, | |
{ id: '2', key: 'Category three', value: 58, color: '#f3522b' }, | |
{ id: '3', key: 'Category four', value: 30, color: '#e2e6e3' } | |
] | |
] | |
d3.selectAll('button').on('click', (event, i) => { | |
render(i) | |
}) | |
const wrapper = document.querySelector('.wrapper').getBoundingClientRect() | |
const arcSize = 16 | |
const padding = 20 | |
const labelValueMargin = 30 | |
const tooltipMargin = 40 | |
const width = wrapper.width | |
const height = wrapper.height | |
const radius = Math.min(width, height) | |
const center = radius / 2 | |
let inTransition = false | |
let labelTitle = '' | |
let labelValue = '' | |
let selected = null | |
const svg = d3.select('.wrapper') | |
.append('svg') | |
.attr('viewBox', `0 0 ${radius} ${radius}`) | |
.attr('width', `${radius}px`) | |
.attr('height', `${radius}px`) | |
.append('g') | |
.attr('transform', `translate(${radius / 2}, ${radius / 2})`) | |
const pie = d3.pie() | |
.value((d) => d.value) | |
.padAngle(0.01) | |
const arc = d3.arc() | |
.innerRadius((center - arcSize) - padding) | |
.outerRadius(center - padding) | |
const tooltip = d3.select('.wrapper') | |
.append('div') | |
.attr('class', 'tooltip') | |
const label = d3.select('.wrapper') | |
.append('div') | |
.attr('class', 'label') | |
label.append('p') | |
.attr('class', 'label-title') | |
.style('max-width', (center - arcSize) + 'px') | |
label.append('p') | |
.attr('class', 'label-value') | |
function render(index) { | |
clearDonut() | |
clearLabel() | |
renderDonut(index) | |
} | |
function clearDonut() { | |
svg.selectAll('.donut').remove() | |
} | |
function renderDonut(index) { | |
inTransition = true | |
selected = null | |
const donut = svg.append('g') | |
.attr('class', 'donut') | |
.selectAll('path') | |
.data(pie(jsons[index])) | |
.enter() | |
.append('path') | |
.attr('class', 'path') | |
.attr('d', arc) | |
.attr('fill', (d) => d.data.color) | |
.style('cursor', 'pointer') | |
const total = jsons[index].reduce((prev, curr) => prev + curr.value, 0) | |
labelTitle = 'Total' | |
labelValue = total | |
setLabel() | |
donut.on('click', function (d, i) { | |
if (inTransition) return | |
if (selected && selected === d) { | |
unselect() | |
} else { | |
selected = d | |
select(selected) | |
} | |
hideTooltip() | |
setLabel(d.data.key, d.data.value) | |
}) | |
donut.on('mouseover', function (d, i) { | |
if (inTransition || selected) return | |
showTooltip(d.data) | |
console.log(d3.select(this)); | |
d3.select(this) | |
.transition('arc-fill-in') | |
.duration(250) | |
.attr('fill', d3.color(d.data.color).darker(0.6)) | |
}) | |
donut.on('mousemove', function (d, i) { | |
if (inTransition || selected) return | |
moveTooltip(this) | |
}) | |
donut.on('mouseout', function (d, i) { | |
if (inTransition || selected) return | |
hideTooltip(d.data) | |
setLabel() | |
d3.select(this) | |
.transition('arc-fill-out') | |
.duration(250) | |
.attr('fill', d.data.color) | |
}) | |
donut.transition('enter-donut') | |
.duration(500) | |
.attrTween('d', (d) => { | |
const interpolate = d3.interpolate({ startAngle: 0, endAngle: 0 }, d) | |
return (t) => arc(interpolate(t)) | |
}) | |
.on('end', () => { inTransition = false }) | |
} | |
function select(selected) { | |
svg.selectAll('.donut path') | |
.transition('arc-fill-in-out') | |
.duration(250) | |
.attr('fill', (_d, _i) => { | |
if (selected === _d) { | |
return selected.data.color | |
} else { | |
return '#e2e6e3' | |
} | |
}) | |
} | |
function unselect() { | |
selected = null | |
svg.selectAll('.donut path') | |
.transition('arc-fill-in-out') | |
.duration(250) | |
.attr('fill', (d) => d.data.color) | |
} | |
function setLabel(key, value) { | |
label.select('.label-title').text(key ? key : labelTitle) | |
label.select('.label-value') | |
.transition() | |
.tween('text', function () { | |
const selection = d3.select(this) | |
const start = d3.select(this).text() | |
const end = value ? value : labelValue | |
const interpolator = d3.interpolateNumber(start, end) | |
return (t) => { | |
selection.text(Math.round(interpolator(t))) | |
} | |
}) | |
.duration(500) | |
} | |
function clearLabel() { | |
label.select('.label-title').text('') | |
label.select('.label-value').text('') | |
} | |
function showTooltip(item) { | |
tooltip.html(` | |
<div class="name"> | |
<span style="background-color: ${item.color};"></span> | |
<p>${item.key}</p> | |
</div> | |
<p class="value">${item.value}%</p> | |
`) | |
tooltip.style('left', d3.event.pageX + 'px') | |
.style('top', d3.event.pageY - tooltipMargin + 'px') | |
tooltip.transition('show-tooltip') | |
.duration(250) | |
.style('opacity', 1) | |
} | |
function moveTooltip(item) { | |
tooltip.style('left', (d3.event.pageX - wrapper.left) + 'px') | |
.style('top', (d3.event.pageY - wrapper.top - tooltipMargin) + 'px') | |
} | |
function hideTooltip() { | |
tooltip.style('opacity', 0) | |
} | |
render(0) | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment