jirametrics 2.2.1 → 2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +13 -25
  3. data/lib/jirametrics/aging_work_bar_chart.rb +57 -39
  4. data/lib/jirametrics/aging_work_in_progress_chart.rb +1 -1
  5. data/lib/jirametrics/aging_work_table.rb +9 -26
  6. data/lib/jirametrics/board_config.rb +2 -2
  7. data/lib/jirametrics/chart_base.rb +27 -39
  8. data/lib/jirametrics/cycletime_histogram.rb +1 -1
  9. data/lib/jirametrics/cycletime_scatterplot.rb +1 -1
  10. data/lib/jirametrics/daily_wip_by_age_chart.rb +1 -1
  11. data/lib/jirametrics/daily_wip_chart.rb +1 -13
  12. data/lib/jirametrics/dependency_chart.rb +1 -1
  13. data/lib/jirametrics/{story_point_accuracy_chart.rb → estimate_accuracy_chart.rb} +31 -25
  14. data/lib/jirametrics/examples/standard_project.rb +1 -1
  15. data/lib/jirametrics/expedited_chart.rb +3 -1
  16. data/lib/jirametrics/exporter.rb +2 -2
  17. data/lib/jirametrics/file_config.rb +5 -7
  18. data/lib/jirametrics/file_system.rb +11 -2
  19. data/lib/jirametrics/groupable_issue_chart.rb +2 -4
  20. data/lib/jirametrics/hierarchy_table.rb +4 -4
  21. data/lib/jirametrics/html/aging_work_table.erb +3 -3
  22. data/lib/jirametrics/html_report_config.rb +61 -74
  23. data/lib/jirametrics/issue.rb +70 -39
  24. data/lib/jirametrics/project_config.rb +12 -6
  25. data/lib/jirametrics/sprint_burndown.rb +11 -0
  26. data/lib/jirametrics/status_collection.rb +4 -1
  27. data/lib/jirametrics/throughput_chart.rb +1 -1
  28. data/lib/jirametrics.rb +1 -1
  29. metadata +5 -7
  30. data/lib/jirametrics/experimental/generator.rb +0 -210
  31. data/lib/jirametrics/experimental/info.rb +0 -77
  32. /data/lib/jirametrics/html/{story_point_accuracy_chart.erb → estimate_accuracy_chart.erb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f08f0ccb466afa1cba17db109091ab8333b52ed1ec8c1d0b0920c400052f43e7
4
- data.tar.gz: f127c794e8fc4c447cab24cd7d2dc149c17dfec6742c6238ed8c5a3f7faeabcb
3
+ metadata.gz: 2e7264aee468cd14d42fa1c7ba8f737177ba99a78b3f7250d46771f687d4d619
4
+ data.tar.gz: da813f37b2a6047d6ab844f483008ecdb8c579786bd7bba7f2c01f038a259949
5
5
  SHA512:
6
- metadata.gz: 6e07a75c020a54dc077d25b2db1742fc143a4174d5b453813d0d2f3969984f1fc8eb6af18cfa092ecf8ef9d07a4754f9b6013e39c170ee2c3b34122aebf0a387
7
- data.tar.gz: abddaf6fa818847dc6427df90c919c6ade29cf79735c0a2fd64a31b7815d730e146a695fa0696b8896c60f66828dc16c207852a8c116db7fdabc21c90e429964
6
+ metadata.gz: c234046627a2c92729c346f44cf78471e74545e01ce3a9810c6c22ed659f6479a0146303ac7fb0652512feafcf8077025577d2a4c039dddf2fd70d6899360adb
7
+ data.tar.gz: 81f24c30b8d56f8811ed298ca17cebcee6cb32afdbea5cfd12332587aabaa2701b023dc620b651e111226bf66bac9285f84a893549c00c45bd0eb992694a2c64
@@ -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
+ log "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,30 +58,13 @@ 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
65
  @project_config.add_issues issues
65
66
  end
66
67
 
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
-
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
82
- end
83
-
84
68
  def find_time_range projects:
85
69
  raise "Can't calculate aggregated range as no projects were included." if projects.empty?
86
70
 
@@ -92,8 +76,12 @@ class AggregateConfig
92
76
  latest = range.end if latest.nil? || range.end > latest
93
77
  end
94
78
 
95
- raise "Can't calculate range" if earliest.nil? || latest.nil?
96
-
97
79
  earliest..latest
98
80
  end
81
+
82
+ private
83
+
84
+ def log message
85
+ @project_config.exporter.file_system.log message
86
+ end
99
87
  end
@@ -5,7 +5,7 @@ require 'jirametrics/chart_base'
5
5
  class AgingWorkBarChart < ChartBase
6
6
  @@next_id = 0
7
7
 
8
- def initialize block = nil
8
+ def initialize block
9
9
  super()
10
10
 
11
11
  header_text 'Aging Work Bar Chart'
@@ -27,61 +27,77 @@ class AgingWorkBarChart < ChartBase
27
27
  <li>The bottom bar indicated #{color_block '--expedited-color'} expedited.</li>
28
28
  </ol>
29
29
  </p>
30
- #{ describe_non_working_days }
30
+ #{describe_non_working_days}
31
31
  HTML
32
32
 
33
33
  # Because this one will size itself as needed, we start with a smaller default size
34
34
  @canvas_height = 80
35
35
 
36
- instance_eval(&block) if block
36
+ instance_eval(&block)
37
37
  end
38
38
 
39
39
  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
40
+ aging_issues = select_aging_issues issues: @issues
46
41
 
47
42
  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: CssVariable['--expedited-color'],
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
43
+ sort_by_age! issues: aging_issues, today: today
44
+
45
+ grow_chart_height_if_too_many_issues aging_issue_count: aging_issues.size
46
+
47
+ data_sets = aging_issues
48
+ .collect { |issue| data_sets_for_one_issue issue: issue, today: today }
49
+ .flatten
50
+ .compact
77
51
 
78
52
  percentage = calculate_percent_line
79
53
  percentage_line_x = date_range.end - calculate_percent_line if percentage
80
54
 
55
+ if aging_issues.empty?
56
+ @description_text = "<p>There is no aging work</p>"
57
+ return render_top_text(binding) #if aging_issues.empty?
58
+ end
59
+
81
60
  wrap_and_render(binding, __FILE__)
82
61
  end
83
62
 
84
- def grow_chart_height_if_too_many_issues aging_issue_count
63
+ def data_sets_for_one_issue issue:, today:
64
+ cycletime = issue.board.cycletime
65
+ issue_start_time = cycletime.started_time(issue)
66
+ issue_start_date = issue_start_time.to_date
67
+ issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
68
+ [
69
+ status_data_sets(issue: issue, label: issue_label, today: today),
70
+ blocked_data_sets(
71
+ issue: issue,
72
+ issue_label: issue_label,
73
+ stack: 'blocked',
74
+ issue_start_time: issue_start_time
75
+ ),
76
+ data_set_by_block(
77
+ issue: issue,
78
+ issue_label: issue_label,
79
+ title_label: 'Expedited',
80
+ stack: 'expedited',
81
+ color: CssVariable['--expedited-color'],
82
+ start_date: issue_start_date
83
+ ) { |day| issue.expedited_on_date?(day) }
84
+ ]
85
+ end
86
+
87
+ def sort_by_age! issues:, today:
88
+ issues.sort! do |a, b|
89
+ a.board.cycletime.age(b, today: today) <=> b.board.cycletime.age(a, today: today)
90
+ end
91
+ end
92
+
93
+ def select_aging_issues issues:
94
+ issues.select do |issue|
95
+ cycletime = issue.board.cycletime
96
+ cycletime.started_time(issue) && cycletime.stopped_time(issue).nil?
97
+ end
98
+ end
99
+
100
+ def grow_chart_height_if_too_many_issues aging_issue_count:
85
101
  px_per_bar = 8
86
102
  bars_per_issue = 3
87
103
  preferred_height = aging_issue_count * px_per_bar * bars_per_issue
@@ -220,6 +236,8 @@ class AgingWorkBarChart < ChartBase
220
236
  }
