jirametrics 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/jirametrics +4 -0
- data/lib/jirametrics/aggregate_config.rb +89 -0
- data/lib/jirametrics/aging_work_bar_chart.rb +235 -0
- data/lib/jirametrics/aging_work_in_progress_chart.rb +148 -0
- data/lib/jirametrics/aging_work_table.rb +149 -0
- data/lib/jirametrics/anonymizer.rb +186 -0
- data/lib/jirametrics/blocked_stalled_change.rb +43 -0
- data/lib/jirametrics/board.rb +85 -0
- data/lib/jirametrics/board_column.rb +14 -0
- data/lib/jirametrics/board_config.rb +31 -0
- data/lib/jirametrics/change_item.rb +80 -0
- data/lib/jirametrics/chart_base.rb +239 -0
- data/lib/jirametrics/columns_config.rb +42 -0
- data/lib/jirametrics/cycletime_config.rb +69 -0
- data/lib/jirametrics/cycletime_histogram.rb +74 -0
- data/lib/jirametrics/cycletime_scatterplot.rb +128 -0
- data/lib/jirametrics/daily_wip_by_age_chart.rb +88 -0
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +77 -0
- data/lib/jirametrics/daily_wip_chart.rb +123 -0
- data/lib/jirametrics/data_quality_report.rb +278 -0
- data/lib/jirametrics/dependency_chart.rb +217 -0
- data/lib/jirametrics/discard_changes_before.rb +37 -0
- data/lib/jirametrics/download_config.rb +41 -0
- data/lib/jirametrics/downloader.rb +337 -0
- data/lib/jirametrics/examples/aggregated_project.rb +36 -0
- data/lib/jirametrics/examples/standard_project.rb +111 -0
- data/lib/jirametrics/expedited_chart.rb +169 -0
- data/lib/jirametrics/experimental/generator.rb +209 -0
- data/lib/jirametrics/experimental/info.rb +77 -0
- data/lib/jirametrics/exporter.rb +127 -0
- data/lib/jirametrics/file_config.rb +119 -0
- data/lib/jirametrics/fix_version.rb +21 -0
- data/lib/jirametrics/groupable_issue_chart.rb +44 -0
- data/lib/jirametrics/grouping_rules.rb +13 -0
- data/lib/jirametrics/hierarchy_table.rb +31 -0
- data/lib/jirametrics/html/aging_work_bar_chart.erb +72 -0
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +52 -0
- data/lib/jirametrics/html/aging_work_table.erb +60 -0
- data/lib/jirametrics/html/collapsible_issues_panel.erb +32 -0
- data/lib/jirametrics/html/cycletime_histogram.erb +41 -0
- data/lib/jirametrics/html/cycletime_scatterplot.erb +103 -0
- data/lib/jirametrics/html/daily_wip_chart.erb +63 -0
- data/lib/jirametrics/html/data_quality_report.erb +126 -0
- data/lib/jirametrics/html/expedited_chart.erb +67 -0
- data/lib/jirametrics/html/hierarchy_table.erb +29 -0
- data/lib/jirametrics/html/index.erb +66 -0
- data/lib/jirametrics/html/sprint_burndown.erb +116 -0
- data/lib/jirametrics/html/story_point_accuracy_chart.erb +57 -0
- data/lib/jirametrics/html/throughput_chart.erb +65 -0
- data/lib/jirametrics/html_report_config.rb +217 -0
- data/lib/jirametrics/issue.rb +521 -0
- data/lib/jirametrics/issue_link.rb +60 -0
- data/lib/jirametrics/json_file_loader.rb +9 -0
- data/lib/jirametrics/project_config.rb +442 -0
- data/lib/jirametrics/rules.rb +34 -0
- data/lib/jirametrics/self_or_issue_dispatcher.rb +15 -0
- data/lib/jirametrics/sprint.rb +43 -0
- data/lib/jirametrics/sprint_burndown.rb +335 -0
- data/lib/jirametrics/sprint_issue_change_data.rb +31 -0
- data/lib/jirametrics/status.rb +26 -0
- data/lib/jirametrics/status_collection.rb +67 -0
- data/lib/jirametrics/story_point_accuracy_chart.rb +139 -0
- data/lib/jirametrics/throughput_chart.rb +91 -0
- data/lib/jirametrics/tree_organizer.rb +96 -0
- data/lib/jirametrics/trend_line_calculator.rb +74 -0
- data/lib/jirametrics.rb +85 -0
- metadata +167 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jirametrics/self_or_issue_dispatcher'
|
4
|
+
|
5
|
+
class ColumnsConfig
|
6
|
+
include SelfOrIssueDispatcher
|
7
|
+
|
8
|
+
attr_reader :columns, :file_config
|
9
|
+
|
10
|
+
def initialize file_config:, block:
|
11
|
+
@columns = []
|
12
|
+
@file_config = file_config
|
13
|
+
@block = block
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
instance_eval(&@block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def write_headers headers = nil
|
21
|
+
@write_headers = headers unless headers.nil?
|
22
|
+
@write_headers
|
23
|
+
end
|
24
|
+
|
25
|
+
def date label, proc
|
26
|
+
@columns << [:date, label, proc]
|
27
|
+
end
|
28
|
+
|
29
|
+
def datetime label, proc
|
30
|
+
@columns << [:datetime, label, proc]
|
31
|
+
end
|
32
|
+
|
33
|
+
def string label, proc
|
34
|
+
@columns << [:string, label, proc]
|
35
|
+
end
|
36
|
+
|
37
|
+
def column_entry_times board_id: nil
|
38
|
+
@file_config.project_config.find_board_by_id(board_id).visible_columns.each do |column|
|
39
|
+
date column.name, first_time_in_status(*column.status_ids)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jirametrics/self_or_issue_dispatcher'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
class CycleTimeConfig
|
7
|
+
include SelfOrIssueDispatcher
|
8
|
+
|
9
|
+
attr_reader :label, :parent_config
|
10
|
+
|
11
|
+
def initialize parent_config:, label:, block:, today: Date.today
|
12
|
+
@parent_config = parent_config
|
13
|
+
@label = label
|
14
|
+
@today = today
|
15
|
+
instance_eval(&block) unless block.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
def start_at block = nil
|
19
|
+
@start_at = block unless block.nil?
|
20
|
+
@start_at
|
21
|
+
end
|
22
|
+
|
23
|
+
def stop_at block = nil
|
24
|
+
@stop_at = block unless block.nil?
|
25
|
+
@stop_at
|
26
|
+
end
|
27
|
+
|
28
|
+
def in_progress? issue
|
29
|
+
started_time(issue) && stopped_time(issue).nil?
|
30
|
+
end
|
31
|
+
|
32
|
+
def done? issue
|
33
|
+
stopped_time(issue)
|
34
|
+
end
|
35
|
+
|
36
|
+
def started_time issue
|
37
|
+
@start_at.call(issue)
|
38
|
+
end
|
39
|
+
|
40
|
+
def stopped_time issue
|
41
|
+
@stop_at.call(issue)
|
42
|
+
end
|
43
|
+
|
44
|
+
def cycletime issue
|
45
|
+
start = started_time(issue)
|
46
|
+
stop = stopped_time(issue)
|
47
|
+
return nil if start.nil? || stop.nil?
|
48
|
+
|
49
|
+
(stop.to_date - start.to_date).to_i + 1
|
50
|
+
end
|
51
|
+
|
52
|
+
def age issue, today: nil
|
53
|
+
start = started_time(issue)
|
54
|
+
stop = today || @today || Date.today
|
55
|
+
return nil if start.nil? || stop.nil?
|
56
|
+
|
57
|
+
(stop.to_date - start.to_date).to_i + 1
|
58
|
+
end
|
59
|
+
|
60
|
+
def possible_statuses
|
61
|
+
if parent_config.is_a? BoardConfig
|
62
|
+
project_config = parent_config.project_config
|
63
|
+
else
|
64
|
+
# TODO: This will go away when cycletimes are no longer supported inside html_reports
|
65
|
+
project_config = parent_config.file_config.project_config
|
66
|
+
end
|
67
|
+
project_config.possible_statuses
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jirametrics/groupable_issue_chart'
|
4
|
+
|
5
|
+
class CycletimeHistogram < ChartBase
|
6
|
+
include GroupableIssueChart
|
7
|
+
attr_accessor :possible_statuses
|
8
|
+
|
9
|
+
def initialize block = nil
|
10
|
+
super()
|
11
|
+
|
12
|
+
header_text 'Cycletime Histogram'
|
13
|
+
description_text <<-HTML
|
14
|
+
<p>
|
15
|
+
The Cycletime Histogram shows how many items completed in a certain timeframe. This can be
|
16
|
+
useful for determining how many different types of work are flowing through, based on the
|
17
|
+
lengths of time they take.
|
18
|
+
</p>
|
19
|
+
HTML
|
20
|
+
|
21
|
+
init_configuration_block(block) do
|
22
|
+
grouping_rules do |issue, rule|
|
23
|
+
rule.label = issue.type
|
24
|
+
rule.color = color_for type: issue.type
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def run
|
30
|
+
stopped_issues = completed_issues_in_range include_unstarted: true
|
31
|
+
|
32
|
+
# For the histogram, we only want to consider items that have both a start and a stop time.
|
33
|
+
histogram_issues = stopped_issues.select { |issue| issue.board.cycletime.started_time(issue) }
|
34
|
+
rules_to_issues = group_issues histogram_issues
|
35
|
+
|
36
|
+
data_sets = rules_to_issues.keys.collect do |rules|
|
37
|
+
data_set_for(
|
38
|
+
histogram_data: histogram_data_for(issues: rules_to_issues[rules]),
|
39
|
+
label: rules.label,
|
40
|
+
color: rules.color
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
wrap_and_render(binding, __FILE__)
|
45
|
+
end
|
46
|
+
|
47
|
+
def histogram_data_for issues:
|
48
|
+
count_hash = {}
|
49
|
+
issues.each do |issue|
|
50
|
+
days = issue.board.cycletime.cycletime(issue)
|
51
|
+
count_hash[days] = (count_hash[days] || 0) + 1 if days.positive?
|
52
|
+
end
|
53
|
+
count_hash
|
54
|
+
end
|
55
|
+
|
56
|
+
def data_set_for histogram_data:, label:, color:
|
57
|
+
keys = histogram_data.keys.sort
|
58
|
+
{
|
59
|
+
type: 'bar',
|
60
|
+
label: label,
|
61
|
+
data: keys.sort.collect do |key|
|
62
|
+
next if histogram_data[key].zero?
|
63
|
+
|
64
|
+
{
|
65
|
+
x: key,
|
66
|
+
y: histogram_data[key],
|
67
|
+
title: "#{histogram_data[key]} items completed in #{label_days key}"
|
68
|
+
}
|
69
|
+
end.compact,
|
70
|
+
backgroundColor: color,
|
71
|
+
borderRadius: 0
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jirametrics/groupable_issue_chart'
|
4
|
+
|
5
|
+
class CycletimeScatterplot < ChartBase
|
6
|
+
include GroupableIssueChart
|
7
|
+
|
8
|
+
attr_accessor :possible_statuses
|
9
|
+
|
10
|
+
def initialize block = nil
|
11
|
+
super()
|
12
|
+
|
13
|
+
header_text 'Cycletime Scatterplot'
|
14
|
+
description_text <<-HTML
|
15
|
+
<p>
|
16
|
+
This chart shows only completed work and indicates both what day it completed as well as
|
17
|
+
how many days it took to get done. Hovering over a dot will show you the ID of the work item.
|
18
|
+
</p>
|
19
|
+
<p>
|
20
|
+
The gray line indicates the 85th percentile (<%= overall_percent_line %> days). 85% of all
|
21
|
+
items on this chart fall on or below the line and the remaining 15% are above the line. 85%
|
22
|
+
is a reasonable proxy for "most" so that we can say that based on this data set, we can
|
23
|
+
predict that most work of this type will complete in <%= overall_percent_line %> days or
|
24
|
+
less. The other lines reflect the 85% line for that respective type of work.
|
25
|
+
</p>
|
26
|
+
<p>
|
27
|
+
The gray vertical bars indicate weekends, when theoretically we aren't working.
|
28
|
+
</p>
|
29
|
+
HTML
|
30
|
+
|
31
|
+
init_configuration_block block do
|
32
|
+
grouping_rules do |issue, rule|
|
33
|
+
rule.label = issue.type
|
34
|
+
rule.color = color_for type: issue.type
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
@percentage_lines = []
|
39
|
+
@highest_cycletime = 0
|
40
|
+
end
|
41
|
+
|
42
|
+
def run
|
43
|
+
completed_issues = completed_issues_in_range include_unstarted: false
|
44
|
+
|
45
|
+
data_sets = create_datasets completed_issues
|
46
|
+
overall_percent_line = calculate_percent_line(completed_issues)
|
47
|
+
@percentage_lines << [overall_percent_line, 'gray']
|
48
|
+
|
49
|
+
return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
|
50
|
+
|
51
|
+
wrap_and_render(binding, __FILE__)
|
52
|
+
end
|
53
|
+
|
54
|
+
def create_datasets completed_issues
|
55
|
+
data_sets = []
|
56
|
+
|
57
|
+
groups = group_issues completed_issues
|
58
|
+
|
59
|
+
groups.each_key do |rules|
|
60
|
+
completed_issues_by_type = groups[rules]
|
61
|
+
label = rules.label
|
62
|
+
color = rules.color
|
63
|
+
percent_line = calculate_percent_line completed_issues_by_type
|
64
|
+
data = completed_issues_by_type.collect { |issue| data_for_issue(issue) }.compact
|
65
|
+
data_sets << {
|
66
|
+
label: "#{label} (85% at #{label_days(percent_line)})",
|
67
|
+
data: data,
|
68
|
+
fill: false,
|
69
|
+
showLine: false,
|
70
|
+
backgroundColor: color
|
71
|
+
}
|
72
|
+
|
73
|
+
data_sets << trend_line_data_set(label: label, data: data, color: color)
|
74
|
+
|
75
|
+
@percentage_lines << [percent_line, color]
|
76
|
+
end
|
77
|
+
data_sets
|
78
|
+
end
|
79
|
+
|
80
|
+
def show_trend_lines
|
81
|
+
@show_trend_lines = true
|
82
|
+
end
|
83
|
+
|
84
|
+
def trend_line_data_set label:, data:, color:
|
85
|
+
points = data.collect do |hash|
|
86
|
+
[Time.parse(hash[:x]).to_i, hash[:y]]
|
87
|
+
end
|
88
|
+
|
89
|
+
# The trend calculation works with numbers only so convert Time to an int and back
|
90
|
+
calculator = TrendLineCalculator.new(points)
|
91
|
+
data_points = calculator.chart_datapoints range: time_range.begin.to_i..time_range.end.to_i, max_y: @highest_cycletime
|
92
|
+
data_points.each do |point_hash|
|
93
|
+
point_hash[:x] = chart_format Time.at(point_hash[:x])
|
94
|
+
end
|
95
|
+
|
96
|
+
{
|
97
|
+
type: 'line',
|
98
|
+
label: "#{label} Trendline",
|
99
|
+
data: data_points,
|
100
|
+
fill: false,
|
101
|
+
borderWidth: 1,
|
102
|
+
markerType: 'none',
|
103
|
+
borderColor: color,
|
104
|
+
borderDash: [6, 3],
|
105
|
+
pointStyle: 'dash',
|
106
|
+
hidden: !@show_trend_lines
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
def data_for_issue issue
|
111
|
+
cycle_time = issue.board.cycletime.cycletime(issue)
|
112
|
+
return nil if cycle_time < 1 # These will get called out on the quality report
|
113
|
+
|
114
|
+
@highest_cycletime = cycle_time if @highest_cycletime < cycle_time
|
115
|
+
|
116
|
+
{
|
117
|
+
y: cycle_time,
|
118
|
+
x: chart_format(issue.board.cycletime.stopped_time(issue)),
|
119
|
+
title: ["#{issue.key} : #{issue.summary} (#{label_days(cycle_time)})"]
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
def calculate_percent_line completed_issues
|
124
|
+
times = completed_issues.collect { |issue| issue.board.cycletime.cycletime(issue) }
|
125
|
+
index = times.size * 85 / 100
|
126
|
+
times.sort[index]
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jirametrics/daily_wip_chart'
|
4
|
+
|
5
|
+
class DailyWipByAgeChart < DailyWipChart
|
6
|
+
def default_header_text
|
7
|
+
'Daily WIP grouped by Age'
|
8
|
+
end
|
9
|
+
|
10
|
+
def default_description_text
|
11
|
+
<<-HTML
|
12
|
+
<p>
|
13
|
+
This chart shows the highest WIP on each given day. The WIP is color coded so you can see
|
14
|
+
how old it is and hovering over the bar will show you exactly which work items it relates
|
15
|
+
to. The green bar underneath, shows how many items completed on that day.
|
16
|
+
</p>
|
17
|
+
<p>
|
18
|
+
"Completed without being started" reflects the fact that while we know that it completed
|
19
|
+
that day, we were unable to determine when it had started. These items will show up in
|
20
|
+
white at the top. Note that the white is approximate because we don't know exactly when
|
21
|
+
it started so we're guessing.
|
22
|
+
</p>
|
23
|
+
HTML
|
24
|
+
end
|
25
|
+
|
26
|
+
def default_grouping_rules issue:, rules:
|
27
|
+
cycletime = issue.board.cycletime
|
28
|
+
started = cycletime.started_time(issue)&.to_date
|
29
|
+
stopped = cycletime.stopped_time(issue)&.to_date
|
30
|
+
|
31
|
+
rules.issue_hint = "(age: #{label_days (rules.current_date - started + 1).to_i})" if started
|
32
|
+
|
33
|
+
if stopped && started.nil? # We can't tell when it started
|
34
|
+
not_started stopped: stopped, rules: rules, created: issue.created.to_date
|
35
|
+
elsif stopped == rules.current_date
|
36
|
+
stopped_today rules: rules
|
37
|
+
else
|
38
|
+
group_by_age started: started, rules: rules
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def not_started stopped:, rules:, created:
|
43
|
+
if stopped == rules.current_date
|
44
|
+
rules.label = 'Completed but not started'
|
45
|
+
rules.color = '#66FF66'
|
46
|
+
rules.group_priority = -1
|
47
|
+
else
|
48
|
+
rules.label = 'Start date unknown'
|
49
|
+
rules.color = 'white'
|
50
|
+
rules.group_priority = 11
|
51
|
+
created_days = rules.current_date - created + 1
|
52
|
+
rules.issue_hint = "(created: #{label_days created_days.to_i} earlier, stopped on #{stopped})"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def stopped_today rules:
|
57
|
+
rules.label = 'Completed'
|
58
|
+
rules.color = '#009900'
|
59
|
+
rules.group_priority = -2
|
60
|
+
end
|
61
|
+
|
62
|
+
def group_by_age started:, rules:
|
63
|
+
age = rules.current_date - started + 1
|
64
|
+
|
65
|
+
case age
|
66
|
+
when 1
|
67
|
+
rules.label = 'Less than a day'
|
68
|
+
rules.color = '#aaaaaa'
|
69
|
+
rules.group_priority = 10 # Highest is top
|
70
|
+
when 2..7
|
71
|
+
rules.label = 'A week or less'
|
72
|
+
rules.color = '#80bfff'
|
73
|
+
rules.group_priority = 9
|
74
|
+
when 8..14
|
75
|
+
rules.label = 'Two weeks or less'
|
76
|
+
rules.color = '#ffd700'
|
77
|
+
rules.group_priority = 8
|
78
|
+
when 15..28
|
79
|
+
rules.label = 'Four weeks or less'
|
80
|
+
rules.color = '#ce6300'
|
81
|
+
rules.group_priority = 7
|
82
|
+
else
|
83
|
+
rules.label = 'More than four weeks'
|
84
|
+
rules.color = '#990000'
|
85
|
+
rules.group_priority = 6
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jirametrics/daily_wip_chart'
|
4
|
+
|
5
|
+
class DailyWipByBlockedStalledChart < DailyWipChart
|
6
|
+
def default_header_text
|
7
|
+
'Daily WIP, grouped by Blocked and Stalled statuses'
|
8
|
+
end
|
9
|
+
|
10
|
+
def default_description_text
|
11
|
+
<<-HTML
|
12
|
+
<p>
|
13
|
+
This chart highlights work that is blocked or stalled on each given day. In Jira terms, blocked
|
14
|
+
means that the issue has been "flagged". Stalled indicates that the item hasn't had any updates in 5 days.
|
15
|
+
</p>
|
16
|
+
<p>
|
17
|
+
Note that if an item tracks as both blocked and stalled, it will only show up in the flagged totals.
|
18
|
+
It will not be double counted.
|
19
|
+
</p>
|
20
|
+
<p>
|
21
|
+
The white section reflects items that have stopped but for which we can't identify the start date. As
|
22
|
+
a result, we are unable to properly show the WIP for these items.
|
23
|
+
</p>
|
24
|
+
HTML
|
25
|
+
end
|
26
|
+
|
27
|
+
def key_blocked_stalled_change issue:, date:, end_time:
|
28
|
+
stalled_change = nil
|
29
|
+
blocked_change = nil
|
30
|
+
|
31
|
+
issue.blocked_stalled_changes_on_date(date: date, end_time: end_time) do |change|
|
32
|
+
blocked_change = change if change.blocked?
|
33
|
+
stalled_change = change if change.stalled?
|
34
|
+
end
|
35
|
+
|
36
|
+
return blocked_change if blocked_change
|
37
|
+
return stalled_change if stalled_change
|
38
|
+
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def default_grouping_rules issue:, rules:
|
43
|
+
started = issue.board.cycletime.started_time(issue)
|
44
|
+
stopped_date = issue.board.cycletime.stopped_time(issue)&.to_date
|
45
|
+
change = key_blocked_stalled_change issue: issue, date: rules.current_date, end_time: time_range.end
|
46
|
+
|
47
|
+
stopped_today = stopped_date == rules.current_date
|
48
|
+
|
49
|
+
if stopped_today && started.nil?
|
50
|
+
rules.label = 'Completed but not started'
|
51
|
+
rules.color = '#66FF66'
|
52
|
+
rules.group_priority = -1
|
53
|
+
elsif stopped_today
|
54
|
+
rules.label = 'Completed'
|
55
|
+
rules.color = '#009900'
|
56
|
+
rules.group_priority = -2
|
57
|
+
elsif started.nil?
|
58
|
+
rules.label = 'Start date unknown'
|
59
|
+
rules.color = 'white'
|
60
|
+
rules.group_priority = 4
|
61
|
+
elsif change&.blocked?
|
62
|
+
rules.label = 'Blocked'
|
63
|
+
rules.color = 'red'
|
64
|
+
rules.group_priority = 1
|
65
|
+
rules.issue_hint = "(#{change.reasons})"
|
66
|
+
elsif change&.stalled?
|
67
|
+
rules.label = 'Stalled'
|
68
|
+
rules.color = 'orange'
|
69
|
+
rules.group_priority = 2
|
70
|
+
rules.issue_hint = "(#{change.reasons})"
|
71
|
+
else
|
72
|
+
rules.label = 'Active'
|
73
|
+
rules.color = 'lightgray'
|
74
|
+
rules.group_priority = 3
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jirametrics/chart_base'
|
4
|
+
|
5
|
+
class DailyGroupingRules < GroupingRules
|
6
|
+
attr_accessor :current_date, :group_priority, :issue_hint
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super()
|
10
|
+
@group_priority = 0
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class DailyWipChart < ChartBase
|
15
|
+
attr_accessor :possible_statuses
|
16
|
+
|
17
|
+
def initialize block = nil
|
18
|
+
super()
|
19
|
+
|
20
|
+
header_text default_header_text
|
21
|
+
description_text default_description_text
|
22
|
+
|
23
|
+
if block
|
24
|
+
instance_eval(&block)
|
25
|
+
else
|
26
|
+
grouping_rules do |issue, rules|
|
27
|
+
default_grouping_rules issue: issue, rules: rules
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def run
|
33
|
+
issue_rules_by_active_date = group_issues_by_active_dates
|
34
|
+
possible_rules = select_possible_rules issue_rules_by_active_date
|
35
|
+
|
36
|
+
data_sets = possible_rules.collect do |grouping_rule|
|
37
|
+
make_data_set grouping_rule: grouping_rule, issue_rules_by_active_date: issue_rules_by_active_date
|
38
|
+
end
|
39
|
+
|
40
|
+
wrap_and_render(binding, __FILE__)
|
41
|
+
end
|
42
|
+
|
43
|
+
def default_header_text = 'Daily WIP'
|
44
|
+
def default_description_text = ''
|
45
|
+
|
46
|
+
def default_grouping_rules issue:, rules: # rubocop:disable Lint/UnusedMethodArgument
|
47
|
+
raise 'If you use this class directly then you must provide grouping_rules'
|
48
|
+
end
|
49
|
+
|
50
|
+
def select_possible_rules issue_rules_by_active_date
|
51
|
+
possible_rules = []
|
52
|
+
issue_rules_by_active_date.each_pair do |_date, issues_rules_list|
|
53
|
+
issues_rules_list.each do |_issue, rules|
|
54
|
+
possible_rules << rules unless possible_rules.any? { |r| r.group == rules.group }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
possible_rules.sort_by!(&:group_priority)
|
58
|
+
end
|
59
|
+
|
60
|
+
def group_issues_by_active_dates
|
61
|
+
hash = {}
|
62
|
+
|
63
|
+
@issues.each do |issue|
|
64
|
+
cycletime = issue.board.cycletime
|
65
|
+
start = cycletime.started_time(issue)&.to_date
|
66
|
+
stop = cycletime.stopped_time(issue)&.to_date
|
67
|
+
next if start.nil? && stop.nil?
|
68
|
+
|
69
|
+
# If it stopped but never started then assume it started at creation so the data points
|
70
|
+
# will be available for the config.
|
71
|
+
start = issue.created.to_date if stop && start.nil?
|
72
|
+
start = @date_range.begin if start < @date_range.begin
|
73
|
+
|
74
|
+
start.upto(stop || @date_range.end) do |date|
|
75
|
+
rule = configure_rule issue: issue, date: date
|
76
|
+
(hash[date] ||= []) << [issue, rule] unless rule.ignored?
|
77
|
+
end
|
78
|
+
end
|
79
|
+
hash
|
80
|
+
end
|
81
|
+
|
82
|
+
def make_data_set grouping_rule:, issue_rules_by_active_date:
|
83
|
+
positive = grouping_rule.group_priority >= 0
|
84
|
+
|
85
|
+
data = issue_rules_by_active_date.collect do |date, issue_rules|
|
86
|
+
# issues = []
|
87
|
+
issue_strings = issue_rules
|
88
|
+
.select { |_issue, rules| rules.group == grouping_rule.group }
|
89
|
+
.sort_by { |issue, _rules| issue.key_as_i }
|
90
|
+
.collect { |issue, rules| "#{issue.key} : #{issue.summary.strip} #{rules.issue_hint}" }
|
91
|
+
title = ["#{grouping_rule.label} (#{label_issues issue_strings.size})"] + issue_strings
|
92
|
+
|
93
|
+
{
|
94
|
+
x: date,
|
95
|
+
y: positive ? issue_strings.size : -issue_strings.size,
|
96
|
+
title: title
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
{
|
101
|
+
type: 'bar',
|
102
|
+
label: grouping_rule.label,
|
103
|
+
data: data,
|
104
|
+
backgroundColor: grouping_rule.color || random_color,
|
105
|
+
borderColor: 'gray',
|
106
|
+
borderWidth: grouping_rule.color == 'white' ? 1 : 0,
|
107
|
+
borderRadius: positive ? 0 : 5
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
def configure_rule issue:, date:
|
112
|
+
raise 'grouping_rules must be set' if @group_by_block.nil?
|
113
|
+
|
114
|
+
rules = DailyGroupingRules.new
|
115
|
+
rules.current_date = date
|
116
|
+
@group_by_block.call issue, rules
|
117
|
+
rules
|
118
|
+
end
|
119
|
+
|
120
|
+
def grouping_rules &block
|
121
|
+
@group_by_block = block
|
122
|
+
end
|
123
|
+
end
|