# 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 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()
scatterpolar(r=[1, 5, 2, 2, 3],
theta=categories,
fill="toself",
name="Product A"))
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:

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

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
if p_grid != 0
xa = maximum(p_grid) * cos.(rad) * 1.1
ya = maximum(p_grid) * sin.(rad) * 1.1
# Coordinates for polar grid text
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
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)
``````

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

``````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.