jirametrics 2.2.1 → 2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/jirametrics/aggregate_config.rb +13 -25
- data/lib/jirametrics/aging_work_bar_chart.rb +57 -39
- data/lib/jirametrics/aging_work_in_progress_chart.rb +1 -1
- data/lib/jirametrics/aging_work_table.rb +9 -26
- data/lib/jirametrics/board_config.rb +2 -2
- data/lib/jirametrics/chart_base.rb +27 -39
- data/lib/jirametrics/cycletime_histogram.rb +1 -1
- data/lib/jirametrics/cycletime_scatterplot.rb +1 -1
- data/lib/jirametrics/daily_wip_by_age_chart.rb +1 -1
- data/lib/jirametrics/daily_wip_chart.rb +1 -13
- data/lib/jirametrics/dependency_chart.rb +1 -1
- data/lib/jirametrics/{story_point_accuracy_chart.rb → estimate_accuracy_chart.rb} +31 -25
- data/lib/jirametrics/examples/standard_project.rb +1 -1
- data/lib/jirametrics/expedited_chart.rb +3 -1
- data/lib/jirametrics/exporter.rb +2 -2
- data/lib/jirametrics/file_config.rb +5 -7
- data/lib/jirametrics/file_system.rb +11 -2
- data/lib/jirametrics/groupable_issue_chart.rb +2 -4
- data/lib/jirametrics/hierarchy_table.rb +4 -4
- data/lib/jirametrics/html/aging_work_table.erb +3 -3
- data/lib/jirametrics/html_report_config.rb +61 -74
- data/lib/jirametrics/issue.rb +70 -39
- data/lib/jirametrics/project_config.rb +12 -6
- data/lib/jirametrics/sprint_burndown.rb +11 -0
- data/lib/jirametrics/status_collection.rb +4 -1
- data/lib/jirametrics/throughput_chart.rb +1 -1
- data/lib/jirametrics.rb +1 -1
- metadata +5 -7
- data/lib/jirametrics/experimental/generator.rb +0 -210
- data/lib/jirametrics/experimental/info.rb +0 -77
- /data/lib/jirametrics/html/{story_point_accuracy_chart.erb → estimate_accuracy_chart.erb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e7264aee468cd14d42fa1c7ba8f737177ba99a78b3f7250d46771f687d4d619
|
4
|
+
data.tar.gz: da813f37b2a6047d6ab844f483008ecdb8c579786bd7bba7f2c01f038a259949
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c234046627a2c92729c346f44cf78471e74545e01ce3a9810c6c22ed659f6479a0146303ac7fb0652512feafcf8077025577d2a4c039dddf2fd70d6899360adb
|
7
|
+
data.tar.gz: 81f24c30b8d56f8811ed298ca17cebcee6cb32afdbea5cfd12332587aabaa2701b023dc620b651e111226bf66bac9285f84a893549c00c45bd0eb992694a2c64
|
@@ -19,14 +19,15 @@ class AggregateConfig
|
|
19
19
|
raise "#{@project_config.name}: When aggregating, you must include at least one other project"
|
20
20
|
end
|
21
21
|
|
22
|
-
# If the
|
22
|
+
# If the time range wasn't set then calculate it now
|
23
23
|
@project_config.time_range = find_time_range projects: @included_projects if @project_config.time_range.nil?
|
24
24
|
|
25
|
-
adjust_issue_links
|
25
|
+
adjust_issue_links issues: @project_config.issues
|
26
26
|
end
|
27
27
|
|
28
|
-
|
29
|
-
|
28
|
+
# IssueLinks just have a reference to the key. Walk through all of them to see if we have a full
|
29
|
+
# issue that we'd already loaded. If we do, then replace it.
|
30
|
+
def adjust_issue_links issues:
|
30
31
|
issues.each do |issue|
|
31
32
|
issue.issue_links.each do |link|
|
32
33
|
other_issue_key = link.other_issue.key
|
@@ -40,7 +41,7 @@ class AggregateConfig
|
|
40
41
|
def include_issues_from project_name
|
41
42
|
project = @project_config.exporter.project_configs.find { |p| p.name == project_name }
|
42
43
|
if project.nil?
|
43
|
-
|
44
|
+
log "Warning: Aggregated project #{@project_config.name.inspect} is attempting to load " \
|
44
45
|
"project #{project_name.inspect} but it can't be found. Is it disabled?"
|
45
46
|
return
|
46
47
|
end
|
@@ -57,30 +58,13 @@ class AggregateConfig
|
|
57
58
|
else
|
58
59
|
issues = project.file_configs.first.issues
|
59
60
|
if project.file_configs.size > 1
|
60
|
-
|
61
|
+
log 'More than one file section is defined. For the aggregated view, we always use ' \
|
61
62
|
'the first file section'
|
62
63
|
end
|
63
64
|
end
|
64
65
|
@project_config.add_issues issues
|
65
66
|
end
|
66
67
|
|
67
|
-
def date_range range
|
68
|
-
@project_config.time_range = date_range_to_time_range(
|
69
|
-
date_range: range, timezone_offset: project_config.exporter.timezone_offset
|
70
|
-
)
|
71
|
-
end
|
72
|
-
|
73
|
-
def date_range_to_time_range date_range:, timezone_offset:
|
74
|
-
start_of_first_day = Time.new(
|
75
|
-
date_range.begin.year, date_range.begin.month, date_range.begin.day, 0, 0, 0, timezone_offset
|
76
|
-
)
|
77
|
-
end_of_last_day = Time.new(
|
78
|
-
date_range.end.year, date_range.end.month, date_range.end.day, 23, 59, 59, timezone_offset
|
79
|
-
)
|
80
|
-
|
81
|
-
start_of_first_day..end_of_last_day
|
82
|
-
end
|
83
|
-
|
84
68
|
def find_time_range projects:
|
85
69
|
raise "Can't calculate aggregated range as no projects were included." if projects.empty?
|
86
70
|
|
@@ -92,8 +76,12 @@ class AggregateConfig
|
|
92
76
|
latest = range.end if latest.nil? || range.end > latest
|
93
77
|
end
|
94
78
|
|
95
|
-
raise "Can't calculate range" if earliest.nil? || latest.nil?
|
96
|
-
|
97
79
|
earliest..latest
|
98
80
|
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def log message
|
85
|
+
@project_config.exporter.file_system.log message
|
86
|
+
end
|
99
87
|
end
|
@@ -5,7 +5,7 @@ require 'jirametrics/chart_base'
|
|
5
5
|
class AgingWorkBarChart < ChartBase
|
6
6
|
@@next_id = 0
|
7
7
|
|
8
|
-
def initialize block
|
8
|
+
def initialize block
|
9
9
|
super()
|
10
10
|
|
11
11
|
header_text 'Aging Work Bar Chart'
|
@@ -27,61 +27,77 @@ class AgingWorkBarChart < ChartBase
|
|
27
27
|
<li>The bottom bar indicated #{color_block '--expedited-color'} expedited.</li>
|
28
28
|
</ol>
|
29
29
|
</p>
|
30
|
-
#{
|
30
|
+
#{describe_non_working_days}
|
31
31
|
HTML
|
32
32
|
|
33
33
|
# Because this one will size itself as needed, we start with a smaller default size
|
34
34
|
@canvas_height = 80
|
35
35
|
|
36
|
-
instance_eval(&block)
|
36
|
+
instance_eval(&block)
|
37
37
|
end
|
38
38
|
|
39
39
|
def run
|
40
|
-
aging_issues = @issues
|
41
|
-
cycletime = issue.board.cycletime
|
42
|
-
cycletime.started_time(issue) && cycletime.stopped_time(issue).nil?
|
43
|
-
end
|
44
|
-
|
45
|
-
grow_chart_height_if_too_many_issues aging_issues.size
|
40
|
+
aging_issues = select_aging_issues issues: @issues
|
46
41
|
|
47
42
|
today = date_range.end
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
|
57
|
-
[
|
58
|
-
status_data_sets(issue: issue, label: issue_label, today: today),
|
59
|
-
blocked_data_sets(
|
60
|
-
issue: issue,
|
61
|
-
issue_label: issue_label,
|
62
|
-
stack: 'blocked',
|
63
|
-
issue_start_time: issue_start_time
|
64
|
-
),
|
65
|
-
data_set_by_block(
|
66
|
-
issue: issue,
|
67
|
-
issue_label: issue_label,
|
68
|
-
title_label: 'Expedited',
|
69
|
-
stack: 'expedited',
|
70
|
-
color: CssVariable['--expedited-color'],
|
71
|
-
start_date: issue_start_date
|
72
|
-
) { |day| issue.expedited_on_date?(day) }
|
73
|
-
].compact.flatten.each do |data|
|
74
|
-
data_sets << data
|
75
|
-
end
|
76
|
-
end
|
43
|
+
sort_by_age! issues: aging_issues, today: today
|
44
|
+
|
45
|
+
grow_chart_height_if_too_many_issues aging_issue_count: aging_issues.size
|
46
|
+
|
47
|
+
data_sets = aging_issues
|
48
|
+
.collect { |issue| data_sets_for_one_issue issue: issue, today: today }
|
49
|
+
.flatten
|
50
|
+
.compact
|
77
51
|
|
78
52
|
percentage = calculate_percent_line
|
79
53
|
percentage_line_x = date_range.end - calculate_percent_line if percentage
|
80
54
|
|
55
|
+
if aging_issues.empty?
|
56
|
+
@description_text = "<p>There is no aging work</p>"
|
57
|
+
return render_top_text(binding) #if aging_issues.empty?
|
58
|
+
end
|
59
|
+
|
81
60
|
wrap_and_render(binding, __FILE__)
|
82
61
|
end
|
83
62
|
|
84
|
-
def
|
63
|
+
def data_sets_for_one_issue issue:, today:
|
64
|
+
cycletime = issue.board.cycletime
|
65
|
+
issue_start_time = cycletime.started_time(issue)
|
66
|
+
issue_start_date = issue_start_time.to_date
|
67
|
+
issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
|
68
|
+
[
|
69
|
+
status_data_sets(issue: issue, label: issue_label, today: today),
|
70
|
+
blocked_data_sets(
|
71
|
+
issue: issue,
|
72
|
+
issue_label: issue_label,
|
73
|
+
stack: 'blocked',
|
74
|
+
issue_start_time: issue_start_time
|
75
|
+
),
|
76
|
+
data_set_by_block(
|
77
|
+
issue: issue,
|
78
|
+
issue_label: issue_label,
|
79
|
+
title_label: 'Expedited',
|
80
|
+
stack: 'expedited',
|
81
|
+
color: CssVariable['--expedited-color'],
|
82
|
+
start_date: issue_start_date
|
83
|
+
) { |day| issue.expedited_on_date?(day) }
|
84
|
+
]
|
85
|
+
end
|
86
|
+
|
87
|
+
def sort_by_age! issues:, today:
|
88
|
+
issues.sort! do |a, b|
|
89
|
+
a.board.cycletime.age(b, today: today) <=> b.board.cycletime.age(a, today: today)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def select_aging_issues issues:
|
94
|
+
issues.select do |issue|
|
95
|
+
cycletime = issue.board.cycletime
|
96
|
+
cycletime.started_time(issue) && cycletime.stopped_time(issue).nil?
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def grow_chart_height_if_too_many_issues aging_issue_count:
|
85
101
|
px_per_bar = 8
|
86
102
|
bars_per_issue = 3
|
87
103
|
preferred_height = aging_issue_count * px_per_bar * bars_per_issue
|
@@ -220,6 +236,8 @@ class AgingWorkBarChart < ChartBase
|
|
220
236
|
}
|
221
237
|
end
|
222
238
|
|
239
|
+
return [] if data.empty?
|
240
|
+
|
223
241
|
{
|
224
242
|
type: 'bar',
|
225
243
|
data: data,
|
@@ -4,6 +4,7 @@ require 'jirametrics/chart_base'
|
|
4
4
|
|
5
5
|
class AgingWorkTable < ChartBase
|
6
6
|
attr_accessor :today, :board_id
|
7
|
+
attr_reader :any_scrum_boards
|
7
8
|
|
8
9
|
def initialize block
|
9
10
|
super()
|
@@ -23,20 +24,21 @@ class AgingWorkTable < ChartBase
|
|
23
24
|
</p>
|
24
25
|
TEXT
|
25
26
|
|
26
|
-
instance_eval(&block)
|
27
|
+
instance_eval(&block)
|
27
28
|
end
|
28
29
|
|
29
30
|
def run
|
30
31
|
@today = date_range.end
|
31
|
-
aging_issues = select_aging_issues
|
32
|
+
aging_issues = select_aging_issues + expedited_but_not_started
|
32
33
|
|
33
|
-
|
34
|
+
wrap_and_render(binding, __FILE__)
|
35
|
+
end
|
36
|
+
|
37
|
+
def expedited_but_not_started
|
38
|
+
@issues.select do |issue|
|
34
39
|
cycletime = issue.board.cycletime
|
35
40
|
cycletime.started_time(issue).nil? && cycletime.stopped_time(issue).nil? && issue.expedited?
|
36
|
-
end
|
37
|
-
aging_issues += expedited_but_not_started.sort_by(&:created)
|
38
|
-
|
39
|
-
wrap_and_render(binding, __FILE__)
|
41
|
+
end.sort_by(&:created)
|
40
42
|
end
|
41
43
|
|
42
44
|
def select_aging_issues
|
@@ -54,10 +56,6 @@ class AgingWorkTable < ChartBase
|
|
54
56
|
aging_issues.sort { |a, b| b.board.cycletime.age(b, today: @today) <=> a.board.cycletime.age(a, today: @today) }
|
55
57
|
end
|
56
58
|
|
57
|
-
def icon_span title:, icon:
|
58
|
-
"<span title='#{title}' style='font-size: 0.8em;'>#{icon}</span>"
|
59
|
-
end
|
60
|
-
|
61
59
|
def expedited_text issue
|
62
60
|
return unless issue.expedited?
|
63
61
|
|
@@ -85,13 +83,6 @@ class AgingWorkTable < ChartBase
|
|
85
83
|
end
|
86
84
|
end
|
87
85
|
|
88
|
-
def unmapped_status_text issue
|
89
|
-
icon_span(
|
90
|
-
title: "Not visible: The status #{issue.status.name.inspect} is not mapped to any column and will not be visible",
|
91
|
-
icon: ' 👀'
|
92
|
-
)
|
93
|
-
end
|
94
|
-
|
95
86
|
def fix_versions_text issue
|
96
87
|
issue.fix_versions.collect do |fix|
|
97
88
|
if fix.released?
|
@@ -124,19 +115,11 @@ class AgingWorkTable < ChartBase
|
|
124
115
|
end.join('<br />')
|
125
116
|
end
|
126
117
|
|
127
|
-
def current_status_visible? issue
|
128
|
-
issue.board.visible_columns.any? { |column| column.status_ids.include? issue.status.id }
|
129
|
-
end
|
130
|
-
|
131
118
|
def age_cutoff age = nil
|
132
119
|
@age_cutoff = age.to_i if age
|
133
120
|
@age_cutoff
|
134
121
|
end
|
135
122
|
|
136
|
-
def any_scrum_boards?
|
137
|
-
@any_scrum_boards
|
138
|
-
end
|
139
|
-
|
140
123
|
def parent_hierarchy issue
|
141
124
|
result = []
|
142
125
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class BoardConfig
|
4
|
-
attr_reader :id, :project_config
|
4
|
+
attr_reader :id, :project_config, :board
|
5
5
|
|
6
6
|
def initialize id:, block:, project_config:
|
7
7
|
@id = id
|
@@ -26,6 +26,6 @@ class BoardConfig
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def expedited_priority_names *priority_names
|
29
|
-
@board.expedited_priority_names = priority_names
|
29
|
+
@board.expedited_priority_names = priority_names
|
30
30
|
end
|
31
31
|
end
|
@@ -2,9 +2,9 @@
|
|
2
2
|
|
3
3
|
class ChartBase
|
4
4
|
attr_accessor :timezone_offset, :board_id, :all_boards, :date_range,
|
5
|
-
:time_range, :data_quality, :holiday_dates, :settings
|
5
|
+
:time_range, :data_quality, :holiday_dates, :settings, :issues, :file_system
|
6
6
|
attr_writer :aggregated_project
|
7
|
-
attr_reader :
|
7
|
+
attr_reader :canvas_width, :canvas_height
|
8
8
|
|
9
9
|
@@chart_counter = 0
|
10
10
|
|
@@ -34,7 +34,7 @@ class ChartBase
|
|
34
34
|
caller_binding.eval "chart_id='chart#{next_id}'" # chart_id=chart3
|
35
35
|
|
36
36
|
@html_directory = "#{pathname.dirname}/html"
|
37
|
-
erb = ERB.new
|
37
|
+
erb = ERB.new file_system.load "#{@html_directory}/#{$1}.erb"
|
38
38
|
erb.result(caller_binding)
|
39
39
|
end
|
40
40
|
|
@@ -100,7 +100,7 @@ class ChartBase
|
|
100
100
|
issues_id = next_id
|
101
101
|
|
102
102
|
issue_descriptions.sort! { |a, b| a[0].key_as_i <=> b[0].key_as_i }
|
103
|
-
erb = ERB.new
|
103
|
+
erb = ERB.new file_system.load "#{@html_directory}/collapsible_issues_panel.erb"
|
104
104
|
erb.result(binding)
|
105
105
|
end
|
106
106
|
|
@@ -153,17 +153,6 @@ class ChartBase
|
|
153
153
|
end
|
154
154
|
end
|
155
155
|
|
156
|
-
def sprints_in_time_range board
|
157
|
-
board.sprints.select do |sprint|
|
158
|
-
sprint_end_time = sprint.completed_time || sprint.end_time
|
159
|
-
sprint_start_time = sprint.start_time
|
160
|
-
next false if sprint_start_time.nil?
|
161
|
-
|
162
|
-
time_range.include?(sprint_start_time) || time_range.include?(sprint_end_time) ||
|
163
|
-
(sprint_start_time < time_range.begin && sprint_end_time > time_range.end)
|
164
|
-
end || []
|
165
|
-
end
|
166
|
-
|
167
156
|
def chart_format object
|
168
157
|
if object.is_a? Time
|
169
158
|
# "2022-04-09T11:38:30-07:00"
|
@@ -173,12 +162,14 @@ class ChartBase
|
|
173
162
|
end
|
174
163
|
end
|
175
164
|
|
176
|
-
def header_text text
|
177
|
-
@header_text = text
|
165
|
+
def header_text text = nil
|
166
|
+
@header_text = text if text
|
167
|
+
@header_text
|
178
168
|
end
|
179
169
|
|
180
|
-
def description_text text
|
181
|
-
@description_text = text
|
170
|
+
def description_text text = nil
|
171
|
+
@description_text = text if text
|
172
|
+
@description_text
|
182
173
|
end
|
183
174
|
|
184
175
|
def format_integer number
|
@@ -188,26 +179,35 @@ class ChartBase
|
|
188
179
|
def format_status name_or_id, board:, is_category: false
|
189
180
|
begin
|
190
181
|
statuses = board.possible_statuses.expand_statuses([name_or_id])
|
191
|
-
rescue
|
192
|
-
return "<span style='color: red'>#{name_or_id}</span>"
|
193
|
-
|
194
|
-
throw e
|
182
|
+
rescue StatusNotFoundError => e
|
183
|
+
return "<span style='color: red'>#{name_or_id}</span>"
|
195
184
|
end
|
196
|
-
raise "Expected exactly one match and got #{statuses.inspect} for #{name_or_id.inspect}" if statuses.size > 1
|
197
185
|
|
198
186
|
status = statuses.first
|
199
187
|
color = status_category_color status
|
200
188
|
|
189
|
+
visibility = ''
|
190
|
+
if is_category == false && board.visible_columns.none? { |column| column.status_ids.include? status.id }
|
191
|
+
visibility = icon_span(
|
192
|
+
title: "Not visible: The status #{status.name.inspect} is not mapped to any column and will not be visible",
|
193
|
+
icon: ' 👀'
|
194
|
+
)
|
195
|
+
|
196
|
+
end
|
201
197
|
text = is_category ? status.category_name : status.name
|
202
|
-
"<span title='Category: #{status.category_name}'>#{color_block color.name} #{text}</span
|
198
|
+
"<span title='Category: #{status.category_name}'>#{color_block color.name} #{text}</span>#{visibility}"
|
199
|
+
end
|
200
|
+
|
201
|
+
def icon_span title:, icon:
|
202
|
+
"<span title='#{title}' style='font-size: 0.8em;'>#{icon}</span>"
|
203
203
|
end
|
204
204
|
|
205
205
|
def status_category_color status
|
206
206
|
case status.category_name
|
207
|
-
when nil then 'black'
|
208
207
|
when 'To Do' then CssVariable['--status-category-todo-color']
|
209
208
|
when 'In Progress' then CssVariable['--status-category-inprogress-color']
|
210
209
|
when 'Done' then CssVariable['--status-category-done-color']
|
210
|
+
else 'black' # Theoretically impossible but seen in prod.
|
211
211
|
end
|
212
212
|
end
|
213
213
|
|
@@ -225,18 +225,6 @@ class ChartBase
|
|
225
225
|
@canvas_responsive
|
226
226
|
end
|
227
227
|
|
228
|
-
def filter_issues &block
|
229
|
-
@filter_issues_block = block
|
230
|
-
end
|
231
|
-
|
232
|
-
def issues= issues
|
233
|
-
@issues = issues
|
234
|
-
return unless @filter_issues_block
|
235
|
-
|
236
|
-
@issues = issues.filter_map { |i| @filter_issues_block.call(i) }.uniq
|
237
|
-
puts @issues.collect(&:key).join(', ')
|
238
|
-
end
|
239
|
-
|
240
228
|
def color_block color, title: nil
|
241
229
|
result = +''
|
242
230
|
result << "<div class='color_block' style='background: var(#{color});'"
|
@@ -252,5 +240,5 @@ class ChartBase
|
|
252
240
|
and any other holidays mentioned in the configuration.
|
253
241
|
</div>
|
254
242
|
TEXT
|
255
|
-
end
|
243
|
+
end
|
256
244
|
end
|
@@ -14,7 +14,7 @@ end
|
|
14
14
|
class DailyWipChart < ChartBase
|
15
15
|
attr_accessor :possible_statuses
|
16
16
|
|
17
|
-
def initialize block
|
17
|
+
def initialize block
|
18
18
|
super()
|
19
19
|
|
20
20
|
header_text default_header_text
|
@@ -27,9 +27,6 @@ class DailyWipChart < ChartBase
|
|
27
27
|
default_grouping_rules issue: issue, rules: rules
|
28
28
|
end
|
29
29
|
end
|
30
|
-
|
31
|
-
# Because this one will size itself as needed, we start with a smaller default size
|
32
|
-
# @canvas_height = 80
|
33
30
|
end
|
34
31
|
|
35
32
|
def run
|
@@ -45,8 +42,6 @@ class DailyWipChart < ChartBase
|
|
45
42
|
end + data_sets
|
46
43
|
end
|
47
44
|
|
48
|
-
# grow_chart_height_if_too_many_data_sets data_sets.size
|
49
|
-
|
50
45
|
wrap_and_render(binding, __FILE__)
|
51
46
|
end
|
52
47
|
|
@@ -174,11 +169,4 @@ class DailyWipChart < ChartBase
|
|
174
169
|
hidden: false
|
175
170
|
}
|
176
171
|
end
|
177
|
-
|
178
|
-
def grow_chart_height_if_too_many_data_sets count
|
179
|
-
px_per_bar = 8
|
180
|
-
bars_per_issue = 0.5
|
181
|
-
preferred_height = count * px_per_bar * bars_per_issue
|
182
|
-
@canvas_height = preferred_height if @canvas_height.nil? || @canvas_height < preferred_height
|
183
|
-
end
|
184
172
|
end
|
@@ -52,7 +52,7 @@ class DependencyChart < ChartBase
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def run
|
55
|
-
instance_eval(&@rules_block)
|
55
|
+
instance_eval(&@rules_block)
|
56
56
|
|
57
57
|
dot_graph = build_dot_graph
|
58
58
|
return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if dot_graph.nil?
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class
|
4
|
-
def initialize configuration_block
|
3
|
+
class EstimateAccuracyChart < ChartBase
|
4
|
+
def initialize configuration_block
|
5
5
|
super()
|
6
6
|
|
7
7
|
header_text 'Estimate Accuracy'
|
@@ -12,7 +12,7 @@ class StoryPointAccuracyChart < ChartBase
|
|
12
12
|
</div>
|
13
13
|
<div class="p">
|
14
14
|
The #{color_block '--estimate-accuracy-chart-completed-fill-color'} completed dots indicate
|
15
|
-
cycletimes.
|
15
|
+
cycletimes.
|
16
16
|
<% if @has_aging_data %>
|
17
17
|
The #{color_block '--estimate-accuracy-chart-active-fill-color'} aging dots
|
18
18
|
(click on the legend to turn them on) show the current
|
@@ -27,7 +27,7 @@ class StoryPointAccuracyChart < ChartBase
|
|
27
27
|
@y_axis_block = ->(issue, start_time) { story_points_at(issue: issue, start_time: start_time)&.to_f }
|
28
28
|
@y_axis_sort_order = nil
|
29
29
|
|
30
|
-
instance_eval(&configuration_block)
|
30
|
+
instance_eval(&configuration_block)
|
31
31
|
end
|
32
32
|
|
33
33
|
def run
|
@@ -39,26 +39,7 @@ class StoryPointAccuracyChart < ChartBase
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def scan_issues
|
42
|
-
aging_hash =
|
43
|
-
completed_hash = {}
|
44
|
-
|
45
|
-
issues.each do |issue|
|
46
|
-
cycletime = issue.board.cycletime
|
47
|
-
start_time = cycletime.started_time(issue)
|
48
|
-
stop_time = cycletime.stopped_time(issue)
|
49
|
-
|
50
|
-
next unless start_time
|
51
|
-
|
52
|
-
hash = stop_time ? completed_hash : aging_hash
|
53
|
-
|
54
|
-
estimate = @y_axis_block.call issue, start_time
|
55
|
-
cycle_time = ((stop_time&.to_date || date_range.end) - start_time.to_date).to_i + 1
|
56
|
-
|
57
|
-
next if estimate.nil?
|
58
|
-
|
59
|
-
key = [estimate, cycle_time]
|
60
|
-
(hash[key] ||= []) << issue
|
61
|
-
end
|
42
|
+
completed_hash, aging_hash = split_into_completed_and_aging issues: issues
|
62
43
|
|
63
44
|
@has_aging_data = !aging_hash.empty?
|
64
45
|
|
@@ -93,7 +74,32 @@ class StoryPointAccuracyChart < ChartBase
|
|
93
74
|
'borderColor' => border_color,
|
94
75
|
'hidden' => starts_hidden
|
95
76
|
}
|
96
|
-
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def split_into_completed_and_aging issues:
|
81
|
+
aging_hash = {}
|
82
|
+
completed_hash = {}
|
83
|
+
|
84
|
+
issues.each do |issue|
|
85
|
+
cycletime = issue.board.cycletime
|
86
|
+
start_time = cycletime.started_time(issue)
|
87
|
+
stop_time = cycletime.stopped_time(issue)
|
88
|
+
|
89
|
+
next unless start_time
|
90
|
+
|
91
|
+
hash = stop_time ? completed_hash : aging_hash
|
92
|
+
|
93
|
+
estimate = @y_axis_block.call issue, start_time
|
94
|
+
cycle_time = ((stop_time&.to_date || date_range.end) - start_time.to_date).to_i + 1
|
95
|
+
|
96
|
+
next if estimate.nil?
|
97
|
+
|
98
|
+
key = [estimate, cycle_time]
|
99
|
+
(hash[key] ||= []) << issue
|
100
|
+
end
|
101
|
+
|
102
|
+
[completed_hash, aging_hash]
|
97
103
|
end
|
98
104
|
|
99
105
|
def hash_sorter
|
@@ -20,7 +20,7 @@ class ExpeditedChart < ChartBase
|
|
20
20
|
attr_accessor :issues, :cycletime, :possible_statuses, :date_range
|
21
21
|
attr_reader :expedited_label
|
22
22
|
|
23
|
-
def initialize
|
23
|
+
def initialize block
|
24
24
|
super()
|
25
25
|
|
26
26
|
header_text 'Expedited work'
|
@@ -38,6 +38,8 @@ class ExpeditedChart < ChartBase
|
|
38
38
|
</div>
|
39
39
|
#{describe_non_working_days}
|
40
40
|
HTML
|
41
|
+
|
42
|
+
instance_eval(&block)
|
41
43
|
end
|
42
44
|
|
43
45
|
def run
|
data/lib/jirametrics/exporter.rb
CHANGED
@@ -32,11 +32,12 @@ class Exporter
|
|
32
32
|
|
33
33
|
def initialize file_system: FileSystem.new
|
34
34
|
@project_configs = []
|
35
|
-
@timezone_offset = '+00:00'
|
36
35
|
@target_path = '.'
|
37
36
|
@holiday_dates = []
|
38
37
|
@downloading = false
|
39
38
|
@file_system = file_system
|
39
|
+
|
40
|
+
timezone_offset '+00:00'
|
40
41
|
end
|
41
42
|
|
42
43
|
def export name_filter:
|
@@ -79,7 +80,6 @@ class Exporter
|
|
79
80
|
end
|
80
81
|
|
81
82
|
def project name: nil, &block
|
82
|
-
raise 'target_path was never set!' if @target_path.nil?
|
83
83
|
raise 'jira_config not set' if @jira_config.nil?
|
84
84
|
|
85
85
|
@project_configs << ProjectConfig.new(
|