Skip to content

Instantly share code, notes, and snippets.

@ephys
Last active June 26, 2021 18:02
Show Gist options
  • Save ephys/27c1755348a15e0e1928c519afe9b6f3 to your computer and use it in GitHub Desktop.
Save ephys/27c1755348a15e0e1928c519afe9b6f3 to your computer and use it in GitHub Desktop.
Donuts! With SVG! And they're lightweight!
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'));
}
.svg {
transform: rotate(90deg) scaleY(-1);
}
.circle {
transform: scaleX(-1);
transform-origin: center;
}
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',
};
@ephys
Copy link
Author

ephys commented Jun 26, 2021

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.

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment