jirametrics 2.11 → 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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/bin/jirametrics-mcp +5 -0
  3. data/lib/jirametrics/aggregate_config.rb +10 -2
  4. data/lib/jirametrics/aging_work_bar_chart.rb +191 -133
  5. data/lib/jirametrics/aging_work_in_progress_chart.rb +43 -11
  6. data/lib/jirametrics/aging_work_table.rb +14 -17
  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 +5 -3
  11. data/lib/jirametrics/board.rb +34 -11
  12. data/lib/jirametrics/board_config.rb +5 -1
  13. data/lib/jirametrics/board_feature.rb +14 -0
  14. data/lib/jirametrics/board_movement_calculator.rb +10 -2
  15. data/lib/jirametrics/cfd_data_builder.rb +108 -0
  16. data/lib/jirametrics/change_item.rb +43 -20
  17. data/lib/jirametrics/chart_base.rb +143 -6
  18. data/lib/jirametrics/css_variable.rb +1 -1
  19. data/lib/jirametrics/cumulative_flow_diagram.rb +208 -0
  20. data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +22 -5
  21. data/lib/jirametrics/cycletime_histogram.rb +15 -101
  22. data/lib/jirametrics/cycletime_scatterplot.rb +17 -83
  23. data/lib/jirametrics/daily_view.rb +306 -0
  24. data/lib/jirametrics/daily_wip_by_age_chart.rb +4 -5
  25. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +14 -4
  26. data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -2
  27. data/lib/jirametrics/daily_wip_chart.rb +30 -8
  28. data/lib/jirametrics/data_quality_report.rb +43 -12
  29. data/lib/jirametrics/dependency_chart.rb +6 -3
  30. data/lib/jirametrics/download_config.rb +15 -0
  31. data/lib/jirametrics/downloader.rb +128 -71
  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 +74 -12
  35. data/lib/jirametrics/estimation_configuration.rb +25 -0
  36. data/lib/jirametrics/examples/aggregated_project.rb +2 -2
  37. data/lib/jirametrics/examples/standard_project.rb +42 -27
  38. data/lib/jirametrics/expedited_chart.rb +3 -1
  39. data/lib/jirametrics/exporter.rb +26 -6
  40. data/lib/jirametrics/file_config.rb +10 -12
  41. data/lib/jirametrics/file_system.rb +59 -3
  42. data/lib/jirametrics/fix_version.rb +13 -0
  43. data/lib/jirametrics/flow_efficiency_scatterplot.rb +5 -1
  44. data/lib/jirametrics/github_gateway.rb +115 -0
  45. data/lib/jirametrics/groupable_issue_chart.rb +11 -1
  46. data/lib/jirametrics/grouping_rules.rb +26 -4
  47. data/lib/jirametrics/html/aging_work_bar_chart.erb +5 -5
  48. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +3 -1
  49. data/lib/jirametrics/html/aging_work_table.erb +7 -0
  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 +40 -5
  53. data/lib/jirametrics/html/estimate_accuracy_chart.erb +4 -12
  54. data/lib/jirametrics/html/expedited_chart.erb +6 -14
  55. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +4 -8
  56. data/lib/jirametrics/html/index.css +320 -69
  57. data/lib/jirametrics/html/index.erb +11 -20
  58. data/lib/jirametrics/html/index.js +164 -0
  59. data/lib/jirametrics/html/legacy_colors.css +174 -0
  60. data/lib/jirametrics/html/sprint_burndown.erb +17 -15
  61. data/lib/jirametrics/html/throughput_chart.erb +42 -11
  62. data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +61 -59
  63. data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +15 -11
  64. data/lib/jirametrics/html/wip_by_column_chart.erb +250 -0
  65. data/lib/jirametrics/html_generator.rb +32 -0
  66. data/lib/jirametrics/html_report_config.rb +52 -55
  67. data/lib/jirametrics/issue.rb +329 -106
  68. data/lib/jirametrics/issue_collection.rb +33 -0
  69. data/lib/jirametrics/issue_printer.rb +97 -0
  70. data/lib/jirametrics/jira_gateway.rb +81 -14
  71. data/lib/jirametrics/mcp_server.rb +531 -0
  72. data/lib/jirametrics/project_config.rb +151 -18
  73. data/lib/jirametrics/pull_request.rb +30 -0
  74. data/lib/jirametrics/pull_request_cycle_time_histogram.rb +77 -0
  75. data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +88 -0
  76. data/lib/jirametrics/pull_request_review.rb +13 -0
  77. data/lib/jirametrics/raw_javascript.rb +17 -0
  78. data/lib/jirametrics/settings.json +6 -1
  79. data/lib/jirametrics/sprint.rb +13 -0
  80. data/lib/jirametrics/sprint_burndown.rb +45 -37
  81. data/lib/jirametrics/sprint_issue_change_data.rb +3 -3
  82. data/lib/jirametrics/status.rb +1 -1
  83. data/lib/jirametrics/status_collection.rb +7 -0
  84. data/lib/jirametrics/stitcher.rb +81 -0
  85. data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
  86. data/lib/jirametrics/throughput_chart.rb +73 -23
  87. data/lib/jirametrics/time_based_histogram.rb +139 -0
  88. data/lib/jirametrics/time_based_scatterplot.rb +107 -0
  89. data/lib/jirametrics/user.rb +12 -0
  90. data/lib/jirametrics/wip_by_column_chart.rb +236 -0
  91. data/lib/jirametrics.rb +83 -64
  92. metadata +65 -6
