janela 0.9.0 → 0.11.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.
data/docs/roadmap.md CHANGED
@@ -8,7 +8,7 @@ Topics: roadmap, releases, scope, planning
8
8
 
9
9
  <svg viewBox="0 0 680 360" width="100%" role="img" aria-labelledby="vista-title vista-desc" class="vista-art" style="display: block; margin: 1.75rem 0; border-radius: 10px;">
10
10
  <title id="vista-title">Vista</title>
11
- <desc id="vista-desc">Sea and sky with a horizon across them. Eight lights burn on the near water, one for each issue in 1.0, and three sit far off at the horizon for the work still in sight past it. A low sun rises and sets on the horizon as the pointer moves up and down, and never climbs higher: the sky above is empty, because what is not coming is not in view.</desc>
11
+ <desc id="vista-desc">Sea and sky with a horizon across them. Six lights burn on the near water, one for each issue in 1.0, and three sit far off at the horizon for the work still in sight past it. A low sun rises and sets on the horizon as the pointer moves up and down, and never climbs higher: the sky above is empty, because what is not coming is not in view.</desc>
12
12
 
13
13
  <style>
14
14
  .vista-art .v-far { transform: translate3d(calc(var(--vitral-shift-x, 0) * 13px), calc(var(--vitral-shift-y, 0) * 7px), 0); transition: transform .45s cubic-bezier(.2,.7,.3,1); }
@@ -99,10 +99,6 @@ Topics: roadmap, releases, scope, planning
99
99
  <path d="M427 234 h10" stroke-width="1" stroke-opacity="0.2"/>
100
100
  </g>
101
101
  <g class="v-near">
102
- <circle cx="74" cy="330" r="22" fill="url(#vLamp)" opacity="0.4"/>
103
- <circle cx="74" cy="330" r="4.3" fill="#FFF6E2" opacity="0.88"/>
104
- <circle cx="154" cy="302" r="20" fill="url(#vLamp)" opacity="0.4"/>
105
- <circle cx="154" cy="302" r="4.0" fill="#FFF6E2" opacity="0.88"/>
106
102
  <circle cx="238" cy="340" r="21" fill="url(#vLamp)" opacity="0.4"/>
107
103
  <circle cx="238" cy="340" r="4.2" fill="#FFF6E2" opacity="0.88"/>
108
104
  <circle cx="312" cy="283" r="18" fill="url(#vLamp)" opacity="0.4"/>
@@ -143,30 +139,29 @@ parameters change only on a major version after 1.0. Until then they can
143
139
  change in any release, and every change of that kind carries an entry in
144
140
  `UPGRADING.md`.
145
141
 
146
- **A pane can be read.** Janela draws tables, bars and lines. A
147
- part-to-whole split currently has to be drawn as bars, a table row cannot
148
- carry context beside its label, and a chart takes Chart.js's default
149
- proportions whether or not they suit the page. Those are not extra
150
- features. They are the 5% not finished, and most of them were found by
151
- people installing the gem rather than reading it.
142
+ **A pane can be read.** Janela draws tables, bars, lines, doughnuts and
143
+ pies. A table row still cannot carry context beside its label, a single
144
+ number has no way to say how prominent it is, and a chart takes whatever
145
+ height its width gives it whether or not that suits the page. Those are
146
+ not extra features. They are the 5% not finished, and most of them were
147
+ found by people installing the gem rather than reading it.
152
148
 
153
149
  **The doctor can be trusted.** `rails janela:doctor` checks an
154
150
  installation for the mistakes that produce a dashboard showing numbers
155
- nobody should see. Two of its checks have no test that makes them fire.
156
- A check nobody can prove is working is a check nobody should rely on.
151
+ nobody should see. Every one of its checks now has a test that makes it
152
+ fire and one that leaves it quiet, because a check nobody can prove is
153
+ working is a check nobody should rely on.
157
154
 
158
155
  ## In 1.0
159
156
 
160
- The eight lights burning in the near ground.
157
+ The six lights burning in the near ground.
161
158
 
162
159
  | Issue | What |
163
160
  | --- | --- |
