Radar plot in Plots.jl or Makie.jl?

Is there a simple way to make a radar plot (like a polar plot but categorical) using Plots.jl or Makie.jl ?

I can see these should be possible with a package named ECharts, but it doesn’t support VSCode and seems a bit abandoned and it forces very old version of the other packages…

Nothing premade yet in Makie, but the primitives look not very complicated if someone wanted to add it :slight_smile: The thing is more that the axis is unusual, and I guess it would be fastest to just plot this into a normal Axis where the decorations are hidden, than to build a completely new Axis object with projections etc.

With PlotlyJS.jl you can get a radar plot, as a polar plot with categorical theta:

using PlotlyJS
categories = String["processing cost","mechanical properties",
                    "chemical stability", "thermal stability", "device integration"]
fig = Plot()
addtraces!(fig, 
           scatterpolar(r=[1, 5, 2, 2, 3],
                        theta=categories,
                        fill="toself",
                        name="Product A"))
addtraces!(fig, 
           scatterpolar(r=[4, 3, 2.5, 1, 2],
                        theta=categories,
                        fill="toself",
                        name="Product B"))
relayout!(fig, width =600,  polar=attr(radialaxis=attr(visible=true, range=[0, 5])),
          showlegend=false)
display(fig)

3 Likes

FWIW, find here a home-made Plots.jl solution:

using Random, Measures, Plots; gr()

# INPUT DATA:
Random.seed!(1789)
n = 8
R1, R2 = rand(n+1),  rand(n+1)
R1[n+1], R2[n+1] = R1[1], R2[1]
labels = 'C' .* string.(1:n)

# PLOT:
θ = LinRange(0, 2pi, n+1)
z = 1.15*exp.(im*2π*(0:n-1)/n)
plot(θ, R1, proj=:polar, ms=3, m=:o, c=:blue, fill=(true,:blues), fa=0.4, lims=(0,1), xaxis=false, margin=5mm)
plot!(θ, R2, proj=:polar, ms=3, m=:o, c=:red, fill=(true,:reds), fa=0.4, lims=(0,1), xaxis=false)
annotate!(real.(z), imag.(z), text.(labels,12,"Computer Modern"))

3 Likes

There is still a problem with real world data: the alignment of the label should be conditional to the angle:
image
Any way to impose it ?

Yes, we can create a justifyme() function:

using Random, Measures, Plots; gr()

# INPUT DATA:
Random.seed!(1789)
n = 8
R1, R2 = rand(n+1),  rand(n+1)
R1[n+1], R2[n+1] = R1[1], R2[1]
labels = "Long Category" .* string.(1:n)

# PLOT:
justifyme(θ) = (0≤θ<π/2 || 3π/2<θ≤2π) ? :left : (π/2<θ<3π/2) ? :right : :center
θ = LinRange(0, 2pi, n+1)
z = 1.15*exp.(im*2π*(0:n-1)/n)
plot(θ, R1, proj=:polar, ms=3, m=:o, c=:blue, fill=(true,:blues), fa=0.4, lims=(0,1), xaxis=false, margin=10mm)
plot!(θ, R2, proj=:polar, ms=3, m=:o, c=:red, fill=(true,:reds), fa=0.4, lims=(0,1), xaxis=false)
annotate!(real.(z), imag.(z), text.(labels,12,justifyme.(θ[1:n]),"Computer Modern"))

1 Like

