Skip to content

Instantly share code, notes, and snippets.

@volodalexey
Forked from mbostock/.block
Last active July 26, 2018 07:10
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 volodalexey/1b3a0b505b8fc4dc844979a540b63a42 to your computer and use it in GitHub Desktop.
Save volodalexey/1b3a0b505b8fc4dc844979a540b63a42 to your computer and use it in GitHub Desktop.
D3 v5 ES6 Donut/Pie Chart (optimized for React or Vue)

Donut/Pie Chart Forked from https://gist.github.com/mbostock/3887193

optimized for React or Vue

It means you can invoke renderChart() as many times as you wish.

E.g. in React:

import React, { Component } from 'react'
import { renderChart, destroyChart } from '...'

class Chart extends Component {
  ...
  render () {
    return <div ref={(el) => this.$wrapper = el}/>
  }
  
  componentDidUpdate () {
    renderChart(this.$wrapper, this.props.data)
  }
  
  componentWillUnmount () {
    destroyChart(this.$wrapper)
  }
  ...
}

E.g. in Vue:

<template>
  <div ref="$wrapper"></div>
</template>

<script>
import { renderChart, destroyChart } from '...'

export default {
  name: 'chart',
  ...
  watch: {
    data () {
      renderChart(this.$refs.$wrapper, this.data)
    }
  },
  beforeDestroy () {
    destroyChart(this.$refs.$wrapper)
  }
  ...
}
</script>
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<title>D3 v5 ES6 Donut/Pie Chart (optimized for React or Vue)</title>
<script src='//d3js.org/d3.v5.min.js'></script>
</head>
<body>
<div id='wrapper'></div>
<script>
function renderChart (wrapper, curData) {
if (!wrapper) {
return
}
const { select: d3Select, pie: d3Pie,
arc: d3Arc, interpolate: d3Interpolate,
easeBounce: d3EaseBounce, easeElastic: d3EaseElastic,
} = d3
const width = 500
const height = 500
const DURATION = 1500
// https://groups.google.com/forum/#!topic/d3-js/Rlv0O8xsFhs
const svgData = d3Select(wrapper).selectAll('svg').data(['dummy data'])
const svgEnter = svgData.enter().append('svg')
svgEnter.attr('width', width)
svgEnter.attr('height', height)
svgEnter.append('g')
.attr('transform', `translate(${width / 2},${height / 2})`)
.attr('class', 'donut-chart')
const svgMerge = svgData.merge(svgEnter)
const gMerge = svgMerge.select('g.donut-chart')
const pieChart = d3Pie().sort(null).value(pd => pd.value) // by default, data sorts in descending value. this will mess with our animation so we set it to null
const radius = Math.min(width, height) / 2
const arcPath = d3Arc()
.outerRadius(radius - radius * 0.1)
.innerRadius(radius - radius * 0.5)
const arcLabel = d3Arc()
.outerRadius(radius - radius * 0.1)
.innerRadius(radius - radius * 0.4)
const pieData = gMerge.selectAll('.pie-slice').data(pieChart(curData))
const pieEnter = pieData.enter()
.append('g')
.attr('class', 'pie-slice')
pieEnter.append('path')
.attr('fill', d => d.data.fill)
.attr('d', arcPath)
.each(function () {
this._current = { startAngle: 0, endAngle: 0 }
})
pieEnter.append('g')
.attr('class', 'label-text-wrapper')
.append('text')
.text(d => d.data.label)
const pieMerge = pieData.merge(pieEnter)
pieMerge.select('.label-text-wrapper')
.transition()
.duration( DURATION )
.attr('transform', (d) => `translate(${arcLabel.centroid(d)})`)
pieMerge.select('text')
.transition()
.duration( DURATION )
.tween('text', function (d) {
const textI = d3Interpolate(this.textContent, d.data.label)
return (t) => {
this.textContent = textI(t)
}
})
pieMerge.select('path') // https://codepen.io/stefanjudis/pen/gkHwJ
.transition()
.duration( DURATION )
.attrTween('d', function (d) {
const _interpolate = d3Interpolate( this._current, d )
this._current = _interpolate( 0 )
return (t) => {
return arcPath( _interpolate( t ) )
}
})
const pathAnimate = (path, dir) => { // http://bl.ocks.org/erichoco/6694616
switch(dir) {
case 0:
path.transition()
.duration(DURATION / 2)
.ease(d3EaseBounce)
.attr('d', d3Arc()
.outerRadius(radius - radius * 0.1)
.innerRadius(radius - radius * 0.5)
)
break;
case 1:
path.transition()
.duration(DURATION / 2)
.ease(d3EaseElastic)
.attr('d', d3Arc()
.outerRadius(radius - radius * 0.05)
.innerRadius(radius - radius * 0.6)
)
break;
}
}
const tooltipData = d3Select(wrapper)
.selectAll('div').data(['dummy data'])
const tooltipEnter = tooltipData.enter()
.append('div')
.attr('class', 'tooltip')
.style('background', '#ffffff')
.style('color', '#000000')
.style('display', 'none')
.style('top', 0)
.style('left', 0)
.style('padding', '10px')
.style('position', 'absolute')
const tooltipMerge = tooltipData.merge(tooltipEnter)
// https://codepen.io/lisaofalltrades/pen/jZyzKo
pieMerge.on('mouseover', (d, index) => {
tooltipMerge
.text(d.data.tooltip)
.style('box-shadow', `0 0 5px ${curData[index].fill}`)
.style('display', 'block')
pathAnimate(d3Select(d3.event.target), 1)
})
pieMerge.on('mousemove', () => {
tooltipMerge
.style('top', (d3.event.layerY + 10) + 'px') // always 10px below the cursor
.style('left', (d3.event.layerX + 10) + 'px'); // always 10px to the right of the mouse
})
pieMerge.on('mouseout', (d, index) => {
tooltipMerge
.style('display', 'none')
pathAnimate(d3Select(d3.event.fromElement), 0)
})
pieMerge.on('click', (d) => {
console.log('clicked!', d)
})
}
function destroyChart (wrapper) {
const {select: d3Select} = d3
d3Select(wrapper).selectAll('*').remove()
}
document.addEventListener('DOMContentLoaded', () => {
setInterval(() => {
const random1 = Math.round(Math.random()*100)
const random2 = Math.round(Math.random()*(100 - random1))
const random3 = Math.round(Math.random()*(100 - random1 - random2))
const random4 = 100 - random1 - random2 - random3
renderChart(document.querySelector('#wrapper'), [
{ fill: '#d78dcb', value: random1, label: `${random1}%`, tooltip: `${random1}% Tooltip` },
{ fill: '#8abe6e', value: random2, label: `${random2}%`, tooltip: `${random2}% Tooltip` },
{ fill: '#5a98d5', value: random3, label: `${random3}%`, tooltip: `${random3}% Tooltip` },
{ fill: '#858585', value: random4, label: `${random4}%`, tooltip: `${random4}% Tooltip` },
])
}, 5000)
const random1 = Math.round(Math.random()*100)
const random2 = Math.round(Math.random()*(100 - random1))
const random3 = Math.round(Math.random()*(100 - random1 - random2))
const random4 = 100 - random1 - random2 - random3
renderChart(document.querySelector('#wrapper'), [
{ fill: '#d78dcb', value: random1, label: `${random1}%`, tooltip: `${random1}% Tooltip` },
{ fill: '#8abe6e', value: random2, label: `${random2}%`, tooltip: `${random2}% Tooltip` },
{ fill: '#5a98d5', value: random3, label: `${random3}%`, tooltip: `${random3}% Tooltip` },
{ fill: '#858585', value: random4, label: `${random4}%`, tooltip: `${random4}% Tooltip` },
])
})
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment