jirametrics 2.4 → 2.30

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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/bin/jirametrics-mcp +5 -0
  3. data/lib/jirametrics/aggregate_config.rb +16 -3
  4. data/lib/jirametrics/aging_work_bar_chart.rb +193 -133
  5. data/lib/jirametrics/aging_work_in_progress_chart.rb +138 -42
  6. data/lib/jirametrics/aging_work_table.rb +63 -19
  7. data/lib/jirametrics/anonymizer.rb +81 -6
  8. data/lib/jirametrics/atlassian_document_format.rb +160 -0
  9. data/lib/jirametrics/bar_chart_range.rb +17 -0
  10. data/lib/jirametrics/blocked_stalled_change.rb +6 -4
  11. data/lib/jirametrics/board.rb +74 -22
  12. data/lib/jirametrics/board_config.rb +11 -3
  13. data/lib/jirametrics/board_feature.rb +14 -0
  14. data/lib/jirametrics/board_movement_calculator.rb +155 -0
  15. data/lib/jirametrics/cfd_data_builder.rb +108 -0
  16. data/lib/jirametrics/change_item.rb +54 -18
  17. data/lib/jirametrics/chart_base.rb +203 -30
  18. data/lib/jirametrics/css_variable.rb +2 -2
  19. data/lib/jirametrics/cumulative_flow_diagram.rb +208 -0
  20. data/lib/jirametrics/cycle_time_config.rb +137 -0
  21. data/lib/jirametrics/cycletime_histogram.rb +17 -38
  22. data/lib/jirametrics/cycletime_scatterplot.rb +18 -87
  23. data/lib/jirametrics/daily_view.rb +306 -0
  24. data/lib/jirametrics/daily_wip_by_age_chart.rb +5 -8
  25. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +15 -5
  26. data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -6
  27. data/lib/jirametrics/daily_wip_chart.rb +36 -16
  28. data/lib/jirametrics/data_quality_report.rb +251 -42
  29. data/lib/jirametrics/dependency_chart.rb +42 -12
  30. data/lib/jirametrics/download_config.rb +27 -0
  31. data/lib/jirametrics/downloader.rb +185 -110
  32. data/lib/jirametrics/downloader_for_cloud.rb +287 -0
  33. data/lib/jirametrics/downloader_for_data_center.rb +95 -0
  34. data/lib/jirametrics/estimate_accuracy_chart.rb +75 -14
  35. data/lib/jirametrics/estimation_configuration.rb +25 -0
  36. data/lib/jirametrics/examples/aggregated_project.rb +9 -23
  37. data/lib/jirametrics/examples/standard_project.rb +57 -58
  38. data/lib/jirametrics/expedited_chart.rb +11 -10
  39. data/lib/jirametrics/exporter.rb +51 -14
  40. data/lib/jirametrics/file_config.rb +21 -6
  41. data/lib/jirametrics/file_system.rb +96 -4
  42. data/lib/jirametrics/fix_version.rb +13 -0
  43. data/lib/jirametrics/flow_efficiency_scatterplot.rb +115 -0
  44. data/lib/jirametrics/github_gateway.rb +115 -0
  45. data/lib/jirametrics/groupable_issue_chart.rb +12 -4
  46. data/lib/jirametrics/grouping_rules.rb +26 -4
  47. data/lib/jirametrics/html/aging_work_bar_chart.erb +8 -17
  48. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +24 -5
  49. data/lib/jirametrics/html/aging_work_table.erb +13 -4
  50. data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
  51. data/lib/jirametrics/html/cumulative_flow_diagram.erb +503 -0
  52. data/lib/jirametrics/html/daily_wip_chart.erb +41 -15
  53. data/lib/jirametrics/html/estimate_accuracy_chart.erb +4 -12
  54. data/lib/jirametrics/html/expedited_chart.erb +7 -24
  55. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +81 -0
  56. data/lib/jirametrics/html/hierarchy_table.erb +1 -1
  57. data/lib/jirametrics/html/index.css +336 -62
  58. data/lib/jirametrics/html/index.erb +16 -21
  59. data/lib/jirametrics/html/index.js +164 -0
  60. data/lib/jirametrics/html/legacy_colors.css +174 -0
  61. data/lib/jirametrics/html/sprint_burndown.erb +18 -25
  62. data/lib/jirametrics/html/throughput_chart.erb +43 -21
  63. data/lib/jirametrics/html/time_based_histogram.erb +123 -0
  64. data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +16 -21
  65. data/lib/jirametrics/html/wip_by_column_chart.erb +250 -0
  66. data/lib/jirametrics/html_generator.rb +32 -0
  67. data/lib/jirametrics/html_report_config.rb +83 -76
  68. data/lib/jirametrics/issue.rb +499 -91
  69. data/lib/jirametrics/issue_collection.rb +33 -0
  70. data/lib/jirametrics/issue_printer.rb +97 -0
  71. data/lib/jirametrics/jira_gateway.rb +96 -16
  72. data/lib/jirametrics/mcp_server.rb +531 -0
  73. data/lib/jirametrics/project_config.rb +374 -130
  74. data/lib/jirametrics/pull_request.rb +30 -0
  75. data/lib/jirametrics/pull_request_cycle_time_histogram.rb +77 -0
  76. data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +88 -0
  77. data/lib/jirametrics/pull_request_review.rb +13 -0
  78. data/lib/jirametrics/raw_javascript.rb +17 -0
  79. data/lib/jirametrics/rules.rb +2 -2
  80. data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
  81. data/lib/jirametrics/settings.json +10 -2
  82. data/lib/jirametrics/sprint.rb +13 -0
  83. data/lib/jirametrics/sprint_burndown.rb +47 -39
  84. data/lib/jirametrics/sprint_issue_change_data.rb +3 -3
  85. data/lib/jirametrics/status.rb +84 -19
  86. data/lib/jirametrics/status_collection.rb +83 -38
  87. data/lib/jirametrics/stitcher.rb +81 -0
  88. data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
  89. data/lib/jirametrics/throughput_chart.rb +73 -23
  90. data/lib/jirametrics/time_based_histogram.rb +139 -0
  91. data/lib/jirametrics/time_based_scatterplot.rb +107 -0
  92. data/lib/jirametrics/user.rb +12 -0
  93. data/lib/jirametrics/value_equality.rb +2 -2
  94. data/lib/jirametrics/wip_by_column_chart.rb +236 -0
  95. data/lib/jirametrics.rb +101 -66
  96. metadata +72 -16
  97. data/lib/jirametrics/cycletime_config.rb +0 -69
  98. data/lib/jirametrics/discard_changes_before.rb +0 -37
  99. data/lib/jirametrics/html/cycletime_histogram.erb +0 -47
  100. data/lib/jirametrics/html/data_quality_report.erb +0 -126
