jirametrics 2.8 → 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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +1 -1
  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 +32 -4
  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 +8 -1
  10. data/lib/jirametrics/chart_base.rb +40 -10
  11. data/lib/jirametrics/css_variable.rb +1 -1
  12. data/lib/jirametrics/cycletime_config.rb +10 -4
  13. data/lib/jirametrics/cycletime_histogram.rb +65 -2
  14. data/lib/jirametrics/data_quality_report.rb +53 -34
  15. data/lib/jirametrics/downloader.rb +0 -14
  16. data/lib/jirametrics/examples/standard_project.rb +2 -2
  17. data/lib/jirametrics/exporter.rb +10 -20
  18. data/lib/jirametrics/file_config.rb +21 -4
  19. data/lib/jirametrics/file_system.rb +23 -4
  20. data/lib/jirametrics/flow_efficiency_scatterplot.rb +1 -1
  21. data/lib/jirametrics/groupable_issue_chart.rb +1 -3
  22. data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -12
  23. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +22 -5
  24. data/lib/jirametrics/html/aging_work_table.erb +6 -4
  25. data/lib/jirametrics/html/cycletime_histogram.erb +74 -0
  26. data/lib/jirametrics/html/cycletime_scatterplot.erb +1 -10
  27. data/lib/jirametrics/html/daily_wip_chart.erb +1 -10
  28. data/lib/jirametrics/html/expedited_chart.erb +1 -10
  29. data/lib/jirametrics/html/hierarchy_table.erb +1 -1
  30. data/lib/jirametrics/html/index.css +11 -2
  31. data/lib/jirametrics/html/index.erb +8 -1
  32. data/lib/jirametrics/html/sprint_burndown.erb +1 -10
  33. data/lib/jirametrics/html/throughput_chart.erb +1 -10
  34. data/lib/jirametrics/html_report_config.rb +18 -23
  35. data/lib/jirametrics/issue.rb +77 -32
  36. data/lib/jirametrics/jira_gateway.rb +16 -3
  37. data/lib/jirametrics/project_config.rb +102 -45
  38. data/lib/jirametrics/status.rb +26 -7
  39. data/lib/jirametrics/status_collection.rb +69 -68
  40. data/lib/jirametrics/value_equality.rb +2 -2
  41. data/lib/jirametrics.rb +0 -1
  42. metadata +5 -9
  43. data/lib/jirametrics/discard_changes_before.rb +0 -37
@@ -38,22 +38,13 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
38
38
  plugins: {
39
39
  annotation: {
40
40
  annotations: {
41
- <% holidays.each_with_index do |range, index| %>
42
- holiday<%= index %>: {
43
- drawTime: 'beforeDraw',
44
- type: 'box',
45
- xMin: '<%= range.begin %>T00:00:00',
46
- xMax: '<%= range.end %>T23:59:59',
47
- backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
48
- borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
49
- },
50
- <% end %>
41
+ <%= working_days_annotation %>
51
42
 
52
43
  <% if percentage_line_x %>
53
44
  line: {
54
45
  type: 'line',
55
- xMin: '<%= percentage_line_x %>',
56
- xMax: '<%= percentage_line_x %>',
46
+ scaleID: 'x',
47
+ value: '<%= percentage_line_x %>',
57
48
  borderColor: <%= CssVariable.new('--aging-work-bar-chart-percentage-line-color').to_json %>,
58
49
  borderWidth: 1,
59
50
  drawTime: 'afterDraw'
@@ -6,7 +6,7 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
6
6
  {
7
7
  type: 'bar',
8
8
  data: {
9
- labels: [<%= column_headings.collect(&:inspect).join(',') %>],
9
+ labels: [<%= @board_columns.collect { |c| c.name.inspect }.join(',') %>],
10
10
  datasets: <%= JSON.generate(data_sets) %>
11
11
  },
12
12
  options: {
@@ -22,8 +22,10 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
22
22
  labelString: 'Date Completed'
23
23
  },
24
24
  grid: {
25
- color: <%= CssVariable['--grid-line-color'].to_json %>
25
+ color: <%= CssVariable['--grid-line-color'].to_json %>,
26
+ z: 1 // draw the grid lines on top of the bars
26
27
  },
28
+ stacked: true
27
29
  },
28
30
  y: {
29
31
  scaleLabel: {
@@ -35,8 +37,11 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
35
37
  text: 'Age in days'
36
38
  },
37
39
  grid: {
38
- color: <%= CssVariable['--grid-line-color'].to_json %>
40
+ color: <%= CssVariable['--grid-line-color'].to_json %>,
41
+ z: 1 // draw the grid lines on top of the bars
39
42
  },
43
+ stacked: true,
44
+ max: <%= (@max_age * 1.1).to_i %>
40
45
  }
41
46
  },
42
47
  plugins: {
@@ -44,14 +49,26 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
44
49
  callbacks: {
45
50
  label: function(context) {
46
51
  if( typeof(context.dataset.data[context.dataIndex]) == "number" ) {
47
- return "85% of the issues, leave this column in "+context.dataset.data[context.dataIndex]+" days";
52
+ let full_data = <%= @bar_data.inspect %>;
53
+ let columnIndex = context.dataIndex;
54
+ let rowIndex = context.datasetIndex - <%= @row_index_offset %>;
55
+ return context.dataset.label + " of completed work items left this column in " +full_data[rowIndex][columnIndex] + " days or less";
48
56
  }
49
57
  else {
50
- return context.dataset.data[context.dataIndex].title
58
+ return context.dataset.data[context.dataIndex].title;
51
59
  }
52
60
  }
53
61
  }
62
+ },
63
+ legend: {
64
+ labels: {
65
+ filter: function(item, chart) {
66
+ // Logic to remove a particular legend item goes here
67
+ return !item.text.includes('%');
68
+ }
69
+ }
54
70
  }
71
+
55
72
  }
56
73
  }
57
74
  });
