Skip to content

Instantly share code, notes, and snippets.

@edgarberm
Created June 19, 2019 12:35
Show Gist options
  • Save edgarberm/a3c8c7a9301134959b710ac966d5665e to your computer and use it in GitHub Desktop.
Save edgarberm/a3c8c7a9301134959b710ac966d5665e to your computer and use it in GitHub Desktop.
d3js (v5) donut chart
<!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