railsui_charts 0.2.1 → 0.2.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '049e45e39a745fa833e9ba7835a09ecf960cb837b5d86c07c48d311201cddd6e'
4
- data.tar.gz: 5b649ab130a83f43fa87490699fc5514eb00ee9eb92d5bdf6d059e31ac61f73e
3
+ metadata.gz: edb2d338949929fa733699ed8c06cc91edd16c4e5bf9621db0e1db0fa019b245
4
+ data.tar.gz: dfb7e2e6dabeb591d544fdcf68070ea997451bd7f1fb19bb36a5f793a9160bbd
5
5
  SHA512:
6
- metadata.gz: 9d86406916a9fc276372ae9f200f0a0b7fd4d27327caa0e56264d1e79f7eb315c1dde4293c2c275da3f63d02ba47df38524c8d7cb3119ce7204cb36cf5754580
7
- data.tar.gz: 2aacd7f09219f90924d07f5412fc4a25e62fcf312bd4903817138fea5f326d0d1ba48ec3169c416ac5e94141d43dd801c47dde340acc2a7b432c1761abd0aee8
6
+ metadata.gz: 40dc738c972145c4b092a9802f77ea3a406b97305cf2c9355f209ac3bdd97612982fc6fea6c62b83e6ba20686fc67313b04bb1a18152685a68f11724df2bbe40
7
+ data.tar.gz: 04a089986c882997b1b297ed048495facf4a6da6761b9bafdfc4c5fb80a1d094cffe544fd30670469736d46c3ca497f9ffd6b3345a65663ff72e57e84a5a7bcb
data/CHANGELOG.md CHANGED
@@ -6,6 +6,69 @@ public API may change between minor versions.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.3]
10
+
11
+ ### Fixed
12
+
13
+ - The installer no longer writes a stylesheet import that cannot resolve. From
14
+ 0.1.0 through 0.2.2 it appended `@import "../../stylesheets/railsui_charts";`
15
+ to `app/assets/tailwind/application.css`. Relative to that file the path names
16
+ `app/stylesheets` — not the application's own `app/assets/stylesheets`, and not
17
+ the gem's copy, which lives outside the application where no relative path can
18
+ reach it. Tailwind does not quietly skip an import it cannot resolve; it fails
19
+ the build. So an application that ran the installer and used Tailwind did not
20
+ get unstyled charts, it got no stylesheet build at all, and one that did not
21
+ use Tailwind never received the CSS by any route.
22
+
23
+ The stylesheet is now served from the gem through the asset pipeline, which
24
+ already carried it, and the installer links it in the application layout.
25
+ Nothing is copied, so `bundle update` moves the CSS the way it already moves
26
+ the Ruby and the JavaScript. Running `rails g railsui_charts:install` again
27
+ takes the old import back out.
28
+
29
+ - Chart data tables are hidden by the gem's own `.railsui-chart-data-table`
30
+ rather than by the host application's `sr-only`. An application not running
31
+ Tailwind has no such class, and rendered the table at full size in the middle
32
+ of the page. Even where it exists it does not hide a table: `width: 1px` is a
33
+ suggestion a table box ignores, because CSS never sizes one below its
34
+ min-content width. The table laid out at full width and, being positioned,
35
+ pushed the page's scroll width past the viewport — which on a phone reads as
36
+ the whole page sliding sideways. `table-layout: fixed` is what makes the width
37
+ stick. `sr-only` rides along for applications that already style it.
38
+
39
+ ### Added
40
+
41
+ - `@getrailsui/charts/styles.css`, for an application that would rather pull the
42
+ stylesheet into its own Tailwind build than link it separately.
43
+
44
+ ## [0.2.2]
45
+
46
+ ### Added
47
+
48
+ - `accessible_table:` on `railsui_chart`, for charts whose series are not the
49
+ numbers a reader wants. A caller passes `{ headers:, rows: }` and it replaces
50
+ the table derived from the series. The CSV export in Rails UI Charts Pro is
51
+ built from that table, so it is corrected by the same option. This is what
52
+ lets a waterfall describe itself as movement rather than as the invisible
53
+ plinth it stacks to get the shape drawn.
54
+
55
+ ### Fixed
56
+
57
+ - Series that do not share an x-axis now align to the union of their labels, or
58
+ to an explicit `categories:` when one is given, since series order is not
59
+ always reading order. Categories were read off the first series alone and
60
+ every later series was flattened to bare values from position zero, so a
61
+ projection labelled November landed on January, on top of the history it was
62
+ meant to continue. Apex draws the resulting gaps as gaps.
63
+ - A complex combo keeps a `categories:` it was given. Derived lists are still
64
+ stripped, because one alongside numeric pair data makes Apex plot nothing, but
65
+ Apex reads `xaxis.categories` ahead of everything else and without one sets the
66
+ axis from the first series alone. For a forecast that was ruinous: the band is
67
+ drawn first so it sits behind the lines, so the projection's months became the
68
+ whole axis and the earlier history disappeared.
69
+
70
+ ## [0.2.1]
71
+
9
72
  ### Fixed
10
73
 
11
74
  - Sparkline charts now use the Rails UI tooltip renderer, so hovered points show
@@ -123,7 +186,10 @@ First release.
123
186
  reachable only by hovering a mark
124
187
  - Animation stops when the reader has asked for reduced motion
125
188
 
126
- [Unreleased]: https://github.com/getrailsui/railsui_charts/compare/v0.2.0...HEAD
189
+ [Unreleased]: https://github.com/getrailsui/railsui_charts/compare/v0.2.3...HEAD
190
+ [0.2.3]: https://github.com/getrailsui/railsui_charts/compare/v0.2.2...v0.2.3
191
+ [0.2.2]: https://github.com/getrailsui/railsui_charts/compare/v0.2.1...v0.2.2
192
+ [0.2.1]: https://github.com/getrailsui/railsui_charts/compare/v0.2.0...v0.2.1
127
193
  [0.2.0]: https://github.com/getrailsui/railsui_charts/compare/v0.1.2...v0.2.0
128
194
  [0.1.2]: https://github.com/getrailsui/railsui_charts/compare/v0.1.1...v0.1.2
129
195
  [0.1.1]: https://github.com/getrailsui/railsui_charts/compare/v0.1.0...v0.1.1
data/README.md CHANGED
@@ -42,8 +42,31 @@ bundle install
42
42
  rails g railsui_charts:install
