jirametrics 2.7 → 2.11

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +4 -4
  3. data/lib/jirametrics/aging_work_bar_chart.rb +7 -5
  4. data/lib/jirametrics/aging_work_in_progress_chart.rb +105 -41
  5. data/lib/jirametrics/aging_work_table.rb +50 -2
  6. data/lib/jirametrics/board.rb +33 -5
  7. data/lib/jirametrics/board_config.rb +6 -2
  8. data/lib/jirametrics/board_movement_calculator.rb +147 -0
  9. data/lib/jirametrics/change_item.rb +19 -6
  10. data/lib/jirametrics/chart_base.rb +59 -21
  11. data/lib/jirametrics/css_variable.rb +1 -1
  12. data/lib/jirametrics/cycletime_config.rb +37 -5
  13. data/lib/jirametrics/cycletime_histogram.rb +67 -2
  14. data/lib/jirametrics/data_quality_report.rb +174 -35
  15. data/lib/jirametrics/download_config.rb +2 -2
  16. data/lib/jirametrics/downloader.rb +44 -25
  17. data/lib/jirametrics/examples/aggregated_project.rb +2 -5
  18. data/lib/jirametrics/examples/standard_project.rb +4 -6
  19. data/lib/jirametrics/expedited_chart.rb +7 -7
  20. data/lib/jirametrics/exporter.rb +10 -20
  21. data/lib/jirametrics/file_config.rb +23 -6
  22. data/lib/jirametrics/file_system.rb +39 -4
  23. data/lib/jirametrics/flow_efficiency_scatterplot.rb +2 -4
  24. data/lib/jirametrics/groupable_issue_chart.rb +1 -3
  25. data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -12
  26. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +22 -5
  27. data/lib/jirametrics/html/aging_work_table.erb +6 -4
  28. data/lib/jirametrics/html/cycletime_histogram.erb +74 -0
  29. data/lib/jirametrics/html/cycletime_scatterplot.erb +1 -10
  30. data/lib/jirametrics/html/daily_wip_chart.erb +1 -10
  31. data/lib/jirametrics/html/expedited_chart.erb +1 -10
  32. data/lib/jirametrics/html/hierarchy_table.erb +1 -1
  33. data/lib/jirametrics/html/index.css +28 -5
  34. data/lib/jirametrics/html/index.erb +8 -4
  35. data/lib/jirametrics/html/sprint_burndown.erb +1 -10
  36. data/lib/jirametrics/html/throughput_chart.erb +1 -10
  37. data/lib/jirametrics/html_report_config.rb +32 -23
  38. data/lib/jirametrics/issue.rb +104 -44
  39. data/lib/jirametrics/jira_gateway.rb +16 -3
  40. data/lib/jirametrics/project_config.rb +223 -120
  41. data/lib/jirametrics/sprint_burndown.rb +1 -1
  42. data/lib/jirametrics/status.rb +81 -26
  43. data/lib/jirametrics/status_collection.rb +74 -40
  44. data/lib/jirametrics/throughput_chart.rb +1 -1
  45. data/lib/jirametrics/value_equality.rb +2 -2
  46. data/lib/jirametrics.rb +7 -1
  47. metadata +8 -13
  48. data/lib/jirametrics/discard_changes_before.rb +0 -37
  49. data/lib/jirametrics/html/data_quality_report.erb +0 -138
@@ -39,6 +39,7 @@ class Downloader
39
39
  # board_ids = @download_config.board_ids
40
40
 
41
41
  remove_old_files
42
+ update_status_history_file
42
43
  download_statuses
43
44
  find_board_ids.each do |id|
44
45
  board = download_board_configuration board_id: id
@@ -66,7 +67,7 @@ class Downloader
66
67
 
67
68
  def download_issues board:
68
69
  log " Downloading primary issues for board #{board.id}", both: true
69
- path = "#{@target_path}#{@download_config.project_config.file_prefix}_issues/"
70
+ path = File.join(@target_path, "#{file_prefix}_issues/")
70
71
  unless Dir.exist?(path)
71
72
  log " Creating path #{path}"
72
73
  Dir.mkdir(path)
