jirametrics 2.22 → 2.24
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/aggregate_config.rb +10 -2
- data/lib/jirametrics/aging_work_bar_chart.rb +15 -1
- data/lib/jirametrics/aging_work_table.rb +1 -1
- data/lib/jirametrics/anonymizer.rb +74 -1
- data/lib/jirametrics/atlassian_document_format.rb +104 -93
- data/lib/jirametrics/blocked_stalled_change.rb +5 -3
- data/lib/jirametrics/board.rb +20 -8
- data/lib/jirametrics/board_feature.rb +14 -0
- data/lib/jirametrics/change_item.rb +4 -3
- data/lib/jirametrics/chart_base.rb +87 -1
- data/lib/jirametrics/css_variable.rb +1 -1
- data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +1 -2
- data/lib/jirametrics/cycletime_histogram.rb +15 -103
- data/lib/jirametrics/cycletime_scatterplot.rb +8 -97
- data/lib/jirametrics/daily_view.rb +32 -9
- data/lib/jirametrics/daily_wip_chart.rb +27 -7
- data/lib/jirametrics/data_quality_report.rb +31 -7
- data/lib/jirametrics/download_config.rb +15 -0
- data/lib/jirametrics/downloader.rb +76 -5
- data/lib/jirametrics/downloader_for_cloud.rb +39 -0
- data/lib/jirametrics/downloader_for_data_center.rb +2 -1
- data/lib/jirametrics/estimate_accuracy_chart.rb +42 -4
- data/lib/jirametrics/examples/aggregated_project.rb +1 -1
- data/lib/jirametrics/examples/standard_project.rb +20 -9
- data/lib/jirametrics/expedited_chart.rb +2 -0
- data/lib/jirametrics/exporter.rb +3 -1
- data/lib/jirametrics/file_system.rb +4 -0
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +2 -0
- data/lib/jirametrics/github_gateway.rb +106 -0
- data/lib/jirametrics/groupable_issue_chart.rb +2 -0
- data/lib/jirametrics/grouping_rules.rb +21 -3
- data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -4
- data/lib/jirametrics/html/aging_work_table.erb +3 -0
- data/lib/jirametrics/html/daily_wip_chart.erb +5 -4
- data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -12
- data/lib/jirametrics/html/expedited_chart.erb +3 -13
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -8
- data/lib/jirametrics/html/index.css +114 -0
- data/lib/jirametrics/html/index.erb +5 -0
- data/lib/jirametrics/html/index.js +52 -2
- data/lib/jirametrics/html/sprint_burndown.erb +7 -13
- data/lib/jirametrics/html/throughput_chart.erb +5 -8
- data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +57 -59
- data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +3 -4
- data/lib/jirametrics/html_report_config.rb +2 -0
- data/lib/jirametrics/issue.rb +84 -95
- data/lib/jirametrics/issue_printer.rb +97 -0
- data/lib/jirametrics/jira_gateway.rb +6 -3
- data/lib/jirametrics/project_config.rb +66 -6
- data/lib/jirametrics/pull_request.rb +30 -0
- data/lib/jirametrics/pull_request_review.rb +13 -0
- data/lib/jirametrics/raw_javascript.rb +4 -0
- data/lib/jirametrics/settings.json +3 -1
- data/lib/jirametrics/sprint_burndown.rb +2 -0
- data/lib/jirametrics/stitcher.rb +2 -1
- data/lib/jirametrics/throughput_chart.rb +7 -1
- data/lib/jirametrics/time_based_histogram.rb +139 -0
- data/lib/jirametrics/time_based_scatterplot.rb +100 -0
- metadata +12 -5
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
class ChartBase
|
|
4
4
|
attr_accessor :timezone_offset, :board_id, :all_boards, :date_range,
|
|
5
5
|
:time_range, :data_quality, :holiday_dates, :settings, :issues, :file_system,
|
|
6
|
-
:atlassian_document_format
|
|
6
|
+
:atlassian_document_format, :x_axis_title, :y_axis_title, :fix_versions
|
|
7
7
|
attr_writer :aggregated_project
|
|
8
8
|
attr_reader :canvas_width, :canvas_height
|
|
9
9
|
|
|
@@ -80,10 +80,20 @@ class ChartBase
|
|
|
80
80
|
"#{days} day#{'s' unless days == 1}"
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
def label_hours hours
|
|
84
|
+
return 'unknown' if hours.nil?
|
|
85
|
+
|
|
86
|
+
"#{hours} hour#{'s' unless hours == 1}"
|
|
87
|
+
end
|
|
88
|
+
|
|
83
89
|
def label_issues count
|
|
84
90
|
"#{count} issue#{'s' unless count == 1}"
|
|
85
91
|
end
|
|
86
92
|
|
|
93
|
+
def to_human_readable number
|
|
94
|
+
number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
95
|
+
end
|
|
96
|
+
|
|
87
97
|
def daily_chart_dataset date_issues_list:, color:, label:, positive: true
|
|
88
98
|
{
|
|
89
99
|
type: 'bar',
|
|
@@ -155,6 +165,56 @@ class ChartBase
|
|
|
155
165
|
end.join
|
|
156
166
|
end
|
|
157
167
|
|
|
168
|
+
LABEL_POSITIONS = %w[5% 25% 45% 65%].freeze
|
|
169
|
+
|
|
170
|
+
def date_annotation
|
|
171
|
+
annotations = settings['date_annotations'] || []
|
|
172
|
+
in_range = annotations
|
|
173
|
+
.map { |a| [a, normalize_annotation_datetime(a['date'])] }
|
|
174
|
+
.select { |(_, dt)| date_range.cover?(Date.parse(dt)) }
|
|
175
|
+
.sort_by { |(_, dt)| dt }
|
|
176
|
+
|
|
177
|
+
positions = stagger_label_positions(in_range.map { |(_, dt)| dt })
|
|
178
|
+
|
|
179
|
+
in_range.each_with_index.collect do |(a, normalized), index|
|
|
180
|
+
<<~TEXT
|
|
181
|
+
dateAnnotation#{index}: {
|
|
182
|
+
type: 'line',
|
|
183
|
+
xMin: #{normalized.to_json},
|
|
184
|
+
xMax: #{normalized.to_json},
|
|
185
|
+
borderColor: 'rgba(0,0,0,0.7)',
|
|
186
|
+
borderWidth: 1,
|
|
187
|
+
label: {
|
|
188
|
+
display: true,
|
|
189
|
+
content: #{a['label'].to_json},
|
|
190
|
+
position: #{positions[index].to_json}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
TEXT
|
|
194
|
+
end.join
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def stagger_label_positions datetimes
|
|
198
|
+
return [] if datetimes.empty?
|
|
199
|
+
|
|
200
|
+
threshold_days = (date_range.end - date_range.begin).to_f / 5.0
|
|
201
|
+
slot = 0
|
|
202
|
+
[LABEL_POSITIONS[0]] + datetimes.each_cons(2).map do |a, b|
|
|
203
|
+
days_apart = (Date.parse(b) - Date.parse(a)).to_f.abs
|
|
204
|
+
slot = days_apart < threshold_days ? slot + 1 : 0
|
|
205
|
+
LABEL_POSITIONS[slot % LABEL_POSITIONS.size]
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def normalize_annotation_datetime value
|
|
210
|
+
offset = timezone_offset || '+00:00'
|
|
211
|
+
if value.include?('T')
|
|
212
|
+
value.match?(/([+-]\d{2}:\d{2}|Z)$/) ? value : "#{value}#{offset}"
|
|
213
|
+
else
|
|
214
|
+
"#{value}T00:00:00#{offset}"
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
158
218
|
# Return only the board columns for the current board.
|
|
159
219
|
def current_board
|
|
160
220
|
if @board_id.nil?
|
|
@@ -245,6 +305,13 @@ class ChartBase
|
|
|
245
305
|
"<span title='#{title}' style='font-size: 0.8em;'>#{icon}</span>"
|
|
246
306
|
end
|
|
247
307
|
|
|
308
|
+
def not_visible_text issue
|
|
309
|
+
reasons = issue.reasons_not_visible_on_board
|
|
310
|
+
return nil if reasons.empty?
|
|
311
|
+
|
|
312
|
+
"<span style='background: var(--warning-banner)'>Not visible on board: #{reasons.join(', ')}</span>"
|
|
313
|
+
end
|
|
314
|
+
|
|
248
315
|
def status_category_color status
|
|
249
316
|
case status.category.key
|
|
250
317
|
when 'new' then CssVariable['--status-category-todo-color']
|
|
@@ -310,4 +377,23 @@ class ChartBase
|
|
|
310
377
|
def seam_end type = 'chart'
|
|
311
378
|
"\n<!-- seam-end | chart#{@@chart_counter} | #{self.class} | #{header_text} | #{type} -->"
|
|
312
379
|
end
|
|
380
|
+
|
|
381
|
+
def render_axis_title axis_direction
|
|
382
|
+
text = case axis_direction
|
|
383
|
+
when :x
|
|
384
|
+
x_axis_title
|
|
385
|
+
when :y
|
|
386
|
+
y_axis_title
|
|
387
|
+
else
|
|
388
|
+
raise "Unexpected axis_direction: #{axis_direction}"
|
|
389
|
+
end
|
|
390
|
+
return '' unless text
|
|
391
|
+
|
|
392
|
+
<<~CONTENT
|
|
393
|
+
title: {
|
|
394
|
+
display: true,
|
|
395
|
+
text: "#{text}"
|
|
396
|
+
},
|
|
397
|
+
CONTENT
|
|
398
|
+
end
|
|
313
399
|
end
|
|
@@ -6,10 +6,9 @@ require 'date'
|
|
|
6
6
|
class CycleTimeConfig
|
|
7
7
|
include SelfOrIssueDispatcher
|
|
8
8
|
|
|
9
|
-
attr_reader :label, :
|
|
9
|
+
attr_reader :label, :settings, :file_system
|
|
10
10
|
|
|
11
11
|
def initialize possible_statuses:, label:, block:, settings:, file_system: nil, today: Date.today
|
|
12
|
-
|
|
13
12
|
@possible_statuses = possible_statuses
|
|
14
13
|
@label = label
|
|
15
14
|
@today = today
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'jirametrics/
|
|
3
|
+
require 'jirametrics/time_based_histogram'
|
|
4
4
|
|
|
5
|
-
class CycletimeHistogram <
|
|
6
|
-
include GroupableIssueChart
|
|
5
|
+
class CycletimeHistogram < TimeBasedHistogram
|
|
7
6
|
attr_accessor :possible_statuses
|
|
8
|
-
attr_reader :show_stats
|
|
9
7
|
|
|
10
8
|
def initialize block
|
|
11
9
|
super()
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
@
|
|
11
|
+
@x_axis_title = 'Cycletime in days'
|
|
12
|
+
@y_axis_title = 'Count'
|
|
15
13
|
|
|
16
14
|
header_text 'Cycletime Histogram'
|
|
17
15
|
description_text <<-HTML
|
|
@@ -30,112 +28,26 @@ class CycletimeHistogram < ChartBase
|
|
|
30
28
|
end
|
|
31
29
|
end
|
|
32
30
|
|
|
33
|
-
def
|
|
34
|
-
@percentiles = percs unless percs.nil?
|
|
35
|
-
@percentiles
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def disable_stats
|
|
39
|
-
@show_stats = false
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def run
|
|
31
|
+
def all_items
|
|
43
32
|
stopped_issues = completed_issues_in_range include_unstarted: true
|
|
44
33
|
|
|
45
34
|
# For the histogram, we only want to consider items that have both a start and a stop time.
|
|
46
|
-
|
|
47
|
-
rules_to_issues = group_issues histogram_issues
|
|
48
|
-
|
|
49
|
-
the_stats = {}
|
|
50
|
-
|
|
51
|
-
overall_stats = stats_for histogram_data: histogram_data_for(issues: histogram_issues), percentiles: @percentiles
|
|
52
|
-
the_stats[:all] = overall_stats
|
|
53
|
-
data_sets = rules_to_issues.keys.collect do |rules|
|
|
54
|
-
the_issue_type = rules.label
|
|
55
|
-
the_histogram = histogram_data_for(issues: rules_to_issues[rules])
|
|
56
|
-
the_stats[the_issue_type] = stats_for histogram_data: the_histogram, percentiles: @percentiles if @show_stats
|
|
57
|
-
|
|
58
|
-
data_set_for(
|
|
59
|
-
histogram_data: the_histogram,
|
|
60
|
-
label: the_issue_type,
|
|
61
|
-
color: rules.color
|
|
62
|
-
)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
if data_sets.empty?
|
|
66
|
-
return "<h1 class='foldable'>#{@header_text}</h1><div>No data matched the selected criteria. Nothing to show.</div>"
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
wrap_and_render(binding, __FILE__)
|
|
35
|
+
stopped_issues.select { |issue| issue.board.cycletime.started_stopped_times(issue).first }
|
|
70
36
|
end
|
|
71
37
|
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
issues.each do |issue|
|
|
75
|
-
days = issue.board.cycletime.cycletime(issue)
|
|
76
|
-
count_hash[days] = (count_hash[days] || 0) + 1 if days.positive?
|
|
77
|
-
end
|
|
78
|
-
count_hash
|
|
38
|
+
def value_for_item issue
|
|
39
|
+
issue.board.cycletime.cycletime(issue)
|
|
79
40
|
end
|
|
80
41
|
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
total_values = histogram_data.values.sum
|
|
85
|
-
|
|
86
|
-
# Calculate the average
|
|
87
|
-
weighted_sum = histogram_data.reduce(0) { |sum, (value, frequency)| sum + (value * frequency) }
|
|
88
|
-
average = total_values.zero? ? 0 : weighted_sum.to_f / total_values
|
|
89
|
-
|
|
90
|
-
# Find the mode (or modes!) and the spread of the distribution
|
|
91
|
-
sorted_histogram = histogram_data.sort_by { |_value, frequency| frequency }
|
|
92
|
-
max_freq = sorted_histogram[-1][1]
|
|
93
|
-
mode = sorted_histogram.select { |_v, f| f == max_freq }
|
|
94
|
-
|
|
95
|
-
minmax = histogram_data.keys.minmax
|
|
96
|
-
|
|
97
|
-
# Calculate percentiles
|
|
98
|
-
sorted_values = histogram_data.keys.sort
|
|
99
|
-
cumulative_counts = {}
|
|
100
|
-
cumulative_sum = 0
|
|
101
|
-
|
|
102
|
-
sorted_values.each do |value|
|
|
103
|
-
cumulative_sum += histogram_data[value]
|
|
104
|
-
cumulative_counts[value] = cumulative_sum
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
percentile_results = {}
|
|
108
|
-
percentiles.each do |percentile|
|
|
109
|
-
rank = (percentile / 100.0) * total_values
|
|
110
|
-
percentile_value = sorted_values.find { |value| cumulative_counts[value] >= rank }
|
|
111
|
-
percentile_results[percentile] = percentile_value
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
{
|
|
115
|
-
average: average,
|
|
116
|
-
mode: mode.collect(&:first).sort,
|
|
117
|
-
min: minmax[0],
|
|
118
|
-
max: minmax[1],
|
|
119
|
-
percentiles: percentile_results
|
|
120
|
-
}
|
|
42
|
+
def title_for_item count:, value:
|
|
43
|
+
"#{count} items completed in #{label_days value}"
|
|
121
44
|
end
|
|
122
45
|
|
|
123
|
-
def
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
type: 'bar',
|
|
127
|
-
label: label,
|
|
128
|
-
data: keys.sort.filter_map do |key|
|
|
129
|
-
next if histogram_data[key].zero?
|
|
46
|
+
def sort_items items
|
|
47
|
+
items.sort_by(&:key_as_i)
|
|
48
|
+
end
|
|
130
49
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
y: histogram_data[key],
|
|
134
|
-
title: "#{histogram_data[key]} items completed in #{label_days key}"
|
|
135
|
-
}
|
|
136
|
-
end,
|
|
137
|
-
backgroundColor: color,
|
|
138
|
-
borderRadius: 0
|
|
139
|
-
}
|
|
50
|
+
def label_for_item issue, hint:
|
|
51
|
+
"#{issue.key} : #{issue.summary}#{" #{hint}" if hint}"
|
|
140
52
|
end
|
|
141
53
|
end
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'jirametrics/
|
|
4
|
-
|
|
5
|
-
class CycletimeScatterplot < ChartBase
|
|
6
|
-
include GroupableIssueChart
|
|
3
|
+
require 'jirametrics/time_based_scatterplot'
|
|
7
4
|
|
|
5
|
+
class CycletimeScatterplot < TimeBasedScatterplot
|
|
8
6
|
attr_accessor :possible_statuses
|
|
9
7
|
|
|
10
8
|
def initialize block
|
|
@@ -26,6 +24,8 @@ class CycletimeScatterplot < ChartBase
|
|
|
26
24
|
</div>
|
|
27
25
|
#{describe_non_working_days}
|
|
28
26
|
HTML
|
|
27
|
+
@x_axis_title = 'Date completed'
|
|
28
|
+
@y_axis_title = 'Cycletime in days'
|
|
29
29
|
|
|
30
30
|
init_configuration_block block do
|
|
31
31
|
grouping_rules do |issue, rule|
|
|
@@ -33,9 +33,6 @@ class CycletimeScatterplot < ChartBase
|
|
|
33
33
|
rule.color = color_for type: issue.type
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
|
-
|
|
37
|
-
@percentage_lines = []
|
|
38
|
-
@highest_y_value = 0
|
|
39
36
|
end
|
|
40
37
|
|
|
41
38
|
def all_items
|
|
@@ -51,96 +48,10 @@ class CycletimeScatterplot < ChartBase
|
|
|
51
48
|
end
|
|
52
49
|
|
|
53
50
|
def title_value item
|
|
54
|
-
|
|
55
|
-
|
|
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)
|
|
65
|
-
@percentage_lines << [overall_percent_line, CssVariable['--cycletime-scatterplot-overall-trendline-color']]
|
|
66
|
-
|
|
67
|
-
return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
|
|
68
|
-
|
|
69
|
-
wrap_and_render(binding, __FILE__)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def create_datasets items
|
|
73
|
-
data_sets = []
|
|
74
|
-
|
|
75
|
-
group_issues(items).each do |rules, completed_items_by_type|
|
|
76
|
-
label = rules.label
|
|
77
|
-
color = rules.color
|
|
78
|
-
percent_line = calculate_percent_line completed_items_by_type
|
|
79
|
-
data = completed_items_by_type.filter_map { |issue| data_for_issue(issue) }
|
|
80
|
-
data_sets << {
|
|
81
|
-
label: "#{label} (85% at #{label_days(percent_line)})",
|
|
82
|
-
data: data,
|
|
83
|
-
fill: false,
|
|
84
|
-
showLine: false,
|
|
85
|
-
backgroundColor: color
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
data_sets << trend_line_data_set(label: label, data: data, color: color)
|
|
89
|
-
|
|
90
|
-
@percentage_lines << [percent_line, color]
|
|
91
|
-
end
|
|
92
|
-
data_sets
|
|
51
|
+
hint = @issue_hints&.fetch(item, nil)
|
|
52
|
+
"#{item.key} : #{item.summary} (#{label_days(y_value(item))})#{" #{hint}" if hint}"
|
|
93
53
|
end
|
|
94
54
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def trend_line_data_set label:, data:, color:
|
|
100
|
-
points = data.collect do |hash|
|
|
101
|
-
[Time.parse(hash[:x]).to_i, hash[:y]]
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# The trend calculation works with numbers only so convert Time to an int and back
|
|
105
|
-
calculator = TrendLineCalculator.new(points)
|
|
106
|
-
data_points = calculator.chart_datapoints(
|
|
107
|
-
range: time_range.begin.to_i..time_range.end.to_i,
|
|
108
|
-
max_y: @highest_y_value
|
|
109
|
-
)
|
|
110
|
-
data_points.each do |point_hash|
|
|
111
|
-
point_hash[:x] = chart_format Time.at(point_hash[:x])
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
{
|
|
115
|
-
type: 'line',
|
|
116
|
-
label: "#{label} Trendline",
|
|
117
|
-
data: data_points,
|
|
118
|
-
fill: false,
|
|
119
|
-
borderWidth: 1,
|
|
120
|
-
markerType: 'none',
|
|
121
|
-
borderColor: color,
|
|
122
|
-
borderDash: [6, 3],
|
|
123
|
-
pointStyle: 'dash',
|
|
124
|
-
hidden: !@show_trend_lines
|
|
125
|
-
}
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def data_for_issue item
|
|
129
|
-
cycle_time = y_value(item)
|
|
130
|
-
return nil if cycle_time < 1 # These will get called out on the quality report
|
|
131
|
-
|
|
132
|
-
@highest_y_value = cycle_time if @highest_y_value < cycle_time
|
|
133
|
-
|
|
134
|
-
{
|
|
135
|
-
y: cycle_time,
|
|
136
|
-
x: chart_format(x_value(item)),
|
|
137
|
-
title: [title_value(item)]
|
|
138
|
-
}
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def calculate_percent_line items
|
|
142
|
-
times = items.collect { |item| y_value(item) }
|
|
143
|
-
index = times.size * 85 / 100
|
|
144
|
-
times.sort[index]
|
|
145
|
-
end
|
|
55
|
+
# Kept for backwards compatibility with existing callers and specs
|
|
56
|
+
alias data_for_issue data_for_item
|
|
146
57
|
end
|
|
@@ -9,7 +9,8 @@ class DailyView < ChartBase
|
|
|
9
9
|
header_text 'Daily View'
|
|
10
10
|
description_text <<-HTML
|
|
11
11
|
<div class="p">
|
|
12
|
-
This view shows all the items you'll want to discuss during your daily
|
|
12
|
+
This view shows all the items (<%= aging_issues.count %>) you'll want to discuss during your daily
|
|
13
|
+
coordination meeting
|
|
13
14
|
(aka daily scrum, standup), in the order that you should be discussing them. The most important
|
|
14
15
|
items are at the top, and the least at the bottom.
|
|
15
16
|
</div>
|
|
@@ -86,9 +87,14 @@ class DailyView < ChartBase
|
|
|
86
87
|
lines << ["#{marker} Blocked by flag"] if blocked_stalled.flag
|
|
87
88
|
lines << ["#{marker} Blocked by status: #{blocked_stalled.status}"] if blocked_stalled.blocked_by_status?
|
|
88
89
|
blocked_stalled.blocking_issue_keys&.each do |key|
|
|
89
|
-
lines << ["#{marker} Blocked by issue: #{key}"]
|
|
90
90
|
blocking_issue = issues.find { |i| i.key == key }
|
|
91
|
-
|
|
91
|
+
if blocking_issue
|
|
92
|
+
lines << "<section><div class=\"foldable startFolded\">#{marker} Blocked by issue: #{key}</div>"
|
|
93
|
+
lines << blocking_issue
|
|
94
|
+
lines << '</section>'
|
|
95
|
+
else
|
|
96
|
+
lines << ["#{marker} Blocked by issue: #{key}"]
|
|
97
|
+
end
|
|
92
98
|
end
|
|
93
99
|
elsif blocked_stalled.stalled_by_status?
|
|
94
100
|
lines << ["#{color_block '--stalled-color'} Stalled by status: #{blocked_stalled.status}"]
|
|
@@ -146,7 +152,18 @@ class DailyView < ChartBase
|
|
|
146
152
|
line << "Assignee: <img src='#{issue.assigned_to_icon_url}' class='icon' /> <b>#{issue.assigned_to}</b>"
|
|
147
153
|
end
|
|
148
154
|
|
|
149
|
-
|
|
155
|
+
if issue.due_date
|
|
156
|
+
today = date_range.end
|
|
157
|
+
days = (issue.due_date - today).to_i
|
|
158
|
+
relative =
|
|
159
|
+
if days.zero? then 'today'
|
|
160
|
+
elsif days.positive? then "in #{label_days days}"
|
|
161
|
+
else "#{label_days(-days)} ago"
|
|
162
|
+
end
|
|
163
|
+
content = "#{issue.due_date} (#{relative})"
|
|
164
|
+
content = "<span style='background: var(--warning-banner)'>#{content}</span>" if days.negative?
|
|
165
|
+
line << "Due: <b>#{content}</b>"
|
|
166
|
+
end
|
|
150
167
|
|
|
151
168
|
block = lambda do |collection, label|
|
|
152
169
|
unless collection.empty?
|
|
@@ -166,7 +183,7 @@ class DailyView < ChartBase
|
|
|
166
183
|
|
|
167
184
|
return lines if subtasks.empty?
|
|
168
185
|
|
|
169
|
-
lines <<
|
|
186
|
+
lines << "<section><div class=\"foldable startFolded\">Child issues (#{subtasks.count})</div>"
|
|
170
187
|
lines += subtasks
|
|
171
188
|
lines << '</section>'
|
|
172
189
|
|
|
@@ -237,9 +254,10 @@ class DailyView < ChartBase
|
|
|
237
254
|
|
|
238
255
|
def make_description_lines issue
|
|
239
256
|
description = issue.raw['fields']['description']
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
257
|
+
return [] unless description
|
|
258
|
+
|
|
259
|
+
text = "<div class='foldable startFolded'>Description</div><div>#{atlassian_document_format.to_html(description)}</div>"
|
|
260
|
+
[[text]]
|
|
243
261
|
end
|
|
244
262
|
|
|
245
263
|
def assemble_issue_lines issue, child:
|
|
@@ -247,6 +265,7 @@ class DailyView < ChartBase
|
|
|
247
265
|
|
|
248
266
|
lines = []
|
|
249
267
|
lines << [make_title_line(issue: issue, done: done)]
|
|
268
|
+
lines << make_not_visible_line(issue)
|
|
250
269
|
lines += make_parent_lines(issue) unless child
|
|
251
270
|
lines += make_stats_lines(issue: issue, done: done)
|
|
252
271
|
unless done
|
|
@@ -256,7 +275,7 @@ class DailyView < ChartBase
|
|
|
256
275
|
lines += make_child_lines(issue)
|
|
257
276
|
lines += make_history_lines(issue)
|
|
258
277
|
end
|
|
259
|
-
lines
|
|
278
|
+
lines.compact
|
|
260
279
|
end
|
|
261
280
|
|
|
262
281
|
def render_issue issue, child:
|
|
@@ -278,4 +297,8 @@ class DailyView < ChartBase
|
|
|
278
297
|
end
|
|
279
298
|
result << '</div>'
|
|
280
299
|
end
|
|
300
|
+
|
|
301
|
+
def make_not_visible_line issue
|
|
302
|
+
not_visible_text issue
|
|
303
|
+
end
|
|
281
304
|
end
|
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
require 'jirametrics/chart_base'
|
|
4
4
|
|
|
5
5
|
class DailyGroupingRules < GroupingRules
|
|
6
|
-
attr_accessor :current_date, :group_priority, :issue_hint
|
|
6
|
+
attr_accessor :current_date, :group_priority, :issue_hint, :highlight
|
|
7
7
|
|
|
8
8
|
def initialize
|
|
9
9
|
super
|
|
10
10
|
@group_priority = 0
|
|
11
11
|
end
|
|
12
|
+
|
|
13
|
+
def group
|
|
14
|
+
[@label, @color, @highlight ? true : false]
|
|
15
|
+
end
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
class DailyWipChart < ChartBase
|
|
@@ -19,6 +23,8 @@ class DailyWipChart < ChartBase
|
|
|
19
23
|
|
|
20
24
|
header_text default_header_text
|
|
21
25
|
description_text default_description_text
|
|
26
|
+
@x_axis_title = nil
|
|
27
|
+
@y_axis_title = 'Count of items'
|
|
22
28
|
|
|
23
29
|
instance_eval(&block) if block
|
|
24
30
|
|
|
@@ -33,8 +39,15 @@ class DailyWipChart < ChartBase
|
|
|
33
39
|
issue_rules_by_active_date = group_issues_by_active_dates
|
|
34
40
|
possible_rules = select_possible_rules issue_rules_by_active_date
|
|
35
41
|
|
|
42
|
+
conflicting_labels = possible_rules
|
|
43
|
+
.group_by(&:label)
|
|
44
|
+
.select { |_label, rules| rules.any?(&:highlight) && rules.any? { |r| !r.highlight } }
|
|
45
|
+
.keys
|
|
46
|
+
|
|
36
47
|
data_sets = possible_rules.collect do |grouping_rule|
|
|
37
|
-
|
|
48
|
+
suffix = conflicting_labels.include?(grouping_rule.label) && grouping_rule.highlight ? '*' : ''
|
|
49
|
+
make_data_set grouping_rule: grouping_rule, issue_rules_by_active_date: issue_rules_by_active_date,
|
|
50
|
+
label_suffix: suffix
|
|
38
51
|
end
|
|
39
52
|
if @trend_lines
|
|
40
53
|
data_sets = @trend_lines.filter_map do |group_labels, line_color|
|
|
@@ -82,16 +95,16 @@ class DailyWipChart < ChartBase
|
|
|
82
95
|
hash
|
|
83
96
|
end
|
|
84
97
|
|
|
85
|
-
def make_data_set grouping_rule:, issue_rules_by_active_date:
|
|
98
|
+
def make_data_set grouping_rule:, issue_rules_by_active_date:, label_suffix: ''
|
|
86
99
|
positive = grouping_rule.group_priority >= 0
|
|
100
|
+
display_label = "#{grouping_rule.label}#{label_suffix}"
|
|
87
101
|
|
|
88
102
|
data = issue_rules_by_active_date.collect do |date, issue_rules|
|
|
89
|
-
# issues = []
|
|
90
103
|
issue_strings = issue_rules
|
|
91
104
|
.select { |_issue, rules| rules.group == grouping_rule.group }
|
|
92
105
|
.sort_by { |issue, _rules| issue.key_as_i }
|
|
93
106
|
.collect { |issue, rules| "#{issue.key} : #{issue.summary.strip} #{rules.issue_hint}" }
|
|
94
|
-
title = ["#{
|
|
107
|
+
title = ["#{display_label} (#{label_issues issue_strings.size})"] + issue_strings
|
|
95
108
|
|
|
96
109
|
{
|
|
97
110
|
x: date,
|
|
@@ -100,11 +113,18 @@ class DailyWipChart < ChartBase
|
|
|
100
113
|
}
|
|
101
114
|
end
|
|
102
115
|
|
|
116
|
+
color = grouping_rule.color || random_color
|
|
117
|
+
background_color = if grouping_rule.highlight
|
|
118
|
+
RawJavascript.new("createDiagonalPattern(#{color.to_json})")
|
|
119
|
+
else
|
|
120
|
+
color
|
|
121
|
+
end
|
|
122
|
+
|
|
103
123
|
{
|
|
104
124
|
type: 'bar',
|
|
105
|
-
label:
|
|
125
|
+
label: display_label,
|
|
106
126
|
data: data,
|
|
107
|
-
backgroundColor:
|
|
127
|
+
backgroundColor: background_color,
|
|
108
128
|
borderColor: CssVariable['--wip-chart-border-color'],
|
|
109
129
|
borderWidth: grouping_rule.color.to_s == 'var(--body-background)' ? 1 : 0,
|
|
110
130
|
borderRadius: positive ? 0 : 5
|
|
@@ -45,6 +45,7 @@ class DataQualityReport < ChartBase
|
|
|
45
45
|
scan_for_completed_issues_without_a_start_time entry: entry
|
|
46
46
|
scan_for_status_change_after_done entry: entry
|
|
47
47
|
scan_for_backwards_movement entry: entry, backlog_statuses: backlog_statuses
|
|
48
|
+
scan_for_issue_not_in_active_sprint entry: entry
|
|
48
49
|
scan_for_issues_not_created_in_a_backlog_status entry: entry, backlog_statuses: backlog_statuses
|
|
49
50
|
scan_for_stopped_before_started entry: entry
|
|
50
51
|
scan_for_issues_not_started_with_subtasks_that_have entry: entry
|
|
@@ -68,7 +69,7 @@ class DataQualityReport < ChartBase
|
|
|
68
69
|
result << render_problem_type(:status_changes_after_done)
|
|
69
70
|
result << render_problem_type(:backwards_through_status_categories)
|
|
70
71
|
result << render_problem_type(:backwords_through_statuses)
|
|
71
|
-
result << render_problem_type(:
|
|
72
|
+
result << render_problem_type(:issue_not_visible_on_board)
|
|
72
73
|
result << render_problem_type(:created_in_wrong_status)
|
|
73
74
|
result << render_problem_type(:stopped_before_started)
|
|
74
75
|
result << render_problem_type(:issue_not_started_but_subtasks_have)
|
|
@@ -194,7 +195,7 @@ class DataQualityReport < ChartBase
|
|
|
194
195
|
# If it's been moved back to backlog then it's on a different report. Ignore it here.
|
|
195
196
|
detail = nil if backlog_statuses.any? { |s| s.name == change.value }
|
|
196
197
|
|
|
197
|
-
entry.report(problem_key: :
|
|
198
|
+
entry.report(problem_key: :issue_not_visible_on_board, detail: detail) unless detail.nil?
|
|
198
199
|
elsif change.old_value.nil?
|
|
199
200
|
# Do nothing
|
|
200
201
|
elsif index < last_index
|
|
@@ -223,6 +224,29 @@ class DataQualityReport < ChartBase
|
|
|
223
224
|
end
|
|
224
225
|
end
|
|
225
226
|
|
|
227
|
+
def scan_for_issue_not_in_active_sprint entry:
|
|
228
|
+
issue = entry.issue
|
|
229
|
+
return unless issue.board.scrum?
|
|
230
|
+
return if issue.sprints.any?(&:active?)
|
|
231
|
+
|
|
232
|
+
entry.report(problem_key: :issue_not_visible_on_board, detail: 'Issue is not in an active sprint')
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def scan_for_issue_never_visible_on_board entry:
|
|
236
|
+
issue = entry.issue
|
|
237
|
+
ever_visible = issue.changes.any? do |change|
|
|
238
|
+
next unless change.status?
|
|
239
|
+
|
|
240
|
+
issue.board.visible_columns.any? { |col| col.status_ids.include?(change.value_id) }
|
|
241
|
+
end
|
|
242
|
+
return if ever_visible
|
|
243
|
+
|
|
244
|
+
entry.report(
|
|
245
|
+
problem_key: :issue_not_visible_on_board,
|
|
246
|
+
detail: 'Issue has never been in a status mapped to a visible column on the board'
|
|
247
|
+
)
|
|
248
|
+
end
|
|
249
|
+
|
|
226
250
|
def scan_for_issues_not_created_in_a_backlog_status entry:, backlog_statuses:
|
|
227
251
|
creation_change = entry.issue.changes.find { |issue| issue.status? }
|
|
228
252
|
|
|
@@ -295,7 +319,7 @@ class DataQualityReport < ChartBase
|
|
|
295
319
|
return "#{delta} hours" if delta < 24
|
|
296
320
|
|
|
297
321
|
delta /= 24
|
|
298
|
-
|
|
322
|
+
label_days delta
|
|
299
323
|
end
|
|
300
324
|
|
|
301
325
|
def scan_for_incomplete_subtasks_when_issue_done entry:
|
|
@@ -409,12 +433,12 @@ class DataQualityReport < ChartBase
|
|
|
409
433
|
HTML
|
|
410
434
|
end
|
|
411
435
|
|
|
412
|
-
def
|
|
436
|
+
def render_issue_not_visible_on_board problems
|
|
413
437
|
<<-HTML
|
|
414
438
|
#{label_issues problems.size} were not visible on the board for some period of time. This may impact
|
|
415
|
-
timings as the work was likely to have been forgotten if it wasn't visible.
|
|
416
|
-
|
|
417
|
-
|
|
439
|
+
timings as the work was likely to have been forgotten if it wasn't visible. An issue can be not visible
|
|
440
|
+
for two reasons: the issue was in a status that is not mapped to any visible column on the board
|
|
441
|
+
(look in "unmapped statuses" on your board), or for scrum boards, the issue was not in an active sprint.
|
|
418
442
|
HTML
|
|
419
443
|
end
|
|
420
444
|
|