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.
- checksums.yaml +4 -4
- data/lib/jirametrics/aggregate_config.rb +19 -26
- data/lib/jirametrics/aging_work_bar_chart.rb +79 -54
- data/lib/jirametrics/aging_work_in_progress_chart.rb +106 -40
- data/lib/jirametrics/aging_work_table.rb +84 -54
- data/lib/jirametrics/anonymizer.rb +6 -5
- data/lib/jirametrics/blocked_stalled_change.rb +24 -4
- data/lib/jirametrics/board.rb +51 -23
- data/lib/jirametrics/board_config.rb +9 -4
- data/lib/jirametrics/board_movement_calculator.rb +155 -0
- data/lib/jirametrics/change_item.rb +56 -21
- data/lib/jirametrics/chart_base.rb +101 -61
- data/lib/jirametrics/columns_config.rb +4 -0
- data/lib/jirametrics/css_variable.rb +33 -0
- data/lib/jirametrics/cycletime_config.rb +59 -8
- data/lib/jirametrics/cycletime_histogram.rb +69 -4
- data/lib/jirametrics/cycletime_scatterplot.rb +11 -15
- data/lib/jirametrics/daily_view.rb +277 -0
- data/lib/jirametrics/daily_wip_by_age_chart.rb +44 -20
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +37 -35
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +38 -0
- data/lib/jirametrics/daily_wip_chart.rb +61 -14
- data/lib/jirametrics/data_quality_report.rb +222 -41
- data/lib/jirametrics/dependency_chart.rb +54 -23
- data/lib/jirametrics/download_config.rb +12 -0
- data/lib/jirametrics/downloader.rb +86 -56
- data/lib/jirametrics/estimate_accuracy_chart.rb +173 -0
- data/lib/jirametrics/estimation_configuration.rb +25 -0
- data/lib/jirametrics/examples/aggregated_project.rb +22 -39
- data/lib/jirametrics/examples/standard_project.rb +26 -48
- data/lib/jirametrics/expedited_chart.rb +28 -25
- data/lib/jirametrics/exporter.rb +59 -32
- data/lib/jirametrics/file_config.rb +35 -14
- data/lib/jirametrics/file_system.rb +48 -3
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +111 -0
- data/lib/jirametrics/groupable_issue_chart.rb +2 -6
- data/lib/jirametrics/grouping_rules.rb +7 -1
- data/lib/jirametrics/hierarchy_table.rb +4 -4
- data/lib/jirametrics/html/aging_work_bar_chart.erb +13 -16
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +28 -5
- data/lib/jirametrics/html/aging_work_table.erb +21 -25
- data/lib/jirametrics/html/cycletime_histogram.erb +83 -3
- data/lib/jirametrics/html/cycletime_scatterplot.erb +9 -12
- data/lib/jirametrics/html/daily_wip_chart.erb +17 -13
- data/lib/jirametrics/html/{story_point_accuracy_chart.erb → estimate_accuracy_chart.erb} +9 -4
- data/lib/jirametrics/html/expedited_chart.erb +10 -13
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
- data/lib/jirametrics/html/hierarchy_table.erb +2 -2
- data/lib/jirametrics/html/index.css +280 -0
- data/lib/jirametrics/html/index.erb +33 -39
- data/lib/jirametrics/html/sprint_burndown.erb +10 -14
- data/lib/jirametrics/html/throughput_chart.erb +10 -13
- data/lib/jirametrics/html_report_config.rb +110 -86
- data/lib/jirametrics/issue.rb +390 -109
- data/lib/jirametrics/issue_collection.rb +33 -0
- data/lib/jirametrics/jira_gateway.rb +33 -12
- data/lib/jirametrics/project_config.rb +276 -147
- data/lib/jirametrics/rules.rb +2 -2
- data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
- data/lib/jirametrics/settings.json +11 -0
- data/lib/jirametrics/sprint.rb +1 -0
- data/lib/jirametrics/sprint_burndown.rb +59 -40
- data/lib/jirametrics/sprint_issue_change_data.rb +3 -3
- data/lib/jirametrics/status.rb +84 -19
- data/lib/jirametrics/status_collection.rb +86 -39
- data/lib/jirametrics/throughput_chart.rb +12 -4
- data/lib/jirametrics/user.rb +12 -0
- data/lib/jirametrics/value_equality.rb +2 -2
- data/lib/jirametrics.rb +29 -7
- metadata +20 -17
- data/lib/jirametrics/discard_changes_before.rb +0 -37
- data/lib/jirametrics/experimental/generator.rb +0 -210
- data/lib/jirametrics/experimental/info.rb +0 -77
- data/lib/jirametrics/html/data_quality_report.erb +0 -126
- 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
|
|
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
|
-
</
|
|
19
|
-
<p>
|
|
20
|
-
The
|
|
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
|
-
</
|
|
26
|
-
|
|
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, '
|
|
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
|
-
|
|
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.
|
|
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> <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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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 = '
|
|
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 = '
|
|
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 = '
|
|
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 = '
|
|
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 = '
|
|
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 = '
|
|
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 = '
|
|
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 = '
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
</
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
44
|
-
stopped_date =
|
|
45
|
-
|
|
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 = '
|
|
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 = '
|
|
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 = '
|
|
61
|
+
rules.color = '--body-background'
|
|
60
62
|
rules.group_priority = 4
|
|
61
63
|
elsif change&.blocked?
|
|
62
64
|
rules.label = 'Blocked'
|
|
63
|
-
rules.color = '
|
|
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 = '
|
|
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 = '
|
|
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
|