jirametrics 2.13 → 2.30

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/bin/jirametrics-mcp +5 -0
  3. data/lib/jirametrics/aggregate_config.rb +10 -2
  4. data/lib/jirametrics/aging_work_bar_chart.rb +191 -133
  5. data/lib/jirametrics/aging_work_in_progress_chart.rb +43 -11
  6. data/lib/jirametrics/aging_work_table.rb +9 -7
  7. data/lib/jirametrics/anonymizer.rb +81 -6
  8. data/lib/jirametrics/atlassian_document_format.rb +101 -97
  9. data/lib/jirametrics/bar_chart_range.rb +17 -0
  10. data/lib/jirametrics/blocked_stalled_change.rb +5 -3
  11. data/lib/jirametrics/board.rb +32 -8
  12. data/lib/jirametrics/board_config.rb +4 -1
  13. data/lib/jirametrics/board_feature.rb +14 -0
  14. data/lib/jirametrics/board_movement_calculator.rb +2 -2
  15. data/lib/jirametrics/cfd_data_builder.rb +108 -0
  16. data/lib/jirametrics/change_item.rb +14 -6
  17. data/lib/jirametrics/chart_base.rb +141 -3
  18. data/lib/jirametrics/css_variable.rb +1 -1
  19. data/lib/jirametrics/cumulative_flow_diagram.rb +208 -0
  20. data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +21 -4
  21. data/lib/jirametrics/cycletime_histogram.rb +15 -101
  22. data/lib/jirametrics/cycletime_scatterplot.rb +17 -83
  23. data/lib/jirametrics/daily_view.rb +85 -53
  24. data/lib/jirametrics/daily_wip_by_age_chart.rb +4 -5
  25. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +14 -4
  26. data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -2
  27. data/lib/jirametrics/daily_wip_chart.rb +30 -8
  28. data/lib/jirametrics/data_quality_report.rb +43 -12
  29. data/lib/jirametrics/dependency_chart.rb +6 -3
  30. data/lib/jirametrics/download_config.rb +15 -0
  31. data/lib/jirametrics/downloader.rb +117 -100
  32. data/lib/jirametrics/downloader_for_cloud.rb +287 -0
  33. data/lib/jirametrics/downloader_for_data_center.rb +95 -0
  34. data/lib/jirametrics/estimate_accuracy_chart.rb +42 -4
  35. data/lib/jirametrics/examples/aggregated_project.rb +2 -2
  36. data/lib/jirametrics/examples/standard_project.rb +41 -28
  37. data/lib/jirametrics/expedited_chart.rb +3 -1
  38. data/lib/jirametrics/exporter.rb +26 -6
  39. data/lib/jirametrics/file_config.rb +9 -11
  40. data/lib/jirametrics/file_system.rb +59 -3
  41. data/lib/jirametrics/fix_version.rb +13 -0
  42. data/lib/jirametrics/flow_efficiency_scatterplot.rb +5 -1
  43. data/lib/jirametrics/github_gateway.rb +115 -0
  44. data/lib/jirametrics/groupable_issue_chart.rb +11 -1
  45. data/lib/jirametrics/grouping_rules.rb +26 -4
  46. data/lib/jirametrics/html/aging_work_bar_chart.erb +5 -5
  47. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +3 -1
  48. data/lib/jirametrics/html/aging_work_table.erb +5 -0
  49. data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
  50. data/lib/jirametrics/html/cumulative_flow_diagram.erb +503 -0
  51. data/lib/jirametrics/html/daily_wip_chart.erb +40 -5
  52. data/lib/jirametrics/html/estimate_accuracy_chart.erb +4 -12
  53. data/lib/jirametrics/html/expedited_chart.erb +6 -14
  54. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +4 -8
  55. data/lib/jirametrics/html/index.css +249 -69
  56. data/lib/jirametrics/html/index.erb +9 -35
  57. data/lib/jirametrics/html/index.js +164 -0
  58. data/lib/jirametrics/html/legacy_colors.css +174 -0
  59. data/lib/jirametrics/html/sprint_burndown.erb +17 -15
  60. data/lib/jirametrics/html/throughput_chart.erb +42 -11
  61. data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +61 -59
  62. data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +15 -11
  63. data/lib/jirametrics/html/wip_by_column_chart.erb +250 -0
  64. data/lib/jirametrics/html_generator.rb +32 -0
  65. data/lib/jirametrics/html_report_config.rb +52 -57
  66. data/lib/jirametrics/issue.rb +304 -101
  67. data/lib/jirametrics/issue_printer.rb +97 -0
  68. data/lib/jirametrics/jira_gateway.rb +77 -17
  69. data/lib/jirametrics/mcp_server.rb +531 -0
  70. data/lib/jirametrics/project_config.rb +128 -12
  71. data/lib/jirametrics/pull_request.rb +30 -0
  72. data/lib/jirametrics/pull_request_cycle_time_histogram.rb +77 -0
  73. data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +88 -0
  74. data/lib/jirametrics/pull_request_review.rb +13 -0
  75. data/lib/jirametrics/raw_javascript.rb +17 -0
  76. data/lib/jirametrics/settings.json +5 -1
  77. data/lib/jirametrics/sprint.rb +12 -0
  78. data/lib/jirametrics/sprint_burndown.rb +10 -4
  79. data/lib/jirametrics/status.rb +1 -1
  80. data/lib/jirametrics/stitcher.rb +81 -0
  81. data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
  82. data/lib/jirametrics/throughput_chart.rb +73 -23
  83. data/lib/jirametrics/time_based_histogram.rb +139 -0
  84. data/lib/jirametrics/time_based_scatterplot.rb +107 -0
  85. data/lib/jirametrics/wip_by_column_chart.rb +236 -0
  86. data/lib/jirametrics.rb +83 -69
  87. metadata +60 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f219c386b574f433dbaf649b9b7cccc42114401b45ff703a7c8f228c52d815c4
