jirametrics 1.0.0 → 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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/bin/jirametrics +1 -0
  3. data/lib/jirametrics/aggregate_config.rb +28 -25
  4. data/lib/jirametrics/aging_work_bar_chart.rb +82 -59
  5. data/lib/jirametrics/aging_work_in_progress_chart.rb +109 -43
  6. data/lib/jirametrics/aging_work_table.rb +78 -43
  7. data/lib/jirametrics/anonymizer.rb +9 -8
  8. data/lib/jirametrics/blocked_stalled_change.rb +27 -12
  9. data/lib/jirametrics/board.rb +61 -26
  10. data/lib/jirametrics/board_config.rb +8 -4
  11. data/lib/jirametrics/board_movement_calculator.rb +147 -0
  12. data/lib/jirametrics/change_item.rb +31 -10
  13. data/lib/jirametrics/chart_base.rb +105 -64
  14. data/lib/jirametrics/columns_config.rb +4 -0
  15. data/lib/jirametrics/css_variable.rb +33 -0
  16. data/lib/jirametrics/cycletime_config.rb +59 -8
  17. data/lib/jirametrics/cycletime_histogram.rb +71 -6
  18. data/lib/jirametrics/cycletime_scatterplot.rb +16 -17
  19. data/lib/jirametrics/daily_wip_by_age_chart.rb +44 -20
  20. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +37 -35
  21. data/lib/jirametrics/daily_wip_by_parent_chart.rb +38 -0
  22. data/lib/jirametrics/daily_wip_chart.rb +62 -15
  23. data/lib/jirametrics/data_quality_report.rb +225 -44
  24. data/lib/jirametrics/dependency_chart.rb +59 -27
  25. data/lib/jirametrics/download_config.rb +11 -18
  26. data/lib/jirametrics/downloader.rb +100 -123
  27. data/lib/jirametrics/{story_point_accuracy_chart.rb → estimate_accuracy_chart.rb} +49 -39
  28. data/lib/jirametrics/examples/aggregated_project.rb +49 -4
  29. data/lib/jirametrics/examples/standard_project.rb +31 -46
  30. data/lib/jirametrics/expedited_chart.rb +31 -28
  31. data/lib/jirametrics/exporter.rb +69 -50
  32. data/lib/jirametrics/file_config.rb +34 -13
  33. data/lib/jirametrics/file_system.rb +81 -0
  34. data/lib/jirametrics/flow_efficiency_scatterplot.rb +111 -0
  35. data/lib/jirametrics/groupable_issue_chart.rb +2 -15
  36. data/lib/jirametrics/grouping_rules.rb +7 -1
  37. data/lib/jirametrics/hierarchy_table.rb +5 -5
  38. data/lib/jirametrics/html/aging_work_bar_chart.erb +13 -16
  39. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +28 -5
  40. data/lib/jirametrics/html/aging_work_table.erb +19 -25
  41. data/lib/jirametrics/html/cycletime_histogram.erb +83 -3
  42. data/lib/jirametrics/html/cycletime_scatterplot.erb +9 -12
  43. data/lib/jirametrics/html/daily_wip_chart.erb +17 -13
  44. data/lib/jirametrics/html/{story_point_accuracy_chart.erb → estimate_accuracy_chart.erb} +9 -4
  45. data/lib/jirametrics/html/expedited_chart.erb +10 -13
  46. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
  47. data/lib/jirametrics/html/hierarchy_table.erb +2 -2
  48. data/lib/jirametrics/html/index.css +209 -0
  49. data/lib/jirametrics/html/index.erb +16 -39
  50. data/lib/jirametrics/html/sprint_burndown.erb +10 -14
  51. data/lib/jirametrics/html/throughput_chart.erb +10 -13
  52. data/lib/jirametrics/html_report_config.rb +110 -113
  53. data/lib/jirametrics/issue.rb +361 -114
  54. data/lib/jirametrics/issue_link.rb +0 -7
  55. data/lib/jirametrics/jira_gateway.rb +77 -0
  56. data/lib/jirametrics/project_config.rb +277 -164
  57. data/lib/jirametrics/rules.rb +3 -22
  58. data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
  59. data/lib/jirametrics/settings.json +10 -0
  60. data/lib/jirametrics/sprint_burndown.rb +29 -11
  61. data/lib/jirametrics/sprint_issue_change_data.rb +4 -9
  62. data/lib/jirametrics/status.rb +96 -9
  63. data/lib/jirametrics/status_collection.rb +81 -39
  64. data/lib/jirametrics/throughput_chart.rb +14 -6
  65. data/lib/jirametrics/trend_line_calculator.rb +4 -4
  66. data/lib/jirametrics/value_equality.rb +23 -0
  67. data/lib/jirametrics.rb +41 -8
  68. metadata +20 -30
  69. data/lib/jirametrics/discard_changes_before.rb +0 -37
  70. data/lib/jirametrics/experimental/generator.rb +0 -209
  71. data/lib/jirametrics/experimental/info.rb +0 -77
  72. data/lib/jirametrics/html/data_quality_report.erb +0 -126
  73. data/lib/jirametrics/json_file_loader.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0dbe04bbc480c61f2bb1d09cdab71e9ed47fb8265336b2a1f05551bff78dec17
