Radar Chart
A radar chart for comparing several variables on radial axes.
Installation
Charts ship in the separate, optional poetry-charts
gem — not bundled with poetry-ui. If you
don't have it yet, add it and wire it into your app:
bundle add poetry-charts
bin/rails g poetry:install --charts
Default
<% data = [
{ month: "January", desktop: 186 },
{ month: "February", desktop: 305 },
{ month: "March", desktop: 237 },
{ month: "April", desktop: 73 },
{ month: "May", desktop: 209 },
{ month: "June", desktop: 214 }
]
config = { desktop: { label: "Desktop", color: "var(--chart-1)" } } %>
<div class="w-full max-w-xl">
<%= poetry_chart(:radar, data: data, config: config, id: "radar-default") do |chart| %>
<% chart.with_angle_axis(data_key: :month) %>
<% chart.with_grid %>
<% chart.with_radar(data_key: :desktop) %>
<% chart.with_tooltip(indicator: :line) %>
<% end %>
</div>
Dots
<%# dots: true marks every vertex (r 4, solid). %>
<% data = [
{ month: "January", desktop: 186 },
{ month: "February", desktop: 305 },
{ month: "March", desktop: 237 },
{ month: "April", desktop: 73 },
{ month: "May", desktop: 209 },
{ month: "June", desktop: 214 }
]
config = { desktop: { label: "Desktop", color: "var(--chart-1)" } } %>
<div class="w-full max-w-xl">
<%= poetry_chart(:radar, data: data, config: config, id: "radar-dots") do |chart| %>
<% chart.with_angle_axis(data_key: :month) %>
<% chart.with_grid %>
<% chart.with_radar(data_key: :desktop, dots: true) %>
<% end %>
</div>
Grid circle
<%# type: :circle swaps the polygon rings for circles. %>
<% data = [
{ month: "January", desktop: 186 },
{ month: "February", desktop: 305 },
{ month: "March", desktop: 237 },
{ month: "April", desktop: 73 },
{ month: "May", desktop: 209 },
{ month: "June", desktop: 214 }
]
config = { desktop: { label: "Desktop", color: "var(--chart-1)" } } %>
<div class="w-full max-w-xl">
<%= poetry_chart(:radar, data: data, config: config, id: "radar-grid-circle") do |chart| %>
<% chart.with_angle_axis(data_key: :month) %>
<% chart.with_grid(type: :circle) %>
<% chart.with_radar(data_key: :desktop, dots: true) %>
<% end %>
</div>
Grid fill
<%# fill: :desktop tints the grid disc with that series' color (opacity 0.2). %>
<% data = [
{ month: "January", desktop: 186 },
{ month: "February", desktop: 305 },
{ month: "March", desktop: 237 },
{ month: "April", desktop: 73 },
{ month: "May", desktop: 209 },
{ month: "June", desktop: 214 }
]
config = { desktop: { label: "Desktop", color: "var(--chart-1)" } } %>
<div class="w-full max-w-xl">
<%= poetry_chart(:radar, data: data, config: config, id: "radar-grid-fill") do |chart| %>
<% chart.with_angle_axis(data_key: :month) %>
<% chart.with_grid(fill: :desktop) %>
<% chart.with_radar(data_key: :desktop) %>
<% end %>
</div>
Grid none
<%# No with_grid slot: just the angle axis labels and the shape. %>
<% data = [
{ month: "January", desktop: 186 },
{ month: "February", desktop: 305 },
{ month: "March", desktop: 237 },
{ month: "April", desktop: 73 },
{ month: "May", desktop: 209 },
{ month: "June", desktop: 214 }
]
config = { desktop: { label: "Desktop", color: "var(--chart-1)" } } %>
<div class="w-full max-w-xl">
<%= poetry_chart(:radar, data: data, config: config, id: "radar-grid-none") do |chart| %>
<% chart.with_angle_axis(data_key: :month) %>
<% chart.with_radar(data_key: :desktop, dots: true) %>
<% end %>
</div>
Legend
<% data = [
{ month: "January", desktop: 186, mobile: 80 },
{ month: "February", desktop: 305, mobile: 200 },
{ month: "March", desktop: 237, mobile: 120 },
{ month: "April", desktop: 73, mobile: 190 },
{ month: "May", desktop: 209, mobile: 130 },
{ month: "June", desktop: 214, mobile: 140 }
]
config = { desktop: { label: "Desktop", color: "var(--chart-1)" },
mobile: { label: "Mobile", color: "var(--chart-2)" } } %>
<div class="w-full max-w-xl">
<%= poetry_chart(:radar, data: data, config: config, id: "radar-legend") do |chart| %>
<% chart.with_angle_axis(data_key: :month) %>
<% chart.with_grid %>
<% chart.with_radar(data_key: :desktop) %>
<% chart.with_radar(data_key: :mobile, fill_opacity: 1) %>
<% chart.with_legend %>
<% end %>
</div>
Lines only
<%# Lines only: fill_opacity: 0 drops the area, stroke_width: 2 keeps the
outline readable; radial_lines: false hides the grid spokes. %>
<% data = [
{ month: "January", desktop: 186, mobile: 80 },
{ month: "February", desktop: 305, mobile: 200 },
{ month: "March", desktop: 237, mobile: 120 },
{ month: "April", desktop: 73, mobile: 190 },
{ month: "May", desktop: 209, mobile: 130 },
{ month: "June", desktop: 214, mobile: 140 }
]
config = { desktop: { label: "Desktop", color: "var(--chart-1)" },
mobile: { label: "Mobile", color: "var(--chart-2)" } } %>
<div class="w-full max-w-xl">
<%= poetry_chart(:radar, data: data, config: config, id: "radar-lines-only") do |chart| %>
<% chart.with_angle_axis(data_key: :month) %>
<% chart.with_grid(radial_lines: false) %>
<% chart.with_radar(data_key: :desktop, fill_opacity: 0, stroke_width: 2) %>
<% chart.with_radar(data_key: :mobile, fill_opacity: 0, stroke_width: 2) %>
<% end %>
</div>
Multiple
<% data = [
{ month: "January", desktop: 186, mobile: 80 },
{ month: "February", desktop: 305, mobile: 200 },
{ month: "March", desktop: 237, mobile: 120 },
{ month: "April", desktop: 73, mobile: 190 },
{ month: "May", desktop: 209, mobile: 130 },
{ month: "June", desktop: 214, mobile: 140 }
]
config = { desktop: { label: "Desktop", color: "var(--chart-1)" },
mobile: { label: "Mobile", color: "var(--chart-2)" } } %>
<div class="w-full max-w-xl">
<%= poetry_chart(:radar, data: data, config: config, id: "radar-multiple") do |chart| %>
<% chart.with_angle_axis(data_key: :month) %>
<% chart.with_grid %>
<% chart.with_radar(data_key: :desktop) %>
<%# Only the first series is translucent (0.6); the second stays opaque. %>
<% chart.with_radar(data_key: :mobile, fill_opacity: 1) %>
<% end %>
</div>
API
Poetry::Charts::RadarChart::Component — options are
constructor keywords (the poetry_* helper forwards them);
slots are composed inside the block. Generated from the gem's source documentation.
| Option | Type | Details | Description |
|---|---|---|---|
| config: | Object | required | The series config - key => { label:, color: } - naming and coloring every series. |
| data: | Object | required | The rows to plot: an array of hashes, one per category. |
| height: | Integer | defaults to 250 |
ViewBox height in pixels. |
| id: | String | Explicit DOM id token, stable across renders; otherwise the chart gets a unique per-render id. | |
| label: | String | Accessible name for the chart SVG; defaults to one built from the configured series. | |
| margin: | Object | Margin overrides ({ top:, right:, bottom:, left: }), merged over the slim polar default. | |
| outer_radius: | Object | defaults to "80%" |
The rim radius: a percent string of the max radius, or pixels. |
| width: | Integer | defaults to 250 |
ViewBox width in pixels; the rendered chart scales to its container. |
Slots
| Writer | Description |
|---|---|
| with_angle_axis | The category labels around the rim: data_key: names the field; tick_formatter: reshapes each label. |
| with_grid | The polar grid: type: :circle swaps polygons for circles; radial_lines: false drops the spokes; fill: tints every ring with a series color at opacity:. |
| with_legend | The legend row: align:, items:, and hide_icon:. |
| with_radar | A radar series bound to data_key:. fill_opacity: 0 with stroke_width: 2 draws lines only; dots: marks every vertex. |
| with_tooltip | The hover tooltip - multi-series rows under the category label. |
Styling
Every part carries a stable data-slot attribute — target
[data-slot=…] from your own CSS to restyle it. State rides
data attributes on the parts below. This contract is verified against rendered DOM in CI.
| Part | Description |
|---|---|
| [data-slot=chart-svg] | The chart canvas (<svg>) - the aria-label surface, the tooltip's focus/keyboard surface (role=application when it attaches), and the motion rig's mount |
| [data-slot=chart-polar-grid] | The polar grid (<g>, aria-hidden) - ring and spoke linework behind the series |
| [data-slot=chart-radars] | The series layer (<g>) - every radar polygon renders here; the CSS entrance scales this group from the polar center |
| [data-slot=chart-radar] | One series' closed polygon (<path>) - config color, 0.6 fill by default |
| [data-slot=chart-dots] | A series' vertex dots (<g>), rendered when dots: true |
| [data-slot=chart-dot] | One vertex dot (<circle>) - solid, at the series color |
| [data-slot=chart-angle-axis] | The category labels around the rim (<g> of <text>) |
| [data-slot=chart-hit-wedges] | The tooltip's hit layer (<g>), rendered when the tooltip attaches |
| [data-slot=chart-hit-wedge] | One per-category hit wedge (<path>, transparent but painted so it hit-tests) - the tooltip's hover target |
| [data-slot=chart-coordinates] | The embedded JSON payload (<script>) the tooltip controller reads - per-category anchors and pre-formatted values, zero chart math in the browser |
State attributes
| Part | Attribute | Condition | Values |
|---|---|---|---|
| chart-svg | data-animate | when animate (the default) - the entrance tier's flag the motion stylesheet and controller key off | — |
| chart-svg | data-motion | runtime, when animate - the motion engine's lifecycle stamp | entrance · morph · settled |
| chart-radar | data-key | always - the series key | — |
| chart-dots | data-key | always - the series key | — |
| chart-hit-wedge | data-index | always - the datum index | — |
| chart-hit-wedge | data-active | the hovered/arrow-keyed category - the tooltip controller reflects the active index here at runtime | — |
CSS variables
| Part | Variable | Description |
|---|---|---|
| chart-svg | --poetry-motion-delay | the motion rig's entrance delay (animation_begin) |
| chart-svg | --poetry-motion-duration | the motion rig's entrance duration (animation_duration) |
| chart-svg | --poetry-motion-easing | the motion rig's easing keyword (animation_easing) |
| chart-svg | --poetry-motion-center | the polar center the CSS entrance scales the series from |
Wiring
The Stimulus surface each element carries — declared in the component, verified against rendered DOM in CI. Bare actions fire on the element's default event.
| Element | Controller | Wiring |
|---|---|---|
| frame | poetry--charts--tooltip (if tooltip?) | registers · value sync (if) |
| frame | poetry--charts--motion (if animate?) | registers |
| svg | poetry--charts--tooltip (if tooltip?) | move on pointermove · leave on pointerleave · focus on focus · blur on blur · keydown on keydown · target svg |
| svg | poetry--charts--tooltip (if tooltip?) | enter on pointerover |
| coordinates | poetry--charts--tooltip (if tooltip?) | target data |
| tooltip_layer | poetry--charts--tooltip (if tooltip?) | target tooltip |