@@ -1,11 +1,12 @@
1
1
  <table class='standard'>
2
2
  <thead>
3
3
  <tr>
4
- <th>Age (days)</th>
5
- <th>E</th>
6
- <th>B</th>
4
+ <th title="Age in days">Age</th>
5
+ <th title="Expedited">E</th>
6
+ <th title="Blocked / Stalled">B/S</th>
7
7
  <th>Issue</th>
8
8
  <th>Status</th>
9
+ <th>Forecast</th>
9
10
  <th>Fix versions</th>
10
11
  <% if any_scrum_boards %>
11
12
  <th>Sprints</th>
@@ -40,7 +41,8 @@
40
41
  </div>
41
42
  <% end %>
42
43
  </td>
43
- <td><%= format_status issue.status.name, board: issue.board %></td>
44
+ <td><%= format_status issue.status, board: issue.board %></td>
45
+ <td><%= dates_text(issue) %></td>
44
46
  <td><%= fix_versions_text(issue) %></td>
45
47
  <% if any_scrum_boards %>
46
48
  <td><%= sprints_text(issue) %></td>
@@ -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
 
@@ -27,8 +28,16 @@
27
28
  --throughput_chart_total_line_color: gray;
28
29
 
29
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
+
30
36
  --aging-work-in-progress-by-age-trend-line-color: gray;
31
37
 
38
+ --aging-work-table-date-in-jeopardy: yellow;
39
+ --aging-work-table-date-overdue: red;
40
+
32
41
  --hierarchy-table-inactive-item-text-color: gray;
33
42
 
34
43
  --wip-chart-completed-color: #00ff00;
@@ -135,6 +144,8 @@ ul.quality_report {
135
144
 
136
145
  @media screen and (prefers-color-scheme: dark) {
137
146
  :root {
147
+ --warning-banner: #9F2B00;
148
+
138
149
  --non-working-days-color: #2f2f2f;
139
150
  --type-story-color: #6fb86f;
140
151
  --type-task-color: #0021b3;
@@ -150,8 +161,6 @@ ul.quality_report {
150
161
  --dead-color: black;
151
162
  --wip-chart-active-color: #2551c1;
152
163
 
153
- --aging-work-in-progress-chart-shading-color: #b4b4b4;
154
-
155
164
  --status-category-inprogress-color: #1c49bb;
156
165
 
157
166
  --cycletime-scatterplot-overall-trendline-color: gray;
@@ -23,13 +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>
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>
33
40
  <%= "\n" + @sections.collect { |text, type| text if type == :header }.compact.join("\n\n") %>
34
41
  <%= "\n" + @sections.collect { |text, type| text if type == :body }.compact.join("\n\n") %>
35
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,7 +64,7 @@ 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
70
  html create_footer
@@ -101,9 +101,8 @@ class HtmlReportConfig
101
101
  base_css
102
102
  end
103
103
 
104
- def board_id id = nil
105
- @board_id = id unless id.nil?
106
- @board_id
104
+ def board_id id
105
+ @board_id = id
107
106
  end
108
107
 
109
108
  def timezone_offset
@@ -143,19 +142,6 @@ class HtmlReportConfig
143
142
  end
144
143
  end
145
144
 
146
- def discard_changes_before_hook issues_cutoff_times
147
- # raise 'Cycletime must be defined before using discard_changes_before' unless @cycletime
148
-
149
- @original_issue_times = {}
150
- issues_cutoff_times.each do |issue, cutoff_time|
151
- started = issue.board.cycletime.started_stopped_times(issue).first
152
- if started && started <= cutoff_time
153
- # We only need to log this if data was discarded
154
- @original_issue_times[issue] = { cutoff_time: cutoff_time, started_time: started }
155
- end
156
- end
157
- end
158
-
159
145
  def dependency_chart &block
160
146
  execute_chart DependencyChart.new block
161
147
  end
@@ -175,7 +161,7 @@ class HtmlReportConfig
175
161
  chart.settings = settings
176
162
 
177
163
  chart.all_boards = project_config.all_boards
178
- chart.board_id = find_board_id if chart.respond_to? :board_id=
164
+ chart.board_id = find_board_id
179
165
  chart.holiday_dates = project_config.exporter.holiday_dates
180
166
 
181
167
  time_range = @file_config.project_config.time_range
@@ -184,6 +170,7 @@ class HtmlReportConfig
184
170
 
185
171
  after_init_block&.call chart
186
172
 
173
+ @charts << chart
187
174
  html chart.run
188
175
  end
189
176
 
@@ -216,4 +203,12 @@ class HtmlReportConfig
216
203
  </section>
217
204
  HTML
218
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
219
214
  end