4
- data.tar.gz: 69c590731144d58f3235e72fb04f7d766b47a635c47301c5c87f1dbfc2cb13d5
3
+ metadata.gz: 670df42835c7d41e30dcde01ead744a132f81313165297d434334e272de795c7
4
+ data.tar.gz: 12822842210b4aeddb7f59e6681a9f1bd25060bae0ff98e8490d14e895dea657
5
5
  SHA512:
6
- metadata.gz: 494a2760b3f32fab17432735527e20fdd60105ca670437bb6cf382bd2844d78313f985db7cf3e1534f5bc67f7f2def4d598a58d46009018d14dbbb0ab70e2ce2
7
- data.tar.gz: c77baf2ff091972595cc126de4a03dea02f756b01db33cf90144583d65de81d07f5b37856a920d3870d72f4b7100709a243099e29fe79f78b52d96d207641545
6
+ metadata.gz: a4f36b8921c94a457710dc68251a9a2ddb38be2f0f7027991cf381fdf1cff28d2e549e508314733a3c753f583fd3c695b7e4979e1bb7be43c45de90ec001cbab
7
+ data.tar.gz: 28a2553b31cd4de5fce0c943da38643ad13dbfad3a8bddc77f4764e179394968966bb676e70daa75a4faeb42a67027fb7efb63895b56c01a3db563a029df66aa
data/bin/jirametrics CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'jirametrics'
4
5
  JiraMetrics.start(ARGV)
@@ -3,7 +3,7 @@
3
3
  require 'date'
4
4
 
5
5
  class AggregateConfig
6
- attr_reader :project_config
6
+ attr_reader :project_config, :included_projects
7
7
 
8
8
  def initialize project_config:, block:
9
9
  @project_config = project_config
@@ -19,14 +19,15 @@ class AggregateConfig
19
19
  raise "#{@project_config.name}: When aggregating, you must include at least one other project"
20
20
  end
21
21
 
22
- # If the date range wasn't set then calculate it now
22
+ # If the time range wasn't set then calculate it now
23
23
  @project_config.time_range = find_time_range projects: @included_projects if @project_config.time_range.nil?
24
24
 
25
- adjust_issue_links
25
+ adjust_issue_links issues: @project_config.issues
26
26
  end
27
27
 
28
- def adjust_issue_links
29
- issues = @project_config.issues
28
+ # IssueLinks just have a reference to the key. Walk through all of them to see if we have a full
29
+ # issue that we'd already loaded. If we do, then replace it.
30
+ def adjust_issue_links issues:
30
31
  issues.each do |issue|
31
32
  issue.issue_links.each do |link|
32
33
  other_issue_key = link.other_issue.key
@@ -40,7 +41,7 @@ class AggregateConfig
40
41
  def include_issues_from project_name
41
42
  project = @project_config.exporter.project_configs.find { |p| p.name == project_name }
42
43
  if project.nil?
43
- puts "Warning: Aggregated project #{@project_config.name.inspect} is attempting to load " \
44
+ file_system.warning "Aggregated project #{@project_config.name.inspect} is attempting to load " \
44
45
  "project #{project_name.inspect} but it can't be found. Is it disabled?"
