railsui_charts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +50 -0
- data/LICENSE.md +21 -0
- data/README.md +540 -0
- data/Rakefile +34 -0
- data/app/assets/stylesheets/railsui_charts.css +710 -0
- data/app/controllers/railsui_charts/demo_controller.rb +27 -0
- data/app/javascript/controllers/railsui_chart_controller.js +438 -0
- data/app/javascript/controllers/railsui_chart_filters_controller.js +14 -0
- data/app/javascript/controllers/railsui_metric_dialog_controller.js +27 -0
- data/app/views/railsui_charts/demo/index.html.erb +45 -0
- data/config/routes.rb +4 -0
- data/lib/generators/railsui_charts/install/install_generator.rb +52 -0
- data/lib/generators/railsui_charts/install/templates/railsui_chart_controller.js +438 -0
- data/lib/generators/railsui_charts/install/templates/railsui_chart_filters_controller.js +14 -0
- data/lib/generators/railsui_charts/install/templates/railsui_metric_dialog_controller.js +27 -0
- data/lib/railsui_charts/apex_options_builder.rb +1027 -0
- data/lib/railsui_charts/chart_helper.rb +195 -0
- data/lib/railsui_charts/configuration.rb +89 -0
- data/lib/railsui_charts/engine.rb +19 -0
- data/lib/railsui_charts/filter_helper.rb +90 -0
- data/lib/railsui_charts/filters.rb +130 -0
- data/lib/railsui_charts/interval.rb +110 -0
- data/lib/railsui_charts/metric_helper.rb +219 -0
- data/lib/railsui_charts/version.rb +5 -0
- data/lib/railsui_charts.rb +26 -0
- metadata +86 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7c8dc823b54699d34aa3086a00c101d2af00be11f62c1934ce61cb2d47fca6be
|
|
4
|
+
data.tar.gz: 1b3e956f025a5121406c9a11d66d17b43208e43fde6afd445a965bc2ffccb612
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7834d2e0790b218314615c6948fa6e59c0280a3f806d528fc931edc721383bc31ff24cb4344f3716fc269b7f8c697fdace6044d9ef3ecfcbe9461e303b371a77
|
|
7
|
+
data.tar.gz: 2425d97d2acd25b4147d37b31319cc83842f1db83d46d3d15582bed61dc212aef0c1dce0c5a46425a9b0e14874a6541c93fe7d1fb31440aaf53dc137de9e82cb
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. This project follows
|
|
4
|
+
[Semantic Versioning](https://semver.org). While the version is below 1.0 the
|
|
5
|
+
public API may change between minor versions.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0]
|
|
10
|
+
|
|
11
|
+
First release.
|
|
12
|
+
|
|
13
|
+
### Charts
|
|
14
|
+
|
|
15
|
+
- `railsui_chart` renders line, area, bar, column, sparkline, pie, donut,
|
|
16
|
+
scatter, bubble, radar, polar area, and range bar charts from a hash, an
|
|
17
|
+
array, or a set of named series
|
|
18
|
+
- Combo charts, where each series names its own type, and a second y-axis for
|
|
19
|
+
series measured in different units
|
|
20
|
+
- Range bars take `from:`/`to:` pairs, so a timeline is expressible without
|
|
21
|
+
arithmetic in the view
|
|
22
|
+
|
|
23
|
+
### Metrics and layout
|
|
24
|
+
|
|
25
|
+
- `railsui_metric_card` for a single value with an optional delta and sparkline
|
|
26
|
+
- `railsui_small_multiples` for one shape repeated across several series
|
|
27
|
+
- Filter rows that read and write query parameters, with interval bucketing
|
|
28
|
+
handled by `RailsuiCharts::Interval`
|
|
29
|
+
|
|
30
|
+
### States
|
|
31
|
+
|
|
32
|
+
- Empty, loading, and error states for every chart type, so a view does not have
|
|
33
|
+
to decide what to render when a query comes back with nothing
|
|
34
|
+
|
|
35
|
+
### Theming
|
|
36
|
+
|
|
37
|
+
- Every colour, type size, and piece of geometry is a CSS custom property or a
|
|
38
|
+
Ruby setting, so charts follow the host application's palette, including dark
|
|
39
|
+
mode
|
|
40
|
+
- The categorical palette is validated against colour-vision deficiency, a
|
|
41
|
+
chroma floor, and a 3:1 contrast requirement in both light and dark
|
|
42
|
+
|
|
43
|
+
### Accessibility
|
|
44
|
+
|
|
45
|
+
- Every chart ships a visually hidden data table alongside it, so no value is
|
|
46
|
+
reachable only by hovering a mark
|
|
47
|
+
- Animation stops when the reader has asked for reduced motion
|
|
48
|
+
|
|
49
|
+
[Unreleased]: https://github.com/getrailsui/railsui_charts/compare/v0.1.0...HEAD
|
|
50
|
+
[0.1.0]: https://github.com/getrailsui/railsui_charts/releases/tag/v0.1.0
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andy Leverenz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
# Rails UI Charts
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
|
|
5
|
+
Production-ready chart components for Rails. Built on [ApexCharts](https://apexcharts.com), wrapped in Rails-native helpers, and designed for Tailwind CSS.
|
|
6
|
+
|
|
7
|
+
**Live demo:** [railsui.com/charts](https://railsui.com/charts)
|
|
8
|
+
|
|
9
|
+
**Preview everything locally:** the live demo above renders every supported chart type. To see them in your own app after installing, drop this into any view:
|
|
10
|
+
|
|
11
|
+
```erb
|
|
12
|
+
<%= railsui_chart [{ x: "Jan", y: 10 }, { x: "Feb", y: 20 }], type: :line %>
|
|
13
|
+
<%= railsui_chart [{ x: "A", y: 30 }, { x: "B", y: 50 }], type: :bar %>
|
|
14
|
+
<%= railsui_chart [{ x: "A", y: 30 }, { x: "B", y: 50 }], type: :pie %>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or clone the [railsui.com site](https://github.com/justalever/railsui_app) and visit `/charts` for the full showcase.
|
|
18
|
+
|
|
19
|
+
## Why Rails UI Charts?
|
|
20
|
+
|
|
21
|
+
AI can generate a first-draft chart in seconds. The hard part is the last 20%: accessibility, responsive behavior, dark mode, Turbo lifecycle support, loading and empty states, and a stable API that does not break when you upgrade.
|
|
22
|
+
|
|
23
|
+
Rails UI Charts gives you that polish as a drop-in Rails component.
|
|
24
|
+
|
|
25
|
+
- **Rails-native** — plain ERB helpers, Stimulus controllers, no React required
|
|
26
|
+
- **Accessible** — every chart renders a screen-reader-friendly data table
|
|
27
|
+
- **Tailwind-themed** — colors controlled by CSS variables
|
|
28
|
+
- **Turbo-ready** — charts initialize and destroy correctly inside Turbo Frames and Streams
|
|
29
|
+
- **AI-friendly** — clear API and documented conventions so coding assistants use the canonical component instead of inventing their own
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Add to your Gemfile:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
gem "railsui_charts"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then run:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bundle install
|
|
43
|
+
rails g railsui_charts:install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The generator adds the CSS import and copies the Stimulus controller. You still need ApexCharts in your JavaScript:
|
|
47
|
+
|
|
48
|
+
**Build mode:**
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
yarn add apexcharts
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**No-build (importmap):**
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
# config/importmap.rb
|
|
58
|
+
pin "apexcharts", to: "https://esm.sh/apexcharts@3.45.2"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
All charts use the same `railsui_chart` helper. Change the `type:` to switch chart kinds.
|
|
64
|
+
|
|
65
|
+
### Line chart
|
|
66
|
+
|
|
67
|
+
```erb
|
|
68
|
+
<%= railsui_chart @daily_signups, type: :line %>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Area chart
|
|
72
|
+
|
|
73
|
+
```erb
|
|
74
|
+
<%= railsui_chart @monthly_revenue,
|
|
75
|
+
type: :area,
|
|
76
|
+
label: "Revenue",
|
|
77
|
+
format: :currency %>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Column chart
|
|
81
|
+
|
|
82
|
+
```erb
|
|
83
|
+
<%= railsui_chart @plans,
|
|
84
|
+
type: :column,
|
|
85
|
+
label: "Customers" %>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Horizontal bar chart
|
|
89
|
+
|
|
90
|
+
```erb
|
|
91
|
+
<%= railsui_chart @plans,
|
|
92
|
+
type: :bar,
|
|
93
|
+
label: "Customers" %>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Pie chart
|
|
97
|
+
|
|
98
|
+
```erb
|
|
99
|
+
<%= railsui_chart @plan_distribution, type: :pie %>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Donut chart
|
|
103
|
+
|
|
104
|
+
```erb
|
|
105
|
+
<%= railsui_chart @plan_distribution, type: :donut %>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Scatter chart
|
|
109
|
+
|
|
110
|
+
```erb
|
|
111
|
+
<%= railsui_chart @experiments, type: :scatter %>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Bubble chart
|
|
115
|
+
|
|
116
|
+
```erb
|
|
117
|
+
<%= railsui_chart @market_segments,
|
|
118
|
+
type: :bubble,
|
|
119
|
+
label: "Segments" %>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Data points accept an optional `:z` value for bubble size:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
[{ x: 10, y: 20, z: 15 }, { x: 25, y: 35, z: 30 }]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Radar chart
|
|
129
|
+
|
|
130
|
+
```erb
|
|
131
|
+
<%= railsui_chart @feature_scores, type: :radar, label: "Score" %>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Polar area chart
|
|
135
|
+
|
|
136
|
+
```erb
|
|
137
|
+
<%= railsui_chart @traffic_sources, type: :polar_area %>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Sparkline
|
|
141
|
+
|
|
142
|
+
```erb
|
|
143
|
+
<%= railsui_chart @page_views, type: :sparkline %>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Several series
|
|
147
|
+
|
|
148
|
+
Pass an array of `{ name:, data: }` instead of a bare series. The `data` key is what tells the two apart.
|
|
149
|
+
|
|
150
|
+
```erb
|
|
151
|
+
<%= railsui_chart [
|
|
152
|
+
{ name: "Starter", data: @starter },
|
|
153
|
+
{ name: "Pro", data: @pro },
|
|
154
|
+
{ name: "Enterprise", data: @enterprise }
|
|
155
|
+
], type: :column %>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Each series takes the next palette slot in order, and two or more always carry a legend — colour is never the only thing telling them apart.
|
|
159
|
+
|
|
160
|
+
### Combo and dual axis
|
|
161
|
+
|
|
162
|
+
Give a series its own `type:` and the chart draws more than one shape. Give it `axis: :right` and it gets its own scale.
|
|
163
|
+
|
|
164
|
+
```erb
|
|
165
|
+
<%= railsui_chart [
|
|
166
|
+
{ name: "Revenue", data: @revenue, type: :column, format: :short_currency },
|
|
167
|
+
{ name: "Churn rate", data: @churn, type: :line, axis: :right, format: :percentage }
|
|
168
|
+
] %>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
There is no `type: :combo` to remember — a series naming a type is what makes the chart mixed.
|
|
172
|
+
|
|
173
|
+
`format:` on a series dresses its own axis and its own row in the tooltip, so money and percentages read in their own units. Both scales are cut into the same number of intervals so their gridlines land on each other, and each is fitted to its own values. A side carrying columns reaches zero; a side carrying only lines does not, since forcing zero onto a rate hovering near 3% flattens it against the top of the plot.
|
|
174
|
+
|
|
175
|
+
Axis labels take the colour of the series they measure. With two scales, position alone does not say which belongs to which.
|
|
176
|
+
|
|
177
|
+
### Timelines and ranges
|
|
178
|
+
|
|
179
|
+
`:range_bar` plots spans rather than points. Pass `from:` and `to:` with whatever `Time` or `Date` you already have — Apex wants milliseconds, and handing it a `Time` gives "Invalid Date" rather than an error.
|
|
180
|
+
|
|
181
|
+
```erb
|
|
182
|
+
<%= railsui_chart [
|
|
183
|
+
{ x: "web", from: deploy.started_at, to: deploy.finished_at },
|
|
184
|
+
{ x: "api", from: incident.began_at, to: incident.resolved_at }
|
|
185
|
+
], type: :range_bar %>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Rows sharing a label stack onto one lane. A bare two-element `y` works too, for a range that is not about time.
|
|
189
|
+
|
|
190
|
+
### Stacking
|
|
191
|
+
|
|
192
|
+
```erb
|
|
193
|
+
<%= railsui_chart @plans, type: :column, stacked: true %>
|
|
194
|
+
<%= railsui_chart @plans, type: :column, stacked: :percent %>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Stacking answers "what is this made of over time", which a grouped chart cannot. `:percent` switches from totals to share. Segments separate with a 2px gap in the surface colour rather than a stroke, so the divider never reads as data.
|
|
198
|
+
|
|
199
|
+
### Small multiples
|
|
200
|
+
|
|
201
|
+
```erb
|
|
202
|
+
<%= railsui_small_multiples @plans, type: :area, columns: 3 %>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
One small chart per series, **sharing a y-scale**. This is the honest answer when there are more categories than a single chart can hold: eight lines on one axis is a plate of spaghetti, and a ninth colour is not distinguishable from the others anyway. Facets scale where colour does not.
|
|
206
|
+
|
|
207
|
+
Every facet takes the same colour, because the title carries identity — spending a hue on it would say nothing extra. The shared scale is the point: left to themselves, each facet would fit its own data and a small series would look like a large one.
|
|
208
|
+
|
|
209
|
+
### Comparing against a previous period
|
|
210
|
+
|
|
211
|
+
Pass `compare:` a second series and it rides underneath the first as a dashed,
|
|
212
|
+
muted line on the **same axis** — never a second y-scale.
|
|
213
|
+
|
|
214
|
+
```erb
|
|
215
|
+
<%= railsui_chart @this_year,
|
|
216
|
+
type: :area,
|
|
217
|
+
label: "This year",
|
|
218
|
+
compare: @last_year,
|
|
219
|
+
compare_label: "Last year",
|
|
220
|
+
format: :short_currency %>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Supported for `:line`, `:area`, `:column`, and `:sparkline`.
|
|
224
|
+
|
|
225
|
+
### Metric
|
|
226
|
+
|
|
227
|
+
A compact label / value / delta stack with an optional sparkline.
|
|
228
|
+
|
|
229
|
+
```erb
|
|
230
|
+
<%= railsui_metric
|
|
231
|
+
label: "Monthly revenue",
|
|
232
|
+
value: 48_290,
|
|
233
|
+
change: 12.4,
|
|
234
|
+
format: :currency,
|
|
235
|
+
history: @monthly_revenue %>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Metric card
|
|
239
|
+
|
|
240
|
+
The full dashboard card: label, value, delta, previous-period line, comparison
|
|
241
|
+
chart, and footer. The delta is computed from `value` and `previous`.
|
|
242
|
+
|
|
243
|
+
```erb
|
|
244
|
+
<%= railsui_metric_card
|
|
245
|
+
label: "MRR",
|
|
246
|
+
value: 18_450,
|
|
247
|
+
previous: 17_200,
|
|
248
|
+
format: :currency,
|
|
249
|
+
history: @mrr_this_period,
|
|
250
|
+
compare: @mrr_last_period,
|
|
251
|
+
updated_at: "Updated 1 second ago",
|
|
252
|
+
details_path: dashboard_path %>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Direction and *goodness* are separate. A falling churn rate is a win, so pass
|
|
256
|
+
`positive_is_good: false` and the negative delta reads green:
|
|
257
|
+
|
|
258
|
+
```erb
|
|
259
|
+
<%= railsui_metric_card label: "Churn rate", value: 2.4, previous: 2.8,
|
|
260
|
+
format: :percentage, positive_is_good: false, history: @churn %>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Time series data
|
|
264
|
+
|
|
265
|
+
`railsui_chart` takes a grouped hash straight from the database:
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
@revenue = Order.paid.where(created_at: range)
|
|
269
|
+
.group("TO_CHAR(created_at, 'Mon')").sum(:total)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
```erb
|
|
273
|
+
<%= railsui_chart @revenue, type: :column, format: :short_currency %>
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Note that a `GROUP BY` only returns rows that exist, so a month with no orders
|
|
277
|
+
is simply absent and the axis quietly shortens. Filling those gaps —
|
|
278
|
+
along with bucketing, summing rows that collapse together, and labelling — is
|
|
279
|
+
what `RailsuiChartsPro::TimeSeries` does. See [Full access](#full-access).
|
|
280
|
+
|
|
281
|
+
## Filters
|
|
282
|
+
|
|
283
|
+
Filters belong in one row above the charts, never inside a chart card — a per-card date picker invites two cards to disagree about what "this week" means.
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
def dashboard
|
|
287
|
+
@filters = RailsuiCharts::Filters.new(params)
|
|
288
|
+
@signups = Signup.where(created_at: @filters.range)
|
|
289
|
+
.group("TO_CHAR(created_at, 'Mon DD')").count
|
|
290
|
+
end
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
```erb
|
|
294
|
+
<%= turbo_frame_tag "dashboard" do %>
|
|
295
|
+
<%= railsui_chart_filters @filters, url: dashboard_path, frame: "dashboard" %>
|
|
296
|
+
<%= railsui_metric_card label: "Signups", value: @signups.values.sum, history: @signups %>
|
|
297
|
+
<% end %>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
It submits as a plain GET form, so every slice is a shareable URL and the page still works with JavaScript off. With Turbo, only the frame re-renders.
|
|
301
|
+
|
|
302
|
+
`Filters` resolves the window and refuses combinations that do not read — hourly buckets across twelve months is 8,760 points nobody can look at, so it falls back to the preset's own interval.
|
|
303
|
+
|
|
304
|
+
| Method | Returns |
|
|
305
|
+
|---|---|
|
|
306
|
+
| `range` | the selected date range |
|
|
307
|
+
| `interval` | `:hour`, `:day`, `:week`, or `:month` |
|
|
308
|
+
| `compare?` | whether a comparison was requested |
|
|
309
|
+
| `previous_range` | the equal-length window immediately before, or `nil` |
|
|
310
|
+
| `summary` | `["Last 7 days", "Daily", "Compared to previous period"]` |
|
|
311
|
+
|
|
312
|
+
Presets: last 24 hours, 7 days, 30 days, 90 days, 12 months, and month to date.
|
|
313
|
+
|
|
314
|
+
`previous_range` returns a *range*, not data — run your query again with it. Inventing the previous period's numbers is not the library's job.
|
|
315
|
+
|
|
316
|
+
## Supported chart types
|
|
317
|
+
|
|
318
|
+
| Type | Description |
|
|
319
|
+
|------|-------------|
|
|
320
|
+
| `:line` | Smooth line chart |
|
|
321
|
+
| `:area` | Gradient-filled area chart |
|
|
322
|
+
| `:bar` | Horizontal bar chart |
|
|
323
|
+
| `:column` | Vertical column chart |
|
|
324
|
+
| `:pie` | Pie chart with legend |
|
|
325
|
+
| `:donut` | Donut chart with legend |
|
|
326
|
+
| `:scatter` | X/Y scatter plot |
|
|
327
|
+
| `:bubble` | Bubble chart with size-encoded values |
|
|
328
|
+
| `:radar` | Radar / spider chart |
|
|
329
|
+
| `:polar_area` | Polar area chart |
|
|
330
|
+
| `:sparkline` | Tiny line chart for metric cards |
|
|
331
|
+
| `:range_bar` | Spans on an axis: timelines, Gantt, durations |
|
|
332
|
+
|
|
333
|
+
## Data formats
|
|
334
|
+
|
|
335
|
+
Charts accept an array of values, arrays of `[x, y]`, or hashes with `:x` and `:y` keys:
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
railsui_chart [10, 20, 30]
|
|
339
|
+
railsui_chart [["Jan", 10], ["Feb", 20]]
|
|
340
|
+
railsui_chart [{ x: "Jan", y: 10 }, { x: "Feb", y: 20 }]
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Formatting
|
|
344
|
+
|
|
345
|
+
Format y-axis labels and tooltips with the `format:` option:
|
|
346
|
+
|
|
347
|
+
```erb
|
|
348
|
+
<%= railsui_chart @monthly_revenue, type: :area, format: :currency %>
|
|
349
|
+
<%= railsui_chart @growth, type: :line, format: :percentage %>
|
|
350
|
+
<%= railsui_chart @page_views, type: :line, format: :human %>
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Supported formats: `:currency`, `:short_currency`, `:percentage`, `:human`, and `:number` (default). Currency uses the configured currency symbol (`$` by default). `:short_currency` renders compact axis labels like `$19K`.
|
|
354
|
+
|
|
355
|
+
## Axis options
|
|
356
|
+
|
|
357
|
+
```erb
|
|
358
|
+
<%= railsui_chart @data, type: :line,
|
|
359
|
+
axis: :right, # hang the scale on the right, Stripe-style
|
|
360
|
+
edge_labels: true, # label only the first and last x tick
|
|
361
|
+
curve: "smooth" %> # default is "straight"
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Styling
|
|
365
|
+
|
|
366
|
+
Colors are controlled by CSS variables. Override them in your Tailwind CSS or custom stylesheet:
|
|
367
|
+
|
|
368
|
+
```css
|
|
369
|
+
:root {
|
|
370
|
+
--rui-chart-primary: #6366f1;
|
|
371
|
+
--rui-chart-muted: #94a3b8;
|
|
372
|
+
--rui-chart-grid: rgba(148, 163, 184, 0.22);
|
|
373
|
+
--rui-chart-text: #64748b;
|
|
374
|
+
--rui-chart-surface: #ffffff;
|
|
375
|
+
--rui-chart-positive: #047857;
|
|
376
|
+
--rui-chart-negative: #b91c1c;
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Type is themed the same way. These reach ApexCharts as CSS strings, so the controller resolves them against the chart's own element — which means setting one on a card scopes it to that card's charts:
|
|
381
|
+
|
|
382
|
+
```css
|
|
383
|
+
:root {
|
|
384
|
+
--rui-chart-font-family: inherit;
|
|
385
|
+
--rui-chart-font-size: 12px; /* axis and data labels */
|
|
386
|
+
--rui-chart-font-size-sm: 11px; /* legend, and under 640px */
|
|
387
|
+
--rui-chart-text-size: 0.8125rem; /* tooltip rows, metric labels, tables */
|
|
388
|
+
--rui-chart-value-size: 1.375rem; /* the metric card headline */
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Geometry cannot ride that channel. ApexCharts does arithmetic on a border radius and a stroke width, and a resolved CSS variable arrives as a string — `"4" + 1` is `"41"`. Those live in an initializer:
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
RailsuiCharts.configure do |config|
|
|
396
|
+
config.geometry[:bar_radius] = 4
|
|
397
|
+
config.geometry[:stroke_width] = 2
|
|
398
|
+
config.geometry[:marker_size] = 0
|
|
399
|
+
config.geometry[:marker_hover_size] = 6
|
|
400
|
+
end
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Dark mode is detected via `prefers-color-scheme`, a `dark` class, or a `data-theme="dark"` attribute on the document element, and charts re-render when it changes.
|
|
404
|
+
|
|
405
|
+
### Categorical colors
|
|
406
|
+
|
|
407
|
+
Charts that show several categories at once (pie, donut, polar area, radar, bubble) draw from eight numbered slots:
|
|
408
|
+
|
|
409
|
+
```css
|
|
410
|
+
:root {
|
|
411
|
+
--rui-chart-series-1: #6366f1;
|
|
412
|
+
--rui-chart-series-2: #ea580c;
|
|
413
|
+
/* … through --rui-chart-series-8 */
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Slots are assigned in order and **never cycled** — a fifth category takes slot 5, not slot 1 again.
|
|
418
|
+
|
|
419
|
+
The default order is not a style choice. It was picked by validating every ordering of these hues against the lightness band, chroma floor, colorblind separation (protanopia and deuteranopia), a normal-vision floor, and 3:1 contrast, in both light and dark. If you swap in your own brand hues, re-validate rather than assuming the guarantees carry over.
|
|
420
|
+
|
|
421
|
+
Forms where any two marks sit side by side — pie, donut, polar area, scatter, bubble — hold to a stricter all-pairs test that these hues clear for the **first four slots**. Past four categories, fold the tail into an "Other" bucket or switch to a bar chart rather than adding a ninth hue.
|
|
422
|
+
|
|
423
|
+
## Tooltips
|
|
424
|
+
|
|
425
|
+
Charts render their own tooltip rather than Apex's. It leads with the metric and how much it moved, then lists dated rows with values right-aligned, and it draws from CSS variables so it follows the theme instead of being a dark slab on a light page.
|
|
426
|
+
|
|
427
|
+
Three options control what it says:
|
|
428
|
+
|
|
429
|
+
```erb
|
|
430
|
+
<%= railsui_chart @revenue, type: :line,
|
|
431
|
+
tooltip_heading: :category, # :series or :category — defaults to :series when comparing
|
|
432
|
+
tooltip_delta: false, # hide the change badge
|
|
433
|
+
tooltip_style: false %> # hand the tooltip back to Apex entirely
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Appearance is CSS variables, so a tooltip can be restyled without touching the cards around it:
|
|
437
|
+
|
|
438
|
+
```css
|
|
439
|
+
:root {
|
|
440
|
+
--rui-chart-tooltip-bg: #ffffff;
|
|
441
|
+
--rui-chart-tooltip-text: #111827;
|
|
442
|
+
--rui-chart-tooltip-muted: #6b7280;
|
|
443
|
+
--rui-chart-tooltip-border: rgba(17, 24, 39, 0.14);
|
|
444
|
+
--rui-chart-tooltip-radius: 0.5rem;
|
|
445
|
+
--rui-chart-tooltip-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
The change badge uses `--rui-chart-positive` and `--rui-chart-negative`. Direction and goodness are separate here as they are on the card: `railsui_metric_card` passes `positive_is_good` down, so a falling churn rate reads green in the tooltip too.
|
|
450
|
+
|
|
451
|
+
Passing your own `tooltip: { custom: ... }` also takes precedence — the built-in one steps aside.
|
|
452
|
+
|
|
453
|
+
## States
|
|
454
|
+
|
|
455
|
+
A new account has no data, a Turbo frame spends a moment fetching, and queries time out. Each state holds the chart's footprint so nothing below it moves.
|
|
456
|
+
|
|
457
|
+
### Empty
|
|
458
|
+
|
|
459
|
+
No branching needed — `railsui_chart` renders the empty panel when the data comes back with nothing:
|
|
460
|
+
|
|
461
|
+
```erb
|
|
462
|
+
<%= railsui_chart @revenue, type: :area, height: 240 %>
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Say more when it helps:
|
|
466
|
+
|
|
467
|
+
```erb
|
|
468
|
+
<%= railsui_chart @revenue, type: :area, height: 240,
|
|
469
|
+
empty: { title: "No revenue yet",
|
|
470
|
+
description: "Charges appear here once you take your first payment." } %>
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
A series of zeroes is **not** empty. A quiet day still has something to say, and a flat line at zero is how to say it.
|
|
474
|
+
|
|
475
|
+
### Loading
|
|
476
|
+
|
|
477
|
+
```erb
|
|
478
|
+
<%= railsui_metric_card_skeleton chart_height: 180 %>
|
|
479
|
+
<%= railsui_chart_skeleton height: 240, type: :donut %>
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Render one server-side and let a Turbo Stream swap in the real thing.
|
|
483
|
+
|
|
484
|
+
The card skeleton stands in for the text that is coming and leaves the plot area empty — a slab where the chart goes claims more about the shape of the data than a loading state can know. A standalone chart skeleton keeps faint gridlines, since it has nothing else to say it is a chart, and takes the shape of its `type:`.
|
|
485
|
+
|
|
486
|
+
On **refetch**, don't reach for the skeleton. Any element inside a container marked `aria-busy="true"` — which is what Turbo does to a frame while it loads — holds its previous render at reduced opacity instead. The numbers stay on screen and the layout stays still; a skeleton would throw the chart away and flash.
|
|
487
|
+
|
|
488
|
+
### Error
|
|
489
|
+
|
|
490
|
+
```erb
|
|
491
|
+
<%= railsui_chart_error height: 240,
|
|
492
|
+
title: "Couldn't load revenue",
|
|
493
|
+
description: "The query timed out. Try a shorter range." %>
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
A failure reads as a failure rather than as an absence, so nobody mistakes a broken query for a quiet month.
|
|
497
|
+
|
|
498
|
+
## Accessibility
|
|
499
|
+
|
|
500
|
+
Every chart renders a visually hidden table with the underlying data for screen readers, so no value is reachable only by hovering a mark. Comparison series get their own column. Disable it with `accessible: false` if you provide your own alternative.
|
|
501
|
+
|
|
502
|
+
Charts also respect `prefers-reduced-motion` and skip their entry animation.
|
|
503
|
+
|
|
504
|
+
## Performance
|
|
505
|
+
|
|
506
|
+
Charts draw when they scroll into view rather than all at once on page load, so
|
|
507
|
+
a long dashboard does not spend its first seconds laying out charts nobody is
|
|
508
|
+
looking at. Each one starts 300px before it reaches the viewport, and the
|
|
509
|
+
helper reserves its height server-side so nothing shifts as they arrive.
|
|
510
|
+
|
|
511
|
+
Browsers without `IntersectionObserver` render immediately, as before.
|
|
512
|
+
|
|
513
|
+
## Turbo support
|
|
514
|
+
|
|
515
|
+
The Stimulus controller initializes charts on `connect` and destroys them on `disconnect`, so charts work inside Turbo Frames and Turbo Streams without leaks.
|
|
516
|
+
|
|
517
|
+
## Full access
|
|
518
|
+
|
|
519
|
+
A Rails UI membership adds `railsui_charts_pro`, which builds on this gem
|
|
520
|
+
rather than replacing it:
|
|
521
|
+
|
|
522
|
+
- **Time series** — `TimeSeries` fills the gaps a `GROUP BY` leaves behind, buckets to any interval, and sums rows that collapse together
|
|
523
|
+
- **Waterfall and funnel** — the bridging arithmetic and the ordinal ramp handled
|
|
524
|
+
- **Cohort and retention grids** — monthly cohorts on a single-hue ramp
|
|
525
|
+
- **Treemap and bar lists** — part-to-whole past the four-slot cap, and the ranked row every overview ends with
|
|
526
|
+
- **Annotations** — deploy markers, incident bands, and target lines
|
|
527
|
+
- **Live updates and export** — Turbo Stream broadcasts, and PNG and CSV download
|
|
528
|
+
|
|
529
|
+
Installed over GitHub with your existing credentials — no license key, nothing
|
|
530
|
+
calls home:
|
|
531
|
+
|
|
532
|
+
```ruby
|
|
533
|
+
gem "railsui_charts_pro", github: "getrailsui/railsui_charts_pro"
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
[Get full access](https://railsui.com/pricing)
|
|
537
|
+
|
|
538
|
+
## License
|
|
539
|
+
|
|
540
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new("test:ruby") do |t|
|
|
7
|
+
t.libs << "test"
|
|
8
|
+
t.libs << "lib"
|
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# No package.json, no node_modules, no test runner dependency: `node --test` is
|
|
13
|
+
# built in, and the only thing it was missing was a way to satisfy the two bare
|
|
14
|
+
# imports the controllers make. test/javascript/stubs.mjs does that with Node's
|
|
15
|
+
# own module hooks, which need Node 22.15 or newer.
|
|
16
|
+
#
|
|
17
|
+
# Skipped rather than failed when Node is absent, so a Ruby-only contributor
|
|
18
|
+
# can still run the suite.
|
|
19
|
+
desc "Run the JavaScript tests"
|
|
20
|
+
task "test:js" do
|
|
21
|
+
unless system("node --version > /dev/null 2>&1")
|
|
22
|
+
warn "Skipping JavaScript tests: node is not installed"
|
|
23
|
+
next
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
abort "JavaScript tests failed" unless system(
|
|
27
|
+
"node --import ./test/javascript/stubs.mjs --test 'test/javascript/*.test.mjs'"
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
desc "Run every test"
|
|
32
|
+
task test: ["test:ruby", "test:js"]
|
|
33
|
+
|
|
34
|
+
task default: :test
|