jirametrics 2.7 → 2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +4 -4
  3. data/lib/jirametrics/aging_work_bar_chart.rb +7 -5
  4. data/lib/jirametrics/aging_work_in_progress_chart.rb +105 -41
  5. data/lib/jirametrics/aging_work_table.rb +50 -2
  6. data/lib/jirametrics/board.rb +33 -5
  7. data/lib/jirametrics/board_config.rb +6 -2
  8. data/lib/jirametrics/board_movement_calculator.rb +147 -0
  9. data/lib/jirametrics/change_item.rb +19 -6
  10. data/lib/jirametrics/chart_base.rb +59 -21
  11. data/lib/jirametrics/css_variable.rb +1 -1
  12. data/lib/jirametrics/cycletime_config.rb +37 -5
  13. data/lib/jirametrics/cycletime_histogram.rb +67 -2
  14. data/lib/jirametrics/data_quality_report.rb +174 -35
  15. data/lib/jirametrics/download_config.rb +2 -2
  16. data/lib/jirametrics/downloader.rb +44 -25
  17. data/lib/jirametrics/examples/aggregated_project.rb +2 -5
  18. data/lib/jirametrics/examples/standard_project.rb +4 -6
  19. data/lib/jirametrics/expedited_chart.rb +7 -7
  20. data/lib/jirametrics/exporter.rb +10 -20
  21. data/lib/jirametrics/file_config.rb +23 -6
  22. data/lib/jirametrics/file_system.rb +39 -4
  23. data/lib/jirametrics/flow_efficiency_scatterplot.rb +2 -4
  24. data/lib/jirametrics/groupable_issue_chart.rb +1 -3
  25. data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -12
  26. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +22 -5
  27. data/lib/jirametrics/html/aging_work_table.erb +6 -4
  28. data/lib/jirametrics/html/cycletime_histogram.erb +74 -0
  29. data/lib/jirametrics/html/cycletime_scatterplot.erb +1 -10
  30. data/lib/jirametrics/html/daily_wip_chart.erb +1 -10
  31. data/lib/jirametrics/html/expedited_chart.erb +1 -10
  32. data/lib/jirametrics/html/hierarchy_table.erb +1 -1
  33. data/lib/jirametrics/html/index.css +28 -5
  34. data/lib/jirametrics/html/index.erb +8 -4
  35. data/lib/jirametrics/html/sprint_burndown.erb +1 -10
  36. data/lib/jirametrics/html/throughput_chart.erb +1 -10
  37. data/lib/jirametrics/html_report_config.rb +32 -23
  38. data/lib/jirametrics/issue.rb +104 -44
  39. data/lib/jirametrics/jira_gateway.rb +16 -3
  40. data/lib/jirametrics/project_config.rb +223 -120
  41. data/lib/jirametrics/sprint_burndown.rb +1 -1
  42. data/lib/jirametrics/status.rb +81 -26
  43. data/lib/jirametrics/status_collection.rb +74 -40
  44. data/lib/jirametrics/throughput_chart.rb +1 -1
  45. data/lib/jirametrics/value_equality.rb +2 -2
  46. data/lib/jirametrics.rb +7 -1
  47. metadata +8 -13
  48. data/lib/jirametrics/discard_changes_before.rb +0 -37
  49. data/lib/jirametrics/html/data_quality_report.erb +0 -138
@@ -1,6 +1,57 @@
1
1
  <div class="chart">
2
2
  <canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
3
3
  </div>
