How to Create a Radar Chart in Plotly

Radar charts (also called spider charts or star plots) display multivariate data on axes radiating from a central point. Each axis represents a different variable, and values are plotted as distances...

Key Insights

  • Radar charts excel at visualizing multivariate data where you need to compare performance across multiple dimensions simultaneously, but they require normalized scales to be effective
  • Plotly’s go.Scatterpolar() provides interactive radar charts with minimal code, supporting multiple series overlays, custom styling, and dynamic filtering through its built-in legend controls
  • Always limit radar charts to 5-8 categories and 2-3 data series maximum—beyond that, the visualization becomes cluttered and loses its comparative advantage over simpler alternatives like grouped bar charts

Introduction to Radar Charts

Radar charts (also called spider charts or star plots) display multivariate data on axes radiating from a central point. Each axis represents a different variable, and values are plotted as distances from the center, connected to form a polygon. This makes them ideal for comparing profiles across multiple dimensions—think employee skill assessments, product feature comparisons, or sports analytics.

The key advantage is instant visual pattern recognition. You can immediately see where subjects excel or lag across categories. However, they’re frequently misused. Radar charts only work when comparing entities across the same set of metrics, and those metrics must be on comparable scales.

Plotly is the right tool for radar charts because it delivers interactivity out of the box. Users can hover for exact values, toggle series on and off, and zoom into specific regions. The Python API is straightforward, and the resulting charts render beautifully in Jupyter notebooks, web apps, or exported as standalone HTML files.

Basic Radar Chart Setup

Plotly uses go.Scatterpolar() to create radar charts. The fundamental structure requires two lists: theta for category labels and r for values.

import plotly.graph_objects as go

# Define categories and values
categories = ['Python', 'JavaScript', 'SQL', 'Docker', 'AWS']
values = [9, 6, 8, 7, 5]

# Create the radar chart
fig = go.Figure()

fig.add_trace(go.Scatterpolar(
    r=values,
    theta=categories,
    fill='toself',
    name='Current Skills'
))

fig.update_layout(
    polar=dict(
        radialaxis=dict(
            visible=True,
            range=[0, 10]
        )
    ),
    showlegend=True,
    title='Developer Skill Assessment'
)

fig.show()

This creates a basic five-sided radar chart. The fill='toself' parameter fills the area inside the polygon, making the shape more prominent. The range=[0, 10] ensures all categories use the same scale—critical for accurate visual comparison.

Notice we explicitly set the radial range. Without this, Plotly auto-scales based on your data, which can distort comparisons if you later add more series with different value ranges.

Customizing Chart Appearance

Visual customization transforms a functional chart into a polished deliverable. Control colors, opacity, line properties, and axis formatting to match your brand or improve readability.

import plotly.graph_objects as go

categories = ['Speed', 'Reliability', 'Features', 'Support', 'Price']
values = [8, 9, 6, 7, 8]

fig = go.Figure()

fig.add_trace(go.Scatterpolar(
    r=values,
    theta=categories,
    fill='toself',
    fillcolor='rgba(99, 110, 250, 0.3)',
    line=dict(color='rgb(99, 110, 250)', width=3),
    marker=dict(size=8, color='rgb(99, 110, 250)'),
    name='Our Product'
))

fig.update_layout(
    polar=dict(
        radialaxis=dict(
            visible=True,
            range=[0, 10],
            showline=False,
            showgrid=True,
            gridcolor='lightgray',
            tickfont=dict(size=12)
        ),
        angularaxis=dict(
            showgrid=True,
            gridcolor='lightgray',
            linewidth=2
        )
    ),
    showlegend=True,
    title=dict(
        text='Product Performance Metrics',
        font=dict(size=20)
    ),
    font=dict(family='Arial', size=14)
)

fig.show()

The fillcolor uses RGBA values where the alpha channel (0.3) controls transparency. This prevents the filled area from obscuring grid lines. The line dictionary sets border properties, while marker controls the data point indicators at each vertex.

Grid customization happens in polar.radialaxis and polar.angularaxis. Setting showline=False on the radial axis removes the outer circle while keeping grid lines visible for reference.

Multi-Series Comparison

The real power of radar charts emerges when comparing multiple entities. Add traces for each series you want to overlay.

import plotly.graph_objects as go

categories = ['Offense', 'Defense', 'Speed', 'Stamina', 'Technique', 'Teamwork']

player_a = [9, 6, 8, 7, 8, 9]
player_b = [7, 9, 6, 8, 7, 8]
player_c = [8, 7, 9, 6, 9, 7]

fig = go.Figure()

fig.add_trace(go.Scatterpolar(
    r=player_a,
    theta=categories,
    fill='toself',
    fillcolor='rgba(255, 99, 71, 0.2)',
    line=dict(color='rgb(255, 99, 71)', width=2),
    name='Player A'
))

fig.add_trace(go.Scatterpolar(
    r=player_b,
    theta=categories,
    fill='toself',
    fillcolor='rgba(50, 205, 50, 0.2)',
    line=dict(color='rgb(50, 205, 50)', width=2),
    name='Player B'
))

fig.add_trace(go.Scatterpolar(
    r=player_c,
    theta=categories,
    fill='toself',
    fillcolor='rgba(30, 144, 255, 0.2)',
    line=dict(color='rgb(30, 144, 255)', width=2),
    name='Player C'
))

fig.update_layout(
    polar=dict(
        radialaxis=dict(
            visible=True,
            range=[0, 10]
        )
    ),
    showlegend=True,
    title='Player Performance Comparison'
)

