jirametrics 2.1.1 → 2.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/jirametrics/aggregate_config.rb +1 -1
- data/lib/jirametrics/aging_work_bar_chart.rb +1 -4
- data/lib/jirametrics/aging_work_table.rb +13 -1
- data/lib/jirametrics/chart_base.rb +17 -2
- data/lib/jirametrics/cycletime_scatterplot.rb +4 -7
- data/lib/jirametrics/daily_wip_by_age_chart.rb +35 -9
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +13 -6
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +42 -0
- data/lib/jirametrics/daily_wip_chart.rb +65 -4
- data/lib/jirametrics/data_quality_report.rb +1 -1
- data/lib/jirametrics/downloader.rb +7 -4
- data/lib/jirametrics/examples/aggregated_project.rb +14 -19
- data/lib/jirametrics/examples/standard_project.rb +1 -15
- data/lib/jirametrics/expedited_chart.rb +12 -13
- data/lib/jirametrics/html/aging_work_table.erb +10 -18
- data/lib/jirametrics/html/daily_wip_chart.erb +8 -0
- data/lib/jirametrics/html/index.css +13 -1
- data/lib/jirametrics/html_report_config.rb +11 -1
- data/lib/jirametrics/issue.rb +2 -1
- data/lib/jirametrics/jira_gateway.rb +4 -1
- data/lib/jirametrics/project_config.rb +2 -2
- data/lib/jirametrics/sprint_burndown.rb +8 -2
- data/lib/jirametrics/story_point_accuracy_chart.rb +14 -8
- data/lib/jirametrics/throughput_chart.rb +6 -1
- data/lib/jirametrics.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f08f0ccb466afa1cba17db109091ab8333b52ed1ec8c1d0b0920c400052f43e7
|
4
|
+
data.tar.gz: f127c794e8fc4c447cab24cd7d2dc149c17dfec6742c6238ed8c5a3f7faeabcb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6e07a75c020a54dc077d25b2db1742fc143a4174d5b453813d0d2f3969984f1fc8eb6af18cfa092ecf8ef9d07a4754f9b6013e39c170ee2c3b34122aebf0a387
|
7
|
+
data.tar.gz: abddaf6fa818847dc6427df90c919c6ade29cf79735c0a2fd64a31b7815d730e146a695fa0696b8896c60f66828dc16c207852a8c116db7fdabc21c90e429964
|
@@ -27,10 +27,7 @@ class AgingWorkBarChart < ChartBase
|
|
27
27
|
<li>The bottom bar indicated #{color_block '--expedited-color'} expedited.</li>
|
28
28
|
</ol>
|
29
29
|
</p>
|
30
|
-
|
31
|
-
The gray backgrounds indicate weekends and the red vertical line indicates the 85% point for all
|
32
|
-
items in this time period. Anything that started to the left of that is now an outlier.
|
33
|
-
</p>
|
30
|
+
#{ describe_non_working_days }
|
34
31
|
HTML
|
35
32
|
|
36
33
|
# Because this one will size itself as needed, we start with a smaller default size
|
@@ -11,6 +11,18 @@ class AgingWorkTable < ChartBase
|
|
11
11
|
@dead_threshold = 45
|
12
12
|
@age_cutoff = 0
|
13
13
|
|
14
|
+
header_text 'Aging Work Table'
|
15
|
+
description_text <<-TEXT
|
16
|
+
<p>
|
17
|
+
This chart shows all active (started but not completed) work, ordered from oldest at the top to
|
18
|
+
newest at the bottom.
|
19
|
+
</p>
|
20
|
+
<p>
|
21
|
+
If there are expedited items that haven't yet started then they're at the bottom of the table.
|
22
|
+
By the very definition of expedited, if we haven't started them already, we'd better get on that.
|
23
|
+
</p>
|
24
|
+
TEXT
|
25
|
+
|
14
26
|
instance_eval(&block) if block
|
15
27
|
end
|
16
28
|
|
@@ -24,7 +36,7 @@ class AgingWorkTable < ChartBase
|
|
24
36
|
end
|
25
37
|
aging_issues += expedited_but_not_started.sort_by(&:created)
|
26
38
|
|
27
|
-
|
39
|
+
wrap_and_render(binding, __FILE__)
|
28
40
|
end
|
29
41
|
|
30
42
|
def select_aging_issues
|
@@ -38,11 +38,17 @@ class ChartBase
|
|
38
38
|
erb.result(caller_binding)
|
39
39
|
end
|
40
40
|
|
41
|
-
|
42
|
-
def wrap_and_render caller_binding, file
|
41
|
+
def render_top_text caller_binding
|
43
42
|
result = +''
|
44
43
|
result << "<h1>#{@header_text}</h1>" if @header_text
|
45
44
|
result << ERB.new(@description_text).result(caller_binding) if @description_text
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
# Render the file and then wrap it with standard headers and quality checks.
|
49
|
+
def wrap_and_render caller_binding, file
|
50
|
+
result = +''
|
51
|
+
result << render_top_text(caller_binding)
|
46
52
|
result << render(caller_binding, file)
|
47
53
|
result
|
48
54
|
end
|
@@ -238,4 +244,13 @@ class ChartBase
|
|
238
244
|
result << '></div>'
|
239
245
|
result
|
240
246
|
end
|
247
|
+
|
248
|
+
def describe_non_working_days
|
249
|
+
<<-TEXT
|
250
|
+
<div class='p'>
|
251
|
+
The #{color_block '--non-working-days-color'} vertical bars indicate non-working days; weekends
|
252
|
+
and any other holidays mentioned in the configuration.
|
253
|
+
</div>
|
254
|
+
TEXT
|
255
|
+
end
|
241
256
|
end
|
@@ -12,11 +12,11 @@ class CycletimeScatterplot < ChartBase
|
|
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
|
-
<div
|
18
|
+
</div>
|
19
|
+
<div class="p">
|
20
20
|
The #{color_block '--cycletime-scatterplot-overall-trendline-color'} line indicates the 85th
|
21
21
|
percentile (<%= overall_percent_line %> days). 85% of all
|
22
22
|
items on this chart fall on or below the line and the remaining 15% are above the line. 85%
|
@@ -24,10 +24,7 @@ class CycletimeScatterplot < ChartBase
|
|
24
24
|
predict that most work of this type will complete in <%= overall_percent_line %> days or
|
25
25
|
less. The other lines reflect the 85% line for that respective type of work.
|
26
26
|
</div>
|
27
|
-
|
28
|
-
The #{color_block '--non-working-days-color'} vertical bars indicate weekends, when theoretically
|
29
|
-
we aren't working.
|
30
|
-
</div>
|
27
|
+
#{ describe_non_working_days }
|
31
28
|
HTML
|
32
29
|
|
33
30
|
init_configuration_block block do
|
@@ -3,23 +3,48 @@
|
|
3
3
|
require 'jirametrics/daily_wip_chart'
|
4
4
|
|
5
5
|
class DailyWipByAgeChart < DailyWipChart
|
6
|
+
def initialize block = nil
|
7
|
+
super(block)
|
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
|
|
@@ -31,6 +56,7 @@ class DailyWipByAgeChart < DailyWipChart
|
|
31
56
|
rules.issue_hint = "(age: #{label_days (rules.current_date - started + 1).to_i})" if started
|
32
57
|
|
33
58
|
if stopped && started.nil? # We can't tell when it started
|
59
|
+
@has_completed_but_not_started = true
|
34
60
|
not_started stopped: stopped, rules: rules, created: issue.created.to_date
|
35
61
|
elsif stopped == rules.current_date
|
36
62
|
stopped_today rules: rules
|
@@ -9,7 +9,7 @@ class DailyWipByBlockedStalledChart < DailyWipChart
|
|
9
9
|
|
10
10
|
def default_description_text
|
11
11
|
<<-HTML
|
12
|
-
<div>
|
12
|
+
<div class="p">
|
13
13
|
This chart highlights work that is #{color_block '--blocked-color'} blocked,
|
14
14
|
#{color_block '--stalled-color'} stalled, or
|
15
15
|
#{color_block '--wip-chart-active-color'} active on each given day.
|
@@ -21,14 +21,20 @@ class DailyWipByBlockedStalledChart < DailyWipChart
|
|
21
21
|
item in five days.</li>
|
22
22
|
</ul>
|
23
23
|
</div>
|
24
|
-
<p>
|
24
|
+
<div class="p">
|
25
25
|
Note that if an item tracks as both blocked and stalled, it will only show up in the blocked totals.
|
26
26
|
It will not be double counted.
|
27
|
-
</p>
|
28
|
-
<div>
|
29
|
-
The #{color_block '--body-background'} shaded section reflects items that have stopped but for which we can't identify the start date. As
|
30
|
-
a result, we are unable to properly show the WIP for these items.
|
31
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}
|
32
38
|
HTML
|
33
39
|
end
|
34
40
|
|
@@ -55,6 +61,7 @@ class DailyWipByBlockedStalledChart < DailyWipChart
|
|
55
61
|
stopped_today = stopped_date == rules.current_date
|
56
62
|
|
57
63
|
if stopped_today && started.nil?
|
64
|
+
@has_completed_but_not_started = true
|
58
65
|
rules.label = 'Completed but not started'
|
59
66
|
rules.color = '--wip-chart-completed-but-not-started-color'
|
60
67
|
rules.group_priority = -1
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jirametrics/daily_wip_chart'
|
4
|
+
|
5
|
+
class DailyWipByParentChart < DailyWipChart
|
6
|
+
def initialize block
|
7
|
+
super(block)
|
8
|
+
end
|
9
|
+
|
10
|
+
def default_header_text
|
11
|
+
'Daily WIP, grouped by the parent ticket (Epic, Feature, etc)'
|
12
|
+
end
|
13
|
+
|
14
|
+
def default_description_text
|
15
|
+
<<-HTML
|
16
|
+
<div class="p">
|
17
|
+
How much work is in progress, grouped by the parent of the issue. This will give us an
|
18
|
+
indication of how focused we are on higher level objectives. If there are many parent
|
19
|
+
tickets in progress at the same time, either this team has their focus scattered or we
|
20
|
+
aren't doing a good job of
|
21
|
+
<a href="https://improvingflow.com/2024/02/21/slicing-epics.html">splitting those parent
|
22
|
+
tickets</a>. Neither of those is desirable.
|
23
|
+
</div>
|
24
|
+
<div class="p">
|
25
|
+
The #{color_block '--body-background'} shading at the top shows items that don't have a parent
|
26
|
+
at all.
|
27
|
+
</div>
|
28
|
+
#{describe_non_working_days}
|
29
|
+
HTML
|
30
|
+
end
|
31
|
+
|
32
|
+
def default_grouping_rules issue:, rules:
|
33
|
+
parent = issue.parent&.key
|
34
|
+
if parent
|
35
|
+
rules.label = parent
|
36
|
+
else
|
37
|
+
rules.label = 'No parent'
|
38
|
+
rules.group_priority = 1000
|
39
|
+
rules.color = '--body-background'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -20,13 +20,16 @@ class DailyWipChart < ChartBase
|
|
20
20
|
header_text default_header_text
|
21
21
|
description_text default_description_text
|
22
22
|
|
23
|
-
if block
|
24
|
-
|
25
|
-
|
23
|
+
instance_eval(&block) if block
|
24
|
+
|
25
|
+
unless @group_by_block
|
26
26
|
grouping_rules do |issue, rules|
|
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
|
30
33
|
end
|
31
34
|
|
32
35
|
def run
|
@@ -36,6 +39,13 @@ class DailyWipChart < ChartBase
|
|
36
39
|
data_sets = possible_rules.collect do |grouping_rule|
|
37
40
|
make_data_set grouping_rule: grouping_rule, issue_rules_by_active_date: issue_rules_by_active_date
|
38
41
|
end
|
42
|
+
if @trend_lines
|
43
|
+
data_sets = @trend_lines.filter_map do |group_labels, line_color|
|
44
|
+
trend_line_data_set(data: data_sets, group_labels: group_labels, color: line_color)
|
45
|
+
end + data_sets
|
46
|
+
end
|
47
|
+
|
48
|
+
# grow_chart_height_if_too_many_data_sets data_sets.size
|
39
49
|
|
40
50
|
wrap_and_render(binding, __FILE__)
|
41
51
|
end
|
@@ -109,7 +119,7 @@ class DailyWipChart < ChartBase
|
|
109
119
|
end
|
110
120
|
|
111
121
|
def configure_rule issue:, date:
|
112
|
-
raise
|
122
|
+
raise "#{self.class}: grouping_rules must be set" if @group_by_block.nil?
|
113
123
|
|
114
124
|
rules = DailyGroupingRules.new
|
115
125
|
rules.current_date = date
|
@@ -120,4 +130,55 @@ class DailyWipChart < ChartBase
|
|
120
130
|
def grouping_rules &block
|
121
131
|
@group_by_block = block
|
122
132
|
end
|
133
|
+
|
134
|
+
def add_trend_line group_labels:, line_color:
|
135
|
+
(@trend_lines ||= []) << [group_labels, line_color]
|
136
|
+
end
|
137
|
+
|
138
|
+
def trend_line_data_set data:, group_labels:, color:
|
139
|
+
day_wip_hash = {}
|
140
|
+
data.each do |top_level|
|
141
|
+
next unless group_labels.include? top_level[:label]
|
142
|
+
|
143
|
+
top_level[:data].each do |datapoint|
|
144
|
+
date = datapoint[:x]
|
145
|
+
day_wip_hash[date] = (day_wip_hash[date] || 0) + datapoint[:y]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
points = day_wip_hash
|
150
|
+
.collect { |date, wip| [date.jd, wip] }
|
151
|
+
.sort_by(&:first)
|
152
|
+
|
153
|
+
calculator = TrendLineCalculator.new(points)
|
154
|
+
return nil unless calculator.valid?
|
155
|
+
|
156
|
+
data_points = calculator.chart_datapoints(
|
157
|
+
range: date_range.begin.jd..date_range.end.jd,
|
158
|
+
max_y: points.collect { |_date, wip| wip }.max
|
159
|
+
)
|
160
|
+
data_points.each do |point_hash|
|
161
|
+
point_hash[:x] = chart_format Date.jd(point_hash[:x])
|
162
|
+
end
|
163
|
+
|
164
|
+
{
|
165
|
+
type: 'line',
|
166
|
+
label: "Trendline",
|
167
|
+
data: data_points,
|
168
|
+
fill: false,
|
169
|
+
borderWidth: 1,
|
170
|
+
markerType: 'none',
|
171
|
+
borderColor: CssVariable[color],
|
172
|
+
borderDash: [6, 3],
|
173
|
+
pointStyle: 'dash',
|
174
|
+
hidden: false
|
175
|
+
}
|
176
|
+
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
|
123
184
|
end
|
@@ -271,7 +271,7 @@ class DataQualityReport < ChartBase
|
|
271
271
|
board_names = entry_list.collect { |entry| entry.issue.board.name.inspect }
|
272
272
|
entry_list.first.report(
|
273
273
|
problem_key: :issue_on_multiple_boards,
|
274
|
-
detail: "Found on boards: #{board_names.join(', ')}"
|
274
|
+
detail: "Found on boards: #{board_names.sort.join(', ')}"
|
275
275
|
)
|
276
276
|
end
|
277
277
|
end
|
@@ -148,9 +148,9 @@ class Downloader
|
|
148
148
|
|
149
149
|
def exit_if_call_failed json
|
150
150
|
# Sometimes Jira returns the singular form of errorMessage and sometimes the plural. Consistency FTW.
|
151
|
-
return unless json['errorMessages'] || json['errorMessage']
|
151
|
+
return unless json['error'] || json['errorMessages'] || json['errorMessage']
|
152
152
|
|
153
|
-
log "Download failed. See #{@logfile_name} for details.", both: true
|
153
|
+
log "Download failed. See #{@file_system.logfile_name} for details.", both: true
|
154
154
|
log " #{JSON.pretty_generate(json)}"
|
155
155
|
exit 1
|
156
156
|
end
|
@@ -168,13 +168,16 @@ class Downloader
|
|
168
168
|
def download_board_configuration board_id:
|
169
169
|
log " Downloading board configuration for board #{board_id}", both: true
|
170
170
|
json = @jira_gateway.call_url relative_url: "/rest/agile/1.0/board/#{board_id}/configuration"
|
171
|
-
exit_if_call_failed json
|
172
171
|
|
173
|
-
|
172
|
+
exit_if_call_failed json
|
174
173
|
|
175
174
|
file_prefix = @download_config.project_config.file_prefix
|
176
175
|
@file_system.save_json json: json, filename: "#{@target_path}#{file_prefix}_board_#{board_id}_configuration.json"
|
177
176
|
|
177
|
+
# We have a reported bug that blew up on this line. Moved it after the save so we can
|
178
|
+
# actually look at the returned json.
|
179
|
+
@board_id_to_filter_id[board_id] = json['filter']['id'].to_i
|
180
|
+
|
178
181
|
download_sprints board_id: board_id if json['type'] == 'scrum'
|
179
182
|
end
|
180
183
|
|
@@ -30,32 +30,27 @@ class Exporter
|
|
30
30
|
end
|
31
31
|
|
32
32
|
html_report do
|
33
|
+
html '<h1>Boards included in this report</h1><ul>', type: :header
|
34
|
+
board_lines = []
|
35
|
+
included_projects.each do |project|
|
36
|
+
project.all_boards.values.each do |board|
|
37
|
+
board_lines << "<a href='#{project.file_prefix}.html'>#{board.name}</a> from project #{project.name}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
board_lines.sort.each { |line| html "<li>#{line}</li>", type: :header }
|
41
|
+
html '</ul>', type: :header
|
42
|
+
|
33
43
|
cycletime_scatterplot do
|
34
44
|
show_trend_lines
|
45
|
+
# For an aggregated report we group by board rather than by type
|
35
46
|
grouping_rules do |issue, rules|
|
36
47
|
rules.label = issue.board.name
|
37
48
|
end
|
38
49
|
end
|
39
50
|
# aging_work_in_progress_chart
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
<p>How much work is in progress, grouped by the parent of the issue. This will give us an
|
44
|
-
indication of how focused we are on higher level objectives. If there are many parent
|
45
|
-
tickets in progress at the same time, either this team has their focus scattered or we
|
46
|
-
aren't doing a good job of
|
47
|
-
<a href="https://improvingflow.com/2024/02/21/slicing-epics.html">splitting those parent
|
48
|
-
tickets</a>. Neither of those is desirable.</p>
|
49
|
-
<p>If you're expecting all work items to have parents and there are a lot that don't,
|
50
|
-
that's also something to look at. Consider whether there is even value in aggregating
|
51
|
-
these projects if they don't share parent dependencies. Aggregation helps us when we're
|
52
|
-
looking at related work and if there aren't parent dependencies then the work may not
|
53
|
-
be related.</p>
|
54
|
-
TEXT
|
55
|
-
grouping_rules do |issue, rules|
|
56
|
-
rules.label = issue.parent&.key || 'No parent'
|
57
|
-
rules.color = 'white' if rules.label == 'No parent'
|
58
|
-
end
|
51
|
+
daily_wip_by_parent_chart do
|
52
|
+
# When aggregating, the chart tends to need more vertical space
|
53
|
+
canvas height: 400, width: 800
|
59
54
|
end
|
60
55
|
aging_work_table do
|
61
56
|
# In an aggregated report, we likely only care about items that are old so exclude anything
|
@@ -83,21 +83,7 @@ class Exporter
|
|
83
83
|
aging_work_table
|
84
84
|
daily_wip_by_age_chart
|
85
85
|
daily_wip_by_blocked_stalled_chart
|
86
|
-
|
87
|
-
header_text 'Daily WIP by Parent'
|
88
|
-
description_text <<-TEXT
|
89
|
-
How much work is in progress, grouped by the parent of the issue. This will give us an
|
90
|
-
indication of how focused we are on higher level objectives. If there are many parent
|
91
|
-
tickets in progress at the same time, either this team has their focus scattered or we
|
92
|
-
aren't doing a good job of
|
93
|
-
<a href="https://improvingflow.com/2024/02/21/slicing-epics.html">splitting those parent
|
94
|
-
tickets</a>. Neither of those is desirable.
|
95
|
-
TEXT
|
96
|
-
grouping_rules do |issue, rules|
|
97
|
-
rules.label = issue.parent&.key || 'No parent'
|
98
|
-
rules.color = '--body-background' if rules.label == 'No parent'
|
99
|
-
end
|
100
|
-
end
|
86
|
+
daily_wip_by_parent_chart
|
101
87
|
expedited_chart
|
102
88
|
sprint_burndown
|
103
89
|
story_point_accuracy_chart
|
@@ -25,19 +25,18 @@ class ExpeditedChart < ChartBase
|
|
25
25
|
|
26
26
|
header_text 'Expedited work'
|
27
27
|
description_text <<-HTML
|
28
|
-
<p>
|
28
|
+
<div class="p">
|
29
29
|
This chart only shows issues that have been expedited at some point. We care about these as
|
30
30
|
any form of expedited work will affect the entire system and will slow down non-expedited work.
|
31
31
|
Refer to this article on
|
32
32
|
<a href="https://improvingflow.com/2021/06/16/classes-of-service.html">classes of service</a>
|
33
33
|
for a longer explanation on why we want to avoid expedited work.
|
34
|
-
</
|
35
|
-
<p>
|
36
|
-
The
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
</p>
|
34
|
+
</div>
|
35
|
+
<div class="p">
|
36
|
+
The colour of the line indicates time that this issue was #{color_block '--expedited-color'} expedited
|
37
|
+
or #{color_block '--expedited-chart-no-longer-expedited'} not expedited.
|
38
|
+
</div>
|
39
|
+
#{describe_non_working_days}
|
41
40
|
HTML
|
42
41
|
end
|
43
42
|
|
@@ -127,20 +126,20 @@ class ExpeditedChart < ChartBase
|
|
127
126
|
case action
|
128
127
|
when :issue_started
|
129
128
|
data << make_point(issue: issue, time: time, label: 'Started', expedited: expedited)
|
130
|
-
dot_colors << '
|
129
|
+
dot_colors << CssVariable['--expedited-chart-dot-issue-started-color']
|
131
130
|
point_styles << 'rect'
|
132
131
|
when :issue_stopped
|
133
132
|
data << make_point(issue: issue, time: time, label: 'Completed', expedited: expedited)
|
134
|
-
dot_colors << '
|
133
|
+
dot_colors << CssVariable['--expedited-chart-dot-issue-stopped-color']
|
135
134
|
point_styles << 'rect'
|
136
135
|
when :expedite_start
|
137
136
|
data << make_point(issue: issue, time: time, label: 'Expedited', expedited: true)
|
138
|
-
dot_colors << '
|
137
|
+
dot_colors << CssVariable['--expedited-chart-dot-expedite-started-color']
|
139
138
|
point_styles << 'circle'
|
140
139
|
expedited = true
|
141
140
|
when :expedite_stop
|
142
141
|
data << make_point(issue: issue, time: time, label: 'Not expedited', expedited: false)
|
143
|
-
dot_colors << '
|
142
|
+
dot_colors << CssVariable['--expedited-chart-dot-expedite-stopped-color']
|
144
143
|
point_styles << 'circle'
|
145
144
|
expedited = false
|
146
145
|
else
|
@@ -152,7 +151,7 @@ class ExpeditedChart < ChartBase
|
|
152
151
|
last_change_time = expedite_data[-1][0].to_date
|
153
152
|
if last_change_time && last_change_time <= date_range.end && stopped_time.nil?
|
154
153
|
data << make_point(issue: issue, time: date_range.end, label: 'Still ongoing', expedited: expedited)
|
155
|
-
dot_colors << '
|
154
|
+
dot_colors << '' # It won't be visible so it doesn't matter
|
156
155
|
point_styles << 'dash'
|
157
156
|
end
|
158
157
|
end
|
@@ -1,20 +1,3 @@
|
|
1
|
-
<h1>Aging Work Table</h1>
|
2
|
-
<p>
|
3
|
-
This chart shows all active (started but not completed) work, ordered from oldest at the top to
|
4
|
-
newest at the bottom.
|
5
|
-
</p>
|
6
|
-
<p>
|
7
|
-
If there are expedited items that haven't yet started then they're at the bottom of the table. By the
|
8
|
-
very definition of expedited, if we haven't started them already, we'd better get on that.
|
9
|
-
</p>
|
10
|
-
<p>
|
11
|
-
<% if age_cutoff > 0 %>
|
12
|
-
Items less than <%= label_days age_cutoff %> old have been excluded from this chart to provide more
|
13
|
-
focus on the older items. The exception are items that are either expedited or blocked - these are
|
14
|
-
shown no matter how old they are.
|
15
|
-
<% end %>
|
16
|
-
</p>
|
17
|
-
|
18
1
|
<table class='standard'>
|
19
2
|
<thead>
|
20
3
|
<tr>
|
@@ -31,9 +14,18 @@
|
|
31
14
|
</tr>
|
32
15
|
</thead>
|
33
16
|
<tbody>
|
17
|
+
<% show_age_cutoff_bar_at = age_cutoff %>
|
34
18
|
<% aging_issues.each do |issue| %>
|
19
|
+
<% issue_age = issue.board.cycletime.age(issue, today: @today) %>
|
20
|
+
<% if show_age_cutoff_bar_at && issue_age&.<(show_age_cutoff_bar_at) %>
|
21
|
+
<tr><th colspan=100 style="text-align: left; padding-top: 1em;">
|
22
|
+
The items below are less than <%= label_days age_cutoff %> old, and are only on this report
|
23
|
+
because they're either expedited or blocked.
|
24
|
+
</th></tr>
|
25
|
+
<% show_age_cutoff_bar_at = nil %>
|
26
|
+
<% end %>
|
35
27
|
<tr>
|
36
|
-
<td style="text-align: right;"><%=
|
28
|
+
<td style="text-align: right;"><%= issue_age || 'Not started' %></td>
|
37
29
|
<td><%= expedited_text(issue) %></td>
|
38
30
|
<td><%= blocked_text(issue) %></td>
|
39
31
|
<td>
|
@@ -61,6 +61,14 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
61
61
|
},
|
62
62
|
<% end %>
|
63
63
|
}
|
64
|
+
},
|
65
|
+
legend: {
|
66
|
+
labels: {
|
67
|
+
filter: function(item, chart) {
|
68
|
+
// Logic to remove a particular legend item goes here
|
69
|
+
return !item.text.includes('Trendline');
|
70
|
+
}
|
71
|
+
}
|
64
72
|
}
|
65
73
|
}
|
66
74
|
}
|
@@ -26,6 +26,7 @@
|
|
26
26
|
--throughput_chart_total_line_color: gray;
|
27
27
|
|
28
28
|
--aging-work-in-progress-chart-shading-color: lightgray;
|
29
|
+
--aging-work-in-progress-by-age-trend-line-color: gray;
|
29
30
|
|
30
31
|
--hierarchy-table-inactive-item-text-color: gray;
|
31
32
|
|
@@ -45,12 +46,18 @@
|
|
45
46
|
--estimate-accuracy-chart-active-border-color: red;
|
46
47
|
|
47
48
|
--expedited-chart-no-longer-expedited: gray;
|
49
|
+
--expedited-chart-dot-issue-started-color: orange;
|
50
|
+
--expedited-chart-dot-issue-stopped-color: green;
|
51
|
+
--expedited-chart-dot-expedite-started-color: red;
|
52
|
+
--expedited-chart-dot-expedite-stopped-color: green;
|
48
53
|
|
49
54
|
--sprint-burndown-sprint-color-1: blue;
|
50
55
|
--sprint-burndown-sprint-color-2: orange;
|
51
56
|
--sprint-burndown-sprint-color-3: green;
|
52
57
|
--sprint-burndown-sprint-color-4: red;
|
53
58
|
--sprint-burndown-sprint-color-5: brown;
|
59
|
+
|
60
|
+
|
54
61
|
}
|
55
62
|
|
56
63
|
body {
|
@@ -100,6 +107,11 @@ table.standard {
|
|
100
107
|
background-color: white;
|
101
108
|
}
|
102
109
|
|
110
|
+
div.p {
|
111
|
+
margin: 0.5em 0;
|
112
|
+
padding: 0;
|
113
|
+
}
|
114
|
+
|
103
115
|
div.color_block {
|
104
116
|
display: inline-block;
|
105
117
|
width: 0.9em;
|
@@ -132,7 +144,7 @@ div.color_block {
|
|
132
144
|
|
133
145
|
--hierarchy-table-inactive-item-text-color: #939393;
|
134
146
|
|
135
|
-
|
147
|
+
--wip-chart-completed-color: #03cb03;
|
136
148
|
--wip-chart-completed-but-not-started-color: #99FF99;
|
137
149
|
--wip-chart-duration-less-than-day-color: #d2d988;
|
138
150
|
--wip-chart-duration-week-or-less-color: #dfcd00;
|
@@ -26,6 +26,11 @@ class HtmlReportConfig
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
+
# Mostly this is its own method so it can be called from the config
|
30
|
+
def included_projects
|
31
|
+
@file_config.project_config.aggregate_config.included_projects
|
32
|
+
end
|
33
|
+
|
29
34
|
def run
|
30
35
|
instance_eval(&@block)
|
31
36
|
|
@@ -102,6 +107,10 @@ class HtmlReportConfig
|
|
102
107
|
execute_chart DailyWipByBlockedStalledChart.new
|
103
108
|
end
|
104
109
|
|
110
|
+
def daily_wip_by_parent_chart &block
|
111
|
+
execute_chart DailyWipByParentChart.new block
|
112
|
+
end
|
113
|
+
|
105
114
|
def throughput_chart &block
|
106
115
|
execute_chart ThroughputChart.new(block)
|
107
116
|
end
|
@@ -119,7 +128,8 @@ class HtmlReportConfig
|
|
119
128
|
end
|
120
129
|
|
121
130
|
def html string, type: :body
|
122
|
-
|
131
|
+
allowed_types = %i[body header]
|
132
|
+
raise "Unexpected type: #{type} allowed_types: #{allowed_types.inspect}" unless allowed_types.include? type
|
123
133
|
|
124
134
|
@sections << [string, type]
|
125
135
|
end
|
data/lib/jirametrics/issue.rb
CHANGED
@@ -284,8 +284,9 @@ class Issue
|
|
284
284
|
blocking_status = change.value
|
285
285
|
end
|
286
286
|
elsif change.link?
|
287
|
+
# Example: "This issue is satisfied by ANON-30465"
|
287
288
|
unless /^This issue (?<link_text>.+) (?<issue_key>.+)$/ =~ (change.value || change.old_value)
|
288
|
-
puts "Can't parse link text: #{change.value || change.old_value}"
|
289
|
+
puts "Issue(#{key}) Can't parse link text: #{change.value || change.old_value}"
|
289
290
|
next
|
290
291
|
end
|
291
292
|
|
@@ -13,7 +13,10 @@ class JiraGateway
|
|
13
13
|
|
14
14
|
def call_url relative_url:
|
15
15
|
command = make_curl_command url: "#{@jira_url}#{relative_url}"
|
16
|
-
|
16
|
+
result = call_command command
|
17
|
+
JSON.parse result
|
18
|
+
rescue => e # rubocop:disable Style/RescueStandardError
|
19
|
+
puts "Error #{e.inspect} when parsing result: #{result.inspect}"
|
17
20
|
end
|
18
21
|
|
19
22
|
def call_command command
|
@@ -8,7 +8,7 @@ class ProjectConfig
|
|
8
8
|
|
9
9
|
attr_reader :target_path, :jira_config, :all_boards, :possible_statuses,
|
10
10
|
:download_config, :file_configs, :exporter, :data_version, :name, :board_configs,
|
11
|
-
:settings
|
11
|
+
:settings, :aggregate_config
|
12
12
|
attr_accessor :time_range, :jira_url, :id
|
13
13
|
|
14
14
|
def initialize exporter:, jira_config:, block:, target_path: '.', name: '', id: nil
|
@@ -115,7 +115,7 @@ class ProjectConfig
|
|
115
115
|
board_id = $1.to_i
|
116
116
|
load_board board_id: board_id, filename: "#{@target_path}#{file}"
|
117
117
|
end
|
118
|
-
raise "No boards found in #{@target_path.inspect}" if @all_boards.empty?
|
118
|
+
raise "No boards found for #{@file_prefix} in #{@target_path.inspect}" if @all_boards.empty?
|
119
119
|
end
|
120
120
|
|
121
121
|
def load_board board_id:, filename:
|
@@ -22,7 +22,13 @@ class SprintBurndown < ChartBase
|
|
22
22
|
|
23
23
|
@summary_stats = {}
|
24
24
|
header_text 'Sprint burndown'
|
25
|
-
description_text
|
25
|
+
description_text <<-TEXT
|
26
|
+
<div class="p">
|
27
|
+
Burndowns for all sprints in this time period. The different colours are only to
|
28
|
+
differentiate one sprint from another as they may overlap time periods.
|
29
|
+
</div>
|
30
|
+
#{describe_non_working_days}
|
31
|
+
TEXT
|
26
32
|
end
|
27
33
|
|
28
34
|
def options= arg
|
@@ -55,7 +61,7 @@ class SprintBurndown < ChartBase
|
|
55
61
|
end
|
56
62
|
|
57
63
|
result = +''
|
58
|
-
result <<
|
64
|
+
result << render_top_text(binding)
|
59
65
|
|
60
66
|
possible_colours = (1..5).collect { |i| CssVariable["--sprint-burndown-sprint-color-#{i}"] }
|
61
67
|
charts_to_generate = []
|
@@ -6,16 +6,20 @@ class StoryPointAccuracyChart < ChartBase
|
|
6
6
|
|
7
7
|
header_text 'Estimate Accuracy'
|
8
8
|
description_text <<-HTML
|
9
|
-
<p>
|
9
|
+
<div class="p">
|
10
10
|
This chart graphs estimates against actual recorded cycle times. Since
|
11
11
|
estimates can change over time, we're graphing the estimate at the time that the story started.
|
12
|
-
</
|
13
|
-
<p>
|
14
|
-
The completed dots indicate
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
12
|
+
</div>
|
13
|
+
<div class="p">
|
14
|
+
The #{color_block '--estimate-accuracy-chart-completed-fill-color'} completed dots indicate
|
15
|
+
cycletimes.
|
16
|
+
<% if @has_aging_data %>
|
17
|
+
The #{color_block '--estimate-accuracy-chart-active-fill-color'} aging dots
|
18
|
+
(click on the legend to turn them on) show the current
|
19
|
+
age of items, which will give you a hint as to where they might end up. If they're already
|
20
|
+
far to the right then you know you have a problem.
|
21
|
+
<% end %>
|
22
|
+
</div>
|
19
23
|
HTML
|
20
24
|
|
21
25
|
@y_axis_label = 'Story Point Estimates'
|
@@ -56,6 +60,8 @@ class StoryPointAccuracyChart < ChartBase
|
|
56
60
|
(hash[key] ||= []) << issue
|
57
61
|
end
|
58
62
|
|
63
|
+
@has_aging_data = !aging_hash.empty?
|
64
|
+
|
59
65
|
[
|
60
66
|
[completed_hash, 'Completed', 'completed', false],
|
61
67
|
[aging_hash, 'Still in progress', 'active', true]
|
@@ -9,7 +9,12 @@ class ThroughputChart < ChartBase
|
|
9
9
|
super()
|
10
10
|
|
11
11
|
header_text 'Throughput Chart'
|
12
|
-
description_text
|
12
|
+
description_text <<-TEXT
|
13
|
+
<div class="p">
|
14
|
+
This chart shows how many items we completed per week
|
15
|
+
</div>
|
16
|
+
#{describe_non_working_days}
|
17
|
+
TEXT
|
13
18
|
|
14
19
|
init_configuration_block(block) do
|
15
20
|
grouping_rules do |issue, rule|
|
data/lib/jirametrics.rb
CHANGED
@@ -66,6 +66,7 @@ class JiraMetrics < Thor
|
|
66
66
|
require 'jirametrics/sprint'
|
67
67
|
require 'jirametrics/issue'
|
68
68
|
require 'jirametrics/daily_wip_by_age_chart'
|
69
|
+
require 'jirametrics/daily_wip_by_parent_chart'
|
69
70
|
require 'jirametrics/aging_work_in_progress_chart'
|
70
71
|
require 'jirametrics/cycletime_scatterplot'
|
71
72
|
require 'jirametrics/sprint_issue_change_data'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jirametrics
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Bowler
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-05-
|
11
|
+
date: 2024-05-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: random-word
|
@@ -80,6 +80,7 @@ files:
|
|
80
80
|
- lib/jirametrics/cycletime_scatterplot.rb
|
81
81
|
- lib/jirametrics/daily_wip_by_age_chart.rb
|
82
82
|
- lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb
|
83
|
+
- lib/jirametrics/daily_wip_by_parent_chart.rb
|
83
84
|
- lib/jirametrics/daily_wip_chart.rb
|
84
85
|
- lib/jirametrics/data_quality_report.rb
|
85
86
|
- lib/jirametrics/dependency_chart.rb
|