Radial Bar Chart
A radial bar chart with bars wrapped around a circular axis.
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
<%# One angular bar per browser row over a muted background track -
the shadcn "simple" radial block. %>
<% config = {
visitors: { label: "Visitors" },
chrome: { label: "Chrome", color: "var(--chart-1)" },
safari: { label: "Safari", color: "var(--chart-2)" },
firefox: { label: "Firefox", color: "var(--chart-3)" },
edge: { label: "Edge", color: "var(--chart-4)" },
other: { label: "Other", color: "var(--chart-5)" }
}
data = [
{ browser: "chrome", visitors: 275, fill: "var(--color-chrome)" },
{ browser: "safari", visitors: 200, fill: "var(--color-safari)" },
{ browser: "firefox", visitors: 187, fill: "var(--color-firefox)" },
{ browser: "edge", visitors: 173, fill: "var(--color-edge)" },
{ browser: "other", visitors: 90, fill: "var(--color-other)" }
] %>
<div class="w-full max-w-xl">
<%= poetry_chart(:radial, data: data, config: config, id: "radial-default",
name_key: :browser, inner_radius: 30, outer_radius: 110) do |chart| %>
<% chart.with_radial_bar(data_key: :visitors, background: true) %>
<% chart.with_tooltip %>
<% end %>
</div>
Grid
<%# Polar-grid circles mark every ring boundary - the shadcn "grid"
radial block. %>
<% config = {
visitors: { label: "Visitors" },
chrome: { label: "Chrome", color: "var(--chart-1)" },
safari: { label: "Safari", color: "var(--chart-2)" },
firefox: { label: "Firefox", color: "var(--chart-3)" },
edge: { label: "Edge", color: "var(--chart-4)" },
other: { label: "Other", color: "var(--chart-5)" }
}
data = [
{ browser: "chrome", visitors: 275, fill: "var(--color-chrome)" },
{ browser: "safari", visitors: 200, fill: "var(--color-safari)" },
{ browser: "firefox", visitors: 187, fill: "var(--color-firefox)" },
{ browser: "edge", visitors: 173, fill: "var(--color-edge)" },
{ browser: "other", visitors: 90, fill: "var(--color-other)" }
] %>
<div class="w-full max-w-xl">
<%= poetry_chart(:radial, data: data, config: config, id: "radial-grid",
name_key: :browser, inner_radius: 30, outer_radius: 100) do |chart| %>
<% chart.with_polar_grid %>
<% chart.with_radial_bar(data_key: :visitors) %>
<% end %>
</div>
Label
<%# insideStart labels ride each arc a few degrees into the sweep;
the -90..380 span is the shadcn "label" block's overshoot spiral. %>
<% config = {
visitors: { label: "Visitors" },
chrome: { label: "Chrome", color: "var(--chart-1)" },
safari: { label: "Safari", color: "var(--chart-2)" },
firefox: { label: "Firefox", color: "var(--chart-3)" },
edge: { label: "Edge", color: "var(--chart-4)" },
other: { label: "Other", color: "var(--chart-5)" }
}
data = [
{ browser: "chrome", visitors: 275, fill: "var(--color-chrome)" },
{ browser: "safari", visitors: 200, fill: "var(--color-safari)" },
{ browser: "firefox", visitors: 187, fill: "var(--color-firefox)" },
{ browser: "edge", visitors: 173, fill: "var(--color-edge)" },
{ browser: "other", visitors: 90, fill: "var(--color-other)" }
] %>
<div class="w-full max-w-xl">
<%= poetry_chart(:radial, data: data, config: config, id: "radial-label",
name_key: :browser, start_angle: -90, end_angle: 380,
inner_radius: 30, outer_radius: 110) do |chart| %>
<% chart.with_radial_bar(data_key: :visitors, background: true,
labels: :inside_start, label_key: :browser) %>
<% end %>
</div>
Shape
<%# The gauge look: disc tracks from with_polar_grid(radii:, fills:) plus
a center label - the shadcn "shape" radial block. %>
<% config = {
visitors: { label: "Visitors" },
safari: { label: "Safari", color: "var(--chart-2)" }
}
data = [{ browser: "safari", visitors: 1260, fill: "var(--color-safari)" }] %>
<div class="w-full max-w-xl">
<%= poetry_chart(:radial, data: data, config: config, id: "radial-shape",
name_key: :browser, end_angle: 100,
inner_radius: 65, outer_radius: 95) do |chart| %>
<% chart.with_polar_grid(radii: [86, 74], fills: %i[muted background]) %>
<% chart.with_radial_bar(data_key: :visitors, background: true) %>
<% chart.with_center_label(title: "1,260", subtitle: "Visitors") %>
<% end %>
</div>
Stacked
<%# Two series share the ring and stack along the ANGLE (stack: :a). The
domain is [0, dataMax] over the raw values (recharts' default), so
mobile fills 570/1260 = 81deg and desktop stacks on and clips at the
180deg end - the shadcn "stacked" radial block. compact: the smaller
center number that sits above the flat baseline. %>
<% config = {
desktop: { label: "Desktop", color: "var(--chart-1)" },
mobile: { label: "Mobile", color: "var(--chart-2)" }
}
data = [{ month: "january", desktop: 1260, mobile: 570 }] %>
<div class="w-full max-w-xl">
<%= poetry_chart(:radial, data: data, config: config, id: "radial-stacked",
name_key: :month, end_angle: 180,
inner_radius: 80, outer_radius: 110) do |chart| %>
<% chart.with_radial_bar(data_key: :mobile, stack: :a, corner_radius: 5) %>
<% chart.with_radial_bar(data_key: :desktop, stack: :a, corner_radius: 5) %>
<% chart.with_center_label(title: "1,830", subtitle: "Visitors", compact: true) %>
<% end %>
</div>
Text
<%# A thin 250-degree gauge with pill caps (corner_radius: 10 on a 10px
ring) and center text - the shadcn "text" radial block. %>
<% config = {
visitors: { label: "Visitors" },
safari: { label: "Safari", color: "var(--chart-2)" }
}
data = [{ browser: "safari", visitors: 200, fill: "var(--color-safari)" }] %>
<div class="w-full max-w-xl">
<%= poetry_chart(:radial, data: data, config: config, id: "radial-text",
name_key: :browser, start_angle: 0, end_angle: 250,
inner_radius: 80, outer_radius: 90) do |chart| %>
<% chart.with_polar_grid(radii: [90, 80], fills: %i[muted background]) %>
<% chart.with_radial_bar(data_key: :visitors, background: true, corner_radius: 10) %>
<% chart.with_center_label(title: "200", subtitle: "Visitors") %>
<% end %>
</div>
API
Poetry::Charts::RadialBarChart::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 - name => { label:, color: } - naming and coloring the rings. |
| data: | Object | required | The rows to plot: an array of hashes, one ring per row. |
| end_angle: | Integer | defaults to 360 |
Where the sweep ends - 180 makes a half gauge. |
| 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. | |
| inner_radius: | Object | defaults to "20%" |
The innermost ring edge: a percent string of the max radius, or pixels. |
| 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. | |
| max_value: | Object | The angle-axis maximum: nil = the largest single value, so the largest ring closes the full sweep EXACTLY (nicing it would leave a notch); stacked gauges pass the stack total when the segments should fill the span. | |
| name_key: | String | defaults to "name" |
The row key naming each ring. |
| outer_radius: | Object | defaults to "80%" |
The outermost ring edge: a percent string of the max radius, or pixels. |
| start_angle: | Integer | defaults to 0 |
Where the sweep starts, in degrees. |
| width: | Integer | defaults to 250 |
ViewBox width in pixels; the rendered chart scales to its container. |
Slots
| Writer | Description |
|---|---|
| with_center_label | The center text: a title line plus an optional subtitle. The default is the full gauge's big centered number; compact: shrinks it and sits it just above a half gauge's flat baseline. |
| with_legend | The legend row: align:, items:, and hide_icon:. |
| with_polar_grid | The disc track behind the rings: radii: places the circles (default: each ring's centerline), fills: tints them, and radial_lines: draws the faint value spokes that show through ring gaps and the open wedge (on by default; gauges turn them off). |
| with_radial_bar | A radial series reading data_key: values. background: draws the muted track ring; bars sharing a stack: id share the ring and stack by angle; corner_radius: rounds the arc ends. |
| with_tooltip | The hover tooltip; the ring name carries the label, so hide_label defaults on. |
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-radial-series] | One series' ring group (<g>) |
| [data-slot=chart-radial-background] | The muted track ring (<path>) behind a bar, rendered when background: true |
| [data-slot=chart-radial-bar] | One angular bar (<path>) - a ring per data row, sweep proportional to value |
| [data-slot=chart-labels] | A series' value labels (<g> of <text>, aria-hidden), rendered when the series opts into labels |
| [data-slot=chart-center-label] | The center text (<text>) - title tspan plus optional subtitle filling the chart's middle |
| [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-radial-series | data-key | always - the series key | — |
| chart-radial-bar | data-key | always - the series key | — |
| chart-radial-bar | data-index | on the first series' bars - the datum index the tooltip walks | — |
| chart-radial-bar | data-active | the hovered/arrow-keyed bar - the tooltip controller reflects the active index here at runtime | — |
| chart-radial-bar | data-motion-group | when animate - the motion rig's sweep group (one per ring) | — |
| chart-radial-bar | data-motion-sector | when animate - the motion rig's server-computed sector params for the fan-out sweep | — |
| chart-labels | data-key | always - the series key | — |
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) |
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 |