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
@@ -7,25 +7,24 @@ 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'
14
14
  description_text <<-HTML
15
- <p>
15
+ <div class="p">
16
16
  This chart shows only completed work and indicates both what day it completed as well as
17
17
  how many days it took to get done. Hovering over a dot will show you the ID of the work item.
18
- </p>
19
- <p>
20
- The gray line indicates the 85th percentile (<%= overall_percent_line %> days). 85% of all
18
+ </div>
19
+ <div class="p">
20
+ The #{color_block '--cycletime-scatterplot-overall-trendline-color'} line indicates the 85th
21
+ percentile (<%= overall_percent_line %> days). 85% of all
21
22
  items on this chart fall on or below the line and the remaining 15% are above the line. 85%
22
23
  is a reasonable proxy for "most" so that we can say that based on this data set, we can
23
24
  predict that most work of this type will complete in <%= overall_percent_line %> days or
24
25
  less. The other lines reflect the 85% line for that respective type of work.
25
- </p>
26
- <p>
27
- The gray vertical bars indicate weekends, when theoretically we aren't working.
28
- </p>
26
+ </div>
27
+ #{describe_non_working_days}
29
28
  HTML
30
29
 
31
30
  init_configuration_block block do
@@ -44,7 +43,7 @@ class CycletimeScatterplot < ChartBase
44
43
 
45
44
  data_sets = create_datasets completed_issues
46
45
  overall_percent_line = calculate_percent_line(completed_issues)
47
- @percentage_lines << [overall_percent_line, 'gray']
46
+ @percentage_lines << [overall_percent_line, CssVariable['--cycletime-scatterplot-overall-trendline-color']]
48
47
 
49
48
  return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
50
49
 
@@ -54,10 +53,7 @@ class CycletimeScatterplot < ChartBase
54
53
  def create_datasets completed_issues
55
54
  data_sets = []
56
55
 
57
- groups = group_issues completed_issues
58
-
59
- groups.each_key do |rules|
60
- completed_issues_by_type = groups[rules]
56
+ group_issues(completed_issues).each do |rules, completed_issues_by_type|
61
57
  label = rules.label
62
58
  color = rules.color
63
59
  percent_line = calculate_percent_line completed_issues_by_type
@@ -118,7 +114,7 @@ class CycletimeScatterplot < ChartBase
118
114
 
119
115
  {
120
116
  y: cycle_time,
121
- x: chart_format(issue.board.cycletime.stopped_time(issue)),
117
+ x: chart_format(issue.board.cycletime.started_stopped_times(issue).last),
122
118
  title: ["#{issue.key} : #{issue.summary} (#{label_days(cycle_time)})"]
123
119
  }
