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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsuiCharts
|
|
4
|
+
module MetricHelper
|
|
5
|
+
# A compact label / value / delta stack with an optional sparkline.
|
|
6
|
+
def railsui_metric(label:, value:, change: nil, comparison: nil, history: nil, format: :number, **options)
|
|
7
|
+
change ||= comparison
|
|
8
|
+
sparkline = metric_sparkline(history, options)
|
|
9
|
+
|
|
10
|
+
content_tag(:div, class: "railsui-metric") do
|
|
11
|
+
safe_join([
|
|
12
|
+
content_tag(:p, label, class: "railsui-metric-label"),
|
|
13
|
+
content_tag(:div, class: "railsui-metric-value-row") do
|
|
14
|
+
safe_join([
|
|
15
|
+
content_tag(:span, format_metric_value(value, format), class: "railsui-metric-value"),
|
|
16
|
+
change ? metric_delta(change) : nil
|
|
17
|
+
].compact)
|
|
18
|
+
end,
|
|
19
|
+
sparkline
|
|
20
|
+
].compact)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The full dashboard card: label, value, delta, previous-period line, a
|
|
25
|
+
# comparison chart, and a footer. This is the piece a Rails app actually
|
|
26
|
+
# drops into a dashboard — the chart alone is rarely the whole job.
|
|
27
|
+
# `expand: true` turns the footer link into an expanded view of the same
|
|
28
|
+
# series — a card chart is 180px tall, which is enough to read a trend and
|
|
29
|
+
# not enough to read a value. Takes precedence over `details_path`.
|
|
30
|
+
def railsui_metric_card(label:, value:, previous: nil, change: nil, history: nil, compare: nil,
|
|
31
|
+
format: :number, updated_at: nil, details_path: nil,
|
|
32
|
+
details_label: "More details", positive_is_good: true,
|
|
33
|
+
chart_height: 180, expand: false, **options)
|
|
34
|
+
change ||= percentage_change(value, previous)
|
|
35
|
+
expand &&= history.present?
|
|
36
|
+
# The chart cannot know that a falling churn rate is a win, so the card
|
|
37
|
+
# tells it — the tooltip colours its delta the same way the value does.
|
|
38
|
+
chart_options = options.merge(label: options[:label] || label, trend_up_is_good: positive_is_good)
|
|
39
|
+
|
|
40
|
+
card = content_tag(:div, class: "railsui-metric-card", data: expand ? { controller: "railsui-metric-dialog" } : {}) do
|
|
41
|
+
safe_join([
|
|
42
|
+
metric_card_head(label, value, change, previous, format, positive_is_good),
|
|
43
|
+
metric_card_chart(history, compare, format, chart_height, chart_options),
|
|
44
|
+
metric_card_footer(updated_at, details_path, details_label, expand: expand),
|
|
45
|
+
expand ? metric_dialog(label, value, change, previous, history, compare, format, positive_is_good, chart_options) : nil
|
|
46
|
+
].compact)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
card
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The card's own placeholder. It stands in for the text that is coming and
|
|
53
|
+
# leaves the plot area empty: a grey slab where the chart goes claims more
|
|
54
|
+
# about the shape of the data than a loading state can know.
|
|
55
|
+
def railsui_metric_card_skeleton(chart_height: 180, footer: true, label: "Loading metric")
|
|
56
|
+
content_tag(:div, class: "railsui-metric-card", role: "status", aria: { busy: true, label: label }) do
|
|
57
|
+
safe_join([
|
|
58
|
+
content_tag(:div, class: "railsui-metric-card__head") do
|
|
59
|
+
safe_join(%w[label value meta].map do |part|
|
|
60
|
+
content_tag(:span, "", class: "railsui-skeleton-bar railsui-skeleton-bar--#{part}", aria: { hidden: true })
|
|
61
|
+
end)
|
|
62
|
+
end,
|
|
63
|
+
content_tag(:div, "", class: "railsui-metric-card__chart", style: "min-height: #{chart_height.to_i}px"),
|
|
64
|
+
footer ? content_tag(:div, class: "railsui-metric-card__footer") do
|
|
65
|
+
content_tag(:span, "", class: "railsui-skeleton-bar railsui-skeleton-bar--footer", aria: { hidden: true })
|
|
66
|
+
end : nil
|
|
67
|
+
].compact)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def metric_card_head(label, value, change, previous, format, positive_is_good)
|
|
74
|
+
content_tag(:div, class: "railsui-metric-card__head") do
|
|
75
|
+
safe_join([
|
|
76
|
+
content_tag(:p, label, class: "railsui-metric-card__label"),
|
|
77
|
+
content_tag(:div, class: "railsui-metric-card__value-row") do
|
|
78
|
+
safe_join([
|
|
79
|
+
content_tag(:span, format_metric_value(value, format), class: "railsui-metric-card__value"),
|
|
80
|
+
change ? metric_delta(change, positive_is_good: positive_is_good) : nil
|
|
81
|
+
].compact)
|
|
82
|
+
end,
|
|
83
|
+
previous ? content_tag(:p, "#{format_metric_value(previous, format)} previous period", class: "railsui-metric-card__previous") : nil
|
|
84
|
+
].compact)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def metric_card_chart(history, compare, format, height, options)
|
|
89
|
+
# No early return on blank history: railsui_chart renders its empty panel
|
|
90
|
+
# at the same height, so a card with nothing to plot keeps its shape
|
|
91
|
+
# instead of collapsing and shuffling the grid around it.
|
|
92
|
+
content_tag(:div, class: "railsui-metric-card__chart") do
|
|
93
|
+
safe_join([
|
|
94
|
+
railsui_chart(
|
|
95
|
+
history,
|
|
96
|
+
type: :line,
|
|
97
|
+
compare: compare,
|
|
98
|
+
# The value above is already spelled out in full, so the axis takes
|
|
99
|
+
# the compact form: $19K rather than $19,000.00.
|
|
100
|
+
format: format == :currency ? :short_currency : format,
|
|
101
|
+
height: height,
|
|
102
|
+
# The end labels are rendered below as plain text instead. Apex
|
|
103
|
+
# centres a label on its data point and clips at the canvas edge, so
|
|
104
|
+
# an edge label either loses its first characters or has to be
|
|
105
|
+
# bought with padding that eats into the plot.
|
|
106
|
+
xaxis: { labels: { show: false } },
|
|
107
|
+
grid: { padding: { left: 0, right: 4, top: 0, bottom: 0 } },
|
|
108
|
+
# Scale on the right, so the plot starts flush with the card edge.
|
|
109
|
+
axis: :right,
|
|
110
|
+
# The "previous period" line above already names the comparison, so a
|
|
111
|
+
# legend box would only restate it and eat the card's height.
|
|
112
|
+
legend: { show: false },
|
|
113
|
+
**options
|
|
114
|
+
),
|
|
115
|
+
metric_card_axis(history)
|
|
116
|
+
].compact)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# First and last labels, flush to the card edges. Nothing to clip, and the
|
|
121
|
+
# plot keeps its full width.
|
|
122
|
+
def metric_card_axis(history)
|
|
123
|
+
labels = Array(history).filter_map { |point| point[:x] || point["x"] if point.is_a?(Hash) }
|
|
124
|
+
return if labels.length < 2
|
|
125
|
+
|
|
126
|
+
content_tag(:div, class: "railsui-metric-card__axis") do
|
|
127
|
+
safe_join([content_tag(:span, labels.first), content_tag(:span, labels.last)])
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def metric_card_footer(updated_at, details_path, details_label, expand: false)
|
|
132
|
+
return if updated_at.blank? && details_path.blank? && !expand
|
|
133
|
+
|
|
134
|
+
action = if expand
|
|
135
|
+
content_tag(:button, details_label, type: "button", class: "railsui-metric-card__details",
|
|
136
|
+
data: { action: "railsui-metric-dialog#open" })
|
|
137
|
+
elsif details_path.present?
|
|
138
|
+
link_to(details_label, details_path, class: "railsui-metric-card__details")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
content_tag(:div, class: "railsui-metric-card__footer") do
|
|
142
|
+
safe_join([content_tag(:span, updated_at, class: "railsui-metric-card__updated"), action].compact)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def metric_dialog(label, value, change, previous, history, compare, format, positive_is_good, options)
|
|
147
|
+
content_tag(:dialog,
|
|
148
|
+
class: "railsui-metric-dialog",
|
|
149
|
+
data: { "railsui-metric-dialog-target": "dialog", action: "click->railsui-metric-dialog#closeOnBackdrop" }) do
|
|
150
|
+
content_tag(:div, class: "railsui-metric-dialog__panel") do
|
|
151
|
+
safe_join([
|
|
152
|
+
content_tag(:div, class: "railsui-metric-dialog__head") do
|
|
153
|
+
safe_join([
|
|
154
|
+
metric_card_head(label, value, change, previous, format, positive_is_good),
|
|
155
|
+
content_tag(:button, "×".html_safe, type: "button", class: "railsui-metric-dialog__close",
|
|
156
|
+
aria: { label: "Close" }, data: { action: "railsui-metric-dialog#close" })
|
|
157
|
+
])
|
|
158
|
+
end,
|
|
159
|
+
# 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)
|
|
165
|
+
])
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def format_metric_value(value, format)
|
|
171
|
+
case format
|
|
172
|
+
when :currency, :short_currency
|
|
173
|
+
number_to_currency(value)
|
|
174
|
+
when :percentage
|
|
175
|
+
number_to_percentage(value, precision: 1)
|
|
176
|
+
when :human
|
|
177
|
+
number_to_human(value, units: { thousand: "K", million: "M", billion: "B" }, format: "%n%u", precision: 3)
|
|
178
|
+
else
|
|
179
|
+
number_with_delimiter(value)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Plain coloured text, not a pill. A filled badge competes with the value
|
|
184
|
+
# it is annotating.
|
|
185
|
+
def metric_delta(change, positive_is_good: true)
|
|
186
|
+
return if change.nil?
|
|
187
|
+
|
|
188
|
+
trend = trend_direction(change, positive_is_good: positive_is_good)
|
|
189
|
+
sign = change.to_f.positive? ? "+" : ""
|
|
190
|
+
|
|
191
|
+
content_tag(:span, "#{sign}#{format_change(change)}%", class: "railsui-metric-delta railsui-metric-delta--#{trend}")
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def format_change(change)
|
|
195
|
+
rounded = change.to_f.round(2)
|
|
196
|
+
rounded == rounded.to_i ? rounded.to_i : rounded
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Direction and goodness are separate: a falling churn rate is a win, so the
|
|
200
|
+
# colour follows meaning rather than the sign.
|
|
201
|
+
def trend_direction(change, positive_is_good: true)
|
|
202
|
+
return "neutral" if change.nil? || change.to_f.zero?
|
|
203
|
+
|
|
204
|
+
change.to_f.positive? == positive_is_good ? "positive" : "negative"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def percentage_change(value, previous)
|
|
208
|
+
return if value.nil? || previous.nil? || previous.to_f.zero?
|
|
209
|
+
|
|
210
|
+
((value.to_f - previous.to_f) / previous.to_f * 100).round(2)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def metric_sparkline(history, options)
|
|
214
|
+
return if history.blank?
|
|
215
|
+
|
|
216
|
+
railsui_chart(history, type: :sparkline, height: 40, accessible: false, **options.except(:id))
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "railsui_charts/version"
|
|
4
|
+
require "active_support/core_ext/object/json"
|
|
5
|
+
require "active_support/core_ext/object/blank"
|
|
6
|
+
require "active_support/core_ext/date/calculations"
|
|
7
|
+
require "active_support/core_ext/time/calculations"
|
|
8
|
+
|
|
9
|
+
module RailsuiCharts
|
|
10
|
+
autoload :Configuration, "railsui_charts/configuration"
|
|
11
|
+
autoload :ApexOptionsBuilder, "railsui_charts/apex_options_builder"
|
|
12
|
+
autoload :ChartHelper, "railsui_charts/chart_helper"
|
|
13
|
+
autoload :MetricHelper, "railsui_charts/metric_helper"
|
|
14
|
+
autoload :FilterHelper, "railsui_charts/filter_helper"
|
|
15
|
+
autoload :Filters, "railsui_charts/filters"
|
|
16
|
+
autoload :Interval, "railsui_charts/interval"
|
|
17
|
+
|
|
18
|
+
mattr_accessor :config
|
|
19
|
+
@@config = RailsuiCharts::Configuration.new
|
|
20
|
+
|
|
21
|
+
def self.configure
|
|
22
|
+
yield(config)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
require "railsui_charts/engine" if defined?(Rails)
|
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: railsui_charts
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andy Leverenz
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.0'
|
|
26
|
+
description: Production-ready chart and metric components for Rails, built on ApexCharts.
|
|
27
|
+
Server-rendered, accessible, and designed for Tailwind CSS.
|
|
28
|
+
email:
|
|
29
|
+
- railsui@justalever.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- CHANGELOG.md
|
|
35
|
+
- LICENSE.md
|
|
36
|
+
- README.md
|
|
37
|
+
- Rakefile
|
|
38
|
+
- app/assets/stylesheets/railsui_charts.css
|
|
39
|
+
- app/controllers/railsui_charts/demo_controller.rb
|
|
40
|
+
- app/javascript/controllers/railsui_chart_controller.js
|
|
41
|
+
- app/javascript/controllers/railsui_chart_filters_controller.js
|
|
42
|
+
- app/javascript/controllers/railsui_metric_dialog_controller.js
|
|
43
|
+
- app/views/railsui_charts/demo/index.html.erb
|
|
44
|
+
- config/routes.rb
|
|
45
|
+
- lib/generators/railsui_charts/install/install_generator.rb
|
|
46
|
+
- lib/generators/railsui_charts/install/templates/railsui_chart_controller.js
|
|
47
|
+
- lib/generators/railsui_charts/install/templates/railsui_chart_filters_controller.js
|
|
48
|
+
- lib/generators/railsui_charts/install/templates/railsui_metric_dialog_controller.js
|
|
49
|
+
- lib/railsui_charts.rb
|
|
50
|
+
- lib/railsui_charts/apex_options_builder.rb
|
|
51
|
+
- lib/railsui_charts/chart_helper.rb
|
|
52
|
+
- lib/railsui_charts/configuration.rb
|
|
53
|
+
- lib/railsui_charts/engine.rb
|
|
54
|
+
- lib/railsui_charts/filter_helper.rb
|
|
55
|
+
- lib/railsui_charts/filters.rb
|
|
56
|
+
- lib/railsui_charts/interval.rb
|
|
57
|
+
- lib/railsui_charts/metric_helper.rb
|
|
58
|
+
- lib/railsui_charts/version.rb
|
|
59
|
+
homepage: https://railsui.com/charts
|
|
60
|
+
licenses:
|
|
61
|
+
- MIT
|
|
62
|
+
metadata:
|
|
63
|
+
homepage_uri: https://railsui.com/charts
|
|
64
|
+
documentation_uri: https://railsui.com/docs/charts
|
|
65
|
+
source_code_uri: https://github.com/getrailsui/railsui_charts
|
|
66
|
+
changelog_uri: https://github.com/getrailsui/railsui_charts/blob/main/CHANGELOG.md
|
|
67
|
+
bug_tracker_uri: https://github.com/getrailsui/railsui_charts/issues
|
|
68
|
+
rubygems_mfa_required: 'true'
|
|
69
|
+
rdoc_options: []
|
|
70
|
+
require_paths:
|
|
71
|
+
- lib
|
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: 2.7.0
|
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
requirements: []
|
|
83
|
+
rubygems_version: 3.6.9
|
|
84
|
+
specification_version: 4
|
|
85
|
+
summary: Rails-native chart components for RailsUI
|
|
86
|
+
test_files: []
|