Building an Animated Radar Chart in React Native with Reanimated and SVG
Building an Animated Radar Chart in React Native
Radar charts (also called spider or web charts) are powerful visualization tools for displaying multivariate data. They allow you to plot multiple quantitative variables on axes starting from the same center point, making them ideal for comparing multiple entities or showing patterns across dimensions.
In this tutorial, we’ll build an animated radar chart in React Native by understanding each fundamental building block, then assembling them into a complete component.
Why Use Radar Charts?
Radar charts excel at:
- Comparing performance across multiple metrics simultaneously
- Identifying patterns, strengths, and weaknesses
- Visualizing balance or imbalance across dimensions
Common applications include:
- Skill assessments and performance evaluations
- Product feature comparisons
- Sports player statistics
- Personality profiles
Prerequisites
To follow along, you’ll need:
- A React Native development environment
- Basic understanding of TypeScript
- The following dependencies:
npm install react-native-reanimated react-native-svg
# or
yarn add react-native-reanimated react-native-svg
Building Block 1: Understanding Coordinate Systems
A radar chart plots data in a circular format, which requires converting between polar coordinates (angle and distance) and Cartesian coordinates (x,y) that we use to draw on the screen.
Basic Polar to Cartesian Conversion
// Convert polar coordinates (angle and distance) to Cartesian (x,y)
const angleInRadians = (Math.PI / 180) * angleInDegrees; // Convert degrees to radians
const x = centerX + radius * Math.cos(angleInRadians);
const y = centerY + radius * Math.sin(angleInRadians);
For our radar chart, we need to calculate points around a circle based on the number of categories:
// Calculate points for an n-sided polygon (n = number of categories)
const calculatePoints = (centerX, centerY, radius, categoryCount) => {
const points = [];
for (let i = 0; i < categoryCount; i++) {
// Calculate angle - start from top (270° or -90° or -π/2 radians)
const angle = (2 * Math.PI * i / categoryCount) - Math.PI / 2;
// Convert to Cartesian coordinates
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
points.push({ x, y });
}
return points;
};
This function gives us the vertices of a regular polygon, which we’ll use to position our category axes and data points.
Building Block 2: SVG Fundamentals
React Native doesn’t have built-in support for drawing complex shapes, so we’ll use react-native-svg
to create our chart.
Basic SVG Setup
import Svg, { G, Line, Circle, Text, Polygon } from 'react-native-svg';
const BasicSvgExample = () => (
<Svg width={300} height={300}>
<G>
{/* SVG elements will go here */}
<Circle cx={150} cy={150} r={50} fill="blue" />
<Line x1={100} y1={100} x2={200} y2={200} stroke="red" strokeWidth={2} />
<Text x={150} y={150} textAnchor="middle">Hello SVG</Text>
</G>
</Svg>
);
For our radar chart, we’ll use:
Polygon
to draw the web/grid and data polygonsLine
to draw axis linesCircle
for data pointsText
for labels
Building Block 3: Data Structures
Before we start drawing, we need to define how our data will be structured:
// Model for a single data point
interface DataValue {
value: number; // The actual value for this category
}
// Model for a complete dataset (one polygon on the chart)
interface DataSet {
values: DataValue[]; // Values for each category
label: string; // Dataset name
color: string; // Color for this dataset
}
// The complete data model for the chart
interface ChartData {
dataSets: DataSet[]; // Multiple datasets to display
labels: string[]; // Labels for each category/axis
maxValue?: number; // Optional max value for scaling
}
Here’s how you’d define sample data:
const sampleData: ChartData = {
dataSets: [
{
values: [
{ value: 8 }, // First category
{ value: 7 }, // Second category
{ value: 5 }, // Third category
{ value: 9 }, // Fourth category
{ value: 6 }, // Fifth category
{ value: 8 } // Sixth category
],
label: 'Dataset A',
color: '#FF4560'
},
{
values: [
{ value: 5 },
{ value: 9 },
{ value: 7 },
{ value: 6 },
{ value: 8 },
{ value: 4 }
],
label: 'Dataset B',
color: '#2E93fA'
}
],
labels: ['Category 1', 'Category 2', 'Category 3', 'Category 4', 'Category 5', 'Category 6'],
maxValue: 10 // Values range from 0-10
};
Building Block 4: Drawing the Web (Grid)
The “web” or grid consists of axis lines and concentric polygons. Let’s create functions to generate these elements:
// Generate points for concentric web polygons
const generateWebPoints = (centerX, centerY, radius, categoryCount, levelCount) => {
const webs = [];
// Create multiple levels of polygons (from center outward)
for (let level = 1; level <= levelCount; level++) {
const scaleFactor = level / levelCount;
const webRadius = radius * scaleFactor;
// Calculate points for this level
const points = calculatePoints(centerX, centerY, webRadius, categoryCount);
webs.push(points);
}
return webs;
};
// Render the web polygons
const renderWebPolygons = (webs, webColor, webLineWidth) => {
return webs.map((points, index) => (
<Polygon
key={`web-${index}`}
points={points.map(p => `${p.x},${p.y}`).join(' ')}
fill="none"
stroke={webColor}
strokeWidth={webLineWidth}
/>
));
};
// Generate axis lines (from center to each category point)
const generateAxisLines = (centerX, centerY, radius, categoryCount) => {
const lines = [];
// Calculate outer points
const outerPoints = calculatePoints(centerX, centerY, radius, categoryCount);
// Create lines from center to each point
for (let i = 0; i < categoryCount; i++) {
lines.push({
x1: centerX,
y1: centerY,
x2: outerPoints[i].x,
y2: outerPoints[i].y
});
}
return lines;
};
// Render the axis lines
const renderAxisLines = (lines, axisColor, axisWidth) => {
return lines.map((line, index) => (
<Line
key={`axis-${index}`}
x1={line.x1}
y1={line.y1}
x2={line.x2}
y2={line.y2}
stroke={axisColor}
strokeWidth={axisWidth}
/>
));
};
Building Block 5: Data Processing and Color Management
Before rendering our chart, we need to process the raw data and manage colors effectively:
5.1 Finding Maximum Values for Scaling
When scaling our data, we need to determine the maximum value to create proportional representations:
// Find maximum values for proper scaling
const findMaxValue = (data) => {
// Use provided maxValue if available
if (data.maxValue !== undefined) {
return data.maxValue;
}
// Otherwise calculate from all datasets
const allValues = data.dataSets
.flatMap(dataset => dataset.values.map(v => v.value));
// Return max value or fallback to 1 to avoid division by zero
return Math.max(...allValues, 1);
};
5.2 Color Management
Proper color handling is essential for visual clarity and accessibility:
// Handle various color formats and provide fallbacks
const resolveColor = (color, defaultColor = '#1E88E5') => {
// If color is missing, use default
if (!color) return defaultColor;
// Handle numeric color values (sometimes returned by processColor)
if (typeof color === 'number') {
return defaultColor; // In real app, could convert from int to hex
}
return color;
};
// Generate a contrasting color for highlights or strokes
const generateContrastColor = (baseColor) => {
// Simple contrast - a real implementation might use HSL manipulation
return baseColor === '#FFFFFF' ? '#000000' : '#FFFFFF';
};
5.3 Data Normalization
Normalizing data ensures fair visual comparison between datasets:
// Normalize values to a consistent scale
const normalizeValue = (value, maxValue) => {
// Prevent division by zero and negative values
if (maxValue <= 0) return 0;
if (value <= 0) return 0;
// Cap at 1.0 for values exceeding the max
return Math.min(value / maxValue, 1.0);
};
Building Block 6: Converting Data to Points
Now we can convert our processed data to coordinates for rendering:
// Convert dataset values to drawable points
const convertDataToPoints = (dataset, categoryCount, maxValue, centerX, centerY, radius) => {
// Calculate points for each value
return dataset.values.map((value, index) => {
// Calculate angle for this category (starting from top)
const angle = (2 * Math.PI * index / categoryCount) - Math.PI / 2;
// Normalize value and calculate distance from center
const normalizedValue = normalizeValue(value.value, maxValue);
const distance = radius * normalizedValue;
// Convert to Cartesian coordinates
const x = centerX + distance * Math.cos(angle);
const y = centerY + distance * Math.sin(angle);
// Return point with original value for reference
return { x, y, value: value.value, angle };
});
};
// Process all datasets
const processData = (data, centerX, centerY, radius) => {
if (!data?.dataSets?.length) return [];
// Find max value for all datasets
const maxValue = findMaxValue(data);
const categoryCount = data.labels?.length || 0;
return data.dataSets.map(dataset => {
// Convert values to points
const points = convertDataToPoints(
dataset,
categoryCount,
maxValue,
centerX,
centerY,
radius
);
// Resolve color
const color = resolveColor(dataset.color);
return {
...dataset,
points,
color
};
});
};
Building Block 7: Calculating Grid Elements
Let’s split the grid calculation into distinct parts:
7.1 Calculating Web Grid Points
The web consists of concentric polygons that show value gradations:
// Calculate points for a single level of the web
const calculateWebLevel = (centerX, centerY, radius, categoryCount, levelFactor) => {
const points = [];
// Scale the radius by the level factor (0-1)
const scaledRadius = radius * levelFactor;
// Calculate points for this level
for (let i = 0; i < categoryCount; i++) {
const angle = (2 * Math.PI * i / categoryCount) - Math.PI / 2;
const x = centerX + scaledRadius * Math.cos(angle);
const y = centerY + scaledRadius * Math.sin(angle);
points.push({ x, y });
}
return points;
};
// Generate all web levels
const generateWebLevels = (centerX, centerY, radius, categoryCount, levelCount = 5) => {
const webLevels = [];
// Create each level from inside out
for (let level = 1; level <= levelCount; level++) {
const levelFactor = level / levelCount;
const levelPoints = calculateWebLevel(
centerX,
centerY,
radius,
categoryCount,
levelFactor
);
webLevels.push({
level,
levelFactor,
points: levelPoints
});
}
return webLevels;
};
7.2 Calculating Axis Lines
Axis lines connect the center to each category point:
// Calculate axis lines from center to each category point
const calculateAxisLines = (centerX, centerY, radius, categoryCount) => {
const axisLines = [];
for (let i = 0; i < categoryCount; i++) {
// Calculate end point angle
const angle = (2 * Math.PI * i / categoryCount) - Math.PI / 2;
// Calculate end point
const endX = centerX + radius * Math.cos(angle);
const endY = centerY + radius * Math.sin(angle);
axisLines.push({
index: i,
angle,
line: {
x1: centerX,
y1: centerY,
x2: endX,
y2: endY
}
});
}
return axisLines;
};
7.3 Calculating Minor Grid Lines
For more detailed grids, we can add minor lines between major axes:
// Calculate minor grid lines for more detailed grids
const calculateMinorGridLines = (centerX, centerY, radius, categoryCount, minorLinesPerSegment = 1) => {
const minorLines = [];
if (minorLinesPerSegment <= 0) return minorLines;
// Calculate angle between major axes
const angleStep = (2 * Math.PI) / categoryCount;
// Calculate minor angle steps
const minorAngleStep = angleStep / (minorLinesPerSegment + 1);
// For each major segment
for (let i = 0; i < categoryCount; i++) {
const startAngle = (2 * Math.PI * i / categoryCount) - Math.PI / 2;
// Create the minor lines within this segment
for (let j = 1; j <= minorLinesPerSegment; j++) {
const angle = startAngle + (minorAngleStep * j);
const endX = centerX + radius * Math.cos(angle);
const endY = centerY + radius * Math.sin(angle);
minorLines.push({
line: {
x1: centerX,
y1: centerY,
x2: endX,
y2: endY
},
angle
});
}
}
return minorLines;
};
Building Block 8: Label Positioning and Formatting
Properly positioning and formatting labels is crucial for readability:
8.1 Calculating Label Positions
Label positions need careful calculation based on their circular arrangement:
// Calculate positions for category labels
const calculateLabelPositions = (centerX, centerY, radius, categoryCount, labels, padding = 20) => {
if (!labels || labels.length === 0) return [];
return Array.from({ length: categoryCount }).map((_, index) => {
// Calculate angle for this label
const angle = (2 * Math.PI * index / categoryCount) - Math.PI / 2;
// Position label outside the chart with padding
const x = centerX + (radius + padding) * Math.cos(angle);
const y = centerY + (radius + padding) * Math.sin(angle);
// Label content
const text = index < labels.length ? labels[index] : `Category ${index + 1}`;
// Determine text alignment based on position
let textAnchor, verticalAlignment;
// Right side
if (angle > -Math.PI/4 && angle < Math.PI/4) {
textAnchor = "start";
verticalAlignment = "middle";
}
// Left side
else if (angle > 3*Math.PI/4 || angle < -3*Math.PI/4) {
textAnchor = "end";
verticalAlignment = "middle";
}
// Top
else if (angle >= -3*Math.PI/4 && angle <= -Math.PI/4) {
textAnchor = "middle";
verticalAlignment = "end";
}
// Bottom
else {
textAnchor = "middle";
verticalAlignment = "start";
}
return {
index,
text,
angle,
x,
y,
textAnchor,
verticalAlignment
};
});
};
8.2 Value Label Formatting
For showing values at data points:
// Format value labels for display
const formatValueLabel = (value, options = {}) => {
const {
decimalPlaces = 1,
prefix = '',
suffix = '',
abbreviate = false
} = options;
let formattedValue = value;
// Round to specified decimal places
if (typeof value === 'number') {
formattedValue = value.toFixed(decimalPlaces);
// Abbreviate large numbers
if (abbreviate) {
if (value >= 1000000) {
formattedValue = (value / 1000000).toFixed(1) + 'M';
} else if (value >= 1000) {
formattedValue = (value / 1000).toFixed(1) + 'K';
}
}
}
return `${prefix}${formattedValue}${suffix}`;
};
8.3 Value Scale Calculation
To display value scales on the web grid:
// Calculate values for grid levels
const calculateScaleValues = (maxValue, levelCount) => {
return Array.from({ length: levelCount }).map((_, index) => {
const levelFactor = (index + 1) / levelCount;
return maxValue * levelFactor;
});
};
// Position scale labels on the grid
const positionScaleLabels = (centerX, centerY, scaleValues, referenceAxis = 0) => {
const angle = (2 * Math.PI * referenceAxis / scaleValues.length) - Math.PI / 2;
return scaleValues.map((value, index) => {
const distance = radius * ((index + 1) / scaleValues.length);
const x = centerX + distance * Math.cos(angle);
const y = centerY + distance * Math.sin(angle);
return {
value,
x,
y
};
});
};
Building Block 9: Animated Components
Animation enhances understanding by showing transitions. Let’s break down our animated components:
9.1 Creating Animated Polygon Components
import Animated, {
useSharedValue,
useAnimatedProps,
withTiming,
interpolate,
Extrapolate
} from 'react-native-reanimated';
import { Polygon, Circle } from 'react-native-svg';
// Create animated SVG components
const AnimatedPolygon = Animated.createAnimatedComponent(Polygon);
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
9.2 Animating Data Polygons
// Animated polygon component for data visualization
const AnimatedDataPolygon = ({
dataset,
animationProgress,
centerX,
centerY,
animationConfig = {}
}) => {
const {
fillOpacity = 0.3,
strokeWidth = 2.5,
animationType = 'fromCenter'
} = animationConfig;
// Define animation behavior based on type
const getAnimatedPoint = (point, progress) => {
if (animationType === 'fromCenter') {
// Animate from center to final position
const x = interpolate(
progress,
[0, 1],
[centerX, point.x],
Extrapolate.CLAMP
);
const y = interpolate(
progress,
[0, 1],
[centerY, point.y],
Extrapolate.CLAMP
);
return `${x},${y}`;
}
else if (animationType === 'grow') {
// Grow from center but maintain shape
const angle = point.angle;
const normalizedDistance = interpolate(
progress,
[0, 1],
[0, point.normalizedValue],
Extrapolate.CLAMP
);
const distance = radius * normalizedDistance;
const x = centerX + distance * Math.cos(angle);
const y = centerY + distance * Math.sin(angle);
return `${x},${y}`;
}
// Default to no animation
return `${point.x},${point.y}`;
};
// Create animated properties
const animatedProps = useAnimatedProps(() => {
// Create points string with animation
const pointsString = dataset.points.map(point =>
getAnimatedPoint(point, animationProgress.value)
).join(' ');
// Return animated properties
return {
points: pointsString,
fill: dataset.color,
fillOpacity,
stroke: dataset.color,
strokeWidth
};
});
return <AnimatedPolygon animatedProps={animatedProps} />;
};
9.3 Animating Data Points
// Animated circle component for data points
const AnimatedDataPoint = ({
point,
color,
animationProgress,
centerX,
centerY,
onPress,
accessibilityLabel
}) => {
// Create animated properties
const animatedProps = useAnimatedProps(() => {
// Animate from center
const cx = interpolate(
animationProgress.value,
[0, 1],
[centerX, point.x],
Extrapolate.CLAMP
);
const cy = interpolate(
animationProgress.value,
[0, 1],
[centerY, point.y],
Extrapolate.CLAMP
);
// Scale size with animation
const r = interpolate(
animationProgress.value,
[0, 0.8, 1],
[0, 6, 5],
Extrapolate.CLAMP
);
// Return animated properties
return {
cx,
cy,
r
};
});
return (
<AnimatedCircle
animatedProps={animatedProps}
fill={color}
stroke="#FFFFFF"
strokeWidth={1.5}
accessible={true}
accessibilityLabel={accessibilityLabel}
onPress={onPress}
/>
);
};
Building Block 10: Rendering the Complete Chart
Now we can assemble all these specialized components to create our radar chart:
const RadarChart = ({
data,
style = {},
webColor = '#CCCCCC',
webLineWidth = 1,
gridLevels = 5,
textColor = '#333333',
animationDuration = 1000,
showAnimation = true,
showTooltips = false,
onPointPress = null,
minorGridLines = 0
}) => {
// Component implementation assembling all building blocks
// Set up dimensions
const dimensions = Dimensions.get('window');
const width = style.width || dimensions.width;
const height = style.height || dimensions.width * 0.8;
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(centerX, centerY) * 0.8;
// Animation progress
const animationProgress = useSharedValue(0);
// For interactive tooltips
const [selectedPoint, setSelectedPoint] = useState(null);
// Get category count
const categoryCount = data?.labels?.length || 0;
// Process data using our building blocks
const maxValue = findMaxValue(data);
const processedData = processData(data, centerX, centerY, radius);
// Calculate web/grid elements
const webLevels = generateWebLevels(centerX, centerY, radius, categoryCount, gridLevels);
const axisLines = calculateAxisLines(centerX, centerY, radius, categoryCount);
const minorLines = calculateMinorGridLines(centerX, centerY, radius, categoryCount, minorGridLines);
// Calculate labels
const categoryLabels = calculateLabelPositions(centerX, centerY, radius, categoryCount, data.labels);
const scaleValues = calculateScaleValues(maxValue, gridLevels);
// Trigger animation when data changes
useEffect(() => {
if (showAnimation) {
animationProgress.value = 0;
animationProgress.value = withTiming(1, { duration: animationDuration });
} else {
animationProgress.value = 1;
}
}, [data, showAnimation, animationDuration]);
// Handle point press
const handlePointPress = (pointData) => {
setSelectedPoint(pointData);
if (onPointPress) {
onPointPress(pointData);
}
};
// Clear tooltip
const handleBackgroundPress = () => {
setSelectedPoint(null);
};
// Render the chart components
return (
<View style={[styles.container, style]}>
<Svg width={width} height={height} onPress={handleBackgroundPress}>
<G>
{/* Render components using our building blocks */}
{/* ... */}
</G>
</Svg>
</View>
);
};
Building Block 11: Interactivity and Accessibility
Let’s enhance our chart with interaction and accessibility features:
11.1 Interactive Tooltips
// Tooltip component to show on data point selection
const Tooltip = ({ point, label, value, color }) => {
return (
<G x={point.x} y={point.y - 30}>
{/* Background */}
<Rect
x={-50}
y={-30}
width={100}
height={30}
rx={5}
fill="white"
stroke={color}
strokeWidth={1}
/>
{/* Value */}
<Text
x={0}
y={-15}
textAnchor="middle"
fontWeight="bold"
fontSize={12}
fill={color}
>
{value}
</Text>
{/* Label */}
<Text
x={0}
y={5}
textAnchor="middle"
fontSize={10}
fill="#333333"
>
{label}
</Text>
</G>
);
};
11.2 Accessibility Enhancement
// Add comprehensive accessibility support
const makeAccessible = (element, label, role, hint) => {
return React.cloneElement(element, {
accessible: true,
accessibilityLabel: label,
accessibilityRole: role,
accessibilityHint: hint
});
};
// Create accessibility-enhanced data point
const AccessibleDataPoint = ({ point, dataset, category, index, ...props }) => {
const accessibilityLabel = `${dataset.label}, ${category}: ${point.value}`;
const accessibilityHint = "Double tap to show details";
return (
<Circle
{...props}
accessible={true}
accessibilityLabel={accessibilityLabel}
accessibilityHint={accessibilityHint}
accessibilityRole="button"
/>
);
};
Building the Complete Chart Component
Now let’s assemble all these building blocks into a complete, reusable component:
import React, { useMemo, useEffect, useState } from 'react';
import { View, StyleSheet, Dimensions } from 'react-native';
import Svg, { G, Line, Circle, Text, Polygon, Rect } from 'react-native-svg';
import Animated, {
useSharedValue,
useAnimatedProps,
withTiming,
interpolate,
Extrapolate,
} from 'react-native-reanimated';
// Create Animated SVG components
const AnimatedPolygon = Animated.createAnimatedComponent(Polygon);
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
const RadarChart = ({
data,
style = {},
webColor = '#CCCCCC',
webLineWidth = 1,
gridLevels = 5,
textColor = '#333333',
animationDuration = 1000,
showAnimation = true,
showTooltips = false,
}) => {
// Set up dimensions
const dimensions = Dimensions.get('window');
const width = style.width || dimensions.width;
const height = style.height || dimensions.width * 0.8;
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(centerX, centerY) * 0.8;
// Animation progress
const animationProgress = useSharedValue(0);
// For interactive tooltips
const [selectedPoint, setSelectedPoint] = useState(null);
// Trigger animation when data changes
useEffect(() => {
// Reset animation
animationProgress.value = 0;
// Animate to final state
animationProgress.value = withTiming(1, { duration: 1000 });
}, [data]);
// Calculate category count
const categoryCount = data?.labels?.length || 0;
// Generate web points and axis lines
const webPoints = useMemo(() => {
if (!categoryCount) return [];
return generateWebPoints(centerX, centerY, radius, categoryCount, gridLevels);
}, [centerX, centerY, radius, categoryCount, gridLevels]);
const axisLines = useMemo(() => {
if (!categoryCount) return [];
return generateAxisLines(centerX, centerY, radius, categoryCount);
}, [centerX, centerY, radius, categoryCount]);
// Process data for visualization
const processedData = useMemo(() => {
return processData(data, centerX, centerY, radius);
}, [data, centerX, centerY, radius]);
// Generate label positions
const labels = useMemo(() => {
return generateLabels(data, centerX, centerY, radius);
}, [data, centerX, centerY, radius]);
// Handle point selection
const handlePointPress = (pointData) => {
setSelectedPoint(pointData);
};
// Clear tooltip when tapping elsewhere
const handleBackgroundPress = () => {
setSelectedPoint(null);
};
return (
<View style={[styles.container, style]}>
<Svg width={width} height={height} onPress={handleBackgroundPress}>
<G>
{/* 1. Draw the web (grid) */}
{webPoints.map((points, i) => (
<Polygon
key={`web-${i}`}
points={points.map(p => `${p.x},${p.y}`).join(' ')}
fill="none"
stroke={webColor}
strokeWidth={webLineWidth}
/>
))}
{/* 2. Draw axis lines */}
{axisLines.map((line, i) => (
<Line
key={`axis-${i}`}
x1={line.x1}
y1={line.y1}
x2={line.x2}
y2={line.y2}
stroke={webColor}
strokeWidth={webLineWidth}
/>
))}
{/* 3. Draw data polygons with animation */}
{processedData.map((dataset, i) => (
<AnimatedDataPolygon
key={`dataset-${i}`}
dataset={dataset}
animationProgress={animationProgress}
centerX={centerX}
centerY={centerY}
/>
))}
{/* 4. Draw data points */}
{processedData.map((dataset, i) =>
dataset.points.map((point, j) => (
<Circle
key={`point-${i}-${j}`}
cx={point.x}
cy={point.y}
r={5}
fill={dataset.color}
stroke="#FFFFFF"
strokeWidth={1.5}
accessible={true}
accessibilityLabel={`${dataset.label}, ${data.labels[j]}: ${point.value}`}
onPress={() => handlePointPress({
point,
label: data.labels[j],
value: point.value,
datasetName: dataset.label
})}
/>
))
)}
{/* 5. Draw category labels */}
{labels.map((label, i) => (
<Text
key={`label-${i}`}
x={label.x}
y={label.y}
textAnchor={label.textAnchor}
alignmentBaseline="middle"
fontSize={12}
fontWeight="bold"
fill={textColor}
>
{label.label}
</Text>
))}
{/* 6. Show tooltip if a point is selected */}
{showTooltips && selectedPoint && (
<Tooltip
point={selectedPoint.point}
label={`${selectedPoint.datasetName}: ${selectedPoint.label}`}
value={selectedPoint.value}
/>
)}
</G>
</Svg>
</View>
);
};
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
},
});
export default RadarChart;
Using the Chart Component
Here’s how to use the radar chart in your React Native application:
import React, { useState } from 'react';
import { View, Text, Button, StyleSheet, SafeAreaView } from 'react-native';
import RadarChart from './RadarChart';
const SkillsComparisonApp = () => {
const [data, setData] = useState({
dataSets: [
{
values: [
{ value: 8 },
{ value: 7 },
{ value: 5 },
{ value: 9 },
{ value: 6 },
{ value: 8 }
],
label: 'Developer A',
color: '#FF4560'
},
{
values: [
{ value: 5 },
{ value: 9 },
{ value: 7 },
{ value: 6 },
{ value: 8 },
{ value: 4 }
],
label: 'Developer B',
color: '#2E93fA'
}
],
labels: ['React', 'TypeScript', 'GraphQL', 'HTML/CSS', 'Node.js', 'React Native'],
maxValue: 10
});
// Generate random data to demonstrate animation
const randomizeData = () => {
setData({
...data,
dataSets: data.dataSets.map(dataset => ({
...dataset,
values: dataset.values.map(() => ({
value: Math.floor(Math.random() * 10) + 1
}))
}))
});
};
return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>Developer Skills Comparison</Text>
<RadarChart
data={data}
style={styles.chart}
animationDuration={1500}
showTooltips={true}
/>
<Button
title="Randomize Skills"
onPress={randomizeData}
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#FFFFFF'
},
title: {
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
marginVertical: 16
},
chart: {
height: 350,
width: '100%',
marginBottom: 20
}
});
export default SkillsComparisonApp;
Customization Options
The radar chart component supports various customization options:
- Data structure: Format and normalize your data
- Colors: Change colors for web, axes, and datasets
- Dimensions: Adjust size, radius, and padding
- Animation: Enable/disable animations and control duration
- Interaction: Add tooltips and tap handling
- Accessibility: Provide proper labels for screen readers
Performance Considerations
For optimal performance:
- Memoization: Use
useMemo
for expensive calculations - Throttling: Limit animation updates on slower devices
- Optimize SVG: Don’t render unnecessary elements
Conclusion
By understanding each building block of a radar chart - from coordinate systems to data processing to animation - you can create an effective, customizable visualization component for your React Native applications.
This radar chart implementation is powerful yet flexible, allowing you to visualize multivariate data in an engaging way. The animation helps users understand transitions between data states, and the interactive features improve data exploration.