221
237
  end
222
238
 
239
+ return [] if data.empty?
240
+
223
241
  {
224
242
  type: 'bar',
225
243
  data: data,
@@ -8,7 +8,7 @@ class AgingWorkInProgressChart < ChartBase
8
8
  attr_accessor :possible_statuses, :board_id
9
9
  attr_reader :board_columns
10
10
 
11
- def initialize block = nil
11
+ def initialize block
12
12
  super()
13
13
  header_text 'Aging Work in Progress'
14
14
  description_text <<-HTML
@@ -4,6 +4,7 @@ require 'jirametrics/chart_base'
4
4
 
5
5
  class AgingWorkTable < ChartBase
6
6
  attr_accessor :today, :board_id
7
+ attr_reader :any_scrum_boards
7
8
 
8
9
  def initialize block
9
10
  super()
@@ -23,20 +24,21 @@ class AgingWorkTable < ChartBase
23
24
  </p>
24
25
  TEXT
25
26
 
26
- instance_eval(&block) if block
27
+ instance_eval(&block)
27
28
  end
28
29
 
29
30
  def run
30
31
  @today = date_range.end
31
- aging_issues = select_aging_issues
32
+ aging_issues = select_aging_issues + expedited_but_not_started
32
33
 
33
- expedited_but_not_started = @issues.select do |issue|
34
+ wrap_and_render(binding, __FILE__)
35
+ end
36
+
37
+ def expedited_but_not_started
38
+ @issues.select do |issue|
34
39
  cycletime = issue.board.cycletime
35
40
  cycletime.started_time(issue).nil? && cycletime.stopped_time(issue).nil? && issue.expedited?
36
- end
37
- aging_issues += expedited_but_not_started.sort_by(&:created)
38
-
39
- wrap_and_render(binding, __FILE__)
41
+ end.sort_by(&:created)
40
42
  end
41
43
 
42
44
  def select_aging_issues
@@ -54,10 +56,6 @@ class AgingWorkTable < ChartBase
54
56
  aging_issues.sort { |a, b| b.board.cycletime.age(b, today: @today) <=> a.board.cycletime.age(a, today: @today) }
55
57
  end
56
58
 
57
- def icon_span title:, icon:
58
- "<span title='#{title}' style='font-size: 0.8em;'>#{icon}</span>"
59
- end
60
-
61
59
  def expedited_text issue
62
60
  return unless issue.expedited?
63
61
 
@@ -85,13 +83,6 @@ class AgingWorkTable < ChartBase
85
83
  end
86
84
  end
87
85
 
88
- def unmapped_status_text issue
89
- icon_span(
90
- title: "Not visible: The status #{issue.status.name.inspect} is not mapped to any column and will not be visible",
91
- icon: ' 👀'
92
- )
93
- end
94
-
95
86
  def fix_versions_text issue
96
87
  issue.fix_versions.collect do |fix|
97
88
  if fix.released?
@@ -124,19 +115,11 @@ class AgingWorkTable < ChartBase
124
115
  end.join('<br />')
125
116
  end
126
117
 
127
- def current_status_visible? issue
128
- issue.board.visible_columns.any? { |column| column.status_ids.include? issue.status.id }
129
- end
130
-
131
118
  def age_cutoff age = nil
132
119
  @age_cutoff = age.to_i if age
133
120
  @age_cutoff
134
121
  end
135
122
 
136
- def any_scrum_boards?
137
- @any_scrum_boards
138
- end
139
-
140
123
  def parent_hierarchy issue
141
124
  result = []
142
125
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class BoardConfig
4
- attr_reader :id, :project_config
4
+ attr_reader :id, :project_config, :board
5
5
 
6
6
  def initialize id:, block:, project_config:
7
7
  @id = id
@@ -26,6 +26,6 @@ class BoardConfig
26
26
  end
27
27
 
28
28
  def expedited_priority_names *priority_names
29
- @board.expedited_priority_names = priority_names unless priority_names.empty?
29
+ @board.expedited_priority_names = priority_names
30
30
  end
31
31
  end
@@ -2,9 +2,9 @@
2
2
 
3
3
  class ChartBase
4
4
  attr_accessor :timezone_offset, :board_id, :all_boards, :date_range,
5
- :time_range, :data_quality, :holiday_dates, :settings
5
+ :time_range, :data_quality, :holiday_dates, :settings, :issues, :file_system
6
6
  attr_writer :aggregated_project
7
- attr_reader :issues, :canvas_width, :canvas_height
7
+ attr_reader :canvas_width, :canvas_height
8
8
 
9
9
  @@chart_counter = 0
10
10
 
@@ -34,7 +34,7 @@ class ChartBase
34
34
  caller_binding.eval "chart_id='chart#{next_id}'" # chart_id=chart3
35
35
 
36
36
  @html_directory = "#{pathname.dirname}/html"
37
- erb = ERB.new File.read "#{@html_directory}/#{$1}.erb"
37
+ erb = ERB.new file_system.load "#{@html_directory}/#{$1}.erb"
38
38
  erb.result(caller_binding)
39
39
  end
40
40
 
@@ -100,7 +100,7 @@ class ChartBase
100
100
  issues_id = next_id
101
101
 
102
102
  issue_descriptions.sort! { |a, b| a[0].key_as_i <=> b[0].key_as_i }
103
- erb = ERB.new File.read "#{@html_directory}/collapsible_issues_panel.erb"
103
+ erb = ERB.new file_system.load "#{@html_directory}/collapsible_issues_panel.erb"
104
104
  erb.result(binding)
105
105
  end
106
106
 
@@ -153,17 +153,6 @@ class ChartBase
153
153
  end
154
154
  end
155
155
 
156
- def sprints_in_time_range board
157
- board.sprints.select do |sprint|
158
- sprint_end_time = sprint.completed_time || sprint.end_time
159
- sprint_start_time = sprint.start_time
160
- next false if sprint_start_time.nil?
161
-
162
- time_range.include?(sprint_start_time) || time_range.include?(sprint_end_time) ||
163
- (sprint_start_time < time_range.begin && sprint_end_time > time_range.end)
164
- end || []
165
- end
166
-
167
156
  def chart_format object
168
157
  if object.is_a? Time
169
158
  # "2022-04-09T11:38:30-07:00"
@@ -173,12 +162,14 @@ class ChartBase
173
162
  end
174
163
  end
175
164
 
176
- def header_text text
177
- @header_text = text
165
+ def header_text text = nil
166
+ @header_text = text if text
167
+ @header_text
178
168
  end
179
169
 
180
- def description_text text
181
- @description_text = text
170
+ def description_text text = nil
171
+ @description_text = text if text
172
+ @description_text
182
173
  end
183
174
 
184
175
  def format_integer number
@@ -188,26 +179,35 @@ class ChartBase
188
179
  def format_status name_or_id, board:, is_category: false
189
180
  begin
190
181
  statuses = board.possible_statuses.expand_statuses([name_or_id])
191
- rescue RuntimeError => e
192
- return "<span style='color: red'>#{name_or_id}</span>" if e.message.match?(/^Status not found:/)
193
-
194
- throw e
182
+ rescue StatusNotFoundError => e
183
+ return "<span style='color: red'>#{name_or_id}</span>"
195
184
  end
196
- raise "Expected exactly one match and got #{statuses.inspect} for #{name_or_id.inspect}" if statuses.size > 1
197
185
 
198
186
  status = statuses.first
199
187
  color = status_category_color status
200
188
 
189
+ visibility = ''
190
+ if is_category == false && board.visible_columns.none? { |column| column.status_ids.include? status.id }
191
+ visibility = icon_span(
192
+ title: "Not visible: The status #{status.name.inspect} is not mapped to any column and will not be visible",
193
+ icon: ' 👀'
194
+ )
195
+
196
+ end
201
197
  text = is_category ? status.category_name : status.name
202
- "<span title='Category: #{status.category_name}'>#{color_block color.name} #{text}</span>"
198
+ "<span title='Category: #{status.category_name}'>#{color_block color.name} #{text}</span>#{visibility}"
199
+ end
200
+
201
+ def icon_span title:, icon:
202
+ "<span title='#{title}' style='font-size: 0.8em;'>#{icon}</span>"
203
203
  end
204
204
 
205
205
  def status_category_color status
206
206
  case status.category_name
207
- when nil then 'black'
208
207
  when 'To Do' then CssVariable['--status-category-todo-color']
209
208
  when 'In Progress' then CssVariable['--status-category-inprogress-color']
210
209
  when 'Done' then CssVariable['--status-category-done-color']
210
+ else 'black' # Theoretically impossible but seen in prod.
211
211
  end
212
212
  end
213
213
 
@@ -225,18 +225,6 @@ class ChartBase
225
225
  @canvas_responsive
226
226
  end
227
227
 
228
- def filter_issues &block
229
- @filter_issues_block = block
230
- end
231
-
232
- def issues= issues
233
- @issues = issues
234
- return unless @filter_issues_block
235
-
236
- @issues = issues.filter_map { |i| @filter_issues_block.call(i) }.uniq
237
- puts @issues.collect(&:key).join(', ')
238
- end
239
-
240
228
  def color_block color, title: nil
241
229
  result = +''
242
230
  result << "<div class='color_block' style='background: var(#{color});'"
@@ -252,5 +240,5 @@ class ChartBase
252
240
  and any other holidays mentioned in the configuration.
253
241
  </div>
254
242
  TEXT
255
- end
243
+ end
256
244
  end
@@ -6,7 +6,7 @@ class CycletimeHistogram < ChartBase
6
6
  include GroupableIssueChart
7
7
  attr_accessor :possible_statuses
8
8
 
9
- def initialize block = nil
9
+ def initialize block
10
10
  super()
11
11
 
12
12
  header_text 'Cycletime Histogram'
@@ -7,7 +7,7 @@ class CycletimeScatterplot < ChartBase
7
7
 
8
8
  attr_accessor :possible_statuses
9
9
 
10
- def initialize block = nil
10
+ def initialize block
11
11
  super()
12
12
 
13
13
  header_text 'Cycletime Scatterplot'
@@ -3,7 +3,7 @@
3
3
  require 'jirametrics/daily_wip_chart'
4
4
 
5
5
  class DailyWipByAgeChart < DailyWipChart
6
- def initialize block = nil
6
+ def initialize block
7
7
  super(block)
8
8
 
9
9
  add_trend_line line_color: '--aging-work-in-progress-by-age-trend-line-color', group_labels: [
@@ -14,7 +14,7 @@ end
14
14
  class DailyWipChart < ChartBase
15
15
  attr_accessor :possible_statuses
16
16
 
17
- def initialize block = nil
17
+ def initialize block
18
18
  super()
19
19
 
20
20
  header_text default_header_text
@@ -27,9 +27,6 @@ class DailyWipChart < ChartBase
27
27
  default_grouping_rules issue: issue, rules: rules
28
28
  end
29
29
  end
30
-
31
- # Because this one will size itself as needed, we start with a smaller default size
32
- # @canvas_height = 80
33
30
  end
34
31
 
35
32
  def run
@@ -45,8 +42,6 @@ class DailyWipChart < ChartBase
45
42
  end + data_sets
46
43
  end
47
44
 
48
- # grow_chart_height_if_too_many_data_sets data_sets.size
49
-
50
45
  wrap_and_render(binding, __FILE__)
51
46
  end
52
47
 
@@ -174,11 +169,4 @@ class DailyWipChart < ChartBase
174
169
  hidden: false
175
170
  }
176
171
  end
177
-
178
- def grow_chart_height_if_too_many_data_sets count
179
- px_per_bar = 8
180
- bars_per_issue = 0.5
181
- preferred_height = count * px_per_bar * bars_per_issue
182
- @canvas_height = preferred_height if @canvas_height.nil? || @canvas_height < preferred_height
183
- end
184
172
  end
@@ -52,7 +52,7 @@ class DependencyChart < ChartBase
52
52
  end
53
53
 
54
54
  def run
55
- instance_eval(&@rules_block) if @rules_block
55
+ instance_eval(&@rules_block)
56
56
 
57
57
  dot_graph = build_dot_graph
58
58
  return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if dot_graph.nil?
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class StoryPointAccuracyChart < ChartBase
4
- def initialize configuration_block = nil
3
+ class EstimateAccuracyChart < ChartBase
4
+ def initialize configuration_block
5
5
  super()
6
6
 
7
7
  header_text 'Estimate Accuracy'
@@ -12,7 +12,7 @@ class StoryPointAccuracyChart < ChartBase
12
12
  </div>
13
13
  <div class="p">
14
14
  The #{color_block '--estimate-accuracy-chart-completed-fill-color'} completed dots indicate
15
- cycletimes.
15
+ cycletimes.
16
16
  <% if @has_aging_data %>
17
17
  The #{color_block '--estimate-accuracy-chart-active-fill-color'} aging dots
18
18
  (click on the legend to turn them on) show the current
@@ -27,7 +27,7 @@ class StoryPointAccuracyChart < ChartBase
27
27
  @y_axis_block = ->(issue, start_time) { story_points_at(issue: issue, start_time: start_time)&.to_f }
28
28
  @y_axis_sort_order = nil
29
29
 
30
- instance_eval(&configuration_block) if configuration_block
30
+ instance_eval(&configuration_block)
31
31
  end
32
32
 
33
33
  def run
@@ -39,26 +39,7 @@ class StoryPointAccuracyChart < ChartBase
39
39
  end
40
40
 
41
41
  def scan_issues
42
- aging_hash = {}
43
- completed_hash = {}
44
-
45
- issues.each do |issue|
46
- cycletime = issue.board.cycletime
47
- start_time = cycletime.started_time(issue)
48
- stop_time = cycletime.stopped_time(issue)
49
-
50
- next unless start_time
51
-
52
- hash = stop_time ? completed_hash : aging_hash
53
-
54
- estimate = @y_axis_block.call issue, start_time
55
- cycle_time = ((stop_time&.to_date || date_range.end) - start_time.to_date).to_i + 1
56
-
57
- next if estimate.nil?
58
-
59
- key = [estimate, cycle_time]
60
- (hash[key] ||= []) << issue
61
- end
42
+ completed_hash, aging_hash = split_into_completed_and_aging issues: issues
62
43
 
63
44
  @has_aging_data = !aging_hash.empty?
64
45
 
@@ -93,7 +74,32 @@ class StoryPointAccuracyChart < ChartBase
93
74
  'borderColor' => border_color,
94
75
  'hidden' => starts_hidden
95
76
  }