@@ -0,0 +1,164 @@
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
+
10
+ function makeFoldable() {
11
+ // Get all elements with the "foldable" class
12
+ const foldableElements = document.querySelectorAll('.foldable');
13
+
14
+ if (foldableElements.length === 0) {
15
+ return; // No foldable elements found
16
+ }
17
+
18
+ // Process each foldable element
19
+ foldableElements.forEach((element, index) => {
20
+ // Skip if this is the footer element
21
+ if (element.id === 'footer') {
22
+ return;
23
+ }
24
+
25
+ // Create a unique ID for this section
26
+ const sectionId = `foldable-section-${index}`;
27
+ const toggleId = `foldable-toggle-${index}`;
28
+
29
+ // Create a container div for the foldable element and its content
30
+ const container = document.createElement('div');
31
+ container.className = 'foldable-section';
32
+ container.id = sectionId;
33
+
34
+ // Create a toggle button
35
+ const toggleButton = document.createElement(element.tagName); //'button');
36
+ toggleButton.id = toggleId;
37
+ toggleButton.className = 'foldable-toggle-btn';
38
+ toggleButton.innerHTML = '▼ ' + element.innerHTML;
39
+
40
+ // Create a content container
41
+ const contentContainer = document.createElement('div');
42
+ contentContainer.className = 'foldable-content';
43
+ contentContainer.style.cssText = `
44
+ border-left: 2px solid #ccc;
45
+ padding-left: 15px;
46
+ `;
47
+
48
+ // Move the foldable element into the container and replace it with the toggle button
49
+ element.parentNode.insertBefore(container, element);
50
+ container.appendChild(toggleButton);
51
+ container.appendChild(contentContainer);
52
+
53
+ // Move all elements between this foldable element and the next foldable element (or end of document) into the content container
54
+ let nextElement = element.nextElementSibling;
55
+ while (nextElement && !nextElement.classList.contains('foldable')) {
56
+ // Skip the footer element
57
+ if (nextElement.id === 'footer') {
58
+ break;
59
+ }
60
+
61
+ const temp = nextElement.nextElementSibling;
62
+ contentContainer.appendChild(nextElement);
63
+ nextElement = temp;
64
+ }
65
+
66
+ // Remove the original foldable element
67
+ element.remove();
68
+
69
+ // Add click event to toggle visibility
70
+ toggleButton.addEventListener('click', function() {
71
+ const content = this.nextElementSibling;
72
+ if (content.style.display === 'none') {
73
+ content.style.display = 'block';
74
+ this.innerHTML = '▼ ' + this.innerHTML.substring(2);
75
+ } else {
76
+ content.style.display = 'none';
77
+ this.innerHTML = '▶ ' + this.innerHTML.substring(2);
78
+ }
79
+ });
80
+
81
+ // Initially show the content (you can change this to 'none' if you want sections collapsed by default)
82
+ contentContainer.style.display = 'block';
83
+ if(element.classList.contains('startFolded')) {
84
+ toggleButton.click();
85
+ }
86
+ });
87
+ }
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
+
127
+ // Auto-initialize when DOM is loaded
128
+ document.addEventListener('DOMContentLoaded', function() {
129
+ makeFoldable();
130
+ initThemeToggle();
131
+ });
132
+
133
+
134
+ // If we switch between light/dark mode then force a refresh so all charts will redraw correctly
135
+ // in the other colour scheme. Skip reload if a manual theme override is set.
136
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
137
+ if (!document.documentElement.hasAttribute('data-theme')) {
138
+ location.reload();
139
+ }
140
+ })
141
+
142
+ // Draw a diagonal pattern to highlight sections of a bar chart. Based on code found at:
143
+ // https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns
144
+ function createDiagonalPattern(color = 'black') {
145
+ // create a 5x5 px canvas for the pattern's base shape
146
+ let shape = document.createElement('canvas')
147
+ shape.width = 5
148
+ shape.height = 5
149
+ // get the context for drawing
150
+ let c = shape.getContext('2d')
151
+ // draw 1st line of the shape
152
+ c.strokeStyle = color
153
+ c.beginPath()
154
+ c.moveTo(1, 0)
155
+ c.lineTo(5, 4)
156
+ c.stroke()
157
+ // draw 2nd line of the shape
158
+ c.beginPath()
159
+ c.moveTo(0, 4)
160
+ c.lineTo(1, 5)
161
+ c.stroke()
162
+ // create the pattern from the shape
163
+ return c.createPattern(shape, 'repeat')
164
+ }
@@ -0,0 +1,174 @@
1
+ /* Legacy color scheme for jirametrics
2
+ *
3
+ * The default colors were updated to improve accessibility for people with
4
+ * color vision deficiencies. If you prefer the original colors, add this to
5
+ * your project config:
6
+ *
7
+ * setting['include_css'] = './legacy_colors.css'
8
+ *
9
+ * and copy this file to the same directory as your config file.
10
+ */
11
+
12
+ /* Light mode */
13
+ :root,
14
+ html[data-theme="light"] {
15
+ --body-background: white;
16
+ --default-text-color: black;
17
+ --grid-line-color: lightgray;
18
+ --warning-banner: yellow;
19
+
20
+ --cycletime-scatterplot-overall-trendline-color: gray;
21
+
22
+ --non-working-days-color: #F0F0F0;
23
+ --expedited-color: red;
24
+ --blocked-color: #FF7400;
25
+ --stalled-color: orange;
26
+ --dead-color: black;
27
+
28
+ --type-story-color: #4bc14b;
29
+ --type-task-color: blue;
30
+ --type-bug-color: orange;
31
+ --type-spike-color: #9400D3;
32
+
33
+ --status-category-todo-color: gray;
34
+ --status-category-inprogress-color: #2663ff;
35
+ --status-category-done-color: #00ff00;
36
+ --status-category-unknown-color: black;
37
+
38
+ --aging-work-bar-chart-percentage-line-color: red;
39
+ --aging-work-bar-chart-separator-color: white;
40
+
41
+ --throughput_chart_total_line_color: gray;
42
+
43
+ --aging-work-in-progress-chart-shading-color: lightgray;
44
+ --aging-work-in-progress-chart-shading-50-color: #2E8BC0;
45
+ --aging-work-in-progress-chart-shading-85-color: #ADD8E6;
46
+ --aging-work-in-progress-chart-shading-98-color: #FF8A8A;
47
+ --aging-work-in-progress-chart-shading-100-color: #FF2E2E;
48
+
49
+ --aging-work-in-progress-by-age-trend-line-color: gray;
50
+
51
+ --aging-work-table-date-in-jeopardy: yellow;
52
+ --aging-work-table-date-overdue: red;
53
+
54
+ --hierarchy-table-inactive-item-text-color: gray;
55
+
56
+ --wip-chart-completed-color: #00ff00;
57
+ --wip-chart-completed-but-not-started-color: #99FF99;
58
+ --wip-chart-duration-less-than-day-color: #ffef41;
59
+ --wip-chart-duration-week-or-less-color: #dcc900;
60
+ --wip-chart-duration-two-weeks-or-less-color: #dfa000;
61
+ --wip-chart-duration-four-weeks-or-less-color: #eb7200;
62
+ --wip-chart-duration-more-than-four-weeks-color: #e70000;
63
+ --wip-chart-active-color: #326cff;
64
+ --wip-chart-border-color: gray;
65
+
66
+ --wip-by-column-chart-bar-fill-color: #0072B2;
67
+ --wip-by-column-chart-bar-text-color: #ffffff;
68
+ --wip-by-column-chart-limit-line-color: #D55E00;
69
+ --wip-by-column-chart-recommendation-color: #009E73;
70
+
71
+ --estimate-accuracy-chart-completed-fill-color: #00ff00;
72
+ --estimate-accuracy-chart-completed-border-color: green;
73
+ --estimate-accuracy-chart-active-fill-color: #FFCCCB;
74
+ --estimate-accuracy-chart-active-border-color: red;
75
+
76
+ --expedited-chart-no-longer-expedited: gray;
77
+ --expedited-chart-dot-issue-started-color: orange;
78
+ --expedited-chart-dot-issue-stopped-color: green;
79
+ --expedited-chart-dot-expedite-started-color: red;
80
+ --expedited-chart-dot-expedite-stopped-color: green;
81
+
82
+ --sprint-burndown-sprint-color-1: blue;
83
+ --sprint-burndown-sprint-color-2: orange;
84
+ --sprint-burndown-sprint-color-3: green;
85
+ --sprint-burndown-sprint-color-4: red;
86
+ --sprint-burndown-sprint-color-5: brown;
87
+ --sprint-burndown-sprint-color-6: blue; /* wraps back to color-1 (legacy had only 5) */
88
+ --sprint-burndown-sprint-color-7: orange; /* wraps back to color-2 (legacy had only 5) */
89
+
90
+ --sprint-color: lightblue;
91
+
92
+ --daily-view-selected-issue-background: lightgray;
93
+ --daily-view-issue-border: green;
94
+ --daily-view-selected-issue-border: red;
95
+
96
+ --priority-color-highest: #dc2626;
97
+ --priority-color-high: #ea580c;
98
+ --priority-color-medium: #9ca3af;
99
+ --priority-color-low: #0891b2;
100
+ --priority-color-lowest: #64748b;
101
+ --priority-color-notset: gray;
102
+ --priority-color-critical: red;
103
+ }
104
+
105
+ /* Dark mode */
106
+ html[data-theme="dark"] {
107
+ --warning-banner: #9F2B00;
108
+ --non-working-days-color: #2f2f2f;
109
+ --type-story-color: #6fb86f;
110
+ --type-task-color: #0021b3;
111
+ --type-bug-color: #bb5603;
112
+ --body-background: #343434;
113
+ --default-text-color: #aaa;
114
+ --grid-line-color: #424242;
115
+ --expedited-color: #b90000;
116
+ --blocked-color: #c75b02;
117
+ --stalled-color: #ae7202;
118
+ --wip-chart-active-color: #2551c1;
119
+ --status-category-inprogress-color: #1c49bb;
120
+ --hierarchy-table-inactive-item-text-color: #939393;
121
+ --wip-by-column-chart-bar-fill-color: #56B4E9;
122
+ --wip-by-column-chart-bar-text-color: #000000;
123
+ --wip-by-column-chart-limit-line-color: #E69F00;
124
+ --wip-by-column-chart-recommendation-color: #2DCB9A;
125
+ --wip-chart-completed-color: #03cb03;
126
+ --wip-chart-duration-less-than-day-color: #d2d988;
127
+ --wip-chart-duration-week-or-less-color: #dfcd00;
128
+ --wip-chart-duration-two-weeks-or-less-color: #cf9400;
129
+ --wip-chart-duration-four-weeks-or-less-color: #c25e00;
130
+ --wip-chart-duration-more-than-four-weeks-color: #8e0000;
131
+ --daily-view-selected-issue-background: #474747;
132
+ --priority-color-highest: #ef4444;
133
+ --priority-color-high: #f97316;
134
+ --priority-color-low: #06b6d4;
135
+ --priority-color-lowest: #94a3b8;
136
+ }
137
+
138
+ @media screen and (prefers-color-scheme: dark) {
139
+ :root {
140
+ --warning-banner: #9F2B00;
141
+ --non-working-days-color: #2f2f2f;
142
+ --type-story-color: #6fb86f;
143
+ --type-task-color: #0021b3;
144
+ --type-bug-color: #bb5603;
145
+ --body-background: #343434;
146
+ --default-text-color: #aaa;
147
+ --grid-line-color: #424242;
148
+ --expedited-color: #b90000;
149
+ --blocked-color: #c75b02;
150
+ --stalled-color: #ae7202;
151
+ --dead-color: black;
152
+ --wip-chart-active-color: #2551c1;
153
+ --status-category-inprogress-color: #1c49bb;
154
+ --cycletime-scatterplot-overall-trendline-color: gray;
155
+ --hierarchy-table-inactive-item-text-color: #939393;
156
+ --wip-by-column-chart-bar-fill-color: #56B4E9;
157
+ --wip-by-column-chart-bar-text-color: #000000;
158
+ --wip-by-column-chart-limit-line-color: #E69F00;
159
+ --wip-by-column-chart-recommendation-color: #2DCB9A;
160
+ --wip-chart-completed-color: #03cb03;
161
+ --wip-chart-completed-but-not-started-color: #99FF99;
162
+ --wip-chart-duration-less-than-day-color: #d2d988;
163
+ --wip-chart-duration-week-or-less-color: #dfcd00;
164
+ --wip-chart-duration-two-weeks-or-less-color: #cf9400;
165
+ --wip-chart-duration-four-weeks-or-less-color: #c25e00;
166
+ --wip-chart-duration-more-than-four-weeks-color: #8e0000;
167
+ --daily-view-selected-issue-background: #474747;
168
+ --priority-color-highest: #ef4444;
169
+ --priority-color-high: #f97316;
170
+ --priority-color-medium: #9ca3af;
171
+ --priority-color-low: #06b6d4;
172
+ --priority-color-lowest: #94a3b8;
173
+ }
174
+ }
@@ -1,5 +1,6 @@
1
1
  <h2>Burndown by <%= y_axis_title %></h2>
2
2
 
3
+ <%= seam_start %>
3
4
  <div class="chart">
4
5
  <canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
5
6
  </div>
@@ -21,10 +22,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
21
22
  time: {
22
23
  format: 'YYYY-MM-DD'
23
24
  },
24
- scaleLabel: {
25
- display: true,
26
- labelString: 'Date'
27
- },
25
+ <%= render_axis_title :x %>
28
26
  min: "<%= date_range.begin.to_s %>",
29
27
  max: "<%= (date_range.end + 1).to_s %>",
30
28
  grid: {
@@ -32,14 +30,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
32
30
  },
33
31
  },
34
32
  y: {
35
- scaleLabel: {
36
- display: true,
37
- labelString: 'Items remaining'
38
- },
39
- title: {
40
- display: true,
41
- text: "<%= y_axis_title %>"
42
- },
33
+ <%= render_axis_title :y %>
43
34
  min: 0.0,
44
35
  grid: {
45
36
  color: <%= CssVariable['--grid-line-color'].to_json %>
@@ -56,32 +47,29 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
56
47
  },
57
48
  annotation: {
58
49
  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 %>
50
+ <%= working_days_annotation %>
69
51
  }
70
52
  }
71
53
  }
72
54
  }
73
55
  });
74
56
  </script>
