jirametrics 2.2.1 → 2.3
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 +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(
|