jirametrics 2.0 → 2.12.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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +19 -26
  3. data/lib/jirametrics/aging_work_bar_chart.rb +79 -54
  4. data/lib/jirametrics/aging_work_in_progress_chart.rb +106 -40
  5. data/lib/jirametrics/aging_work_table.rb +84 -54
  6. data/lib/jirametrics/anonymizer.rb +6 -5
  7. data/lib/jirametrics/blocked_stalled_change.rb +24 -4
  8. data/lib/jirametrics/board.rb +51 -23
  9. data/lib/jirametrics/board_config.rb +9 -4
  10. data/lib/jirametrics/board_movement_calculator.rb +155 -0
  11. data/lib/jirametrics/change_item.rb +56 -21
  12. data/lib/jirametrics/chart_base.rb +101 -61
  13. data/lib/jirametrics/columns_config.rb +4 -0
  14. data/lib/jirametrics/css_variable.rb +33 -0
  15. data/lib/jirametrics/cycletime_config.rb +59 -8
  16. data/lib/jirametrics/cycletime_histogram.rb +69 -4
  17. data/lib/jirametrics/cycletime_scatterplot.rb +11 -15
  18. data/lib/jirametrics/daily_view.rb +277 -0
  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 +61 -14
  23. data/lib/jirametrics/data_quality_report.rb +222 -41
  24. data/lib/jirametrics/dependency_chart.rb +54 -23
  25. data/lib/jirametrics/download_config.rb +12 -0
  26. data/lib/jirametrics/downloader.rb +86 -56
  27. data/lib/jirametrics/estimate_accuracy_chart.rb +173 -0
  28. data/lib/jirametrics/estimation_configuration.rb +25 -0
  29. data/lib/jirametrics/examples/aggregated_project.rb +22 -39
  30. data/lib/jirametrics/examples/standard_project.rb +26 -48
  31. data/lib/jirametrics/expedited_chart.rb +28 -25
  32. data/lib/jirametrics/exporter.rb +59 -32
  33. data/lib/jirametrics/file_config.rb +35 -14
  34. data/lib/jirametrics/file_system.rb +48 -3
  35. data/lib/jirametrics/flow_efficiency_scatterplot.rb +111 -0
  36. data/lib/jirametrics/groupable_issue_chart.rb +2 -6
  37. data/lib/jirametrics/grouping_rules.rb +7 -1
  38. data/lib/jirametrics/hierarchy_table.rb +4 -4
  39. data/lib/jirametrics/html/aging_work_bar_chart.erb +13 -16
  40. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +28 -5
  41. data/lib/jirametrics/html/aging_work_table.erb +21 -25
  42. data/lib/jirametrics/html/cycletime_histogram.erb +83 -3
  43. data/lib/jirametrics/html/cycletime_scatterplot.erb +9 -12
  44. data/lib/jirametrics/html/daily_wip_chart.erb +17 -13
  45. data/lib/jirametrics/html/{story_point_accuracy_chart.erb → estimate_accuracy_chart.erb} +9 -4
  46. data/lib/jirametrics/html/expedited_chart.erb +10 -13
  47. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
  48. data/lib/jirametrics/html/hierarchy_table.erb +2 -2
  49. data/lib/jirametrics/html/index.css +280 -0
  50. data/lib/jirametrics/html/index.erb +33 -39
  51. data/lib/jirametrics/html/sprint_burndown.erb +10 -14
  52. data/lib/jirametrics/html/throughput_chart.erb +10 -13
  53. data/lib/jirametrics/html_report_config.rb +110 -86
  54. data/lib/jirametrics/issue.rb +390 -109
  55. data/lib/jirametrics/issue_collection.rb +33 -0
  56. data/lib/jirametrics/jira_gateway.rb +33 -12
  57. data/lib/jirametrics/project_config.rb +276 -147
  58. data/lib/jirametrics/rules.rb +2 -2
  59. data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
  60. data/lib/jirametrics/settings.json +11 -0
  61. data/lib/jirametrics/sprint.rb +1 -0
  62. data/lib/jirametrics/sprint_burndown.rb +59 -40
  63. data/lib/jirametrics/sprint_issue_change_data.rb +3 -3
  64. data/lib/jirametrics/status.rb +84 -19
  65. data/lib/jirametrics/status_collection.rb +86 -39
  66. data/lib/jirametrics/throughput_chart.rb +12 -4
  67. data/lib/jirametrics/user.rb +12 -0
  68. data/lib/jirametrics/value_equality.rb +2 -2
  69. data/lib/jirametrics.rb +29 -7
  70. metadata +20 -17
  71. data/lib/jirametrics/discard_changes_before.rb +0 -37
  72. data/lib/jirametrics/experimental/generator.rb +0 -210
  73. data/lib/jirametrics/experimental/info.rb +0 -77
  74. data/lib/jirametrics/html/data_quality_report.erb +0 -126
  75. data/lib/jirametrics/story_point_accuracy_chart.rb +0 -134
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb163ed079209739994cb9a963533c6afaddc6ca987bf9c428aeee94945ecf40
4
- data.tar.gz: 571d3d09cde660cd7038d7d996e0c5e44cb7dfe21fdf892a931ef51db84fb9cf
3
+ metadata.gz: 7565c5b6320b9c4375a3feb27fcaf80f96d9c058041b71584f287e6a3d7eb399
4
+ data.tar.gz: 4554c7edbc7c42e01ad4838094ab922819c2e71818a46907f25740e35c327aa6
5
5
  SHA512:
