Chart
Charts drawn with D3 and built from parts: a grid, axes, series, a tooltip and a legend, or a layer you draw yourself.
Installation
Shadwire follows shadcn/ui's open code model: the component's source is copied into your application and becomes yours. Install it with the CLI:
The chart registry item installs these files:
-
app/components/ui_component.rb -
app/components/ui/chart_component.rb -
app/components/ui/chart/layer_component.rb -
app/components/ui/chart/grid_component.rb -
app/components/ui/chart/x_axis_component.rb -
app/components/ui/chart/y_axis_component.rb -
app/components/ui/chart/bar_component.rb -
app/components/ui/chart/line_component.rb -
app/components/ui/chart/area_component.rb -
app/components/ui/chart/pie_component.rb -
app/components/ui/chart/radial_bar_component.rb -
app/components/ui/chart/radar_component.rb -
app/components/ui/chart/polar_grid_component.rb -
app/components/ui/chart/polar_angle_axis_component.rb -
app/components/ui/chart/label_list_component.rb -
app/components/ui/chart/tooltip_component.rb -
app/components/ui/chart/legend_component.rb -
app/helpers/ui/chart_helper.rb -
app/javascript/controllers/ui_chart_controller.js -
app/javascript/controllers/ui_chart_grid_controller.js -
app/javascript/controllers/ui_chart_axis_controller.js -
app/javascript/controllers/ui_chart_bar_controller.js -
app/javascript/controllers/ui_chart_line_controller.js -
app/javascript/controllers/ui_chart_area_controller.js -
app/javascript/controllers/ui_chart_pie_controller.js -
app/javascript/controllers/ui_chart_radial_bar_controller.js -
app/javascript/controllers/ui_chart_radar_controller.js -
app/javascript/controllers/ui_chart_polar_grid_controller.js -
app/javascript/controllers/ui_chart_polar_angle_axis_controller.js -
app/javascript/controllers/ui_chart_label_list_controller.js -
app/javascript/controllers/ui_chart_tooltip_controller.js -
app/javascript/controllers/ui_chart_legend_controller.js -
vendor/shadwire/shadwire.css
This component uses Stimulus, so the application needs importmap-rails and stimulus-rails with automatic controller loading.
Haven't installed the CLI yet? gem install shadwire, then shadwire init.
How a chart is put together
A chart is a container with parts inside it. The container, ui_chart,
holds what all the parts share: the rows, the config and the layout. Each part, such as
ui_chart_grid, ui_chart_x_axis,
ui_chart_bar or ui_chart_tooltip, draws one layer, and
the layers stack in the order you write them, like the children of a Recharts chart. You don't
pick a chart type: put bars and a line in the same container and you get a combined chart. Plain
HTML inside the container is drawn on top.
D3 does the drawing. The container's Stimulus controller, ui-chart, measures
the chart, computes the scales and stacks, and passes them to every part in a
ui-chart:render event. Each part has its own controller that draws its own
<svg> with D3. That means you can delete the files of parts you don't use,
and write new parts when you need them. Colors are CSS custom properties, so switching the theme
recolors the chart without redrawing it. D3 is only loaded on pages that have a chart.
Your first chart
This section builds a bar chart, then adds a grid, an axis, a tooltip and a legend, one part at a time.
Start with the rows
One hash per category, with whatever keys you like; each part's data_key:
says which field it reads. The rows usually come from a query. Dates and times are sent as ISO
strings, which the axes and the tooltip format for you, and a BigDecimal
arrives as a string and is read as a number.
Define the config
The config holds what the rows don't: each series' label and color. It is separate from the data so that several charts can share one config, and so the rows can come from anywhere.
Compose the chart
Add one ui_chart_bar per series. The container is
aspect-video by default; also give it a min-h-* or a
height, or it has no room to draw.
Add a grid
ui_chart_grid draws a line at each tick of the value axis.
Add an axis
The x axis labels the categories with the data_key: field. The months are
dates, so tick_format: is a d3-time-format specifier here, and the labels
read Jan, Feb… in the page's language.
Add a tooltip
Hover over a month and ui_chart_tooltip lists each series' value for that
month, under the month's full name.
Add a legend
ui_chart_legend shows the series with the labels from the config, below the
plot.
Bar chart
A grid, an axis, a tooltip and a legend around two series of bars: the chart built step by step above.
Composition
Chart config
Each key in the config is a series, or a category that a pie, a ring or a legend shows. It takes
a label:, either a color: or a theme:
with one color per theme, and optionally a Lucide icon:, which the legend and
the tooltip show instead of the color swatch.
Theming
Each color in the config becomes a --color-<key> custom property on the
chart, and every part draws with those. Set them to the theme's chart tokens and the chart
follows the theme, including dark mode.
Any CSS color works too (hex, hsl(), oklch()). The
variables are not limited to the parts: a part's own color:, a row's
fill and your own markup inside the chart can use them as well.
Tooltip
The tooltip shows a label (the category) and one row per series, with an indicator, a name and a
value. indicator: is :dot, :line or
:dashed; hide_label: and hide_indicator:
hide those. cursor: shades the category under the pointer, and
default_index: shows the tooltip for a category before anyone hovers.
label_key: and name_key: take the label and the names from
another config entry or field. Here the label is Total visitors and each name is the
browser the slice represents:
Legend
The legend lists the series (or, for a pie, its slices) with the labels and colors from the
config. With name_key: it lists the rows instead, named by that field, and
vertical_align: :top moves it above the plot.
Formatting
The axes' tick_format:, the tooltip's label_format: and
value_format: and the label lists' format: are D3
specifiers: d3-format for numbers, d3-time-format for dates. Without one, numbers are formatted
for the page's language.
Month and day names and number separators come from the app's own Rails translations
(date.month_names, date.abbr_day_names,
number.format.delimiter and so on), so an app in Portuguese gets
Fev and 1.234,5 without configuring the chart.
A layer of your own
The built-in parts work the same way as one you write yourself. ui_chart_layer
adds an <svg> over the chart and connects it to your own Stimulus controller,
which receives the chart's scales and draws with D3. This one draws a goal line:
A part receives three events, in this order. event.detail.chart contains the
rows, the config, the plot area, the category and value scales, the polar geometry, a formatter
and d3 itself.
Accessibility
label: gives the chart its accessible name. The drawing is hidden from
assistive technology, and the legend is plain text. When the chart has a tooltip, it can take
focus: the arrow keys, Home and End move between categories,
Escape closes the tooltip, and screen readers announce what it shows. If the user
prefers reduced motion, nothing animates.
Examples
Horizontal bars
A vertical layout runs the months down the y axis. Two label lists inside the bars print the month and the value.
Stacked bars
Bars with the same stack id are stacked, and the radius only rounds the outer corners of each stack.
Lines
A natural curve and a step, both with dots, under a tooltip with a line indicator.
Stacked areas
Three months of daily rows under a gradient. The dates are formatted in the page's language, and labels that would overlap are skipped.
Donut
A pie with an inner radius and plain HTML in the middle: layers can be mixed with HTML.
Radar
Two radars over a polar grid, with the months around them.
Radial bars
A ring per browser over a muted track, labelled where each ring starts.
Tooltip indicators
The dot, line and dashed indicators, each showing Tuesday before anyone hovers.
A layer of your own
A goal line drawn on the chart's value scale by a Stimulus controller from the app itself.
API reference
Every part accepts class / class_name and **attrs.
| Component | Argument | Default | Description |
|---|---|---|---|
Chart |
config |
{} |
The series, and the categories a pie or a legend shows, as { key: { label:, color:, icon: } }, or theme: { light:, dark: } in place of color:. Each color becomes --color-<key> on the chart. |
Chart |
rows |
[] |
The data, one hash per category. It is called rows: rather than data: so that data: still sets HTML data attributes. |
Chart |
layout |
:horizontal |
:horizontal runs the categories along the x axis; :vertical down the y axis, for horizontal bars. |
Chart |
stack_offset |
:none |
D3's offset for the series that share a stack_id:: :none, :expand (to 100%), :diverging, :silhouette or :wiggle. |
Chart |
margin |
{} |
Space around the plot, in pixels per side: { left: 12, right: 12 }. The axes and the legend add their own space on top of this. |
Chart |
inner_radius |
0 |
The inner radius of pies, rings and radars: in pixels, or as a percentage of the available space ("30%"). |
Chart |
outer_radius |
"80%" |
Their outer radius, in the same units. |
Chart |
start_angle |
0 |
Where pies, rings and radars start, in degrees clockwise from twelve o'clock. |
Chart |
end_angle |
360 |
Where they end, also in degrees clockwise from twelve o'clock. |
Chart |
label |
nil |
The chart's accessible name. |
Chart::Layer |
controller |
nil |
The Stimulus controller that draws the layer. It receives ui-chart:render, with the chart's scales in event.detail.chart. |
Chart::Layer |
options |
{} |
Whatever that controller needs, as event.detail.options. |
Chart::Grid |
horizontal |
nil |
Draws the horizontal lines. If neither is set, only the lines across the value axis are drawn. |
Chart::Grid |
vertical |
nil |
Draws the vertical lines, with the same default. |
Chart::XAxis |
data_key |
nil |
On the category axis, the field whose values label the categories and the tooltip. |
Chart::XAxis |
hide |
false |
Hides the axis but keeps its categories. |
Chart::XAxis |
tick_line |
false |
Draws a tick at each label. |
Chart::XAxis |
axis_line |
false |
Draws the axis line. |
Chart::XAxis |
tick_margin |
8 |
Pixels between the plot and the labels. |
Chart::XAxis |
tick_count |
5 |
About how many ticks the value axis gets. |
Chart::XAxis |
tick_format |
nil |
A d3-format specifier for numbers ("~s", "$,.0f") or a d3-time-format one for dates ("%b %d"). |
Chart::XAxis |
min_tick_gap |
5 |
The minimum space between two category labels, in pixels; labels that would overlap are skipped. |
Chart::XAxis |
domain |
nil |
The value axis' [min, max]; nil at either end leaves that end to the data. |
Chart::XAxis |
height |
nil |
The space the axis takes, in pixels. If unset, it is measured from the labels. |
Chart::YAxis |
data_key |
nil |
On the category axis (the y axis in a vertical layout), the field whose values label the categories and the tooltip. |
Chart::YAxis |
hide |
false |
Hides the axis but keeps its categories. |
Chart::YAxis |
tick_line |
false |
Draws a tick at each label. |
Chart::YAxis |
axis_line |
false |
Draws the axis line. |
Chart::YAxis |
tick_margin |
8 |
Pixels between the plot and the labels. |
Chart::YAxis |
tick_count |
5 |
About how many ticks the value axis gets. |
Chart::YAxis |
tick_format |
nil |
A d3-format specifier for numbers ("~s", "$,.0f") or a d3-time-format one for dates ("%b %d"). |
Chart::YAxis |
min_tick_gap |
5 |
The minimum space between two category labels, in pixels; labels that would overlap are skipped. |
Chart::YAxis |
domain |
nil |
The value axis' [min, max]; nil at either end leaves that end to the data. |
Chart::YAxis |
width |
nil |
The space the axis takes, in pixels. If unset, it is measured from the labels. |
Chart::Bar |
data_key |
— |
The field each bar shows. |
Chart::Bar |
color |
nil |
The bars' color, overriding the config's. |
Chart::Bar |
stack_id |
nil |
Bars with the same stack_id stack; the others stand side by side. |
Chart::Bar |
radius |
0 |
The corners' radius in pixels: one number, or [top_left, top_right, bottom_right, bottom_left]. |
Chart::Line |
data_key |
— |
The field the line runs through. |
Chart::Line |
color |
nil |
The line's color, overriding the config's. |
Chart::Line |
curve |
:natural |
D3's curve: :natural, :linear, :monotone, :step, :step_before, :step_after, :basis, :cardinal or :catmull_rom. |
Chart::Line |
stroke_width |
2 |
The line's width, in pixels. |
Chart::Line |
dot |
false |
Marks every point. |
Chart::Line |
connect_nulls |
false |
Runs across a missing value instead of breaking there. |
Chart::Area |
data_key |
— |
The field the area rises to. |
Chart::Area |
color |
nil |
The area's color, overriding the config's. |
Chart::Area |
stack_id |
nil |
Areas with the same stack_id stack. |
Chart::Area |
curve |
:natural |
D3's curve, as on the line. |
Chart::Area |
fill_opacity |
0.4 |
The fill's opacity, from 0 to 1. |
Chart::Area |
gradient |
false |
Fills with a fade of the color, strongest at the top. |
Chart::Area |
stroke_width |
1 |
The edge's width, in pixels. |
Chart::Area |
dot |
false |
Marks every point. |
Chart::Area |
connect_nulls |
false |
Runs across a missing value instead of breaking there. |
Chart::Pie |
data_key |
— |
The field each slice's size comes from. |
Chart::Pie |
name_key |
nil |
The field naming each slice: the config key that labels and colors it. |
Chart::Pie |
inner_radius |
nil |
Overrides the chart's: pixels or a percentage. Anything above zero makes a donut. |
Chart::Pie |
outer_radius |
nil |
Overrides the chart's. |
Chart::Pie |
padding_angle |
0 |
A gap between the slices, in degrees. |
Chart::Pie |
corner_radius |
0 |
Rounds the slices' corners, in pixels. |
Chart::Pie |
stroke_width |
0 |
A stroke between the slices in the background color; class: "stroke-card" matches a card. |
Chart::RadialBar |
data_key |
— |
The field each ring's sweep comes from. |
Chart::RadialBar |
name_key |
nil |
The field naming each ring, which then takes its config entry's label and color. |
Chart::RadialBar |
color |
nil |
The rings' color, overriding the config's. |
Chart::RadialBar |
stack_id |
nil |
Radial bars with the same stack_id stack along their rings. |
Chart::RadialBar |
background |
false |
Draws the rest of each ring as a muted track. |
Chart::RadialBar |
corner_radius |
0 |
Rounds the rings' ends, in pixels. |
Chart::Radar |
data_key |
— |
The field each corner's distance comes from. |
Chart::Radar |
color |
nil |
The polygon's color, overriding the config's. |
Chart::Radar |
fill_opacity |
0.6 |
The fill's opacity, from 0 to 1; 0 leaves the outline alone. |
Chart::Radar |
stroke_width |
0 |
Outlines the polygon, in pixels. |
Chart::Radar |
dot |
false |
Marks every corner. |
Chart::PolarGrid |
grid_type |
:polygon |
:polygon, rings through the spokes, or :circle. |
Chart::PolarGrid |
radial_lines |
true |
Draws a spoke per category. |
Chart::PolarAngleAxis |
data_key |
nil |
The field whose values label the spokes and the tooltip. |
Chart::PolarAngleAxis |
tick_format |
nil |
A d3 specifier for the labels. |
Chart::PolarAngleAxis |
tick_margin |
8 |
Pixels between the outer ring and the labels. |
Chart::LabelList |
data_key |
nil |
The field to print; unset, the value. A field that names a config entry prints the entry's label. |
Chart::LabelList |
position |
nil |
Where each label goes: :top, :bottom, :left, :right, :center, and :inside_top, :inside_bottom, :inside_left or :inside_right on bars; :outside, :inside or :inside_start on slices and rings. |
Chart::LabelList |
offset |
5 |
Pixels between a label and its point. |
Chart::LabelList |
format |
nil |
A d3 specifier for the labels. |
Chart::Tooltip |
indicator |
:dot |
The mark beside each name: :dot, :line or :dashed. |
Chart::Tooltip |
hide_label |
false |
Leaves the label out. |
Chart::Tooltip |
hide_indicator |
false |
Leaves the indicators out. |
Chart::Tooltip |
label_key |
nil |
Takes the label from this config entry or field instead of the category. |
Chart::Tooltip |
name_key |
nil |
Takes each name from this field of the row, through the config. |
Chart::Tooltip |
label_format |
nil |
A d3 specifier for the label ("%B"). |
Chart::Tooltip |
value_format |
nil |
A d3 specifier for the values (",d"). |
Chart::Tooltip |
cursor |
true |
Shades the category the pointer is on. |
Chart::Tooltip |
default_index |
nil |
Shows the tooltip for this category before anyone hovers. |
Chart::Legend |
name_key |
nil |
Lists each row, named by this field, instead of each series. |
Chart::Legend |
hide_icon |
false |
Shows the color swatch even when the config has an icon. |
Chart::Legend |
vertical_align |
:bottom |
The edge it sits at: :bottom or :top. |