keystone_ui 0.44.0 → 0.46.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 399e1a496d6abbf8fc37149cbf05ea62223222c1cf6b5678dc400b286a17f06a
4
- data.tar.gz: 5d7566e89343b57e8a35a6693e4fe6107184203299a880d58eab541b6ff16d81
3
+ metadata.gz: 1209af69898ff5cfa1879680b8d9de9a9ca569474d6471a85f906f1bdcac7425
4
+ data.tar.gz: 4f7d5750c98755ccc5ac4a7c17095f018219e731417101d60e2ea5b2f2e164d1
5
5
  SHA512:
6
- metadata.gz: e0e0ab5ac2704a8612ec78bd80987d61ddfaee104cf0eedf976075e8ea28b4d64ac26990e061055165d334f06c8951b0eb8083b6005cb666dd8d47fec7ec5a7f
7
- data.tar.gz: 8de679cd46c03c8c8856196fc1d73d87def5ab3910c7b142d744ab7fca02c8b011f0cea93a2a2b42979d677b84c7448c32880d2ab6783851506188622bba7c30
6
+ metadata.gz: 251cec86be528ce9676410c4eb8f317df3954060faa79c4e841c918681618b39f7778d2fd22e51116ec12fd4237b5dd1f617d549bd06e0c4149c412869b5da25
7
+ data.tar.gz: d1f9983ec813ce4ef08ee138470df4c7d60b583a07742fa0855a1d4b5ca7b69a5d1c3c0b98a0ea11defbd026c9cbe7aa99cc0a65e53613808e33446722666cbf
@@ -0,0 +1,13 @@
1
+ <svg class="ks-bar-chart" viewBox="0 0 <%= Keystone::Ui::BarChartComponent::WIDTH %> <%= Keystone::Ui::BarChartComponent::HEIGHT %>" role="img">
2
+ <% ticks.each do |tick| %>
3
+ <line class="ks-bar-chart-grid" x1="<%= Keystone::Ui::BarChartComponent::LEFT %>" x2="<%= Keystone::Ui::BarChartComponent::WIDTH - Keystone::Ui::BarChartComponent::RIGHT %>" y1="<%= tick[:y].round(2) %>" y2="<%= tick[:y].round(2) %>"></line>
4
+ <text class="ks-bar-chart-text" data-axis-tick x="<%= axis_x %>" y="<%= (tick[:y] + 4).round(2) %>" text-anchor="end"><%= tick[:label] %></text>
5
+ <% end %>
6
+ <% drawn_bars.each do |bar| %>
7
+ <rect class="<%= bar[:classes] %>" x="<%= bar[:x].round(2) %>" y="<%= bar[:y].round(2) %>" width="<%= bar[:width].round(2) %>" height="<%= bar[:height].round(2) %>"></rect>
8
+ <% if show_values? %>
9
+ <text class="ks-bar-chart-value" x="<%= bar[:center].round(2) %>" y="<%= (bar[:y] - 4).round(2) %>" text-anchor="middle"><%= bar[:value] %></text>
10
+ <% end %>
11
+ <text class="ks-bar-chart-text" data-bar-label x="<%= bar[:center].round(2) %>" y="<%= label_baseline %>" text-anchor="middle"><%= bar[:label] %></text>
12
+ <% end %>
13
+ </svg>
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class BarChartComponent < ViewComponent::Base
6
+ WIDTH = 600
7
+ HEIGHT = 240
8
+ LEFT = 40
9
+ RIGHT = 8
10
+ TOP = 18
11
+ BOTTOM = 24
12
+ BAR_SHARE = 0.7
13
+ TICK_COUNT = 4
14
+ ROUND_STEPS = [ 1, 2, 2.5, 5, 10 ].freeze
15
+
16
+ attr_reader :bars
17
+
18
+ def initialize(bars:, show_values: false)
19
+ @bars = bars
20
+ @show_values = show_values
21
+ end
22
+
23
+ def show_values?
24
+ @show_values
25
+ end
26
+
27
+ def drawn_bars
28
+ bars.each_with_index.map do |bar, index|
29
+ height = bar[:value].to_f / axis_max * plot_height
30
+ { x: LEFT + (index * band) + (band * (1 - BAR_SHARE) / 2), y: TOP + plot_height - height, width: band * BAR_SHARE, height: height,
31
+ center: LEFT + (index * band) + (band / 2), label: bar[:label], value: bar[:value],
32
+ classes: [ "ks-bar-chart-bar", ("ks-bar-chart-bar-shaded" if bar[:shaded]) ].compact.join(" ") }
33
+ end
34
+ end
35
+
36
+ def ticks
37
+ (0..TICK_COUNT).map do |position|
38
+ value = (step * position).round(6)
39
+ { y: TOP + plot_height - (value / axis_max * plot_height), label: (value % 1).zero? ? value.to_i.to_s : value.to_s }
40
+ end
41
+ end
42
+
43
+ def axis_x
44
+ LEFT - 6
45
+ end
46
+
47
+ def label_baseline
48
+ HEIGHT - 6
49
+ end
50
+
51
+ private
52
+
53
+ def axis_max
54
+ step * (largest / step).ceil
55
+ end
56
+
57
+ def step
58
+ @step ||= begin
59
+ raw = largest / TICK_COUNT
60
+ magnitude = 10.0**Math.log10(raw).floor
61
+ ROUND_STEPS.map { |round| round * magnitude }.find { |candidate| candidate >= raw }
62
+ end
63
+ end
64
+
65
+ def largest
66
+ bars.map { |bar| bar[:value].to_f }.select(&:positive?).max || 1.0
67
+ end
68
+
69
+ def band
70
+ plot_width / [ bars.size, 1 ].max
71
+ end
72
+
73
+ def plot_width
74
+ WIDTH - LEFT - RIGHT
75
+ end
76
+
77
+ def plot_height
78
+ HEIGHT - TOP - BOTTOM
79
+ end
80
+ end
81
+ end
82
+ end
@@ -3,10 +3,11 @@
3
3
  module Keystone
