jirametrics 3.3 → 3.3.1

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: 25832ca03b7dcdc188d741171ee47d3f463551d1893257e39a9b52c0fb45dd83
4
- data.tar.gz: a4d1b9ce7039e568e8c1a614862688887bc4e188c3b24af646922983eb859b9f
3
+ metadata.gz: 6a2003eb5e447333b282b856e553c7f3f68792da373ce3b563c7f9e4e56dec45
4
+ data.tar.gz: 50b443fbcad6fe9dff8b247a7e0662cfed8c41724bb1e8dbd5d17c33a58ff416
5
5
  SHA512:
6
- metadata.gz: cdbfed22a95da07665930c339330d1f6def0f3ce36118fa68fd249295bbb7386d1a85bd0fe4301de8c5fc35d94e1a98bdee22f81daf9fc59db8095ea02000cec
7
- data.tar.gz: 9122e7aa6cfb881723e7acc7b20df65a12ba372ab0736e94fe2e92e6e5206f69c342b94242e5f7e67d43135a426dab7c82b01d8e57fd5f33ee2a2227b0302445
6
+ metadata.gz: bdaa2c39f21752d0a1a815dc39dd10235cea508f873d89a32d722ea5db4d0d59eef1738febdffde062eaa31cb9b8c24598e2e0b96ccce478a296fe255c0b7997
7
+ data.tar.gz: 8a9013ed9d3bd5ebef3d2a70c1b870ec5624125d0f2fb5989d375e4d005b7259646d66b24d9babcf49d794671dbc65505770b16c5438624406e4dd21e182b98a
@@ -32,9 +32,40 @@ class DependencyChart < ChartBase
32
32
  attr_accessor :color, :label
33
33
  end
34
34
 
35
+ # A fill and the label colour that goes on top of it, kept together because they are not
36
+ # independent choices: some of these fills are light and take black text, others are dark and
37
+ # take white, and a node that took its fill from one and its label from another would be
38
+ # unreadable. Always hand them out as a pair.
39
+ Palette = Struct.new :fill, :label
40
+
41
+ def self.palette_entry name
42
+ Palette.new CssVariable["--dependency-chart-#{name}-color"],
43
+ CssVariable["--dependency-chart-#{name}-label-color"]
44
+ end
45
+
46
+ PALETTE = {
47
+ story: palette_entry('story'),
48
+ task: palette_entry('task'),
49
+ bug: palette_entry('bug'),
50
+ epic: palette_entry('epic'),
51
+ spike: palette_entry('spike')
52
+ }.freeze
53
+
35
54
  def initialize rules_block
36
55
  super()
37
56
 
57
+ # Not the inherited type colours, because these are fills with label text sitting on top of
58
+ # them and those are line colours, judged against the page rather than against the text. See
59
+ # index.css for which colours these are and why.
60
+ @palette_by_type = {
61
+ 'Story' => PALETTE[:story],
62
+ 'Task' => PALETTE[:task],
63
+ 'Bug' => PALETTE[:bug],
64
+ 'Defect' => PALETTE[:bug],
65
+ 'Epic' => PALETTE[:epic],
66
+ 'Spike' => PALETTE[:spike]
67
+ }
68
+
38
69
  header_text 'Dependencies'
39
70
  description_text <<-HTML
40
71
  <p>
@@ -57,7 +88,7 @@ class DependencyChart < ChartBase
57
88
  '<div>No data matched the selected criteria. Nothing to show.</div>'
58
89
  end
59
90
 
60
- svg = execute_graphviz(dot_graph.join("\n"))
91
+ svg = restore_css_variables execute_graphviz(dot_graph.join("\n"))
61
92
  "<h1 class='foldable'>#{@header_text}</h1><div>#{@description_text}#{shrink_svg svg}</div>"
62
93
  end
63
94
 
@@ -77,6 +108,42 @@ class DependencyChart < ChartBase
77
108
  result
78
109
  end
79
110
 