43
43
  ```
44
44
 
45
- The generator adds the stylesheet import. The JavaScript is served from the gem
46
- rather than copied into your app, so `bundle update` moves all of it.
45
+ The generator links the stylesheet in your layout. Both the CSS and the
46
+ JavaScript are served from the gem rather than copied into your app, so
47
+ `bundle update` moves all of it.
48
+
49
+ ### Stylesheet
50
+
51
+ The generator adds this to your layout's `<head>`:
52
+
53
+ ```erb
54
+ <%= stylesheet_link_tag "railsui_charts" %>
55
+ ```
56
+
57
+ The engine puts the file on the asset path and precompiles it, so this works on
58
+ Propshaft and on Sprockets without anything further.
59
+
60
+ If you would rather the CSS went through your own Tailwind build — to control
61
+ where it lands in the cascade, say — install the npm package below and import it
62
+ instead of linking it:
63
+
64
+ ```css
65
+ /* app/assets/tailwind/application.css */
66
+ @import "@getrailsui/charts/styles.css";
67
+ ```
68
+
69
+ Do one or the other, not both.
47
70
 
48
71
  ### Bundled apps (esbuild, bun, rollup, webpack)
49
72
 
@@ -75,6 +98,23 @@ import { registerRailsuiCharts } from "@getrailsui/charts"
75
98
  registerRailsuiCharts(application)
76
99
  ```
77
100
 
101
+ ### Upgrading from 0.2.2 and earlier
102
+
103
+ Those versions' installer wrote an `@import` into `app/assets/tailwind/application.css`
104
+ that could never resolve — it named `app/stylesheets`, which is neither your app's
105
+ stylesheets nor the gem's. Tailwind fails the build on an import it cannot find, so
106
+ if you have this line, your CSS build is broken:
107
+
108
+ ```css
109
+ @import "../../stylesheets/railsui_charts";
110
+ ```
111
+
112
+ Running the installer again removes it and links the stylesheet properly:
113
+
114
+ ```bash
115
+ rails g railsui_charts:install
116
+ ```
117
+
78
118
  ### Upgrading from 0.1.x
79
119
 
80
120
  0.1.x copied the controllers into `app/javascript/controllers`. Those copies are
