jirametrics 2.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b424b3a1faea57826e0afa0d607b5edea1b824a14af63d2f4fc6b00e4f0c6190
4
- data.tar.gz: c4fa2e71b82a118140cbcfaeb8000133917189c5449d7aa03a93d601b2df410b
3
+ metadata.gz: 0f5551ca8d38a0b12579429080055e21bb161066e7bb2237ada5d47628a212de
4
+ data.tar.gz: 7bd667226812044854dde31dfac4f347380b077369ec711bec5d6cb98a0082e8
5
5
  SHA512:
6
- metadata.gz: b361d8f4ab3952de3fbc3b39f70704e6817a157d9166218ad436f80f13ca80d83b26a10a002c08528da298a9e8a961ed699dbb121fc6a7edda75572209c0501f
7
- data.tar.gz: 86ce2be29726bf2870d8a8bda0e6fd12184f09946e1cbb04fd66b8666b2ae878ec405e20f2b1658f42ccc6f6b783222853d95cb1603b34158a0ada3e4e37c897
6
+ metadata.gz: 397a3c4eb84497df209c702858a77def1d9606eecf84d08a8ab7b4f0a6decd968f77ed617f8189acb4916e372742bfc8352bb0fcd2b5ad74e04880c03680fece
7
+ data.tar.gz: 8aa2cbc8d7e60ed80749bf58c48d78689adcb65892ad133a378956b308fdb3d326a665eb3a884bd9d29783142639813f696c77f393697e285a0b7769fbe0ad18
@@ -3,7 +3,7 @@
3
3
  require 'date'
4
4
 
5
5
  class AggregateConfig
6
- attr_reader :project_config
6
+ attr_reader :project_config, :included_projects
7
7
 
8
8
  def initialize project_config:, block:
9
9
  @project_config = project_config
@@ -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
- <p>
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
- render(binding, __FILE__)
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
- # Render the file and then wrap it with standard headers and quality checks.
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
- </p>
19
- <div style="padding-bottom: 0.7em">
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
- <div>
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 green bar underneath, shows how many items completed on that day.
16
- </p>
17
- <p>
18
- "Completed without being started" reflects the fact that while we know that it completed
19
- that day, we were unable to determine when it had started. These items will show up in
20
- white at the top. Note that the this grouping is approximate because we don't know exactly when
21
- it started so we're guessing.
22
- </p>
27
+ to. The #{color_block '--wip-chart-completed-color'}
28
+ #{color_block '--wip-chart-completed-but-not-started-color'}
29
+ bars underneath, show how many items completed on that day.
30
+ </div>
31
+ <% if @has_completed_but_not_started %>
32
+ <div class="p">
33
+ #{color_block '--wip-chart-completed-but-not-started-color'} "Completed but not started"
34
+ reflects the fact that while we know that it completed that day, we were unable to determine when
35
+ it had started; it had moved directly from a To Do status to a Done status.
36
+ The #{color_block '--body-background'} shading at the top shows when they might
37
+ have been active. Note that the this grouping is approximate as we just don't know for sure.
38
+ </div>
39
+ <% end %>
40
+ #{describe_non_working_days}
41
+ <div class="p">
42
+ The #{color_block '--aging-work-in-progress-by-age-trend-line-color'} dashed line is a general trend line.
43
+ <% if @has_completed_but_not_started %>
44
+ Note that this trend line only includes items where we know both the start and end times of
45
+ the work so it may not be as accurate as we hope.
46
+ <% end %>
47
+ </div>
23
48
  HTML
24
49
  end
25
50
 
@@ -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
- instance_eval(&block)
25
- else
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 'grouping_rules must be set' if @group_by_block.nil?
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
- @board_id_to_filter_id[board_id] = json['filter']['id'].to_i
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
- daily_wip_chart do
41
- header_text 'Daily WIP by Parent'
42
- description_text <<-TEXT
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
- daily_wip_chart do
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
- </p>
35
- <p>
36
- The lines indicate time that this issue was expedited. When the line is red then the issue was
37
- expedited at that time. When it's gray then it wasn't. Orange dots indicate the date the work
38
- was started and green dots represent the completion date. Lastly, the vertical height of the
39
- lines/dots indicates how long it's been since this issue was created.
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 << 'orange'
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 << 'green'
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 << 'red'
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 << 'gray'
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 << 'blue' # It won't be visible so it doesn't matter
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;"><%= issue.board.cycletime.age(issue, today: @today) || 'Not started' %></td>
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
- --wip-chart-completed-color: #03cb03;
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
- raise "Unexpected type: #{type}" unless %i[body header].include? type
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
@@ -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
- JSON.parse call_command command
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 << '<h1>Sprint Burndowns</h1>'
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
- </p>
13
- <p>
14
- The completed dots indicate cycletimes. The aging dots (click on the legend to turn them on)
15
- show the current
16
- age of items, which will give you a hint as to where they might end up. If they're already
17
- far to the right then you know you have a problem.
18
- </p>
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 'This chart shows how many items we completed per unit of time'
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.1.1
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-08 00:00:00.000000000 Z
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