4
- data.tar.gz: 68af46ec7256e3b4004c4fd8016d52b17b0fa2bb717e9265b164dc5fa59fca56
3
+ metadata.gz: a1f64f63f13e8cb59d3b18fb1e1ad90f77ca06d0e2f59a75ff7b7bae4db1870f
4
+ data.tar.gz: 9b7d6b8759102d7590e86c114d2ac8b1b2e7c4cc9f45a168002752196a6bf797
5
5
  SHA512:
6
- metadata.gz: 71d289169562b3e4029549ab0db5ef1c4d25bc933d860f6f2a834f6caa39bbb5de93db2932996b7a5d75b4b6fb16f021ac5666cad3d57092dea6b4db4f8b7e97
7
- data.tar.gz: 425c213d81982d99f9d933d7ad8dbeb4f87f92ec97188d763b51edcb2f7b85a1072da80c0a5df8566a6325223e9b35d98b180e18fd67d1e50cb2d44b1b0e3872
6
+ metadata.gz: 8ec0bee468f8c34c001ea9151b0d78b1018d246cc86f9c2588a70ee55e3940b6263f69032884ec5c2dc596d3182909829f6ee63fe51b6c65444ce667bf70a6ca
7
+ data.tar.gz: 9b9f5337d0fc671639f9f651977cb2ecc60b2d27105cdce1b75a2a4188c093c111ba29ed880adccb515d548b8bd9209b4cf703482a0ede4ccea6b182212a3a57
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'jirametrics'
5
+ JiraMetrics.start(['mcp'] + ARGV)
@@ -65,8 +65,16 @@ class AggregateConfig
65
65
 
66
66
  if issues.nil?
67
67
  file_system.warning "No issues found for #{project_name}"
68
- else
69
- @project_config.add_issues issues
68
+ return
69
+ end
70
+
71
+ @project_config.add_issues issues
72
+
73
+ # Bring fix versions over
74
+ project.fix_versions.each do |fix_version|
75
+ unless @project_config.fix_versions.find { |fv| fv.id == fix_version.id }
76
+ @project_config.fix_versions << fix_version
77
+ end
70
78
  end