45
46
  return
46
47
  end
@@ -48,28 +49,25 @@ class AggregateConfig
48
49
  @project_config.jira_url = project.jira_url if @project_config.jira_url.nil?
49
50
  unless @project_config.jira_url == project.jira_url
50
51
  raise 'Not allowed to aggregate projects from different Jira instances: ' \
51
- "#{@project_config.jira_url.inspect} and #{project.jira_url.inspect}"
52
+ "#{@project_config.jira_url.inspect} and #{project.jira_url.inspect}. For project #{project_name}"
52
53
  end
53
54
 
54
55
  @included_projects << project
55
- @project_config.add_issues project.issues
56
- end
57
-
58
- def date_range range
59
- @project_config.time_range = date_range_to_time_range(
60
- date_range: range, timezone_offset: project_config.exporter.timezone_offset
61
- )
62
- end
63
-
64
- def date_range_to_time_range date_range:, timezone_offset:
65
- start_of_first_day = Time.new(
66
- date_range.begin.year, date_range.begin.month, date_range.begin.day, 0, 0, 0, timezone_offset
67
- )
68
- end_of_last_day = Time.new(
69
- date_range.end.year, date_range.end.month, date_range.end.day, 23, 59, 59, timezone_offset
70
- )
56
+ if project.file_configs.empty?
57
+ issues = project.issues
58
+ else
59
+ issues = project.file_configs.first.issues
60
+ if project.file_configs.size > 1
61
+ log 'More than one file section is defined. For the aggregated view, we always use ' \
62
+ 'the first file section'
63
+ end
64
+ end
71
65
 
72
- start_of_first_day..end_of_last_day
66
+ if issues.nil?
67
+ file_system.warning "No issues found for #{project_name}"
68
+ else
69
+ @project_config.add_issues issues
70
+ end
73
71
  end
74
72
 
75
73
  def find_time_range projects:
@@ -83,7 +81,12 @@ class AggregateConfig
83
81
  latest = range.end if latest.nil? || range.end > latest
84
82
  end
85
83
 
86
- raise "Can't calculate range" if earliest.nil? || latest.nil?
87
84
  earliest..latest
88
85
  end
86
+
87
+ private
88
+
89
+ def file_system
90
+ @project_config.exporter.file_system
91
+ end
89
92
  end
@@ -3,9 +3,7 @@
3
3
  require 'jirametrics/chart_base'
4
4
 
5
5
  class AgingWorkBarChart < ChartBase
6
- @@next_id = 0
7
-
8
- def initialize block = nil
6
+ def initialize block
9
7
  super()
10
8
 
11
9
  header_text 'Aging Work Bar Chart'
@@ -17,71 +15,87 @@ class AgingWorkBarChart < ChartBase
17
15
  <p>
18
16
  There are potentially three bars for each issue, although a bar may be missing if the issue has no
19
17
  information relevant to that. Hovering over any of the bars will provide more details.
20
- <ol><li>The top bar tells you what status the issue is in at any time. Any statuses in the status
21
- category of "To Do" will be in blue. Any in the category of "In Progress" will be in a
22
- yellow and any in "Done" will be green.</li>
23
- <li>The middle bar indicates blocked and stalled states. A lighter orange is stalled and a darker,
24
- reddish colour is blocked.</li>
25
- <li>The bottom bar indicated an expedited state.</li></ol>
26
- </p>
27
- <p>
28
- The gray backgrounds indicate weekends and the red vertical line indicates the 85% point for all
29
- items in this time period. Anything that started to the left of that is now an outlier.
18
+ <ol>
19
+ <li>The top bar tells you what status the issue is in at any time. The colour indicates the
20
+ status category, which will be one of #{color_block '--status-category-todo-color'} To Do,
21
+ #{color_block '--status-category-inprogress-color'} In Progress,
22
+ or #{color_block '--status-category-done-color'} Done</li>
23
+ <li>The middle bar indicates #{color_block '--blocked-color'} blocked
24
+ or #{color_block '--stalled-color'} stalled.</li>
25
+ <li>The bottom bar indicated #{color_block '--expedited-color'} expedited.</li>
26
+ </ol>
30
27
  </p>
