jirametrics 1.0.0
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 +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
|