@@ -1,3 +1,4 @@
1
+ <%= seam_start %>
1
2
  <div class="chart">
2
3
  <canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
3
4
  </div>
@@ -10,36 +11,37 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
10
11
  options: {
11
12
  title: {
12
13
  display: true,
13
- text: "Cycletime Scatterplot"
14
+ text: "<%= @header_text %>"
14
15
  },
15
16
  responsive: <%= canvas_responsive? %>, // If responsive is true then it fills the screen
16
17
  scales: {
17
18
  x: {
18
19
  type: "time",
19
20
  scaleLabel: {
20
- display: true,
21
- labelString: 'Date Completed'
21
+ display: true
22
22
  },
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
- labelString: 'Days',
33
- min: 0,
34
- max: <%= @highest_cycletime %>
35
- },
36
- title: {
37
- display: true,
38
- text: 'Cycle time in days'
34
+ display: true
39
35
  },
36
+ <%= render_axis_title :y %>
40
37
  grid: {
41
38
  color: <%= CssVariable['--grid-line-color'].to_json %>
42
39
  },
40
+ ticks: {
41
+ callback: function(value, index, ticks) {
42
+ return index === ticks.length - 1 ? null : value;
43
+ }
44
+ }
43
45
  }
44
46
  },
45
47
  plugins: {
@@ -54,6 +56,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
54
56
  annotation: {
55
57
  annotations: {
56
58
  <%= working_days_annotation %>
59
+ <%= date_annotation %>
57
60
 
58
61
  <% @percentage_lines.each_with_index do |args, index| %>
59
62
  <% percent, color = args %>
@@ -98,3 +101,4 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
98
101
  }
99
102
  });
100
103
  </script>
