jirametrics 2.20.1 → 2.22
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.
- checksums.yaml +4 -4
- data/lib/jirametrics/aging_work_bar_chart.rb +176 -134
- data/lib/jirametrics/bar_chart_range.rb +17 -0
- data/lib/jirametrics/board.rb +4 -0
- data/lib/jirametrics/board_config.rb +2 -1
- data/lib/jirametrics/change_item.rb +10 -3
- data/lib/jirametrics/chart_base.rb +31 -0
- data/lib/jirametrics/cycletime_config.rb +4 -5
- data/lib/jirametrics/cycletime_scatterplot.rb +36 -17
- data/lib/jirametrics/daily_wip_by_age_chart.rb +3 -4
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +13 -3
- data/lib/jirametrics/daily_wip_chart.rb +1 -1
- data/lib/jirametrics/data_quality_report.rb +2 -0
- data/lib/jirametrics/exporter.rb +4 -2
- data/lib/jirametrics/fix_version.rb +13 -0
- data/lib/jirametrics/groupable_issue_chart.rb +7 -1
- data/lib/jirametrics/html/aging_work_bar_chart.erb +2 -1
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +2 -0
- data/lib/jirametrics/html/aging_work_table.erb +2 -0
- data/lib/jirametrics/html/cycletime_histogram.erb +2 -0
- data/lib/jirametrics/html/cycletime_scatterplot.erb +6 -6
- data/lib/jirametrics/html/daily_wip_chart.erb +2 -0
- data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -0
- data/lib/jirametrics/html/expedited_chart.erb +3 -1
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -0
- data/lib/jirametrics/html/index.css +17 -0
- data/lib/jirametrics/html/index.erb +1 -1
- data/lib/jirametrics/html/index.js +24 -0
- data/lib/jirametrics/html/sprint_burndown.erb +6 -0
- data/lib/jirametrics/html/throughput_chart.erb +2 -2
- data/lib/jirametrics/html_generator.rb +31 -0
- data/lib/jirametrics/html_report_config.rb +5 -24
- data/lib/jirametrics/issue.rb +97 -4
- data/lib/jirametrics/project_config.rb +12 -2
- data/lib/jirametrics/raw_javascript.rb +13 -0
- data/lib/jirametrics/sprint.rb +12 -0
- data/lib/jirametrics/sprint_burndown.rb +6 -2
- data/lib/jirametrics/stitcher.rb +75 -0
- data/lib/jirametrics.rb +8 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 40a0ee85ee8d7d0d2ff071357afdea2aefdfeea8734f96eb789721d4a9f2607b
|
|
4
|
+
data.tar.gz: 11008f97848d8e3034cf95c5f615496c5677d8057f40b06d2724247d6087318d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1e5ad6c1d5dddf5a89cc63498f1967f35c4761b6418c90019c8c6756599efb5b8badaf0ac4d50f94be4644508a97641430f178b3d40217cb02560aa017f33b80
|
|
7
|
+
data.tar.gz: a6a7f74dadbb8a2f7961a02e396a39dd994ff1a11756f57a5eadfd60af5c71bd79c3df65b7bfd77ca998059018f60cd6306b2006512b7cf5f760d75e2d0dfdf4
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'jirametrics/chart_base'
|
|
4
|
+
require 'jirametrics/bar_chart_range'
|
|
4
5
|
|
|
5
6
|
class AgingWorkBarChart < ChartBase
|
|
6
7
|
def initialize block
|
|
7
8
|
super()
|
|
8
9
|
|
|
10
|
+
@age_cutoff = nil
|
|
9
11
|
header_text 'Aging Work Bar Chart'
|
|
10
12
|
description_text <<-HTML
|
|
11
13
|
<p>
|
|
@@ -13,16 +15,16 @@ class AgingWorkBarChart < ChartBase
|
|
|
13
15
|
newest at the bottom.
|
|
14
16
|
</p>
|
|
15
17
|
<p>
|
|
16
|
-
There are
|
|
17
|
-
information relevant to that. Hovering over any of the bars will provide more details.
|
|
18
|
+
There are three bars for each issue, and hovering over any of the bars will provide more details.
|
|
18
19
|
<ol>
|
|
19
|
-
<li>The
|
|
20
|
+
<li>Status: The status the issue was in at any time. The colour indicates the
|
|
20
21
|
status category, which will be one of #{color_block '--status-category-todo-color'} To Do,
|
|
21
22
|
#{color_block '--status-category-inprogress-color'} In Progress,
|
|
22
23
|
or #{color_block '--status-category-done-color'} Done</li>
|
|
23
|
-
<li>
|
|
24
|
+
<li>Activity: This bar indicates #{color_block '--blocked-color'} blocked
|
|
24
25
|
or #{color_block '--stalled-color'} stalled.</li>
|
|
25
|
-
<li>
|
|
26
|
+
<li>Priority: This shows the priority over time. If one of these priorities is considered expedited
|
|
27
|
+
then it will be drawn with diagonal lines.</li>
|
|
26
28
|
</ol>
|
|
27
29
|
</p>
|
|
28
30
|
#{describe_non_working_days}
|
|
@@ -36,6 +38,7 @@ class AgingWorkBarChart < ChartBase
|
|
|
36
38
|
|
|
37
39
|
def run
|
|
38
40
|
aging_issues = select_aging_issues issues: @issues
|
|
41
|
+
adjust_time_date_ranges_to_start_from_earliest_issue_start(aging_issues)
|
|
39
42
|
|
|
40
43
|
today = date_range.end
|
|
41
44
|
sort_by_age! issues: aging_issues, today: today
|
|
@@ -58,134 +61,125 @@ class AgingWorkBarChart < ChartBase
|
|
|
58
61
|
wrap_and_render(binding, __FILE__)
|
|
59
62
|
end
|
|
60
63
|
|
|
64
|
+
def adjust_time_date_ranges_to_start_from_earliest_issue_start aging_issues
|
|
65
|
+
earliest_start_time = aging_issues.collect do |issue|
|
|
66
|
+
issue.board.cycletime.started_stopped_times(issue).first
|
|
67
|
+
end.min
|
|
68
|
+
return if earliest_start_time.nil? || earliest_start_time >= @time_range.begin
|
|
69
|
+
|
|
70
|
+
@time_range = earliest_start_time..@time_range.end
|
|
71
|
+
@date_range = @time_range.begin.to_date..@time_range.end.to_date
|
|
72
|
+
end
|
|
73
|
+
|
|
61
74
|
def data_sets_for_one_issue issue:, today:
|
|
62
75
|
cycletime = issue.board.cycletime
|
|
63
|
-
issue_start_time
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
[
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
issue_label: issue_label,
|
|
71
|
-
stack: 'blocked',
|
|
72
|
-
issue_start_time: issue_start_time
|
|
73
|
-
),
|
|
74
|
-
data_set_by_block(
|
|
75
|
-
issue: issue,
|
|
76
|
-
issue_label: issue_label,
|
|
77
|
-
title_label: 'Expedited',
|
|
78
|
-
stack: 'expedited',
|
|
79
|
-
color: CssVariable['--expedited-color'],
|
|
80
|
-
start_date: issue_start_date
|
|
81
|
-
) { |day| issue.expedited_on_date?(day) }
|
|
76
|
+
issue_start_time = cycletime.started_stopped_times(issue).first
|
|
77
|
+
end_of_today = Time.parse("#{today}T23:59:59#{@timezone_offset}")
|
|
78
|
+
|
|
79
|
+
bar_data = [
|
|
80
|
+
['status', collect_status_ranges(issue: issue, now: end_of_today)],
|
|
81
|
+
['blocked', collect_blocked_stalled_ranges(issue: issue, issue_start_time: issue_start_time)],
|
|
82
|
+
['priority', collect_priority_ranges(issue: issue)]
|
|
82
83
|
]
|
|
84
|
+
bar_data << ['sprints', collect_sprint_ranges(issue: issue)] if current_board.scrum?
|
|
85
|
+
|
|
86
|
+
issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
|
|
87
|
+
bar_data.collect do |stack, ranges|
|
|
88
|
+
bar_chart_range_to_data_set y_value: issue_label, ranges: ranges, stack: stack, issue_start_time: issue_start_time
|
|
89
|
+
end
|
|
83
90
|
end
|
|
84
91
|
|
|
85
92
|
def sort_by_age! issues:, today:
|
|
86
93
|
issues.sort! do |a, b|
|
|
87
|
-
|
|
94
|
+
b.board.cycletime.age(b, today: today) <=> a.board.cycletime.age(a, today: today)
|
|
88
95
|
end
|
|
89
96
|
end
|
|
90
97
|
|
|
91
98
|
def select_aging_issues issues:
|
|
92
99
|
issues.select do |issue|
|
|
93
100
|
started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
|
|
94
|
-
started_time && stopped_time.nil?
|
|
101
|
+
next false unless started_time && stopped_time.nil?
|
|
102
|
+
|
|
103
|
+
age = (date_range.end - started_time.to_date).to_i + 1
|
|
104
|
+
!(@age_cutoff && @age_cutoff >= age)
|
|
95
105
|
end
|
|
96
106
|
end
|
|
97
107
|
|
|
98
108
|
def grow_chart_height_if_too_many_issues aging_issue_count:
|
|
99
|
-
px_per_bar =
|
|
109
|
+
px_per_bar = 10
|
|
100
110
|
bars_per_issue = 3
|
|
111
|
+
bars_per_issue += 1 if current_board.scrum?
|
|
112
|
+
|
|
101
113
|
preferred_height = aging_issue_count * px_per_bar * bars_per_issue
|
|
102
114
|
@canvas_height = preferred_height if @canvas_height.nil? || @canvas_height < preferred_height
|
|
103
115
|
end
|
|
104
116
|
|
|
105
|
-
def
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
issue_started_time, _issue_stopped_time = cycletime.started_stopped_times(issue)
|
|
109
|
-
|
|
117
|
+
def collect_status_ranges issue:, now:
|
|
118
|
+
ranges = []
|
|
119
|
+
issue_started_time = issue.board.cycletime.started_stopped_times(issue).first
|
|
110
120
|
previous_start = nil
|
|
111
121
|
previous_status = nil
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
unless previous_start.nil? || previous_start < issue_started_time
|
|
120
|
-
hash = {
|
|
121
|
-
type: 'bar',
|
|
122
|
-
data: [{
|
|
123
|
-
x: [chart_format(previous_start), chart_format(change.time)],
|
|
124
|
-
y: label,
|
|
125
|
-
title: "#{issue.type} : #{change.value}"
|
|
126
|
-
}],
|
|
127
|
-
backgroundColor: status_category_color(status),
|
|
128
|
-
borderColor: CssVariable['--aging-work-bar-chart-separator-color'],
|
|
129
|
-
borderWidth: {
|
|
130
|
-
top: 0,
|
|
131
|
-
right: 1,
|
|
132
|
-
bottom: 0,
|
|
133
|
-
left: 0
|
|
134
|
-
},
|
|
135
|
-
stacked: true,
|
|
136
|
-
stack: 'status'
|
|
137
|
-
}
|
|
138
|
-
data_sets << hash if date_range.include?(change.time.to_date)
|
|
122
|
+
issue.status_changes.each do |change|
|
|
123
|
+
new_status = issue.find_or_create_status id: change.value_id, name: change.value
|
|
124
|
+
if previous_start.nil?
|
|
125
|
+
previous_start = change.time
|
|
126
|
+
previous_status = new_status
|
|
127
|
+
next
|
|
139
128
|
end
|
|
140
129
|
|
|
130
|
+
previous_start = issue_started_time if issue_started_time > previous_start
|
|
131
|
+
|
|
132
|
+
ranges << BarChartRange.new(
|
|
133
|
+
start: previous_start,
|
|
134
|
+
stop: change.time,
|
|
135
|
+
color: status_category_color(previous_status),
|
|
136
|
+
title: previous_status.to_s
|
|
137
|
+
)
|
|
141
138
|
previous_start = change.time
|
|
142
|
-
previous_status =
|
|
139
|
+
previous_status = new_status
|
|
143
140
|
end
|
|
144
141
|
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
ranges << BarChartRange.new(
|
|
143
|
+
start: previous_start,
|
|
144
|
+
stop: now,
|
|
145
|
+
color: status_category_color(previous_status),
|
|
146
|
+
title: previous_status.to_s
|
|
147
|
+
)
|
|
148
|
+
ranges
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def bar_chart_range_to_data_set y_value:, ranges:, stack:, issue_start_time:
|
|
152
|
+
ranges.filter_map do |bar_chart_range|
|
|
153
|
+
next if bar_chart_range.stop < issue_start_time
|
|
154
|
+
|
|
155
|
+
background_color = bar_chart_range.color
|
|
156
|
+
if bar_chart_range.highlight
|
|
157
|
+
background_color = RawJavascript.new("createDiagonalPattern(#{background_color.to_json})")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
{
|
|
147
161
|
type: 'bar',
|
|
148
162
|
data: [{
|
|
149
|
-
x: [chart_format(
|
|
150
|
-
y:
|
|
151
|
-
title:
|
|
163
|
+
x: [chart_format([bar_chart_range.start, issue_start_time].max), chart_format(bar_chart_range.stop)],
|
|
164
|
+
y: y_value,
|
|
165
|
+
title: bar_chart_range.title
|
|
152
166
|
}],
|
|
153
|
-
backgroundColor:
|
|
167
|
+
backgroundColor: background_color,
|
|
168
|
+
borderColor: CssVariable['--aging-work-bar-chart-separator-color'],
|
|
169
|
+
borderWidth: {
|
|
170
|
+
top: 0,
|
|
171
|
+
right: 1,
|
|
172
|
+
bottom: 0,
|
|
173
|
+
left: 0
|
|
174
|
+
},
|
|
154
175
|
stacked: true,
|
|
155
|
-
stack:
|
|
176
|
+
stack: stack
|
|
156
177
|
}
|
|
157
178
|
end
|
|
158
|
-
|
|
159
|
-
data_sets
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def one_block_change_data_set starting_change:, ending_time:, issue_label:, stack:, issue_start_time:
|
|
163
|
-
if settings['blocked_color']
|
|
164
|
-
file_system.deprecated message: 'blocked color should be set via css now', date: '2024-05-03'
|
|
165
|
-
end
|
|
166
|
-
if settings['stalled_color']
|
|
167
|
-
file_system.deprecated message: 'stalled color should be set via css now', date: '2024-05-03'
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
color = settings['blocked_color'] || '--blocked-color'
|
|
171
|
-
color = settings['stalled_color'] || '--stalled-color' if starting_change.stalled?
|
|
172
|
-
{
|
|
173
|
-
backgroundColor: CssVariable[color],
|
|
174
|
-
data: [
|
|
175
|
-
{
|
|
176
|
-
title: starting_change.reasons,
|
|
177
|
-
x: [chart_format([issue_start_time, starting_change.time].max), chart_format(ending_time)],
|
|
178
|
-
y: issue_label
|
|
179
|
-
}
|
|
180
|
-
],
|
|
181
|
-
stack: stack,
|
|
182
|
-
stacked: true,
|
|
183
|
-
type: 'bar'
|
|
184
|
-
}
|
|
185
179
|
end
|
|
186
180
|
|
|
187
|
-
def
|
|
188
|
-
|
|
181
|
+
def collect_blocked_stalled_ranges issue:, issue_start_time:
|
|
182
|
+
results = []
|
|
189
183
|
starting_change = nil
|
|
190
184
|
|
|
191
185
|
issue.blocked_stalled_changes(end_time: time_range.end).each do |change|
|
|
@@ -195,58 +189,102 @@ class AgingWorkBarChart < ChartBase
|
|
|
195
189
|
end
|
|
196
190
|
|
|
197
191
|
if change.time >= issue_start_time
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
192
|
+
color = settings['blocked_color'] || '--blocked-color'
|
|
193
|
+
color = settings['stalled_color'] || '--stalled-color' if starting_change.stalled?
|
|
194
|
+
|
|
195
|
+
results << BarChartRange.new(
|
|
196
|
+
start: starting_change.time, stop: change.time, color: CssVariable[color], title: starting_change.reasons
|
|
201
197
|
)
|
|
202
198
|
end
|
|
203
199
|
|
|
204
200
|
starting_change = change
|
|
205
201
|
end
|
|
202
|
+
results
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def collect_priority_ranges issue:
|
|
206
|
+
expedited_priority_names = settings['expedited_priority_names']
|
|
207
|
+
|
|
208
|
+
previous_change = nil
|
|
209
|
+
results = []
|
|
210
|
+
|
|
211
|
+
issue.changes.each do |change|
|
|
212
|
+
next unless change.priority?
|
|
213
|
+
|
|
214
|
+
if previous_change.nil?
|
|
215
|
+
previous_change = change
|
|
216
|
+
next
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
results << create_range_for_priority(
|
|
220
|
+
previous_change: previous_change, stop_time: change.time,
|
|
221
|
+
expedited_priority_names: expedited_priority_names
|
|
222
|
+
)
|
|
223
|
+
previous_change = change
|
|
224
|
+
end
|
|
206
225
|
|
|
207
|
-
|
|
226
|
+
results << create_range_for_priority(
|
|
227
|
+
previous_change: previous_change, stop_time: time_range.end,
|
|
228
|
+
expedited_priority_names: expedited_priority_names
|
|
229
|
+
)
|
|
230
|
+
results
|
|
208
231
|
end
|
|
209
232
|
|
|
210
|
-
def
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
233
|
+
def collect_sprint_ranges issue:
|
|
234
|
+
results = []
|
|
235
|
+
open_sprints = {}
|
|
236
|
+
|
|
237
|
+
issue.changes.each do |change|
|
|
238
|
+
next unless change.sprint?
|
|
239
|
+
|
|
240
|
+
removed_sprint_ids = change.old_value_id - change.value_id
|
|
241
|
+
added_sprint_ids = change.value_id - change.old_value_id
|
|
242
|
+
|
|
243
|
+
removed_sprint_ids.each do |id|
|
|
244
|
+
data = open_sprints.delete(id)
|
|
245
|
+
next unless data
|
|
246
|
+
|
|
247
|
+
completed = data[:sprint].completed_time
|
|
248
|
+
stop = completed ? [change.time, completed].min : change.time
|
|
249
|
+
results << BarChartRange.new(
|
|
250
|
+
start: data[:start_time], stop: stop,
|
|
251
|
+
color: CssVariable['--sprint-color'], title: data[:sprint].name
|
|
252
|
+
)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
added_sprint_ids.each do |id|
|
|
256
|
+
sprint = issue.board.sprints.find { |s| s.id == id }
|
|
257
|
+
next unless sprint
|
|
258
|
+
next if sprint.future?
|
|
259
|
+
|
|
260
|
+
start_time = [sprint.start_time, change.time].max
|
|
261
|
+
open_sprints[id] = { start_time: start_time, sprint: sprint }
|
|
230
262
|
end
|
|
231
263
|
end
|
|
232
264
|
|
|
233
|
-
|
|
234
|
-
data
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
265
|
+
open_sprints.each_value do |data|
|
|
266
|
+
stop = data[:sprint].completed_time || time_range.end
|
|
267
|
+
results << BarChartRange.new(
|
|
268
|
+
start: data[:start_time], stop: stop,
|
|
269
|
+
color: CssVariable['--sprint-color'], title: data[:sprint].name
|
|
270
|
+
)
|
|
239
271
|
end
|
|
240
272
|
|
|
241
|
-
|
|
273
|
+
results
|
|
274
|
+
end
|
|
242
275
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
276
|
+
def create_range_for_priority previous_change:, stop_time:, expedited_priority_names:
|
|
277
|
+
expedited = expedited_priority_names.include?(previous_change.value)
|
|
278
|
+
title = "Priority: #{previous_change.value}"
|
|
279
|
+
title << ' (expedited)' if expedited
|
|
280
|
+
|
|
281
|
+
BarChartRange.new(
|
|
282
|
+
start: previous_change.time,
|
|
283
|
+
stop: stop_time,
|
|
284
|
+
color: CssVariable["--priority-color-#{previous_change.value.downcase.gsub(/\s/, '')}"],
|
|
285
|
+
title: title,
|
|
286
|
+
highlight: expedited
|
|
287
|
+
)
|
|
250
288
|
end
|
|
251
289
|
|
|
252
290
|
def calculate_percent_line percentage: 85
|
|
@@ -255,4 +293,8 @@ class AgingWorkBarChart < ChartBase
|
|
|
255
293
|
|
|
256
294
|
days[days.length * percentage / 100]
|
|
257
295
|
end
|
|
296
|
+
|
|
297
|
+
def age_cutoff days
|
|
298
|
+
@age_cutoff = days
|
|
299
|
+
end
|
|
258
300
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'jirametrics/value_equality'
|
|
4
|
+
|
|
5
|
+
class BarChartRange
|
|
6
|
+
include ValueEquality
|
|
7
|
+
|
|
8
|
+
attr_accessor :start, :stop, :color, :title, :highlight
|
|
9
|
+
|
|
10
|
+
def initialize start:, stop:, color:, title:, highlight: false
|
|
11
|
+
@start = start
|
|
12
|
+
@stop = stop
|
|
13
|
+
@color = color
|
|
14
|
+
@title = title
|
|
15
|
+
@highlight = highlight
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/jirametrics/board.rb
CHANGED
|
@@ -24,7 +24,8 @@ class BoardConfig
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
@board.cycletime = CycleTimeConfig.new(
|
|
27
|
-
|
|
27
|
+
possible_statuses: project_config.possible_statuses,
|
|
28
|
+
label: label, block: block, file_system: project_config.file_system,
|
|
28
29
|
settings: project_config.settings
|
|
29
30
|
)
|
|
30
31
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class ChangeItem
|
|
4
|
-
attr_reader :field, :value_id, :old_value_id, :raw, :author_raw
|
|
4
|
+
attr_reader :field, :value_id, :old_value_id, :raw, :author_raw, :field_id
|
|
5
5
|
attr_accessor :value, :old_value, :time
|
|
6
6
|
|
|
7
7
|
def initialize raw:, author_raw:, time:, artificial: false
|
|
@@ -13,9 +13,15 @@ class ChangeItem
|
|
|
13
13
|
|
|
14
14
|
@field = @raw['field']
|
|
15
15
|
@value = @raw['toString']
|
|
16
|
-
@value_id = @raw['to'].to_i
|
|
17
16
|
@old_value = @raw['fromString']
|
|
18
|
-
|
|
17
|
+
if sprint?
|
|
18
|
+
@value_id = @raw['to'].split(', ').collect(&:to_i)
|
|
19
|
+
@old_value_id = (@raw['from'] || '').split(', ').collect(&:to_i)
|
|
20
|
+
else
|
|
21
|
+
@value_id = @raw['to'].to_i
|
|
22
|
+
@old_value_id = @raw['from']&.to_i
|
|
23
|
+
end
|
|
24
|
+
@field_id = @raw['fieldId']
|
|
19
25
|
@artificial = artificial
|
|
20
26
|
end
|
|
21
27
|
|
|
@@ -54,6 +60,7 @@ class ChangeItem
|
|
|
54
60
|
message << ':' << old_value_id.inspect if status?
|
|
55
61
|
end
|
|
56
62
|
message << ", time: #{time_to_s(@time).inspect}"
|
|
63
|
+
message << ", field_id: #{@field_id.inspect}" if @field_id
|
|
57
64
|
message << ', artificial' if artificial?
|
|
58
65
|
message << ')'
|
|
59
66
|
message
|
|
@@ -22,6 +22,14 @@ class ChartBase
|
|
|
22
22
|
@canvas_responsive = true
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
def call_before_run &proc
|
|
26
|
+
(@call_before_run_procs ||= []) << proc
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def before_run
|
|
30
|
+
@call_before_run_procs&.each { |proc| proc.call }
|
|
31
|
+
end
|
|
32
|
+
|
|
25
33
|
def aggregated_project?
|
|
26
34
|
@aggregated_project
|
|
27
35
|
end
|
|
@@ -279,4 +287,27 @@ class ChartBase
|
|
|
279
287
|
</div>
|
|
280
288
|
TEXT
|
|
281
289
|
end
|
|
290
|
+
|
|
291
|
+
# Set a cycletime for just this one chart, overriding the one for the report.
|
|
292
|
+
def cycletime &block
|
|
293
|
+
call_before_run do
|
|
294
|
+
@cycletime = CycleTimeConfig.new(
|
|
295
|
+
possible_statuses: possible_statuses, label: nil, block: block, file_system: file_system,
|
|
296
|
+
settings: settings
|
|
297
|
+
)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Returns the cycletime in use right now, which may be specific to the chart or across the report.
|
|
302
|
+
def cycletime_for_issue issue
|
|
303
|
+
@cycletime || issue.board.cycletime
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def seam_start type = 'chart'
|
|
307
|
+
"\n<!-- seam-start | chart#{@@chart_counter} | #{self.class} | #{header_text} | #{type} -->"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def seam_end type = 'chart'
|
|
311
|
+
"\n<!-- seam-end | chart#{@@chart_counter} | #{self.class} | #{header_text} | #{type} -->"
|
|
312
|
+
end
|
|
282
313
|
end
|
|
@@ -6,15 +6,14 @@ require 'date'
|
|
|
6
6
|
class CycleTimeConfig
|
|
7
7
|
include SelfOrIssueDispatcher
|
|
8
8
|
|
|
9
|
-
attr_reader :label, :
|
|
9
|
+
attr_reader :label, :possible_statuses, :settings, :file_system
|
|
10
10
|
|
|
11
|
-
def initialize
|
|
11
|
+
def initialize possible_statuses:, label:, block:, settings:, file_system: nil, today: Date.today
|
|
12
12
|
|
|
13
|
-
@
|
|
13
|
+
@possible_statuses = possible_statuses
|
|
14
14
|
@label = label
|
|
15
15
|
@today = today
|
|
16
16
|
@settings = settings
|
|
17
|
-
@cache_cycletime_calculations = settings['cache_cycletime_calculations']
|
|
18
17
|
|
|
19
18
|
# If we hit something deprecated and this is nil then we'll blow up. Although it's ugly, this
|
|
20
19
|
# may make it easier to find problems in the test code ;-)
|
|
@@ -68,7 +67,7 @@ class CycleTimeConfig
|
|
|
68
67
|
def started_stopped_changes issue
|
|
69
68
|
cache_key = "#{issue.key}:#{issue.board.id}"
|
|
70
69
|
last_result = (@cache ||= {})[cache_key]
|
|
71
|
-
return *last_result if last_result &&
|
|
70
|
+
return *last_result if last_result && settings['cache_cycletime_calculations']
|
|
72
71
|
|
|
73
72
|
started = @start_at.call(issue)
|
|
74
73
|
stopped = @stop_at.call(issue)
|
|
@@ -35,14 +35,33 @@ class CycletimeScatterplot < ChartBase
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
@percentage_lines = []
|
|
38
|
-
@
|
|
38
|
+
@highest_y_value = 0
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def
|
|
42
|
-
|
|
41
|
+
def all_items
|
|
42
|
+
completed_issues_in_range include_unstarted: false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def x_value item
|
|
46
|
+
item.board.cycletime.started_stopped_times(item).last
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def y_value item
|
|
50
|
+
item.board.cycletime.cycletime(item)
|
|
51
|
+
end
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
def title_value item
|
|
54
|
+
"#{item.key} : #{item.summary} (#{label_days(y_value(item))})"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def y_axis_heading
|
|
58
|
+
'Cycle time in days'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def run
|
|
62
|
+
items = all_items
|
|
63
|
+
data_sets = create_datasets items
|
|
64
|
+
overall_percent_line = calculate_percent_line(items)
|
|
46
65
|
@percentage_lines << [overall_percent_line, CssVariable['--cycletime-scatterplot-overall-trendline-color']]
|
|
47
66
|
|
|
48
67
|
return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
|
|
@@ -50,14 +69,14 @@ class CycletimeScatterplot < ChartBase
|
|
|
50
69
|
wrap_and_render(binding, __FILE__)
|
|
51
70
|
end
|
|
52
71
|
|
|
53
|
-
def create_datasets
|
|
72
|
+
def create_datasets items
|
|
54
73
|
data_sets = []
|
|
55
74
|
|
|
56
|
-
group_issues(
|
|
75
|
+
group_issues(items).each do |rules, completed_items_by_type|
|
|
57
76
|
label = rules.label
|
|
58
77
|
color = rules.color
|
|
59
|
-
percent_line = calculate_percent_line
|
|
60
|
-
data =
|
|
78
|
+
percent_line = calculate_percent_line completed_items_by_type
|
|
79
|
+
data = completed_items_by_type.filter_map { |issue| data_for_issue(issue) }
|
|
61
80
|
data_sets << {
|
|
62
81
|
label: "#{label} (85% at #{label_days(percent_line)})",
|
|
63
82
|
data: data,
|
|
@@ -86,7 +105,7 @@ class CycletimeScatterplot < ChartBase
|
|
|
86
105
|
calculator = TrendLineCalculator.new(points)
|
|
87
106
|
data_points = calculator.chart_datapoints(
|
|
88
107
|
range: time_range.begin.to_i..time_range.end.to_i,
|
|
89
|
-
max_y: @
|
|
108
|
+
max_y: @highest_y_value
|
|
90
109
|
)
|
|
91
110
|
data_points.each do |point_hash|
|
|
92
111
|
point_hash[:x] = chart_format Time.at(point_hash[:x])
|
|
@@ -106,21 +125,21 @@ class CycletimeScatterplot < ChartBase
|
|
|
106
125
|
}
|
|
107
126
|
end
|
|
108
127
|
|
|
109
|
-
def data_for_issue
|
|
110
|
-
cycle_time =
|
|
128
|
+
def data_for_issue item
|
|
129
|
+
cycle_time = y_value(item)
|
|
111
130
|
return nil if cycle_time < 1 # These will get called out on the quality report
|
|
112
131
|
|
|
113
|
-
@
|
|
132
|
+
@highest_y_value = cycle_time if @highest_y_value < cycle_time
|
|
114
133
|
|
|
115
134
|
{
|
|
116
135
|
y: cycle_time,
|
|
117
|
-
x: chart_format(
|
|
118
|
-
title: [
|
|
136
|
+
x: chart_format(x_value(item)),
|
|
137
|
+
title: [title_value(item)]
|
|
119
138
|
}
|
|
120
139
|
end
|
|
121
140
|
|
|
122
|
-
def calculate_percent_line
|
|
123
|
-
times =
|
|
141
|
+
def calculate_percent_line items
|
|
142
|
+
times = items.collect { |item| y_value(item) }
|
|
124
143
|
index = times.size * 85 / 100
|
|
125
144
|
times.sort[index]
|
|
126
145
|
end
|