jirametrics 2.13 → 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 (87) 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 +9 -7
  7. data/lib/jirametrics/anonymizer.rb +81 -6
  8. data/lib/jirametrics/atlassian_document_format.rb +101 -97
  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 +32 -8
  12. data/lib/jirametrics/board_config.rb +4 -1
  13. data/lib/jirametrics/board_feature.rb +14 -0
  14. data/lib/jirametrics/board_movement_calculator.rb +2 -2
  15. data/lib/jirametrics/cfd_data_builder.rb +108 -0
  16. data/lib/jirametrics/change_item.rb +14 -6
  17. data/lib/jirametrics/chart_base.rb +141 -3
  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} +21 -4
  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 +85 -53
  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 +117 -100
  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 +42 -4
  35. data/lib/jirametrics/examples/aggregated_project.rb +2 -2
  36. data/lib/jirametrics/examples/standard_project.rb +41 -28
  37. data/lib/jirametrics/expedited_chart.rb +3 -1
  38. data/lib/jirametrics/exporter.rb +26 -6
  39. data/lib/jirametrics/file_config.rb +9 -11
  40. data/lib/jirametrics/file_system.rb +59 -3
  41. data/lib/jirametrics/fix_version.rb +13 -0
  42. data/lib/jirametrics/flow_efficiency_scatterplot.rb +5 -1
  43. data/lib/jirametrics/github_gateway.rb +115 -0
  44. data/lib/jirametrics/groupable_issue_chart.rb +11 -1
  45. data/lib/jirametrics/grouping_rules.rb +26 -4
  46. data/lib/jirametrics/html/aging_work_bar_chart.erb +5 -5
  47. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +3 -1
  48. data/lib/jirametrics/html/aging_work_table.erb +5 -0
  49. data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
  50. data/lib/jirametrics/html/cumulative_flow_diagram.erb +503 -0
  51. data/lib/jirametrics/html/daily_wip_chart.erb +40 -5
  52. data/lib/jirametrics/html/estimate_accuracy_chart.erb +4 -12
  53. data/lib/jirametrics/html/expedited_chart.erb +6 -14
  54. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +4 -8
  55. data/lib/jirametrics/html/index.css +249 -69
  56. data/lib/jirametrics/html/index.erb +9 -35
  57. data/lib/jirametrics/html/index.js +164 -0
  58. data/lib/jirametrics/html/legacy_colors.css +174 -0
  59. data/lib/jirametrics/html/sprint_burndown.erb +17 -15
  60. data/lib/jirametrics/html/throughput_chart.erb +42 -11
  61. data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +61 -59
  62. data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +15 -11
  63. data/lib/jirametrics/html/wip_by_column_chart.erb +250 -0
  64. data/lib/jirametrics/html_generator.rb +32 -0
  65. data/lib/jirametrics/html_report_config.rb +52 -57
  66. data/lib/jirametrics/issue.rb +304 -101
  67. data/lib/jirametrics/issue_printer.rb +97 -0
  68. data/lib/jirametrics/jira_gateway.rb +77 -17
  69. data/lib/jirametrics/mcp_server.rb +531 -0
  70. data/lib/jirametrics/project_config.rb +128 -12
  71. data/lib/jirametrics/pull_request.rb +30 -0
  72. data/lib/jirametrics/pull_request_cycle_time_histogram.rb +77 -0
  73. data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +88 -0
  74. data/lib/jirametrics/pull_request_review.rb +13 -0
  75. data/lib/jirametrics/raw_javascript.rb +17 -0
  76. data/lib/jirametrics/settings.json +5 -1
  77. data/lib/jirametrics/sprint.rb +12 -0
  78. data/lib/jirametrics/sprint_burndown.rb +10 -4
  79. data/lib/jirametrics/status.rb +1 -1
  80. data/lib/jirametrics/stitcher.rb +81 -0
  81. data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
  82. data/lib/jirametrics/throughput_chart.rb +73 -23
  83. data/lib/jirametrics/time_based_histogram.rb +139 -0
  84. data/lib/jirametrics/time_based_scatterplot.rb +107 -0
  85. data/lib/jirametrics/wip_by_column_chart.rb +236 -0
  86. data/lib/jirametrics.rb +83 -69
  87. metadata +60 -6
