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 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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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