jirametrics 2.9 → 2.12.1
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 +1 -1
- data/lib/jirametrics/aging_work_in_progress_chart.rb +105 -41
- data/lib/jirametrics/aging_work_table.rb +56 -13
- data/lib/jirametrics/board.rb +38 -10
- data/lib/jirametrics/board_config.rb +1 -0
- data/lib/jirametrics/board_movement_calculator.rb +155 -0
- data/lib/jirametrics/change_item.rb +37 -16
- data/lib/jirametrics/chart_base.rb +22 -5
- data/lib/jirametrics/css_variable.rb +1 -1
- data/lib/jirametrics/cycletime_config.rb +1 -1
- data/lib/jirametrics/cycletime_histogram.rb +65 -2
- data/lib/jirametrics/daily_view.rb +277 -0
- data/lib/jirametrics/data_quality_report.rb +1 -1
- data/lib/jirametrics/downloader.rb +11 -14
- data/lib/jirametrics/estimate_accuracy_chart.rb +34 -10
- data/lib/jirametrics/estimation_configuration.rb +25 -0
- data/lib/jirametrics/examples/standard_project.rb +2 -0
- data/lib/jirametrics/exporter.rb +10 -8
- data/lib/jirametrics/file_config.rb +10 -5
- data/lib/jirametrics/file_system.rb +4 -0
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +1 -1
- data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -12
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +22 -5
- data/lib/jirametrics/html/aging_work_table.erb +7 -3
- data/lib/jirametrics/html/cycletime_histogram.erb +74 -0
- data/lib/jirametrics/html/cycletime_scatterplot.erb +1 -10
- data/lib/jirametrics/html/daily_wip_chart.erb +1 -10
- data/lib/jirametrics/html/expedited_chart.erb +1 -10
- data/lib/jirametrics/html/index.css +82 -2
- data/lib/jirametrics/html/index.erb +25 -1
- data/lib/jirametrics/html/sprint_burndown.erb +1 -10
- data/lib/jirametrics/html/throughput_chart.erb +1 -10
- data/lib/jirametrics/html_report_config.rb +2 -0
- data/lib/jirametrics/issue.rb +68 -27
- data/lib/jirametrics/issue_collection.rb +33 -0
- data/lib/jirametrics/jira_gateway.rb +20 -4
- data/lib/jirametrics/project_config.rb +25 -8
- data/lib/jirametrics/settings.json +2 -1
- data/lib/jirametrics/sprint.rb +1 -0
- data/lib/jirametrics/sprint_burndown.rb +35 -33
- data/lib/jirametrics/sprint_issue_change_data.rb +3 -3
- data/lib/jirametrics/status.rb +3 -6
- data/lib/jirametrics/status_collection.rb +6 -0
- data/lib/jirametrics/user.rb +12 -0
- data/lib/jirametrics.rb +4 -0
- metadata +7 -2
|
@@ -22,15 +22,18 @@ class EstimateAccuracyChart < ChartBase
|
|
|
22
22
|
</div>
|
|
23
23
|
HTML
|
|
24
24
|
|
|
25
|
-
@y_axis_label = 'Story Point Estimates'
|
|
26
25
|
@y_axis_type = 'linear'
|
|
27
|
-
@y_axis_block = ->(issue, start_time) {
|
|
26
|
+
@y_axis_block = ->(issue, start_time) { estimate_at(issue: issue, start_time: start_time)&.to_f }
|
|
28
27
|
@y_axis_sort_order = nil
|
|
29
28
|
|
|
30
29
|
instance_eval(&configuration_block)
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
def run
|
|
33
|
+
if @y_axis_label.nil?
|
|
34
|
+
text = current_board.estimation_configuration.units == :story_points ? 'Story Points' : 'Days'
|
|
35
|
+
@y_axis_label = "Estimated #{text}"
|
|
36
|
+
end
|
|
34
37
|
data_sets = scan_issues
|
|
35
38
|
|
|
36
39
|
return '' if data_sets.empty?
|
|
@@ -41,6 +44,7 @@ class EstimateAccuracyChart < ChartBase
|
|
|
41
44
|
def scan_issues
|
|
42
45
|
completed_hash, aging_hash = split_into_completed_and_aging issues: issues
|
|
43
46
|
|
|
47
|
+
estimation_units = current_board.estimation_configuration.units
|
|
44
48
|
@has_aging_data = !aging_hash.empty?
|
|
45
49
|
|
|
46
50
|
[
|
|
@@ -53,9 +57,13 @@ class EstimateAccuracyChart < ChartBase
|
|
|
53
57
|
# We sort so that the smaller circles are in front of the bigger circles.
|
|
54
58
|
data = hash.sort(&hash_sorter).collect do |key, values|
|
|
55
59
|
estimate, cycle_time = *key
|
|
56
|
-
|
|
57
|
-
title = [
|
|
58
|
-
|
|
60
|
+
|
|
61
|
+
title = [
|
|
62
|
+
"Estimate: #{estimate_label(estimate: estimate, estimation_units: estimation_units)}, " \
|
|
63
|
+
"Cycletime: #{label_days(cycle_time)}, " \
|
|
64
|
+
"#{values.size} issues"
|
|
65
|
+
] + values.collect { |issue| "#{issue.key}: #{issue.summary}" }
|
|
66
|
+
|
|
59
67
|
{
|
|
60
68
|
'x' => cycle_time,
|
|
61
69
|
'y' => estimate,
|
|
@@ -77,6 +85,18 @@ class EstimateAccuracyChart < ChartBase
|
|
|
77
85
|
end
|
|
78
86
|
end
|
|
79
87
|
|
|
88
|
+
def estimate_label estimate:, estimation_units:
|
|
89
|
+
if @y_axis_type == 'linear'
|
|
90
|
+
if estimation_units == :story_points
|
|
91
|
+
estimate_label = "#{estimate}pts"
|
|
92
|
+
elsif estimation_units == :seconds
|
|
93
|
+
estimate_label = label_days estimate
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
estimate_label = estimate.to_s if estimate_label.nil?
|
|
97
|
+
estimate_label
|
|
98
|
+
end
|
|
99
|
+
|
|
80
100
|
def split_into_completed_and_aging issues:
|
|
81
101
|
aging_hash = {}
|
|
82
102
|
completed_hash = {}
|
|
@@ -126,14 +146,18 @@ class EstimateAccuracyChart < ChartBase
|
|
|
126
146
|
end
|
|
127
147
|
end
|
|
128
148
|
|
|
129
|
-
def
|
|
130
|
-
|
|
149
|
+
def estimate_at issue:, start_time:, estimation_configuration: current_board.estimation_configuration
|
|
150
|
+
estimate = nil
|
|
151
|
+
|
|
131
152
|
issue.changes.each do |change|
|
|
132
|
-
return
|
|
153
|
+
return estimate if change.time >= start_time
|
|
133
154
|
|
|
134
|
-
|
|
155
|
+
if change.field == estimation_configuration.display_name || change.field == estimation_configuration.field_id
|
|
156
|
+
estimate = change.value
|
|
157
|
+
estimate = estimate.to_f / (24 * 60 * 60) if estimation_configuration.units == :seconds
|
|
158
|
+
end
|
|
135
159
|
end
|
|
136
|
-
|
|
160
|
+
estimate
|
|
137
161
|
end
|
|
138
162
|
|
|
139
163
|
def y_axis label:, sort_order: nil, &block
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class EstimationConfiguration
|
|
4
|
+
attr_reader :units, :display_name, :field_id
|
|
5
|
+
|
|
6
|
+
def initialize raw:
|
|
7
|
+
@units = :story_points
|
|
8
|
+
@display_name = 'Story Points'
|
|
9
|
+
|
|
10
|
+
# If there wasn't an estimation section they rely on all defaults
|
|
11
|
+
return if raw.nil?
|
|
12
|
+
|
|
13
|
+
if raw['type'] == 'field'
|
|
14
|
+
@field_id = raw['field']['fieldId']
|
|
15
|
+
@display_name = raw['field']['displayName']
|
|
16
|
+
if @field_id == 'timeoriginalestimate'
|
|
17
|
+
@units = :seconds
|
|
18
|
+
@display_name = 'Original estimate'
|
|
19
|
+
end
|
|
20
|
+
elsif raw['type'] == 'issueCount'
|
|
21
|
+
@display_name = 'Issue Count'
|
|
22
|
+
@units = :issue_count
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/jirametrics/exporter.rb
CHANGED
|
@@ -64,12 +64,8 @@ class Exporter
|
|
|
64
64
|
selected = []
|
|
65
65
|
each_project_config(name_filter: name_filter) do |project|
|
|
66
66
|
project.evaluate_next_level
|
|
67
|
-
# next if project.aggregated_project?
|
|
68
67
|
|
|
69
68
|
project.run load_only: true
|
|
70
|
-
project.board_configs.each do |board_config|
|
|
71
|
-
board_config.run
|
|
72
|
-
end
|
|
73
69
|
project.issues.each do |issue|
|
|
74
70
|
selected << [project, issue] if keys.include? issue.key
|
|
75
71
|
end
|
|
@@ -79,9 +75,13 @@ class Exporter
|
|
|
79
75
|
raise unless e.message.start_with? 'This is an aggregated project and issues should have been included'
|
|
80
76
|
end
|
|
81
77
|
|
|
82
|
-
selected.
|
|
83
|
-
|
|
84
|
-
|
|
78
|
+
if selected.empty?
|
|
79
|
+
file_system.log "No issues found to match #{keys.collect(&:inspect).join(', ')}"
|
|
80
|
+
else
|
|
81
|
+
selected.each do |project, issue|
|
|
82
|
+
file_system.log "\nProject #{project.name}", also_write_to_stderr: true
|
|
83
|
+
file_system.log issue.dump, also_write_to_stderr: true
|
|
84
|
+
end
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
|
|
@@ -116,7 +116,9 @@ class Exporter
|
|
|
116
116
|
|
|
117
117
|
def jira_config filename = nil
|
|
118
118
|
if filename
|
|
119
|
-
@jira_config = file_system.load_json(filename)
|
|
119
|
+
@jira_config = file_system.load_json(filename, fail_on_error: false)
|
|
120
|
+
raise "Unable to load Jira configuration file and cannot continue: #{filename.inspect}" if @jira_config.nil?
|
|
121
|
+
|
|
120
122
|
@jira_config['url'] = $1 if @jira_config['url'] =~ /^(.+)\/+$/
|
|
121
123
|
end
|
|
122
124
|
@jira_config
|
|
@@ -13,7 +13,7 @@ class FileConfig
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def run
|
|
16
|
-
@issues = project_config.issues
|
|
16
|
+
@issues = project_config.issues
|
|
17
17
|
instance_eval(&@block)
|
|
18
18
|
|
|
19
19
|
if @columns
|
|
@@ -66,15 +66,20 @@ class FileConfig
|
|
|
66
66
|
# is that all empty values in the first column should be at the bottom.
|
|
67
67
|
def sort_output all_lines
|
|
68
68
|
all_lines.sort do |a, b|
|
|
69
|
+
result = nil
|
|
69
70
|
if a[0] == b[0]
|
|
70
|
-
a[1..] <=> b[1..]
|
|
71
|
+
result = a[1..] <=> b[1..]
|
|
71
72
|
elsif a[0].nil?
|
|
72
|
-
1
|
|
73
|
+
result = 1
|
|
73
74
|
elsif b[0].nil?
|
|
74
|
-
-1
|
|
75
|
+
result = -1
|
|
75
76
|
else
|
|
76
|
-
a[0] <=> b[0]
|
|
77
|
+
result = a[0] <=> b[0]
|
|
77
78
|
end
|
|
79
|
+
|
|
80
|
+
# This will only happen if one of the objects isn't comparable. Seen in production.
|
|
81
|
+
result = -1 if result.nil?
|
|
82
|
+
result
|
|
78
83
|
end
|
|
79
84
|
end
|
|
80
85
|
|
|
@@ -35,6 +35,10 @@ class FileSystem
|
|
|
35
35
|
log "Warning: #{message}", more: more, also_write_to_stderr: true
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
def error message, more: nil
|
|
39
|
+
log "Error: #{message}", more: more, also_write_to_stderr: true
|
|
40
|
+
end
|
|
41
|
+
|
|
38
42
|
def log message, more: nil, also_write_to_stderr: false
|
|
39
43
|
message += " See #{logfile_name} for more details about this message." if more
|
|
40
44
|
|
|
@@ -27,7 +27,7 @@ class FlowEfficiencyScatterplot < ChartBase
|
|
|
27
27
|
</mfrac>
|
|
28
28
|
</math>
|
|
29
29
|
</div>
|
|
30
|
-
<div style="background:
|
|
30
|
+
<div style="background: var(--warning-banner)">Note that for this calculation to be accurate, we must be moving items into a
|
|
31
31
|
blocked or stalled state the moment we stop working on it, and most teams don't do that.
|
|
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>
|
|
@@ -38,22 +38,13 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
38
38
|
plugins: {
|
|
39
39
|
annotation: {
|
|
40
40
|
annotations: {
|
|
41
|
-
|
|
42
|
-
holiday<%= index %>: {
|
|
43
|
-
drawTime: 'beforeDraw',
|
|
44
|
-
type: 'box',
|
|
45
|
-
xMin: '<%= range.begin %>T00:00:00',
|
|
46
|
-
xMax: '<%= range.end %>T23:59:59',
|
|
47
|
-
backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
|
|
48
|
-
borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
|
|
49
|
-
},
|
|
50
|
-
<% end %>
|
|
41
|
+
<%= working_days_annotation %>
|
|
51
42
|
|
|
52
43
|
<% if percentage_line_x %>
|
|
53
44
|
line: {
|
|
54
45
|
type: 'line',
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
scaleID: 'x',
|
|
47
|
+
value: '<%= percentage_line_x %>',
|
|
57
48
|
borderColor: <%= CssVariable.new('--aging-work-bar-chart-percentage-line-color').to_json %>,
|
|
58
49
|
borderWidth: 1,
|
|
59
50
|
drawTime: 'afterDraw'
|
|
@@ -6,7 +6,7 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
|
|
|
6
6
|
{
|
|
7
7
|
type: 'bar',
|
|
8
8
|
data: {
|
|
9
|
-
labels: [<%=
|
|
9
|
+
labels: [<%= @board_columns.collect { |c| c.name.inspect }.join(',') %>],
|
|
10
10
|
datasets: <%= JSON.generate(data_sets) %>
|
|
11
11
|
},
|
|
12
12
|
options: {
|
|
@@ -22,8 +22,10 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
|
|
|
22
22
|
labelString: 'Date Completed'
|
|
23
23
|
},
|
|
24
24
|
grid: {
|
|
25
|
-
color: <%= CssVariable['--grid-line-color'].to_json
|
|
25
|
+
color: <%= CssVariable['--grid-line-color'].to_json %>,
|
|
26
|
+
z: 1 // draw the grid lines on top of the bars
|
|
26
27
|
},
|
|
28
|
+
stacked: true
|
|
27
29
|
},
|
|
28
30
|
y: {
|
|
29
31
|
scaleLabel: {
|
|
@@ -35,8 +37,11 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
|
|
|
35
37
|
text: 'Age in days'
|
|
36
38
|
},
|
|
37
39
|
grid: {
|
|
38
|
-
color: <%= CssVariable['--grid-line-color'].to_json
|
|
40
|
+
color: <%= CssVariable['--grid-line-color'].to_json %>,
|
|
41
|
+
z: 1 // draw the grid lines on top of the bars
|
|
39
42
|
},
|
|
43
|
+
stacked: true,
|
|
44
|
+
max: <%= (@max_age * 1.1).to_i %>
|
|
40
45
|
}
|
|
41
46
|
},
|
|
42
47
|
plugins: {
|
|
@@ -44,14 +49,26 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
|
|
|
44
49
|
callbacks: {
|
|
45
50
|
label: function(context) {
|
|
46
51
|
if( typeof(context.dataset.data[context.dataIndex]) == "number" ) {
|
|
47
|
-
|
|
52
|
+
let full_data = <%= @bar_data.inspect %>;
|
|
53
|
+
let columnIndex = context.dataIndex;
|
|
54
|
+
let rowIndex = context.datasetIndex - <%= @row_index_offset %>;
|
|
55
|
+
return context.dataset.label + " of completed work items left this column in " +full_data[rowIndex][columnIndex] + " days or less";
|
|
48
56
|
}
|
|
49
57
|
else {
|
|
50
|
-
return context.dataset.data[context.dataIndex].title
|
|
58
|
+
return context.dataset.data[context.dataIndex].title;
|
|
51
59
|
}
|
|
52
60
|
}
|
|
53
61
|
}
|
|
62
|
+
},
|
|
63
|
+
legend: {
|
|
64
|
+
labels: {
|
|
65
|
+
filter: function(item, chart) {
|
|
66
|
+
// Logic to remove a particular legend item goes here
|
|
67
|
+
return !item.text.includes('%');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
54
70
|
}
|
|
71
|
+
|
|
55
72
|
}
|
|
56
73
|
}
|
|
57
74
|
});
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
<table class='standard'>
|
|
2
2
|
<thead>
|
|
3
3
|
<tr>
|
|
4
|
-
<th
|
|
5
|
-
<th>E</th>
|
|
6
|
-
<th>B</th>
|
|
4
|
+
<th title="Age in days">Age</th>
|
|
5
|
+
<th title="Expedited">E</th>
|
|
6
|
+
<th title="Blocked / Stalled">B/S</th>
|
|
7
|
+
<th title="Priority">P</th>
|
|
7
8
|
<th>Issue</th>
|
|
8
9
|
<th>Status</th>
|
|
10
|
+
<th>Forecast</th>
|
|
9
11
|
<th>Fix versions</th>
|
|
10
12
|
<% if any_scrum_boards %>
|
|
11
13
|
<th>Sprints</th>
|
|
@@ -28,6 +30,7 @@
|
|
|
28
30
|
<td style="text-align: right;"><%= issue_age || 'Not started' %></td>
|
|
29
31
|
<td><%= expedited_text(issue) %></td>
|
|
30
32
|
<td><%= blocked_text(issue) %></td>
|
|
33
|
+
<td><%= priority_text(issue) %></td>
|
|
31
34
|
<td>
|
|
32
35
|
<% parent_hierarchy(issue).each_with_index do |parent, index| %>
|
|
33
36
|
<% color = parent != issue ? "var(--hierarchy-table-inactive-item-text-color)" : 'var(--default-text-color)' %>
|
|
@@ -41,6 +44,7 @@
|
|
|
41
44
|
<% end %>
|
|
42
45
|
</td>
|
|
43
46
|
<td><%= format_status issue.status, board: issue.board %></td>
|
|
47
|
+
<td><%= dates_text(issue) %></td>
|
|
44
48
|
<td><%= fix_versions_text(issue) %></td>
|
|
45
49
|
<% if any_scrum_boards %>
|
|
46
50
|
<td><%= sprints_text(issue) %></td>
|
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
<div class="chart">
|
|
2
2
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
3
3
|
</div>
|
|
4
|
+
<%
|
|
5
|
+
if show_stats
|
|
6
|
+
link_id = next_id
|
|
7
|
+
issues_id = next_id
|
|
8
|
+
%>
|
|
9
|
+
[<a id='<%= link_id %>' href="#" onclick='expand_collapse("<%= link_id %>", "<%= issues_id %>"); return false;'>Show details</a>]
|
|
10
|
+
<div id="<%= issues_id %>" style="display: none;">
|
|
11
|
+
<div>
|
|
12
|
+
<table class="standard">
|
|
13
|
+
<tr>
|
|
14
|
+
<th>Issue Type</th>
|
|
15
|
+
<th>Min</th>
|
|
16
|
+
<th>Max</th>
|
|
17
|
+
<th>Avg</th>
|
|
18
|
+
<th>Mode</th>
|
|
19
|
+
<% percentiles.each do |p| %>
|
|
20
|
+
<th><%= p %>th</th>
|
|
21
|
+
<% end %>
|
|
22
|
+
</tr>
|
|
23
|
+
<% the_stats.each do |k, v| %>
|
|
24
|
+
<tr>
|
|
25
|
+
<td><%= k %></td>
|
|
26
|
+
<td style="text-align: right;"><%= v[:min] %></td>
|
|
27
|
+
<td style="text-align: right;"><%= v[:max] %></td>
|
|
28
|
+
<td style="text-align: right;"><%= sprintf('%.2f', v[:average]) %></td>
|
|
29
|
+
<td><%= v[:mode].join(', ') %></td>
|
|
30
|
+
<% percentiles.each do |p| %>
|
|
31
|
+
<td style="text-align: right;"><%= v[:percentiles][p] %></td>
|
|
32
|
+
<% end %>
|
|
33
|
+
</tr>
|
|
34
|
+
<% end %>
|
|
35
|
+
</table>
|
|
36
|
+
</div>
|
|
37
|
+
<div>
|
|
38
|
+
<p>These statistics help understand the <i>"shape"</i> of the cycletime histogram distribution, to help us with predictions.</p>
|
|
39
|
+
<ul>
|
|
40
|
+
<li><b>Min & Max:</b> the observed spread for the data set. Useful to judge how wide the variation is. </li>
|
|
41
|
+
<li><b>Average:</b> the arithmetic mean of the data set. Useful as a <i>"typical representative"</i> of the complete set.</li>
|
|
42
|
+
<li><b>Mode:</b> the most repeated value(s) in the data set. This is the value we're most likely to remember. </li>
|
|
43
|
+
<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>
|
|
44
|
+
<ul>
|
|
45
|
+
<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>
|
|
46
|
+
<li><b>85%</b>: useful to establish service level expectations, accounting for rare events..</li>
|
|
47
|
+
<li><b>98% (or higher)</b>: useful to gauge worst case expectations..</li>
|
|
48
|
+
</ul>
|
|
49
|
+
</ul>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<%
|
|
53
|
+
end
|
|
54
|
+
%>
|
|
4
55
|
<script>
|
|
5
56
|
new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
6
57
|
{
|
|
@@ -21,6 +72,8 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
21
72
|
grid: {
|
|
22
73
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
23
74
|
},
|
|
75
|
+
min: 0,
|
|
76
|
+
offset: false, // Gets rid of the ugly padding on left.
|
|
24
77
|
},
|
|
25
78
|
y: {
|
|
26
79
|
stacked: true,
|
|
@@ -34,6 +87,27 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
34
87
|
}
|
|
35
88
|
},
|
|
36
89
|
plugins: {
|
|
90
|
+
annotation: {
|
|
91
|
+
annotations: {
|
|
92
|
+
<%
|
|
93
|
+
results = the_stats[:all][:percentiles]
|
|
94
|
+
results.each do |percentile, value|
|
|
95
|
+
%>
|
|
96
|
+
percentile<%= percentile.to_s %>: {
|
|
97
|
+
type: 'line',
|
|
98
|
+
scaleID: 'x',
|
|
99
|
+
value: <%= value %>,
|
|
100
|
+
borderWidth: 1,
|
|
101
|
+
drawTime: 'beforeDatasetsDraw',
|
|
102
|
+
label: {
|
|
103
|
+
enabled: true,
|
|
104
|
+
content: '<%= "#{percentile}%" %>',
|
|
105
|
+
position: 'start',
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
<% end %>
|
|
109
|
+
},
|
|
110
|
+
},
|
|
37
111
|
tooltip: {
|
|
38
112
|
callbacks: {
|
|
39
113
|
label: function(context) {
|
|
@@ -53,16 +53,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
53
53
|
autocolors: false,
|
|
54
54
|
annotation: {
|
|
55
55
|
annotations: {
|
|
56
|
-
|
|
57
|
-
holiday<%= index %>: {
|
|
58
|
-
drawTime: 'beforeDraw',
|
|
59
|
-
type: 'box',
|
|
60
|
-
xMin: '<%= range.begin %>T00:00:00',
|
|
61
|
-
xMax: '<%= range.end %>T23:59:59',
|
|
62
|
-
backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
|
|
63
|
-
borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
|
|
64
|
-
},
|
|
65
|
-
<% end %>
|
|
56
|
+
<%= working_days_annotation %>
|
|
66
57
|
|
|
67
58
|
<% @percentage_lines.each_with_index do |args, index| %>
|
|
68
59
|
<% percent, color = args %>
|
|
@@ -50,16 +50,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
50
50
|
},
|
|
51
51
|
annotation: {
|
|
52
52
|
annotations: {
|
|
53
|
-
|
|
54
|
-
holiday<%= index %>: {
|
|
55
|
-
drawTime: 'beforeDraw',
|
|
56
|
-
type: 'box',
|
|
57
|
-
xMin: '<%= range.begin %>T00:00:00',
|
|
58
|
-
xMax: '<%= range.end %>T23:59:59',
|
|
59
|
-
backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
|
|
60
|
-
borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
|
|
61
|
-
},
|
|
62
|
-
<% end %>
|
|
53
|
+
<%= working_days_annotation %>
|
|
63
54
|
}
|
|
64
55
|
},
|
|
65
56
|
legend: {
|
|
@@ -55,16 +55,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
55
55
|
autocolors: false,
|
|
56
56
|
annotation: {
|
|
57
57
|
annotations: {
|
|
58
|
-
|
|
59
|
-
holiday<%= index %>: {
|
|
60
|
-
drawTime: 'beforeDraw',
|
|
61
|
-
type: 'box',
|
|
62
|
-
xMin: '<%= range.begin %>T00:00:00',
|
|
63
|
-
xMax: '<%= range.end %>T23:59:59',
|
|
64
|
-
backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
|
|
65
|
-
borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
|
|
66
|
-
},
|
|
67
|
-
<% end %>
|
|
58
|
+
<%= working_days_annotation %>
|
|
68
59
|
}
|
|
69
60
|
}
|
|
70
61
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
--body-background: white;
|
|
3
3
|
--default-text-color: black;
|
|
4
4
|
--grid-line-color: lightgray;
|
|
5
|
+
--warning-banner: yellow;
|
|
5
6
|
|
|
6
7
|
--cycletime-scatterplot-overall-trendline-color: gray;
|
|
7
8
|
|
|
@@ -27,8 +28,16 @@
|
|
|
27
28
|
--throughput_chart_total_line_color: gray;
|
|
28
29
|
|
|
29
30
|
--aging-work-in-progress-chart-shading-color: lightgray;
|
|
31
|
+
--aging-work-in-progress-chart-shading-50-color: #2E8BC0; // green;
|
|
32
|
+
--aging-work-in-progress-chart-shading-85-color: #ADD8E6; // yellow;
|
|
33
|
+
--aging-work-in-progress-chart-shading-98-color: #FF8A8A; // orange;
|
|
34
|
+
--aging-work-in-progress-chart-shading-100-color: #FF2E2E; // red;
|
|
35
|
+
|
|
30
36
|
--aging-work-in-progress-by-age-trend-line-color: gray;
|
|
31
37
|
|
|
38
|
+
--aging-work-table-date-in-jeopardy: yellow;
|
|
39
|
+
--aging-work-table-date-overdue: red;
|
|
40
|
+
|
|
32
41
|
--hierarchy-table-inactive-item-text-color: gray;
|
|
33
42
|
|
|
34
43
|
--wip-chart-completed-color: #00ff00;
|
|
@@ -58,6 +67,9 @@
|
|
|
58
67
|
--sprint-burndown-sprint-color-4: red;
|
|
59
68
|
--sprint-burndown-sprint-color-5: brown;
|
|
60
69
|
|
|
70
|
+
--daily-view-selected-issue-background: lightgray;
|
|
71
|
+
--daily-view-issue-border: green;
|
|
72
|
+
--daily-view-selected-issue-border: red;
|
|
61
73
|
|
|
62
74
|
}
|
|
63
75
|
|
|
@@ -133,8 +145,68 @@ ul.quality_report {
|
|
|
133
145
|
border-top: 1px solid gray;
|
|
134
146
|
}
|
|
135
147
|
|
|
148
|
+
div.daily_issue:hover {
|
|
149
|
+
background: var(--daily-view-selected-issue-background);
|
|
150
|
+
border-color: var(--daily-view-selected-issue-border);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
div.daily_issue {
|
|
154
|
+
border: 1px solid var(--daily-view-issue-border);
|
|
155
|
+
padding: 0.5em;
|
|
156
|
+
.heading {
|
|
157
|
+
vertical-align: middle;
|
|
158
|
+
display: flex;
|
|
159
|
+
flex-wrap: wrap;
|
|
160
|
+
column-gap: 0.5em;
|
|
161
|
+
align-items: center;
|
|
162
|
+
}
|
|
163
|
+
table {
|
|
164
|
+
margin-left: 1em;
|
|
165
|
+
td {
|
|
166
|
+
vertical-align: top;
|
|
167
|
+
}
|
|
168
|
+
.time {
|
|
169
|
+
white-space: nowrap;
|
|
170
|
+
font-size: 0.8em;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
.icon {
|
|
174
|
+
width: 1em;
|
|
175
|
+
height: 1em;
|
|
176
|
+
}
|
|
177
|
+
.account_id {
|
|
178
|
+
font-weight: bold;
|
|
179
|
+
}
|
|
180
|
+
.field {
|
|
181
|
+
border: 1px solid black;
|
|
182
|
+
color: white;
|
|
183
|
+
background: black;
|
|
184
|
+
padding-left: 0.2em;
|
|
185
|
+
padding-right: 0.2em;
|
|
186
|
+
border-radius: 0.2em;
|
|
187
|
+
}
|
|
188
|
+
.label {
|
|
189
|
+
border: 1px solid black;
|
|
190
|
+
padding-left: 0.2em;
|
|
191
|
+
padding-right: 0.2em;
|
|
192
|
+
border-radius: 0.2em;
|
|
193
|
+
}
|
|
194
|
+
margin-bottom: 0.5em;
|
|
195
|
+
}
|
|
196
|
+
div.child_issue:hover {
|
|
197
|
+
background: var(--body-background);
|
|
198
|
+
}
|
|
199
|
+
div.child_issue {
|
|
200
|
+
border: 1px dashed green;
|
|
201
|
+
margin: 0.2em;
|
|
202
|
+
margin-left: 1.5em;
|
|
203
|
+
padding: 0.5em;
|
|
204
|
+
}
|
|
205
|
+
|
|
136
206
|
@media screen and (prefers-color-scheme: dark) {
|
|
137
207
|
:root {
|
|
208
|
+
--warning-banner: #9F2B00;
|
|
209
|
+
|
|
138
210
|
--non-working-days-color: #2f2f2f;
|
|
139
211
|
--type-story-color: #6fb86f;
|
|
140
212
|
--type-task-color: #0021b3;
|
|
@@ -150,8 +222,6 @@ ul.quality_report {
|
|
|
150
222
|
--dead-color: black;
|
|
151
223
|
--wip-chart-active-color: #2551c1;
|
|
152
224
|
|
|
153
|
-
--aging-work-in-progress-chart-shading-color: #b4b4b4;
|
|
154
|
-
|
|
155
225
|
--status-category-inprogress-color: #1c49bb;
|
|
156
226
|
|
|
157
227
|
--cycletime-scatterplot-overall-trendline-color: gray;
|
|
@@ -165,6 +235,8 @@ ul.quality_report {
|
|
|
165
235
|
--wip-chart-duration-two-weeks-or-less-color: #cf9400;
|
|
166
236
|
--wip-chart-duration-four-weeks-or-less-color: #c25e00;
|
|
167
237
|
--wip-chart-duration-more-than-four-weeks-color: #8e0000;
|
|
238
|
+
|
|
239
|
+
--daily-view-selected-issue-background: #474747;
|
|
168
240
|
}
|
|
169
241
|
|
|
170
242
|
h1 {
|
|
@@ -197,4 +269,12 @@ ul.quality_report {
|
|
|
197
269
|
div.color_block {
|
|
198
270
|
border: 1px solid lightgray;
|
|
199
271
|
}
|
|
272
|
+
|
|
273
|
+
div.daily_issue {
|
|
274
|
+
.field {
|
|
275
|
+
color: var(--default-text-color);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
200
280
|
}
|
|
@@ -18,18 +18,42 @@
|
|
|
18
18
|
document.getElementById(issues_id).style.display = 'none'
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
function toggle_visibility(open_link_id, close_link_id, toggleable_id) {
|
|
23
|
+
let open_link = document.getElementById(open_link_id)
|
|
24
|
+
let close_link = document.getElementById(close_link_id)
|
|
25
|
+
let toggleable_element = document.getElementById(toggleable_id)
|
|
26
|
+
|
|
27
|
+
if(open_link.style.display == 'none') {
|
|
28
|
+
open_link.style.display = 'block'
|
|
29
|
+
close_link.style.display = 'none'
|
|
30
|
+
toggleable_element.style.display = 'none'
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
open_link.style.display = 'none'
|
|
34
|
+
close_link.style.display = 'block'
|
|
35
|
+
toggleable_element.style.display = 'block'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
21
38
|
// If we switch between light/dark mode then force a refresh so all charts will redraw correctly
|
|
22
39
|
// in the other colour scheme.
|
|
23
40
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
|
|
24
41
|
location.reload()
|
|
25
42
|
})
|
|
26
|
-
|
|
27
43
|
</script>
|
|
28
44
|
<style>
|
|
29
45
|
<%= css %>
|
|
30
46
|
</style>
|
|
47
|
+
<script type="text/javascript">
|
|
48
|
+
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--default-text-color');
|
|
49
|
+
</script>
|
|
31
50
|
</head>
|
|
32
51
|
<body>
|
|
52
|
+
<noscript>
|
|
53
|
+
<div style="padding: 1em; background: gray; color: white; font-size: 2em;">
|
|
54
|
+
Javascript is currently disabled and that means that almost all of the charts in this report won't render. If you'd loaded this from a folder on SharePoint then save it locally and load it again.
|
|
55
|
+
</div>
|
|
56
|
+
</noscript>
|
|
33
57
|
<%= "\n" + @sections.collect { |text, type| text if type == :header }.compact.join("\n\n") %>
|
|
34
58
|
<%= "\n" + @sections.collect { |text, type| text if type == :body }.compact.join("\n\n") %>
|
|
35
59
|
</body>
|