jirametrics 2.22 → 2.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/jirametrics/aggregate_config.rb +10 -2
- data/lib/jirametrics/aging_work_bar_chart.rb +11 -0
- data/lib/jirametrics/aging_work_table.rb +1 -1
- data/lib/jirametrics/anonymizer.rb +74 -1
- data/lib/jirametrics/atlassian_document_format.rb +104 -93
- data/lib/jirametrics/blocked_stalled_change.rb +5 -3
- data/lib/jirametrics/board.rb +17 -3
- data/lib/jirametrics/change_item.rb +4 -3
- data/lib/jirametrics/chart_base.rb +80 -1
- data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +1 -2
- data/lib/jirametrics/cycletime_histogram.rb +15 -103
- data/lib/jirametrics/cycletime_scatterplot.rb +8 -97
- data/lib/jirametrics/daily_wip_chart.rb +27 -7
- data/lib/jirametrics/download_config.rb +15 -0
- data/lib/jirametrics/downloader.rb +76 -5
- data/lib/jirametrics/downloader_for_cloud.rb +39 -0
- data/lib/jirametrics/downloader_for_data_center.rb +2 -1
- data/lib/jirametrics/estimate_accuracy_chart.rb +42 -4
- data/lib/jirametrics/examples/standard_project.rb +15 -5
- data/lib/jirametrics/expedited_chart.rb +2 -0
- data/lib/jirametrics/exporter.rb +3 -1
- data/lib/jirametrics/file_system.rb +4 -0
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +2 -0
- data/lib/jirametrics/github_gateway.rb +99 -0
- data/lib/jirametrics/groupable_issue_chart.rb +2 -0
- data/lib/jirametrics/grouping_rules.rb +1 -1
- data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -4
- data/lib/jirametrics/html/daily_wip_chart.erb +5 -4
- data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -12
- data/lib/jirametrics/html/expedited_chart.erb +3 -13
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -8
- data/lib/jirametrics/html/sprint_burndown.erb +7 -13
- data/lib/jirametrics/html/throughput_chart.erb +5 -8
- data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +57 -59
- data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +3 -4
- data/lib/jirametrics/html_report_config.rb +1 -0
- data/lib/jirametrics/issue.rb +37 -74
- data/lib/jirametrics/issue_printer.rb +97 -0
- data/lib/jirametrics/project_config.rb +32 -5
- data/lib/jirametrics/pull_request.rb +30 -0
- data/lib/jirametrics/pull_request_review.rb +13 -0
- data/lib/jirametrics/raw_javascript.rb +4 -0
- data/lib/jirametrics/settings.json +3 -1
- data/lib/jirametrics/sprint_burndown.rb +2 -0
- data/lib/jirametrics/stitcher.rb +2 -1
- data/lib/jirametrics/throughput_chart.rb +7 -1
- data/lib/jirametrics/time_based_histogram.rb +139 -0
- data/lib/jirametrics/time_based_scatterplot.rb +100 -0
- metadata +11 -5
|
@@ -6,8 +6,8 @@ class Exporter
|
|
|
6
6
|
def standard_project name:, file_prefix:, ignore_issues: nil, starting_status: nil, boards: {},
|
|
7
7
|
default_board: nil, anonymize: false, settings: {}, status_category_mappings: {},
|
|
8
8
|
rolling_date_count: 90, no_earlier_than: nil, ignore_types: %w[Sub-task Subtask Epic],
|
|
9
|
-
show_experimental_charts: false
|
|
10
|
-
|
|
9
|
+
show_experimental_charts: false, github_repos: nil
|
|
10
|
+
exporter = self
|
|
11
11
|
project name: name do
|
|
12
12
|
puts name
|
|
13
13
|
file_prefix file_prefix
|
|
@@ -35,19 +35,20 @@ class Exporter
|
|
|
35
35
|
download do
|
|
36
36
|
self.rolling_date_count(rolling_date_count) if rolling_date_count
|
|
37
37
|
self.no_earlier_than(no_earlier_than) if no_earlier_than
|
|
38
|
+
github_repo github_repos if github_repos
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
issues.reject! do |issue|
|
|
41
42
|
ignore_types.include? issue.type
|
|
42
43
|
end
|
|
43
44
|
|
|
45
|
+
exporter.filter_issues issues, ignore_issues
|
|
46
|
+
|
|
44
47
|
discard_changes_before status_becomes: (starting_status || :backlog) # rubocop:disable Style/RedundantParentheses
|
|
45
48
|
|
|
46
49
|
file do
|
|
47
50
|
file_suffix '.html'
|
|
48
51
|
|
|
49
|
-
issues.reject! { |issue| ignore_issues.include? issue.key } if ignore_issues
|
|
50
|
-
|
|
51
52
|
html_report do
|
|
52
53
|
board_id default_board if default_board
|
|
53
54
|
|
|
@@ -87,7 +88,6 @@ class Exporter
|
|
|
87
88
|
daily_wip_by_blocked_stalled_chart
|
|
88
89
|
daily_wip_by_parent_chart
|
|
89
90
|
flow_efficiency_scatterplot if show_experimental_charts
|
|
90
|
-
expedited_chart
|
|
91
91
|
sprint_burndown
|
|
92
92
|
estimate_accuracy_chart
|
|
93
93
|
dependency_chart
|
|
@@ -95,4 +95,14 @@ class Exporter
|
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
end
|
|
98
|
+
|
|
99
|
+
# Extracted as a separate method so it can be tested independently, without needing to invoke
|
|
100
|
+
# the full standard_project DSL setup.
|
|
101
|
+
def filter_issues issues, ignore_issues
|
|
102
|
+
return unless ignore_issues
|
|
103
|
+
|
|
104
|
+
issues.reject! do |issue|
|
|
105
|
+
ignore_issues.is_a?(Proc) ? ignore_issues.call(issue) : ignore_issues.include?(issue.key)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
98
108
|
end
|
data/lib/jirametrics/exporter.rb
CHANGED
|
@@ -40,6 +40,7 @@ class Exporter
|
|
|
40
40
|
|
|
41
41
|
def download name_filter:
|
|
42
42
|
@downloading = true
|
|
43
|
+
github_pr_cache = {}
|
|
43
44
|
each_project_config(name_filter: name_filter) do |project|
|
|
44
45
|
project.evaluate_next_level
|
|
45
46
|
next if project.aggregated_project?
|
|
@@ -56,7 +57,8 @@ class Exporter
|
|
|
56
57
|
downloader = Downloader.create(
|
|
57
58
|
download_config: project.download_config,
|
|
58
59
|
file_system: file_system,
|
|
59
|
-
jira_gateway: gateway
|
|
60
|
+
jira_gateway: gateway,
|
|
61
|
+
github_pr_cache: github_pr_cache
|
|
60
62
|
)
|
|
61
63
|
downloader.run
|
|
62
64
|
end
|
|
@@ -61,6 +61,10 @@ class FileSystem
|
|
|
61
61
|
logfile.puts more if more
|
|
62
62
|
return unless also_write_to_stderr
|
|
63
63
|
|
|
64
|
+
# Obscure edge-case where we're trying to log something before logging is even
|
|
65
|
+
# set up. Quick escape here so that we don't dump the error twice.
|
|
66
|
+
return if logfile == $stdout
|
|
67
|
+
|
|
64
68
|
$stderr.puts message # rubocop:disable Style/StderrPuts
|
|
65
69
|
end
|
|
66
70
|
|
|
@@ -32,6 +32,8 @@ class FlowEfficiencyScatterplot < ChartBase
|
|
|
32
32
|
So be aware that your team may have to change their behaviours if you want this chart to be useful.
|
|
33
33
|
</div>
|
|
34
34
|
HTML
|
|
35
|
+
@x_axis_title = 'Total time (days)'
|
|
36
|
+
@y_axis_title = 'Time adding value (days)'
|
|
35
37
|
|
|
36
38
|
init_configuration_block block do
|
|
37
39
|
grouping_rules do |issue, rule|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
class GithubGateway
|
|
7
|
+
attr_reader :repo
|
|
8
|
+
|
|
9
|
+
def initialize repo:, project_keys:, file_system:, raw_pr_cache: {}
|
|
10
|
+
@repo = repo
|
|
11
|
+
@project_keys = project_keys
|
|
12
|
+
@file_system = file_system
|
|
13
|
+
@raw_pr_cache = raw_pr_cache
|
|
14
|
+
@issue_key_pattern = build_issue_key_pattern
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def fetch_pull_requests since: nil
|
|
18
|
+
raw_prs = @raw_pr_cache[[@repo, since]] ||= fetch_raw_pull_requests(since: since)
|
|
19
|
+
raw_prs.filter_map { |pr| build_pr_data(pr) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def fetch_raw_pull_requests since: nil
|
|
23
|
+
# Note: 'commits' is intentionally excluded — including it triggers GitHub's GraphQL node
|
|
24
|
+
# limit (authors sub-connection × PRs × commits exceeds 500,000 nodes). Branch name,
|
|
25
|
+
# title, and body are sufficient for issue key extraction in the vast majority of cases.
|
|
26
|
+
json_fields = %w[number title body headRefName createdAt closedAt mergedAt
|
|
27
|
+
url state reviews additions deletions changedFiles].join(',')
|
|
28
|
+
args = ['pr', 'list', '--state', 'all', '--limit', '5000', '--json', json_fields]
|
|
29
|
+
args += ['--repo', @repo]
|
|
30
|
+
args += ['--search', "updated:>=#{since}"] if since
|
|
31
|
+
|
|
32
|
+
@file_system.log " Downloading pull requests from #{@repo}", also_write_to_stderr: true
|
|
33
|
+
run_command(args)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build_pr_data raw_pr
|
|
37
|
+
issue_keys = extract_issue_keys(raw_pr)
|
|
38
|
+
return nil if issue_keys.empty?
|
|
39
|
+
|
|
40
|
+
PullRequest.new(raw: {
|
|
41
|
+
'number' => raw_pr['number'],
|
|
42
|
+
'repo' => @repo,
|
|
43
|
+
'url' => raw_pr['url'],
|
|
44
|
+
'title' => raw_pr['title'],
|
|
45
|
+
'branch' => raw_pr['headRefName'],
|
|
46
|
+
'opened_at' => raw_pr['createdAt'],
|
|
47
|
+
'closed_at' => raw_pr['closedAt'],
|
|
48
|
+
'merged_at' => raw_pr['mergedAt'],
|
|
49
|
+
'state' => raw_pr['state'],
|
|
50
|
+
'issue_keys' => issue_keys,
|
|
51
|
+
'reviews' => extract_reviews(raw_pr['reviews'] || []),
|
|
52
|
+
'additions' => raw_pr['additions'],
|
|
53
|
+
'deletions' => raw_pr['deletions'],
|
|
54
|
+
'changed_files' => raw_pr['changedFiles']
|
|
55
|
+
})
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def extract_issue_keys raw_pr
|
|
59
|
+
return [] if @issue_key_pattern.nil?
|
|
60
|
+
|
|
61
|
+
sources = [
|
|
62
|
+
raw_pr['headRefName'],
|
|
63
|
+
raw_pr['title'],
|
|
64
|
+
raw_pr['body']
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
sources.compact
|
|
68
|
+
.flat_map { |s| s.scan(@issue_key_pattern) }
|
|
69
|
+
.uniq
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def extract_reviews raw_reviews
|
|
73
|
+
raw_reviews
|
|
74
|
+
.select { |r| %w[APPROVED CHANGES_REQUESTED].include?(r['state']) }
|
|
75
|
+
.map do |r|
|
|
76
|
+
{
|
|
77
|
+
'author' => r.dig('author', 'login'),
|
|
78
|
+
'submitted_at' => r['submittedAt'],
|
|
79
|
+
'state' => r['state']
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def build_issue_key_pattern
|
|
87
|
+
return nil if @project_keys.empty?
|
|
88
|
+
|
|
89
|
+
keys_pattern = @project_keys.map { |k| Regexp.escape(k) }.join('|')
|
|
90
|
+
Regexp.new("\\b(?:#{keys_pattern})-\\d+\\b")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def run_command args
|
|
94
|
+
stdout, stderr, status = Open3.capture3('gh', *args)
|
|
95
|
+
raise "GitHub CLI command failed for #{@repo}: #{stderr}" unless status.success?
|
|
96
|
+
|
|
97
|
+
JSON.parse(stdout)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -16,6 +16,7 @@ module GroupableIssueChart
|
|
|
16
16
|
def group_issues completed_issues
|
|
17
17
|
result = {}
|
|
18
18
|
ignored_issues = []
|
|
19
|
+
@issue_hints = {}
|
|
19
20
|
completed_issues.each do |issue|
|
|
20
21
|
rules = GroupingRules.new
|
|
21
22
|
@group_by_block.call(issue, rules)
|
|
@@ -24,6 +25,7 @@ module GroupableIssueChart
|
|
|
24
25
|
next
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
@issue_hints[issue] = rules.issue_hint
|
|
27
29
|
(result[rules] ||= []) << issue
|
|
28
30
|
end
|
|
29
31
|
|
|
@@ -16,11 +16,9 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
16
16
|
x: {
|
|
17
17
|
type: 'time',
|
|
18
18
|
min: '<%= @date_range.begin.to_s %>',
|
|
19
|
-
max: '<%= (@date_range.end ).to_s %>',
|
|
19
|
+
max: '<%= (@date_range.end + 1).to_s %>',
|
|
20
20
|
stacked: false,
|
|
21
|
-
|
|
22
|
-
display: false
|
|
23
|
-
},
|
|
21
|
+
<%= render_axis_title :x %>
|
|
24
22
|
grid: {
|
|
25
23
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
26
24
|
},
|
|
@@ -31,6 +29,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
31
29
|
ticks: {
|
|
32
30
|
display: true
|
|
33
31
|
},
|
|
32
|
+
<%= render_axis_title :y %>
|
|
34
33
|
grid: {
|
|
35
34
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
36
35
|
},
|
|
@@ -21,7 +21,10 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
21
21
|
time: {
|
|
22
22
|
unit: 'day'
|
|
23
23
|
},
|
|
24
|
+
min: "<%= date_range.begin.to_s %>",
|
|
25
|
+
max: "<%= (date_range.end + 1).to_s %>",
|
|
24
26
|
stacked: true,
|
|
27
|
+
<%= render_axis_title :x %>
|
|
25
28
|
grid: {
|
|
26
29
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
27
30
|
},
|
|
@@ -32,10 +35,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
32
35
|
display: true,
|
|
33
36
|
labelString: 'WIP'
|
|
34
37
|
},
|
|
35
|
-
|
|
36
|
-
display: true,
|
|
37
|
-
text: 'Count of items'
|
|
38
|
-
},
|
|
38
|
+
<%= render_axis_title :y %>
|
|
39
39
|
grid: {
|
|
40
40
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
41
41
|
},
|
|
@@ -52,6 +52,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
52
52
|
annotation: {
|
|
53
53
|
annotations: {
|
|
54
54
|
<%= working_days_annotation %>
|
|
55
|
+
<%= date_annotation %>
|
|
55
56
|
}
|
|
56
57
|
},
|
|
57
58
|
legend: {
|
|
@@ -9,10 +9,6 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
9
9
|
datasets: <%= JSON.generate(data_sets) %>
|
|
10
10
|
},
|
|
11
11
|
options: {
|
|
12
|
-
title: {
|
|
13
|
-
display: true,
|
|
14
|
-
text: "Sprint Burndown"
|
|
15
|
-
},
|
|
16
12
|
responsive: <%= canvas_responsive? %>, // If responsive is true then it fills the screen
|
|
17
13
|
scales: {
|
|
18
14
|
x: {
|
|
@@ -21,10 +17,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
21
17
|
display: true,
|
|
22
18
|
labelString: 'Date'
|
|
23
19
|
},
|
|
24
|
-
|
|
25
|
-
display: true,
|
|
26
|
-
text: "Cycletime (days)"
|
|
27
|
-
},
|
|
20
|
+
<%= render_axis_title :x %>
|
|
28
21
|
min: 0,
|
|
29
22
|
grid: {
|
|
30
23
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
@@ -39,10 +32,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
39
32
|
display: true,
|
|
40
33
|
labelString: 'Items remaining'
|
|
41
34
|
},
|
|
42
|
-
|
|
43
|
-
display: true,
|
|
44
|
-
text: "<%= @y_axis_label %>"
|
|
45
|
-
},
|
|
35
|
+
<%= render_axis_title :y %>
|
|
46
36
|
min: 0.0,
|
|
47
37
|
grid: {
|
|
48
38
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
@@ -20,25 +20,15 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
20
20
|
scales: {
|
|
21
21
|
x: {
|
|
22
22
|
type: "time",
|
|
23
|
-
|
|
24
|
-
display: true,
|
|
25
|
-
labelString: 'Date Completed'
|
|
26
|
-
},
|
|
23
|
+
<%= render_axis_title :x %>
|
|
27
24
|
min: "<%= date_range.begin.to_s %>",
|
|
28
|
-
max: "<%= date_range.end.to_s %>",
|
|
25
|
+
max: "<%= (date_range.end + 1).to_s %>",
|
|
29
26
|
grid: {
|
|
30
27
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
31
28
|
},
|
|
32
29
|
},
|
|
33
30
|
y: {
|
|
34
|
-
|
|
35
|
-
display: true,
|
|
36
|
-
labelString: 'Days'
|
|
37
|
-
},
|
|
38
|
-
title: {
|
|
39
|
-
display: true,
|
|
40
|
-
text: 'Age in days'
|
|
41
|
-
},
|
|
31
|
+
<%= render_axis_title :y %>
|
|
42
32
|
min: 0,
|
|
43
33
|
grid: {
|
|
44
34
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
@@ -20,10 +20,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
20
20
|
display: true,
|
|
21
21
|
labelString: 'Days'
|
|
22
22
|
},
|
|
23
|
-
|
|
24
|
-
display: true,
|
|
25
|
-
text: 'Total time (days)'
|
|
26
|
-
},
|
|
23
|
+
<%= render_axis_title :x %>
|
|
27
24
|
grid: {
|
|
28
25
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
29
26
|
},
|
|
@@ -36,10 +33,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
36
33
|
min: 0,
|
|
37
34
|
max: <%= @highest_cycletime %>
|
|
38
35
|
},
|
|
39
|
-
|
|
40
|
-
display: true,
|
|
41
|
-
text: 'Time adding value (days)'
|
|
42
|
-
},
|
|
36
|
+
<%= render_axis_title :y %>
|
|
43
37
|
grid: {
|
|
44
38
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
45
39
|
},
|
|
@@ -22,10 +22,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
22
22
|
time: {
|
|
23
23
|
format: 'YYYY-MM-DD'
|
|
24
24
|
},
|
|
25
|
-
|
|
26
|
-
display: true,
|
|
27
|
-
labelString: 'Date'
|
|
28
|
-
},
|
|
25
|
+
<%= render_axis_title :x %>
|
|
29
26
|
min: "<%= date_range.begin.to_s %>",
|
|
30
27
|
max: "<%= (date_range.end + 1).to_s %>",
|
|
31
28
|
grid: {
|
|
@@ -33,14 +30,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
33
30
|
},
|
|
34
31
|
},
|
|
35
32
|
y: {
|
|
36
|
-
|
|
37
|
-
display: true,
|
|
38
|
-
labelString: 'Items remaining'
|
|
39
|
-
},
|
|
40
|
-
title: {
|
|
41
|
-
display: true,
|
|
42
|
-
text: "<%= y_axis_title %>"
|
|
43
|
-
},
|
|
33
|
+
<%= render_axis_title :y %>
|
|
44
34
|
min: 0.0,
|
|
45
35
|
grid: {
|
|
46
36
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
@@ -77,7 +67,9 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
77
67
|
<table class='standard' style="margin-left: 1em;">
|
|
78
68
|
<thead>
|
|
79
69
|
<th>Sprint</th>
|
|
80
|
-
<th>
|
|
70
|
+
<th>Started</th>
|
|
71
|
+
<th>Completed</th>
|
|
72
|
+
<th>Days</th>
|
|
81
73
|
<th>State</th>
|
|
82
74
|
<th>Started</th>
|
|
83
75
|
<th>Completed</th>
|
|
@@ -90,6 +82,8 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
90
82
|
<% @summary_stats.keys.sort_by(&:start_time).each do |sprint| %>
|
|
91
83
|
<tr>
|
|
92
84
|
<td><%= sprint.name %></td>
|
|
85
|
+
<td><%= sprint.start_time.to_date %></td>
|
|
86
|
+
<td><%= sprint.completed_time&.to_date %></td>
|
|
93
87
|
<td><%= sprint.day_count %></td>
|
|
94
88
|
<td><%= sprint.raw['state'] %></td>
|
|
95
89
|
<% stats = @summary_stats[sprint] %>
|
|
@@ -20,10 +20,9 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
20
20
|
time: {
|
|
21
21
|
format: 'YYYY-MM-DD'
|
|
22
22
|
},
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
},
|
|
23
|
+
min: "<%= date_range.begin.to_s %>",
|
|
24
|
+
max: "<%= (date_range.end + 1).to_s %>",
|
|
25
|
+
<%= render_axis_title :x %>
|
|
27
26
|
grid: {
|
|
28
27
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
29
28
|
},
|
|
@@ -32,10 +31,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
32
31
|
scaleLabel: {
|
|
33
32
|
display: true,
|
|
34
33
|
},
|
|
35
|
-
|
|
36
|
-
display: true,
|
|
37
|
-
text: 'Count of items'
|
|
38
|
-
},
|
|
34
|
+
<%= render_axis_title :y %>
|
|
39
35
|
min: 0,
|
|
40
36
|
grid: {
|
|
41
37
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
@@ -53,6 +49,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
53
49
|
annotation: {
|
|
54
50
|
annotations: {
|
|
55
51
|
<%= working_days_annotation %>
|
|
52
|
+
<%= date_annotation %>
|
|
56
53
|
}
|
|
57
54
|
}
|
|
58
55
|
}
|
|
@@ -2,57 +2,6 @@
|
|
|
2
2
|
<div class="chart">
|
|
3
3
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
4
4
|
</div>
|
|
5
|
-
<%
|
|
6
|
-
if show_stats
|
|
7
|
-
link_id = next_id
|
|
8
|
-
issues_id = next_id
|
|
9
|
-
%>
|
|
10
|
-
<div class='foldable' style="padding-left: 1em;">Statistics</div>
|
|
11
|
-
<div id="<%= issues_id %>" style="padding-left: 1em;">
|
|
12
|
-
<div>
|
|
13
|
-
<table class="standard">
|
|
14
|
-
<tr>
|
|
15
|
-
<th>Issue Type</th>
|
|
16
|
-
<th>Min</th>
|
|
17
|
-
<th>Max</th>
|
|
18
|
-
<th>Avg</th>
|
|
19
|
-
<th>Mode</th>
|
|
20
|
-
<% percentiles.each do |p| %>
|
|
21
|
-
<th><%= p %>th</th>
|
|
22
|
-
<% end %>
|
|
23
|
-
</tr>
|
|
24
|
-
<% the_stats.each do |k, v| %>
|
|
25
|
-
<tr>
|
|
26
|
-
<td><%= k %></td>
|
|
27
|
-
<td style="text-align: right;"><%= v[:min] %></td>
|
|
28
|
-
<td style="text-align: right;"><%= v[:max] %></td>
|
|
29
|
-
<td style="text-align: right;"><%= sprintf('%.2f', v[:average]) %></td>
|
|
30
|
-
<td><%= v[:mode].join(', ') %></td>
|
|
31
|
-
<% percentiles.each do |p| %>
|
|
32
|
-
<td style="text-align: right;"><%= v[:percentiles][p] %></td>
|
|
33
|
-
<% end %>
|
|
34
|
-
</tr>
|
|
35
|
-
<% end %>
|
|
36
|
-
</table>
|
|
37
|
-
</div>
|
|
38
|
-
<div>
|
|
39
|
-
<p>These statistics help understand the <i>"shape"</i> of the cycletime histogram distribution, to help us with predictions.</p>
|
|
40
|
-
<ul>
|
|
41
|
-
<li><b>Min & Max:</b> the observed spread for the data set. Useful to judge how wide the variation is. </li>
|
|
42
|
-
<li><b>Average:</b> the arithmetic mean of the data set. Useful as a <i>"typical representative"</i> of the complete set.</li>
|
|
43
|
-
<li><b>Mode:</b> the most repeated value(s) in the data set. This is the value we're most likely to remember. </li>
|
|
44
|
-
<li><b>Percentiles:</b> they partition the data set. If X is the Nth percentile, it means that N% of cycletime values are X or less. Typical percentiles of interest are:</li>
|
|
45
|
-
<ul>
|
|
46
|
-
<li><b>50%</b>: also known as the <b>Median</b>. Useful to establish short feedback loops, to monitor that it's not drifting to the right.</li>
|
|
47
|
-
<li><b>85%</b>: useful to establish service level expectations, accounting for rare events..</li>
|
|
48
|
-
<li><b>98% (or higher)</b>: useful to gauge worst case expectations..</li>
|
|
49
|
-
</ul>
|
|
50
|
-
</ul>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
<%
|
|
54
|
-
end
|
|
55
|
-
%>
|
|
56
5
|
<script>
|
|
57
6
|
new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
58
7
|
{
|
|
@@ -66,22 +15,17 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
66
15
|
x: {
|
|
67
16
|
type: 'linear',
|
|
68
17
|
stacked: true,
|
|
69
|
-
|
|
70
|
-
display: true,
|
|
71
|
-
text: 'Cycletime in days'
|
|
72
|
-
},
|
|
18
|
+
<%= render_axis_title :x %>
|
|
73
19
|
grid: {
|
|
74
20
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
75
21
|
},
|
|
76
22
|
min: 0,
|
|
23
|
+
<%= @max_x_value.nil? ? '' : "max: #{@max_x_value}," %>
|
|
77
24
|
offset: false, // Gets rid of the ugly padding on left.
|
|
78
25
|
},
|
|
79
26
|
y: {
|
|
80
27
|
stacked: true,
|
|
81
|
-
|
|
82
|
-
display: true,
|
|
83
|
-
text: 'Number of items that had that cycletime'
|
|
84
|
-
},
|
|
28
|
+
<%= render_axis_title :y %>
|
|
85
29
|
grid: {
|
|
86
30
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
87
31
|
},
|
|
@@ -121,3 +65,57 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
121
65
|
});
|
|
122
66
|
</script>
|
|
123
67
|
<%= seam_end %>
|
|
68
|
+
|
|
69
|
+
<%= seam_start 'stats_table' %>
|
|
70
|
+
<%
|
|
71
|
+
if show_stats
|
|
72
|
+
link_id = next_id
|
|
73
|
+
issues_id = next_id
|
|
74
|
+
%>
|
|
75
|
+
<div class='foldable' style="padding-left: 1em;">Statistics</div>
|
|
76
|
+
<div id="<%= issues_id %>" style="padding-left: 1em;">
|
|
77
|
+
<div>
|
|
78
|
+
<table class="standard">
|
|
79
|
+
<tr>
|
|
80
|
+
<th>Type</th>
|
|
81
|
+
<th>Min</th>
|
|
82
|
+
<th>Max</th>
|
|
83
|
+
<th>Avg</th>
|
|
84
|
+
<th>Mode</th>
|
|
85
|
+
<% percentiles.each do |p| %>
|
|
86
|
+
<th><%= p %>th</th>
|
|
87
|
+
<% end %>
|
|
88
|
+
</tr>
|
|
89
|
+
<% the_stats.each do |k, v| %>
|
|
90
|
+
<tr>
|
|
91
|
+
<td><%= k %></td>
|
|
92
|
+
<td style="text-align: right;"><%= v[:min] %></td>
|
|
93
|
+
<td style="text-align: right;"><%= v[:max] %></td>
|
|
94
|
+
<td style="text-align: right;"><%= sprintf('%.2f', v[:average]) %></td>
|
|
95
|
+
<td><%= v[:mode].join(', ') %></td>
|
|
96
|
+
<% percentiles.each do |p| %>
|
|
97
|
+
<td style="text-align: right;"><%= v[:percentiles][p] %></td>
|
|
98
|
+
<% end %>
|
|
99
|
+
</tr>
|
|
100
|
+
<% end %>
|
|
101
|
+
</table>
|
|
102
|
+
</div>
|
|
103
|
+
<div>
|
|
104
|
+
<p>These statistics help understand the <i>"shape"</i> of the histogram distribution, to help us with predictions.</p>
|
|
105
|
+
<ul>
|
|
106
|
+
<li><b>Min & Max:</b> the observed spread for the data set. Useful to judge how wide the variation is. </li>
|
|
107
|
+
<li><b>Average:</b> the arithmetic mean of the data set. Useful as a <i>"typical representative"</i> of the complete set.</li>
|
|
108
|
+
<li><b>Mode:</b> the most repeated value(s) in the data set. This is the value we're most likely to remember. </li>
|
|
109
|
+
<li><b>Percentiles:</b> they partition the data set. If X is the Nth percentile, it means that N% of values are X or less. Typical percentiles of interest are:</li>
|
|
110
|
+
<ul>
|
|
111
|
+
<li><b>50%</b>: also known as the <b>Median</b>. Useful to establish short feedback loops, to monitor that it's not drifting to the right.</li>
|
|
112
|
+
<li><b>85%</b>: useful to establish service level expectations, accounting for rare events..</li>
|
|
113
|
+
<li><b>98% (or higher)</b>: useful to gauge worst case expectations..</li>
|
|
114
|
+
</ul>
|
|
115
|
+
</ul>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<%
|
|
119
|
+
end
|
|
120
|
+
%>
|
|
121
|
+
<%= seam_end 'stats_table' %>
|
|
@@ -23,6 +23,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
23
23
|
grid: {
|
|
24
24
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
25
25
|
},
|
|
26
|
+
<%= render_axis_title :x %>
|
|
26
27
|
min: "<%= date_range.begin.to_s %>",
|
|
27
28
|
max: "<%= (date_range.end + 1).to_s %>"
|
|
28
29
|
},
|
|
@@ -32,10 +33,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
32
33
|
min: 0,
|
|
33
34
|
max: <%= @highest_y_value %>
|
|
34
35
|
},
|
|
35
|
-
|
|
36
|
-
display: true,
|
|
37
|
-
text: '<%= y_axis_heading %>'
|
|
38
|
-
},
|
|
36
|
+
<%= render_axis_title :y %>
|
|
39
37
|
grid: {
|
|
40
38
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
41
39
|
},
|
|
@@ -53,6 +51,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
53
51
|
annotation: {
|
|
54
52
|
annotations: {
|
|
55
53
|
<%= working_days_annotation %>
|
|
54
|
+
<%= date_annotation %>
|
|
56
55
|
|
|
57
56
|
<% @percentage_lines.each_with_index do |args, index| %>
|
|
58
57
|
<% percent, color = args %>
|
|
@@ -147,6 +147,7 @@ class HtmlReportConfig < HtmlGenerator
|
|
|
147
147
|
chart.all_boards = project_config.all_boards
|
|
148
148
|
chart.board_id = find_board_id
|
|
149
149
|
chart.holiday_dates = project_config.exporter.holiday_dates
|
|
150
|
+
chart.fix_versions = project_config.fix_versions
|
|
150
151
|
|
|
151
152
|
time_range = @file_config.project_config.time_range
|
|
152
153
|
chart.date_range = time_range.begin.to_date..time_range.end.to_date
|