104
+ <%= seam_end %>
@@ -0,0 +1,250 @@
1
+ <%= seam_start %>
2
+ <div class="chart" style="position:relative;">
3
+ <canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
4
+ <div id="<%= chart_id %>-tooltip" style="
5
+ display:none; position:absolute; pointer-events:none;
6
+ background:rgba(0,0,0,0.75); color:#fff; border-radius:4px;
7
+ padding:4px 8px; font:12px sans-serif; white-space:nowrap;
8
+ "></div>
9
+ </div>
10
+ <script>
11
+ (function() {
12
+ var wipData = <%= @wip_data.to_json %>;
13
+ var wipLimits = <%= @wip_limits.to_json %>;
14
+ var recommendations = <%= @recommendations.to_json %>;
15
+ var maxWip = <%= @max_wip %>;
16
+ var gridColor = <%= CssVariable['--grid-line-color'].to_json %>;
17
+ var barFillColor = <%= CssVariable['--wip-by-column-chart-bar-fill-color'].to_json %>;
18
+ var barTextColor = <%= CssVariable['--wip-by-column-chart-bar-text-color'].to_json %>;
19
+ var limitColor = <%= CssVariable['--wip-by-column-chart-limit-line-color'].to_json %>;
20
+ var recColor = <%= CssVariable['--wip-by-column-chart-recommendation-color'].to_json %>;
21
+ var tooltipEl = document.getElementById(<%= "#{chart_id}-tooltip".inspect %>);
22
+
23
+ var hitAreas = [];
24
+
25
+ var rectPlugin = {
26
+ id: 'wipRects',
27
+ afterDraw: function(chart) {
28
+ var ctx = chart.ctx;
29
+ var xScale = chart.scales['x'];
30
+ var yScale = chart.scales['y'];
31
+ var slotWidth = xScale.width / Math.max(xScale.ticks.length, 1);
32
+
33
+ hitAreas = [];
34
+
35
+ // 1. Draw y-axis gridlines at integer band boundaries
36
+ ctx.save();
37
+ ctx.beginPath();
38
+ ctx.rect(xScale.left, yScale.top, xScale.right - xScale.left, yScale.bottom - yScale.top);
39
+ ctx.clip();
40
+ ctx.strokeStyle = gridColor;
41
+ ctx.lineWidth = 1;
42
+ ctx.setLineDash([]);
43
+ for (var gi = 0; gi <= maxWip + 1; gi++) {
44
+ var gy = yScale.getPixelForValue(gi);
45
+ ctx.beginPath();
46
+ ctx.moveTo(xScale.left, gy);
47
+ ctx.lineTo(xScale.right, gy);
48
+ ctx.stroke();
49
+ }
50
+ ctx.restore();
51
+
52
+ // 2. Draw WIP limit lines (behind rectangles)
53
+ wipLimits.forEach(function(limits, colIndex) {
54
+ var xCenter = xScale.getPixelForValue(colIndex);
55
+ var halfSlot = slotWidth * 0.45;
56
+
57
+ [['min', 'bottom'], ['max', 'top']].forEach(function(pair) {
58
+ var type = pair[0];
59
+ var baseline = pair[1];
60
+ var val = limits[type];
61
+ if (val === null || val === undefined) return;
62
+
63
+ var y = yScale.getPixelForValue(val + 0.5);
64
+
65
+ ctx.save();
66
+ ctx.strokeStyle = limitColor;
67
+ ctx.lineWidth = 2;
68
+ ctx.setLineDash([5, 3]);
69
+ ctx.beginPath();
70
+ ctx.moveTo(xCenter - halfSlot, y);
71
+ ctx.lineTo(xCenter + halfSlot, y);
72
+ ctx.stroke();
73
+ ctx.restore();
74
+
75
+ ctx.save();
76
+ ctx.fillStyle = limitColor;
77
+ ctx.font = 'bold 10px sans-serif';
78
+ ctx.textAlign = 'right';
79
+ ctx.textBaseline = baseline;
80
+ ctx.fillText(type + ': ' + val, xCenter + halfSlot, baseline === 'bottom' ? y - 2 : y + 2);
81
+ ctx.restore();
82
+ });
83
+ });
84
+
85
+ <% if @show_recommendations %>
86
+ // 3. Draw recommendation lines (behind rectangles, label on left)
87
+ recommendations.forEach(function(rec, colIndex) {
88
+ if (rec === null || rec === undefined || rec === 0) return;
89
+ var xCenter = xScale.getPixelForValue(colIndex);
90
+ var halfSlot = slotWidth * 0.45;
91
+ var y = yScale.getPixelForValue(rec + 0.5);
92
+
93
+ ctx.save();
94
+ ctx.strokeStyle = recColor;
95
+ ctx.lineWidth = 2;
96
+ ctx.setLineDash([5, 3]);
97
+ ctx.beginPath();
98
+ ctx.moveTo(xCenter - halfSlot, y);
99
+ ctx.lineTo(xCenter + halfSlot, y);
100
+ ctx.stroke();
101
+ ctx.restore();
102
+
103
+ ctx.save();
104
+ ctx.fillStyle = recColor;
105
+ ctx.font = 'bold 10px sans-serif';
106
+ ctx.textAlign = 'left';
107
+ ctx.textBaseline = 'top';
108
+ ctx.fillText('rec: ' + rec, xCenter - halfSlot, y + 2);
109
+ ctx.restore();
110
+ });
111
+ <% end %>
112
+
113
+ // 4. Draw WIP rectangles centered in their bands (wip + 0.5)
114
+ var yStep = Math.abs(yScale.getPixelForValue(0.5) - yScale.getPixelForValue(1.5));
115
+
116
+ wipData.forEach(function(colData, colIndex) {
117
+ var xCenter = xScale.getPixelForValue(colIndex);
118
+
119
+ colData.forEach(function(entry) {
120
+ var wip = entry['wip'];
121
+ var pct = entry['pct'];
122
+ var rectWidth = slotWidth * pct / 100;
123
+ var rectHeight = yStep * 0.8;
124
+ var yCenter = yScale.getPixelForValue(wip + 0.5);
125
+ var x1 = xCenter - rectWidth / 2;
126
+ var y1 = yCenter - rectHeight / 2;
127
+
128
+ ctx.save();
129
+ ctx.fillStyle = barFillColor;
130
+ ctx.strokeStyle = barFillColor;
131
+ ctx.lineWidth = 1;
132
+ ctx.fillRect(x1, y1, rectWidth, rectHeight);
133
+ ctx.strokeRect(x1, y1, rectWidth, rectHeight);
134
+
135
+ ctx.fillStyle = barTextColor;
136
+ ctx.font = '11px sans-serif';
137
+ ctx.textAlign = 'center';
138
+ ctx.textBaseline = 'middle';
139
+ if (rectWidth > 25) {
140
+ ctx.fillText(pct + '%', xCenter, yCenter);
141
+ }
142
+ ctx.restore();
143
+
144
+ var hitWidth = Math.max(rectWidth, slotWidth);
145
+ hitAreas.push({
146
+ x1: xCenter - hitWidth / 2, y1: y1,
147
+ x2: xCenter + hitWidth / 2, y2: y1 + rectHeight,
148
+ label: 'WIP ' + wip + ': ' + pct + '%'
149
+ });
150
+ });
151
+ });
152
+ }
153
+ };
154
+
155
+ var canvas = document.getElementById(<%= chart_id.inspect %>);
156
+
157
+ canvas.addEventListener('mousemove', function(e) {
158
+ var rect = canvas.getBoundingClientRect();
159
+ var mx = e.clientX - rect.left;
160
+ var my = e.clientY - rect.top;
161
+
162
+ var hit = null;
163
+ for (var i = 0; i < hitAreas.length; i++) {
164
+ var a = hitAreas[i];
165
+ if (mx >= a.x1 && mx <= a.x2 && my >= a.y1 && my <= a.y2) {
166
+ hit = a;
167
+ break;
168
+ }
169
+ }
170
+
171
+ if (hit) {
172
+ tooltipEl.textContent = hit.label;
173
+ tooltipEl.style.display = 'block';
174
+ tooltipEl.style.left = (mx + 10) + 'px';
175
+ tooltipEl.style.top = (my - 20) + 'px';
176
+ } else {
177
+ tooltipEl.style.display = 'none';
178
+ }
179
+ });
180
+
181
+ canvas.addEventListener('mouseleave', function() {
182
+ tooltipEl.style.display = 'none';
183
+ });
184
+
185
+ new Chart(canvas.getContext('2d'),
186
+ {
187
+ type: 'bar',
188
+ plugins: [rectPlugin],
189
+ data: {
190
+ labels: <%= @column_names.to_json %>,
191
+ datasets: [{
192
+ data: [],
193
+ backgroundColor: 'transparent'
194
+ }]
195
+ },
196
+ options: {
197
+ responsive: <%= canvas_responsive? %>,
198
+ scales: {
199
+ x: {
200
+ grid: {
201
+ color: gridColor,
202
+ z: 1
203
+ }
204
+ },
205
+ y: {
206
+ title: {
207
+ display: true,
208
+ text: 'WIP'
209
+ },
210
+ grid: {
211
+ display: false
212
+ },
213
+ min: 0,
214
+ max: <%= @max_wip + 1 %>,
215
+ afterBuildTicks: function(scale) {
216
+ scale.ticks = [];
217
+ for (var i = 0; i <= maxWip; i++) {
218
+ scale.ticks.push({ value: i + 0.5 });
219
+ }
220
+ },
221
+ ticks: {
222
+ callback: function(value) {
223
+ return Math.round(value - 0.5);
224
+ }
225
+ }
226
+ }
227
+ },
228
+ plugins: {
229
+ legend: {
230
+ display: false
231
+ },
232
+ tooltip: {
233
+ enabled: false
234
+ }
235
+ }
236
+ }
237
+ });
238
+ })();
239
+ </script>
240
+ <%= seam_end %>
241
+ <% unless @recommendation_texts.empty? %>
242
+ <div style="margin-top: 0.5em;">
243
+ <strong>WIP limit recommendations</strong>
244
+ <ul style="margin: 0.3em 0 0 0; padding-left: 1.5em;">
245
+ <% @recommendation_texts.each do |text| %>
246
+ <li><%= text %></li>
247
+ <% end %>
248
+ </ul>
249
+ </div>
250
+ <% end %>
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class HtmlGenerator
4
+ attr_accessor :file_system, :settings
5
+
6
+ def create_html output_filename:, settings:, project_name: ''
7
+ @settings = settings
8
+ project_name = project_name.to_s
9
+ html_directory = "#{Pathname.new(File.realpath(__FILE__)).dirname}/html"
10
+ css = load_css html_directory: html_directory
11
+ javascript = file_system.load(File.join(html_directory, 'index.js'))
12
+ erb = ERB.new file_system.load(File.join(html_directory, 'index.erb'))
13
+ file_system.save_file content: erb.result(binding), filename: output_filename
14
+ end
15
+
16
+ def load_css html_directory:
17
+ base_css_filename = File.join(html_directory, 'index.css')
18
+ base_css = file_system.load(base_css_filename)
19
+
20
+ extra_css_filename = settings['include_css']
21
+ if extra_css_filename
22
+ if File.exist?(extra_css_filename)
23
+ base_css << "\n\n" << file_system.load(extra_css_filename)
24
+ log("Loaded CSS: #{extra_css_filename}")
25
+ else
26
+ log("Unable to find specified CSS file: #{extra_css_filename}")
27
+ end
28
+ end
29
+
30
+ base_css
31
+ end
32
+ end
@@ -3,7 +3,7 @@
3
3
  require 'erb'