124
120
  end
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DailyView < ChartBase
4
+ attr_accessor :possible_statuses
5
+
6
+ def initialize _block
7
+ super()
8
+
9
+ header_text 'Daily View'
10
+ description_text <<-HTML
11
+ <div class="p">
12
+ This view shows all the items you'll want to discuss during your daily coordination meeting
13
+ (aka daily scrum, standup), in the order that you should be discussing them. The most important
14
+ items are at the top, and the least at the bottom.
15
+ </div>
16
+ <div class="p">
17
+ By default, we sort by priority first and then by age within each of those priorities.
18
+ Hover over the issue to make it stand out more.
19
+ </div>
20
+ HTML
21
+ end
22
+
23
+ def run
24
+ aging_issues = select_aging_issues
25
+
26
+ return "<h1>#{@header_text}</h1>There are no items currently in progress" if aging_issues.empty?
27
+
28
+ result = +''
29
+ result << render_top_text(binding)
30
+ aging_issues.each do |issue|
31
+ result << render_issue(issue, child: false)
32
+ end
33
+ result
34
+ end
35
+
36
+ def select_aging_issues
37
+ aging_issues = issues.select do |issue|
38
+ started_at, stopped_at = issue.board.cycletime.started_stopped_times(issue)
39
+ started_at && !stopped_at
40
+ end
41
+
42
+ today = date_range.end
43
+ aging_issues.collect do |issue|
44
+ [issue, issue.priority_name, issue.board.cycletime.age(issue, today: today)]
45
+ end.sort(&issue_sorter).collect(&:first)
46
+ end
47
+
48
+ def issue_sorter
49
+ priority_names = settings['priority_order']
50
+ lambda do |a, b|
51
+ a_issue, a_priority, a_age = *a
52
+ b_issue, b_priority, b_age = *b
53
+
54
+ a_priority_index = priority_names.index(a_priority)
55
+ b_priority_index = priority_names.index(b_priority)
56
+
57
+ if a_priority_index.nil? && b_priority_index.nil?
58
+ result = a_priority <=> b_priority
59
+ elsif a_priority_index.nil?
60
+ result = 1
61
+ elsif b_priority_index.nil?
62
+ result = -1
63
+ else
64
+ result = b_priority_index <=> a_priority_index
65
+ end
66
+
67
+ result = b_age <=> a_age if result.zero?
68
+ result = a_issue <=> b_issue if result.zero?
69
+ result
70
+ end
71
+ end
72
+
73
+ def make_blocked_stalled_lines issue
74
+ today = date_range.end
75
+ started_date = issue.board.cycletime.started_stopped_times(issue).first&.to_date
76
+ return [] unless started_date
77
+
78
+ blocked_stalled = issue.blocked_stalled_by_date(
79
+ date_range: today..today, chart_end_time: time_range.end, settings: settings
80
+ )[today]
81
+ return [] unless blocked_stalled
82
+
83
+ lines = []
84
+ if blocked_stalled.blocked?
85
+ marker = color_block '--blocked-color'
86
+ lines << ["#{marker} Blocked by flag"] if blocked_stalled.flag
87
+ lines << ["#{marker} Blocked by status: #{blocked_stalled.status}"] if blocked_stalled.blocked_by_status?
88
+ blocked_stalled.blocking_issue_keys&.each do |key|
89
+ lines << ["#{marker} Blocked by issue: #{key}"]
90
+ blocking_issue = issues.find { |i| i.key == key }
91
+ lines << blocking_issue if blocking_issue
92
+ end
93
+ elsif blocked_stalled.stalled_by_status?
94
+ lines << ["#{color_block '--stalled-color'} Stalled by status: #{blocked_stalled.status}"]
95
+ else
96
+ lines << ["#{color_block '--stalled-color'} Stalled by inactivity: #{blocked_stalled.stalled_days} days"]
97
+ end
98
+ lines
99
+ end
100
+
101
+ def make_issue_label issue
102
+ "<img src='#{issue.type_icon_url}' title='#{issue.type}' class='icon' /> " \
103
+ "<b><a href='#{issue.url}'>#{issue.key}</a></b> &nbsp;<i>#{issue.summary}</i>"
104
+ end
105
+
106
+ def make_title_line issue
107
+ title_line = +''
108
+ title_line << color_block('--expedited-color', title: 'Expedited') if issue.expedited?
109
+ title_line << make_issue_label(issue)
110
+ title_line
111
+ end
112
+
113
+ def make_parent_lines issue
114
+ lines = []
115
+ parent_key = issue.parent_key
116
+ if parent_key
117
+ parent = issues.find_by_key key: parent_key, include_hidden: true
118
+ text = parent ? make_issue_label(parent) : parent_key
119
+ lines << ["Parent: #{text}"]
120
+ end
121
+ lines
122
+ end
123
+
124
+ def make_stats_lines issue
125
+ line = []
126
+
127
+ line << "<img src='#{issue.priority_url}' class='icon' /> <b>#{issue.priority_name}</b>"
128
+
129
+ age = issue.board.cycletime.age(issue, today: date_range.end)
130
+ line << "Age: <b>#{age ? label_days(age) : '(Not Started)'}</b>"
131
+
132
+ line << "Status: <b>#{format_status issue.status, board: issue.board}</b>"
133
+
134
+ column = issue.board.visible_columns.find { |c| c.status_ids.include?(issue.status.id) }
135
+ line << "Column: <b>#{column&.name || '(not visible on board)'}</b>"
136
+
137
+ if issue.assigned_to
138
+ line << "Assignee: <img src='#{issue.assigned_to_icon_url}' class='icon' /> <b>#{issue.assigned_to}</b>"
139
+ end
140
+
141
+ line << "Due: <b>#{issue.due_date}</b>" if issue.due_date
142
+
143
+ block = lambda do |collection, label|
144
+ unless collection.empty?
145
+ text = collection.collect { |l| "<span class='label'>#{l}</span>" }.join(' ')
146
+ line << "#{label} #{text}"
147
+ end
148
+ end
149
+ block.call issue.labels, 'Labels:'
150
+ block.call issue.component_names, 'Components:'
151
+
152
+ [line]
153
+ end
154
+
155
+ def make_child_lines issue
156
+ lines = []
157
+ subtasks = issue.subtasks.reject { |i| i.done? }
158
+
159
+ unless subtasks.empty?
160
+ icon_urls = subtasks.collect(&:type_icon_url).uniq.collect { |url| "<img src='#{url}' class='icon' />" }
161
+ lines << (icon_urls << 'Incomplete child issues')
162
+ lines += subtasks
163
+ end
164
+ lines
165
+ end
166
+
167
+ def jira_rich_text_to_html text
168
+ text
169
+ .gsub(/{color:(#\w{6})}([^{]+){color}/, '<span style="color: \1">\2</span>') # Colours
170
+ .gsub(/\[~accountid:([^\]]+)\]/) { expand_account_id $1 } # Tagged people
171
+ .gsub(/\[([^\|]+)\|(https?[^\]]+)\]/, '<a href="\2">\1</a>') # URLs
172
+ .gsub("\n", '<br />')
173
+ end
174
+
175
+ def expand_account_id account_id
176
+ user = @users.find { |u| u.account_id == account_id }
177
+ text = account_id
178
+ text = "@#{user.display_name}" if user
179
+ "<span class='account_id'>#{text}</span>"
180
+ end
181
+
182
+ def make_history_lines issue
183
+ history = issue.changes.reverse
184
+ lines = []
185
+
186
+ id = next_id
187
+ lines << [
188
+ "<a href=\"javascript:toggle_visibility('open#{id}', 'close#{id}', 'table#{id}');\">" \
189
+ "<span id='open#{id}'>▶ Issue History</span>" \
190
+ "<span id='close#{id}' style='display: none'>▼ Issue History</span></a>"
191
+ ]
192
+ table = +''
193
+ table << "<table id='table#{id}' style='display: none'>"
194
+ history.each do |c|
195
+ time = c.time.strftime '%b %d, %I:%M%P'
196
+
197
+ table << '<tr>'
198
+ table << "<td><span class='time' title='Timestamp: #{c.time}'>#{time}</span></td>"
199
+ table << "<td><img src='#{c.author_icon_url}' class='icon' title='#{c.author}' /></td>"
200
+ text = history_text change: c, board: issue.board
201
+ table << "<td><span class='field'>#{c.field_as_human_readable}</span> #{text}</td>"
202
+ table << '</tr>'
203
+ end
204
+ table << '</table>'
205
+ lines << [table]
206
+ lines
207
+ end
208
+
209
+ def history_text change:, board:
210
+ if change.comment?
211
+ jira_rich_text_to_html(change.value)
212
+ elsif change.status?
213
+ convertor = ->(id) { format_status(board.possible_statuses.find_by_id(id), board: board) }
214
+ to = convertor.call(change.value_id)
215
+ if change.old_value
216
+ from = convertor.call(change.old_value_id)
217
+ "Changed from #{from} to #{to}"
218
+ else
219
+ "Set to #{to}"
220
+ end
221
+ elsif %w[priority assignee duedate issuetype].include?(change.field)
222
+ "Changed from \"#{change.old_value}\" to \"#{change.value}\""
223
+ elsif change.flagged?
224
+ change.value == '' ? 'Off' : 'On'
225
+ else
226
+ change.value
227
+ end
228
+ end
229
+
230
+ def make_sprints_lines issue
231
+ return [] unless issue.board.scrum?
232
+
233
+ sprint_names = issue.sprints.collect do |sprint|
234
+ if sprint.closed?
235
+ "<s>#{sprint.name}</s>"
236
+ else
237
+ sprint.name
238
+ end
239
+ end
240
+
241
+ return [['Sprints: NONE']] if sprint_names.empty?
242
+
243
+ [[+'Sprints: ' << sprint_names
244
+ .collect { |name| "<span class='label'>#{name}</span>" }
245
+ .join(' ')]]
246
+ end
247
+
248
+ def assemble_issue_lines issue, child:
249
+ lines = []
250
+ lines << [make_title_line(issue)]
251
+ lines += make_parent_lines(issue) unless child
252
+ lines += make_stats_lines(issue)
253
+ lines += make_sprints_lines(issue)
254
+ lines += make_blocked_stalled_lines(issue)
255
+ lines += make_child_lines(issue)
256
+ lines += make_history_lines(issue)
257
+ lines
258
+ end
259
+
260
+ def render_issue issue, child:
261
+ css_class = child ? 'child_issue' : 'daily_issue'
262
+ result = +''
263
+ result << "<div class='#{css_class}'>"
264
+ assemble_issue_lines(issue, child: child).each do |row|
265
+ if row.is_a? Issue
266
+ result << render_issue(row, child: true)
267
+ else
268
+ result << '<div class="heading">'
269
+ row.each do |chunk|
270
+ result << "<div>#{chunk}</div>"
271
+ end
272
+ result << '</div>'
273
+ end
274
+ end
275
+ result << '</div>'
276
+ end
277
+ end
@@ -3,34 +3,58 @@
3
3
  require 'jirametrics/daily_wip_chart'
4
4
 
5
5
  class DailyWipByAgeChart < DailyWipChart
6
+ def initialize block
7
+ super
8
+
9
+ add_trend_line line_color: '--aging-work-in-progress-by-age-trend-line-color', group_labels: [
10
+ 'Less than a day',
11
+ 'A week or less',
12
+ 'Two weeks or less',
13
+ 'Four weeks or less',
14
+ 'More than four weeks'
15
+ ]
16
+ end
17
+
6
18
  def default_header_text
7
19
  'Daily WIP grouped by Age'
8
20
  end
9
21
 
10
22
  def default_description_text
11
23
  <<-HTML
12
- <p>
24
+ <div class="p">
13
25
  This chart shows the highest WIP on each given day. The WIP is color coded so you can see
14
26
  how old it is and hovering over the bar will show you exactly which work items it relates
15
- to. The green bar underneath, shows how many items completed on that day.
16
- </p>
17
- <p>
18
- "Completed without being started" reflects the fact that while we know that it completed
19
- that day, we were unable to determine when it had started. These items will show up in
20
- white at the top. Note that the white is approximate because we don't know exactly when
21
- it started so we're guessing.
22
- </p>
27
+ to. The #{color_block '--wip-chart-completed-color'}
28
+ #{color_block '--wip-chart-completed-but-not-started-color'}
29
+ bars underneath, show how many items completed on that day.
30
+ </div>
31
+ <% if @has_completed_but_not_started %>
32
+ <div class="p">
33
+ #{color_block '--wip-chart-completed-but-not-started-color'} "Completed but not started"
34
+ reflects the fact that while we know that it completed that day, we were unable to determine when
35
+ it had started; it had moved directly from a To Do status to a Done status.
36
+ The #{color_block '--body-background'} shading at the top shows when they might
37
+ have been active. Note that the this grouping is approximate as we just don't know for sure.
38
+ </div>
39
+ <% end %>
40
+ #{describe_non_working_days}
41
+ <div class="p">
42
+ The #{color_block '--aging-work-in-progress-by-age-trend-line-color'} dashed line is a general trend line.
43
+ <% if @has_completed_but_not_started %>
44
+ Note that this trend line only includes items where we know both the start and end times of
45
+ the work so it may not be as accurate as we hope.
46
+ <% end %>
47
+ </div>
23
48
  HTML
24
49
  end
25
50
 
26
51
  def default_grouping_rules issue:, rules:
27
- cycletime = issue.board.cycletime
28
- started = cycletime.started_time(issue)&.to_date
29
- stopped = cycletime.stopped_time(issue)&.to_date
52
+ started, stopped = issue.board.cycletime.started_stopped_dates(issue)
30
53
 
31
54
  rules.issue_hint = "(age: #{label_days (rules.current_date - started + 1).to_i})" if started
32
55
 
33
56
  if stopped && started.nil? # We can't tell when it started
57
+ @has_completed_but_not_started = true
34
58
  not_started stopped: stopped, rules: rules, created: issue.created.to_date
35
59
  elsif stopped == rules.current_date
36
60
  stopped_today rules: rules
@@ -42,11 +66,11 @@ class DailyWipByAgeChart < DailyWipChart
42
66
  def not_started stopped:, rules:, created:
43
67
  if stopped == rules.current_date
44
68
  rules.label = 'Completed but not started'
45
- rules.color = '#66FF66'
69
+ rules.color = '--wip-chart-completed-but-not-started-color'
46
70
  rules.group_priority = -1
47
71
  else
48
72
  rules.label = 'Start date unknown'
49
- rules.color = 'white'
73
+ rules.color = '--body-background'
50
74
  rules.group_priority = 11
51
75
  created_days = rules.current_date - created + 1
52
76
  rules.issue_hint = "(created: #{label_days created_days.to_i} earlier, stopped on #{stopped})"
@@ -55,7 +79,7 @@ class DailyWipByAgeChart < DailyWipChart
55
79
 
56
80
  def stopped_today rules:
57
81
  rules.label = 'Completed'
58
- rules.color = '#009900'
82
+ rules.color = '--wip-chart-completed-color'
59
83
  rules.group_priority = -2
60
84
  end
61
85
 
@@ -65,23 +89,23 @@ class DailyWipByAgeChart < DailyWipChart
65
89
  case age
66
90
  when 1
67
91
  rules.label = 'Less than a day'
68
- rules.color = '#aaaaaa'
92
+ rules.color = '--wip-chart-duration-less-than-day-color'
69
93
  rules.group_priority = 10 # Highest is top
70
94
  when 2..7
71
95
  rules.label = 'A week or less'
72
- rules.color = '#80bfff'
96
+ rules.color = '--wip-chart-duration-week-or-less-color'
73
97
  rules.group_priority = 9
74
98
  when 8..14
75
99
  rules.label = 'Two weeks or less'
76
- rules.color = '#ffd700'
100
+ rules.color = '--wip-chart-duration-two-weeks-or-less-color'
77
101
  rules.group_priority = 8
78
102
  when 15..28
79
103
  rules.label = 'Four weeks or less'
80
- rules.color = '#ce6300'
104
+ rules.color = '--wip-chart-duration-four-weeks-or-less-color'
81
105
  rules.group_priority = 7
82
106
  else
83
107
  rules.label = 'More than four weeks'
84
- rules.color = '#990000'
108
+ rules.color = '--wip-chart-duration-more-than-four-weeks-color'
85
109
  rules.group_priority = 6
86
110
  end
87
111
  end
@@ -9,68 +9,70 @@ class DailyWipByBlockedStalledChart < DailyWipChart
9
9
 
10
10
  def default_description_text
11
11
  <<-HTML
12
- <p>
13
- This chart highlights work that is blocked or stalled on each given day. In Jira terms, blocked
14
- means that the issue has been "flagged". Stalled indicates that the item hasn't had any updates in 5 days.
15
- </p>
16
- <p>
17
- Note that if an item tracks as both blocked and stalled, it will only show up in the flagged totals.
12
+ <div class="p">
13
+ This chart highlights work that is #{color_block '--blocked-color'} blocked,
14
+ #{color_block '--stalled-color'} stalled, or
15
+ #{color_block '--wip-chart-active-color'} active on each given day.
16
+ <ul>
17
+ <li>#{color_block '--blocked-color'} Blocked could mean that the item has been flagged or it's
18
+ in a status that is configured as blocked, or it could have a link showing that it is blocked
19
+ by another item. It all depends how the report has been configured.</li>
20
+ <li>#{color_block '--stalled-color'} Stalled indicates that there has been no activity on this
21
+ item in five days.</li>
22
+ </ul>
23
+ </div>
24
+ <div class="p">
25
+ Note that if an item tracks as both blocked and stalled, it will only show up in the blocked totals.
18
26
  It will not be double counted.
19
- </p>
20
- <p>
21
- The white section reflects items that have stopped but for which we can't identify the start date. As
22
- a result, we are unable to properly show the WIP for these items.
23
- </p>
27
+ </div>
28
+ <% if @has_completed_but_not_started %>
29
+ <div class="p">
30
+ #{color_block '--wip-chart-completed-but-not-started-color'} "Completed but not started"
31
+ reflects the fact that while we know that it completed that day, we were unable to determine when
32
+ it had started; it had moved directly from a To Do status to a Done status.
33
+ The #{color_block '--body-background'} shading at the top shows when they might
34
+ have been active. Note that the this grouping is approximate as we just don't know for sure.
35
+ </div>
36
+ <% end %>
37
+ #{describe_non_working_days}
24
38
  HTML
25
39
  end
26
40
 
27
- def key_blocked_stalled_change issue:, date:, end_time:
28
- stalled_change = nil
29
- blocked_change = nil
30
-
31
- issue.blocked_stalled_changes_on_date(date: date, end_time: end_time) do |change|
32
- blocked_change = change if change.blocked?
33
- stalled_change = change if change.stalled?
34
- end
35
-
36
- return blocked_change if blocked_change
37
- return stalled_change if stalled_change
38
-
39
- nil
40
- end
41
-
42
41
  def default_grouping_rules issue:, rules:
43
- started = issue.board.cycletime.started_time(issue)
44
- stopped_date = issue.board.cycletime.stopped_time(issue)&.to_date
45
- change = key_blocked_stalled_change issue: issue, date: rules.current_date, end_time: time_range.end
42
+ started, stopped = issue.board.cycletime.started_stopped_times(issue)
43
+ stopped_date = stopped&.to_date
44
+
45
+ date = rules.current_date
46
+ change = issue.blocked_stalled_by_date(date_range: date..date, chart_end_time: time_range.end)[date]
46
47
 
47
48
  stopped_today = stopped_date == rules.current_date
48
49
 
49
50
  if stopped_today && started.nil?
51
+ @has_completed_but_not_started = true
50
52
  rules.label = 'Completed but not started'
51
- rules.color = '#66FF66'
53
+ rules.color = '--wip-chart-completed-but-not-started-color'
52
54
  rules.group_priority = -1
53
55
  elsif stopped_today
54
56
  rules.label = 'Completed'
55
- rules.color = '#009900'
57
+ rules.color = '--wip-chart-completed-color'
56
58
  rules.group_priority = -2
57
59
  elsif started.nil?
58
60
  rules.label = 'Start date unknown'
59
- rules.color = 'white'
61
+ rules.color = '--body-background'
60
62
  rules.group_priority = 4
61
63
  elsif change&.blocked?
62
64
  rules.label = 'Blocked'
63
- rules.color = 'red'
65
+ rules.color = '--blocked-color'
64
66
  rules.group_priority = 1
65
67
  rules.issue_hint = "(#{change.reasons})"
66
68
  elsif change&.stalled?
67
69
  rules.label = 'Stalled'
68
- rules.color = 'orange'
70
+ rules.color = '--stalled-color'
69
71
  rules.group_priority = 2
70
72
  rules.issue_hint = "(#{change.reasons})"
71
73
  else
72
74
  rules.label = 'Active'
73
- rules.color = 'lightgray'
75
+ rules.color = '--wip-chart-active-color'
74
76
  rules.group_priority = 3
75
77
  end
76
78
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jirametrics/daily_wip_chart'
4
+
5
+ class DailyWipByParentChart < DailyWipChart
6
+ def default_header_text
7
+ 'Daily WIP, grouped by the parent ticket (Epic, Feature, etc)'
8
+ end
9
+
10
+ def default_description_text
11
+ <<-HTML
12
+ <div class="p">
13
+ How much work is in progress, grouped by the parent of the issue. This will give us an
14
+ indication of how focused we are on higher level objectives. If there are many parent
15
+ tickets in progress at the same time, either this team has their focus scattered or we
16
+ aren't doing a good job of
17
+ <a href="https://improvingflow.com/2024/02/21/slicing-epics.html">splitting those parent
18
+ tickets</a>. Neither of those is desirable.
19
+ </div>
20
+ <div class="p">
21
+ The #{color_block '--body-background'} shading at the top shows items that don't have a parent
22
+ at all.
23
+ </div>
24
+ #{describe_non_working_days}
25
+ HTML
26
+ end
27
+
28
+ def default_grouping_rules issue:, rules:
29
+ parent = issue.parent&.key
30
+ if parent
31
+ rules.label = parent
32
+ else
33
+ rules.label = 'No parent'
34
+ rules.group_priority = 1000
35
+ rules.color = '--body-background'
36
+ end
37
+ end
38
+ end