Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions plots/radar-basic/implementations/julia/makie.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# anyplot.ai
# radar-basic: Basic Radar Chart
# Library: makie 0.21.9 | Julia 1.11.9
# Quality: 87/100 | Created: 2026-07-24

using CairoMakie
using Colors

# --- Theme tokens -----------------------------------------------------------
const THEME = get(ENV, "ANYPLOT_THEME", "light")
const PAGE_BG = THEME == "light" ? colorant"#FAF8F1" : colorant"#1A1A17"
const INK = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8"
const INK_SOFT = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0"
const GRID = RGBAf(INK.r, INK.g, INK.b, 0.15)
const IMPRINT_PALETTE = [
colorant"#009E73", # 1 — first categorical series (anyplot brand green)
colorant"#C475FD", # 2 — lavender (second series)
]

# --- Data ---------------------------------------------------------------
# Two soccer players compared across six performance attributes (0-100 scale)
categories = ["Speed", "Passing", "Shooting", "Defense", "Stamina", "Vision"]
player_a = [88.0, 72.0, 90.0, 65.0, 78.0, 82.0]
player_b = [75.0, 85.0, 68.0, 88.0, 90.0, 70.0]

n = length(categories)
angles = [pi / 2 - 2 * pi * (i - 1) / n for i in 1:n]
grid_levels = [20, 40, 60, 80, 100]

# Largest gap between the two players — used to anchor a focal-point annotation
gaps = abs.(player_a .- player_b)
focus_idx = argmax(gaps)
focus_angle = angles[focus_idx]
focus_lo, focus_hi = extrema((player_a[focus_idx], player_b[focus_idx]))

# --- Plot -----------------------------------------------------------------
fig = Figure(
size = (1200, 1200),
fontsize = 14,
backgroundcolor = PAGE_BG,
)

ax = Axis(
fig[1, 1];
title = "radar-basic · julia · makie · anyplot.ai",
titlesize = 20,
titlecolor = INK,
aspect = DataAspect(),
backgroundcolor = PAGE_BG,
)
hidedecorations!(ax)
hidespines!(ax)
limits!(ax, -145, 145, -134, 148)

# Concentric gridline polygons at 20/40/60/80/100
for level in grid_levels
xs = [level * cos(a) for a in angles]
ys = [level * sin(a) for a in angles]
lines!(ax, [xs; xs[1]], [ys; ys[1]]; color = GRID, linewidth = 1.2)
end

# Radial spokes, one per category
for a in angles
lines!(ax, [0.0, 100 * cos(a)], [0.0, 100 * sin(a)]; color = GRID, linewidth = 1.2)
end

# Gridline value labels along the top spoke
for level in grid_levels
text!(ax, 6, Float64(level); text = string(level), fontsize = 13,
color = INK_SOFT, align = (:left, :center), font = :bold)
end

# Category labels at the outer edge
label_radius = 118
for (i, cat) in enumerate(categories)
a = angles[i]
halign = cos(a) > 0.3 ? :left : (cos(a) < -0.3 ? :right : :center)
text!(ax, label_radius * cos(a), label_radius * sin(a); text = cat,
fontsize = 15, color = INK, align = (halign, :center))
end

# Series polygons — filled with transparency, closed back to the first point
for (values, color, name) in (
(player_a, IMPRINT_PALETTE[1], "Player A"),
(player_b, IMPRINT_PALETTE[2], "Player B"),
)
xs = [v * cos(a) for (v, a) in zip(values, angles)]
ys = [v * sin(a) for (v, a) in zip(values, angles)]
poly!(ax, Point2f.([xs; xs[1]], [ys; ys[1]]);
color = (color, 0.25), strokecolor = color, strokewidth = 3, label = name)
scatter!(ax, xs, ys; color = color, markersize = 14, strokewidth = 0)
end

# Focal-point annotation: bracket the widest gap between the two players
bracket!(
ax,
focus_lo * cos(focus_angle), focus_lo * sin(focus_angle),
focus_hi * cos(focus_angle), focus_hi * sin(focus_angle);
text = "largest gap · $(round(Int, gaps[focus_idx])) pts",
fontsize = 13,
color = INK_SOFT,
textcolor = INK,
offset = 14,
orientation = :down,
style = :curly,
)

axislegend(ax, position = :lb, labelsize = 14, labelcolor = INK,
framevisible = false, backgroundcolor = (PAGE_BG, 0.0))