4
4
  require 'jirametrics/self_or_issue_dispatcher'
5
5
 
6
- class HtmlReportConfig
6
+ class HtmlReportConfig < HtmlGenerator
7
7
  include SelfOrIssueDispatcher
8
8
 
9
9
  attr_reader :file_config, :sections, :charts
@@ -20,37 +20,66 @@ class HtmlReportConfig
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
-
37
23
  define_chart name: 'daily_wip_by_type', classname: 'DailyWipChart',
38
24
  deprecated_warning: 'This is the same as daily_wip_chart. Please use that one', deprecated_date: '2024-05-23'
39
25
  define_chart name: 'story_point_accuracy_chart', classname: 'EstimateAccuracyChart',
40
26
  deprecated_warning: 'Renamed to estimate_accuracy_chart. Please use that one', deprecated_date: '2024-05-23'
41
27
 
42
28
  def initialize file_config:, block:
29
+ super()
43
30
  @file_config = file_config
44
31
  @block = block
45
32
  @sections = [] # Where we store the chunks of text that will be assembled into the HTML
46
33
  @charts = [] # Where we store all the charts we executed so we can assert against them.
47
34
  end
48
35
 
36
+ def method_missing name, *_args, board_id: nil, **_kwargs, &block
37
+ class_name = name.to_s.split('_').map(&:capitalize).join
38
+ klass = resolve_chart_class(class_name)
39
+ return super if klass.nil?
40
+
41
+ block ||= ->(_) {}
42
+
43
+ if klass.instance_method(:board_id=).owner == klass
44
+ execute_chart_per_board klass: klass, block: block, board_id: board_id
45
+ else
46
+ execute_chart klass.new(block)
47
+ end
48
+ end
49
+
50
+ def resolve_chart_class class_name
51
+ klass = Object.const_get(class_name)
52
+ klass < ChartBase ? klass : nil
53
+ rescue NameError
54
+ nil
55
+ end
56
+
57
+ def execute_chart_per_board klass:, block:, board_id:
58
+ all_boards = @file_config.project_config.all_boards
59
+ ids = board_id ? [board_id] : issues.collect { |i| i.board.id }.uniq
60
+ ids = ids.sort_by { |id| all_boards[id]&.name || '' }
61
+ ids.each_with_index do |id, index|
62
+ execute_chart(klass.new(block)) do |chart|
63
+ chart.board_id = id
64
+ # We're showing the description only on the first one in order to reduce noise on the report
65
+ chart.description_text nil unless index.zero?
66
+ end
67
+ end
68
+ end
69
+
70
+ def respond_to_missing? name, include_private = false
71
+ class_name = name.to_s.split('_').map(&:capitalize).join
72
+ !resolve_chart_class(class_name).nil? || super
73
+ end
74
+
49
75
  def cycletime label = nil, &block
