Charts
poetry-charts computes every chart in Ruby on the server and ships the finished SVG in the initial HTML. There is no client chart engine to load: the geometry pipeline runs in Ruby, and the browser only adds interactivity on top of coordinates the server already embedded.
What it is
The chart tier of the Poetry family. Data goes in as an array of hashes; Ruby derives domains, scales, ticks, points, and paths, and the chart arrives as SVG markup styled by the same theme tokens as every other Poetry component. Nine chart families ship in the gallery: area, bar, line, pie, radar, radial bar, scatter, and composed, plus the adapter mount for bring-your-own engines. A small Stimulus layer supplies the tooltip, legend, and hover states by reading server-embedded coordinates, so no chart math runs in the browser.
Install
Add the gem next to poetry-ui and let the install generator wire it:
# Gemfile
gem "poetry-charts"
bundle install
bin/rails g poetry:install --charts
--charts imports the chart stylesheet into your CSS
build and registers the controllers. Wiring by hand is two steps:
the engine merges the @poetry/charts importmap pins
automatically, so you register the controllers in your Stimulus
entrypoint and import the motion stylesheet in your CSS build. Skip
the stylesheet and charts render static.
import { registerPoetryChartsControllers } from "@poetry/charts"
registerPoetryChartsControllers(application)
@import "poetry-charts/app/assets/stylesheets/poetry-charts.css";
Basic usage
A chart takes data: (one hash per x category) and
config: (label and color per series), then composes
declaratively from slots: grid, axes, series, tooltip, legend.
data = [
{ month: "January", desktop: 186, mobile: 80 },
{ month: "February", desktop: 305, mobile: 200 },
{ month: "March", desktop: 237, mobile: 120 }
]
config = {
desktop: { label: "Desktop", color: "var(--chart-1)" },
mobile: { label: "Mobile", color: "var(--chart-2)" }
}
<%= poetry_area_chart(id: "visits", data: data, config: config) do |chart| %>
<% chart.with_grid %>
<% chart.with_x_axis data_key: :month %>
<% chart.with_area data_key: :desktop, stack: :a %>
<% chart.with_area data_key: :mobile, stack: :a %>
<% chart.with_tooltip %>
<% chart.with_legend %>
<% end %>
poetry_area_chart, poetry_line_chart, and
poetry_bar_chart are dedicated helpers; every family is
also reachable through the poetry_chart(type, ...)
dispatcher with :area, :line,
:bar, :pie, :radar,
:radial, :scatter, or
:composed. The chrome has helpers of its own:
poetry_chart_container,
poetry_chart_tooltip_content, and
poetry_chart_legend_content.
<%= poetry_chart(:pie, data: browsers, config: config) do |chart| %>
<% chart.with_pie data_key: :visitors, name_key: :browser, inner_radius: 60 %>
<% chart.with_tooltip %>
<% end %>
Works wherever HTML renders
Because the finished chart is already in the markup, it stays valid
with JavaScript disabled, in print stylesheets, in generated PDFs,
and in HTML email. Entrance animation is on by default and runs in
CSS between server-computed states; pass animate: false
for a static chart, and users with reduced motion always get the
finished chart in the initial paint.
Interactive charts are real forms: submit a filter and the chart
re-renders on the server. Give a chart a stable id: and
it morphs between those renders, starting from the old geometry and
tweening to the new one; a shape change replays the entrance
instead. The interactive filter demo
shows the round trip, and the
stable IDs guide covers the id
contract.
Live updates
For data that cannot round-trip through a form submit, the cartesian
trio (area, line, bar) takes live: true: the chart
embeds its spec payload plus a client renderer proven byte-equal to
the Ruby engine, and redraws in place when new data arrives, either
by updating the embedded payload (what a
turbo_stream.update does) or through an update event on
the chart frame.
<%= poetry_line_chart(id: "visits-live", data: data, config: config, live: true) do |chart| %>
<% chart.with_x_axis data_key: :month %>
<% chart.with_line data_key: :desktop %>
<% chart.with_tooltip %>
<% end %>
Updates tween through the same motion machinery and the tooltip keeps
serving fresh values mid-stream. Live data carries pre-formatted
category strings, since formatter lambdas cannot ride a JSON payload;
the server raises a teaching error if you try. Live mode also unlocks
the client-side window: with_brush and
zoom: true slice the data in the browser. See the
live streaming,
synced tooltips, and
brush and zoom demos.
Bring your own engine
Every chart also compiles to a closed, versioned chart-spec. Pass
engine: and the same call routes to the adapter path:
the server embeds the frozen spec v1 next to a mount element, and a
client adapter registered under that name draws into it. An adapter
is a duck type over the spec (render, update, destroy), so any
engine fits per chart, canvas engines included for very large
datasets. The adapter path takes series: and
axes: arguments in place of slots.
<%= poetry_chart :bar, engine: "chartjs", data: data, config: config,
series: [{ data_key: :desktop }, { data_key: :mobile }],
axes: { x: { data_key: :month } } %>
The adapter guide documents the reference adapter, the registration call, and the full adapter protocol.
The theme palette
Series colors are theme decisions, not chart arguments. The active
theme defines the
--chart-1 through --chart-5 ramp, your
config points series at those variables, and the chart container
emits a --color-<key> variable per series for the
SVG to consume. Switching themes or flipping dark mode restyles
every chart with zero re-render.
:root {
--chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815);
}