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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +15 -5
- data/UPGRADING.md +81 -0
- data/app/assets/javascripts/janela/chart_controller.js +76 -10
- data/app/assets/javascripts/janela/frame_controller.js +25 -5
- data/app/assets/stylesheets/janela.css +41 -0
- data/app/models/janela/frame.rb +29 -0
- data/app/models/janela/pane.rb +2 -2
- data/app/models/janela/query.rb +144 -12
- data/app/views/janela/queries/_query.html.erb +14 -11
- data/app/views/janela/queries/_ring.html.erb +41 -0
- data/config/locales/en.yml +6 -0
- data/db/migrate/20260928000001_add_default_filter_to_janela_frames.rb +10 -0
- data/docs/composing.md +19 -0
- data/docs/decisions/006-time-dimensions-with-groupdate.md +1 -1
- data/docs/decisions/043-a-frames-default-filter-names-the-model-it-narrows.md +163 -0
- data/docs/decisions/044-a-categorical-dimension-can-say-what-it-excludes.md +140 -0
- data/docs/decisions/045-clicking-a-time-bucket-filters-the-frame-to-its-range.md +206 -0
- data/docs/decisions/046-a-ring-is-server-drawn-svg-and-a-palette-is-eight-fixed-colours.md +199 -0
- data/docs/decisions/INDEX.md +15 -11
- data/docs/roadmap.md +15 -20
- data/docs/theming.md +14 -2
- data/lib/janela/definition.rb +20 -3
- data/lib/janela/dimension.rb +31 -2
- data/lib/janela/version.rb +1 -1
- metadata +15 -15
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.
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
features. They are the 5% not finished, and most of them were
|
|
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.
|
|
156
|
-
|
|
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
|
|
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
|
-
- **
|
|
182
|
-
Clicking a month
|
|
183
|
-
pane to weeks within it
|
|
184
|
-
|
|
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
|
-
|
|
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
|
data/lib/janela/definition.rb
CHANGED
|
@@ -51,9 +51,7 @@ module Janela
|
|
|
51
51
|
|
|
52
52
|
if dimension.time?
|
|
53
53
|
granularity = Dimension.granularity!(granularity || dimension.granularity)
|
|
54
|
-
|
|
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
|
|
data/lib/janela/dimension.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/janela/version.rb
CHANGED
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.
|
|
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.
|
|
213
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|