50
76
  @file_config.project_config.all_boards.each_value do |board|
51
77
  raise 'Multiple cycletimes not supported' if board.cycletime
52
78
 
53
- board.cycletime = CycleTimeConfig.new(parent_config: self, label: label, block: block, file_system: file_system)
79
+ board.cycletime = CycleTimeConfig.new(
80
+ possible_statuses: file_config.project_config, label: label, block: block,
81
+ file_system: file_system, settings: settings
82
+ )
54
83
  end
55
84
  end
56
85
 
@@ -69,10 +98,8 @@ class HtmlReportConfig
69
98
 
70
99
  html create_footer
71
100
 
72
- html_directory = "#{Pathname.new(File.realpath(__FILE__)).dirname}/html"
73
- css = load_css html_directory: html_directory
74
- erb = ERB.new file_system.load(File.join(html_directory, 'index.erb'))
75
- file_system.save_file content: erb.result(binding), filename: @file_config.output_filename
101
+ create_html output_filename: @file_config.output_filename, settings: settings,
102
+ project_name: @file_config.project_config.name
76
103
  end
77
104
 
78
105
  def file_system
@@ -83,24 +110,6 @@ class HtmlReportConfig
83
110
  file_system.log message
84
111
  end
85
112
 
86
- def load_css html_directory:
87
- base_css_filename = File.join(html_directory, 'index.css')
88
- base_css = file_system.load(base_css_filename)
89
- log("Loaded CSS: #{base_css_filename}")
90
-
91
- extra_css_filename = settings['include_css']
92
- if extra_css_filename
93
- if File.exist?(extra_css_filename)
94
- base_css << "\n\n" << file_system.load(extra_css_filename)
95
- log("Loaded CSS: #{extra_css_filename}")
96
- else
97
- log("Unable to find specified CSS file: #{extra_css_filename}")
98
- end
99
- end
100
-
101
- base_css
102
- end
103
-
104
113
  def board_id id