6
- metadata.gz: 3212ef8b2d1a2d8eb2be8e614534aa4b614e8f8003f94d0b8cd0239839fbca90c30f8ce062effede748567f53c01c4235ec179d53fd04f01f9f0bd7a288ddb9b
7
- data.tar.gz: f25bc331a72956f24dac94205630db7cff9d98a933ccd76f2ddff1132702dfd284c9ad526518c6e91169fb3cc41e604695d1c792f2b2496b870e93a6ff165684
6
+ metadata.gz: 021c1f989117b1fbb8708183d9ad8e79b049060989c1f20d31121a486d25f1ae2d1c036e013279479cd3ac6f5b101a48d8c9f4943570107c9cc4b40485ea6a4f
7
+ data.tar.gz: 7f7e334a2161e23ef5ae8a754aa3f12524c1bb130893ca3df3e8574f7a44286ef33c8a03b2ac2790ccb8e8b792ce0fb6c17b1d69d2da068ea48dbce1b4c514a1
@@ -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
@@ -57,28 +58,16 @@ class AggregateConfig
57
58
  else
58
59
  issues = project.file_configs.first.issues
59
60
  if project.file_configs.size > 1
60
- puts 'More than one file section is defined. For the aggregated view, we always use ' \
61
+ log 'More than one file section is defined. For the aggregated view, we always use ' \
61
62
  'the first file section'
62
63
  end
63
64
  end
64
- @project_config.add_issues issues
65
- end
66
-
67
- def date_range range
68
- @project_config.time_range = date_range_to_time_range(
69
- date_range: range, timezone_offset: project_config.exporter.timezone_offset
70
- )
71
- end
72
65
 
73
- def date_range_to_time_range date_range:, timezone_offset:
74
- start_of_first_day = Time.new(
75
- date_range.begin.year, date_range.begin.month, date_range.begin.day, 0, 0, 0, timezone_offset
76
- )
77
- end_of_last_day = Time.new(
78
- date_range.end.year, date_range.end.month, date_range.end.day, 23, 59, 59, timezone_offset
79
- )
80
-
81
- 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
82
71
  end
83
72
 
84
73
  def find_time_range projects:
@@ -92,8 +81,12 @@ class AggregateConfig
92
81
  latest = range.end if latest.nil? || range.end > latest
93
82
  end
94
83
 
95
- raise "Can't calculate range" if earliest.nil? || latest.nil?
96
-
97
84
  earliest..latest
98
85
  end
86
+
87
+ private
88
+
89
+ def file_system
90
+ @project_config.exporter.file_system
91
+ end
99
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,69 +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.</li>
21
- <li>The middle bar indicates blocked and stalled states. A lighter orange is stalled and a darker,
22
- reddish colour is blocked.</li>
23
- <li>The bottom bar indicated an expedited state.</li></ol>
24
- </p>
25
- <p>
26
- The gray backgrounds indicate weekends and the red vertical line indicates the 85% point for all
27
- 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>
28
27
  </p>
28
+ #{describe_non_working_days}
29
29
  HTML
30
30
 
31
31
  # Because this one will size itself as needed, we start with a smaller default size
32
32
  @canvas_height = 80
33
33
 
34
- instance_eval(&block) if block
34
+ instance_eval(&block)
35
35
  end
36
36
 
37
37
  def run