@@ -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,38 +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
- 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, *_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
+
50
75
  def cycletime label = nil, &block
51
76
  @file_config.project_config.all_boards.each_value do |board|
52
77
  raise 'Multiple cycletimes not supported' if board.cycletime
53
78
 
54
- 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
+ )
55
83
  end
56
84
  end
57
85
 
@@ -70,10 +98,8 @@ class HtmlReportConfig
70
98
 
71
99
  html create_footer
72
100
 
73
- html_directory = "#{Pathname.new(File.realpath(__FILE__)).dirname}/html"
74
- css = load_css html_directory: html_directory
75
- erb = ERB.new file_system.load(File.join(html_directory, 'index.erb'))
76
- 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
77
103
  end
78
104
 
79
105
  def file_system
@@ -84,24 +110,6 @@ class HtmlReportConfig
84
110
  file_system.log message
85
111
  end
86
112
 
87
- def load_css html_directory:
88
- base_css_filename = File.join(html_directory, 'index.css')
89
- base_css = file_system.load(base_css_filename)
90
- log("Loaded CSS: #{base_css_filename}")
91
-
92
- extra_css_filename = settings['include_css']
93
- if extra_css_filename
94
- if File.exist?(extra_css_filename)
95
- base_css << "\n\n" << file_system.load(extra_css_filename)
96
- log("Loaded CSS: #{extra_css_filename}")
97
- else
98
- log("Unable to find specified CSS file: #{extra_css_filename}")
99
- end
100
- end
101
-
102
- base_css
103
- end
104
-
105
113
  def board_id id
106
114
  @board_id = id
107
115
  end
@@ -110,24 +118,9 @@ class HtmlReportConfig
110
118
  @file_config.project_config.exporter.timezone_offset
111
119
  end
112
120
 
113
- def aging_work_in_progress_chart board_id: nil, &block
114
- block ||= ->(_) {}
115
-
116
- if board_id.nil?
117
- ids = issues.collect { |i| i.board.id }.uniq.sort
118
- else
119
- ids = [board_id]
120
- end
121
-
122
- ids.each do |id|
123
- execute_chart(AgingWorkInProgressChart.new(block)) do |chart|
124
- chart.board_id = id
125
- end
126
- end
127
- end
128
-
129
121
  def random_color
130
- "##{Random.bytes(3).unpack1('H*')}"
122
+ @palette_index = (@palette_index || -1) + 1
123
+ ChartBase::OKABE_ITO_PALETTE[@palette_index % ChartBase::OKABE_ITO_PALETTE.size]
131
124
  end
132
125
 
133
126
  def html string, type: :body
@@ -160,11 +153,12 @@ class HtmlReportConfig
160
153
  chart.time_range = project_config.time_range
161
154
  chart.timezone_offset = timezone_offset
162
155
  chart.settings = settings
163
- chart.users = project_config.users
156
+ chart.atlassian_document_format = project_config.atlassian_document_format
164
157
 
165
158
  chart.all_boards = project_config.all_boards
166
159
  chart.board_id = find_board_id
167
160
  chart.holiday_dates = project_config.exporter.holiday_dates
161
+ chart.fix_versions = project_config.fix_versions
168
162
 
169
163
  time_range = @file_config.project_config.time_range
170
164
  chart.date_range = time_range.begin.to_date..time_range.end.to_date
@@ -173,6 +167,7 @@ class HtmlReportConfig
173
167
  after_init_block&.call chart
174
168
 
175
169
  @charts << chart
170
+ chart.before_run
176
171
  html chart.run
177
172
  end
178
173