react-native-segmented-round-display
Version:
react-native-segmented-round-display provides a simple ARC component, drawn with react-native-svg, it can have one or more segments and its easy configurable.
261 lines (231 loc) • 6.54 kB
JavaScript
import React, { useState, useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import Svg, { G, Text, Path, Rect } from "react-native-svg";
import { Spring } from "react-spring/renderprops-native";
const SegmentedRoundDisplay = ({
segments,
filledArcWidth,
emptyArcWidth,
arcSpacing,
totalArcSize,
emptyArcColor,
filledArcColor,
radius,
style,
animationDuration,
animated,
formatValue,
incompleteArcColor,
displayValue,
valueBoxColor,
valueFontColor,
}) => {
const [arcs, setArcs] = useState([]);
const totalArcs = segments.length;
const totalSpaces = totalArcs - 1;
const totalSpacing = totalSpaces * arcSpacing;
const arcSize = (totalArcSize - totalSpacing) / totalArcs;
const arcsStart = 90 - totalArcSize / 2;
const margin = 35;
const svgWidth = (radius + filledArcWidth) * 2 + 2 * margin;
const svgHeight = (radius + filledArcWidth) * 2 + 2 * margin;
const totalFilledValue = segments.reduce(
(acc, actual) => acc + actual.filled,
0
);
const createArcs = useCallback(() => {
const newArcs = segments.map((goal, index) => {
const newArc = {
centerX: radius + filledArcWidth + margin,
centerY: radius + filledArcWidth + margin,
start: arcsStart + index * arcSize,
end: arcsStart + arcSize + index * arcSize,
isComplete: goal.total === goal.filled,
};
if (index !== 0) {
newArc.start += arcSpacing * index;
newArc.end += arcSpacing * index;
}
newArc.filled = scaleValue(
goal.filled,
[0, goal.total],
[newArc.start, newArc.end]
);
return newArc;
});
setArcs(newArcs);
}, [segments, arcSize, arcSpacing, filledArcWidth, arcsStart, radius]);
const renderDisplayValue = (angle, value) => {
const arc = arcs[arcs.length - 1];
if (!arc) {
return <G></G>;
}
const pos = polarToCartesian(
arc.centerX,
arc.centerY,
radius,
(angle || arc.filled) + 3
);
const boxFinalPosition = {
x: pos.x - 40,
y: pos.y + 6,
};
const formatedValue = formatValue
? formatValue(value || totalFilledValue)
: parseInt(value || totalFilledValue, 10);
return (
<G>
<Rect
x={boxFinalPosition.x}
y={boxFinalPosition.y}
width="80"
height="25"
fill={valueBoxColor}
rx={3}
/>
<Rect
width="10"
height="10"
fill={valueBoxColor}
transform={`translate(${pos.x},${pos.y}) rotate(45)`}
rx={2}
/>
<Text
x={pos.x}
fontWeight="bold"
fontSize={14}
y={boxFinalPosition.y + 18}
fill={valueFontColor}
textAnchor="middle"
>
{formatedValue}
</Text>
</G>
);
};
useEffect(() => {
createArcs();
}, [segments, createArcs]);
if (arcs.length === 0) {
return <></>;
}
return (
<Svg width={svgWidth} height={svgHeight} style={style}>
{arcs.map((arc, index) => (
<G key={index.toString()}>
<Path
fill="none"
stroke={emptyArcColor}
strokeWidth={emptyArcWidth}
strokeLinecap="round"
d={drawArc(arc.centerX, arc.centerY, radius, arc.start, arc.end)}
/>
{animated && arc.filled > arc.start && (
<Spring
from={{ x: arc.start, y: 0 }}
to={{ x: arc.filled + 0.6, y: filledArcWidth }}
config={{
duration: animationDuration / totalArcs,
delay: (animationDuration / totalArcs) * index,
}}
>
{(props) => (
<Path
fill="none"
stroke={
arc.isComplete
? filledArcColor
: incompleteArcColor || filledArcColor
}
strokeWidth={props.y}
strokeLinecap="round"
d={drawArc(
arc.centerX,
arc.centerY,
radius,
arc.start,
props.x
)}
/>
)}
</Spring>
)}
{!animated && arc.filled > arc.start && (
<Path
fill="none"
stroke={
arc.isComplete
? filledArcColor
: incompleteArcColor || filledArcColor
}
strokeWidth={filledArcWidth}
strokeLinecap="round"
d={drawArc(
arc.centerX,
arc.centerY,
radius,
arc.start,
arc.filled
)}
/>
)}
</G>
))}
{displayValue && (
<G>
{!animated && renderDisplayValue()}
{animated && (
<Spring
from={{ x: arcsStart, value: 0 }}
to={{ x: arcs[arcs.length - 1].filled, value: totalFilledValue }}
config={{ duration: animationDuration }}
>
{(props) => renderDisplayValue(props.x, props.value)}
</Spring>
)}
</G>
)}
</Svg>
);
};
SegmentedRoundDisplay.propTypes = {
segments: PropTypes.arrayOf(
PropTypes.shape({
total: PropTypes.number.isRequired,
filled: PropTypes.number.isRequired,
})
),
filledArcWidth: PropTypes.number,
emptyArcWidth: PropTypes.number,
arcSpacing: PropTypes.number,
totalArcSize: PropTypes.number,
radius: PropTypes.number,
emptyArcColor: PropTypes.string,
filledArcColor: PropTypes.string,
formatAmount: PropTypes.func,
style: PropTypes.object,
animationDuration: PropTypes.number,
animated: PropTypes.bool,
formatValue: PropTypes.func,
incompleteArcColor: PropTypes.string,
displayValue: PropTypes.bool,
valueBoxColor: PropTypes.string,
valueFontColor: PropTypes.string,
};
SegmentedRoundDisplay.defaultProps = {
segments: [],
filledArcWidth: 7,
emptyArcWidth: 7,
arcSpacing: 7,
totalArcSize: 280,
radius: 100,
emptyArcColor: "#ADB1CC",
filledArcColor: "#5ECCAA",
animationDuration: 1000,
animated: true,
incompleteArcColor: "#23318C",
displayValue: false,
valueBoxColor: "#23318C",
valueFontColor: "#FFFFFF",
};
export default SegmentedRoundDisplay;