71
79
  end
72
80
 
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'jirametrics/chart_base'
4
+ require 'jirametrics/bar_chart_range'
4
5
 
5
6
  class AgingWorkBarChart < ChartBase
6
7
  def initialize block
7
8
  super()
8
9
 
10
+ @age_cutoff = nil
9
11
  header_text 'Aging Work Bar Chart'
10
12
  description_text <<-HTML
11
13
  <p>
@@ -13,16 +15,19 @@ class AgingWorkBarChart < ChartBase
13
15
  newest at the bottom.
14
16
  </p>
15
17
  <p>
16
- There are potentially three bars for each issue, although a bar may be missing if the issue has no
17
- information relevant to that. Hovering over any of the bars will provide more details.
18
+ There are <%= (aggregated_project? || current_board.scrum?) ? 'four' : 'three' %> bars for each issue, and hovering over any of the bars will provide more details.
18
19
  <ol>
19
- <li>The top bar tells you what status the issue is in at any time. The colour indicates the
20
+ <li>Status: The status the issue was in at any time. The colour indicates the
20
21
  status category, which will be one of #{color_block '--status-category-todo-color'} To Do,
21
22
  #{color_block '--status-category-inprogress-color'} In Progress,
22
23
  or #{color_block '--status-category-done-color'} Done</li>
23
- <li>The middle bar indicates #{color_block '--blocked-color'} blocked
24
+ <li>Activity: This bar indicates #{color_block '--blocked-color'} blocked
24
25
  or #{color_block '--stalled-color'} stalled.</li>
25
- <li>The bottom bar indicated #{color_block '--expedited-color'} expedited.</li>
26
+ <li>Priority: This shows the priority over time. If one of these priorities is considered expedited
27
+ then it will be drawn with diagonal lines.</li>
28
+ <% if aggregated_project? || current_board.scrum? %>
29
+ <li>Sprints: The sprints that the issue was in.</li>
30
+ <% end %>
26
31
  </ol>
27
32
  </p>
28
33
  #{describe_non_working_days}
@@ -36,6 +41,7 @@ class AgingWorkBarChart < ChartBase
36
41
 
37
42
  def run
38
43
  aging_issues = select_aging_issues issues: @issues
44
+ adjust_time_date_ranges_to_start_from_earliest_issue_start(aging_issues)
39
45
 
40
46
  today = date_range.end
41
47
  sort_by_age! issues: aging_issues, today: today
@@ -58,134 +64,134 @@ class AgingWorkBarChart < ChartBase
58
64
  wrap_and_render(binding, __FILE__)
59
65
  end
60
66
 
67
+ def adjust_time_date_ranges_to_start_from_earliest_issue_start aging_issues
68
+ earliest_start_time = aging_issues.collect do |issue|
69
+ issue.started_stopped_times.first
70
+ end.min
71
+ return if earliest_start_time.nil? || earliest_start_time >= @time_range.begin
72
+
73
+ @time_range = earliest_start_time..@time_range.end
74
+ @date_range = @time_range.begin.to_date..@time_range.end.to_date
75
+ end
76
+
61
77
  def data_sets_for_one_issue issue:, today:
62
78
  cycletime = issue.board.cycletime