57
+ <%= seam_end %>
75
58
 
76
59
  <%
77
60
  link_id = next_id
78
61
  issues_id = next_id
79
62
  %>
80
- [<a id='<%= link_id %>' href="#" onclick='expand_collapse("<%= link_id %>", "<%= issues_id %>"); return false;'>Show details</a>]
81
- <div id="<%= issues_id %>" style="display: none;">
63
+ <section>
64
+ <div class='foldable startFolded'>Show statistics</div>
65
+ <div id="<%= issues_id %>">
66
+ <%= seam_start 'stats_table' %>
82
67
  <table class='standard' style="margin-left: 1em;">
83
68
  <thead>
84
69
  <th>Sprint</th>
70
+ <th>Started</th>
71
+ <th>Completed</th>
72
+ <th>Days</th>
85
73
  <th>State</th>
86
74
  <th>Started</th>
87
75
  <th>Completed</th>
@@ -94,6 +82,9 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
94
82
  <% @summary_stats.keys.sort_by(&:start_time).each do |sprint| %>
95
83
  <tr>
96
84
  <td><%= sprint.name %></td>
85
+ <td><%= sprint.start_time.to_date %></td>
86
+ <td><%= sprint.completed_time&.to_date %></td>
87
+ <td><%= sprint.day_count %></td>
97
88
  <td><%= sprint.raw['state'] %></td>