Hi @sylvaticus , this is what I did using Makie.jl (and using also @rafael.guerra’s justifyme() function. It can be refined and a recipe would be better, but had no time to study how to.

using CairoMakie, Colors, Random

# For horizintal and vertical text alignment:
justifyme(θ) = (0≤θ<π/2 || 3π/2<θ≤2π) ? :left : (π/2<θ<3π/2) ? :right : :center
justifymeV(θ) = π/4≤θ≤3π/4 ? :bottom : 5π/4<θ≤7π/4 ? :top : :center

# Radar plot function:
function radarplot(ax::Axis, v; p_grid = maximum(v) * (1.0:5.0) / 5.0, title = "", labels = eachindex(v), labelsize = 1, points=true, spokeswidth= 1.5, spokescolor=:salmon, fillalpha=0.2, linewidth=1.5)
    # Axis attributes
    ax.xgridvisible = false
    ax.ygridvisible = false
    ax.xminorgridvisible = false
    ax.yminorgridvisible = false
    ax.leftspinevisible = false
    ax.rightspinevisible = false
    ax.bottomspinevisible = false
    ax.topspinevisible = false
    ax.xminorticksvisible = false
    ax.yminorticksvisible = false
    ax.xticksvisible = false
    ax.yticksvisible = false
    ax.xticklabelsvisible = false
    ax.yticklabelsvisible = false
    ax.aspect = DataAspect()
    ax.title = title
    #
    l = length(v)
    rad = (0:(l-1)) * 2π / l
    # Point coordinates
    x = v .* cos.(rad)
    y = v .* sin.(rad)
    if p_grid != 0
        # Coordinates for radial grid
        xa = maximum(p_grid) * cos.(rad) * 1.1
        ya = maximum(p_grid) * sin.(rad) * 1.1
        # Coordinates for polar grid text
        radC = (rad[Int(round(l / 2))] + rad[1 + Int(round(l / 2))]) / 2.0
        xc = p_grid * cos(radC)
        yc = p_grid * sin(radC)
        for i in p_grid
            poly!(ax, Circle(Point2f(0, 0), i), color = :transparent, strokewidth=1, strokecolor=ax.xgridcolor)
        end
        text!(ax, xc, yc, text=string.(p_grid), textsize = 12, align = (:center, :baseline), color=ax.xlabelcolor)
        arrows!(ax, zeros(l), zeros(l), xa, ya, color=ax.xgridcolor, linestyle=:solid, arrowhead=' ')
        if length(labels) == l
            for i in eachindex(rad)
                text!(ax, xa[i], ya[i], text=string(labels[i]), textsize = labelsize, markerspace = :data, align = (justifyme(rad[i]), justifymeV(rad[i])), color=ax.xlabelcolor)
            end
        elseif length(labels) > 1
            printstyled("WARNING! Labels omitted:  they don't match with the points ($(length(labels)) vs $l).\n", bold=true, color=:yellow)
        end
    end
    pp = poly!(ax, [(x[i], y[i]) for i in eachindex(x)], strokecolor=RGB{Float32}(0.4, 0.4, 0.4))
    cc = to_value(pp.color)
    m_color = RGBA{Float32}(comp1(cc), comp2(cc), comp3(cc), fillalpha)
    s_color = RGB{Float32}(comp1(cc), comp2(cc), comp3(cc))
    pp.color = m_color
    pp.strokecolor = s_color
    pp.strokewidth= linewidth
    arrows!(ax, zeros(l), zeros(l), x, y, color=spokescolor, linewidth=spokeswidth, arrowhead=' ')
    if points
        scatter!(ax, x, y)
    end
    ax
end

# INPUT DATA:
Random.seed!(1789)
n = 8
R1, R2 = rand(n),  rand(n)
labels = "Cat." .* string.(1:n)

fig = Figure()
ax=Axis(fig[1,1])
radarplot(ax, R1; labels=labels, spokeswidth = 0, p_grid=0.2:0.2:1.0, labelsize=0.07)
radarplot(ax, R2; title="My Radar",  labels="", spokeswidth = 0, p_grid=0)

What is inconvenient is the label size: in order to get them always displayed in the boundigbox, and not truncated, I had to specify the size in data space (markerspace = :data), instead of in pixel space (which would allow to keep text size independent of data scaling; see documenatation page)

2 Likes

A trick which I use for the same problem in Axis3 is to calculate the label anchor positions in pixels and then plot them into the container scene of the Axis (this stretches further than the inner part). That’s also how the open PR for a polar axis implemented it I think:

1 Like

FYI, I’ve added a radar function to GMT.jl too (master)

using GMT
radar([10.5 20.5 30.6 40.9 46], axeslimts=[15, 25, 50, 90, 50],
      labels=["Spoons","Forks","Knifes","Dishes","Oranges"],
      annotall=true, marker=:circ, fill=true, show=true)

1 Like

Thanks @jules, I will look at it.
For the moment a quick (and improper) fix I am using is to set label size proportional to data and resolution: it is quite insensitive to data and resolution scaling but it works only when there is just one axis/panel:

labelsize = maximum(v) * 40 / minimum(to_value(ax.scene.parent.camera.resolution))

where v is the vector of values to plot.