63
- issue_start_time, _stopped_time = cycletime.started_stopped_times(issue)
64
- issue_start_date = issue_start_time.to_date
65
- issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
66
- [
67
- status_data_sets(issue: issue, label: issue_label, today: today),
68
- blocked_data_sets(
69
- issue: issue,
70
- issue_label: issue_label,
71
- stack: 'blocked',
72
- issue_start_time: issue_start_time
73
- ),
74
- data_set_by_block(
75
- issue: issue,
76
- issue_label: issue_label,
77
- title_label: 'Expedited',
78
- stack: 'expedited',
79
- color: CssVariable['--expedited-color'],
80
- start_date: issue_start_date
81
- ) { |day| issue.expedited_on_date?(day) }
79
+ issue_start_time = cycletime.started_stopped_times(issue).first
80
+ end_of_today = Time.parse("#{today}T23:59:59#{@timezone_offset}")
81
+
82
+ bar_data = [
83
+ ['status', collect_status_ranges(issue: issue, now: end_of_today)],
84
+ ['blocked', collect_blocked_stalled_ranges(issue: issue, issue_start_time: issue_start_time)],
85
+ ['priority', collect_priority_ranges(issue: issue)]
82
86
  ]
87
+ bar_data << ['sprints', collect_sprint_ranges(issue: issue)] if aggregated_project? || current_board.scrum?
88
+
89
+ bar_data.each { |entry| clip_ranges_to_start_time(ranges: entry.last, issue_start_time: issue_start_time) }
90
+
91
+ issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
92
+ bar_data.collect do |stack, ranges|
93
+ bar_chart_range_to_data_set y_value: issue_label, ranges: ranges, stack: stack, issue_start_time: issue_start_time
94
+ end
83
95
  end
84
96
 
85
97
  def sort_by_age! issues:, today:
86
98
  issues.sort! do |a, b|
87
- a.board.cycletime.age(b, today: today) <=> b.board.cycletime.age(a, today: today)
99
+ b.board.cycletime.age(b, today: today) <=> a.board.cycletime.age(a, today: today)
88
100
  end
89
101
  end
90
102
 
91
103
  def select_aging_issues issues:
92
104
  issues.select do |issue|
93
- started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
94
- started_time && stopped_time.nil?
105
+ started_time, stopped_time = issue.started_stopped_times
106
+ next false unless started_time && stopped_time.nil?
107
+
108
+ age = (date_range.end - started_time.to_date).to_i + 1
109
+ !(@age_cutoff && @age_cutoff >= age)
95
110
  end
96
111
  end
97
112
 
98
113
  def grow_chart_height_if_too_many_issues aging_issue_count:
99
- px_per_bar = 8
114
+ px_per_bar = 10
100
115
  bars_per_issue = 3
116
+ bars_per_issue += 1 if aggregated_project? || current_board.scrum?
117
+
101
118
  preferred_height = aging_issue_count * px_per_bar * bars_per_issue
102
119
  @canvas_height = preferred_height if @canvas_height.nil? || @canvas_height < preferred_height
103
120
  end
104
121
 
105
- def status_data_sets issue:, label:, today:
106
- cycletime = issue.board.cycletime
122
+ def clip_ranges_to_start_time ranges:, issue_start_time:
123
+ return if issue_start_time.nil?
107
124
 
108
- issue_started_time, _issue_stopped_time = cycletime.started_stopped_times(issue)
125
+ ranges.each { |range| range.start = issue_start_time if range.start < issue_start_time }
126
+ ranges.reject! { |range| range.start >= range.stop }
127
+ end
109
128
 
129
+ def collect_status_ranges issue:, now:
130
+ ranges = []
131
+ issue_started_time = issue.started_stopped_times.first
110
132
  previous_start = nil
111
133
  previous_status = nil
112
-
113
- data_sets = []
114
- issue.changes.each do |change|
115
- next unless change.status?
116
-
117
- status = issue.find_or_create_status id: change.value_id, name: change.value
118
-
119
- unless previous_start.nil? || previous_start < issue_started_time
120
- hash = {
121
- type: 'bar',
122
- data: [{
123
- x: [chart_format(previous_start), chart_format(change.time)],
124
- y: label,
125
- title: "#{issue.type} : #{change.value}"
126
- }],
127
- backgroundColor: status_category_color(status),
128
- borderColor: CssVariable['--aging-work-bar-chart-separator-color'],
129
- borderWidth: {
130
- top: 0,
131
- right: 1,
132
- bottom: 0,
133
- left: 0
134
- },
135
- stacked: true,
136
- stack: 'status'
137
- }
138
- data_sets << hash if date_range.include?(change.time.to_date)
134
+ issue.status_changes.each do |change|
135
+ new_status = issue.find_or_create_status id: change.value_id, name: change.value
136
+ if previous_start.nil?
137
+ previous_start = change.time
138
+ previous_status = new_status
139
+ next
139
140
  end
140
141
 
142
+ previous_start = issue_started_time if issue_started_time > previous_start
143
+
144
+ ranges << BarChartRange.new(
145
+ start: previous_start,
146
+ stop: change.time,
147
+ color: status_category_color(previous_status),
148
+ title: previous_status.to_s
149
+ )
141
150
  previous_start = change.time
142
- previous_status = status
151
+ previous_status = new_status
143
152
  end
144
153
 
145
- if previous_start
146
- data_sets << {
154
+ ranges << BarChartRange.new(
155
+ start: previous_start,
156
+ stop: now,
157
+ color: status_category_color(previous_status),
158
+ title: previous_status.to_s
159
+ )
160
+ ranges
161
+ end
162
+
163
+ def bar_chart_range_to_data_set y_value:, ranges:, stack:, issue_start_time:
164
+ ranges.filter_map do |bar_chart_range|
165
+ next if bar_chart_range.stop < issue_start_time
166
+
167
+ background_color = bar_chart_range.color
168
+ if bar_chart_range.highlight
169
+ background_color = RawJavascript.new("createDiagonalPattern(#{background_color.to_json})")
170
+ end
171
+
172
+ {
147
173
  type: 'bar',
148
174
  data: [{
149
- x: [chart_format(previous_start), chart_format("#{today}T00:00:00#{@timezone_offset}")],
150
- y: label,
151
- title: "#{issue.type} : #{previous_status.name}"
175
+ x: [chart_format([bar_chart_range.start, issue_start_time].max), chart_format(bar_chart_range.stop)],
176
+ y: y_value,
177
+ title: bar_chart_range.title
152
178
  }],
153
- backgroundColor: status_category_color(previous_status),
179
+ backgroundColor: background_color,
180
+ borderColor: CssVariable['--aging-work-bar-chart-separator-color'],
181
+ borderWidth: {
182
+ top: 0,
183
+ right: 1,
184
+ bottom: 0,
185
+ left: 0
186
+ },
154
187
  stacked: true,
155
- stack: 'status'
188
+ stack: stack
156
189
  }
157
190
  end
158
-
159
- data_sets
160
- end
161
-
162
- def one_block_change_data_set starting_change:, ending_time:, issue_label:, stack:, issue_start_time:
163
- if settings['blocked_color']
164
- file_system.deprecated message: 'blocked color should be set via css now', date: '2024-05-03'
165
- end
166
- if settings['stalled_color']
167
- file_system.deprecated message: 'stalled color should be set via css now', date: '2024-05-03'
168
- end
169
-
170
- color = settings['blocked_color'] || '--blocked-color'
171
- color = settings['stalled_color'] || '--stalled-color' if starting_change.stalled?
172
- {
173
- backgroundColor: CssVariable[color],
174
- data: [
175
- {
176
- title: starting_change.reasons,
177
- x: [chart_format([issue_start_time, starting_change.time].max), chart_format(ending_time)],
178
- y: issue_label
179
- }
180
- ],
181
- stack: stack,
182
- stacked: true,
183
- type: 'bar'
184
- }
185
191
  end
186
192
 
187
- def blocked_data_sets issue:, issue_label:, issue_start_time:, stack:
188
- data_sets = []
193
+ def collect_blocked_stalled_ranges issue:, issue_start_time:
194
+ results = []
189
195
  starting_change = nil
190
196
 
191
197
  issue.blocked_stalled_changes(end_time: time_range.end).each do |change|
@@ -195,58 +201,106 @@ class AgingWorkBarChart < ChartBase
195
201
  end
196
202
 
197
203
  if change.time >= issue_start_time
198
- data_sets << one_block_change_data_set(
199
- starting_change: starting_change, ending_time: change.time,
200
- issue_label: issue_label, stack: stack, issue_start_time: issue_start_time
204
+ color = settings['blocked_color'] || '--blocked-color'
205
+ color = settings['stalled_color'] || '--stalled-color' if starting_change.stalled?
206
+
207
+ results << BarChartRange.new(
208
+ start: starting_change.time, stop: change.time, color: CssVariable[color], title: starting_change.reasons
201
209
  )
202
210
  end
203
211
 
204
212
  starting_change = change
205
213
  end
214
+ results
215
+ end
216
+
217
+ def collect_priority_ranges issue:
218
+ expedited_priority_names = settings['expedited_priority_names']
219
+
220
+ previous_change = nil
221
+ results = []
222
+
223
+ issue.changes.each do |change|
224
+ next unless change.priority?
225
+
226
+ if previous_change.nil?
227
+ previous_change = change
228
+ next
229
+ end
230
+
231
+ results << create_range_for_priority(
232
+ previous_change: previous_change, stop_time: change.time,
233
+ expedited_priority_names: expedited_priority_names
234
+ )
235
+ previous_change = change
236
+ end
206
237
 
207
- data_sets
238
+ if previous_change
239
+ results << create_range_for_priority(
240
+ previous_change: previous_change, stop_time: time_range.end,
241
+ expedited_priority_names: expedited_priority_names
242
+ )
243
+ end
244
+ results
208
245
  end
209
246
 
210
- def data_set_by_block(
211
- issue:, issue_label:, title_label:, stack:, color:, start_date:, end_date: date_range.end
212
- )
213
- started = nil
214
- ended = nil
215
- data = []
216
-
217
- (start_date..end_date).each do |day|
218
- if yield(day)
219
- started = day if started.nil?
220
- ended = day
221
- elsif ended
222
- data << {
223
- x: [chart_format(started), chart_format(ended)],
224
- y: issue_label,
225
- title: "#{issue.type} : #{title_label} #{label_days (ended - started).to_i + 1}"
226
- }
227
-
228
- started = nil
229
- ended = nil
247
+ def collect_sprint_ranges issue:
248
+ results = []
249
+ open_sprints = {}
250
+
251
+ issue.changes.each do |change|
252
+ next unless change.sprint?
253
+
254
+ removed_sprint_ids = change.old_value_id - change.value_id
255
+ added_sprint_ids = change.value_id - change.old_value_id
256
+
257
+ removed_sprint_ids.each do |id|
258
+ data = open_sprints.delete(id)
259
+ next unless data
260
+
261
+ completed = data[:sprint].completed_time
262
+ stop = completed ? [change.time, completed].min : change.time
263
+ results << BarChartRange.new(
264
+ start: data[:start_time], stop: stop,
265
+ color: CssVariable['--sprint-color'], title: data[:sprint].name
266
+ )
267
+ end
268
+
269
+ added_sprint_ids.each do |id|
270
+ sprint = issue.board.sprints.find { |s| s.id == id }
271
+ next unless sprint
272
+ next if sprint.future?
273
+
274
+ start_time = [sprint.start_time, change.time].max
275
+ open_sprints[id] = { start_time: start_time, sprint: sprint }
230
276
  end
231
277
  end
232
278
 
233
- if started
234
- data << {
235
- x: [chart_format(started), chart_format(ended)],
236
- y: issue_label,
237
- title: "#{issue.type} : #{title_label} #{label_days (end_date - started).to_i + 1}"
238
- }
279
+ open_sprints.each_value do |data|
280
+ next if data[:sprint].future?
281
+
282
+ stop = data[:sprint].completed_time || time_range.end
283
+ results << BarChartRange.new(
284
+ start: data[:start_time], stop: stop,
285
+ color: CssVariable['--sprint-color'], title: data[:sprint].name
286
+ )
239
287
  end
240
288
 
241
- return [] if data.empty?
289
+ results
290
+ end
242
291
 
243
- {
244
- type: 'bar',
245
- data: data,
246
- backgroundColor: color,
247
- stacked: true,
248
- stack: stack
249
- }
292
+ def create_range_for_priority previous_change:, stop_time:, expedited_priority_names:
293
+ expedited = expedited_priority_names.include?(previous_change.value)
294
+ title = "Priority: #{previous_change.value}"
295
+ title << ' (expedited)' if expedited
296
+
297
+ BarChartRange.new(
298
+ start: previous_change.time,
299
+ stop: stop_time,
300
+ color: CssVariable["--priority-color-#{previous_change.value.downcase.gsub(/\s/, '')}"],
301
+ title: title,
302
+ highlight: expedited
303
+ )
250
304
  end
251
305
 
252
306
  def calculate_percent_line percentage: 85
@@ -255,4 +309,8 @@ class AgingWorkBarChart < ChartBase
255
309
 
256
310
  days[days.length * percentage / 100]
257
311
  end
312
+
313
+ def age_cutoff days
314
+ @age_cutoff = days
315
+ end
258
316
  end
@@ -6,6 +6,7 @@ require 'jirametrics/board_movement_calculator'
6
6
 
7
7
  class AgingWorkInProgressChart < ChartBase
8
8
  include GroupableIssueChart
9
+
9
10
  attr_accessor :possible_statuses, :board_id
10
11
  attr_reader :board_columns
11
12
 
@@ -55,7 +56,7 @@ class AgingWorkInProgressChart < ChartBase
55
56
  def run
56
57
  determine_board_columns
57
58
 
58
- @header_text += " on board: #{@all_boards[@board_id].name}"
59
+ @header_text += " on board: #{current_board.name}"
59
60
  data_sets = make_data_sets
60
61
 
61
62
  adjust_visibility_of_unmapped_status_column data_sets: data_sets
@@ -76,7 +77,7 @@ class AgingWorkInProgressChart < ChartBase
76
77
 
77
78
  @fake_column = BoardColumn.new({
78
79
  'name' => '[Unmapped Statuses]',
79
- 'statuses' => unmapped_statuses.collect { |id| { 'id' => id.to_s } }.uniq # rubocop:disable Performance/ChainArrayAllocation
80
+ 'statuses' => unmapped_statuses.collect { |id| { 'id' => id.to_s } }.uniq
80
81
  })
81
82
  @board_columns = columns + [@fake_column]
82
83
  end
@@ -114,14 +115,7 @@ class AgingWorkInProgressChart < ChartBase
114
115
 
115
116
  calculator = BoardMovementCalculator.new board: @all_boards[@board_id], issues: issues, today: date_range.end
116
117
 
117
- column_indexes_to_remove = []
118
- unless @show_all_columns
119
- column_indexes_to_remove = indexes_of_leading_and_trailing_zeros(calculator.age_data_for(percentage: 100))
120
-
121
- column_indexes_to_remove.reverse_each do |index|
122
- @board_columns.delete_at index
123
- end
124
- end
118
+ column_indexes_to_remove = trim_board_columns data_sets: data_sets, calculator: calculator
125
119
 
126
120
  @row_index_offset = data_sets.size
127
121
 
@@ -177,6 +171,44 @@ class AgingWorkInProgressChart < ChartBase
177
171
  result
178
172
  end
179
173
 
174
+ def trim_board_columns data_sets:, calculator:
175
+ return [] if @show_all_columns
176
+
177
+ columns_with_aging_items = data_sets.flat_map do |set|
178
+ set['data'].filter_map { |d| d['x'] if d.is_a? Hash }
179
+ end.uniq
180
+
181
+ # @fake_column is always the last element and is handled separately.
182
+ real_column_count = @board_columns.size - 1
183
+
184
+ # The last visible column always has artificially inflated age_data because
185
+ # ages_of_issues_when_leaving_column uses `today` as end_date when there is no
186
+ # next column. Exclude it from the right-boundary search so it is only kept when
187
+ # it has current aging items (handled by the last_aging fallback below).
188
+ age_data = calculator.age_data_for(percentage: 100)
189
+ last_data = (0...(real_column_count - 1)).to_a.reverse.find { |i| !age_data[i].zero? }
190
+
191
+ in_current = ->(i) { columns_with_aging_items.include?(@board_columns[i].name) }
192
+ first_aging = (0...real_column_count).find(&in_current)
193
+ last_aging = (0...real_column_count).to_a.reverse.find(&in_current)
194
+
195
+ # Combine: include any column with age_data (up to but not including the last visible
196
+ # column) and any column with current aging items.
197
+ first_data = (0...real_column_count).find { |i| !age_data[i].zero? }
198
+ left_bound = [first_data, first_aging].compact.min
199
+ right_bound = [last_data, last_aging].compact.max
200
+
201
+ indexes_to_remove =
202
+ if left_bound && right_bound
203
+ (0...left_bound).to_a + ((right_bound + 1)...real_column_count).to_a
204
+ else
205
+ (0...real_column_count).to_a
206
+ end
207
+
208
+ indexes_to_remove.reverse_each { |index| @board_columns.delete_at index }
209
+ indexes_to_remove
210
+ end
211
+
180
212
  def column_for issue:
181
213
  @board_columns.find do |board_column|
182
214
  board_column.status_ids.include? issue.status.id
@@ -192,7 +224,7 @@ class AgingWorkInProgressChart < ChartBase
192
224
  end
193
225
  end
194
226
 
195
- if has_unmapped
227
+ if has_unmapped && @description_text
196
228
  @description_text += "<p>The items shown in #{column_name.inspect} are not visible on the " \
197
229
  'board but are still active. Most likely everyone has forgotten about them.</p>'
198
230
  else
@@ -45,20 +45,21 @@ class AgingWorkTable < ChartBase
45
45
  # This is its own method simply so the tests can initialize the calculator without doing a full run.
46
46
  def initialize_calculator
47
47
  @today = date_range.end
48
- @calculator = BoardMovementCalculator.new board: current_board, issues: issues, today: @today
48
+ @calculators = @all_boards.transform_values do |board|
49
+ BoardMovementCalculator.new board: board, issues: issues, today: @today
50
+ end
49
51
  end
50
52
 
51
53
  def expedited_but_not_started
52
54
  @issues.select do |issue|
53
- started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
55
+ started_time, stopped_time = issue.started_stopped_times
54
56
  started_time.nil? && stopped_time.nil? && issue.expedited?
55
57
  end.sort_by(&:created)
56
58
  end
57
59
 
58
60
  def select_aging_issues
59
61
  aging_issues = @issues.select do |issue|
60
- cycletime = issue.board.cycletime
61
- started, stopped = cycletime.started_stopped_times(issue)
62
+ started, stopped = issue.started_stopped_times
62
63
  next false if started.nil? || stopped
63
64
  next true if issue.blocked_on_date?(@today, end_time: time_range.end) || issue.expedited?
64
65
 
@@ -77,7 +78,7 @@ class AgingWorkTable < ChartBase
77
78
  end
78
79
 
79
80
  def blocked_text issue
80
- started_time, _stopped_time = issue.board.cycletime.started_stopped_times(issue)
81
+ started_time, _stopped_time = issue.started_stopped_times
81
82
  return nil if started_time.nil?
82
83
 
83
84
  current = issue.blocked_stalled_changes(end_time: time_range.end)[-1]
@@ -124,7 +125,8 @@ class AgingWorkTable < ChartBase
124
125
  due = issue.due_date
125
126
  message = nil
126
127
 
127
- days_remaining, error = @calculator.forecasted_days_remaining_and_message issue: issue, today: @today
128
+ calculator = @calculators[issue.board.id]
129
+ days_remaining, error = calculator.forecasted_days_remaining_and_message issue: issue, today: @today
128
130
 
129
131
  unless error
130
132
  if due
@@ -174,6 +176,6 @@ class AgingWorkTable < ChartBase
174
176
  end
175
177
 
176
178
  def priority_text issue
177
- "<img src='#{issue.priority_url}' title='Priority: #{issue.priority_name}' />"
179
+ "<img src='#{issue.priority_url}' title='Priority: #{issue.priority_name}' style='max-width: 1em;'/>"
178
180
  end
179
181
  end