38
- aging_issues = @issues.select do |issue|
39
- cycletime = issue.board.cycletime
40
- cycletime.started_time(issue) && cycletime.stopped_time(issue).nil?
41
- end
42
-
43
- grow_chart_height_if_too_many_issues aging_issues.size
38
+ aging_issues = select_aging_issues issues: @issues
44
39
 
45
40
  today = date_range.end
46
- aging_issues.sort! do |a, b|
47
- a.board.cycletime.age(b, today: today) <=> b.board.cycletime.age(a, today: today)
48
- end
49
- data_sets = []
50
- aging_issues.each do |issue|
51
- cycletime = issue.board.cycletime
52
- issue_start_time = cycletime.started_time(issue)
53
- issue_start_date = issue_start_time.to_date
54
- issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
55
- [
56
- status_data_sets(issue: issue, label: issue_label, today: today),
57
- blocked_data_sets(
58
- issue: issue,
59
- issue_label: issue_label,
60
- stack: 'blocked',
61
- issue_start_time: issue_start_time
62
- ),
63
- data_set_by_block(
64
- issue: issue,
65
- issue_label: issue_label,
66
- title_label: 'Expedited',
67
- stack: 'expedited',
68
- color: 'red',
69
- start_date: issue_start_date
70
- ) { |day| issue.expedited_on_date?(day) }
71
- ].compact.flatten.each do |data|
72
- data_sets << data
73
- end
74
- 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
75
49
 
76
50
  percentage = calculate_percent_line
77
51
  percentage_line_x = date_range.end - calculate_percent_line if percentage
78
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
+
79
58
  wrap_and_render(binding, __FILE__)
80
59
  end
81
60
 
82
- 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:
83
99
  px_per_bar = 8
84
100
  bars_per_issue = 3
85
101
  preferred_height = aging_issue_count * px_per_bar * bars_per_issue
@@ -89,7 +105,7 @@ class AgingWorkBarChart < ChartBase
89
105
  def status_data_sets issue:, label:, today:
90
106
  cycletime = issue.board.cycletime
91
107
 
92
- issue_started_time = cycletime.started_time(issue)
108
+ issue_started_time, _issue_stopped_time = cycletime.started_stopped_times(issue)
93
109
 
94
110
  previous_start = nil
95
111
  previous_status = nil
@@ -98,7 +114,7 @@ class AgingWorkBarChart < ChartBase
98
114
  issue.changes.each do |change|
99
115
  next unless change.status?
100
116
 
101
- status = issue.find_status_by_name change.value
117
+ status = issue.find_or_create_status id: change.value_id, name: change.value
102
118
 
103
119
  unless previous_start.nil? || previous_start < issue_started_time
104
120
  hash = {
@@ -109,7 +125,7 @@ class AgingWorkBarChart < ChartBase
109
125
  title: "#{issue.type} : #{change.value}"
110
126
  }],
111
127
  backgroundColor: status_category_color(status),
112
- borderColor: 'white',
128
+ borderColor: CssVariable['--aging-work-bar-chart-separator-color'],
113
129
  borderWidth: {
114
130
  top: 0,
115
131
  right: 1,
@@ -144,10 +160,17 @@ class AgingWorkBarChart < ChartBase
144
160
  end
145
161
 
146
162
  def one_block_change_data_set starting_change:, ending_time:, issue_label:, stack:, issue_start_time:
147
- color = settings['colors']['blocked']
148
- 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?
149
172
  {
150
- backgroundColor: color,
173
+ backgroundColor: CssVariable[color],
151
174
  data: [
152
175
  {
153
176
  title: starting_change.reasons,
@@ -215,6 +238,8 @@ class AgingWorkBarChart < ChartBase
215
238
  }
216
239
  end
217
240
 
241
+ return [] if data.empty?
242
+
218
243
  {
219
244
  type: 'bar',
220
245
  data: data,
@@ -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,7 +87,7 @@ 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
  {
@@ -72,7 +98,10 @@ class AgingWorkInProgressChart < ChartBase
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
  }
@@ -82,44 +111,70 @@ class AgingWorkInProgressChart < ChartBase
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.filter_map 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
106
152
 
107
- accumulated_status_ids += column.status_ids
108
- [column.name, accumulated_status_ids.dup]
109
- end.reverse
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
159
+
160
+ output
110
161
  end
111
162
 
112
- def ages_of_issues_that_crossed_column_boundary issues:, status_ids:
113
- issues.filter_map 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
175
+ result << index if list[index].zero?
122
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