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 polygons
  • Line to draw axis lines
  • Circle for data points
  • Text 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:

  1. Memoization: Use useMemo for expensive calculations
  2. Throttling: Limit animation updates on slower devices
  3. 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.

Resources for Further Learning