4
+ <%
5
+ if show_stats
6
+ link_id = next_id
7
+ issues_id = next_id
8
+ %>
9
+ [<a id='<%= link_id %>' href="#" onclick='expand_collapse("<%= link_id %>", "<%= issues_id %>"); return false;'>Show details</a>]
10
+ <div id="<%= issues_id %>" style="display: none;">
11
+ <div>
12
+ <table class="standard">
13
+ <tr>
14
+ <th>Issue Type</th>
15
+ <th>Min</th>
16
+ <th>Max</th>
17
+ <th>Avg</th>
18
+ <th>Mode</th>
19
+ <% percentiles.each do |p| %>
20
+ <th><%= p %>th</th>
21
+ <% end %>
22
+ </tr>
23
+ <% the_stats.each do |k, v| %>
24
+ <tr>
25
+ <td><%= k %></td>
26
+ <td style="text-align: right;"><%= v[:min] %></td>
27
+ <td style="text-align: right;"><%= v[:max] %></td>
28
+ <td style="text-align: right;"><%= sprintf('%.2f', v[:average]) %></td>
29
+ <td><%= v[:mode].join(', ') %></td>
30
+ <% percentiles.each do |p| %>
31
+ <td style="text-align: right;"><%= v[:percentiles][p] %></td>
32
+ <% end %>
33
+ </tr>
34
+ <% end %>
35
+ </table>
36
+ </div>
37
+ <div>
38
+ <p>These statistics help understand the <i>"shape"</i> of the cycletime histogram distribution, to help us with predictions.</p>
39
+ <ul>
40
+ <li><b>Min & Max:</b> the observed spread for the data set. Useful to judge how wide the variation is. </li>
41
+ <li><b>Average:</b> the arithmetic mean of the data set. Useful as a <i>"typical representative"</i> of the complete set.</li>
42
+ <li><b>Mode:</b> the most repeated value(s) in the data set. This is the value we're most likely to remember. </li>
43
+ <li><b>Percentiles:</b> they partition the data set. If X is the Nth percentile, it means that N% of cycletime values are X or less. Typical percentiles of interest are:</li>
44
+ <ul>
45
+ <li><b>50%</b>: also known as the <b>Median</b>. Useful to establish short feedback loops, to monitor that it's not drifting to the right.</li>
46
+ <li><b>85%</b>: useful to establish service level expectations, accounting for rare events..</li>
47
+ <li><b>98% (or higher)</b>: useful to gauge worst case expectations..</li>
48
+ </ul>
49
+ </ul>
50
+ </div>
51
+ </div>
52
+ <%
53
+ end
54
+ %>
4
55
  <script>
5
56
  new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
6
57
  {
@@ -21,6 +72,8 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
21
72
  grid: {
22
73
  color: <%= CssVariable['--grid-line-color'].to_json %>
23
74
  },
75
+ min: 0,
76
+ offset: false, // Gets rid of the ugly padding on left.
24
77
  },
25
78
  y: {
26
79
  stacked: true,
@@ -34,6 +87,27 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
34
87
  }
35
88
  },
