keystone_ui 0.29.5 → 0.30.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: 5df45efe6cf162e4a96fa164e01d6ccdf623f8a84d14c530fd5dca4b42eec941
4
- data.tar.gz: f1366496636edb0d10f9e50c4b75c4dddc21801634cce63ba67cf4bfe009dfd6
3
+ metadata.gz: 53f39974ced2866fb949b3bfbe0e1be0359977df8a890f9bf83e20b8a04cff11
4
+ data.tar.gz: 7dc3d1ae90c88bd89ccff1c67a861f6e391558605edb5aaaa136f3c674e47cb6
5
5
  SHA512:
6
- metadata.gz: f249e58a7511397eab772a9e1c9c54c323c0583ddafe41c4da6741d54d8ee57e3590d9c400f75fbd7f5a71d4aa4b0fa30cb0f6223ee6ab817b0240ab539b976b
7
- data.tar.gz: dd617c68d26134469be161339b9c0158a2cd39f4f95ee29d9f6e3546f85b4e60cc4009a2d30d431f31792c3dea9752489df8e48032493b5dde5ab872e5a12135
6
+ metadata.gz: ce349e68888fb9085cf9475eb3a96cb6dec23e193df5912905e559f5371dcb7d936d7e6b80a09d820fa28a91da0993c67add11578729dd37e62f25b3d7bac787
7
+ data.tar.gz: 37cfadba853c471e4ba8a4e9cf94c941e9a554151a76ba9bba4573b80bf80b49850ac713a55da7f148b3aabd6fb253a36b1089c5c132f0afa86d1072aa6e6216
@@ -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,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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KeystoneUi
4
- VERSION = "0.29.5"
4
+ VERSION = "0.30.0"
5
5
  end
@@ -323,10 +323,20 @@ 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:, height: :md)` — a line chart. `labels:` is the
327
- x-axis labels; `series:` is `[{ name:, data:, color:, dashed: }, ...]` where
328
- `color:` (a CSS color string for the line) and `dashed: true` are optional.
329
- `height:` `:sm` `:md` `:lg`.
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.
330
340
  - `ui_funnel(steps:)` — a conversion funnel. `steps:` is
331
341
  `[{ label:, value:, color: }, ...]` in order, with `color:` optional. Bar
332
342
  widths are relative to the first step; the caption between two layers is the
@@ -443,6 +453,12 @@ outer element. See Conventions before using it.
443
453
  On a radio card or a checkbox row, text every reader needs to choose goes in `hint:` and
444
454
  text only some will want goes in `info:`. Which is which is the app's own
445
455
  wording, so ask the developer rather than pick.
456
+ For a line chart, pass `dates:` when each value belongs to a calendar day
457
+ and `labels:` when the horizontal axis is anything else, such as week names
458
+ or categories. With `dates:`, a day with no value leaves a wider gap between
459
+ its neighbours rather than a point at zero. Whether a missing day is left
460
+ out or passed as a zero is the app's own rule, so ask the developer rather
461
+ than pick. Never format dates into strings and pass them as `labels:`.
446
462
  Put the actions on a record, such as Edit and Delete, in an action menu
447
463
  rather than in a row of buttons: `menu:` on the `ui_section` that shows the
448
464
  record, `table.actions` for a table row, or `ui_action_menu` anywhere else.
@@ -129,6 +129,16 @@ 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 line chart is labelled or dated.** A line chart draws one line per
133
+ series, and each series is a name and its values, with an optional colour
134
+ and an optional dashed line. Its horizontal axis is given one of two ways,
135
+ never both and never neither, and the wrong combination raises
136
+ `ArgumentError`. Labels are text, one per value, spaced evenly and shown as
137
+ written. Dates are one day per value, and a dated chart places each point by
138
+ its day, so a gap of a week is seven times as wide as a gap of a day. A dated
139
+ axis runs from the first day to the last, marks only whole days, and reads
140
+ each day as a date such as "Oct 2, 2026", which is also what a hovered point
141
+ shows. A time of day is dropped, so a dated chart has one position per day.
132
142
  - **A calculation is text.** The calculation behind a figure is the opposite
133
143
  case. It is a list of groups, each with an optional title and its lines, and
134
144
  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.8.0 or later with it.
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
 
@@ -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
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.29.5
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Schneider