28
+ #{describe_non_working_days}
31
29
  HTML
32
30
 
33
31
  # Because this one will size itself as needed, we start with a smaller default size
34
32
  @canvas_height = 80
35
33
 
36
- instance_eval(&block) if block
34
+ instance_eval(&block)
37
35
  end
38
36
 
39
37
  def run
40
- aging_issues = @issues.select do |issue|
41
- cycletime = issue.board.cycletime
42
- cycletime.started_time(issue) && cycletime.stopped_time(issue).nil?
43
- end
44
-
45
- grow_chart_height_if_too_many_issues aging_issues.size
38
+ aging_issues = select_aging_issues issues: @issues
46
39
 
47
40
  today = date_range.end
48
- aging_issues.sort! do |a, b|
49
- a.board.cycletime.age(b, today: today) <=> b.board.cycletime.age(a, today: today)
50
- end
51
- data_sets = []
52
- aging_issues.each do |issue|
53
- cycletime = issue.board.cycletime
54
- issue_start_time = cycletime.started_time(issue)
55
- issue_start_date = issue_start_time.to_date
56
- issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
57
- [
58
- status_data_sets(issue: issue, label: issue_label, today: today),
59
- blocked_data_sets(
60
- issue: issue,
61
- issue_label: issue_label,
62
- stack: 'blocked',
63
- issue_start_time: issue_start_time
64
- ),
65
- data_set_by_block(
66
- issue: issue,
67
- issue_label: issue_label,
68
- title_label: 'Expedited',
69
- stack: 'expedited',
70
- color: 'red',
71
- start_date: issue_start_date
72
- ) { |day| issue.expedited_on_date?(day) }
73
- ].compact.flatten.each do |data|
74
- data_sets << data
75
- end
76
- end
41
+ sort_by_age! issues: aging_issues, today: today
42
+
43
+ grow_chart_height_if_too_many_issues aging_issue_count: aging_issues.size
44
+
45
+ data_sets = aging_issues
46
+ .collect { |issue| data_sets_for_one_issue issue: issue, today: today }
47
+ .flatten
48
+ .compact
77
49
 
78
50
  percentage = calculate_percent_line
79
51
  percentage_line_x = date_range.end - calculate_percent_line if percentage
80
52
 
53
+ if aging_issues.empty?
54
+ @description_text = '<p>There is no aging work</p>'
55
+ return render_top_text(binding)
56
+ end
57
+
81
58
  wrap_and_render(binding, __FILE__)
82
59
  end
83
60
 
84
- def grow_chart_height_if_too_many_issues aging_issue_count
61
+ def data_sets_for_one_issue issue:, today:
62
+ 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) }
82
+ ]
83
+ end
84
+
85
+ def sort_by_age! issues:, today:
86
+ issues.sort! do |a, b|
87
+ a.board.cycletime.age(b, today: today) <=> b.board.cycletime.age(a, today: today)
88
+ end
89
+ end
90
+
91
+ def select_aging_issues issues:
92
+ issues.select do |issue|
93
+ started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
94
+ started_time && stopped_time.nil?
95
+ end
96
+ end
97
+
98
+ def grow_chart_height_if_too_many_issues aging_issue_count:
85
99
  px_per_bar = 8
86
100
  bars_per_issue = 3
87
101
  preferred_height = aging_issue_count * px_per_bar * bars_per_issue
@@ -91,7 +105,7 @@ class AgingWorkBarChart < ChartBase
91
105
  def status_data_sets issue:, label:, today:
92
106
  cycletime = issue.board.cycletime
93
107
 
94
- issue_started_time = cycletime.started_time(issue)
108
+ issue_started_time, _issue_stopped_time = cycletime.started_stopped_times(issue)
95
109
 
96
110
  previous_start = nil
97
111
  previous_status = nil
@@ -100,7 +114,7 @@ class AgingWorkBarChart < ChartBase
100
114
  issue.changes.each do |change|
101
115
  next unless change.status?
102
116
 
103
- status = issue.find_status_by_name change.value
117
+ status = issue.find_or_create_status id: change.value_id, name: change.value
104
118
 