4
4
  module Ui
5
5
  class Column
6
- attr_reader :key, :header_text
6
+ attr_reader :key, :header_text, :align
7
7
 
8
- def initialize(key, header_text, mobile_hidden: false, sortable: false, hideable: false, locked: false)
8
+ def initialize(key, header_text, mobile_hidden: false, sortable: false, hideable: false, locked: false, align: nil)
9
9
  @key = key
10
+ @align = align
10
11
  @header_text = header_text
11
12
  @mobile_hidden = mobile_hidden
12
13
  @sortable = sortable
@@ -36,6 +36,14 @@
36
36
  <% end %>
37
37
  <% else %>
38
38
  <% @items.each_with_index do |item, idx| %>
39
+ <% if (group = group_starting_at(idx)) %>
40
+ <tr class="<%= Keystone::Ui::DataTableComponent::HEAD_CLASSES %>" data-table-group>
41
+ <% group_cells(group).each do |cell| %>
42
+ <th scope="colgroup" class="<%= cell[:classes] %>"><%= cell[:value] %></th>
43
+ <% end %>
44
+ <% if actions? %><th></th><% end %>
45
+ </tr>
46
+ <% end %>
39
47
  <tr>
40
48
  <% row_cells[idx].each do |cell| %>
41
49
  <td class="<%= cell[:classes] %>">
@@ -60,5 +68,15 @@
60
68
  <% end %>
61
69
  <% end %>
62
70
  </tbody>
71
+ <% if total_cells.any? %>
72
+ <tfoot class="<%= Keystone::Ui::DataTableComponent::HEAD_CLASSES %>">
73
+ <tr>
74
+ <% total_cells.each do |cell| %>
75
+ <th scope="row" class="<%= cell[:classes] %>"><%= cell[:value] %></th>
76
+ <% end %>
77
+ <% if actions? %><th></th><% end %>
78
+ </tr>
79
+ </tfoot>
80
+ <% end %>
63
81
  </table>
64
82
  </div>
@@ -16,6 +16,7 @@ module Keystone
16
16
  ROW_CLASSES_MIDDLE = "ks-table-cell-middle text-sm whitespace-nowrap"
17
17
  ROW_CLASSES_LAST = "ks-table-cell-last text-right text-sm whitespace-nowrap"
18
18
 
19
+ ALIGN_CLASSES = { start: "text-left", end: "text-right" }.freeze
19
20
  MOBILE_HIDDEN_CLASSES = "hidden sm:table-cell"
20
21
  HEADER_LOCKED_CLASSES = "ks-table-header-locked sticky left-0"
21
22
  CELL_LOCKED_CLASSES = "ks-table-cell-locked sticky left-0"
@@ -29,8 +30,11 @@ module Keystone
29
30
  SORT_DESC_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 3a.75.75 0 0 1 .75.75v10.638l3.96-4.158a.75.75 0 1 1 1.08 1.04l-5.25 5.5a.75.75 0 0 1-1.08 0l-5.25-5.5a.75.75 0 0 1 1.08-1.04l3.96 4.158V3.75A.75.75 0 0 1 10 3Z" clip-rule="evenodd" /></svg>'
