jirametrics 3.2 → 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 +4 -4
- data/lib/jirametrics/aging_work_bar_chart.rb +111 -10
- data/lib/jirametrics/aging_work_in_progress_chart.rb +10 -6
- data/lib/jirametrics/aging_work_table.rb +22 -7
- data/lib/jirametrics/blocked_stalled_change_stream_builder.rb +15 -2
- data/lib/jirametrics/board_movement_calculator.rb +18 -7
- data/lib/jirametrics/chart_base.rb +39 -5
- data/lib/jirametrics/color_palette.rb +61 -0
- data/lib/jirametrics/cumulative_flow_diagram.rb +9 -6
- data/lib/jirametrics/cycletime_scatterplot.rb +67 -8
- data/lib/jirametrics/daily_wip_chart.rb +1 -1
- data/lib/jirametrics/dependency_chart.rb +106 -15
- data/lib/jirametrics/exporter.rb +77 -11
- data/lib/jirametrics/groupable_issue_chart.rb +27 -2
- data/lib/jirametrics/grouping_rules.rb +13 -1
- data/lib/jirametrics/html/aging_work_bar_chart.erb +16 -5
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +1 -1
- data/lib/jirametrics/html/index.css +77 -5
- data/lib/jirametrics/html/index.erb +6 -2
- data/lib/jirametrics/html/index.js +16 -0
- data/lib/jirametrics/html/legacy_colors.css +18 -0
- data/lib/jirametrics/html/time_based_histogram.erb +34 -18
- data/lib/jirametrics/html/time_based_scatterplot.erb +29 -9
- data/lib/jirametrics/html_generator.rb +20 -1
- data/lib/jirametrics/html_report_config.rb +3 -3
- data/lib/jirametrics/percentile_validation.rb +26 -0
- data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +1 -0
- data/lib/jirametrics/settings.json +1 -0
- data/lib/jirametrics/time_based_histogram.rb +47 -15
- data/lib/jirametrics/time_based_scatterplot.rb +96 -14
- data/lib/jirametrics/trend_line_calculator.rb +5 -2
- data/lib/jirametrics/wip_by_column_chart.rb +1 -1
- metadata +3 -1
|
@@ -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
|
-
|
|
88
|
-
result << ',
|
|
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
|
-
|
|
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
|
-
#
|
|
112
|
-
#
|
|
113
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
data/lib/jirametrics/exporter.rb
CHANGED
|
@@ -257,35 +257,89 @@ class Exporter
|
|
|
257
257
|
|
|
258
258
|
def info key, name_filter:
|
|
259
259
|
selected = []
|
|
260
|
+
searched = []
|
|
260
261
|
file_system.log_only = true
|
|
261
262
|
each_project_config(name_filter: name_filter) do |project|
|
|
262
263
|
project.evaluate_next_level
|
|
263
264
|
|
|
264
265
|
project.run load_only: true
|
|
265
|
-
|
|
266
|
+
matches = matching_issues_in(project, key)
|
|
267
|
+
searched << describe_search(project: project, matches: matches.size)
|
|
268
|
+
selected.concat matches
|
|
266
269
|
rescue => e # rubocop:disable Style/RescueStandardError
|
|
267
270
|
# This happens when we're attempting to load an aggregated project because it hasn't been
|
|
268
271
|
# properly initialized. Since we don't care about aggregated projects, we just ignore it.
|
|
269
272
|
raise unless e.message.start_with? 'This is an aggregated project and issues should have been included'
|
|
273
|
+
|
|
274
|
+
searched << " #{project.name.inspect}: skipped, this is an aggregated project"
|
|
270
275
|
end
|
|
271
276
|
file_system.log_only = false
|
|
272
277
|
|
|
278
|
+
report_info_results key: key, selected: selected, where_we_looked: describe_where_we_looked(key, searched)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Verbose in the log, sparse in the terminal. Where we looked is almost always what you need when
|
|
282
|
+
# an issue you know was downloaded cannot be found, and it is noise the rest of the time.
|
|
283
|
+
def describe_where_we_looked key, searched
|
|
284
|
+
return "Searched these projects for #{key.inspect}:\n#{searched.join "\n"}" unless searched.empty?
|
|
285
|
+
|
|
286
|
+
'No project configurations were searched at all. Either none are defined in the config file ' \
|
|
287
|
+
'or none of them matched the name filter.'
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def report_info_results key:, selected:, where_we_looked:
|
|
273
291
|
if selected.empty?
|
|
274
|
-
file_system.log
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
292
|
+
file_system.log(
|
|
293
|
+
"No issues found to match #{key.inspect}", more: where_we_looked, also_write_to_stderr: true
|
|
294
|
+
)
|
|
295
|
+
return
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
file_system.log where_we_looked
|
|
299
|
+
selected.each do |project, issue, ignored|
|
|
300
|
+
file_system.log "\nProject #{project.name}", also_write_to_stderr: true
|
|
301
|
+
file_system.log(IGNORED_ISSUE_NOTE, also_write_to_stderr: true) if ignored
|
|
302
|
+
file_system.log issue.dump, also_write_to_stderr: true
|
|
280
303
|
end
|
|
281
304
|
end
|
|
282
305
|
|
|
306
|
+
# Only the unexpected case is announced. Not being filtered is the common one, and saying so
|
|
307
|
+
# every time would be noise on top of the dump that was actually asked for. Note the claim is
|
|
308
|
+
# about the filter, not about charts: an issue that survives filtering may still be absent from
|
|
309
|
+
# a given chart for its own reasons, so there is no matching note for the other case.
|
|
310
|
+
IGNORED_ISSUE_NOTE = 'IGNORED by a filter such as ignore_types or ignore_issues. No chart uses it.'
|
|
311
|
+
|
|
312
|
+
# One line per project saying where its issues were read from and what was there, so that a
|
|
313
|
+
# missing issue can be traced to the wrong directory rather than guessed at.
|
|
314
|
+
def describe_search project:, matches:
|
|
315
|
+
prefix = project.get_file_prefix raise_if_not_set: false
|
|
316
|
+
path = prefix.nil? ? '(no file_prefix set)' : File.join(project.target_path.to_s, "#{prefix}_issues")
|
|
317
|
+
counts =
|
|
318
|
+
begin
|
|
319
|
+
collection = project.issues
|
|
320
|
+
excluded = collection.hidden.size
|
|
321
|
+
detail = "#{collection.size} issues loaded"
|
|
322
|
+
detail << ", #{excluded} excluded by filters" if excluded.positive?
|
|
323
|
+
"#{detail}, #{matches} matched"
|
|
324
|
+
rescue StandardError
|
|
325
|
+
'could not be read'
|
|
326
|
+
end
|
|
327
|
+
" #{project.name.inspect}: #{path} (#{counts})"
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Asking about a specific issue should always show that issue, whether or not the report used it.
|
|
331
|
+
# An issue removed by something like ignore_types is exactly when somebody runs info to find out
|
|
332
|
+
# what happened, and reporting it as missing sends them hunting for a download problem that is
|
|
333
|
+
# not there. The hidden list is searched first. Returns [project, issue, ignored] triples.
|
|
283
334
|
def matching_issues_in project, key
|
|
284
335
|
matches = []
|
|
285
|
-
project.issues
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
matches << [project,
|
|
336
|
+
collection = project.issues
|
|
337
|
+
[[collection.hidden, true], [collection, false]].each do |issues, ignored|
|
|
338
|
+
issues.each do |issue|
|
|
339
|
+
matches << [project, issue, ignored] if key == issue.key
|
|
340
|
+
issue.subtasks.each do |subtask|
|
|
341
|
+
matches << [project, subtask, ignored] if key == subtask.key
|
|
342
|
+
end
|
|
289
343
|
end
|
|
290
344
|
end
|
|
291
345
|
matches
|
|
@@ -317,9 +371,21 @@ class Exporter
|
|
|
317
371
|
|
|
318
372
|
def target_path path = nil
|
|
319
373
|
unless path.nil?
|
|
374
|
+
previous = @target_path
|
|
320
375
|
@target_path = path
|
|
321
376
|
@target_path += File::SEPARATOR unless @target_path.end_with? File::SEPARATOR
|
|
322
377
|
FileUtils.mkdir_p @target_path
|
|
378
|
+
# A config is allowed to switch target directories partway through, and when it does, an
|
|
379
|
+
# issue can be downloaded into one and looked for in another. Recording every change makes
|
|
380
|
+
# that visible rather than something to guess at. See GitHub issue 77.
|
|
381
|
+
# The constructor seeds this with a bare '.', and the setter always appends a separator, so
|
|
382
|
+
# an unseparated '.' is the untouched default rather than something the config chose. Setting
|
|
383
|
+
# the same path twice is not a change and saying so would be actively misleading.
|
|
384
|
+
if previous == '.'
|
|
385
|
+
file_system.diagnostic "target_path set to #{@target_path.inspect}"
|
|
386
|
+
elsif previous != @target_path
|
|
387
|
+
file_system.diagnostic "target_path changed from #{previous.inspect} to #{@target_path.inspect}"
|
|
388
|
+
end
|
|
323
389
|
end
|
|
324
390
|
@target_path
|
|
325
391
|
end
|
|
@@ -31,14 +31,39 @@ module GroupableIssueChart
|
|
|
31
31
|
|
|
32
32
|
@issue_hints[issue] = rules.issue_hint
|
|
33
33
|
@issue_periods[issue] = rules.last_day_of_period
|
|
34
|
-
|
|
34
|
+
accumulate_issue_for_group result, rules, issue
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
completed_issues.reject! { |issue| ignored_issues.include? issue }
|
|
38
38
|
|
|
39
39
|
result.each_key do |rules|
|
|
40
|
-
rules.color =
|
|
40
|
+
rules.color = next_palette_color if rules.color.nil?
|
|
41
41
|
end
|
|
42
42
|
result
|
|
43
43
|
end
|
|
44
|
+
|
|
45
|
+
# Ruby's Hash keeps whichever key object it saw first, so a later issue whose rules object
|
|
46
|
+
# has different percentiles needs to be reconciled against that retained key rather than
|
|
47
|
+
# just appended under its own (discarded) key.
|
|
48
|
+
def accumulate_issue_for_group result, rules, issue
|
|
49
|
+
existing_key = result.keys.find { |key| key.eql? rules }
|
|
50
|
+
reconcile_percentiles existing_key, rules if existing_key
|
|
51
|
+
(result[existing_key || rules] ||= []) << issue
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The retained hash key is whichever rules object arrived first, so a later issue setting a
|
|
55
|
+
# different list would silently lose. That can only be a config error, so say so.
|
|
56
|
+
def reconcile_percentiles existing_key, rules
|
|
57
|
+
incoming = rules.percentiles
|
|
58
|
+
return if incoming.nil?
|
|
59
|
+
|
|
60
|
+
existing = existing_key.percentiles
|
|
61
|
+
if existing.nil?
|
|
62
|
+
existing_key.percentiles = incoming
|
|
63
|
+
elsif existing != incoming
|
|
64
|
+
raise ArgumentError,
|
|
65
|
+
"group #{existing_key.label.inspect} was given conflicting percentiles: " \
|
|
66
|
+
"#{existing.inspect} and #{incoming.inspect}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
44
69
|
end
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'jirametrics/percentile_validation'
|
|
4
|
+
|
|
3
5
|
class GroupingRules < Rules
|
|
6
|
+
include PercentileValidation
|
|
7
|
+
|
|
4
8
|
attr_accessor :label, :issue_hint, :label_hint
|
|
5
|
-
attr_reader :color, :last_day_of_period
|
|
9
|
+
attr_reader :color, :last_day_of_period, :percentiles
|
|
10
|
+
|
|
11
|
+
# nil means "inherit whatever the chart is configured with" and an empty list means "draw no
|
|
12
|
+
# lines for this group", so neither is validated. Everything else gets the same treatment as
|
|
13
|
+
# the chart level setter, because these numbers become JavaScript identifiers downstream and a
|
|
14
|
+
# bad one takes the whole chart out with a syntax error.
|
|
15
|
+
def percentiles= list
|
|
16
|
+
@percentiles = list.nil? ? nil : validate_percentiles(list)
|
|
17
|
+
end
|
|
6
18
|
|
|
7
19
|
def last_day_of_period= value
|
|
8
20
|
@last_day_of_period = value.is_a?(String) ? Date.parse(value) : value
|
|
@@ -40,15 +40,26 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
40
40
|
annotations: {
|
|
41
41
|
<%= working_days_annotation %>
|
|
42
42
|
|
|
43
|
-
<%
|
|
44
|
-
|
|
43
|
+
<% @percentage_lines.each do |percentage_line| %>
|
|
44
|
+
<%= percentage_line[:id].to_json %>: {
|
|
45
45
|
type: 'line',
|
|
46
46
|
scaleID: 'x',
|
|
47
|
-
value: '<%=
|
|
47
|
+
value: '<%= percentage_line[:x] %>',
|
|
48
48
|
borderColor: <%= CssVariable.new('--aging-work-bar-chart-percentage-line-color').to_json %>,
|
|
49
49
|
borderWidth: 1,
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
hitTolerance: 6,
|
|
51
|
+
drawTime: 'afterDraw',
|
|
52
|
+
label: {
|
|
53
|
+
display: false,
|
|
54
|
+
content: <%= "#{ordinal percentage_line[:percentile]} percentile of completed work".to_json %>,
|
|
55
|
+
position: 'start',
|
|
56
|
+
backgroundColor: 'rgba(0,0,0,0.85)',
|
|
57
|
+
color: '#fff',
|
|
58
|
+
font: { size: 11 }
|
|
59
|
+
},
|
|
60
|
+
enter(ctx) { ctx.element.label.options.display = true; ctx.chart.draw(); },
|
|
61
|
+
leave(ctx) { ctx.element.label.options.display = false; ctx.chart.draw(); }
|
|
62
|
+
},
|
|
52
63
|
<% end %>
|
|
53
64
|
}
|
|
54
65
|
},
|
|
@@ -57,7 +57,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
57
57
|
}
|
|
58
58
|
nextVisibility = !!legend.chart.getDatasetMeta(i).hidden;
|
|
59
59
|
|
|
60
|
-
// Hide/show the
|
|
60
|
+
// Hide/show the percentile lines for that dataset
|
|
61
61
|
legend.chart.options.plugins.annotation.annotations["line"+(i/2)].display = nextVisibility;
|
|
62
62
|
|
|
63
63
|
// Hide/show the trendline for this dataset, if they were enabled. The trendline is always
|
|
@@ -8,6 +8,20 @@
|
|
|
8
8
|
--cycletime-scatterplot-cap-rule-color: #555555;
|
|
9
9
|
--cycletime-scatterplot-cap-gutter-color: rgba(0, 0, 0, 0.05);
|
|
10
10
|
|
|
11
|
+
/* Palette for things that need telling apart where no specific colour is wanted. Okabe-Ito,
|
|
12
|
+
chosen because it stays distinguishable under the common forms of colour vision deficiency.
|
|
13
|
+
If you need a SPECIFIC colour, configure it rather than relying on whichever slot comes up.
|
|
14
|
+
Add a slot by defining the next number; the count is read from this file, not hardcoded.
|
|
15
|
+
These deliberately have no dark mode overrides yet: lightening them for dark backgrounds
|
|
16
|
+
collapses blue into sky blue, and getting it right needs measuring. See jirametrics-60z. */
|
|
17
|
+
--palette-color-1: #0072B2; /* Okabe-Ito blue */
|
|
18
|
+
--palette-color-2: #E69F00; /* Okabe-Ito orange */
|
|
19
|
+
--palette-color-3: #009E73; /* Okabe-Ito bluish green */
|
|
20
|
+
--palette-color-4: #56B4E9; /* Okabe-Ito sky blue */
|
|
21
|
+
--palette-color-5: #D55E00; /* Okabe-Ito vermilion */
|
|
22
|
+
--palette-color-6: #CC79A7; /* Okabe-Ito reddish purple */
|
|
23
|
+
--palette-color-7: #F0E442; /* Okabe-Ito yellow */
|
|
24
|
+
|
|
11
25
|
--non-working-days-color: #F0F0F0;
|
|
12
26
|
--expedited-color: #D55E00; /* Okabe-Ito vermilion */
|
|
13
27
|
--blocked-color: #D55E00; /* Okabe-Ito vermilion */
|
|
@@ -19,6 +33,41 @@
|
|
|
19
33
|
--type-bug-color: #D55E00; /* Okabe-Ito vermilion */
|
|
20
34
|
--type-spike-color: #CC79A7; /* Okabe-Ito reddish purple */
|
|
21
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
|
+
|
|
22
71
|
--status-category-todo-color: gray;
|
|
23
72
|
--status-category-inprogress-color: #0072B2; /* Okabe-Ito blue */
|
|
24
73
|
--status-category-done-color: #009E73; /* Okabe-Ito bluish green */
|
|
@@ -234,6 +283,10 @@ div.child_issue {
|
|
|
234
283
|
html[data-theme="dark"] {
|
|
235
284
|
--warning-banner: #9F2B00;
|
|
236
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
|
+
|
|
237
290
|
--type-story-color: #2DCB9A; /* lighter bluish green for dark bg */
|
|
238
291
|
--type-task-color: #56B4E9; /* sky blue for dark bg */
|
|
239
292
|
--type-bug-color: #E69F00; /* orange instead of vermilion for dark bg */
|
|
@@ -254,7 +307,14 @@ html[data-theme="dark"] {
|
|
|
254
307
|
--wip-by-column-chart-limit-line-color: #E69F00; /* Okabe-Ito orange */
|
|
255
308
|
--wip-by-column-chart-recommendation-color: #2DCB9A; /* lighter bluish green for dark bg */
|
|
256
309
|
--wip-chart-completed-color: #2DCB9A; /* lighter bluish green */
|
|
257
|
-
|
|
310
|
+
/* Was inheriting the light #92D9C0, which sat too close to the completed colour above for
|
|
311
|
+
anyone with protanopia or deuteranopia. Those two mean "completed" and "completed but not
|
|
312
|
+
started", so they are exactly the pair a reader must separate. See jirametrics-3fg. */
|
|
313
|
+
--wip-chart-completed-but-not-started-color: #00A062;
|
|
314
|
+
/* Was #DE9AC4, a lightened reddish purple that collided with the sky blue used for the fastest
|
|
315
|
+
band. Those are the two ends of the duration ramp, so confusing them is the worst case. This
|
|
316
|
+
stays in the same warm family so the ramp still reads cool to warm as work ages. */
|
|
317
|
+
--wip-chart-duration-more-than-four-weeks-color: #B16BAE;
|
|
258
318
|
--estimate-accuracy-chart-completed-border-color: #2DCB9A;
|
|
259
319
|
--estimate-accuracy-chart-active-border-color: #E69F00;
|
|
260
320
|
--expedited-chart-dot-issue-stopped-color: #2DCB9A;
|
|
@@ -266,7 +326,10 @@ html[data-theme="dark"] {
|
|
|
266
326
|
--sprint-burndown-sprint-color-4: #CC79A7; /* reddish purple (vermilion → orange conflicts with color-2) */
|
|
267
327
|
--sprint-burndown-sprint-color-5: #F0E442; /* yellow */
|
|
268
328
|
--sprint-burndown-sprint-color-6: #D55E00; /* vermilion (sky blue conflicts with color-1) */
|
|
269
|
-
--sprint-burndown-sprint-color-7: #
|
|
329
|
+
--sprint-burndown-sprint-color-7: #388CF3; /* lightened blue. Was #92D9C0 light teal, which
|
|
330
|
+
sat too close to color-3 for anyone with protanopia or deuteranopia. Blue is the one
|
|
331
|
+
Okabe-Ito hue the dark set was missing, since the original is too dark on this
|
|
332
|
+
background. See jirametrics-60z for the measurements. */
|
|
270
333
|
--daily-view-selected-issue-background: #474747;
|
|
271
334
|
--daily-view-issue-border: #2DCB9A;
|
|
272
335
|
--daily-view-selected-issue-border: #E69F00;
|
|
@@ -376,6 +439,12 @@ html[data-theme="light"] {
|
|
|
376
439
|
--warning-banner: #9F2B00;
|
|
377
440
|
|
|
378
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
|
+
|
|
379
448
|
--type-story-color: #2DCB9A; /* lighter bluish green for dark bg */
|
|
380
449
|
--type-task-color: #56B4E9; /* sky blue for dark bg */
|
|
381
450
|
--type-bug-color: #E69F00; /* orange instead of vermilion for dark bg */
|
|
@@ -405,12 +474,12 @@ html[data-theme="light"] {
|
|
|
405
474
|
--hierarchy-table-inactive-item-text-color: #939393;
|
|
406
475
|
|
|
407
476
|
--wip-chart-completed-color: #2DCB9A; /* lighter bluish green */
|
|
408
|
-
--wip-chart-completed-but-not-started-color: #
|
|
477
|
+
--wip-chart-completed-but-not-started-color: #00A062; /* see jirametrics-3fg */
|
|
409
478
|
--wip-chart-duration-less-than-day-color: #56B4E9;
|
|
410
479
|
--wip-chart-duration-week-or-less-color: #F0E442;
|
|
411
480
|
--wip-chart-duration-two-weeks-or-less-color: #E69F00;
|
|
412
481
|
--wip-chart-duration-four-weeks-or-less-color: #D55E00;
|
|
413
|
-
--wip-chart-duration-more-than-four-weeks-color: #
|
|
482
|
+
--wip-chart-duration-more-than-four-weeks-color: #B16BAE; /* see jirametrics-3fg */
|
|
414
483
|
|
|
415
484
|
--estimate-accuracy-chart-completed-border-color: #2DCB9A;
|
|
416
485
|
--estimate-accuracy-chart-active-border-color: #E69F00;
|
|
@@ -425,7 +494,10 @@ html[data-theme="light"] {
|
|
|
425
494
|
--sprint-burndown-sprint-color-4: #CC79A7; /* reddish purple (vermilion → orange conflicts with color-2) */
|
|
426
495
|
--sprint-burndown-sprint-color-5: #F0E442; /* yellow */
|
|
427
496
|
--sprint-burndown-sprint-color-6: #D55E00; /* vermilion (sky blue conflicts with color-1) */
|
|
428
|
-
--sprint-burndown-sprint-color-7: #
|
|
497
|
+
--sprint-burndown-sprint-color-7: #388CF3; /* lightened blue. Was #92D9C0 light teal, which
|
|
498
|
+
sat too close to color-3 for anyone with protanopia or deuteranopia. Blue is the one
|
|
499
|
+
Okabe-Ito hue the dark set was missing, since the original is too dark on this
|
|
500
|
+
background. See jirametrics-60z for the measurements. */
|
|
429
501
|
|
|
430
502
|
--daily-view-selected-issue-background: #474747;
|
|
431
503
|
--daily-view-issue-border: #2DCB9A;
|
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
<title><%= project_name.empty? ? 'JiraMetrics' : "JiraMetrics - #{project_name}" %></title>
|
|
5
5
|
<link rel="icon" type="image/png" href="https://github.com/mikebowler/jirametrics/blob/main/favicon.png?raw=true" />
|
|
6
6
|
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.js"></script>
|
|
7
|
-
|
|
7
|
+
<!-- Pinned to a major version, not to an exact one: patch and minor releases still arrive
|
|
8
|
+
automatically, but a new major cannot land in someone's report unannounced. The charts
|
|
9
|
+
depend on specific annotation plugin behaviour (hitTolerance, enter/leave, the label's own
|
|
10
|
+
drawTime), and nothing in the test suite reaches browser behaviour to catch a break. -->
|
|
11
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
|
8
12
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@^1"></script>
|
|
9
|
-
<script src="https://
|
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@3"></script>
|
|
10
14
|
<script type="text/javascript">
|
|
11
15
|
<%= javascript %>
|
|
12
16
|
</script>
|
|
@@ -141,6 +141,22 @@ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', eve
|
|
|
141
141
|
|
|
142
142
|
// Draw a diagonal pattern to highlight sections of a bar chart. Based on code found at:
|
|
143
143
|
// https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns
|
|
144
|
+
// Apply an alpha to a colour that may only be resolvable in the browser, such as one that came
|
|
145
|
+
// from a CSS variable. Ruby cannot do this arithmetic any more now that palette colours are
|
|
146
|
+
// variables rather than literals.
|
|
147
|
+
function withAlpha(color, alpha) {
|
|
148
|
+
const probe = document.createElement('canvas').getContext('2d')
|
|
149
|
+
probe.fillStyle = color
|
|
150
|
+
const resolved = probe.fillStyle // normalised by the browser to #rrggbb or rgba(...)
|
|
151
|
+
if (resolved.startsWith('#')) {
|
|
152
|
+
const r = parseInt(resolved.substr(1, 2), 16)
|
|
153
|
+
const g = parseInt(resolved.substr(3, 2), 16)
|
|
154
|
+
const b = parseInt(resolved.substr(5, 2), 16)
|
|
155
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`
|
|
156
|
+
}
|
|
157
|
+
return resolved.replace(/^rgb\(/, 'rgba(').replace(/\)$/, `, ${alpha})`)
|
|
158
|
+
}
|
|
159
|
+
|
|
144
160
|
function createDiagonalPattern(color = 'black') {
|
|
145
161
|
// create a 5x5 px canvas for the pattern's base shape
|
|
146
162
|
let shape = document.createElement('canvas')
|
|
@@ -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;
|