Skip to content

Instantly share code, notes, and snippets.

@leefsmp
Last active January 9, 2018 05:23
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 leefsmp/e5c111624d67149d276267441acf39de to your computer and use it in GitHub Desktop.
Save leefsmp/e5c111624d67149d276267441acf39de to your computer and use it in GitHub Desktop.
A responsive PieChart in pure React + SVG
/////////////////////////////////////////////////////////
// ReactPie: a responsive PieChart in pure React + SVG
// by Philippe Leefsma, Jan 2017
//
/////////////////////////////////////////////////////////
import PieSegment from './ReactPieSegment'
import ReactTooltip from 'react-tooltip'
import Measure from 'react-measure'
import PropTypes from 'prop-types'
import Stopwatch from 'Stopwatch'
import easing from 'easing-js'
import React from 'react'
import './ReactPie.scss'
export default class ReactPie extends React.Component {
/////////////////////////////////////////////////////////
// Defines a few default properties
//
/////////////////////////////////////////////////////////
static defaultProps = {
innerRadius: 0.35,
outerRadius: 0.90,
fillOpacity: 0.95,
strokeWidth: 1.0,
data: []
}
/////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////
constructor (props) {
super (props)
this.disableTooltip = this.disableTooltip.bind(this)
const size = { width:0, height:0 }
const segments = this.resizeSegments(
this.loadSegments(props.data),
size)
this.state = {
tooltipActive: false,
tooltip: '',
segments,
size
}
}
/////////////////////////////////////////////////////////
// Upon properties change, reload the pie only if
// dataGuid has changed. This is controlled by the
// parent component
//
/////////////////////////////////////////////////////////
componentWillReceiveProps (props) {
if (props.dataGuid !== this.props.dataGuid) {
const segments = this.resizeSegments(
this.loadSegments(props.data),
this.state.size)
this.animate ({
onUpdate: (tween) => {
const stepSegments = segments.map((segment) => {
return Object.assign({}, segment, {
delta: segment.delta * tween
})
})
this.setState(Object.assign({},
this.state, {
segments: stepSegments
}))
},
duration: 850
})
}
}
/////////////////////////////////////////////////////////
// Loads pie segments
//
/////////////////////////////////////////////////////////
loadSegments (data) {
const total = data.reduce((res, entry) => {
return entry.value + res
}, 0)
let startAcc = 0.0
return data.map((entry, index) => {
const delta = (2 * Math.PI * entry.value/total)
const label = Math.floor(100 * entry.value/total) + '%'
const strokeColor = entry.strokeColor ||
this.props.strokeColor ||
entry.color
const key = entry.key || this.guid()
const start = startAcc
startAcc += delta
const dir = {
x: Math.cos(start + 0.5 * delta),
y: Math.sin(start + 0.5 * delta)
}
return {
delta: delta - 0.25 * Math.PI/180,
fillColor: entry.color,
value: entry.value,
color: entry.color,
expandTween: 0.0,
expanded: false,
strokeColor,
label,
index,
start,
dir,
key
}
})
}
/////////////////////////////////////////////////////////
// Resize pie segments after parent container
// has been resized for example
//
/////////////////////////////////////////////////////////
resizeSegments (segments, size) {
const { innerRadius, outerRadius } = this.props
const { width, height } = size
const outerRadiusPx =
0.5 * outerRadius * Math.min(
width, height)
const innerRadiusPx =
0.5 * innerRadius * Math.min(
width, height)
const centre = {
y: height * 0.5,
x: width * 0.5
}
return segments.map((segment) => {
return Object.assign({}, segment, {
rOut: outerRadiusPx,
rIn: innerRadiusPx,
centre
})
})
}
/////////////////////////////////////////////////////////
// Generates a guid
//
/////////////////////////////////////////////////////////
guid (format = 'xxx-xxx-xxx') {
var d = new Date().getTime()
const guid = format.replace(
/[xy]/g,
function (c) {
var r = (d + Math.random() * 16) % 16 | 0
d = Math.floor(d / 16)
return (c == 'x' ? r : (r & 0x7 | 0x8)).toString(16)
})
return guid
}
/////////////////////////////////////////////////////////
// Animation: {
// onUpdate,
// duration,
// easing
// }
//
/////////////////////////////////////////////////////////
animate ({onUpdate, duration, easing}) {
const stopwatch = new Stopwatch()
let dt = 0.0
const animationStep = () => {
dt += stopwatch.getElapsedMs()
if (dt > duration) {
onUpdate(1.0)
} else {
const param = dt/duration
const animParam = easing
? easing(param, duration/1000)
: param
onUpdate(animParam)
}
if (dt < duration) {
requestAnimationFrame(animationStep)
}
}
animationStep()
}
/////////////////////////////////////////////////////////
// Mouse over segment handler
//
/////////////////////////////////////////////////////////
onSegmentMouseOver (e, segment) {
const entry = this.props.data[segment.index]
this.setState(Object.assign({},
this.state, {
tooltipActive: true,
tooltip: entry.label
}))
if (this.props.onSegmentMouseOver) {
this.props.onSegmentMouseOver(entry)
}
}
/////////////////////////////////////////////////////////
// Segment clicked handler
//
/////////////////////////////////////////////////////////
onSegmentClicked (e, segment) {
let expandOutIdx = -1
let expandInIdx = -1
const segments = this.state.segments.map((s, idx) => {
const nextExpanded = (s.key === segment.key && !segment.expanded)
if (s.expanded !== nextExpanded) {
nextExpanded
? expandOutIdx = idx
: expandInIdx = idx
}
return(Object.assign({}, s, {
expanded: nextExpanded
}))
})
this.animate({
onUpdate: (tween) => {
if (expandInIdx > -1) {
segments[expandInIdx].expandTween = 1.0 - tween
}
if (expandOutIdx > -1) {
segments[expandOutIdx].expandTween = tween
}
this.setState(Object.assign({},
this.state, {
segments
}))
},
easing: (t, duration) => {
return easing.easeOutElastic(
t, 0, 1, duration)
},
duration: 750
})
if (this.props.onSegmentClicked) {
const entry = this.props.data[segment.index]
this.props.onSegmentClicked(
entry, segment.expanded)
}
}
/////////////////////////////////////////////////////////
// Expands a segment
//
/////////////////////////////////////////////////////////
expandSegment (segment) {
const offsetRad = Math.PI/180 * 1.2 * segment.expandTween
const offset = segment.rOut * 0.05 * segment.expandTween
const centre = {
x: segment.centre.x + segment.dir.x * offset,
y: segment.centre.y + segment.dir.y * offset
}
return Object.assign({}, segment, {
delta: segment.delta - 2 * offsetRad,
start: segment.start + offsetRad,
rOut: segment.rOut + offset,
rIn: segment.rIn + offset,
centre: centre
})
}
/////////////////////////////////////////////////////////
// Disable tooltip
//
/////////////////////////////////////////////////////////
disableTooltip () {
this.setState(Object.assign({},
this.state, {
tooltipActive: false
}))
}
/////////////////////////////////////////////////////////
// React render
//
/////////////////////////////////////////////////////////
render () {
const segments = this.state.segments.map((segment) => {
const onSegmentMouseOver = (e) => {
this.onSegmentMouseOver(e, segment)
e.stopPropagation()
}
const onSegmentClicked = (e) => {
this.onSegmentClicked(e, segment)
}
segment = segment.expandTween
? this.expandSegment(segment)
: segment
return (
<PieSegment
strokeColor={segment.strokeColor}
onMouseOver={onSegmentMouseOver}
fillColor={segment.fillColor}
onClick={onSegmentClicked}
centre={segment.centre}
delta={segment.delta}
start={segment.start}
label={segment.label}
rOut={segment.rOut}
rIn={segment.rIn}
key={segment.key}
/>
)
})
const tooltipCls = this.state.tooltipActive
? 'react-pie-tooltip-container active'
: 'react-pie-tooltip-container'
return (
<Measure bounds onResize={(rect) => {
const size = {
height: rect.bounds.height,
width: rect.bounds.width
}
const segments = this.resizeSegments(
this.state.segments, size)
this.setState(Object.assign({},
this.state, {
segments,
size
}))
}}>
{
({ measureRef }) =>
<div ref={measureRef} className="react-pie">
<div className="react-pie-inner"
onMouseMove={this.disableTooltip}
data-for="react-pie-tooltip"
data-tip=''>
<svg>
{segments}
</svg>
</div>
<div className={tooltipCls}>
<ReactTooltip
getContent={[() => <div>{this.state.tooltip}</div>]}
className="react-pie-tooltip"
id="react-pie-tooltip"
effect="float"
/>
</div>
</div>
}
</Measure>
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment