jirametrics 2.22 → 2.27

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +10 -2
  3. data/lib/jirametrics/aging_work_bar_chart.rb +20 -6
  4. data/lib/jirametrics/aging_work_table.rb +4 -5
  5. data/lib/jirametrics/anonymizer.rb +74 -1
  6. data/lib/jirametrics/atlassian_document_format.rb +93 -93
  7. data/lib/jirametrics/blocked_stalled_change.rb +5 -3
  8. data/lib/jirametrics/board.rb +20 -8
  9. data/lib/jirametrics/board_feature.rb +14 -0
  10. data/lib/jirametrics/board_movement_calculator.rb +2 -2
  11. data/lib/jirametrics/cfd_data_builder.rb +108 -0
  12. data/lib/jirametrics/change_item.rb +4 -3
  13. data/lib/jirametrics/chart_base.rb +94 -2
  14. data/lib/jirametrics/css_variable.rb +1 -1
  15. data/lib/jirametrics/cumulative_flow_diagram.rb +208 -0
  16. data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +1 -2
  17. data/lib/jirametrics/cycletime_histogram.rb +15 -103
  18. data/lib/jirametrics/cycletime_scatterplot.rb +13 -98
  19. data/lib/jirametrics/daily_view.rb +36 -12
  20. data/lib/jirametrics/daily_wip_by_age_chart.rb +1 -1
  21. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +1 -1
  22. data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -2
  23. data/lib/jirametrics/daily_wip_chart.rb +29 -7
  24. data/lib/jirametrics/data_quality_report.rb +38 -12
  25. data/lib/jirametrics/dependency_chart.rb +2 -2
  26. data/lib/jirametrics/download_config.rb +15 -0
  27. data/lib/jirametrics/downloader.rb +87 -5
  28. data/lib/jirametrics/downloader_for_cloud.rb +52 -10
  29. data/lib/jirametrics/downloader_for_data_center.rb +2 -1
  30. data/lib/jirametrics/estimate_accuracy_chart.rb +42 -4
  31. data/lib/jirametrics/examples/aggregated_project.rb +2 -2
  32. data/lib/jirametrics/examples/standard_project.rb +29 -19
  33. data/lib/jirametrics/expedited_chart.rb +3 -1
  34. data/lib/jirametrics/exporter.rb +3 -1
  35. data/lib/jirametrics/file_system.rb +35 -2
  36. data/lib/jirametrics/flow_efficiency_scatterplot.rb +5 -1
  37. data/lib/jirametrics/github_gateway.rb +115 -0
  38. data/lib/jirametrics/groupable_issue_chart.rb +4 -0
  39. data/lib/jirametrics/grouping_rules.rb +26 -4
  40. data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -4
  41. data/lib/jirametrics/html/aging_work_table.erb +3 -0
  42. data/lib/jirametrics/html/cumulative_flow_diagram.erb +503 -0
  43. data/lib/jirametrics/html/daily_wip_chart.erb +38 -5
  44. data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -12
  45. data/lib/jirametrics/html/expedited_chart.erb +3 -13
  46. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -8
  47. data/lib/jirametrics/html/index.css +117 -0
  48. data/lib/jirametrics/html/index.erb +6 -0
  49. data/lib/jirametrics/html/index.js +52 -2
  50. data/lib/jirametrics/html/sprint_burndown.erb +7 -13
  51. data/lib/jirametrics/html/throughput_chart.erb +40 -9
  52. data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +59 -59
  53. data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +11 -7
  54. data/lib/jirametrics/html_generator.rb +2 -1
  55. data/lib/jirametrics/html_report_config.rb +23 -16
  56. data/lib/jirametrics/issue.rb +101 -96
  57. data/lib/jirametrics/issue_printer.rb +97 -0
  58. data/lib/jirametrics/jira_gateway.rb +6 -3
  59. data/lib/jirametrics/mcp_server.rb +305 -0
  60. data/lib/jirametrics/project_config.rb +80 -7
  61. data/lib/jirametrics/pull_request.rb +30 -0
  62. data/lib/jirametrics/pull_request_cycle_time_histogram.rb +77 -0
  63. data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +88 -0
  64. data/lib/jirametrics/pull_request_review.rb +13 -0
  65. data/lib/jirametrics/raw_javascript.rb +4 -0
  66. data/lib/jirametrics/settings.json +3 -1
  67. data/lib/jirametrics/sprint_burndown.rb +3 -1
  68. data/lib/jirametrics/status.rb +1 -1
  69. data/lib/jirametrics/stitcher.rb +7 -1
  70. data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
  71. data/lib/jirametrics/throughput_chart.rb +73 -23
  72. data/lib/jirametrics/time_based_histogram.rb +139 -0
  73. data/lib/jirametrics/time_based_scatterplot.rb +107 -0
  74. data/lib/jirametrics.rb +28 -0
  75. metadata +47 -5
@@ -87,6 +87,9 @@
87
87
  body {
88
88
  background-color: var(--body-background);
89
89
  color: var(--default-text-color);
90
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
91
+ font-size: 14px;
92
+ line-height: 1.5;
90
93
  }
91
94
 
92
95
  dl, dd, dt {
@@ -214,6 +217,120 @@ div.child_issue {
214
217
  padding: 0.5em;
215
218
  }
216
219
 
220
+ /* Dark CSS variables — shared by the media query and the forced dark theme */
221
+ html[data-theme="dark"] {
222
+ --warning-banner: #9F2B00;
223
+ --non-working-days-color: #2f2f2f;
224
+ --type-story-color: #6fb86f;
225
+ --type-task-color: #0021b3;
226
+ --type-bug-color: #bb5603;
227
+ --body-background: #343434;
228
+ --default-text-color: #aaa;
229
+ --grid-line-color: #424242;
230
+ --expedited-color: #b90000;
231
+ --blocked-color: #c75b02;
232
+ --stalled-color: #ae7202;
233
+ --wip-chart-active-color: #2551c1;
234
+ --status-category-inprogress-color: #1c49bb;
235
+ --hierarchy-table-inactive-item-text-color: #939393;
236
+ --wip-chart-completed-color: #03cb03;
237
+ --wip-chart-duration-less-than-day-color: #d2d988;
238
+ --wip-chart-duration-week-or-less-color: #dfcd00;
239
+ --wip-chart-duration-two-weeks-or-less-color: #cf9400;
240
+ --wip-chart-duration-four-weeks-or-less-color: #c25e00;
241
+ --wip-chart-duration-more-than-four-weeks-color: #8e0000;
242
+ --daily-view-selected-issue-background: #474747;
243
+ --priority-color-highest: #ef4444;
244
+ --priority-color-high: #f97316;
245
+ --priority-color-low: #06b6d4;
246
+ --priority-color-lowest: #94a3b8;
247
+
248
+ a[href] { color: #1e8ad6; }
249
+ a[href]:hover { color: #3ba0e6; }
250
+ .chart { background: var(--body-background); }
251
+ table.standard {
252
+ th { background: var(--body-background); }
253
+ tr:nth-child(odd) { background-color: #656565; }
254
+ }
255
+ div.color_block { border: 1px solid lightgray; }
256
+ div.daily_issue {
257
+ .field { color: var(--default-text-color); }
258
+ }
259
+ }
260
+
261
+ /* Force light mode — overrides the dark media query when user explicitly picks light */
262
+ html[data-theme="light"] {
263
+ --warning-banner: yellow;
264
+ --non-working-days-color: #F0F0F0;
265
+ --type-story-color: #4bc14b;
266
+ --type-task-color: blue;
267
+ --type-bug-color: orange;
268
+ --body-background: white;
269
+ --default-text-color: black;
270
+ --grid-line-color: lightgray;
271
+ --expedited-color: red;
272
+ --blocked-color: #FF7400;
273
+ --stalled-color: orange;
274
+ --wip-chart-active-color: #326cff;
275
+ --status-category-inprogress-color: #2663ff;
276
+ --hierarchy-table-inactive-item-text-color: gray;
277
+ --wip-chart-completed-color: #00ff00;
278
+ --wip-chart-duration-less-than-day-color: #ffef41;
279
+ --wip-chart-duration-week-or-less-color: #dcc900;
280
+ --wip-chart-duration-two-weeks-or-less-color: #dfa000;
281
+ --wip-chart-duration-four-weeks-or-less-color: #eb7200;
282
+ --wip-chart-duration-more-than-four-weeks-color: #e70000;
283
+ --daily-view-selected-issue-background: lightgray;
284
+ --priority-color-highest: #dc2626;
285
+ --priority-color-high: #ea580c;
286
+ --priority-color-low: #0891b2;
287
+ --priority-color-lowest: #64748b;
288
+
289
+ a[href] { color: revert; }
290
+ a[href]:hover { color: revert; }
291
+ .chart { background: white; }
292
+ table.standard {
293
+ th { background: white; }
294
+ tr:nth-child(odd) { background-color: #eee; }
295
+ }
296
+ div.color_block { border: 1px solid black; }
297
+ }
298
+
299
+ /* Theme toggle widget */
300
+ #theme-toggle {
301
+ position: fixed;
302
+ top: 0.5rem;
303
+ right: 0.5rem;
304
+ display: flex;
305
+ gap: 2px;
306
+ background: var(--body-background);
307
+ border: 1px solid var(--grid-line-color);
308
+ border-radius: 6px;
309
+ padding: 3px;
310
+ z-index: 1000;
311
+
312
+ button {
313
+ background: none;
314
+ border: none;
315
+ cursor: pointer;
316
+ padding: 2px 6px;
317
+ border-radius: 4px;
318
+ font-size: 1rem;
319
+ color: var(--default-text-color);
320
+ opacity: 0.5;
321
+ }
322
+
323
+ button:hover {
324
+ opacity: 1;
325
+ background: var(--grid-line-color);
326
+ }
327
+
328
+ button.active {
329
+ opacity: 1;
330
+ background: var(--grid-line-color);
331
+ }
332
+ }
333
+
217
334
  @media screen and (prefers-color-scheme: dark) {
218
335
  :root {
219
336
  --warning-banner: #9F2B00;
@@ -1,6 +1,7 @@
1
1
  <html>
2
2
  <head>
3
3
  <meta charset="UTF-8">
4
+ <title><%= project_name.empty? ? 'JiraMetrics' : "JiraMetrics - #{project_name}" %></title>
4
5
  <link rel="icon" type="image/png" href="https://github.com/mikebowler/jirametrics/blob/main/favicon.png?raw=true" />
5
6
  <script src="https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.js"></script>
6
7
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@@ -17,6 +18,11 @@
17
18
  </script>
18
19
  </head>
19
20
  <body>
21
+ <div id="theme-toggle">
22
+ <button id="theme-btn-system" title="Use system preference">⊙</button>
23
+ <button id="theme-btn-light" title="Light mode">☀</button>
24
+ <button id="theme-btn-dark" title="Dark mode">☾</button>
25
+ </div>
20
26
  <noscript>
21
27
  <div style="padding: 1em; background: red; color: white; font-size: 2em;">
22
28
  Javascript is currently disabled and that means that almost all of the charts in this report won't render. If you've loaded this from a folder on SharePoint then save it locally and load it again.
@@ -1,3 +1,12 @@
1
+ // Apply saved theme immediately (before Chart.js reads CSS variables) so charts
2
+ // initialize with the correct colour scheme.
3
+ (function () {
4
+ const saved = localStorage.getItem('jirametrics:theme');
5
+ if (saved) {
6
+ document.documentElement.setAttribute('data-theme', saved);
7
+ }
8
+ }());
9
+
1
10
  function makeFoldable() {
2
11
  // Get all elements with the "foldable" class
3
12
  const foldableElements = document.querySelectorAll('.foldable');
@@ -77,16 +86,57 @@ function makeFoldable() {
77
86
  });
78
87
  }
79
88
 
89
+ function initThemeToggle() {
90
+ const html = document.documentElement;
91
+ const saved = localStorage.getItem('jirametrics:theme');
92
+ if (saved) {
93
+ html.setAttribute('data-theme', saved);
94
+ }
95
+
96
+ function updateActiveButton(theme) {
97
+ ['system', 'light', 'dark'].forEach(t => {
98
+ const btn = document.getElementById(`theme-btn-${t}`);
99
+ if (btn) {
100
+ btn.classList.toggle('active', t === theme);
101
+ }
102
+ });
103
+ }
104
+
105
+ function setTheme(theme) {
106
+ if (theme === 'system') {
107
+ html.removeAttribute('data-theme');
108
+ localStorage.removeItem('jirametrics:theme');
109
+ } else {
110
+ html.setAttribute('data-theme', theme);
111
+ localStorage.setItem('jirametrics:theme', theme);
112
+ }
113
+ updateActiveButton(theme);
114
+ location.reload();
115
+ }
116
+
117
+ updateActiveButton(saved || 'system');
118
+
119
+ ['system', 'light', 'dark'].forEach(theme => {
120
+ const btn = document.getElementById(`theme-btn-${theme}`);
121
+ if (btn) {
122
+ btn.addEventListener('click', () => setTheme(theme));
123
+ }
124
+ });
125
+ }
126
+
80
127
  // Auto-initialize when DOM is loaded
81
128
  document.addEventListener('DOMContentLoaded', function() {
82
129
  makeFoldable();
130
+ initThemeToggle();
83
131
  });
84
132
 
85
133
 
86
134
  // If we switch between light/dark mode then force a refresh so all charts will redraw correctly
87
- // in the other colour scheme.
135
+ // in the other colour scheme. Skip reload if a manual theme override is set.
88
136
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
89
- location.reload()
137
+ if (!document.documentElement.hasAttribute('data-theme')) {
138
+ location.reload();
139
+ }
90
140
  })
91
141
 
92
142
  // Draw a diagonal pattern to highlight sections of a bar chart. Based on code found at:
@@ -22,10 +22,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
22
22
  time: {
23
23
  format: 'YYYY-MM-DD'
24
24
  },
25
- scaleLabel: {
26
- display: true,
27
- labelString: 'Date'
28
- },
25
+ <%= render_axis_title :x %>
29
26
  min: "<%= date_range.begin.to_s %>",
30
27
  max: "<%= (date_range.end + 1).to_s %>",
31
28
  grid: {
@@ -33,14 +30,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
33
30
  },
34
31
  },
35
32
  y: {
36
- scaleLabel: {
37
- display: true,
38
- labelString: 'Items remaining'
39
- },
40
- title: {
41
- display: true,
42
- text: "<%= y_axis_title %>"
43
- },
33
+ <%= render_axis_title :y %>
44
34
  min: 0.0,
45
35
  grid: {
46
36
  color: <%= CssVariable['--grid-line-color'].to_json %>
@@ -77,7 +67,9 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
77
67
  <table class='standard' style="margin-left: 1em;">
78
68
  <thead>
79
69
  <th>Sprint</th>
80
- <th>Length</th>
70
+ <th>Started</th>
71
+ <th>Completed</th>
72
+ <th>Days</th>
81
73
  <th>State</th>
82
74
  <th>Started</th>
83
75
  <th>Completed</th>
@@ -90,6 +82,8 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
90
82
  <% @summary_stats.keys.sort_by(&:start_time).each do |sprint| %>
91
83
  <tr>
92
84
  <td><%= sprint.name %></td>
85
+ <td><%= sprint.start_time.to_date %></td>
86
+ <td><%= sprint.completed_time&.to_date %></td>
93
87
  <td><%= sprint.day_count %></td>
94
88
  <td><%= sprint.raw['state'] %></td>
95
89
  <% stats = @summary_stats[sprint] %>
@@ -3,6 +3,11 @@
3
3
  <canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
4
4
  </div>
5
5
  <script>
6
+ if (!Chart.Tooltip.positioners.legendItem) {
7
+ Chart.Tooltip.positioners.legendItem = function(items) {
8
+ return this.chart._legendHoverPosition || Chart.Tooltip.positioners.average.call(this, items);
9
+ };
10
+ }
6
11
  new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
7
12
  type: 'scatter',
8
13
  data: {
@@ -20,10 +25,9 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
20
25
  time: {
21
26
  format: 'YYYY-MM-DD'
22
27
  },
23
- scaleLabel: {
24
- display: true,
25
- labelString: 'Date Completed'
26
- },
28
+ min: "<%= date_range.begin.to_s %>",
29
+ max: "<%= (date_range.end + 1).to_s %>",
30
+ <%= render_axis_title :x %>
27
31
  grid: {
28
32
  color: <%= CssVariable['--grid-line-color'].to_json %>
29
33
  },
@@ -32,10 +36,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
32
36
  scaleLabel: {
33
37
  display: true,
34
38
  },
35
- title: {
36
- display: true,
37
- text: 'Count of items'
38
- },
39
+ <%= render_axis_title :y %>
39
40
  min: 0,
40
41
  grid: {
41
42
  color: <%= CssVariable['--grid-line-color'].to_json %>
@@ -44,15 +45,45 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
44
45
  },
45
46
  plugins: {
46
47
  tooltip: {
48
+ position: 'legendItem',
47
49
  callbacks: {
50
+ title: function(contexts) {
51
+ if (contexts[0]?.chart._legendHoverIndex != null) return '';
52
+ },
48
53
  label: function(context) {
49
- return context.dataset.data[context.dataIndex].title
54
+ if (context.chart._legendHoverIndex != null) {
55
+ return context.dataset.label_hint || '';
56
+ }
57
+ return context.dataset.data[context.dataIndex].title;
50
58
  }
51
59
  }
52
60
  },
53
61
  annotation: {
54
62
  annotations: {
55
63
  <%= working_days_annotation %>
64
+ <%= date_annotation %>
65
+ }
66
+ },
67
+ legend: {
68
+ onHover: function(event, legendItem, legend) {
69
+ const chart = legend.chart;
70
+ const dataset = chart.data.datasets[legendItem.datasetIndex];
71
+ if (!dataset?.label_hint) return;
72
+ chart._legendHoverIndex = legendItem.datasetIndex;
73
+ chart._legendHoverPosition = { x: event.x, y: event.y };
74
+ const firstNonZero = dataset.data.findIndex(d => d?.y !== 0);
75
+ if (firstNonZero === -1) return;
76
+ chart.tooltip.setActiveElements(
77
+ [{ datasetIndex: legendItem.datasetIndex, index: firstNonZero }],
78
+ { x: event.x, y: event.y }
79
+ );
80
+ chart.update();
81
+ },
82
+ onLeave: function(event, legendItem, legend) {
83
+ legend.chart._legendHoverIndex = null;
84
+ legend.chart._legendHoverPosition = null;
85
+ legend.chart.tooltip.setActiveElements([], { x: 0, y: 0 });
86
+ legend.chart.update();
56
87
  }
57
88
  }
58
89
  }
@@ -1,58 +1,8 @@
1
+ <div>
1
2
  <%= seam_start %>
2
3
  <div class="chart">
3
4
  <canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
4
5
  </div>
5
- <%
6
- if show_stats
7
- link_id = next_id
8
- issues_id = next_id
9
- %>
10
- <div class='foldable' style="padding-left: 1em;">Statistics</div>
11
- <div id="<%= issues_id %>" style="padding-left: 1em;">
12
- <div>
13
- <table class="standard">
14
- <tr>
15
- <th>Issue Type</th>
16
- <th>Min</th>
17
- <th>Max</th>
18
- <th>Avg</th>
19
- <th>Mode</th>
20
- <% percentiles.each do |p| %>
21
- <th><%= p %>th</th>
22
- <% end %>
23
- </tr>
24
- <% the_stats.each do |k, v| %>
25
- <tr>
26
- <td><%= k %></td>
27
- <td style="text-align: right;"><%= v[:min] %></td>
28
- <td style="text-align: right;"><%= v[:max] %></td>
29
- <td style="text-align: right;"><%= sprintf('%.2f', v[:average]) %></td>
30
- <td><%= v[:mode].join(', ') %></td>
31
- <% percentiles.each do |p| %>
32
- <td style="text-align: right;"><%= v[:percentiles][p] %></td>
33
- <% end %>
34
- </tr>
35
- <% end %>
36
- </table>
37
- </div>
38
- <div>
39
- <p>These statistics help understand the <i>"shape"</i> of the cycletime histogram distribution, to help us with predictions.</p>
40
- <ul>
41
- <li><b>Min & Max:</b> the observed spread for the data set. Useful to judge how wide the variation is. </li>
42
- <li><b>Average:</b> the arithmetic mean of the data set. Useful as a <i>"typical representative"</i> of the complete set.</li>
43
- <li><b>Mode:</b> the most repeated value(s) in the data set. This is the value we're most likely to remember. </li>
44
- <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>
45
- <ul>
46
- <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>
47
- <li><b>85%</b>: useful to establish service level expectations, accounting for rare events..</li>
48
- <li><b>98% (or higher)</b>: useful to gauge worst case expectations..</li>
49
- </ul>
50
- </ul>
51
- </div>
52
- </div>
53
- <%
54
- end
55
- %>
56
6
  <script>
57
7
  new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
58
8
  {
@@ -66,22 +16,17 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
66
16
  x: {
67
17
  type: 'linear',
68
18
  stacked: true,
69
- title: {
70
- display: true,
71
- text: 'Cycletime in days'
72
- },
19
+ <%= render_axis_title :x %>
73
20
  grid: {
74
21
  color: <%= CssVariable['--grid-line-color'].to_json %>
75
22
  },
76
23
  min: 0,
24
+ <%= @max_x_value.nil? ? '' : "max: #{@max_x_value}," %>
77
25
  offset: false, // Gets rid of the ugly padding on left.
78
26
  },
79
27
  y: {
80
28
  stacked: true,
81
- title: {
82
- display: true,
83
- text: 'Number of items that had that cycletime'
84
- },
29
+ <%= render_axis_title :y %>
85
30
  grid: {
86
31
  color: <%= CssVariable['--grid-line-color'].to_json %>
87
32
  },
@@ -121,3 +66,58 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
121
66
  });
122
67
  </script>
123
68
  <%= seam_end %>
69
+
70
+ <%= seam_start 'stats_table' %>
71
+ <%
72
+ if show_stats
73
+ link_id = next_id
74
+ issues_id = next_id
75
+ %>
76
+ <div class='foldable' style="padding-left: 1em;">Statistics</div>
77
+ <div id="<%= issues_id %>" style="padding-left: 1em;">
78
+ <div>
79
+ <table class="standard">
80
+ <tr>
81
+ <th>Type</th>
82
+ <th>Min</th>
83
+ <th>Max</th>
84
+ <th>Avg</th>
85
+ <th>Mode</th>
86
+ <% percentiles.each do |p| %>
87
+ <th><%= p %>th</th>
88
+ <% end %>
89
+ </tr>
90
+ <% the_stats.each do |k, v| %>
91
+ <tr>
92
+ <td><%= k %></td>
93
+ <td style="text-align: right;"><%= v[:min] %></td>
94
+ <td style="text-align: right;"><%= v[:max] %></td>
95
+ <td style="text-align: right;"><%= sprintf('%.2f', v[:average]) %></td>
96
+ <td><%= v[:mode].join(', ') %></td>
97
+ <% percentiles.each do |p| %>
98
+ <td style="text-align: right;"><%= v[:percentiles][p] %></td>
99
+ <% end %>
100
+ </tr>
101
+ <% end %>
102
+ </table>
103
+ </div>
104
+ <div>
105
+ <p>These statistics help understand the <i>"shape"</i> of the histogram distribution, to help us with predictions.</p>
106
+ <ul>
107
+ <li><b>Min & Max:</b> the observed spread for the data set. Useful to judge how wide the variation is. </li>
108
+ <li><b>Average:</b> the arithmetic mean of the data set. Useful as a <i>"typical representative"</i> of the complete set.</li>
109
+ <li><b>Mode:</b> the most repeated value(s) in the data set. This is the value we're most likely to remember. </li>
110
+ <li><b>Percentiles:</b> they partition the data set. If X is the Nth percentile, it means that N% of values are X or less. Typical percentiles of interest are:</li>
111
+ <ul>
112
+ <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>
113
+ <li><b>85%</b>: useful to establish service level expectations, accounting for rare events..</li>
114
+ <li><b>98% (or higher)</b>: useful to gauge worst case expectations..</li>
115
+ </ul>
116
+ </ul>
117
+ </div>
118
+ </div>
119
+ <%
120
+ end
121
+ %>
122
+ <%= seam_end 'stats_table' %>
123
+ </div>
@@ -23,22 +23,25 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
23
23
  grid: {
24
24
  color: <%= CssVariable['--grid-line-color'].to_json %>
25
25
  },
26
+ <%= render_axis_title :x %>
26
27
  min: "<%= date_range.begin.to_s %>",
27
28
  max: "<%= (date_range.end + 1).to_s %>"
28
29
  },
29
30
  y: {
31
+ min: 0,
32
+ max: <%= (@highest_y_value * 1.1).ceil %>,
30
33
  scaleLabel: {
31
- display: true,
32
- min: 0,
33
- max: <%= @highest_y_value %>
34
- },
35
- title: {
36
- display: true,
37
- text: '<%= y_axis_heading %>'
34
+ display: true
38
35
  },
36
+ <%= render_axis_title :y %>
39
37
  grid: {
40
38
  color: <%= CssVariable['--grid-line-color'].to_json %>
41
39
  },
40
+ ticks: {
41
+ callback: function(value, index, ticks) {
42
+ return index === ticks.length - 1 ? null : value;
43
+ }
44
+ }
42
45
  }
43
46
  },
44
47
  plugins: {
@@ -53,6 +56,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
53
56
  annotation: {
54
57
  annotations: {
55
58
  <%= working_days_annotation %>
59
+ <%= date_annotation %>
56
60
 
57
61
  <% @percentage_lines.each_with_index do |args, index| %>
58
62
  <% percent, color = args %>
@@ -3,8 +3,9 @@
3
3
  class HtmlGenerator
4
4
  attr_accessor :file_system, :settings
5
5
 
6
- def create_html output_filename:, settings:
6
+ def create_html output_filename:, settings:, project_name: ''
7
7
  @settings = settings
8
+ project_name = project_name.to_s
8
9
  html_directory = "#{Pathname.new(File.realpath(__FILE__)).dirname}/html"
9
10
  css = load_css html_directory: html_directory
10
11
  javascript = file_system.load(File.join(html_directory, 'index.js'))
@@ -20,33 +20,38 @@ class HtmlReportConfig < HtmlGenerator
20
20
  module_eval lines.join("\n"), __FILE__, __LINE__
21
21
  end
22
22
 
23
- define_chart name: 'aging_work_bar_chart', classname: 'AgingWorkBarChart'
24
- define_chart name: 'aging_work_table', classname: 'AgingWorkTable'
25
- define_chart name: 'cycletime_scatterplot', classname: 'CycletimeScatterplot'
26
- define_chart name: 'daily_wip_chart', classname: 'DailyWipChart'
27
- define_chart name: 'daily_wip_by_age_chart', classname: 'DailyWipByAgeChart'
28
- define_chart name: 'daily_wip_by_blocked_stalled_chart', classname: 'DailyWipByBlockedStalledChart'
29
- define_chart name: 'daily_wip_by_parent_chart', classname: 'DailyWipByParentChart'
30
- define_chart name: 'throughput_chart', classname: 'ThroughputChart'
31
- define_chart name: 'expedited_chart', classname: 'ExpeditedChart'
32
- define_chart name: 'cycletime_histogram', classname: 'CycletimeHistogram'
33
- define_chart name: 'estimate_accuracy_chart', classname: 'EstimateAccuracyChart'
34
- define_chart name: 'hierarchy_table', classname: 'HierarchyTable'
35
- define_chart name: 'flow_efficiency_scatterplot', classname: 'FlowEfficiencyScatterplot'
36
- define_chart name: 'daily_view', classname: 'DailyView'
37
-
38
23
  define_chart name: 'daily_wip_by_type', classname: 'DailyWipChart',
39
24
  deprecated_warning: 'This is the same as daily_wip_chart. Please use that one', deprecated_date: '2024-05-23'
40
25
  define_chart name: 'story_point_accuracy_chart', classname: 'EstimateAccuracyChart',
41
26
  deprecated_warning: 'Renamed to estimate_accuracy_chart. Please use that one', deprecated_date: '2024-05-23'
42
27
 
43
28
  def initialize file_config:, block:
29
+ super()
44
30
  @file_config = file_config
45
31
  @block = block
46
32
  @sections = [] # Where we store the chunks of text that will be assembled into the HTML
47
33
  @charts = [] # Where we store all the charts we executed so we can assert against them.
48
34
  end
49
35
 
36
+ def method_missing name, &block
37
+ class_name = name.to_s.split('_').map(&:capitalize).join
38
+ klass = Object.const_get(class_name)
39
+ raise NameError unless klass < ChartBase
40
+
41
+ block ||= ->(_) {}
42
+ execute_chart klass.new(block)
43
+ rescue NameError
44
+ super
45
+ end
46
+
47
+ def respond_to_missing? name, include_private = false
48
+ class_name = name.to_s.split('_').map(&:capitalize).join
49
+ klass = Object.const_get(class_name)
50
+ klass < ChartBase
51
+ rescue NameError
52
+ super
53
+ end
54
+
50
55
  def cycletime label = nil, &block
51
56
  @file_config.project_config.all_boards.each_value do |board|
52
57
  raise 'Multiple cycletimes not supported' if board.cycletime
@@ -73,7 +78,8 @@ class HtmlReportConfig < HtmlGenerator
73
78
 
74
79
  html create_footer
75
80
 
76
- create_html output_filename: @file_config.output_filename, settings: settings
81
+ create_html output_filename: @file_config.output_filename, settings: settings,
82
+ project_name: @file_config.project_config.name
77
83
  end
78
84
 
79
85
  def file_system
@@ -147,6 +153,7 @@ class HtmlReportConfig < HtmlGenerator
147
153
  chart.all_boards = project_config.all_boards
148
154
  chart.board_id = find_board_id
149
155
  chart.holiday_dates = project_config.exporter.holiday_dates
156
+ chart.fix_versions = project_config.fix_versions
150
157
 
151
158
  time_range = @file_config.project_config.time_range
152
159
  chart.date_range = time_range.begin.to_date..time_range.end.to_date