36
89
  plugins: {
90
+ annotation: {
91
+ annotations: {
92
+ <%
93
+ results = the_stats[:all][:percentiles]
94
+ results.each do |percentile, value|
95
+ %>
96
+ percentile<%= percentile.to_s %>: {
97
+ type: 'line',
98
+ scaleID: 'x',
99
+ value: <%= value %>,
100
+ borderWidth: 1,
101
+ drawTime: 'beforeDatasetsDraw',
102
+ label: {
103
+ enabled: true,
104
+ content: '<%= "#{percentile}%" %>',
105
+ position: 'start',
106
+ }
107
+ },
108
+ <% end %>
109
+ },
110
+ },
37
111
  tooltip: {
38
112
  callbacks: {
39
113
  label: function(context) {
@@ -53,16 +53,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
53
53
  autocolors: false,
54
54
  annotation: {
55
55
  annotations: {
56
- <% holidays.each_with_index do |range, index| %>
57
- holiday<%= index %>: {
58
- drawTime: 'beforeDraw',
59
- type: 'box',
60
- xMin: '<%= range.begin %>T00:00:00',
61
- xMax: '<%= range.end %>T23:59:59',
62
- backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
63
- borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
64
- },
65
- <% end %>
56
+ <%= working_days_annotation %>
66
57
 
67
58
  <% @percentage_lines.each_with_index do |args, index| %>
68
59
  <% percent, color = args %>
@@ -50,16 +50,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
50
50
  },
51
51
  annotation: {
52
52
  annotations: {
53
- <% holidays.each_with_index do |range, index| %>
54
- holiday<%= index %>: {
55
- drawTime: 'beforeDraw',
56
- type: 'box',
57
- xMin: '<%= range.begin %>T00:00:00',
58
- xMax: '<%= range.end %>T23:59:59',
59
- backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
60
- borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
61
- },
62
- <% end %>
53
+ <%= working_days_annotation %>
63
54
  }
64
55
  },
65
56
  legend: {
@@ -55,16 +55,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
55
55
  autocolors: false,
56
56
  annotation: {
57
57
  annotations: {
58
- <% holidays.each_with_index do |range, index| %>
59
- holiday<%= index %>: {
60
- drawTime: 'beforeDraw',
61
- type: 'box',
62
- xMin: '<%= range.begin %>T00:00:00',
63
- xMax: '<%= range.end %>T23:59:59',
64
- backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
65
- borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
66
- },
67
- <% end %>
58
+ <%= working_days_annotation %>
68
59
  }
69
60
  }
70
61
  }
@@ -22,7 +22,7 @@
22
22
  </span>
23
23
  </td>
24
24
  <td><span style="color: <%= color %>; font-style: italic;"><%= issue.summary[0..80] %></span></td>
25
- <td><%= format_status issue.status.name, board: issue.board %></td>
25
+ <td><%= format_status issue.status, board: issue.board %></td>
26
26
  </tr>
27
27
  <% end %>
28
28
  </tbody>
@@ -2,6 +2,7 @@
2
2
  --body-background: white;
3
3
  --default-text-color: black;
4
4
  --grid-line-color: lightgray;
5
+ --warning-banner: yellow;
5
6
 
6
7
  --cycletime-scatterplot-overall-trendline-color: gray;
7
8
 
@@ -19,6 +20,7 @@
19
20
  --status-category-todo-color: gray;
20
21
  --status-category-inprogress-color: #2663ff;
21
22
  --status-category-done-color: #00ff00;
23
+ --status-category-unknown-color: black;
22
24
 
23
25
  --aging-work-bar-chart-percentage-line-color: red;
24
26
  --aging-work-bar-chart-separator-color: white;
@@ -26,8 +28,16 @@
26
28
  --throughput_chart_total_line_color: gray;
27
29
 
28
30
  --aging-work-in-progress-chart-shading-color: lightgray;
31
+ --aging-work-in-progress-chart-shading-50-color: #2E8BC0; // green;
32
+ --aging-work-in-progress-chart-shading-85-color: #ADD8E6; // yellow;
33
+ --aging-work-in-progress-chart-shading-98-color: #FF8A8A; // orange;
34
+ --aging-work-in-progress-chart-shading-100-color: #FF2E2E; // red;
35
+
29
36
  --aging-work-in-progress-by-age-trend-line-color: gray;
30
37
 
38
+ --aging-work-table-date-in-jeopardy: yellow;
39
+ --aging-work-table-date-overdue: red;
40
+
31
41
  --hierarchy-table-inactive-item-text-color: gray;
32
42
 
33
43
  --wip-chart-completed-color: #00ff00;
@@ -99,9 +109,6 @@ table.standard {
99
109
  background-color: #eee;
100
110
  }
101
111
  }
102
- .quality_note_bullet {
103
- color: red;
104
- }
105
112
 
106
113
  .chart {
107
114
  background-color: white;
@@ -119,8 +126,26 @@ div.color_block {
119
126
  border: 1px solid black;
120
127
  }
121
128
 
129
+ ul.quality_report {
130
+ list-style-type: '⮕';
131
+ ::marker {
132
+ color: red;
133
+ }
134
+ li {
135
+ padding: 0.2em;
136
+ }
137
+ }
138
+
139
+ #footer {
140
+ text-align: center;
141
+ margin-top: 1em;
142
+ border-top: 1px solid gray;
143
+ }
144
+
122
145
  @media screen and (prefers-color-scheme: dark) {
123
146
  :root {
147
+ --warning-banner: #9F2B00;
148
+
124
149
  --non-working-days-color: #2f2f2f;
125
150
  --type-story-color: #6fb86f;
126
151
  --type-task-color: #0021b3;
@@ -136,8 +161,6 @@ div.color_block {
136
161
  --dead-color: black;
137
162
  --wip-chart-active-color: #2551c1;
138
163
 
139
- --aging-work-in-progress-chart-shading-color: #b4b4b4;
140
-
141
164
  --status-category-inprogress-color: #1c49bb;
142
165
 
143
166
  --cycletime-scatterplot-overall-trendline-color: gray;
@@ -23,16 +23,20 @@
23
23
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
24
24
  location.reload()
25
25
  })
26
-
27
26
  </script>
28
27
  <style>
29
28
  <%= css %>
30
29
  </style>
30
+ <script type="text/javascript">
31
+ Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--default-text-color');
32
+ </script>
31
33
  </head>
32
34
  <body>
33
- <div>
34
- Page generated <%= (timezone_offset.nil? ? DateTime.now : DateTime.now.new_offset(timezone_offset)).strftime('%Y-%b-%d at %I:%M:%S%P') %>
35
- </div>
35
+ <noscript>
36
+ <div style="padding: 1em; background: gray; color: white; font-size: 2em;">
37
+ Javascript is currently disabled and that means that almost all of the charts in this report won't render. If you'd loaded this from a folder on SharePoint then save it locally and load it again.
38
+ </div>
39
+ </noscript>
36
40
  <%= "\n" + @sections.collect { |text, type| text if type == :header }.compact.join("\n\n") %>
37
41
  <%= "\n" + @sections.collect { |text, type| text if type == :body }.compact.join("\n\n") %>
38
42
  </body>
@@ -56,16 +56,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
56
56
  },
57
57
  annotation: {
58
58
  annotations: {
59
- <% holidays().each_with_index do |range, index| %>
60
- holiday<%= index %>: {
61
- drawTime: 'beforeDraw',
62
- type: 'box',
63
- xMin: '<%= range.begin %>T00:00:00',
64
- xMax: '<%= range.end %>T23:59:59',
65
- backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
66
- borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
67
- },
68
- <% end %>
59
+ <%= working_days_annotation %>
69
60
  }
70
61
  }
71
62
  }
@@ -52,16 +52,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
52
52
  },
53
53
  annotation: {
54
54
  annotations: {
55
- <% holidays.each_with_index do |range, index| %>
56
- holiday<%= index %>: {
57
- drawTime: 'beforeDraw',
58
- type: 'box',
59
- xMin: '<%= range.begin %>T00:00:00',
60
- xMax: '<%= range.end %>T23:59:59',
61
- backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
62
- borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
63
- },
64
- <% end %>
55
+ <%= working_days_annotation %>
65
56
  }
66
57
  }
67
58
  }
@@ -5,16 +5,15 @@ require 'jirametrics/self_or_issue_dispatcher'
5
5
 
6
6
  class HtmlReportConfig
7
7
  include SelfOrIssueDispatcher
8
- include DiscardChangesBefore
9
8
 
10
- attr_reader :file_config, :sections
9
+ attr_reader :file_config, :sections, :charts
11
10
 
12
11
  def self.define_chart name:, classname:, deprecated_warning: nil, deprecated_date: nil
13
12
  lines = []
14
13
  lines << "def #{name} &block"
15
14
  lines << ' block = ->(_) {} unless block'
16
15
  if deprecated_warning
17
- lines << " deprecated date: #{deprecated_date.inspect}, message: #{deprecated_warning.inspect}"
16
+ lines << " file_system.deprecated date: #{deprecated_date.inspect}, message: #{deprecated_warning.inspect}"
18
17
  end
19
18
  lines << " execute_chart #{classname}.new(block)"
20
19
  lines << 'end'
@@ -43,14 +42,15 @@ class HtmlReportConfig
43
42
  def initialize file_config:, block:
44
43
  @file_config = file_config
45
44
  @block = block
46
- @sections = []
45
+ @sections = [] # Where we store the chunks of text that will be assembled into the HTML
46
+ @charts = [] # Where we store all the charts we executed so we can assert against them.
47
47
  end
48
48
 
49
49
  def cycletime label = nil, &block
50
50
  @file_config.project_config.all_boards.each_value do |board|
51
51
  raise 'Multiple cycletimes not supported' if board.cycletime
52
52
 
53
- board.cycletime = CycleTimeConfig.new(parent_config: self, label: label, block: block)
53
+ board.cycletime = CycleTimeConfig.new(parent_config: self, label: label, block: block, file_system: file_system)
54
54
  end
55
55
  end
56
56
 
@@ -64,9 +64,11 @@ class HtmlReportConfig
64
64
 
65
65
  # The quality report has to be generated last because otherwise cycletime won't have been
66
66
  # set. Then we have to rotate it to the first position so it's at the top of the report.
67
- execute_chart DataQualityReport.new(@original_issue_times || {})
67
+ execute_chart DataQualityReport.new(file_config.project_config.discarded_changes_data)
68
68
  @sections.rotate!(-1)
69
69
 
70
+ html create_footer
71
+
70
72
  html_directory = "#{Pathname.new(File.realpath(__FILE__)).dirname}/html"
71
73
  css = load_css html_directory: html_directory
72
74
  erb = ERB.new file_system.load(File.join(html_directory, 'index.erb'))
@@ -99,9 +101,8 @@ class HtmlReportConfig
99
101
  base_css
100
102
  end
101
103
 
102
- def board_id id = nil
103
- @board_id = id unless id.nil?
104
- @board_id
104
+ def board_id id
105
+ @board_id = id
105
106
  end
106
107
 
107
108
  def timezone_offset
@@ -141,19 +142,6 @@ class HtmlReportConfig
141
142
  end
142
143
  end
143
144
 
144
- def discard_changes_before_hook issues_cutoff_times
145
- # raise 'Cycletime must be defined before using discard_changes_before' unless @cycletime
146
-
147
- @original_issue_times = {}
148
- issues_cutoff_times.each do |issue, cutoff_time|
149
- started = issue.board.cycletime.started_stopped_times(issue).first
150
- if started && started <= cutoff_time
151
- # We only need to log this if data was discarded
152
- @original_issue_times[issue] = { cutoff_time: cutoff_time, started_time: started }
153
- end
154
- end
155
- end
156
-
157
145
  def dependency_chart &block
158
146
  execute_chart DependencyChart.new block
159
147
  end
@@ -173,7 +161,7 @@ class HtmlReportConfig
173
161
  chart.settings = settings
174
162
 
175
163
  chart.all_boards = project_config.all_boards
176
- chart.board_id = find_board_id if chart.respond_to? :board_id=
164
+ chart.board_id = find_board_id
177
165
  chart.holiday_dates = project_config.exporter.holiday_dates
178
166
 
179
167
  time_range = @file_config.project_config.time_range
@@ -182,6 +170,7 @@ class HtmlReportConfig
182
170
 
183
171
  after_init_block&.call chart
184
172
 
173
+ @charts << chart
185
174
  html chart.run
186
175
  end
187
176
 
@@ -202,4 +191,24 @@ class HtmlReportConfig
202
191
  def boards
203
192
  @file_config.project_config.board_configs.collect(&:id).collect { |id| find_board id }
204
193
  end
194
+
195
+ def create_footer now: DateTime.now
196
+ now = now.new_offset(timezone_offset)
197
+ version = Gem.loaded_specs['jirametrics']&.version || 'Next'
198
+
199
+ <<~HTML
200
+ <section id="footer">
201
+ Report generated on <b>#{now.strftime('%Y-%b-%d')}</b> at <b>#{now.strftime('%I:%M:%S%P %Z')}</b>
202
+ with <a href="https://jirametrics.org">JiraMetrics</a> <b>v#{version}</b>
203
+ </section>
204
+ HTML
205
+ end
206
+
207
+ def discard_changes_before status_becomes: nil, &block
208
+ file_system.deprecated(
209
+ date: '2025-01-09',
210
+ message: 'discard_changes_before is now only supported at the project level'
211
+ )
212
+ file_config.project_config.discard_changes_before status_becomes: status_becomes, &block
213
+ end
205
214
  end