96
- end.compact
77
+ end
78
+ end
79
+
80
+ def split_into_completed_and_aging issues:
81
+ aging_hash = {}
82
+ completed_hash = {}
83
+
84
+ issues.each do |issue|
85
+ cycletime = issue.board.cycletime
86
+ start_time = cycletime.started_time(issue)
87
+ stop_time = cycletime.stopped_time(issue)
88
+
89
+ next unless start_time
90
+
91
+ hash = stop_time ? completed_hash : aging_hash
92
+
93
+ estimate = @y_axis_block.call issue, start_time
94
+ cycle_time = ((stop_time&.to_date || date_range.end) - start_time.to_date).to_i + 1
95
+
96
+ next if estimate.nil?
97
+
98
+ key = [estimate, cycle_time]
99
+ (hash[key] ||= []) << issue
100
+ end
101
+
102
+ [completed_hash, aging_hash]
97
103
  end
98
104
 
99
105
  def hash_sorter
@@ -86,7 +86,7 @@ class Exporter
86
86
  daily_wip_by_parent_chart
87
87
  expedited_chart
88
88
  sprint_burndown
89
- story_point_accuracy_chart
89
+ estimate_accuracy_chart
90
90
 
91
91
  dependency_chart do
92
92
  link_rules do |link, rules|