@@ -102,8 +103,6 @@ class Downloader
102
103
  json = @jira_gateway.call_url relative_url: '/rest/api/2/search' \
103
104
  "?jql=#{escaped_jql}&maxResults=#{max_results}&startAt=#{start_at}&expand=changelog&fields=*all"
104
105
 
105
- exit_if_call_failed json
106
-
107
106
  json['issues'].each do |issue_json|
108
107
  issue_json['exporter'] = {
109
108
  'in_initial_query' => initial_query
@@ -138,45 +137,62 @@ class Downloader
138
137
  end
139
138
  end
140
139
 
141
- def exit_if_call_failed json
142
- # Sometimes Jira returns the singular form of errorMessage and sometimes the plural. Consistency FTW.
143
- return unless json['error'] || json['errorMessages'] || json['errorMessage']
144
-
145
- log "Download failed. See #{@file_system.logfile_name} for details.", both: true
146
- log " #{JSON.pretty_generate(json)}"
147
- exit 1
148
- end
149
-
150
140
  def download_statuses
151
141
  log ' Downloading all statuses', both: true
152
142
  json = @jira_gateway.call_url relative_url: '/rest/api/2/status'
153
143
 
154
144
  @file_system.save_json(
155
145
  json: json,
156
- filename: "#{@target_path}#{@download_config.project_config.file_prefix}_statuses.json"
146
+ filename: File.join(@target_path, "#{file_prefix}_statuses.json")
157
147
  )
158
148
  end
159
149
 
150
+ def update_status_history_file
151
+ status_filename = File.join(@target_path, "#{file_prefix}_statuses.json")
152
+ return unless file_system.file_exist? status_filename
153
+
154
+ status_json = file_system.load_json(status_filename)
155
+
156
+ history_filename = File.join(@target_path, "#{file_prefix}_status_history.json")
157
+ history_json = file_system.load_json(history_filename) if file_system.file_exist? history_filename
158
+
159
+ if history_json
160
+ file_system.log ' Updating status history file', also_write_to_stderr: true
161
+ else
162
+ file_system.log ' Creating status history file', also_write_to_stderr: true
163
+ history_json = []
164
+ end
165
+
166
+ status_json.each do |status_item|
167
+ id = status_item['id']
168
+ history_item = history_json.find { |s| s['id'] == id }
169
+ history_json.delete(history_item) if history_item
170
+ history_json << status_item
171
+ end
172
+
173
+ file_system.save_json(filename: history_filename, json: history_json)
174
+ end
175
+
160
176
  def download_board_configuration board_id:
161
177
  log " Downloading board configuration for board #{board_id}", both: true
162
178
  json = @jira_gateway.call_url relative_url: "/rest/agile/1.0/board/#{board_id}/configuration"
163
179
 
164
- exit_if_call_failed json
165
-
166
- file_prefix = @download_config.project_config.file_prefix
167
- @file_system.save_json json: json, filename: "#{@target_path}#{file_prefix}_board_#{board_id}_configuration.json"
180
+ @file_system.save_json(
181
+ json: json,
182
+ filename: File.join(@target_path, "#{file_prefix}_board_#{board_id}_configuration.json")
183
+ )
168
184
 
169
185
  # We have a reported bug that blew up on this line. Moved it after the save so we can
170
186
  # actually look at the returned json.
171
187
  @board_id_to_filter_id[board_id] = json['filter']['id'].to_i
172
188
 
173
189
  download_sprints board_id: board_id if json['type'] == 'scrum'
174
- Board.new raw: json
190
+ # TODO: Should be passing actual statuses, not empty list
191
+ Board.new raw: json, possible_statuses: StatusCollection.new
175
192
  end
176
193
 
177
194
  def download_sprints board_id:
178
195
  log " Downloading sprints for board #{board_id}", both: true
179
- file_prefix = @download_config.project_config.file_prefix
180
196
  max_results = 100
181
197
  start_at = 0
182
198
  is_last = false
@@ -184,11 +200,10 @@ class Downloader
184
200
  while is_last == false
185
201
  json = @jira_gateway.call_url relative_url: "/rest/agile/1.0/board/#{board_id}/sprint?" \
186
202
  "maxResults=#{max_results}&startAt=#{start_at}"
187
- exit_if_call_failed json
188
203
 
189
204
  @file_system.save_json(
190
205
  json: json,
191
- filename: "#{@target_path}#{file_prefix}_board_#{board_id}_sprints_#{start_at}.json"
206
+ filename: File.join(@target_path, "#{file_prefix}_board_#{board_id}_sprints_#{start_at}.json")
192
207
  )
193
208
  is_last = json['isLast']
194
209
  max_results = json['maxResults']
@@ -201,7 +216,7 @@ class Downloader
201
216
  end
202
217
 
203
218
  def metadata_pathname
204
- "#{@target_path}#{@download_config.project_config.file_prefix}_meta.json"
219
+ File.join(@target_path, "#{file_prefix}_meta.json")
205
220
  end
206
221
 
207
222
  def load_metadata
@@ -244,17 +259,17 @@ class Downloader
244
259
  end
245
260
 
246
261
  def remove_old_files
247
- file_prefix = @download_config.project_config.file_prefix
248
262
  Dir.foreach @target_path do |file|
249
263
  next unless file.match?(/^#{file_prefix}_\d+\.json$/)
264
+ next if file == "#{file_prefix}_status_history.json"
250
265
 
251
- File.unlink "#{@target_path}#{file}"
266
+ File.unlink File.join(@target_path, file)
252
267
  end
253
268
 
254
269
  return if @cached_data_format_is_current
255
270
 
256
271
  # Also throw away all the previously downloaded issues.
257
- path = File.join @target_path, "#{file_prefix}_issues"
272
+ path = File.join(@target_path, "#{file_prefix}_issues")
258
273
  return unless File.exist? path
259
274
 
260
275
  Dir.foreach path do |file|
@@ -292,4 +307,8 @@ class Downloader
292
307
 
293
308
  segments.join ' AND '
294
309
  end
310
+
311
+ def file_prefix
312
+ @download_config.project_config.get_file_prefix
313
+ end
295
314
  end
@@ -3,8 +3,6 @@
3
3
  # This file is really intended to give you ideas about how you might configure your own reports, not
4
4
  # as a complete setup that will work in every case.
5
5
  #
6
- # See https://github.com/mikebowler/jirametrics/wiki/Examples-folder for more details
7
- #
8
6
  # The point of an AGGREGATED report is that we're now looking at a higher level. We might use this in a
9
7
  # S2 meeting (Scrum of Scrums) to talk about the things that are happening across teams, not within a
10
8
  # single team. For that reason, we look at slightly different things that we would on a single team board.
@@ -13,6 +11,7 @@ class Exporter
13
11
  def aggregated_project name:, project_names:, settings: {}
14
12
  project name: name do
15
13
  puts name
14
+ file_prefix name
16
15
  self.settings.merge! settings
17
16
 
18
17
  aggregate do
@@ -21,8 +20,6 @@ class Exporter
21
20
  end
22
21
  end
23
22
 
24
- file_prefix name
25
-
26
23
  file do
27
24
  file_suffix '.html'
28
25
  issues.reject! do |issue|
@@ -34,7 +31,7 @@ class Exporter
34
31
  board_lines = []
35
32
  included_projects.each do |project|
36
33
  project.all_boards.each_value do |board|
37
- board_lines << "<a href='#{project.file_prefix}.html'>#{board.name}</a> from project #{project.name}"
34
+ board_lines << "<a href='#{project.get_file_prefix}.html'>#{board.name}</a> from project #{project.name}"
38
35
  end
39
36
  end
40
37
  board_lines.sort.each { |line| html "<li>#{line}</li>", type: :header }
@@ -2,8 +2,6 @@
2
2
 
3
3
  # This file is really intended to give you ideas about how you might configure your own reports, not
4
4
  # as a complete setup that will work in every case.
5
- #
6
- # See https://github.com/mikebowler/jirametrics/wiki/Examples-folder for more
7
5
  class Exporter
8
6
  def standard_project name:, file_prefix:, ignore_issues: nil, starting_status: nil, boards: {},
9
7
  default_board: nil, anonymize: false, settings: {}, status_category_mappings: {},
@@ -12,15 +10,15 @@ class Exporter
12
10
 
13
11
  project name: name do
14
12
  puts name
15
- self.anonymize if anonymize
13
+ file_prefix file_prefix
16
14
 
15
+ self.anonymize if anonymize
17
16
  self.settings.merge! settings
18
17
 
19
18
  status_category_mappings.each do |status, category|
20
19
  status_category_mapping status: status, category: category
21
20
  end
22
21
 
23
- file_prefix file_prefix
24
22
  download do
25
23
  self.rolling_date_count(rolling_date_count) if rolling_date_count
26
24
  self.no_earlier_than(no_earlier_than) if no_earlier_than
@@ -30,8 +28,8 @@ class Exporter
30
28
  block = boards[board_id]
31
29
  if block == :default
32
30
  block = lambda do |_|
33
- start_at first_time_in_status_category('In Progress')
34
- stop_at still_in_status_category('Done')
31
+ start_at first_time_in_status_category(:indeterminate)
32
+ stop_at still_in_status_category(:done)
35
33
  end
36
34
  end
37
35
  board id: board_id do
@@ -63,7 +63,7 @@ class ExpeditedChart < ChartBase
63
63
  next unless change.priority?
64
64
 
65
65
  if expedited_priority_names.include? change.value
66
- expedite_start = change.time
66
+ expedite_start = change.time.to_date
67
67
  elsif expedite_start
68
68
  start_date = expedite_start.to_date
69
69
  stop_date = change.time.to_date
@@ -72,7 +72,7 @@ class ExpeditedChart < ChartBase
72
72
  (start_date < date_range.begin && stop_date > date_range.end)
73
73
 
74
74
  result << [expedite_start, :expedite_start]
75
- result << [change.time, :expedite_stop]
75
+ result << [change.time.to_date, :expedite_stop]
76
76
  end
77
77
  expedite_start = nil
78
78
  end
@@ -109,11 +109,11 @@ class ExpeditedChart < ChartBase
109
109
 
110
110
  def make_expedite_lines_data_set issue:, expedite_data:
111
111
  cycletime = issue.board.cycletime
112
- started_time, stopped_time = cycletime.started_stopped_times(issue)
112
+ started_date, stopped_date = cycletime.started_stopped_dates(issue)
113
113
 
114
- expedite_data << [started_time, :issue_started] if started_time
115
- expedite_data << [stopped_time, :issue_stopped] if stopped_time
116
- expedite_data.sort_by! { |a| a[0] }
114
+ expedite_data << [started_date, :issue_started] if started_date
115
+ expedite_data << [stopped_date, :issue_stopped] if stopped_date
116
+ expedite_data.sort_by!(&:first)
117
117
 
118
118
  # If none of the data would be visible on the chart then skip it.
119
119
  return nil unless expedite_data.any? { |time, _action| time.to_date >= date_range.begin }
@@ -150,7 +150,7 @@ class ExpeditedChart < ChartBase
150
150
 
151
151
  unless expedite_data.empty?
152
152
  last_change_time = expedite_data[-1][0].to_date
153
- if last_change_time && last_change_time <= date_range.end && stopped_time.nil?
153
+ if last_change_time && last_change_time <= date_range.end && stopped_date.nil?
154
154
  data << make_point(issue: issue, time: date_range.end, label: 'Still ongoing', expedited: expedited)
155
155
  dot_colors << '' # It won't be visible so it doesn't matter
156
156
  point_styles << 'dash'
@@ -2,18 +2,6 @@
2
2
 
3
3
  require 'fileutils'
4
4
 
5
- class Object
6
- def deprecated message:, date:
7
- text = +''
8
- text << "Deprecated(#{date}): "
9
- text << message
10
- caller(1..2).each do |line|
11
- text << "\n-> Called from #{line}"
12
- end
13
- warn text
14
- end
15
- end
16
-
17
5
  class Exporter
18
6
  attr_reader :project_configs
19
7
  attr_accessor :file_system
@@ -76,12 +64,8 @@ class Exporter
76
64
  selected = []
77
65
  each_project_config(name_filter: name_filter) do |project|
78
66
  project.evaluate_next_level
79
- # next if project.aggregated_project?
80
67
 
81
68
  project.run load_only: true
82
- project.board_configs.each do |board_config|
83
- board_config.run
84
- end
85
69
  project.issues.each do |issue|
86
70
  selected << [project, issue] if keys.include? issue.key
87
71
  end
@@ -91,9 +75,13 @@ class Exporter
91
75
  raise unless e.message.start_with? 'This is an aggregated project and issues should have been included'
92
76
  end
93
77
 
94
- selected.each do |project, issue|
95
- puts "\nProject #{project.name}"
96
- puts issue.dump
78
+ if selected.empty?
79
+ file_system.log "No issues found to match #{keys.collect(&:inspect).join(', ')}"
80
+ else
81
+ selected.each do |project, issue|
82
+ file_system.log "\nProject #{project.name}", also_write_to_stderr: true
83
+ file_system.log issue.dump, also_write_to_stderr: true
84
+ end
97
85
  end
98
86
  end
99
87
 
@@ -128,7 +116,9 @@ class Exporter
128
116
 
129
117
  def jira_config filename = nil
130
118
  if filename
131
- @jira_config = file_system.load_json(filename)
119
+ @jira_config = file_system.load_json(filename, fail_on_error: false)
120
+ raise "Unable to load Jira configuration file and cannot continue: #{filename.inspect}" if @jira_config.nil?
121
+
132
122
  @jira_config['url'] = $1 if @jira_config['url'] =~ /^(.+)\/+$/
133
123
  end
134
124
  @jira_config
@@ -56,7 +56,7 @@ class FileConfig
56
56
  def output_filename
57
57
  segments = []
58
58
  segments << project_config.target_path
59
- segments << project_config.file_prefix
59
+ segments << project_config.get_file_prefix
60
60
  segments << (@file_suffix || "-#{@today}.csv")
61
61
  segments.join
62
62
  end
@@ -66,15 +66,20 @@ class FileConfig
66
66
  # is that all empty values in the first column should be at the bottom.
67
67
  def sort_output all_lines
68
68
  all_lines.sort do |a, b|
69
+ result = nil
69
70
  if a[0] == b[0]
70
- a[1..] <=> b[1..]
71
+ result = a[1..] <=> b[1..]
71
72
  elsif a[0].nil?
72
- 1
73
+ result = 1
73
74
  elsif b[0].nil?
74
- -1
75
+ result = -1
75
76
  else
76
- a[0] <=> b[0]
77
+ result = a[0] <=> b[0]
77
78
  end
79
+
80
+ # This will only happen if one of the objects isn't comparable. Seen in production.
81
+ result = -1 if result.nil?
82
+ result
78
83
  end
79
84
  end
80
85
 
@@ -85,6 +90,11 @@ class FileConfig
85
90
 
86
91
  def html_report &block
87
92
  assert_only_one_filetype_config_set
93
+ if block.nil?
94
+ project_config.file_system.warning 'No charts were specified for the report. This is almost certainly a mistake.'
95
+ block = ->(_) {}
96
+ end
97
+
88
98
  @html_report = HtmlReportConfig.new file_config: self, block: block
89
99
  end
90
100
 
@@ -103,7 +113,7 @@ class FileConfig
103
113
  def to_datetime object
104
114
  return nil if object.nil?
105
115
 
106
- object = object.to_datetime
116
+ object = object.to_time.to_datetime
107
117
  object = object.new_offset(@timezone_offset) if @timezone_offset
108
118
  object
109
119
  end
@@ -120,4 +130,11 @@ class FileConfig
120
130
  @file_suffix = suffix unless suffix.nil?
121
131
  @file_suffix
122
132
  end
133
+
134
+ def children
135
+ result = []
136
+ result << @columns if @columns
137
+ result << @html_report if @html_report
138
+ result
139
+ end
123
140
  end
@@ -6,14 +6,18 @@ class FileSystem
6
6
  attr_accessor :logfile, :logfile_name
7
7
 
8
8
  # Effectively the same as File.read except it forces the encoding to UTF-8
9
- def load filename
9
+ def load filename, supress_deprecation: false
10
+ if filename.end_with?('.json') && !supress_deprecation
11
+ deprecated(message: 'call load_json instead', date: '2024-11-13')
12
+ end
13
+
10
14
  File.read filename, encoding: 'UTF-8'
11
15
  end
12
16
 
13
17
  def load_json filename, fail_on_error: true
14
18
  return nil if fail_on_error == false && File.exist?(filename) == false
15
19
 
16
- JSON.parse load(filename)
20
+ JSON.parse load(filename, supress_deprecation: true)
17
21
  end
18
22
 
19
23
  def save_json json:, filename:
@@ -27,9 +31,22 @@ class FileSystem
27
31
  File.write(filename, content)
28
32
  end
29
33
 
30
- def log message, also_write_to_stderr: false
34
+ def warning message, more: nil
35
+ log "Warning: #{message}", more: more, also_write_to_stderr: true
36
+ end
37
+
38
+ def error message, more: nil
39
+ log "Error: #{message}", more: more, also_write_to_stderr: true
40
+ end
41
+
42
+ def log message, more: nil, also_write_to_stderr: false
43
+ message += " See #{logfile_name} for more details about this message." if more
44
+
31
45
  logfile.puts message
32
- $stderr.puts message if also_write_to_stderr # rubocop:disable Style/StderrPuts
46
+ logfile.puts more if more
47
+ return unless also_write_to_stderr
48
+
49
+ $stderr.puts message # rubocop:disable Style/StderrPuts
33
50
  end
34
51
 
35
52
  # In some Jira instances, a sizeable portion of the JSON is made up of empty fields. I've seen
@@ -43,4 +60,22 @@ class FileSystem
43
60
  end
44
61
  node
45
62
  end
63
+
64
+ def foreach root, &block
65
+ Dir.foreach root, &block
66
+ end
67
+
68
+ def file_exist? filename
69
+ File.exist? filename
70
+ end
71
+
72
+ def deprecated message:, date:, depth: 2
73
+ text = +''
74
+ text << "Deprecated(#{date}): "
75
+ text << message
76
+ caller(1..depth).each do |line|
77
+ text << "\n-> Called from #{line}"
78
+ end
79
+ log text, also_write_to_stderr: true
80
+ end
46
81
  end
@@ -19,17 +19,15 @@ class FlowEfficiencyScatterplot < ChartBase
19
19
  </div>
20
20
  <div class="p">
21
21
  <math>
22
- <mn>Flow efficiency</mn>
22
+ <mn>Flow efficiency (%)</mn>
23
23
  <mo>=</mo>
24
24
  <mfrac>
25
25
  <mrow><mn>Time adding value</mn></mrow>
26
26
  <mrow><mn>Total time</mn></mrow>
27
27
  </mfrac>
28
- <mo>x</mo>
29
- <mn>100%</mn>
30
28
  </math>
31
29
  </div>
32
- <div style="background: yellow">Note that for this calculation to be accurate, we must be moving items into a
30
+ <div style="background: var(--warning-banner)">Note that for this calculation to be accurate, we must be moving items into a
33
31
  blocked or stalled state the moment we stop working on it, and most teams don't do that.
34
32
  So be aware that your team may have to change their behaviours if you want this chart to be useful.
35
33
  </div>
@@ -6,9 +6,7 @@ require 'jirametrics/grouping_rules'
6
6
  module GroupableIssueChart
7
7
  def init_configuration_block user_provided_block, &default_block
8
8
  instance_eval(&user_provided_block)
9
- return if @group_by_block
10
-
11
- instance_eval(&default_block)
9
+ instance_eval(&default_block) unless @group_by_block
12
10
  end
13
11
 
14
12
  def grouping_rules &block
@@ -38,22 +38,13 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
38
38
  plugins: {
39
39
  annotation: {
40
40
  annotations: {
41
- <% holidays.each_with_index do |range, index| %>
42
- holiday<%= index %>: {
43
- drawTime: 'beforeDraw',
44
- type: 'box',
45
- xMin: '<%= range.begin %>T00:00:00',
46
- xMax: '<%= range.end %>T23:59:59',
47
- backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
48
- borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
49
- },
50
- <% end %>
41
+ <%= working_days_annotation %>
51
42
 
52
43
  <% if percentage_line_x %>
53
44
  line: {
54
45
  type: 'line',
55
- xMin: '<%= percentage_line_x %>',
56
- xMax: '<%= percentage_line_x %>',
46
+ scaleID: 'x',
47
+ value: '<%= percentage_line_x %>',
57
48
  borderColor: <%= CssVariable.new('--aging-work-bar-chart-percentage-line-color').to_json %>,
58
49
  borderWidth: 1,
59
50
  drawTime: 'afterDraw'
@@ -6,7 +6,7 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
6
6
  {
7
7
  type: 'bar',
8
8
  data: {
9
- labels: [<%= column_headings.collect(&:inspect).join(',') %>],
9
+ labels: [<%= @board_columns.collect { |c| c.name.inspect }.join(',') %>],
10
10
  datasets: <%= JSON.generate(data_sets) %>
11
11
  },
12
12
  options: {
@@ -22,8 +22,10 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
22
22
  labelString: 'Date Completed'
23
23
  },
24
24
  grid: {
25
- color: <%= CssVariable['--grid-line-color'].to_json %>
25
+ color: <%= CssVariable['--grid-line-color'].to_json %>,
26
+ z: 1 // draw the grid lines on top of the bars
26
27
  },
28
+ stacked: true
27
29
  },
28
30
  y: {
29
31
  scaleLabel: {
@@ -35,8 +37,11 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
35
37
  text: 'Age in days'
36
38
  },
37
39
  grid: {
38
- color: <%= CssVariable['--grid-line-color'].to_json %>
40
+ color: <%= CssVariable['--grid-line-color'].to_json %>,
41
+ z: 1 // draw the grid lines on top of the bars
39
42
  },
43
+ stacked: true,
44
+ max: <%= (@max_age * 1.1).to_i %>
40
45
  }
41
46
  },
42
47
  plugins: {
@@ -44,14 +49,26 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
44
49
  callbacks: {
45
50
  label: function(context) {
46
51
  if( typeof(context.dataset.data[context.dataIndex]) == "number" ) {
47
- return "85% of the issues, leave this column in "+context.dataset.data[context.dataIndex]+" days";
52
+ let full_data = <%= @bar_data.inspect %>;
53
+ let columnIndex = context.dataIndex;
54
+ let rowIndex = context.datasetIndex - <%= @row_index_offset %>;
55
+ return context.dataset.label + " of completed work items left this column in " +full_data[rowIndex][columnIndex] + " days or less";
48
56
  }
49
57
  else {
50
- return context.dataset.data[context.dataIndex].title
58
+ return context.dataset.data[context.dataIndex].title;
51
59
  }
52
60
  }
53
61
  }
62
+ },
63
+ legend: {
64
+ labels: {
65
+ filter: function(item, chart) {
66
+ // Logic to remove a particular legend item goes here
67
+ return !item.text.includes('%');
68
+ }
69
+ }
54
70
  }
71
+
55
72
  }
56
73
  }
57
74
  });
@@ -1,11 +1,12 @@
1
1
  <table class='standard'>
2
2
  <thead>
3
3
  <tr>
4
- <th>Age (days)</th>
5
- <th>E</th>
6
- <th>B</th>
4
+ <th title="Age in days">Age</th>
5
+ <th title="Expedited">E</th>
6
+ <th title="Blocked / Stalled">B/S</th>
7
7
  <th>Issue</th>
8
8
  <th>Status</th>
9
+ <th>Forecast</th>
9
10
  <th>Fix versions</th>
10
11
  <% if any_scrum_boards %>
11
12
  <th>Sprints</th>
@@ -40,7 +41,8 @@
40
41
  </div>
41
42
  <% end %>
42
43
  </td>
43
- <td><%= format_status issue.status.name, board: issue.board %></td>
44
+ <td><%= format_status issue.status, board: issue.board %></td>
45
+ <td><%= dates_text(issue) %></td>
44
46
  <td><%= fix_versions_text(issue) %></td>
45
47
  <% if any_scrum_boards %>
46
48
  <td><%= sprints_text(issue) %></td>