Last active
June 26, 2021 18:02
-
-
Save ephys/27c1755348a15e0e1928c519afe9b6f3 to your computer and use it in GitHub Desktop.
Donuts! With SVG! And they're lightweight!
This file contains hidden or 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
export function getSvgCircleTotalLength(svgCircle: SVGCircleElement) { | |
// WORKAROUND - iOS (tested on safari 13): | |
// getTotalLength always returns 0 on circle SVGs, unless observed through the inspector. | |
// if (svgCircle.getTotalLength) { | |
// return svgCircle.getTotalLength(); | |
// } | |
return 2 * Math.PI * Number(svgCircle.getAttribute('r')); | |
} |
This file contains hidden or 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
.svg { | |
transform: rotate(90deg) scaleY(-1); | |
} | |
.circle { | |
transform: scaleX(-1); | |
transform-origin: center; | |
} |
This file contains hidden or 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
import classes from 'classnames'; | |
import { createRef, useState, useEffect, useLayoutEffect } from 'react'; | |
import { getSvgCircleTotalLength } from '../../utils/dom-utils'; | |
import css from './donut.module.scss'; | |
type Props = { | |
progress: number, | |
size: number, | |
strokeWidth?: number, | |
className?: string, | |
transition?: string, | |
background?: string, | |
foreground?: string, | |
animateInitial?: boolean, | |
}; | |
export default function Donut(props: Props) { | |
const { size, strokeWidth, transition, background, foreground = 'currentColor', animateInitial = false } = props; | |
let { progress } = props; | |
if (progress > 1 || progress < 0) { | |
if (process.env.NODE_ENV !== 'production') { | |
console.error('Donut has invalid % :', progress); | |
} else { | |
progress = Math.max(Math.min(progress, 1), 0); | |
} | |
} | |
const pathRef = createRef<SVGCircleElement>(); | |
const [pathLength, setPathLength] = useState(null); | |
const [mayTransition, setMayTransition] = useState(false); | |
const [showActualProgress, setShowActualProgress] = useState(!animateInitial); | |
useLayoutEffect(() => { | |
setPathLength(getSvgCircleTotalLength(pathRef.current)); | |
}, [pathRef]); | |
useEffect(() => { | |
if (pathLength && !mayTransition) { | |
setTimeout(() => { | |
setMayTransition(true); | |
}, 1); | |
} | |
if (mayTransition && animateInitial) { | |
setTimeout(() => { | |
setShowActualProgress(true); | |
}, 1); | |
} | |
}, [pathLength, mayTransition, animateInitial]); // don't want to re-run when mayTransition changes | |
const visualProgress = animateInitial | |
? (showActualProgress ? progress : 0) | |
: progress; | |
return ( | |
<svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox={`0 0 ${size} ${size}`} className={classes(props.className, css.svg)}> | |
{background && ( | |
<circle | |
strokeLinecap="round" | |
stroke={background} | |
strokeWidth={strokeWidth} | |
cx={size / 2} | |
cy={size / 2} | |
r={(size - strokeWidth) / 2} | |
fill="none" | |
fillRule="evenodd" | |
className={css.circle} | |
/> | |
)} | |
<circle | |
strokeLinecap="round" | |
stroke={foreground} | |
strokeWidth={strokeWidth} | |
cx={size / 2} | |
cy={size / 2} | |
r={(size - strokeWidth) / 2} | |
fill="none" | |
fillRule="evenodd" | |
ref={pathRef} | |
strokeDasharray={pathLength} | |
strokeDashoffset={pathLength - (pathLength * visualProgress)} | |
className={css.circle} | |
style={{ | |
transition: !mayTransition ? '' : `stroke-dashoffset ${transition}`, | |
}} | |
/> | |
</svg> | |
); | |
} | |
Donut.defaultProps = { | |
strokeWidth: 3, | |
transition: '0.5s ease', | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Explainer
We used to use highcharts for our donut charts but that library is too heavy, the generated SVG was too complex, and it cause a lot of performance issues.
This implementation generates Donut SVGs that can be animated and transitioned very easily, while being very lightweight.