30
31
  SORT_NEUTRAL_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 3a.75.75 0 0 1 .55.24l3.25 3.5a.75.75 0 1 1-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 0 1-1.1-1.02l3.25-3.5A.75.75 0 0 1 10 3Zm-3.76 9.2a.75.75 0 0 1 1.06.04l2.7 2.908 2.7-2.908a.75.75 0 1 1 1.1 1.02l-3.25 3.5a.75.75 0 0 1-1.1 0l-3.25-3.5a.75.75 0 0 1 .04-1.06Z" clip-rule="evenodd" /></svg>'
31
32
 
32
- def initialize(items:, columns:, empty_message: nil, sort: nil, sort_direction: nil, sort_url: nil, hidden_columns: [], key: nil)
33
+ def initialize(items:, columns:, empty_message: nil, sort: nil, sort_direction: nil, sort_url: nil, hidden_columns: [], key: nil, group_by: nil, group_totals: {}, total: nil)
33
34
  @items = items.to_a
35
+ @group_by = group_by
36
+ @group_totals = group_totals.to_h.transform_keys(&:to_s)
37
+ @total = total&.to_h&.transform_keys(&:to_s)
34
38
  @all_columns = columns.map { |col| normalize_column(col) }
35
39
  @default_hidden = hidden_columns
36
40
  @layout = SavedLayout.new(columns: @all_columns, value: nil, default_hidden: hidden_columns)
@@ -83,7 +87,7 @@ module Keystone
83
87
 
84
88
  cell = {
85
89
  label: column.header_text,
86
- classes: tokens.join(" "),
90
+ classes: aligned(tokens.join(" "), column),
87
91
  scope: "col"
88
92
  }
89
93
 
@@ -127,7 +131,7 @@ module Keystone
127
131
 
128
132
  cell = {
129
133
  value: resolve_value(item, column.key),
130
- classes: tokens.join(" ")
134
+ classes: aligned(tokens.join(" "), column)
131
135
  }
132
136
 
133
137
  link_block = @link_blocks[column.key]
@@ -146,6 +150,28 @@ module Keystone
146
150
  visual_column_count
147
151
  end
148
152
 
153
+ def group_starting_at(index)
154
+ return unless @group_by
155
+
156
+ name = resolve_value(@items[index], @group_by)
157
+ name if index.zero? || name != resolve_value(@items[index - 1], @group_by)
158
+ end
159
+
160
+ def total_cells
161
+ return [] unless @total
162
+
163
+ @columns.map.with_index do |column, index|
164
+ { value: @total[column.key.to_s], classes: aligned(header_classes_for(index), column) }
165
+ end
166
+ end
167
+
168
+ def group_cells(name)
169
+ subtotals = @group_totals.fetch(name.to_s, {}).to_h.transform_keys(&:to_s)
170
+ @columns.map.with_index do |column, index|
171
+ { value: index.zero? ? name : subtotals[column.key.to_s], classes: aligned(header_classes_for(index), column) }
172
+ end
173
+ end
174
+
149
175
  private
150
176
 
151
177
  def apply_saved_layout
@@ -170,6 +196,12 @@ module Keystone
170
196
  end
171
197
  end
172
198
 
199
+ def aligned(classes, column)
200
+ return classes unless column.align
201
+
202
+ [ *classes.split.grep_v(/\Atext-(left|right)\z/), ALIGN_CLASSES.fetch(column.align) ].join(" ")
203
+ end
204
+
173
205
  def resolve_value(item, key)
174
206
  if item.respond_to?(key)
175
207
  item.public_send(key)
@@ -21,6 +21,10 @@ module KeystoneUiHelper
21
21
  render Keystone::Ui::LineChartComponent.new(**args)
22
22
  end
23
23
 
24
+ def ui_bar_chart(**args)
25
+ render Keystone::Ui::BarChartComponent.new(**args)
26
+ end
27
+
24
28
  def ui_cta_banner(**args, &block)
25
29
  render Keystone::Ui::CtaBannerComponent.new(**args), &block
26
30
  end
@@ -21,6 +21,7 @@ module Keystone
21
21
  Keystone::Ui::ModalComponent,
22
22
  Keystone::Ui::SelectComponent,
23
23
  Keystone::Ui::BadgeComponent,
24
+ Keystone::Ui::BarChartComponent,
24
25
  Keystone::Ui::CopyButtonComponent,
25
26
  Keystone::Ui::CardComponent,
26
27
  Keystone::Ui::ButtonComponent,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KeystoneUi