105
119
  unless previous_start.nil? || previous_start < issue_started_time
106
120
  hash = {
@@ -111,7 +125,7 @@ class AgingWorkBarChart < ChartBase
111
125
  title: "#{issue.type} : #{change.value}"
112
126
  }],
113
127
  backgroundColor: status_category_color(status),
114
- borderColor: 'white',
128
+ borderColor: CssVariable['--aging-work-bar-chart-separator-color'],
115
129
  borderWidth: {
116
130
  top: 0,
117
131
  right: 1,
@@ -146,10 +160,17 @@ class AgingWorkBarChart < ChartBase
146
160
  end
147
161
 
148
162
  def one_block_change_data_set starting_change:, ending_time:, issue_label:, stack:, issue_start_time:
149
- color = settings['colors']['blocked']
150
- color = settings['colors']['stalled'] if starting_change.stalled?
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?
151
172
  {
152
- backgroundColor: color,
173
+ backgroundColor: CssVariable[color],
153
174
  data: [
154
175
  {
155
176
  title: starting_change.reasons,
@@ -187,14 +208,14 @@ class AgingWorkBarChart < ChartBase
187
208
  end
188
209
 
189
210
  def data_set_by_block(
190
- issue:, issue_label:, title_label:, stack:, color:, start_date:, end_date: date_range.end, &block
211
+ issue:, issue_label:, title_label:, stack:, color:, start_date:, end_date: date_range.end
191
212
  )
192
213
  started = nil
193
214
  ended = nil
194
215
  data = []
195
216
 
196
217
  (start_date..end_date).each do |day|
197
- if block.call(day)
218
+ if yield(day)
198
219
  started = day if started.nil?
199
220
  ended = day
200
221
  elsif ended
@@ -217,6 +238,8 @@ class AgingWorkBarChart < ChartBase
217
238
  }
218
239
  end
219
240
 
241
+ return [] if data.empty?
242
+
220
243
  {
221
244
  type: 'bar',
222
245
  data: data,
@@ -227,7 +250,7 @@ class AgingWorkBarChart < ChartBase
227
250
  end
228
251
 
229
252
  def calculate_percent_line percentage: 85
230
- days = completed_issues_in_range.collect { |issue| issue.board.cycletime.cycletime(issue) }.compact.sort
253
+ days = completed_issues_in_range.filter_map { |issue| issue.board.cycletime.cycletime(issue) }.sort
231
254
  return nil if days.empty?
232
255
 
233
256
  days[days.length * percentage / 100]
@@ -2,26 +2,48 @@
2
2
 
3
3
  require 'jirametrics/chart_base'
4
4
  require 'jirametrics/groupable_issue_chart'
5
+ require 'jirametrics/board_movement_calculator'
5
6
 
6
7
  class AgingWorkInProgressChart < ChartBase
7
8
  include GroupableIssueChart
8
9
  attr_accessor :possible_statuses, :board_id
9
10
  attr_reader :board_columns
10
11
 
11
- def initialize block = nil
12
+ def initialize block
12
13
  super()
13
14
  header_text 'Aging Work in Progress'
14
15
  description_text <<-HTML
15
16
  <p>
16
17
  This chart shows only work items that have started but not completed, grouped by the column
17
- they're currently in. Hovering over a dot will show you the ID of that work item.
18
+ they're currently in. Hovering over a dot will show you the ID of that work item.
18
19
  </p>
19
20
  <p>
20
- The gray area indicates the 85% mark for work items that have passed through here - 85% of
21
- previous work items left this column while still inside the gray area. Any work items above
22
- the gray area are outliers and they are the items that you should pay special attention to.
21
+ The shaded areas indicate what percentage of the work has passed that column within that time.
22
+ Notes:
23
+ <ul>
24
+ <li>It only shows columns that are considered "in progress". If you see a column that wouldn't normally
25
+ be thought of that way, then likely issues were moving backwards or continued to progress after hitting
26
+ that column.</li>
27
+ <li>If you see a colour group that drops as it moves to the right, that generally indicates that
28
+ a different number of data points is being included in each column. Probably because tickets moved
29
+ backwards athough it could also indicate that a ticket jumped over columns as it moved to the right.
30
+ </li>
31
+ </ul>
23
32
  </p>
33
+ <div style="border: 1px solid gray; padding: 0.2em">
34
+ <% @percentiles.keys.sort.reverse.each do |percent| %>
35
+ <span style="padding-left: 0.5em; padding-right: 0.5em; vertical-align: middle;"><%= color_block @percentiles[percent] %> <%= percent %>%</span>
36
+ <% end %>
37
+ </div>
24
38
  HTML
39
+ percentiles(
40
+ 50 => '--aging-work-in-progress-chart-shading-50-color',
41
+ 85 => '--aging-work-in-progress-chart-shading-85-color',
42
+ 98 => '--aging-work-in-progress-chart-shading-98-color',
43
+ 100 => '--aging-work-in-progress-chart-shading-100-color'
44
+ )
45
+ show_all_columns false
46
+
25
47
  init_configuration_block(block) do
26
48
  grouping_rules do |issue, rule|
27
49
  rule.label = issue.type
@@ -35,13 +57,17 @@ class AgingWorkInProgressChart < ChartBase
35
57
 
36
58
  @header_text += " on board: #{@all_boards[@board_id].name}"
37
59
  data_sets = make_data_sets
38
- column_headings = @board_columns.collect(&:name)
39
60
 
40
- adjust_visibility_of_unmapped_status_column data_sets: data_sets, column_headings: column_headings
61
+ adjust_visibility_of_unmapped_status_column data_sets: data_sets
62
+ adjust_chart_height
41
63
 
42
64
  wrap_and_render(binding, __FILE__)
43
65
  end
44
66
 
67
+ def show_all_columns show = true # rubocop:disable Style/OptionalBooleanParameter
68
+ @show_all_columns = show
69
+ end
70
+
45
71
  def determine_board_columns
46
72
  unmapped_statuses = current_board.possible_statuses.collect(&:id)
47
73
 
@@ -50,7 +76,7 @@ class AgingWorkInProgressChart < ChartBase
50
76
 
51
77
  @fake_column = BoardColumn.new({
52
78
  'name' => '[Unmapped Statuses]',
53
- 'statuses' => unmapped_statuses.collect { |id| { 'id' => id.to_s } }.uniq
79
+ 'statuses' => unmapped_statuses.collect { |id| { 'id' => id.to_s } }.uniq # rubocop:disable Performance/ChainArrayAllocation
54
80
  })
55
81
  @board_columns = columns + [@fake_column]
56
82
  end
@@ -61,65 +87,94 @@ class AgingWorkInProgressChart < ChartBase
61
87
  board.id == @board_id && board.cycletime.in_progress?(issue)
62
88
  end
63
89
 
64
- percentage = 85
90
+ @max_age = 20
65
91
  rules_to_issues = group_issues aging_issues
66
92
  data_sets = rules_to_issues.keys.collect do |rules|
67
93
  {
68
94
  'type' => 'line',
69
95
  'label' => rules.label,
70
- 'data' => rules_to_issues[rules].collect do |issue|
96
+ 'data' => rules_to_issues[rules].filter_map do |issue|
71
97
  age = issue.board.cycletime.age(issue, today: date_range.end)
72
98
  column = column_for issue: issue
73
99
  next if column.nil?
74
100
 
75
- { 'y' => age,
101
+ @max_age = age if age > @max_age
102
+
103
+ {
104
+ 'y' => age,
76
105
  'x' => column.name,
77
106
  'title' => ["#{issue.key} : #{issue.summary} (#{label_days age})"]
78
107
  }
79
- end.compact,
108
+ end,
80
109
  'fill' => false,
81
110
  'showLine' => false,
82
111
  'backgroundColor' => rules.color
83
112
  }
84
113
  end
85
- data_sets << {
86
- 'type' => 'bar',
87
- 'label' => "#{percentage}%",
88
- 'barPercentage' => 1.0,
89
- 'categoryPercentage' => 1.0,
90
- 'data' => days_at_percentage_threshold_for_all_columns(percentage: percentage, issues: @issues).drop(1)
91
- }
92
- end
93
114
 
94
- def days_at_percentage_threshold_for_all_columns percentage:, issues:
95
- accumulated_status_ids_per_column.collect do |_column, status_ids|
96
- ages = ages_of_issues_that_crossed_column_boundary issues: issues, status_ids: status_ids
97
- index = ages.size * percentage / 100
98
- ages.sort[index.to_i] || 0
115
+ calculator = BoardMovementCalculator.new board: @all_boards[@board_id], issues: issues, today: date_range.end
116
+
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
125
+
126
+ @row_index_offset = data_sets.size
127
+
128
+ bar_data = []
129
+ calculator.stacked_age_data_for(percentages: @percentiles.keys).each do |percentage, data|
130
+ column_indexes_to_remove.reverse_each { |index| data.delete_at index }
131
+ color = @percentiles[percentage]
132
+
133
+ data_sets << {
134
+ 'type' => 'bar',
135
+ 'label' => "#{percentage}%",
136
+ 'barPercentage' => 1.0,
137
+ 'categoryPercentage' => 1.0,
138
+ 'backgroundColor' => color,
139
+ 'data' => data
140
+ }
141
+ bar_data << data
99
142
  end
143
+ @bar_data = adjust_bar_data bar_data
144
+
145
+ data_sets
100
146
  end
101
147
 
102
- def accumulated_status_ids_per_column
103
- accumulated_status_ids = []
104
- @board_columns.reverse.collect do |column|
105
- next if column == @fake_column
148
+ def adjust_bar_data input
149
+ return [] if input.empty?
150
+
151
+ row_size = input.first.size
152
+
153
+ output = []
154
+ output << input.first
155
+ input.drop(1).each do |row|
156
+ previous_row = output.last
157
+ output << 0.upto(row_size - 1).collect { |i| row[i] + previous_row[i] }
158
+ end
106
159
 
107
- accumulated_status_ids += column.status_ids
108
- [column.name, accumulated_status_ids.dup]
109
- end.compact.reverse
160
+ output
110
161
  end
111
162
 
112
- def ages_of_issues_that_crossed_column_boundary issues:, status_ids:
113
- issues.collect do |issue|
114
- stop = issue.first_time_in_status(*status_ids)
115
- start = issue.board.cycletime.started_time(issue)
163
+ def indexes_of_leading_and_trailing_zeros list
164
+ result = []
165
+ 0.upto(list.size - 1) do |index|
166
+ break unless list[index].zero?
167
+
168
+ result << index
169
+ end
116
170
 
117
- # Skip if either it hasn't crossed the boundary or we can't tell when it started.
118
- next if stop.nil? || start.nil?
119
- next if stop < start
171
+ stop_at = result.empty? ? 0 : (result.last + 1)
172
+ (list.size - 1).downto(stop_at).each do |index|
173
+ break unless list[index].zero?
120
174
 
121
- (stop.to_date - start.to_date).to_i + 1
122
- end.compact
175
+ result << index if list[index].zero?
176
+ end
177
+ result
123
178
  end
124
179
 
125
180
  def column_for issue:
@@ -128,7 +183,7 @@ class AgingWorkInProgressChart < ChartBase
128
183
  end
129
184
  end
130
185
 
131
- def adjust_visibility_of_unmapped_status_column data_sets:, column_headings:
186
+ def adjust_visibility_of_unmapped_status_column data_sets:
132
187
  column_name = @fake_column.name
133
188
 
134
189
  has_unmapped = data_sets.any? do |set|
@@ -141,8 +196,19 @@ class AgingWorkInProgressChart < ChartBase
141
196
  @description_text += "<p>The items shown in #{column_name.inspect} are not visible on the " \
142
197
  'board but are still active. Most likely everyone has forgotten about them.</p>'
143
198
  else
144
- column_headings.pop
199
+ # @column_headings.pop
145
200
  @board_columns.pop
146
201
  end
147
202
  end
203
+
204
+ def percentiles percentile_color_hash
205
+ @percentiles = percentile_color_hash.transform_values { |value| CssVariable[value] }
206
+ end
207
+
208
+ def adjust_chart_height
209
+ min_height = @max_age * 5
210
+
211
+ @canvas_height = min_height if min_height > @canvas_height
212
+ @canvas_height = 400 if min_height > 400
213
+ end
148
214
  end