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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +10 -2
  3. data/lib/jirametrics/aging_work_bar_chart.rb +11 -0
  4. data/lib/jirametrics/aging_work_table.rb +1 -1
  5. data/lib/jirametrics/anonymizer.rb +74 -1
  6. data/lib/jirametrics/atlassian_document_format.rb +104 -93
  7. data/lib/jirametrics/blocked_stalled_change.rb +5 -3
  8. data/lib/jirametrics/board.rb +17 -3
  9. data/lib/jirametrics/change_item.rb +4 -3
  10. data/lib/jirametrics/chart_base.rb +80 -1
  11. data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +1 -2
  12. data/lib/jirametrics/cycletime_histogram.rb +15 -103
  13. data/lib/jirametrics/cycletime_scatterplot.rb +8 -97
  14. data/lib/jirametrics/daily_wip_chart.rb +27 -7
  15. data/lib/jirametrics/download_config.rb +15 -0
  16. data/lib/jirametrics/downloader.rb +76 -5
  17. data/lib/jirametrics/downloader_for_cloud.rb +39 -0
  18. data/lib/jirametrics/downloader_for_data_center.rb +2 -1
  19. data/lib/jirametrics/estimate_accuracy_chart.rb +42 -4
  20. data/lib/jirametrics/examples/standard_project.rb +15 -5
  21. data/lib/jirametrics/expedited_chart.rb +2 -0
  22. data/lib/jirametrics/exporter.rb +3 -1
  23. data/lib/jirametrics/file_system.rb +4 -0
  24. data/lib/jirametrics/flow_efficiency_scatterplot.rb +2 -0
  25. data/lib/jirametrics/github_gateway.rb +99 -0
  26. data/lib/jirametrics/groupable_issue_chart.rb +2 -0
  27. data/lib/jirametrics/grouping_rules.rb +1 -1
  28. data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -4
  29. data/lib/jirametrics/html/daily_wip_chart.erb +5 -4
  30. data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -12
  31. data/lib/jirametrics/html/expedited_chart.erb +3 -13
  32. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -8
  33. data/lib/jirametrics/html/sprint_burndown.erb +7 -13
  34. data/lib/jirametrics/html/throughput_chart.erb +5 -8
  35. data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +57 -59
  36. data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +3 -4
  37. data/lib/jirametrics/html_report_config.rb +1 -0
  38. data/lib/jirametrics/issue.rb +37 -74
  39. data/lib/jirametrics/issue_printer.rb +97 -0
  40. data/lib/jirametrics/project_config.rb +32 -5
  41. data/lib/jirametrics/pull_request.rb +30 -0
  42. data/lib/jirametrics/pull_request_review.rb +13 -0
  43. data/lib/jirametrics/raw_javascript.rb +4 -0
  44. data/lib/jirametrics/settings.json +3 -1
  45. data/lib/jirametrics/sprint_burndown.rb +2 -0
  46. data/lib/jirametrics/stitcher.rb +2 -1
  47. data/lib/jirametrics/throughput_chart.rb +7 -1
  48. data/lib/jirametrics/time_based_histogram.rb +139 -0
  49. data/lib/jirametrics/time_based_scatterplot.rb +100 -0
  50. 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
@@ -38,6 +38,8 @@ class ExpeditedChart < ChartBase
38
38
  </div>
39
39
  #{describe_non_working_days}
40
40
  HTML
41
+ @x_axis_title = 'Date'
42
+ @y_axis_title = 'Age in days'
41
43
 
42
44
  instance_eval(&block)
43
45
  end
@@ -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
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class GroupingRules < Rules
4
- attr_accessor :label
4
+ attr_accessor :label, :issue_hint
5
5
  attr_reader :color
6
6
 
7
7
  def eql? other
@@ -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
- title: {
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
- title: {
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
- title: {
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
- title: {
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
- scaleLabel: {
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
- scaleLabel: {
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
- title: {
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
- title: {
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
- scaleLabel: {
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
- scaleLabel: {
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>Length</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
- scaleLabel: {
24
- display: true,
25
- labelString: 'Date Completed'
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
- title: {
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
- title: {
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
- title: {
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
- title: {
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