4
- VERSION = "0.44.0"
4
+ VERSION = "0.46.0"
5
5
  end
@@ -82,6 +82,7 @@ not available in the app yet, that is `keystone_ui-install`'s job.
82
82
  - `ui_swipe_deck` — a stack of cards accepted or rejected by button or swipe.
83
83
  - `ui_chart_card` — a titled card holding a chart.
84
84
  - `ui_line_chart` — a line chart with one line per series, over labels or dates.
85
+ - `ui_bar_chart` — a bar chart drawn on the server, which needs no JavaScript and prints.
85
86
  - `ui_funnel` — a conversion funnel drawn as bars or one joined shape.
86
87
  - `ui_bucket` — an upright container filled toward a goal.
87
88
  - `ui_bucket_series` — a wrapping row of buckets.
@@ -333,13 +334,18 @@ two exceptions: `ui_page`'s `padding:` treats any value other than `:none` as
333
334
 
334
335
  8. Build tables.
335
336
 
336
- - `ui_data_table(items:, columns:, empty_message: nil, sort: nil, sort_direction: nil, sort_url: nil, hidden_columns: [], key: nil)`
337
+ - `ui_data_table(items:, columns:, empty_message: nil, sort: nil, sort_direction: nil, sort_url: nil, hidden_columns: [], key: nil, group_by: nil, group_totals: {}, total: nil)`
337
338
  takes a block yielding the table. `items:` are records or hashes, and a
338
339
  cell's value is the column key called on the item, or `item[key]`.
340
+ `group_by:` lists rows under heading rows named by that key's value,
341
+ `group_totals:` (`{ "Group name" => { column_key => value } }`) puts
342
+ subtotals in each heading row, and `total:` (`{ column_key => value }`)
343
+ ends the table with a total row. Pass every subtotal and total already
344
+ calculated. `align: :start` or `:end` on a `Column` aligns it left or right.
339
345
  - `columns:` takes `{ key: "Label" }` hashes when every column is plain.
340
346
  Switch the whole set to `Keystone::Ui::Column` objects once one column
341
347
  needs an option.
342
- - `Keystone::Ui::Column.new(key, header_text, mobile_hidden: false, sortable: false, hideable: false, locked: false)`
348
+ - `Keystone::Ui::Column.new(key, header_text, mobile_hidden: false, sortable: false, hideable: false, locked: false, align: nil)`
343
349
  — `mobile_hidden:` hides the column below `sm:`, `sortable:` gives it a
344
350
  sort header, and `hideable:` lets it be hidden and moved.
345
351
  `locked: true` on the first column keeps it in view while the rest scrolls
@@ -450,6 +456,10 @@ two exceptions: `ui_page`'s `padding:` treats any value other than `:none` as
450
456
 
451
457
  - `ui_chart_card(title:, height: :md)` takes a block holding a chart, with
452
458
  `height:` `:sm` `:md` `:lg`.
459
+ - `ui_bar_chart(bars:, show_values: false)` — `bars:` is
460
+ `[{ label:, value:, shaded: }, ...]`, where `shaded: true` draws a bar
461
+ faded. It is drawn on the server as an SVG, so use it, not
462
+ `ui_line_chart`, for a chart that must print or show without JavaScript.
453
463
  - `ui_line_chart(series:, labels: nil, dates: nil, height: :md)` —
454
464
  `series:` is `[{ name:, data:, color:, dashed: }, ...]`, where `color:`
455
465
  is a CSS colour and `dashed: true` is optional. Pass exactly one of
@@ -11,6 +11,7 @@ develop:
11
11
  - ui_stat_card
12
12
  - ui_chart_card
13
13
  - ui_line_chart
14
+ - ui_bar_chart
14
15
  - ui_cta_banner
15
16
  - ui_feature_grid
16
17
  - ui_hero
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.44.0
4
+ version: 0.46.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.20.0
18
+ version: 0.22.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.20.0
25
+ version: 0.22.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: view_component
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -82,6 +82,8 @@ files:
82
82
  - app/components/keystone/ui/alert_component.rb
83
83
  - app/components/keystone/ui/badge_component.html.erb
84
84
  - app/components/keystone/ui/badge_component.rb
85
+ - app/components/keystone/ui/bar_chart_component.html.erb
86
+ - app/components/keystone/ui/bar_chart_component.rb
85
87
  - app/components/keystone/ui/bottom_nav_component.html.erb
86
88
  - app/components/keystone/ui/bottom_nav_component.rb
87
89
  - app/components/keystone/ui/bottom_nav_item_component.html.erb