@@ -141,6 +141,9 @@ var railsui_chart_controller_default = class extends Controller {
141
141
  const configuredSeries = w.config?.series?.[seriesIndex] || options.series?.[seriesIndex] || {};
142
142
  const point = this.configuredPoint(options, w, seriesIndex, dataPointIndex) || {};
143
143
  const title = point.name || point.label || point.x || configuredSeries.name;
144
+ if (Array.isArray(point.y)) {
145
+ return this.arrayPointTooltipMarkup(point, configuredSeries, options, w, seriesIndex, dataPointIndex, format);
146
+ }
144
147
  const row = {
145
148
  label: configuredSeries.name || "Value",
146
149
  value: point.y ?? series?.[seriesIndex]?.[dataPointIndex],
@@ -149,6 +152,31 @@ var railsui_chart_controller_default = class extends Controller {
149
152
  };
150
153
  return this.tooltipMarkup(title, [row], null, format);
151
154
  }
155
+ arrayPointTooltipMarkup(point, configuredSeries, options, w, seriesIndex, dataPointIndex, format) {
156
+ const labels = options.tooltip_value_labels || this.defaultArrayValueLabels(options.chart?.type, point.y.length);
157
+ const color = this.pointColor(options, w, seriesIndex, dataPointIndex);
158
+ const rows = point.y.map((value, index) => ({
159
+ label: labels[index] || `Value ${index + 1}`,
160
+ value,
161
+ color: options.tooltip_value_colors?.[index] || color,
162
+ format: null
163
+ }));
164
+ if (point.v !== void 0 && point.v !== null) {
165
+ rows.push({
166
+ label: options.tooltip_volume_label || "Volume",
167
+ value: point.v,
168
+ color: options.tooltip_volume_color || color,
169
+ format: this.formatterFor(options.volume_format || "human", options.currency)
170
+ });
171
+ }
172
+ return this.tooltipMarkup(point.name || point.label || point.x || configuredSeries.name, rows, null, format);
173
+ }
174
+ defaultArrayValueLabels(type, length) {
175
+ if (type === "candlestick" && length === 4) return ["Open", "High", "Low", "Close"];
176
+ if (type === "boxPlot" && length === 5) return ["Min", "Q1", "Median", "Q3", "Max"];
177
+ if (type === "rangeArea" && length === 2) return ["Low", "High"];
178
+ return [];
179
+ }
152
180
  rangeBarTooltipMarkup({ series, seriesIndex, dataPointIndex, w }, options, format) {
153
181
  const configuredSeries = w.config?.series?.[seriesIndex] || options.series?.[seriesIndex] || {};
154
182
  const point = configuredSeries.data?.[dataPointIndex] || {};
@@ -448,18 +476,49 @@ import { Controller as Controller3 } from "@hotwired/stimulus";
448
476
  var railsui_metric_dialog_controller_default = class extends Controller3 {
449
477
  static targets = ["dialog"];
450
478
  open() {
479
+ this.dialogTarget.classList.remove("railsui-metric-dialog--closing");
451
480
  this.dialogTarget.showModal();
452
- this.dialogTarget.querySelectorAll(".railsui-chart").forEach((chart) => {
453
- chart.dispatchEvent(new CustomEvent("railsui-chart:refresh"));
481
+ requestAnimationFrame(() => {
482
+ this.dialogTarget.classList.add("railsui-metric-dialog--open");
483
+ this.refreshCharts();
454
484
  });
455
485
  }
456
486
  close() {
457
- this.dialogTarget.close();
487
+ this.closeDialog();
488
+ }
489
+ cancel(event) {
490
+ event.preventDefault();
491
+ this.closeDialog();
458
492
  }
459
493
  // Clicking the backdrop lands on the dialog element itself; a click anywhere
460
494
  // inside lands on a child.
461
495
  closeOnBackdrop(event) {
462
- if (event.target === this.dialogTarget) this.dialogTarget.close();
496
+ if (event.target === this.dialogTarget) this.closeDialog();
497
+ }
498
+ refreshCharts() {
499
+ this.dialogTarget.querySelectorAll(".railsui-chart").forEach((chart) => {
500
+ chart.dispatchEvent(new CustomEvent("railsui-chart:refresh"));
501
+ });
502
+ }
503
+ closeDialog() {
504
+ const dialog = this.dialogTarget;
505
+ if (!dialog.open) return;
506
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
507
+ dialog.classList.remove("railsui-metric-dialog--open", "railsui-metric-dialog--closing");
508
+ dialog.close();
509
+ return;
510
+ }
511
+ dialog.classList.remove("railsui-metric-dialog--open");
512
+ dialog.classList.add("railsui-metric-dialog--closing");
513
+ let closed = false;
514
+ const finish = () => {
515
+ if (closed) return;
516
+ closed = true;
517
+ dialog.classList.remove("railsui-metric-dialog--closing");
518
+ dialog.close();
519
+ };
520
+ dialog.addEventListener("transitionend", finish, { once: true });
521
+ setTimeout(finish, 240);
463
522
  }
464
523
  };
465
524
 
@@ -110,6 +110,51 @@
110
110
  width: 100%;
111
111
  }
112
112
 
113
+ /* ------------------------------------------------------------- chart data */
114
+
115
+ /* The table behind every chart: out of sight, still in the accessibility tree,
116
+ so no value is reachable only by hovering a mark.
117
+ *
118
+ * Hidden here rather than by borrowing the host's `sr-only`, for two reasons.
119
+ * An app that is not running Tailwind has no such class, and the table renders
120
+ * at full size in the middle of the page. And even where it exists, `sr-only`
121
+ * does not hide a table: it shrinks an element to 1px and clips the rest, but
122
+ * width is only a suggestion to a table box, which CSS never sizes below its
123
+ * min-content width. The hidden table lays out at its full width, and since it
124
+ * is positioned, it pushes the page's scroll width past the viewport — which
125
+ * on a phone reads as the whole page sliding sideways.
126
+ *
127
+ * Fixed layout is what makes the 1px stick. The caption needs the same
128
+ * treatment for the same reason: left in flow, its own min-content width holds
129
+ * the table open on its own. Neither is removed from the accessibility tree.
130
+ *
131
+ * The height stays at the table's own — that one is a minimum a table will not
132
+ * go under either, and `contain` does not apply to a table box. It costs
133
+ * nothing: the element is out of flow, and a page carrying a chart is taller
134
+ * than the table under it. Collapsing it would take `display: block`, which
135
+ * throws away the table semantics this element exists for. */
136
+ .railsui-chart-data-table {
137
+ position: absolute;
138
+ width: 1px;
139
+ height: 1px;
140
+ padding: 0;
141
+ margin: -1px;
142
+ border: 0;
143
+ overflow: hidden;
144
+ white-space: nowrap;
145
+ clip-path: inset(50%);
146
+ table-layout: fixed;
147
+ }
148
+
149
+ .railsui-chart-data-table > caption {
150
+ position: absolute;
151
+ width: 1px;
152
+ height: 1px;
153
+ overflow: hidden;
154
+ white-space: nowrap;
155
+ clip-path: inset(50%);
156
+ }
157
+
113
158
  /* -------------------------------------------------------- small multiples */
114
159
 
115
160
  .railsui-small-multiples {
@@ -518,6 +563,12 @@
518
563
  margin-top: 0.5rem;
519
564
  }
520
565
 
566
+ .railsui-metric-card__chart .railsui-chart,
567
+ .railsui-metric-card__chart .apexcharts-canvas,
568
+ .railsui-metric-card__chart .apexcharts-svg {
569
+ overflow: visible !important;
570
+ }
571
+
521
572
  /* Rendered outside the plot so the first and last labels sit flush with the
522
573
  card edges. Apex centres them on their data point and clips at the canvas
523
574
  edge, which costs either the label's first characters or plot width. */
@@ -568,22 +619,41 @@
568
619
  /* --------------------------------------------------------- expanded view */
569
620
 
570
621
  .railsui-metric-dialog {
571
- width: min(56rem, calc(100vw - 2rem));
572
- max-height: calc(100vh - 4rem);
622
+ width: min(64rem, calc(100vw - 2rem));
623
+ max-height: calc(100vh - 2rem);
573
624
  padding: 0;
574
625
  border: 1px solid var(--rui-chart-border);
575
626
  border-radius: 0.75rem;
576
627
  background-color: var(--rui-chart-surface);
577
628
  color: var(--rui-chart-metric-value);
578
- overflow: hidden;
629
+ opacity: 0;
630
+ overflow: auto;
631
+ transform: translateY(0.5rem) scale(0.98);
632
+ transition: opacity 160ms ease, transform 180ms ease;
579
633
  }
580
634
 
581
635
  .railsui-metric-dialog::backdrop {
582
- background-color: rgba(15, 23, 42, 0.5);
636
+ background-color: transparent;
637
+ transition: background-color 160ms ease;
638
+ }
639
+
640
+ .railsui-metric-dialog--open {
641
+ opacity: 1;
642
+ transform: translateY(0) scale(1);
643
+ }
644
+
645
+ .railsui-metric-dialog--open::backdrop {
646
+ background-color: rgba(15, 23, 42, 0.48);
647
+ }
648
+
649
+ .railsui-metric-dialog--closing {
650
+ opacity: 0;
651
+ transform: translateY(0.25rem) scale(0.99);
583
652
  }
584
653
 
585
654
  .railsui-metric-dialog__panel {
586
- padding: 1.25rem 1.5rem 1.5rem;
655
+ min-width: 0;
656
+ padding: 1.25rem 1.75rem 1.5rem;
587
657
  }
588
658
 
589
659
  .railsui-metric-dialog__head {
@@ -595,21 +665,67 @@
595
665
  }
596
666
 
597
667
  .railsui-metric-dialog__close {
668
+ display: inline-flex;
669
+ align-items: center;
670
+ justify-content: center;
598
671
  flex-shrink: 0;
599
- width: 1.75rem;
600
- height: 1.75rem;
672
+ width: 2.5rem;
673
+ height: 2.5rem;
601
674
  border: 0;
602
- border-radius: 9999px;
675
+ border-radius: 0.375rem;
603
676
  background-color: transparent;
604
677
  color: var(--rui-chart-metric-muted);
605
- font-size: 1.25rem;
606
- line-height: 1;
607
678
  cursor: pointer;
608
- transition: background-color 120ms ease;
679
+ transition: background-color 120ms ease, color 120ms ease;
609
680
  }
610
681
 
611
- .railsui-metric-dialog__close:hover {
682
+ .railsui-metric-dialog__close:hover,
683
+ .railsui-metric-dialog__close:focus-visible {
612
684
  background-color: var(--rui-chart-hover);
685
+ color: var(--rui-chart-metric-value);
686
+ }
687
+
688
+ .railsui-metric-dialog__close:focus-visible {
689
+ outline: 2px solid var(--rui-chart-primary);
690
+ outline-offset: 2px;
691
+ }
692
+
693
+ .railsui-metric-dialog__close-icon {
694
+ width: 1.5rem;
695
+ height: 1.5rem;
696
+ pointer-events: none;
697
+ }
698
+
699
+ .railsui-metric-dialog__chart,
700
+ .railsui-metric-dialog__chart .railsui-chart,
701
+ .railsui-metric-dialog__chart .apexcharts-canvas,
702
+ .railsui-metric-dialog__chart .apexcharts-svg {
703
+ overflow: visible !important;
704
+ }
705
+
706
+ @media (max-width: 640px) {
707
+ .railsui-metric-dialog {
708
+ width: calc(100vw - 1rem);
709
+ max-height: calc(100dvh - 1rem);
710
+ border-radius: 0.625rem;
711
+ }
712
+
713
+ .railsui-metric-dialog__panel {
714
+ padding: 1rem;
715
+ }
716
+
717
+ .railsui-metric-dialog__head {
718
+ gap: 0.75rem;
719
+ margin-bottom: 0.75rem;
720
+ }
721
+ }
722
+
723
+ @media (prefers-reduced-motion: reduce) {
724
+ .railsui-metric-dialog,
725
+ .railsui-metric-dialog::backdrop,
726
+ .railsui-metric-dialog__close {
727
+ transition: none;
728
+ }
613
729
  }
614
730
 
615
731
  /* ---------------------------------------------------------------- deltas */
@@ -193,6 +193,10 @@ export default class extends Controller {
193
193
  const configuredSeries = w.config?.series?.[seriesIndex] || options.series?.[seriesIndex] || {}
194
194
  const point = this.configuredPoint(options, w, seriesIndex, dataPointIndex) || {}
195
195
  const title = point.name || point.label || point.x || configuredSeries.name
196
+ if (Array.isArray(point.y)) {
197
+ return this.arrayPointTooltipMarkup(point, configuredSeries, options, w, seriesIndex, dataPointIndex, format)
198
+ }
199
+
196
200
  const row = {
197
201
  label: configuredSeries.name || "Value",
198
202
  value: point.y ?? series?.[seriesIndex]?.[dataPointIndex],
@@ -203,6 +207,36 @@ export default class extends Controller {
203
207
  return this.tooltipMarkup(title, [row], null, format)
204
208
  }
205
209
 
210
+ arrayPointTooltipMarkup(point, configuredSeries, options, w, seriesIndex, dataPointIndex, format) {
211
+ const labels = options.tooltip_value_labels || this.defaultArrayValueLabels(options.chart?.type, point.y.length)
212
+ const color = this.pointColor(options, w, seriesIndex, dataPointIndex)
213
+ const rows = point.y.map((value, index) => ({
214
+ label: labels[index] || `Value ${index + 1}`,
215
+ value,
216
+ color: options.tooltip_value_colors?.[index] || color,
217
+ format: null
218
+ }))
219
+
220
+ if (point.v !== undefined && point.v !== null) {
221
+ rows.push({
222
+ label: options.tooltip_volume_label || "Volume",
223
+ value: point.v,
224
+ color: options.tooltip_volume_color || color,
225
+ format: this.formatterFor(options.volume_format || "human", options.currency)
226
+ })
227
+ }
228
+
229
+ return this.tooltipMarkup(point.name || point.label || point.x || configuredSeries.name, rows, null, format)
230
+ }
231
+
232
+ defaultArrayValueLabels(type, length) {
233
+ if (type === "candlestick" && length === 4) return ["Open", "High", "Low", "Close"]
234
+ if (type === "boxPlot" && length === 5) return ["Min", "Q1", "Median", "Q3", "Max"]
235
+ if (type === "rangeArea" && length === 2) return ["Low", "High"]
236
+
237
+ return []
238
+ }
239
+
206
240
  rangeBarTooltipMarkup({ series, seriesIndex, dataPointIndex, w }, options, format) {
207
241
  const configuredSeries = w.config?.series?.[seriesIndex] || options.series?.[seriesIndex] || {}
208
242
  const point = configuredSeries.data?.[dataPointIndex] || {}
@@ -6,22 +6,61 @@ export default class extends Controller {
6
6
  static targets = ["dialog"]
7
7
 
8
8
  open() {
9
+ this.dialogTarget.classList.remove("railsui-metric-dialog--closing")
9
10
  this.dialogTarget.showModal()
10
11
 
11
- // The chart inside was laid out while the dialog was closed, which means
12
- // it measured zero and drew nothing. Now that it has a size, tell it.
13
- this.dialogTarget.querySelectorAll(".railsui-chart").forEach((chart) => {
14
- chart.dispatchEvent(new CustomEvent("railsui-chart:refresh"))
12
+ requestAnimationFrame(() => {
13
+ this.dialogTarget.classList.add("railsui-metric-dialog--open")
14
+ this.refreshCharts()
15
15
  })
16
16
  }
17
17
 
18
18
  close() {
19
- this.dialogTarget.close()
19
+ this.closeDialog()
20
+ }
21
+
22
+ cancel(event) {
23
+ event.preventDefault()
24
+ this.closeDialog()
20
25
  }
21
26
 
22
27
  // Clicking the backdrop lands on the dialog element itself; a click anywhere
23
28
  // inside lands on a child.
24
29
  closeOnBackdrop(event) {
25
- if (event.target === this.dialogTarget) this.dialogTarget.close()
30
+ if (event.target === this.dialogTarget) this.closeDialog()
31
+ }
32
+
33
+ refreshCharts() {
34
+ // The chart inside was laid out while the dialog was closed, which means
35
+ // it measured zero and drew nothing. Now that it has a size, tell it.
36
+ this.dialogTarget.querySelectorAll(".railsui-chart").forEach((chart) => {
37
+ chart.dispatchEvent(new CustomEvent("railsui-chart:refresh"))
38
+ })
39
+ }
40
+
41
+ closeDialog() {
42
+ const dialog = this.dialogTarget
43
+ if (!dialog.open) return
44
+
45
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
46
+ dialog.classList.remove("railsui-metric-dialog--open", "railsui-metric-dialog--closing")
47
+ dialog.close()
48
+ return
49
+ }
50
+
51
+ dialog.classList.remove("railsui-metric-dialog--open")
52
+ dialog.classList.add("railsui-metric-dialog--closing")
53
+
54
+ let closed = false
55
+ const finish = () => {
56
+ if (closed) return
57
+
58
+ closed = true
59
+ dialog.classList.remove("railsui-metric-dialog--closing")
60
+ dialog.close()
61
+ }
62
+
63
+ dialog.addEventListener("transitionend", finish, { once: true })
64
+ setTimeout(finish, 240)
26
65
  }
27
66
  }
@@ -8,23 +8,62 @@ module RailsuiCharts
8
8
  # version was installed — a fix could ship and reach nobody. Bundled apps
9
9
  # take it from npm, importmap apps from the pin this engine adds, and both
10
10
  # follow the gem from then on.
11
- def add_css_import
12
- path = "app/assets/tailwind/application.css"
13
- import_statement = %q(@import "../../stylesheets/railsui_charts";)
14
- full_path = File.join(destination_root, path)
15
-
16
- unless File.exist?(full_path)
17
- say "⚠️ #{path} not found. Add this import manually:", :yellow
18
- say " #{import_statement}"
11
+ #
12
+ # The stylesheet now follows the same rule, for the same reason and after
13
+ # the same mistake. See remove_broken_css_import.
14
+
15
+ LAYOUT = "app/views/layouts/application.html.erb"
16
+ TAILWIND_ENTRY = "app/assets/tailwind/application.css"
17
+ STYLESHEET_TAG = %q(<%= stylesheet_link_tag "railsui_charts" %>).freeze
18
+
19
+ # What 0.1.0 through 0.2.2 wrote into the Tailwind entry. It never worked.
20
+ # Relative to app/assets/tailwind it names app/stylesheets — not the
21
+ # application's own app/assets/stylesheets, and certainly not the file it
22
+ # was after, which lives inside the gem where no relative path from the
23
+ # application can reach it. Tailwind does not quietly skip an import it
24
+ # cannot resolve; it fails the build. So this is removed rather than
25
+ # corrected, from anyone who ran the old generator.
26
+ BROKEN_IMPORT = %r{^@import\s+"\.\./\.\./stylesheets/railsui_charts";?[ \t]*\r?\n}.freeze
27
+
28
+ def remove_broken_css_import
29
+ path = File.join(destination_root, TAILWIND_ENTRY)
30
+ return unless File.exist?(path)
31
+
32
+ contents = File.read(path)
33
+ return unless contents.match?(BROKEN_IMPORT)
34
+
35
+ File.write(path, contents.sub(BROKEN_IMPORT, ""))
36
+ say "✓ Removed the unresolvable @import from #{TAILWIND_ENTRY}", :green
37
+ say " It named a path that does not exist and failed the Tailwind build.", :yellow
38
+ end
39
+
40
+ # Served from the gem through the asset pipeline, which already carries it:
41
+ # the engine puts app/assets/stylesheets on the load path and precompiles
42
+ # the file. Nothing is copied, so `bundle update` moves the CSS too.
43
+ def add_stylesheet_link
44
+ path = File.join(destination_root, LAYOUT)
45
+
46
+ unless File.exist?(path)
47
+ say "⚠️ #{LAYOUT} not found. Add this to your layout's <head>:", :yellow
48
+ say " #{STYLESHEET_TAG}"
19
49
  return
20
50
  end
21
51
 
22
- if File.read(full_path).include?(import_statement)
23
- say "✓ RailsUI Charts CSS import already present", :green
24
- else
25
- append_to_file path, "#{import_statement}\n"
26
- say "✓ Added RailsUI Charts CSS import to #{path}", :green
52
+ contents = File.read(path)
53
+
54
+ if contents.include?(%("railsui_charts"))
55
+ say "✓ Rails UI Charts stylesheet already linked", :green
56
+ return
57
+ end
58
+
59
+ unless contents.match?(%r{</head>})
60
+ say "⚠️ No </head> found in #{LAYOUT}. Add this to your layout's <head>:", :yellow
61
+ say " #{STYLESHEET_TAG}"
62
+ return
27
63
  end
64
+
65
+ inject_into_file LAYOUT, " #{STYLESHEET_TAG}\n", before: %r{^[ \t]*</head>}
66
+ say "✓ Linked the Rails UI Charts stylesheet in #{LAYOUT}", :green
28
67
  end
29
68
 
30
69
  def print_next_steps
@@ -51,6 +90,13 @@ module RailsuiCharts
51
90
  say "ApexCharts is a peer dependency; pin it too:", :cyan
52
91
  say ' pin "apexcharts", to: "https://esm.sh/apexcharts@3.45.2"', :cyan
53
92
  say ""
93
+ elsif File.exist?(File.join(destination_root, TAILWIND_ENTRY))
94
+ say "Prefer the CSS inside your Tailwind build? Once the package is", :cyan
95
+ say "installed you can drop the stylesheet_link_tag and import it:", :cyan
96
+ say ""
97
+ say " /* #{TAILWIND_ENTRY} */", :cyan
98
+ say ' @import "@getrailsui/charts/styles.css";', :cyan
99
+ say ""
54
100
  end
55
101
 
56
102
  say "Then use <%= railsui_chart data, type: :area %> in your views.", :cyan
@@ -132,6 +132,12 @@ module RailsuiCharts
132
132
  @series.any? { |series| series[:type].present? }
133
133
  end
134
134
 
135
+ def complex_combo?
136
+ combo? && @series.each_with_index.any? do |series, index|
137
+ RailsuiCharts.config.complex_point_types.include?(series_point_type(series, index))
138
+ end
139
+ end
140
+
135
141
  def dual_axis?
136
142
  @series.any? { |series| series[:axis] == :right }
137
143
  end
@@ -155,6 +161,9 @@ module RailsuiCharts
155
161
  normalized = { x: point[:x], y: span_or_value(point), z: point[:z] }
156
162
  normalized[:meta] = point[:meta] if point.key?(:meta)
157
163
  normalized[:name] = point[:name] if point.key?(:name)
164
+ normalized[:label] = point[:label] if point.key?(:label)
165
+ normalized[:v] = point[:v] if point.key?(:v)
166
+ normalized[:points] = point[:points] if point.key?(:points)
158
167
  normalized
159
168
  when Array
160
169
  { x: point[0], y: point[1], z: point[2] }
@@ -174,9 +183,49 @@ module RailsuiCharts
174
183
  end
175
184
 
176
185
  def categories
186
+ return category_union if misaligned_series?
187
+
177
188
  @data.map { |d| d[:x] }.compact
178
189
  end
179
190
 
191
+ # The union of every series' labels, in the order they first appear.
192
+ #
193
+ # Categories used to be read off the first series alone, and every later
194
+ # series was then flattened to bare values and drawn from position zero.
195
+ # For series that share an x-axis — the ordinary case — that is the same
196
+ # thing. For series that do not, it silently plotted them in the wrong
197
+ # place: a projection labelled November landed on January, on top of the
198
+ # history it was meant to continue.
199
+ def category_union
200
+ # A caller who states the axis gets it. Series order decides first
201
+ # appearance otherwise, and that is not always the reading order: a
202
+ # forecast draws its band first so it sits behind the lines, which would
203
+ # otherwise put next November before last January.
204
+ @category_union ||= Array(@options[:categories]).presence ||
205
+ @series.flat_map { |series| series_labels(series) }.uniq
206
+ end
207
+
208
+ # Only when the series genuinely disagree. A series of bare values carries
209
+ # no labels to align on, and aligning identical label lists would be the
210
+ # same list — so in both cases nothing changes.
211
+ def misaligned_series?
212
+ return @misaligned unless @misaligned.nil?
213
+
214
+ @misaligned =
215
+ if @series.length < 2
216
+ false
217
+ elsif @options[:categories].present?
218
+ @series.all? { |series| series_labels(series).any? }
219
+ else
220
+ labels = @series.map { |series| series_labels(series) }
221
+ labels.none?(&:empty?) && labels.uniq.length > 1
222
+ end
223
+ end
224
+
225
+ def series_labels(series)
226
+ Array(series[:data]).filter_map { |point| point[:x] if point.is_a?(Hash) }
227
+ end
228
+
180
229
  # A bare array of values carries no labels, and Apex sizes the x-axis from
181
230
  # this list: given an empty one it draws nothing at all — no canvas, no
182
231
  # error, just an element that stays empty. Positions stand in, which is
@@ -537,18 +586,23 @@ module RailsuiCharts
537
586
  return series_values if circular?
538
587
 
539
588
  plotted = @series.each_with_index.map do |series, index|
540
- entry = { name: series[:name] || (index.zero? ? series_label : "Series #{index + 1}"), data: points_for(series[:data]) }
589
+ point_type = series_point_type(series, index)
590
+ entry = { name: series[:name] || (index.zero? ? series_label : "Series #{index + 1}"), data: points_for(series[:data], point_type) }
541
591
  # Only on a combo. Naming a type on every series of an ordinary chart
542
592
  # would override the chart-level one and quietly ignore `type:`.
543
- entry[:type] = series_type(series, index) if combo?
593
+ entry[:type] = apex_type_for(point_type) if combo?
544
594
  entry
545
595
  end
546
596
 
547
597
  comparing? ? plotted + [{ name: compare_label, data: @compare.map { |point| point[:y] } }] : plotted
548
598
  end
549
599
 
550
- def points_for(points)
551
- case @type
600
+ def series_point_type(series, index)
601
+ (series[:type] || (index.zero? ? @type : @series.first[:type]) || @type).to_sym
602
+ end
603
+
604
+ def points_for(points, point_type = @type)
605
+ case point_type
552
606
  when :scatter then points.map { |point| [point[:x], point[:y]] }
553
607
  when :bubble then points.map { |point| { x: point[:x], y: point[:y], z: point[:z] || 1 } }
554
608
  when :range_bar
@@ -563,14 +617,35 @@ module RailsuiCharts
563
617
  else
564
618
  # A registered type may draw its own labels rather than read them off an
565
619
  # axis, in which case flattening the point to a value loses them.
566
- return points.map { |point| { x: point[:x], y: point[:y] } } if labelled_points?
620
+ return points.map { |point| preserved_point(point) } if preserved_points?(point_type) || preserve_combo_points?(points)
621
+ return aligned_values(points) if misaligned_series?
567
622
 
568
623
  points.map { |point| point[:y] }
569
624
  end
570
625
  end
571
626
 
572
- def labelled_points?
573
- RailsuiCharts.config.labelled_point_types.include?(@type)
627
+ # A slot per shared category, nil where this series has nothing. Apex draws
628
+ # a nil as a gap, which is what a forecast that begins after the history
629
+ # ends should look like.
630
+ def aligned_values(points)
631
+ lookup = points.each_with_object({}) do |point, memo|
632
+ memo[point[:x]] = point[:y] if point.is_a?(Hash)
633
+ end
634
+
635
+ category_union.map { |category| lookup[category] }
636
+ end
637
+
638
+ def preserve_combo_points?(points)
639
+ complex_combo? && points.any? { |point| point[:x].present? }
640
+ end
641
+
642
+ def preserved_points?(type)
643
+ RailsuiCharts.config.labelled_point_types.include?(type) ||
644
+ RailsuiCharts.config.complex_point_types.include?(type)
645
+ end
646
+
647
+ def preserved_point(point)
648
+ point.slice(:x, :y, :z, :meta, :name, :label, :v, :points).compact
574
649
  end
575
650
 
576
651
  # Apex plots a range against a time axis in milliseconds. Handing it a Time
@@ -599,7 +674,7 @@ module RailsuiCharts
599
674
  return { labels: { show: false } } if circular?
600
675
  return numeric_axis(:x) if numeric_xaxis?
601
676
 
602
- {
677
+ options = {
603
678
  categories: axis_categories,
604
679
  labels: {
605
680
  show: categories.any? && !sparkline?,
@@ -617,6 +692,14 @@ module RailsuiCharts
617
692
  tooltip: { enabled: false },
618
693
  type: "category"
619
694
  }
695
+
696
+ # A complex combo carries its own x in every point, and a derived category
697
+ # list alongside numeric pair data makes Apex plot nothing. An explicit
698
+ # one is different: Apex reads xaxis.categories ahead of everything else,
699
+ # and without it a combo takes its whole axis from the first series alone
700
+ # — which draws every other series from position zero.
701
+ options = options.except(:categories) if complex_combo? && @options[:categories].blank?
702
+ options
620
703
  end
621
704
 
622
705
  # Scatter and bubble plot real x values. Handing Apex a `categories` array
@@ -781,10 +864,10 @@ module RailsuiCharts
781
864
  timeline? || @type == :bar
782
865
  end
783
866
 
784
- # Two scales, so the axis is coloured like its series. One scale keeps the
785
- # quiet neutral: there is nothing to tell apart.
786
- def axis_ink(index)
787
- resolve_var(categorical_palette[index] || config_color(:text))
867
+ # Series colour belongs to the marks and legend. Keep both scales neutral
868
+ # so the numbers stay readable and do not compete with the data.
869
+ def axis_ink(_index)
870
+ config_color(:text)
788
871
  end
789
872
 
790
873
  # Both sides fitted to their own values, then cut into the same number of
@@ -848,7 +931,7 @@ module RailsuiCharts
848
931
 
849
932
  def axis_values(side)
850
933
  values = @series.select { |series| axis_side(series) == side }
851
- .flat_map { |series| series[:data].map { |point| point[:y] } }
934
+ .flat_map { |series| series[:data].flat_map { |point| Array(point[:y]) } }
852
935
  # A column grows from a baseline, so its scale has to contain one. A line
853
936
  # can float, and forcing zero on a churn rate hovering near 3% would
854
937
  # flatten it against the top of the plot.
@@ -986,7 +1069,13 @@ module RailsuiCharts
986
1069
  # A mixed chart takes its shape from each series, and Apex wants the
987
1070
  # chart-level type to be "line" while they do — set to "bar" it draws
988
1071
  # every series as bars whatever they asked for.
989
- return "line" if combo?
1072
+ if combo?
1073
+ complex = @series.map.with_index { |series, index| series_point_type(series, index) }
1074
+ .find { |type| RailsuiCharts.config.complex_point_types.include?(type) }
1075
+ return apex_type_for(complex) if complex
1076
+
1077
+ return "line"
1078
+ end
990
1079
 
991
1080
  apex_type_for(@type)
992
1081
  end
@@ -995,6 +1084,8 @@ module RailsuiCharts
995
1084
  case type&.to_sym
996
1085
  when :sparkline then "line"
997
1086
  when :range_bar then "rangeBar"
1087
+ when :range_area then "rangeArea"
1088
+ when :box_plot then "boxPlot"
998
1089
  when :column, :bar then "bar"
999
1090
  when :donut then "donut"
1000
1091
  when :polar_area then "polarArea"
@@ -6,6 +6,11 @@ module RailsuiCharts
6
6
  CIRCULAR_TYPES = %i[pie donut polar_area radar].freeze
7
7
 
8
8
  def railsui_chart(data, type: :line, **options)
9
+ # A chart whose series are not the numbers a reader wants can say so.
10
+ # Deleted rather than read, so it never reaches the builder and never
11
+ # gets serialised into the Stimulus value alongside the real config.
12
+ explicit_table = options.delete(:accessible_table)
13
+
9
14
  # An empty dataset is a normal day one, not an error. Rendering axes
10
15
  # around nothing looks like a chart that failed rather than a chart with
11
16
  # nothing to show yet.
@@ -14,7 +19,14 @@ module RailsuiCharts
14
19
  config = ApexOptionsBuilder.new(data, type: type, **options).build
15
20
  id = options[:id] || "rui-chart-#{SecureRandom.hex(4)}"
16
21
 
17
- table = options[:accessible] != false ? accessibility_table(config, id: id) : nil
22
+ table =
23
+ if options[:accessible] == false
24
+ nil
25
+ elsif explicit_table.present?
26
+ explicit_accessibility_table(explicit_table, id: id)
27
+ else
28
+ accessibility_table(config, id: id)
29
+ end
18
30
 
19
31
  content_tag(:div,
20
32
  id: id,
@@ -151,7 +163,40 @@ module RailsuiCharts
151
163
  end
152
164
 
153
165
  # Every chart ships a visually hidden table so no value is reachable only by
154
- # hovering a mark.
166
+ # hovering a mark. The hiding is the gem's own — `sr-only` rides along for
167
+ # apps that already style it, but a host utility cannot be relied on to
168
+ # exist, and Tailwind's does not actually hide a table. See
169
+ # railsui-chart-data-table in the stylesheet.
170
+ #
171
+ # The table a caller supplied, rather than one derived from the series.
172
+ #
173
+ # This is the accessible half of an extension point: a downstream helper
174
+ # that stacks scaffolding series to get a shape Apex can draw — a waterfall
175
+ # holding bars at a running total, say — can still tell a screen reader
176
+ # what the chart means. The CSV export is built from this table too, so it
177
+ # gets the same correction for free.
178
+ def explicit_accessibility_table(table, id:)
179
+ headers = Array(table[:headers] || table["headers"])
180
+ rows = Array(table[:rows] || table["rows"])
181
+ return if rows.blank?
182
+
183
+ content_tag(:table, class: "railsui-chart-data-table sr-only", aria: { label: "Chart data" }) do
184
+ safe_join([
185
+ content_tag(:caption, "Data for chart #{id}"),
186
+ content_tag(:thead) do
187
+ content_tag(:tr) { safe_join(headers.map { |header| content_tag(:th, header) }) }
188
+ end,
189
+ content_tag(:tbody) do
190
+ safe_join(rows.map do |row|
191
+ content_tag(:tr) do
192
+ safe_join(Array(row).map { |cell| content_tag(:td, cell_value(cell)) })
193
+ end
194
+ end)
195
+ end
196
+ ])
197
+ end
198
+ end
199
+
155
200
  def accessibility_table(config, id:)
156
201
  series = config[:series]
157
202
  return if series.blank?
@@ -163,13 +208,23 @@ module RailsuiCharts
163
208
  else
164
209
  columns = Array(series).map { |s| { name: s[:name] || "Value", data: s[:data] } }
165
210
  categories = config.dig(:xaxis, :categories) || []
211
+
212
+ if categories.blank?
213
+ point_categories = accessibility_point_categories(columns)
214
+ if point_categories.present?
215
+ categories = point_categories
216
+ columns = columns.map do |column|
217
+ column.merge(data: accessibility_align_points(column[:data], categories))
218
+ end
219
+ end
220
+ end
166
221
  end
167
222
  comparison_categories = config[:compare_categories] || []
168
223
 
169
224
  rows = columns.first&.dig(:data)
170
225
  return if rows.blank?
171
226
 
172
- content_tag(:table, class: "sr-only", aria: { label: "Chart data" }) do
227
+ content_tag(:table, class: "railsui-chart-data-table sr-only", aria: { label: "Chart data" }) do
173
228
  safe_join([
174
229
  content_tag(:caption, "Data for chart #{id}"),
175
230
  content_tag(:thead) do
@@ -202,6 +257,20 @@ module RailsuiCharts
202
257
  headers
203
258
  end
204
259
 
260
+ def accessibility_point_categories(columns)
261
+ columns.flat_map do |column|
262
+ Array(column[:data]).filter_map { |value| value[:x] if value.is_a?(Hash) && value.key?(:x) }
263
+ end.uniq
264
+ end
265
+
266
+ def accessibility_align_points(data, categories)
267
+ points = Array(data).each_with_object({}) do |value, indexed|
268
+ indexed[value[:x]] = value if value.is_a?(Hash) && value.key?(:x)
269
+ end
270
+
271
+ categories.map { |category| points[category] }
272
+ end
273
+
205
274
  def accessibility_category_cells(categories, comparison_categories, index)
206
275
  cells = [content_tag(:td, categories[index] || index + 1)]
207
276
  cells << content_tag(:td, comparison_categories[index] || index + 1) if comparison_categories.present?
@@ -215,7 +284,9 @@ module RailsuiCharts
215
284
  # A bubble carries three numbers that all mean something here. Anything
216
285
  # else keeps its label in the category column already, so repeating it
217
286
  # beside the value just reads as "A, 6" to a screen reader.
218
- value.key?(:z) ? value.values_at(:x, :y, :z).compact.join(", ") : value[:y]
287
+ return value.values_at(:x, :y, :z).compact.join(", ") if value.key?(:z)
288
+
289
+ cell_value(value[:y])
219
290
  else value
220
291
  end
221
292
  end
@@ -3,7 +3,7 @@
3
3
  module RailsuiCharts
4
4
  class Configuration
5
5
  attr_accessor :default_height, :default_currency, :colors, :series_colors, :theme_css_prefix, :extra_types
6
- attr_accessor :labelled_point_types, :typography, :geometry
6
+ attr_accessor :labelled_point_types, :complex_point_types, :typography, :geometry
7
7
 
8
8
  # Categorical hues carry identity, so the order is the colourblind-safety
9
9
  # mechanism rather than a style choice — it was picked by validating every
@@ -27,6 +27,7 @@ module RailsuiCharts
27
27
  # Chart types registered by an extension, such as Rails UI Charts Pro.
28
28
  @extra_types = []
29
29
  @labelled_point_types = []
30
+ @complex_point_types = []
30
31
  @theme_css_prefix = "--rui-chart"
31
32
  @series_colors = (1..SERIES_COUNT).map { |i| "var(--rui-chart-series-#{i}, #{SERIES_FALLBACKS[i - 1]})" }
32
33
  @colors = {
@@ -76,11 +77,16 @@ module RailsuiCharts
76
77
  # `points: :labelled` keeps each point as {x:, y:}. Most types put the label
77
78
  # on the axis and send bare values, but a treemap draws its labels inside
78
79
  # the rectangles, so they have to stay in the data.
80
+ #
81
+ # `points: :complex` keeps the whole point too, but for a different reason:
82
+ # some Apex types put several numbers in one y-value, such as OHLC candles,
83
+ # box plots, and range areas.
79
84
  def register_type(type, points: :values)
80
85
  type = type.to_sym
81
86
 
82
87
  @extra_types |= [type]
83
88
  @labelled_point_types |= [type] if points == :labelled
89
+ @complex_point_types |= [type] if points == :complex
84
90
 
85
91
  type
86
92
  end
@@ -146,27 +146,49 @@ module RailsuiCharts
146
146
  def metric_dialog(label, value, change, previous, history, compare, format, positive_is_good, options)
147
147
  content_tag(:dialog,
148
148
  class: "railsui-metric-dialog",
149
- data: { "railsui-metric-dialog-target": "dialog", action: "click->railsui-metric-dialog#closeOnBackdrop" }) do
149
+ data: {
150
+ "railsui-metric-dialog-target": "dialog",
151
+ action: "click->railsui-metric-dialog#closeOnBackdrop cancel->railsui-metric-dialog#cancel"
152
+ }) do
150
153
  content_tag(:div, class: "railsui-metric-dialog__panel") do
151
154
  safe_join([
152
155
  content_tag(:div, class: "railsui-metric-dialog__head") do
153
156
  safe_join([
154
157
  metric_card_head(label, value, change, previous, format, positive_is_good),
155
- content_tag(:button, "&times;".html_safe, type: "button", class: "railsui-metric-dialog__close",
156
- aria: { label: "Close" }, data: { action: "railsui-metric-dialog#close" })
158
+ content_tag(:button, metric_dialog_close_icon, type: "button", class: "railsui-metric-dialog__close",
159
+ aria: { label: "Close" }, data: { action: "railsui-metric-dialog#close" })
157
160
  ])
158
161
  end,
159
162
  # Room to read values rather than just a trend, so the axis labels
160
- # the card suppresses come back. They centre on their data point,
161
- # so the outermost two need room or Apex clips them at the canvas.
162
- railsui_chart(history, type: :line, compare: compare, format: format, height: 420,
163
- axis: :right, legend: { show: compare.present? },
164
- grid: { padding: { left: 28, right: 12 } }, **options)
163
+ # the card suppresses come back. The header keeps the exact value;
164
+ # the axis uses compact labels so it stays legible inside the
165
+ # dialog without clipping at the canvas edge.
166
+ content_tag(:div, class: "railsui-metric-dialog__chart") do
167
+ railsui_chart(history, type: :line, compare: compare, format: metric_dialog_chart_format(format), height: 420,
168
+ axis: :right, legend: { show: compare.present? },
169
+ yaxis: { labels: { minWidth: 48, maxWidth: 72, offsetX: 4 } },
170
+ grid: { padding: { left: 32, right: 72 } }, **options)
171
+ end
165
172
  ])
166
173
  end
167
174
  end
168
175
  end
169
176
 
177
+ def metric_dialog_chart_format(format)
178
+ format == :currency ? :short_currency : format
179
+ end
180
+
181
+ def metric_dialog_close_icon
182
+ content_tag(:svg,
183
+ content_tag(:path, "", "stroke-linecap": "round", "stroke-linejoin": "round", d: "M6 18 18 6M6 6l12 12"),
184
+ class: "railsui-metric-dialog__close-icon",
185
+ viewBox: "0 0 24 24",
186
+ fill: "none",
187
+ stroke: "currentColor",
188
+ "stroke-width": 2,
189
+ aria: { hidden: true })
190
+ end
191
+
170
192
  def format_metric_value(value, format)
171
193
  case format
172
194
  when :currency, :short_currency
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsuiCharts
4
- VERSION = "0.2.1"
4
+ VERSION = "0.2.3"
5
5
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getrailsui/charts",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Rails-native chart components built on ApexCharts. Stimulus controllers for the railsui_charts gem.",
5
5
  "author": "Andy Leverenz <railsui@justalever.com>",
6
6
  "license": "MIT",
@@ -14,15 +14,18 @@
14
14
  "module": "app/javascript/railsui_charts/index.js",
15
15
  "exports": {
16
16
  ".": "./app/javascript/railsui_charts/index.js",
17
+ "./styles.css": "./app/assets/stylesheets/railsui_charts.css",
17
18
  "./controllers/*": "./app/javascript/controllers/*"
18
19
  },
19
20
  "files": [
20
21
  "app/javascript",
21
22
  "app/assets/javascripts",
23
+ "app/assets/stylesheets",
22
24
  "README.md",
23
25
  "LICENSE.md",
24
26
  "CHANGELOG.md"
25
27
  ],
28
+ "packageManager": "yarn@4.11.0",
26
29
  "scripts": {
27
30
  "build": "esbuild app/javascript/railsui_charts/index.js --bundle --format=esm --external:@hotwired/stimulus --external:apexcharts --outfile=app/assets/javascripts/railsui_charts.js"
28
31
  },
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: railsui_charts
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Leverenz