jirametrics 2.1.1 → 2.2.0
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 +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 +16 -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: 0f5551ca8d38a0b12579429080055e21bb161066e7bb2237ada5d47628a212de
|
4
|
+
data.tar.gz: 7bd667226812044854dde31dfac4f347380b077369ec711bec5d6cb98a0082e8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 397a3c4eb84497df209c702858a77def1d9606eecf84d08a8ab7b4f0a6decd968f77ed617f8189acb4916e372742bfc8352bb0fcd2b5ad74e04880c03680fece
|
7
|
+
data.tar.gz: 8aa2cbc8d7e60ed80749bf58c48d78689adcb65892ad133a378956b308fdb3d326a665eb3a884bd9d29783142639813f696c77f393697e285a0b7769fbe0ad18
|
@@ -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,16 @@ 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
|
+
end
|
46
|
+
|
47
|
+
# Render the file and then wrap it with standard headers and quality checks.
|
48
|
+
def wrap_and_render caller_binding, file
|
49
|
+
result = +''
|
50
|
+
result << render_top_text(caller_binding)
|
46
51
|
result << render(caller_binding, file)
|
47
52
|
result
|
48
53
|
end
|
@@ -238,4 +243,13 @@ class ChartBase
|
|
238
243
|
result << '></div>'
|
239
244
|
result
|
240
245
|
end
|
246
|
+
|
247
|
+
def describe_non_working_days
|
248
|
+
<<-TEXT
|
249
|
+
<div class='p'>
|
250
|
+
The #{color_block '--non-working-days-color'} vertical bars indicate non-working days; weekends
|
251
|
+
and any other holidays mentioned in the configuration.
|
252
|
+
</div>
|
253
|
+
TEXT
|
254
|
+
end
|
241
255
|
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.0
|
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
|