164
161
  | [#24](https://github.com/retail-tasker/janela/issues/24) | A chart's height and aspect ratio are the host's to set |
165
162
  | [#27](https://github.com/retail-tasker/janela/issues/27) | A ratio measure, so refusing `average:` over a boolean offers somewhere to go |
166
- | [#30](https://github.com/retail-tasker/janela/issues/30) | Doughnut and pie, and a categorical palette that makes them readable |
167
163
  | [#31](https://github.com/retail-tasker/janela/issues/31) | A pane says how prominent it is |
168
164
  | [#34](https://github.com/retail-tasker/janela/issues/34) | A table pane carries an attribute column beside its label |
169
- | [#53](https://github.com/retail-tasker/janela/issues/53) | The last two doctor checks get tests |
170
165
  | [#14](https://github.com/retail-tasker/janela/issues/14) | A Sprockets host serves the engine's JavaScript, or is told it cannot |
171
166
  | [#6](https://github.com/retail-tasker/janela/issues/6) | How contributions are accepted, who cuts a release, where to report a vulnerability |
172
167
 
@@ -178,10 +173,10 @@ Progress is tracked on the
178
173
  The three lights far off at the horizon. Wanted, not blocking a stable
179
174
  release, and being on this list is not a refusal.
180
175
 
181
- - **Drill-down on time panes** ([#18](https://github.com/retail-tasker/janela/issues/18)).
182
- Clicking a month could filter every other pane to it, or narrow that
183
- pane to weeks within it. Both are reasonable, they need different
184
- things from the URL, and ADR 006 left the question open on purpose. It
176
+ - **Drilling down on time panes** ([#65](https://github.com/retail-tasker/janela/issues/65)).
177
+ Clicking a month now filters every other pane to it (#18). Narrowing
178
+ that pane itself to the weeks within it is a different gesture, with
179
+ its own questions about getting back out and about the URL, and it
185
180
  needs a decision record before any code.
186
181
  - **A command palette for the demo** ([#41](https://github.com/retail-tasker/janela/issues/41)).
187
182
  The demo site, not the gem.
data/docs/theming.md CHANGED
@@ -25,13 +25,15 @@ the theme.
25
25
 
26
26
  ### Custom properties
27
27
 
28
- Three, and setting them moves everything that depends on them.
28
+ Setting any of these moves everything that depends on it.
29
29
 
30
30
  | Property | Default | What it does |
31
31
  | --- | --- | --- |
32
32
  | `--janela-space` | `0.25rem` | The base unit of the whole spacing scale. Every gap and padding is a multiple of it. |
33
33
  | `--janela-line` | `rgba(128, 128, 128, 0.3)` | Rules between rows, borders on cards and fields. |
34
- | `--janela-accent` | `rgb(54, 162, 235)` | A selected value, a hovered card. |
34
+ | `--janela-accent` | `rgb(54, 162, 235)` | A selected value, a hovered card, a line chart (#62), and the first colour of the palette below. |
35
+ | `--janela-series-1` to `--janela-series-8` | the accent, then `#eb6834`, `#1baf7a`, `#eda100`, `#e87ba4`, `#008300`, `#4a3aa7`, `#e34948` | The colour a bar, a doughnut slice or a pie slice is drawn in, by its position in the pane, first to eighth (ADR 046). |
36
+ | `--janela-series-other` | `#8c8c8c` | Every position after the eighth. The palette is never cycled, so the ninth value is not drawn like the first. |
35
37
 
36
38
  ```css
37
39
  :root {
@@ -70,11 +72,21 @@ the names a theme spends most of its time on.
70
72
  | `janela-value-number` | `<strong>` | the number itself |
71
73
  | `janela-chart` | `<canvas>` | a bar or line pane, inside its `<figure>` |
72
74
  | `janela-chart-title` | `<figcaption>` | its caption (ADR 042) |
75
+ | `janela-ring` | `<figure>` | a doughnut or pie pane (ADR 046) |
76
+ | `janela-ring-svg` | `<svg>` | its picture; `data-hole` is `true` for a doughnut |
77
+ | `janela-ring-slice` | `<path>` | one slice; `janela-dim` is added to those not selected while something is |
78
+ | `janela-legend` | `<table>` | its legend, one row of swatch, button and value per slice |
79
+ | `janela-swatch` | `<span>` | the colour beside a legend label |
73
80
  | `janela-empty` | `<p>` | a pane whose query returned nothing |
74
81
  | `janela-error` | `<p>` | a pane that could not be read |
75
82
  | `janela-content` | `<div>` | a stored pane holding words or a host partial rather than a query (ADR 039) |
76
83
  | `janela-content-heading` | `<h2>` | a text pane's heading |
77
84
 
85
+ The dark values that passed the palette check on a dark surface are
86
+ `#3987e5`, `#d95926`, `#199e70`, `#c98500`, `#d55181`, `#008300`,
87
+ `#9085e9` and `#e66767`. `janela.css` ships no dark scheme (ADR 023), so
88
+ a dark theme sets the eight properties to them.
89
+
78
90
  A table pane renders a `<caption>` and a chart pane a `<figcaption>`,
79
91
  each its own accessible name; the chart's canvas points to its
80
92
  figcaption with `aria-labelledby` rather than repeating the string in
@@ -51,9 +51,7 @@ module Janela
51
51
 
52
52
  if dimension.time?
53
53
  granularity = Dimension.granularity!(granularity || dimension.granularity)
54
- options = granularity == "week" ? { week_start: Date.beginning_of_week } : {}
55
- buckets = relation.group_by_period(granularity, dimension.qualified_column, **options)
56
- measure.apply(buckets).transform_keys { |bucket| dimension.label(bucket, granularity) }
54
+ bucketed(measure, dimension, relation, granularity).transform_keys { |bucket| dimension.label(bucket, granularity) }
57
55
  else
58
56
  grouped = relation.group(dimension.attribute).order(Arel.sql("#{measure.sql_alias} DESC"))
59
57
  grouped = grouped.limit(limit ? limit!(limit) : MAXIMUM)
@@ -61,6 +59,20 @@ module Janela
61
59
  end
62
60
  end
63
61
 
62
+ # The buckets of a time dimension keyed by the bucket itself, where query
63
+ # keys them by label. A label is lossy ("Sep 2026", "Q3 2026"), and a click
64
+ # on a bucket has to write the range it covers, which only the bucket can
65
+ # say (ADR 045).
66
+ def series(measure_name, by:, where: {}, on: nil, granularity: nil)
67
+ measure = measure!(measure_name)
68
+ dimension = dimension!(by)
69
+ raise Error, "#{by.inspect} is not a time dimension" unless dimension.time?
70
+
71
+ relation = filter(on || model.all, where)
72
+ relation = relation.left_joins(dimension.through) if dimension.through
73
+ bucketed(measure, dimension, relation, Dimension.granularity!(granularity || dimension.granularity))
74
+ end
75
+
64
76
  def dimension!(name)
65
77
  dimensions.fetch(name) { raise NotFound, "#{model} has no janela dimension #{name.inspect}" }
66
78
  end
@@ -106,6 +118,11 @@ module Janela
106
118
  end
107
119
 
108
120
  private
121
+ def bucketed(measure, dimension, relation, granularity)
122
+ options = granularity == "week" ? { week_start: Date.beginning_of_week } : {}
123
+ measure.apply(relation.group_by_period(granularity, dimension.qualified_column, **options))
124
+ end
125
+
109
126
  def filter(relation, params)
110
127
  return relation if params.empty?
111
128
 
@@ -4,12 +4,20 @@ module Janela
4
4
 
5
5
  # What a click produces (ADR 024), plus not_null: excluding the null
6
6
  # group is documented, tested behaviour ADR 025 did not measure and would
7
- # otherwise silently break.
8
- CATEGORICAL_PREDICATES = %w[eq in null not_null].freeze
7
+ # otherwise silently break. not_eq and not_in (ADR 044) say what a
8
+ # dimension excludes, which an _in list naming every other value cannot
9
+ # do without rotting the day a new value appears.
10
+ CATEGORICAL_PREDICATES = %w[eq in not_eq not_in null not_null].freeze
9
11
 
10
12
  # A time dimension additionally narrows a range (ADR 006).
11
13
  TIME_PREDICATES = (CATEGORICAL_PREDICATES + %w[gteq gt lteq lt]).freeze
12
14
 
15
+ # How far a bucket reaches, so a click on it can name the range it covers
16
+ # (ADR 045). A quarter is three months, which is why it is not a step of
17
+ # its own.
18
+ STEPS = { "hour" => 1.hour, "day" => 1.day, "week" => 1.week, "month" => 1.month,
19
+ "quarter" => 3.months, "year" => 1.year }.freeze
20
+
13
21
  # A group of rows whose dimension is null. Labelled rather than blank, and
14
22
  # filtered with Ransack's null predicate rather than an empty string.
15
23
  NONE = "(none)".freeze
@@ -54,6 +62,27 @@ module Janela
54
62
  time? ? TIME_PREDICATES : CATEGORICAL_PREDICATES
55
63
  end
56
64
 
65
+ # The bucket's start and the next bucket's start, in the zone the bucket
66
+ # was made in. The end is exclusive, so one bucket's end is the next
67
+ # one's start and no row falls in both (ADR 045).
68
+ def span(bucket, granularity = self.granularity)
69
+ from = bucket.in_time_zone
70
+ [ from, from + STEPS.fetch(granularity.to_s) ]
71
+ end
72
+
73
+ # The same range as the two values a filter carries: dates for a date
74
+ # column, and for a timestamp the zone's ISO 8601 with its offset, which
75
+ # Ransack reads back in the same zone (measured against Brisbane).
76
+ def bounds(bucket, granularity = self.granularity)
77
+ span(bucket, granularity).map { |moment| date_column? ? moment.to_date.iso8601 : moment.iso8601 }
78
+ end
79
+
80
+ def date_column?
81
+ klass.type_for_attribute(column.to_s).type == :date
82
+ rescue ActiveRecord::ActiveRecordError
83
+ false
84
+ end
85
+
57
86
  def attribute
58
87
  klass.arel_table[column]
59
88
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Janela
4
- VERSION = "0.9.0"
4
+ VERSION = "0.11.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: janela
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jay Killeen
@@ -129,6 +129,7 @@ files:
129
129
  - app/views/janela/panes/new.html.erb
130
130
  - app/views/janela/panes/show.html.erb
131
131
  - app/views/janela/queries/_query.html.erb
132
+ - app/views/janela/queries/_ring.html.erb
132
133
  - app/views/janela/queries/show.html.erb
133
134
  - app/views/janela/shared/_errors.html.erb
134
135
  - app/views/layouts/janela/application.html.erb
@@ -141,6 +142,7 @@ files:
141
142
  - db/migrate/20260921000001_add_owner_to_janela_snapshots.rb
142
143
  - db/migrate/20260924000001_add_content_to_janela_panes.rb
143
144
  - db/migrate/20260924000002_add_key_to_janela_frames.rb
145
+ - db/migrate/20260928000001_add_default_filter_to_janela_frames.rb
144
146
  - docs/composing.md
145
147
  - docs/decisions/001-built-to-be-forked.md
146
148
  - docs/decisions/002-measures-and-dimensions-over-ransack.md
@@ -184,6 +186,10 @@ files:
184
186
  - docs/decisions/040-a-host-can-fix-a-frames-filter.md
185
187
  - docs/decisions/041-a-host-finds-its-frame-by-owner-and-key.md
186
188
  - docs/decisions/042-a-charts-title-is-a-figcaption.md
189
+ - docs/decisions/043-a-frames-default-filter-names-the-model-it-narrows.md
190
+ - docs/decisions/044-a-categorical-dimension-can-say-what-it-excludes.md
191
+ - docs/decisions/045-clicking-a-time-bucket-filters-the-frame-to-its-range.md
192
+ - docs/decisions/046-a-ring-is-server-drawn-svg-and-a-palette-is-eight-fixed-colours.md
187
193
  - docs/decisions/INDEX.md
188
194
  - docs/multi-tenancy.md
189
195
  - docs/naming.md
@@ -209,22 +215,16 @@ metadata:
209
215
  bug_tracker_uri: https://github.com/retail-tasker/janela/issues
210
216
  rubygems_mfa_required: 'true'
211
217
  post_install_message: |
212
- Janela 0.9.0: a migration if you use stored frames, and a markup
213
- check if you wrote CSS or JavaScript against a chart pane.
218
+ Janela 0.11.0: no migration. Two things to look for, and if
219
+ neither describes you there is nothing to do.
214
220
 
215
- 1. If you use stored frames, take two migrations together:
216
- bin/rails janela:install:migrations && bin/rails db:migrate
217
- A pane can now hold words or a host partial instead of a query
218
- (ADR 039), and Janela::Frame.for(owner, key) finds a frame kept
219
- for one of your pages without a column of your own (ADR 041).
220
- Existing frames and panes are unaffected either way.
221
+ Bar charts are drawn in a palette now, not all one colour (ADR 046).
222
+ If you want the old look, UPGRADING.md has the lines that restore it.
221
223
 
222
- 2. A chart pane's title is now a visible <figcaption> inside a
223
- <figure> wrapping the canvas, not only the canvas's aria-label
224
- (ADR 042). janela-pane moved from the <canvas> to the <figure>;
225
- janela-chart stayed on the canvas. If you selected
226
- .janela-pane.janela-chart as one element, or read a chart's
227
- title from aria-label, both need updating.
224
+ A range a reader sends in q[...], such as q[placed_on_gteq], no
225
+ longer narrows the time pane it names, since clicking a time bucket
226
+ now writes exactly that (ADR 045). A range you fix yourself with
227
+ where: or default_where still does.
228
228
 
229
229
  Steps: UPGRADING.md in this gem, or
230
230
  https://github.com/retail-tasker/janela/blob/main/UPGRADING.md