105
114
  @board_id = id
106
115
  end
@@ -109,24 +118,9 @@ class HtmlReportConfig
109
118
  @file_config.project_config.exporter.timezone_offset
110
119
  end
111
120
 
112
- def aging_work_in_progress_chart board_id: nil, &block
113
- block ||= ->(_) {}
114
-
115
- if board_id.nil?
116
- ids = issues.collect { |i| i.board.id }.uniq.sort
117
- else
118
- ids = [board_id]
119
- end
120
-
121
- ids.each do |id|
122
- execute_chart(AgingWorkInProgressChart.new(block)) do |chart|
123
- chart.board_id = id
124
- end
125
- end
126
- end
127
-
128
121
  def random_color
129
- "##{Random.bytes(3).unpack1('H*')}"
122
+ @palette_index = (@palette_index || -1) + 1
123
+ ChartBase::OKABE_ITO_PALETTE[@palette_index % ChartBase::OKABE_ITO_PALETTE.size]
130
124
  end
131
125
 
132
126
  def html string, type: :body
@@ -159,10 +153,12 @@ class HtmlReportConfig
159
153
  chart.time_range = project_config.time_range
160
154
  chart.timezone_offset = timezone_offset
161
155
  chart.settings = settings
156
+ chart.atlassian_document_format = project_config.atlassian_document_format
162
157
 
163
158
  chart.all_boards = project_config.all_boards
164
159
  chart.board_id = find_board_id
165
160
  chart.holiday_dates = project_config.exporter.holiday_dates
161
+ chart.fix_versions = project_config.fix_versions
166
162
 
167
163
  time_range = @file_config.project_config.time_range
168
164
  chart.date_range = time_range.begin.to_date..time_range.end.to_date
@@ -171,6 +167,7 @@ class HtmlReportConfig
171
167
  after_init_block&.call chart
172
168
 
173
169
  @charts << chart
170
+ chart.before_run
174
171
  html chart.run
175
172
  end
176
173