keystone_ui 0.29.5 → 0.31.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 +4 -4
- data/app/assets/javascripts/keystone_ui/line_chart_controller.js +19 -6
- data/app/components/keystone/ui/funnel_component.html.erb +33 -12
- data/app/components/keystone/ui/funnel_component.rb +27 -4
- data/app/components/keystone/ui/line_chart_component.html.erb +1 -1
- data/app/components/keystone/ui/line_chart_component.rb +18 -3
- data/lib/keystone_ui/version.rb +1 -1
- data/the_local/agents/keystone_ui-develop.md +40 -11
- data/the_local/agents/keystone_ui-info.md +17 -0
- data/the_local/agents/keystone_ui-install.md +1 -1
- data/the_local/interface.yml +2 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6c7e18e911657e41ed1a4eff2133400015bd7a58e0c437f6886d219173f01fa
|
|
4
|
+
data.tar.gz: 91b2f652bb7313d9cda361e126e969d06b64827e80118888a73cce3840471f68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 466ea8fbb32ed4683db9537a52d7ba41d667f79dfa52aec27aaab85f5fc14b872fcc03b7d880136936133685fdc1af5f839aa4392c5ba698af62694e4a5832b0
|
|
7
|
+
data.tar.gz: 435630df203142fa669f0d93333e6b2637b2d4ec2d25b6e8fa1a7aa00e54ba5fdace3a3416905c144f706f18373ad78b8b11c067f5d1a62ee6453655358b4b77
|
|
@@ -6,9 +6,26 @@ const loadChart = async () => {
|
|
|
6
6
|
return Chart
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
const MILLISECONDS_IN_A_DAY = 86_400_000
|
|
10
|
+
|
|
11
|
+
export const dayLabel = (day) =>
|
|
12
|
+
new Date(day * MILLISECONDS_IN_A_DAY).toLocaleDateString("en-US", { timeZone: "UTC", month: "short", day: "numeric", year: "numeric" })
|
|
13
|
+
|
|
14
|
+
const DATED_OPTIONS = {
|
|
15
|
+
scales: { x: { type: "linear", bounds: "data", ticks: { precision: 0, callback: dayLabel } } },
|
|
16
|
+
plugins: { tooltip: { callbacks: { title: ([point]) => dayLabel(point.parsed.x) } } }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const chartOptions = (dated) => ({
|
|
20
|
+
responsive: true,
|
|
21
|
+
maintainAspectRatio: false,
|
|
22
|
+
interaction: { mode: "index", intersect: false },
|
|
23
|
+
...(dated ? DATED_OPTIONS : {})
|
|
24
|
+
})
|
|
25
|
+
|
|
9
26
|
export default class extends Controller {
|
|
10
27
|
static targets = ["canvas"]
|
|
11
|
-
static values = { data: Object }
|
|
28
|
+
static values = { data: Object, dated: Boolean }
|
|
12
29
|
|
|
13
30
|
async connect() {
|
|
14
31
|
const Chart = await loadChart()
|
|
@@ -17,11 +34,7 @@ export default class extends Controller {
|
|
|
17
34
|
this.chart = new Chart(this.canvasTarget, {
|
|
18
35
|
type: "line",
|
|
19
36
|
data: this.resolveColors(this.dataValue),
|
|
20
|
-
options:
|
|
21
|
-
responsive: true,
|
|
22
|
-
maintainAspectRatio: false,
|
|
23
|
-
interaction: { mode: "index", intersect: false }
|
|
24
|
-
}
|
|
37
|
+
options: chartOptions(this.datedValue)
|
|
25
38
|
})
|
|
26
39
|
}
|
|
27
40
|
|
|
@@ -1,14 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
<%
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
<% if joined? %>
|
|
2
|
+
<div class="<%= JOINED_CLASSES %>">
|
|
3
|
+
<% layers.each do |layer| %>
|
|
4
|
+
<% if layer.conversion_percent %>
|
|
5
|
+
<div></div>
|
|
6
|
+
<div class="<%= BAND_ROW_CLASSES %>">
|
|
7
|
+
<div class="<%= BAND_CLASSES %>" style="clip-path: <%= band_clip_path(layer) %>"></div>
|
|
8
|
+
<div class="<%= BAND_LABEL_ROW_CLASSES %>"><span class="<%= BAND_LABEL_CLASSES %>"><%= layer.conversion_percent %>%</span></div>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
11
|
+
<div class="<%= WORDS_CLASSES %>">
|
|
9
12
|
<span class="<%= value_classes %>"><%= layer.value %></span>
|
|
13
|
+
<span class="<%= label_classes %>"><%= layer.label %></span>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="<%= BLOCK_CLASSES %>">
|
|
16
|
+
<div class="<%= BLOCK_FILL_CLASSES %> <%= layer.color_classes %>" style="width: <%= layer.width_percent %>%"></div>
|
|
10
17
|
</div>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
<% end %>
|
|
19
|
+
</div>
|
|
20
|
+
<% else %>
|
|
21
|
+
<div class="<%= container_classes %>">
|
|
22
|
+
<% layers.each do |layer| %>
|
|
23
|
+
<% if layer.conversion_percent %>
|
|
24
|
+
<div class="<%= transition_classes %>">↓ <%= layer.conversion_percent %>%</div>
|
|
25
|
+
<% end %>
|
|
26
|
+
<div class="<%= layer_classes %>">
|
|
27
|
+
<div class="<%= row_classes %>">
|
|
28
|
+
<span class="<%= label_classes %>"><%= layer.label %></span>
|
|
29
|
+
<span class="<%= value_classes %>"><%= layer.value %></span>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="<%= bar_classes %> <%= layer.color_classes %>" style="width: <%= layer.width_percent %>%"></div>
|
|
32
|
+
</div>
|
|
33
|
+
<% end %>
|
|
34
|
+
</div>
|
|
35
|
+
<% end %>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Keystone
|
|
4
4
|
module Ui
|
|
5
5
|
class FunnelComponent < ViewComponent::Base
|
|
6
|
-
Layer = Struct.new(:label, :value, :width_percent, :conversion_percent, :color_classes, keyword_init: true)
|
|
6
|
+
Layer = Struct.new(:label, :value, :width_percent, :conversion_percent, :color_classes, :previous_width_percent, keyword_init: true)
|
|
7
7
|
|
|
8
8
|
CONTAINER_CLASSES = "ks-funnel"
|
|
9
9
|
LAYER_CLASSES = "ks-funnel-layer"
|
|
@@ -12,6 +12,14 @@ module Keystone
|
|
|
12
12
|
VALUE_CLASSES = "ks-funnel-value text-sm tabular-nums"
|
|
13
13
|
BAR_CLASSES = "ks-funnel-bar h-8 transition-all"
|
|
14
14
|
TRANSITION_CLASSES = "ks-funnel-transition text-center text-xs"
|
|
15
|
+
BAND_LABEL_ROW_CLASSES = "absolute inset-0 flex items-center justify-center"
|
|
16
|
+
BAND_ROW_CLASSES = "relative h-7"
|
|
17
|
+
BLOCK_FILL_CLASSES = "h-full"
|
|
18
|
+
BLOCK_CLASSES = "flex justify-center h-12"
|
|
19
|
+
WORDS_CLASSES = "flex flex-col justify-center"
|
|
20
|
+
BAND_LABEL_CLASSES = "ks-funnel-band-label text-xs"
|
|
21
|
+
BAND_CLASSES = "ks-funnel-band absolute inset-0"
|
|
22
|
+
JOINED_CLASSES = "ks-funnel-joined grid grid-cols-[max-content_1fr]"
|
|
15
23
|
STEP_COLOR_CLASSES = {
|
|
16
24
|
accent: "ks-funnel-bar-accent",
|
|
17
25
|
sky: "ks-funnel-bar-sky",
|
|
@@ -20,10 +28,24 @@ module Keystone
|
|
|
20
28
|
rose: "ks-funnel-bar-rose"
|
|
21
29
|
}.freeze
|
|
22
30
|
|
|
23
|
-
attr_reader :steps
|
|
31
|
+
attr_reader :steps, :shape
|
|
32
|
+
|
|
33
|
+
def initialize(steps:, shape: :bars)
|
|
34
|
+
raise ArgumentError, "a funnel's shape is :bars or :joined, got #{shape.inspect}" unless %i[bars joined].include?(shape)
|
|
24
35
|
|
|
25
|
-
def initialize(steps:)
|
|
26
36
|
@steps = steps
|
|
37
|
+
@shape = shape
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def joined?
|
|
41
|
+
shape == :joined
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def band_clip_path(layer)
|
|
45
|
+
top = (100 - layer.previous_width_percent) / 2.0
|
|
46
|
+
bottom = (100 - layer.width_percent) / 2.0
|
|
47
|
+
|
|
48
|
+
"polygon(#{format("%g%%", top)} 0, #{format("%g%%", 100 - top)} 0, #{format("%g%%", 100 - bottom)} 100%, #{format("%g%%", bottom)} 100%)"
|
|
27
49
|
end
|
|
28
50
|
|
|
29
51
|
def layers
|
|
@@ -35,7 +57,8 @@ module Keystone
|
|
|
35
57
|
value: step[:value],
|
|
36
58
|
width_percent: width_percent(step[:value]),
|
|
37
59
|
conversion_percent: conversion_percent(step[:value], previous),
|
|
38
|
-
color_classes: color_classes(step[:color], index)
|
|
60
|
+
color_classes: color_classes(step[:color], index),
|
|
61
|
+
previous_width_percent: previous && width_percent(previous)
|
|
39
62
|
)
|
|
40
63
|
previous = step[:value]
|
|
41
64
|
layer
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
<div class="<%= container_classes %>" data-controller="line-chart" data-line-chart-data-value="<%= chart_data_json %>">
|
|
1
|
+
<div class="<%= container_classes %>" data-controller="line-chart" data-line-chart-data-value="<%= chart_data_json %>" data-line-chart-dated-value="<%= dated? %>">
|
|
2
2
|
<canvas class="max-w-full" data-line-chart-target="canvas"></canvas>
|
|
3
3
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "date"
|
|
3
4
|
require "json"
|
|
4
5
|
|
|
5
6
|
module Keystone
|
|
@@ -12,21 +13,29 @@ module Keystone
|
|
|
12
13
|
}.freeze
|
|
13
14
|
|
|
14
15
|
DASH_PATTERN = [ 6, 6 ].freeze
|
|
16
|
+
EPOCH = Date.new(1970, 1, 1)
|
|
17
|
+
|
|
18
|
+
def initialize(series:, labels: nil, dates: nil, height: :md)
|
|
19
|
+
raise ArgumentError, "a line chart takes labels: or dates:" unless labels.nil? ^ dates.nil?
|
|
15
20
|
|
|
16
|
-
def initialize(series:, labels:, height: :md)
|
|
17
21
|
@series = series
|
|
18
22
|
@labels = labels
|
|
23
|
+
@dates = dates
|
|
19
24
|
@height = height
|
|
20
25
|
end
|
|
21
26
|
|
|
22
27
|
def chart_data
|
|
23
|
-
{ labels: @labels, datasets: @series.map { |s| dataset_for(s) } }
|
|
28
|
+
{ labels: @labels, datasets: @series.map { |s| dataset_for(s) } }.compact
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
def chart_data_json
|
|
27
32
|
chart_data.to_json
|
|
28
33
|
end
|
|
29
34
|
|
|
35
|
+
def dated?
|
|
36
|
+
!@dates.nil?
|
|
37
|
+
end
|
|
38
|
+
|
|
30
39
|
def height_class
|
|
31
40
|
HEIGHT_CLASSES.fetch(@height)
|
|
32
41
|
end
|
|
@@ -38,11 +47,17 @@ module Keystone
|
|
|
38
47
|
private
|
|
39
48
|
|
|
40
49
|
def dataset_for(series)
|
|
41
|
-
dataset = { label: series[:name], data: series[:data] }
|
|
50
|
+
dataset = { label: series[:name], data: points_for(series[:data]) }
|
|
42
51
|
dataset[:borderColor] = series[:color] if series[:color]
|
|
43
52
|
dataset[:borderDash] = DASH_PATTERN if series[:dashed]
|
|
44
53
|
dataset
|
|
45
54
|
end
|
|
55
|
+
|
|
56
|
+
def points_for(values)
|
|
57
|
+
return values unless @dates
|
|
58
|
+
|
|
59
|
+
@dates.zip(values).map { |date, value| { x: (date.to_date - EPOCH).to_i, y: value } }
|
|
60
|
+
end
|
|
46
61
|
end
|
|
47
62
|
end
|
|
48
63
|
end
|
data/lib/keystone_ui/version.rb
CHANGED
|
@@ -323,17 +323,34 @@ outer element. See Conventions before using it.
|
|
|
323
323
|
|
|
324
324
|
- `ui_chart_card(title:, height: :md)` — takes a block holding a chart. `height:`
|
|
325
325
|
`:sm` `:md` `:lg`.
|
|
326
|
-
- `ui_line_chart(series:, labels
|
|
327
|
-
|
|
328
|
-
`
|
|
329
|
-
`
|
|
330
|
-
|
|
331
|
-
`
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
326
|
+
- `ui_line_chart(series:, labels: nil, dates: nil, height: :md)` — a line chart
|
|
327
|
+
with one line per series. `series:` is
|
|
328
|
+
`[{ name:, data:, color:, dashed: }, ...]` where `data:` is the array of
|
|
329
|
+
values, and `color:` (a CSS color string for the line) and `dashed: true` are
|
|
330
|
+
optional. `height:` `:sm` `:md` `:lg`. Pass exactly one of `labels:` and
|
|
331
|
+
`dates:` for the horizontal axis: passing both, or neither, raises
|
|
332
|
+
`ArgumentError`. `labels:` is an array of strings, one per value, spaced
|
|
333
|
+
evenly and shown as written. `dates:` is an array of `Date`, `Time` or
|
|
334
|
+
`DateTime` values, one per value, matched to each series' `data:` by
|
|
335
|
+
position. A dated chart places each point by its day, so a gap of a week is
|
|
336
|
+
seven times as wide as a gap of a day. Its axis runs from the first day to
|
|
337
|
+
the last, marks only whole days, and reads each day as a date such as
|
|
338
|
+
"Oct 2, 2026", which is also the heading a hovered point shows. The time of
|
|
339
|
+
day is dropped, so pass one value per day.
|
|
340
|
+
- `ui_funnel(steps:, shape: :bars)` — a conversion funnel. `steps:` is
|
|
341
|
+
`[{ label:, value:, color: }, ...]` in order, with `color:` optional. Each
|
|
342
|
+
step's width is its value as a share of the first step's value, and the
|
|
343
|
+
percent shown between two steps is the second step's value divided by the
|
|
344
|
+
first's. Each step takes the next colour in the order `:accent` `:sky`
|
|
345
|
+
`:violet` `:amber` `:rose`, starting again after `:rose`; a step passing one
|
|
346
|
+
of those symbols as `color:` uses it instead, and any other symbol raises.
|
|
347
|
+
Values are printed as given. Divide-by-zero safe, no JavaScript. `shape:`
|
|
348
|
+
`:bars` draws one bar per step, left-aligned, with the label and value on a
|
|
349
|
+
row above it and a `↓ N%` caption between two bars. `shape: :joined` draws
|
|
350
|
+
one shape: each step's value over its label in a column on the left, each
|
|
351
|
+
step's block centred at its width, and between two blocks a neutral band
|
|
352
|
+
that narrows from the block above to the block below, with the percent on it.
|
|
353
|
+
Any other `shape:` raises `ArgumentError`.
|
|
337
354
|
- `ui_bucket(goal:, actual:, label: nil, over: :success)` — an upright container
|
|
338
355
|
for one target, filled from the bottom toward `goal:`. It shows the optional
|
|
339
356
|
`label` on top, then `goal`, the container, `actual`, and the percent reached.
|
|
@@ -443,6 +460,18 @@ outer element. See Conventions before using it.
|
|
|
443
460
|
On a radio card or a checkbox row, text every reader needs to choose goes in `hint:` and
|
|
444
461
|
text only some will want goes in `info:`. Which is which is the app's own
|
|
445
462
|
wording, so ask the developer rather than pick.
|
|
463
|
+
For a line chart, pass `dates:` when each value belongs to a calendar day
|
|
464
|
+
and `labels:` when the horizontal axis is anything else, such as week names
|
|
465
|
+
or categories. With `dates:`, a day with no value leaves a wider gap between
|
|
466
|
+
its neighbours rather than a point at zero. Whether a missing day is left
|
|
467
|
+
out or passed as a zero is the app's own rule, so ask the developer rather
|
|
468
|
+
than pick. Never format dates into strings and pass them as `labels:`.
|
|
469
|
+
For a funnel, whether it is drawn as separate bars or as one joined shape
|
|
470
|
+
is the developer's choice, so ask before passing `shape:`. If a joined
|
|
471
|
+
funnel shows its blocks but no band between them, the app's
|
|
472
|
+
keystone_ui-styles version does not define the `ks-funnel-band` classes, so
|
|
473
|
+
stop and hand that part to `keystone_ui-install`, and add no classes to fix
|
|
474
|
+
it.
|
|
446
475
|
Put the actions on a record, such as Edit and Delete, in an action menu
|
|
447
476
|
rather than in a row of buttons: `menu:` on the `ui_section` that shows the
|
|
448
477
|
record, `table.actions` for a table row, or `ui_action_menu` anywhere else.
|
|
@@ -129,6 +129,23 @@ holds the catalog.
|
|
|
129
129
|
- **Figures are numbers.** Components that measure or compare values, such as
|
|
130
130
|
progress bars, funnels and buckets, do arithmetic on them. Pass a number, not
|
|
131
131
|
a formatted string such as `"9,000"`. A bucket raises on one.
|
|
132
|
+
- **A funnel has two shapes.** Both size each step against the first step and
|
|
133
|
+
show the percent carried from one step to the next. The default draws one bar
|
|
134
|
+
per step, each with its label and value on a row above it. The joined shape
|
|
135
|
+
draws the steps as one figure: each step's value and label sit in a column on
|
|
136
|
+
the left, each step is a block centred at its width, and a neutral band
|
|
137
|
+
between two blocks narrows from one width to the next and holds the percent.
|
|
138
|
+
Any other shape raises `ArgumentError`. Neither shape needs JavaScript.
|
|
139
|
+
- **A line chart is labelled or dated.** A line chart draws one line per
|
|
140
|
+
series, and each series is a name and its values, with an optional colour
|
|
141
|
+
and an optional dashed line. Its horizontal axis is given one of two ways,
|
|
142
|
+
never both and never neither, and the wrong combination raises
|
|
143
|
+
`ArgumentError`. Labels are text, one per value, spaced evenly and shown as
|
|
144
|
+
written. Dates are one day per value, and a dated chart places each point by
|
|
145
|
+
its day, so a gap of a week is seven times as wide as a gap of a day. A dated
|
|
146
|
+
axis runs from the first day to the last, marks only whole days, and reads
|
|
147
|
+
each day as a date such as "Oct 2, 2026", which is also what a hovered point
|
|
148
|
+
shows. A time of day is dropped, so a dated chart has one position per day.
|
|
132
149
|
- **A calculation is text.** The calculation behind a figure is the opposite
|
|
133
150
|
case. It is a list of groups, each with an optional title and its lines, and
|
|
134
151
|
each line is a label, the working and the result. They are passed as
|
|
@@ -40,7 +40,7 @@ built on ViewComponent; hook it in before building any screen with those helpers
|
|
|
40
40
|
## How to use it
|
|
41
41
|
|
|
42
42
|
1. Confirm the prerequisites: Ruby >= 3.2 and **tailwindcss-rails v4+** in the
|
|
43
|
-
host app. The gem brings ViewComponent and keystone_ui-styles 0.
|
|
43
|
+
host app. The gem brings ViewComponent and keystone_ui-styles 0.9.0 or later with it.
|
|
44
44
|
Tailwind does not have to be initialized first, because the generator creates
|
|
45
45
|
the stylesheet if it is missing.
|
|
46
46
|
|
data/the_local/interface.yml
CHANGED
|
@@ -98,6 +98,7 @@ sources:
|
|
|
98
98
|
- app/components/keystone/ui/info_component.rb
|
|
99
99
|
- app/components/keystone/ui/info_component.html.erb
|
|
100
100
|
- app/assets/javascripts/keystone_ui/info_controller.js
|
|
101
|
+
- app/assets/javascripts/keystone_ui/line_chart_controller.js
|
|
101
102
|
- app/components/keystone/ui/breakdown_component.rb
|
|
102
103
|
- app/components/keystone/ui/breakdown_component.html.erb
|
|
103
104
|
- app/components/keystone/ui/feature_grid_component.rb
|
|
@@ -111,6 +112,7 @@ sources:
|
|
|
111
112
|
- app/components/keystone/ui/breadcrumbs_component.rb
|
|
112
113
|
- app/components/keystone/ui/breadcrumbs_component.html.erb
|
|
113
114
|
- app/components/keystone/ui/funnel_component.rb
|
|
115
|
+
- app/components/keystone/ui/funnel_component.html.erb
|
|
114
116
|
- app/components/keystone/ui/bucket_component.rb
|
|
115
117
|
- app/components/keystone/ui/bucket_series_component.rb
|
|
116
118
|
- app/components/keystone/ui/grid_component.rb
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: keystone_ui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.31.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tyler Schneider
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.10.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.10.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: view_component
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|