fig.show()

Each trace represents one player. Low opacity (0.2) on fills prevents visual clutter when polygons overlap. Users can click legend items to toggle individual players on and off—a built-in Plotly feature that requires zero additional code.

Keep multi-series charts to three overlays maximum. Beyond that, the chart becomes a tangled mess where comparative insights are lost.

Advanced Features

Plotly’s interactivity extends beyond basic hover tooltips. Customize hover templates to show exactly the information users need.

import plotly.graph_objects as go

categories = ['Innovation', 'Market Share', 'Customer Satisfaction', 
              'Growth Rate', 'Profitability', 'Brand Recognition']

company_data = [8.5, 7.2, 9.1, 6.8, 7.5, 8.9]

fig = go.Figure()

fig.add_trace(go.Scatterpolar(
    r=company_data,
    theta=categories,
    fill='toself',
    fillcolor='rgba(99, 110, 250, 0.25)',
    line=dict(color='rgb(99, 110, 250)', width=2),
    name='Q4 2024',
    hovertemplate='<b>%{theta}</b><br>' +
                  'Score: %{r:.1f}/10<br>' +
                  '<extra></extra>'
))

# Add a second quarter for comparison
q3_data = [7.8, 6.9, 8.5, 7.2, 7.1, 8.3]

fig.add_trace(go.Scatterpolar(
    r=q3_data,
    theta=categories,
    fill='toself',
    fillcolor='rgba(239, 85, 59, 0.25)',
    line=dict(color='rgb(239, 85, 59)', width=2, dash='dash'),
    name='Q3 2024',
    hovertemplate='<b>%{theta}</b><br>' +
                  'Score: %{r:.1f}/10<br>' +
                  '<extra></extra>'
))

fig.update_layout(
    polar=dict(
        radialaxis=dict(
            visible=True,
            range=[0, 10],
            tickmode='linear',
            tick0=0,
            dtick=2
        )
    ),
    showlegend=True,
    title='Quarterly Performance Metrics',
    hovermode='closest'
)

fig.show()

The hovertemplate uses %{theta} and %{r} placeholders to reference category names and values. The <extra></extra> tag removes the default trace name from the hover box (since we’re already showing it in the legend).

The dash='dash' parameter on the second trace’s line creates a dashed border, adding visual distinction beyond just color—helpful for colorblind users or when printing in grayscale.

Real-World Use Case

Here’s a practical example: comparing smartphones across key purchase criteria.

import plotly.graph_objects as go

categories = ['Battery Life', 'Camera Quality', 'Performance', 
              'Display', 'Build Quality', 'Value', 'Software']

iphone_scores = [7, 9, 9, 9, 9, 6, 8]
samsung_scores = [8, 9, 9, 9, 8, 7, 7]
pixel_scores = [8, 8, 8, 8, 7, 8, 9]

fig = go.Figure()

phones = [
    ('iPhone 15 Pro', iphone_scores, 'rgb(0, 113, 227)'),
    ('Galaxy S24', samsung_scores, 'rgb(20, 20, 20)'),
    ('Pixel 8 Pro', pixel_scores, 'rgb(66, 133, 244)')
]

for name, scores, color in phones:
    fig.add_trace(go.Scatterpolar(
        r=scores,
        theta=categories,
        fill='toself',
        fillcolor=color.replace('rgb', 'rgba').replace(')', ', 0.2)'),
        line=dict(color=color, width=2),
        name=name,
        hovertemplate='<b>%{theta}</b><br>' +
                      '%{r}/10<br>' +
                      '<extra></extra>'
    ))

fig.update_layout(
    polar=dict(
        radialaxis=dict(
            visible=True,
            range=[0, 10],
            tickmode='linear',
            tick0=0,
            dtick=2,
            showline=False,
            gridcolor='rgba(0, 0, 0, 0.1)'
        ),
        angularaxis=dict(
            gridcolor='rgba(0, 0, 0, 0.1)'
        )
    ),
    showlegend=True,
    title=dict(
        text='Flagship Smartphone Comparison 2024',
        font=dict(size=22)
    ),
    height=600,
    font=dict(family='Helvetica', size=14)
)

fig.show()

This visualization immediately shows that all three phones score similarly on core features, but the Pixel edges ahead on software and value while the iPhone leads slightly on camera and performance. These insights would be harder to extract from a table or bar chart.

Best Practices and Pitfalls

Normalize your data. Radar charts fail when categories use different scales. Convert everything to a common range (0-10, 0-100, or percentiles) before plotting.

Limit categories to 5-8. More than eight axes creates a cluttered, unreadable chart. If you have more metrics, consider grouping them into composite scores or using a different visualization.

Don’t compare unrelated metrics. Radar charts imply that all dimensions are equally important and somewhat related. Comparing “revenue” with “employee satisfaction” on the same chart rarely makes sense.

Watch for misleading areas. The filled polygon’s area doesn’t scale linearly with values. A small increase in one metric can create a disproportionately large visual change. Always include the radial axis scale so users can read exact values.

Consider alternatives. If you’re only comparing two or three metrics, a simple bar chart is clearer. If categories have a natural order (like time series), use a line chart instead. Radar charts work best when showing profiles across unordered, equally-weighted dimensions.

Use consistent colors across dashboards. If “Player A” is always red in your application, keep it red in every chart. Consistency reduces cognitive load.

Radar charts are powerful when used correctly but problematic when forced into inappropriate scenarios. Master the basics with Plotly, customize thoughtfully, and always ask whether another chart type might communicate your data more clearly.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.