@@ -20,7 +20,7 @@ class ExpeditedChart < ChartBase
20
20
  attr_accessor :issues, :cycletime, :possible_statuses, :date_range
21
21
  attr_reader :expedited_label
22
22
 
23
- def initialize
23
+ def initialize block
24
24
  super()
25
25
 
26
26
  header_text 'Expedited work'
@@ -38,6 +38,8 @@ class ExpeditedChart < ChartBase
38
38
  </div>
39
39
  #{describe_non_working_days}
40
40
  HTML
41
+
42
+ instance_eval(&block)
41
43
  end
42
44
 
43
45
  def run
@@ -32,11 +32,12 @@ class Exporter
32
32
 
33
33
  def initialize file_system: FileSystem.new
34
34
  @project_configs = []
35
- @timezone_offset = '+00:00'
36
35
  @target_path = '.'
37
36
  @holiday_dates = []
38
37
  @downloading = false
39
38
  @file_system = file_system
39
+
40
+ timezone_offset '+00:00'
40
41
  end
41
42
 
42
43
  def export name_filter:
@@ -79,7 +80,6 @@ class Exporter
79
80
  end
80
81
 
81
82
  def project name: nil, &block
82
- raise 'target_path was never set!' if @target_path.nil?
83
83
  raise 'jira_config not set' if @jira_config.nil?
84
84
 
85
85
  @project_configs << ProjectConfig.new(