123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- import {SerializedStyles} from '@emotion/react';
- import styled from '@emotion/styled';
- import {AnimatePresence, motion} from 'framer-motion';
- import testableTransition from 'sentry/utils/testableTransition';
- import theme, {Theme} from 'sentry/utils/theme';
- type TextProps = {
- percent: number;
- theme: Theme;
- textCss?: Props['textCss'];
- };
- type Props = React.HTMLAttributes<SVGSVGElement> & {
- value: number;
- /**
- * Apply a micro animation when the text value changes
- */
- animateText?: boolean;
- /**
- * The color of the ring background
- */
- backgroundColor?: string;
- /**
- * The width of the progress ring bar
- */
- barWidth?: number;
- maxValue?: number;
- minValue?: number;
- /**
- * The color of the ring bar. A function may be provided to compute the color
- * based on the percent value filled of the progress bar.
- */
- progressColor?: string;
- /**
- * Endcaps on the progress bar
- */
- progressEndcaps?: React.SVGAttributes<SVGCircleElement>['strokeLinecap'];
- size?: number;
- /**
- * Text to display in the center of the ring
- */
- text?: React.ReactNode;
- /**
- * The css to apply to the center text. A function may be provided to compute
- * styles based on the state of the progress bar.
- */
- textCss?: (p: TextProps) => SerializedStyles;
- };
- const Text = styled('div')<Omit<TextProps, 'theme'>>`
- position: absolute;
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100%;
- width: 100%;
- color: ${p => p.theme.chartLabel};
- font-size: ${p => p.theme.fontSizeExtraSmall};
- transition: color 100ms;
- ${p => p.textCss && p.textCss(p)}
- `;
- const AnimatedText = motion(Text);
- AnimatedText.defaultProps = {
- initial: {opacity: 0, y: -10},
- animate: {opacity: 1, y: 0},
- exit: {opacity: 0, y: 10},
- transition: testableTransition(),
- };
- const ProgressRing = ({
- value,
- minValue = 0,
- maxValue = 100,
- size = 20,
- barWidth = 3,
- text,
- textCss,
- animateText = false,
- progressColor = theme.green300,
- backgroundColor = theme.gray200,
- progressEndcaps,
- ...p
- }: Props) => {
- const radius = size / 2 - barWidth / 2;
- const circumference = 2 * Math.PI * radius;
- const boundedValue = Math.min(Math.max(value, minValue), maxValue);
- const progress = (boundedValue - minValue) / (maxValue - minValue);
- const percent = progress * 100;
- const progressOffset = (1 - progress) * circumference;
- const TextComponent = animateText ? AnimatedText : Text;
- let textNode = (
- <TextComponent key={text?.toString()} {...{textCss, percent}}>
- {text}
- </TextComponent>
- );
- textNode = animateText ? (
- <AnimatePresence initial={false}>{textNode}</AnimatePresence>
- ) : (
- textNode
- );
- return (
- <RingSvg
- role="img"
- height={radius * 2 + barWidth}
- width={radius * 2 + barWidth}
- {...p}
- >
- <RingBackground
- r={radius}
- barWidth={barWidth}
- cx={radius + barWidth / 2}
- cy={radius + barWidth / 2}
- color={backgroundColor}
- />
- <RingBar
- strokeDashoffset={progressOffset}
- strokeLinecap={progressEndcaps}
- circumference={circumference}
- r={radius}
- barWidth={barWidth}
- cx={radius + barWidth / 2}
- cy={radius + barWidth / 2}
- color={progressColor}
- />
- <foreignObject height="100%" width="100%">
- {text !== undefined && textNode}
- </foreignObject>
- </RingSvg>
- );
- };
- const RingSvg = styled('svg')`
- position: relative;
- `;
- const RingBackground = styled('circle')<{barWidth: number; color: string}>`
- fill: none;
- stroke: ${p => p.color};
- stroke-width: ${p => p.barWidth}px;
- transition: stroke 100ms;
- `;
- const RingBar = styled('circle')<{
- barWidth: number;
- circumference: number;
- color: string;
- }>`
- fill: none;
- stroke: ${p => p.color};
- stroke-width: ${p => p.barWidth}px;
- stroke-dasharray: ${p => p.circumference} ${p => p.circumference};
- transform: rotate(-90deg);
- transform-origin: 50% 50%;
- transition: stroke-dashoffset 200ms, stroke 100ms;
- `;
- export default ProgressRing;
- // We export components to allow for css selectors
- export {RingBackground, RingBar, Text as RingText};
|