98
89
  <% stats = @summary_stats[sprint] %>
99
90
  <td><%= stats.started %></td>
@@ -110,6 +101,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
110
101
  <% end %>
111
102
  </tbody>
112
103
  </table>
104
+ <%= seam_end 'stats_table' %>
113
105
 
114
106
  <p>Legend:
115
107
  <ul>
@@ -118,4 +110,5 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
118
110
  <% end %>
119
111
  </ul>
120
112
  </p>
121
- </div>
113
+ </div>
114
+ </section>
@@ -1,8 +1,13 @@
1
-
1
+ <%= seam_start %>
2
2
  <div class="chart">
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,28 +45,49 @@ 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
- <% 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 %>
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();
65
87
  }
66
88
  }
67
89
  }
68
90
  }
69
91
  });
70
92
  </script>
71
-
93
+ <%= seam_end %>
@@ -0,0 +1,123 @@
1
+ <div>
2
+ <%= seam_start %>
3
+ <div class="chart">
4
+ <canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
5
+ </div>
6
+ <script>
7
+ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
8
+ {
9
+ type: 'bar',
10
+ data: {
11
+ datasets: <%= JSON.generate(data_sets) %>
12
+ },
13
+ options: {
14
+ responsive: <%= canvas_responsive? %>, // If responsive is true then it fills the screen
15
+ scales: {
16
+ x: {
17
+ type: 'linear',
18
+ stacked: true,
19
+ <%= render_axis_title :x %>
20
+ grid: {
21
+ color: <%= CssVariable['--grid-line-color'].to_json %>
22
+ },
23
+ min: 0,
24
+ <%= @max_x_value.nil? ? '' : "max: #{@max_x_value}," %>
25
+ offset: false, // Gets rid of the ugly padding on left.
26
+ },
27
+ y: {
28
+ stacked: true,
29
+ <%= render_axis_title :y %>
30
+ grid: {
31
+ color: <%= CssVariable['--grid-line-color'].to_json %>
32
+ },
33
+ }
34
+ },
35
+ plugins: {
36
+ annotation: {
37
+ annotations: {
38
+ <%
39
+ results = the_stats[:all][:percentiles]
40
+ results.each do |percentile, value|
41
+ %>
42
+ percentile<%= percentile.to_s %>: {
43
+ type: 'line',
44
+ scaleID: 'x',
45
+ value: <%= value %>,
46
+ borderWidth: 1,
47
+ drawTime: 'beforeDatasetsDraw',
48
+ label: {
49
+ enabled: true,
50
+ content: '<%= "#{percentile}%" %>',
51
+ position: 'start',
52
+ }
53
+ },
54
+ <% end %>
55
+ },
56
+ },
57
+ tooltip: {
58
+ callbacks: {
59
+ label: function(context) {
60
+ return context.dataset.data[context.dataIndex].title
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ });
67
+ </script>
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>