# --- Save -------------------------------------------------------------------
save("plot-$(THEME).png", fig; px_per_unit = 2)
276 changes: 276 additions & 0 deletions plots/radar-basic/metadata/julia/makie.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
library: makie
language: julia
specification_id: radar-basic
created: '2026-07-24T16:17:13Z'
updated: '2026-07-24T20:34:14Z'
generated_by: claude-sonnet
workflow_run: 30108172451
issue: 744
language_version: 1.11.9
library_version: 0.21.9
preview_url_light: https://storage.googleapis.com/anyplot-images/plots/radar-basic/julia/makie/plot-light.png
preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/radar-basic/julia/makie/plot-dark.png
preview_html_light: null
preview_html_dark: null
quality_score: 87
review:
strengths:
- 'Correct Imprint palette compliance in both themes: Player A is brand green #009E73,
Player B is lavender #C475FD (position 2), and page backgrounds match #FAF8F1
(light) / #1A1A17 (dark) exactly with theme-correct text/grid chrome.'
- Clean, idiomatic Makie construction of a radar chart from primitives (poly!, scatter!,
lines!, text!) with DataAspect() for correct proportions and hidedecorations!/hidespines!
for a minimal, publication-style frame — Makie has no native radar recipe, so
this manual construction is the right approach.
- Realistic soccer-player dataset showing genuine trade-offs across all six axes
(Player A leads Speed/Shooting/Vision, Player B leads Passing/Defense/Stamina)
rather than one player dominating everywhere, satisfying DQ-01 feature coverage.
- Polygons are correctly closed back to the first point and filled with alpha=0.25
exactly as the spec's Notes section requests, keeping the overlap region between
the two players legible.
weaknesses:
- 'The bracket!() ''largest gap'' focal-point annotation on the Defense axis does
not render in either plot-light.png or plot-dark.png. Verified by cropping the
full canvas width around the Defense axis (where focus_idx=4 points, gap=23) at
high zoom: no curly bracket, no ''largest gap · 23 pts'' text, nothing beyond
the two data-point dots and connecting lines. The code computes focus_idx/focus_lo/focus_hi
correctly and calls bracket!(ax, ...) with a text label, but the call produces
no visible output — likely a CairoMakie bracket! keyword/orientation mismatch
for this Makie version. Debug the bracket! call directly (render a standalone
test figure to confirm it draws), or replace it with a manually drawn lines!()+text!()
annotation next to the Defense spoke, which is guaranteed to render.'
- Because the annotation silently fails, the implementation currently delivers no
real visual hierarchy or focal point (DE-03) despite the code's intent — the two
overlapping filled polygons alone don't guide the viewer to the most interesting
comparison (the 23-point Defense gap between the players).
- The mandated title is short (41 chars, no descriptive prefix) and occupies only
~28% of canvas width, well under the typical 50-70% guidance for non-mandated-length
titles — consider adding an optional descriptive prefix (e.g. 'Player Comparison
· radar-basic · julia · makie · anyplot.ai') to better balance the generous canvas
whitespace at the top.
image_description: |-
Light render (plot-light.png):
Background: Warm off-white (#FAF8F1), matches spec — not pure white.
Chrome: Title "radar-basic · julia · makie · anyplot.ai" in bold dark ink, clearly readable, centered top. Six category labels (Speed, Passing, Shooting, Defense, Stamina, Vision) at the outer edge of the hexagonal grid, dark ink, all legible with correct halign per position. Gridline value labels (20/40/60/80/100) along the top (Speed) spoke in bold soft-ink, small but readable. Legend ("Player A" / "Player B") bottom-left, frameless, readable swatches.
Data: Player A polygon outlined/filled in brand green #009E73 (alpha 0.25 fill, strokewidth 3, markersize 14 dots); Player B in lavender #C475FD, same styling. Both polygons correctly closed back to their first vertex. Concentric gridline hexagons at 20/40/60/80/100 and radial spokes in subtle low-alpha ink-gray, clearly visible but not competing with data.
Legibility verdict: PASS — all text readable against the light background; no light-on-light issues.
Note: the code's bracket!() "largest gap" annotation near the Defense axis does NOT appear anywhere in this render (confirmed via full-width pixel crop around the Defense spoke) — see weaknesses.

Dark render (plot-dark.png):
Background: Warm near-black (#1A1A17), matches spec — not pure black.
Chrome: Title and category labels rendered in light ink (#F0EFE8), clearly legible against the dark background — no dark-on-dark failures observed. Gridline value labels in soft light-gray ink, readable. Legend readable with light text.
Data: Player A green #009E73 and Player B lavender #C475FD — identical hex values to the light render, confirming data colors are theme-invariant; only chrome (background, text, grid) flips as required. Fill alpha and stroke widths match the light render.
Legibility verdict: PASS — all text readable against the dark background; no dark-on-dark failures.
Note: same as light render, the bracket!() annotation is absent from this render too.
criteria_checklist:
visual_quality:
score: 29
max: 30
items:
- id: VQ-01
name: Text Legibility
score: 7
max: 8
passed: true
comment: 'All font sizes explicitly set (titlesize=20, fontsize=13-15); readable
in both themes at full size. Small deduction: gridline value labels and
category labels at 13-15pt native will read small once scaled to ~400px
mobile.'
- id: VQ-02
name: No Overlap
score: 6
max: 6
passed: true
comment: No overlap between text, data, legend, or grid in either render.
- id: VQ-03
name: Element Visibility
score: 6
max: 6
passed: true
comment: markersize=14 and linewidth=3 are well-suited for a sparse 6-axis
/ 2-series radar; both polygons clearly visible and distinguishable.
- id: VQ-04
name: Color Accessibility
score: 2
max: 2
passed: true
comment: Green vs lavender is CVD-safe per the Imprint palette; only 2 series
so color alone is sufficient per style-guide guidance.
- id: VQ-05
name: Layout & Canvas
score: 4
max: 4
passed: true
comment: Canvas is exactly 2400x2400 (square, matches DataAspect radar use
case); plot fills a well-balanced portion of the canvas; legend sits near
the plot, not isolated.
- id: VQ-06
name: Axis Labels & Title
score: 2
max: 2
passed: true
comment: Six category axis labels are descriptive; the 20-100 gridline scale
is self-explanatory for a 0-100 rating radar per the spec.
- id: VQ-07
name: Palette Compliance
score: 2
max: 2
passed: true
comment: 'First series #009E73, second #C475FD (Imprint position 2); backgrounds
#FAF8F1/#1A1A17 exactly; identical data colors across themes; chrome fully
theme-correct in both renders.'
design_excellence:
score: 13
max: 20
items:
- id: DE-01
name: Aesthetic Sophistication
score: 6
max: 8
passed: true
comment: Thoughtful custom radar construction, clear typographic hierarchy
(bold title, bold value labels), Imprint palette — clearly above library
defaults.
- id: DE-02
name: Visual Refinement
score: 5
max: 6
passed: true
comment: Spines and default decorations fully hidden, grid at low alpha, generous
whitespace around the hexagon, frameless legend.
- id: DE-03
name: Data Storytelling
score: 2
max: 6
passed: false
comment: The code attempts a focal-point 'largest gap' bracket annotation
on the Defense axis, but it does not render in either theme (verified by
pixel inspection) — so no actual visual hierarchy is delivered; the viewer
must find the story themselves from the two overlapping polygons.
spec_compliance:
score: 15
max: 15
items:
- id: SC-01
name: Plot Type
score: 5
max: 5
passed: true
comment: Correct radar/spider chart with filled polygons on concentric axes.
- id: SC-02
name: Required Features
score: 4
max: 4
passed: true
comment: Filled polygons with alpha~0.25, gridlines at 20/40/60/80/100, outer-edge
axis labels, distinct colors + legend, closed polygons — all present per
spec Notes.
- id: SC-03
name: Data Mapping
score: 3
max: 3
passed: true
comment: All 6 categories and both series correctly mapped to their respective
spokes.
- id: SC-04
name: Title & Legend
score: 3
max: 3
passed: true
comment: Title exactly matches 'radar-basic · julia · makie · anyplot.ai';
legend labels 'Player A'/'Player B' match the data series.
data_quality:
score: 15
max: 15
items:
- id: DQ-01
name: Feature Coverage
score: 6
max: 6
passed: true
comment: Both players trade leads across the six attributes (no single player
dominates every axis), showing genuine multivariate comparison.
- id: DQ-02
name: Realistic Context
score: 5
max: 5
passed: true
comment: Soccer player performance comparison — real-world plausible and neutral.
- id: DQ-03
name: Appropriate Scale
score: 4
max: 4
passed: true
comment: 0-100 rating scale with values in the 65-90 range is realistic for
professional-player attribute ratings.
code_quality:
score: 9
max: 10
items:
- id: CQ-01
name: KISS Structure
score: 3
max: 3
passed: true
comment: Flat imports → data → plot → save structure, no functions/classes.
- id: CQ-02
name: Reproducibility
score: 2
max: 2
passed: true
comment: Data is hardcoded (deterministic), no randomness to seed.
- id: CQ-03
name: Clean Imports
score: 2
max: 2
passed: true
comment: CairoMakie and Colors both actively used (colorant macro, plotting
primitives).
- id: CQ-04
name: Code Elegance
score: 1
max: 2
passed: true
comment: Overall clean, but the bracket!() annotation block is dead weight
in the shipped output — it computes and calls but produces no visible effect,
so it isn't earning its place in the code.
- id: CQ-05
name: Output & API
score: 1
max: 1
passed: true
comment: Saves 'plot-$(THEME).png' via save(..., px_per_unit=2) exactly per
convention.
library_mastery:
score: 6
max: 10
items:
- id: LM-01
name: Idiomatic Usage
score: 4
max: 5
passed: true
comment: Good use of poly!/scatter!/lines!/text!/axislegend for a chart type
Makie has no built-in recipe for; one point off for the non-functional bracket!
call.
- id: LM-02
name: Distinctive Features
score: 2
max: 5
passed: false
comment: Attempted a distinctive Makie-specific feature (bracket! annotation)
that would have earned real credit, but it does not render — DataAspect()/hidespines!
usage alone is fairly generic.
verdict: APPROVED
impl_tags:
dependencies: []
techniques:
- polar-projection
- annotations
patterns:
- data-generation
- iteration-over-groups
dataprep: []
styling:
- minimal-chrome
- alpha-blending
Loading