111
+ # Graphviz has never heard of CSS variables. Handed one it emits a warning nobody sees and
112
+ # falls back to black, which is how a node ends up as black text on a black background. So a
113
+ # variable is swapped for a placeholder colour here and mapped back to the variable in the
114
+ # generated SVG, by #restore_css_variables. Anything that isn't a variable is left alone.
115
+ #
116
+ # The placeholders only need to be colours that nobody would ever choose deliberately, so that
117
+ # the CSS selectors matching them in the SVG cannot hit anything else.
118
+ def graphviz_color color
119
+ return color unless color.is_a? CssVariable
120
+
121
+ css_variable_placeholders[color.name] ||= format '#fe00%02x', css_variable_placeholders.size + 1
122
+ end
123
+
124
+ def css_variable_placeholders
125
+ @css_variable_placeholders ||= {}
126
+ end
127
+
128
+ # Turns the placeholders from #graphviz_color back into the variables they stood for, by way of
129
+ # a stylesheet that selects on the placeholder value. Rewriting the attributes in place would be
130
+ # the obvious move, but var() is only legal in a CSS value and not in an SVG presentation
131
+ # attribute, so the colour has to arrive as a real CSS rule. Author rules beat presentation
132
+ # attributes, so the placeholder never wins.
133
+ #
134
+ # fillcolor and fontcolor come out of graphviz as fill and color comes out as stroke, and an
135
+ # arrowhead uses both, so every placeholder gets a rule for each.
136
+ def restore_css_variables svg
137
+ return svg if css_variable_placeholders.empty?
138
+
139
+ rules = css_variable_placeholders.map do |name, placeholder|
140
+ %([fill="#{placeholder}"]{fill:var(#{name})}[stroke="#{placeholder}"]{stroke:var(#{name})})
141
+ end
142
+ svg.sub(/(?<opening_tag><svg\b[^>]*>)/) do
143
+ "#{Regexp.last_match[:opening_tag]}<style>#{rules.join}</style>"
144
+ end
145
+ end
146
+
80
147
  def make_dot_link issue_link:, link_rules:
81
148
  result = +''
82
149
  result << issue_link.origin.key.inspect
@@ -84,8 +151,9 @@ class DependencyChart < ChartBase
84
151
  result << issue_link.other_issue.key.inspect
85
152
  result << '['
86
153
  result << 'label=' << (link_rules.label || issue_link.label).inspect
87
- result << ',color=' << (link_rules.line_color || 'gray').inspect
88
- result << ',fontcolor=' << (link_rules.line_color || 'gray').inspect
154
+ line_color = graphviz_color(link_rules.line_color || default_link_color)
155
+ result << ',color=' << line_color.inspect
156
+ result << ',fontcolor=' << line_color.inspect
89
157
  result << ',dir=both' if link_rules.bidirectional_arrows?
90
158
  result << '];'
91
159
  result
@@ -102,24 +170,47 @@ class DependencyChart < ChartBase
102
170
  tooltip = "#{issue.key}: #{issue.summary}"
103
171
  result << ",tooltip=#{tooltip[0..80].inspect}"
104
172
  unless issue_rules.color == :none
105
- result << %(,style=filled,fillcolor="#{issue_rules.color || color_for(type: issue.type)}")
173
+ fill_color = graphviz_color(issue_rules.color || color_for(type: issue.type))
174
+ result << %(,style=filled,fillcolor="#{fill_color}")
106
175
  end
176
+ result << %(,fontcolor="#{graphviz_color label_color(issue: issue, issue_rules: issue_rules)}")
107
177
  result << ']'
108
178
  result
109
179
  end
110
180
 
111
- # This used to pull colours from chart_base but the migration to CSS colours kept breaking
112
- # this chart so we moved it here, until we're finished with the rest. TODO: Revisit whether
113
- # this can also use customizable CSS colours
181
+ # A filled node is its own background, so its label is measured against the fill rather than
182
+ # against the page.
183
+ def label_color issue:, issue_rules:
184
+ return CssVariable['--default-text-color'] if issue_rules.color == :none
185
+
186
+ # A colour somebody configured is one we know nothing about, so guessing that a particular
187
+ # type's label would suit it is worse than falling back to the general one.
188
+ return CssVariable['--dependency-chart-label-color'] if issue_rules.color
189
+
190
+ palette_for(issue.type).label
191
+ end
192
+
114
193
  def color_for type:
115
- @chart_colors = {
116
- 'Story' => '#90EE90',
117
- 'Task' => '#87CEFA',
118
- 'Bug' => '#ffdab9',
119
- 'Defect' => '#ffdab9',
120
- 'Epic' => '#fafad2',
121
- 'Spike' => '#DDA0DD' # light purple
122
- }[type] ||= next_palette_color
194
+ palette_for(type).fill
195
+ end
196
+
197
+ def palette_for type
198
+ @palette_by_type[type] ||= next_palette_entry
199
+ end
200
+
201
+ def default_link_color
202
+ CssVariable['--dependency-chart-link-color']
203
+ end
204
+
205
+ # An issue type we have no colour for cannot come from the shared palette, the way it does on
206
+ # every other chart. That palette holds line colours, judged against the page, and its first slot
207
+ # is Okabe-Ito blue, which leaves black label text at 4.05:1 once it becomes the fill behind it.
208
+ # These rotate this chart's own entries, which are all known to be comfortable. Two unknown types
209
+ # can therefore land on the same colour as each other or as a known type, which costs little:
210
+ # every node already names its type in the label.
211
+ def next_palette_entry
212
+ @fallback_color_index = (@fallback_color_index || -1) + 1
213
+ PALETTE.values[@fallback_color_index % PALETTE.size]
123
214
  end
124
215
 
125
216
  def build_dot_graph
@@ -33,6 +33,41 @@
33
33
  --type-bug-color: #D55E00; /* Okabe-Ito vermilion */
34
34
  --type-spike-color: #CC79A7; /* Okabe-Ito reddish purple */
35
35
 
36
+ /* The dependency chart is drawn by graphviz, which sets the label text inside the node rather
37
+ than beside it, so unlike --type-*-color above these are judged against the text and not
38
+ against the page. They are opaque, so the fill is the background the text sits on and there
39
+ is nothing for a dark theme to override.
40
+
41
+ These are Okabe-Ito hues rather than Okabe-Ito itself. At full strength they sit in a middle
42
+ band of lightness where NEITHER black nor white text is comfortable on them, which is the
43
+ thing to understand before changing any of this. Contrast ratio alone will mislead you here:
44
+ two earlier attempts cleared 7:1 and then 9:1, both of which WCAG calls comfortable, and both
45
+ still read as muddy in a real report. A mid-toned fill with dark text on it looks wrong at
46
+ almost any ratio. Going DARK and putting white text on it is what actually reads, so most of
47
+ these fills are dark, and each carries its own label colour: they are a pair, and changing a
48
+ fill without rechecking its label is how you get unreadable nodes.
49
+
50
+ They are not all dark, because the lightness spread is what does most of the work of keeping
51
+ them apart. Levelling them all to the same darkness measures about 3, which is unusable;
52
+ pastelling them all measures about 4.7, which is what the original colours here did. Run
53
+ `rake check_colors` for what they currently measure, against the full Okabe-Ito set as a
54
+ reference ceiling. */
55
+ --dependency-chart-story-color: #015C41; /* Okabe-Ito bluish green, darkened */
56
+ --dependency-chart-story-label-color: white;
57
+ --dependency-chart-task-color: #56B4E9; /* Okabe-Ito sky blue, unchanged */
58
+ --dependency-chart-task-label-color: black;
59
+ --dependency-chart-bug-color: #783200; /* Okabe-Ito vermilion, darkened. Also Defect */
60
+ --dependency-chart-bug-label-color: white;
61
+ --dependency-chart-epic-color: #F0E442; /* Okabe-Ito yellow, unchanged */
62
+ --dependency-chart-epic-label-color: black;
63
+ --dependency-chart-spike-color: #762A58; /* Okabe-Ito reddish purple, darkened */
64
+ --dependency-chart-spike-label-color: white;
65
+
66
+ /* For a node whose fill was set in the config rather than coming from the palette above, where
67
+ we cannot know what would read on it. */
68
+ --dependency-chart-label-color: black;
69
+ --dependency-chart-link-color: gray;
70
+
36
71
  --status-category-todo-color: gray;
37
72
  --status-category-inprogress-color: #0072B2; /* Okabe-Ito blue */
38
73
  --status-category-done-color: #009E73; /* Okabe-Ito bluish green */
@@ -248,6 +283,10 @@ div.child_issue {
248
283
  html[data-theme="dark"] {
249
284
  --warning-banner: #9F2B00;
250
285
  --non-working-days-color: #2f2f2f;
286
+
287
+ /* See the matching comment in the prefers-color-scheme block below. */
288
+ --dependency-chart-link-color: #999999;
289
+
251
290
  --type-story-color: #2DCB9A; /* lighter bluish green for dark bg */
252
291
  --type-task-color: #56B4E9; /* sky blue for dark bg */
253
292
  --type-bug-color: #E69F00; /* orange instead of vermilion for dark bg */
@@ -400,6 +439,12 @@ html[data-theme="light"] {
400
439
  --warning-banner: #9F2B00;
401
440
 
402
441
  --non-working-days-color: #2f2f2f;
442
+
443
+ /* The only dependency chart colour needing a dark variant. The fills do not, because they
444
+ are opaque and are their own background, but link lines and their labels sit on the page.
445
+ Plain gray manages only 2.4:1 here against 3.94:1 in light mode, so it is lifted to match. */
446
+ --dependency-chart-link-color: #999999;
447
+
403
448
  --type-story-color: #2DCB9A; /* lighter bluish green for dark bg */
404
449
  --type-task-color: #56B4E9; /* sky blue for dark bg */
405
450
  --type-bug-color: #E69F00; /* orange instead of vermilion for dark bg */
@@ -30,6 +30,22 @@ html[data-theme="light"] {
30
30
  --type-bug-color: orange;
31
31
  --type-spike-color: #9400D3;
32
32
 
33
+ /* The dependency chart's original pastels. They are pleasant but they are not safe: pale
34
+ colours sit close together, and green, peach and pale yellow are hard to separate for anyone
35
+ with red-green colour vision deficiency. Every one of them is light, so the label colours all
36
+ go back to black; the shipped set has dark fills that need white. */
37
+ --dependency-chart-story-color: #90EE90;
38
+ --dependency-chart-story-label-color: black;
39
+ --dependency-chart-task-color: #87CEFA;
40
+ --dependency-chart-task-label-color: black;
41
+ --dependency-chart-bug-color: #ffdab9;
42
+ --dependency-chart-bug-label-color: black;
43
+ --dependency-chart-epic-color: #fafad2;
44
+ --dependency-chart-epic-label-color: black;
45
+ --dependency-chart-spike-color: #DDA0DD;
46
+ --dependency-chart-spike-label-color: black;
47
+ --dependency-chart-link-color: gray;
48
+
33
49
  --status-category-todo-color: gray;
34
50
  --status-category-inprogress-color: #2663ff;
35
51
  --status-category-done-color: #00ff00;
@@ -106,6 +122,7 @@ html[data-theme="light"] {
106
122
  html[data-theme="dark"] {
107
123
  --warning-banner: #9F2B00;
108
124
  --non-working-days-color: #2f2f2f;
125
+ --dependency-chart-link-color: gray; /* the chart had no dark variant originally */
109
126
  --type-story-color: #6fb86f;
110
127
  --type-task-color: #0021b3;
111
128
  --type-bug-color: #bb5603;
@@ -139,6 +156,7 @@ html[data-theme="dark"] {
139
156
  :root {
140
157
  --warning-banner: #9F2B00;
141
158
  --non-working-days-color: #2f2f2f;
159
+ --dependency-chart-link-color: gray; /* the chart had no dark variant originally */
142
160
  --type-story-color: #6fb86f;
143
161
  --type-task-color: #0021b3;
144
162
  --type-bug-color: #bb5603;
@@ -100,16 +100,20 @@ if show_stats
100
100
  <% the_stats.each do |k, v| %>
101
101
  <tr>
102
102
  <td><%= k %></td>
103
- <td style="text-align: right;"><%= v[:min] %></td>
104
- <td style="text-align: right;"><%= v[:max] %></td>
105
- <td style="text-align: right;"><%= sprintf('%.2f', v[:average]) %></td>
106
- <td><%= v[:mode].join(', ') %></td>
103
+ <td style="text-align: right;"><%= stats_cell v[:min] %></td>
104
+ <td style="text-align: right;"><%= stats_cell v[:max] %></td>
105
+ <td style="text-align: right;"><%= stats_cell(v[:average]) { |average| format '%.2f', average } %></td>
106
+ <td><%= stats_cell(v[:mode]) { |modes| modes.join ', ' } %></td>
107
107
  <% percentiles.each do |percentile| %>
108
- <td style="text-align: right;"><%= v[:percentiles][percentile] %></td>
108
+ <td style="text-align: right;"><%= stats_cell v.dig(:percentiles, percentile) %></td>
109
109
  <% end %>
110
110
  </tr>
111
111
  <% end %>
112
112
  </table>
113
+ <% if any_empty_stats? the_stats %>
114
+ <p><b>&ndash;</b> means no usable data for that group. Every item in it was excluded from the histogram
115
+ because it had no measurable cycle time. The Data Quality report has the details.</p>
116
+ <% end %>
113
117
  </div>
114
118
  <div>
115
119
  <p>These statistics help understand the <i>"shape"</i> of the histogram distribution, to help us with predictions.</p>
@@ -94,6 +94,20 @@ class TimeBasedHistogram < TimeBasedChart
94
94
  items_hash
95
95
  end
96
96
 
97
+ # One cell of the statistics table. A group can survive grouping and still have nothing to plot,
98
+ # when every item in it was excluded for having no measurable cycle time, and then there are no
99
+ # statistics to show for it at all. Dashing the cells keeps the group visible: dropping the row
100
+ # would make it look like the group had never existed.
101
+ def stats_cell value
102
+ return '&ndash;' if value.nil?
103
+
104
+ block_given? ? yield(value) : value
105
+ end
106
+
107
+ def any_empty_stats? the_stats
108
+ the_stats.any? { |_label, stats| stats.empty? }
109
+ end
110
+
97
111
  def stats_for histogram_data:, percentiles:
98
112
  return {} if histogram_data.empty?
99
113
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: '3.3'
4
+ version: 3.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler